Path: blob/master/lib/msf/ui/console/command_dispatcher/creds.rb
19591 views
# -*- coding: binary -*-12require 'rexml/document'3require 'metasploit/framework/password_crackers/hashcat/formatter'4require 'metasploit/framework/password_crackers/jtr/formatter'56module Msf7module Ui8module Console9module CommandDispatcher1011class Creds12require 'tempfile'1314include Msf::Ui::Console::CommandDispatcher15include Metasploit::Credential::Creation16include Msf::Ui::Console::CommandDispatcher::Common1718#19# The dispatcher's name.20#21def name22"Credentials Backend"23end2425#26# Returns the hash of commands supported by this dispatcher.27#28def commands29{30"creds" => "List all credentials in the database"31}32end3334def allowed_cred_types35%w(password ntlm hash KrbEncKey) + Metasploit::Credential::NonreplayableHash::VALID_JTR_FORMATS36end3738#39# Returns true if the db is connected, prints an error and returns40# false if not.41#42# All commands that require an active database should call this before43# doing anything.44# TODO: abstract the db methods to a mixin that can be used by both dispatchers45#46def active?47if not framework.db.active48print_error("Database not connected")49return false50end51true52end5354#55# Miscellaneous option helpers56#5758#59# Can return return active or all, on a certain host or range, on a60# certain port or range, and/or on a service name.61#62def cmd_creds(*args)63return unless active?6465# Short-circuit help66if args.delete("-h") || args.delete("--help")67cmd_creds_help68return69end7071subcommand = args.shift7273case subcommand74when 'help'75cmd_creds_help76when 'add'77creds_add(*args)78else79# then it's not actually a subcommand80args.unshift(subcommand) if subcommand81creds_search(*args)82end8384end8586#87# TODO: this needs to be cleaned up to use the new syntax88#89def cmd_creds_help90print_line91print_line "With no sub-command, list credentials. If an address range is"92print_line "given, show only credentials with logins on hosts within that"93print_line "range."9495print_line96print_line "Usage - Listing credentials:"97print_line " creds [filter options] [address range]"98print_line99print_line "Usage - Adding credentials:"100print_line " creds add uses the following named parameters."101{102user: 'Public, usually a username',103password: 'Private, private_type Password.',104ntlm: 'Private, private_type NTLM Hash.',105postgres: 'Private, private_type postgres MD5',106pkcs12: 'Private, private_type pkcs12 archive file, must be a file path.',107'ssh-key' => 'Private, private_type SSH key, must be a file path.',108hash: 'Private, private_type Nonreplayable hash',109jtr: 'Private, private_type John the Ripper hash type.',110realm: 'Realm, ',111'realm-type' => "Realm, realm_type (#{Metasploit::Model::Realm::Key::SHORT_NAMES.keys.join(' ')}), defaults to domain.",112'adcs-ca' => 'CA, Certificate Authority that issued the pkcs12 certificate',113'adcs-template' => 'ADCS Template, template used to issue the pkcs12 certificate',114'pkcs12-password' => 'The password to decrypt the Pkcs12, defaults to an empty password'115}.each_pair do |keyword, description|116print_line " #{keyword.to_s.ljust 10}: #{description}"117end118print_line119print_line "Examples: Adding"120print_line " # Add a user, password and realm"121print_line " creds add user:admin password:notpassword realm:workgroup"122print_line " # Add a user and password"123print_line " creds add user:guest password:'guest password'"124print_line " # Add a password"125print_line " creds add password:'password without username'"126print_line " # Add a user with an NTLMHash"127print_line " creds add user:admin ntlm:E2FC15074BF7751DD408E6B105741864:A1074A69B1BDE45403AB680504BBDD1A"128print_line " # Add a NTLMHash"129print_line " creds add ntlm:E2FC15074BF7751DD408E6B105741864:A1074A69B1BDE45403AB680504BBDD1A"130print_line " # Add a Postgres MD5"131print_line " creds add user:postgres postgres:md5be86a79bf2043622d58d5453c47d4860"132print_line " # Add a user with a PKCS12 file archive"133print_line " creds add user:alice pkcs12:/path/to/certificate.pfx"134print_line " # Add a user with an SSH key"135print_line " creds add user:sshadmin ssh-key:/path/to/id_rsa"136print_line " # Add a user and a NonReplayableHash"137print_line " creds add user:other hash:d19c32489b870735b5f587d76b934283 jtr:md5"138print_line " # Add a NonReplayableHash"139print_line " creds add hash:d19c32489b870735b5f587d76b934283"140141print_line142print_line "General options"143print_line " -h,--help Show this help information"144print_line " -o <file> Send output to a file in csv/jtr (john the ripper) format."145print_line " If file name ends in '.jtr', that format will be used."146print_line " If file name ends in '.hcat', the hashcat format will be used."147print_line " csv by default."148print_line " -d,--delete Delete one or more credentials"149print_line150print_line "Filter options for listing"151print_line " -P,--password <text> List passwords that match this text"152print_line " -p,--port <portspec> List creds with logins on services matching this port spec"153print_line " -s <svc names> List creds matching comma-separated service names"154print_line " -u,--user <text> List users that match this text"155print_line " -t,--type <type> List creds of the specified type: password, ntlm, hash or any valid JtR format"156print_line " -O,--origins <IP> List creds that match these origins"157print_line " -r,--realm <realm> List creds that match this realm"158print_line " -R,--rhosts Set RHOSTS from the results of the search"159print_line " -v,--verbose Don't truncate long password hashes"160161print_line162print_line "Examples, John the Ripper hash types:"163print_line " Operating Systems (starts with)"164print_line " Blowfish ($2a$) : bf"165print_line " BSDi (_) : bsdi"166print_line " DES : des,crypt"167print_line " MD5 ($1$) : md5"168print_line " SHA256 ($5$) : sha256,crypt"169print_line " SHA512 ($6$) : sha512,crypt"170print_line " Databases"171print_line " MSSQL : mssql"172print_line " MSSQL 2005 : mssql05"173print_line " MSSQL 2012/2014 : mssql12"174print_line " MySQL < 4.1 : mysql"175print_line " MySQL >= 4.1 : mysql-sha1"176print_line " Oracle : des,oracle"177print_line " Oracle 11 : raw-sha1,oracle11"178print_line " Oracle 11 (H type): dynamic_1506"179print_line " Oracle 12c : oracle12c"180print_line " Postgres : postgres,raw-md5"181182print_line183print_line "Examples, listing:"184print_line " creds # Default, returns all credentials"185print_line " creds 1.2.3.4/24 # Return credentials with logins in this range"186print_line " creds -O 1.2.3.4/24 # Return credentials with origins in this range"187print_line " creds -p 22-25,445 # nmap port specification"188print_line " creds -s ssh,smb # All creds associated with a login on SSH or SMB services"189print_line " creds -t ntlm # All NTLM creds"190print_line191192print_line "Example, deleting:"193print_line " # Delete all SMB credentials"194print_line " creds -d -s smb"195print_line196end197198# @param private_type [Symbol] See `Metasploit::Credential::Creation#create_credential`199# @param username [String]200# @param password [String]201# @param realm [String]202# @param realm_type [String] A key in `Metasploit::Model::Realm::Key::SHORT_NAMES`203def creds_add(*args)204params = args.inject({}) do |hsh, n|205opt = n.split(':') # Splitting the string on colons.206hsh[opt[0]] = opt[1..-1].join(':') # everything before the first : is the key, reasembling everything after the colon. why ntlm hashes207hsh208end209210begin211params.assert_valid_keys('user','password','realm','realm-type','ntlm','ssh-key','hash','address','port','protocol', 'service-name', 'jtr', 'pkcs12', 'postgres', 'adcs-ca', 'adcs-template', 'pkcs12-password')212rescue ArgumentError => e213print_error(e.message)214end215216# Verify we only have one type of private217if params.slice('password','ntlm','ssh-key','hash', 'pkcs12', 'postgres').length > 1218private_keys = params.slice('password','ntlm','ssh-key','hash', 'pkcs12', 'postgres').keys219print_error("You can only specify a single Private type. Private types given: #{private_keys.join(', ')}")220return221end222223login_keys = params.slice('address','port','protocol','service-name')224if login_keys.any? and login_keys.length < 3225missing_login_keys = ['host','port','proto','service-name'] - login_keys.keys226print_error("Creating a login requires a address, a port, and a protocol. Missing params: #{missing_login_keys}")227return228end229230data = {231workspace_id: framework.db.workspace.id,232origin_type: :import,233filename: 'msfconsole'234}235236data[:username] = params['user'] if params.key? 'user'237238if params.key? 'realm'239if params.key? 'realm-type'240if Metasploit::Model::Realm::Key::SHORT_NAMES.key? params['realm-type']241data[:realm_key] = Metasploit::Model::Realm::Key::SHORT_NAMES[params['realm-type']]242else243valid = Metasploit::Model::Realm::Key::SHORT_NAMES.keys.map{|n|"'#{n}'"}.join(", ")244print_error("Invalid realm type: #{params['realm_type']}. Valid Values: #{valid}")245end246else247data[:realm_key] = Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN248end249data[:realm_value] = params['realm']250end251252if params.key? 'password'253data[:private_type] = :password254data[:private_data] = params['password']255end256257if params.key? 'ntlm'258data[:private_type] = :ntlm_hash259data[:private_data] = params['ntlm']260end261262if params.key? 'ssh-key'263begin264key_data = File.read(params['ssh-key'])265rescue ::Errno::EACCES, ::Errno::ENOENT => e266print_error("Failed to add ssh key: #{e}")267end268data[:private_type] = :ssh_key269data[:private_data] = key_data270end271272if params.key? 'pkcs12'273begin274# pkcs12 is a binary format, but for persisting we Base64 encode it275pkcs12_data = Base64.strict_encode64(File.binread(params['pkcs12']))276rescue ::Errno::EACCES, ::Errno::ENOENT => e277print_error("Failed to add pkcs12 archive: #{e}")278end279data[:private_type] = :pkcs12280data[:private_data] = pkcs12_data281data[:private_metadata] = {}282data[:private_metadata][:adcs_ca] = params['adcs-ca'] if params['adcs-ca']283data[:private_metadata][:adcs_template] = params['adcs-template'] if params['adcs-template']284data[:private_metadata][:pkcs12_password] = params['pkcs12-password'] if params['pkcs12-password']285end286287if params.key? 'hash'288data[:private_type] = :nonreplayable_hash289data[:private_data] = params['hash']290data[:jtr_format] = params['jtr'] if params.key? 'jtr'291end292293if params.key? 'postgres'294data[:private_type] = :postgres_md5295if params['postgres'].downcase.start_with?('md5')296data[:private_data] = params['postgres']297data[:jtr_format] = 'postgres'298else299print_error("Postgres MD5 hashes should start with 'md5'")300end301end302303begin304if login_keys.any?305data[:address] = params['address']306data[:port] = params['port']307data[:protocol] = params['protocol']308data[:service_name] = params['service-name']309framework.db.create_credential_and_login(data)310else311framework.db.create_credential(data)312end313rescue ActiveRecord::RecordInvalid => e314print_error("Failed to add #{data[:private_type]}: #{e}")315end316end317318def service_from_origin(core)319# Depending on the origin of the cred, there may or may not be a way to retrieve the associated service320case core.origin321when Metasploit::Credential::Origin::Service322return core.origin.service323end324end325326def build_service_info(service)327if service.name.present?328info = "#{service.port}/#{service.proto} (#{service.name})"329else330info = "#{service.port}/#{service.proto}"331end332info333end334335def creds_search(*args)336host_ranges = []337origin_ranges = []338port_ranges = []339svcs = []340rhosts = []341opts = {}342343set_rhosts = false344truncate = true345346cred_table_columns = [ 'host', 'origin' , 'service', 'public', 'private', 'realm', 'private_type', 'JtR Format', 'cracked_password' ]347delete_count = 0348search_term = nil349350while (arg = args.shift)351case arg352when '-o'353output_file = args.shift354if (!output_file)355print_error('Invalid output filename')356return357end358output_file = ::File.expand_path(output_file)359truncate = false360when '-p', '--port'361unless (arg_port_range(args.shift, port_ranges, true))362return363end364when '-t', '--type'365ptype = args.shift366opts[:ptype] = ptype367if (!ptype)368print_error('Argument required for -t')369return370end371when '-s', '--service'372service = args.shift373if (!service)374print_error('Argument required for -s')375return376end377svcs = service.split(/[\s]*,[\s]*/)378opts[:svcs] = svcs379when '-P', '--password'380if !(opts[:pass] = args.shift)381print_error('Argument required for -P')382return383end384when '-u', '--user'385if !(opts[:user] = args.shift)386print_error('Argument required for -u')387return388end389when '-d', '--delete'390mode = :delete391when '-R', '--rhosts'392set_rhosts = true393when '-O', '--origins'394hosts = args.shift395opts[:hosts] = hosts396if !hosts397print_error('Argument required for -O')398return399end400arg_host_range(hosts, origin_ranges)401when '-S', '--search-term'402search_term = args.shift403opts[:search_term] = search_term404when '-v', '--verbose'405truncate = false406when '-r', '--realm'407opts[:realm] = args.shift408else409# Anything that wasn't an option is a host to search for410unless (arg_host_range(arg, host_ranges))411return412end413end414end415416# If we get here, we're searching. Delete implies search417418if ptype419type = case ptype.downcase420when 'password'421Metasploit::Credential::Password422when 'hash'423Metasploit::Credential::NonreplayableHash424when 'ntlm'425Metasploit::Credential::NTLMHash426when 'KrbEncKey'.downcase427Metasploit::Credential::KrbEncKey428when 'pkcs12'429Metasploit::Credential::Pkcs12430when *Metasploit::Credential::NonreplayableHash::VALID_JTR_FORMATS431opts[:jtr_format] = ptype432Metasploit::Credential::NonreplayableHash433else434print_error("Unrecognized credential type #{ptype} -- must be one of #{allowed_cred_types.join(',')}")435return436end437end438439opts[:type] = type if type440441# normalize442ports = port_ranges.flatten.uniq443opts[:ports] = ports unless ports.empty?444svcs.flatten!445tbl_opts = {446'Header' => "Credentials",447# For now, don't perform any word wrapping on the cred table as it breaks the workflow of448# copying credentials and pasting them into applications449'WordWrap' => false,450'Columns' => cred_table_columns,451'SearchTerm' => search_term452}453454opts[:workspace] = framework.db.workspace455cred_cores = framework.db.creds(opts).to_a456cred_cores.sort_by!(&:id)457matched_cred_ids = []458cracked_cred_ids = []459460if output_file&.ends_with?('.hcat')461output_file = ::File.open(output_file, 'wb')462output_formatter = Metasploit::Framework::PasswordCracker::Hashcat::Formatter.method(:hash_to_hashcat)463elsif output_file&.ends_with?('.jtr')464output_file = ::File.open(output_file, 'wb')465output_formatter = Metasploit::Framework::PasswordCracker::JtR::Formatter.method(:hash_to_jtr)466else467output_file = ::File.open(output_file, 'wb') unless output_file.blank?468tbl = Rex::Text::Table.new(tbl_opts)469end470471filter_cred_cores(cred_cores, opts, origin_ranges, host_ranges) do |core, service, origin, cracked_password_core|472matched_cred_ids << core.id473cracked_cred_ids << cracked_password_core.id if cracked_password_core.present?474475if output_file && output_formatter476formatted = output_formatter.call(core)477output_file.puts(formatted) unless formatted.blank?478end479480unless tbl.nil?481public_val = core.public ? core.public.username : ''482if core.private483# Show the human readable description by default, unless the user ran with `--verbose` and wants to see the cred data484private_val = truncate ? core.private.to_s : core.private.data485else486private_val = ''487end488if truncate && private_val.to_s.length > 88489private_val = "#{private_val[0,76]} (TRUNCATED)"490end491realm_val = core.realm ? core.realm.value : ''492human_val = core.private ? core.private.class.model_name.human : ''493if human_val == ''494jtr_val = '' #11433, private can be nil495else496jtr_val = core.private.jtr_format ? core.private.jtr_format : ''497end498499if service.nil?500host = ''501service_info = ''502else503host = service.host.address504rhosts << host unless host.blank?505service_info = build_service_info(service)506end507cracked_password_val = cracked_password_core&.private&.data.to_s508tbl << [509host,510origin,511service_info,512public_val,513private_val,514realm_val,515human_val, #private type516jtr_val,517cracked_password_val518]519end520end521522if output_file.nil?523print_line(tbl.to_s)524else525output_file.write(tbl.to_csv) if output_formatter.nil?526output_file.close527print_status("Wrote creds to #{output_file.path}")528end529530if mode == :delete531result = framework.db.delete_credentials(ids: matched_cred_ids.concat(cracked_cred_ids).uniq)532delete_count = result.size533end534535# Finally, handle the case where the user wants the resulting list536# of hosts to go into RHOSTS.537set_rhosts_from_addrs(rhosts.uniq) if set_rhosts538print_status("Deleted #{delete_count} creds") if delete_count > 0539end540541def cmd_creds_tabs(str, words)542case words.length543when 1544# subcommands545tabs = [ 'add-ntlm', 'add-password', 'add-hash', 'add-ssh-key', ]546when 2547tabs = if words[1] == 'add-ssh-key'548tab_complete_filenames(str, words)549else550[]551end552#when 5553# tabs = Metasploit::Model::Realm::Key::SHORT_NAMES.keys554else555tabs = []556end557return tabs558end559560protected561562# @param [Array<Metasploit::Credential::Core>] cores The list of cores to filter563# @param [Hash] opts564# @param [Array<Rex::Socket::RangeWalker>] origin_ranges565# @param [Array<Rex::Socket::RangeWalker>] host_ranges566# @yieldparam [Metasploit::Credential::Core] core567# @yieldparam [Mdm::Service] service568# @yieldparam [Metasploit::Credential::Origin] origin569# @yieldparam [Metasploit::Credential::Origin::CrackedPassword] cracked_password_core570def filter_cred_cores(cores, opts, origin_ranges, host_ranges)571# Some creds may have been cracked that exist outside of the filtered cores list, let's resolve them all to show the cracked value572cores_by_id = cores.each_with_object({}) { |core, hash| hash[core.id] = core }573# Map of any originating core ids that have been cracked; The value is cracked core value574cracked_core_id_to_cracked_value = cores.each_with_object({}) do |core, hash|575next unless core.origin.kind_of?(Metasploit::Credential::Origin::CrackedPassword)576hash[core.origin.metasploit_credential_core_id] = core577end578579cores.each do |core|580# Skip the cracked password if it's planned to be shown on the originating core row in a separate column581is_duplicate_cracked_password_row = core.origin.kind_of?(Metasploit::Credential::Origin::CrackedPassword) &&582cracked_core_id_to_cracked_value.key?(core.origin.metasploit_credential_core_id) &&583# The core might exist outside of the currently available cores to render584cores_by_id.key?(core.origin.metasploit_credential_core_id)585next if is_duplicate_cracked_password_row586587# Exclude non-blank username creds if that's what we're after588if opts[:user] == '' && core.public && !(core.public.username.blank?)589next590end591592# Exclude non-blank password creds if that's what we're after593if opts[:pass] == '' && core.private && !(core.private.data.blank?)594next595end596597origin = ''598if core.origin.kind_of?(Metasploit::Credential::Origin::Service)599service = framework.db.services(id: core.origin.service_id).first600origin = service.host.address601elsif core.origin.kind_of?(Metasploit::Credential::Origin::Session)602session = framework.db.sessions(id: core.origin.session_id).first603origin = session.host.address604end605606if origin_ranges.present? && !origin_ranges.any? { |range| range.include?(origin) }607next608end609610cracked_password_core = cracked_core_id_to_cracked_value.fetch(core.id, nil)611if core.logins.empty?612service = service_from_origin(core)613next if service.nil? && host_ranges.present? # If we're filtering by login IP and we're here there's no associated login, so skip614615yield core, service, origin, cracked_password_core616else617core.logins.each do |login|618service = framework.db.services(id: login.service_id).first619# If none of this Core's associated Logins is for a host within620# the user-supplied RangeWalker, then we don't have any reason to621# print it out. However, we treat the absence of ranges as meaning622# all hosts.623if host_ranges.present? && !host_ranges.any? { |range| range.include?(service.host.address) }624next625end626627yield core, service, origin, cracked_password_core628end629end630end631end632633end634635end end end end636637638