Save The Earth! - 地球環境を守ろう!
URL: https://2022.angstromctf.com/challenges
OSCP の勉強をしていて、自分のWeb関連のスキルが弱いと感じたので、
まだ OSCP の勉強の真っ最中ですが、CTFのWeb問題だけは時々トライしていこうと思います。
いちおう、210点を獲得し、順位は814位でした。
以下は解いたチャレンジです。(相変わらず、ほとんど解けてないです。。。)
Xtra Salty Sardines でやったことは、自分で今後も参照することもありそうなので、writeup残しておきます。
[Web]: Xtra Salty Sardines (70 points)
Challenge
Clam was intensely brainstorming new challenge ideas, when his stomach growled! He opened his favorite tin of salty sardines, took a bite out of them, and then got a revolutionary new challenge idea. What if he wrote a site with an extremely suggestive acronym?
Admin Bot
Attachment:
中身:
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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
|
const express = require("express");
const path = require("path");
const fs = require("fs");
const cookieParser = require("cookie-parser");
const app = express();
const port = Number(process.env.PORT) || 8080;
const sardines = {};
const alpha = "abcdefghijklmnopqrstuvwxyz";
const secret = process.env.ADMIN_SECRET || "secretpw";
const flag = process.env.FLAG || "actf{placeholder_flag}";
function genId() {
let ret = "";
for (let i = 0; i < 10; i++) {
ret += alpha[Math.floor(Math.random() * alpha.length)];
}
return ret;
}
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
// the admin bot will be able to access this
app.get("/flag", (req, res) => {
if (req.cookies.secret === secret) {
res.send(flag);
} else {
res.send("you can't view this >:(");
}
});
app.post("/mksardine", (req, res) => {
if (!req.body.name) {
res.status(400).type("text/plain").send("please include a name");
return;
}
// no pesky chars allowed
const name = req.body.name
.replace("&", "&")
.replace('"', """)
.replace("'", "'")
.replace("<", "<")
.replace(">", ">");
if (name.length === 0 || name.length > 2048) {
res.status(400)
.type("text/plain")
.send("sardine name must be 1-2048 chars");
return;
}
const id = genId();
sardines[id] = name;
res.redirect("/sardines/" + id);
});
app.get("/", (req, res) => {
res.sendFile(path.join(__dirname, "index.html"));
});
app.get("/sardines/:sardine", (req, res) => {
const name = sardines[req.params.sardine];
if (!name) {
res.status(404).type("text/plain").send("sardine not found :(");
return;
}
const sardine = fs
.readFileSync(path.join(__dirname, "sardine.html"), "utf8")
.replaceAll("$NAME", name.replaceAll("$", "$$$$"));
res.type("text/html").send(sardine);
});
app.listen(port, () => {
console.log(`Server listening on port ${port}.`);
});
|
Solution
イワシの缶に名前が付けられるようになっています。かなり塩っぱそう(extra salty)。
XSSですね。
以下はAdmin Botです。URLを指定すると、アクセスしてくれるみたいです。
ソースコードを見ると、いくつかの特殊文字はエスケープされるみたいです。
1
2
3
4
5
|
.replace("&", "&")
.replace('"', """)
.replace("'", "'")
.replace("<", "<")
.replace(">", ">");
|
とりあえず、"<h2>aa</h2>" を入れてみたところ、どうやらフィルターは同じ文字に対して一回だけかかるみたいです。
フィルターは簡単に回避できそう。
フラグは、Admin Bot のみアクセスできるようになっています。
1
2
3
4
5
6
7
8
|
// the admin bot will be able to access this
app.get("/flag", (req, res) => {
if (req.cookies.secret === secret) {
res.send(flag);
} else {
res.send("you can't view this >:(");
}
});
|
iframeを2つ用意して、iframe1でフラグへのアクセス、iframe2ではiframe1の値を取り出して外部に送信するXSSを用意します。
イメージとしては、こんな感じです。https://beeceptor.com/ で書いたルールです。
(結果として、これじゃダメなんですが)
XSSは、以下の通りです。最初の3文字は、フィルター回避用です。
これを、イワシの缶の名前としてセットして、URLを生成します。その後、そのURLをiframe2にセットします。
で、このiframe1とiframe2を含むページをどこでホストするかなんですが、https://beeceptor.com/ でやってしまうと Cookie が iframe1 に適用されずに Admin Botでもフラグが取れなくなってしまいます。
なので、そのページ自体も、イワシの缶の名前を使って生成します。
ブラウザでアクセスしてみると、こんな感じです。
これを、Admin Bot にアクセスさせると、フラグが得られます。
Flag: actf{those_sardines_are_yummy_yummy_in_my_tummy}
Author
CaptureAmerica @ CTF フラxxグゲット
LastMod
2022-05-06