From 7a9102b2eeb83e4a0dbfe0e14564307e3c6d4177 Mon Sep 17 00:00:00 2001 From: mpgn Date: Wed, 17 May 2023 16:39:11 -0400 Subject: [PATCH 1/4] decrypt laps password --- cme/protocols/ldap/smbldap.py | 178 ---------------------------------- cme/protocols/smb.py | 42 ++++---- cme/protocols/winrm.py | 40 +++++--- pyproject.toml | 3 +- 4 files changed, 50 insertions(+), 213 deletions(-) delete mode 100644 cme/protocols/ldap/smbldap.py diff --git a/cme/protocols/ldap/smbldap.py b/cme/protocols/ldap/smbldap.py deleted file mode 100644 index 8d0c0075..00000000 --- a/cme/protocols/ldap/smbldap.py +++ /dev/null @@ -1,178 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -from impacket.ldap import ldap as ldap_impacket -from impacket.krb5.kerberosv5 import KerberosError -from cme.logger import CMEAdapter - - -ldap_error_status = { - "1": "STATUS_NOT_SUPPORTED", - "533": "STATUS_ACCOUNT_DISABLED", - "701": "STATUS_ACCOUNT_EXPIRED", - "531": "STATUS_ACCOUNT_RESTRICTION", - "530": "STATUS_INVALID_LOGON_HOURS", - "532": "STATUS_PASSWORD_EXPIRED", - "773": "STATUS_PASSWORD_MUST_CHANGE", - "775": "USER_ACCOUNT_LOCKED", - "50": "LDAP_INSUFFICIENT_ACCESS", - "KDC_ERR_CLIENT_REVOKED": "KDC_ERR_CLIENT_REVOKED", - "KDC_ERR_PREAUTH_FAILED": "KDC_ERR_PREAUTH_FAILED", -} - - -class LDAPConnect: - def __init__(self, host, port, hostname): - self.logger = None - self.proto_logger(host, port, hostname) - - def proto_logger(self, host, port, hostname): - self.logger = CMEAdapter(extra={"protocol": "LDAP", "host": host, "port": port, "hostname": hostname}) - - def kerberos_login( - self, - domain, - username, - password="", - ntlm_hash="", - aesKey="", - kdcHost="", - useCache=False, - ): - lmhash = "" - nthash = "" - - if kdcHost is None: - kdcHost = domain - - # This checks to see if we didn't provide the LM Hash - if ntlm_hash and ntlm_hash.find(":") != -1: - lmhash, nthash = ntlm_hash.split(":") - elif ntlm_hash: - nthash = ntlm_hash - - # Create the baseDN - baseDN = "" - domainParts = domain.split(".") - for i in domainParts: - baseDN += f"dc={i}," - # Remove last ',' - baseDN = baseDN[:-1] - - try: - ldapConnection = ldap_impacket.LDAPConnection(f"ldap://{kdcHost}", baseDN) - ldapConnection.kerberosLogin( - username, - password, - domain, - lmhash, - nthash, - aesKey, - kdcHost=kdcHost, - useCache=False, - ) - # Connect to LDAP - out = f"{domain}{username}:{password if password else ntlm_hash}" - self.logger.extra["protocol"] = "LDAP" - self.logger.extra["port"] = "389" - return ldapConnection - except ldap_impacket.LDAPSessionError as e: - if str(e).find("strongerAuthRequired") >= 0: - # We need to try SSL - try: - ldapConnection = ldap_impacket.LDAPConnection(f"ldaps://{kdcHost}", baseDN) - ldapConnection.login( - username, - password, - domain, - lmhash, - nthash, - aesKey, - kdcHost=kdcHost, - useCache=False, - ) - self.logger.extra["protocol"] = "LDAPS" - self.logger.extra["port"] = "636" - # self.logger.success(out) - return ldapConnection - except ldap_impacket.LDAPSessionError as e: - errorCode = str(e).split()[-2][:-1] - self.logger.fail( - f"{domain}\\{username}:{password if password else ntlm_hash} {ldap_error_status[errorCode] if errorCode in ldap_error_status else ''}", - color="magenta" if errorCode in ldap_error_status else "red", - ) - else: - errorCode = str(e).split()[-2][:-1] - self.logger.fail( - f"{domain}\\{username}:{password if password else ntlm_hash} {ldap_error_status[errorCode] if errorCode in ldap_error_status else ''}", - color="magenta" if errorCode in ldap_error_status else "red", - ) - return False - - except OSError as e: - self.logger.debug(f"{domain}\\{username}:{password if password else ntlm_hash} {'Error connecting to the domain, please add option --kdcHost with the FQDN of the domain controller'}") - return False - except KerberosError as e: - self.logger.fail( - f"{domain}\\{username}:{password if password else ntlm_hash} {str(e)}", - color="red", - ) - return False - - def auth_login(self, domain, username, password, ntlm_hash): - lmhash = "" - nthash = "" - - # This checks to see if we didn't provide the LM Hash - if ntlm_hash and ntlm_hash.find(":") != -1: - lmhash, nthash = ntlm_hash.split(":") - elif ntlm_hash: - nthash = ntlm_hash - - # Create the baseDN - baseDN = "" - domainParts = domain.split(".") - for i in domainParts: - baseDN += f"dc={i}," - # Remove last ',' - baseDN = baseDN[:-1] - - try: - ldapConnection = ldap_impacket.LDAPConnection(f"ldap://{domain}", baseDN, domain) - ldapConnection.login(username, password, domain, lmhash, nthash) - - # Connect to LDAP - out = "{domain}\\{username}:{password if password else ntlm_hash}" - self.logger.extra["protocol"] = "LDAP" - self.logger.extra["port"] = "389" - # self.logger.success(out) - - return ldapConnection - - except ldap_impacket.LDAPSessionError as e: - if str(e).find("strongerAuthRequired") >= 0: - # We need to try SSL - try: - ldapConnection = ldap_impacket.LDAPConnection(f"ldaps://{domain}", baseDN, domain) - ldapConnection.login(username, password, domain, lmhash, nthash) - self.logger.extra["protocol"] = "LDAPS" - self.logger.extra["port"] = "636" - # self.logger.success(out) - return ldapConnection - except ldap_impacket.LDAPSessionError as e: - errorCode = str(e).split()[-2][:-1] - self.logger.fail( - f"{domain}\\{username}:{password if password else ntlm_hash} {ldap_error_status[errorCode] if errorCode in ldap_error_status else ''}", - color="magenta" if errorCode in ldap_error_status else "red", - ) - else: - errorCode = str(e).split()[-2][:-1] - self.logger.fail( - f"{domain}\\{username}:{password if password else ntlm_hash} {ldap_error_status[errorCode] if errorCode in ldap_error_status else ''}", - color="magenta" if errorCode in ldap_error_status else "red", - ) - return False - - except OSError as e: - self.logger.debug(f"{domain}\\{username}:{password if password else ntlm_hash} {'Error connecting to the domain, please add option --kdcHost with the FQDN of the domain controller'}") - return False diff --git a/cme/protocols/smb.py b/cme/protocols/smb.py index 5b5665ae..f8ef2994 100755 --- a/cme/protocols/smb.py +++ b/cme/protocols/smb.py @@ -41,7 +41,7 @@ from cme.protocols.smb.smbspider import SMBSpider from cme.protocols.smb.passpol import PassPolDump from cme.protocols.smb.samruser import UserSamrDump from cme.protocols.smb.samrfunc import SamrFunc -from cme.protocols.ldap.smbldap import LDAPConnect +from cme.protocols.ldap.laps import LDAPConnect, LAPSv2Extract from cme.helpers.logger import highlight from cme.helpers.misc import * from cme.helpers.bloodhound import add_user_bh @@ -568,23 +568,34 @@ class smb(connection): msMCSAdmPwd = "" sAMAccountName = "" - username = "" + username_laps = "" from impacket.ldap import ldapasn1 as ldapasn1_impacket results = [r for r in results if isinstance(r, ldapasn1_impacket.SearchResultEntry)] if len(results) != 0: for host in results: - values = {str(attr["type"]).lower(): str(attr["vals"][0]) for attr in host["attributes"]} + values = {str(attr["type"]).lower(): attr["vals"][0] for attr in host["attributes"]} if "mslaps-encryptedpassword" in values: - self.logger.fail("LAPS password is encrypted and currently CrackMapExec doesn't support the decryption...") - return False - elif "mslaps-password" in values: - r = loads(values["mslaps-password"]) + msMCSAdmPwd = values["mslaps-encryptedpassword"] + d = LAPSv2Extract( + bytes(msMCSAdmPwd), + username[0] if username else "", + password[0] if password else "", + domain, + ntlm_hash[0] if ntlm_hash else "", + self.args.kerberos, + 339) + data = d.run() + r = loads(data) msMCSAdmPwd = r["p"] - username = r["n"] + username_laps = r["n"] + elif "mslaps-password" in values: + r = loads(str(values["mslaps-password"])) + msMCSAdmPwd = r["p"] + username_laps = r["n"] elif "ms-mcs-admpwd" in values: - msMCSAdmPwd = values["ms-mcs-admpwd"] + msMCSAdmPwd = str(values["ms-mcs-admpwd"]) else: self.logger.fail("No result found with attribute ms-MCS-AdmPwd or msLAPS-Password") logging.debug(f"Host: {sAMAccountName:<20} Password: {msMCSAdmPwd} {self.hostname}") @@ -593,7 +604,7 @@ class smb(connection): return False - self.username = self.args.laps if not username else username + self.username = self.args.laps if not username_laps else username_laps self.password = msMCSAdmPwd if msMCSAdmPwd == "": @@ -615,16 +626,7 @@ class smb(connection): return self.laps_search(self.args.username, self.args.password, self.args.hash, self.domain) return True - def kerberos_login( - self, - domain, - username, - password="", - ntlm_hash="", - aesKey="", - kdcHost="", - useCache=False, - ): + def kerberos_login( self, domain, username, password="", ntlm_hash="", aesKey="", kdcHost="", useCache=False): logging.getLogger("impacket").disabled = True # Re-connect since we logged off if not self.no_ntlm: diff --git a/cme/protocols/winrm.py b/cme/protocols/winrm.py index 9cc1365e..0ff3dada 100644 --- a/cme/protocols/winrm.py +++ b/cme/protocols/winrm.py @@ -2,19 +2,20 @@ # -*- coding: utf-8 -*- import binascii import hashlib -from datetime import datetime import os import requests + +from datetime import datetime +from pypsrp.client import Client + from impacket.smbconnection import SMBConnection +from impacket.examples.secretsdump import LocalOperations, LSASecrets, SAMHashes from cme.config import process_secret from cme.connection import * from cme.helpers.bloodhound import add_user_bh -from cme.protocols.ldap.smbldap import LDAPConnect +from cme.protocols.ldap.laps import LDAPConnect, LAPSv2Extract from cme.logger import CMEAdapter -from pypsrp.client import Client -from impacket.examples.secretsdump import LocalOperations, LSASecrets, SAMHashes - class winrm(connection): def __init__(self, args, db, host): @@ -213,25 +214,36 @@ class winrm(connection): msMCSAdmPwd = "" sAMAccountName = "" - username = "" + username_laps = "" from impacket.ldap import ldapasn1 as ldapasn1_impacket results = [r for r in results if isinstance(r, ldapasn1_impacket.SearchResultEntry)] if len(results) != 0: for host in results: - values = {str(attr["type"]).lower(): str(attr["vals"][0]) for attr in host["attributes"]} + values = {str(attr["type"]).lower(): attr["vals"][0] for attr in host["attributes"]} if "mslaps-encryptedpassword" in values: - self.logger.fail("LAPS password is encrypted and currently CrackMapExec doesn't" " support the decryption...") - return False + from json import loads + msMCSAdmPwd = values["mslaps-encryptedpassword"] + d = LAPSv2Extract( + bytes(msMCSAdmPwd), + username[0] if username else "", + password[0] if password else "", + domain, + ntlm_hash[0] if ntlm_hash else "", + self.args.kerberos, + 339) + data = d.run() + r = loads(data) + msMCSAdmPwd = r["p"] + username_laps = r["n"] elif "mslaps-password" in values: from json import loads - - r = loads(values["mslaps-password"]) + r = loads(str(values["mslaps-password"])) msMCSAdmPwd = r["p"] - username = r["n"] + username_laps = r["n"] elif "ms-mcs-admpwd" in values: - msMCSAdmPwd = values["ms-mcs-admpwd"] + msMCSAdmPwd = str(values["ms-mcs-admpwd"]) else: self.logger.fail("No result found with attribute ms-MCS-AdmPwd or" " msLAPS-Password") self.logger.debug("Host: {:<20} Password: {} {}".format(sAMAccountName, msMCSAdmPwd, self.hostname)) @@ -239,7 +251,7 @@ class winrm(connection): self.logger.fail("msMCSAdmPwd or msLAPS-Password is empty or account cannot read LAPS" " property for {}".format(self.hostname)) return False - self.username = self.args.laps if not username else username + self.username = self.args.laps if not username_laps else username_laps self.password = msMCSAdmPwd if msMCSAdmPwd == "": diff --git a/pyproject.toml b/pyproject.toml index be1e0a02..881ffffb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,7 @@ neo4j = "^4.1.1" pylnk3 = "^0.4.2" pypsrp = "^0.7.0" paramiko = "^2.7.2" -impacket = { git = "https://github.com/mpgn/impacket.git", branch = "master" } +impacket = { git = "https://github.com/mpgn/impacket.git", branch = "gkdi" } dsinternals = "^1.2.4" xmltodict = "^0.12.0" terminaltables = "^3.1.0" @@ -51,6 +51,7 @@ masky = "^0.2.0" sqlalchemy = "^2.0.4" aiosqlite = "^0.18.0" pytest = "^7.2.2" +pyasn1-modules = "^0.3.0" [tool.poetry.dev-dependencies] flake8 = "*" From 4e93894784eac41d29b0adf095e90a6a90a58921 Mon Sep 17 00:00:00 2001 From: mpgn Date: Thu, 18 May 2023 03:35:23 -0400 Subject: [PATCH 2/4] add missing file --- cme/protocols/ldap/laps.py | 265 +++++++++++++++++++++++++++++++++++++ 1 file changed, 265 insertions(+) create mode 100644 cme/protocols/ldap/laps.py diff --git a/cme/protocols/ldap/laps.py b/cme/protocols/ldap/laps.py new file mode 100644 index 00000000..2ff2e509 --- /dev/null +++ b/cme/protocols/ldap/laps.py @@ -0,0 +1,265 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from pyasn1.codec.der import decoder +from pyasn1_modules import rfc5652 + +from impacket.ldap import ldap as ldap_impacket +from impacket.krb5.kerberosv5 import KerberosError +from impacket.dcerpc.v5 import transport +from impacket.dcerpc.v5.epm import hept_map +from impacket.dcerpc.v5.gkdi import MSRPC_UUID_GKDI, GkdiGetKey, GroupKeyEnvelope +from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_INTEGRITY, RPC_C_AUTHN_LEVEL_PKT_PRIVACY +from impacket.dpapi_ng import EncryptedPasswordBlob, KeyIdentifier, compute_kek, create_sd, decrypt_plaintext, unwrap_cek + +from cme.logger import CMEAdapter + +ldap_error_status = { + "1": "STATUS_NOT_SUPPORTED", + "533": "STATUS_ACCOUNT_DISABLED", + "701": "STATUS_ACCOUNT_EXPIRED", + "531": "STATUS_ACCOUNT_RESTRICTION", + "530": "STATUS_INVALID_LOGON_HOURS", + "532": "STATUS_PASSWORD_EXPIRED", + "773": "STATUS_PASSWORD_MUST_CHANGE", + "775": "USER_ACCOUNT_LOCKED", + "50": "LDAP_INSUFFICIENT_ACCESS", + "KDC_ERR_CLIENT_REVOKED": "KDC_ERR_CLIENT_REVOKED", + "KDC_ERR_PREAUTH_FAILED": "KDC_ERR_PREAUTH_FAILED", +} + + +class LDAPConnect: + def __init__(self, host, port, hostname): + self.logger = None + self.proto_logger(host, port, hostname) + + def proto_logger(self, host, port, hostname): + self.logger = CMEAdapter(extra={"protocol": "LDAP", "host": host, "port": port, "hostname": hostname}) + + def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", kdcHost="", useCache=False): + lmhash = "" + nthash = "" + + if kdcHost is None: + kdcHost = domain + + # This checks to see if we didn't provide the LM Hash + if ntlm_hash and ntlm_hash.find(":") != -1: + lmhash, nthash = ntlm_hash.split(":") + elif ntlm_hash: + nthash = ntlm_hash + + # Create the baseDN + baseDN = "" + domainParts = domain.split(".") + for i in domainParts: + baseDN += f"dc={i}," + # Remove last ',' + baseDN = baseDN[:-1] + + try: + ldapConnection = ldap_impacket.LDAPConnection(f"ldap://{kdcHost}", baseDN) + ldapConnection.kerberosLogin( + username, + password, + domain, + lmhash, + nthash, + aesKey, + kdcHost=kdcHost, + useCache=False, + ) + # Connect to LDAP + out = f"{domain}{username}:{password if password else ntlm_hash}" + self.logger.extra["protocol"] = "LDAP" + self.logger.extra["port"] = "389" + return ldapConnection + except ldap_impacket.LDAPSessionError as e: + if str(e).find("strongerAuthRequired") >= 0: + # We need to try SSL + try: + ldapConnection = ldap_impacket.LDAPConnection(f"ldaps://{kdcHost}", baseDN) + ldapConnection.login( + username, + password, + domain, + lmhash, + nthash, + aesKey, + kdcHost=kdcHost, + useCache=False, + ) + self.logger.extra["protocol"] = "LDAPS" + self.logger.extra["port"] = "636" + # self.logger.success(out) + return ldapConnection + except ldap_impacket.LDAPSessionError as e: + errorCode = str(e).split()[-2][:-1] + self.logger.fail( + f"{domain}\\{username}:{password if password else ntlm_hash} {ldap_error_status[errorCode] if errorCode in ldap_error_status else ''}", + color="magenta" if errorCode in ldap_error_status else "red", + ) + else: + errorCode = str(e).split()[-2][:-1] + self.logger.fail( + f"{domain}\\{username}:{password if password else ntlm_hash} {ldap_error_status[errorCode] if errorCode in ldap_error_status else ''}", + color="magenta" if errorCode in ldap_error_status else "red", + ) + return False + + except OSError as e: + self.logger.debug(f"{domain}\\{username}:{password if password else ntlm_hash} {'Error connecting to the domain, please add option --kdcHost with the FQDN of the domain controller'}") + return False + except KerberosError as e: + self.logger.fail( + f"{domain}\\{username}:{password if password else ntlm_hash} {str(e)}", + color="red", + ) + return False + + def auth_login(self, domain, username, password, ntlm_hash): + lmhash = "" + nthash = "" + + # This checks to see if we didn't provide the LM Hash + if ntlm_hash and ntlm_hash.find(":") != -1: + lmhash, nthash = ntlm_hash.split(":") + elif ntlm_hash: + nthash = ntlm_hash + + # Create the baseDN + baseDN = "" + domainParts = domain.split(".") + for i in domainParts: + baseDN += f"dc={i}," + # Remove last ',' + baseDN = baseDN[:-1] + + try: + ldapConnection = ldap_impacket.LDAPConnection(f"ldap://{domain}", baseDN, domain) + ldapConnection.login(username, password, domain, lmhash, nthash) + + # Connect to LDAP + out = "{domain}\\{username}:{password if password else ntlm_hash}" + self.logger.extra["protocol"] = "LDAP" + self.logger.extra["port"] = "389" + # self.logger.success(out) + + return ldapConnection + + except ldap_impacket.LDAPSessionError as e: + if str(e).find("strongerAuthRequired") >= 0: + # We need to try SSL + try: + ldapConnection = ldap_impacket.LDAPConnection(f"ldaps://{domain}", baseDN, domain) + ldapConnection.login(username, password, domain, lmhash, nthash) + self.logger.extra["protocol"] = "LDAPS" + self.logger.extra["port"] = "636" + # self.logger.success(out) + return ldapConnection + except ldap_impacket.LDAPSessionError as e: + errorCode = str(e).split()[-2][:-1] + self.logger.fail( + f"{domain}\\{username}:{password if password else ntlm_hash} {ldap_error_status[errorCode] if errorCode in ldap_error_status else ''}", + color="magenta" if errorCode in ldap_error_status else "red", + ) + else: + errorCode = str(e).split()[-2][:-1] + self.logger.fail( + f"{domain}\\{username}:{password if password else ntlm_hash} {ldap_error_status[errorCode] if errorCode in ldap_error_status else ''}", + color="magenta" if errorCode in ldap_error_status else "red", + ) + return False + + except OSError as e: + self.logger.debug(f"{domain}\\{username}:{password if password else ntlm_hash} {'Error connecting to the domain, please add option --kdcHost with the FQDN of the domain controller'}") + return False + +class LAPSv2Extract: + def __init__(self, data, username, password, domain, ntlm_hash, do_kerberos, port): + + if ntlm_hash.find(":") != -1: + self.lmhash, self.nthash = ntlm_hash.split(":") + else: + self.nthash = ntlm_hash + self.lmhash = '' + + self.data = data + self.username = username + self.password = password + self.domain = domain + self.do_kerberos = do_kerberos + self.logger = None + self.proto_logger(self.domain, port, self.domain) + + def proto_logger(self, host, port, hostname): + self.logger = CMEAdapter(extra={"protocol": "LDAP", "host": host, "port": port, "hostname": hostname}) + + def rpc_connect(self, username, password, domain, lmhash, nthash, doKerberos, dcHost): + stringBinding = hept_map(destHost=dcHost, remoteIf=MSRPC_UUID_GKDI, protocol = 'ncacn_ip_tcp') + + rpctransport = transport.DCERPCTransportFactory(stringBinding) + if hasattr(rpctransport, 'set_credentials'): + rpctransport.set_credentials(username=username, password=password, domain=domain, lmhash=lmhash, nthash=nthash) + if doKerberos: + rpctransport.set_kerberos(doKerberos, kdcHost=dcHost) + if dcHost: + rpctransport.setRemoteHost(dcHost) + dce = rpctransport.get_dce_rpc() + dce.set_auth_level(RPC_C_AUTHN_LEVEL_PKT_INTEGRITY) + dce.set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY) + self.logger.info("[-] Connecting to %s" % stringBinding) + try: + dce.connect() + except Exception as e: + self.logger.info("Something went wrong, check error status => %s" % str(e)) + return None + self.logger.info("[+] Connected") + self.logger.info("[+] Binding to MS-GKDI") + try: + dce.bind(MSRPC_UUID_GKDI) + except Exception as e: + import traceback + traceback.self.logger.info_stack() + self.logger.info("Something went wrong, check error status => %s" % str(e)) + return None + self.logger.info("[+] Successfully bound") + return dce + + def run(self): + self.logger.info('[-] Unpacking blob') + rawblob = EncryptedPasswordBlob(self.data) + parsed_cms_data, remaining = decoder.decode(rawblob['Blob'], asn1Spec=rfc5652.ContentInfo()) + enveloped_data_blob = parsed_cms_data['content'] + parsed_enveloped_data, _ = decoder.decode(enveloped_data_blob, asn1Spec=rfc5652.EnvelopedData()) + + recipient_infos = parsed_enveloped_data['recipientInfos'] + kek_recipient_info = recipient_infos[0]['kekri'] + kek_identifier = kek_recipient_info['kekid'] + key_id = KeyIdentifier(bytes(kek_identifier['keyIdentifier'])) + tmp,_ = decoder.decode(kek_identifier['other']['keyAttr']) + sid = tmp['field-1'][0][0][1].asOctets().decode("utf-8") + target_sd = create_sd(sid) + + # 3. Connect on RPC over TCP to MS-GKDI to call opnum 0 GetKey + dce = self.rpc_connect(username=self.username, password=self.password, domain=self.domain, lmhash=self.lmhash, nthash=self.nthash, doKerberos=self.do_kerberos, dcHost=self.domain) + if dce is None: + return + self.logger.info("[-] Calling MS-GKDI GetKey") + resp = GkdiGetKey(dce, target_sd=target_sd, l0=key_id['L0Index'], l1=key_id['L1Index'], l2=key_id['L2Index'], root_key_id=key_id['RootKeyId']) + self.logger.info("[-] Decrypting password") + # 4. Unpack GroupKeyEnvelope + gke = GroupKeyEnvelope(b''.join(resp['pbbOut'])) + + kek = compute_kek(gke, key_id) + + enc_content_parameter = bytes(parsed_enveloped_data["encryptedContentInfo"]["contentEncryptionAlgorithm"]["parameters"]) + iv, _ = decoder.decode(enc_content_parameter) + iv = bytes(iv[0]) + + cek = unwrap_cek(kek, bytes(kek_recipient_info['encryptedKey'])) + + plaintext = decrypt_plaintext(cek, iv, remaining) + self.logger.info(plaintext[:-18].decode('utf-16le')) + return plaintext[:-18].decode('utf-16le') \ No newline at end of file From 7c2304c8cb98b76454730cab28887a95c57a671b Mon Sep 17 00:00:00 2001 From: mpgn Date: Thu, 18 May 2023 03:36:20 -0400 Subject: [PATCH 3/4] remove space --- cme/protocols/smb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cme/protocols/smb.py b/cme/protocols/smb.py index f8ef2994..abe70128 100755 --- a/cme/protocols/smb.py +++ b/cme/protocols/smb.py @@ -626,7 +626,7 @@ class smb(connection): return self.laps_search(self.args.username, self.args.password, self.args.hash, self.domain) return True - def kerberos_login( self, domain, username, password="", ntlm_hash="", aesKey="", kdcHost="", useCache=False): + def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", kdcHost="", useCache=False): logging.getLogger("impacket").disabled = True # Re-connect since we logged off if not self.no_ntlm: From d3839fc0cd56d37a6fda85141b69dfc67f90f200 Mon Sep 17 00:00:00 2001 From: mpgn Date: Fri, 19 May 2023 05:54:37 -0400 Subject: [PATCH 4/4] update laps --- cme/protocols/ldap/laps.py | 113 +++++++++++++++++++------------------ cme/protocols/smb.py | 1 + cme/protocols/winrm.py | 1 + 3 files changed, 60 insertions(+), 55 deletions(-) diff --git a/cme/protocols/ldap/laps.py b/cme/protocols/ldap/laps.py index 2ff2e509..e56ffc25 100644 --- a/cme/protocols/ldap/laps.py +++ b/cme/protocols/ldap/laps.py @@ -177,8 +177,7 @@ class LDAPConnect: return False class LAPSv2Extract: - def __init__(self, data, username, password, domain, ntlm_hash, do_kerberos, port): - + def __init__(self, data, username, password, domain, ntlm_hash, do_kerberos, kdcHost, port): if ntlm_hash.find(":") != -1: self.lmhash, self.nthash = ntlm_hash.split(":") else: @@ -190,76 +189,80 @@ class LAPSv2Extract: self.password = password self.domain = domain self.do_kerberos = do_kerberos + self.kdcHost = kdcHost self.logger = None self.proto_logger(self.domain, port, self.domain) def proto_logger(self, host, port, hostname): self.logger = CMEAdapter(extra={"protocol": "LDAP", "host": host, "port": port, "hostname": hostname}) - def rpc_connect(self, username, password, domain, lmhash, nthash, doKerberos, dcHost): - stringBinding = hept_map(destHost=dcHost, remoteIf=MSRPC_UUID_GKDI, protocol = 'ncacn_ip_tcp') - - rpctransport = transport.DCERPCTransportFactory(stringBinding) - if hasattr(rpctransport, 'set_credentials'): - rpctransport.set_credentials(username=username, password=password, domain=domain, lmhash=lmhash, nthash=nthash) - if doKerberos: - rpctransport.set_kerberos(doKerberos, kdcHost=dcHost) - if dcHost: - rpctransport.setRemoteHost(dcHost) - dce = rpctransport.get_dce_rpc() - dce.set_auth_level(RPC_C_AUTHN_LEVEL_PKT_INTEGRITY) - dce.set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY) - self.logger.info("[-] Connecting to %s" % stringBinding) - try: - dce.connect() - except Exception as e: - self.logger.info("Something went wrong, check error status => %s" % str(e)) - return None - self.logger.info("[+] Connected") - self.logger.info("[+] Binding to MS-GKDI") - try: - dce.bind(MSRPC_UUID_GKDI) - except Exception as e: - import traceback - traceback.self.logger.info_stack() - self.logger.info("Something went wrong, check error status => %s" % str(e)) - return None - self.logger.info("[+] Successfully bound") - return dce - def run(self): + KDSCache = {} self.logger.info('[-] Unpacking blob') - rawblob = EncryptedPasswordBlob(self.data) - parsed_cms_data, remaining = decoder.decode(rawblob['Blob'], asn1Spec=rfc5652.ContentInfo()) - enveloped_data_blob = parsed_cms_data['content'] - parsed_enveloped_data, _ = decoder.decode(enveloped_data_blob, asn1Spec=rfc5652.EnvelopedData()) + try: + encryptedLAPSBlob = EncryptedPasswordBlob(self.data) + parsed_cms_data, remaining = decoder.decode(encryptedLAPSBlob['Blob'], asn1Spec=rfc5652.ContentInfo()) + enveloped_data_blob = parsed_cms_data['content'] + parsed_enveloped_data, _ = decoder.decode(enveloped_data_blob, asn1Spec=rfc5652.EnvelopedData()) - recipient_infos = parsed_enveloped_data['recipientInfos'] - kek_recipient_info = recipient_infos[0]['kekri'] - kek_identifier = kek_recipient_info['kekid'] - key_id = KeyIdentifier(bytes(kek_identifier['keyIdentifier'])) - tmp,_ = decoder.decode(kek_identifier['other']['keyAttr']) - sid = tmp['field-1'][0][0][1].asOctets().decode("utf-8") - target_sd = create_sd(sid) - - # 3. Connect on RPC over TCP to MS-GKDI to call opnum 0 GetKey - dce = self.rpc_connect(username=self.username, password=self.password, domain=self.domain, lmhash=self.lmhash, nthash=self.nthash, doKerberos=self.do_kerberos, dcHost=self.domain) - if dce is None: + recipient_infos = parsed_enveloped_data['recipientInfos'] + kek_recipient_info = recipient_infos[0]['kekri'] + kek_identifier = kek_recipient_info['kekid'] + key_id = KeyIdentifier(bytes(kek_identifier['keyIdentifier'])) + tmp,_ = decoder.decode(kek_identifier['other']['keyAttr']) + sid = tmp['field-1'][0][0][1].asOctets().decode("utf-8") + target_sd = create_sd(sid) + except Exception as e: + logging.error('Cannot unpack msLAPS-EncryptedPassword blob due to error %s' % str(e)) return - self.logger.info("[-] Calling MS-GKDI GetKey") - resp = GkdiGetKey(dce, target_sd=target_sd, l0=key_id['L0Index'], l1=key_id['L1Index'], l2=key_id['L2Index'], root_key_id=key_id['RootKeyId']) - self.logger.info("[-] Decrypting password") - # 4. Unpack GroupKeyEnvelope - gke = GroupKeyEnvelope(b''.join(resp['pbbOut'])) + + # Check if item is in cache + if key_id['RootKeyId'] in KDSCache: + self.logger.info("Got KDS from cache") + gke = KDSCache[key_id['RootKeyId']] + else: + # Connect on RPC over TCP to MS-GKDI to call opnum 0 GetKey + stringBinding = hept_map(destHost=self.domain, remoteIf=MSRPC_UUID_GKDI, protocol='ncacn_ip_tcp') + rpctransport = transport.DCERPCTransportFactory(stringBinding) + if hasattr(rpctransport, 'set_credentials'): + rpctransport.set_credentials(username=self.username, password=self.password, domain=self.domain, lmhash=self.lmhash, nthash=self.nthash) + if self.do_kerberos: + self.logger.info("Connecting using kerberos") + rpctransport.set_kerberos(self.do_kerberos, kdcHost=self.kdcHost) + + dce = rpctransport.get_dce_rpc() + dce.set_auth_level(RPC_C_AUTHN_LEVEL_PKT_INTEGRITY) + dce.set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY) + self.logger.info("Connecting to %s" % stringBinding) + try: + dce.connect() + except Exception as e: + logging.error("Something went wrong, check error status => %s" % str(e)) + return False + self.logger.info("Connected") + try: + dce.bind(MSRPC_UUID_GKDI) + except Exception as e: + logging.error("Something went wrong, check error status => %s" % str(e)) + return False + self.logger.info("Successfully bound") + + + self.logger.info("Calling MS-GKDI GetKey") + resp = GkdiGetKey(dce, target_sd=target_sd, l0=key_id['L0Index'], l1=key_id['L1Index'], l2=key_id['L2Index'], root_key_id=key_id['RootKeyId']) + self.logger.info("Decrypting password") + # Unpack GroupKeyEnvelope + gke = GroupKeyEnvelope(b''.join(resp['pbbOut'])) + KDSCache[gke['RootKeyId']] = gke kek = compute_kek(gke, key_id) - + self.logger.info("KEK:\t%s" % kek) enc_content_parameter = bytes(parsed_enveloped_data["encryptedContentInfo"]["contentEncryptionAlgorithm"]["parameters"]) iv, _ = decoder.decode(enc_content_parameter) iv = bytes(iv[0]) cek = unwrap_cek(kek, bytes(kek_recipient_info['encryptedKey'])) - + self.logger.info("CEK:\t%s" % cek) plaintext = decrypt_plaintext(cek, iv, remaining) self.logger.info(plaintext[:-18].decode('utf-16le')) return plaintext[:-18].decode('utf-16le') \ No newline at end of file diff --git a/cme/protocols/smb.py b/cme/protocols/smb.py index abe70128..e2fdbea9 100755 --- a/cme/protocols/smb.py +++ b/cme/protocols/smb.py @@ -585,6 +585,7 @@ class smb(connection): domain, ntlm_hash[0] if ntlm_hash else "", self.args.kerberos, + self.args.kdcHost, 339) data = d.run() r = loads(data) diff --git a/cme/protocols/winrm.py b/cme/protocols/winrm.py index 0ff3dada..db581cdf 100644 --- a/cme/protocols/winrm.py +++ b/cme/protocols/winrm.py @@ -232,6 +232,7 @@ class winrm(connection): domain, ntlm_hash[0] if ntlm_hash else "", self.args.kerberos, + self.args.kdcHost, 339) data = d.run() r = loads(data)