Aller au contenu

Accueil / Sécurité et identité

DCSync: lire la réplication MS-DRSR dans Wireshark

DCSync vu de l'intérieur: droits de réplication, appel DRSUAPI GetNCChanges déchiffré dans Wireshark et événement 4662 sur le contrôleur de domaine.

Rémi Vernier
11 novembre 2026 · 8 min de lecture

Répliquer, un simple droit?

Cet été, l’équipe de cirosec a publié la première partie d’une étude sur le contournement des détections de Microsoft Defender for Identity en 2026. L’alerte DCSync y figure: sa logique repose sur la résolution NNR (Network Name Resolution), qui permet au capteur de savoir si la machine source est un contrôleur de domaine, et des réponses NNR falsifiées suffisent à la tromper. En avril, Trellix rappelait de son côté dans un billet sur la détection réseau qu’une requête DsGetNCChanges est une fonction tout à fait légitime du protocole, et qu’elle ne devient suspecte que lorsqu’elle part d’une machine qui n’est pas un contrôleur de domaine.

Si l’alerte d’un produit peut être contournée, il reste le protocole lui-même et le journal de sécurité du contrôleur de domaine, et c’est là que nous allons regarder.

Les contrôleurs de domaine se répliquent entre eux grâce au protocole [MS-DRSR], porté par l’interface RPC drsuapi (UUID e3514235-4b06-11d1-ab04-00c04fc2dcd2, version 4.0). Le contrôleur de destination tire les modifications du contrôleur source en appelant la méthode IDL_DRSGetNCChanges (opnum 3). Rien n’oblige l’appelant à être un contrôleur de domaine: n’importe quel client qui parle DRSUAPI et dispose des bons droits peut demander la réplication d’un objet, attributs secrets compris. C’est cela, DCSync, référencé par MITRE sous T1003.006, et utilisé par des groupes comme LAPSUS$, Storm-0501 ou Mustang Panda. L’attaquant n’exécute aucun code sur le contrôleur, ne touche ni à NTDS.dit ni à LSASS: il demande poliment.

Les droits en question sont des droits de contrôle d’accès étendus, posés sur la tête du contexte de nommage, ici l’objet DC=test,DC=local. Le Threat Hunter Playbook en donne la liste avec leurs GUID:

  • DS-Replication-Get-Changes (« Replicating Directory Changes »): 1131f6aa-9c07-11d1-f79f-00c04fc2dcd2
  • DS-Replication-Get-Changes-All (« Replicating Directory Changes All »): 1131f6ad-9c07-11d1-f79f-00c04fc2dcd2
  • DS-Replication-Get-Changes-In-Filtered-Set: 89e95b76-444d-4c62-991a-0facbeda640c

Le premier suffit pour répliquer les attributs ordinaires, et de nombreux outils de synchronisation en ont besoin. Le second autorise la réplication des données secrètes du domaine: unicodePwd, ntPwdHistory, supplementalCredentials (les clés Kerberos AES), etc… C’est la combinaison des deux qui rend DCSync possible. Par défaut, seuls les groupes Administrateurs, Contrôleurs de domaine et Contrôleurs de domaine d’entreprise les détiennent, et Admins du domaine en hérite au travers d’Administrateurs. Il faut y ajouter les comptes à qui un projet les a donnés, à commencer par le compte de connecteur MSOL_ d’Entra Connect, qui en a besoin pour Password Hash Sync comme nous l’avions vu dans l’article sur Password Hash Sync et Seamless SSO.

Attention: un compte qui a GenericAll ou WriteDacl sur la racine du domaine, ou une entrée « Tous les droits étendus » (ObjectType vide), peut s’accorder lui-même ces droits. Il faut donc aussi le compter parmi les comptes capables de DCSync.

Pour dresser l’inventaire, ce petit script lit l’ACL de la racine du domaine et ne garde que les entrées concernées:

shell
Import-Module ActiveDirectory
$droits = @{
  '1131f6aa-9c07-11d1-f79f-00c04fc2dcd2' = 'DS-Replication-Get-Changes'
  '1131f6ad-9c07-11d1-f79f-00c04fc2dcd2' = 'DS-Replication-Get-Changes-All'
  '89e95b76-444d-4c62-991a-0facbeda640c' = 'DS-Replication-Get-Changes-In-Filtered-Set'
  '00000000-0000-0000-0000-000000000000' = 'Tous les droits etendus'
}
(Get-Acl "AD:\DC=test,DC=local").Access |
  Where-Object { $droits.ContainsKey($_.ObjectType.ToString()) -and
                 $_.ActiveDirectoryRights -match 'ExtendedRight|GenericAll' } |
  Select-Object IdentityReference, AccessControlType,
                @{n='Droit';e={$droits[$_.ObjectType.ToString()]}}

Présentation de la maquette

Nom du domaine: test.local (TESTLOCAL)

Contrôleurs de domaine: dc01.test.local (192.168.0.101) et dc02.test.local (192.168.0.102)

Serveur membre: srv01.test.local (192.168.0.110)

Compte de test: svc.audit@test.local, objet ciblé: utilisateur.test

Pour reproduire l’appel sans compte d’administration, nous déléguons les deux droits au compte svc.audit, puis nous activons l’audit des accès au service d’annuaire sur les contrôleurs (une GPO sur l’OU Domain Controllers est préférable à auditpol en production):

code
dsacls "DC=test,DC=local" /G "TESTLOCAL\svc.audit:CA;Replicating Directory Changes"
dsacls "DC=test,DC=local" /G "TESTLOCAL\svc.audit:CA;Replicating Directory Changes All"

auditpol /set /subcategory:"Directory Service Access" /success:enable

La capture se fait sur dc01, avec Wireshark ou nativement avec netsh trace comme décrit dans l’article Outils d’analyse réseau. Pour déchiffrer le trafic de svc.audit, nous générons un keytab avec ktpass, comme dans l’article sur les entêtes WWW-Authenticate:

shell
ktpass /out svc.audit.keytab /princ svc.audit@TEST.LOCAL /pass * /ptype KRB5_NT_PRINCIPAL /crypto All

Attention, sans /mapuser: avec cette option, ktpass modifie le compte dans l’annuaire. Ici nous voulons seulement calculer les clés à partir du mot de passe.

GetNCChanges sur le réseau

Commençons par une réplication tout à fait normale. Sur dc01, nous forçons une réplication entrante depuis dc02:

code
repadmin /replicate dc01.test.local dc02.test.local DC=test,DC=local

Avec le filtre d’affichage « drsuapi », la séquence est toujours la même: résolution du port dynamique via l’Endpoint Mapper (tcp/135), bind sur l’interface DRSUAPI, DsBind (opnum 0), puis DsGetNCChanges (opnum 3). Le contenu, lui, n’est pas lisible:

code
DCE/RPC Request, Fragment: Single, FragLen: 1172, Call: 3, Ctx: 0
    Opnum: 3
    Auth Info: SPNEGO, Packet privacy, AuthContextId(0)
        Auth type: SPNEGO (9)
        Auth level: Packet privacy (6)
    Encrypted stub data: 8f3a61c2e07d...
DRSUAPI
    Operation: DsGetNCChanges (3)

La réplication utilise le niveau d’authentification Packet privacy (6): seul le stub est chiffré. L’en-tête DCE/RPC, avec l’opnum, reste en clair, de même que les adresses IP.

-> Sans aucune clé, un capteur réseau sait donc qui appelle GetNCChanges et sur quel contrôleur. C’est sur ce signal que s’appuient les sondes NDR.

Passons à srv01. Après un « klist purge » pour que la capture contienne l’AS-REQ, nous lançons sous l’identité de svc.audit un outil d’audit de mots de passe qui implémente le client DRSUAPI (le module DSInternals de Michael Grafnetter, conçu pour l’audit de la qualité des mots de passe), en ne demandant que l’objet utilisateur.test. Le schéma ci dessous reprend les échanges observés côté dc01, avec en bas la réplication légitime de dc02 pour comparaison.

