Note : Cet article est ma tentative de mettre sur papier ma compréhension des trusts Active Directory. Je ne suis pas Microsoft, je ne suis pas un expert certifié sur le sujet. Il peut y avoir des erreurs ou des incompréhensions. Prenez ça comme un point de départ pour creuser, pas comme une vérité absolue.
Disclaimer : Les techniques mentionnées dans cette série sont présentées à des fins éducatives et d’audit légal uniquement.
Les trusts Active Directory, je trouve qu’on n’en parle pas assez. La documentation Microsoft existe, mais elle est dispersée, et semble parfois contradictoire. Qui plus est, elle ne met pas en avant les détails qui intéressent un pentesteur ou quelqu’un qui veut vraiment comprendre ce qu’il se passe.
Ce premier article pose les bases : qu’est-ce qu’un trust concrètement au niveau de la base de données AD, comment les clés inter-domaines sont créées et stockées, ce que sont les trust accounts, et quels attributs sont intéressants à connaître. Je consacrerai d’autres articles à l’exploitation concrète de ces trusts.
Pour illustrer, je m’appuie à la fois sur un SI fictif avec des noms communs utilisés par Microsoft, et sur un petit lab. La forêt serval.int est reliée à trois domaines : son domaine enfant dev.serval.int (trust intra-forêt), la forêt technovate.lan (forest trust), et external.lan via un external trust entrant (inbound). Un simple --dc-list donne déjà la cartographie des trusts et de leurs types :

Le Trusted Domain Object
Quand on établit un trust entre deux domaines, Active Directory crée un objet dans l’annuaire pour le représenter. Cet objet s’appelle un TDO (Trusted Domain Object), et c’est lui qui contient tout ce qui définit le trust : sa direction, ses propriétés, les clés cryptographiques associées.
Il se trouve dans le conteneur System de chaque domaine participant (documenté dans la spec MS-ADTS - Trusted Domain Object) :
CN=contoso.local,CN=System,DC=corp,DC=local
graph TB subgraph FOREST["Forêt : corp.local"] subgraph DOMAIN["DC=corp,DC=local"] subgraph SYSTEM["CN=System"] TDO1["CN=contoso.local<br/>objectClass : trustedDomain"] TDO2["CN=fabrikam.com<br/>objectClass : trustedDomain"] end end end
La classe de l’objet est trustedDomain. On peut l’interroger directement via LDAP avec un filtre basique :
# LDAP brut avec nxc
nxc ldap "$DC_IP" -u "$USER" -p "$PASSWORD" \
--query "(objectClass=trustedDomain)" \
"trustType trustDirection trustAttributes securityIdentifier flatName"# PowerShell natif
Get-ADObject -Filter {objectClass -eq "trustedDomain"} -SearchBase "CN=System,DC=corp,DC=local" -Properties *
# PowerView
Get-DomainTrustSur le lab, la requête (objectClass=trustedDomain) renvoie un objet par trust, soit trois ici (dev.serval.int, technovate.lan et external.lan). Chacun expose son distinguishedName sous CN=System :

PowerView est pratique, mais il ne montre pas tout. Pour bien comprendre le mécanisme, rien de mieux qu’une lecture des attributs LDAP bruts, au moins une fois.
Les attributs essentiels
trustType
Cet attribut indique le type technique du domaine distant, et non le type du trust. Autrement dit, il décrit ce avec quoi on parle, pas la relation elle-même.
| Valeur | Nom | Ce que ça veut dire |
|---|---|---|
| 1 | DOWNLEVEL | Domaine Windows NT4, pré-Active Directory. Pas de Kerberos : l’authentification se fait via NTLM uniquement. Ces trusts ne peuvent pas être transitifs. |
| 2 | UPLEVEL | Domaine Active Directory. Kerberos disponible, transitivité possible, tous les mécanismes modernes fonctionnent. |
| 3 | MIT | Realm Kerberos non-Windows (MIT Kerberos, typiquement Linux/Unix). Kerberos fonctionne, mais sans les extensions Microsoft (PAC, etc.). |
| 4 | DCE | Héritage DCE/RPC. Pratiquement jamais rencontré en pratique. |
En pratique, on verra presque exclusivement le 2. Le 1 trahit un vestige NT4 qui n’a jamais été migré. Le 3 apparait dans les environnements mixtes où des serveurs Linux participent à l’infrastructure Kerberos.
Sur le lab, les trois trusts (dev.serval.int, technovate.lan et external.lan) pointent vers des domaines Active Directory, donc trustType = 2 (UPLEVEL) dans les trois cas :

