# ESC17 - Au-delà de WSUS : détournement de session Kerberos sur LDAPS

> ESC17 casse le TLS de LDAPS, mais l'authentification vit au-dessus : capture de bind en clair et détournement de session NTLM comme Kerberos sur le DC.

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

---

*Une série de recherche par <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>) et <a href="https://www.linkedin.com/in/alexis-paret/" target="_blank" rel="noopener noreferrer">Alexis PARET</a> (nnino).*
*Chaque technique présentée dans cette série a été reproduite de bout en bout dans un environnement de laboratoire entièrement à jour (DC Windows Server 2025 pleinement patché, au 25 juillet 2026).*

Le [premier volet](/blog/esc17-introduction-threat-model/) a posé la primitive : ESC17 fabrique une **identité de serveur TLS approuvée à l'échelle du domaine**, pour n'importe quel nom d'hôte. WSUS en était la démonstration la plus spectaculaire, parce qu'une mise à jour manipulée y devient du code SYSTEM. Ce troisième volet s'attaque à un service bien plus central, et bien plus subtil : **l'annuaire lui-même, en LDAP / LDAPS.**

L'intuition naïve serait : « je termine le TLS avec mon certificat ESC17, donc je lis tout l'annuaire ». La lecture passive du trafic est en effet possible, mais l'essentiel est ailleurs. Il se joue dans **l'authentification** : que devient un bind LDAP quand on lui retire son transport TLS ? La réponse n'est pas binaire. Elle dépend du **type de bind** et de **deux réglages du contrôleur de domaine**. Et au bout de cette analyse se trouve un résultat que nous ne pensions pas trouver : le **détournement d'une session Kerberos déjà authentifiée**, un mécanisme réputé inattaquable.

## LDAP est une pile à deux couches

La confusion classique consiste à traiter « LDAPS » comme un bloc monolithique. Ce n'en est pas un. Une session LDAP authentifiée superpose **deux couches indépendantes** :

1. **Le transport**, soit du LDAP en clair (389/tcp), soit un canal TLS : LDAPS (636/tcp) ou StartTLS sur 389.
2. **Le bind applicatif**, la preuve d'identité négociée *à l'intérieur* de la session : soit un **simple bind** (DN + mot de passe), soit un **bind SASL** via GSS-SPNEGO, qui encapsule à son tour **NTLM** ou **Kerberos**.

ESC17 agit sur la **couche 1** : on termine le TLS avec un certificat approuvé, on voit le trafic en clair. La vraie question, celle qui décide de l'impact, est : **la couche 2 survit-elle à un transport compromis ?** Autrement dit, l'authentification apporte-t-elle sa *propre* protection d'intégrité, indépendante du TLS, ou fait-elle aveuglément confiance au canal ?

> **La question qui gouverne tout cet article**
> Un bind LDAP protège-t-il ses messages *lui-même* (signature/scellement applicatif), ou délègue-t-il entièrement la protection au TLS ? Si c'est la seconde option, et que le TLS est à nous, alors la session authentifiée est à nous aussi.

Deux réglages de politique côté DC arbitrent cette question. Il faut les avoir en tête avant tout le reste.

## Les deux protections : LDAP signing et channel binding

**LDAP signing (`LDAPServerIntegrity`).** Contrôlé par la clé `HKLM\SYSTEM\CurrentControlSet\Services\NTDS\Parameters\LDAPServerIntegrity` (`1` = aucune exigence, `2` = signature requise, valeur par défaut renforcée sur les DC modernes). Quand la signature est *requise*, un bind SASL sur un **transport en clair (389)** doit négocier une couche de sécurité GSS (signature/scellement), faute de quoi le DC le rejette. C'est ce qui protège l'annuaire d'un simple sniffing sur 389.

Le point décisif, et contre-intuitif, tient en une phrase du protocole Microsoft. **[MS-ADTS], section *SASL Authentication* :**

> *Active Directory permet d'effectuer des binds SASL sur une connexion protégée par SSL/TLS, [mais] il **n'autorise pas** l'utilisation de mécanismes de chiffrement/vérification d'intégrité de couche SASL sur une telle connexion.*

Concrètement, **sur LDAPS (636), la couche de sécurité GSS est *interdite*.** Le DC considère que le TLS fournit déjà la protection, donc le bind SASL, NTLM comme Kerberos, s'effectue **sans signature ni scellement applicatif**. Toute la protection du trafic post-bind repose alors *exclusivement* sur le TLS. Or ce TLS, avec ESC17, est le nôtre.

**LDAP channel binding (`LdapEnforceChannelBinding`).** Contrôlé par `...\NTDS\Parameters\LdapEnforceChannelBinding` (`0` = jamais, `1` = si supporté, `2` = toujours). C'est **le vrai garde-fou contre ESC17 en LDAPS**. Quand il vaut `2`, le client intègre dans son jeton d'authentification un *Channel Binding Token*, à savoir le `tls-server-end-point`, c'est-à-dire le **hash du certificat TLS réellement présenté par le serveur**. Le DC vérifie ce hash contre son *propre* certificat. Si nous avons terminé le TLS avec notre certificat ESC17, le CBT calculé par le client vaut `hash(cert ESC17)` ≠ `hash(cert réel du DC)` → **le bind est rejeté**, quels que soient nos efforts. Le channel binding lie l'authentification au certificat exact du serveur, et c'est précisément la mitigation que Microsoft a poussée avec l'avis <a href="https://msrc.microsoft.com/update-guide/en-us/advisory/ADV190023" target="_blank" rel="noopener noreferrer"><strong>ADV190023</strong></a>.

