Contents

Detect and alert on unusual high phish or malware email volume

Microsoft does a great job in detecting and preventing malware and phishing emails with Microsoft Defender for Office 365. Since this feature works mostly in the background, even larger than usual malware campaigns are prevented without the Administrator noticing.

Campaign view

Using the campaign view you can check for those events and get a good overview which kind of malware is targeting your users.

/en/detect-alert-unusual-high-phish-malware-email-volume/images/campaigns-overview.png
Campaign overview

You can also drill down even further to identify which users were targeted as part of this campaign.

/en/detect-alert-unusual-high-phish-malware-email-volume/images/DetailedCampaignView.png
Detailed campaign view

This data allows you to concentrate your attack simulation training or other in house security training on those users to make them more vary about potential threats.

But this method has one major drawback, your team has to have the time and resources to do this kind of retroactive work, while on the other hand not being alerted that there is higher than usual activity against your users.

Custom detection based on a fixed baseline

So let’s use advanced hunting and custom detections to implement a solution to get an alert when a manual defined threshold is reached.

The following KQL query visualizes the incoming mails that are send directly to quarantine and are categorized as either malware or phishing attempts.

let Threshold = 50;
EmailEvents
| where EmailAction == "Send to quarantine"
| where ThreatTypes has "Malware" or ThreatTypes has "Phish"
| summarize Count=count() by bin(Timestamp, 1h)
| extend Threshold = Threshold
| render timechart

/en/detect-alert-unusual-high-phish-malware-email-volume/images/IncomingPhishAndSpam.png
Incoming Phish and Spam

Based on this chart you can go ahead and tweak the threshold for your environment. I would recommend to use the data from the last 30 days for this.

But since a custom detection needs additional information to get triggered, namely an timestamp and a report id, we have to add this information.

// Author: Fabian Bader
// Blog: https://cloudbrothers.info/en/detect-alert-unusual-high-phish-malware-email-volume
//
// Change the threshold based on your environment
let Threshold = 50;
// Query all mail that was send to quarantine and is either malware or phish
let PhishAndMalwareEmail = EmailEvents
| where EmailAction == "Send to quarantine"
| where ThreatTypes has "Malware" or ThreatTypes has "Phish";
// Check in which timespan the threshold was breached
let TimeAboveThreshold = PhishAndMalwareEmail
| summarize Count=count() by bin(Timestamp, 1h)
| extend Threshold = Threshold
| where Count>=Threshold;
// Only return mail events that are within in a timespan that breached the threshold
PhishAndMalwareEmail
| extend BinTime = bin(Timestamp, 1h)
| where BinTime in (TimeAboveThreshold)
// Only return a single result from the dataset, remove this line if you want alerts for every mail
// This can result in many alerts and not all of them might be summarized in a single incident
| limit 1

This query first queries all the malware and phish mail and then checks if the defined threshold was breached based on the total malicious mail within a one hour timespan. Only mail that was part of this timespan will be used to create an alert.

Because you will receive an alert for every mail, the default is to limit the result set to 1 item. Based on this alert you team can check the campaign view for additional information. If you remove the limit you will have the information about all targeted users in the created incident, but you might end up with multiple incidents. This is because not all mails will be related to the same campaign and the summarize logic will break them up.

To create your custom alert run the query and then click “Create detection rule” and fill out the necessary information. I would recommend to schedule this alert every hour.

/en/detect-alert-unusual-high-phish-malware-email-volume/images/CreateCustomAlert.png
Create custom alert

As MITRE technique select “T1566 Phishing” or something appropriate to your environment.

/en/detect-alert-unusual-high-phish-malware-email-volume/images/MITREtechniques.png
MITRE techniques

For impacted entities select “RecipientEmailAddress” and “ReceipientObjectId”

/en/detect-alert-unusual-high-phish-malware-email-volume/images/ImpactedEntities.png
Impacted entities

Custom detection based on anomaly detection

The initial query uses a hardcoded threshold, but KQL supports a native way of detecting anomalies in your datasets.

series_decompose_anomalies

This function calculates, based on a dynamic array, an anomaly score for each item and you can use this score to define your threshold and filter out normal volumes of quarantined mail items.

You still have to set a threshold, but it’s not based on the total number of emails, it’s based on how severe the anomaly needs to be.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Author: Fabian Bader
// Blog: https://cloudbrothers.info/en/detect-alert-unusual-high-phish-malware-email-volume
//
// Query all EmailEvents of the last 14 days and filter out only those timespans that are detected as anomalies
let Anomalies = EmailEvents
| where Timestamp > ago(14d)
| where EmailAction == "Send to quarantine"
| where ThreatTypes has "Malware" or ThreatTypes has "Phish"
// Create a time series with an 1 hour step including the timestamp as well as the number of quarantined emails within this timespan
| make-series Total=count() on Timestamp from startofday(ago(14d)) to startofday(ago(0d)) step 1h
// Use series_decompose_anomalies based on this data to calculate the anomaly score
// Modify the second value (Threshold) based on your environment
| extend (anomalies, score, baseline) = series_decompose_anomalies(Total, 10, -1, 'none')
// Expand the results back to single row items
| mv-expand Total to typeof(double), Timestamp to typeof(datetime), anomalies to typeof(double), score to typeof(double), baseline to typeof(long)
// Filter out timespans not detected as anomaly
| where anomalies > 0
// only return the timestamp of the timespan
| project Timestamp;
// Only return mail events that are within in a timespan that was anomalous
EmailEvents
| where EmailAction == "Send to quarantine"
| where ThreatTypes has "Malware" or ThreatTypes has "Phish"
| extend BinTime = bin(Timestamp, 1h)
| where BinTime in (Anomalies)
// Only return a single result from the dataset, remove this line if you want alerts for every mail
// This can result in many alerts and not all of them might be summarized in a single incident
| limit 1

Conclusion

I hope this custom detection rule and alerting technique is useful for you and your team or you just use it as a base to create your own KQL query. If this is the case, share your query with the community over on GitHub or Twitter.