Path: blob/master/modules/post/multi/recon/sudo_commands.rb
19849 views
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##45class MetasploitModule < Msf::Post67include Msf::Post::File8include Msf::Post::Linux::Priv9include Msf::Auxiliary::Report1011def initialize(info = {})12super(13update_info(14info,15'Name' => 'Sudo Commands',16'Description' => %q{17This module examines the sudoers configuration for the session user18and lists the commands executable via sudo.1920This module also inspects each command and reports potential avenues21for privileged code execution due to poor file system permissions or22permitting execution of executables known to be useful for privesc,23such as utilities designed for file read/write, user modification,24or execution of arbitrary operating system commands.2526Note, you may need to provide the password for the session user.27},28'License' => MSF_LICENSE,29'Author' => [ 'bcoles' ],30'Platform' => [ 'bsd', 'linux', 'osx', 'solaris', 'unix' ],31'SessionTypes' => [ 'meterpreter', 'shell' ],32'Notes' => {33'Stability' => [CRASH_SAFE],34'SideEffects' => [IOC_IN_LOGS],35'Reliability' => []36}37)38)39register_options [40OptString.new('SUDO_PATH', [ true, 'Path to sudo executable', '/usr/bin/sudo' ]),41OptString.new('PASSWORD', [ false, 'Password for the current user', '' ])42]43end4445def sudo_path46datastore['SUDO_PATH'].to_s47end4849def password50datastore['PASSWORD'].to_s51end5253def eop_bins54%w[55cat chgrp chmod chown cp echo find less ln mkdir more mv tail tar56usermod useradd userdel57env crontab58awk gdb gawk lua irb ld node perl php python python2 python3 ruby tclsh wish59ncat netcat netcat.traditional nc nc.traditional openssl socat telnet telnetd60ash bash csh dash ksh sh zsh61su sudo62expect ionice nice script setarch strace taskset time63wget curl ftp scp sftp ssh tftp64nmap65ed emacs man nano vi vim visudo66dpkg rpm rpmquery67]68end6970#71# Check if a sudo command offers prvileged code execution72#73def check_eop(cmd)74# drop args for simplicity (at the risk of false positives)75cmd = cmd.split(/\s/).first7677if cmd.eql? 'ALL'78print_good 'sudo any command!'79return true80end8182base_dir = File.dirname cmd83base_name = File.basename cmd8485if file_exist? cmd86if writable? cmd87print_good "#{cmd} is writable!"88return true89end90elsif writable? base_dir91print_good "#{cmd} does not exist and #{base_dir} is writable!"92return true93end9495if eop_bins.include? base_name96print_good "#{cmd} matches known privesc executable '#{base_name}' !"97return true98end99100false101end102103#104# Retrieve list of sudo commands for current session user105#106def sudo_list107# try non-interactive (-n) without providing a password108cmd = "#{sudo_path} -n -l"109vprint_status "Executing: #{cmd}"110output = cmd_exec(cmd).to_s111112if output.start_with?('usage:') || output.include?('illegal option') || output.include?('a password is required')113# try with a password from stdin (-S)114cmd = "echo #{password} | #{sudo_path} -S -l"115vprint_status "Executing: #{cmd}"116output = cmd_exec(cmd).to_s117end118119output120end121122#123# Format sudo output and extract permitted commands124#125def parse_sudo(sudo_data)126cmd_data = sudo_data.scan(/may run the following commands.*?$(.*)\z/m).flatten.first127128# remove leading whitespace from each line and remove linewraps129formatted_data = ''130cmd_data.split("\n").reject { |line| line.eql?('') }.each do |line|131formatted_line = line.gsub(/^\s*/, '').to_s132if formatted_line.start_with? '('133formatted_data << "\n#{formatted_line}"134else135formatted_data << " #{formatted_line}"136end137end138139formatted_data.split("\n").reject { |line| line.eql?('') }.each do |line|140run_as = line.scan(/^\((.+?)\)/).flatten.first141142if run_as.blank?143print_warning "Could not parse sudoers entry: #{line.inspect}"144next145end146147user = run_as.split(':')[0].to_s.strip || ''148group = run_as.split(':')[1].to_s.strip || ''149no_passwd = false150151cmds = line.scan(/^\(.+?\) (.+)$/).flatten.first152if cmds.start_with? 'NOPASSWD:'153no_passwd = true154cmds = cmds.gsub(/^NOPASSWD:\s*/, '')155end156157# Commands are separated by commas but may also contain commas (escaped with a backslash)158# so we temporarily replace escaped commas with some junk159# later, we'll replace each instance of the junk with a comma160junk = Rex::Text.rand_text_alpha(10)161cmds = cmds.gsub('\, ', junk)162163cmds.split(', ').each do |cmd|164cmd = cmd.gsub(junk, ', ').strip165166if cmd.start_with? '('167run_as = cmd.scan(/^\((.+?)\)/).flatten.first168169if run_as.blank?170print_warning "Could not parse sudo command: #{cmd.inspect}"171next172end173174user = run_as.split(':')[0].to_s.strip || ''175group = run_as.split(':')[1].to_s.strip || ''176cmd = cmd.scan(/^\(.+?\) (.+)$/).flatten.first177end178179msg = "Command: #{cmd.inspect}"180msg << " RunAsUsers: #{user}" unless user.eql? ''181msg << " RunAsGroups: #{group}" unless group.eql? ''182msg << ' without providing a password' if no_passwd183vprint_status msg184185eop = check_eop cmd186187@results << [cmd, user, group, no_passwd ? '' : 'True', eop ? 'True' : '']188end189end190rescue StandardError => e191print_error "Could not parse sudo output: #{e.message}"192end193194def run195if is_root?196fail_with Failure::BadConfig, 'Session already has root privileges'197end198199unless executable? sudo_path200print_error 'Could not find sudo executable'201return202end203204output = sudo_list205vprint_line output206vprint_line207208if output.include? 'Sorry, try again'209fail_with Failure::NoAccess, 'Incorrect password'210end211212if output =~ /^Sorry, .* may not run sudo/213fail_with Failure::NoAccess, 'Session user is not permitted to execute any commands with sudo'214end215216if output !~ /may run the following commands/217fail_with Failure::NoAccess, 'Incorrect password, or the session user is not permitted to execute any commands with sudo'218end219220@results = Rex::Text::Table.new(221'Header' => 'Sudo Commands',222'Indent' => 2,223'Columns' =>224[225'Command',226'RunAsUsers',227'RunAsGroups',228'Password?',229'Privesc?'230]231)232233parse_sudo output234235if @results.rows.empty?236print_status 'Found no sudo commands for the session user'237return238end239240print_line241print_line @results.to_s242243path = store_loot(244'sudo.commands',245'text/csv',246session,247@results.to_csv,248'sudo.commands.txt',249'Sudo Commands'250)251252print_good "Output stored in: #{path}"253end254end255256257