Files
metasploit-gs/modules/post/multi/recon/sudo_commands.rb
T

252 lines
7.0 KiB
Ruby
Raw Normal View History

2018-05-14 18:31:24 +00:00
##
# 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
2018-05-15 18:53:52 +00:00
datastore['PASSWORD'].to_s
2018-05-14 18:31:24 +00:00
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
2018-05-25 04:20:25 +00:00
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
2018-05-14 18:31:24 +00:00
su sudo
2018-05-25 04:20:25 +00:00
expect ionice nice script setarch strace taskset time
wget curl ftp scp sftp ssh tftp
2018-05-14 18:31:24 +00:00
nmap
2018-05-25 04:20:25 +00:00
ed emacs man nano vi vim visudo
dpkg rpm rpmquery
2018-05-14 18:31:24 +00:00
]
end
2018-05-15 18:53:52 +00:00
#
# Check if a sudo command offers prvileged code execution
#
2018-05-14 18:31:24 +00:00
def check_eop(cmd)
2018-05-15 18:53:52 +00:00
# drop args for simplicity (at the risk of false positives)
2018-05-14 18:31:24 +00:00
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
2018-11-04 05:28:32 +00:00
if writable? cmd
2018-05-14 18:31:24 +00:00
print_good "#{cmd} is writable!"
return true
end
2018-11-04 05:28:32 +00:00
elsif writable? base_dir
2018-05-14 18:31:24 +00:00
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
2018-05-15 18:53:52 +00:00
cmd = "#{sudo_path} -n -l"
2018-05-14 18:31:24 +00:00
vprint_status "Executing: #{cmd}"
output = cmd_exec(cmd).to_s
2018-05-15 18:53:52 +00:00
if output.start_with?('usage:') || output.include?('illegal option') || output.include?('a password is required')
2018-05-14 18:31:24 +00:00
# try with a password from stdin (-S)
2018-05-15 18:53:52 +00:00
cmd = "echo #{password} | #{sudo_path} -S -l"
2018-05-14 18:31:24 +00:00
vprint_status "Executing: #{cmd}"
output = cmd_exec(cmd).to_s
end
output
end
2018-05-15 18:53:52 +00:00
#
# 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
2018-05-14 18:31:24 +00:00
end
2018-05-15 18:53:52 +00:00
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
2018-05-14 18:31:24 +00:00
end
2018-05-15 18:53:52 +00:00
user = run_as.split(':')[0].to_s.strip || ''
group = run_as.split(':')[1].to_s.strip || ''
2018-05-14 18:31:24 +00:00
no_passwd = false
2018-05-15 18:53:52 +00:00
cmds = line.scan(/^\(.+?\) (.+)$/).flatten.first
if cmds.start_with? 'NOPASSWD:'
2018-05-14 18:31:24 +00:00
no_passwd = true
2018-05-15 18:53:52 +00:00
cmds = cmds.gsub(/^NOPASSWD:\s*/, '')
2018-05-14 18:31:24 +00:00
end
2018-05-15 18:53:52 +00:00
# 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
2018-05-14 18:31:24 +00:00
2018-05-15 18:53:52 +00:00
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
2018-05-14 18:31:24 +00:00
2018-05-15 18:53:52 +00:00
eop = check_eop cmd
2018-05-14 18:31:24 +00:00
2018-05-15 18:53:52 +00:00
@results << [cmd, user, group, no_passwd ? '' : 'True', eop ? 'True' : '']
end
2018-05-14 18:31:24 +00:00
end
2018-05-15 18:53:52 +00:00
rescue => e
print_error "Could not parse sudo ouput: #{e.message}"
2018-05-14 18:31:24 +00:00
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
2018-05-15 18:53:52 +00:00
if output =~ /^Sorry, .* may not run sudo/
fail_with Failure::NoAccess, 'Session user is not permitted to execute any commands with sudo'
2018-05-14 18:31:24 +00:00
end
2018-05-15 18:53:52 +00:00
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'
2018-05-14 18:31:24 +00:00
end
@results = Rex::Text::Table.new(
'Header' => 'Sudo Commands',
'Indent' => 2,
'Columns' =>
[
'Command',
'RunAsUsers',
'RunAsGroups',
'Password?',
'Privesc?'
]
)
2018-05-15 18:53:52 +00:00
parse_sudo output
2018-05-14 18:31:24 +00:00
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