返回顶部
首页 > 资讯 > 后端开发 > PHP编程 >ThinkPHP5之SQLI审计分析(一)
  • 203
分享到

ThinkPHP5之SQLI审计分析(一)

phpTP代码审计 2023-10-20 08:10:27 203人浏览 泡泡鱼
摘要

说明 该文章来源于徒弟lu2ker转载至此处,更多文章可参考:https://github.com/lu2ker/ 文章目录 说明0x00 测试代码做了什么?0x01 调用

说明

该文章来源于徒弟lu2ker转载至此处,更多文章可参考:https://github.com/lu2ker/

文章目录

Time:8-31

影响版本:5.0.13<=ThinkPHP<=5.0.15、 5.1.0<=ThinkPHP<=5.1.5

Payload:

/public/index.PHP/index/index?username[0]=inc&username[1]=updatexml(1,concat(0x7,user(),0x7e),1)&username[2]=1

这是一篇由已知漏洞寻找利用过程的文章,跟着**参考链接**学习分析,以下是收获记录。


0x00 测试代码做了什么?

phpnamespace app\index\controller;class Index{    public function index()    {        $username = request()->get('username/a');        db('users')->insert(['username' => $username]);        return 'Update success';    }}

index控制器是默认的TP框架程序的入口,该测试代码在index控制器下新建了一个index方法(实际上本来的index方法是tp的欢迎页面,这里是覆盖替换掉原来的)。逐行来看该测试代码:获取get请求的username参数->进行数据库insert操作->输出Update success

其中,username/a不明白是什么意思,就跟进get方法去看一下它怎么处理的:

thinkphp/library/think/Request.php

在这里插入图片描述

第676行可以看到$name允许是一个数组,但是测试代码中传入的是username/a字符串674行GET方式接受的数据最后进入input方法,接着看下做了如何处理:

在这里插入图片描述

这里看到将$name参数以/分割为了$name$type,而$type接下来被用到的地方是强制类型转换:

在这里插入图片描述

到这里就知道了a是一种修饰符,查看开发手册得知所有的定义好的修饰符。

在这里插入图片描述

同样的下断点看也可以证明$username最后其实是一个数组:(并不是因为payload是数组形式,而是由/a修饰符决定的)

在这里插入图片描述

然后接下来就进入到执行数据库插入操作的insert方法了。(9行这条代码的意思是向users表的username字段插入$username)

0x01 调用链分析

现在已知$username是一个数组,传入insert方法。跟进insert跳到:

thinkphp/library/think/db/Query.php

在这里插入图片描述

发现其内又调用了$this->builder->insert方法,且注释是生成sql语句。文件内搜索看看builder是如何定义的:

在这里插入图片描述

为了方便,直接在130行下断点,得到了$class的值:(这里可以随便传个参数比如username=1进去,因为主要是为了知道调用的哪里)

在这里插入图片描述

所以,$this->builder实际上是一个Mysql类的对象,而mysql类**(thinkphp/library/think/db/builder/Mysql.php)**是继承于Builder类的,且Mysql类里面并没有实现insert方法,所以最终调用的还是Builder类的insert方法:

thinkphp/library/think/db/Builder.php

在这里插入图片描述

经过上述跟进代码分析,最终得知,入口调用的insert方法最终调用的实际上是调用的Builder->insert()方法来生成SQL语句。接下来应该由内向外分析看看有没有什么有效的过滤措施。

0x02 分析最内层调用的处理

