Day 6 — new FP class: 9026502 fired 3× from a wide-port Linode-US scanner (non-port-80, rule needs qualifier) May 2026 · coverage live across three sensors

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.

Apache HTTP Server CVE-2026-23918 mod_http2 Double-Free DoS Preemptive Coverage Operations Writeup
Sensors live
3 / 3
sensor1 + sensor2 + sensor3
Bait pattern
2.4.66
Server: Apache/2.4.66 (Ubuntu) mod_http2/2.0.32
Suricata SIDs
4
9026500-9026503
Listener
80
existing HTTP honeypot, no new port
Real exploits caught
0
day 0 — bait just live
Scanner dork
intext:"Apache/2.4.66" "HTTP/2"
the recon-stage fingerprint

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:

HTTP/1.1 200 OK Server: Apache/2.4.66 (Ubuntu) mod_http2/2.0.32 X-Powered-By: PHP/7.4.3 Upgrade: h2,h2c Content-Type: text/html; charset=UTF-8 Connection: close <html>…Apache2 Ubuntu Default Page…</html>

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

# Suricata SIDs (HoneyLens local range) sid 9026500 — HONEYLENS CVE-2026-23918 Apache mod_http2 — cleartext h2c upgrade attempt sid 9026501 — HONEYLENS CVE-2026-23918 Apache mod_http2 — recon burst (HEAD) sid 9026502 — HONEYLENS CVE-2026-23918 Apache mod_http2 — recon burst (GET /) sid 9026503 — HONEYLENS CVE-2026-23918 Apache mod_http2 — python h2-library UA # Indicators captured by the HTTP honeypot (sensor_events.attack_types) attack_type cve_2026_23918_recon Upgrade: h2c|h2 OR UA in python-{requests,httpx,urllib3,h2} # Banner / Server shape exposed to attackers server_header Apache/2.4.66 (Ubuntu) mod_http2/2.0.32 upgrade_hint h2,h2c # UA families worth flagging (covered by text_rules.py) ua_re ^(?:python-(?:requests|httpx|urllib3|h2)|h2|hyper-h2)/ ua_re CVE-?2026-?23918[-_]?(Scanner|Checker|Probe|Tester|Hunter) ua_re apache[-_/]?\s*2\.4\.66.{0,16}(?:scan|probe|check|hunt|dork) # Public dork that maps to our bait google_dork intext:"Apache/2.4.66" "HTTP/2"

What We're Watching For

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:

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:

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:

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 90265009026503 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.