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

  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值