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/lib/msf/ui/console/command_dispatcher/creds.rb
Views: 11788
# -*- 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}.each_pair do |keyword, description|113print_line " #{keyword.to_s.ljust 10}: #{description}"114end115print_line116print_line "Examples: Adding"117print_line " # Add a user, password and realm"118print_line " creds add user:admin password:notpassword realm:workgroup"119print_line " # Add a user and password"120print_line " creds add user:guest password:'guest password'"121print_line " # Add a password"122print_line " creds add password:'password without username'"123print_line " # Add a user with an NTLMHash"124print_line " creds add user:admin ntlm:E2FC15074BF7751DD408E6B105741864:A1074A69B1BDE45403AB680504BBDD1A"125print_line " # Add a NTLMHash"126print_line " creds add ntlm:E2FC15074BF7751DD408E6B105741864:A1074A69B1BDE45403AB680504BBDD1A"127print_line " # Add a Postgres MD5"128print_line " creds add user:postgres postgres:md5be86a79bf2043622d58d5453c47d4860"129print_line " # Add a user with a PKCS12 file archive"130print_line " creds add user:alice pkcs12:/path/to/certificate.pfx"131print_line " # Add a user with an SSH key"132print_line " creds add user:sshadmin ssh-key:/path/to/id_rsa"133print_line " # Add a user and a NonReplayableHash"134print_line " creds add user:other hash:d19c32489b870735b5f587d76b934283 jtr:md5"135print_line " # Add a NonReplayableHash"136print_line " creds add hash:d19c32489b870735b5f587d76b934283"137138print_line139print_line "General options"140print_line " -h,--help Show this help information"141print_line " -o <file> Send output to a file in csv/jtr (john the ripper) format."142print_line " If file name ends in '.jtr', that format will be used."143print_line " If file name ends in '.hcat', the hashcat format will be used."144print_line " csv by default."145print_line " -d,--delete Delete one or more credentials"146print_line147print_line "Filter options for listing"148print_line " -P,--password <text> List passwords that match this text"149print_line " -p,--port <portspec> List creds with logins on services matching this port spec"150print_line " -s <svc names> List creds matching comma-separated service names"151print_line " -u,--user <text> List users that match this text"152print_line " -t,--type <type> List creds of the specified type: password, ntlm, hash or any valid JtR format"153print_line " -O,--origins <IP> List creds that match these origins"154print_line " -r,--realm <realm> List creds that match this realm"155print_line " -R,--rhosts Set RHOSTS from the results of the search"156print_line " -v,--verbose Don't truncate long password hashes"157158print_line159print_line "Examples, John the Ripper hash types:"160print_line " Operating Systems (starts with)"161print_line " Blowfish ($2a$) : bf"162print_line " BSDi (_) : bsdi"163print_line " DES : des,crypt"164print_line " MD5 ($1$) : md5"165print_line " SHA256 ($5$) : sha256,crypt"166print_line " SHA512 ($6$) : sha512,crypt"167print_line " Databases"168print_line " MSSQL : mssql"169print_line " MSSQL 2005 : mssql05"170print_line " MSSQL 2012/2014 : mssql12"171print_line " MySQL < 4.1 : mysql"172print_line " MySQL >= 4.1 : mysql-sha1"173print_line " Oracle : des,oracle"174print_line " Oracle 11 : raw-sha1,oracle11"175print_line " Oracle 11 (H type): dynamic_1506"176print_line " Oracle 12c : oracle12c"177print_line " Postgres : postgres,raw-md5"178179print_line180print_line "Examples, listing:"181print_line " creds # Default, returns all credentials"182print_line " creds 1.2.3.4/24 # Return credentials with logins in this range"183print_line " creds -O 1.2.3.4/24 # Return credentials with origins in this range"184print_line " creds -p 22-25,445 # nmap port specification"185print_line " creds -s ssh,smb # All creds associated with a login on SSH or SMB services"186print_line " creds -t ntlm # All NTLM creds"187print_line188189print_line "Example, deleting:"190print_line " # Delete all SMB credentials"191print_line " creds -d -s smb"192print_line193end194195# @param private_type [Symbol] See `Metasploit::Credential::Creation#create_credential`196# @param username [String]197# @param password [String]198# @param realm [String]199# @param realm_type [String] A key in `Metasploit::Model::Realm::Key::SHORT_NAMES`200def creds_add(*args)201params = args.inject({}) do |hsh, n|202opt = n.split(':') # Splitting the string on colons.203hsh[opt[0]] = opt[1..-1].join(':') # everything before the first : is the key, reasembling everything after the colon. why ntlm hashes204hsh205end206207begin208params.assert_valid_keys('user','password','realm','realm-type','ntlm','ssh-key','hash','address','port','protocol', 'service-name', 'jtr', 'pkcs12', 'postgres')209rescue ArgumentError => e210print_error(e.message)211end212213# Verify we only have one type of private214if params.slice('password','ntlm','ssh-key','hash', 'pkcs12', 'postgres').length > 1215private_keys = params.slice('password','ntlm','ssh-key','hash', 'pkcs12', 'postgres').keys216print_error("You can only specify a single Private type. Private types given: #{private_keys.join(', ')}")217return218end219220login_keys = params.slice('address','port','protocol','service-name')221if login_keys.any? and login_keys.length < 3222missing_login_keys = ['host','port','proto','service-name'] - login_keys.keys223print_error("Creating a login requires a address, a port, and a protocol. Missing params: #{missing_login_keys}")224return225end226227data = {228workspace_id: framework.db.workspace.id,229origin_type: :import,230filename: 'msfconsole'231}232233data[:username] = params['user'] if params.key? 'user'234235if params.key? 'realm'236if params.key? 'realm-type'237if Metasploit::Model::Realm::Key::SHORT_NAMES.key? params['realm-type']238data[:realm_key] = Metasploit::Model::Realm::Key::SHORT_NAMES[params['realm-type']]239else240valid = Metasploit::Model::Realm::Key::SHORT_NAMES.keys.map{|n|"'#{n}'"}.join(", ")241print_error("Invalid realm type: #{params['realm_type']}. Valid Values: #{valid}")242end243else244data[:realm_key] = Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN245end246data[:realm_value] = params['realm']247end248249if params.key? 'password'250data[:private_type] = :password251data[:private_data] = params['password']252end253254if params.key? 'ntlm'255data[:private_type] = :ntlm_hash256data[:private_data] = params['ntlm']257end258259if params.key? 'ssh-key'260begin261key_data = File.read(params['ssh-key'])262rescue ::Errno::EACCES, ::Errno::ENOENT => e263print_error("Failed to add ssh key: #{e}")264end265data[:private_type] = :ssh_key266data[:private_data] = key_data267end268269if params.key? 'pkcs12'270begin271# pkcs12 is a binary format, but for persisting we Base64 encode it272pkcs12_data = Base64.strict_encode64(File.binread(params['pkcs12']))273rescue ::Errno::EACCES, ::Errno::ENOENT => e274print_error("Failed to add pkcs12 archive: #{e}")275end276data[:private_type] = :pkcs12277data[:private_data] = pkcs12_data278end279280if params.key? 'hash'281data[:private_type] = :nonreplayable_hash282data[:private_data] = params['hash']283data[:jtr_format] = params['jtr'] if params.key? 'jtr'284end285286if params.key? 'postgres'287data[:private_type] = :postgres_md5288if params['postgres'].downcase.start_with?('md5')289data[:private_data] = params['postgres']290data[:jtr_format] = 'postgres'291else292print_error("Postgres MD5 hashes should start with 'md5'")293end294end295296begin297if login_keys.any?298data[:address] = params['address']299data[:port] = params['port']300data[:protocol] = params['protocol']301data[:service_name] = params['service-name']302framework.db.create_credential_and_login(data)303else304framework.db.create_credential(data)305end306rescue ActiveRecord::RecordInvalid => e307print_error("Failed to add #{data['private_type']}: #{e}")308end309end310311def service_from_origin(core)312# Depending on the origin of the cred, there may or may not be a way to retrieve the associated service313case core.origin314when Metasploit::Credential::Origin::Service315return core.origin.service316end317end318319def build_service_info(service)320if service.name.present?321info = "#{service.port}/#{service.proto} (#{service.name})"322else323info = "#{service.port}/#{service.proto}"324end325info326end327328def creds_search(*args)329host_ranges = []330origin_ranges = []331port_ranges = []332svcs = []333rhosts = []334opts = {}335336set_rhosts = false337truncate = true338339cred_table_columns = [ 'host', 'origin' , 'service', 'public', 'private', 'realm', 'private_type', 'JtR Format', 'cracked_password' ]340delete_count = 0341search_term = nil342343while (arg = args.shift)344case arg345when '-o'346output_file = args.shift347if (!output_file)348print_error('Invalid output filename')349return350end351output_file = ::File.expand_path(output_file)352truncate = false353when '-p', '--port'354unless (arg_port_range(args.shift, port_ranges, true))355return356end357when '-t', '--type'358ptype = args.shift359opts[:ptype] = ptype360if (!ptype)361print_error('Argument required for -t')362return363end364when '-s', '--service'365service = args.shift366if (!service)367print_error('Argument required for -s')368return369end370svcs = service.split(/[\s]*,[\s]*/)371opts[:svcs] = svcs372when '-P', '--password'373if !(opts[:pass] = args.shift)374print_error('Argument required for -P')375return376end377when '-u', '--user'378if !(opts[:user] = args.shift)379print_error('Argument required for -u')380return381end382when '-d', '--delete'383mode = :delete384when '-R', '--rhosts'385set_rhosts = true386when '-O', '--origins'387hosts = args.shift388opts[:hosts] = hosts389if !hosts390print_error('Argument required for -O')391return392end393arg_host_range(hosts, origin_ranges)394when '-S', '--search-term'395search_term = args.shift396opts[:search_term] = search_term397when '-v', '--verbose'398truncate = false399when '-r', '--realm'400opts[:realm] = args.shift401else402# Anything that wasn't an option is a host to search for403unless (arg_host_range(arg, host_ranges))404return405end406end407end408409# If we get here, we're searching. Delete implies search410411if ptype412type = case ptype.downcase413when 'password'414Metasploit::Credential::Password415when 'hash'416Metasploit::Credential::PasswordHash417when 'ntlm'418Metasploit::Credential::NTLMHash419when 'KrbEncKey'.downcase420Metasploit::Credential::KrbEncKey421when *Metasploit::Credential::NonreplayableHash::VALID_JTR_FORMATS422opts[:jtr_format] = ptype423Metasploit::Credential::NonreplayableHash424else425print_error("Unrecognized credential type #{ptype} -- must be one of #{allowed_cred_types.join(',')}")426return427end428end429430opts[:type] = type if type431432# normalize433ports = port_ranges.flatten.uniq434opts[:ports] = ports unless ports.empty?435svcs.flatten!436tbl_opts = {437'Header' => "Credentials",438# For now, don't perform any word wrapping on the cred table as it breaks the workflow of439# copying credentials and pasting them into applications440'WordWrap' => false,441'Columns' => cred_table_columns,442'SearchTerm' => search_term443}444445opts[:workspace] = framework.db.workspace446cred_cores = framework.db.creds(opts).to_a447cred_cores.sort_by!(&:id)448matched_cred_ids = []449cracked_cred_ids = []450451if output_file&.ends_with?('.hcat')452output_file = ::File.open(output_file, 'wb')453output_formatter = Metasploit::Framework::PasswordCracker::Hashcat::Formatter.method(:hash_to_hashcat)454elsif output_file&.ends_with?('.jtr')455output_file = ::File.open(output_file, 'wb')456output_formatter = Metasploit::Framework::PasswordCracker::JtR::Formatter.method(:hash_to_jtr)457else458output_file = ::File.open(output_file, 'wb') unless output_file.blank?459tbl = Rex::Text::Table.new(tbl_opts)460end461462filter_cred_cores(cred_cores, opts, origin_ranges, host_ranges) do |core, service, origin, cracked_password_core|463matched_cred_ids << core.id464cracked_cred_ids << cracked_password_core.id if cracked_password_core.present?465466if output_file && output_formatter467formatted = output_formatter.call(core)468output_file.puts(formatted) unless formatted.blank?469end470471unless tbl.nil?472public_val = core.public ? core.public.username : ''473if core.private474# Show the human readable description by default, unless the user ran with `--verbose` and wants to see the cred data475private_val = truncate ? core.private.to_s : core.private.data476else477private_val = ''478end479if truncate && private_val.to_s.length > 88480private_val = "#{private_val[0,76]} (TRUNCATED)"481end482realm_val = core.realm ? core.realm.value : ''483human_val = core.private ? core.private.class.model_name.human : ''484if human_val == ''485jtr_val = '' #11433, private can be nil486else487jtr_val = core.private.jtr_format ? core.private.jtr_format : ''488end489490if service.nil?491host = ''492service_info = ''493else494host = service.host.address495rhosts << host unless host.blank?496service_info = build_service_info(service)497end498cracked_password_val = cracked_password_core&.private&.data.to_s499tbl << [500host,501origin,502service_info,503public_val,504private_val,505realm_val,506human_val, #private type507jtr_val,508cracked_password_val509]510end511end512513if output_file.nil?514print_line(tbl.to_s)515else516output_file.write(tbl.to_csv) if output_formatter.nil?517output_file.close518print_status("Wrote creds to #{output_file.path}")519end520521if mode == :delete522result = framework.db.delete_credentials(ids: matched_cred_ids.concat(cracked_cred_ids).uniq)523delete_count = result.size524end525526# Finally, handle the case where the user wants the resulting list527# of hosts to go into RHOSTS.528set_rhosts_from_addrs(rhosts.uniq) if set_rhosts529print_status("Deleted #{delete_count} creds") if delete_count > 0530end531532def cmd_creds_tabs(str, words)533case words.length534when 1535# subcommands536tabs = [ 'add-ntlm', 'add-password', 'add-hash', 'add-ssh-key', ]537when 2538tabs = if words[1] == 'add-ssh-key'539tab_complete_filenames(str, words)540else541[]542end543#when 5544# tabs = Metasploit::Model::Realm::Key::SHORT_NAMES.keys545else546tabs = []547end548return tabs549end550551protected552553# @param [Array<Metasploit::Credential::Core>] cores The list of cores to filter554# @param [Hash] opts555# @param [Array<Rex::Socket::RangeWalker>] origin_ranges556# @param [Array<Rex::Socket::RangeWalker>] host_ranges557# @yieldparam [Metasploit::Credential::Core] core558# @yieldparam [Mdm::Service] service559# @yieldparam [Metasploit::Credential::Origin] origin560# @yieldparam [Metasploit::Credential::Origin::CrackedPassword] cracked_password_core561def filter_cred_cores(cores, opts, origin_ranges, host_ranges)562# Some creds may have been cracked that exist outside of the filtered cores list, let's resolve them all to show the cracked value563cores_by_id = cores.each_with_object({}) { |core, hash| hash[core.id] = core }564# Map of any originating core ids that have been cracked; The value is cracked core value565cracked_core_id_to_cracked_value = cores.each_with_object({}) do |core, hash|566next unless core.origin.kind_of?(Metasploit::Credential::Origin::CrackedPassword)567hash[core.origin.metasploit_credential_core_id] = core568end569570cores.each do |core|571# Skip the cracked password if it's planned to be shown on the originating core row in a separate column572is_duplicate_cracked_password_row = core.origin.kind_of?(Metasploit::Credential::Origin::CrackedPassword) &&573cracked_core_id_to_cracked_value.key?(core.origin.metasploit_credential_core_id) &&574# The core might exist outside of the currently available cores to render575cores_by_id.key?(core.origin.metasploit_credential_core_id)576next if is_duplicate_cracked_password_row577578# Exclude non-blank username creds if that's what we're after579if opts[:user] == '' && core.public && !(core.public.username.blank?)580next581end582583# Exclude non-blank password creds if that's what we're after584if opts[:pass] == '' && core.private && !(core.private.data.blank?)585next586end587588origin = ''589if core.origin.kind_of?(Metasploit::Credential::Origin::Service)590service = framework.db.services(id: core.origin.service_id).first591origin = service.host.address592elsif core.origin.kind_of?(Metasploit::Credential::Origin::Session)593session = framework.db.sessions(id: core.origin.session_id).first594origin = session.host.address595end596597if origin_ranges.present? && !origin_ranges.any? { |range| range.include?(origin) }598next599end600601cracked_password_core = cracked_core_id_to_cracked_value.fetch(core.id, nil)602if core.logins.empty?603service = service_from_origin(core)604next if service.nil? && host_ranges.present? # If we're filtering by login IP and we're here there's no associated login, so skip605606yield core, service, origin, cracked_password_core607else608core.logins.each do |login|609service = framework.db.services(id: login.service_id).first610# If none of this Core's associated Logins is for a host within611# the user-supplied RangeWalker, then we don't have any reason to612# print it out. However, we treat the absence of ranges as meaning613# all hosts.614if host_ranges.present? && !host_ranges.any? { |range| range.include?(service.host.address) }615next616end617618yield core, service, origin, cracked_password_core619end620end621end622end623624end625626end end end end627628629