Le tableau de vérité qui en découle, et qui structure toute la suite :

| Réglage DC | Effet sur l'attaque ESC17 en LDAPS |
|:---|:---|
| Signing `LDAPServerIntegrity=2` (requis) | **Ne mord pas sur 636** : le TLS satisfait l'exigence, la couche SASL est de toute façon absente. Ne bloque que le sniffing sur 389 en clair. |
| Channel binding `LdapEnforceChannelBinding=0` (jamais) | **Aucune protection** : rien ne lie l'auth à notre faux certificat. |
| Channel binding `LdapEnforceChannelBinding=2` (toujours) | **Blocage total** : notre certificat ESC17 ≠ certificat du DC → bind rejeté (NTLM *et* Kerberos). |

Tout ce qui suit a été validé dans la configuration **la plus fréquente en entreprise, et pourtant suffisante** : signature **requise** (`=2`), channel binding **désactivé** (`=0`). Un durcissement à moitié fait, exactement le genre d'écart que la couche 2 est censée rattraper, mais ne rattrape pas.

## Deux protections, deux attaques : laquelle bloque quoi

Le détail le plus important à saisir, et le plus souvent confondu, c'est que **ces deux réglages ne protègent pas de la même attaque** :

| Réglage DC | Ce qu'il bloque |
|:---|:---|
| **Channel binding** (`LdapEnforceChannelBinding=2`) | notre **détournement de session** (MITM ESC17 sur LDAPS) |
| **LDAP signing** (`LDAPServerIntegrity=2`) | le **relais NTLM** vers LDAP |

Ce sont **deux protections orthogonales**, contre **deux attaques distinctes**. D'où la matrice de ce qui reste ouvert :

| Signing | Channel binding | Ce qui reste possible |
|:---:|:---:|:---|
| requis | requis | rien des deux (durcissement complet) |
| requis | **absent** | **détournement de session** (notre cas de lab, hijack NTLM & Kerberos) |
| **absent** | requis | **relais NTLM → LDAP** |
| **absent** | **absent** | **les deux** : hijack *et* relais |

Notre laboratoire est la 2ᵉ ligne : signing requis (le relais est fermé) mais channel binding absent (le hijack est ouvert). C'est ce que le reste de l'article exploite.

**Et si le LDAP signing est absent (lignes 3-4) ?** Une autre voie s'ouvre, indépendante d'ESC17 : le **relais NTLM vers LDAP**, le scénario <a href="https://github.com/fortra/impacket" target="_blank" rel="noopener noreferrer"><code>ntlmrelayx</code></a> classique (la raison même de l'avis **ADV190023**). Sans exigence de signature sur `ldap://` (389), et sans channel binding sur `ldaps://` (636), une authentification NTLM **coercée** (PetitPotam, PrinterBug, WebDAV…) peut être **relayée** vers le LDAP du DC et y exécuter des écritures privilégiées **en tant que la victime coercée** : **RBCD**, **ajout de machine**, **octroi de DCSync**, **Shadow Credentials** (`msDS-KeyCredentialLink`), modification d'ACL. Aucun certificat, aucune terminaison TLS : c'est souvent le chemin le plus rapide vers le domaine.

Ce relais est un angle **complémentaire**, avec ses propres contraintes :

- **NTLM uniquement.** Kerberos n'est pas relayable (l'AP-REQ est lié à la clé du SPN cible). En environnement Kerberos-only / NTLM désactivé, le relais tombe, et il reste le **détournement ESC17** (qui, lui, couvre Kerberos).
- **Il faut coercer** une authentification entrante, et l'on est borné par **les droits de l'identité coercée** (relayer un compte machine privilégié → RBCD → prise de contrôle, relayer un utilisateur lambda → impact limité).
- Il n'aide **pas** sur LDAPS si le channel binding est activé : la protection du relais NTLM→LDAPS est **exactement** le même CBT que celui de notre hijack.

Retenons-le : **le channel binding protège du détournement, le LDAP signing protège du relais, il faut activer les DEUX.** En désactiver un seul laisse une attaque grande ouverte. Les désactiver tous les deux (encore courant) offre les deux voies à la fois.

## Le scellement GSS, ce qui décide du détournement

Tout l'article tient sur un concept qu'il faut poser clairement : le **scellement** (GSS *sign & seal*). Quand un bind SASL (NTLM ou Kerberos) réussit, les deux parties dérivent une **clé de session** à partir de l'authentification. Si une **couche de sécurité GSS** est négociée, chaque message LDAP *post-bind* est ensuite **signé et chiffré** avec cette clé (`GSS_Wrap`) : le trafic devient opaque et infalsifiable pour quiconque n'a pas la clé, y compris un MITM qui possède pourtant le TLS.