Ce qui change concrètement selon la valeur :
- Avec un DOWNLEVEL (
1), Kerberos n’est pas utilisable entre les domaines, ce qui exclut toute forge de ticket inter-realm. - Avec un MIT (
3), le PAC Microsoft n’est pas garanti, ce qui impacte certaines attaques qui reposent sur sa structure.
trustDirection
Cet attribut donne la direction du trust :
| Valeur | Signification |
|---|---|
| 0 | Disabled |
| 1 | Inbound (ce domaine est le trusted, ses utilisateurs peuvent accéder au domaine distant) |
| 2 | Outbound (ce domaine est le trusting, les utilisateurs du domaine distant peuvent accéder à ses ressources) |
| 3 | Bidirectional (two-way) |
J’ai eu du mal à comprendre ce champ parce que la terminologie est contre-intuitive. Un trust Inbound vers contoso.local signifie que contoso.local nous fait confiance, nos utilisateurs peuvent potentiellement accéder à ses ressources. L’accès va dans le sens inverse de la confiance. L’attribut est lu depuis la perspective du domaine où se trouve le TDO.
Sur le lab, ça tombe bien, les trois directions ne sont pas identiques : dev.serval.int et technovate.lan sont bidirectionnels (trustDirection = 3), tandis qu’external.lan est en 1 (Inbound). On récupère aussi au passage le trustPartner (FQDN du domaine partenaire) et le flatName (nom NetBIOS : DEV, TECHNOVATE, EXTERNAL) :

C’est là que la notion de perspective prend tout son sens. Si on se connecte cette fois sur external.lan (EXTDC01) et qu’on lit son TDO pour serval.int, le même trust ressort en trustDirection = 2 (Outbound) :

Le 1 (Inbound) vu depuis serval.int et le 2 (Outbound) vu depuis external.lan décrivent une seule et même relation à sens unique (external.lan fait confiance à serval.int), simplement lue depuis chaque extrémité.
trustAttributes
Celui-là est le plus riche. C’est un bitmask, et chaque bit correspond à une propriété du trust. La valeur brute est un entier, on la lit bit par bit. Ça peut rappeler des souvenirs si vous avez déjà fouillé l’attribut userAccountControl sur les comptes AD, c’est exactement le même principe :
| Flag (hex) | Nom | Signification |
|---|---|---|
0x00000001 | NON_TRANSITIVE | Le trust ne se propage pas |
0x00000002 | UPLEVEL_ONLY | Kerberos seulement, pas NTLM |
0x00000004 | QUARANTINED_DOMAIN | SID Filtering actif |
0x00000008 | FOREST_TRANSITIVE | Trust entre forêts, transitif |
0x00000010 | CROSS_ORGANIZATION | Domaine hors forêt |
0x00000020 | WITHIN_FOREST | Domaine dans la même forêt |
0x00000040 | TREAT_AS_EXTERNAL | Forest trust traité comme external (SID filtering strict) |
0x00000080 | USES_RC4_ENCRYPTION | Clés RC4 seulement (pas d’AES) |
0x00000200 | CROSS_ORGANIZATION_NO_TGT_DELEGATION | Désactive la délégation TGT cross-org |
0x00000400 | PIM_TRUST | PAM/PIM trust (shadow principal) |
0x00000800 | CROSS_ORGANIZATION_ENABLE_TGT_DELEGATION | Active explicitement la délégation TGT cross-org |
Un trust parent-child typique aura trustAttributes = 0x00000020 (WITHIN_FOREST). Un external trust portera typiquement 0x00000004 (QUARANTINED_DOMAIN, le SID filtering étant activé par défaut). Un forest trust transitif aura 0x00000008 (FOREST_TRANSITIVE).
Pour décoder ça proprement :
# Décoder une valeur brute de trustAttributes bit à bit (ex: 4)
attr=4
echo " trustAttributes (raw): $attr (0x$(printf '%x' $attr))"
echo " NON_TRANSITIVE: $(( (attr & 0x1) != 0 ))"
echo " QUARANTINED: $(( (attr & 0x4) != 0 ))"
echo " FOREST_TRANSITIVE: $(( (attr & 0x8) != 0 ))"
echo " WITHIN_FOREST: $(( (attr & 0x20) != 0 ))"
echo " CROSS_ORGANIZATION: $(( (attr & 0x10) != 0 ))"
echo " USES_RC4_ONLY: $(( (attr & 0x80) != 0 ))"$trust = Get-ADObject -Filter {objectClass -eq "trustedDomain"} `
-SearchBase "CN=System,DC=corp,DC=local" `
-Properties trustAttributes, trustDirection, trustType, securityIdentifier
foreach ($t in $trust) {
$attr = $t.trustAttributes
Write-Host "=== $($t.Name) ==="
Write-Host " trustAttributes (raw): $attr (0x$("{0:X}" -f $attr))"
Write-Host " NON_TRANSITIVE: $(($attr -band 0x1) -ne 0)"
Write-Host " QUARANTINED: $(($attr -band 0x4) -ne 0)"
Write-Host " FOREST_TRANSITIVE: $(($attr -band 0x8) -ne 0)"
Write-Host " WITHIN_FOREST: $(($attr -band 0x20) -ne 0)"
Write-Host " CROSS_ORGANIZATION: $(($attr -band 0x10) -ne 0)"
Write-Host " USES_RC4_ONLY: $(($attr -band 0x80) -ne 0)"
}Exécuté sur le lab, le script décode chaque flag : dev.serval.int ressort en WITHIN_FOREST (0x20, trust intra-forêt parent-child), technovate.lan en FOREST_TRANSITIVE (0x8, forest trust), et external.lan en QUARANTINED_DOMAIN (0x4, SID filtering actif, typique d’un external trust) :

