QUACK: Hindering Deserialization Attacks via Static Duck Typing

Black Hat USA 2025 · Day 1 · Briefings

Overview

Researchers from Brown and Columbia universities present QUACK, a static program analysis tool that automatically infers which PHP classes a developer intended to allow through unserialize() calls — and then enforces that restriction using PHP's native allowedclasses parameter. Evaluated against 11 real applications with known CVEs, QUACK blocked 100% of auto-generated exploits and eliminated 97% of the methods available to an attacker's property-oriented programming (POP) chains, all without requiring any changes to application logic. ---

Watch on YouTube

Visual summary for QUACK: Hindering Deserialization Attacks via Static Duck Typing
Visual summary for QUACK: Hindering Deserialization Attacks via Static Duck Typing

Key moments

  1. 4:00 PHP deserialization gadget chains exploit excess classes in global namespace
  2. 7:59 Root cause: attacker instantiates unintended classes during unserialize() calls
  3. 10:00 Demo: Moodle CVE exploited via XML upload triggering unserialize RCE gadget chain
  4. 13:59 QUACK system: static duck typing analysis identifies unauthorized class instantiations
  5. 17:59 Key result: QUACK blocks 100% of tested PHP gadget chains with no app changes
  6. 21:59 Novel defense: restrict deserialization to structurally-matching classes only
  7. 25:59 QUACK evaluated on real CVEs: zero false positives on benign deserialization
  8. 30:00 Approach generalizes: same duck-typing principle applicable to Java and Python

QUACK: Hindering Deserialization Attacks via Static Duck Typing

Speakers: Neo, PhD Student, Brown University; Andreas, PhD Student, Columbia University (work led by Yaniv, with advisors Vassilis and Zhufeng)

Conference: Black Hat USA 2025 — August 6-7, 2025, Mandalay Bay, Las Vegas

YouTube: https://www.youtube.com/watch?v=hrtVuYfswCg

Reading Time: ~8 minutes

Type: Briefing

TL;DR

Researchers from Brown and Columbia universities present QUACK, a static program analysis tool that automatically infers which PHP classes a developer intended to allow through unserialize() calls — and then enforces that restriction using PHP's native allowed_classes parameter. Evaluated against 11 real applications with known CVEs, QUACK blocked 100% of auto-generated exploits and eliminated 97% of the methods available to an attacker's property-oriented programming (POP) chains, all without requiring any changes to application logic.

Introduction

Deserialization vulnerabilities have been a persistent plague in PHP applications for over a decade, producing a steady stream of CVEs against widely deployed platforms. Despite the well-understood attack mechanics — first documented in a 2010 Black Hat talk by Stefan Esser — defensive advice has remained largely limited to "don't use unserialize()." The result is predictable: CVEs continue to appear every year, PHP's own documentation carries a prominent red-box warning about passing untrusted data to unserialize(), and yet only 0.1% of unserialize() calls on public GitHub repositories use the allowed_classes argument that would restrict which classes can be instantiated.

QUACK addresses this gap by automating what developers rarely do manually: statically analyzing a PHP application to determine, from program context alone, exactly which classes the programmer intended to deserialize at each call site — and then producing a policy that can be mechanically applied to lock down the application.

How PHP Deserialization Attacks Work

▶ Watch: Deserialization Attack Mechanics (02:00)

PHP's unserialize() takes a string and reconstructs the PHP object it describes. When that string is attacker-controlled, the attacker can specify not just the property values of an object but its entire type — and can nest a hierarchy of objects by setting properties to new objects. The application's runtime then holds injected objects of types the developer never intended to instantiate.

The attacker's goal is not random object injection but control flow hijacking. PHP provides "magic methods" — methods prefixed with __ — that are automatically invoked in certain circumstances: __wakeup() fires immediately on deserialization, __destruct() fires when an object is garbage-collected, __toString() fires when an object is used in a string context. These automatic invocations give attackers a guaranteed execution trigger.

From there, the technique called property-oriented programming (POP) chains together method invocations across multiple injected objects, each calling the next, until execution reaches a "sink" — typically a function like system() or eval() with attacker-controlled arguments. The key insight the researchers emphasize: POP chains work because the attacker has access to far more classes than the developer intended. Restricting that class set is the attack surface to shrink.

▶ Watch: Live Moodle CVE Exploit Demo (10:00)

A live demonstration targets a real CVE in Moodle, the popular e-learning platform. The vulnerable code path accepts an XML quiz upload and passes a field of that XML directly to unserialize(). The exploit — a POP chain constructing a CommandExecutor-class object through a chain of intermediate class instantiations — executes system() with attacker-supplied arguments, printing ASCII art of the Black Hat logo on screen.

Existing Mitigations and Their Shortcomings

▶ Watch: Existing Mitigations Overview (12:00)

Three existing defensive recommendations exist, all with significant limitations:

  1. Replace unserialize() with JSON — works only for simple data types (arrays, primitives). Cannot represent complex PHP object graphs.
  2. HMAC integrity protection on serialized data — effective when the application itself generates the serialized string, but not when the application must accept serialized input from third parties or existing stored data.
  3. PHP's native allowed_classes argument — the most targeted defense: passing an array of class names restricts unserialize() to only instantiating those classes; anything else becomes a dummy __PHP_Incomplete_Class object with no methods. The problem is that only 0.1% of real-world unserialize() calls use this parameter, because manually identifying the intended class at each call site across a large codebase is tedious — and because many developers are simply unaware the option exists.

QUACK automates the third approach.

How QUACK Works: Static Duck Typing

▶ Watch: QUACK Design and Duck Typing Inference (16:01)

The name comes from the "duck typing" principle: if an object swims and flies, it must be a duck. QUACK observes how the deserialized object is actually used in the application code — method calls, property accesses, instanceof checks, type annotations — and infers the set of classes that are structurally compatible with those usages.

The analysis proceeds in two phases:

Phase 1 — Available Classes: QUACK performs a recursive file-inclusion traversal from the file containing the unserialize() call, collecting every class defined in files reachable via PHP include/require statements (both forward and backward — files that include the current file are also considered, since execution could reach unserialize() from any of them). PHP autoloaders are handled as a special case. This establishes the universe of classes physically available at the deserialization point.

Phase 2 — Possible Classes via Duck Typing: QUACK statically analyzes every use of the deserialized object and applies inference rules:

  • Method calls: $obj->fly() → only classes that define fly() are possible types
  • Property accesses: $obj->featherColor → only classes that declare this property are candidates
  • instanceof checks: $obj instanceof Duck → the type must be Duck
  • Type annotations: developer-provided PHP type hints narrow the candidate set further
  • Nested properties: if $obj->pet->bark() is called, QUACK infers the type of $obj (must have pet property) and the type of $obj->pet (must have bark() method) independently, adding both to the allowed_classes set

When multiple uses exist on the same variable, QUACK takes the intersection of per-use candidate sets. After determining possible types for the outermost object and any nested objects, QUACK intersects the possible-classes set with the available-classes set to produce the final allowed_classes policy.

QUACK is implemented on top of the Gern static PHP analysis framework.

Effectiveness: 97% Method Reduction, 100% Exploit Block

▶ Watch: QUACK Defense Demo and Evaluation (24:01)

The same Moodle CVE from the attack demo is used to demonstrate QUACK in defense mode. QUACK's output is a JSON file indicating that the unserialize() call on line 93 of the vulnerable file should only allow the class DDWTOS_choice. After manually patching the allowed_classes argument (which could also be automated via AST rewriting), the same exploit payload is submitted — and the POP chain does not execute. The quiz import succeeds normally; no command execution occurs.

Across the full evaluation of 11 applications with real CVEs (15 total vulnerable unserialize() call sites):

  • QUACK blocked all methods at 12 of 15 vulnerable call sites — meaning even zero classes needed to be allowed, because the developer was not actually using the deserialized object as a typed PHP object.
  • Across all 15 sites, QUACK eliminated 97% of methods available to an attacker's POP chain.
  • An automated exploit generation tool successfully generated exploits for 5 of the CVEs before QUACK; after applying QUACK's policy, it generated zero exploits.

Limitations and Future Directions

▶ Watch: Limitations and Outlook (28:07)

QUACK is explicitly a defense-in-depth tool, not a complete solution. Two categories of attack remain outside its scope:

  1. Data-only attacks: If the attacker can achieve their goal by manipulating the properties of a legitimately allowed class — without needing to instantiate different types — QUACK's class restriction provides no protection. These attacks are rarer but possible when the allowed class itself exposes dangerous functionality directly.
  2. Unsound analysis from PHP's dynamic nature: PHP's dynamic class loading, variable class names, and other metaprogramming features make perfect static analysis undecidable. QUACK may over-approximate (allow more classes than needed) in edge cases it does not yet handle.

The researchers highlight two directions for development. First, community testing: because PHP's surface is large, they invite users to run QUACK against their own codebases and file GitHub issues when the tool fails, to close coverage gaps. Second, IDE integration: an ideal deployment would have QUACK integrated directly into development environments, providing real-time type-restriction recommendations as a developer types unserialize().

On the question of language portability, the duck-typing concept should extend to Java and C# (which have similar restricted deserialization APIs), and a companion paper at the same academic venue demonstrated this for Java. Python's pickle module is explicitly out of scope: its execution model is far more expressive than PHP's unserialize(), permitting arbitrary code execution as part of the serialization format itself.

Notable Quotes

"We've seen numerous talks about the dangers of deserialization. Many of the talks, with few exceptions, are about attacks — and our defensive advice tends to be a warning to developers not to do this, resulting in CVEs over the years regardless."

— Neo, 00:00

"Only around 0.1% of calls to unserialize() in GitHub repositories even use the allowed_classes argument. Developers probably are just not aware of the dangers of serialization, or it is pretty tedious to manually go through every single call."

— Neo, 14:01

"The attacker is able to pull off this attack by having access to more classes than the programmer intended to be instantiated at this deserialization point."

— Andreas, 10:00

"The exploit chain did not execute. And now we can learn from this quiz."

— Neo, after QUACK-patched Moodle blocks the exploit, 26:07

Key Takeaways

  • PHP's allowed_classes parameter is the right defense mechanism — it is already in the language, it works, and QUACK automates the hard part of using it correctly.
  • Zero classes needed at 12 of 15 vulnerable call sites — in most real applications, unserialize() is being used where json_decode() or a typed constructor would suffice; QUACK identifies this automatically.
  • 97% method elimination blocks automated exploit generation — even when the allowed class set is non-empty, restricting it to the developer-intended types destroys the POP chains attackers rely on.
  • QUACK is open-source and accepts community input — if you maintain PHP applications and are interested in testing coverage gaps, the GitHub repository is the right place to engage.
  • Data-only deserialization attacks remain an open problem — when the allowed class itself exposes dangerous functionality, QUACK cannot help. Applications should still follow the principle of least privilege in class design.

Slides PDF: Not available for this session.

Reviews

Dr. Zero (Offensive Security Researcher) — SOLID

Solid academic work automating the boring part of PHP deserialization defense. The 97% method elimination and 100% exploit block numbers are credible, and the duck typing inference approach is genuinely clever. But this is a PHP deserialization paper — a decade-old vulnerability class — and it won't change anyone's life who isn't maintaining a legacy PHP codebase.

Heather Calloway (CISO) — SOLID

Brown and Columbia University released QUACK, a static duck typing defense against PHP deserialization attacks that reduces available gadget chains by 97% without requiring source code modification. Concrete defensive tool for a real and persistent attack class. Limited governance story beyond PHP's continued presence in legacy infrastructure.

→ Top-rated talks at Black Hat USA 2025

All talks from Black Hat USA 2025