Skip to main content

ESC17 - Beyond WSUS: Hijacking Kerberos Sessions over LDAPS

Published on 28 min read

A research series by Alexis MARTIN (qu35t) and Alexis PARET (nnino). Every technique in this series was reproduced end to end in a fully patched lab environment (fully patched Windows Server 2025 DC, as of July 25, 2026).

The first article laid out the primitive: ESC17 manufactures a domain-trusted TLS server identity, for any hostname. WSUS was its most spectacular demonstration, because a manipulated update there becomes SYSTEM code. This third article tackles a far more central, and far more subtle, service: the directory itself, over LDAP / LDAPS.

The naive intuition would be: “I terminate TLS with my ESC17 certificate, so I read the whole directory.” Passive reading of the traffic is indeed possible, but the real prize is elsewhere. It plays out in authentication: what becomes of an LDAP bind when you strip away its TLS transport? The answer is not binary. It depends on the type of bind and on two domain-controller settings. And at the end of this analysis lies a result we did not expect to find: the hijacking of an already-authenticated Kerberos session, a mechanism reputed to be unattackable.

LDAP is a two-layer stack

The classic confusion is to treat “LDAPS” as a monolithic block. It is not one. An authenticated LDAP session layers two independent layers:

  1. The transport, either plaintext LDAP (389/tcp) or a TLS channel: LDAPS (636/tcp) or StartTLS on 389.
  2. The application bind, the proof of identity negotiated inside the session: either a simple bind (DN + password), or a SASL bind via GSS-SPNEGO, which in turn wraps NTLM or Kerberos.

ESC17 acts on layer 1: you terminate TLS with a trusted certificate, you see the traffic in the clear. The real question, the one that decides impact, is: does layer 2 survive a compromised transport? Put another way, does authentication bring its own integrity protection, independent of TLS, or does it blindly trust the channel?

The question that governs this entire article Does an LDAP bind protect its messages itself (application-layer signing/sealing), or does it delegate protection entirely to TLS? If it is the latter, and TLS is ours, then the authenticated session is ours too.

Two policy settings on the DC side arbitrate this question. You have to keep them in mind before anything else.

The two protections: LDAP signing and channel binding

LDAP signing (LDAPServerIntegrity). Controlled by the key HKLM\SYSTEM\CurrentControlSet\Services\NTDS\Parameters\LDAPServerIntegrity (1 = no requirement, 2 = signing required, the hardened default on modern DCs). When signing is required, a SASL bind over a plaintext transport (389) must negotiate a GSS security layer (signing/sealing), failing which the DC rejects it. This is what protects the directory from plain sniffing on 389.

The decisive, and counter-intuitive, point comes down to a single sentence of the Microsoft protocol. [MS-ADTS], section SASL Authentication:

Active Directory allows SASL binds to be performed on an SSL/TLS-protected connection, [but] it does not allow the use of SASL-layer encryption/integrity verification mechanisms on such a connection.

Concretely, over LDAPS (636), the GSS security layer is forbidden. The DC considers that TLS already provides protection, so the SASL bind, NTLM as well as Kerberos, is performed with no application-layer signing or sealing. All protection of the post-bind traffic then rests exclusively on TLS. And that TLS, with ESC17, is ours.