On retrouve les mêmes valeurs brutes en interrogeant directement les TDO via LDAP avec nxc (32 pour dev.serval.int, 8 pour technovate.lan, 4 pour external.lan) :

securityIdentifier
Il contient le SID du domaine distant. C’est une information précieuse pour les attaques, qu’il s’agisse d’injection de SID History ou de la construction de SIDs de groupes privilégiés, donc je le note dès l’énumération.
flatName
Il contient le nom NetBIOS du domaine distant. On le retrouve dans les chemins UNC et dans certaines authentifications NTLM.
msDS-SupportedEncryptionTypes
C’est l’un des attributs essentiels du TDO d’après la spec MS-ADTS, et il indique quels types de chiffrement Kerberos sont négociés pour ce trust. C’est un bitmask défini par la spec MS-KILE section 2.2.7 (Supported Encryption Types Bit Flags) : 0x1 = DES-CBC-CRC, 0x2 = DES-CBC-MD5, 0x4 = RC4-HMAC, 0x8 = AES128-CTS-HMAC-SHA1-96, 0x10 = AES256-CTS-HMAC-SHA1-96 (les bits supérieurs couvrent FAST 0x20, compound identity 0x40, claims 0x80, etc.). Une valeur de 4 signifie donc RC4 uniquement, sans AES, ce qui a des implications offensives : RC4 est plus craquable, et un trust limité à RC4 facilite le downgrade et certaines attaques sur les tickets.
Une particularité : contrairement à la plupart des autres attributs du TDO, celui-ci n’est présent que s’il a été explicitement configuré sur le trust. Tant qu’il n’a pas été défini, il ne fait tout simplement pas partie de l’objet. En LDAP, un attribut a toujours au moins une valeur, donc un attribut non posé est absent de l’entrée et n’est jamais renvoyé, même si on le demande explicitement. Le KDC retombe alors sur ses types de chiffrement par défaut. Sur le lab, une fois le support AES activé, il ressort à 24 (0x18 = AES128 0x8 + AES256 0x10), soit de l’AES uniquement, sans RC4 ni DES :