srv01 svc.audit dc01 KDC + DRSUAPI dc02 AS-REQ / TGS-REQ (Kerberos, tcp/88) AS-REP / TGS-REP EPM tcp/135: Map DRSUAPI port dynamique 49xxx Bind e3514235-… (Packet privacy) DsBind (opnum 0) DsCrackNames (opnum 12) DsGetNCChanges (opnum 3) EXOP_REPL_OBJ unicodePwd, supplementalCredentials… Journal Sécurité dc01 4662 Control Access opnum 3 (compte dc02$)
Appel DRSUAPI depuis srv01 et réplication légitime depuis dc02, vus depuis dc01

Dans Wireshark, « Edit / Preferences / Protocols / KRB5 », nous cochons « Try to decrypt Kerberos blobs » et indiquons svc.audit.keytab. La clé de svc.audit ouvre l’AS-REP, Wireshark y apprend la clé de session du TGT, puis celle du ticket de service dans le TGS-REP, et enfin la clé qui protège le contexte RPC. Le stub de la requête devient lisible (extrait simplifié, les libellés varient un peu selon la version de Wireshark):

code
DRSUAPI, DsGetNCChanges
    Operation: DsGetNCChanges (3)
    bind_handle: 0000000057c1e2a4...
    level: 8
    DsGetNCChangesRequest8
        destination_dsa_guid: 9f2c4b1e-...
        naming_context
            dn: CN=utilisateur.test,CN=Users,DC=test,DC=local
        extended_op: DRSUAPI_EXOP_REPL_OBJ (6)

La différence avec la réplication de dc02 saute aux yeux: un contrôleur de domaine demande les changements d’un contexte de nommage entier à partir de son vecteur de mise à jour, alors qu’ici la requête vise un seul objet avec l’opération étendue EXOP_REPL_OBJ.

Dans la réponse, nous retrouvons l’objet avec ses attributs identifiés par leur ATTRTYP: 0x0009005A (unicodePwd), 0x0009005E (ntPwdHistory), 0x0009007D (supplementalCredentials). Leurs valeurs restent opaques, car MS-DRSR chiffre une seconde fois les attributs secrets avec une clé dérivée de la clé de session RPC. Pour une analyse après incident, la capture suffit pourtant à savoir quels comptes ont été répliqués et quels secrets sont partis.

Attention: le déchiffrement n’est pas toujours complet. Le ticket #19699 du projet Wireshark décrit des paquets DRSUAPI qui restent chiffrés alors que les échanges Kerberos le sont. Pour aller plus loin, les supports de Pass the SALT 2023 sur le déchiffrement Kerberos et NTLM dans Wireshark présentent très bien la méthode, DRSUAPI compris. Le keytab contient l’équivalent du mot de passe de svc.audit: il se supprime dès la fin de l’analyse.

Ce que voit le défenseur

Sur dc01, chaque appel produit un événement 4662 dans le journal de sécurité:

code
An operation was performed on an object.
Subject:
    Security ID:     TESTLOCAL\svc.audit
    Account Name:    svc.audit
    Logon ID:        0x5A3F21
Object:
    Object Server:   DS
    Object Type:     %{19195a5b-6da0-11d0-afd3-00c04fd930c9}
    Object Name:     %{4c1f6d3a-...}
Operation:
    Accesses:        Control Access
    Access Mask:     0x100
    Properties:      Control Access
                     {1131f6ad-9c07-11d1-f79f-00c04fc2dcd2}
                     {19195a5b-6da0-11d0-afd3-00c04fd930c9}

Object Type est la classe domainDNS, Object Name le GUID de l’objet domaine (pas son DN), le masque 0x100 indique un contrôle d’accès étendu et Properties contient le GUID du droit vérifié. Cet événement n’apparaît que si la sous-catégorie « Audit Directory Service Access » est activée en succès, ce qui n’est pas le cas par défaut, et si la SACL de la racine du domaine contient une entrée d’audit qui couvre ces droits. Vérifiez cette SACL sur votre propre domaine plutôt que de la supposer.

