4
2

More than 3 years have passed since last update.

SECCON Beginners CTF 2021(#ctf4b) writeup

Last updated at Posted at 2021-05-23

SECCON Beginners CTF 2021(#ctf4b) writeup

結果はこんな感じでした

チーム名: SatoHack365
順位: 144
チームで解けた問題数: 13
チームで解けたスコア: 1110
自分が解けた問題数(メンバーが先に解いたものを含む): 8
自分が解けた問題のスコア: 575

去年は出てませんが,一昨年は 0 完だったので結構成長していました.

crypto

simple_RSA (75 pt)

受け取ったファイルを見ると公開鍵eが 3 と,ものすごく小さいです(一般的には 65537 など).Low Public Exponent Attack ができます.

import sys
import gmpy2

def long_to_bytes(x):
    return x.to_bytes((x.bit_length() + 7) // 8, 'big')

def low_public_exponent_attack(c, e, n=0):
    while True:
        m, exact_root = gmpy2.iroot(c, e)
        if exact_root == True:
            break
        c += n
    return int(m)

if __name__ == '__main__':
    n = ファイルに書かれた n
    e = 3
    c = ファイルに書かれた c
    m = low_public_exponent_attack(c, e, n)
    flag = long_to_bytes(m).decode().strip()
    print(flag)
ctf4b{0,1,10,11...It's_so_annoying.___I'm_done}

web

osoba (51 pt)

app.py のコードをみます.

page = request.args.get('page', 'public/index.html')
response = make_response(send_file(page))

ここでは URL クエリパラメータpageに与えられた値のパスにあるファイルを,そのまま出力しているみたいです.デフォルトではpublic/index.htmlつまり index.html をそのまま出力します.よってディレクトリトラバーサル攻撃ができます.

現在のルートディレクトリはsrcのようなので,https://osoba.quals.beginners.seccon.jp/?page=../flagにアクセスするとフラグが入手できます.

ctf4b{omisoshiru_oishi_keredomo_tsukuruno_taihen}

Werewolf (70 pt)

player.roleWEREWOLFに変えることができればフラグが取れるらしいです.ここで,二つ確認すべき点があります.

1) Python には完全なプライベート変数は存在しません.
以下のコードを見てみます.

class Player:
    def __init__(self):
        self.__role = "VILLAGER"
    @property
    def role(self):
        return self.__role

このコードでplayer = Player()とした後にplayer.roleの値は_Player__roleとするとプライベート変数なのに取得することができます.カスだね,Python ちゃん

2) player.__dict__[k] = vでキーkvの変数を作成する.もし存在していれば上書き

よって以下のように POST を遅れば flag が取れます.

curl -w '\n' 'https://werewolf.quals.beginners.seccon.jp/' --data '_Player__role=WEREWOLF' -XPOST
ctf4b{there_are_so_many_hackers_among_us}

cant_use_db (108 pt)

品物を買った後に走るスクリプトに
time.sleep(random.uniform(-0.2, 0.2) + 1.0)
という箇所があります.データベースを使用していないため,排他制御を気にしているのか 1 秒待っているみたいです.逆に言うと,お金があるかちゃんと確認してから 1 秒後にお金が取られます.そこで,このスリープ時間に急いでクリックして,もう一個リクエスト送ったら balance の値は不定になりそうですが,soup と noodles はちゃんと増えます.

コツは Soup → Noodles の順で早く押すとだいたい Balance が $10000 になるので,あとはゆっくり Noodles を買って Flag をもらいに行きます.

ctf4b{r4m3n_15_4n_3553n714l_d15h_f0r_h4ck1n6}

misc

git-leak (58 pt)

与えられたファイルに書いてあるreflogコマンドを使ってみます.

e0b545f (HEAD -> master, origin/master) HEAD@{0}: commit (amend): feat: めもを追
80f3044 HEAD@{1}: commit (amend): feat: めもを追加
b3bfb5c HEAD@{2}: rebase -i (finish): returning to refs/heads/master
b3bfb5c HEAD@{3}: commit (amend): feat: めもを追加
7387982 HEAD@{4}: rebase -i: fast-forward
36a4809 HEAD@{5}: rebase -i (start): checkout HEAD~2
7387982 HEAD@{6}: reset: moving to HEAD
7387982 HEAD@{7}: commit: feat: めもを追加
36a4809 HEAD@{8}: commit: feat: commit-treeの説明を追加
9ac9b0c HEAD@{9}: commit: change: 順番を変更
8fc078d HEAD@{10}: commit: feat: git cat-fileの説明を追加
d3b47fe HEAD@{11}: commit: feat: fsckを追記する
f66de64 HEAD@{12}: commit: feat: reflogの説明追加
d5aeffe HEAD@{13}: commit: feat: resetの説明を追加
a4f7fe9 HEAD@{14}: commit: feat: git logの説明を追加
9fcb006 HEAD@{15}: commit: feat: git commitの説明追加
6d21e22 HEAD@{16}: commit: feat: git addの説明を追加
656db59 HEAD@{17}: commit: feat: add README.md
c27f346 HEAD@{18}: commit (initial): initial commit

僕もよくわかってませんが,rebaseした後でも元に戻せるようです.rebaseする前のHEAD@{7}に戻りましょう.

git reset --hard HEAD@{7}

すると flag.txt というファイルが出現(復元)されます.

ctf4b{0verwr1te_1s_n0t_c0mplete_1n_G1t}

Mail_Address_Validator (76 pt)

ReDoS 攻撃という攻撃が成立します.運よく数日前に僕はスライドにまとめていました.

正規表現の脆弱性 ReDoS 攻撃 を簡単に説明する回

ちなみにこの問題の簡易版の問題も作り,公開しています(おそらく記事公開後,数日で消えます).

https://redos-server.herokuapp.com/

詳しくはスライド参照していただきたいのですが,正規表現は下手に書くと match に時間がかかる場合があります.今回 main.rb には

pattern = /\A([\w+\-].?)+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z/i

と書かれていますが,後半の+)*が ReDoS できそうです.なので適当に.を入れて最後にbを入れてこんな感じにすればだいたい処理時間が爆発します.