msDS-TrustForestTrustInfo
C’est un attribut propre aux forest trusts. Il contient les informations de filtrage, c’est-à-dire quels SIDs et quels suffixes UPN/DNS sont autorisés à traverser. Comme il s’agit d’une structure binaire, on ne la lit pas à la main : côté PowerView, Get-ForestTrust renvoie ces informations via l’API .NET GetAllTrustRelationships() (et non en parsant l’attribut brut).
Les types de trusts, vus depuis le TDO
Il n’existe pas d’attribut unique qui donne la catégorie du trust en clair (« parent-child », « forest »…), bien que ces dernières soient documentées par Microsoft. On la lit essentiellement dans trustAttributes. Par exemple, FOREST_TRANSITIVE (0x8) marque un forest trust, alors que WITHIN_FOREST (0x20) marque un trust interne à la forêt ; l’absence des deux décrit plutôt un trust external. Cependant, si on remarque une chose, c’est que le flag à lui seul ne suffit pas à déterminer la catégorie, étant donné que parent-child, tree-root et shortcut portent tous WITHIN_FOREST. On les départage alors par la position des domaines dans la hiérarchie DNS et le mode de création (automatique ou manuel), comme détaillé ci-dessous.
Parent-Child : son trustAttributes porte WITHIN_FOREST (0x20). Les deux domaines appartiennent à la même forêt, et l’un est l’enfant de l’autre dans l’arborescence DNS. Il est bidirectionnel, transitif, et c’est Active Directory qui le crée automatiquement.
Tree-Root : il porte lui aussi WITHIN_FOREST, mais il relie cette fois deux racines d’arbres distinctes au sein de la même forêt. Pour le reste, la mécanique est identique à celle du parent-child.
Cross-Link (Shortcut) : on retrouve encore WITHIN_FOREST, sauf qu’ici le trust est créé manuellement, entre deux domaines qui ne sont pas directement parent et enfant. Il sert à raccourcir le chemin Kerberos.
External : il se reconnaît surtout à QUARANTINED_DOMAIN (0x4), c’est-à-dire le SID filtering activé par défaut, et il ne porte ni WITHIN_FOREST ni FOREST_TRANSITIVE. Il reste non transitif par nature. Sur le lab, external.lan ressort justement à 0x4.
Forest : on le reconnaît à FOREST_TRANSITIVE, parfois accompagné de CROSS_ORGANIZATION. Il ne porte jamais WITHIN_FOREST.
Realm (MIT Kerberos) : ici, c’est le trustType qui parle, et il vaut 3. Ce type de trust est souvent NON_TRANSITIVE.
Comme dit précédemment, ces catégories ne sont pas stockées telles quelles : ce sont des classifications administratives définies par Microsoft, qu’on reconstruit à partir des attributs bruts. La référence qui les décrit toutes les six est How Domain and Forest Trusts Work. Understanding Trust Types donne en plus un tableau synthétique des 4 types créés manuellement (external, realm, forest, shortcut).
graph TB subgraph FA["Forêt A"] ROOT["corp.local (root)"] EU["eu.corp.local"] US["us.corp.local"] ASIA["asia.corp (root, arbre 2)"] end subgraph FB["Forêt B"] ADATUM["adatum.com"] end EXT["fabrikam.com (hors forêt)"] ROOT -->|"Parent-Child (auto)"| EU ROOT -->|"Parent-Child (auto)"| US EU <-.->|"Shortcut (manuel)"| US ROOT <-->|"Tree-Root (auto)"| ASIA ROOT <-->|"Forest Trust"| ADATUM ROOT <-->|"External Trust"| EXT
La transitivité
La transitivité détermine si un trust s’étend au-delà des deux domaines entre lesquels il a été créé. C’est un concept distinct de la direction : la direction dit dans quel sens va la confiance, la transitivité dit jusqu’où elle se propage. Microsoft le résume ainsi dans Understanding Trust Transitivity : « Transitivity determines whether a trust can be extended outside the two domains between which the trust was formed. »
Un trust transitif se propage. Si A fait confiance à B de façon transitive et que B fait confiance à C, alors A fait confiance à C sans qu’on ait créé de lien direct. C’est exactement ce qui permet, dans une forêt, qu’un compte de n’importe quel domaine s’authentifie dans n’importe quel autre : les trusts parent-child et tree-root sont transitifs, donc la confiance remonte et redescend toute l’arborescence.
Un trust non transitif s’arrête, lui, aux deux domaines concernés. La confiance ne déborde pas vers le reste de la forêt ni au-delà. Un external trust en est l’exemple typique : il relie exactement deux domaines, et rien d’autre.
Côté types, ça se répartit ainsi :
- Transitifs : parent-child et tree-root (automatiques), ainsi que shortcut et forest (manuels). Un realm trust peut l’être ou non, selon sa création.
- Non transitifs : external (toujours), et un realm trust quand il est créé comme tel.
Dans le TDO, l’information se lit sur le flag NON_TRANSITIVE (0x1) de trustAttributes : s’il est posé, le trust ne se propage pas ; s’il est absent (sur un type qui le permet), le trust est transitif. Le flag FOREST_TRANSITIVE (0x8), lui, marque la transitivité spécifique aux forest trusts.
Une transitivité bornée pour les forest trusts
Il y a un piège à connaître. Un forest trust est transitif à l’intérieur des deux forêts (tous les domaines de la forêt A font confiance à tous les domaines de la forêt B), mais cette transitivité ne se chaîne pas vers une troisième forêt. Si la forêt A a un forest trust vers B, et B un forest trust vers C, alors A ne fait pas confiance à C. Il faut un forest trust explicite entre A et C. Microsoft le dit noir sur blanc dans How trust relationships work for forests in Active Directory : « Forest trusts can only be created between two forests and can’t be implicitly extended to a third forest. »
graph LR A["Forêt A"] <-->|"Forest Trust"| B["Forêt B"] B <-->|"Forest Trust"| C["Forêt C"] A -. "aucune confiance (transitivité bornée)" .-> C
Concrètement, côté offensif : la transitivité élargit la surface d’attaque, puisque compromettre un domaine d’une forêt peut ouvrir l’accès au reste de la forêt via les trusts internes transitifs. En revanche, elle ne permet pas de rebondir indéfiniment de forêt en forêt. Chaque saut inter-forêt réclame son propre trust.
Les Trust Accounts
C’est un détail qui, je trouve, manque dans beaucoup d’explications, et qui prête souvent à confusion. Il faut distinguer deux objets : le TDO (l’objet trustedDomain, présent dans chaque domaine du trust) et le compte de trust (un principal TRUST_ACCOUNT, stocké côté trusted uniquement).
Pour un trust bidirectionnel entre corp.local et contoso.local, chaque domaine héberge un compte de trust nommé d’après l’autre : corp.local contient un compte CONTOSO$ et contoso.local contient un compte CORP$.
Pour un trust unidirectionnel où corp.local fait confiance à contoso.local (corp.local = trusting, contoso.local = trusted), c’est le domaine trusted qui héberge le compte de trust : contoso.local contient un compte CORP$ en plus du TDO. Côté trusting, corp.local ne stocke que le TDO contoso.local, sans compte TRUST_ACCOUNT pour cette relation. La raison semble être Kerberos. Dans ce trust à sens unique, les utilisateurs qui traversent la frontière sont ceux de contoso.local (le trusted), qui vont chercher des ressources dans corp.local. Quand l’un d’eux le fait, c’est le KDC de son propre domaine, contoso.local, qui doit lui émettre un ticket de référral vers le realm distant. Le nom de ce principal suit une convention fixée par la RFC 4120, §7.3 : krbtgt/<realm cible>@<realm émetteur>. Ici l’émetteur est contoso.local et la cible corp.local, d’où krbtgt/CORP.LOCAL@CONTOSO.LOCAL.
Or ce référral, comme tout ticket, est chiffré avec la clé de son principal serveur. Et pour un saut inter-realm, cette clé n’est pas celle du krbtgt habituel mais la clé partagée du trust, comme le précise RFC 4120, §1.2 :
A client is then able to obtain a ticket-granting ticket for the remote realm’s ticket-granting service from its local realm. When that ticket-granting ticket is used, the remote ticket-granting service uses the inter-realm key (which usually differs from its own normal TGS key) to decrypt the ticket-granting ticket; thus it is certain that the ticket was issued by the client’s own TGS.
En plus du TDO, cette clé inter-realm est aussi matérialisée sous forme d’un compte : CORP$, présent uniquement côté trusted. La règle vient de la spec MS-ADTS, Essential Attributes of Interdomain Trust Accounts : un compte de trust n’existe que pour un trust entrant ou bidirectionnel. Notre trust étant à sens unique, seul contoso.local (le côté trusted, dont le TDO est entrant) possède le compte CORP$ ; côté corp.local (le trusting, TDO sortant), il n’y a que le TDO. La clé inter-realm, elle, est présente des deux côtés dans les TDO, donc les deux KDC font leur crypto Kerberos sans avoir besoin de ce compte : le trust account est une copie supplémentaire du secret sous forme de principal, pas la seule détentrice de la clé.
Ces comptes portent le flag INTERDOMAIN_TRUST_ACCOUNT dans l’attribut userAccountControl, et leur samAccountType vaut 0x30000002 (TRUST_ACCOUNT). Leur seul rôle est de stocker le secret partagé qui sert de base à la clé inter-domaine.
# Filtrer sur le flag UAC INTERDOMAIN_TRUST_ACCOUNT (0x800 = 2048)
nxc ldap "$DC_IP" -u "$USER" -p "$PASSWORD" \
--query "(userAccountControl:1.2.840.113556.1.4.803:=2048)" \
"sAMAccountName userAccountControl sAMAccountType"
# Ou sur le samAccountType TRUST_ACCOUNT (0x30000002 = 805306370)
nxc ldap "$DC_IP" -u "$USER" -p "$PASSWORD" \
--query "(samAccountType=805306370)" \
"sAMAccountName userAccountControl sAMAccountType"# Les voir dans l'AD
Get-ADObject -Filter {samAccountType -eq 805306370} -Properties * |
Where-Object {$_.Name -like "*$"}
# Filtrer explicitement sur le flag UAC INTERDOMAIN_TRUST_ACCOUNT (0x800)
Get-ADUser -LDAPFilter "(userAccountControl:1.2.840.113556.1.4.803:=2048)"Sur le lab, serval.int héberge les comptes DEV$, TECHNOVATE$ et EXTERNAL$ (nommés d’après le NetBIOS du partenaire) dans CN=Users. Leur userAccountControl vaut 2080, soit 0x820 = INTERDOMAIN_TRUST_ACCOUNT (0x800) + PASSWD_NOTREQD (0x20). À noter : external.lan est un trust entrant, et le compte EXTERNAL$ existe donc bien côté serval.int (le côté qui reçoit l’authentification) :