La réplication normale de dc02 produit le même événement, sous le compte dc02$. La tentation est d’exclure tous les comptes qui finissent par « $ », mais cirosec rappelle qu’un attaquant peut opérer avec l’identité d’un compte machine de contrôleur de domaine, par exemple après un relais vers AD CS (ESC8). Je préfère exclure uniquement les comptes des contrôleurs connus, puis rapprocher le Logon ID de l’événement 4624 correspondant (champ TargetLogonId, type 3) pour obtenir l’adresse IP source:

code
$droits = '1131f6aa-9c07-11d1-f79f-00c04fc2dcd2','1131f6ad-9c07-11d1-f79f-00c04fc2dcd2','89e95b76-444d-4c62-991a-0facbeda640c'
$dcs = (Get-ADDomainController -Filter *).Name | ForEach-Object { "$_`$" }
Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4662} | ForEach-Object {
    $d = @{}; ([xml]$_.ToXml()).Event.EventData.Data | ForEach-Object { $d[$_.Name] = $_.'#text' }
    [pscustomobject]@{ Heure=$_.TimeCreated; Compte=$d.SubjectUserName; LogonId=$d.SubjectLogonId; Props=$d.Properties }
} | Where-Object { $p = $_.Props; ($droits | Where-Object { $p -match $_ }) -and ($dcs -notcontains $_.Compte) }

Côté réseau, le filtre « drsuapi && dcerpc.opnum == 3 &&!(ip.src in {192.168.0.101 192.168.0.102}) » isole les appels GetNCChanges qui ne viennent pas d’un contrôleur. Sur la maquette, il ne reste que srv01. En production il restera aussi le serveur Entra Connect: c’est une exception à documenter.

Ces deux signaux comptent parce que, vu de l’annuaire, un DCSync n’a rien d’une attaque. Le scénario type commence par un poste compromis, se poursuit par une élévation vers l’un des comptes inventoriés plus haut (souvent le compte MSOL_ sur un serveur Entra Connect moins protégé que les contrôleurs, ou un compte qui a WriteDacl sur la racine), puis se termine par une seule requête qui rapporte la clé de krbtgt. À partir de là, l’attaquant forge des tickets pour n’importe quel compte, réutilise les hashs NT de tous les utilisateurs, récupère les clés des comptes d’approbation pour passer dans les forêts approuvées et la clé du compte AZUREADSSOACC$ pour rejouer Seamless SSO vers Entra ID. Il n’y a rien à révoquer côté session: il faut réinitialiser deux fois le mot de passe de krbtgt en laissant la réplication se faire entre les deux, puis ceux des comptes de service, d’approbation et d’AZUREADSSOACC$.

Attention, un attaquant qui veut revenir plus tard ajoute souvent une ACE de réplication à un compte discret. La sous-catégorie « Audit Directory Service Changes » produit alors un événement 5136 sur la modification de nTSecurityDescriptor de la racine du domaine, et le script d’inventaire de la première partie, lancé régulièrement, le fera apparaître.

Pour le durcissement, il faut réduire la liste des détenteurs au strict nécessaire, traiter le serveur Entra Connect comme un contrôleur de domaine (tier 0) et, si votre parc le permet, poser un filtre RPC sur l’interface DRSUAPI et l’opnum 3 qui n’autorise que les contrôleurs et le serveur Entra Connect, comme le détaille NVISO. Sur la maquette, il ne reste plus qu’à retirer les droits de svc.audit:

code
dsacls "DC=test,DC=local" /R "TESTLOCAL\svc.audit"
Rémi Vernier

Rémi Vernier

Responsable des opérations cybersécurité dans un grand groupe international. Identité, Active Directory, Entra ID, cloud hybride, et désormais SAP, IA et modèles en local.

Tous les articles → LinkedIn →

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *