Home SERVICES
All Services Web App Security Network Testing Cloud Security Active Directory Red Team AI Red Teaming
COMPANY
About Us Certifications FAQ
Process Industries Blog Request a Quote
Back to Blog
Web Application

Deserialization Attacks: Exploiting Untrusted Data in Modern Web Frameworks

Deserialization vulnerabilities occupy a unique position in the web application attack surface: they are the only class where attacker-controlled data can directly become attacker-controlled code execution. No SQL injection, no XSS — the application itself reconstructs your payload into live objects and invokes their methods. OWASP has listed insecure deserialization in its Top 10 since 2017 for good reason. In engagements we conduct across financial services, SaaS platforms, and enterprise Java applications, it remains one of the highest-severity findings we deliver — and one of the most frequently misunderstood by development teams.

This post covers the mechanics of deserialization attacks across the four most commonly affected platforms — Java, .NET, Python, and PHP — describes how we locate and confirm vulnerable endpoints during black-box assessments, and details the mitigations that actually prevent exploitation rather than those that merely slow it down.

Why Deserialization Is Fundamentally Dangerous

Serialization converts an in-memory object graph into a transmittable or storable byte sequence. Deserialization reverses the process. The danger lies in what happens during that reversal: the deserializer must reconstruct objects, set their properties, and in many language runtimes, call lifecycle methods — readObject() in Java, __wakeup() in PHP, __reduce__() in Python — before the application code ever sees the result.

The key insight that makes this so severe is that the attacker does not need to find a bug in the application's business logic. They need only to find a deserialization endpoint and identify a usable "gadget chain" — a sequence of classes already present in the application's classpath or namespace whose normal method calls, when chained together during deserialization, produce an attacker-controlled side effect such as OS command execution or arbitrary file write.

The classes in these chains are not themselves buggy. They are perfectly normal utility classes from legitimate libraries. The vulnerability is the act of deserializing untrusted data using a powerful, unrestricted deserializer.

Java: ObjectInputStream and Gadget Chain Exploitation

Java's native serialization format — the binary stream beginning with the magic bytes AC ED 00 05 (hex) or rO0AB (base64) — is the most widely exploited deserialization mechanism in enterprise applications. Any endpoint that passes attacker-controlled bytes into ObjectInputStream.readObject() without allowlisting accepted types is potentially exploitable if the application's classpath contains a usable gadget library.

How Gadget Chains Work

When Java deserializes an object, it calls readObject() on each object in the stream. If the stream contains an object whose class defines a custom readObject() method, that method executes immediately. Gadget chains exploit this by constructing a serialized object graph where each step in the deserialization process calls into the next, ultimately reaching a method that invokes arbitrary code.

The most commonly referenced gadget libraries include:

  • Apache Commons Collections (CC1–CC7): The original gadget chains, discovered by Chris Frohoff and Gabriel Lawrence in 2015. The CC1 chain exploits InvokerTransformer to call Runtime.exec() via a chain of Map and Transformer objects.
  • Spring Framework gadgets: Usable when Spring is on the classpath, which it is in the vast majority of enterprise Java applications.
  • Groovy: The ConvertedClosure gadget chain is functional when the Groovy runtime is present, common in Gradle-based builds and Jenkins pipelines.
  • JRE-only chains (JDK7u21, JDK8u20): These gadget chains require no third-party libraries — only the JDK itself — making them relevant even in hardened environments that have removed vulnerable dependencies.

ysoserial and Payload Generation

ysoserial is the standard research tool for generating Java deserialization payloads. It accepts a gadget chain name and a command string and outputs a serialized Java object that, when deserialized by a vulnerable endpoint, executes the specified command. The tool is well-documented and its use is limited to authorized penetration testing and security research.

During engagements, our typical workflow for a suspected Java deserialization endpoint is:

  1. Confirm the AC ED 00 05 magic bytes in the intercepted request body or cookie value.
  2. Attempt out-of-band detection using a DNS callback payload (safe, no code execution) — ysoserial's URLDNS chain is ideal for this as it only triggers a DNS lookup and has no side effects.
  3. Upon DNS callback confirmation, identify which gadget library is available through error messages or application behavior differences.
  4. Execute a controlled proof-of-concept command (typically a sleep or a write to a known file path) to confirm code execution without causing service disruption.

