Hunting tip of the month: Downloads originating from email links

In our July hunting tip, we shared a few advanced hunting queries based on browser download events and explained in depth where this data is from and what it means. In this August post, we are going to build on top of that and discuss more complex queries that join several noisy signals into stronger signals that you can use to hunt.


If you haven’t tried advanced hunting yet, sign up for a free Windows Defender ATP trial and experience how fast and convenient it is to hunt for possible breach activities in your network.


Step 1: Look for email links that result in browser downloads

Let’s start by looking for downloaded files that originate from links sent via email. We could join two events by matching the email link URL with the download URL (FileOriginUrl).

let emailLinks =
    // Filter on link clicks from Outlook (do case insensitive match using =~)
    | where ActionType == "BrowserLaunchedToOpenUrl" and isnotempty(RemoteUrl) and InitiatingProcessFileName =~ "outlook.exe" 
    | project ComputerName, MailLink=RemoteUrl;
| where isnotempty(FileOriginUrl) and InitiatingProcessFileName in~ ("chrome.exe", "browser_broker.exe")
| project FileName, FileOriginUrl, FileOriginReferrerUrl, ComputerName, EventTime, SHA1
| join kind=inner (emailLinks) on ComputerName, $left.FileOriginUrl == $right.MailLink
| count

On a reasonably large tenant, we got 2,404 results. This is a decent number of results, though a reasonable one to start slicing and dicing. But maybe there is something we have missed?


Step 2: Cover links to web pages

Our previous query returned only email links pointing directly to the downloads. More often than not, an email contains a link to a page, and from there the user clicks on the download link.

To cover such cases, we also check if the FileOriginReferrerUrl matches the email link. Let’s take the previous query and modify the join to support a match on either the FileOriginUrl or FileOriginReferrerUrl column.


We take this line:

| join kind=inner (emailLinks) on ComputerName, $left.FileOriginUrl == $right.MailLink 

And modify it as follows:

| join kind=inner (emailLinks) on ComputerName
| where FileOriginUrl == MailLink or FileOriginReferrerUrl == MailLink

Our results have grown by a factor of 10 to 22,293 rows. But is that all?


Step 3: Consider Office 365 ATP Safe Links time-of-click verification

Office 365 ATP Safe Links stops phishing and other email threats by modifying original links on emails to a URL in safelinks.protection.outlook.com, with the original URL provided as a query parameter. After doing time-of-click verification of the original URL, Safe Links redirect users to that URL.


If you have Safe Links in your organization, the extra redirection layer may render our step 2 query ineffective. Users actually open the Safe Link, which then redirects to the original URL in the email. So, the URL reported in the BrowserLaunchedToOpenUrl event and the one reported as FileOriginReferrerUrl are not the same if any other redirection or browsing took place.


To address this limitation, we parse the original URLs from the Safe Links URLs and join the file creation events on both the unmodified link and the parsed link. 

let emailLinks =
    | where ActionType == "BrowserLaunchedToOpenUrl" and isnotempty(RemoteUrl) and InitiatingProcessFileName =~ "outlook.exe" 
    | extend IsOutlookSafeLink=(tostring(parse_url(RemoteUrl).Host) endswith "safelinks.protection.outlook.com")
    | project ComputerName, MailLink=RemoteUrl, MailLinkTime=EventTime,
        ParsedMailLink=iff(IsOutlookSafeLink, url_decode(tostring(parse_url(RemoteUrl)["Query Parameters"]["url"])), RemoteUrl);
| where isnotempty(FileOriginUrl) and InitiatingProcessFileName in~ ("chrome.exe", "browser_broker.exe")
| project FileName, FileOriginUrl, FileOriginReferrerUrl, ComputerName, EventTime, SHA1
// join all downloads with all email links on the same machine 
| join kind=inner (emailLinks) on ComputerName
// Filter results to get only downloads from relevant URLs
| where FileOriginUrl == MailLink or FileOriginReferrerUrl == MailLink or FileOriginReferrerUrl =~ ParsedMailLink
| count

We now get even more results: 28,455 rows. But, wait, there are still attacks we are missing.


Step 4: A different approach – join by proximate time

The previously discussed queries are very accurate and work well when you know exactly what you are looking for. However, there are many attacks that these queries might miss. Specifically, if the attack scenario involves some more browsing, redirects, iframes, and other factors that can cause the mail link not to match FileOriginUrl or FileOriginReferrerUrl.


One way to address this is to stop relying on URL matches. Instead, you can look for file downloads that occur shortly after an email links is opened.


In this example, we look for downloads that occur within three minutes after an email link is opened. This will obviously include some unrelated downloads that just happen to be close and, on the other hand, miss cases where there was a long period of time between the link being opened and the download occurring. Still, we recommend taking this approach when you are hunting for the unknown.


So, we take the query from step 3 and replace this line: 

| where FileOriginUrl == MailLink or FileOriginReferrerUrl == MailLink or FileOriginReferrerUrl =~ ParsedMailLink

With this one:

| where (EventTime-MailLinkTime) between (0min..3min)

This new approach returns 44,120 potentially relevant download events. Now is a good time to start filtering the data and zooming in on the more interesting downloads. After all, the vast majority of downloads that start from an email link are benign, and we need some way to find the needle in the haystack.


Step 5: Slice and dice

There are many ways to slice and dice this data, but a very sensible approach would be by file extension. So, let us count the number of matched downloads per each file extension:


