教程
🛡️

网络安全

Web 安全、渗透测试、密码学与安全合规的系统化教程。

ThinkPHP 框架审计:路由、控制器与那些年爆过的洞

ThinkPHP 是国内用得最广的 PHP 框架,也是审计高频目标。这篇讲清它的路由解析、控制器调度、SQL 构造、模板引擎的运作方式,并复盘版本差异导致的 RCE、SQL 注入、变量覆盖等经典漏洞,给出一套可复用的 ThinkPHP 审计路线。

· 更新于 2026-07-19 阅读量 --

ThinkPHP(简称 TP)是国产 PHP 框架里生态最庞大的一个,大量政企系统、CMS、业务后台都基于它。框架把很多底层逻辑封装好了,审计时就不能像审裸 PHP 那样只盯危险函数,而要先懂框架的路由怎么解析、控制器怎么调度、SQL 怎么构造,才知道哪里会被绕、哪里框架本身就埋了雷。这一篇就带你拆 ThinkPHP。

本文所有手法仅用于你对自有或已授权的 ThinkPHP 代码做安全评估。

先懂框架的骨架

ThinkPHP 的请求生命周期大致是:入口 index.php → 加载框架 → 解析路由 → 定位到某个控制器(Controller)的某个方法(action) → 执行业务 → 返回。审计前先找到这几样:

  • application/(老版本)或 app/(新版本)下的模块、控制器、方法结构。
  • 路由配置:route/route.phpconfig/route.php,看有没有自定义路由规则。
  • 全局配置:config/app.phpdatabase.php,留意 app_debugurl_param_type、数据库前缀等。

理解骨架的好处是:你能快速判断”用户请求 ?s=/module/controller/action 会落到哪段代码”,这是 TP 审计的基本功。

路由解析与参数注入

ThinkPHP 支持 pathinfo 形式的 URL,比如 ?s=/index/Index/test/name/xxx,框架会把 name/xxx 解析成参数。这里有两个经典坑:

  • 版本差异导致的解析差异。不同 TP 版本对路由、参数绑定的处理不同,某些旧版本在解析时存在变量覆盖未授权访问(比如直接访问本应登录后才可调的内部方法)。
  • method 覆盖。一些版本允许通过 _method 参数覆盖请求方法,配合框架的 CSRF/鉴权钩子,可能绕过部分校验。

审计时,先确认目标 TP 版本(看 thinkphp/base.php 里的 THINK_VERSIONcomposer.lock),再对照该版本已知的修复点去代码里找”是否还残留旧写法”。

SQL 注入:构造方式决定打法

ThinkPHP 的数据库层既有查询构造器(链式 where),也支持原生 query/execute。注入风险分两类:

  • 构造器使用不当:比如 where("name=$name") 直接拼字符串,或 where 的数组条件里键名可控(where([$field => $value])$field 来自用户输入),导致字段名注入。
  • exp 表达式注入:旧版本里 where('name', 'exp', $input) 会把 $input 原样拼进 SQL,若 $input 用户可控就是直接的 SQL 注入。
  • order/group 拼接:排序、分组参数常直接拼 SQL,预处理管不到,易被注入。
// 危险写法:字段名来自用户输入
$where[$_GET['field']] = $_GET['value'];   // field 可控,可注入
Db::table('user')->where($where)->select();

