thinkphp任意文件删除典型触发点在unlink()调用处,核心是用户可控输入(如反序列化$this->files)未经路径规范化与白名单校验直接传入,常见于windows.php的__destruct()→removefiles()链。

thinkphp 任意文件删除的典型触发点在哪
核心就在 unlink() 调用处,且参数直接或间接来自用户输入,未做路径规范化与白名单校验。常见位置包括:控制器中接收 filename、filedelurl、path 等参数后拼接路径;反序列化链中 __destruct() 或 __wakeup() 方法调用 unlink() 时使用了未过滤的 $this->files 或类似成员变量。
比如 ThinkPHP 5.1.37 的 thinkprocesspipesWindows.php 中,removeFiles() 直接遍历 $this->files 并执行 @unlink($file),而 $this->files 是 private 属性,可通过反序列化控制——这就是典型的“可控输入 → 无校验 → unlink”链。
如何快速定位 unlink 的危险调用
别靠肉眼翻,用命令行精准筛:
-
grep -rn "unlink(" --include="*.php" ./thinkphp/—— 找所有显式调用 -
grep -rn "$this->.*files" --include="*.php" ./thinkphp/ | grep -i "unlink|remove"—— 锁定类属性参与删除的场景 -
grep -rn "__destruct|__wakeup" --include="*.php" ./thinkphp/—— 重点看销毁/唤醒时是否触发文件操作
注意:ThinkPHP 6+ 中很多删除逻辑已下沉到模型或命令行类,也要扫 app/command/ 和 app/model/ 下的自定义方法,不能只盯 controller。
为什么 realpath() + is_writable() 不足以防御任意删除
这两个函数常被误认为“安全屏障”,但实际漏洞频发,原因很实在:
-
realpath()对不存在路径返回false,但攻击者可先构造一个存在的父目录(如/var/www/html/uploads/),再用../../etc/passwd绕过——realpath('../../etc/passwd')在当前工作目录下可能解析失败,但若当前目录是/var/www/html/uploads/,则解析成功 -
is_writable()检查的是最终路径的权限,不是原始输入的合法性;攻击者删的是/tmp/foo,只要该路径可写就过检,跟业务是否该删它完全无关 - 更隐蔽的是符号链接绕过:
ln -s /etc/shadow /var/www/html/uploads/link,然后传link,realpath()返回真实路径,is_writable()返回 true,unlink()照删不误
修复时最容易忽略的边界情况
加白名单、限制目录前缀只是基础,真正卡住利用的往往是下面三点:
- 没处理 Windows 路径分隔符:
str_replace('\', '/', $input)必须在规范化前做,否则c:windows..oot.ini会逃逸 - 没拒绝空字节和 URL 编码绕过:
%2e%2e%2f、..%c0%af这类编码在进入 PHP 前可能被 Web 服务器解码,必须在第一步就urldecode()并清理 - 没校验路径是否落在预期根目录内:
if (strpos(realpath($path), $allowed_root) !== 0)比str_starts_with()更可靠,因为后者不处理符号链接展开后的实际路径
最麻烦的是反序列化入口——这类漏洞往往藏在日志、缓存、队列等非 HTTP 入口里,unlink 调用不在控制器里,而在底层类销毁时发生,审计时容易漏掉。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











