diff --git a/cme/protocols/ldap/smbldap.py b/cme/protocols/ldap/laps.py similarity index 58% rename from cme/protocols/ldap/smbldap.py rename to cme/protocols/ldap/laps.py index 8d0c0075..e56ffc25 100644 --- a/cme/protocols/ldap/smbldap.py +++ b/cme/protocols/ldap/laps.py @@ -1,10 +1,18 @@ #!/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 cme.logger import CMEAdapter +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", @@ -29,16 +37,7 @@ class LDAPConnect: 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, - ): + def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", kdcHost="", useCache=False): lmhash = "" nthash = "" @@ -176,3 +175,94 @@ class LDAPConnect: 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, kdcHost, 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.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 run(self): + KDSCache = {} + self.logger.info('[-] Unpacking blob') + 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) + except Exception as e: + logging.error('Cannot unpack msLAPS-EncryptedPassword blob due to error %s' % str(e)) + return + + # 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 5b5665ae..e2fdbea9 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,35 @@ 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, + self.args.kdcHost, + 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 +605,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 +627,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..db581cdf 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,37 @@ 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, + self.args.kdcHost, + 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 +252,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 = "*"