CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

CoCalc provides the best real-time collaborative environment for Jupyter Notebooks, LaTeX documents, and SageMath, scalable from individual users to large groups and classes!

GitHub Repository: rapid7/metasploit-framework
Path: blob/master/plugins/capture.rb
Views: 1903
1
require 'uri'
2
require 'rex/sync/event'
3
require 'fileutils'
4
5
module Msf
6
#
7
# Combines several Metasploit modules related to spoofing names and capturing credentials
8
# into one plugin
9
#
10
class Plugin::Capture < Msf::Plugin
11
12
class ConsoleCommandDispatcher
13
include Msf::Ui::Console::CommandDispatcher
14
15
class CaptureJobListener
16
def initialize(name, done_event, dispatcher)
17
@name = name
18
@done_event = done_event
19
@dispatcher = dispatcher
20
end
21
22
def waiting(_id)
23
self.succeeded = true
24
@dispatcher.print_good("#{@name} started")
25
@done_event.set
26
end
27
28
def start(id); end
29
30
def completed(id, result, mod); end
31
32
def failed(_id, _error, _mod)
33
@dispatcher.print_error("#{@name} failed to start")
34
@done_event.set
35
end
36
37
attr_accessor :succeeded
38
39
end
40
41
HELP_REGEX = /^-?-h(?:elp)?$/.freeze
42
43
def initialize(*args)
44
super(*args)
45
@active_job_ids = {}
46
@active_loggers = {}
47
@stop_opt_parser = Rex::Parser::Arguments.new(
48
'--session' => [ true, 'Session to stop (otherwise all capture jobs on all sessions will be stopped)' ],
49
['-h', '--help'] => [ false, 'Display this message' ]
50
)
51
52
@start_opt_parser = Rex::Parser::Arguments.new(
53
'--session' => [ true, 'Session to bind on' ],
54
['-i', '--ip'] => [ true, 'IP to bind to' ],
55
'--spoofip' => [ true, 'IP to use for spoofing (poisoning); default is the bound IP address' ],
56
'--regex' => [ true, 'Regex to match for spoofing' ],
57
['-b', '--basic'] => [ false, 'Use Basic auth for HTTP listener (default is NTLM)' ],
58
'--cert' => [ true, 'Path to SSL cert for encrypted communication' ],
59
'--configfile' => [ true, 'Path to a config file' ],
60
'--logfile' => [ true, 'Path to store logs' ],
61
'--hashdir' => [ true, 'Directory to store hash results' ],
62
'--stdout' => [ false, 'Show results in stdout' ],
63
['-v', '--verbose'] => [ false, 'Verbose output' ],
64
['-h', '--help'] => [ false, 'Display this message' ]
65
)
66
end
67
68
def name
69
'HashCapture'
70
end
71
72
def commands
73
{
74
'captureg' => 'Start credential capturing services'
75
}
76
end
77
78
# The main handler for the request command.
79
#
80
# @param args [Array<String>] The array of arguments provided by the user.
81
# @return [nil]
82
def cmd_captureg(*args)
83
# short circuit the whole deal if they need help
84
return help if args.empty?
85
return help if args.length == 1 && args.first =~ HELP_REGEX
86
return help(args.last) if args.length == 2 && args.first =~ HELP_REGEX
87
88
begin
89
if args.first == 'stop'
90
listeners_stop(args)
91
return
92
end
93
94
if args.first == 'start'
95
listeners_start(args)
96
return
97
end
98
return help
99
rescue ArgumentError => e
100
print_error(e.message)
101
end
102
end
103
104
def tab_complete_start(str, words)
105
last_word = words[-1]
106
case last_word
107
when '--session'
108
return framework.sessions.keys.map(&:to_s)
109
when '--cert', '--configfile', '--logfile'
110
return tab_complete_filenames(str, words)
111
when '--hashdir'
112
return tab_complete_directory(str, words)
113
when '-i', '--ip', '--spoofip'
114
return tab_complete_source_address
115
116
end
117
118
if @start_opt_parser.arg_required?(last_word)
119
# The previous word needs an argument; we can't provide any help
120
return []
121
end
122
123
# Otherwise, we are expecting another flag next
124
result = @start_opt_parser.option_keys.select { |opt| opt.start_with?(str) }
125
return result
126
end
127
128
def tab_complete_stop(str, words)
129
last_word = words[-1]
130
case last_word
131
when '--session'
132
return framework.sessions.keys.map(&:to_s) + ['local']
133
end
134
if @stop_opt_parser.arg_required?(words[-1])
135
# The previous word needs an argument; we can't provide any help
136
return []
137
end
138
139
@stop_opt_parser.option_keys.select { |opt| opt.start_with?(str) }
140
end
141
142
def cmd_captureg_tabs(str, words)
143
return ['start', 'stop'] if words.length == 1
144
145
if words[1] == 'start'
146
tab_complete_start(str, words)
147
elsif words[1] == 'stop'
148
tab_complete_stop(str, words)
149
end
150
end
151
152
def listeners_start(args)
153
config = parse_start_args(args)
154
if config[:show_help]
155
help('start')
156
return
157
end
158
159
# Make sure there is no capture happening on that session already
160
session = config[:session]
161
if session.nil?
162
session = 'local'
163
end
164
165
if @active_job_ids.key?(session)
166
active_jobs = @active_job_ids[session]
167
168
# If there are active job IDs on this session, we should fail: there's already a capture going on.
169
# Make them stop it first.
170
# The exception is if all jobs have been manually terminated, then let's treat it
171
# as if the capture was stopped, and allow starting now.
172
active_jobs.each do |job_id|
173
next unless framework.jobs.key?(job_id.to_s)
174
175
session_str = ''
176
unless session.nil?
177
session_str = ' on this session'
178
end
179
print_error("A capture is already in progress#{session_str}. Stop the existing capture then restart a new one")
180
return
181
end
182
end
183
184
if @active_loggers.key?(session)
185
logger = @active_loggers[session]
186
logger.close
187
end
188
189
# Start afresh
190
@active_job_ids[session] = []
191
@active_loggers.delete(session)
192
193
transform_params(config)
194
validate_params(config)
195
196
modules = {
197
# Capturing
198
'DRDA' => 'auxiliary/server/capture/drda',
199
'FTP' => 'auxiliary/server/capture/ftp',
200
'IMAP' => 'auxiliary/server/capture/imap',
201
'LDAP' => 'auxiliary/server/capture/ldap',
202
'MSSQL' => 'auxiliary/server/capture/mssql',
203
'MySQL' => 'auxiliary/server/capture/mysql',
204
'POP3' => 'auxiliary/server/capture/pop3',
205
'Postgres' => 'auxiliary/server/capture/postgresql',
206
'PrintJob' => 'auxiliary/server/capture/printjob_capture',
207
'SIP' => 'auxiliary/server/capture/sip',
208
'SMB' => 'auxiliary/server/capture/smb',
209
'SMTP' => 'auxiliary/server/capture/smtp',
210
'Telnet' => 'auxiliary/server/capture/telnet',
211
'VNC' => 'auxiliary/server/capture/vnc',
212
213
# SSL versions
214
'FTPS' => 'auxiliary/server/capture/ftp',
215
'IMAPS' => 'auxiliary/server/capture/imap',
216
'POP3S' => 'auxiliary/server/capture/pop3',
217
'SMTPS' => 'auxiliary/server/capture/smtp',
218
219
# Poisoning
220
# 'DNS' => 'auxiliary/spoof/dns/native_spoofer',
221
'NBNS' => 'auxiliary/spoof/nbns/nbns_response',
222
'LLMNR' => 'auxiliary/spoof/llmnr/llmnr_response',
223
'mDNS' => 'auxiliary/spoof/mdns/mdns_response'
224
# 'WPAD' => 'auxiliary/server/wpad',
225
}
226
227
encrypted = ['HTTPS_NTLM', 'HTTPS_Basic', 'FTPS', 'IMAPS', 'POP3S', 'SMTPS']
228
229
if config[:http_basic]
230
modules['HTTP'] = 'auxiliary/server/capture/http_basic'
231
modules['HTTPS'] = 'auxiliary/server/capture/http_basic'
232
else
233
modules['HTTP'] = 'auxiliary/server/capture/http_ntlm'
234
modules['HTTPS'] = 'auxiliary/server/capture/http_ntlm'
235
end
236
237
modules_to_run = []
238
logfile = config[:logfile]
239
print_line("Logging results to #{logfile}")
240
logdir = ::File.dirname(logfile)
241
FileUtils.mkdir_p(logdir)
242
hashdir = config[:hashdir]
243
print_line("Hash results stored in #{hashdir}")
244
FileUtils.mkdir_p(hashdir)
245
246
if config[:stdout]
247
logger = Rex::Ui::Text::Output::Tee.new(logfile)
248
else
249
logger = Rex::Ui::Text::Output::File.new(logfile, 'ab')
250
end
251
252
@active_loggers[session] = logger
253
254
config[:services].each do |service|
255
svc = service['type']
256
unless service['enabled']
257
# This service turned off in config
258
next
259
end
260
261
module_name = modules[svc]
262
if module_name.nil?
263
print_error("Unknown service: #{svc}")
264
return
265
end
266
267
# Special case for two variants of HTTP
268
if svc.start_with?('HTTP')
269
if config[:http_basic]
270
svc += '_Basic'
271
else
272
svc += '_NTLM'
273
end
274
end
275
276
mod = framework.modules.create(module_name)
277
# Bail if we couldn't
278
unless mod
279
# Error: this should exist
280
load_error = framework.modules.load_error_by_name(module_name)
281
if load_error
282
print_error("Failed to load #{module_name}: #{load_error}")
283
else
284
print_error("Failed to load #{module_name}")
285
end
286
return
287
end
288
289
datastore = {}
290
# Capturers
291
datastore['SRVHOST'] = config[:srvhost]
292
datastore['CAINPWFILE'] = File.join(config[:hashdir], "cain_#{svc}")
293
datastore['JOHNPWFILE'] = File.join(config[:hashdir], "john_#{svc}")
294
295
# Poisoners
296
datastore['SPOOFIP'] = config[:spoof_ip]
297
datastore['SPOOFIP4'] = config[:spoof_ip]
298
datastore['REGEX'] = config[:spoof_regex]
299
datastore['ListenerComm'] = config[:session]
300
301
opts = {}
302
opts['Options'] = datastore
303
opts['RunAsJob'] = true
304
opts['LocalOutput'] = logger
305
if config[:verbose]
306
datastore['VERBOSE'] = true
307
end
308
309
method = "configure_#{svc.downcase}"
310
if respond_to?(method)
311
send(method, datastore, config)
312
end
313
314
if encrypted.include?(svc)
315
configure_tls(datastore, config)
316
end
317
318
# Before running everything, let's do some basic validation of settings
319
mod_dup = mod.replicant
320
mod_dup._import_extra_options(opts)
321
mod_dup.options.validate(mod_dup.datastore)
322
323
modules_to_run.append([svc, mod, opts])
324
end
325
326
modules_to_run.each do |svc, mod, opts|
327
event = Rex::Sync::Event.new(false, false)
328
job_listener = CaptureJobListener.new(mod.name, event, self)
329
330
result = Msf::Simple::Auxiliary.run_simple(mod, opts, job_listener: job_listener)
331
job_id = result[1]
332
333
# Wait for the event to trigger (socket server either waiting, or failed)
334
event.wait
335
next unless job_listener.succeeded
336
337
# Keep track of it so we can close it upon a `stop` command
338
@active_job_ids[session].append(job_id)
339
job = framework.jobs[job_id.to_s]
340
# Rename the job for display (to differentiate between the encrypted/plaintext ones in particular)
341
if config[:session].nil?
342
session_str = 'local'
343
else
344
session_str = "session #{config[:session].to_i}"
345
end
346
job.send(:name=, "Capture (#{session_str}): #{svc}")
347
end
348
349
print_good('Started capture jobs')
350
end
351
352
def listeners_stop(args)
353
options = parse_stop_args(args)
354
if options[:show_help]
355
help('stop')
356
return
357
end
358
359
session = options[:session]
360
job_id_clone = @active_job_ids.clone
361
job_id_clone.each do |session_id, jobs|
362
next unless session.nil? || session == session_id
363
364
jobs.each do |job_id|
365
framework.jobs.stop_job(job_id) unless framework.jobs[job_id.to_s].nil?
366
end
367
jobs.clear
368
@active_job_ids.delete(session_id)
369
end
370
371
loggers_clone = @active_loggers.clone
372
loggers_clone.each do |session_id, logger|
373
if session.nil? || session == session_id
374
logger.close
375
@active_loggers.delete(session_id)
376
end
377
end
378
379
print_line('Capture listeners stopped')
380
end
381
382
# Print the appropriate help text depending on an optional option parser.
383
#
384
# @param first_arg [String] the first argument to this command
385
# @return [nil]
386
def help(first_arg = nil)
387
if first_arg == 'start'
388
print_line('Usage: captureg start -i <ip> [options]')
389
print_line(@start_opt_parser.usage)
390
elsif first_arg == 'stop'
391
print_line('Usage: captureg stop [options]')
392
print_line(@stop_opt_parser.usage)
393
else
394
print_line('Usage: captureg [start|stop] [options]')
395
print_line('')
396
print_line('Use captureg --help [start|stop] for more detailed usage help')
397
end
398
end
399
400
def default_options
401
{
402
ntlm_challenge: nil,
403
ntlm_domain: nil,
404
services: {},
405
spoof_ip: nil,
406
spoof_regex: '.*',
407
srvhost: nil,
408
http_basic: false,
409
session: nil,
410
ssl_cert: nil,
411
verbose: false,
412
show_help: false,
413
stdout: false,
414
logfile: nil,
415
hashdir: nil
416
}
417
end
418
419
def default_logfile(options)
420
session = 'local'
421
session = options[:session].to_s unless options[:session].nil?
422
423
name = "capture_#{session}_#{Time.now.strftime('%Y%m%d%H%M%S')}_#{Rex::Text.rand_text_numeric(6)}.txt"
424
File.join(Msf::Config.log_directory, "captures/#{name}")
425
end
426
427
def default_hashdir(options)
428
session = 'local'
429
session = options[:session].to_s unless options[:session].nil?
430
431
name = "capture_#{session}_#{Time.now.strftime('%Y%m%d%H%M%S')}_#{Rex::Text.rand_text_numeric(6)}"
432
File.join(Msf::Config.loot_directory, "captures/#{name}")
433
end
434
435
def read_config(filename)
436
options = {}
437
File.open(filename, 'rb') do |f|
438
yamlconf = YAML.safe_load(f)
439
options = {
440
ntlm_challenge: yamlconf['ntlm_challenge'],
441
ntlm_domain: yamlconf['ntlm_domain'],
442
services: yamlconf['services'],
443
spoof_regex: yamlconf['spoof_regex'],
444
http_basic: yamlconf['http_basic'],
445
ssl_cert: yamlconf['ssl_cert'],
446
logfile: yamlconf['logfile'],
447
hashdir: yamlconf['hashdir']
448
}
449
end
450
end
451
452
def parse_stop_args(args)
453
options = {
454
session: nil,
455
show_help: false
456
}
457
458
@start_opt_parser.parse(args) do |opt, _idx, val|
459
case opt
460
when '--session'
461
options[:session] = val
462
when '-h'
463
options[:show_help] = true
464
end
465
end
466
467
options
468
end
469
470
def parse_start_args(args)
471
config_file = File.join(Msf::Config.config_directory, 'capture_config.yaml')
472
# See if there was a config file set
473
@start_opt_parser.parse(args) do |opt, _idx, val|
474
case opt
475
when '--configfile'
476
config_file = val
477
end
478
end
479
480
options = default_options
481
config_options = read_config(config_file)
482
options = options.merge(config_options)
483
484
@start_opt_parser.parse(args) do |opt, _idx, val|
485
case opt
486
when '--session'
487
options[:session] = val
488
when '-i', '--ip'
489
options[:srvhost] = val
490
when '--spoofip'
491
options[:spoof_ip] = val
492
when '--regex'
493
options[:spoof_regex] = val
494
when '-v', '--verbose'
495
options[:verbose] = true
496
when '--basic', '-b'
497
options[:http_basic] = true
498
when '--cert'
499
options[:ssl_cert] = val
500
when '--stdout'
501
options[:stdout] = true
502
when '--logfile'
503
options[:logfile] = val
504
when '--hashdir'
505
options[:hashdir] = val
506
when '-h', '--help'
507
options[:show_help] = true
508
end
509
end
510
511
options
512
end
513
514
def poison_included(options)
515
poisoners = ['mDNS', 'LLMNR', 'NBNS']
516
options[:services].each do |svc|
517
if svc['enabled'] && poisoners.member?(svc['type'])
518
return true
519
end
520
end
521
false
522
end
523
524
# Fill in implied parameters to make the running code neater
525
def transform_params(options)
526
# If we've been given a specific IP to listen on, use that as our poisoning IP
527
if options[:spoof_ip].nil? && Rex::Socket.is_ip_addr?(options[:srvhost]) && Rex::Socket.addr_atoi(options[:srvhost]) != 0
528
options[:spoof_ip] = options[:srvhost]
529
end
530
531
unless options[:session].nil?
532
options[:session] = framework.sessions.get(options[:session])&.sid
533
# UDP is not supported on remote sessions
534
udp = ['NBNS', 'LLMNR', 'mDNS', 'SIP']
535
options[:services].each do |svc|
536
if svc['enabled'] && udp.member?(svc['type'])
537
print_line("Skipping #{svc['type']}: UDP server not supported over a remote session")
538
svc['enabled'] = false
539
end
540
end
541
end
542
543
if options[:logfile].nil?
544
options[:logfile] = default_logfile(options)
545
end
546
547
if options[:hashdir].nil?
548
options[:hashdir] = default_hashdir(options)
549
end
550
end
551
552
def validate_params(options)
553
unless options[:srvhost] && Rex::Socket.is_ip_addr?(options[:srvhost])
554
raise ArgumentError, 'Must provide a valid IP address to listen on'
555
end
556
# If we're running poisoning (which is disabled remotely, so excluding that situation),
557
# we need either a specific srvhost to use, or a specific spoof IP
558
if options[:spoof_ip].nil? && poison_included(options)
559
raise ArgumentError, 'Must provide a specific IP address to use for poisoning'
560
end
561
unless Rex::Socket.is_ip_addr?(options[:spoof_ip])
562
raise ArgumentError, 'Spoof IP must be a valid IP address'
563
end
564
unless options[:ssl_cert].nil? || File.file?(options[:ssl_cert])
565
raise ArgumentError, "File #{options[:ssl_cert]} not found"
566
end
567
unless options[:session].nil? || framework.sessions.get(options[:session])
568
raise ArgumentError, "Session #{options[:session].to_i} not found"
569
end
570
end
571
572
def configure_tls(datastore, config)
573
datastore['SSL'] = true
574
datastore['SSLCert'] = config[:ssl_cert]
575
end
576
577
def configure_smb(datastore, config)
578
datastore['SMBDOMAIN'] = config[:ntlm_domain]
579
datastore['CHALLENGE'] = config[:ntlm_challenge]
580
end
581
582
def configure_ldap(datastore, config)
583
datastore['DOMAIN'] = config[:ntlm_domain]
584
datastore['CHALLENGE'] = config[:ntlm_challenge]
585
end
586
587
def configure_mssql(datastore, config)
588
datastore['DOMAIN_NAME'] = config[:ntlm_domain]
589
datastore['CHALLENGE'] = config[:ntlm_challenge]
590
end
591
592
def configure_http_ntlm(datastore, config)
593
datastore['DOMAIN'] = config[:ntlm_domain]
594
datastore['CHALLENGE'] = config[:ntlm_challenge]
595
datastore['SRVPORT'] = 80
596
datastore['URIPATH'] = '/'
597
end
598
599
def configure_http_basic(datastore, _config)
600
datastore['URIPATH'] = '/'
601
end
602
603
def configure_https_basic(datastore, _config)
604
datastore['SRVPORT'] = 443
605
datastore['URIPATH'] = '/'
606
end
607
608
def configure_https_ntlm(datastore, config)
609
datastore['DOMAIN'] = config[:ntlm_domain]
610
datastore['CHALLENGE'] = config[:ntlm_challenge]
611
datastore['SRVPORT'] = 443
612
datastore['URIPATH'] = '/'
613
end
614
615
def configure_ftps(datastore, _config)
616
datastore['SRVPORT'] = 990
617
end
618
619
def configure_imaps(datastore, _config)
620
datastore['SRVPORT'] = 993
621
end
622
623
def configure_pop3s(datastore, _config)
624
datastore['SRVPORT'] = 995
625
end
626
627
def configure_smtps(datastore, _config)
628
datastore['SRVPORT'] = 587
629
end
630
end
631
632
def initialize(framework, opts)
633
super
634
add_console_dispatcher(ConsoleCommandDispatcher)
635
filename = 'capture_config.yaml'
636
user_config_file = File.join(Msf::Config.config_directory, filename)
637
unless File.exist?(user_config_file)
638
# Initialise user config file with the installed one
639
base_config_file = File.join(Msf::Config.data_directory, filename)
640
unless File.exist?(base_config_file)
641
print_error('Plugin config file not found!')
642
return
643
end
644
FileUtils.cp(base_config_file, user_config_file)
645
end
646
end
647
648
def cleanup
649
remove_console_dispatcher('HashCapture')
650
end
651
652
def name
653
'Credential Capture'
654
end
655
656
def desc
657
'Start all credential capture and spoofing services'
658
end
659
660
end
661
end
662
663