Le placement du compte de trust côté trusted (et la distinction avec le TDO) est confirmé par la documentation Microsoft How Domain and Forest Trusts Work, qui décrit la rotation du mot de passe : le contrôleur du domaine trusting pousse le secret vers le compte de trust situé dans le domaine trusted. Pour aller plus loin sur les attributs du compte : la spec MS-ADTS - userAccountControl et LDAPWiki - INTERDOMAIN_TRUST_ACCOUNT.
Le mot de passe de ces comptes est changé automatiquement tous les 30 jours.
graph TB KEY(["Trust Key (secret partagé)<br/>rotation ~30 jours"]) ACC1["CONTOSO$ (dans corp.local)<br/>UAC : INTERDOMAIN_TRUST_ACCOUNT<br/>samAccountType : 0x30000002"] -- "détient" --> KEY ACC2["CORP$ (dans contoso.local)<br/>UAC : INTERDOMAIN_TRUST_ACCOUNT<br/>samAccountType : 0x30000002"] -- "détient" --> KEY
Les clés inter-domaines
Cette section est celle dont je suis le moins certain. Je pose ce que je comprends du mécanisme, mais plusieurs détails (nommage exact dans les LSA secrets, nom des attributs dans le TDO, algorithme de dérivation AES) mériteraient d’être vérifiés dans les specs MS-ADTS et MS-LSAD avant d’être pris pour argent comptant.
Les deux KDCs partagent un secret, le mot de passe du trust, d’où sont dérivées les clés inter-realm (RC4 et AES). Ce sont elles qui permettent à un DC de valider qu’un ticket venant d’un autre domaine est légitime. Concrètement, quand un utilisateur de corp.local veut accéder à une ressource dans contoso.local, son KDC lui émet un ticket de référral chiffré avec la clé inter-realm. Le KDC de contoso.local est le seul à pouvoir le déchiffrer, vérifier sa validité, et lui délivrer un ST en échange.
sequenceDiagram participant A as alice@corp.local participant KC as KDC corp.local participant KS as KDC contoso.local participant R as Ressource contoso.local A->>KC: 1. AS-REQ (demande TGT) KC-->>A: 2. AS-REP (TGT pour corp.local) A->>KC: 3. TGS-REQ (ticket pour contoso.local ?) KC-->>A: 4. TGS-REP (referral TGT, chiffré avec trust key) A->>KS: 5. TGS-REQ (referral TGT) Note over KC,KS: Validation via trust key partagée KS-->>A: 6. TGS-REP (ST pour la ressource) A->>R: 7. Accès avec le ST
Où elle est stockée
Le secret de la clé inter-domaine est présent des deux côtés du trust, mais sous des formes différentes selon le côté. Les deux domaines stockent la clé dans leur TDO via les attributs trustAuthIncoming / trustAuthOutgoing décrits ci-dessous. Côté trusted, la clé est en plus exposée comme mot de passe d’un compte de trust dédié (CORP$) : la spec MS-ADTS, section 6.1.6.8 (Essential Attributes of Interdomain Trust Accounts) associe ce compte au TDO via le nom NetBIOS du partenaire (flatName du TDO = nom NetBIOS, sAMAccountName du compte = <NetBIOS>$) et précise qu’il n’existe que pour un trust entrant ou bidirectionnel. La synchronisation entre le TDO et le compte est documentée dans MS-LSAD : à la création ou la mise à jour du trust (LsarSetInformationTrustedDomain, LsarCreateTrustedDomainEx2), le unicodePwd du compte est dérivé du mot de passe en clair (AuthType = 0x2) présent dans les « Trust Incoming Passwords » du TDO au moment de l’opération. Le clair est éphémère : ce qui est stocké dans l’attribut est son hash NT (format OWF).
D’après MS-ADTS, le TDO porte deux attributs qui contiennent les credentials du trust :
trustAuthIncoming: credentials pour les authentifications entrantestrustAuthOutgoing: credentials pour les authentifications sortantes
Ces attributs contiennent une structure LSAPR_TRUSTED_DOMAIN_AUTH_INFORMATION (définie dans la spec MS-LSAD, section 2.2.7.11), qui enveloppe des tableaux de LSAPR_AUTH_INFORMATION (section 2.2.7.17). Chaque LSAPR_AUTH_INFORMATION a notamment un champ AuthType (1 = clé RC4HMAC dérivée, c’est-à-dire le hash NT ; 2 = mot de passe en clair) et un champ AuthInfo qui contient la valeur elle-même. La structure parente distingue une version courante et une version précédente (champs IncomingPreviousAuthenticationInformation et OutgoingPreviousAuthenticationInformation), ce qui permet de gérer la rotation sans casser les tickets en cours.
// MS-LSAD, section 2.2.7.11
typedef struct _LSAPR_TRUSTED_DOMAIN_AUTH_INFORMATION {
[range(0,1)] unsigned long IncomingAuthInfos;
PLSAPR_AUTH_INFORMATION IncomingAuthenticationInformation; // courante
PLSAPR_AUTH_INFORMATION IncomingPreviousAuthenticationInformation; // precedente
[range(0,1)] unsigned long OutgoingAuthInfos;
PLSAPR_AUTH_INFORMATION OutgoingAuthenticationInformation; // courante
PLSAPR_AUTH_INFORMATION OutgoingPreviousAuthenticationInformation; // precedente
} LSAPR_TRUSTED_DOMAIN_AUTH_INFORMATION,
*PLSAPR_TRUSTED_DOMAIN_AUTH_INFORMATION;// MS-LSAD, section 2.2.7.17
typedef struct _LSAPR_AUTH_INFORMATION {
LARGE_INTEGER LastUpdateTime;
unsigned long AuthType;
[range(0,65536)] unsigned long AuthInfoLength;
[size_is(AuthInfoLength)] unsigned char* AuthInfo;
} LSAPR_AUTH_INFORMATION,
*PLSAPR_AUTH_INFORMATION;Ce que l’on comprend c’est que ces attributs stockent le mot de passe (ou son hash), pas les clés Kerberos dérivées. Les clés RC4 et AES utilisées pour chiffrer les tickets seraient dérivées à la volée de ce secret au moment où le KDC en a besoin.
Ce qui est certain : ces données ne sont pas lisibles via une requête LDAP classique, même en tant qu’admin du domaine. En pratique, on y accède via DCSync (lsadump::dcsync) ou directement via lsadump::trust sur un DC compromis.
Pour ça, j’utilise tdo_dump, un PoC d’AlmondOffSec, que j’installe depuis mon fork :
pipx install git+https://github.com/Goultarde/tdo_dumpEn dumpant les clés du TDO sur les deux DC du trust (serval.int et external.lan), on retrouve le même secret de chaque côté. C’est la preuve concrète que la clé inter-domaine est bien partagée entre les deux realms :

