Initial Access
T1566 — Phishing
Adversaries may send phishing messages — spearphishing links, attachments, or service-based lures — to gain initial access via credential harvesting, malicious attachments, or social engineering.
Treat a reported or detected phishing message as a potential initial access event, not just a spam nuisance. Confirm whether the message was opened, links clicked, attachments executed, or credentials entered, and scope to every recipient of the same campaign.
Platforms
Office 365, SaaS, Windows, Linux, macOS
Priority / status
high / complete
Evidence to collect
- Sender address and display name
- Message headers (SPF/DKIM/DMARC results)
- Recipient list
- URLs and attachment hashes
- Click/detonation verdicts
- Any subsequent sign-in or endpoint activity
D3-SRA · detect
Sender Reputation Analysis
Analyze sender reputation, SPF/DKIM/DMARC results, and message content for known phishing indicators.
Tooling: Defender for Office 365, Sentinel
D3-UA · detect
URL Analysis
Detonate and analyze embedded URLs for credential-harvesting or malware delivery.
Tooling: Defender for Office 365 Safe Links
D3-ER · evict
Email Removal
Purge the message from every recipient mailbox once confirmed malicious.
Tooling: Defender for Office 365
D3-EF · isolate
Email Filtering
Block the sender domain/URL tenant-wide once confirmed malicious.
Tooling: Defender for Office 365
| ATT&CK Technique | D3FEND Technique | Practical SOC Action | Tooling |
|---|---|---|---|
| T1566 Phishing | Sender Reputation Analysis | Analyze sender reputation, SPF/DKIM/DMARC results, and message content for known phishing indicators. | Defender for Office 365, Sentinel |
| T1566 Phishing | URL Analysis | Detonate and analyze embedded URLs for credential-harvesting or malware delivery. | Defender for Office 365 Safe Links |
| T1566 Phishing | Email Removal | Purge the message from every recipient mailbox once confirmed malicious. | Defender for Office 365 |
| T1566 Phishing | Email Filtering | Block the sender domain/URL tenant-wide once confirmed malicious. | Defender for Office 365 |
T1566 Phishing → D3FEND → SOC action
Investigation steps — Microsoft
- MDE portal → Device inventory → search device name → Isolate device
- Record: device name, user account, parent Office app, child process and full command line
- Is the command line encoded? (contains -EncodedCommand, FromBase64String) → decode immediately (see Step 2)
- Is the user a privileged account (IT admin, finance, C-suite)? → escalate to SOC Manager immediately
- Notify on-call Senior Analyst and open P1 incident ticket
- Download URLs → submit to VirusTotal immediately
- File paths → check DeviceFileEvents for dropped files
- Persistence commands → check registry and scheduled tasks
Investigation steps — generic
- Confirm the message was actually delivered and opened, and by whom.
- Determine whether a link was clicked or an attachment executed.
- Check for credential entry on any resulting landing page.
- Identify whether any endpoint activity followed (process execution, network connections).
- Real detection reference: "Phishing Click Followed by Account Change" -- Detects a user clicking a suspicious/phishing URL followed shortly by account changes often seen after credential capture, such as MFA method changes, password reset activity, or risky consent/registration events.
Response actions
| Action | Risk | Automation safe | Approval required |
|---|---|---|---|
| Isolate device — MDE portal | Medium | No | Yes |
| Notify customer primary security contact by phone | Low | Yes | No |
| Block sender domain — MDO / Exchange Online | Medium | No | Yes |
| Block attachment SHA256 — MDE custom indicators | Medium | No | Yes |
| Revoke user sessions — Entra ID portal | Medium | No | Yes |
| Open P1 incident ticket — tag PHISHING-MACRO-P1 | Low | Yes | No |
| Search all campaign recipients and check their devices | Low | Yes | No |
| Block all identified C2 IPs/domains — MDE indicators + Senti | Medium | No | Yes |
KQL
GEN-IA-005 — Phishing Click Followed by Account Change
let lookback = 1h;
let correlationWindow = 45m;
let suspiciousAccountOperations = dynamic([
"User registered security info",
"User changed default security info",
"User deleted security info",
"Admin registered security info",
"Reset password (self-service)",
"Reset user password",
"Change user password",
"Consent to application",
"Add app role assignment to service principal"
]);
let urlClickEventsSource =
union isfuzzy=true UrlClickEvents,
(datatable(TimeGenerated:datetime, Url:string, AccountUpn:string, ThreatTypes:string, IsClickedThrough:string, ActionType:string, NetworkMessageId:string, IPAddress:string)[]);
let auditLogsSource =
union isfuzzy=true AuditLogs,
(datatable(TimeGenerated:datetime, OperationName:string, InitiatedBy:dynamic, TargetResources:dynamic, Result:string, AdditionalDetails:dynamic)[]);
let suspiciousClicks =
urlClickEventsSource
| where TimeGenerated >= ago(lookback)
| extend AccountUpn = tolower(tostring(column_ifexists("AccountUpn", ""))),
Url = tostring(column_ifexists("Url", "")),
ThreatTypes = tostring(column_ifexists("ThreatTypes", "")),
IsClickedThrough = tolower(tostring(column_ifexists("IsClickedThrough", "false"))),
ActionType = tostring(column_ifexists("ActionType", "")),
ClickIP = tostring(column_ifexists("IPAddress", ""))
| where isnotempty(AccountUpn) and isnotempty(Url)
| where ThreatTypes has "Phish" or IsClickedThrough == "true" or ActionType in~ ("ClickAllowed", "ClickThrough")
| extend ParsedUrl = parse_url(Url),
UrlHost = tolower(tostring(ParsedUrl.Host))
| project ClickTime = TimeGenerated, AccountUpn, Url, UrlHost, ThreatTypes, ActionType, IsClickedThrough, ClickIP, NetworkMessageId;
let accountChanges =
auditLogsSource
| where TimeGenerated >= ago(lookback)
| where OperationName has_any (suspiciousAccountOperations)
| extend Actor = tolower(tostring(InitiatedBy.user.userPrincipalName)),
TargetUser = tolower(tostring(TargetResources[0].userPrincipalName)),
TargetDisplayName = tostring(TargetResources[0].displayName),
ChangeDetails = tostring(AdditionalDetails)
| extend CorrelationUser = coalesce(Actor, TargetUser)
| where isnotempty(CorrelationUser)
| project ChangeTime = TimeGenerated, CorrelationUser, Actor, TargetUser, TargetDisplayName, OperationName, Result, ChangeDetails;
suspiciousClicks
| join kind=inner accountChanges on $left.AccountUpn == $right.CorrelationUser
| where ChangeTime between (ClickTime .. ClickTime + correlationWindow)
| project
TimeGenerated = ChangeTime,
AccountUpn,
ClickTime,
ChangeTime,
Url,
UrlHost,
ThreatTypes,
ActionType,
ClickIP,
OperationName,
Actor,
TargetUser,
TargetDisplayName,
Result,
ChangeDetails,
NetworkMessageId
| extend AlertTitle = strcat("Phishing click followed by account change: ", AccountUpn),
timestamp = TimeGenerated,
AccountCustomEntity = AccountUpn,
IPCustomEntity = ClickIP,
URLCustomEntity = Url
| order by TimeGenerated desc GEN-RD-001 — Malicious OAuth Application Registered or Consented
let HighRiskPerms = dynamic([
"Mail.Read", "Mail.ReadWrite", "Mail.Send", "MailboxSettings.ReadWrite",
"Files.Read.All", "Files.ReadWrite.All", "Sites.Read.All", "Sites.ReadWrite.All",
"Calendars.ReadWrite", "Contacts.Read", "People.Read.All",
"User.ReadWrite.All", "Group.ReadWrite.All", "Directory.ReadWrite.All",
"Application.ReadWrite.All", "AppRoleAssignment.ReadWrite.All",
"RoleManagement.ReadWrite.Directory", "offline_access", "full_access_as_app"
]);
let auditLogsSource =
union isfuzzy=true AuditLogs,
(datatable(TimeGenerated:datetime, OperationName:string, InitiatedBy:dynamic, TargetResources:dynamic, AdditionalDetails:dynamic, Result:string)[]);
let NewAppRegistrations =
auditLogsSource
| where TimeGenerated >= ago(1h)
| where OperationName in~ ("Add application", "Add service principal", "Add OAuth2PermissionGrant")
| extend Actor = tostring(InitiatedBy.user.userPrincipalName),
AppName = tostring(TargetResources[0].displayName),
Permissions = strcat(tostring(TargetResources[0].modifiedProperties), " ", tostring(AdditionalDetails))
| where Permissions has_any (HighRiskPerms)
| project TimeGenerated, Actor, AppName, OperationName, Detail = Permissions, Result, DetectionPath = "HighRiskAppRegistration";
let ConsentGrants =
auditLogsSource
| where TimeGenerated >= ago(1h)
| where OperationName =~ "Consent to application"
| extend Actor = tostring(InitiatedBy.user.userPrincipalName),
AppName = tostring(TargetResources[0].displayName),
Scopes = strcat(tostring(TargetResources[0].modifiedProperties), " ", tostring(AdditionalDetails))
| where Scopes has_any (HighRiskPerms)
| project TimeGenerated, Actor, AppName, OperationName, Detail = Scopes, Result, DetectionPath = "HighRiskConsentGrant";
NewAppRegistrations
| union ConsentGrants
| extend timestamp = TimeGenerated,
AccountCustomEntity = Actor
| order by TimeGenerated desc GEN-IA-012 — Spearphishing Link With Endpoint Follow-On
let lookback = 1d;
let endpointWindow = 45m;
let tiLookback = 14d;
let suspiciousSchemes = dynamic(["http", "https"]);
let urlClickEventsSource =
union isfuzzy=true UrlClickEvents,
(datatable(TimeGenerated:datetime, Url:string, AccountUpn:string, NetworkMessageId:string, ThreatTypes:string, IsClickedThrough:string, ActionType:string)[]);
let emailEventsSource =
union isfuzzy=true EmailEvents,
(datatable(TimeGenerated:datetime, EmailDirection:string, NetworkMessageId:string, RecipientEmailAddress:string, SenderFromAddress:string, SenderFromDomain:string, Subject:string, LatestDeliveryAction:string)[]);
let deviceNetworkEventsSource =
union isfuzzy=true DeviceNetworkEvents,
(datatable(TimeGenerated:datetime, DeviceId:string, DeviceName:string, RemoteUrl:string, RemoteIP:string, InitiatingProcessAccountUpn:string, InitiatingProcessAccountName:string, InitiatingProcessFileName:string, InitiatingProcessCommandLine:string)[]);
let tiUrlDomainIndicators =
union isfuzzy=true ThreatIntelligenceIndicator, ThreatIntelIndicators
| where TimeGenerated >= ago(tiLookback)
| where tobool(column_ifexists("Active", true))
| where isempty(todatetime(column_ifexists("ExpirationDateTime", datetime(null)))) or todatetime(column_ifexists("ExpirationDateTime", now() + 365d)) > now()
| extend IndicatorUrl = tolower(tostring(column_ifexists("Url", ""))),
IndicatorDomain = tolower(tostring(column_ifexists("DomainName", ""))),
ThreatType = tostring(column_ifexists("ThreatType", "")),
Provider = tostring(coalesce(column_ifexists("IndicatorProvider", ""), column_ifexists("SourceSystem", "SentinelTI"))),
ConfidenceScore = toint(coalesce(column_ifexists("ConfidenceScore", 0), 0))
| where isnotempty(IndicatorUrl) or isnotempty(IndicatorDomain)
| summarize arg_max(TimeGenerated, *) by IndicatorUrl, IndicatorDomain
| project IndicatorUrl, IndicatorDomain, ThreatType, Provider, ConfidenceScore;
let clickedPhishLinks =
urlClickEventsSource
| where TimeGenerated >= ago(lookback)
| extend Url = tostring(column_ifexists("Url", ""))
| where isnotempty(Url)
| extend ParsedUrl = parse_url(Url)
| extend UrlHost = tolower(tostring(ParsedUrl.Host)),
UrlScheme = tolower(tostring(ParsedUrl.Scheme)),
AccountUpn = tolower(tostring(column_ifexists("AccountUpn", ""))),
NetworkMessageId = tostring(column_ifexists("NetworkMessageId", "")),
ThreatTypes = tostring(column_ifexists("ThreatTypes", "")),
IsClickedThrough = tolower(tostring(column_ifexists("IsClickedThrough", "false"))),
ActionType = tostring(column_ifexists("ActionType", ""))
| where UrlScheme in (suspiciousSchemes)
| where isnotempty(UrlHost)
| where ThreatTypes has "Phish" or IsClickedThrough == "true" or ActionType in~ ("ClickAllowed", "ClickThrough")
| project ClickTime = TimeGenerated, AccountUpn, NetworkMessageId, ThreatTypes, Url, UrlHost, IsClickedThrough, ActionType;
let emailContext =
emailEventsSource
| where TimeGenerated >= ago(lookback)
| where tostring(column_ifexists("EmailDirection", "")) =~ "Inbound"
| project NetworkMessageId = tostring(column_ifexists("NetworkMessageId", "")),
RecipientEmailAddress = tolower(tostring(column_ifexists("RecipientEmailAddress", ""))),
SenderFromAddress = tostring(column_ifexists("SenderFromAddress", "")),
SenderFromDomain = tostring(column_ifexists("SenderFromDomain", "")),
Subject = tostring(column_ifexists("Subject", "")),
DeliveryAction = tostring(column_ifexists("LatestDeliveryAction", ""));
let endpointFollowOn =
deviceNetworkEventsSource
| where TimeGenerated >= ago(lookback)
| extend DstHost = tolower(tostring(column_ifexists("RemoteUrl", ""))),
InitiatingProcessAccountUpn = tolower(tostring(column_ifexists("InitiatingProcessAccountUpn", "")))
| where isnotempty(DstHost)
| project NetworkTime = TimeGenerated, DeviceId, DeviceName, DstHost,
RemoteIP = tostring(column_ifexists("RemoteIP", "")),
InitiatingProcessAccountUpn,
InitiatingProcessAccountName = tostring(column_ifexists("InitiatingProcessAccountName", "")),
InitiatingProcessFileName = tostring(column_ifexists("InitiatingProcessFileName", "")),
InitiatingProcessCommandLine = tostring(column_ifexists("InitiatingProcessCommandLine", ""));
clickedPhishLinks
| join kind=leftouter emailContext on NetworkMessageId
| where isempty(DeliveryAction) or DeliveryAction !in~ ("Blocked", "Dropped", "Quarantined")
| join kind=leftouter endpointFollowOn on $left.UrlHost == $right.DstHost
| where isnull(NetworkTime)
or NetworkTime between (ClickTime .. ClickTime + endpointWindow)
or (isnotempty(AccountUpn) and isnotempty(InitiatingProcessAccountUpn) and AccountUpn == InitiatingProcessAccountUpn and NetworkTime between (ClickTime .. ClickTime + endpointWindow))
| join kind=leftouter tiUrlDomainIndicators on $left.UrlHost == $right.IndicatorDomain
| extend UrlMatchedInTI = isnotempty(IndicatorDomain),
RiskScore = 0
+ iif(ThreatTypes has "Phish", 2, 0)
+ iif(IsClickedThrough == "true" or ActionType in~ ("ClickAllowed", "ClickThrough"), 2, 0)
+ iif(isnotempty(NetworkTime), 2, 0)
+ iif(UrlMatchedInTI, 3, 0)
+ iif(Provider has_any ("phishtank", "openphish"), 1, 0)
| where RiskScore >= 5
| project TimeGenerated = coalesce(NetworkTime, ClickTime), ClickTime, NetworkTime,
AccountUpn, RecipientEmailAddress, SenderFromAddress, SenderFromDomain, Subject,
NetworkMessageId, ThreatTypes, Url, UrlHost, IsClickedThrough, ActionType,
DeviceName, DeviceId, RemoteIP, InitiatingProcessFileName, InitiatingProcessCommandLine,
InitiatingProcessAccountName, UrlMatchedInTI, TIProvider = Provider,
TIThreatType = ThreatType, TIConfidenceScore = ConfidenceScore, RiskScore
| extend AlertTitle = strcat("Spearphishing link compromise chain: ", coalesce(AccountUpn, RecipientEmailAddress), " -> ", UrlHost),
timestamp = TimeGenerated,
AccountCustomEntity = coalesce(AccountUpn, RecipientEmailAddress, InitiatingProcessAccountName),
HostCustomEntity = DeviceName,
IPCustomEntity = RemoteIP,
URLCustomEntity = Url Escalation criteria
- Executive, finance, or privileged account among recipients.
- Credentials were entered on a harvesting page.
- Attachment was executed and produced endpoint activity.
- Campaign reached a large number of recipients.
- OAuth consent grant followed the message.
False positive considerations
- Signed corporate macro launching a helper script — InitiatingProcessSHA256 matches a known and approved template hash
- RMM agent launched via Office integration — Child process is a signed binary from an approved vendor folder
- Legitimate PDF conversion tool (Office plugin) — Vendor-signed binary, consistent across all users, same path
# T1566 - Phishing
## SOC Recommendation
Treat a reported or detected phishing message as a potential initial access event, not just a spam nuisance. Confirm whether the message was opened, links clicked, attachments executed, or credentials entered, and scope to every recipient of the same campaign.
## D3FEND Mappings
| D3FEND Technique | Relationship | Practical SOC Action |
|---|---|---|
| Sender Reputation Analysis | Detect | Analyze sender reputation, SPF/DKIM/DMARC results, and message content for known phishing indicators. |
| URL Analysis | Detect | Detonate and analyze embedded URLs for credential-harvesting or malware delivery. |
| Email Removal | Evict | Purge the message from every recipient mailbox once confirmed malicious. |
| Email Filtering | Isolate | Block the sender domain/URL tenant-wide once confirmed malicious. |
## Investigation Steps
1. MDE portal → Device inventory → search device name → Isolate device
2. Record: device name, user account, parent Office app, child process and full command line
3. Is the command line encoded? (contains -EncodedCommand, FromBase64String) → decode immediately (see Step 2)
4. Is the user a privileged account (IT admin, finance, C-suite)? → escalate to SOC Manager immediately
5. Notify on-call Senior Analyst and open P1 incident ticket
6. Download URLs → submit to VirusTotal immediately
7. File paths → check DeviceFileEvents for dropped files
8. Persistence commands → check registry and scheduled tasks
## Evidence to Collect
- Sender address and display name
- Message headers (SPF/DKIM/DMARC results)
- Recipient list
- URLs and attachment hashes
- Click/detonation verdicts
- Any subsequent sign-in or endpoint activity
## Response Actions
| Action | Risk | Automation Safe | Approval Required |
|---|---|---|---|
| Isolate device — MDE portal | Medium | No | Yes |
| Notify customer primary security contact by phone | Low | Yes | No |
| Block sender domain — MDO / Exchange Online | Medium | No | Yes |
| Block attachment SHA256 — MDE custom indicators | Medium | No | Yes |
| Revoke user sessions — Entra ID portal | Medium | No | Yes |
| Open P1 incident ticket — tag PHISHING-MACRO-P1 | Low | Yes | No |
| Search all campaign recipients and check their devices | Low | Yes | No |
| Block all identified C2 IPs/domains — MDE indicators + Senti | Medium | No | Yes |
## KQL
```kql
let lookback = 1h;
let correlationWindow = 45m;
let suspiciousAccountOperations = dynamic([
"User registered security info",
"User changed default security info",
"User deleted security info",
"Admin registered security info",
"Reset password (self-service)",
"Reset user password",
"Change user password",
"Consent to application",
"Add app role assignment to service principal"
]);
let urlClickEventsSource =
union isfuzzy=true UrlClickEvents,
(datatable(TimeGenerated:datetime, Url:string, AccountUpn:string, ThreatTypes:string, IsClickedThrough:string, ActionType:string, NetworkMessageId:string, IPAddress:string)[]);
let auditLogsSource =
union isfuzzy=true AuditLogs,
(datatable(TimeGenerated:datetime, OperationName:string, InitiatedBy:dynamic, TargetResources:dynamic, Result:string, AdditionalDetails:dynamic)[]);
let suspiciousClicks =
urlClickEventsSource
| where TimeGenerated >= ago(lookback)
| extend AccountUpn = tolower(tostring(column_ifexists("AccountUpn", ""))),
Url = tostring(column_ifexists("Url", "")),
ThreatTypes = tostring(column_ifexists("ThreatTypes", "")),
IsClickedThrough = tolower(tostring(column_ifexists("IsClickedThrough", "false"))),
ActionType = tostring(column_ifexists("ActionType", "")),
ClickIP = tostring(column_ifexists("IPAddress", ""))
| where isnotempty(AccountUpn) and isnotempty(Url)
| where ThreatTypes has "Phish" or IsClickedThrough == "true" or ActionType in~ ("ClickAllowed", "ClickThrough")
| extend ParsedUrl = parse_url(Url),
UrlHost = tolower(tostring(ParsedUrl.Host))
| project ClickTime = TimeGenerated, AccountUpn, Url, UrlHost, ThreatTypes, ActionType, IsClickedThrough, ClickIP, NetworkMessageId;
let accountChanges =
auditLogsSource
| where TimeGenerated >= ago(lookback)
| where OperationName has_any (suspiciousAccountOperations)
| extend Actor = tolower(tostring(InitiatedBy.user.userPrincipalName)),
TargetUser = tolower(tostring(TargetResources[0].userPrincipalName)),
TargetDisplayName = tostring(TargetResources[0].displayName),
ChangeDetails = tostring(AdditionalDetails)
| extend CorrelationUser = coalesce(Actor, TargetUser)
| where isnotempty(CorrelationUser)
| project ChangeTime = TimeGenerated, CorrelationUser, Actor, TargetUser, TargetDisplayName, OperationName, Result, ChangeDetails;
suspiciousClicks
| join kind=inner accountChanges on $left.AccountUpn == $right.CorrelationUser
| where ChangeTime between (ClickTime .. ClickTime + correlationWindow)
| project
TimeGenerated = ChangeTime,
AccountUpn,
ClickTime,
ChangeTime,
Url,
UrlHost,
ThreatTypes,
ActionType,
ClickIP,
OperationName,
Actor,
TargetUser,
TargetDisplayName,
Result,
ChangeDetails,
NetworkMessageId
| extend AlertTitle = strcat("Phishing click followed by account change: ", AccountUpn),
timestamp = TimeGenerated,
AccountCustomEntity = AccountUpn,
IPCustomEntity = ClickIP,
URLCustomEntity = Url
| order by TimeGenerated desc
```
```kql
let HighRiskPerms = dynamic([
"Mail.Read", "Mail.ReadWrite", "Mail.Send", "MailboxSettings.ReadWrite",
"Files.Read.All", "Files.ReadWrite.All", "Sites.Read.All", "Sites.ReadWrite.All",
"Calendars.ReadWrite", "Contacts.Read", "People.Read.All",
"User.ReadWrite.All", "Group.ReadWrite.All", "Directory.ReadWrite.All",
"Application.ReadWrite.All", "AppRoleAssignment.ReadWrite.All",
"RoleManagement.ReadWrite.Directory", "offline_access", "full_access_as_app"
]);
let auditLogsSource =
union isfuzzy=true AuditLogs,
(datatable(TimeGenerated:datetime, OperationName:string, InitiatedBy:dynamic, TargetResources:dynamic, AdditionalDetails:dynamic, Result:string)[]);
let NewAppRegistrations =
auditLogsSource
| where TimeGenerated >= ago(1h)
| where OperationName in~ ("Add application", "Add service principal", "Add OAuth2PermissionGrant")
| extend Actor = tostring(InitiatedBy.user.userPrincipalName),
AppName = tostring(TargetResources[0].displayName),
Permissions = strcat(tostring(TargetResources[0].modifiedProperties), " ", tostring(AdditionalDetails))
| where Permissions has_any (HighRiskPerms)
| project TimeGenerated, Actor, AppName, OperationName, Detail = Permissions, Result, DetectionPath = "HighRiskAppRegistration";
let ConsentGrants =
auditLogsSource
| where TimeGenerated >= ago(1h)
| where OperationName =~ "Consent to application"
| extend Actor = tostring(InitiatedBy.user.userPrincipalName),
AppName = tostring(TargetResources[0].displayName),
Scopes = strcat(tostring(TargetResources[0].modifiedProperties), " ", tostring(AdditionalDetails))
| where Scopes has_any (HighRiskPerms)
| project TimeGenerated, Actor, AppName, OperationName, Detail = Scopes, Result, DetectionPath = "HighRiskConsentGrant";
NewAppRegistrations
| union ConsentGrants
| extend timestamp = TimeGenerated,
AccountCustomEntity = Actor
| order by TimeGenerated desc
```
```kql
let lookback = 1d;
let endpointWindow = 45m;
let tiLookback = 14d;
let suspiciousSchemes = dynamic(["http", "https"]);
let urlClickEventsSource =
union isfuzzy=true UrlClickEvents,
(datatable(TimeGenerated:datetime, Url:string, AccountUpn:string, NetworkMessageId:string, ThreatTypes:string, IsClickedThrough:string, ActionType:string)[]);
let emailEventsSource =
union isfuzzy=true EmailEvents,
(datatable(TimeGenerated:datetime, EmailDirection:string, NetworkMessageId:string, RecipientEmailAddress:string, SenderFromAddress:string, SenderFromDomain:string, Subject:string, LatestDeliveryAction:string)[]);
let deviceNetworkEventsSource =
union isfuzzy=true DeviceNetworkEvents,
(datatable(TimeGenerated:datetime, DeviceId:string, DeviceName:string, RemoteUrl:string, RemoteIP:string, InitiatingProcessAccountUpn:string, InitiatingProcessAccountName:string, InitiatingProcessFileName:string, InitiatingProcessCommandLine:string)[]);
let tiUrlDomainIndicators =
union isfuzzy=true ThreatIntelligenceIndicator, ThreatIntelIndicators
| where TimeGenerated >= ago(tiLookback)
| where tobool(column_ifexists("Active", true))
| where isempty(todatetime(column_ifexists("ExpirationDateTime", datetime(null)))) or todatetime(column_ifexists("ExpirationDateTime", now() + 365d)) > now()
| extend IndicatorUrl = tolower(tostring(column_ifexists("Url", ""))),
IndicatorDomain = tolower(tostring(column_ifexists("DomainName", ""))),
ThreatType = tostring(column_ifexists("ThreatType", "")),
Provider = tostring(coalesce(column_ifexists("IndicatorProvider", ""), column_ifexists("SourceSystem", "SentinelTI"))),
ConfidenceScore = toint(coalesce(column_ifexists("ConfidenceScore", 0), 0))
| where isnotempty(IndicatorUrl) or isnotempty(IndicatorDomain)
| summarize arg_max(TimeGenerated, *) by IndicatorUrl, IndicatorDomain
| project IndicatorUrl, IndicatorDomain, ThreatType, Provider, ConfidenceScore;
let clickedPhishLinks =
urlClickEventsSource
| where TimeGenerated >= ago(lookback)
| extend Url = tostring(column_ifexists("Url", ""))
| where isnotempty(Url)
| extend ParsedUrl = parse_url(Url)
| extend UrlHost = tolower(tostring(ParsedUrl.Host)),
UrlScheme = tolower(tostring(ParsedUrl.Scheme)),
AccountUpn = tolower(tostring(column_ifexists("AccountUpn", ""))),
NetworkMessageId = tostring(column_ifexists("NetworkMessageId", "")),
ThreatTypes = tostring(column_ifexists("ThreatTypes", "")),
IsClickedThrough = tolower(tostring(column_ifexists("IsClickedThrough", "false"))),
ActionType = tostring(column_ifexists("ActionType", ""))
| where UrlScheme in (suspiciousSchemes)
| where isnotempty(UrlHost)
| where ThreatTypes has "Phish" or IsClickedThrough == "true" or ActionType in~ ("ClickAllowed", "ClickThrough")
| project ClickTime = TimeGenerated, AccountUpn, NetworkMessageId, ThreatTypes, Url, UrlHost, IsClickedThrough, ActionType;
let emailContext =
emailEventsSource
| where TimeGenerated >= ago(lookback)
| where tostring(column_ifexists("EmailDirection", "")) =~ "Inbound"
| project NetworkMessageId = tostring(column_ifexists("NetworkMessageId", "")),
RecipientEmailAddress = tolower(tostring(column_ifexists("RecipientEmailAddress", ""))),
SenderFromAddress = tostring(column_ifexists("SenderFromAddress", "")),
SenderFromDomain = tostring(column_ifexists("SenderFromDomain", "")),
Subject = tostring(column_ifexists("Subject", "")),
DeliveryAction = tostring(column_ifexists("LatestDeliveryAction", ""));
let endpointFollowOn =
deviceNetworkEventsSource
| where TimeGenerated >= ago(lookback)
| extend DstHost = tolower(tostring(column_ifexists("RemoteUrl", ""))),
InitiatingProcessAccountUpn = tolower(tostring(column_ifexists("InitiatingProcessAccountUpn", "")))
| where isnotempty(DstHost)
| project NetworkTime = TimeGenerated, DeviceId, DeviceName, DstHost,
RemoteIP = tostring(column_ifexists("RemoteIP", "")),
InitiatingProcessAccountUpn,
InitiatingProcessAccountName = tostring(column_ifexists("InitiatingProcessAccountName", "")),
InitiatingProcessFileName = tostring(column_ifexists("InitiatingProcessFileName", "")),
InitiatingProcessCommandLine = tostring(column_ifexists("InitiatingProcessCommandLine", ""));
clickedPhishLinks
| join kind=leftouter emailContext on NetworkMessageId
| where isempty(DeliveryAction) or DeliveryAction !in~ ("Blocked", "Dropped", "Quarantined")
| join kind=leftouter endpointFollowOn on $left.UrlHost == $right.DstHost
| where isnull(NetworkTime)
or NetworkTime between (ClickTime .. ClickTime + endpointWindow)
or (isnotempty(AccountUpn) and isnotempty(InitiatingProcessAccountUpn) and AccountUpn == InitiatingProcessAccountUpn and NetworkTime between (ClickTime .. ClickTime + endpointWindow))
| join kind=leftouter tiUrlDomainIndicators on $left.UrlHost == $right.IndicatorDomain
| extend UrlMatchedInTI = isnotempty(IndicatorDomain),
RiskScore = 0
+ iif(ThreatTypes has "Phish", 2, 0)
+ iif(IsClickedThrough == "true" or ActionType in~ ("ClickAllowed", "ClickThrough"), 2, 0)
+ iif(isnotempty(NetworkTime), 2, 0)
+ iif(UrlMatchedInTI, 3, 0)
+ iif(Provider has_any ("phishtank", "openphish"), 1, 0)
| where RiskScore >= 5
| project TimeGenerated = coalesce(NetworkTime, ClickTime), ClickTime, NetworkTime,
AccountUpn, RecipientEmailAddress, SenderFromAddress, SenderFromDomain, Subject,
NetworkMessageId, ThreatTypes, Url, UrlHost, IsClickedThrough, ActionType,
DeviceName, DeviceId, RemoteIP, InitiatingProcessFileName, InitiatingProcessCommandLine,
InitiatingProcessAccountName, UrlMatchedInTI, TIProvider = Provider,
TIThreatType = ThreatType, TIConfidenceScore = ConfidenceScore, RiskScore
| extend AlertTitle = strcat("Spearphishing link compromise chain: ", coalesce(AccountUpn, RecipientEmailAddress), " -> ", UrlHost),
timestamp = TimeGenerated,
AccountCustomEntity = coalesce(AccountUpn, RecipientEmailAddress, InitiatingProcessAccountName),
HostCustomEntity = DeviceName,
IPCustomEntity = RemoteIP,
URLCustomEntity = Url
```
## Escalation Criteria
- Executive, finance, or privileged account among recipients.
- Credentials were entered on a harvesting page.
- Attachment was executed and produced endpoint activity.
- Campaign reached a large number of recipients.
- OAuth consent grant followed the message.
## False Positive Considerations
- Signed corporate macro launching a helper script — InitiatingProcessSHA256 matches a known and approved template hash
- RMM agent launched via Office integration — Child process is a signed binary from an approved vendor folder
- Legitimate PDF conversion tool (Office plugin) — Vendor-signed binary, consistent across all users, same path
Generated by SOC Response Atlas by Basyrix.
Want to push this directly to Confluence? Upgrade to Basyrix Pro.
- /api/techniques/T1566.json
- /api/recommendations/T1566.json
- /api/d3fend/T1566.json
- /api/mappings/T1566.json
- /api/confluence/T1566.md
Example curl:
curl https://atlas.basyrix.com/api/recommendations/T1566.json Response:
{
"technique_id": "T1566",
"name": "Phishing",
"priority": "high",
"status": "complete",
"version": "0.1.0",
"last_reviewed": "2026-07-23",
"generated_by": "SOC Response Atlas by Basyrix",
"tactics": [
"Initial Access"
],
"platforms": [
"Office 365",
"SaaS",
"Windows",
"Linux",
"macOS"
],
"summary": "Adversaries may send phishing messages — spearphishing links, attachments, or service-based lures — to gain initial access via credential harvesting, malicious attachments, or social engineering.\n",
"soc_recommendation": "Treat a reported or detected phishing message as a potential initial access event, not just a spam nuisance. Confirm whether the message was opened, links clicked, attachments executed, or credentials entered, and scope to every recipient of the same campaign.\n",
"d3fend_mappings": [
{
"id": "D3-SRA",
"name": "Sender Reputation Analysis",
"relationship": "detect",
"practical_action": "Analyze sender reputation, SPF/DKIM/DMARC results, and message content for known phishing indicators.\n",
"tooling": [
"Defender for Office 365",
"Sentinel"
]
},
{
"id": "D3-UA",
"name": "URL Analysis",
"relationship": "detect",
"practical_action": "Detonate and analyze embedded URLs for credential-harvesting or malware delivery.",
"tooling": [
"Defender for Office 365 Safe Links"
]
},
{
"id": "D3-ER",
"name": "Email Removal",
"relationship": "evict",
"practical_action": "Purge the message from every recipient mailbox once confirmed malicious.",
"tooling": [
"Defender for Office 365"
]
},
{
"id": "D3-EF",
"name": "Email Filtering",
"relationship": "isolate",
"practical_action": "Block the sender domain/URL tenant-wide once confirmed malicious.",
"tooling": [
"Defender for Office 365"
]
}
],
"investigation_steps": {
"microsoft": [
"MDE portal → Device inventory → search device name → Isolate device",
"Record: device name, user account, parent Office app, child process and full command line",
"Is the command line encoded? (contains -EncodedCommand, FromBase64String) → decode immediately (see Step 2)",
"Is the user a privileged account (IT admin, finance, C-suite)? → escalate to SOC Manager immediately",
"Notify on-call Senior Analyst and open P1 incident ticket",
"Download URLs → submit to VirusTotal immediately",
"File paths → check DeviceFileEvents for dropped files",
"Persistence commands → check registry and scheduled tasks"
],
"generic": [
"Confirm the message was actually delivered and opened, and by whom.",
"Determine whether a link was clicked or an attachment executed.",
"Check for credential entry on any resulting landing page.",
"Identify whether any endpoint activity followed (process execution, network connections).",
"Real detection reference: \"Phishing Click Followed by Account Change\" -- Detects a user clicking a suspicious/phishing URL followed shortly by account changes often seen after credential capture, such as MFA method changes, password reset activity, or risky consent/registration events."
]
},
"evidence_to_collect": [
"Sender address and display name",
"Message headers (SPF/DKIM/DMARC results)",
"Recipient list",
"URLs and attachment hashes",
"Click/detonation verdicts",
"Any subsequent sign-in or endpoint activity"
],
"response_actions": [
{
"name": "Isolate device — MDE portal",
"category": "Containment",
"risk": "Medium",
"automation_safe": false,
"approval_required": true,
"tool": "Defender for Endpoint"
},
{
"name": "Notify customer primary security contact by phone",
"category": "Containment",
"risk": "Low",
"automation_safe": true,
"approval_required": false,
"tool": "See investigation guide"
},
{
"name": "Block sender domain — MDO / Exchange Online",
"category": "Containment",
"risk": "Medium",
"automation_safe": false,
"approval_required": true,
"tool": "Defender for Office 365"
},
{
"name": "Block attachment SHA256 — MDE custom indicators",
"category": "Containment",
"risk": "Medium",
"automation_safe": false,
"approval_required": true,
"tool": "Defender for Endpoint"
},
{
"name": "Revoke user sessions — Entra ID portal",
"category": "Containment",
"risk": "Medium",
"automation_safe": false,
"approval_required": true,
"tool": "Entra ID"
},
{
"name": "Open P1 incident ticket — tag PHISHING-MACRO-P1",
"category": "Containment",
"risk": "Low",
"automation_safe": true,
"approval_required": false,
"tool": "See investigation guide"
},
{
"name": "Search all campaign recipients and check their devices",
"category": "Eradication",
"risk": "Low",
"automation_safe": true,
"approval_required": false,
"tool": "See investigation guide"
},
{
"name": "Block all identified C2 IPs/domains — MDE indicators + Senti",
"category": "Eradication",
"risk": "Medium",
"automation_safe": false,
"approval_required": true,
"tool": "Defender for Endpoint",
"notes": "Block all identified C2 IPs/domains — MDE indicators + Sentinel watchlist"
}
],
"queries": {
"kql": [
{
"name": "GEN-IA-005 — Phishing Click Followed by Account Change",
"description": "Detects a user clicking a suspicious/phishing URL followed shortly by account changes often seen after credential capture, such as MFA method changes, password reset activity, or risky consent/registration events. (Source: Bell Integration baseline detection library, mapped via sub-technique T1566.002.)",
"query": "let lookback = 1h;\nlet correlationWindow = 45m;\nlet suspiciousAccountOperations = dynamic([\n \"User registered security info\",\n \"User changed default security info\",\n \"User deleted security info\",\n \"Admin registered security info\",\n \"Reset password (self-service)\",\n \"Reset user password\",\n \"Change user password\",\n \"Consent to application\",\n \"Add app role assignment to service principal\"\n]);\nlet urlClickEventsSource =\n union isfuzzy=true UrlClickEvents,\n (datatable(TimeGenerated:datetime, Url:string, AccountUpn:string, ThreatTypes:string, IsClickedThrough:string, ActionType:string, NetworkMessageId:string, IPAddress:string)[]);\nlet auditLogsSource =\n union isfuzzy=true AuditLogs,\n (datatable(TimeGenerated:datetime, OperationName:string, InitiatedBy:dynamic, TargetResources:dynamic, Result:string, AdditionalDetails:dynamic)[]);\nlet suspiciousClicks =\n urlClickEventsSource\n | where TimeGenerated >= ago(lookback)\n | extend AccountUpn = tolower(tostring(column_ifexists(\"AccountUpn\", \"\"))),\n Url = tostring(column_ifexists(\"Url\", \"\")),\n ThreatTypes = tostring(column_ifexists(\"ThreatTypes\", \"\")),\n IsClickedThrough = tolower(tostring(column_ifexists(\"IsClickedThrough\", \"false\"))),\n ActionType = tostring(column_ifexists(\"ActionType\", \"\")),\n ClickIP = tostring(column_ifexists(\"IPAddress\", \"\"))\n | where isnotempty(AccountUpn) and isnotempty(Url)\n | where ThreatTypes has \"Phish\" or IsClickedThrough == \"true\" or ActionType in~ (\"ClickAllowed\", \"ClickThrough\")\n | extend ParsedUrl = parse_url(Url),\n UrlHost = tolower(tostring(ParsedUrl.Host))\n | project ClickTime = TimeGenerated, AccountUpn, Url, UrlHost, ThreatTypes, ActionType, IsClickedThrough, ClickIP, NetworkMessageId;\nlet accountChanges =\n auditLogsSource\n | where TimeGenerated >= ago(lookback)\n | where OperationName has_any (suspiciousAccountOperations)\n | extend Actor = tolower(tostring(InitiatedBy.user.userPrincipalName)),\n TargetUser = tolower(tostring(TargetResources[0].userPrincipalName)),\n TargetDisplayName = tostring(TargetResources[0].displayName),\n ChangeDetails = tostring(AdditionalDetails)\n | extend CorrelationUser = coalesce(Actor, TargetUser)\n | where isnotempty(CorrelationUser)\n | project ChangeTime = TimeGenerated, CorrelationUser, Actor, TargetUser, TargetDisplayName, OperationName, Result, ChangeDetails;\nsuspiciousClicks\n| join kind=inner accountChanges on $left.AccountUpn == $right.CorrelationUser\n| where ChangeTime between (ClickTime .. ClickTime + correlationWindow)\n| project\n TimeGenerated = ChangeTime,\n AccountUpn,\n ClickTime,\n ChangeTime,\n Url,\n UrlHost,\n ThreatTypes,\n ActionType,\n ClickIP,\n OperationName,\n Actor,\n TargetUser,\n TargetDisplayName,\n Result,\n ChangeDetails,\n NetworkMessageId\n| extend AlertTitle = strcat(\"Phishing click followed by account change: \", AccountUpn),\n timestamp = TimeGenerated,\n AccountCustomEntity = AccountUpn,\n IPCustomEntity = ClickIP,\n URLCustomEntity = Url\n| order by TimeGenerated desc"
},
{
"name": "GEN-RD-001 — Malicious OAuth Application Registered or Consented",
"description": "Flags new OAuth application registration or third-party consent with high-risk Graph/Exchange scopes. PS-001 now covers persistence via service principal credential additions. (Source: Bell Integration baseline detection library, mapped via sub-technique T1566.002.)",
"query": "let HighRiskPerms = dynamic([\n \"Mail.Read\", \"Mail.ReadWrite\", \"Mail.Send\", \"MailboxSettings.ReadWrite\",\n \"Files.Read.All\", \"Files.ReadWrite.All\", \"Sites.Read.All\", \"Sites.ReadWrite.All\",\n \"Calendars.ReadWrite\", \"Contacts.Read\", \"People.Read.All\",\n \"User.ReadWrite.All\", \"Group.ReadWrite.All\", \"Directory.ReadWrite.All\",\n \"Application.ReadWrite.All\", \"AppRoleAssignment.ReadWrite.All\",\n \"RoleManagement.ReadWrite.Directory\", \"offline_access\", \"full_access_as_app\"\n]);\nlet auditLogsSource =\n union isfuzzy=true AuditLogs,\n (datatable(TimeGenerated:datetime, OperationName:string, InitiatedBy:dynamic, TargetResources:dynamic, AdditionalDetails:dynamic, Result:string)[]);\nlet NewAppRegistrations =\n auditLogsSource\n | where TimeGenerated >= ago(1h)\n | where OperationName in~ (\"Add application\", \"Add service principal\", \"Add OAuth2PermissionGrant\")\n | extend Actor = tostring(InitiatedBy.user.userPrincipalName),\n AppName = tostring(TargetResources[0].displayName),\n Permissions = strcat(tostring(TargetResources[0].modifiedProperties), \" \", tostring(AdditionalDetails))\n | where Permissions has_any (HighRiskPerms)\n | project TimeGenerated, Actor, AppName, OperationName, Detail = Permissions, Result, DetectionPath = \"HighRiskAppRegistration\";\nlet ConsentGrants =\n auditLogsSource\n | where TimeGenerated >= ago(1h)\n | where OperationName =~ \"Consent to application\"\n | extend Actor = tostring(InitiatedBy.user.userPrincipalName),\n AppName = tostring(TargetResources[0].displayName),\n Scopes = strcat(tostring(TargetResources[0].modifiedProperties), \" \", tostring(AdditionalDetails))\n | where Scopes has_any (HighRiskPerms)\n | project TimeGenerated, Actor, AppName, OperationName, Detail = Scopes, Result, DetectionPath = \"HighRiskConsentGrant\";\nNewAppRegistrations\n| union ConsentGrants\n| extend timestamp = TimeGenerated,\n AccountCustomEntity = Actor\n| order by TimeGenerated desc"
},
{
"name": "GEN-IA-012 — Spearphishing Link With Endpoint Follow-On",
"description": "Higher-fidelity spearphishing link detector. Keeps the original URL-click and TI scoring, and adds endpoint network follow-on correlation. (Source: Bell Integration baseline detection library, mapped via sub-technique T1566.002.)",
"query": "let lookback = 1d;\nlet endpointWindow = 45m;\nlet tiLookback = 14d;\nlet suspiciousSchemes = dynamic([\"http\", \"https\"]);\nlet urlClickEventsSource =\n union isfuzzy=true UrlClickEvents,\n (datatable(TimeGenerated:datetime, Url:string, AccountUpn:string, NetworkMessageId:string, ThreatTypes:string, IsClickedThrough:string, ActionType:string)[]);\nlet emailEventsSource =\n union isfuzzy=true EmailEvents,\n (datatable(TimeGenerated:datetime, EmailDirection:string, NetworkMessageId:string, RecipientEmailAddress:string, SenderFromAddress:string, SenderFromDomain:string, Subject:string, LatestDeliveryAction:string)[]);\nlet deviceNetworkEventsSource =\n union isfuzzy=true DeviceNetworkEvents,\n (datatable(TimeGenerated:datetime, DeviceId:string, DeviceName:string, RemoteUrl:string, RemoteIP:string, InitiatingProcessAccountUpn:string, InitiatingProcessAccountName:string, InitiatingProcessFileName:string, InitiatingProcessCommandLine:string)[]);\nlet tiUrlDomainIndicators =\n union isfuzzy=true ThreatIntelligenceIndicator, ThreatIntelIndicators\n | where TimeGenerated >= ago(tiLookback)\n | where tobool(column_ifexists(\"Active\", true))\n | where isempty(todatetime(column_ifexists(\"ExpirationDateTime\", datetime(null)))) or todatetime(column_ifexists(\"ExpirationDateTime\", now() + 365d)) > now()\n | extend IndicatorUrl = tolower(tostring(column_ifexists(\"Url\", \"\"))),\n IndicatorDomain = tolower(tostring(column_ifexists(\"DomainName\", \"\"))),\n ThreatType = tostring(column_ifexists(\"ThreatType\", \"\")),\n Provider = tostring(coalesce(column_ifexists(\"IndicatorProvider\", \"\"), column_ifexists(\"SourceSystem\", \"SentinelTI\"))),\n ConfidenceScore = toint(coalesce(column_ifexists(\"ConfidenceScore\", 0), 0))\n | where isnotempty(IndicatorUrl) or isnotempty(IndicatorDomain)\n | summarize arg_max(TimeGenerated, *) by IndicatorUrl, IndicatorDomain\n | project IndicatorUrl, IndicatorDomain, ThreatType, Provider, ConfidenceScore;\nlet clickedPhishLinks =\n urlClickEventsSource\n | where TimeGenerated >= ago(lookback)\n | extend Url = tostring(column_ifexists(\"Url\", \"\"))\n | where isnotempty(Url)\n | extend ParsedUrl = parse_url(Url)\n | extend UrlHost = tolower(tostring(ParsedUrl.Host)),\n UrlScheme = tolower(tostring(ParsedUrl.Scheme)),\n AccountUpn = tolower(tostring(column_ifexists(\"AccountUpn\", \"\"))),\n NetworkMessageId = tostring(column_ifexists(\"NetworkMessageId\", \"\")),\n ThreatTypes = tostring(column_ifexists(\"ThreatTypes\", \"\")),\n IsClickedThrough = tolower(tostring(column_ifexists(\"IsClickedThrough\", \"false\"))),\n ActionType = tostring(column_ifexists(\"ActionType\", \"\"))\n | where UrlScheme in (suspiciousSchemes)\n | where isnotempty(UrlHost)\n | where ThreatTypes has \"Phish\" or IsClickedThrough == \"true\" or ActionType in~ (\"ClickAllowed\", \"ClickThrough\")\n | project ClickTime = TimeGenerated, AccountUpn, NetworkMessageId, ThreatTypes, Url, UrlHost, IsClickedThrough, ActionType;\nlet emailContext =\n emailEventsSource\n | where TimeGenerated >= ago(lookback)\n | where tostring(column_ifexists(\"EmailDirection\", \"\")) =~ \"Inbound\"\n | project NetworkMessageId = tostring(column_ifexists(\"NetworkMessageId\", \"\")),\n RecipientEmailAddress = tolower(tostring(column_ifexists(\"RecipientEmailAddress\", \"\"))),\n SenderFromAddress = tostring(column_ifexists(\"SenderFromAddress\", \"\")),\n SenderFromDomain = tostring(column_ifexists(\"SenderFromDomain\", \"\")),\n Subject = tostring(column_ifexists(\"Subject\", \"\")),\n DeliveryAction = tostring(column_ifexists(\"LatestDeliveryAction\", \"\"));\nlet endpointFollowOn =\n deviceNetworkEventsSource\n | where TimeGenerated >= ago(lookback)\n | extend DstHost = tolower(tostring(column_ifexists(\"RemoteUrl\", \"\"))),\n InitiatingProcessAccountUpn = tolower(tostring(column_ifexists(\"InitiatingProcessAccountUpn\", \"\")))\n | where isnotempty(DstHost)\n | project NetworkTime = TimeGenerated, DeviceId, DeviceName, DstHost,\n RemoteIP = tostring(column_ifexists(\"RemoteIP\", \"\")),\n InitiatingProcessAccountUpn,\n InitiatingProcessAccountName = tostring(column_ifexists(\"InitiatingProcessAccountName\", \"\")),\n InitiatingProcessFileName = tostring(column_ifexists(\"InitiatingProcessFileName\", \"\")),\n InitiatingProcessCommandLine = tostring(column_ifexists(\"InitiatingProcessCommandLine\", \"\"));\nclickedPhishLinks\n| join kind=leftouter emailContext on NetworkMessageId\n| where isempty(DeliveryAction) or DeliveryAction !in~ (\"Blocked\", \"Dropped\", \"Quarantined\")\n| join kind=leftouter endpointFollowOn on $left.UrlHost == $right.DstHost\n| where isnull(NetworkTime)\n or NetworkTime between (ClickTime .. ClickTime + endpointWindow)\n or (isnotempty(AccountUpn) and isnotempty(InitiatingProcessAccountUpn) and AccountUpn == InitiatingProcessAccountUpn and NetworkTime between (ClickTime .. ClickTime + endpointWindow))\n| join kind=leftouter tiUrlDomainIndicators on $left.UrlHost == $right.IndicatorDomain\n| extend UrlMatchedInTI = isnotempty(IndicatorDomain),\n RiskScore = 0\n + iif(ThreatTypes has \"Phish\", 2, 0)\n + iif(IsClickedThrough == \"true\" or ActionType in~ (\"ClickAllowed\", \"ClickThrough\"), 2, 0)\n + iif(isnotempty(NetworkTime), 2, 0)\n + iif(UrlMatchedInTI, 3, 0)\n + iif(Provider has_any (\"phishtank\", \"openphish\"), 1, 0)\n| where RiskScore >= 5\n| project TimeGenerated = coalesce(NetworkTime, ClickTime), ClickTime, NetworkTime,\n AccountUpn, RecipientEmailAddress, SenderFromAddress, SenderFromDomain, Subject,\n NetworkMessageId, ThreatTypes, Url, UrlHost, IsClickedThrough, ActionType,\n DeviceName, DeviceId, RemoteIP, InitiatingProcessFileName, InitiatingProcessCommandLine,\n InitiatingProcessAccountName, UrlMatchedInTI, TIProvider = Provider,\n TIThreatType = ThreatType, TIConfidenceScore = ConfidenceScore, RiskScore\n| extend AlertTitle = strcat(\"Spearphishing link compromise chain: \", coalesce(AccountUpn, RecipientEmailAddress), \" -> \", UrlHost),\n timestamp = TimeGenerated,\n AccountCustomEntity = coalesce(AccountUpn, RecipientEmailAddress, InitiatingProcessAccountName),\n HostCustomEntity = DeviceName,\n IPCustomEntity = RemoteIP,\n URLCustomEntity = Url"
}
],
"spl": [
{
"name": "Post-phishing endpoint activity",
"description": "Review endpoint activity for recipients shortly after delivery.",
"query": "index=endpoint user IN (\"<recipient1>\", \"<recipient2>\") earliest=<delivery_time>\n"
}
],
"esql": [
{
"name": "3db029b3-fbb7-4697-ad07-33cbfd5bd080 — Entra ID OAuth Device Code Flow with Concurrent Sign-ins",
"description": "(ESQL) Identifies Entra ID device code authentication flows where multiple user agents are observed within the same session. This pattern is indicative of device code phishing, where an attacker's polling client (e.g., Python script) and the victim's browser both appear in the same authentication session... (Source: Elastic's official detection-rules repository (Elastic License v2).)",
"query": "from logs-azure.signinlogs-* metadata _id, _version, _index\n\n| where event.category == \"authentication\" and data_stream.dataset == \"azure.signinlogs\" and\n azure.signinlogs.properties.original_transfer_method == \"deviceCodeFlow\"\n\n// Track events with deviceCode authentication protocol (browser auth) vs polling client\n| eval is_device_code_auth = case(azure.signinlogs.properties.authentication_protocol == \"deviceCode\", 1, 0)\n\n| stats Esql.count_logon = count(*),\n Esql.device_code_auth_count = sum(is_device_code_auth),\n Esql.timestamp_values = values(@timestamp),\n Esql.source_ip_count_distinct = count_distinct(source.ip),\n Esql.user_agent_count_distinct = count_distinct(user_agent.original),\n Esql.user_agent_values = values(user_agent.original),\n Esql.authentication_protocol_values = values(azure.signinlogs.properties.authentication_protocol),\n Esql.azure_signinlogs_properties_client_app_values = values(azure.signinlogs.properties.app_display_name),\n Esql.azure_signinlogs_properties_app_id_values = values(azure.signinlogs.properties.app_id),\n Esql.azure_signinlogs_properties_resource_display_name_values = values(azure.signinlogs.properties.resource_display_name),\n Esql.azure_signinlogs_properties_auth_requirement_values = values(azure.signinlogs.properties.authentication_requirement),\n Esql.azure_signinlogs_properties_tenant_id = values(azure.tenant_id),\n Esql.azure_signinlogs_properties_status_error_code_values = values(azure.signinlogs.properties.status.error_code),\n Esql.message_values = values(message),\n Esql.azure_signinlogs_properties_resource_id_values = values(azure.signinlogs.properties.resource_id),\n Esql.source_ip_values = values(source.ip)\n by azure.signinlogs.properties.session_id, azure.signinlogs.identity\n\n// Require: 2+ events, at least one deviceCode auth protocol event, and either 2+ IPs or 2+ user agents\n| where Esql.count_logon >= 2 and Esql.device_code_auth_count >= 1 and (Esql.source_ip_count_distinct >= 2 or Esql.user_agent_count_distinct >= 2)\n| keep\n Esql.*,\n azure.signinlogs.properties.session_id,\n azure.signinlogs.identity"
},
{
"name": "e3bd85e9-7aff-46eb-b60e-20dfc9020d98 — Entra ID Concurrent Sign-in with Suspicious Properties",
"description": "(ESQL) Identifies concurrent azure signin events for the same user and from multiple sources, and where one of the authentication event has some suspicious properties often associated to DeviceCode and OAuth phishing. Adversaries may steal Refresh Tokens (RTs) via phishing to bypass multi-factor authentication (MFA) and gain unauthorized access to Azure resources. (Source: Elastic's official detection-rules repository (Elastic License v2).)",
"query": "from logs-azure.signinlogs-* metadata _id, _version, _index\n\n// Scheduled to run every hour, reviewing events from past hour\n| where\n @timestamp > now() - 1 hours\n and data_stream.dataset == \"azure.signinlogs\"\n and source.ip is not null\n and azure.signinlogs.identity is not null\n and to_lower(event.outcome) == \"success\"\n\n// keep relevant raw fields\n| keep\n @timestamp,\n azure.signinlogs.identity,\n source.ip,\n azure.signinlogs.properties.authentication_requirement,\n azure.signinlogs.properties.app_id,\n azure.signinlogs.properties.resource_display_name,\n azure.signinlogs.properties.authentication_protocol,\n azure.signinlogs.properties.app_display_name\n\n// case classifications for identity usage\n| eval\n Esql.azure_signinlogs_properties_authentication_device_code_case = case(\n azure.signinlogs.properties.authentication_protocol == \"deviceCode\"\n and azure.signinlogs.properties.authentication_requirement != \"multiFactorAuthentication\",\n azure.signinlogs.identity,\n null),\n\n Esql.azure_signinlogs_auth_visual_studio_case = case(\n azure.signinlogs.properties.app_id == \"aebc6443-996d-45c2-90f0-388ff96faa56\"\n and azure.signinlogs.properties.resource_display_name == \"Microsoft Graph\",\n azure.signinlogs.identity,\n null),\n\n Esql.azure_signinlogs_auth_other_case = case(\n azure.signinlogs.properties.authentication_protocol != \"deviceCode\"\n and azure.signinlogs.properties.app_id != \"aebc6443-996d-45c2-90f0-388ff96faa56\",\n azure.signinlogs.identity,\n null)\n\n// Aggregate metrics by user identity\n| stats\n Esql.event_count = count(*),\n Esql.azure_signinlogs_properties_authentication_device_code_case_count_distinct = count_distinct(Esql.azure_signinlogs_properties_authentication_device_code_case),\n Esql.azure_signinlogs_properties_auth_visual_studio_count_distinct = count_distinct(Esql.azure_signinlogs_auth_visual_studio_case),\n Esql.azure_signinlogs_properties_auth_other_count_distinct = count_distinct(Esql.azure_signinlogs_auth_other_case),\n Esql.azure_signinlogs_properties_source_ip_count_distinct = count_distinct(source.ip),\n Esql.azure_signinlogs_properties_source_ip_values = values(source.ip),\n Esql.azure_signinlogs_properties_client_app_values = values(azure.signinlogs.properties.app_display_name),\n Esql.azure_signinlogs_properties_resource_display_name_values = values(azure.signinlogs.properties.resource_display_name),\n Esql.azure_signinlogs_properties_auth_requirement_values = values(azure.signinlogs.properties.authentication_requirement)\n by azure.signinlogs.identity\n\n// Detect multiple unique IPs for one user with signs of deviceCode or VSC OAuth usage\n| where\n Esql.azure_signinlogs_properties_source_ip_count_distinct >= 2\n and (\n Esql.azure_signinlogs_properties_authentication_device_code_case_count_distinct > 0\n or Esql.azure_signinlogs_properties_auth_visual_studio_count_distinct > 0\n )"
}
]
},
"automation": {
"safe": [
"Add recommendation as Sentinel incident comment.",
"Purge confirmed-malicious message from all mailboxes.",
"Block confirmed-malicious sender domain/URL.",
"Create ServiceNow SecOps task."
],
"approval_required": [
"Reset password for affected recipients.",
"Disable account for a recipient who entered credentials."
]
},
"escalation_criteria": [
"Executive, finance, or privileged account among recipients.",
"Credentials were entered on a harvesting page.",
"Attachment was executed and produced endpoint activity.",
"Campaign reached a large number of recipients.",
"OAuth consent grant followed the message."
],
"false_positive_considerations": [
"Signed corporate macro launching a helper script — InitiatingProcessSHA256 matches a known and approved template hash",
"RMM agent launched via Office integration — Child process is a signed binary from an approved vendor folder",
"Legitimate PDF conversion tool (Office plugin) — Vendor-signed binary, consistent across all users, same path"
],
"confluence": {
"title": "T1566 - Phishing Response Guidance",
"labels": [
"mitre",
"attack",
"d3fend",
"secops",
"basyrix"
],
"sections": [
"summary",
"d3fend_mappings",
"investigation_steps",
"response_actions",
"queries",
"automation",
"escalation_criteria",
"false_positive_considerations"
]
}
}