🖋️

【AlpacaCTF(Daily Alpaca)】Writeup - Emojify

に公開

脆弱性を探す

とりあえず配られているemojify.tar.gzの中身を見ます。パット見た感じbackendは処理、frontendは表面(入力フォーム)、secretはなんか怪しい、という感じ。(なんと始め見たときはsecretに気づきませんでした。馬鹿すぎ。)
ということでまずsecretフォルダの中身を見ます。package.jsonとpackage-lock.jsonはちらっと見た感じあまり重要そうではなかったので飛ばします。問題はindex.js。

index.js
import express from "express";

const FLAG = process.env.FLAG ?? "Alpaca{REDACTED}";

express()
  // http://secret:1337/flag
  .get("/flag", (req, res) => res.send(FLAG))
  .listen(1337);

明らかにFlagのヒントになりそうです。少しちゃんと読んでみると、http://secret:1337/flag というところにアクセス出来た瞬間勝ちそうですね。さてどうしようか。とりあえずスポーンしたチャレンジインスタンスにアクセスしてみると、何やらテキストボックスとShowという文字が。

問題のタイトルがemojifyなので、多分絵文字が出てくるのかな。。。と思いながら、適当に入れてみます。
案の定thinkingが表示されました。非常にthinking。
この時点では、なんとなくリクエストをいじって規定外(さっきのsecret)にアクセスすれば絵文字の代わりにFlagを出せるかな。。。といったところです。「じゃぁリクエストの仕組みを見るか。」となったのでfrontendのindex.jsを見てみます。

index.js
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);

んー。パット見た感じ私の知識レベルではexpress()の中でindex.htmlのテキストボックスからテキストを取ってきてそれをfetch、エラーがあればエラーを吐くというところまでしかわからなかったです。ただおそらくwafとかいうのでなんかやってるんだろうなー...と。
そこで、wafについて調べてみました。
(今回はこちらを参照させていただいています→
https://www.jbsvc.co.jp/useful/security/what-is-waf.html)

なるほど。つまりはブラックリスト型でリクエスト(多分送られてきたpathでかな?)をはじくためのものだということです。多分。
であれば、そのブラックリストのフィルタの足りない部分か、そもそもブラックリストに入っていないところから回り込めば解決しそう。実際にwafで何をブロックしているかを見ます。

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;
};

ここがwafの抜粋です。上から順に、「文字列かどうか」「/で始まるかどうか」「emojiが含まれているかどうか」を見ていて、falseなら弾いてそれぞれエラーメッセージを送るという感じですかね。ここまで来たらなんとなくでできそうですが、私は「んーこれの何がダメなんだろう。。。」となってしまいました。。。ということで、こういう時はAIを有効活用しましょう。ChatGPTくんに何がダメなの?とド直球で聞いてきました。

結果、「emojiの位置が指定されていないことが脆弱性だよー」と教えてくれました。そういうことね。

解く

ここまでくればさすがに解けるかも。つまりは「emojiを入れる(けど無視させて)→さっきのflagページにアクセス→Flagをゲット」という流れでいけばよいわけですね。
じゃぁemojiを入れるけど無視する方法って何だろう。。。知らないのでまたチャッピーに聞きます。

へーそうなんだ。じゃぁさっそく今回の攻撃に当てはめていきます。curlで

http://IPアドレス:31211/api?path=/emoji@secret:1337/flag

を見てみます。結果、うまくいきませんでした。なんでだろう。ということでまたチャッピー。

なるほど。理解した。めちゃめちゃ簡単に言うと、環境によって通らないらしいです。画像をそのまま書き写すことになりますが、@はホスト部分でしか意味を持たないので、/emoji@secret:1337/はただのpathとして認識されていると。
じゃぁ@じゃない方でやればいいのかな。同時にチャッピーが//を使う方法を教えてくれました。

どうやら//から始まるpathは、ホスト指定URL扱いになるため、backendの部分(もともとのhostかな?)を無効にできるらしいです。加えてemojiを入れないとwafに引っかかるので、fetch内でのパス正規化を利用して、

/emoji/../flag

を使うと。ということで、このURLを打ってみます。

→フラグを取得できました(具体的なフラグは伏せます)

Discussion