Land #10021, post/multi/recon/sudo_commands module
parent
8cd65324c6
commit
94abd923f3
|
@ -0,0 +1,92 @@
|
|||
## Description
|
||||
|
||||
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.
|
||||
|
||||
|
||||
## Verification Steps
|
||||
|
||||
1. Start `msfconsole`
|
||||
2. Get a session
|
||||
3. `use post/multi/recon/sudo_commands`
|
||||
4. `set SESSION [SESSION]`
|
||||
5. `run`
|
||||
6. You should receive a list of available `sudo` commands
|
||||
|
||||
|
||||
## Options
|
||||
|
||||
**SESSION**
|
||||
|
||||
Which session to use, which can be viewed with `sessions`
|
||||
|
||||
**SUDO_PATH**
|
||||
|
||||
Path to sudo executable (default: `/usr/bin/sudo`)
|
||||
|
||||
**PASSWORD**
|
||||
|
||||
Password for the session user
|
||||
|
||||
|
||||
## Scenarios
|
||||
|
||||
```
|
||||
msf5 > use post/multi/recon/sudo_commands
|
||||
msf5 post(multi/recon/sudo_commands) > set session 1
|
||||
session => 1
|
||||
msf5 post(multi/recon/sudo_commands) > set verbose true
|
||||
verbose => true
|
||||
msf5 post(multi/recon/sudo_commands) > run
|
||||
|
||||
[*] Executing: /usr/bin/sudo -n -l
|
||||
Matching Defaults entries for wvu on localhost:
|
||||
!visiblepw, always_set_home, match_group_by_gid, env_reset, env_keep="COLORS DISPLAY HOSTNAME HISTSIZE KDEDIR LS_COLORS", env_keep+="MAIL PS1 PS2 QTDIR USERNAME LANG LC_ADDRESS LC_CTYPE", env_keep+="LC_COLLATE LC_IDENTIFICATION LC_MEASUREMENT LC_MESSAGES", env_keep+="LC_MONETARY LC_NAME LC_NUMERIC LC_PAPER LC_TELEPHONE", env_keep+="LC_TIME LC_ALL LANGUAGE LINGUAS _XKB_CHARSET XAUTHORITY", secure_path=/sbin\:/bin\:/usr/sbin\:/usr/bin
|
||||
|
||||
User wvu may run the following commands on localhost:
|
||||
(ALL) ALL
|
||||
(ALL) NOPASSWD: ALL
|
||||
(root) /sbin/mount /mnt/cdrom, /sbin/umount /mnt/cdrom
|
||||
(root) /sbin/shutdown -h now
|
||||
|
||||
[*] Command: "ALL" RunAsUsers: ALL
|
||||
[+] sudo any command!
|
||||
[*] Command: "ALL" RunAsUsers: ALL without providing a password
|
||||
[+] sudo any command!
|
||||
[*] Command: "/sbin/mount /mnt/cdrom" RunAsUsers: root
|
||||
[*] Command: "/sbin/umount /mnt/cdrom" RunAsUsers: root
|
||||
[*] Command: "/sbin/shutdown -h now" RunAsUsers: root
|
||||
|
||||
Sudo Commands
|
||||
=============
|
||||
|
||||
Command RunAsUsers RunAsGroups Password? Privesc?
|
||||
------- ---------- ----------- --------- --------
|
||||
/sbin/mount /mnt/cdrom root True
|
||||
/sbin/shutdown -h now root True
|
||||
/sbin/umount /mnt/cdrom root True
|
||||
ALL ALL True True
|
||||
ALL ALL True
|
||||
|
||||
[+] Output stored in: /Users/user/.msf4/loot/20180613134731_default_192.168.56.101_sudo.commands_305964.txt
|
||||
[*] Post module execution completed
|
||||
msf5 post(multi/recon/sudo_commands) > cat /Users/user/.msf4/loot/20180613134731_default_192.168.56.101_sudo.commands_305964.txt
|
||||
[*] exec: cat /Users/user/.msf4/loot/20180613134731_default_192.168.56.101_sudo.commands_305964.txt
|
||||
|
||||
Command,RunAsUsers,RunAsGroups,Password?,Privesc?
|
||||
"/sbin/mount /mnt/cdrom","root","","True",""
|
||||
"/sbin/shutdown -h now","root","","True",""
|
||||
"/sbin/umount /mnt/cdrom","root","","True",""
|
||||
"ALL","ALL","","True","True"
|
||||
"ALL","ALL","","","True"
|
||||
msf5 post(multi/recon/sudo_commands) >
|
||||
```
|
||||
|
|
@ -0,0 +1,255 @@
|
|||
##
|
||||
# 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_writable?(path)
|
||||
cmd_exec("test -w '#{path}' && echo true").include? 'true'
|
||||
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 is_writable? cmd
|
||||
print_good "#{cmd} is writable!"
|
||||
return true
|
||||
end
|
||||
elsif is_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
|
Loading…
Reference in New Issue