CVE-2026-23918 Apache mod_http2 — A Double-Free and a Honest Server Header
Coverage period: 2026-05-28 (day 0) → ongoing.
Detection went live on the three production HoneyLens sensors the same day the
CXSECURITY advisory and the
exploit-db PoC 52577 landed.
Four Suricata signatures, an updated Server: banner that names the vulnerable build
(and only the vulnerable build), and a classifier hook for the recon-stage HTTP shape.
CVE-2026-23918 is a CWE-415 double-free in Apache HTTP Server 2.4.66's
mod_http2 stream-cleanup path. If a client opens an HTTP/2 stream with a HEADERS
frame and immediately follows with RST_STREAM before the stream is fully registered, two
cleanup callbacks race to free the same memory region — the worker crashes.
Denial-of-service only; not RCE. CVSS-pending, advisory rates it Medium.
2.4.67 is the fixed branch.
Server: Apache/2.4.66 (Ubuntu) mod_http2/2.0.32intext:"Apache/2.4.66" "HTTP/2"The Vulnerability in One Minute
Apache's HTTP/2 implementation lives in mod_http2, a separate worker pool that
runs alongside the classic event MPM. Each HTTP/2 stream gets a small bookkeeping struct, registered
in two places: a per-connection stream table and a per-worker cleanup queue. If a client
sends HEADERS and immediately RST_STREAM, both cleanup callbacks fire concurrently against a stream
that was never fully registered. The race resolves into two paths trying to free the same
memory region, the worker double-frees, and glibc aborts. The attacker spends one TLS connection;
the defender loses one worker.
The active payload requires HTTP/2 over TLS (ALPN negotiation for h2).
Our HTTP honeypot serves HTTP/1.1 only — the active DoS can't fully land here. That's
deliberate: we want the recon, the failed ALPN attempt, and the actor IP. We don't need to
crash to capture the operator.
The Bait Shape
The exploit-db PoC has a passive-scan mode that hits GET / on a target list and
keys on the Server: header. The Google dork is
intext:"Apache/2.4.66" "HTTP/2". Both substrings need to be in our response, so:
Apache/2.4.66 covers the dork's first half. mod_http2/2.0.32 is the
plausible co-bundled module version for that Apache release. Upgrade: h2,h2c covers
the dork's second half — the literal string "HTTP/2" doesn't appear, but Google's tokeniser
keys on substrings inside the h2 upgrade hint and on the mod_http2
product name. We deliberately did not spoof a TLS listener with ALPN h2; the goal of
day 0 is to attract recon, not to stand up a fake-crashing HTTP/2 endpoint.
Side benefit: the previous default banner was Apache/2.4.41 (Ubuntu) — the
Apache 2.20.04-focal default from April 2020. That made the fleet look like an aging unpatched
host (which it was supposed to). The new banner makes it look like a host that upgraded
recently to a specifically vulnerable build, which is a sharper bait shape for any operator
following the disclosure cycle.
The Four Suricata Signatures
SID 9026500 — cleartext h2c upgrade attempt
Matches the explicit HTTP/1.1→HTTP/2 cleartext upgrade pattern: a request with
Upgrade: h2c + Connection: Upgrade, HTTP2-Settings.
Real browsers don't h2c-upgrade against unknown hosts — this is exclusively
tooling traffic. The exploit's HTTP/1.1 fallback path emits exactly this shape.
SID 9026501 — recon burst, HEAD method
10+ HEAD / requests from a single source within 30 seconds.
Matches the exploit's --scan mode that bangs through a target list
reading Server headers, and matches the Censys/Shodan-style banner-crawler pattern when
it's aimed at a single host with high concurrency. detection_filter so we
get the first hit after threshold, not every hit in the storm.
SID 9026502 — recon burst, GET / variant
Same pattern as 9026501 but for passive scanners that use GET / instead of
HEAD /. Threshold tuned slightly looser (15 hits in 60 seconds) because
plain GET / has more legitimate uses than HEAD /.
SID 9026503 — python h2-library UA fingerprint
The exploit-db PoC uses Python's h2 library. UAs of the
python-{requests,httpx,urllib3,h2}/… and bare h2/…
shapes hitting our HTTP honeypot get flagged. Low confidence on its own (security researchers
and CI scanners hit us with the same UAs); paired with 9026500/9026501 it's high-confidence
recon.
A fifth slot (9026504) is reserved for a future TLS-listener-with-ALPN-h2 bait if we
decide it's worth standing one up. The trade-off there is the complexity of half-terminating
h2 framing just to capture the exploit's active payload — the bug is a crash, not an
information leak, so we'd be capturing intent more than artefacts.
The Classifier Hook
hp_http.py learned a new attack type: cve_2026_23918_recon. It promotes
to MEDIUM when the request carries Upgrade: h2c or Upgrade: h2, and
to LOW when the User-Agent matches the python h2-library family. Per-event severity, no
cross-event tracking yet — that lands on the AI triage pass which already correlates
multiple events per source. The text-rule pack classification/text_rules.py got
three matching SCANNER patterns
(Scanner-Apache-2466-h2-Library,
Scanner-CVE-2026-23918-Family,
Scanner-Apache-2466-Probe) so the same UA shapes light up across all surfaces,
not just the HTTP honeypot.
IOC Vocabulary
What We're Watching For
- First scanner pass. Hours, not days. Shodan/Censys/ZoomEye
re-banner-crawl public IPv4 weekly; our spoofed Server header will show up in the next
crawl cycle and feed the next round of dork queries. We expect the first
9026501/9026502firing within 24-72 hours. - Self-doxxing CVE-aware kits. The PAN-OS hunt produced operators who
literally embedded the CVE in the User-Agent (
CVE-2026-0300-Checkerfamily).CVE-2026-23918-Scannershapes are pre-registered in the text-rules pack; first hit gets a SCANNER classification automatically. - Cross-vector pivot. The Apache 2.4.66 dork results overlap heavily with the existing pool of "any unpatched LAMP host" targets. We expect the operators currently running PHP-CGI / WordPress / phpMyAdmin sweeps to add this CVE to their rotation rather than spawn a new dedicated kit.
- The exploit-DB PoC publication wave. Public exploit code lowers the skill floor by an order of magnitude. Once exploit 52577 propagates into Metasploit modules and "h2flood"-style scripts on GitHub, the volume jumps.
Why We Don't Run Vulnerable Apache
We deliberately don't terminate HTTP/2 with a real-or-near-real Apache build. The double-free kills a worker per exploit attempt; standing up an actual vulnerable Apache and watching it crash repeatedly would give the attacker exactly the success signal they're looking for. The bait is the Server header, the Upgrade hint, and the failed-handshake IoC chain. That's enough to attribute the recon and to feed the hunting list.
Status & Next Steps
Daily review cadence as long as the hunt is open — same playbook as the CVE-2026-0300 hunt and the CVE-2026-42945 NGINX Rift hunt. First real firing closes the day-0 watch and starts the live observation period. Once an operator IP lands in the hunting list we publish the actor profile, the TLS-handshake JA4 fingerprint, and the cross-vector overlap with the other two open hunts. Next update: when a real firing lands, or at the 14-day-silence mark if the hunt closes quiet.
§ 2026-05-29 · Day 1 review — SID 9026503 fired 22× in 12 hours, but zero true positives. Honest framing: the rule was too permissive, fixed in rev:2.
First-light telemetry on the bait came back fast. Within hours of deploy SID
9026503 (the python-h2-library UA rule) had fired 22 times across five
actor groups on sensor2. All zero of them were CVE-2026-23918 scans. They were:
- 14× from
101.47.23.214— cloud-secrets enumerator hitting port 80 in a ~3-second burst from 22:50:58 to 22:51:01. The UA on the 9026503-firing requests waspython-requests/2.28.0, but the actor also rotated throughMozilla/5.0 (Chrome 120),Firefox/121.0,Googlebot/2.1, andcurl/7.68.0across the same campaign. Paths:/aliyun.json,/alicloud.json,/qcloud.yml,/cos.json,/.tencent/credentials,/.qcloud/credentials,/.env.local,/.env.staging,/.env.production,/secrets.json,/.gitlab-ci.yml,/web.config,/js/app.js.map. Chinese-cloud-provider secrets focus. Not Apache. - 4× from MCP-server probes (
45.156.129.80+109.105.210.72) —python-httpx/0.28.1hittingPOST /mcp+GET /sseon ports 8080 and 80. This is an emerging surface — attackers are probing for exposed Anthropic Model Context Protocol servers. New attack pattern worth its own writeup. Not Apache. - 5× from
111.7.100.40-42cluster —python-requests/2.27.1hittingGET /_all/_stats/docson port 9200. Elasticsearch enumeration from one /24 cluster across three IPs. Not Apache.
The problem: SID 9026503 rev:1 alerted on python-(requests|httpx|urllib3|h2)/
UAs. The three generic Python HTTP-client UAs are too broad — they
match every CI scanner, every cloud-secrets kit, every researcher's quick recon script.
The h2/python-h2/hyper-h2 branch is fine
(HTTP/2-specific tooling); the generic branch isn't.
Fix shipped same day as rev:2 — SID 9026503 now matches only
^(python-h2|h2|hyper-h2)/. The classifier hook in hp_http.py
and the matching pattern in classification/text_rules.py got the same
tightening. Day-1 telemetry would have produced zero firings under the
new rule, which is the right answer until a real h2-library exploit kit shows up.
The other three SIDs (9026500 h2c upgrade, 9026501 HEAD-flood,
9026502 GET-/-flood) stayed silent — no firings. The real Apache
2.4.66 fingerprint actors haven't shown up yet; the Server-header bait went live less
than 12 hours ago and the next Shodan/Censys re-banner-crawl is the first event that
could feed the dork queries.
Two collateral findings worth carrying into other writeups:
101.47.23.214cloud-secrets hunter pattern — UA-rotation through five distinct browser/bot/tool strings while spraying Chinese-cloud secret paths. Worth adding as an actor cluster to the hunting list even though they're not Apache-relevant.- MCP server probing is now an active attack pattern —
POST /mcp+GET /sseis the canonical Model Context Protocol handshake. Two distinct IPs probed for it in the last 12 hours, both withpython-httpx/0.28.1. Likely a Shodan-class scanner cataloguing exposed MCP servers ahead of a future exploit wave. Candidate for a dedicated MCP honeypot surface.
Continuing daily reviews. Day-2 expectation: the rev:2 rule stays quiet (correctly) and the recon-burst threshold rules (9026501 / 9026502) start picking up the post-bait scanner pass as it propagates through public banner indexes.
§ 2026-05-30 · Day 2 review — rev:2 vindicated, zero firings across all four SIDs and the classifier
First clean window since the bait went live. From the rev:2 deploy
(~2026-05-29 20:00 CEST) through this morning’s pull,
SIDs 9026500–9026503 fired zero times across all three
sensors, and the cve_2026_23918_recon
classifier in hp_http.py tagged zero events on sensor1 and
sensor2. That’s the right answer: the rule was firing on noise,
we removed the noise source, the rule is now correctly quiet.
The carry-over actors from day 1 went mostly quiet too:
- 101.47.23.214 (cloud-secrets hunter): no return traffic in the last 24 hours. The day-1 burst was a one-shot campaign — 14 hits in 3 seconds, then radio silence. Common pattern for kit-runner traffic.
- 45.156.129.80 / 109.105.210.72 (MCP server
probers): no further
/mcp+/ssetraffic on the honeypot. Either the scanner moved off to its next target list, or this was a focused single-pass enumeration. We’ll keep the pattern on the watch-list for a couple more days — if it comes back from rotated IPs that’ll confirm it’s a structured scan campaign. - 111.7.100.40-42 (Elasticsearch enum cluster): no return either. The day-1 firings were the back-end of a sweep that had been running before the rev:1 rule even existed — we just happened to overlap with their wave.
On the real-target side: zero Apache 2.4.66 fingerprint hits. The Server-header bait has been live ~36 hours. The Shodan re-banner-crawl typically runs weekly, so we expect the first scanner pass between day 3 and day 7 once our spoofed banner makes it into the public banner index.
Continuing daily reviews. Day-3 expectation unchanged — rev:2 stays quiet on noise, the threshold rules pick up the first post-bait scanner pass when it arrives.
§ 2026-05-31 · Day 3 review — second consecutive clean day, zero firings, zero classifier hits
Second consecutive day with zero firings on SIDs
9026500–9026503 and zero
cve_2026_23918_recon classifier hits in
hp_http.py. rev:2 continues to be the correct
answer. The first scanner pass against the
Apache/2.4.66 (Ubuntu) mod_http2/2.0.32 bait still
hasn’t arrived — the Shodan re-banner-crawl that
would normally trigger it runs ~weekly, so days 3–7 are
still the expected window.
Carry-over actors from day 1: zero returns from any of the five
false-positive sources (cloud-secrets hunter
101.47.23.214, MCP probers
45.156.129.80 + 109.105.210.72,
Elasticsearch enum cluster 111.7.100.40-42). The
day-1 wave was genuinely a one-shot campaign; the rev:2 fix
removed the FP risk without losing the real recon shape.
Cross-thread for the day: the PAN-OS captive-portal surface took a 6-firing wave overnight via Tor exits — see the CVE-2026-0300 T+18 review. NGINX Rift day 7 silence holds (modulo the 6 PAN-OS cross-hits). The new CVE-2026-0257 GP forged-cookie hunt opened yesterday, day 1 silent.
§ 2026-06-03 · Day 6 review — three 9026502 firings, all FPs from a wide-port Linode-US scanner. Tightening the rule with a port-80 qualifier.
Three more clean days (2026-05-31, 06-01, 06-02) on the
Apache/2.4.66 bait against the honeypot. Zero hits
on SIDs 9026500, 9026501, 9026503, and zero
cve_2026_23918_recon classifier hits in
hp_http.py across all three sensors.
SID 9026502 (repeated GET / from single source,
15-per-60s detection-filter) fired three times on 2026-06-01 at
21:01:27–21:01:28 CEST on slim. The source was
172.105.102.42 — Linode-US, same
172.105.102.0/24 cluster we documented at the
NGINX Rift day-2 review
as a one-day campaign. They’re back, running an even
wider port sweep this time.
The shape of the FP: 9026502 has no
destination-port qualifier — just
http $EXTERNAL_NET any -> $HOME_NET any with
a path match on root. 172.105.102.42 fired the
rule on port 6080 (PAN-OS captive portal) and port 9042
(Cassandra), not on the Apache honeypot’s actual
port 80. The threshold rule triggered because Suricata’s
HTTP parser engaged on those ports (they’re in our
custom detection-ports config), saw 15+ GET /
in a minute from one source, and fired regardless of which
honeypot was serving them.
Wider context for 172.105.102.42: it’s a
broad-spectrum scanner sweeping
port 6080, 6379, 1521, 1883, 11211, 9042, 6443, 4443,
443, 8883 in a single ~2-minute window, hitting paths
like /cslu/v1/core/conf (Cisco SLU),
/CFIDE/componentutils/ (ColdFusion), and generic
GET /. It’s not Apache-mod_http2-specific
traffic in any sense.
Rule fix planned for rev:3: add
$HOME_NET 80 destination-port qualifier to
9026502 (and 9026501 for the HEAD-flood variant). That alone
eliminates the FP family because the Apache-2.4.66 bait only
lives on port 80; any HTTP scanner hitting a non-80 port
isn’t a CVE-2026-23918 candidate by definition.
Real Apache fingerprint hits still 0. The Shodan re-banner-crawl window has expired without us showing up in their next crawl results — either our IP didn’t get picked up this cycle or the operator class chasing CVE-2026-23918 has narrowed (which would fit the DoS-not-RCE nature of the bug; high-value actors don’t hunt DoS primitives that hard).
Cross-thread: CVE-2026-0300 T+21 three-day silence streak; NGINX Rift day 10 one new FP (Spark-Tor); CVE-2026-0257 day 4 silence holds.