Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
rapid7
GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/post/multi/recon/sudo_commands.rb
19849 views
1
##
2
# This module requires Metasploit: https://metasploit.com/download
3
# Current source: https://github.com/rapid7/metasploit-framework
4
##
5
6
class MetasploitModule < Msf::Post
7
8
include Msf::Post::File
9
include Msf::Post::Linux::Priv
10
include Msf::Auxiliary::Report
11
12
def initialize(info = {})
13
super(
14
update_info(
15
info,
16
'Name' => 'Sudo Commands',
17
'Description' => %q{
18
This module examines the sudoers configuration for the session user
19
and lists the commands executable via sudo.
20
21
This module also inspects each command and reports potential avenues
22
for privileged code execution due to poor file system permissions or
23
permitting execution of executables known to be useful for privesc,
24
such as utilities designed for file read/write, user modification,
25
or execution of arbitrary operating system commands.
26
27
Note, you may need to provide the password for the session user.
28
},
29
'License' => MSF_LICENSE,
30
'Author' => [ 'bcoles' ],
31
'Platform' => [ 'bsd', 'linux', 'osx', 'solaris', 'unix' ],
32
'SessionTypes' => [ 'meterpreter', 'shell' ],
33
'Notes' => {
34
'Stability' => [CRASH_SAFE],
35
'SideEffects' => [IOC_IN_LOGS],
36
'Reliability' => []
37
}
38
)
39
)
40
register_options [
41
OptString.new('SUDO_PATH', [ true, 'Path to sudo executable', '/usr/bin/sudo' ]),
42
OptString.new('PASSWORD', [ false, 'Password for the current user', '' ])
43
]
44
end
45
46
def sudo_path
47
datastore['SUDO_PATH'].to_s
48
end
49
50
def password
51
datastore['PASSWORD'].to_s
52
end
53
54
def eop_bins
55
%w[
56
cat chgrp chmod chown cp echo find less ln mkdir more mv tail tar
57
usermod useradd userdel
58
env crontab
59
awk gdb gawk lua irb ld node perl php python python2 python3 ruby tclsh wish
60
ncat netcat netcat.traditional nc nc.traditional openssl socat telnet telnetd
61
ash bash csh dash ksh sh zsh
62
su sudo
63
expect ionice nice script setarch strace taskset time
64
wget curl ftp scp sftp ssh tftp
65
nmap
66
ed emacs man nano vi vim visudo
67
dpkg rpm rpmquery
68
]
69
end
70
71
#
72
# Check if a sudo command offers prvileged code execution
73
#
74
def check_eop(cmd)
75
# drop args for simplicity (at the risk of false positives)
76
cmd = cmd.split(/\s/).first
77
78
if cmd.eql? 'ALL'
79
print_good 'sudo any command!'
80
return true
81
end
82
83
base_dir = File.dirname cmd
84
base_name = File.basename cmd
85
86
if file_exist? cmd
87
if writable? cmd
88
print_good "#{cmd} is writable!"
89
return true
90
end
91
elsif writable? base_dir
92
print_good "#{cmd} does not exist and #{base_dir} is writable!"
93
return true
94
end
95
96
if eop_bins.include? base_name
97
print_good "#{cmd} matches known privesc executable '#{base_name}' !"
98
return true
99
end
100
101
false
102
end
103
104
#
105
# Retrieve list of sudo commands for current session user
106
#
107
def sudo_list
108
# try non-interactive (-n) without providing a password
109
cmd = "#{sudo_path} -n -l"
110
vprint_status "Executing: #{cmd}"
111
output = cmd_exec(cmd).to_s
112
113
if output.start_with?('usage:') || output.include?('illegal option') || output.include?('a password is required')
114
# try with a password from stdin (-S)
115
cmd = "echo #{password} | #{sudo_path} -S -l"
116
vprint_status "Executing: #{cmd}"
117
output = cmd_exec(cmd).to_s
118
end
119
120
output
121
end
122
123
#
124
# Format sudo output and extract permitted commands
125
#
126
def parse_sudo(sudo_data)
127
cmd_data = sudo_data.scan(/may run the following commands.*?$(.*)\z/m).flatten.first
128
129
# remove leading whitespace from each line and remove linewraps
130
formatted_data = ''
131
cmd_data.split("\n").reject { |line| line.eql?('') }.each do |line|
132
formatted_line = line.gsub(/^\s*/, '').to_s
133
if formatted_line.start_with? '('
134
formatted_data << "\n#{formatted_line}"
135
else
136
formatted_data << " #{formatted_line}"
137
end
138
end
139
140
formatted_data.split("\n").reject { |line| line.eql?('') }.each do |line|
141
run_as = line.scan(/^\((.+?)\)/).flatten.first
142
143
if run_as.blank?
144
print_warning "Could not parse sudoers entry: #{line.inspect}"
145
next
146
end
147
148
user = run_as.split(':')[0].to_s.strip || ''
149
group = run_as.split(':')[1].to_s.strip || ''
150
no_passwd = false
151
152
cmds = line.scan(/^\(.+?\) (.+)$/).flatten.first
153
if cmds.start_with? 'NOPASSWD:'
154
no_passwd = true
155
cmds = cmds.gsub(/^NOPASSWD:\s*/, '')
156
end
157
158
# Commands are separated by commas but may also contain commas (escaped with a backslash)
159
# so we temporarily replace escaped commas with some junk
160
# later, we'll replace each instance of the junk with a comma
161
junk = Rex::Text.rand_text_alpha(10)
162
cmds = cmds.gsub('\, ', junk)
163
164
cmds.split(', ').each do |cmd|
165
cmd = cmd.gsub(junk, ', ').strip
166
167
if cmd.start_with? '('
168
run_as = cmd.scan(/^\((.+?)\)/).flatten.first
169
170
if run_as.blank?
171
print_warning "Could not parse sudo command: #{cmd.inspect}"
172
next
173
end
174
175
user = run_as.split(':')[0].to_s.strip || ''
176
group = run_as.split(':')[1].to_s.strip || ''
177
cmd = cmd.scan(/^\(.+?\) (.+)$/).flatten.first
178
end
179
180
msg = "Command: #{cmd.inspect}"
181
msg << " RunAsUsers: #{user}" unless user.eql? ''
182
msg << " RunAsGroups: #{group}" unless group.eql? ''
183
msg << ' without providing a password' if no_passwd
184
vprint_status msg
185
186
eop = check_eop cmd
187
188
@results << [cmd, user, group, no_passwd ? '' : 'True', eop ? 'True' : '']
189
end
190
end
191
rescue StandardError => e
192
print_error "Could not parse sudo output: #{e.message}"
193
end
194
195
def run
196
if is_root?
197
fail_with Failure::BadConfig, 'Session already has root privileges'
198
end
199
200
unless executable? sudo_path
201
print_error 'Could not find sudo executable'
202
return
203
end
204
205
output = sudo_list
206
vprint_line output
207
vprint_line
208
209
if output.include? 'Sorry, try again'
210
fail_with Failure::NoAccess, 'Incorrect password'
211
end
212
213
if output =~ /^Sorry, .* may not run sudo/
214
fail_with Failure::NoAccess, 'Session user is not permitted to execute any commands with sudo'
215
end
216
217
if output !~ /may run the following commands/
218
fail_with Failure::NoAccess, 'Incorrect password, or the session user is not permitted to execute any commands with sudo'
219
end
220
221
@results = Rex::Text::Table.new(
222
'Header' => 'Sudo Commands',
223
'Indent' => 2,
224
'Columns' =>
225
[
226
'Command',
227
'RunAsUsers',
228
'RunAsGroups',
229
'Password?',
230
'Privesc?'
231
]
232
)
233
234
parse_sudo output
235
236
if @results.rows.empty?
237
print_status 'Found no sudo commands for the session user'
238
return
239
end
240
241
print_line
242
print_line @results.to_s
243
244
path = store_loot(
245
'sudo.commands',
246
'text/csv',
247
session,
248
@results.to_csv,
249
'sudo.commands.txt',
250
'Sudo Commands'
251
)
252
253
print_good "Output stored in: #{path}"
254
end
255
end
256
257