metasploit-framework/modules/post/multi/recon/sudo_commands.rb

252 lines
7.0 KiB
Ruby

##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Post
include Msf::Post::File
include Msf::Post::Linux::Priv
include Msf::Auxiliary::Report
def initialize(info = {})
super(update_info(info,
'Name' => 'Sudo Commands',
'Description' => %q{
This module examines the sudoers configuration for the session user
and lists the commands executable via sudo.
This module also inspects each command and reports potential avenues
for privileged code execution due to poor file system permissions or
permitting execution of executables known to be useful for privesc,
such as utilities designed for file read/write, user modification,
or execution of arbitrary operating system commands.
Note, you may need to provide the password for the session user.
},
'License' => MSF_LICENSE,
'Author' => [ 'bcoles' ],
'Platform' => [ 'bsd', 'linux', 'osx', 'solaris', 'unix' ],
'SessionTypes' => [ 'meterpreter', 'shell' ]
))
register_options [
OptString.new('SUDO_PATH', [ true, 'Path to sudo executable', '/usr/bin/sudo' ]),
OptString.new('PASSWORD', [ false, 'Password for the current user', '' ])
]
end
def sudo_path
datastore['SUDO_PATH'].to_s
end
def password
datastore['PASSWORD'].to_s
end
def is_executable?(path)
cmd_exec("test -x '#{path}' && echo true").include? 'true'
end
def eop_bins
%w[
cat chgrp chmod chown cp echo find less ln mkdir more mv tail tar
usermod useradd userdel
env crontab
awk gdb gawk lua irb ld node perl php python python2 python3 ruby tclsh wish
ncat netcat netcat.traditional nc nc.traditional openssl socat telnet telnetd
ash bash csh dash ksh sh zsh
su sudo
expect ionice nice script setarch strace taskset time
wget curl ftp scp sftp ssh tftp
nmap
ed emacs man nano vi vim visudo
dpkg rpm rpmquery
]
end
#
# Check if a sudo command offers prvileged code execution
#
def check_eop(cmd)
# drop args for simplicity (at the risk of false positives)
cmd = cmd.split(/\s/).first
if cmd.eql? 'ALL'
print_good 'sudo any command!'
return true
end
base_dir = File.dirname cmd
base_name = File.basename cmd
if file_exist? cmd
if writable? cmd
print_good "#{cmd} is writable!"
return true
end
elsif writable? base_dir
print_good "#{cmd} does not exist and #{base_dir} is writable!"
return true
end
if eop_bins.include? base_name
print_good "#{cmd} matches known privesc executable '#{base_name}' !"
return true
end
false
end
#
# Retrieve list of sudo commands for current session user
#
def sudo_list
# try non-interactive (-n) without providing a password
cmd = "#{sudo_path} -n -l"
vprint_status "Executing: #{cmd}"
output = cmd_exec(cmd).to_s
if output.start_with?('usage:') || output.include?('illegal option') || output.include?('a password is required')
# try with a password from stdin (-S)
cmd = "echo #{password} | #{sudo_path} -S -l"
vprint_status "Executing: #{cmd}"
output = cmd_exec(cmd).to_s
end
output
end
#
# Format sudo output and extract permitted commands
#
def parse_sudo(sudo_data)
cmd_data = sudo_data.scan(/may run the following commands.*?$(.*)\z/m).flatten.first
# remove leading whitespace from each line and remove linewraps
formatted_data = ''
cmd_data.split("\n").reject { |line| line.eql?('') }.each do |line|
formatted_line = line.gsub(/^\s*/, '').to_s
if formatted_line.start_with? '('
formatted_data << "\n#{formatted_line}"
else
formatted_data << " #{formatted_line}"
end
end
formatted_data.split("\n").reject { |line| line.eql?('') }.each do |line|
run_as = line.scan(/^\((.+?)\)/).flatten.first
if run_as.blank?
print_warning "Could not parse sudoers entry: #{line.inspect}"
next
end
user = run_as.split(':')[0].to_s.strip || ''
group = run_as.split(':')[1].to_s.strip || ''
no_passwd = false
cmds = line.scan(/^\(.+?\) (.+)$/).flatten.first
if cmds.start_with? 'NOPASSWD:'
no_passwd = true
cmds = cmds.gsub(/^NOPASSWD:\s*/, '')
end
# Commands are separated by commas but may also contain commas (escaped with a backslash)
# so we temporarily replace escaped commas with some junk
# later, we'll replace each instance of the junk with a comma
junk = Rex::Text.rand_text_alpha(10)
cmds = cmds.gsub('\, ', junk)
cmds.split(', ').each do |cmd|
cmd = cmd.gsub(junk, ', ').strip
if cmd.start_with? '('
run_as = cmd.scan(/^\((.+?)\)/).flatten.first
if run_as.blank?
print_warning "Could not parse sudo command: #{cmd.inspect}"
next
end
user = run_as.split(':')[0].to_s.strip || ''
group = run_as.split(':')[1].to_s.strip || ''
cmd = cmd.scan(/^\(.+?\) (.+)$/).flatten.first
end
msg = "Command: #{cmd.inspect}"
msg << " RunAsUsers: #{user}" unless user.eql? ''
msg << " RunAsGroups: #{group}" unless group.eql? ''
msg << ' without providing a password' if no_passwd
vprint_status msg
eop = check_eop cmd
@results << [cmd, user, group, no_passwd ? '' : 'True', eop ? 'True' : '']
end
end
rescue => e
print_error "Could not parse sudo ouput: #{e.message}"
end
def run
if is_root?
fail_with Failure::BadConfig, 'Session already has root privileges'
end
unless is_executable? sudo_path
print_error 'Could not find sudo executable'
return
end
output = sudo_list
vprint_line output
vprint_line
if output.include? 'Sorry, try again'
fail_with Failure::NoAccess, 'Incorrect password'
end
if output =~ /^Sorry, .* may not run sudo/
fail_with Failure::NoAccess, 'Session user is not permitted to execute any commands with sudo'
end
if output !~ /may run the following commands/
fail_with Failure::NoAccess, 'Incorrect password, or the session user is not permitted to execute any commands with sudo'
end
@results = Rex::Text::Table.new(
'Header' => 'Sudo Commands',
'Indent' => 2,
'Columns' =>
[
'Command',
'RunAsUsers',
'RunAsGroups',
'Password?',
'Privesc?'
]
)
parse_sudo output
if @results.rows.empty?
print_status 'Found no sudo commands for the session user'
return
end
print_line
print_line @results.to_s
path = store_loot(
'sudo.commands',
'text/csv',
session,
@results.to_csv,
'sudo.commands.txt',
'Sudo Commands'
)
print_good "Output stored in: #{path}"
end
end