ThinkPHP的SQL注入问题
0x00 前言
由于之前对于PHP的了解仅限于原生PHP,因此最近利用空闲时间学习了thinkphp3和thinkphp5两个版本的框架,学习后发现两个版本差别还是挺大的,比如:
1、在tp3中Index控制器类命名为IndexController.class.php,在tp5中则为简单的Index.php
2、在tp3中的单字母函数被tp5中提供的助手函数给替代了,如模型层操作时:
在tp3中为:
M('user')->where(["username" => $username])->find();
在tp5中则为:
db('user')->where("username", $username)->find();
3、使用 /id/1001 方式传参时:
在tp3中可以从GET参数中获取:
$data = $_GET['id'];
在tp5中,则改为了使用Request对象的param()方法获取:
$data = $request -> param();
除了这些,还有很多其他的变化,这里就不一一列举了。
虽然tp3和tp5的差别很大,但是对于代码审计人员来说,语法上的差别是可以很快适应的,我们关注的重点应该是安全方面的变化。因此本篇记录下学习到的tp框架中一些常见的sql注入问题。由于刚开始学习tp框架,所以总结的可能不够全面,后面学习到新的再补充。
0x01 where()方法 + exp表达式
1.1 漏洞代码
如下是在tp3.2.5版本中的漏洞代码:
<?php
namespace Home\Controller;
use Think\Controller;
class HelloController extends Controller
{
public function sqli()
{
// $username = I('username');
$username = $_GET['username'];
$data = M('user')->where(["username" => $username])->select();
dump($data);
}
}
产生漏洞的代码是从$_GET[]数组中获取参数的,当使用I()方法获取参数时,该代码是不存在漏洞的,原因后面会讲到。
1.2 漏洞利用
1、当向上面写的controller发送的参数为username=1时,查询结果如下:
2、当发送的参数为 username[0]=exp&username[1]==1 or 1=1时,即可查询到user表中的所有数据:
1.3 漏洞原理
参考ThinkPHP3.2开发手册中对于where()方法的介绍即可知道原因:
由此我们可以知道在上面发送username[0]=exp&username[1]==1 or 1=1参数后,tp3会将 =1 or 1=1 字符串直接拼接到sql语句中,从而产生了注入,在后台实际执行的sql语句为:
SELECT * FROM `user` WHERE `username` =1 or 1=1
前面说到使用$username = I('username');
方式接收参数时不存在漏洞,通过打断点知道,使用I()方法接收参数时,“exp”会变为“exp ”,后面多了一个空格导致表达式出错。
1.4 漏洞修复
如下为在tp5版本中测试的结果:
可以看到,tp5中的where()方法的实现发生了变化。
在tp3中的实现是只接收一个参数,参数值可以是一个一维数组,也可以是一个二维数组;
而在tp5中则是通过三个形参分别接收字段名、查询表达式、查询条件的值,从上面的调试信息中可以看到每个参数值的情况。
但通过测试发现,如果字段名的值为一个二维数组时,tp5还是会跟tp3一样将数组中的值作为查询表达式和查询条件进行解析的,但是会报如下错误:
在报错处设置断点调试后发现,当查询表达式为exp时,tp5会判断查询条件的值是否为Expresion类型的对象,这里没有通过判断,因此抛出异常。所以在tp5中使用上述代码是不存在注入问题的:
但是在tp5中使用如下代码时会存在注入问题,这种情况可以理解为就是直接进行了字符串拼接:
<?php
namespace app\index\controller;
use think\Controller;
use think\Db;
class Hello extends Controller
{
public function sqli()
{
$username = input('username');
$data = db('user')->where("username", "exp", $username)->select();
dump($data);
}
}
有个疑问就是这里为什么没有像上面那样抛出异常。首先可以判断的是这时的查询条件的值是Expresion类型的对象,调试后发现在如下代码处,使用raw()函数处理了查询条件:
raw()函数实现如下,可以看到将查询条件封装为了Expression类型的对象,因此没有抛出异常:
0x02 数据查询方法参数可控导致可拼接操作符
2.1 漏洞代码
如下是在tp3.2.3版本中的漏洞代码,将其中的find()方法换成select()、delete()等方法也是可以的,测试多个版本后,发现在大于3.2.3的版本中该漏洞已被修复:
<?php
namespace Home\Controller;
use Think\Controller;
class HelloController extends Controller
{
public function sqli()
{
$username = I('username');
$data = M('user')->find($username);
dump($data);
}
}
2.2 漏洞利用
1、当向上面写的controller发送的参数为username=35时,查询结果如下,可以发现查询到的数据是user表中的主键id值为35的数据。可以知道如果传给find()方法的参数值为i,则会查询对应表中的主键值为i的记录:
2、当发送的参数为username[where]=1=1时,看到可以查询到一条记录,该记录是我创建的user表中的第一条记录。其实这个时候已经查询到了user表中的所有记录,但由于find方法的实现中添加了limit=1的条件限制,因此每次只能查询到一条记录(如果将find()修改为select()方法,则可以返回所有记录):
3、还可以通过发送username[where]=1 and updatexml(1,concat(0x7e,user(),0x7e),1) 使用报错注入来进一步攻击,攻击手法很多,不多说了:
2.3 漏洞原理
单步调试后发现发送的请求参数是数组时,其中包含的键为where对应的值会被作为where条件拼接到sql语句中,因此产生了注入问题:
从ThinkPHP3.2开发手册或源码中可以知道将username[where]=1=1中的where替换为order、group等操作符也是可以达到同样的攻击效果的。
2.4 漏洞修复
1、在tp3.2.4中该漏洞就被修复了,使用相同的测试代码在tp3.2.4中的测试结果如下:
2、调试后发现,原来在tp3.2.3中时,_parseOptions()方法会解析用户发送的数组中包含的where条件:
3、而在tp3.2.4中,_parseOptions()方法不但不会解析用户发送的数组中包含的where条件,而且会覆盖$options变量的值,因此用户发送的where子句将不会被拼接到sql语句中:
0x03 where()方法 + bind表达式 + save()方法
3.1 漏洞代码
如下是在tp3.2.3版本中的漏洞代码,在大于3.2.3的版本中,如果把I()方法换成$_GET[]等同类方法时,依然存在漏洞,利用方法后面会讲到:
<?php
namespace Home\Controller;
use Think\Controller;
class HelloController extends Controller
{
public function sqli()
{
$id = I('id');
$username = I('username');
$data = M('user')->where(['id' => $id])->save(['username'=>$username]);
dump($data);
}
}
3.2 漏洞利用
1、如下正常访问上述的controller时,会修改user表中的id为33的username字段为admin:
2、当发送参数username=admin&id[0]=bind&id[1]=0 and updatexml(1,concat(0x7e,user(),0x7e),1) 时,则可通过报错注入来进行攻击:
3.3 漏洞原理
1、该漏洞与第1章中的利用where()方法的EXP表达式进行注入的原理类似,只不过这里换成了bind表达式,下面可以看到tp3.2.3直接将上面发送的请求中的id[1]的值直接拼接到了sql的where子句中,从而造成了注入问题:
2、最终生成的sql语句如下,生成的sql语句进行了预编译处理,其中:0的值即为上面发送的username的值“admin”:
3.4 漏洞修复
1、与第1章中的修复方法一样,使用I()方法接收参数时,“bind”会变为“bind ”,多了一个空格,致使表达式匹配出错:
2、那么把代码改成从$_GET[]数组中获取参数呢?这时如果直接使用上面的payload会出现如下错误,发现:0占位符没被替换:
3、分析源码后,发现tp3.2.4在预编译绑定参数时,占位符的名字在前面拼接了字段名+下划线:
4、因此将payload改为username=admin&id[0]=bind&id[1]=username_0%20and%20updatexml(1,concat(0x7e,user(),0x7e),1),即可注入成功:
5、那么在tp5中呢,会报如下错误,通过分析发现,在tp5中,已经移除了bind表达式,因此无法利用该漏洞。
0x04 order()方法
4.1 漏洞代码
如下是在tp3.2.3版本中的漏洞代码,在tp3.2.4中已修复:
<?php
namespace Home\Controller;
use Think\Controller;
class HelloController extends Controller
{
public function sqli()
{
$order = I('order');
$data = M('user')->order($order)->select();
dump($data);
}
}
4.2 漏洞利用
直接发送order=updatexml(1,concat(0x7e,user(),0x7e),1) 参数,即可实现报错注入:
4.3 漏洞原理
原理很简单,就是将用户输入直接拼接到了order子句中:
4.4 漏洞修复
如下可以看到在解析order by子句时,对数组类型和其他类型都进行了校验,致使无法使用包含括号的order by子句:
0x05 总结
在实际的项目中:
第2种 数据查询方法参数可控导致可拼接操作符 类型的漏洞场景应该是很难遇到的,相对鸡肋。
第1种 where()方法 + exp表达式 类型的漏洞出现频率较高,应格外注意。
第3种 where()方法 + bind表达式 + save()方法 和 第4种 order()方法 偶尔也会遇到,也需要注意下。
0x06 参考文章
https://xz.aliyun.com/t/2812#toc-8
https://www.freebuf.com/vuls/236421.html