JohannesLks/CVE-2025-14558: How an IPv6 Router Advertisement Became a Shell Command

A FreeBSD rtsold proof of concept shows how a valid-looking DNS search list can cross from C parsing into shell execution, turning local network presence into code execution.

8 min read View on GitHub More from JohannesLks

A sealed IPv6 Router Advertisement packet floats toward a small UNIX daemon window. The packet is layered like an envelope, and one DNS label hides a tiny shell prompt curling outward like a fuse. It explains how a legitimate network message can become a command execution path.
A Router Advertisement should update network state. In this exploit chain, it becomes the courier for shell syntax.
Key Takeaways

The surprising part of this PoC is not that a packet arrives. It is that a packet that looks like ordinary IPv6 housekeeping can survive several layers of parsing and still reach a shell expansion point. That is the whole story in one sentence: legitimacy is the delivery vehicle.

The repository is tiny by design. It centers on one Python script, `exploit.py`, plus enough documentation to explain the target, the packet shape, and the execution path. That minimalism matters, because it makes the exploit chain easier to study than a sprawling framework would.

The packet that should have been harmless

The attack starts with a Router Advertisement carrying a DNSSL option. On paper, that is routine IPv6 neighbor discovery behavior. In practice, it is a carefully chosen trust path: `rtsold` reads the option, then passes the result to `resolvconf(8)`, where shell semantics take over.

The exploit is a state transition across trust boundaries, not just a packet layout problem.

The key weakness is not memory corruption. It is impedance mismatch. A C parser handles structured network data, then a shell script consumes that data as text. If the boundary is not quoted and sanitized correctly, syntax can cross the line as payload.

A close-up cross-section shows a chain of DNS labels on the left being threaded through a parser gate. On the right, the same string expands inside a shell script, and one unquoted segment spills into command execution. The image explains how a valid-looking payload becomes dangerous at the handoff point.
The exploit survives the parser by staying valid long enough to reach the shell.

How the payload survives DNS rules

The exploit script does not smash its way through DNS constraints. It respects them. The payload is encoded in wire format, then split into labels so it can pass basic validation before the dangerous characters reappear at the shell layer.

def encode_domain(domain):
    parts = domain.split('.')
    out = b''
    for part in parts:
        out += bytes([len(part)]) + part.encode()
    return out + b'\x00'


def encode_payload(cmd):
    wrapped = f'$( {cmd} )'
    labels = []
    while wrapped:
        labels.append(wrapped[:63])
        wrapped = wrapped[63:]
    return encode_domain('.'.join(labels))

That label splitting is the quiet cleverness in the PoC. DNS has rules, and the script uses those rules as camouflage. The payload stays within the protocol long enough to become somebody else's problem.

Assembling the death packet

The rest of the script is a packet composer. It builds the DNSSL option, then wraps it in an ICMPv6 Router Advertisement inside an Ethernet frame aimed at the multicast address that nearby IPv6 listeners will notice. The result is not a scan, but a precise delivery mechanism.

def build_dnssl(payload):
    # Type 31 DNSSL option, padded to 8-byte alignment.
    return struct.pack('!BBH', 31, length_units, lifetime) + payload + padding


def build_ra(dnssl):
    eth = Ether(dst='33:33:00:00:00:01')
    ipv6 = IPv6(dst='ff02::1')
    ra = ICMPv6ND_RA()
    return eth / ipv6 / ra / dnssl

This is why the exploit reads as polished rather than noisy. It is standards-aware. It knows the label rules, the alignment rules, and the neighbor discovery shape the target expects to see.

Why this is a Layer 2 problem before it is a CVE problem

Mental modelClassic exploitThis PoC
Input shapeMalformed bytesStandards-compliant IPv6 control traffic
Failure modeCrash or memory corruptionShell command execution
Required positionInternet reachabilitySame-segment Layer 2 adjacency
Defender blind spotApplication logsLocal network trust assumptions
SignalLoud and irregularQuiet and protocol-shaped

That adjacency requirement is the operational lesson. A Wi-Fi neighbor, an Ethernet neighbor, or a VLAN peer can be enough. If IPv6 discovery traffic is treated as harmless housekeeping, the attack surface stays hidden until it matters.

What this PoC gets right

The repository is sparse, but that is part of its value. It does one thing cleanly: encode the payload, build a valid packet, and transmit it repeatedly. There is no accidental complexity to distract from the exploit chain itself.

As a study object, that makes it better than a larger toolkit would be. You can read the code in one sitting and see the attack boundary clearly: packet construction on one side, shell execution on the other.

What the exploit is really teaching

The broader lesson is not specific to FreeBSD. Any system that moves data from one trust domain to another without rethinking the representation is vulnerable to the same class of mistake. Structure is not a permanent property. It can be lost at a handoff.

That is why this PoC is memorable. It does not depend on exotic memory tricks. It depends on a simple mistake that still survives in modern systems: treating data as safe because it was safe somewhere else.