分析这个生成SQL语句(之前有个注释)的方法具体做了什么,跟进它的parseData方法:(86行

protected function parseData($data, $options)    {        if (empty($data)) {            return [];        }        // 获取绑定信息        $bind = $this->query->getFieldsBind($options['table']);        if ('*' == $options['field']) {            $fields = array_keys($bind);        } else {            $fields = $options['field'];        }        $result = [];        foreach ($data as $key => $val) {            $item = $this->parseKey($key, $options);            if (is_object($val) && method_exists($val, '__toString')) {                // 对象数据写入                $val = $val->__toString();            }            if (false === strpos($key, '.') && !in_array($key, $fields, true)) {                if ($options['strict']) {                    throw new Exception('fields not exists:[' . $key . ']');                }            } elseif (is_null($val)) {                $result[$item] = 'NULL';            } elseif (is_array($val) && !empty($val)) {                switch ($val[0]) {                    case 'exp':                        $result[$item] = $val[1];                        break;                    case 'inc':                        $result[$item] = $this->parseKey($val[1]) . '+' . floatval($val[2]);                        break;                    case 'dec':                        $result[$item] = $this->parseKey($val[1]) . '-' . floatval($val[2]);                        break;                }            } elseif (is_Scalar($val)) {                // 过滤非标量数据                if (0 === strpos($val, ':') && $this->query->isBind(substr($val, 1))) {                    $result[$item] = $val;                } else {                    $key = str_replace('.', '_', $key);                    $this->query->bind('data__' . $key, $val, isset($bind[$key]) ? $bind[$key] : PDO::PARAM_STR);                    $result[$item] = ':data__' . $key;                }            }        }        return $result;    }

这个函数最后return了$result变量,那么我们就看看函数体内是怎样处理$result变量的:

①首先,这个函数的第一个参数是$data,来源于最开始Query.phpinsert方法的2084行:

在这里插入图片描述

进行了一个数组合并操作把合并的结果再赋给$data,那么合并后的$data也是一个数组,且有一个键的键名为username(因为$data本来是['username' =>$username],在入口的第9行)。

②然后,这个函数在100行初始化了$result变量,然后101行用foreach分离$data$key$val$key变成了$item:

在这里插入图片描述

此时的$val是可控的、get方式传入的、等价于$username第113行判断如果$val是个数组,进入switch分支

在这里插入图片描述

$val[0]也就是payload中的username[0]根据不同的值进入不同的三个分支,但是可以看到每个分支的处理都是直接拼接$val[1]$val[2] 赋给 $result[$item],也就是$result['username'],唯一调用的parseKey并没有什么作用:

在这里插入图片描述

③最后,返回$result (也就是赋值给Query.phpinsert方法2084行的$data),至此parseData的主要功能就分析的差不多了。

接着看Builder类的insert方法在调用了parseData之后又干了什么:

在这里插入图片描述

$data中取出键值分别赋值给代表字段名和字段值的变量,简单的替换29-33行定义的SQL语句模型,返回生成好的SQL语句。

在这里插入图片描述

0x03 分析上一层调用的处理

在这里插入图片描述

之后还有两处调用函数,即getBind()getRealSql()但是跟进去看了下都没有任何数据清洗,仅仅是数据处理的一些解析操作。然后就到了execute()去执行SQL语句了。至此,整个处理流程基本上就分析完了,满足SQL注入漏洞的前提条件:①参数用户可控②参数直接拼接到SQL语句中,无任何有效过滤。

0x04 Payload构造

漏洞的利用点还是在最内层调用的parseData方法中,根据刚才的分析已经知道的 v a l 实际上就是传入的 u s e r n a m e ,那么 ‘ val实际上就是传入的username,那么` val实际上就是传入的username,那么val[0] v a l [ 1 ] ‘ 、 ‘ val[1]`、` val[1]val[2]`都是可控的,只要传入满足条件的值即可,因为是insert操作,所以选择用报错函数进行注入:

构造username[0]=inc,进入inc分支。

构造username[1]=updatexml(1,concat(0x7e,user(),0x7e),1),实际的报错语句。

构造username[2]=1,只是为了补齐数组元素个数。

再加上index控制器的index方法的访问路径/public/index.php/index/index/

最后连起来就是

/public/index.php/index/index/?username[0]=inc&username[1]=updatexml(1,concat(0x7e,user(),0x7e),1)&username[2]=1

本意是代码审计,就不考虑再如何利用了。

来源地址:https://blog.csdn.net/Jack0610/article/details/128531003

--结束END--

本文标题: ThinkPHP5之SQLI审计分析(一)

本文链接: https://www.lsjlt.com/news/432772.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

猜你喜欢
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作