CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

CoCalc provides the best real-time collaborative environment for Jupyter Notebooks, LaTeX documents, and SageMath, scalable from individual users to large groups and classes!

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