La règle est donc la suivante :

> **Une session authentifiée est détournable si, et seulement si, son trafic post-bind n'est PAS scellé, et que nous possédons le transport.**

Or c'est Active Directory lui-même qui décide s'il y a scellement, et il le fait de façon **opposée** selon le transport :

| Transport | Scellement GSS post-bind ? | Pourquoi | Détournable (avec cert ESC17) ? |
|:---|:---|:---|:---|
| **LDAP 389 (clair)** | **OUI, systématique** | Sans TLS, l'exigence de *LDAP signing* force la couche de sécurité GSS. Sinon le DC rejette. | ❌ Non, le trafic est scellé, la clé nous manque |
| **LDAPS 636 (TLS)** | **NON, interdit** | **[MS-ADTS]** : AD *n'autorise pas* la couche SASL sur une connexion TLS. Le TLS est réputé suffisant. | ✅ Oui, post-bind en clair *dans le TLS que nous tenons* |

Le point essentiel : **l'absence de scellement en LDAPS n'est pas une faiblesse accidentelle ni une mauvaise config, c'est une décision de conception de Microsoft** (ne pas empiler deux protections). Un bind Kerberos sur 636 est *toujours* non scellé, par construction. C'est exactement ce qui rend le détournement possible : le TLS, censé être la protection, devient le maillon que ESC17 nous offre.

C'est aussi pourquoi le **channel binding** est la seule vraie parade : puisque la protection repose entièrement sur le TLS, il faut **lier l'authentification au certificat TLS réel**, ce que fait le CBT, et ce que notre faux certificat ESC17 ne peut pas satisfaire.

## LDAPS ou StartTLS : la même cible

Une question revient souvent : « ma cible fait du *LDAP over SSL/TLS*, est-ce que c'est du LDAPS, et est-ce concerné ? » Il existe **deux façons** d'obtenir du TLS sur LDAP, et il faut les distinguer :

- **LDAPS (port 636)**, TLS **implicite** : le handshake TLS a lieu **dès l'ouverture de la connexion**, puis tout le LDAP circule à l'intérieur. C'est le « HTTPS du LDAP ».
- **StartTLS (port 389)**, TLS **explicite** : la connexion s'ouvre **en clair** sur le 389, puis le client envoie une **opération étendue *StartTLS*** (`1.3.6.1.4.1.1466.20037`). Si le serveur accepte, le TLS est négocié **sur la même socket**, et le LDAP bascule dans le tunnel.

**Pour l'attaque, une fois le TLS monté, c'est rigoureusement identique.** Les deux aboutissent à une session LDAP protégée par TLS, et :

- La clause **[MS-ADTS]** (pas de couche sign/seal SASL au-dessus de TLS) s'applique **de la même manière** aux deux → le post-bind est **non scellé** dans les deux cas → chevauchable à l'identique.
- Le **channel binding est la même protection** : le CBT est le `tls-server-end-point`, c'est-à-dire le hash du **certificat TLS présenté**, quelle que soit la façon dont le TLS a été établi. Notre certificat ESC17 casse le CBT identiquement sur 636 comme sur un StartTLS 389.

**La seule différence est la mécanique d'interception :**

| | Établissement du TLS | Ce qu'adecrypt fait |
|:---|:---|:---|
| **LDAPS (636)** | implicite, dès la connexion | termine le TLS **immédiatement** avec le cert ESC17 |
| **StartTLS (389)** | explicite, via l'op étendue StartTLS | **intercepte** la requête StartTLS, répond en tant que serveur, puis termine la **montée TLS** avec le cert ESC17 |

<a href="https://github.com/qu35t-code/adecrypt" target="_blank" rel="noopener noreferrer">adecrypt</a> gère les deux (`--ports 636,389`) : le chemin 389 détecte le StartTLS, réalise l'upgrade TLS avec le certificat ESC17, puis rejoint **exactement le même** moteur de chevauchement. Le détournement fonctionne donc à l'identique sur StartTLS.

**Deux nuances à connaître :**

1. **389 *sans* StartTLS** (un bind SASL directement en clair sur 389) → le bind **négocie le scellement** (LDAP signing) → **hors de portée**. Seul le chemin *StartTLS-puis-bind* est chevauchable sur le 389. Autrement dit, sur 389, tout dépend de si le client demande StartTLS avant de s'authentifier.
2. **Cas propre à StartTLS, le *strip*** : puisque le StartTLS est une négociation *en clair*, on peut **refuser** l'upgrade (répondre un échec au StartTLS) pour tenter un *downgrade* de la session en clair. C'est surtout payant contre un **simple bind** : le client, retombé en clair, envoie alors son mot de passe que l'on capture directement. Ce *strip* est impossible contre LDAPS, qui est TLS-only dès le premier octet.

