フィッシングの調査は、たいてい1通のメールから始まります。ユーザーから報告が来る、アラートが上がる、アナリストがメールを開いて送信元とURLを確認する。ここまでは自然な流れですが、実運用では「この1通は悪性か」を判断しただけでは調査が終わりません。
攻撃者はキャンペーン単位で動くからです。送信元インフラ、件名、URL、テンプレートを使い回し、複数のユーザーに同時にばらまきます。報告された1通は、全体のほんの一部かもしれません。
この記事では、1通起点の手作業調査がどこで詰まるのかを整理したうえで、Microsoft Defender XDRのCampaignInfoテーブルを軸に、「誰に届いたか」「誰がクリックしたか」「クリック後に何が起きたか」をKQLで一気に出す方法を説明します。考え方の元になっているのは、Microsoft Tech Communityの「Campaign-Centric Hunting with Microsoft Defender XDR and Microsoft Sentinel」という投稿です。そこにある流れを土台に、Sentinel側の認証ログとの突き合わせ(後述)を足した形でまとめています。
相次ぐ攻撃で、「相関」を見る調査のニーズが増えている
2026年10月、国内で大手企業を狙った攻撃が相次ぎました。報道ベースでは、20社超・1,400万人超の情報が影響を受けたとされています(公式な内訳は未公表)。目立つのは、委託先、クラウド基盤、メール基盤といった「つながっている先」が起点になった事案です。国の側も、各省庁を通じて自治体や民間企業にセキュリティ対策の指示を出しています(法的な非常事態宣言ではなく、行政的な警告という位置づけです)。
こうなると、現場から出てくる質問が変わります。「このメールは悪性か」ではなく、「自社は影響を受けたのか」「どこまで広がっているのか」です。今回の件を受けて、「もう一段踏み込んで調べたい」という声も聞こえてきます。ただ、1つのログを眺めるだけでは答えが出ません。複数のデータを突き合わせる、つまり相関の切り口が必要になります。
この記事で扱う相関の切り口を、先に整理しておきます。
| 相関の切り口 | 確認したいこと | 主なデータ |
|---|---|---|
| メール × クリック | 誰に届き、誰が触ったか |
CampaignInfo、EmailEvents、UrlClickEvents
|
| クリック × 認証 | クリックの直後に、不審なサインインが成功していないか |
UrlClickEvents、imAuthentication
|
| 認証元IP × ユーザー | 同じIPが複数のユーザーで使われていないか(同一インフラの可能性) | imAuthentication |
| アカウント × 配信後の挙動 | 受信トレイルールや転送設定が悪用されていないか |
OfficeActivityなど |
| 委託先・外部接続 × サインイン | 共有している資格情報やサービスプリンシパルに異常がないか | Watchlist、サインインログ |
下の2行は、本記事のクエリでは扱いません。まず上の3行を、KQLで出していきます。
1通起点の手作業調査が詰まるところ
手作業で調べる場合、典型的には次の流れになります。
- メール詳細から送信元・件名・URLを確認する
- 同じ送信元や件名で、他の受信者がいないか検索する
- URLをクリックした人を探す
- クリックした人のサインインログを開き、不審なIPがないか見る
- 配信後に削除(ZAPや手動)されたかを確認する
1件ならこれで回ります。ただ、実運用では次の問題が出てきます。
- 「他の受信者」の検索条件が担当者の経験に依存する
- 件数が増えると、どの案件から見るべきかの判断基準がバラバラになる
- 画面をまたいで突き合わせるため、見落としと所要時間が増える
一番大きいのは、判断の単位が「メール1通」のままだという点です。対応の優先度は、本来そのキャンペーンの影響範囲で決まるべきです。同じ報告でも、ブロック済みで受信者が1人のものと、50人に配信されて2人がクリックしているものでは、緊急度がまったく違います。
キャンペーン単位に切り替える
Defender for Office 365のCampaign Viewsは、フィッシングやマルウェアの協調的な攻撃をキャンペーンとして束ねて表示してくれます。これをKQLから扱えるようにしたのがCampaignInfoテーブルです。主なフィールドは次のとおりです。
| フィールド | 内容 |
|---|---|
| CampaignId / CampaignName | キャンペーンの識別子と名前 |
| CampaignType | PhishやMalwareなどの種別 |
| CampaignSubtype | 騙られているブランドやマルウェアファミリなどの補足 |
| NetworkMessageId | メール1通を識別するID |
| RecipientEmailAddress | 対象になった受信者 |
結合の鍵はNetworkMessageIdです。これでEmailEvents(配信結果)、UrlClickEvents(クリック)、EmailPostDeliveryEvents(配信後の削除など)とつなげられます。
注意点として、CampaignInfoは執筆時点でプレビュー扱いです。本番の自動化に使う前に、テナントでのテーブルの有無、スキーマ、結果を必ず確認してください。
クエリ1: キャンペーンごとの配信状況
まず、直近のキャンペーンが「届いたのか、止まったのか」を見ます。
CampaignInfo
| where Timestamp > ago(14d)
| join kind=leftouter (
EmailEvents
| where Timestamp > ago(14d)
| project NetworkMessageId, RecipientEmailAddress, DeliveryAction, DeliveryLocation
) on NetworkMessageId, RecipientEmailAddress
| summarize
Recipients = dcount(RecipientEmailAddress),
Delivered = dcountif(RecipientEmailAddress, DeliveryAction =~ "Delivered"),
Inbox = dcountif(RecipientEmailAddress, DeliveryLocation has "Inbox")
by CampaignId, CampaignName, CampaignType, CampaignSubtype
| order by Inbox desc, Recipients desc
全件ブロックされたキャンペーンと、受信トレイまで届いたキャンペーンを分けるのが目的です。受信トレイに届いたものから見ていけば、調査の入り口が揃います。
クエリ2: クリックした(クリックスルーした)ユーザー
届いたことが分かったら、次は誰が触ったかです。Safe Linksの警告ページを越えて進んだケース(クリックスルー)は特に重要です。
CampaignInfo
| where Timestamp > ago(14d)
| join kind=inner (
UrlClickEvents
| where Timestamp > ago(14d)
| where IsClickedThrough == true or ActionType == "ClickAllowed"
| project NetworkMessageId, AccountUpn, Url, IsClickedThrough, ClickTime = Timestamp
) on NetworkMessageId
| summarize
FirstClick = min(ClickTime),
ClickThroughs = countif(IsClickedThrough == true),
Urls = make_set(Url, 5)
by CampaignId, CampaignName, CampaignSubtype, AccountUpn
| order by ClickThroughs desc, FirstClick desc
クリックスルーが即「侵害された」を意味するわけではありません。ただ、アカウントを詳しく調べる強い理由にはなります。
クエリ3: クリック後にサインインが成功していないか
ここからがSentinel側の出番です。認証情報を入力させるタイプのフィッシングでは、クリックの直後に、正規のサインインが成功することがあります。この突き合わせを、生のSigninLogsではなく、ASIMのimAuthenticationで行います(理由は以前の記事に書いたとおりです。認証ソースが増えても、同じクエリがそのまま使えます)。
let clicks = CampaignInfo
| where Timestamp > ago(7d)
| join kind=inner (
UrlClickEvents
| where Timestamp > ago(7d)
| where IsClickedThrough == true or ActionType == "ClickAllowed"
| project NetworkMessageId, AccountUpn, ClickTime = Timestamp
) on NetworkMessageId
| project CampaignName, User = tolower(AccountUpn), ClickTime;
imAuthentication
| where TimeGenerated > ago(7d)
| where EventResult == "Success"
| extend User = tolower(TargetUsername)
| join kind=inner clicks on User
| where TimeGenerated between (ClickTime .. ClickTime + 1h)
| project CampaignName, User, ClickTime, SignInTime = TimeGenerated, SrcIpAddr, EventVendor, EventProduct
Defender XDR側のテーブルはTimestamp、Sentinel側のASIMはTimeGeneratedと、時刻のフィールド名が違う点に注意してください。また、TargetUsernameがUPN形式で入るかどうかはソースによって違うので、結合キーの正規化は環境に合わせて調整が必要です。
クエリ4: 同じIPが複数のユーザーで使われていないか
クエリ3では「誰のアカウントに、クリック後のサインインがあったか」を見ました。次は視点を変えて、サインイン元のIPを軸に集計します。同じキャンペーンでクリックしたユーザーが、同じIPからサインインしている場合、攻撃者が同一のインフラを使い回している可能性があります。
let clicks = CampaignInfo
| where Timestamp > ago(7d)
| join kind=inner (
UrlClickEvents
| where Timestamp > ago(7d)
| where IsClickedThrough == true or ActionType == "ClickAllowed"
| project NetworkMessageId, AccountUpn, ClickTime = Timestamp
) on NetworkMessageId
| project CampaignId, CampaignName, User = tolower(AccountUpn), ClickTime;
imAuthentication
| where TimeGenerated > ago(7d)
| where EventResult == "Success"
| extend User = tolower(TargetUsername)
| join kind=inner clicks on User
| where TimeGenerated between (ClickTime .. ClickTime + 1h)
| summarize
Users = dcount(User),
UserList = make_set(User, 10),
FirstSeen = min(TimeGenerated)
by CampaignName, SrcIpAddr
| where Users >= 2
| order by Users desc
1人のユーザーの異常は、出張や在宅でも起こります。ところが、複数のユーザーが同じキャンペーンのクリック後に、同じIPからサインインしているなら話が違います。個別のアラートを見ているだけでは気づきにくい、横のつながりです。社内の共有NATやプロキシのIPも拾うので、結果が出たら、まず自社の出口IPかどうかを確認してください。
優先度をどう決めるか
クエリの結果を並べるだけでは、まだ「どれから見るか」が決まりません。簡単な基準を決めておくと、判断が揃います。
| 状況 | 優先度の目安 |
|---|---|
| 配信前にブロック | 低 |
| 迷惑メールフォルダに配信 | 低〜中 |
| 受信トレイに配信 | 中 |
| 複数の受信トレイに配信 | 中〜高 |
| URLをクリック | 高 |
| 警告をクリックスルー | 高 |
| クリック後に新しい場所からのサインイン成功 | 最優先 |
この基準は元の投稿にある考え方を参考にしたものです。組織のリスクや対応プロセスに合わせて調整してください。
Sentinelで回す
クエリ2とクエリ3は、Sentinelの分析ルール(Scheduled query rule)にできます。ポイントは次のとおりです。
- エンティティマッピング:
AccountUpnをAccount、SrcIpAddrをIP、UrlをURLに対応付ける - アラートのグルーピング: 同じキャンペーン、同じアカウントのアラートを1つのインシデントにまとめる
- 実行間隔と参照期間: クリックと認証のタイムラグを考えて、参照期間は実行間隔より長めにとる
Defender XDRとSentinelを接続していれば、Microsoft XDR由来のインシデントもSecurityIncidentテーブルから見られます。キャンペーン調査の結果と、この既存インシデントの状況を並べて見ると、すでに対応済みの案件を重複して調べるのを避けられます。
実運用で詰まる点
この方法も万能ではありません。実際に入れると、次の点にぶつかります。
-
UrlClickEventsが、メールのメタデータと結合できないケースがある(下書きや送信済みアイテムからのクリックなど) -
CampaignInfoはプレビューなので、スキーマや提供状況が変わりうる - Defender for Office 365のプラン、Campaign Viewsの権限、Defender XDRコネクタやイベントストリーミングの設定、データの保持期間と遅延に依存する
- 結果が多いテナントでは、前の記事でも触れた結果上限に当たる可能性がある。集計を先に行い、行数を「キャンペーン数」「ユーザー数」のレベルに抑える設計が必要
特に1つ目は、結果が「0件」だったときの解釈に効きます。クリックがなかったのか、結合できなかっただけなのかを、これだけでは区別できません。ここは別のログ(プロキシ、エンドポイントなど)で補う前提で考えるのが安全です。
検知の後に残る仕事と、自動化できる部分
ここまでで「誰が影響を受けたか」はKQLで出せます。ただ、ルールを入れて回し始めると、次の仕事が残ります。
- 同じ事象が複数のルールで鳴り、似たインシデントが並ぶ
- 「Phish detected」程度の通知文で、次に何を確認すべきかが分からない
- 出張や在宅による「初めて見るIP」が、誤検知として混ざる
- 毎回、URLやIPの評判を手で調べ、過去に同じ案件がなかったかを探す
最初の手作業の流れ(メールを開く、URLを調べる、他の受信者を探す、サインインを確認する)のうち、クエリで置き換えられるのは「探す」部分です。そのあとの「整理して、読んで、判断できる形にする」部分は、ルールを足しても手間が減りにくいところです。
相関の切り口を増やすほど調査は深くなりますが、そのぶん出てくるアラートも、見るべきケースも増えます。クエリを足すたびに、整理の手間が膨らんでいくわけです。
この後段は、ルールを書くよりも「アラートをどう束ねて、どう優先づけるか」という設計の問題です。参考までに、私たちが開発しているSIEM+(https://siem.plus)で、この部分をどう扱っているかを簡単に紹介します。SIEM+はMicrosoft Sentinelの上に載せるAIトリアージ層で、考え方は「ケース管理」と「AIによる優先づけ」の2つです。
ケース管理: 同じ件を、1つのケースとして扱う
手作業では、似たアラートが並ぶたびに「これは同じ件か」「前にも調べたか」を人が判断します。これをケース単位の管理に寄せています。
- 重複したアラートを、優先度付きのインシデント(ケース)にまとめる
- 新しい事象を、過去のケースと照合してから調査に回す(同じ件を二重に調べない)
- 既知の正常/不審リストを適用し、一度判断した内容を次回以降に引き継ぐ
キャンペーン調査のように、1つの攻撃が複数のユーザー・複数のアラートにまたがる場面では、「1件ずつ開く」より「ケースとして見る」ほうが、調査の単位が揃います。
AIによる優先づけ: 見るべきものを先に出す
もう1つは、「何が重要かを探す」作業の自動化です。
- 怪しいIPやドメインを、脅威インテリジェンスに自動で照会する
- 大量のアラートから、優先して見るべきものをランク付けする
- 結果を、平易な言葉の要約と対応案にして添える
KQLで影響範囲を出すとき、アナリストは「どのクエリを、どの順で回すか」を自分で決めています。トリアージ層は、その手前で「いま見るべきケース」を挙げる役割です。判断と対応は人が行う前提で、見る順番と読む手間を減らす位置づけです。
クエリでキャンペーン全体の広がりを押さえ、そこからたまるアラートの整理と優先づけはトリアージ層に任せる、という分担を想定しています。
まとめ
| 項目 | 1通起点の手作業 | キャンペーン単位のハンティング |
|---|---|---|
| 調査の単位 | メール1通 | キャンペーン(複数の受信者・メール) |
| 影響範囲の把握 | 担当者の検索条件に依存 |
NetworkMessageIdで結合して一括で出せる |
| 優先度の判断 | 経験則でバラつく | 配信・クリック・サインインの有無で基準化できる |
| クリック後の確認 | 別画面でサインインログを確認 | ASIMの認証ログと結合して自動で突き合わせ |
| 検知後の整理 | アラートの見比べ、過去案件の確認、IP/URLの調査を手で実施 | トリアージ層(SIEM+など)がケース管理と、AIによる優先度付け・照会・要約を担う |
| 注意点 | 件数が増えると回らない | プレビュー機能への依存、クリックの結合漏れ |
フィッシング調査は、1通を深く見るより、キャンペーン全体の広がりを先に押さえるほうが、判断が速くなります。Defender XDRとSentinelを両方使っている環境なら、すでに必要なデータは揃っているはずです。まずは、クエリ1で直近のキャンペーンを一覧し、自分のテナントでCampaignInfoが使えるかを確かめるところから始めてみてください。
※クエリは、Defender XDRのAdvanced HuntingのTimestampを前提に書いています。Log Analytics側で実行する場合はTimeGeneratedに読み替え、テーブルの有無・列名・結合結果を本番投入前に確認してください。
参考:
- Campaign-Centric Hunting with Microsoft Defender XDR and Microsoft Sentinel(Microsoft Tech Community)
https://techcommunity.microsoft.com/discussions/microsoftthreatprotection/campaign-centric-hunting-with-microsoft-defender-xdr-and-microsoft-sentinel/4527522 - SIEM+(AIトリアージ層)
https://siem.plus
質問やツッコミがあれば、コメントで教えてください。