Detection rules › Kusto

Discord CDN Risky File Download

Status
available
Severity
medium
Time window
1d
Group by
DeviceProduct, DiscordServerId, dcount_RequestURL, max_TimeGenerated, min_TimeGenerated, set_DeviceAction, set_RequestURL, set_SourceIP, set_SourceUserName
Source
github.com/Azure/Azure-Sentinel

'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

Rule body kusto

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

Each row is a field, operator, and value that the rule matches. The corpus column counts how many other rules in the catalog look for the same combination: high numbers point to widely-used, community-vetted indicators. Blank or 1 shows that the indicator is specific to this rule.

FieldKindValues
DeviceActionne
  • blocked
DeviceVendoreq
  • ZScaler
RequestURLmatch
  • .bin
  • .dll
  • .exe
  • .msi
  • attachments transforms: term
  • cdn.discordapp.com
  • media.discordapp.net
dcount_RequestURLle
  • 1 transforms: cased

Output fields

Fields the rule emits when it matches. Chronicle authors list these in the outcome block; they appear on the detection and $risk_score drives alerting. Sentinel / Defender XDR rules build them up through project / summarize / extend stages. Sentinel maps these into alert fields via entityMappings and customDetails; Defender XDR custom detections surface them as alert fields directly.

FieldSource
DeviceActionTakenproject
DeviceProductproject
EndTimeproject
RequestURLproject
SourceIPproject
SourceUserproject
StartTimeproject