Laravel:不正アクセスを撃退するLOG&Redirectの仕組みを作ってみた

不正アクセスを撃退するLOG&Redirectの仕組み

システムを組んでいるとマジでムカついてくる不正アクセス。
なぜこうも不正アクセスが多いのかホント腹立たしく感じています。
と言う事で、以前の記事で攻撃を受けるURLを書いたりLogに残ったIPから地域調べてみたりしましたが今回は撃退偏です。

まずは仕組みを考える

  1. アクセスlogでIPとアクセスURLの記録をする
  2. 攻撃対象となっている存在しないURLをブラックURLとして管理する
  3. ブラックURLに対してアクセスしてきたIPをブラックIPとして管理する
  4. ブラックIPでも、ブラックURLアクセス5回までは許してやる(出来心的な?許容範囲)
  5. ブラックIPがアクセスしたURLはブラックURLに登録する
  6. 許容範囲を超えたブラックIPは別の場所に勝手に飛んでもらう

はい、こんな感じで行きましょう。

必要となるテーブルを作る

欲しいテーブルは3つです。

  1. 基本的なアクセスログ(アクションログ) ⇒ table名:actlogs
  2. ブラックURLリスト ⇒ table名:blackurllists
  3. ブラックIPリスト ⇒ table名:blackiplists

MigrationとModel、Controllerの箱を作ります。

#マイグレーション作成
php artisan make:migration create_actlogs_table
php artisan make:migration create_blackurllists_table
php artisan make:migration create_blackiplists_table

#モデル作成
php artisan make:model Actlog
php artisan make:model Blackurllist
php artisan make:model Blackiplist

#コントローラー作成
php artisan make:controller ActlogController --resource
php artisan make:controller BlackurllistgController --resource
php artisan make:controller BlackiplistController --resource

マイグレーションファイルを編集します。

#actlogのMigration
<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateActlogsTable extends Migration
{
    public function up()
    {
        Schema::create('actlogs', function (Blueprint $table) {
            $table->increments('id');
            $table->integer('user_id')->unsigned()->nullable()->comment('作業者ID');
            $table->string('route')->nullable();
            $table->string('url')->nullable();
            $table->string('method')->nullable();
            $table->integer('status')->unsigned()->nullable();
            $table->text('message')->nullable();
            $table->string('remote_addr')->nullable();
            $table->string('user_agent')->nullable();
            $table->timestamps();

            $table->index(['created_at']);
            $table->index(['user_id']);
        });
    }

    public function down()
    {
        Schema::dropIfExists('actlogs');
    }
}
#blackurllistのMigration
<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateBlackurllistsTable extends Migration
{
    public function up()
    {
        Schema::create('blackaurllists', function (Blueprint $table) {
            $table->increments('id');
            $table->string('accessurl',255)->comment('要チェックアクセス');
            $table->timestamps();
        });
    }
    public function down()
    {
        Schema::dropIfExists('blackaccesslists');
    }
}
#blackiplistのMigration
<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateBlackiplistsTable extends Migration
{
    public function up()
    {
        Schema::create('blackiplists', function (Blueprint $table) {
            $table->increments('id');
            $table->string('blackip',20)->comment('不正アクセスのあったIP');
            $table->timestamps();
        });
    }
    public function down()
    {
        Schema::dropIfExists('blackiplists');
    }
}

マイグレーションを実行します。

php artisan migrate

これでテーブルが出来ました。

Modelを修正する

■モデル:Actlog

<?php

namespace App\;

use Illuminate\Database\Eloquent\Model;

class Actlog extends Model
{
    protected $guarded = ['*'];
    protected $casts = ['meta' => 'json'];
    const UPDATED_AT = null;

    public function __construct(array $attributes = []) {
        parent::__construct($attributes);
        $connection = env('DB_LOG_CONNECTION', env('DB_CONNECTION', 'mysql'));
    }

    protected $fillable = [
        'user_id',
        'route',
        'url',
        'method',
        'status',
        'message',
        'remote_addr',
        'user_agent',
    ];

    protected $hidden = [
    ];
}

テーブル:actlogsにはjsonがそのまま入る可能性があります。
protected $casts = ['meta' => 'json']; はその為の設定です。

BlackurllistとBlackiplist はartisanで作ったものそのままでOK。

