ThinkPHP 3.2.3 系列漏洞分析

0x00 准备工作

源码下载:https://github.com/top-think/thinkphp/archive/3.2.3.zip

在文件中进行配置,我这里是mysql 8.0 ,DH_HOST 那边不添加 3306会连接不上,正常情况 localhost即可

image-20210119203829433

创建好对应的数据库,并且创建对应的数据表

0x01 SQL where 注入

漏洞分析

添加我们的测试代码 ,这里的users对应的是我们的数据表

image-20210119203817208

    public function index()
    {
        // I 参数获取 get中id参数的数值
        $data = M('users')->find(I('GET.id'));
        var_dump($data);
    }

正常情况访问如下即可

image-20210119204012668

参考 Y4er 师傅的文章进行分析 :

https://y4er.com/post/thinkphp3-vuln/

首先传入 1' 走一下流程

这里tp自己的I函数对参数进行了获取 ,我们进行一个跟进

image-20210119204326159

文件位置 : ThinkPHP/Common/functions.php ,前面都是一些根据请求方法的不同来获取参数的语句 ,我们这里直接看到文件第379行,发现TP 利用自身的C函数获取了配置文件中的DEFAULT_FILTER ,全局搜索发现默认的过滤方法是 htmlspecialchars,对输入进行了过滤

image-20210119204700551

查看开发文档发现该过滤器主要是为了防御xss,进行一些html实体化用的,继续往下看

image-20210119204924902

在同文件第443行左右,发现调用了 think_filter 函数,如果我们输入的是数组的话 就对我们数组中的每个元素都用thinkfilter过滤一遍,跟进该函数

image-20210119205028936

同文件 1731行左右 ,发现是一个正则,对一些SQL注中有可能会用到的词进行了置空

image-20210119205208722

处理完之后返回 $data,根据上面的代码发现没有做太多的过滤,我们传入的 1' 还是1'

image-20210119205408214

接下来$data会传给find方法,继续进行一个跟进

文件位置 :ThinkPHP/Library/Think/Model.class.php 785行左右

首先判断我们传入的参数是否数字或者字符串 如果是的话就转成数组, 此时处理后的 $options['where'] 应为 where=>array('id'=>"1'")

image-20210119205557256

接下来的这个函数我们不会进入 ,这个是为了应对多个主键的问题,我这里主键只有 id 所以 $pk就是只是id 如果有多个的话,pk就是数组了就会进入下面这个判断了

image-20210119205708814

继续往下看,发现$options 传入了_parseOptions 函数 ,这里根据断点可以发现此时出来的时候 where对应的已经是1了所以猜测在这个函数中进行了处理,进行跟进

image-20210119205924072

_parseOptions 函数689行左右,获取了表名和表中的字段值

image-20210119210358397

接下来会检查,$options['where'] 是否是数组,$fields 是否为空,发现将我们的数值传入的 _parseType 函数,继续跟进一波

image-20210119210814153

跟进 _parseType 函数发现这里会根据数据表中的字段属性,来重新对我们的数据进行限定,id对应的类型是 int,这里直接做了 intval 处理导致 我们这里的 ' 在这里丢掉了

image-20210119210907885

就这样数据处理好了之后 在831行进行了sql语句的执行

image-20210119211101946

所以利用链如下 :

id=1' -> I() -> find() -> _parseOptions() -> _parseType(),根据上文知道 _parseType() 去掉了我们的',所以我们得想办法不去执行 _parseType() 方法,在文件704行 的判断条件中,如果$options['where'] 会进入,同时$options 我们是可控的,我们只需要传入 id[where]=1 即可

image-20210119211838463

如果传入的是 id[where]=1 这时候$data就是数组

image-20210119212435894

就不会进入下面这个判断

image-20210119212512876

直接进入 _parseOptions 函数

image-20210119212526682

那么此时 $options['where'] 就是字符串从而不会进入判断不会触发_parseType 函数,从而可以进行sql注入

image-20210119212704540

漏洞POC

id[where]=1 and updatexml(1,concat(0x7e,(select database()),0x7e),1)

image-20210119212910007

由于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了

image-20210119223306796

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'] , 然后直接返回了整个对象

image-20210120154427306

image-20210120154649519

然后又调用了find方法,注意这里的find是没有参数传入的