**Côté réalité :** **LDAPS (636)** domine les intégrations **Windows** et les appliances (« Use SSL » dans la config du connecteur AD). **StartTLS (389)** est très fréquent côté **Linux** (SSSD, `nslcd`, clients OpenLDAP, qui font souvent `ldap://` + `start_tls`). Les deux sont des cibles parfaitement valides. Si votre cible « chiffre son LDAP », elle passe par l'un ou l'autre, et notre chevauchement s'applique dans les deux cas (channel binding non forcé).

## Cas 1, simple bind : le mot de passe en clair

Le cas le plus simple, et de loin le plus répandu sur les équipements tiers (appliances, imprimantes, applications métier, connecteurs LDAP). Un *simple bind* transmet le **DN et le mot de passe en clair** dans le PDU de bind. En 389 pur, n'importe quel sniffer les récupère, c'est pourquoi tant d'intégrations passent « proprement » par LDAPS.

Sauf que LDAPS ne fait que déplacer le secret dans le tunnel TLS, un tunnel que ESC17 nous donne. On termine le TLS avec le certificat ESC17, on déchiffre le bind, **le mot de passe apparaît en clair.** Le simple bind n'a ni signature applicative, ni channel binding (aucune couche GSS n'est en jeu) : **rien ne s'oppose à la capture.**

Validé au lab : un compte de service (`10.10.20.11`, disons une appliance configurée en LDAPS) se lie au DC, et livre son mot de passe instantanément.

