blog.safetech.red
Microsoft Active Directory ADWS Red Team Blue Team Rust

ADWS : Et si on changeait de porte pour contacter l'AD

ADWS (Active Directory Web Services), c'est la porte SOAP présente par défaut sur tous les DC, qui proxifie vers le LDAP local. Ce blog post existe pour présenter le protocol et présenter pourquoi j'ai écrit une lib ADWS en Rust, un shell adshell-rs qui parle LDAP(S) ET ADWS, et comment j'ai porté la collecte de RustHound-CE sur ADWS sans toucher au parsing. Il y a aussi une partie Blue Team avec comment détecter un collecteur de données AD.

03 Nov 2026 16 min – vues Quentin Texier (g0h4n)
Sommaire

English version here.

TL;DR

ADWS, pour Active Directory Web Services, c'est un service SOAP présent par défaut sur tous les contrôleurs de domaine depuis Windows Server 2008 R2. Il écoute sur le port 9389 et, dans les faits, il proxifie les requêtes vers le LDAP local du DC. Autrement dit, on peut lire et écrire dans l'annuaire sans jamais passer par LDAP "classique" (389/636) sur le réseau, ce qui le rend intéressant face aux détections. J'avais besoin d'une brique ADWS en Rust pour d'autres projets, alors je propose une bibliothèque réutilisable, adws, ainsi qu'un petit toolkit, adshell-rs, un shell d'attaque AD qui parle aussi bien LDAP(S) qu'ADWS (SOAP). Enfin, RustHound-CE peut maintenant communiquer via ADWS en version 2.5.40, on choisit --transport adws et on dump via ADWS sans rien changer au code de parsing, seule la méthode de collecte change. Tout ça repose sur des travaux de recherche et des outils déjà existants, que je crédite en fin d'article. Cet article explique ce qu'est ADWS, ce qui est intéressant et ce qui coince, puis fait le tour côté Red et côté Blue avec une partie détection.

C'est quoi ADWS ?

Quand on pense "interroger l'Active Directory", on pense LDAP, le port 389 (ou 636 en LDAPS). C'est le réflexe, et c'est ce que font la grande majorité des outils (SharpHound, BloodHound.py, RustHound-CE côté LDAP, etc.).

Mais ce n'est pas la seule porte. Depuis Windows Server 2008 R2, chaque contrôleur de domaine expose un service nommé Active Directory Web Services (ADWS), qui écoute sur le port TCP 9389. C'est lui qui fait tourner, côté réseau, le module PowerShell Active Directory (Get-ADUser, Get-ADGroup, Get-ADComputer...) et l'Active Directory Administrative Center (ADAC). Donc, si vous avez déjà lancé un Get-ADUser depuis un poste d'admin, vous avez parlé ADWS sans le savoir.

Le point important, et c'est tout l'intérêt du sujet, ADWS est présent par défaut sur les DC, et il proxifie vers le LDAP local du contrôleur. Une lecture faite en ADWS est donc, du point de vue des données, une lecture LDAP, mais encapsulée dans tout autre chose sur le fil.

Schéma : client => ADWS (9389, SOAP/NNS) => LDAP local du DC (389)

SOAP, mais pas que

ADWS, ce n'est pas "juste du SOAP sur HTTP" étant donné qu'aucun trafic HTTP n’est impliqué. C'est une pile .NET complète, que Microsoft documente dans ses Open Specifications. En partant du bas :

  • MC-NMF (.NET Message Framing) : le cadrage des messages sur TCP.
  • MS-NNS (.NET NegotiateStream) : la couche de sécurité, handshake Negotiate puis signature et chiffrement des messages applicatifs. Tout le trafic applicatif est donc sign & seal.
  • NBFS / NBFSE (MC-NBFX / MC-NBFSE) : l'encodage XML binaire .NET, avec son dictionnaire statique. Le SOAP n'est pas transporté en texte, mais en binaire.
  • Et par-dessus, les opérations WS-* avec MS-WSPELD : WS-Enumeration pour les lectures (Enumerate / Pull), WS-Transfer pour les écritures (Put / Delete / Create), le modèle de données MS-ADDM, et les actions custom MS-ADCAP (l'équivalent des cmdlets Get-ADGroupMember, Set-ADAccountPassword, etc.).

Pourquoi une lib ADWS en Rust

Crates.io Version

Le sujet ADWS m'intéresse et j'avais besoin d'une brique ADWS pour plusieurs projets en Rust, pas seulement pour un collecteur. Plutôt que de l'enfouir dans un outil, j'ai choisi de la sortir comme bibliothèque Rust réutilisable et d'en faire profiter la communauté, exactement dans l'esprit de ce que RustHound-CE propose déjà comme lib.

Ça a donné deux dépôts :

  • adws : la bibliothèque. Elle monte toute la pile (NMF, NNS, NBFSE) et expose une API haut niveau, connect, search / search_streaming, et le CRUD complet (modify, delete, create). Le choix clé, c'est qu'elle rend les objets sous forme de SearchEntry compatibles avec la bibliothèque Rust ldap3. Autrement dit, un adws::SearchEntry se convertit en ldap3::SearchEntry gratuitement juste avec un .into() pour ceux qui sont initier à Rust.
  • adshell-rs : le toolkit. Un shell d'attaque AD interactif et des actions (RBCD, Shadow Credentials, DACL, reset de mot de passe, comptes machines...) qui tournent au choix sur LDAP(S) ou sur ADWS (SOAP), via un simple -t adws ou -t ldap.

Ce point de compatibilité ldap3 n'est pas cosmétique. C'est lui qui a rendu la suite possible.

La portation sur RustHound-CE (v2.5.40)

C'est sans doute le résultat le plus parlant. À partir de la version 2.5.40, RustHound-CE sait dump via ADWS. On bascule simplement le protocole de communication qui est LDAP par défaut avec l'argument --transport adws ou en version raccourcis -t adws :

# Collecte classique en LDAP (inchangée)
rusthound-ce -d essos.local -u daenerys.targaryen -p 'BurnThemAll!' -z

# La même collecte, mais via ADWS (SOAP, tcp/9389)
rusthound-ce -d essos.local -u daenerys.targaryen -p 'BurnThemAll!' -z -t adws

Et le plus intéressant dans cette intégration du protocole ADWS, c'est que je n'ai rien eu à changer au code de parsing de RustHound-CE. Tout le objects/* et donc toute la logique qui transforme un objet de l'annuaire en JSON pour être ingéré par BloodHound-CE, tout ça est resté identique. Seule la méthode de collecte change.

Comment ? RustHound-CE fait déjà passer toutes ses lectures par une poignée de fonctions (ldap_auth, get_all_naming_contexts, et enfin la collecte streamée). J'ai introduit un petit trait DirectoryTransport avec deux implémentations, une LDAP (l'existante) et une ADWS (qui s'appuie sur la lib adws). Comme la lib rend des entrées au format SearchEntry de ldap3, tout ce qui est au-dessus du transport reçoit exactement la même chose qu'avant. Le reste est géré de la même manière.

Pour faire simple, j'ai juste ajouté une deuxième tuyauterie d'entrée, le reste de l'usine n'a pas bougé.

Attention, la seule contrepartie, c'est que la brique d'authentification ADWS ne couvre pour l'instant que NTLM (mot de passe et pass-the-hash). Kerberos et pass-the-certificate (PKINIT) pour ADWS sont dans la roadmap, mais pas encore câblés. En LDAP, RustHound-CE garde évidemment toutes ses méthodes d'authentification.

Points intéressants et limites

Autant être honnête, ADWS n'est pas une baguette magique permettant de collecter l'ensemble des données de l'annuaire Active Directory sans être détecté. Voici ce qui est réellement intéressant, et ce qui coince.

Ce qui est intéressant

  • Présent par défaut : Rien à activer, rien à déployer côté cible. Le service tourne sur tous les DC.
  • Hors des radars LDAP : Beaucoup de détections et de monitoring se concentrent sur le trafic réseau LDAP (389/636) et donc sur les requêtes LDAP. En passant par ADWS, on ne génère pas de trafic sur ces ports. On ressort ailleurs, sur 9389, en SOAP binaire chiffré.
  • Les mêmes données que LDAP : Puisqu'ADWS proxifie vers le LDAP local, on récupère le même annuaire, les mêmes attributs, y compris le nTSecurityDescriptor via le contrôle SD-flags. D'où la parité de collecte avec RustHound-CE.
  • Légitime par nature : Le trafic ADWS ressemble à... du module PowerShell AD et de l'ADAC. C'est exactement ce qu'utilisent les admins.

Ce qui coince

  • La pile est lourde à implémenter : NMF, NNS (sign & seal), NBFSE, WS-*, il n'y a pas de raccourci, et c'est pour ça qu'il existe peu d'outils ADWS comparé à LDAP. Une très bonne bibliothèque existe en Golang et a été développée par @MacmodSec, elle se nomme go-adws, et je me suis beaucoup inspiré de son travail pour produire la bibliothèque Rust.
  • La récupération par plage n'existe pas : En LDAP, un attribut multivalué est plafonné à MaxValRange (1500 par défaut) et le client complète le reste avec la ranged retrieval (member;range=1500-*). C'est d'ailleurs une issue qui avait été remontée par @Spyr0-sec sur RustHound-CE (#76) et corrigée par la suite sur le protocole LDAP. ADWS refuse cette syntaxe dans une sélection. Pour les gros groupes (plus de 1500 membres), il faut passer par l'action custom GetADGroupMember (MS-ADCAP), l'équivalent SOAP du cmdlet Get-ADGroupMember, qui résout l'appartenance complète côté serveur. C'est ce que fait la lib afin de correctement collecter les membres d'un groupe dont la valeur est supérieur à 1500.
  • Ce n'est pas "invisible" : Le DC, lui, voit tout. ADWS proxifie vers le LDAP local, donc l'audit d'accès aux objets côté annuaire continue de se déclencher. On évite le trafic LDAP réseau, pas l'instrumentation côté DC (j'y reviens dans la partie Blue du blog-post).
  • L'auth est encore limitée : dans la lib adws Rust seulement (NTLM / PtH) sont implémentés, il restera Kerberos et PKINIT avec Pass-The-Cert à ajouter.

Côté Red Team

Les démonstrations ci-dessous sont réalisées sur le lab GOAD créé par @M4yFly.

L'intérêt opérationnel est simple, faire ce qu'on fait déjà en LDAP, mais par une autre porte, moins surveillée, et activée par défaut. Et surtout j'ai envie de dire d'une manière légitime d'administration tel que réaliserait un administrateur avec le module PowerShell Active Directory.

Cartographier l'AD via ADWS

Pour la reconnaissance BloodHound, on bascule RustHound-CE sur ADWS. La sortie .zip est strictement la même qu'en LDAP, prête pour BloodHound-CE :

# Dump via ADWS (SOAP) au lieu de LDAP, pour éviter le trafic 389/636
rusthound-ce -d essos.local -u daenerys.targaryen -p 'BurnThemAll!' -z -t adws

RustHound-CE exemple avec --transport adws

Côté graphe et côté analyse, rien ne change pour l'opérateur, ce sont les mêmes objets, les mêmes noeuds et donc le même import dans BloodHound-CE. Seul le fil change. Donc vous pouvez importer les fichiers JSON ou le ZIP et commencer votre analyse !

Agir sur l'AD via ADWS

Pour les actions ciblées de lecture et de modification, adshell-rs donne un shell interactif qui parle les deux transports LDAP(S) et ADWS. On peut ainsi choisir ADWS avec la même option que celle présentée pour RustHound-CE soit -t adws :

# Shell interactif via ADWS (SOAP)
./adshell-rs -d essos.local -u daenerys.targaryen -p 'BurnThemAll!' -t adws

demo adshell-rs

De là, on enchaîne les attaques AD habituelles (comptes machines, RBCD, Shadow Credentials, droits DCSync, reset de mot de passe...), mais via le protocol ADWS et non pas LDAP. L'écriture passe par WS-Transfer (Put / Create / Delete), avec les mêmes valeurs binaires que l'on écrirait en LDAP (un descripteur de sécurité RBCD, un blob msDS-KeyCredentialLink, un unicodePwd en UTF-16...).

Le même pass-the-hash qui marche en LDAP marche en ADWS, la brique NNS négocie en NTLM et scelle la session, exactement comme le ferait le module PowerShell AD avec les identifiants d'un admin. Merci à @Devolutions pour leur bibliothèque Rust sspi-rs qui permet de facilement utiliser l'authentification NTLM.

Côté Blue Team : détection, SOC et SIEM

C'est la partie la plus importante, parce que "ADWS évite LDAP" ne veut PAS dire que "ADWS est indétectable". Ça veut dire que la détection ne doit pas se faire au même endroit.

1. Ne cherchez pas à bloquer ADWS. Le service est légitime et utilisé par vos propres admins (module PowerShell AD, ADAC). Couper le port 9389, c'est casser l'administration et donc ne plus pouvoir utiliser les commandes du module PowerShell Active Directory. L'objectif est de surveiller, pas d'interdire.

2. Surveillez le port 9389, pas seulement 389/636. Si votre monitoring réseau et vos règles se concentrent sur LDAP, vous avez un angle mort. Une collecte type RustHound-CE via ADWS, c'est une rafale de Pull WS-Enumeration vers un DC, sur 9389, depuis une machine qui n'a aucune raison de faire de l'administration AD. Le contenu est chiffré (NNS), mais la connexion et le volume, eux, se voient. Une source inhabituelle qui ouvre une session ADWS et aspire l'annuaire, c'est un signal. Voici un exemple de nombre de paquets transitant avec une collecte d'un annuaire Active Directory avec plus de 3000 objets.

# Capture du trafic ADWS
sudo tcpdump -i vboxnet0 -nn 'host 192.168.56.12 and tcp port 9389' -w /tmp/adws-collect.pcap

# Collecte via ADWS
rusthound-ce -d essos.local -u daenerys.targaryen -p 'BurnThemAll!' -z -t adws -c SOAPOnly

# Nombre de paquets ADWS
tcpdump -r /tmp/adws-collect.pcap -nn | wc -l
    # Total: 1233
# Capture du trafic LDAP
sudo tcpdump -i vboxnet0 -nn 'host 192.168.56.12 and tcp port 389' -w /tmp/ldap-collect.pcap

# Collecte via LDAP (défaut)
rusthound-ce -d essos.local -u daenerys.targaryen -p 'BurnThemAll!' -z -c LdapOnly

# Nombre de paquets LDAP
tcpdump -r /tmp/ldap-collect.pcap -nn | wc -l
    # Total: 593

Conclusion ? Ce n’est pas plus discret en volume, au contraire. Sur l'annuaire de test précédent d’environ 3000 objets, la même collecte RustHound-CE génère ~1233 paquets en ADWS contre ~593 en LDAP, soit presque le double. L’overhead de la pile SOAP/NMF/NNS et le fractionnement en Pull WS-Enumeration représente beucoup de requête. L’intérêt d’ADWS n’est donc pas de produire moins de requête, mais de le faire ailleurs (9389) et sous une apparence légitime (du trafic de module PowerShell AD).

3. L'audit côté DC reste votre meilleur allié. C'est le point que les attaquants oublient, ADWS proxifie vers le LDAP local du DC. Donc l'instrumentation côté annuaire continue de fonctionner. En particulier, l'audit d'accès aux objets (event 4662, "An operation was performed on an object") se déclenche sur les lectures d'attributs sensibles, que la requête vienne de LDAP ou d'ADWS, parce qu'au niveau du DC c'est le même accès LDAP interne. Posez des SACL (liste de contrôle d’accès système) sur vos objets et attributs sensibles (secrets, descripteurs, ms-Mcs-AdmPwd...) et vous attrapez la lecture indépendamment du transport. Je vous donne un exemple de quick-win dans la partie suivante du blog-post.

4. Corrélez avec le comportement du compte et de la source. Un compte lambda qui, depuis un poste non-admin, ouvre une session ADWS et énumère des milliers d'objets, c'est anormal. La watchlist utile, ce sont les machines qui n'administrent jamais l'AD mais qui se mettent à parler 9389.

Comme pour tout, testez chez vous. Montez un DC, lancez une collecte via ADWS avec RustHound-CE, et regardez ce que vous voyez (ou pas) dans votre SIEM, sur le réseau et dans les journaux du DC. C'est le meilleur moyen de calibrer vos règles avant de les affronter en vrai.

Détecter simplement qu’un collecteur AD est passé sur votre annuaire

J'aurai tendance à recommander la mise en place d'un groupe honeypot avec une SACL sur son seul attribut member. Un faux groupe "intéressant", sans membre ni privilège réel, dont la lecture de la composition déclenche un event 4662.

Comme aucun usage légitime ne lit ce groupe, tout accès est suspect, et n’importe quel collecteur de données qui énumère l’annuaire AD, que ce soit en LDAP ou en ADWS tombera dessus. Le tout en gardant un signal propre, puisque l’audit est scopé à un seul attribut d’un seul objet.

Voici les commandes pour mettre en place cette protection :

# 1) Activer l'audit d'accès à l'annuaire (sur le DC, une seule fois)
auditpol /set /subcategory:"Directory Service Access" /success:enable /failure:enable

# 2) Créer le groupe honeypot
Import-Module ActiveDirectory
New-ADGroup -Name 'Tier0-Honeypot' -SamAccountName 'Tier0-Honeypot' `
    -GroupScope Global -GroupCategory Security `
    -Path 'CN=Users,DC=essos,DC=local' `
    -Description 'Demo honeypot from g0h4n blog post'

# 3) Préparer la SACL qui permet d'auditer la LECTURE de l'attribut "member" par tout le monde sur notre groupe honeypot 
$dn   = (Get-ADGroup 'Tier0-Honeypot').DistinguishedName
$path = "AD:\$dn"
$acl  = Get-Acl -Path $path -Audit
$everyone   = New-Object System.Security.Principal.SecurityIdentifier 'S-1-1-0'
$memberGuid = [GUID]'bf9679c0-0de6-11d0-a285-00aa003049e2'
$rule = New-Object System.DirectoryServices.ActiveDirectoryAuditRule(
    $everyone,
    [System.DirectoryServices.ActiveDirectoryRights]::ReadProperty,
    [System.Security.AccessControl.AuditFlags]::Success,
    $memberGuid,
    [System.DirectoryServices.ActiveDirectorySecurityInheritance]::None
)

# 4) Appliquer la SACL
$acl.AddAuditRule($rule)
Set-Acl -Path $path -AclObject $acl

Nous pouvons ainsi tester que notre objet honeypot déclenche bien l'event 4662 dès que l'objet est lu :

# Collecte de l'annuaire Active Directory
./rusthound-ce -d essos.local -u 'daenerys.targaryen' -H 34534854d33b398b66684072224bb47a -z -t adws

Event 4662 déclenché pour notre groupe honeypot

Donc même si ADWS déplace la collecte hors des radars LDAP classiques, un simple groupe honeypot bien audité remet le défenseur dans la boucle, quel que soit le transport utilisé par l’attaquant.

MITRE ATT&CK

Pour raccrocher à la matrice, ADWS est avant tout un canal de découverte et de manipulation de l'annuaire. Les techniques ne changent pas, c'est le vecteur qui est alternatif.

TactiqueIDTechniqueDans ce scénario
DiscoveryT1087.002Account Discovery: Domain AccountÉnumération des comptes du domaine via ADWS (WS-Enumeration) au lieu de LDAP
DiscoveryT1069.002Permission Groups Discovery: Domain GroupsCollecte des groupes et de leurs membres, y compris via GetADGroupMember
DiscoveryT1482Domain Trust DiscoveryLecture des relations d'approbation dans l'annuaire
Defense EvasionT1562.001Impair DefensesPasser par ADWS (9389) pour éviter le monitoring centré LDAP (389/636)

Pour aller plus loin

Deux ressources à lire absolument sur le sujet :

Remerciements

Rien de tout ça ne part de zéro. ADWS comme technique offensive a été ouvert et documenté par plusieurs travaux de recherche et plusieurs outils, qui m'ont servi de références de protocole et de fil pour porter la pile en Rust. Un grand merci à eux :

Et bien sûr, tout ça s'appuie sur les Open Specifications Microsoft (MC-NMF, MS-NNS, MC-NBFX/MC-NBFSE, MS-ADDM, MS-ADCAP) qui documentent le protocole.

Crédits et liens

Cet article vous a plu ? Laissez un petit like, ça m'aide à orienter mes prochains articles vers ce qui vous intéresse le plus.

cd ..