How PHP Deserialization Attacks Work
PHP’s unserialize() reconstructs objects from attacker-controlled strings. The attacker controls not just property values but the entire type of the deserialized object, and they can nest objects inside objects. That’s the foundation of a Property-Oriented Programming (POP) chain: a top-level object with a magic method kicks off a sequence of calls across classes the developer never intended to instantiate, eventually reaching a sink like system(). Neo walked through the Moodle CVE exploit live demo (10:46), showing an XML upload that triggers the full chain and prints arbitrary output on screen.
Why Existing Mitigations Fall Short
Three mitigations exist. Switching to JSON works for simple data but breaks complex PHP objects. HMACing the serialized payload only applies when the application itself generated the data. The third option, allowed_classes, restricts unserialize() to a named list of classes and directly defeats POP chains. But:
“only around 0.1% of calls to un serialize in GitHub repositories even use this argument” — Neophytos Christou
Most developers don’t know the attack vector, and auditing an existing codebase to determine which class each call intended is slow work.
QUACK’s Static Duck Typing Approach
QUACK runs two passes over PHP source. First, it walks the include graph around each unserialize() call to build the set of classes PHP could load at that point. Second, it tracks how the deserialized object is actually used: method calls, property accesses, and instanceof checks each narrow the candidate types. That’s static duck typing. QUACK intersects the two sets and outputs a JSON file specifying which class each call should allow. Neo then showed QUACK’s defense demo on Moodle CVE (24:06): one line 93 edit later, the same exploit uploaded cleanly with no system() firing.
Evaluation Results and Attack Surface Limits
QUACK was tested on 11 PHP applications with real CVEs, covering 15 vulnerable unserialize() calls. It blocked 97% of the methods an attacker could reach across all 15. For 12 of those calls, the allowed set was empty: the developer had no reason to call unserialize() at all. An automated exploit-generation tool that built working exploits for five of the CVEs produced zero after QUACK’s patches. One blind spot remains: data-only attacks, where an attacker stays within the permitted class but sets a property like is_admin = true, fall outside QUACK’s scope entirely.
Q&A
Could type annotations or a type checker catch invalid deserializations before they reach runtime? Yes, QUACK already uses PHP type annotations as hints in its inference, and the team noted that more annotation-heavy codebases produce tighter allowed_classes sets. ▶ 32:44
Notable Quotes
only around 0.1% of calls to un serialize in GitHub repositories even use this argument Neophytos Christou · ▶ 15:01
we were able to block all methods for 12 out of these 15 Neophytos Christou · ▶ 27:15
Key Takeaways
- PHP deserialization attacks succeed when attackers access more classes than developers intended to allow.
- QUACK uses static duck typing to automatically infer a minimal allowed_classes set for each unserialize call.
- On 11 real CVE-bearing apps, QUACK blocked 97% of attacker-reachable methods and defeated all auto-generated exploits.