Adversaries may use the same download sources to exfiltrate data or deploy additional malicious payloads after initial compromise. SOC teams should proactively hunt for this behavior in Azure Sentinel to identify potential lateral movement or persistence mechanisms.
KQL Query
let detectedDownloads =
DeviceEvents
| where ActionType == "AntivirusDetection" and isnotempty(FileOriginUrl)
| project Timestamp, FileOriginUrl, FileName, DeviceId,
ThreatName=tostring(parse_json(AdditionalFields).ThreatName)
// Filter out less severe threat categories on which we do not want to pivot
| where ThreatName !startswith "PUA"
and ThreatName !startswith "SoftwareBundler:"
and FileOriginUrl != "about:internet";
let detectedDownloadsSummary =
detectedDownloads
// Get a few examples for each detected Host:
// up to 4 filenames, up to 4 threat names, one full URL)
| summarize DetectedUrl=any(FileOriginUrl),
DetectedFiles=makeset(FileName, 4),
ThreatNames=makeset(ThreatName, 4)
by Host=tostring(parse_url(FileOriginUrl).Host);
// Query for downloads from sites from which other downloads were detected by Windows Defender Antivirus
DeviceFileEvents
| where isnotempty(FileOriginUrl)
| project FileName, FileOriginUrl, DeviceId, Timestamp,
Host=tostring(parse_url(FileOriginUrl).Host), SHA1
// Filter downloads from hosts serving detected files
| join kind=inner(detectedDownloadsSummary) on Host
// Filter out download file create events that were also detected.
// This is needed because sometimes both of these events will be reported,
// and sometimes only the AntivirusDetection event - depending on timing.
| join kind=leftanti(detectedDownloads) on DeviceId, FileOriginUrl
// Summarize a single row per host - with the machines count
// and an example event for a missed download (select the last event)
| summarize MachineCount=dcount(DeviceId), arg_max(Timestamp, *) by Host
// Filter out common hosts, as they probably ones that also serve benign files
| where MachineCount < 20
| project Host, MachineCount, DeviceId, FileName, DetectedFiles,
FileOriginUrl, DetectedUrl, ThreatNames, Timestamp, SHA1
| order by MachineCount desc
id: 351f7035-836c-4f4b-80bb-188220ba9215
name: Pivot from detections to related downloads
description: |
Pivot from downloads detected by Windows Defender Antivirus to other files downloaded from the same sites.
To learn more about the download URL info that is available and see other sample queries,.
Check out this blog post: https://techcommunity.microsoft.com/t5/Threat-Intelligence/Hunting-tip-of-the-month-Browser-downloads/td-p/220454.
requiredDataConnectors:
- connectorId: MicrosoftThreatProtection
dataTypes:
- DeviceEvents
- DeviceFileEvents
query: |
let detectedDownloads =
DeviceEvents
| where ActionType == "AntivirusDetection" and isnotempty(FileOriginUrl)
| project Timestamp, FileOriginUrl, FileName, DeviceId,
ThreatName=tostring(parse_json(AdditionalFields).ThreatName)
// Filter out less severe threat categories on which we do not want to pivot
| where ThreatName !startswith "PUA"
and ThreatName !startswith "SoftwareBundler:"
and FileOriginUrl != "about:internet";
let detectedDownloadsSummary =
detectedDownloads
// Get a few examples for each detected Host:
// up to 4 filenames, up to 4 threat names, one full URL)
| summarize DetectedUrl=any(FileOriginUrl),
DetectedFiles=makeset(FileName, 4),
ThreatNames=makeset(ThreatName, 4)
by Host=tostring(parse_url(FileOriginUrl).Host);
// Query for downloads from sites from which other downloads were detected by Windows Defender Antivirus
DeviceFileEvents
| where isnotempty(FileOriginUrl)
| project FileName, FileOriginUrl, DeviceId, Timestamp,
Host=tostring(parse_url(FileOriginUrl).Host), SHA1
// Filter downloads from hosts serving detected files
| join kind=inner(detectedDownloadsSummary) on Host
// Filter out download file create events that were also detected.
// This is needed because sometimes both of these events will be reported,
// and sometimes only the AntivirusDetection event - depending on timing.
| join kind=leftanti(detectedDownloads) on DeviceId, FileOriginUrl
// Summarize a single row per host - with the machines count
// and an example event for a missed download (select the last event)
| summarize MachineCount=dcount(DeviceId), arg_max(Timestamp, *) by Host
// Filter out common hosts, as they probably ones that also serve benign files
| where MachineCount < 20
| project Host, MachineCount, DeviceId, FileName, DetectedFiles,
FileOriginUrl, DetectedUrl, ThreatNames, Timestamp, SHA1
| order by MachineCount desc
| Sentinel Table | Notes |
|---|---|
DeviceEvents | Ensure this data connector is enabled |
DeviceFileEvents | Ensure this data connector is enabled |
Scenario: A system administrator uses Windows Defender Antivirus to scan a legitimate software update that is downloaded from a known and trusted website.
Filter/Exclusion: Exclude downloads from known Microsoft update servers (e.g., https://download.microsoft.com) or use a custom list of trusted domains in the rule’s url field.
Scenario: A scheduled job runs a PowerShell script that downloads configuration files from an internal repository (e.g., using Invoke-WebRequest or curl).
Filter/Exclusion: Exclude URLs that match internal IP ranges or internal domain names (e.g., *.internal.corp), or use a source_ip filter to exclude internal network addresses.
Scenario: A user downloads a legitimate tool (e.g., 7-Zip, WinRAR, or Chocolatey) from a public repository (e.g., GitHub or official download sites).
Filter/Exclusion: Exclude known safe download URLs using a list of whitelisted domains or hash-based filtering for known safe files.
Scenario: A system administrator uses the Windows Defender ATP console to manually download and install a security update or patch.
Filter/Exclusion: Exclude files with known update hashes or use a file_name filter to exclude common update file names like Windows10Update.exe.
Scenario: A DevOps pipeline uses a CI/CD tool (e.g., Jenkins, GitHub Actions) to download dependencies from a public package registry (e.g., npm, PyPI, or NuGet).
Filter/Exclusion: Exclude URLs that match known package registry domains (e.g., https://registry.npmjs.org, https://pypi.org) or use a process_name filter to exclude CI/CD tool processes.