LDAP channel binding (LdapEnforceChannelBinding). Controlled by ...\NTDS\Parameters\LdapEnforceChannelBinding (0 = never, 1 = when supported, 2 = always). This is the real guardrail against ESC17 over LDAPS. When it is set to 2, the client embeds a Channel Binding Token in its authentication token, namely the tls-server-end-point, that is, the hash of the TLS certificate actually presented by the server. The DC checks this hash against its own certificate. If we terminated TLS with our ESC17 certificate, the CBT the client computes equals hash(ESC17 cert)hash(DC's real cert)the bind is rejected, whatever we do. Channel binding ties the authentication to the exact server certificate, and this is precisely the mitigation Microsoft pushed with advisory ADV190023.

The truth table that follows, and that structures everything to come:

DC settingEffect on the ESC17 attack over LDAPS
Signing LDAPServerIntegrity=2 (required)Does not bite on 636: TLS satisfies the requirement, and the SASL layer is absent anyway. Only blocks plaintext sniffing on 389.
Channel binding LdapEnforceChannelBinding=0 (never)No protection: nothing ties the auth to our fake certificate.
Channel binding LdapEnforceChannelBinding=2 (always)Total block: our ESC17 certificate ≠ the DC’s certificate → bind rejected (NTLM and Kerberos).

Everything that follows was validated in the configuration that is most common in enterprises, and yet sufficient: signing required (=2), channel binding disabled (=0). A half-finished hardening, exactly the kind of gap layer 2 is supposed to catch, but does not.

Two protections, two attacks: which one blocks what

The most important detail to grasp, and the most often confused, is that these two settings do not protect against the same attack:

DC settingWhat it blocks
Channel binding (LdapEnforceChannelBinding=2)our session hijack (ESC17 MITM on LDAPS)
LDAP signing (LDAPServerIntegrity=2)the NTLM relay to LDAP

These are two orthogonal protections, against two distinct attacks. Hence the matrix of what remains open:

SigningChannel bindingWhat stays possible
requiredrequirednothing from either (complete hardening)
requiredabsentsession hijack (our lab case, NTLM & Kerberos hijack)
absentrequiredNTLM relay → LDAP
absentabsentboth: hijack and relay

Our lab is the 2nd row: signing required (the relay is closed) but channel binding absent (the hijack is open). This is what the rest of the article exploits.

And if LDAP signing is absent (rows 3-4)? Another route opens up, independent of ESC17: the NTLM relay to LDAP, the classic ntlmrelayx scenario (the very reason for advisory ADV190023). With no signing requirement on ldap:// (389), and no channel binding on ldaps:// (636), a coerced NTLM authentication (PetitPotam, PrinterBug, WebDAV…) can be relayed to the DC’s LDAP and used to perform privileged writes as the coerced victim: RBCD, machine account creation, DCSync grant, Shadow Credentials (msDS-KeyCredentialLink), ACL modification. No certificate, no TLS termination: it is often the fastest path to the domain.

This relay is a complementary angle, with its own constraints:

  • NTLM only. Kerberos is not relayable (the AP-REQ is bound to the key of the target SPN). In a Kerberos-only / NTLM-disabled environment, the relay falls apart, and what remains is the ESC17 hijack (which does cover Kerberos).
  • You have to coerce an inbound authentication, and you are bounded by the rights of the coerced identity (relaying a privileged machine account → RBCD → takeover, relaying an ordinary user → limited impact).
  • It does not help on LDAPS if channel binding is enabled: the protection against an NTLM→LDAPS relay is exactly the same CBT that protects against our hijack.

Let’s hold onto this: channel binding protects against the hijack, LDAP signing protects against the relay, you have to enable BOTH. Disabling just one leaves an attack wide open. Disabling both (still common) offers both routes at once.

GSS sealing, the thing that decides the hijack

The whole article rests on one concept that has to be stated clearly: sealing (GSS sign & seal). When a SASL bind (NTLM or Kerberos) succeeds, the two parties derive a session key from the authentication. If a GSS security layer is negotiated, each post-bind LDAP message is then signed and encrypted with that key (GSS_Wrap): the traffic becomes opaque and unforgeable to anyone who lacks the key, including a MITM who does hold the TLS.

The rule is therefore this:

An authenticated session is hijackable if, and only if, its post-bind traffic is NOT sealed, and we hold the transport.

And it is Active Directory itself that decides whether there is sealing, and it does so in opposite ways depending on the transport:

TransportGSS post-bind sealing?WhyHijackable (with ESC17 cert)?
LDAP 389 (cleartext)YES, systematicallyWithout TLS, the LDAP signing requirement forces the GSS security layer. Otherwise the DC rejects.❌ No, the traffic is sealed, and we lack the key
LDAPS 636 (TLS)NO, forbidden[MS-ADTS]: AD does not allow the SASL layer on a TLS connection. TLS is deemed sufficient.✅ Yes, post-bind in the clear inside the TLS we hold

The essential point: the absence of sealing over LDAPS is not an accidental weakness or a misconfiguration, it is a Microsoft design decision (not stacking two protections). A Kerberos bind on 636 is always unsealed, by construction. This is exactly what makes the hijack possible: TLS, which is supposed to be the protection, becomes the link ESC17 hands us.

It is also why channel binding is the only real counter: since protection rests entirely on TLS, you have to tie the authentication to the real TLS certificate, which is what the CBT does, and what our fake ESC17 certificate cannot satisfy.

LDAPS or StartTLS: the same target

A question comes up often: “my target does LDAP over SSL/TLS, is that LDAPS, and is it affected?” There are two ways to get TLS over LDAP, and you have to distinguish them:

  • LDAPS (port 636), implicit TLS: the TLS handshake happens as soon as the connection opens, then all of LDAP flows inside. It is the “HTTPS of LDAP.”
  • StartTLS (port 389), explicit TLS: the connection opens in the clear on 389, then the client sends a StartTLS extended operation (1.3.6.1.4.1.1466.20037). If the server accepts, TLS is negotiated on the same socket, and LDAP switches into the tunnel.

For the attack, once TLS is up, it is strictly identical. Both result in a TLS-protected LDAP session, and:

  • The [MS-ADTS] clause (no SASL sign/seal layer over TLS) applies in the same way to both → the post-bind is unsealed in both cases → hijackable identically.
  • Channel binding is the same protection: the CBT is the tls-server-end-point, that is, the hash of the TLS certificate presented, regardless of how TLS was established. Our ESC17 certificate breaks the CBT identically on 636 and on a StartTLS 389.

The only difference is the interception mechanics:

TLS establishmentWhat adecrypt does
LDAPS (636)implicit, as soon as the connection opensterminates TLS immediately with the ESC17 cert
StartTLS (389)explicit, via the StartTLS extended opintercepts the StartTLS request, responds as the server, then terminates the TLS upgrade with the ESC17 cert

adecrypt handles both (--ports 636,389): the 389 path detects the StartTLS, performs the TLS upgrade with the ESC17 certificate, then joins exactly the same hijack engine. The hijack therefore works identically on StartTLS.

Two nuances to know:

  1. 389 without StartTLS (a SASL bind directly in the clear on 389) → the bind negotiates sealing (LDAP signing) → out of reach. Only the StartTLS-then-bind path is hijackable on 389. In other words, on 389, everything depends on whether the client requests StartTLS before authenticating.
  2. A StartTLS-specific case, the strip: since StartTLS is a cleartext negotiation, you can refuse the upgrade (respond with a StartTLS failure) to attempt a downgrade of the session to cleartext. This mostly pays off against a simple bind: the client, having fallen back to cleartext, then sends its password, which we capture directly. This strip is impossible against LDAPS, which is TLS-only from the first byte.

On the reality side: LDAPS (636) dominates Windows integrations and appliances (“Use SSL” in the AD connector config). StartTLS (389) is very common on the Linux side (SSSD, nslcd, OpenLDAP clients, which often do ldap:// + start_tls). Both are perfectly valid targets. If your target “encrypts its LDAP,” it goes through one or the other, and our hijack applies in both cases (channel binding not enforced).

Case 1, simple bind: the password in the clear

The simplest case, and by far the most widespread on third-party equipment (appliances, printers, line-of-business applications, LDAP connectors). A simple bind transmits the DN and password in the clear in the bind PDU. On plain 389, any sniffer grabs them, which is why so many integrations go “cleanly” through LDAPS.

Except that LDAPS merely moves the secret into the TLS tunnel, a tunnel ESC17 gives us. We terminate TLS with the ESC17 certificate, decrypt the bind, and the password appears in the clear. The simple bind has neither application-layer signing nor channel binding (no GSS layer is in play): nothing stands in the way of the capture.

Validated in the lab: a service account (10.10.20.11, say an appliance configured for LDAPS) binds to the DC, and hands over its password instantly.

Terminal
[LDAP] 10.10.20.11 : LDAPS 636 - TLS termination (ESC17) -> DC
[AUTH] 10.10.20.11 : SIMPLE bind -> CLEARTEXT password captured
>>> [LDAP] simple bind CN=svc-app,OU=Services,DC=esc17,DC=local : Password1! (host=10.10.20.11)

adecrypt terminates the LDAPS connection’s TLS with the ESC17 certificate, opens its own connection to the real DC and relays. The first application PDU is a simple bind, so a (DN, password) pair transmitted in the clear inside the tunnel: the CN=svc-app,OU=Services,DC=esc17,DC=local account and its Password1! password drop straight out. The credential is reusable, we then authenticate anywhere that account has rights without replaying the attack.

What it’s worth A simple bind over LDAPS is always interceptable once you hold the TLS. Channel binding does not protect it (it applies only to GSS/SASL binds, and here there is no GSS layer). This is one of ESC17’s most immediate wins on LDAP, and often the most direct, because appliance service accounts are frequently (over)privileged.

Case 2, SASL / NTLM: ride the session, don’t relay it

The case gets more subtle here. A modern Windows client does not do a simple bind: it does a SASL GSS-SPNEGO bind, which negotiates NTLM or Kerberos. Let’s take NTLM first.

The first reflex is to think: “NTLM, I relay it to ntlmrelayx.” Before explaining why we go about it differently, let’s examine the obstacle, and that famous MIC.

The MIC (Message Integrity Code). In an NTLMv2 exchange, the final AUTHENTICATE message (Type 3) embeds a MIC: an HMAC-MD5 computed over the three NTLM messages (Negotiate + Challenge + Authenticate), with the session key derived from the user’s NT hash. Its role is precisely anti-relay: to prevent a man-in-the-middle from modifying the exchange, typically by stripping the flags that enforce LDAP signing, in order to replay the auth elsewhere. Since the MIC is sealed with a key we do not possess (the victim’s NT hash), we can neither forge it nor recompute it after tampering with the messages. SPNEGO’s mechListMIC does the same job one layer up: it protects the integrity of the list of negotiated mechanisms.

“Drop the MIC”, CVE-2019-1040 then CVE-2019-1166. These vulnerabilities allowed exactly that, removing / bypassing the MIC: the relayer could then strip the signing flags and get a cross-protocol NTLM relay to succeed (e.g. a coerced SMB authentication → relayed to LDAP). Microsoft fixed them (June then October 2019), and ntlmrelayx ships the exploit via the --remove-mic option. Very concrete consequence: on an unpatched DC, the NTLM→LDAP relay becomes possible again, whereas on an up-to-date DC, the MIC blocks it. (And even with the MIC bypassed, over LDAPS there would still be channel binding, see below.)

Relaying means, in any case, re-issuing the authentication toward a new session, which is exactly what the MIC/mechListMIC (and, over LDAPS, channel binding) are built to prevent. So we do not relay. We do the opposite: a transparent MITM. The NTLM AUTHENTICATE (Type 3) is forwarded verbatim to the real DC, MIC intact, no modification. The bind succeeds legitimately, as if we were not there. Then, because over LDAPS the SASL security layer is absent (see [MS-ADTS] above), the post-bind LDAP traffic is in the clear in front of us. At that precise instant, when the bind has just succeeded and the client is waiting, we seize the connection to the DC and inject our own operations into it, as the victim. No session key to possess, no MIC to recompute: we forge nothing, we ride an already-authenticated session.

This is what adecrypt’s --ldap-shell does: it detects the first unsealed credentialed bind, seizes the connection, and exposes an interactive LDAP shell. Validated in the lab by riding an administrator’s session:

Terminal
[LDAP] 10.10.20.11 : LDAPS 636 - TLS termination (ESC17) -> DC
[SHELL] session seized from 10.10.20.11 -> acting AS the victim on the DC
[SHELL] interactive LDAP shell on 0.0.0.0:11337
LDAP# grant_dcsync nnino
grant_dcsync nnino -> resultCode=0 (success)

The NTLM bind (Negotiate → Type 1/2/3) is passed verbatim to the DC, which validates it: the bind succeeds for real, the MIC is never touched. The post-bind traffic then arrives in the clear, with no GSS sealing over TLS, and adecrypt seizes the connection to the DC. From there, every operation is seen by the DC as coming from the administrator. adecrypt exposes a local shell (nc <attacker> 11337) from which grant_dcsync nnino adds to nnino the replication rights DS-Replication-Get-Changes[-All] on the domain object. The resultCode=0 confirms the DC accepts the write because it comes from an administrator: no escalation on our part, we borrow the rights of the seized session.

From there, secretsdump.py esc17/nnino@dc01 -just-dc uses these new DCSync rights to replicate the entire NTDS database (hashes of all accounts, including krbtgt). Full domain compromise, from an NTLM session we neither broke, nor relayed, nor even decrypted.

Prerequisite, worth repeating because it is central: channel binding disabled. If it were set to Always, the Type 3 would embed a CBT tied to our fake certificate, the DC would reject it, and the verbatim bind would fail before there was even a session to ride.

NTLM or Kerberos: for the hijack, it’s the same thing

A point to internalize before moving on to Kerberos: our hijack is agnostic to the authentication mechanism. Once the bind is passed verbatim and succeeds, we inject operations into an LDAP connection that is in the clear inside the TLS, and the original mechanism (NTLM or Kerberos) no longer plays any part at all. That is why a single body of code (--ldap-shell) handles both, with the same preconditions (no seal + channel binding not enforced) and the same limits.

The only NTLM/Kerberos difference exists solely on the orthogonal terrain of relaying:

  • NTLM is relayable: the token is not cryptographically bound to the target service, so it can be forwarded elsewhere. That is the whole point of ntlmrelayx.
  • Kerberos essentially is not: the AP-REQ contains a ticket encrypted with the key of the targeted SPN, so redirecting it to another service fails (in the words of Dirk-jan Mollema: “you cannot actually relay Kerberos the way you can relay NTLM”). James Forshaw demonstrated conditional Kerberos relays, but not as a standard route to LDAP.

Except that, and this is the key point, NTLM’s relayability opens no additional access on LDAPS: the protection that stops an NTLM→LDAPS relay is exactly the channel binding that would stop our hijack. Since we already hold the TLS channel and we ride a legitimate session, riding is cleaner than relaying: no coercion, no MIC / Drop-the-MIC battle, no flags to tamper with in the negotiation, and a single primitive for NTLM and Kerberos alike. The relay stays an NTLM-only option, more fragile, to reserve for the case where you cannot get on-path as a MITM on the channel.

Case 3, SASL / Kerberos: the hijack we didn’t think was possible

That leaves the case everyone considers out of reach: Kerberos. The dogma is solid: an AP-REQ is encrypted toward the key of the target machine, you cannot relay it (Dirk-jan Mollema says it flatly: “you cannot actually relay Kerberos the way you can relay NTLM”), and the public relay-to-LDAP tooling (ntlmrelayx) is NTLM only.

But we are not trying to relay. We apply the exact same reasoning as Case 2, and it holds, identically. The AP-REQ is passed verbatim to the real DC: the Kerberos bind succeeds legitimately, the mechListMIC is never touched, there is no downgrade. And crucially, the [MS-ADTS] clause applies in the same way: over LDAPS, the Kerberos bind negotiates no GSS sealing layer. It trusts the TLS. The post-bind traffic is therefore in the clear in front of us, and we ride the authenticated Kerberos session.

Hijacking a Kerberos session over LDAPS: adecrypt terminates TLS with the ESC17 cert, relays the AP-REQ verbatim to the DC (legitimate bind), then seizes the cleartext post-bind connection and injects grant_dcsync as the victim

How to read this diagram: the client believes it is talking to the DC (dc01.esc17.local) and presents its Kerberos AP-REQ. adecrypt, in the middle, does not understand this ticket (it is encrypted toward the DC’s key), but it does not need to: it copies it verbatim to the real DC. The DC decrypts, validates, responds success. The bind is therefore authentic end to end, which is the key to breaking nothing. Only afterward, when the client sends its first LDAP request (in the clear, for lack of sealing over TLS), does adecrypt switch: it keeps the connection for itself and injects its own operations, which the DC attributes to the victim.

The independent, verifiable proof lies in the direction of the security layer: on plaintext 389, a Windows Kerberos bind does derive signing/sealing keys and protects its messages. Being MITM on 389 gains you nothing, the injected operations fail at the GSS MIC. It is only because TLS disables that sealing that TLS termination exposes the session. The premise “no sealing because it trusts TLS” is not a detail: it is the pivot of the whole technique.

adecrypt materializes this with --ldap-shell, which rides both NTLM and Kerberos binds and first confirms the absence of sealing before seizing. Validated in the lab (signing required, channel binding disabled):

Terminal
[LDAP] 10.10.20.11 : LDAPS 636 - TLS termination (ESC17) -> DC
[HIJACK] 10.10.20.11 : credentialed bind OK (GSS/Kerberos) -> post-bind CLEARTEXT (no GSS seal over TLS) -> seizing
[HIJACK] session #1 seized : u:ESC17\Administrator @ 10.10.20.11 (mechListMIC untouched, AP-REQ verbatim)
LDAP# grant_dcsync nnino
grant_dcsync nnino -> resultCode=0 (success)

adecrypt sees the SASL Kerberos bind succeed at the DC, then waits for the first post-bind message, because that is what reveals whether the session is sealed. It arrives in the clear (no GSS layer over TLS, in line with [MS-ADTS]), so the session is hijackable and adecrypt seizes it, without having touched either the mechListMIC or the AP-REQ: the Kerberos bind stays perfectly legitimate. Since a pure Kerberos bind carries no NTLM message to pull the domain from, the shell reads the defaultNamingContext from the RootDSE through the seized session, the only thing we have and enough to query the directory. grant_dcsync nnino is then accepted (resultCode=0) because the write comes from Administrator: from there, a secretsdump.py esc17/nnino@dc01 -just-dc unwinds the entire NTDS database. Hijacking a Kerberos session, all the way to domain compromise.

The power of the result is bounded by the victim’s rights: riding the session of a non-privileged account gives you that account’s rights. That is what the session below shows.

One console, several victims, each one’s rights

adecrypt aggregates all seized sessions into a single multi-session console: sessions lists the hijacked accounts, use <id> switches from one to another, and each operation runs with the rights of the active account. By default, one session is kept per identity. --keep-hijack seizes every connection, which here lets us recover two different accounts from the same machine (10.10.20.11), namely nnino then Administrator:

Terminal
[HIJACK] session #1 seized : u:ESC17\nnino @ 10.10.20.11 -> added to the shell pool
[HIJACK] session #2 seized : u:ESC17\Administrator @ 10.10.20.11 -> added to the shell pool
Terminal
LDAP# sessions
ID IDENTITY SOURCE AGE STATE
* #1 u:ESC17\nnino 10.10.20.11 0m30s alive
#2 u:ESC17\Administrator 10.10.20.11 0m09s alive
LDAP# grant_dcsync nnino # active session = #1 (nnino, non-privileged)
grant_dcsync nnino -> resultCode=50 (insufficientAccessRights)
LDAP# use 2 # switch to Administrator's session
active session -> #2
LDAP# grant_dcsync nnino # same command, but AS Administrator this time
grant_dcsync nnino -> resultCode=0 (success)

The demonstration is clear-cut: the same command fails (resultCode=50) under the nnino identity, then succeeds (resultCode=0) once the active session is switched to Administrator. We never exploit a privilege escalation, we simply borrow the rights of the session we are riding. grant_dcsync nnino grants nnino the replication rights DS-Replication-Get-Changes[-All] on the domain object, and secretsdump.py esc17/nnino@dc01 -just-dc then rolls out the entire NTDS database. Domain compromise, from a Kerberos session we neither broke nor relayed.

Is this realistic? Who speaks Kerberos over LDAPS?

A legitimate objection: “fine, but in real life, who opens an unsealed Kerberos session over LDAPS?” The answer comes down to one observation: LDAPS is the default Active Directory integration mode of nearly all third-party applications and equipment, and its use rests entirely on the assumption that the DC’s certificate is trustworthy. That is the thread running through this series, and ESC17 breaks that assumption.

A reminder of the scope: we only intercept traffic on 636, binds on 389 (sealed) are out of reach. But 636 traffic is far from marginal. Every time one of these clients opens an LDAPS connection, it produces exactly the exploitable bind:

  • Applications & appliances that authenticate their users against AD: intranet portals, ticketing (GLPI, Jira), CI/CD (GitLab, Jenkins), monitoring (Zabbix, Grafana), VPN gateways and firewalls (Fortinet, Palo Alto…), NAS (Synology/QNAP), printers/MFP, vCenter… Almost all of them offer “LDAP over SSL” and the admin ticked it for security’s sake. On each user login, the app does an LDAPS bind. Often a simple bind (service account) → we pick up the cleartext password in passing (Case 1), sometimes a Negotiate/Kerberos bind → we ride the session (Case 3).
  • Self-service password reset portals (SSPR): writing unicodePwd requires a confidential channel, and many portals choose LDAPS for it → LDAPS binds on demand.
  • IAM / provisioning connectors (directory synchronization, identity management) configured for LDAPS.
  • Audit tools / scanners in authenticated mode (AD collection) pointed at LDAPS.
  • Administration: a simple ldp.exe connected over SSL on 636 (Negotiate bind), the trivial GUI trigger to reproduce in the lab, with no certificate warning at all since the ESC17 cert is genuinely trusted.

A key point for the operator: these connections are on demand, triggered by a login, a reset, or a sync, and not subject to slow polling like WSUS. You provoke the traffic simply by connecting to the service concerned.

One observation sums up the situation: it is the choice of LDAPS “for security” that makes the interception stealthy. The certificate being issued by the domain’s certification authority, a client that merely checks the trust chain raises, in principle, no warning (behavior depends on how strictly the client validates).

Recovering a reusable NetNTLMv2 hash (--spnego-downgrade)

The hijack in Cases 2 and 3 assumes a cleartext session to seize on the spot. When that is not the objective, for example when you simply want to walk away with a reusable credential to crack at leisure, or to set up access for later, adecrypt offers a complementary, and perfectly reliable, capability: forcing the Kerberos → NTLM downgrade to recover the connection’s NetNTLMv2 hash.

A client that speaks Kerberos leaves, by default, nothing to crack: no reusable secret transits. But by rewriting the bind’s NegTokenInit to offer only NTLM, you force the client to fall back onto that mechanism. It then emits a full NTLM exchange, and its AUTHENTICATE message (Type 3) carries a NetNTLMv2 response, a challenge/response that is crackable offline (hashcat, mode 5600). We grab it in flight, inside the TLS tunnel we hold.

Terminal
[LDAP] 10.10.20.11 : LDAPS 636 - TLS termination (ESC17) -> DC
[AUTH] 10.10.20.11 : KERBEROS detected -> forced DOWNGRADE to NTLM
[NTLM] 10.10.20.11 : NetNTLMv2 captured -> ./adecrypt_loot/credentials.txt
Administrator::ESC17:1122334455667788:9E1C...F0:0101000000000000...

The client wanted to do Kerberos, but adecrypt rewrites the first SPNEGO token (NegTokenInit) to offer only NTLM: believing the server limited to that mechanism, the client replays its authentication over NTLM. Its AUTHENTICATE message (Type 3) then carries a NetNTLMv2 response derived from the account’s NT hash, captured on the fly and written to credentials.txt in hashcat format (user::domain:server_challenge:NTProofStr:blob), crackable offline with hashcat -m 5600.

Note: this hash is captured whether the bind succeeds or not, because the Type 3 is emitted before the DC rules on it, so even if the mechListMIC check ends up rejecting the downgraded bind, the NetNTLMv2 is already in our hands.

Once the hash is cracked, you have the account’s cleartext password: you can then authenticate directly, without even setting foot back in a MITM. Where Case 3 gives an immediate live session, --spnego-downgrade gives a durable credential. The two angles complement each other, depending on whether you want to act right away or dig in.

Summary

Depending on the type of bind the client presents over LDAPS, the outcome differs. The table below summarizes the four situations, in the configuration studied (LDAP signing required, channel binding absent).

Type of bindWhat we recoverPreconditionsImpact
Simple bindthe cleartext passwordTLS terminated (LDAPS or StartTLS)reusable credential, often a service account
SASL NTLMthe authenticated session (hijack)TLS terminated, post-bind unsealed, channel binding absentdirectory actions as the victim (DCSync, RBCD, Shadow Credentials, ACL…)
SASL Kerberosthe authenticated session (hijack)same as NTLMsame as NTLM
Forced NTLM (--spnego-downgrade)a NetNTLMv2 hashTLS terminatedhash crackable offline, hence a reusable password

The common thread of the first three cases: adecrypt neither breaks nor replays the authentication, it lets it succeed and then exploits the absence of sealing to act on a legitimate session. The fourth is a fallback for when the hijack is not wanted.

Is this a Kerberos vulnerability? No. Is it a 0-day? Also no.

Let’s be precise, because the nuance is the heart of the matter.

This is not a Kerberos flaw. RFC 4120 is explicit, “The application is free to choose whatever protection may be necessary”: message integrity and encryption are optional in Kerberos. RFC 4752 confirms that “no security layer” is a perfectly legal negotiation outcome. The defect is not in the protocol, it is in the way the service uses Kerberos, without tying the authentication to the channel. The principle is, moreover, an old one: Black Hat USA 2010, Attacking Kerberos Deployments, already stated it (“you want a tie between authentication and protocol, but have to build it yourself”).

This is not a 0-day. The link that makes the attack possible, namely “LDAPS MITM without channel binding lets you take over the session,” is a known weakness class, and Microsoft already ships the named mitigation: LdapEnforceChannelBinding=Always (ADV190023). MSRC would classify this as by-design / apply the existing hardening.

Automation with adecrypt

As with the other articles, adecrypt takes the ESC17-eligible certificate and drives the attack end to end.

Terminal
# 1. Server certificate for the DC, via the ESC17-vulnerable template
certipy req -u attacker@esc17.local -p '...' -ca 'ESC17-CA' \
-template 'VulnServer' -dns dc01.esc17.local -out dc01
# 2. MITM LDAPS + hijack shell (NTLM and Kerberos)
adecrypt ldaps --targets 10.10.20.11 --gateway 10.10.20.1 --interface eth0 \
--dc 10.10.20.10:dc01.pfx --ports 636 --ldap-shell
# 3. Multi-session console: list targets, switch, act AS the victim
# nc 127.0.0.1 11337 -> sessions | use <id> | grant_dcsync <user> | set_rbcd | passwd ...

--ldap-shell rides NTLM and Kerberos binds alike (it first confirms the absence of post-bind sealing before seizing) and exposes a single multi-session console: sessions lists the hijacked accounts, use <id> switches, and the deduplication keeps one session per identity (several distinct accounts from the same machine are each seized). --keep-hijack seizes every connection, and --spnego-downgrade remains the offline option (NetNTLMv2 capture) when the hijack is not possible.

Mitigations

A single countermeasure neutralizes all of layer 2 in this article, NTLM as well as Kerberos:

  • Enforce LDAP channel binding: LdapEnforceChannelBinding = 2 (Always). This is the decisive mitigation, it ties the authentication to the DC’s real certificate, and our ESC17 certificate fails immediately. To deploy before worrying about anything else.
  • Fix the ESC17 template at the source: remove the “supply in the request” flag, restrict enrollment rights, or require manager approval, without relying on the EKU value alone.
  • Pin the certificate on centrally managed LDAPS clients, when feasible.
  • Monitor the issuance of Server Authentication certificates carrying an arbitrary SAN, and the appearance of LDAPS binds coming from unexpected hosts.

And, for the simple bind (Case 1), remember that channel binding does not protect it: there, only fixing the ESC17 template and pinning matter.

Actions

Switch languageOpen the RSS feed

Go to

HomeBlogTagsArchives