```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 termine le TLS de la connexion LDAPS avec le certificat ESC17, ouvre sa propre connexion vers le vrai DC et relaie. Le premier PDU applicatif est un simple bind, donc un couple *(DN, mot de passe)* transmis en clair dans le tunnel : le compte `CN=svc-app,OU=Services,DC=esc17,DC=local` et son mot de passe `Password1!` tombent directement. L'identifiant est **réutilisable**, on s'authentifie ensuite partout où ce compte a des droits sans rejouer l'attaque.

> **Ce que ça vaut**
> Un simple bind sur LDAPS est *toujours* interceptable dès lors qu'on tient le TLS. Le channel binding **ne le protège pas** (il ne s'applique qu'aux binds GSS/SASL, or ici il n'y a aucune couche GSS). C'est l'un des acquis les plus immédiats d'ESC17 sur LDAP, et souvent le plus direct, car les comptes de service d'appliances sont fréquemment (sur)privilégiés.

## Cas 2, SASL / NTLM : chevaucher la session, ne pas la relayer

Le cas devient ici plus subtil. Un client Windows moderne ne fait pas de simple bind : il fait un bind **SASL GSS-SPNEGO**, qui négocie NTLM ou Kerberos. Prenons NTLM d'abord.

Le premier réflexe consiste à se dire : « NTLM, je relaie vers `ntlmrelayx` ». Avant d'expliquer pourquoi on procède autrement, examinons l'obstacle, et ce fameux **MIC**.

**Le MIC (Message Integrity Code).** Dans un échange NTLMv2, le message final `AUTHENTICATE` (Type 3) embarque un MIC : un **HMAC-MD5 calculé sur les trois messages NTLM** (Negotiate + Challenge + Authenticate), avec la **clé de session dérivée du hash NT** de l'utilisateur. Son rôle est précisément **anti-relais** : empêcher un homme-du-milieu de *modifier* l'échange, typiquement de retirer les drapeaux qui imposent la signature LDAP, pour pouvoir rejouer l'auth ailleurs. Comme le MIC est scellé par une clé qu'on ne possède pas (le hash NT de la victime), on ne peut ni le forger ni le recalculer après avoir trafiqué les messages. Le **mechListMIC** de SPNEGO fait le même travail une couche au-dessus : il protège l'intégrité de la **liste des mécanismes** négociés.

<strong>« Drop the MIC », <a href="https://nvd.nist.gov/vuln/detail/CVE-2019-1040" target="_blank" rel="noopener noreferrer">CVE-2019-1040</a> puis <a href="https://nvd.nist.gov/vuln/detail/CVE-2019-1166" target="_blank" rel="noopener noreferrer">CVE-2019-1166</a>.</strong> Ces vulnérabilités permettaient exactement de **supprimer / contourner le MIC** : le relayeur pouvait alors retirer les drapeaux de signature et faire aboutir un relais NTLM cross-protocole (p. ex. une authentification SMB coercée → **relayée vers LDAP**). Microsoft les a **corrigées** (juin puis octobre 2019), et `ntlmrelayx` embarque l'exploit via l'option `--remove-mic`. **Conséquence très concrète : sur un DC non patché, le relais NTLM→LDAP redevient possible, alors que sur un DC à jour, le MIC le bloque.** (Et même MIC contourné, sur LDAPS il resterait le channel binding, cf. plus bas.)

Relayer, c'est de toute façon réémettre l'authentification vers une **nouvelle** session, ce que le MIC/mechListMIC (et, sur LDAPS, le channel binding) sont faits pour empêcher. Nous ne relayons donc pas. Nous faisons l'inverse : **un MITM transparent.** L'`AUTHENTICATE` NTLM (Type 3) est transmis **verbatim** au vrai DC, MIC intact, aucune modification. Le bind **réussit légitimement**, comme si nous n'étions pas là. Puis, parce que sur LDAPS la couche de sécurité SASL est *absente* (cf. [MS-ADTS] ci-dessus), le trafic LDAP post-bind est **en clair devant nous**. À cet instant précis, le bind vient de réussir et le client attend, nous **saisissons la connexion** vers le DC et y injectons nos propres opérations, **en tant que la victime**. Aucune clé de session à posséder, aucun MIC à recalculer : nous ne forgeons rien, nous *montons* sur une session déjà authentifiée.

C'est ce que fait `--ldap-shell` d'adecrypt : il détecte le premier bind crédentié non scellé, saisit la connexion, et expose un **shell LDAP interactif**. Validé au lab en chevauchant la session d'un administrateur :

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

Le bind NTLM (Negotiate → Type 1/2/3) est passé **verbatim** au DC, qui valide : le bind réussit pour de vrai, le MIC n'est jamais touché. Le trafic post-bind arrive alors en clair, faute de scellement GSS sur TLS, et adecrypt **saisit** la connexion vers le DC. À partir de là, toute opération est vue par le DC comme provenant de l'administrateur. adecrypt expose un shell local (`nc <attaquant> 11337`) depuis lequel `grant_dcsync nnino` ajoute à `nnino` les droits de réplication `DS-Replication-Get-Changes[-All]` sur l'objet domaine. Le `resultCode=0` confirme que le DC accepte l'écriture parce qu'elle vient d'un administrateur : aucune élévation de notre part, on **emprunte** les droits de la session saisie.

De là, `secretsdump.py esc17/nnino@dc01 -just-dc` utilise ces nouveaux droits DCSync pour **répliquer toute la base NTDS** (hashes de tous les comptes, dont `krbtgt`). **Compromission complète du domaine, à partir d'une session NTLM que nous n'avons ni cassée, ni relayée, ni même déchiffrée.**

Prérequis, à répéter parce qu'il est central : **channel binding désactivé.** S'il était sur `Always`, le Type 3 embarquerait un CBT lié à notre faux certificat, le DC le rejetterait, et le bind verbatim échouerait avant même qu'il y ait une session à chevaucher.

### NTLM ou Kerberos : pour le chevauchement, c'est la même chose

Un point à intégrer avant de passer à Kerberos : **notre chevauchement est agnostique au mécanisme d'authentification.** Une fois le bind passé verbatim et réussi, on injecte des opérations dans une connexion LDAP **en clair dans le TLS**, et le mécanisme d'origine (NTLM ou Kerberos) n'intervient plus du tout. C'est pour cela qu'un seul et même code (`--ldap-shell`) traite les deux, avec les **mêmes préconditions** (pas de seal + channel binding non forcé) et les **mêmes limites**.

La seule différence NTLM/Kerberos n'existe que sur le terrain, orthogonal, du **relais** :

- **NTLM est relayable** : le jeton n'est pas lié cryptographiquement au service cible, on peut le relayer ailleurs. C'est tout l'objet de `ntlmrelayx`.
- **Kerberos ne l'est essentiellement pas** : l'AP-REQ contient un ticket chiffré avec la **clé du SPN visé**, le rediriger vers un autre service échoue (dixit <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> a montré des relais Kerberos conditionnels, mais pas comme voie standard vers LDAP.

Sauf que, et c'est le point clé, **cette relayabilité de NTLM ne nous ouvre aucun accès supplémentaire en LDAPS** : la protection qui arrête un relais NTLM→LDAPS est **exactement** le channel binding qui arrêterait notre chevauchement. Puisqu'on possède déjà le canal TLS et qu'on monte sur une session **légitime**, le chevauchement est plus propre que le relais : aucune coercition, aucune bataille MIC / Drop-the-MIC, aucune négociation de drapeaux à trafiquer, et **une seule primitive pour NTLM comme pour Kerberos**. Le relais reste une option NTLM-only, plus fragile, à réserver au cas où l'on ne peut pas se placer en MITM sur le canal.

## Cas 3, SASL / Kerberos : le détournement qu'on ne croyait pas possible

Reste le cas que tout le monde considère hors de portée : **Kerberos.** Le dogme est solide : un AP-REQ est chiffré vers la clé de la machine cible, on ne peut pas le relayer (Dirk-jan Mollema le dit sans détour : *« you cannot actually relay Kerberos the way you can relay NTLM »*), et l'outillage public de relais vers LDAP (`ntlmrelayx`) est **NTLM uniquement**.

Mais nous ne cherchons pas à *relayer*. Nous appliquons exactement le raisonnement du Cas 2, et il tient, à l'identique. L'AP-REQ est passé **verbatim** au vrai DC : le bind Kerberos réussit légitimement, **le mechListMIC n'est jamais touché, il n'y a aucun downgrade.** Et surtout, la clause [MS-ADTS] s'applique de la même manière : **sur LDAPS, le bind Kerberos ne négocie aucune couche de scellement GSS.** Il fait confiance au TLS. Le trafic post-bind est donc en clair devant nous, et nous chevauchons la session Kerberos authentifiée.

![Détournement d'une session Kerberos sur LDAPS : adecrypt termine le TLS avec le cert ESC17, relaie l'AP-REQ verbatim au DC (bind légitime), puis saisit la connexion post-bind en clair et injecte grant_dcsync en tant que la victime](/images/esc17-ldap-schema-kerberos-hijack.png)

**Comment lire ce schéma :** le client croit parler au DC (`dc01.esc17.local`) et lui présente son AP-REQ Kerberos. adecrypt, au milieu, **ne comprend pas** ce ticket (il est chiffré vers la clé du DC), mais il n'en a pas besoin : il le **recopie tel quel** vers le vrai DC. Le DC déchiffre, valide, répond `success`. Le bind est donc **authentique de bout en bout**, c'est la clé pour ne rien casser. Ce n'est qu'*ensuite*, quand le client envoie sa première requête LDAP (en clair, faute de scellement sur TLS), qu'adecrypt bascule : il garde la connexion pour lui et y injecte ses propres opérations, que le DC attribue à la victime.

La preuve, indépendante et vérifiable, tient dans le **sens de la couche de sécurité** : sur **389 en clair**, un bind Kerberos Windows *dérive bien* des clés de signature/scellement et protège ses messages. Être MITM sur 389 n'apporte rien, les opérations injectées échouent au MIC GSS. C'est **uniquement** parce que le TLS désactive ce scellement que la terminaison TLS expose la session. La prémisse « pas de scellement parce qu'il fait confiance au TLS » n'est pas un détail : c'est **le pivot** de toute la technique.

adecrypt matérialise cela avec `--ldap-shell`, qui chevauche aussi bien les binds NTLM que Kerberos et **confirme d'abord l'absence de scellement** avant de saisir. Validé au lab (signature **requise**, channel binding **désactivé**) :

```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 voit le bind SASL Kerberos réussir au niveau du DC, puis attend le premier message post-bind, car c'est lui qui révèle si la session est scellée. Il arrive en clair (pas de couche GSS sur TLS, conformément à [MS-ADTS]), donc la session est détournable et adecrypt la saisit, sans avoir touché ni le `mechListMIC` ni l'AP-REQ : le bind Kerberos reste parfaitement légitime. Comme un bind Kerberos pur ne porte aucun message NTLM d'où tirer le domaine, le shell lit le `defaultNamingContext` dans le RootDSE au travers de la session saisie, seule chose dont on dispose et qui suffit à interroger l'annuaire. `grant_dcsync nnino` est alors accepté (`resultCode=0`) parce que l'écriture vient d'`Administrator` : de là, un `secretsdump.py esc17/nnino@dc01 -just-dc` déroule toute la base NTDS. **Détournement d'une session Kerberos, jusqu'à la compromission du domaine.**

