252 lines
7.0 KiB
Ruby
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
|