st98 の日記帳 - コピー

なにか変なことが書かれていたり、よくわからない部分があったりすればコメントでご指摘ください。匿名でもコメント可能です🙏 Please feel free to ask questions in English if that's more comfortable for you👍

FAUST CTF 2025 writeup

9/27 - 28という日程で、9時間開催された。BunkyoWesternsで参加して38位。10年前から開催されているAttack & Defenseの伝統的な大会なのだけれども、私は出たことがなかった。ほかのメンバーがインフラ等を整えてくれて、私はただ他チームによる攻撃の通信を解析して脆弱性を突き止め、パッチを作成したり、逆に他チームに攻撃する手順を整理したりしていた。

私はずっとCOBOLで書かれた(!???!?!?)サービスを担当しており、これが結構面白かったので、ちょっとまとめておきたい。COBOLを読むのはめちゃくちゃ久しぶり(これはろくなコードではなく、まともなCOBOLのコードを読むのは初めてだったかも)で、書くのは初めてだった。


cake-configurator

どんなアプリか

立ち上がったサービスに socat -,raw,echo=0 TCP:localhost:4321 で接続すると、次のようなメニューが表示される。R を入力するとユーザ登録が、L を入力するとログインができる。T については後で説明する。

適当なユーザで登録・ログインすると、次のように選択できるメニューが増えた。O を入力するとケーキの注文ができる。V を入力すると注文済みのケーキの情報が閲覧できる。

ケーキの注文では、味やトッピングといった情報が入力できる。この際にTracking-IDというIDが発行され、先程のメニューで T を選んだ際に、このIDを入力することでケーキの情報が閲覧できる。Tracking-IDさえ知っていれば、非ログイン状態でもケーキの情報が確認できるわけだ。

さて、フラグはこのケーキの情報として保存される。SLAチェックとして定期的に運営がスクリプトを走らせるわけだけれども、その一環としてランダムなユーザ名で登録し、フラグをその情報の一部として含むケーキの注文がなされる。そのユーザ名や連番のユーザIDは、スコアボード上でチームに公開されている。

脆弱性1: 認証バイパス

パケットを眺めていると、次のような不思議な行動をしているユーザが印象に残った。

  1. ランダムなユーザ名で登録・ログインする
  2. ログイン済みの状態で、さらに別のユーザとしてログインを試みる

すでにログインしている状態ではメニュー画面に L は選択肢として存在しないはずだが、なぜか通っている。また、手順2でログインしようとしているユーザにはパスワードが設定されているはずだが、なぜかパスワードを入力しないままにログインに成功し、注文情報を読み取ることができていたようだった。そのような認証バイパスができてしまうと、フラグが読み取り放題となってしまう。

一体何が起こっているのだろう。「すでにログイン済みの状態で、さらにログインする」というのがミソらしく、これで再現できた。では、なにが原因なのだろう。セッション維持やログイン画面のコードを読んでいく。まず、ログイン済みなのになぜか再びログインできるという点については、次のコードから、ログイン済みかどうかにかかわらず好きなメニューの項目を選択できることがわかる。メニューでは表示されていないだけで、どれも選べるわけだ。

           ACCEPT WELCOME-SCREEN
           PERFORM UNTIL WS-MENU IS EQUAL TO "Q"
             EVALUATE WS-MENU
               WHEN "L"
                   MOVE SPACES TO WS-MENU
                   CALL "LOGIN" USING WS-UNAME WS-MSG
                   IF WS-UNAME IS NOT EQUAL TO SPACES 
                     MOVE 1 TO WS-LOGGED-IN
                   END-IF
               WHEN "R"
                   MOVE SPACES TO WS-MENU
                   CALL "REGISTER" USING WS-MSG
               WHEN "O"
                   MOVE SPACES TO WS-MENU
                   CALL "CAKEORDER" USING WS-UNAME WS-MSG
               WHEN "T"
                   MOVE SPACES TO WS-MENU 
                   DISPLAY GET-TID-SCREEN
                   DISPLAY EOP-INDICATOR
                   ACCEPT GET-TID-SCREEN
                   CALL "TRACKVIEW" USING WS-TID WS-MSG
               WHEN "V"
                   MOVE SPACES TO WS-MENU 
                   CALL "ORDERVIEW" USING WS-UNAME WS-MSG
               WHEN OTHER DISPLAY MENU-SECTION
                   IF WS-LOGGED-IN IS EQUAL TO 0 
                     DISPLAY LOGGEDOUT-SECTION
                   END-IF
                   IF WS-LOGGED-IN IS EQUAL TO 1 
                     DISPLAY LOGGEDIN-SECTION
                   END-IF
                   DISPLAY RESPONSE-SECTION
                   DISPLAY EOP-INDICATOR
                   ACCEPT RESPONSE-SECTION
                   MOVE SPACES TO WS-MSG
           END-PERFORM.

今ログインしているか、どのユーザでログインしているかという情報をどこで保存しているか。メニュー画面周りのコードを読むと、WS-LOGGED-IN という変数でログイン済みかどうか、また WS-UNAME にログイン済みのユーザ名を入れて管理していることがわかる。

WHEN "L"
    MOVE SPACES TO WS-MENU
    CALL "LOGIN" USING WS-UNAME WS-MSG
    IF WS-UNAME IS NOT EQUAL TO SPACES 
      MOVE 1 TO WS-LOGGED-IN
    END-IF

この WS-UNAME はログイン画面でパスワードの検証前に上書きされている上に、ログインに失敗しても WS-LOGGED-IN1 のまま、つまりログイン状態が維持されてしまう。雑に、もし入力されたクレデンシャルと一致するユーザが存在しない場合には WS-UNAME を空白で埋めるようにした。

diff --git a/cake_backend/src/LOGIN.cob b/cake_backend/src/LOGIN.cob
index 2dabcaa..d0d97f4 100644
--- a/cake_backend/src/LOGIN.cob
+++ b/cake_backend/src/LOGIN.cob
@@ -117,7 +117,10 @@
                 WHERE USERNAME = :SQL-UNAME AND PASSWORD = :SQL-PW
            END-EXEC.
            IF SQLCODE NOT = ZERO PERFORM SQL-ERROR EXIT PARAGRAPH.
-           IF SQL-CNT = 0 MOVE "Invalid username or password" TO WS-MSG.
+           IF SQL-CNT = 0
+             MOVE "Invalid username or password" TO WS-MSG
+             MOVE SPACES TO WS-UNAME
+           END-IF.
            IF SQL-CNT = 1 MOVE "T" TO WS-SUCCESS.
            EXIT PARAGRAPH.
       ******************************************************************

競技終了後に、上記のパッチをバイパスして、うちのサービスからフラグを盗み出しているチームがいたことに気づいた。この攻撃が始まったのは最終盤だったのでそこまで痛くはなかったものの、何が足りなかったのか気になる。ペイロードを確認して、次のような手順で再現できることがわかった。ログイン画面に入る際に問答無用で WS-LOGGED-IN0 にしておけばよかったなあ。

  1. ランダムなユーザ名で登録・ログインする
  2. ログイン済みの状態でログイン画面に遷移し、別のユーザのユーザ名を入力する
  3. ただし、送信はせず、Next Actionとして V を入力する

脆弱性2: 乱数が弱い

Tracking-IDは、5桁のユーザID(連番)と11文字の英数字からなる。ユーザIDはスコアボードから手に入れられるのでよいとして、この11文字の英数字については推測できないだろうかと考えていた。対応するプロシージャは次の通り。RANDOMWS-MICROSEC をシードとして与えた上で、11回乱数を生成している。

      ******************************************************************
       PROCEDURE               DIVISION USING LNK-TID LNK-UNAME LNK-MSG.
      ******************************************************************
           PERFORM SQL-GETUID.

           ACCEPT WS-TIME FROM TIME
           COMPUTE WS-RANDI = FUNCTION RANDOM(WS-MICROSEC)*26 + 1
           PERFORM VARYING WS-NDX FROM 1 BY 1 UNTIL WS-NDX>11
             COMPUTE WS-RANDI = Function RANDOM*36 + 1
             STRING WS-ALPH(WS-RANDI:1) INTO WS-CSB(WS-NDX)
           END-PERFORM.
           MOVE WS-STRARR TO WS-RAND.

           MOVE WS-TRACKINGID TO LNK-TID
           GOBACK.
      ******************************************************************

WS-MICROSEC は次の通り4桁の数値である。1万回ぐらいならブルートフォースできそうに思えたけれども、残念ながら今回は数百チームおり、またtickが2分ごとということで、送信する必要のあるデータのサイズを考えるに非現実的だと考えていた。

       01 WS-TIME.
           10 WS-MICROSEC PIC 9(04).
           10 WS-SEC      PIC 9(02).
           10 WS-MIN      PIC 9(02).
           10 WS-HOUR     PIC 9(02).

しかし、競技も終盤に差し掛かってきたときに、ブルートフォースしているように見える…けれども、実際のところそのパケットの数は信じられないほどに少なく、的確にフラグの含まれる注文のTracking-IDを当ててきているパケットが観測された。一体何が起こっているのだろう。

あらためて WS-MICROSEC の出元を確認する。ACCEPT WS-TIME FROM TIME ということで現在時刻を取ってきており、そのうちのマイクロ秒(ミリ秒では?)の部分を取ってきているのだろうと考えていた。もしかしてこの精度が悪いのか? とまず考えた。

いろいろなドキュメントを読んでいると、TIMEhhmmsstt という8桁の数値であるという記述が見つかる。もう一度 WS-MICROSEC の定義を読み直す。これ、WS-MICROSEC と言いつつ時・分が入っていないか?

       01 WS-TIME.
           10 WS-MICROSEC PIC 9(04).
           10 WS-SEC      PIC 9(02).
           10 WS-MIN      PIC 9(02).
           10 WS-HOUR     PIC 9(02).

当たりだった。sugiさんにexploitを用意してもらいつつ、まずは、雑な対策として英数字のテーブルを入れ替える。これで雑なexploitを走らせているチームは落とせるはずだ。

diff --git a/cake_backend/src/TRACKGEN.cob b/cake_backend/src/TRACKGEN.cob
index 6212454..f75dbed 100644
--- a/cake_backend/src/TRACKGEN.cob
+++ b/cake_backend/src/TRACKGEN.cob
@@ -25,7 +25,7 @@
            10 WS-NDX      PIC S9(02) COMP.
            10 WS-RANDI    PIC 9(02).
            10 WS-ALPH     PIC X(36) VALUES 
-           "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".
+           "60UTYAC5RQVZMJPH3XNKS7GL2WE9BOD1FI48".
        01 WS-STRARR.

また、これ以上フラグが盗まれないように、次のように WS-MICROSEC を10ミリ秒単位の現在時刻が入るようにした。まだ理想的な実装にはなっていないけれども、他チームの攻撃を止めるには十分だろう。それよりも、別の脆弱性を探すことのほうが重要だと考えた。作問者いわく、この2つ以外には脆弱性はなかったらしいけれども……

diff --git a/cake_backend/src/TRACKGEN.cob b/cake_backend/src/TRACKGEN.cob
index f75dbed..0351a40 100644
--- a/cake_backend/src/TRACKGEN.cob
+++ b/cake_backend/src/TRACKGEN.cob
@@ -17,10 +17,9 @@
            10 WS-UID      PIC 9(05).
            10 WS-RAND     PIC X(11).
        01 WS-TIME.
-           10 WS-MICROSEC PIC 9(04).
-           10 WS-SEC      PIC 9(02).
-           10 WS-MIN      PIC 9(02).
            10 WS-HOUR     PIC 9(02).
+           10 WS-MIN      PIC 9(02).
+           10 WS-MICROSEC PIC 9(04).
        01 WS-RNG.
            10 WS-NDX      PIC S9(02) COMP.
            10 WS-RANDI    PIC 9(02).