:::note[Même les comptes Protected Users tombent]
C'est la conséquence la plus contre-intuitive. Le groupe **Protected Users** force ses membres en **Kerberos** et leur interdit NTLM, ce qui neutralise les relais NTLM classiques, y compris le vecteur HTTPS de l'[article précédent](/blog/esc17-https-interne/). Mais le détournement décrit ici ne relaie rien : il **chevauche la session Kerberos légitime** une fois le bind passé verbatim. Puisqu'il opère sur Kerberos exactement comme sur NTLM, **un compte membre de Protected Users, administrateur compris, reste entièrement exposé** dès lors que le channel binding n'est pas imposé. Protected Users ne protège donc pas de cette technique.
:::

La puissance du résultat est bornée par **les droits de la victime** : chevaucher la session d'un compte non privilégié donne les droits de ce compte. C'est ce que montre la session ci-dessous.

### Une console, plusieurs victimes, les droits de chacune

adecrypt agrège toutes les sessions saisies dans **une seule console multi-session** : `sessions` liste les comptes détournés, `use <id>` bascule de l'un à l'autre, et chaque opération s'exécute avec les droits **du compte actif**. Par défaut, une session est gardée **par identité**. `--keep-hijack` saisit toutes les connexions, ce qui permet ici de récupérer **deux comptes différents depuis une même machine** (`10.10.20.11`), à savoir `nnino` puis `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                 # session active = #1 (nnino, non privilégié)
grant_dcsync nnino -> resultCode=50 (insufficientAccessRights)

LDAP# use 2                              # on bascule sur la session d'Administrator
active session -> #2

