Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
rapid7
GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/auxiliary/scanner/discovery/udp_probe.rb
19567 views
1
##
2
# This module requires Metasploit: https://metasploit.com/download
3
# Current source: https://github.com/rapid7/metasploit-framework
4
##
5
6
require 'English'
7
require 'openssl'
8
9
class MetasploitModule < Msf::Auxiliary
10
include Msf::Auxiliary::Report
11
include Msf::Auxiliary::Scanner
12
13
def initialize
14
super(
15
'Name' => 'UDP Service Prober',
16
'Description' => 'Detect common UDP services using sequential probes.',
17
'Author' => 'hdm',
18
'License' => MSF_LICENSE,
19
'Notes' => {
20
'Stability' => [CRASH_SAFE],
21
'SideEffects' => [IOC_IN_LOGS],
22
'Reliability' => []
23
}
24
)
25
26
register_options(
27
[
28
Opt::CHOST,
29
]
30
)
31
32
register_advanced_options(
33
[
34
OptBool.new('RANDOMIZE_PORTS', [false, 'Randomize the order the ports are probed', true])
35
]
36
)
37
38
# Initialize the probes array
39
@probes = []
40
41
# Add the UDP probe method names
42
@probes << 'probe_pkt_dns'
43
@probes << 'probe_pkt_netbios'
44
@probes << 'probe_pkt_portmap'
45
@probes << 'probe_pkt_mssql'
46
@probes << 'probe_pkt_ntp'
47
@probes << 'probe_pkt_snmp1'
48
@probes << 'probe_pkt_snmp2'
49
@probes << 'probe_pkt_sentinel'
50
@probes << 'probe_pkt_db2disco'
51
@probes << 'probe_pkt_citrix'
52
@probes << 'probe_pkt_pca_st'
53
@probes << 'probe_pkt_pca_nq'
54
@probes << 'probe_chargen'
55
end
56
57
def setup
58
super
59
60
if datastore['RANDOMIZE_PORTS']
61
@probes = @probes.sort_by { rand }
62
end
63
end
64
65
# Fingerprint a single host
66
def run_host(ip)
67
@results = {}
68
@thost = ip
69
70
begin
71
udp_sock = nil
72
73
@probes.each do |probe|
74
# Send each probe to each host
75
76
data, port = send(probe, ip)
77
@tport = port
78
79
# Create an unbound UDP socket if no CHOST is specified, otherwise
80
# create a UDP socket bound to CHOST (in order to avail of pivoting)
81
udp_sock = Rex::Socket::Udp.create({
82
'LocalHost' => datastore['CHOST'] || nil,
83
'PeerHost' => ip, 'PeerPort' => port,
84
'Context' => { 'Msf' => framework, 'MsfExploit' => self }
85
})
86
87
udp_sock.put(data)
88
89
r = udp_sock.recvfrom(65535, 0.1) and r[1]
90
parse_reply(r) if r
91
rescue ::Interrupt
92
raise $ERROR_INFO
93
rescue ::Rex::HostUnreachable, ::Rex::ConnectionTimeout, ::Rex::ConnectionRefused, ::IOError
94
nil
95
ensure
96
udp_sock.close if udp_sock
97
end
98
rescue ::Interrupt
99
raise $ERROR_INFO
100
rescue StandardError => e
101
print_error("Unknown error: #{@thost}:#{@tport} #{e.class} #{e} #{e.backtrace}")
102
end
103
104
@results.each_key do |k|
105
next if !@results[k].respond_to?('keys')
106
107
data = @results[k]
108
109
next unless inside_workspace_boundary?(data[:host])
110
111
conf = {
112
host: data[:host],
113
port: data[:port],
114
proto: 'udp',
115
name: data[:app],
116
info: data[:info]
117
}
118
119
if data[:hname]
120
conf[:host_name] = data[:hname].downcase
121
end
122
123
if data[:mac]
124
conf[:mac] = data[:mac].downcase
125
end
126
127
report_service(conf)
128
print_good("Discovered #{data[:app]} on #{k} (#{data[:info]})")
129
end
130
end
131
132
#
133
# The response parsers
134
#
135
def parse_reply(pkt)
136
# Ignore "empty" packets
137
return if !pkt[1]
138
139
if (pkt[1] =~ /^::ffff:/)
140
pkt[1] = pkt[1].sub(/^::ffff:/, '')
141
end
142
143
app = 'unknown'
144
inf = ''
145
maddr = nil
146
hname = nil
147
148
hkey = "#{pkt[1]}:#{pkt[2]}"
149
150
# Work with protocols that return different data in different packets
151
# These are reported at the end of the scanning loop to build state
152
case pkt[2]
153
when 5632
154
155
@results[hkey] ||= {}
156
data = @results[hkey]
157
data[:app] = 'pcAnywhere_stat'
158
data[:port] = pkt[2]
159
data[:host] = pkt[1]
160
161
case pkt[0]
162
163
when /^NR(........................)(........)/
164
name = ::Regexp.last_match(1).dup
165
caps = ::Regexp.last_match(2).dup
166
name = name.gsub(/_+$/, '').gsub("\x00", '').strip
167
caps = caps.gsub(/_+$/, '').gsub("\x00", '').strip
168
data[:name] = name
169
data[:caps] = caps
170
171
when /^ST(.+)/
172
buff = ::Regexp.last_match(1).dup
173
stat = 'Unknown'
174
175
if buff[2, 1].unpack('C')[0] == 67
176
stat = 'Available'
177
end
178
179
if buff[2, 1].unpack('C')[0] == 11
180
stat = 'Busy'
181
end
182
183
data[:stat] = stat
184
end
185
186
if data[:name]
187
inf << "Name: #{data[:name]} "
188
end
189
190
if data[:stat]
191
inf << "- #{data[:stat]} "
192
end
193
194
if data[:caps]
195
inf << "( #{data[:caps]} ) "
196
end
197
data[:info] = inf
198
end
199
200
# Ignore duplicates for the protocols below
201
return if @results[hkey]
202
203
case pkt[2]
204
205
when 19
206
app = 'chargen'
207
return unless chargen_parse(pkt[0])
208
209
@results[hkey] = true
210
211
when 53
212
app = 'DNS'
213
ver = nil
214
215
if !ver && pkt[0] =~ %r{([6789]\.[\w.\-_:()\[\]/=+|{}]+)}i
216
ver = 'BIND ' + ::Regexp.last_match(1)
217
end
218
219
ver = 'Microsoft DNS' if !ver && (pkt[0][2, 4] == "\x81\x04\x00\x01")
220
ver = 'TinyDNS' if !ver && (pkt[0][2, 4] == "\x81\x81\x00\x01")
221
222
ver = pkt[0].unpack('H*')[0] if !ver
223
inf = ver if ver
224
@results[hkey] = true
225
226
when 137
227
app = 'NetBIOS'
228
229
data = pkt[0]
230
231
head = data.slice!(0, 12)
232
233
_, _, quests, answers, = head.unpack('n6')
234
return if quests != 0
235
return if answers == 0
236
237
data.slice!(0, 34)
238
rtype, _, _, rlen = data.slice!(0, 10).unpack('nnNn')
239
buff = data.slice!(0, rlen)
240
241
names = []
242
243
case rtype
244
when 0x21
245
rcnt = buff.slice!(0, 1).unpack('C')[0]
246
1.upto(rcnt) do
247
tname = buff.slice!(0, 15).gsub(/\x00.*/, '').strip
248
ttype = buff.slice!(0, 1).unpack('C')[0]
249
tflag = buff.slice!(0, 2).unpack('n')[0]
250
names << [ tname, ttype, tflag ]
251
end
252
maddr = buff.slice!(0, 6).unpack('C*').map { |c| '%.2x' % c }.join(':')
253
254
names.each do |n|
255
inf << n[0]
256
inf << ':<%.2x>' % n[1]
257
if (n[2] & 0x8000 == 0)
258
inf << ':U :'
259
else
260
inf << ':G :'
261
end
262
end
263
inf << maddr
264
265
if !names.empty?
266
hname = names[0][0]
267
end
268
end
269
270
@results[hkey] = true
271
272
when 111
273
app = 'Portmap'
274
buf = pkt[0]
275
inf = ''
276
buf.slice!(0, 24)
277
svc = []
278
while (buf.length >= 20)
279
rec = buf.slice!(0, 20).unpack('N5')
280
svc << "#{rec[1]} v#{rec[2]} #{rec[3] == 0x06 ? 'TCP' : 'UDP'}(#{rec[4]})"
281
report_service(
282
host: pkt[1],
283
port: rec[4],
284
proto: (rec[3] == 0x06 ? 'tcp' : 'udp'),
285
name: 'sunrpc',
286
info: "#{rec[1]} v#{rec[2]}"
287
)
288
end
289
inf = svc.join(', ')
290
@results[hkey] = true
291
292
when 123
293
app = 'NTP'
294
ver = pkt[0].unpack('H*')[0]
295
ver = 'NTP v3' if (ver =~ /^1c06|^1c05/)
296
ver = 'NTP v4' if (ver =~ /^240304/)
297
ver = 'NTP v4 (unsynchronized)' if (ver =~ /^e40/)
298
ver = 'Microsoft NTP' if (ver =~ /^dc00|^dc0f/)
299
inf = ver if ver
300
@results[hkey] = true
301
302
when 1434
303
app = 'MSSQL'
304
mssql_ping_parse(pkt[0]).each_pair do |k, v|
305
inf += k + '=' + v + ' '
306
end
307
@results[hkey] = true
308
309
when 161
310
app = 'SNMP'
311
312
asn = begin
313
OpenSSL::ASN1.decode(pkt[0])
314
rescue StandardError
315
nil
316
end
317
return if !asn
318
319
snmp_error = begin
320
asn.value[0].value
321
rescue StandardError
322
nil
323
end
324
snmp_comm = begin
325
asn.value[1].value
326
rescue StandardError
327
nil
328
end
329
snmp_data = begin
330
asn.value[2].value[3].value[0]
331
rescue StandardError
332
nil
333
end
334
snmp_oid = begin
335
snmp_data.value[0].value
336
rescue StandardError
337
nil
338
end
339
snmp_info = begin
340
snmp_data.value[1].value
341
rescue StandardError
342
nil
343
end
344
345
return if !(snmp_error && snmp_comm && snmp_data && snmp_oid && snmp_info)
346
347
snmp_info = snmp_info.to_s.gsub(/\s+/, ' ')
348
349
inf = snmp_info
350
@results[hkey] = true
351
352
when 5093
353
app = 'Sentinel'
354
@results[hkey] = true
355
356
when 523
357
app = 'ibm-db2'
358
inf = db2disco_parse(pkt[0])
359
@results[hkey] = true
360
361
when 1604
362
app = 'citrix-ica'
363
return unless citrix_parse(pkt[0])
364
365
@results[hkey] = true
366
367
end
368
369
return unless inside_workspace_boundary?(pkt[1])
370
371
report_service(
372
host: pkt[1],
373
mac: (maddr && (maddr != '00:00:00:00:00:00')) ? maddr : nil,
374
host_name: hname ? hname.downcase : nil,
375
port: pkt[2],
376
proto: 'udp',
377
name: app,
378
info: inf
379
)
380
381
print_good("Discovered #{app} on #{pkt[1]}:#{pkt[2]} (#{inf})")
382
end
383
384
#
385
# Parse a db2disco packet.
386
#
387
def db2disco_parse(data)
388
res = data.split("\x00")
389
"#{res[2]}_#{res[1]}"
390
end
391
392
#
393
# Validate a chargen packet.
394
#
395
def chargen_parse(data)
396
data =~ /ABCDEFGHIJKLMNOPQRSTUVWXYZ|0123456789/i
397
end
398
399
#
400
# Validate this is truly Citrix ICA; returns true or false.
401
#
402
def citrix_parse(data)
403
server_response = "\x30\x00\x02\x31\x02\xfd\xa8\xe3\x02\x00\x06\x44" # Server hello response
404
data =~ /^#{server_response}/
405
end
406
407
#
408
# Parse a 'ping' response and format as a hash
409
#
410
def mssql_ping_parse(data)
411
res = {}
412
var = nil
413
idx = data.index('ServerName')
414
return res if !idx
415
416
data[idx, data.length - idx].split(';').each do |d|
417
if !var
418
var = d
419
elsif !var.empty?
420
res[var] = d
421
var = nil
422
end
423
end
424
425
return res
426
end
427
428
#
429
# The probe definitions
430
#
431
432
def probe_chargen(_ip)
433
pkt = Rex::Text.rand_text_alpha_lower(1)
434
return [pkt, 19]
435
end
436
437
def probe_pkt_dns(_ip)
438
data = [rand(0xffff)].pack('n') +
439
"\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00" \
440
"\x07" + 'VERSION' \
441
"\x04" + 'BIND' \
442
"\x00\x00\x10\x00\x03"
443
444
return [data, 53]
445
end
446
447
def probe_pkt_netbios(_ip)
448
data =
449
[rand(0xffff)].pack('n') +
450
"\x00\x00\x00\x01\x00\x00\x00\x00" \
451
"\x00\x00\x20\x43\x4b\x41\x41\x41" \
452
"\x41\x41\x41\x41\x41\x41\x41\x41" \
453
"\x41\x41\x41\x41\x41\x41\x41\x41" \
454
"\x41\x41\x41\x41\x41\x41\x41\x41" \
455
"\x41\x41\x41\x00\x00\x21\x00\x01"
456
457
return [data, 137]
458
end
459
460
def probe_pkt_portmap(_ip)
461
data =
462
[
463
rand(0xffffffff), # XID
464
0, # Type
465
2, # RPC Version
466
100000, # Program ID
467
2, # Program Version
468
4, # Procedure
469
0, 0, # Credentials
470
0, 0, # Verifier
471
].pack('N*')
472
473
return [data, 111]
474
end
475
476
def probe_pkt_mssql(_ip)
477
return ["\x02", 1434]
478
end
479
480
def probe_pkt_ntp(_ip)
481
data =
482
"\xe3\x00\x04\xfa\x00\x01\x00\x00\x00\x01\x00\x00\x00" \
483
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" \
484
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" \
485
"\x00\xc5\x4f\x23\x4b\x71\xb1\x52\xf3"
486
return [data, 123]
487
end
488
489
def probe_pkt_sentinel(_ip)
490
return ["\x7a\x00\x00\x00\x00\x00", 5093]
491
end
492
493
def probe_pkt_snmp1(_ip)
494
version = 1
495
data = OpenSSL::ASN1::Sequence([
496
OpenSSL::ASN1::Integer(version - 1),
497
OpenSSL::ASN1::OctetString('public'),
498
OpenSSL::ASN1::Set.new([
499
OpenSSL::ASN1::Integer(rand(0x80000000)),
500
OpenSSL::ASN1::Integer(0),
501
OpenSSL::ASN1::Integer(0),
502
OpenSSL::ASN1::Sequence([
503
OpenSSL::ASN1::Sequence([
504
OpenSSL::ASN1.ObjectId('1.3.6.1.2.1.1.1.0'),
505
OpenSSL::ASN1.Null(nil)
506
])
507
]),
508
], 0, :IMPLICIT)
509
]).to_der
510
[data, 161]
511
end
512
513
def probe_pkt_snmp2(_ip)
514
version = 2
515
data = OpenSSL::ASN1::Sequence([
516
OpenSSL::ASN1::Integer(version - 1),
517
OpenSSL::ASN1::OctetString('public'),
518
OpenSSL::ASN1::Set.new([
519
OpenSSL::ASN1::Integer(rand(0x80000000)),
520
OpenSSL::ASN1::Integer(0),
521
OpenSSL::ASN1::Integer(0),
522
OpenSSL::ASN1::Sequence([
523
OpenSSL::ASN1::Sequence([
524
OpenSSL::ASN1.ObjectId('1.3.6.1.2.1.1.1.0'),
525
OpenSSL::ASN1.Null(nil)
526
])
527
]),
528
], 0, :IMPLICIT)
529
]).to_der
530
[data, 161]
531
end
532
533
def probe_pkt_db2disco(_ip)
534
data = "DB2GETADDR\x00SQL05000\x00"
535
[data, 523]
536
end
537
538
# Server hello packet from citrix_published_bruteforce
539
def probe_pkt_citrix(_ip)
540
data =
541
"\x1e\x00\x01\x30\x02\xfd\xa8\xe3\x00\x00\x00\x00\x00" \
542
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" \
543
"\x00\x00\x00\x00"
544
return [data, 1604]
545
end
546
547
def probe_pkt_pca_st(_ip)
548
return ['ST', 5632]
549
end
550
551
def probe_pkt_pca_nq(_ip)
552
return ['NQ', 5632]
553
end
554
end
555
556