【Laravel】標的型攻撃メールの訓練システムをつくる

こんにちは❗フリーランス・エンジニアの 九保すこひ です。

さてさて、以前セキュリティの専門家とお仕事をさせていただいたことがあるのですが、その際に「あるシステム」のことを教えていただきました。

それは・・・・・・

標的型攻撃メール・訓練システム

です。

これは、従業員さんにわざとメールを送信し、誰が「ちゃんと対処できた/できてない」かを調べるシステムのことで、つまり学校でよくやった「抜き打ちテスト」のようなものですね。

※ ちなみに、2018年に約580億円相当の仮想通貨NEMが流出した事件がありましたが、その発端になったのも、標的型攻撃メールだと言われています。

そこで❗

今回はこの標的型メール訓練システムをLaravelでつくってみたいと思います。
ぜひ何かの参考になりましたら嬉しいです😊✨

「出川さんの『兄さん知らないんだ』の
コマーシャル好きだったな・・・」

開発環境: Laravel 8.x

やりたいこと

今回開発するシステムの詳細は次のとおりです。

  • 個別の従業員にメールを送信
  • 誰が「メールを開封した」「添付ファイルを開いた」「リンクをクリックした」のデータを保存する

実装する方法

今回は(擬似的な)画像URLを使います。

例えば、http://example.com/image にアクセスするとPHPが実行されるけれど、実際には画像を表示するということができます。

これを利用して、次のようにパラメータをつけて「誰が何をしたか?」をチェックできるようにします。

<!-- ここは画像が表示されます -->
<img src="http://example.com/image?action=mail_opened&user_id=111">

なお、添付ファイルはzipで圧縮されたHTMLファイルで実装します。

⚠ ご注意
メールサービスによっては画像URLを遮断する設定になっている場合があります。そのため、もし必ず「メール開封」チェックをしたい場合は画像の表示を許可しておく必要があります。

前提として

ログイン機能がすでにインストールされていて、何人かのテストユーザーが保存されていることが前提です。

もしまだの方は以下のどちらかを参考にしてください。

テーブルの中はこんなカンジです。

モデル&マイグレーションをつくる

では、データベースまわりを構築するためにモデル&マイグレーションをつくっていきます。

以下のコマンドを実行してください。

php artisan make:model AttackTraining -m

すると、モデルとマイグレーションが一気に作成されますので、中身をそれぞれ以下のようにします。

モデル

app/Models/AttackTraining.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class AttackTraining extends Model
{
    use HasFactory;

    protected $guarded = ['id'];

    // Relationship
    public function user() {

        return $this->belongsTo(User::class, 'user_id', 'id');

    }

    // Accessor
    public function getEmailOpenedUrlAttribute() {

        return route('attack_training.image', [
           $this->uuid,
           'type' => 'email_opened'
        ]);

    }

    public function getFileOpenedUrlAttribute() {

        return route('attack_training.image', [
           $this->uuid,
           'type' => 'file_opened'
        ]);

    }

    public function getLinkClickedUrlAttribute() {

        return route('attack_training.image', [
           $this->uuid,
           'type' => 'link_clicked'
        ]);

    }

    // Others
    public function generateZipFile() {

        $html = view('html.attack_training')
            ->with(['file_opened_url' => $this->file_opened_url])
            ->render();

        $zip_path = storage_path('app/zip/'. $this->uuid .'.zip');
        $zip = new \ZipArchive();

        if($zip->open($zip_path, \ZipArchive::CREATE) === TRUE) {

            $zip->addFromString('security.html', $html);
            $zip->close();

            return $zip_path;

        }

        return null;

    }
}

なお、この中でやっているのは大まかに以下4つです。

① $guarded を追加

mass assignmentエラーが出ないように$guardedを追加しておきます。

② リレーションシップ

usersテーブルとのリレーションシップをつくっています。

これで、以下のように簡単に関連付けられたユーザーを取得することができるようになります。

$user = $attack_training->user;

③ Accessor

テーブル内には存在しないフィールドを「あたかも存在している」かのようにすることができるのがLaravelの便利機能Accessorです。

AttackTrainingモデルのなかでは3つのAccessorをつくっていて、以下のように使います。

$attack_training = \App\Models\AttackTraining::first();

dump($attack_training->email_opened_url); // メールが開封ときの画像URL
dump($attack_training->file_opened_url); // 添付ファイルを開いたときの画像URL
dump($attack_training->link_clicked_url); // リンクをクリックしたときの画像URL

④ generateZipFile()

訓練メールに添付するzipファイルを作成するためのものです。
以下のようにするとzipファイルが作成できます。

$attack_training = \App\Models\AttackTraining::first();
$zip_path = $attack_training->generateZipFile();

マイグレーション

続いて、マイグレーションです。

database/migrations/****_**_**_******_create_attack_trainings_table.php

// 省略

public function up()
{
    Schema::create('attack_trainings', function (Blueprint $table) {
        $table->id();
        $table->unsignedBigInteger('user_id')->comment('ユーザーID');
        $table->uuid('uuid')->comment('UUID');
        $table->dateTime('email_opened_at')->nullable()->comment('日時:メールを開封した');
        $table->dateTime('file_opened_at')->nullable()->comment('日時:添付ファイルを開いた');
        $table->dateTime('link_clicked_at')->nullable()->comment('日時:リンクをクリックした');
        $table->timestamps();

        $table->foreign('user_id')->references('id')->on('users');
    });
}

// 省略

※ 実際のところ、訓練は何度も実施することになるので、訓練ごとの識別IDを設けるべきですが、複雑になってしまうので割愛しています。

テストデータをつくる

続いて、テストユーザーに対する訓練メールのテストデータをつくるようにします。

以下のコマンドを実行してください。

php artisan make:seed AttackTrainingsTableSeeder

すると、ファイルが作成されるので中身を以下のように変更します。

database/seeders/AttackTrainingsTableSeeder.php

<?php

namespace Database\Seeders;

use App\Models\AttackTraining;
use App\Models\User;
use Illuminate\Database\Seeder;
use Illuminate\Support\Str;

class AttackTrainingsTableSeeder extends Seeder
{
    public function run()
    {
        $users = User::get();

        foreach ($users as $user) {

            $attack_training = new AttackTraining();
            $attack_training->uuid = Str::uuid();
            $attack_training->user_id = $user->id;
            $attack_training->save();

        }
    }
}

変更が完了したら、AttackTrainingsTableSeederLaravelへ登録します。

database/seeders/DatabaseSeeder.php

<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    public function run()
    {
        $this->call(UsersTableSeeder::class); // 👈 ここは UserFactory でもOKです
        $this->call(AttackTrainingsTableSeeder::class); // 👈 ここを追加しました
    }
}

では、これでマイグレーションを初期化してみましょう。

php artisan migrate:fresh --seed

するとテーブルはこのようになります。

画像URLをつくる

では、「実装する方法」で書いた、PHPが実行されるけれど、実際には画像が表示されるURLをつくっていきます。

表示する画像を用意する

今回は以下の画像が表示されるURLをLaravelで作ります。

(ちょっとした遊び心ですが、もちろん本番環境ではもっとしっかりした画像を用意してください😂)

では、この画像を「/storage/app/images/attack_training.png」として保存します。

ルートをつくる

続いてルートです。

routes/web.php

use App\Http\Controllers\AttackTrainingController;

// 省略
Route::get('attack_training/image/{attack_training:uuid}', [AttackTrainingController::class, 'image'])->name('attack_training.image');

実際のURLは以下のようなものになります。

http://******/attack_training/image/89697fde-90f4-4d47-a715-1a4b2ac621fa?type=email_opened

コントローラーをつくる

続いてコントローラーです。

以下のコマンドを実行してください。

php artisan make:controller AttackTrainingController

するとファイルが作成されるので、中身を以下のように変更します。

app/Http/Controllers/AttackTrainingController.php

<?php

namespace App\Http\Controllers;

use App\Models\AttackTraining;
use Illuminate\Http\Request;

class AttackTrainingController extends Controller
{
    public function image(AttackTraining $attack_training, Request $request) {

        $type = $request->type;
        $valid_types = ['email_opened', 'file_opened', 'link_clicked'];

        if(in_array($type, $valid_types)) {

            $attack_training->{$type .'_at'} = now();
            $attack_training->save(); // 👈 ここで訓練メールのデータを保存しています。

            $image_path = storage_path('app/images/attack_training.png');
            $image_data = file_get_contents($image_path);
            return response($image_data)->header('Content-type','image/png');

        }

        abort(404);

    }
}

これで、このURLにアクセスすると画像が表示されますが、裏で訓練メールのデータも保存できるようになりました。

添付ファイル作成機能をつくる

添付するHTMLファイル内で画像を表示することになりますが、この画像のURLは各宛先ごとに個別のパラメータを持たせなければいけません。

そのため、メール送信するごとにHTMLファイルを作成し、さらにzipファイル化できるようにしていきます。

※ なお、PHPzipモジュールがインストールされている必要があります。
phpinfo()で事前に確認しておいてください。

フォルダをつくる

次にzipファイルを保存するフォルダを作成しておきます。
(書き込み権限をつけておいてください)

ビューをつくる

続いて、HTMLの中身を作成します。
今回はテストなので中身は画像だけです。

resources/views/html/attack_training.blade.php

<!doctype html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
</head>
<body>
    <img src="{{ $file_opened_url }}">
</body>
</html>

メール送信部分をつくる

