CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: rapid7/metasploit-framework
Path: blob/master/lib/rex/proto/mssql/client_mixin.rb
Views: 11704
1
module Rex
2
module Proto
3
module MSSQL
4
# A base mixin of useful mssql methods for parsing structures etc
5
module ClientMixin
6
include Msf::Module::UI::Message
7
extend Forwardable
8
def_delegators :@framework_module, :print_prefix, :print_status, :print_error, :print_good, :print_warning, :print_line
9
# Encryption
10
ENCRYPT_OFF = 0x00 #Encryption is available but off.
11
ENCRYPT_ON = 0x01 #Encryption is available and on.
12
ENCRYPT_NOT_SUP = 0x02 #Encryption is not available.
13
ENCRYPT_REQ = 0x03 #Encryption is required.
14
15
# Packet Type
16
TYPE_SQL_BATCH = 1 # (Client) SQL command
17
TYPE_PRE_TDS7_LOGIN = 2 # (Client) Pre-login with version < 7 (unused)
18
TYPE_RPC = 3 # (Client) RPC
19
TYPE_TABLE_RESPONSE = 4 # (Server) Pre-Login Response ,Login Response, Row Data, Return Status, Return Parameters,
20
# Request Completion, Error and Info Messages, Attention Acknowledgement
21
TYPE_ATTENTION_SIGNAL = 6 # (Client) Attention
22
TYPE_BULK_LOAD = 7 # (Client) SQL Command with binary data
23
TYPE_TRANSACTION_MANAGER_REQUEST = 14 # (Client) Transaction request manager
24
TYPE_TDS7_LOGIN = 16 # (Client) Login
25
TYPE_SSPI_MESSAGE = 17 # (Client) Login
26
TYPE_PRE_LOGIN_MESSAGE = 18 # (Client) pre-login with version > 7
27
28
# Status
29
STATUS_NORMAL = 0x00
30
STATUS_END_OF_MESSAGE = 0x01
31
STATUS_IGNORE_EVENT = 0x02
32
STATUS_RESETCONNECTION = 0x08 # TDS 7.1+
33
STATUS_RESETCONNECTIONSKIPTRAN = 0x10 # TDS 7.3+
34
35
# Mappings for ENVCHANGE types
36
# See the TDS Specification here: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tds/2b3eb7e5-d43d-4d1b-bf4d-76b9e3afc791
37
module ENVCHANGE
38
DATABASE = 1
39
LANGUAGE = 2
40
CHARACTER_SET = 3
41
PACKET_SIZE = 4
42
UNICODE_LOCAL_ID = 5
43
UNICODE_COMPARISON_FLAGS = 6
44
SQL_COLLATION = 7
45
BEGIN_TRANSACTION = 8
46
COMMIT_TRANSACTION = 9
47
ROLLBACK_TRANSACTION = 10
48
ENLIST_DTC_TRANSACTION = 11
49
DEFECT_TRANSACTION = 12
50
REAL_TIME_LOG_SHIPPING = 13
51
PROMOTE_TRANSACTION = 15
52
TRANSACTION_MANAGER_ADDRESS = 16
53
TRANSACTION_ENDED = 17
54
COMPLETION_ACKNOWLEDGEMENT = 18
55
NAME_OF_USER_INSTANCE = 19
56
ROUTING_INFORMATION = 20
57
end
58
59
def mssql_print_reply(info)
60
print_status("SQL Query: #{info[:sql]}")
61
62
if info[:done] && info[:done][:rows].to_i > 0
63
print_status("Row Count: #{info[:done][:rows]} (Status: #{info[:done][:status]} Command: #{info[:done][:cmd]})")
64
end
65
66
if info[:errors] && !info[:errors].empty?
67
info[:errors].each do |err|
68
print_error(err)
69
end
70
end
71
72
if info[:rows] && !info[:rows].empty?
73
74
tbl = Rex::Text::Table.new(
75
'Indent' => 1,
76
'Header' => "Response",
77
'Columns' => info[:colnames],
78
'SortIndex' => -1
79
)
80
81
info[:rows].each do |row|
82
tbl << row.map{ |x| x.nil? ? 'nil' : x }
83
end
84
85
print_line(tbl.to_s)
86
end
87
end
88
89
def mssql_prelogin_packet
90
pkt = ""
91
pkt_hdr = ""
92
pkt_data_token = ""
93
pkt_data = ""
94
95
96
pkt_hdr = [
97
TYPE_PRE_LOGIN_MESSAGE, #type
98
STATUS_END_OF_MESSAGE, #status
99
0x0000, #length
100
0x0000, # SPID
101
0x00, # PacketID
102
0x00 #Window
103
]
104
105
version = [0x55010008, 0x0000].pack("Vv")
106
107
# if manually set, we will honour
108
if tdsencryption == true
109
encryption = ENCRYPT_ON
110
else
111
encryption = ENCRYPT_NOT_SUP
112
end
113
114
instoptdata = "MSSQLServer\0"
115
116
threadid = "\0\0" + Rex::Text.rand_text(2)
117
118
idx = 21 # size of pkt_data_token
119
pkt_data_token << [
120
0x00, # Token 0 type Version
121
idx , # VersionOffset
122
version.length, # VersionLength
123
124
0x01, # Token 1 type Encryption
125
idx = idx + version.length, # EncryptionOffset
126
0x01, # EncryptionLength
127
128
0x02, # Token 2 type InstOpt
129
idx = idx + 1, # InstOptOffset
130
instoptdata.length, # InstOptLength
131
132
0x03, # Token 3 type Threadid
133
idx + instoptdata.length, # ThreadIdOffset
134
0x04, # ThreadIdLength
135
136
0xFF
137
].pack('CnnCnnCnnCnnC')
138
139
pkt_data << pkt_data_token
140
pkt_data << version
141
pkt_data << encryption
142
pkt_data << instoptdata
143
pkt_data << threadid
144
145
pkt_hdr[2] = pkt_data.length + 8
146
147
pkt = pkt_hdr.pack('CCnnCC') + pkt_data
148
pkt
149
end
150
151
def parse_prelogin_response(resp)
152
data = {}
153
if resp.length > 5 # minimum size for response specification
154
version_index = resp.slice(1, 2).unpack('n')[0]
155
156
major = resp.slice(version_index, 1).unpack('C')[0]
157
minor = resp.slice(version_index+1, 1).unpack('C')[0]
158
build = resp.slice(version_index+2, 2).unpack('n')[0]
159
160
enc_index = resp.slice(6, 2).unpack('n')[0]
161
data[:encryption] = resp.slice(enc_index, 1).unpack('C')[0]
162
end
163
164
if major && minor && build
165
data[:version] = "#{major}.#{minor}.#{build}"
166
end
167
168
return data
169
end
170
171
def mssql_send_recv(req, timeout=15, check_status = true)
172
sock.put(req)
173
174
# Read the 8 byte header to get the length and status
175
# Read the length to get the data
176
# If the status is 0, read another header and more data
177
178
done = false
179
resp = ""
180
181
while(not done)
182
head = sock.get_once(8, timeout)
183
if !(head && head.length == 8)
184
return false
185
end
186
187
# Is this the last buffer?
188
if head[1, 1] == "\x01" || !check_status
189
done = true
190
end
191
192
# Grab this block's length
193
rlen = head[2, 2].unpack('n')[0] - 8
194
195
while(rlen > 0)
196
buff = sock.get_once(rlen, timeout)
197
return if not buff
198
resp << buff
199
rlen -= buff.length
200
end
201
end
202
203
resp
204
end
205
206
#
207
# Encrypt a password according to the TDS protocol (encode)
208
#
209
def mssql_tds_encrypt(pass)
210
# Convert to unicode, swap 4 bits both ways, xor with 0xa5
211
Rex::Text.to_unicode(pass).unpack('C*').map {|c| (((c & 0x0f) << 4) + ((c & 0xf0) >> 4)) ^ 0xa5 }.pack("C*")
212
end
213
214
def mssql_xpcmdshell(cmd, doprint=false, opts={})
215
force_enable = false
216
begin
217
res = query("EXEC master..xp_cmdshell '#{cmd}'", false, opts)
218
if res[:errors] && !res[:errors].empty?
219
if res[:errors].join =~ /xp_cmdshell/
220
if force_enable
221
print_error("The xp_cmdshell procedure is not available and could not be enabled")
222
raise RuntimeError, "Failed to execute command"
223
else
224
print_status("The server may have xp_cmdshell disabled, trying to enable it...")
225
query(mssql_xpcmdshell_enable())
226
raise RuntimeError, "xp_cmdshell disabled"
227
end
228
end
229
end
230
231
mssql_print_reply(res) if doprint
232
233
return res
234
235
rescue RuntimeError => e
236
if e.to_s =~ /xp_cmdshell disabled/
237
force_enable = true
238
retry
239
end
240
raise e
241
end
242
end
243
#
244
# Parse a raw TDS reply from the server
245
#
246
def mssql_parse_tds_reply(data, info)
247
info[:errors] ||= []
248
info[:colinfos] ||= []
249
info[:colnames] ||= []
250
251
# Parse out the columns
252
cols = data.slice!(0, 2).unpack('v')[0]
253
0.upto(cols-1) do |col_idx|
254
col = {}
255
info[:colinfos][col_idx] = col
256
257
col[:utype] = data.slice!(0, 2).unpack('v')[0]
258
col[:flags] = data.slice!(0, 2).unpack('v')[0]
259
col[:type] = data.slice!(0, 1).unpack('C')[0]
260
case col[:type]
261
when 48
262
col[:id] = :tinyint
263
264
when 52
265
col[:id] = :smallint
266
267
when 56
268
col[:id] = :rawint
269
270
when 61
271
col[:id] = :datetime
272
273
when 34
274
col[:id] = :image
275
col[:max_size] = data.slice!(0, 4).unpack('V')[0]
276
col[:value_length] = data.slice!(0, 2).unpack('v')[0]
277
col[:value] = data.slice!(0, col[:value_length] * 2).gsub("\x00", '')
278
279
when 109
280
col[:id] = :float
281
col[:value_length] = data.slice!(0, 1).unpack('C')[0]
282
283
when 108
284
col[:id] = :numeric
285
col[:value_length] = data.slice!(0, 1).unpack('C')[0]
286
col[:precision] = data.slice!(0, 1).unpack('C')[0]
287
col[:scale] = data.slice!(0, 1).unpack('C')[0]
288
289
when 60
290
col[:id] = :money
291
292
when 110
293
col[:value_length] = data.slice!(0, 1).unpack('C')[0]
294
case col[:value_length]
295
when 8
296
col[:id] = :money
297
when 4
298
col[:id] = :smallmoney
299
else
300
col[:id] = :unknown
301
end
302
303
when 111
304
col[:value_length] = data.slice!(0, 1).unpack('C')[0]
305
case col[:value_length]
306
when 4
307
col[:id] = :smalldatetime
308
when 8
309
col[:id] = :datetime
310
else
311
col[:id] = :unknown
312
end
313
314
when 122
315
col[:id] = :smallmoney
316
317
when 59
318
col[:id] = :float
319
320
when 58
321
col[:id] = :smalldatetime
322
323
when 36
324
col[:id] = :guid
325
col[:value_length] = data.slice!(0, 1).unpack('C')[0]
326
327
when 38
328
col[:id] = :int
329
col[:int_size] = data.slice!(0, 1).unpack('C')[0]
330
331
when 50
332
col[:id] = :bit
333
334
when 99
335
col[:id] = :ntext
336
col[:max_size] = data.slice!(0, 4).unpack('V')[0]
337
col[:codepage] = data.slice!(0, 2).unpack('v')[0]
338
col[:cflags] = data.slice!(0, 2).unpack('v')[0]
339
col[:charset_id] = data.slice!(0, 1).unpack('C')[0]
340
col[:namelen] = data.slice!(0, 1).unpack('C')[0]
341
col[:table_name] = data.slice!(0, (col[:namelen] * 2) + 1).gsub("\x00", '')
342
343
when 104
344
col[:id] = :bitn
345
col[:int_size] = data.slice!(0, 1).unpack('C')[0]
346
347
when 127
348
col[:id] = :bigint
349
350
when 165
351
col[:id] = :hex
352
col[:max_size] = data.slice!(0, 2).unpack('v')[0]
353
354
when 173
355
col[:id] = :hex # binary(2)
356
col[:max_size] = data.slice!(0, 2).unpack('v')[0]
357
358
when 231, 175, 167, 239
359
col[:id] = :string
360
col[:max_size] = data.slice!(0, 2).unpack('v')[0]
361
col[:codepage] = data.slice!(0, 2).unpack('v')[0]
362
col[:cflags] = data.slice!(0, 2).unpack('v')[0]
363
col[:charset_id] = data.slice!(0, 1).unpack('C')[0]
364
365
else
366
col[:id] = :unknown
367
368
# See https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tds/ce3183a6-9d89-47e8-a02f-de5a1a1303de for details about column types
369
info[:errors] << "Unsupported column type: #{col[:type]}. "
370
return info
371
end
372
373
col[:msg_len] = data.slice!(0, 1).unpack('C')[0]
374
375
if col[:msg_len] && col[:msg_len] > 0
376
col[:name] = data.slice!(0, col[:msg_len] * 2).gsub("\x00", '')
377
end
378
info[:colnames] << (col[:name] || 'NULL')
379
end
380
end
381
382
#
383
# Parse individual tokens from a TDS reply
384
#
385
def mssql_parse_reply(data, info)
386
info[:errors] = []
387
return if not data
388
states = []
389
until data.empty? || info[:errors].any?
390
token = data.slice!(0, 1).unpack('C')[0]
391
case token
392
when 0x81
393
states << :mssql_parse_tds_reply
394
mssql_parse_tds_reply(data, info)
395
when 0xd1
396
states << :mssql_parse_tds_row
397
mssql_parse_tds_row(data, info)
398
when 0xe3
399
states << :mssql_parse_env
400
mssql_parse_env(data, info)
401
when 0x79
402
states << :mssql_parse_ret
403
mssql_parse_ret(data, info)
404
when 0xfd, 0xfe, 0xff
405
states << :mssql_parse_done
406
mssql_parse_done(data, info)
407
when 0xad
408
states << :mssql_parse_login_ack
409
mssql_parse_login_ack(data, info)
410
when 0xab
411
states << :mssql_parse_info
412
mssql_parse_info(data, info)
413
when 0xaa
414
states << :mssql_parse_error
415
mssql_parse_error(data, info)
416
when nil
417
break
418
else
419
info[:errors] << "unsupported token: #{token}. Previous states: #{states}"
420
break
421
end
422
end
423
info
424
end
425
426
#
427
# Parse a single row of a TDS reply
428
#
429
def mssql_parse_tds_row(data, info)
430
info[:rows] ||= []
431
row = []
432
433
info[:colinfos].each do |col|
434
435
if(data.length == 0)
436
row << "<EMPTY>"
437
next
438
end
439
440
case col[:id]
441
when :hex
442
str = ""
443
len = data.slice!(0, 2).unpack('v')[0]
444
if len > 0 && len < 65535
445
str << data.slice!(0, len)
446
end
447
row << str.unpack("H*")[0]
448
449
when :guid
450
read_length = data.slice!(0, 1).unpack1('C')
451
if read_length == 0
452
row << nil
453
else
454
row << Rex::Text.to_guid(data.slice!(0, read_length))
455
end
456
457
when :string
458
str = ""
459
len = data.slice!(0, 2).unpack('v')[0]
460
if len > 0 && len < 65535
461
str << data.slice!(0, len)
462
end
463
row << str.gsub("\x00", '')
464
465
when :ntext
466
str = nil
467
ptrlen = data.slice!(0, 1).unpack("C")[0]
468
ptr = data.slice!(0, ptrlen)
469
unless ptrlen == 0
470
timestamp = data.slice!(0, 8)
471
datalen = data.slice!(0, 4).unpack("V")[0]
472
if datalen > 0 && datalen < 65535
473
str = data.slice!(0, datalen).gsub("\x00", '')
474
else
475
str = ''
476
end
477
end
478
row << str
479
480
when :float
481
datalen = data.slice!(0, 1).unpack('C')[0]
482
case datalen
483
when 8
484
row << data.slice!(0, datalen).unpack('E')[0]
485
when 4
486
row << data.slice!(0, datalen).unpack('e')[0]
487
else
488
row << nil
489
end
490
491
when :numeric
492
varlen = data.slice!(0, 1).unpack('C')[0]
493
if varlen == 0
494
row << nil
495
else
496
sign = data.slice!(0, 1).unpack('C')[0]
497
raw = data.slice!(0, varlen - 1)
498
value = ''
499
500
case varlen
501
when 5
502
value = raw.unpack('L')[0]/(10**col[:scale]).to_f
503
when 9
504
value = raw.unpack('Q')[0]/(10**col[:scale]).to_f
505
when 13
506
chunks = raw.unpack('L3')
507
value = chunks[2] << 64 | chunks[1] << 32 | chunks[0]
508
value /= (10**col[:scale]).to_f
509
when 17
510
chunks = raw.unpack('L4')
511
value = chunks[3] << 96 | chunks[2] << 64 | chunks[1] << 32 | chunks[0]
512
value /= (10**col[:scale]).to_f
513
end
514
case sign
515
when 1
516
row << value
517
when 0
518
row << value * -1
519
end
520
end
521
522
when :money
523
datalen = data.slice!(0, 1).unpack('C')[0]
524
if datalen == 0
525
row << nil
526
else
527
raw = data.slice!(0, datalen)
528
rev = raw.slice(4, 4) << raw.slice(0, 4)
529
row << rev.unpack('q')[0]/10000.0
530
end
531
532
when :smallmoney
533
datalen = data.slice!(0, 1).unpack('C')[0]
534
if datalen == 0
535
row << nil
536
else
537
row << data.slice!(0, datalen).unpack('l')[0] / 10000.0
538
end
539
540
when :smalldatetime
541
datalen = data.slice!(0, 1).unpack('C')[0]
542
if datalen == 0
543
row << nil
544
else
545
days = data.slice!(0, 2).unpack('S')[0]
546
minutes = data.slice!(0, 2).unpack('S')[0] / 1440.0
547
row << DateTime.new(1900, 1, 1) + days + minutes
548
end
549
550
when :datetime
551
datalen = data.slice!(0, 1).unpack('C')[0]
552
if datalen == 0
553
row << nil
554
else
555
days = data.slice!(0, 4).unpack('l')[0]
556
minutes = data.slice!(0, 4).unpack('l')[0] / 1440.0
557
row << DateTime.new(1900, 1, 1) + days + minutes
558
end
559
560
when :rawint
561
row << data.slice!(0, 4).unpack('V')[0]
562
563
when :bigint
564
row << data.slice!(0, 8).unpack("H*")[0]
565
566
when :smallint
567
row << data.slice!(0, 2).unpack("v")[0]
568
569
when :smallint3
570
row << [data.slice!(0, 3)].pack("Z4").unpack("V")[0]
571
572
when :tinyint
573
row << data.slice!(0, 1).unpack("C")[0]
574
575
when :bitn
576
has_value = data.slice!(0, 1).unpack("C")[0]
577
if has_value == 0
578
row << nil
579
else
580
row << data.slice!(0, 1).unpack("C")[0]
581
end
582
583
when :bit
584
row << data.slice!(0, 1).unpack("C")[0]
585
586
when :image
587
str = ''
588
len = data.slice!(0, 1).unpack('C')[0]
589
str = data.slice!(0, len) if len && len > 0
590
row << str.unpack("H*")[0]
591
592
when :int
593
len = data.slice!(0, 1).unpack("C")[0]
594
raw = data.slice!(0, len) if len && len > 0
595
596
case len
597
when 0, 255
598
row << ''
599
when 1
600
row << raw.unpack("C")[0]
601
when 2
602
row << raw.unpack('v')[0]
603
when 4
604
row << raw.unpack('V')[0]
605
when 5
606
row << raw.unpack('V')[0] # XXX: missing high byte
607
when 8
608
row << raw.unpack('VV')[0] # XXX: missing high dword
609
else
610
info[:errors] << "invalid integer size: #{len} #{data[0, 16].unpack("H*")[0]}"
611
end
612
else
613
info[:errors] << "unknown column type: #{col.inspect}"
614
end
615
end
616
617
info[:rows] << row
618
info
619
end
620
621
#
622
# Parse a "ret" TDS token
623
#
624
def mssql_parse_ret(data, info)
625
ret = data.slice!(0, 4).unpack('N')[0]
626
info[:ret] = ret
627
info
628
end
629
630
#
631
# Parse a "done" TDS token
632
#
633
def mssql_parse_done(data, info)
634
status, cmd, rows = data.slice!(0, 8).unpack('vvV')
635
info[:done] = { :status => status, :cmd => cmd, :rows => rows }
636
info
637
end
638
639
#
640
# Parse an "error" TDS token
641
#
642
def mssql_parse_error(data, info)
643
len = data.slice!(0, 2).unpack('v')[0]
644
buff = data.slice!(0, len)
645
646
errno, state, sev, elen = buff.slice!(0, 8).unpack('VCCv')
647
emsg = buff.slice!(0, elen * 2)
648
emsg.gsub!("\x00", '')
649
650
info[:errors] << "SQL Server Error ##{errno} (State:#{state} Severity:#{sev}): #{emsg}"
651
info
652
end
653
654
#
655
# Parse an "environment change" TDS token
656
#
657
def mssql_parse_env(data, info)
658
len = data.slice!(0, 2).unpack('v')[0]
659
buff = data.slice!(0, len)
660
type = buff.slice!(0, 1).unpack('C')[0]
661
662
nval = ''
663
nlen = buff.slice!(0, 1).unpack('C')[0] || 0
664
nval = buff.slice!(0, nlen * 2).gsub("\x00", '') if nlen > 0
665
666
oval = ''
667
olen = buff.slice!(0, 1).unpack('C')[0] || 0
668
oval = buff.slice!(0, olen * 2).gsub("\x00", '') if olen > 0
669
670
info[:envs] ||= []
671
info[:envs] << { :type => type, :old => oval, :new => nval }
672
673
self.current_database = nval if type == ENVCHANGE::DATABASE
674
675
info
676
end
677
678
#
679
# Parse an "information" TDS token
680
#
681
def mssql_parse_info(data, info)
682
len = data.slice!(0, 2).unpack('v')[0]
683
buff = data.slice!(0, len)
684
685
errno, state, sev, elen = buff.slice!(0, 8).unpack('VCCv')
686
emsg = buff.slice!(0, elen * 2)
687
emsg.gsub!("\x00", '')
688
689
info[:infos] ||= []
690
info[:infos] << "SQL Server Info ##{errno} (State:#{state} Severity:#{sev}): #{emsg}"
691
info
692
end
693
694
#
695
# Parse a "login ack" TDS token
696
#
697
def mssql_parse_login_ack(data, info)
698
len = data.slice!(0, 2).unpack('v')[0]
699
_buff = data.slice!(0, len)
700
info[:login_ack] = true
701
end
702
703
end
704
end
705
end
706
end
707
708