0x00 准备工作
源码下载:https://github.com/top-think/thinkphp/archive/3.2.3.zip
在文件中进行配置,我这里是mysql 8.0 ,DH_HOST 那边不添加 3306会连接不上,正常情况 localhost即可
创建好对应的数据库,并且创建对应的数据表
0x01 SQL where 注入
漏洞分析
添加我们的测试代码 ,这里的users对应的是我们的数据表
public function index()
{
// I 参数获取 get中id参数的数值
$data = M('users')->find(I('GET.id'));
var_dump($data);
}
正常情况访问如下即可
参考 Y4er 师傅的文章进行分析 :
https://y4er.com/post/thinkphp3-vuln/
首先传入 1'
走一下流程
这里tp自己的I函数对参数进行了获取 ,我们进行一个跟进
文件位置 : ThinkPHP/Common/functions.php
,前面都是一些根据请求方法的不同来获取参数的语句 ,我们这里直接看到文件第379行,发现TP 利用自身的C函数获取了配置文件中的DEFAULT_FILTER
,全局搜索发现默认的过滤方法是 htmlspecialchars,对输入进行了过滤
查看开发文档发现该过滤器主要是为了防御xss,进行一些html实体化用的,继续往下看
在同文件第443行左右,发现调用了 think_filter
函数,如果我们输入的是数组的话 就对我们数组中的每个元素都用thinkfilter过滤一遍,跟进该函数
同文件 1731行左右 ,发现是一个正则,对一些SQL注中有可能会用到的词进行了置空
处理完之后返回 $data,根据上面的代码发现没有做太多的过滤,我们传入的 1'
还是1'
接下来$data会传给find方法,继续进行一个跟进
文件位置 :ThinkPHP/Library/Think/Model.class.php
785行左右
首先判断我们传入的参数是否数字或者字符串 如果是的话就转成数组, 此时处理后的 $options['where'] 应为 where=>array('id'=>"1'")
接下来的这个函数我们不会进入 ,这个是为了应对多个主键的问题,我这里主键只有 id 所以 $pk就是只是id 如果有多个的话,pk就是数组了就会进入下面这个判断了
继续往下看,发现$options 传入了_parseOptions
函数 ,这里根据断点可以发现此时出来的时候 where对应的已经是1了所以猜测在这个函数中进行了处理,进行跟进
_parseOptions
函数689行左右,获取了表名和表中的字段值
接下来会检查,$options['where'] 是否是数组,$fields 是否为空,发现将我们的数值传入的 _parseType 函数,继续跟进一波
跟进 _parseType 函数发现这里会根据数据表中的字段属性,来重新对我们的数据进行限定,id对应的类型是 int,这里直接做了 intval 处理导致 我们这里的 '
在这里丢掉了
就这样数据处理好了之后 在831行进行了sql语句的执行
所以利用链如下 :
id=1'
-> I()
-> find()
-> _parseOptions()
-> _parseType()
,根据上文知道 _parseType()
去掉了我们的'
,所以我们得想办法不去执行 _parseType()
方法,在文件704行 的判断条件中,如果$options['where']
会进入,同时$options 我们是可控的,我们只需要传入 id[where]=1
即可
如果传入的是 id[where]=1
这时候$data就是数组
就不会进入下面这个判断
直接进入 _parseOptions
函数
那么此时 $options['where'] 就是字符串从而不会进入判断不会触发_parseType
函数,从而可以进行sql注入
漏洞POC
id[where]=1 and updatexml(1,concat(0x7e,(select database()),0x7e),1)
由于tp开了debug模式才会有报错,所以我们可以利用时间盲注
http://localhost:8888/?id[where]=1 and (select 1 from(select sleep(2))x)
ps: 这里id只是测试用的参数,在实际过程中,可以尝试别的参数,因为只要使用了 M('users')->find(I('GET.id'));
就能进行触发,恰巧在开发者手册中TP推荐这样来获取参数所以增加了漏洞的利用性
修复
https://github.com/top-think/thinkphp/commit/9e1db19c1e455450cfebb8b573bb51ab7a1cef04
在修复中发现 options 和 this->options 分开了,这样我们传入的options就不会影响到 this->options了
0x02 SQL EXP 注入
漏洞分析
添加我们的测试代码 ,这里利用数组传参
public function index()
{
$User = D('Users');
$map = array('username' => $_GET['username']);
// $map = array('username' => I('username'));
$user = $User->where($map)->find();
var_dump($user);
}
首先数组会传递给 where 我们进行一个跟进
文件位置 :/ThinkPHP/Library/Think/Model.class.php/
大约2009行左右
在2030左右的位置将传入的数组参数传递给了 $this->options['where']
, 然后直接返回了整个对象
然后又调用了find方法,注意这里的find是没有参数传入的
跟进find 方法
文件位置: ThinkPHP/Library/Think/Model.class.php
820行左右 进入 _parseOptions
函数 ,
进入 _parseOptions
函数后 大约683行左右,将传入的$options 中的数据添加到了 $this->options , $this->options在wher那边就进行了赋值了
此时的$options 数据如下
继续往下看,看到 784行的if判断
is_scalar
用于检测传入参数是否是标准的数据类型
由于此时的 $val是 数组所以无法进入下图红框中的那个判断
最终返回的数据如下
然后进入了832行的select方法,我们进行跟进
文件位置:``ThinkPHP/Library/Think/Db/Driver.class.php` 大约 1036行左右
1042行建立了sql,进行跟进
发现将options传给了 parseSql
方法 ,继续跟进 parseSql
方法
继续跟进该方法 看到 parseWhere
方法的 大约 586行左右
$this->parseKey
只是返回了key值而已,问题不大 ,可以看到最后将 $key 和 $val 传入了 parseWhereItem函数,这里的key就是我们的参数名,val就是我们的数值名,继续进行跟进
跟进parseWhereItem函数 ,漏洞点就在这里
首先会判断我们的 $val 是否是数组,如果数组的第一个元素是字符串的话就赋值给$exp,然后看到第624行,如果 $exp = 'exp'的话,直接将我们的 $key 和 $val[1] 进行了拼接 ,并且没有做任何过滤
所以我们只需要控制传入的数值是数组,第一个是exp,第二个是我们的payload即可
http://localhost:8888/index.php?username[0]=exp&username[1]==1%20and%20updatexml(1,concat(0x7e,user(),0x7e),1)
最后到select函数的1044行执行
漏洞POC
http://localhost:8888/index.php?username[0]=exp&username[1]=1%20and%20updatexml(1,concat(0x7e,user(),0x7e),1)
同样的如果没有开启debug模式也可以用时间盲注
http://localhost:8888/index.php?username[0]=exp&username[1]= and (select 1 from(select sleep(2))x)
修复
利用I
函数进行获取,I
函数会对数据都进行一遍过滤
可以看到这里直接将exp过滤了
0x03 SQL bind 注入
漏洞分析
添加我们的测试代码
public function index()
{
$User = M("Users");
$user['id'] = I('id');
$data['password'] = I('password');
var_dump($user);
var_dump($data);
$valu = $User->where($user)->save($data);
var_dump($valu);
}
整体看下来其实可以发现bind注入和上面的exp注入成因非常的相似
I
函数接受id的参数传入where,然后将$data 传入 save函数,我们这里直接跟进save函数
文件位置 :/ThinkPHP/Library/Think/Model.class.php
484行左右
发现数据传入了 update方法,全局搜索update方法
文件位置: /ThinkPHP/Library/Think/Db/Driver.class.php
大约 984行左右
这里我们看到数据传入了 parseSet 方法
进行一个跟进 ,发现会将我们传入的数据编程 key=:value
这样的格式
此时我们的sql语句就变成了 ,注意这里的 :
继续看发现sql语句拼接了 我们where语句 ,跟进查看
进入函数,上面就是一些逻辑的判断,直接看到 589行 ,这里就和上面的exp注入很像了 ,跟进
发现此处进行了拼接 ,这里利用 id[0]=bind id[1]=xxx 来进入该方法,但是这里我们需要注意一点,我们是通过I
函数获取的数据,Tp在利用I
函数获取数据的时候会进行一个实现的过滤,上面的exp就在过滤中,我们查看bind是否在黑名单中
发现并没有在黑名单中
但是此时我们的sql语句中含有 :
,这样是无法执行的 ,查看execute中做了什么处理导致能成功执行
看到同名文件 207行,传入的 $this ->bind 是
然后红框语句进行了一个替换 将 :0
替换成了 1 这样就能正常执行了
漏洞POC
http://localhost:8888/index.php?id[0]=bind&id[1]=0 and updatexml(1,concat(0x7e,user(),0x7e),1)&password=1
同样的如果没有开启debug模式 那么就用盲注即可
http://localhost:8888/index.php?id[0]=bind&id[1]=0 and (select 1 from(select sleep(2))x)&password=1
修复
I
函数中的黑名单添加 bind即可
0x04 总结
整篇文章都是根据https://y4er.com/post/thinkphp3-vuln/ Y4er师傅的文章复现的
其实总体看下来,都是因为 id[] 导致可以绕过 if判断语句导致传入参数没有被过滤直接拼接导致的,学到了很多hh