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