The URLDNS chain is particularly useful because it works regardless of which gadget libraries are present — it only requires the JDK. A serialized payload pointing to a collaborator domain reliably confirms whether a given parameter is passed to ObjectInputStream.readObject().

Java Deserialization in the Wild

We have found Java deserialization vulnerabilities in session cookies encoded as base64 Java serialized objects, in JMX endpoints exposed on internal networks, in custom RMI implementations, and in third-party middleware products — particularly older versions of WebLogic, JBoss, and Jenkins — that are still prevalent in enterprise environments. CVE-2015-4852 (WebLogic), CVE-2017-3248 (WebLogic again), and the Apache Log4Shell-adjacent deserialization paths in Confluence demonstrate that this vulnerability class is not theoretical.

.NET: BinaryFormatter and JsonSerializer Attacks

The .NET ecosystem has its own equivalent of Java's ObjectInputStream problem, distributed across several serialization APIs that have been progressively deprecated or hardened by Microsoft — though legacy usage remains widespread in enterprise .NET Framework applications.

BinaryFormatter

BinaryFormatter is the most dangerous .NET serializer and was formally marked obsolete in .NET 5. It will deserialize any type present in the loaded assemblies without type restriction, making it directly analogous to Java's unrestricted ObjectInputStream. Any .NET application running on .NET Framework 4.x that accepts binary-serialized data via BinaryFormatter.Deserialize() is a candidate for exploitation.

The .NET equivalent of ysoserial is ysoserial.net, maintained by the security research community. It supports gadget chains targeting BinaryFormatter, SoapFormatter, LosFormatter, NetDataContractSerializer, and several others. Commonly used gadget chains include those based on TypeConfuseDelegate, ActivitySurrogateSelector, and WindowsClaimsIdentity.

JSON Serializers: TypeNameHandling in Newtonsoft.Json

Even JSON-based serialization can be exploited when type metadata is included in the payload. Newtonsoft.Json (Json.NET) supports a TypeNameHandling setting that, when set to All, Auto, or Objects, embeds type names in the serialized JSON and reconstructs them during deserialization. An attacker who can control the JSON payload can specify arbitrary types to instantiate.

A simplified illustration of a vulnerable configuration:

// Vulnerable: TypeNameHandling.All tells the deserializer to trust
// the $type field in attacker-controlled JSON
var settings = new JsonSerializerSettings {
    TypeNameHandling = TypeNameHandling.All
};
var obj = JsonConvert.DeserializeObject(userInput, settings);

An attacker-controlled payload can specify a type such as System.Windows.Data.ObjectDataProvider with a MethodName of a system command, achieving remote code execution entirely through JSON. This vector is particularly insidious because developers frequently perceive JSON deserialization as inherently safer than binary formats.

ViewState Deserialization

ASP.NET WebForms applications use ViewState — a base64-encoded, optionally HMAC-signed blob — to maintain page state across postbacks. When the HMAC validation key (machineKey) is known or guessable, an attacker can forge a malicious ViewState payload and submit it. The server deserializes it using LosFormatter, triggering code execution. Leaked machineKey values in web.config files exposed through path traversal or file read vulnerabilities frequently lead directly to ViewState-based RCE.

Python: The pickle Problem

Python's pickle module is explicitly documented as unsafe for untrusted data. The official Python documentation states: "The pickle module is not secure. Only unpickle data you trust." Despite this warning appearing in the stdlib documentation, we continue to find pickle deserialization of attacker-controlled data in production applications — most commonly in machine learning pipelines, caching layers, and session stores.

How pickle Achieves Code Execution

The pickle protocol supports a __reduce__ method that allows objects to define their own reconstruction logic. When an object defining __reduce__ is unpickled, Python calls the returned callable with the returned arguments to reconstruct the object. This mechanism was designed for legitimate serialization of complex objects, but it allows an attacker to define reconstruction logic that is simply an arbitrary function call.

An illustrative (educational) example of what a malicious pickle payload encodes:

import pickle, os

class Exploit(object):
    # __reduce__ is called by the unpickler to reconstruct the object.
    # The returned tuple (callable, args) is invoked as callable(*args).
    # Any callable available in the Python environment can be used here.
    def __reduce__(self):
        return (os.system, ('id',))

# Serializing this object produces a payload that, when unpickled
# by a vulnerable endpoint, executes the specified OS command.
payload = pickle.dumps(Exploit())

The payload is compact — often under 100 bytes — and can be delivered anywhere the application accepts a pickled object: a signed cookie, a cache key, a message queue entry, a model artifact loaded from a user-specified path.

Where We Find pickle in Production

The most common contexts where we encounter exploitable pickle deserialization are:

  • Flask session cookies using a custom serializer: Flask's default cookie signer uses JSON, but applications that override the session serializer with pickle (a pattern found in older StackOverflow answers and legacy codebases) create a direct RCE vector from the session cookie.
  • Redis and Memcached caching layers: When objects are serialized to pickle before being stored in a cache and the cache is accessible to other systems or tenants, a cache poisoning attack can deliver a malicious pickle payload.
  • Machine learning model loading: pickle.load() is the default serialization format for scikit-learn models and many PyTorch checkpoints. Applications that allow users to upload or specify model file paths without validation are directly exploitable.
  • Celery task queues: Celery supports pickle as a message serializer. In environments where the message broker (Redis, RabbitMQ) is accessible to an attacker, injecting pickle-serialized task messages can achieve code execution on worker nodes.

yaml.load() — The Other Python Deserialization Risk

Python's PyYAML library presents a similar risk through yaml.load() when called without specifying a safe loader. YAML's !!python/object/apply tag allows arbitrary Python callables to be instantiated during parsing. Any application that passes attacker-controlled YAML to the full loader is exploitable in the same manner as pickle. The fix — yaml.safe_load() — is a one-character change that eliminates the vector entirely, yet it remains one of the most common findings in Python code audits we perform.

PHP: Object Injection via unserialize()

PHP's unserialize() function reconstructs a PHP object from its serialized string representation. When attacker-controlled data is passed to unserialize(), the attacker can instantiate any class available in the application's autoloaded namespace and control the values of its properties. PHP's magic methods — __wakeup(), __destruct(), __toString(), and others — are invoked automatically at specific points in the object lifecycle, creating gadget chain opportunities.

PHP Magic Methods as Gadget Entry Points

The magic methods most commonly exploited in PHP object injection chains are:

  • __wakeup() — called immediately after unserialization; the first entry point in most chains.
  • __destruct() — called when the object is garbage collected; reliable even if __wakeup() is not available.
  • __toString() — called when the object is used in a string context; frequently reachable through a chain of property assignments.
  • __call() and __get() — called on undefined method or property access; useful for extending chains through framework internals.

PHP frameworks with large class libraries — Laravel, Symfony, Zend, Yii — all contain classes whose magic methods, when chained together, can reach eval(), system(), file write operations, or SSRF-capable HTTP clients. The PHPGGC (PHP Generic Gadget Chains) tool, analogous to ysoserial for PHP, catalogs ready-made gadget chains for all major PHP frameworks and libraries.

Identifying PHP Serialized Data

PHP serialized strings follow a recognizable format. An object serialization begins with O: followed by the class name length, the class name, and the property count. A common pattern to look for in HTTP parameters, cookies, and hidden form fields is the prefix O: or a: (for arrays). Base64-encoded serialized objects are also common — decoding any suspicious base64 blob and checking for this pattern is a standard step in our web application assessments.

For example, a serialized PHP object looks like:

O:8:"UserData":2:{s:4:"name";s:5:"admin";s:4:"role";s:4:"user";}

Replacing the class name and property values with a gadget chain payload and re-encoding achieves object injection. When the application deserializes this value — often in session handling, caching, or a "remember me" cookie — the magic methods fire and the chain executes.

PHP Deserialization in Content Management Systems

WordPress plugins are a particularly rich source of PHP deserialization vulnerabilities. Plugin code frequently calls unserialize() on data retrieved from the database, options table, or user-controlled request parameters, often without any input validation. Several critical CVEs in widely installed plugins (10,000+ active installations) have been PHP object injection vulnerabilities where the WordPress core or another popular plugin provided the usable gadget chain.

Identifying Deserialization Endpoints in Black-Box Testing

Locating deserialization endpoints without source code access requires a combination of traffic analysis, pattern recognition, and behavioral testing. Our black-box methodology for deserialization discovery includes the following steps.

Traffic Interception and Pattern Analysis

Every HTTP request and response in scope should be examined for serialized data signatures:

  • Java: Base64 strings beginning with rO0AB, or raw binary beginning with AC ED 00 05 in request bodies and cookies.
  • PHP: Strings beginning with O:, a:, s:, or i: followed by a colon and digit pattern, in cookies, POST parameters, or hidden fields.
  • Python pickle: Binary data in cookies or API parameters; base64-encoded pickle streams often begin with gASV (protocol 4) or KGxw (older protocols).
  • .NET BinaryFormatter: Binary streams beginning with 00 01 00 00 00 in ViewState fields or custom binary endpoints.
  • ViewState: The __VIEWSTATE hidden field in ASP.NET WebForms pages; decode from base64 and examine for BinaryFormatter or LosFormatter signatures.

Out-of-Band Detection

Before attempting any payload that might cause damage or disruption, we use out-of-band detection payloads that only trigger a DNS lookup or HTTP request to a collaborator server. For Java, the ysoserial URLDNS chain is purpose-built for this. For PHP, a gadget chain that triggers a DNS lookup via an HTTP client is similarly safe. A successful callback confirms that deserialization of the submitted data occurs, even if no code execution gadget is yet available.

Burp Suite Collaborator is the standard tool for out-of-band detection. We generate a unique collaborator subdomain per test case to unambiguously correlate callbacks with specific payloads and endpoints.

Error Message Analysis

Malformed or type-mismatched serialized payloads frequently produce informative error messages that reveal the deserializer in use, the expected class name, and sometimes the full classpath. Submitting a syntactically valid but semantically incorrect serialized object — for example, a Java serialized string where the target expects a specific class — can trigger stack traces that map the application's deserialization path and identify which libraries are loaded.

Timing-Based Detection

When out-of-band channels are blocked by egress filtering, a sleep-based command — Thread.sleep(10000) in Java, time.sleep(10) in Python, ping -n 11 127.0.0.1 in Windows environments — can confirm code execution through response latency. This technique is less reliable than out-of-band detection but can be used as a fallback when the target environment has strict outbound filtering.

Content-Type and Endpoint Enumeration

Endpoints consuming serialized data are often not exposed by default in API documentation or application UI flows. During black-box assessments we look for:

  • Endpoints with Content-Type: application/x-java-serialized-object or application/octet-stream accepting binary POST bodies.
  • JMX, RMI, and management interfaces on non-standard ports (typically 1099, 4444, 7001, 9000–9010).
  • WebSocket connections transmitting binary frames — binary WebSocket data is frequently a serialized object stream.
  • Session cookie values that are base64-encoded and significantly longer than expected for a simple session identifier.
  • API responses that return serialized objects intended for round-trip submission (the application sends you a serialized object and expects you to send it back later).

Defensive Guidance: Mitigations That Actually Work

Mitigating deserialization vulnerabilities requires more than patching individual gadget chains. Gadget chains are discovered continuously — blocking one does not eliminate the vulnerability class. Effective defense addresses the root cause: deserializing untrusted data with powerful, unrestricted deserializers.

1. Eliminate Dangerous Deserializers Entirely

The most effective mitigation is removal. Java's ObjectInputStream, .NET's BinaryFormatter, Python's pickle.loads(), and PHP's unserialize() should not be called on data that originates from outside the trust boundary — which, in a web application, means any data that travels over the network, regardless of whether it appears in a user-controlled parameter.

For Java, this means migrating to JSON (Jackson, Gson) or Protocol Buffers for data exchange, and eliminating ObjectInputStream from the codebase entirely where possible. For .NET, BinaryFormatter should be replaced with System.Text.Json or Newtonsoft.Json with TypeNameHandling.None. For Python, replace pickle with JSON, MessagePack, or Protocol Buffers. For PHP, replace unserialize() with json_decode().

2. Implement Strict Type Allowlisting

Where removal of the deserializer is not immediately feasible, implement a strict type allowlist that prevents the deserializer from instantiating any class not explicitly approved. In Java, this is accomplished using a custom ObjectInputFilter (introduced in JDK 9, backported to JDK 6u141, 7u131, 8u121):

ObjectInputStream ois = new ObjectInputStream(inputStream);
// Only permit deserialization of explicitly approved classes.
// All other types are rejected before any constructor or readObject() runs.
ois.setObjectInputFilter(info -> {
    Class<?> clazz = info.serialClass();
    if (clazz == null) return ObjectInputFilter.Status.UNDECIDED;
    if (clazz == MyExpectedClass.class) return ObjectInputFilter.Status.ALLOWED;
    return ObjectInputFilter.Status.REJECTED;
});

In PHP, the allowed_classes parameter of unserialize() achieves the same effect: unserialize($data, ['allowed_classes' => ['MyClass']]). Passing false disables class instantiation entirely, which is appropriate when only scalar values or arrays are expected.

3. Cryptographic Integrity Verification

All serialized data transmitted to the client and expected back should be signed with a server-side HMAC using a secret key unknown to the client. The signature must be verified before deserialization occurs — not after. This prevents an attacker from submitting forged or modified serialized payloads.

This is a defense-in-depth measure, not a primary control. An attacker who obtains the signing key (through a separate vulnerability such as a path traversal or secret leak) can forge valid signatures. HMAC signing does not make unsafe deserializers safe — it reduces the attack surface by limiting who can submit payloads to the deserialization endpoint.

4. Deserialize in an Isolated Process with Minimal Privileges

If deserialization of complex objects from untrusted sources is genuinely required by the application design, perform it in an isolated, sandboxed process with network egress blocked, filesystem access restricted to a minimal working directory, and no access to sensitive credentials or internal services. This limits the impact of successful exploitation to a contained environment rather than the full application process.

5. Application-Level Monitoring and Detection

Deserialization-based RCE is typically followed by network callbacks, process spawning, or file writes. Runtime Application Self-Protection (RASP) tools and Java agents such as those provided by contrast security can detect deserialization gadget chain invocations at the JVM level. At minimum, process execution from the application server process — particularly cmd.exe, /bin/sh, or curl — should trigger an immediate alert in your EDR or SIEM.

6. Dependency Management and Gadget Library Removal

While gadget chain discovery is an ongoing process and removing known gadget libraries is not a complete mitigation, it meaningfully increases the difficulty of exploitation. Audit your Java and .NET dependency trees for gadget libraries identified in ysoserial and ysoserial.net. If Apache Commons Collections versions prior to 3.2.2 or 4.1 are present and not required, remove or upgrade them. This is a hygiene measure that should accompany, not replace, the deserializer mitigations above.

Key takeaway: Deserialization vulnerabilities are architectural — they are the consequence of trusting a powerful object reconstruction mechanism with attacker-controlled input. Patching gadget libraries is a cat-and-mouse game. The only reliable fix is to never deserialize untrusted data using an unrestricted deserializer. Where immediate migration is not possible, combine a strict type allowlist with HMAC integrity verification and process-level monitoring as interim controls.

Frequently Asked Questions

Is JSON serialization safe from deserialization attacks?

Standard JSON deserialization into plain data structures (dictionaries, lists, strings) is safe. The risk arises when the JSON deserializer is configured to reconstruct typed objects from type metadata embedded in the payload — as with Newtonsoft.Json's TypeNameHandling settings in .NET, or certain Jackson enableDefaultTyping() configurations. Always use data-only JSON deserialization without type polymorphism for untrusted input.

Can a WAF block deserialization attacks?

WAF rules can detect common ysoserial payload signatures and known gadget chain byte patterns, providing some detection value. They are not a reliable preventive control. Gadget chains can be encoded, obfuscated, or constructed in novel ways that bypass signature-based detection. WAF protection should be considered a monitoring layer, not a substitute for fixing the underlying vulnerability.

How do we find deserialization vulnerabilities in our own code before attackers do?

Static analysis tools — Semgrep, CodeQL, SonarQube with appropriate rulesets — can identify calls to dangerous deserializers in Java, .NET, Python, and PHP code. Supplement automated scanning with manual code review focused on any point where data crosses a trust boundary and is subsequently deserialized. A dedicated web application penetration test from an experienced firm provides the black-box perspective that code analysis alone cannot cover.

Are modern framework versions still affected?

Yes. While Microsoft has deprecated BinaryFormatter and Oracle has added ObjectInputFilter to the JDK, neither change automatically fixes applications. Legacy code that predates these mitigations continues to run in production. Framework upgrades do not remove existing unserialize() or ObjectInputStream calls from application code. Each application must be individually assessed and remediated.

RELATED ARTICLES
Explore Web Application Security Testing →