一、防御清单
按"性价比"排序:
1.1 根除"格式错位"(最重要)
Session 反序列化能成立的前提是写与读的 serialize_handler 不一致。只要全链路统一,就断掉了错位解析的物理基础。
-
统一
session.serialize_handler:能全站统一就统一(推荐统一为php_serialize,它格式最规范、不存在逐键|分隔的歧义);各入口/各 SAPI/CLI 与 Web 的 php.ini 要一致; -
不要在部分页面用
ini_set('session.serialize_handler', ...)临时改,改了就极易和别的页面错位; -
更换 session 存储后端(files→Redis 等)时,确认旧数据已清空,避免旧格式文件被新后端读取。
1.2 不要让"用户可控数据"进 session,或至少别原样进
业务上少把请求参数直接 $_SESSION['x'] = $_POST['x'];。若必须存,做校验/白名单/转义,尤其警惕 | 与序列化特征串。
1.3 收敛"任意文件包含"面
Session 反序列化的两条实战链路最终都依赖"能写文件 + 能 include 文件"。堵住 LFI/RFI、对 include 路径做白名单,能同时废掉写马链路与大部分探测。
1.4 会话层面加固
-
session.use_strict_mode = 1:不接受服务器不认识的 session id,增加"自定 id 预测文件名"的难度; -
严格控制 session 文件目录权限(只允许运行用户读写),别放在 web 可访问/可下载的目录;
-
session.save_path不要指向 web 根目录之类能被直接 URL 访问的地方。
1.5 upload_progress 加固(如果确实用不到)
session.upload_progress.enabled = Off
; 或至少
session.upload_progress.cleanup = On ; 确保上传结束立即清
session.upload_progress.freq = 1%
session.upload_progress.min_freq = 1
用不到进度条就干脆关掉,能删掉一个"不需要业务漏洞的 session 写点"。
1.6 代码层纵深
-
反序列化对象做允许类白名单 (PHP 7.4 支持在
session_start()里配?------不对,那是unserialize()的allowed_classes选项;Session 解码默认不过滤类。若可行,用自定义 session handler / 反序列化前做类白名单校验,能挡住未知类注入); -
及时升级 PHP,跟进官方 session 相关修复;
-
安全编码规范里明确"禁止把不可信内容无差别写入 session"这条红线。
一句话记住防御核心:统一 handler 格式 + 用户输入不进 session + 别给文件包含 + 关 upload_progress。
二、审计视角:怎么快速发现这种洞
Step 1 找 session 的"写"与"读"分别用什么 handler
grep serialize_handler / ini_set / session_start(['serialize_handler'
看不同页面是否一致 —— 不一致 = 有戏
Step 2 找"可控数据能否进 session 文件"
a. $_SESSION[...] = $_REQUEST/$_POST/$_GET/可拼接串 ...
b. 无业务写点 → 看 upload_progress.enabled 是否为 On(能否用上传写点)
Step 3 判断写点内容能否承载 '|' + 序列化对象(有没有过滤/转义 '|')
Step 4 找可利用类 / POP 链(危险 __destruct/__wakeup/__toString/...,能 autoload)
注意 php_serialize 写 + php 读 时对象类必须真实存在才会触发
Step 5 构造 payload → 写点提交 → 触发读取 → 验证
CTF 题面上通常给两条线索帮你走完 Step 1:
-
一个文件里
ini_set('session.serialize_handler', 'php_serialize')存了可控值; -
另一个文件/入口只有默认的
session_start()。只要把两者用同一个PHPSESSID串起来,就是完整攻击。
三、CTF 常见题型套路(怎么认出来、突破口在哪)
| 出题套路 | 特征线索 | 突破口 |
|---|---|---|
| 处理器错位(最经典) | 页面 A 用 php_serialize 存用户可控值,页面 B 默认 session_start;提示词常出现 "php_serialize" | 构造 |O:... 注入对象 |
| upload_progress 注入 | 给了上传点但没明显业务写 session;或 hint 提 upload_progress | 文件名/PHP_SESSION_UPLOAD_PROGRESS 字段塞 payload |
| upload_progress + LFI | 存在 include $_GET[file] |
写马 + include sess_<id> getshell |
| 反序列化 POP 链考察 | 给了好几个类/危险函数 | sink 定位 + 串链 |
| 组合题(全链路) | 一个点触发反序列化,要求最终 RCE | 从"注入对象"到"代码执行"要自己串:写点→错位→POP→sink |
做题建议顺序(不管多花哨的题都适用):
-
先读源码,确认 session 相关配置与写点(别一上来就找 POP 链,先确认入口成不成立);
-
用本地同版本 PHP 把 payload 打在本地 session 上,
xxd看文件字节对不对、能不能触发; -
再对着远程提交,利用失败时先回来查"文件字节是否真写对了"。
四、版本与配置差异速查
| 关注点 | 说明 |
|---|---|
php_serialize 处理器 |
PHP ≥ 5.5.4 |
session.upload_progress.* |
PHP ≥ 5.4,默认 enabled=On |
| 解码失败行为 | PHP ≥ 7 报 Failed to decode session object. Session has been destroyed. 并清空 |
__unserialize() 优先于 __wakeup() |
PHP ≥ 7.4 |
| session id 合法字符 | A-Z a-z 0-9 - ,(实测,含下划线会被拒) |
| session 默认文件路径 | Debian/Ubuntu /var/lib/php/sessions;一般发行版 /tmp;可用 session.save_path 改 |
| session 文件命名 | sess_<session_id> |
五、推荐资源(官方为主)
-
PHP 官方手册(中文):
-
Session 配置说明:
https://www.php.net/manual/zh/session.configuration.php -
session.serialize_handler:https://www.php.net/manual/zh/session.configuration.php#ini.session.serialize-handler -
会话上传进度:
https://www.php.net/manual/zh/session.upload-progress.php -
session_start():https://www.php.net/manual/zh/function.session-start.php
-
-
魔术方法、绕过、POP 链两篇。
-
关于"错位注入 /
|分隔"的经典思路,可以搜中文社区关键字:session 反序列化 php_serialize php 竖线注入、upload_progress session 写马 LFI,结合多篇 writeup 对照理解(不同作者对"落盘时机"的描述有出入,以自己实测为准)。 -
练习环境:本地
php -S+ Docker 自建(不要用带非法字符的 session id;跑完记得清理 php -S 后台进程)。
六、最后一张图:全系列知识怎么串成记忆
看一个请求/一个页面 → handler 谁写的?谁读的?
│ 不一致
▼
能把可控字符串(含 | )弄进 session 文件吗?
业务写点? / upload_progress? / LFI 触发?
│ 能
▼
文件里第一个 | 之后的字节 = 你的对象序列化
▼
php_var_unserialize 创建对象 → 魔术方法 → POP 链 → sink(RCE)