Detection rules › Kusto
Discord CDN Risky File Download
'Identifies callouts to Discord CDN addresses for risky file extensions. This detection will trigger when a callout for a risky file is made to a discord server that has only been seen once in your environment. Unique discord servers are identified using the server ID that is included in the request URL (DiscordServerId in query). Discord CDN has been used in multiple campaigns to download additional payloads'
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Command & Control |
Rule body
id: 010bd98c-a6be-498c-bdcd-502308c0fdae
name: Discord CDN Risky File Download
description: |
'Identifies callouts to Discord CDN addresses for risky file extensions. This detection will trigger when a callout for a risky file is made to a discord server that has only been seen once in your environment. Unique discord servers are identified using the server ID that is included in the request URL (DiscordServerId in query). Discord CDN has been used in multiple campaigns to download additional payloads'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: CefAma
dataTypes:
- CommonSecurityLog
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- CommandAndControl
relevantTechniques:
- T1071.001
tags:
- Discord
query: |
let connectionThreshold = 1;
let riskyExtensions = dynamic([".bin",".exe",".dll",".bin",".msi"]);
CommonSecurityLog
| where DeviceVendor =~ "ZScaler"
| where RequestURL has_any("media.discordapp.net", "cdn.discordapp.com")
| where RequestURL has "attachments"
| where DeviceAction !~ "blocked"
| extend DiscordServerId = extract(@"\/attachments\/([0-9]+)\/", 1, RequestURL)
| summarize dcount(RequestURL), make_set(SourceUserName), make_set(SourceIP), make_set(RequestURL), min(TimeGenerated), max(TimeGenerated), make_set(DeviceAction) by DiscordServerId, DeviceProduct
| where dcount_RequestURL <= connectionThreshold
| mv-expand set_SourceUserName to typeof(string), set_RequestURL to typeof(string), set_DeviceAction to typeof(string), set_SourceIP to typeof(string)
| summarize by DiscordServerId, DeviceProduct, dcount_RequestURL, set_SourceUserName, min_TimeGenerated, max_TimeGenerated, set_DeviceAction, set_SourceIP, set_RequestURL
| project StartTime=min_TimeGenerated, EndTime=max_TimeGenerated, DeviceActionTaken=set_DeviceAction, DeviceProduct, SourceUser=set_SourceUserName, SourceIP=set_SourceIP, RequestURL=set_RequestURL
| where RequestURL has_any (riskyExtensions)
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: SourceUser
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SourceIP
- entityType: URL
fieldMappings:
- identifier: Url
columnName: RequestURL
version: 1.0.4
kind: Scheduled
Stages and Predicates
Parameters
let connectionThreshold = 1;
let riskyExtensions = dynamic([".bin",".exe",".dll",".bin",".msi"]);
Stage 1: source
CommonSecurityLog
Stage 2: where
| where DeviceVendor =~ "ZScaler"
Stage 3: where
| where RequestURL has_any("media.discordapp.net", "cdn.discordapp.com")
Stage 4: where
| where RequestURL has "attachments"
Stage 5: where
| where DeviceAction !~ "blocked"
Stage 6: extend
| extend DiscordServerId = extract(@"\/attachments\/([0-9]+)\/", 1, RequestURL)
Stage 7: summarize
| summarize dcount(RequestURL), make_set(SourceUserName), make_set(SourceIP), make_set(RequestURL), min(TimeGenerated), max(TimeGenerated), make_set(DeviceAction) by DiscordServerId, DeviceProduct
Stage 8: where
| where dcount_RequestURL <= connectionThreshold
Stage 9: mv-expand
| mv-expand set_SourceUserName to typeof(string), set_RequestURL to typeof(string), set_DeviceAction to typeof(string), set_SourceIP to typeof(string)
Stage 10: summarize
| summarize by DiscordServerId, DeviceProduct, dcount_RequestURL, set_SourceUserName, min_TimeGenerated, max_TimeGenerated, set_DeviceAction, set_SourceIP, set_RequestURL
Stage 11: project
| project StartTime=min_TimeGenerated, EndTime=max_TimeGenerated, DeviceActionTaken=set_DeviceAction, DeviceProduct, SourceUser=set_SourceUserName, SourceIP=set_SourceIP, RequestURL=set_RequestURL
Stage 12: where
| where RequestURL has_any (riskyExtensions)
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
DeviceAction | ne |
| field:"DeviceAction" kind:ne value:"blocked" |
DeviceVendor | eq |
| field:"DeviceVendor" kind:eq value:"ZScaler" |
RequestURL | match |
| field:"RequestURL" kind:match |
dcount_RequestURL | le |
| field:"dcount_RequestURL" kind:le value:"1" |
Output fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DeviceActionTaken | project |
DeviceProduct | project |
EndTime | project |
RequestURL | project |
SourceIP | project |
SourceUser | project |
StartTime | project |