Controllerの修正

Viewへつなげる設定だけ行って後はお好みでOK。

Log記載ミドルウェアを作成

php artisan make:middleware ActlogMiddleware

作成したミドルウェアをkernelに登録します。

場所:Project >> app >> Http >> Middleware >> Kernel.php

<?php

namespace App\Http;

use Illuminate\Foundation\Http\Kernel as HttpKernel;

class Kernel extends HttpKernel
{
    protected $middleware = [
        .
        .
        .
        \App\Http\Middleware\ActlogMiddleware::class,
    ];
        .
        .
        .
}

さぁ、これでした準備は整いました。
いよいよ不正アクセス締め出しシステムの本丸『ミドルウェア:Actlog』の造作です。

ActlogMiddleware.phpを作る

こんな感じにしました。

<?php

namespace App\Http\Middleware;

use Closure;
use App\Models\Actlog;
use Route;
use App\Models\Blackaccesslist;
use App\Models\Blackiplist;
use Carbon;

class ActlogMiddleware
{
    public function handle($request, Closure $next)
    {
        $response = $next($request);
        $this->actlog($request, $response->status());
        return $response;
    }

    public function actlog($request, $status)
    {
        $user = $request -> user();
        $data = [
            'user_id' => $user ? $user->id : null,
            'route' => Route::currentRouteName(),
            'url' => $request -> path(),
            'method' => $request -> method(),
            'status' => $status,
            'message' => count($request->toArray()) != 0 ? json_encode($request->toArray()) : null,
            'remote_addr' => $request -> ip(),
            'user_agent' => $request -> userAgent(),
        ];
        
        #ブラックIPに登録があるか確認 
        $BlackIp = Blackiplist::where('blackip',$data['remote_addr'])->first();
        #アクセスチェック
        $BlackAccess = Blackurllist::where('accessurl',$data['url'])->where('onoff',0)->first();
        #ログイン履歴があるか確認
        $Actlogs = Actlog::where('remote_addr',$data['remote_addr'])->whereNotNull('user_id')->first();
        
        #アクセス回数を取出す
        $visitcount = 0;
        if(!empty($BlackIp)){$visitcount = $BlackIp->count;}
        switch ($visitcount){
            case 0:
                #ブラックIP値なし
                $vol = self::checkBlackAccess($data,$Actlogs,$BlackAccess);
                break;
            case 1:
            case 2:
            case 3:
            case 4:
            case 5:
                #ブラックIP値あり規定数以下
                $vol = self::BlackCancelerPath($data,$Actlogs,$BlackIp,$BlackAccess);
                break;
            default :
                #ブラックIP値あり既定数以上
                $vol = self::BlackCancelerNG($data,$Actlogs,$BlackIp,$BlackAccess);
                break;
        }
        switch($vol){
            case 0:
            case 1:
                #通常処理
                Actlog::create($data);
                self::ErrorPageSeparater($status);
                break;
            default :
                $input = array('https://google.co.jp','https://www.rakuten.ne.jp','https://amazon.cn');
                #自動退場※勝手にリダイレクト
                return redirect(array_rand($input,1));
                break;
        }
    }
    
    /*
     * ■ブラックIP値なし
     * 1. $Actlogに値がある時何もしない
     * 2. $Actlogに値が無い時
     *   3.不正URLへのアクセスか確認
     *   4.不正URLへのアクセスの時
     *    5.ブラックIPに記載
     *   6.不正URLへのアクセスではない時
     *    7.何もしない
     */
    public function checkBlackAccess($data,$Actlogs,$BlackAccess)
    {
        $value = 0;
        if(empty($Actlogs)){//正式ログイン履歴無し
            if(!empty($BlackAccess)){//不正URLへのアクセスあり
                $item = new Blackiplist;
                $item->blackip = $data['remote_addr'];
                $item->count = 1;
                $item->onoff = 0;//非許可で登録
                $item->save();
                
                $value = 1;
            }
        }
        return $value;
    }
    