aaa@a0a.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaab.aaa
ctf4b{1t_15_n0t_0nly_th3_W3b_th4t_15_4ff3ct3d_by_ReDoS}

depixelization (136 pt)

フラグの文字列を画像に変換して,各文字の上にP,I,Xの三文字を重ねて出力しているみたいです.そこで画像をいくつかに分割し,また ASCII 全ての文字を画像に変換して一致を比較とします.

画像をいくつかに分割
ソースコードより,画像は横 85px らしいです.

import cv2

# 画像読み込み
img = cv2.imread("output.png")

height = img.shape[0]
width = img.shape[1]

for i in range(31):
    img1 = img[0:, i*85: i*85+85]
    cv2.imwrite("out/" + str(i) + ".png", img1)

ASCII 全ての文字を画像に変換

import cv2
import numpy as np

flag = "@!#$%&'()*+,-./0123456789:;<=>?ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_abcdefghijklmnopqrstuvwxyz{|}~"
flag = " "

print("FLAG: " + flag)
cnt= 0
for i in flag:
    images = np.full((100, 85, 3), (255,255,255), dtype=np.uint8)
    # char2img
    img = np.full((100, 85, 3), (255,255,255), dtype=np.uint8)
    cv2.putText(img, 'd', (0, 80), cv2.FONT_HERSHEY_PLAIN, 8, (0, 0, 0), 5, cv2.LINE_AA)

    # pixelization
    cv2.putText(img, "P", (0, 90), cv2.FONT_HERSHEY_PLAIN, 7, (0, 0, 0), 5, cv2.LINE_AA)
    cv2.putText(img, "I", (0, 90), cv2.FONT_HERSHEY_PLAIN, 8, (0, 0, 0), 5, cv2.LINE_AA)
    cv2.putText(img, "X", (0, 90), cv2.FONT_HERSHEY_PLAIN, 9, (0, 0, 0), 5, cv2.LINE_AA)
    simg = cv2.resize(img, None, fx=0.1, fy=0.1, interpolation=cv2.INTER_NEAREST) # WTF :-o
    img = cv2.resize(simg, img.shape[:2][::-1], interpolation=cv2.INTER_NEAREST)

    if images.all():
        images = img
    else:
        images = cv2.hconcat([images, img])
    cv2.imwrite("char/" + i + "_char.png", images)

これを実行した後,md5sumなどで各画像をハッシュ化し,一致している画像ペアを探します.すると,以下のようにフラグが得られそうです.

ctf4b{1f_y0u_p1x_y0u_c4n_[ここ一文字謎]3p1x}

一文字だけ復元できませんでした.そこでその文字と PIX だけの文字を比較します.
スクリーンショット 2021-05-23 0.24.25.png

上の画像が例の文字ですが,oのように見えます.ですがoと比較すると右上に少し何かがあったようです.そこでdと比較すると見た目は一致します.よって

ctf4b{1f_y0u_p1x_y0u_c4n_d3p1x}

です.後に知った情報ですが,OpenCV はバージョンによって結果が少し変わるそうです.なのでどうにか類似度を測ってやるのが最適そうです.

4
2
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2