ThinkPHP 之 SQLI审计分析(三)
Time:9-23
影响版本:ThinkPHP=5.1.22
Payload:
/public/index.php/index/index?orderby[id`|updatexml(1,concat(0x7,user(),0x7e),1)%23]=1
0x00 测试代码做了什么
<?php
namespace app\index\controller;
class Index
{
public function index()
{
$orderby = request()->get('orderby');
$result = db('users')->where(['username' => 'mochazz'])->order($orderby)->find();
var_dump($result);
}
}
以GET方式从orderby中获取参数值,查询users表中username=mochazz,再用order方法添加排序语句最后find获取结果。
request类是在thinkphp/library/think/Request.php中定义的,其get方法:
image-20210923105334245
并没有什么比较关键的东西,之前在研究(username/a)的a是什么意思时,对get到input方法的调用处理流程也分析过,最终的过滤代码是在thinkphp/library/think/Request.php的1408行的filterValue函数,但是由于仅仅是一个框架,并没有添加自定义的过滤器进行过滤。所以这边基本上是传入什么值就获取什么值。未来如果没有特殊情况就不用分析测试代码的作用了。
0x01 分析调用链
- where()方法
emm,这个版本的parseWhereExp方法与之前分析过的5.0.10版本的不太一样,所以还是学习一下吧。
image-20210923112437413
1474-1477行像是修了个小BUG的样子,之所以会这么想是因为getOptions方法是这么写的:
image-20210923112620567
总的来说感觉大概是防止为空时出错吧。不重要。
但有了5.0.10版本分析的经验,知道parseWhereExp方法中主要看的是if-else语句块。这段代码如下:
image-20210923113247824
因为$field是一个数组($field就是测试代码中where中的参数值)所以直接跳到1495行看起,进入parseArrayWhereItems方法。并且,在parseArrayWhereItems处理完后就会直接return了。
image-20210923114535150
1564行if条件满足,foreach拆分username和mochazz,之后会进入1571行的else中对$where数组进行赋值,这里三元运算因为mochazz不是数组,所以第二个元素最终为’=’,然后到1580行的if,也是简单的取值赋值操作。
where方法分析完毕,好像也没什么不同。区别在于5.0.10中没有看parseArrayWhereItems方法,这次补上了。下断点看一下处理后的最终结果:
image-20210923120020929
- order()方法,本次的主角
thinkphp/library/think/db/Query.php的1823-1864行,因为测试代码给定参数的特殊性,程序不会进入部分代码,也为了舍去不必要的篇幅,直接来到1853行开始看起:
image-20210923120807266
1854行,既然where在options中是个数组,那么order在options也应是个数组,很合理。
1857行,判断为数组,之后也是用array_merge进行简单的赋值操作。还是下断点看看执行完的情况:
image-20210923124524845
自始至终没有任何有效的过滤存在意味着where和order值都是可控的。但是具体怎么触发漏洞还是没有个清晰的逻辑,往后看find的执行流程吧。
。。。
实际上,也没什么看头,基本上是在解析$this->options中的值,然后在3041行调用另一处的find方法,而后执行的代码与前几次分析SQLI漏洞的流程基本一致。em先下个断点看一下SQL语句构造完成后是什么: 最终在thinkphp/library/think/db/Connection.php中find方法的826行生成的SQL语句。
image-20210923123045070
明显可以看到是通过反引号闭合id前一个反引号导致的注入攻击。
既然是order by之后的注入,那么漏洞点一定和select方法中调用的parseOrder这个方法有关系:
有了以往的经验,知道具体代码都在thinkphp/library/think/db/Builder.php中,来到847行的parseOrder
image-20210923123938698
阅读855-871行,暂时不看程序不会进入的代码直接来到最后一个else,863-869都没有什么特殊的地方,870会把$sort置为空,871行的parseKey方法很关键,跟进。
image-20210923125813142
它的作用是获取key的值。但是下断点步进发现,调用的实际上不是Builder.php中的parseKey而是Mysql.php中的parseKey,是因为自始至终Mysql都是extends于Builder的,前面一直在Builder中分析代码是因为Mysql中没有写相应的函数,自然就向父类查找了,但是与以往不同的是,parseKey是在Mysql中定义好的。(涨记性,有经验了也得根据实际情况来。)
来到实际执行的parseKey:
thinkphp/library/think/db/builder/Mysql.php的113行:
image-20210923131035953
image-20210923131104960
已知的payload并不满足123行的if,直接会到143行处的if,因为$strict是写死的true,这个if必然会进入,然后执行的操作是往$key的两边直接拼接加反引号,这个时候想到payload的形式,毫无疑问漏洞点就在这里。
最后,将return的$key拼接上$sort,再在parseOrder中的875行拼接前缀ORDER BY,SQL注入就构造好了。
0x03 总结
这次分析并没有那么细,也没有想太多,只是根据payload和测试代码的流程过了一遍。并不是严格意义上的白盒审计,很多程序没有进入的代码并没有认真去分析,不知道其他代码做了什么就很可能还存在漏洞点。还是一样的感觉:目前所学习的代码审计就是一个跳过某段代码和进入某段代码的过程,也就是所谓的“链”。至于绕过过滤这些,感觉不是代码审计的重点,只能算是一种奇巧的思路吧。
加油。
点击关注,共同学习!
[安全狗的自我修养](https://mp.weixin.qq.com/s/E6Kp0fd7_I3VY5dOGtlD4w)
[github haidragon](https://github.com/haidragon)
https://github.com/haidragon