続いて、先ほどのzipファイルを添付してメールで送信する部分です。

ビューをつくる

今回は訓練メールなので、「コレ何かあやしくない❓」というようなヒントを本文の中に含めておくことにします。(太字の部分が中国語の漢字です)

<html>
<body>
    <h1>日本セキュリティ対策機構より</h1>
    <div>
        本日、務上システムにおいて任意のコードが実行可能な脆弱性が報告されました。<br>
        この脆弱性のレベルは「急」です。<br><br>
        早急に添付ファイルの手順にしたがって対策を行ってください。<br><br>
        <img src="{{ $email_opened_url }}">
    </div>
</body>
</html>

※ もちろん「日本セキュリティ対策機構」は実際には存在しない架空の団体です。

Mailableをつくる

続いて、LaravelMailableをつくります。

以下のコマンドを実行してください。

php artisan make:mail AttackTrainingMailable

すると、ファイルが作成されるので中身を以下のように変更します。

<?php

namespace App\Mail;

use App\Models\AttackTraining;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;

class AttackTrainingMailable extends Mailable
{
    use Queueable, SerializesModels;

    private $attack_training;

    /**
     * Create a new message instance.
     *
     * @return void
     */
    public function __construct(AttackTraining $attack_training)
    {
        $this->attack_training = $attack_training;
    }

    /**
     * Build the message.
     *
     * @return $this
     */
    public function build()
    {
        $user = $this->attack_training->user;
        $zip_path = $this->attack_training->generateZipFile();

        return $this
            ->to($user->email)
            ->from('japan.security@example.org', '日本セキュリティ対策機構')
            ->subject('【日本セキュリティ対策機構】緊急警報')
            ->view('email.attack_training')
            ->with(['attack_training' => $this->attack_training])
            ->attach($zip_path, [
                'as' => 'clear-security.zip',
                'mime' => 'application/zip',
            ]);
    }
}

これで訓練メールが送信できるようになりました。

テストしてみる

では、以下のコードを実行してテストしてみましょう❗

$attack_training = \App\Models\AttackTraining::find(1);
\Mail::send(new \App\Mail\AttackTrainingMailable($attack_training));

ちなみに、現在のDBテーブルはこうなっています。(3つのチェック項目が全てNULLです)

では、メール送信してメールボックスを確認してみましょう。

緊急警報」というメールが届きました。
では、中身を開いてみましょう。

はい❗
今回必要な以下3つのコンテンツがきちんと入っています。

  • (開けちゃいけない)添付ファイル
  • (クリックしちゃいけない)リンク
  • (見ちゃいけなかった)メール内の画像

では、この状態でDBテーブルを見てみましょう。

メールが開封されたので、email_opened_atに日時が保存されました。

では、次に添付ファイル「clear-security.zip」をダウンロードして展開してみます。

展開したHTMLファイルをダブルクリックしてブラウザで表示してみましょう。

画像が表示されました。
では、この状態でDBを見てみます。

はい❗
今度は、ファイルが開封された日時としてfile_opened_atが保存されました。

では最後に、メールに書かれていた以下のリンクをクリックしてみましょう。

すると・・・・・・

はい❗
画像だけが表示されました。

DBテーブルを確認してみます。

想定通りlink_clicked_atにデータが保存されています。

全て成功です✨😊👍

ちなみに

訓練メールでは、もちろん一番「メールを開封しないこと」が大事ですが、それと同じくらい大事なのが、「変なメールを開封してしまった」ときちんと報告することです。

そのため、今回の拡張機能としては、attack_reported_atというようなフィールドを用意しておいて、「メールは開封してしまったけど、報告はしてくれた」というデータが保存できるようにしてもいいかもしれません。

おわりに

ということで今回は、標的型攻撃メールの訓練をするためのシステムを開発してみました。

やはりこういった攻撃自体がなくなればいいですが、完全に無くすのは難しいですし、単に資料を見るだけでは、いわゆる「畳水練」になってしまうと思います。

そのため、やはり実際にこういったシステムを使って日頃から訓練しておくのがいいのかもしれません。

以前セキュリティ関連の方に聞いた話ですが、「攻撃するより守る方が難しい」そうです。どこをついてくるか分からないですからね。

セキュリティ技術者のお給料がいいというのもうなずけます。

ぜひ皆さんも試してみてくださいね。

ではでは〜❗

「クライアントさんに頂いた
ソーセージ、ウマウマです🍴😊✨」

開発のご依頼お待ちしております 😊✨ お問い合わせ
また、こちらもお待ちしております。
  • 実案件の開発サポート: 詳細
  • ツイッターのフォロー: 詳細
  • 投げ銭のご支援: 詳細
どうぞよろしくお願いいたします!
このエントリーをはてなブックマークに追加       follow us in feedly