# ESC17 - Beyond WSUS: Hijacking Kerberos Sessions over LDAPS

> ESC17 breaks LDAPS TLS, but authentication lives above it: cleartext bind capture and session hijacking, against NTLM as well as Kerberos, on the DC.

Date: 2026-07-26
Tags: adcs, active-directory, ldap, ldaps, kerberos, ntlm, esc17
URL: https://research.qu35t.pw/en/blog/esc17-ldaps-session-hijack/

---

*A research series by <a href="https://www.linkedin.com/in/alexis-martin3190/" target="_blank" rel="noopener noreferrer">Alexis MARTIN</a> (<a href="https://x.com/QU35T_TV" target="_blank" rel="noopener noreferrer">qu35t</a>) and <a href="https://www.linkedin.com/in/alexis-paret/" target="_blank" rel="noopener noreferrer">Alexis PARET</a> (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](/en/blog/esc17-introduction-threat-model/) 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 <a href="https://msrc.microsoft.com/update-guide/en-us/advisory/ADV190023" target="_blank" rel="noopener noreferrer"><strong>ADV190023</strong></a>.

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

| DC setting | Effect 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 setting | What 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:

| Signing | Channel binding | What stays possible |
|:---:|:---:|:---|
| required | required | nothing from either (complete hardening) |
| required | **absent** | **session hijack** (our lab case, NTLM & Kerberos hijack) |
| **absent** | required | **NTLM relay → LDAP** |
| **absent** | **absent** | **both**: 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 <a href="https://github.com/fortra/impacket" target="_blank" rel="noopener noreferrer"><code>ntlmrelayx</code></a> 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:

| Transport | GSS post-bind sealing? | Why | Hijackable (with ESC17 cert)? |
|:---|:---|:---|:---|
| **LDAP 389 (cleartext)** | **YES, systematically** | Without 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 establishment | What adecrypt does |
|:---|:---|:---|
| **LDAPS (636)** | implicit, as soon as the connection opens | terminates TLS **immediately** with the ESC17 cert |
| **StartTLS (389)** | explicit, via the StartTLS extended op | **intercepts** the StartTLS request, responds as the server, then terminates the **TLS upgrade** with the ESC17 cert |

<a href="https://github.com/qu35t-code/adecrypt" target="_blank" rel="noopener noreferrer">adecrypt</a> 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.

```bash
[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**.

<strong>"Drop the MIC", <a href="https://nvd.nist.gov/vuln/detail/CVE-2019-1040" target="_blank" rel="noopener noreferrer">CVE-2019-1040</a> then <a href="https://nvd.nist.gov/vuln/detail/CVE-2019-1166" target="_blank" rel="noopener noreferrer">CVE-2019-1166</a>.</strong> 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:

```bash
[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 <a href="https://dirkjanm.io/krbrelayx-unconstrained-delegation-abuse-toolkit/" target="_blank" rel="noopener noreferrer">Dirk-jan Mollema</a>: *"you cannot actually relay Kerberos the way you can relay NTLM"*). <a href="https://projectzero.google/2021/10/using-kerberos-for-authentication-relay.html" target="_blank" rel="noopener noreferrer">James Forshaw</a> 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](/images/esc17-ldap-schema-kerberos-hijack-en.png)

**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**):

```bash
[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.**

:::note[Even Protected Users accounts fall]
This is the most counter-intuitive consequence. The **Protected Users** group forces its members onto **Kerberos** and forbids them NTLM, which neutralizes classic NTLM relays, including the HTTPS vector from the [previous article](/en/blog/esc17-https-interne/). But the hijack described here relays nothing: it **rides the legitimate Kerberos session** once the bind has passed verbatim. Since it operates on Kerberos exactly as on NTLM, **a member of Protected Users, administrators included, stays fully exposed** as long as channel binding is not enforced. Protected Users therefore does not protect against this technique.
:::

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`:

```bash
[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
```
```bash
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** (<a href="https://hashcat.net/hashcat/" target="_blank" rel="noopener noreferrer">hashcat</a>, mode 5600). We grab it **in flight**, inside the TLS tunnel we hold.

```bash
[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 bind | What we recover | Preconditions | Impact |
|:---|:---|:---|:---|
| **Simple bind** | the cleartext password | TLS terminated (LDAPS or StartTLS) | reusable credential, often a service account |
| **SASL NTLM** | the authenticated session (hijack) | TLS terminated, post-bind unsealed, channel binding absent | directory actions as the victim (DCSync, RBCD, Shadow Credentials, ACL…) |
| **SASL Kerberos** | the authenticated session (hijack) | same as NTLM | same as NTLM |
| **Forced NTLM** (`--spnego-downgrade`) | a NetNTLMv2 hash | TLS terminated | hash 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.** <a href="https://datatracker.ietf.org/doc/html/rfc4120" target="_blank" rel="noopener noreferrer">RFC 4120</a> is explicit, *"The application is free to choose whatever protection may be necessary"*: message integrity and encryption are **optional** in Kerberos. <a href="https://datatracker.ietf.org/doc/html/rfc4752" target="_blank" rel="noopener noreferrer">RFC 4752</a> 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.

```bash
# 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.