审计 SQL 注入,重点搜 Db::->wherequery(execute(,逐个看参数是否经过构造器或严格预处理。

模板引擎与 RCE

ThinkPHP 的模板引擎(旧版 ThinkPHP\Template,新版 view 驱动)支持模板标签,部分版本在解析模板时存在代码执行风险:比如用户输入的内容被直接当作模板解析(如留言板内容里写了模板标签),或缓存文件可被包含。更经典的,是某些版本的 display/fetch 接收用户可控的模板名,导致文件包含 + 模板解析 = RCE

此外,老版本 TP 爆过多个直接 RCE:例如通过特定 payload 触发框架内部的 call_user_func 调用链、或 Request 类的 filter 被覆盖后执行命令。这些洞往往和”框架版本过旧 + 未打补丁”强相关。审计老系统,先对版本,再找补丁是否打了。

反序列化在 TP 里的身影

ThinkPHP 自身的一些类(如 RequestModel)实现了魔术方法,历史上被构造出反序列化 POP 链:当代码里存在 unserialize(用户可控输入) 的入口(哪怕只是某个缓存、Session 读取点),攻击者就能用 TP 自带类串起一条执行链,最终调用 system 之类。审计反序列化,先找 unserialize 入口,再结合 TP 版本对应的已知 POP 链去匹配类。

一套可复用的 TP 审计路线

  1. 定版本:从 composer.lock 或框架常量确认 TP 版本,列出该版本历史 CVE。
  2. 看路由与鉴权:路由配置 + 全局中间件/控制器基类里的登录校验,判断是否可未授权访问。
  3. 搜危险点Db::wherequeryunserializeincludeeval、模板名可控处。
  4. 追参数流:对每个可疑点,看参数是否用户可控、有无过滤。
  5. 对已知链:用版本对应的 RCE / POP 链做匹配验证。
  6. 动态复现:本地起同版本环境,构造请求验证危害。

ThinkPHP 安全配置与上线清单

审完代码,还要看项目有没有把框架的安全开关打开。一份实用的 ThinkPHP 上线前核查清单:

  • 关闭调试模式app_debug 必须为 false。调试模式会暴露错误栈、数据库账号、路由细节,是信息泄露重灾区,更是某些版本 RCE 的触发前提。
  • 收紧路由:用显式路由替代全自动路由,避免用户能直接访问本应受保护的内部方法;对不需要的模块做 deny 配置。
  • 强制 HTTPS 与全局过滤:开启全局输入过滤,敏感字段在服务端二次校验,不信任任何前端传入。
  • 数据库用预处理:统一走参数化查询,杜绝字符串拼接 SQL,连 order by 的字段也做白名单。
  • 升级到受支持版本:老版本(如 5.0.x 早期、3.2.x)已知漏洞多,应升级到官方仍在维护的分支并及时打补丁。
  • 文件上传严格校验:后缀用白名单、存到非执行目录、重命名,避免 webshell 落地。
  • 日志与缓存目录不可外访:确保 runtime/ 不能被直接通过 Web 访问,防止日志投毒或配置泄露。

把这清单和代码审计结合:代码里发现的每个可疑点,再对照配置看”在当前设置下是否真能利用”。配置到位,很多中危问题会被直接降级甚至消除;配置缺失,一个低危写法也可能被放大成严重事件。

如何练习 ThinkPHP 审计

光看不练记不牢,练习环境随处可得。你可以下载官方历史版本(如 5.0.x、3.2.x)故意留洞的示例,或找明确授权的涉及 ThinkPHP 的 CTF 题目,也可以用各大漏洞平台提供的靶场。练习时建议按这个节奏:先定版本、列出该版本已知漏洞,再照着描述去源码里找对应写法,理解它为什么会被利用;然后尝试自己构造请求复现;最后思考”如果我是开发者,该怎么改”。把”打穿”和”修好”都做一遍,理解才扎实。等你在授权环境里把几个版本的 ThinkPHP 都摸透,再面对真实项目时,看到路由和控制器就会本能地知道该先查哪几处。

补充一个实战提醒:审 ThinkPHP 时,别忘了看它的缓存与日志。框架会把模板编译结果、数据库查询结果缓存到 runtime/ 目录,如果权限配置不当,攻击者可能直接读取这些缓存文件拿到源码或数据;日志文件若记录了完整请求(含令牌、密码),也是信息泄露点。审计配置文件时顺手确认 runtime 目录不可通过 Web 直接访问,日志不要记录敏感字段。这些”非代码”的细节,常常比一行危险函数更致命。

这一篇你该记住的

  • 审 ThinkPHP 先懂骨架:入口 → 路由解析 → 控制器方法 → 返回,先找模块/控制器/路由配置。
  • 确认 TP 版本再审计,旧版本常残留已修复的 RCE、SQL 注入、变量覆盖写法。
  • SQL 注入重点看 Db::where/query/execute,尤其字段名可控exp 表达式、order/group 拼接。
  • 模板名可控 + 文件包含可升级为 RCE;老版本多个直接 RCE 多与未打补丁有关。
  • 反序列化要找 unserialize 入口,结合 TP 自带类的 POP 链 验证。

下一篇我们脱离框架,看真实 PHP 业务代码与 CMS 里那些”逻辑层”的漏洞——越权、支付篡改、鉴权绕过,这些往往比命令执行更隐蔽也更致命。