    /*
     * ■ブラックIP値あり既定数以上
     * 1. $Actlogに値がある時何もしない
     * 2. $Actlogに値が無い時
     *   3.不正URLへのアクセスか確認
     *   4.不正URLへのアクセスの時
     *    5.ブラックIPの値を加算
     *    6.不正URLを加算
     *   7.不正URLへのアクセスではない時
     *    7.不正URLにアクセスURLを記載※但しonoff=1で登録する
     */
    public function BlackCancelerNG($data,$Actlogs,$BlackIp,$BlackAccess)
    {
        $value = 0;
        if(empty($Actlogs)){//正規ログイン履歴なし
            if(!empty($BlackAccess)){//不正URLへのアクセス
                #IP最終アクセスを更新
                $item = Blackiplist::where('id',$BlackIp->id);//ブラックIPリストをサーチ
                $item->update(['count' => ($BlackIp->count)+1]);
                
                #URL最終アクセスを更新
                $item = Blackurllist::where('id',$BlackAccess->id);
                $item->update(['count'=> ($BlackAccess->count)+1]);
                
                $value = 2;
            }else{
                #念のため許可URLの記載も含めチェック ※'/'対策
                $BlackAccess2 = Blackurllist::where('accessurl',$data['url'])->first();
                if(!empty($BlackAccess2)){
                    #URL最終アクセスを更新
                    $item = Blackurllist::where('id',$BlackAccess2->id);
                    $item->update(['count'=> ($BlackAccess2->count)+1]);
                }else{
                    #URLアクセスを新規作成
                    $item = new Blackurllist;
                    $item->accessurl = $data['url']; 
                    $item->count = 1;
                    $item->onoff = 1;
                    $item->save();
                }
                $value = 2;
            }
        }
        return $value;
    }
    
    /*
     * ■ブラックIP値あり既定数以下
     * 1. $Actlogに値がある時何もしない
     * 2. $Actlogに値が無い時
     *   3.不正URLへのアクセスか確認
     *   4.不正URLへのアクセスの時
     *    5.ブラックIPの値を加算
     *    6.不正URLを加算
     *   7.不正URLへのアクセスではない時
     *    7.不正URLにアクセスURLを記載※但しonoff=1で登録する
     */
    public function BlackCancelerPath($data,$Actlogs,$BlackIp,$BlackAccess)
    {
        $value = 0;
        if(empty($Actlogs)){
            if(!empty($BlackAccess)){
                #IP最終アクセスを更新
                $item = Blackiplist::where('id',$BlackIp->id);//ブラックIPリストをサーチ
                $item->update(['count' => ($BlackIp->count)+1]);
                
                #URL最終アクセスを更新
                $item = Blackurllist::where('id',$BlackAccess->id);
                $item->update(['count'=> ($BlackAccess->count)+1]);
                
                $value = 1;
            }else{
                #念のため許可URLの記載も含めチェック ※'/'対策
                $BlackAccess2 = Blackurllist::where('accessurl',$data['url'])->first();
                if(!empty($BlackAccess2)){
                    #URL最終アクセスを更新
                    $item = Blackurllist::where('id',$BlackAccess2->id);
                    $item->update(['count'=> ($BlackAccess2->count)+1]);
                }else{
                    #URLアクセスを新規作成
                    $item = new Blackurllist;
                    $item->accessurl = $data['url']; 
                    $item->count = 1;
                    $item->onoff = 1;
                    $item->save();
                }
                $value = 1;
            }
        }
        return $value;
    }
    
    #Errorフィルター
    public function ErrorPageSeparater($status)
    {
        switch($status){
            case 400:
                return redirect('error/500');
                break;
            case 401:
                return redirect('error/500');
                break;
            case 403:
                return redirect('error/500');
                break;
            case 404:
                return redirect('error/404');
                break;
            case 405:
                return redirect('error/404');
                break;
            case 419:
                return redirect('error/419');
                break;
            case 500:
                return redirect('error/500');
                break;
            case 503:
                return redirect('error/500');
                break;
            case 999:
                return redirect('error/999');
                break;
            default :
                break;
        }
    }
}

不正アクセス野郎だったら勝手にRedirect!

$input = array('https://google.co.jp','https://www.rakuten.ne.jp','https://amazon.cn');
return redirect(array_rand($input,1));

まとめ

これ作ってモニタリングしてると、500回越えの不正アクセスがざらにある事がわかります。

それもACCESS元調べれば大体が中国。セキュリティしっかりしないとですね。

\ 最新情報をチェック /

コメントを残す

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください