PHP magic methods as sinks
TL;DR: PHP’s
__wakeup,__destruct,__toString,__call,__get,__set,__invokeare auto-invoked by the runtime — they’re the entry hooks every POP chain uses to transform a deserialised object graph into method calls and eventually RCE.
What it is
Magic methods are special class methods that PHP calls implicitly on certain events — object construction from a stream, casting to string, method/property access on unknown names, invocation of an object like a function. When attackers control object data via unserialize, the first instruction they get to run is whichever magic method PHP fires automatically. Identifying these in app + dependency code is the start of every PHP POP-chain hunt.
Preconditions / where it applies
- PHP source / Composer dependencies — including vendored frameworks (Laravel, Symfony, Guzzle, Monolog)
- A reachable
unserialize, phar metadata sink, or__unserializeconsumer — see php-deserialization-gadgets - PHP 5.4+ (most magic methods);
__unserializeand__serializeadded in 7.4
Technique
The lifecycle and the magic that fires:
| Trigger | Magic method called |
|---|---|
unserialize($data) end-of-object | __wakeup() (always); __unserialize($arr) if defined (PHP 7.4+, takes precedence over wakeup) |
| Object destruction / script end | __destruct() |
(string)$obj, echo $obj, string concat, strlen($obj) | __toString() |
$obj->unknownProp (read) | __get('unknownProp') |
$obj->unknownProp = x (write) | __set |
$obj->unknownMethod($args) | __call('unknownMethod', $args) |
Klass::unknownStatic($args) | __callStatic |
$obj($args) (call object) | __invoke |
isset($obj->p) / empty(...) | __isset / __unset |
clone $obj | __clone |
var_export($obj) | __set_state |
Why each matters for chains:
__wakeup/__destruct— guaranteed-to-fire entry points. Almost every published gadget starts here.__toString— fires whenever the object reaches a string context. If a__destructcallserror_log($this->msg)and$this->msgis an object you control, you’ve pivoted to__toString.__call— used to pivot from a wrapper class (e.g.Symfony\...\LazyChoiceList::__call) to an arbitrary method on an embedded object.__get— read-side property dispatch; used in chains where the controlled object’s property is read into a sink (e.g.Twig\Template::displayBlock).__invoke— turns a property that is “called” later into arbitrary code; pairs well witharray_map($cb, $arr)if$cbis a controlled object.
Concrete chain pattern:
1
2
3
4
5
class Pwn {
public $cmd;
public function __destruct() { system($this->cmd); }
}
// payload: unserialize('O:3:"Pwn":1:{s:3:"cmd";s:2:"id";}');
Real chains do not have such convenient classes; they bridge through __toString → __call → __invoke to reach a call_user_func or eval in a framework class.
Tools: PHPGGC (PHP Generic Gadget Chains) implements published chains for Laravel, Symfony, Guzzle, Monolog, Slim, Doctrine, CodeIgniter etc.:
1
2
phpggc Laravel/RCE9 system id | base64 -w0
phpggc Monolog/RCE1 'system' 'id'
Detection and defence
- Never
unserializeuntrusted input — usejson_decodeorigbinary; if you must, set theallowed_classesoption to a strict allowlist (unserialize($s, ['allowed_classes' => [SafeDTO::class]])) __wakeup/__destructshould not call methods on instance properties whose type is not enforced viadeclare(strict_types=1)+ typed properties- Run Psalm/PHPStan with
unserializeForbiddenTyperules - Snuffleupagus’
sp.unserialize_hmac.enable=1ties serialized blobs to an HMAC so attacker-crafted strings won’t deserialise - See php-deserialization-gadgets for chain construction, dangerous-php-sinks for sinks reachable from magic methods
References
- PHP manual — magic methods — official list and semantics
- PHPGGC — gadget chain collection
- HackTricks — PHP deserialization — magic-method-driven chains
- Ambionics — PHP unserialize research — modern gadget research blog