| extend Extension=tolower(tostring(parse_path(FileName).Extension))
| summarize count() by Extension

In our data set, the most prominent file extensions were zip, docx, exe, msi and pdf. All of these extensions are used in attacks, but as they are so common it could be challenging to filter out the noise. Instead, we can focus on file extensions that are likely rare in your dataset, but are still often used for malicious purposes, such as hta or js


As the next step, we suggest to add columns with the hosts used in the email link and the download URL, and to use the Filters pane in the UI to filter common and benign hosts or file names.

| extend MailHost=tostring(parse_url(ParsedMailLink).Host), FileOriginHost=tostring(parse_url(FileOriginUrl).Host)



Using this technique, you can very quickly review links that download files with rare extensions, but what shall we do with common ones, such as executables? DO NOT DESPAIR! We just need to find some other signal that we can use to find the bad guys.


Step 6: Use SmartScreen warnings to filter the noise

A quick way to zoom in on suspicious downloads is to take Windows Defender SmartScreen warnings into consideration. SmartScreen warns when the user runs an executable that has low or bad reputation. SmartScreen events are available in advanced hunting for machines running Windows 10 version 1703 and onwards.

// Query taken from: https://github.com/Microsoft/WindowsDefenderATP-Hunting-Queries/blob/master/Delivery/Email%20link%20...

// Query for SmartScreen warnings of unknown executed applications
let smartscreenAppWarnings =     MiscEvents     | where ActionType == "SmartScreenAppWarning"     | project WarnTime=EventTime, ComputerName, WarnedFileName=FileName, WarnedSHA1=SHA1, ActivityId=extractjson("$.ActivityId", AdditionalFields, typeof(string)) // Select only warnings that the user has decided to ignore and has executed the app. | join kind=leftsemi ( MiscEvents | where ActionType == "SmartScreenUserOverride" | project ComputerName, ActivityId=extractjson("$.ActivityId", AdditionalFields, typeof(string))) on ComputerName, ActivityId | project-away ActivityId; // Query for links opened from outlook, that are close in time to a SmartScreen warning let emailLinksNearSmartScreenWarnings =     MiscEvents     | where ActionType == "BrowserLaunchedToOpenUrl" and isnotempty(RemoteUrl) and InitiatingProcessFileName =~ "outlook.exe"     | extend WasOutlookSafeLink=(tostring(parse_url(RemoteUrl).Host) endswith "safelinks.protection.outlook.com")     | project ComputerName, MailLinkTime=EventTime,         MailLink=iff(WasOutlookSafeLink, url_decode(tostring(parse_url(RemoteUrl)["Query Parameters"]["url"])), RemoteUrl)     | join kind=inner smartscreenAppWarnings on ComputerName | where (WarnTime-MailLinkTime) between (0min..4min); // Add the browser download event to tie in all the dots FileCreationEvents | where isnotempty(FileOriginUrl) and InitiatingProcessFileName in~ ("chrome.exe", "browser_broker.exe") | project FileName, FileOriginUrl, FileOriginReferrerUrl, ComputerName, EventTime, SHA1 | join kind=inner emailLinksNearSmartScreenWarnings on ComputerName | where (EventTime-MailLinkTime) between (0min..3min) and (WarnTime-EventTime) between (0min..1min) | project FileName, MailLink, FileOriginUrl, FileOriginReferrerUrl, WarnedFileName, ComputerName, SHA1, WarnedSHA1, EventTime | distinct *

 Now we are down to 286 results!!!


This is still too many to manually review, but it is definitely a good start. We could then quickly slice-and-dice the data to exclude some common benign and internal sites, or combine other suspicious signals. Hopefully, you can explore these tweaks on your own and share your experiences with us.


Hunting with SmartScreen warnings

In step 4, we query for SmartScreen warnings that are ignored by users who decide to run unknown/suspicious applications. This query in the advanced hunting GitHub repository shows more of the SmartScreen app warning events. For example, it lets you differentiate between files that are known to be malicious and files that have low reputation.

If the user ignores a SmartScreen warning for known malware, you should question his motives. However, it is legitimate for a user to ignore a warning for an unknown program. For example, many in-house applications used internally by organizations are unknown to SmartScreen.


SmartScreen also warns when browsing to sites with bad reputation. To hunt on such events, filter on the SmartScreenUrlWarning action type, as shown in this query.


Test your users, raise awareness

Did you hear about the new attack simulator in Office 365? It makes it super easy to simulate a spear-phishing attack on your users, helping you identify vulnerable users and raise awareness.


Using advanced hunting on Windows Defender ATP, you can easily track the results of these simulated attacks, and see which users got phished and how deep have they fallen. You can easily tell which users have:

  1. Opened the link from the email. See the queries above using BrowserLaunchedToOpenUrl.
  2. Clicked on some links in the simulated page. Just point to a page hosted on a different IP address and, in your query, filter on NetworkCommunicationEvents involving that address.
  3. Downloaded a file from the simulated hoax site. See the queries above using FileCreationEvents.
  4. Ignored a SmartScreen warning. See this query on GitHub.
  5. Enabled macros in a document. Include macro code that does something unique you can query on, such as network communication, process create, or file create.

Hope you have enjoyed this post. You’re more than welcome to post questions or comments on this post, or to contribute new interesting queries to the GitHub repository.


So long, and thanks for all the phish. 😊