Le détail à repérer dans la sortie, c’est l’étiquette Incoming vs Outgoing. Sur ce trust entrant, la clé est rangée dans trustAuthIncoming côté serval.int (le côté qui reçoit l’authentification) et dans trustAuthOutgoing côté external.lan (le côté qui l’émet). C’est le même secret des deux côtés, simplement étiqueté selon le sens de la confiance. Et c’est précisément ce qui fait qu’un trust « à sens unique » ne l’est pas vraiment au niveau des clés.
L’outil comme la technique sont de ThePirateWhoSmellsOfSunflowers, qui détaille tout le sujet dans Trust no one: are one-way trusts really one-way?, une lecture que je recommande chaudement sur les external trusts entrants.
Une alternative à tdo_dump arrivera peut-être directement dans impacket : j’ai ouvert une PR (#2207) sur le dépôt de Fortra qui ajoute deux options à secretsdump.py. -trust-keys dumpe les secrets des TDO et en dérive les clés inter-realm Kerberos (AES256, AES128, RC4), versions courante et précédente, pour chaque direction du trust. -just-trust-keys fait la même chose en sautant l’énumération des comptes, pour une sortie plus rapide. La distinction technique par rapport à --just-dc est précisément celle vue plus haut : le sel utilisé pour dériver les clés AES est le FQDN du partenaire, pas son nom NetBIOS, ce qui produit les vraies clés inter-realm et non les clés des comptes de trust.
Si on dumpe le compte de trust EXTERNAL$ directement via NTDS, on récupère ses clés Kerberos : le hash NT (la clé RC4) est identique à celui du TDO, mais ses clés AES sont différentes des clés inter-realm qu’on vient de dumper :

En mettant les deux dumps côte à côte, l’aes256 ne correspond pas :
| Source | aes256 |
|---|---|
| Clé inter-realm | 57ab59ea14a24da59b2afe6d2c26f877c4aa4083135f958f76fee048a0504a95 |
Compte EXTERNAL$ | b0143f241cb84195b48a06cb6c2d09e9144366e2ca0fa6b2db33c3d6ae32a1be |
Pourtant, le mot de passe est le même partout, et donc le hash NT (la clé RC4) aussi, identique dans les deux entrées :
| Source | hash NT |
|---|---|
| Clé inter-realm | 620488aeb440ede3d7f5ac81a0102900 |
Compte EXTERNAL$ | 620488aeb440ede3d7f5ac81a0102900 |
Comment un même mot de passe donne-t-il deux AES différentes ? La réponse tient en un mot : le sel. Les clés AES Kerberos sont dérivées via string2key(mot_de_passe, sel) (RFC 3962, 4096 itérations), donc même mot de passe + sel différent = clé différente. Le code de tdo_dump le montre : même string_to_key, même secret, mêmes itérations, et seul le sel <DOMAINE>krbtgt<PARTENAIRE> change :
b0143f24(la clé du compte, celle du dump NTDS) utilise le nom court (NetBIOS) du partenaire dans le sel ;57ab59ea(la clé inter-realm) utilise son FQDN complet.
Le hash NT (620488), lui, est identique partout, parce que RC4 ne sale rien. Conséquence côté offensif : pour forger un référral inter-realm en AES, il faut la clé inter-realm (57ab59ea), et dumper EXTERNAL$ via --ntds ne suffit pas, il ne donne que b0143f24. C’est exactement ce que tdo_dump va chercher dans le TDO. En RC4 en revanche, le hash NT marche pour tout sans se soucier du sel, d’où la préférence historique pour RC4 dans le forging de tickets de trust.
Sur le lab, un dump NTDS avec nxc sort les secrets des comptes DEV$, TECHNOVATE$ et EXTERNAL$. Leur hash NT est directement utilisable comme clé RC4 inter-domaine, puisque RC4 n’est pas salé (les clés AES, elles, demandent la dérivation salée vue plus haut) :

RC4 vs AES
Si trustAuthIncoming/trustAuthOutgoing stockent le mot de passe, les clés Kerberos en sont dérivées :
- RC4 : le hash NT du mot de passe sert directement de clé. C’est cohérent avec le fonctionnement général de Kerberos RC4 dans AD.
- AES : les clés sont dérivées via
string2key(RFC 3961).
Si USES_RC4_ENCRYPTION est positionné dans trustAttributes, seul RC4 est utilisé pour ce trust. En environnement moderne, AES est normalement disponible.
C’est visible sur le lab : avec nxc smb --ntds --kerberos-keys sur DEV$, TECHNOVATE$ et EXTERNAL$, on récupère les clés Kerberos dérivées en aes256-cts, aes128-cts et des-cbc-md5, en plus du hash NT utilisé pour RC4 :

Rotation
Le secret du trust est renouvelé automatiquement tous les 30 jours, et cette fois c’est explicitement documenté par Microsoft dans How Domain and Forest Trusts Work, section « TDO Passwords » : « every thirty days, the trusting domain controller changes the password stored in the TDO ». C’est donc le DC du domaine trusting (via son émulateur PDC) qui pilote le changement, puis pousse le nouveau secret vers le compte de trust côté trusted. L’ancienne valeur est conservée le temps que les tickets émis avant la rotation arrivent à expiration.
Du point de vue offensif : si on extrait la trust key, on peut avoir la valeur courante et la précédente. Les deux peuvent être valides selon le moment où la rotation a eu lieu.
Les outils d’énumération
rusthound-ce : collecte complète, à importer dans BloodHound.
rusthound-ce -d "$DOMAIN" -u "$USER"@"$DOMAIN" -p "$PASSWORD" \
--zip -f "$DC_HOST"Une fois le zip importé dans BloodHound CE, la requête Cypher MATCH p = (:Domain)-[:SameForestTrust|CrossForestTrust]->(:Domain) (saved query « Map domain trusts ») affiche les trusts sous forme de graphe. On y lit les trois relations : SERVAL.INT ↔ DEV.SERVAL.INT en SameForestTrust (le trust intra-forêt), et SERVAL.INT relié à TECHNOVATE.LAN comme à EXTERNAL.LAN en CrossForestTrust. Deux choses à remarquer ici. D’abord, BloodHound CE ne distingue pas un forest trust d’un external trust : les deux tombent dans la même arête CrossForestTrust, alors que trustAttributes les sépare (FOREST_TRANSITIVE contre QUARANTINED_DOMAIN). Ensuite, le sens des arêtes traduit la direction : dev et technovate ont deux arêtes (bidirectionnels), tandis qu’external.lan n’en a qu’une seule, vers SERVAL.INT, parce que le trust est entrant.

nxc ldap : lecture ciblée des TDOs avec leurs attributs bruts.
nxc ldap "$DC_IP" -u "$USER" -p "$PASSWORD" \
--query "(objectClass=trustedDomain)" \
"name trustType trustDirection trustAttributes securityIdentifier"Énumération rapide, outils présents nativement.
nltest /domain_trusts /all_trustsPowerView (import du module requis).
Get-DomainTrust
Get-DomainTrust -Domain contoso.local # depuis un domaine spécifique
Get-DomainTrustMapping # récursif, trace toutModule ActiveDirectory natif.
Get-ADTrust -Filter * | Select Name, Direction, TrustType, TrustAttributes, SIDFilteringQuarantinedLecture brute des TDOs avec tous les attributs.
Get-ADObject -LDAPFilter "(objectClass=trustedDomain)" `
-SearchBase "CN=System,DC=corp,DC=local" `
-Properties trustType, trustDirection, trustAttributes, `
securityIdentifier, flatName, `
"msDS-SupportedEncryptionTypes", `
"msDS-TrustForestTrustInfo"Les attributs à noter en priorité
Quand on énumère des trusts, voici les informations que je capture en priorité :
- Nom du domaine distant et son
flatName(NetBIOS) trustDirection: dans quel sens va la confiancetrustAttributesen raw hex : est-ce queQUARANTINEDest actif ?WITHIN_FOREST?FOREST_TRANSITIVE?securityIdentifier: le SID du domaine distantmsDS-SupportedEncryptionTypes: RC4 seulement ou AES disponible ?
Pour repérer QUARANTINED (et les autres flags) d’un coup d’œil :
# nxc renvoie la valeur brute, à décoder (cf. le snippet bash plus haut)
nxc ldap "$DC_IP" -u "$USER" -p "$PASSWORD" \
--query "(objectClass=trustedDomain)" \
"name trustAttributes"Get-ADTrust -Filter * | Select-Object Name, Direction,
@{N='SIDFiltering';E={($_.TrustAttributes -band 0x4) -ne 0}},
@{N='WithinForest';E={($_.TrustAttributes -band 0x20) -ne 0}},
@{N='ForestTransitive';E={($_.TrustAttributes -band 0x8) -ne 0}},
@{N='RC4Only';E={($_.TrustAttributes -band 0x80) -ne 0}}Ce qu’il reste à couvrir
Ce premier article pose les bases conceptuelles : ce qu’est un trust au niveau de l’annuaire, comment ses clés et ses comptes sont stockés, et quels attributs lire en priorité. La suite de la série s’attaquera au volet offensif, c’est-à-dire à l’exploitation concrète de ces relations. Je n’ai pas encore figé le découpage des prochains articles, je les publierai au fur et à mesure.
Références
Sources primaires (ce sont les principales pour ce sujet) :
- MS-ADTS : Active Directory Technical Specification - Spec complète, contient les définitions de tous les attributs du TDO
- MS-ADTS : Trusted Domain Object - Section spécifique aux attributs du TDO
- MS-ADTS : trustAttributes - Détail de chaque flag du bitmask
- MS-LSAD : Local Security Authority (Domain Policy) Remote Protocol - Contient les structures LSAPR_AUTH_INFORMATION et le stockage des secrets de trust
- MS-KILE : Kerberos Protocol Extensions - Extensions Microsoft de Kerberos, dont le fonctionnement inter-domaine
- LDAPWiki : trustedDomain - Référence des attributs LDAP de l’objet trustedDomain
- LDAPWiki : trustAttributes - Détail des flags
- LDAPWiki : trustType - Valeurs de trustType
- LDAPWiki : trustDirection - Valeurs de trustDirection
- Microsoft - How Domain and Forest Trusts Work - Vue d’ensemble fonctionnelle (plus accessible que la spec MS-ADTS)
Sources offensives (pour la suite de la série) :