[TsukuCTF 2022] My own writeup
TsukuCTF 2022が10/22 12:20 - 10/23 18:00(JST)の日程で開催されました. webを解いていたので, そのwriteupを以下に記します.
- [web] bughunter(86 solves)
- [web] viewer(8 solves, not solved)
[web] bughunter(86 solves)
問題文
天才ハッカーのつくし君は、どんなサイトの脆弱性でも見つけることができます。 あなたも彼のようにこのサイトの脆弱性を見つけることができますか? 見つけたら私たちに報告してください。 ※ディレクトリの総当たりなどは禁止されています。本問題の解決には、多数のリクエストは不要です。
http://133.130.103.51:31415
解法
問題にRFC9116
がタグ付けされています. RFC9116
で検索すると, こちらのページが見つかります.
RFC9116は、組織が脆弱性の開示方法を説明し、セキュリティ研究者などが発見した脆弱性を報告しやすくするために定義されたものです。この仕組みは単純で、必要な情報を記載した「security.txt」ファイルをWebサイトの「/.well-known/」パスの下に公開することを要求しています。「security.txt」には次の項目が記述されます。
実際に/.well-known/security.txt
をGETすると, flagが書かれていました.
$ curl "http://133.130.103.51:31415/.well-known/security.txt"
Contact: TsukuCTF22{y0u_c4n_c47ch_bu65_4ll_y34r_r0und_1n_7h3_1n73rn37}
Expires: 2022-10-20T15:00:00.000Z
Preferred-Languages: ja, en%
[web] viewer(8 solves, not solved)
問題文
Writeups for TsukuCTF21 have been published. Check them out if you’d like!
解法
解けませんでした. 配布コードのapp.py
は以下のようになっています.
from flask import (
Flask,
abort,
make_response,
render_template,
request,
redirect
)
import redis
import pycurl
from io import BytesIO
import traceback
import uuid
import json
app = Flask(__name__)
# initialization
redis = redis.Redis(host='redis', port=6379, db=0)
flag = "TsukuCTF22{dummy flag}" # the flag is replaced a real flag in a production environment.
id = str(uuid.uuid4())
redis.set(id, json.dumps({"id": id, "name": flag}))
# only 'http' and 'https' should have been allowed, right?
# ref: https://everything.curl.dev/cmdline/urls/scheme#supported-schemes
blacklist_of_scheme = ['dict', 'file', 'ftp', 'gopher', 'imap', 'ldap', 'mqtt', 'pop3', 'rtmp', 'rtsp', 'scp', 'smb', 'smtp', 'telnet']
def url_sanitizer(uri: str) -> str:
if len(uri) == 0 or any([scheme in uri for scheme in blacklist_of_scheme]):
return "https://fans.sechack365.com"
return uri
# a response is also sanitized just in case because the flag is super sensitive information.
blacklist_in_response = ['TsukuCTF22']
def response_sanitizer(body: str) -> str:
if any([scheme in body for scheme in blacklist_in_response]):
return "SANITIZED: a sensitive data is included!"
return body
@app.route("/<path:path>")
def missing_handler(path):
abort(404, "ページが見つかりません")
@app.route("/", methods=["GET", "POST"])
def route_index():
session_id = request.cookies.get('__SESSION_ID')
name = None
if session_id is not None:
res= redis.get(session_id)
if res is not None:
user = json.loads(res)
print(f"user: {user}")
name = user["name"]
if name is not None and "TsukuCTF22{" in name:
name = "tsukushi"
else:
return redirect('/register')
if request.method == "POST":
url = url_sanitizer(request.form.get("url"))
buf = BytesIO()
try:
c = pycurl.Curl()
c.setopt(c.URL, url)
c.setopt(c.WRITEDATA, buf)
c.perform()
c.close()
body = buf.getvalue().decode('utf-8')
print(body)
except Exception as e:
print(e)
traceback.print_exc()
abort("error occurs")
return render_template("index.html", url=url, data=response_sanitizer(body), name=name)
return render_template("index.html", data=None, name=name)
@app.route("/register", methods=["GET"])
def register():
return render_template("register.html")
@app.route("/register", methods=["POST"])
def register_post():
name = request.form.get("name")
redis.set(id, json.dumps({"id": str(uuid.uuid4()), "name": name}))
redis.expire(id, 100)
resp = make_response(redirect('/'))
resp.set_cookie('__SESSION_ID', id)
return resp
@app.route("/logout", methods=["GET"])
def logout():
resp = make_response(redirect('/'))
resp.set_cookie('__SESSION_ID', '', expires=0)
return resp
if __name__ == "__main__":
app.run(debug=False, host="0.0.0.0", port=31555)
以下の部分を見ると, インラインメモリデータストアであるredisにflagを格納していることがわかります.
# initialization
redis = redis.Redis(host='redis', port=6379, db=0)
flag = "TsukuCTF22{dummy flag}" # the flag is replaced a real flag in a production environment.
id = str(uuid.uuid4())
redis.set(id, json.dumps({"id": id, "name": flag}))
また, 以下の部分を見ると, ユーザーから任意のURLを受け取ってそれを元にpycurlを用いてリクエストを送信し, レスポンスをhtmlにレンダリングすることがわかります.
if request.method == "POST":
url = url_sanitizer(request.form.get("url"))
buf = BytesIO()
try:
c = pycurl.Curl()
c.setopt(c.URL, url)
c.setopt(c.WRITEDATA, buf)
c.perform()
c.close()
body = buf.getvalue().decode('utf-8')
print(body)
except Exception as e:
print(e)
traceback.print_exc()
abort("error occurs")
return render_template("index.html", url=url, data=response_sanitizer(body), name=name)
これらの情報からSSRFの脆弱性を用いて, redisからflagを取得するのだろうという予想を立てることができます.
しかし, ユーザーがPOSTするURLは以下のようにサニタイズされており, プロトコルがhttpまたはhttpsしか使用できないようです.
def url_sanitizer(uri: str) -> str:
if len(uri) == 0 or any([scheme in uri for scheme in blacklist_of_scheme]):
return "https://fans.sechack365.com"
return uri
調べると, RedisクライアントはRESPプロトコルを用いてサーバを通信するようです. そして, curlがサポートしているプロトコルではtelnetやgopherを用いて通信ができるようですが, これらのプロトコルはサニタイズによって弾かれます. しかし, httpでも通信ができるという旨の記事を発見したため, httpで通信する方向性で頑張っていましたが, 解けませんでした.
実際に解かれている方のwriteupを見ると, どうやらredisの最近のバージョンではhttpは使用できないようです(参考).
代わりに, URLのサニタイズ部分で小文字のチェックはしていますが, 大文字をまじえた文字列のチェックはしていないのでそれを利用してgopherやdictなどのプロトコルで通信し, flagをredisから抜き出すようです(詳細は割愛).
感想
OSINT中心のCTFでしたが, web問題もしっかり難易度が高くてとても勉強になりました. viewerのような歯応えのある問題をしっかり通せるように頑張りたいです.