# 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.
