Skip to content
Kurisu
Go back

PHP 反序列化进阶:Session 不一致与 phar

之前写过 PHP 反序列化 POP 链基础和 ThinkPHP 5.1.37 链复现。这篇补三个实战问题:没有 unserialize 入口(Session/phar)、payload 被正则拦(回溯绕过)、payload 落地(INTO DUMPFILE / 恶意 MySQL)。

0x01 Session 反序列化:引擎不一致

PHP 的 session 存储与读取各有一套序列化引擎,由 session.serialize_handler 控制:

handler存储格式
php(默认)键名|序列化值
php_serialize纯 serialize() 结果
php_binary长度前缀 + 键名

条件:写入与读取用了不同 handler。 经典组合是 php_serialize 写、php 读——php 用第一个 | 分隔键名和值,值里注入 | 就能让后半段被整体当序列化串解析:

写入(php_serialize):a:1:{s:4:"test";s:30:"O:4:"User":1:{s:4:"name";s:3:"cmd";}"}
读取(php)          :键名 = a:1:{s:4:"test";s:30:"O:4:"User  ← 值 = 剩余部分,直接 unserialize

注入口子:session.upload_progress.enabled=On(默认开)时,带 PHP_SESSION_UPLOAD_PROGRESS 字段的上传请求会把值原样写进 session,无需应用主动写入。

触发面:应用有文件包含(include($_GET[...]))能包含 session.save_path 下的 session 文件(默认 /var/lib/php/sessions/sess_<PHPSESSID>),对象链在解析时执行。

排查三行:两个 handler 是否一致?有没有 upload_progress 可写?有没有包含 session 文件的路径?

0x02 phar 反序列化:不需要 unserialize 函数

没有 unserialize 入口时的主力:phar 的 manifest 里 metadata 是 PHP 序列化格式,而绝大多数文件操作函数处理 phar:// 流时都会自动反序列化它:

// 攻击者把恶意对象放进 phar 的 metadata,上传图片马式的 phar 文件
// (文件头可以是任意内容,只要能过图片校验)

file_exists("phar://uploads/avatar.jpg");   // ← 触发 metadata unserialize
// 同样触发的还有:file_get_contents / fopen / is_dir / filesize /
// copy / unlink / stat / filemtime ... 一大批

三步:造(PHP 生成带恶意 metadata 的 phar,改后缀伪装图片上传)→ 找(搜”文件操作函数 + 用户可控路径”)→ 触发(路径前加 phar://)。意义是把反序列化从”显式 unserialize 调用”扩展到”任何文件函数”,攻击面翻一个量级。

0x03 ThinkPHP 5.1.x 链的关键节点

5.1.3 ~ 5.1.37 的链之前写过完整复现,这里只记认功能不认类名的骨架,好在混淆/二开代码里认出来:

入口对象 __destruct
  → 触发 __toString(如 file_exists 的参数被当字符串)
  → 模型序列化路径:toJson → toArray → getAttr
  → getCatch / filterValue($value, $key, $filter)
  → call_user_func($filter, $value)      ← $filter=system, $value=命令
  → RCE

两个可控点:$filter、$value 各来自对象某属性(常见 think\request 的 filter 链),构造 POP 就是让二者在链尾相遇于 call_user_func/array_walk;5.2 换了中间节点但骨架一样。

0x04 组合拳:正则回溯 → INTO DUMPFILE → 恶意 MySQL

把前面零件串起来的完整链,场景:注入点在,但 payload 过不了 WAF 正则。

① 正则回溯绕过过滤

preg_match 的回溯上限 pcre.backtrack_limit(默认 100 万):payload 前垫大量触发回溯的字符(连续 a 配 .*? 贪婪匹配),超限后 preg_match 返回 false 而非匹配失败——代码把 false 当”没匹配”,过滤失效:

payload = "a" * 1000000 + "恶意内容"
preg_match($black_regex, $payload) === false   // 代码误判为"安全"

② INTO DUMPFILE 落地 phar

注入点活了就用 SELECT ... INTO DUMPFILE 把带恶意 metadata 的 phar 写到 Web 磁盘:写文件必须用 DUMPFILE(单列不转义),OUTFILE 会加换行转义破坏二进制。

SELECT 0x3C3F70687020... INTO DUMPFILE '/var/www/html/uploads/avatar.jpg';

③ 恶意 MySQL 服务器

让目标站的 MySQL 客户端连回攻击者的”假 MySQL 服务端”:服务端握手后发伪造的 LOCAL INFILE 请求,客户端会主动把服务器任意路径的文件读回给服务端(LOAD DATA LOCAL INFILE 语义被反向利用),读源码/配置一气呵成;客户端若还对读入内容反序列化,直接触发对象注入。

拼装思路:回溯破防 → 注入落地文件 → 协议级反向利用,每段都是已知技术,拼起来就是完整 RCE。

0x05 厘清边界:PDO 预编译到底防住了什么

预编译实现分两种:

防不住的三个口子:

  1. 未绑定参数:$pdo->query("SELECT * FROM t WHERE id=" . $_GET['id']) 用了 PDO 却没预编译,等于没防;
  2. ORDER BY / 表名列名:绑定只给值加引号,ORDER BY ? 变成 ORDER BY 'id' 语法失效——排序字段只能白名单,也是最容易漏的地方;
  3. 宽字节:GBK 下 %bf%27 吃掉转义反斜杠,虚假预编译被绕(真实预编译不受影响,所以要”开真预编译”)。

一句话:预编译只保护”值”,不保护 SQL 结构;结构只能白名单。

参考


Share this post:

Previous Post
文件包含到 RCE:filter chain 与内网实战
Next Post
SQL 注入绕过手册:从空格逗号到雷池 WAF