From 94abd923f3bd1844928be2ac0a0c82ad981e5ff5 Mon Sep 17 00:00:00 2001 From: William Vu Date: Thu, 14 Jun 2018 16:33:50 -0500 Subject: [PATCH] Land #10021, post/multi/recon/sudo_commands module --- .../modules/post/multi/recon/sudo_commands.md | 92 +++++++ modules/post/multi/recon/sudo_commands.rb | 255 ++++++++++++++++++ 2 files changed, 347 insertions(+) create mode 100644 documentation/modules/post/multi/recon/sudo_commands.md create mode 100644 modules/post/multi/recon/sudo_commands.rb diff --git a/documentation/modules/post/multi/recon/sudo_commands.md b/documentation/modules/post/multi/recon/sudo_commands.md new file mode 100644 index 0000000000..5bf4399aca --- /dev/null +++ b/documentation/modules/post/multi/recon/sudo_commands.md @@ -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) > + ``` + diff --git a/modules/post/multi/recon/sudo_commands.rb b/modules/post/multi/recon/sudo_commands.rb new file mode 100644 index 0000000000..fb9d0090e7 --- /dev/null +++ b/modules/post/multi/recon/sudo_commands.rb @@ -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