LDAP# grant_dcsync nnino                 # même commande, mais AS Administrator cette fois
grant_dcsync nnino -> resultCode=0 (success)
```

La démonstration est nette : la **même** commande échoue (`resultCode=50`) sous l'identité `nnino`, puis réussit (`resultCode=0`) une fois la session active basculée sur `Administrator`. On n'exploite jamais une élévation de privilèges, on **emprunte** simplement les droits de la session que l'on chevauche. `grant_dcsync nnino` accorde à `nnino` les droits de réplication `DS-Replication-Get-Changes[-All]` sur l'objet domaine, et `secretsdump.py esc17/nnino@dc01 -just-dc` déroule alors toute la base NTDS. **Compromission du domaine, à partir d'une session Kerberos qu'on n'a ni cassée ni relayée.**

## Est-ce réaliste ? Qui parle Kerberos en LDAPS ?

Objection légitime : « d'accord, mais dans la vraie vie, qui ouvre une session Kerberos non scellée sur LDAPS ? » La réponse tient en une observation : **LDAPS est le mode d'intégration Active Directory par défaut de la quasi-totalité des applications et équipements tiers**, et son usage repose *entièrement* sur l'hypothèse que le certificat du DC est digne de confiance. C'est le fil rouge de cette série, et ESC17 brise cette hypothèse.

Rappel du périmètre : on n'intercepte **que le trafic sur 636**, les binds sur 389 (scellés) sont hors de portée. Mais le trafic 636 est loin d'être marginal. Chaque fois qu'un de ces clients ouvre une connexion LDAPS, il produit exactement le bind exploitable :

- **Applications & appliances qui authentifient leurs utilisateurs contre l'AD** : portails intranet, ticketing (GLPI, Jira), CI/CD (GitLab, Jenkins), supervision (Zabbix, Grafana), passerelles VPN et firewalls (Fortinet, Palo Alto…), NAS (Synology/QNAP), imprimantes/MFP, `vCenter`… Presque tous proposent « LDAP over SSL » et l'admin l'a coché *par souci de sécurité*. À chaque login utilisateur, l'appli fait un bind LDAPS. Souvent un **simple bind** (compte de service) → on récupère au passage le **mot de passe en clair** (Cas 1), parfois un bind Negotiate/Kerberos → on chevauche la session (Cas 3).
- **Portails de réinitialisation de mot de passe (SSPR)** : écrire `unicodePwd` **exige un canal confidentiel**, et beaucoup de portails choisissent LDAPS pour cela → binds LDAPS à la demande.
- **Connecteurs IAM / provisioning** (synchronisation d'annuaires, gestion des identités) configurés en LDAPS.
- **Outils d'audit / scanners** en mode authentifié (collecte AD) pointés en LDAPS.
- **Administration** : un simple `ldp.exe` connecté en SSL sur 636 (Bind *Negotiate*), le déclencheur GUI trivial pour reproduire en laboratoire, sans la moindre alerte de certificat puisque le cert ESC17 est réellement approuvé.

Un point clé pour l'opérateur : ces connexions sont **à la demande**, déclenchées par un login, un reset ou une synchro, et non soumises à un *polling* lent comme WSUS. On provoque le trafic en se connectant simplement au service concerné.

Une observation résume la situation : **c'est le choix de LDAPS « pour la sécurité » qui rend l'interception discrète.** Le certificat étant émis par l'autorité de certification du domaine, un client qui se contente de vérifier la chaîne de confiance ne lève en principe aucune alerte (le comportement dépend de la rigueur de validation côté client).

## Récupérer un hash NetNTLMv2 réutilisable (`--spnego-downgrade`)

Le chevauchement des Cas 2 et 3 suppose une session en clair à saisir sur place. Quand ce n'est pas l'objectif, par exemple lorsqu'on veut simplement repartir avec un **identifiant réutilisable** à casser tranquillement, ou préparer un accès pour plus tard, adecrypt offre une capacité complémentaire, et parfaitement fiable : **forcer le downgrade Kerberos → NTLM pour récupérer le hash NetNTLMv2 de la connexion.**

Un client qui parle Kerberos ne laisse, par défaut, rien à cracker : aucun secret réutilisable ne transite. Mais en réécrivant le `NegTokenInit` du bind pour n'offrir que **NTLM**, on force le client à retomber sur ce mécanisme. Il émet alors un échange NTLM complet, et son message `AUTHENTICATE` (Type 3) porte une réponse **NetNTLMv2**, un défi/réponse **crackable hors-ligne** (<a href="https://hashcat.net/hashcat/" target="_blank" rel="noopener noreferrer">hashcat</a>, mode 5600). On le récupère **au vol**, dans le tunnel TLS que l'on possède.

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

Le client voulait faire du Kerberos, mais adecrypt réécrit le premier jeton SPNEGO (`NegTokenInit`) pour ne proposer que NTLM : croyant le serveur limité à ce mécanisme, le client rejoue son authentification en NTLM. Son message `AUTHENTICATE` (Type 3) porte alors une réponse **NetNTLMv2** dérivée du hash NT du compte, capturée au vol et écrite dans `credentials.txt` au format hashcat (`utilisateur::domaine:challenge_serveur:NTProofStr:blob`), crackable hors-ligne en `hashcat -m 5600`.

À noter : ce hash est capturé **que le bind aboutisse ou non**, car le Type 3 est émis *avant* que le DC ne statue, donc même si la vérification mechListMIC finit par rejeter le bind downgradé, le NetNTLMv2 est déjà entre nos mains.

Une fois le hash cassé, on dispose du **mot de passe en clair** du compte : on peut alors s'authentifier directement, sans même remettre les pieds en MITM. Là où le Cas 3 donne une **session vivante immédiate**, `--spnego-downgrade` donne un **identifiant durable**. Les deux angles se complètent, selon qu'on veut agir tout de suite ou s'installer.

## Récapitulatif

Selon le type de bind présenté par le client sur LDAPS, l'issue diffère. Le tableau ci-dessous résume les quatre situations, dans la configuration étudiée (LDAP signing requis, channel binding absent).

| Type de bind | Ce qu'on récupère | Préconditions | Impact |
|:---|:---|:---|:---|
| **Simple bind** | le mot de passe en clair | TLS terminé (LDAPS ou StartTLS) | identifiant réutilisable, souvent un compte de service |
| **SASL NTLM** | la session authentifiée (chevauchement) | TLS terminé, post-bind non scellé, channel binding absent | actions dans l'annuaire en tant que la victime (DCSync, RBCD, Shadow Credentials, ACL…) |
| **SASL Kerberos** | la session authentifiée (chevauchement) | idem NTLM | idem NTLM |
| **NTLM forcé** (`--spnego-downgrade`) | un hash NetNTLMv2 | TLS terminé | hash crackable hors-ligne, donc mot de passe réutilisable |

Le point commun des trois premiers cas : adecrypt ne casse ni ne rejoue l'authentification, il la laisse aboutir puis exploite l'absence de scellement pour agir sur une session légitime. Le quatrième est une solution de secours lorsque le chevauchement n'est pas souhaité.

## Est-ce une vulnérabilité Kerberos ? Non. Est-ce une 0-day ? Non plus.

Soyons précis, car la nuance est le cœur du sujet.

**Ce n'est pas une faille de Kerberos.** La <a href="https://datatracker.ietf.org/doc/html/rfc4120" target="_blank" rel="noopener noreferrer">RFC 4120</a> est explicite, *« The application is free to choose whatever protection may be necessary »* : l'intégrité et le chiffrement des messages sont **optionnels** dans Kerberos. La <a href="https://datatracker.ietf.org/doc/html/rfc4752" target="_blank" rel="noopener noreferrer">RFC 4752</a> confirme que « pas de couche de sécurité » est un résultat de négociation parfaitement légal. Le défaut n'est pas dans le protocole, il est dans la façon dont le service **utilise** Kerberos, sans lier l'authentification au canal. Le principe est d'ailleurs ancien : Black Hat USA 2010, *Attacking Kerberos Deployments*, le formulait déjà (*« you want a tie between authentication and protocol, but have to build it yourself »*).

**Ce n'est pas une 0-day.** Le maillon qui rend l'attaque possible, à savoir « LDAPS MITM sans channel binding permet de prendre la main sur la session », est une **classe de faiblesse connue**, et Microsoft en livre déjà la mitigation nommée : `LdapEnforceChannelBinding=Always` (ADV190023). MSRC classerait cela *by-design / appliquez le durcissement existant*.

## Automatisation avec adecrypt

Comme pour les autres volets, adecrypt prend le certificat éligible ESC17 et pilote l'attaque de bout en bout.

```bash
# 1. Certificat serveur pour le DC, via le modèle vulnérable à ESC17
certipy req -u attacker@esc17.local -p '...' -ca 'ESC17-CA' \
    -template 'VulnServer' -dns dc01.esc17.local -out dc01