跟进find 方法

文件位置: ThinkPHP/Library/Think/Model.class.php 820行左右 进入 _parseOptions 函数 ,

image-20210120155911115

进入 _parseOptions 函数后 大约683行左右,将传入的$options 中的数据添加到了 $this->options , $this->options在wher那边就进行了赋值了

image-20210120161834911

此时的$options 数据如下

image-20210120162013638

继续往下看,看到 784行的if判断

is_scalar 用于检测传入参数是否是标准的数据类型

由于此时的 $val是 数组所以无法进入下图红框中的那个判断

image-20210120161358373

最终返回的数据如下

image-20210120160138849

然后进入了832行的select方法,我们进行跟进

image-20210120160318659

文件位置:``ThinkPHP/Library/Think/Db/Driver.class.php` 大约 1036行左右

1042行建立了sql,进行跟进

image-20210120160609874

发现将options传给了 parseSql 方法 ,继续跟进 parseSql 方法

image-20210120160730150

继续跟进该方法 看到 parseWhere 方法的 大约 586行左右

$this->parseKey 只是返回了key值而已,问题不大 ,可以看到最后将 $key 和 $val 传入了 parseWhereItem函数,这里的key就是我们的参数名,val就是我们的数值名,继续进行跟进

image-20210120162155764

跟进parseWhereItem函数 ,漏洞点就在这里

首先会判断我们的 $val 是否是数组,如果数组的第一个元素是字符串的话就赋值给$exp,然后看到第624行,如果 $exp = 'exp'的话,直接将我们的 $key 和 $val[1] 进行了拼接 ,并且没有做任何过滤

image-20210120162411445

所以我们只需要控制传入的数值是数组,第一个是exp,第二个是我们的payload即可

http://localhost:8888/index.php?username[0]=exp&username[1]==1%20and%20updatexml(1,concat(0x7e,user(),0x7e),1)

最后到select函数的1044行执行

image-20210120163159039

漏洞POC

http://localhost:8888/index.php?username[0]=exp&username[1]=1%20and%20updatexml(1,concat(0x7e,user(),0x7e),1)

image-20210120163227818

同样的如果没有开启debug模式也可以用时间盲注

http://localhost:8888/index.php?username[0]=exp&username[1]= and (select 1 from(select sleep(2))x)

修复

利用I函数进行获取,I函数会对数据都进行一遍过滤

image-20210120164213613

可以看到这里直接将exp过滤了

image-20210120164610536

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函数

image-20210121134250063

文件位置 :/ThinkPHP/Library/Think/Model.class.php 484行左右

发现数据传入了 update方法,全局搜索update方法

image-20210121134534626

文件位置: /ThinkPHP/Library/Think/Db/Driver.class.php 大约 984行左右

这里我们看到数据传入了 parseSet 方法

image-20210121134804667

进行一个跟进 ,发现会将我们传入的数据编程 key=:value 这样的格式

image-20210121135019490

此时我们的sql语句就变成了 ,注意这里的 :

image-20210121135238982

继续看发现sql语句拼接了 我们where语句 ,跟进查看

image-20210121135340497

进入函数,上面就是一些逻辑的判断,直接看到 589行 ,这里就和上面的exp注入很像了 ,跟进

image-20210121135403673

发现此处进行了拼接 ,这里利用 id[0]=bind id[1]=xxx 来进入该方法,但是这里我们需要注意一点,我们是通过I 函数获取的数据,Tp在利用I函数获取数据的时候会进行一个实现的过滤,上面的exp就在过滤中,我们查看bind是否在黑名单中

image-20210121135507169

发现并没有在黑名单中

image-20210121135729977

但是此时我们的sql语句中含有 ,这样是无法执行的 ,查看execute中做了什么处理导致能成功执行

image-20210121140002676

看到同名文件 207行,传入的 $this ->bind 是

image-20210121140247221

然后红框语句进行了一个替换 将 :0 替换成了 1 这样就能正常执行了

image-20210121140107904

漏洞POC

http://localhost:8888/index.php?id[0]=bind&id[1]=0 and updatexml(1,concat(0x7e,user(),0x7e),1)&password=1

image-20210121140346336

同样的如果没有开启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

点赞

发表评论

电子邮件地址不会被公开。必填项已用 * 标注