# 2. MITM LDAPS + shell de chevauchement (NTLM et 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. Console multi-session : lister les cibles, basculer, agir EN TANT QUE la victime
#    nc 127.0.0.1 11337  ->  sessions | use <id> | grant_dcsync <user> | set_rbcd | passwd ...
```

`--ldap-shell` chevauche indifféremment les binds **NTLM et Kerberos** (il confirme d'abord l'absence de scellement post-bind avant de saisir) et expose **une seule console multi-session** : `sessions` liste les comptes détournés, `use <id>` bascule, et le dédoublonnage garde **une session par identité** (plusieurs comptes distincts d'une même machine sont chacun saisis). `--keep-hijack` saisit toutes les connexions, et `--spnego-downgrade` reste l'**option hors-ligne** (capture NetNTLMv2) quand le chevauchement n'est pas possible.

## Mitigations

Une seule contre-mesure neutralise *toute* la couche 2 de cet article, NTLM comme Kerberos :

- **Imposer le channel binding LDAP** : `LdapEnforceChannelBinding = 2` (Always). C'est la mitigation décisive, elle lie l'authentification au certificat réel du DC, et notre certificat ESC17 échoue immédiatement. À déployer **avant** de se soucier du reste.
- **Corriger le modèle ESC17 à la source** : retirer le flag *« supply in the request »*, restreindre les droits d'enrôlement, ou exiger l'approbation d'un gestionnaire, sans se fier à la seule valeur d'EKU.
- **Épingler le certificat** côté clients LDAPS gérés centralement, quand c'est faisable.
- **Surveiller** l'émission de certificats *Server Authentication* portant un SAN arbitraire, et l'apparition de binds LDAPS provenant d'hôtes inattendus.

Et, pour le simple bind (Cas 1), rappeler que **le channel binding ne le protège pas** : là, seuls comptent la correction du modèle ESC17 et l'épinglage.
