Detection rules › Kusto
Kusto rules: 0q
1Password - Potential insider privilege escalation via group
#This will alert when an actor grants, or updates their own permissions via a group. Once this analytics rule is triggered it will group all related future alerts for upto an hour when all related entities are the same. Ref: https://1password.com/ Ref: https://github.com/securehats/
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Privilege Escalation |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| 1Password | any: Group membership (catch-all) |
Rule Body
id: 398a1cf1-f56f-4700-912c-9bf4c8409ebc
name: 1Password - Potential insider privilege escalation via group
version: 1.0.0
kind: Scheduled
description: |-
This will alert when an actor grants, or updates their own permissions via a group. Once this analytics rule is triggered it will group all related future alerts for upto an hour when all related entities are the same.
Ref: https://1password.com/
Ref: https://github.com/securehats/
severity: Medium
requiredDataConnectors:
- connectorId: 1Password
dataTypes:
- OnePasswordEventLogs_CL
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
tactics:
- PrivilegeEscalation
relevantTechniques:
- T1078
query: |-
OnePasswordEventLogs_CL
| where log_source == "auditevents"
| where action has_any("join", "role")
| where object_type == "gm"
| where tostring(actor_details.email) == tostring(aux_details.email)
| extend
TargetUsername = aux_details.email
, ActorUsername = actor_details.email
, SrcIpAddr = session.ip
, GroupRole = case(
aux_info == "R", "Group member"
, aux_info == "A", "Group manager"
, aux_info
)
subTechniques:
- T1078.004
suppressionEnabled: false
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: true
reopenClosedIncident: false
lookbackDuration: 1h
matchingMethod: AllEntities
suppressionDuration: 5h
eventGroupingSettings:
aggregationKind: SingleAlert
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: ActorUsername
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: TargetUsername
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SrcIpAddr
Stages and Predicates
Stage 1: source
OnePasswordEventLogs_CL
Stage 2: where
| where log_source == "auditevents"
Stage 3: where
| where action has_any("join", "role")
Stage 4: where
| where object_type == "gm"
Stage 5: where
| where tostring(actor_details.email) == tostring(aux_details.email)
Stage 6: extend
| extend
TargetUsername = aux_details.email
, ActorUsername = actor_details.email
, SrcIpAddr = session.ip
, GroupRole = case(
aux_info == "R", "Group member"
, aux_info == "A", "Group manager"
, aux_info
)
GroupRole =aux_info == "R""Group member"aux_info == "A""Group manager"aux_infoIndicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
action | match |
| field:"action" kind:match |
log_source | eq |
| field:"log_source" kind:eq value:"auditevents" |
object_type | eq |
| field:"object_type" kind:eq value:"gm" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ActorUsername | extend |
GroupRole | extend |
SrcIpAddr | extend |
TargetUsername | extend |
Apache - Put suspicious file
#'Detects PUT or POST of suspicious file'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Exfiltration |
Rule Body
id: c5d69e46-3b00-11ec-8d3d-0242ac130003
name: Apache - Put suspicious file
description: |
'Detects PUT or POST of suspicious file'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: CustomLogsAma
datatypes:
- ApacheHTTPServer_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
- Exfiltration
relevantTechniques:
- T1190
- T1133
- T1048
query: |
ApacheHTTPServer
| where HttpRequestMethod in~ ("POST", "PUT")
| extend File = extract(@"(.*\/)?(.*)", 2, tostring(UrlOriginal))
| where isnotempty(File)
| where File matches regex @"([a-zA-Z0-9-_]+\.)([a-zA-Z0-9-]+\.[a-zA-Z0-9-]+)"
| extend FileCustomEntity = File, UrlCustomEntity = UrlOriginal
entityMappings:
- entityType: File
fieldMappings:
- identifier: Name
columnName: FileCustomEntity
- entityType: URL
fieldMappings:
- identifier: Url
columnName: UrlCustomEntity
version: 1.0.2
kind: Scheduled
Stages and Predicates
Stage 1: source
ApacheHTTPServer
Stage 2: where
| where HttpRequestMethod in~ ("POST", "PUT")
Stage 3: extend
| extend File = extract(@"(.*\/)?(.*)", 2, tostring(UrlOriginal))
Stage 4: where
| where isnotempty(File)
Stage 5: where
| where File matches regex @"([a-zA-Z0-9-_]+\.)([a-zA-Z0-9-]+\.[a-zA-Z0-9-]+)"
Stage 6: extend
| extend FileCustomEntity = File, UrlCustomEntity = UrlOriginal
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
File | is_not_null | field:"File" kind:is_not_null | |
File | regex_match |
| field:"File" kind:regex_match value:"([a-zA-Z0-9-_]+.)([a-zA-Z0-9-]+.[a-zA-Z0-9-]+)" |
HttpRequestMethod | in |
| field:"HttpRequestMethod" kind:in |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
File | extend |
FileCustomEntity | extend |
UrlCustomEntity | extend |
AWSCloudTrail - Monitor AWS Credential abuse or hijacking
#Identifies AWS STS GetCallerIdentity calls made by assumed-role sessions. An attacker who has assumed the role of a legitimate account can call the GetCallerIdentity function to determine what account they are using. A legitimate user using legitimate credentials would not need to call GetCallerIdentity since they should already know what account they are using. More Information: https://duo.com/decipher/trailblazer-hunts-compromised-credentials-in-aws AWS STS GetCallerIdentity API: https://docs.aws.amazon.com/STS/latest/APIReference/API_GetCallerIdentity.html
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Discovery |
Telemetry Coverage
Rules Detecting the Same Action
These rules filter on the same operation.
Rule Body
id: 32555639-b639-4c2b-afda-c0ae0abefa55
name: AWSCloudTrail - Monitor AWS Credential abuse or hijacking
description: |
Identifies AWS STS `GetCallerIdentity` calls made by assumed-role sessions.
An attacker who has assumed the role of a legitimate account can call the GetCallerIdentity function to determine what account they are using.
A legitimate user using legitimate credentials would not need to call GetCallerIdentity since they should already know what account they are using.
More Information: https://duo.com/decipher/trailblazer-hunts-compromised-credentials-in-aws
AWS STS GetCallerIdentity API: https://docs.aws.amazon.com/STS/latest/APIReference/API_GetCallerIdentity.html
severity: Low
status: Available
requiredDataConnectors:
- connectorId: AWS
dataTypes:
- AWSCloudTrail
- connectorId: AWSS3
dataTypes:
- AWSCloudTrail
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- Discovery
relevantTechniques:
- T1580
query: |
AWSCloudTrail
| where EventName =~ "GetCallerIdentity" and UserIdentityType =~ "AssumedRole"
| extend UserIdentityArn = iif(isempty(UserIdentityArn), tostring(parse_json(Resources)[0].ARN), UserIdentityArn)
| extend UserName = tostring(split(UserIdentityArn, '/')[-1])
| extend AccountName = case(UserIdentityPrincipalid == "Anonymous", "Anonymous", isempty(UserIdentityUserName), UserName, UserIdentityUserName)
| extend AccountName = iif(AccountName contains "@", tostring(split(AccountName, '@', 0)[0]), AccountName),
AccountUPNSuffix = iif(AccountName contains "@", tostring(split(AccountName, '@', 1)[0]), "")
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by SourceIpAddress, EventName, EventTypeName, UserIdentityType, RecipientAccountId, AccountName, AccountUPNSuffix, UserIdentityAccountId, UserIdentityPrincipalid,
UserAgent, UserIdentityUserName, SessionMfaAuthenticated, AWSRegion, EventSource, AdditionalEventData, ResponseElements
| extend timestamp = StartTime
| sort by EndTime desc nulls last
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: AccountName
- identifier: UPNSuffix
columnName: AccountUPNSuffix
- identifier: CloudAppAccountId
columnName: RecipientAccountId
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SourceIpAddress
customDetails:
AWSRegion: AWSRegion
EventName: EventName
EventSource: EventSource
EventType: EventTypeName
alertDetailsOverride:
alertDisplayNameFormat: "AWS credential hijack indicator from {{AccountName}}"
alertDescriptionFormat: "GetCallerIdentity invoked by {{AccountName}} from {{SourceIpAddress}} in {{AWSRegion}}."
version: 1.0.4
kind: Scheduled
Stages and Predicates
Stage 1: source
AWSCloudTrail
Stage 2: where
| where EventName =~ "GetCallerIdentity" and UserIdentityType =~ "AssumedRole"
Stage 3: extend (4 consecutive steps)
| extend UserIdentityArn = iif(isempty(UserIdentityArn), tostring(parse_json(Resources)[0].ARN), UserIdentityArn)
| extend UserName = tostring(split(UserIdentityArn, '/')[-1])
| extend AccountName = case(UserIdentityPrincipalid == "Anonymous", "Anonymous", isempty(UserIdentityUserName), UserName, UserIdentityUserName)
| extend AccountName = iif(AccountName contains "@", tostring(split(AccountName, '@', 0)[0]), AccountName),
AccountUPNSuffix = iif(AccountName contains "@", tostring(split(AccountName, '@', 1)[0]), "")
Stage 4: summarize
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by SourceIpAddress, EventName, EventTypeName, UserIdentityType, RecipientAccountId, AccountName, AccountUPNSuffix, UserIdentityAccountId, UserIdentityPrincipalid,
UserAgent, UserIdentityUserName, SessionMfaAuthenticated, AWSRegion, EventSource, AdditionalEventData, ResponseElements
Stage 5: extend
| extend timestamp = StartTime
Stage 6: sort
| sort by EndTime desc nulls last
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
EventName | eq |
| field:"aws::eventName" kind:eq value:"GetCallerIdentity" |
UserIdentityType | eq |
| field:"aws::userIdentity.type" kind:eq value:"AssumedRole" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AWSRegion | summarize |
AccountName | summarize |
AccountUPNSuffix | summarize |
AdditionalEventData | summarize |
EndTime | summarize |
EventName | summarize |
EventSource | summarize |
EventTypeName | summarize |
RecipientAccountId | summarize |
ResponseElements | summarize |
SessionMfaAuthenticated | summarize |
SourceIpAddress | summarize |
StartTime | summarize |
UserAgent | summarize |
UserIdentityAccountId | summarize |
UserIdentityPrincipalid | summarize |
UserIdentityType | summarize |
UserIdentityUserName | summarize |
timestamp | extend |
BloodHound Attack Path Finding - Non Tier Zero Resource Assigned to Tier Zero Service Principal
#This is a third-party alert feed, not a detection over modeled telemetry. The vendor product raised the finding; this rule forwards it into the SIEM. It is searchable for reference but is excluded from the detection-rule browse and the ATT&CK coverage matrix.
BloodHound Attack Path Finding - Non Tier Zero Resource Assigned to Tier Zero Service Principal
Rule Body
id: 9fe3e89b-fd57-4df1-a6e7-8a875966dada
name: BloodHound Attack Path Finding - Non Tier Zero Resource Assigned to Tier Zero
Service Principal
description: BloodHound Attack Path Finding - Non Tier Zero Resource Assigned to Tier
Zero Service Principal
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: BloodHoundEnterprise
dataTypes:
- BHEAttackPathsData_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics: []
relevantTechniques: []
query: "BHEAttackPathsData_CL \n| where Finding == \"AzureNonT0ManagedIdentityAssignment\""
entityMappings:
- entityType: URL
fieldMappings:
- identifier: Url
columnName: domain_name
customDetails:
Description: ShortDescription
Remediation: ShortRemediation
version: 1.0.0
kind: Scheduled
Stages and Predicates
Stage 1: source
BHEAttackPathsData_CL
Stage 2: where
| where Finding == "AzureNonT0ManagedIdentityAssignment"
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Finding | eq |
| field:"Finding" kind:eq value:"AzureNonT0ManagedIdentityAssignment" |
CYFIRMA - Attack Surface - Malicious Domain/IP Reputation High Rule
#This is a third-party alert feed, not a detection over modeled telemetry. The vendor product raised the finding; this rule forwards it into the SIEM. It is searchable for reference but is excluded from the detection-rule browse and the ATT&CK coverage matrix.
"This alert is raised when CYFIRMA detects a critical reputation score for an IP address linked to your infrastructure. The IP has been previously associated with hacking activity and web application attacks. Denied outbound traffic to a foreign country from a known Microsoft data center IP suggests potential misuse or compromise of cloud infrastructure."
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Reconnaissance | |
| Initial Access | |
| Stealth | |
| Command & Control | |
| Exfiltration | |
| Impact |
Rule Body
id: 7ff6f6d7-9672-4567-99fc-cb8a58c3bce7
name: CYFIRMA - Attack Surface - Malicious Domain/IP Reputation High Rule
description: |
"This alert is raised when CYFIRMA detects a critical reputation score for an IP address linked to your infrastructure.
The IP has been previously associated with hacking activity and web application attacks.
Denied outbound traffic to a foreign country from a known Microsoft data center IP suggests potential misuse or compromise of cloud infrastructure."
version: 1.0.1
kind: Scheduled
severity: High
requiredDataConnectors:
- connectorId: CyfirmaAttackSurfaceAlertsConnector
dataTypes:
- CyfirmaASDomainIPReputationAlerts_CL
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
- InitialAccess
- CommandAndControl
- Reconnaissance
- Impact
- DefenseEvasion
- Exfiltration
relevantTechniques:
- T1566.002
- T1071.001
- T1090.002
- T1595.002
- T1036.005
- T1499
- T1041
query: |
// High Severity - Malicious Domain/IP Reputation Detected
let timeFrame = 5m;
CyfirmaASDomainIPReputationAlerts_CL
| where severity == 'Critical' and TimeGenerated between (ago(timeFrame) .. now())
| extend
FirstSeen=first_seen,
LastSeen=last_seen,
RiskScore=risk_score,
Domain=sub_domain,
TopDomain=top_domain,
NetworkIP=ip,
AlertUID=alert_uid,
UID=uid,
Categories=categories,
IPversion=ip_version,
ISP=isp,
ThreatActors=threat_actors,
Country=country,
LastUsersReported=last_users_reported,
ProviderName='CYFIRMA',
ProductName='DeCYFIR/DeTCT'
| project
TimeGenerated,
Domain,
TopDomain,
RiskScore,
FirstSeen,
LastSeen,
NetworkIP,
AlertUID,
UID,
Categories,
IPversion,
ISP,
ThreatActors,
Country,
LastUsersReported,
ProviderName,
ProductName
entityMappings:
- entityType: DNS
fieldMappings:
- identifier: DomainName
columnName: Domain
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: TopDomain
- identifier: DnsDomain
columnName: Domain
- entityType: IP
fieldMappings:
- identifier: Address
columnName: NetworkIP
customDetails:
TimeGenerated: TimeGenerated
RiskScore: RiskScore
FirstSeen: FirstSeen
LastSeen: LastSeen
LastUsersReported: LastUsersReported
AlertUID: AlertUID
UID: UID
Categories: Categories
IPversion: IPversion
ThreatActors: ThreatActors
Country: Country
ISP: ISP
alertDetailsOverride:
alertDisplayNameFormat: "CYFIRMA - High Severity Malicious Domain/IP Reputation Alert - Domain: {{Domain}}, IP: {{NetworkIP}} "
alertDescriptionFormat: "CYFIRMA - High Severity Malicious Domain/IP Reputation Alert - Domain: {{Domain}}, IP: {{NetworkIP}} "
alertDynamicProperties:
- alertProperty: ProductName
value: ProductName
- alertProperty: ProviderName
value: ProviderName
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: false
reopenClosedIncident: false
lookbackDuration: PT5H
matchingMethod: AllEntities
eventGroupingSettings:
aggregationKind: AlertPerResult
Stages and Predicates
Parameters
let timeFrame = 5m;
Stage 1: source
CyfirmaASDomainIPReputationAlerts_CL
Stage 2: where
| where severity == 'Critical' and TimeGenerated between (ago(timeFrame) .. now())
Stage 3: extend
| extend
FirstSeen=first_seen,
LastSeen=last_seen,
RiskScore=risk_score,
Domain=sub_domain,
TopDomain=top_domain,
NetworkIP=ip,
AlertUID=alert_uid,
UID=uid,
Categories=categories,
IPversion=ip_version,
ISP=isp,
ThreatActors=threat_actors,
Country=country,
LastUsersReported=last_users_reported,
ProviderName='CYFIRMA',
ProductName='DeCYFIR/DeTCT'
Stage 4: project
| project
TimeGenerated,
Domain,
TopDomain,
RiskScore,
FirstSeen,
LastSeen,
NetworkIP,
AlertUID,
UID,
Categories,
IPversion,
ISP,
ThreatActors,
Country,
LastUsersReported,
ProviderName,
ProductName
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
severity | eq |
| field:"severity" kind:eq value:"Critical" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AlertUID | project |
Categories | project |
Country | project |
Domain | project |
FirstSeen | project |
IPversion | project |
ISP | project |
LastSeen | project |
LastUsersReported | project |
NetworkIP | project |
ProductName | project |
ProviderName | project |
RiskScore | project |
ThreatActors | project |
TimeGenerated | project |
TopDomain | project |
UID | project |
CYFIRMA - Data Breach and Web Monitoring - Dark Web Medium Rule
#This is a third-party alert feed, not a detection over modeled telemetry. The vendor product raised the finding; this rule forwards it into the SIEM. It is searchable for reference but is excluded from the detection-rule browse and the ATT&CK coverage matrix.
"Detects critical alerts from CYFIRMA related to sensitive data or credentials leaked on dark web forums. These events often indicate unauthorized access or compromise of enterprise systems, cloud environments, or identity platforms. Immediate investigation is required to assess breach scope and initiate mitigation, including credential resets, access reviews, and threat actor tracking."
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Credential Access | |
| Collection | |
| Exfiltration | |
| Impact |
Rule Body
id: c0afeda7-4832-49a6-8d03-a5d137d513b5
name: CYFIRMA - Data Breach and Web Monitoring - Dark Web Medium Rule
description: |
"Detects critical alerts from CYFIRMA related to sensitive data or credentials leaked on dark web forums.
These events often indicate unauthorized access or compromise of enterprise systems, cloud environments, or identity platforms.
Immediate investigation is required to assess breach scope and initiate mitigation, including credential resets, access reviews, and threat actor tracking."
version: 1.0.1
kind: Scheduled
severity: Medium
requiredDataConnectors:
- connectorId: CyfirmaDigitalRiskAlertsConnector
dataTypes:
- CyfirmaDBWMDarkWebAlerts_CL
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
- CredentialAccess
- Collection
- Exfiltration
- Impact
relevantTechniques:
- T1552.001
- T1555.003
- T1212
- T1119
- T1048
- T1486
query: |
// Medium severity - Data Breach and Web Monitoring - Dark Web
let timeFrame = 5m;
CyfirmaDBWMDarkWebAlerts_CL
| where severity == 'High' and TimeGenerated between (ago(timeFrame) .. now())
| extend
Description=description,
FirstSeen=first_seen,
LastSeen=last_seen,
RiskScore=risk_score,
AlertUID=alert_uid,
UID=uid,
AssetType=asset_type,
AssetValue=signature,
Impact=impact,
Recommendation='',
ProviderName='CYFIRMA',
ProductName='DeCYFIR/DeTCT',
AlertTitle=Alert_title
| project
TimeGenerated,
Description,
RiskScore,
FirstSeen,
LastSeen,
AlertUID,
UID,
AssetType,
AssetValue,
Impact,
ProductName,
ProviderName,
AlertTitle
customDetails:
AssetType: AssetType
AssetValue: AssetValue
RiskScore: RiskScore
Impact: Impact
Description: Description
FirstSeen: FirstSeen
LastSeen: LastSeen
AlertUID: AlertUID
UID: UID
TimeGenerated: TimeGenerated
alertDetailsOverride:
alertDisplayNameFormat: "CYFIRMA - Medium Severity Alert: Critical Data Exposure of Enterprise SSO on Dark Web- {{AlertTitle}} "
alertDescriptionFormat: "{{Description}} "
alertDynamicProperties:
- alertProperty: ProductName
value: ProductName
- alertProperty: ProviderName
value: ProviderName
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: false
reopenClosedIncident: false
lookbackDuration: PT5H
matchingMethod: AllEntities
eventGroupingSettings:
aggregationKind: AlertPerResult
Stages and Predicates
Parameters
let timeFrame = 5m;
Stage 1: source
CyfirmaDBWMDarkWebAlerts_CL
Stage 2: where
| where severity == 'High' and TimeGenerated between (ago(timeFrame) .. now())
Stage 3: extend
| extend
Description=description,
FirstSeen=first_seen,
LastSeen=last_seen,
RiskScore=risk_score,
AlertUID=alert_uid,
UID=uid,
AssetType=asset_type,
AssetValue=signature,
Impact=impact,
Recommendation='',
ProviderName='CYFIRMA',
ProductName='DeCYFIR/DeTCT',
AlertTitle=Alert_title
Stage 4: project
| project
TimeGenerated,
Description,
RiskScore,
FirstSeen,
LastSeen,
AlertUID,
UID,
AssetType,
AssetValue,
Impact,
ProductName,
ProviderName,
AlertTitle
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
severity | eq |
| field:"severity" kind:eq value:"High" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AlertTitle | project |
AlertUID | project |
AssetType | project |
AssetValue | project |
Description | project |
FirstSeen | project |
Impact | project |
LastSeen | project |
ProductName | project |
ProviderName | project |
RiskScore | project |
TimeGenerated | project |
UID | project |
CYFIRMA - High severity Trojan Network Indicators - Monitor Recommended Rule
#This is a third-party alert feed, not a detection over modeled telemetry. The vendor product raised the finding; this rule forwards it into the SIEM. It is searchable for reference but is excluded from the detection-rule browse and the ATT&CK coverage matrix.
"This analytics rule detects network-based indicators flagged by CYFIRMA threat intelligence as associated with Command & Control (C2) infrastructure. These indicators may represent attacker-controlled endpoints used for persistence, data exfiltration, or command delivery to compromised systems."
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Execution | |
| Persistence | |
| Credential Access | |
| Command & Control | |
| Impact |
Rule Body
id: 89fd02b8-3c21-492c-a8de-b3e728d39119
name: CYFIRMA - High severity Trojan Network Indicators - Monitor Recommended Rule
description: |
"This analytics rule detects network-based indicators flagged by CYFIRMA threat intelligence as associated with Command & Control (C2) infrastructure.
These indicators may represent attacker-controlled endpoints used for persistence, data exfiltration, or command delivery to compromised systems."
version: 1.0.1
kind: Scheduled
severity: High
enabled: false
requiredDataConnectors:
- connectorId: CyfirmaCyberIntelligenceDC
dataTypes:
- CyfirmaIndicators_CL
query: |
//Trojan Network Indicators - Monitor Recommended
let timeFrame= 5m;
CyfirmaIndicators_CL
| where ConfidenceScore >= 80
and TimeGenerated between (ago(timeFrame) .. now())
and pattern !contains 'file:hashes' and RecommendedActions has 'Monitor' and Roles has 'Trojan'
| extend IPv4 = extract(@"ipv4-addr:value\s*=\s*'([^']+)'", 1, pattern)
| extend IPv6 = extract(@"ipv6-addr:value\s*=\s*'([^']+)'", 1, pattern)
| extend URL = extract(@"url:value\s*=\s*'([^']+)'", 1, pattern)
| extend Domain = extract(@"domain-name:value\s*=\s*'([^']+)'", 1, pattern)
| extend parsed = parse_json(extensions)
| extend extensionKeys = bag_keys(parsed)
| mv-expand extensionKeys
| extend extensionKeyStr = tostring(extensionKeys)
| extend ext = parsed[extensionKeyStr]
| extend props = ext.properties
| extend
extension_id = extensionKeyStr,
ASN_Owner = props.asn_owner,
ASN = props.asn,
ProviderName = 'CYFIRMA',
ProductName = 'DeCYFIR/DeTCT'
| project
IPv4,
IPv6,
URL,
Domain,
ThreatActors,
RecommendedActions,
Sources,
Roles,
Country,
IPAbuse,
name,
Description,
ConfidenceScore,
IndicatorID,
created,
modified,
valid_from,
Tags,
ThreatType,
TimeGenerated,
SecurityVendors,
ProductName,
ProviderName
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: GreaterThan
triggerThreshold: 0
suppressionDuration: 5m
suppressionEnabled: true
tactics:
- Impact
- Persistence
- DefenseEvasion
- CredentialAccess
- CommandAndControl
- Execution
- InitialAccess
relevantTechniques:
- T1496
- T1053
- T1555
- T1090
- T1071
- T1204
- T1189
- T1053.005
- T1555.003
- T1071.001
- T1204.001
alertDetailsOverride:
alertDisplayNameFormat: "High-Confidence Trojan Network Indicators - Monitor Recommended Rule - {{name}} "
alertDescriptionFormat: "{{Description}} - {{name}} "
alertDynamicProperties:
- alertProperty: ProductName
value: ProductName
- alertProperty: ProviderName
value: ProviderName
customDetails:
ThreatActors: ThreatActors
Sources: Sources
RecommendedActions: RecommendedActions
Roles: Roles
Country: Country
Description: Description
ConfidenceScore: ConfidenceScore
SecurityVendors: SecurityVendors
IndicatorID: IndicatorID
Created: created
Modified: modified
ValidFrom: valid_from
Tags: Tags
ThreatType: ThreatType
TimeGenerated: TimeGenerated
IPAbuse: IPAbuse
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPv4
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPv6
- entityType: DNS
fieldMappings:
- identifier: DomainName
columnName: Domain
- entityType: URL
fieldMappings:
- identifier: Url
columnName: URL
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: false
reopenClosedIncident: false
lookbackDuration: PT5H
matchingMethod: AllEntities
eventGroupingSettings:
aggregationKind: AlertPerResult
Stages and Predicates
Parameters
let timeFrame = 5m;
Stage 1: source
CyfirmaIndicators_CL
Stage 2: where
| where ConfidenceScore >= 80
and TimeGenerated between (ago(timeFrame) .. now())
and pattern !contains 'file:hashes' and RecommendedActions has 'Monitor' and Roles has 'Trojan'
Stage 3: extend (6 consecutive steps)
| extend IPv4 = extract(@"ipv4-addr:value\s*=\s*'([^']+)'", 1, pattern)
| extend IPv6 = extract(@"ipv6-addr:value\s*=\s*'([^']+)'", 1, pattern)
| extend URL = extract(@"url:value\s*=\s*'([^']+)'", 1, pattern)
| extend Domain = extract(@"domain-name:value\s*=\s*'([^']+)'", 1, pattern)
| extend parsed = parse_json(extensions)
| extend extensionKeys = bag_keys(parsed)
Stage 4: mv-expand
| mv-expand extensionKeys
Stage 5: extend (4 consecutive steps)
| extend extensionKeyStr = tostring(extensionKeys)
| extend ext = parsed[extensionKeyStr]
| extend props = ext.properties
| extend
extension_id = extensionKeyStr,
ASN_Owner = props.asn_owner,
ASN = props.asn,
ProviderName = 'CYFIRMA',
ProductName = 'DeCYFIR/DeTCT'
Stage 6: project
| project
IPv4,
IPv6,
URL,
Domain,
ThreatActors,
RecommendedActions,
Sources,
Roles,
Country,
IPAbuse,
name,
Description,
ConfidenceScore,
IndicatorID,
created,
modified,
valid_from,
Tags,
ThreatType,
TimeGenerated,
SecurityVendors,
ProductName,
ProviderName
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
ConfidenceScore | ge |
| field:"ConfidenceScore" kind:ge value:"80" |
RecommendedActions | match |
| field:"RecommendedActions" kind:match value:"Monitor" |
Roles | match |
| field:"Roles" kind:match value:"Trojan" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
pattern | contains | file:hashes | excludes:pattern field:"pattern" value:"file:hashes" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ConfidenceScore | project |
Country | project |
Description | project |
Domain | project |
IPAbuse | project |
IPv4 | project |
IPv6 | project |
IndicatorID | project |
ProductName | project |
ProviderName | project |
RecommendedActions | project |
Roles | project |
SecurityVendors | project |
Sources | project |
Tags | project |
ThreatActors | project |
ThreatType | project |
TimeGenerated | project |
URL | project |
created | project |
modified | project |
name | project |
valid_from | project |
Dataverse - Export activity from terminated or notified employee
#This query identifies Dataverse export activity triggered by terminated, or employees about to leave the organization. This analytics rule uses the TerminatedEmployees watchlist template.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Exfiltration |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Microsoft 365 | any: Dataverse and model-driven apps audit (catch-all) |
Rule Body
id: 0881b209-62c9-4b15-9f9a-e0c1d1b1eb7b
kind: Scheduled
name: Dataverse - Export activity from terminated or notified employee
description: This query identifies Dataverse export activity triggered by terminated,
or employees about to leave the organization. This analytics rule uses the TerminatedEmployees
watchlist template.
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: Dataverse
dataTypes:
- DataverseActivity
queryFrequency: 1h
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- Exfiltration
relevantTechniques:
- T1567
- T1048
query: |
// Set a time period before employee terminatation date to search for export events
let termination_watch_period = 7d;
let query_frequency = 1h;
let exportEvents = dynamic(['ExportToExcel', 'ExportPdfDocument', 'ExportWordDocument', 'ExecutePowerBISql']);
MSBizAppsTerminatedEmployees
| where (UserState =~ "Terminated") or (UserState =~ "Notified" and TerminationDate <= startofday(now()) + termination_watch_period)
| join kind=inner (DataverseActivity
| where TimeGenerated >= ago(query_frequency)
| where Message in (exportEvents))
on $left.UserPrincipalName == $right.UserId
| summarize
FirstEvent = min(TimeGenerated),
LastEvent = max(TimeGenerated),
Event = make_set(Message, 4)
by UserId, InstanceUrl, ClientIp, UserState
| extend
CloudAppId = int(32780),
AccountName = tostring(split(UserId, '@')[0]),
UPNSuffix = tostring(split(UserId, '@')[1])
| project
FirstEvent,
LastEvent,
UserId,
ClientIp,
UserState,
InstanceUrl,
CloudAppId,
AccountName,
UPNSuffix
eventGroupingSettings:
aggregationKind: AlertPerResult
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: AccountName
- identifier: UPNSuffix
columnName: UPNSuffix
- entityType: IP
fieldMappings:
- identifier: Address
columnName: ClientIp
- entityType: CloudApplication
fieldMappings:
- identifier: AppId
columnName: CloudAppId
- identifier: InstanceName
columnName: InstanceUrl
alertDetailsOverride:
alertDisplayNameFormat: 'Dataverse - Export events detected from a terminated employee
in {{InstanceUrl}} '
alertDescriptionFormat: Export events where employee state found matching {{UserState}}
found in {{InstanceUrl}}.
version: 3.2.0
Stages and Predicates
Parameters
let termination_watch_period = 7d;
let query_frequency = 1h;
let exportEvents = dynamic(['ExportToExcel', 'ExportPdfDocument', 'ExportWordDocument', 'ExecutePowerBISql']);
Stage 1: source
MSBizAppsTerminatedEmployees
Stage 2: where
| where (UserState =~ "Terminated") or (UserState =~ "Notified" and TerminationDate <= startofday(now()) + termination_watch_period)
Stage 3: join
| join kind=inner (DataverseActivity
| where TimeGenerated >= ago(query_frequency)
| where Message in (exportEvents))
on $left.UserPrincipalName == $right.UserId
Stage 4: summarize
summarize Event, FirstEvent, LastEvent by UserId, InstanceUrl, ClientIp, UserState
Stage 5: extend
extend AccountName, CloudAppId, UPNSuffix
Stage 6: project
project AccountName, ClientIp, CloudAppId, FirstEvent, InstanceUrl, LastEvent, UPNSuffix, UserId, UserState
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Message | in |
| field:"Message" kind:in |
UserState | eq |
| field:"UserState" kind:eq |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AccountName | project |
ClientIp | project |
CloudAppId | project |
FirstEvent | project |
InstanceUrl | project |
LastEvent | project |
UPNSuffix | project |
UserId | project |
UserState | project |
Dev-0270 WMIC Discovery
#The query below identifies dllhost.exe using WMIC to discover additional hosts and associated domains in the environment.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Discovery |
Telemetry Coverage
Rule Body
id: 6b652b4f-9810-4eec-9027-7aa88ce4db23
name: Dev-0270 WMIC Discovery
description: |
'The query below identifies dllhost.exe using WMIC to discover additional hosts and associated domains in the environment.'
severity: High
requiredDataConnectors:
- connectorId: SecurityEvents
dataTypes:
- SecurityEvent
- connectorId: WindowsSecurityEvents
dataTypes:
- SecurityEvent
- connectorId: MicrosoftThreatProtection
dataTypes:
- DeviceProcessEvents
queryFrequency: 6h
queryPeriod: 6h
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
- Discovery
relevantTechniques:
- T1482
tags:
- Dev-0270
query: |
(union isfuzzy=true
(SecurityEvent
| where EventID==4688
| where CommandLine has "wmic computersystem get domain" and ParentProcessName has "dllhost.exe"
| project TimeGenerated, Computer, Account, AccountDomain, ProcessName, ProcessNameFullPath = NewProcessName, EventID, Activity, CommandLine, EventSourceName, Type
),
(DeviceProcessEvents
| where ProcessCommandLine has "wmic computersystem get domain" and InitiatingProcessFileName =~ "dllhost.exe" and InitiatingProcessCommandLine has "dllhost.exe"
| extend Account = strcat(InitiatingProcessAccountDomain, @'\', InitiatingProcessAccountName), Computer = DeviceName
)
)
| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
| extend AccountName = tostring(split(Account, @'\')[1]), AccountNTDomain = tostring(split(Account, @'\')[0])
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: Account
- identifier: Name
columnName: AccountName
- identifier: NTDomain
columnName: AccountNTDomain
- entityType: Host
fieldMappings:
- identifier: FullName
columnName: Computer
- identifier: HostName
columnName: HostName
- identifier: DnsDomain
columnName: HostNameDomain
version: 1.0.4
kind: Scheduled
Stages and Predicates
Union isfuzzy=true (2 Sources)
Each leg below queries one source; the rule matches if any leg does. Sources: SecurityEvent, DeviceProcessEvents
Leg 1: SecurityEvent
SecurityEvent
| where EventID==4688
| where CommandLine has "wmic computersystem get domain" and ParentProcessName has "dllhost.exe"
| project TimeGenerated, Computer, Account, AccountDomain, ProcessName, ProcessNameFullPath = NewProcessName, EventID, Activity, CommandLine, EventSourceName, Type
Leg 2: DeviceProcessEvents
DeviceProcessEvents
| where ProcessCommandLine has "wmic computersystem get domain" and InitiatingProcessFileName =~ "dllhost.exe" and InitiatingProcessCommandLine has "dllhost.exe"
| extend Account = strcat(InitiatingProcessAccountDomain, @'\', InitiatingProcessAccountName), Computer = DeviceName
Applied to the Combined Result
| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
| extend AccountName = tostring(split(Account, @'\')[1]), AccountNTDomain = tostring(split(Account, @'\')[0])
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
CommandLine | match |
| field:"CommandLine" kind:match value:"wmic computersystem get domain" |
EventID | eq |
| field:"EventID" kind:eq value:"4688" |
InitiatingProcessCommandLine | match |
| field:"ParentCommandLine" kind:match value:"dllhost.exe" |
InitiatingProcessFileName | eq |
| field:"parent_process_basename" kind:eq value:"dllhost.exe" |
ParentProcessName | match |
| field:"ParentImage" kind:match value:"dllhost.exe" |
ProcessCommandLine | match |
| field:"CommandLine" kind:match value:"wmic computersystem get domain" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Account | extend |
AccountDomain | project |
Activity | project |
CommandLine | project |
Computer | extend |
EventID | project |
EventSourceName | project |
ProcessName | project |
ProcessNameFullPath | project |
TimeGenerated | project |
Type | project |
DomainIndex | extend |
HostName | extend |
HostNameDomain | extend |
AccountNTDomain | extend |
AccountName | extend |
Four-Eyes Authorization Request Expired
#Detects when a four-eyes authorization request is expired.
Rule Body
id: a641dbe1-9590-46f8-a6d7-b5f24db6e126
name: Four-Eyes Authorization Request Expired
description: Detects when a four-eyes authorization request is expired.
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: Syslog
dataTypes:
- Syslog
- connectorId: SyslogAma
dataTypes:
- Syslog
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
eventGroupingSettings:
aggregationKind: AlertPerResult
tactics: []
relevantTechniques: []
query: "Veeam_GetSecurityEvents\n| where instanceId == 42405\n| extend Operation =\
\ extract(\"Operation=\\\"([^\\\"]*)\\\"\", 1, SyslogMessage)\n| project\n Date\
\ = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),\n DataSource = original_host,\n\
\ EventId = instanceId,\n [\"Operation\"] = Operation,\n MessageDetails\
\ = Description,\n Severity = SeverityDescription"
version: 1.0.1
kind: Scheduled
customDetails:
Date: Date
VbrHostName: DataSource
EventId: EventId
MessageDetails: MessageDetails
Severity: Severity
Stages and Predicates
Stage 1: source
Veeam_GetSecurityEvents
Stage 2: where
| where instanceId == 42405
Stage 3: extend
| extend Operation = extract("Operation=\"([^\"]*)\"", 1, SyslogMessage)
Stage 4: project
| project
Date = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),
DataSource = original_host,
EventId = instanceId,
["Operation"] = Operation,
MessageDetails = Description,
Severity = SeverityDescription
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
instanceId | eq |
| field:"instanceId" kind:eq value:"42405" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DataSource | project |
Date | project |
EventId | project |
MessageDetails | project |
Operation | project |
Severity | project |
Gain Code Execution on ADFS Server via Remote WMI Execution
#This query detects instances where an attacker has gained the ability to execute code on an ADFS Server through remote WMI Execution. In order to use this query you need to be collecting Sysmon EventIDs 19, 20, and 21. If you do not have Sysmon data in your workspace this query will raise an error stating: Failed to resolve scalar expression named "[@Name]" For more on how WMI was used in Solorigate see https://www.microsoft.com/security/blog/2021/01/20/deep-dive-into-the-solorigate-second-stage-activation-from-sunburst-to-teardrop-and-raindrop/. The query contains some features from the following detections to look for potentially malicious ADFS activity. See them for more details. - ADFS DKM Master Key Export: https://github.com/Azure/Azure-Sentinel/blob/master/Detections/MultipleDataSources/ADFS-DKM-MasterKey-Export.yaml
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Lateral Movement |
Telemetry Coverage
Rule Body
id: 0bd65651-1404-438b-8f63-eecddcec87b4
name: Gain Code Execution on ADFS Server via Remote WMI Execution
description: |
'This query detects instances where an attacker has gained the ability to execute code on an ADFS Server through remote WMI Execution.
In order to use this query you need to be collecting Sysmon EventIDs 19, 20, and 21.
If you do not have Sysmon data in your workspace this query will raise an error stating:
Failed to resolve scalar expression named "[@Name]"
For more on how WMI was used in Solorigate see https://www.microsoft.com/security/blog/2021/01/20/deep-dive-into-the-solorigate-second-stage-activation-from-sunburst-to-teardrop-and-raindrop/.
The query contains some features from the following detections to look for potentially malicious ADFS activity. See them for more details.
- ADFS DKM Master Key Export: https://github.com/Azure/Azure-Sentinel/blob/master/Detections/MultipleDataSources/ADFS-DKM-MasterKey-Export.yaml'
severity: Medium
requiredDataConnectors:
- connectorId: SecurityEvents
dataTypes:
- SecurityEvent
- connectorId: WindowsSecurityEvents
dataTypes:
- SecurityEvents
- connectorId: WindowsForwardedEvents
dataTypes:
- WindowsEvent
queryFrequency: 1d
queryPeriod: 7d
triggerOperator: gt
triggerThreshold: 0
tactics:
- LateralMovement
relevantTechniques:
- T1210
tags:
- Solorigate
- NOBELIUM
query: |
let timeframe = 1d;
// Adjust for a longer timeframe for identifying ADFS Servers
let lookback = 6d;
// Identify ADFS Servers
let ADFS_Servers = ( union isfuzzy=true
( Event
| where TimeGenerated > ago(timeframe+lookback)
| where Source == "Microsoft-Windows-Sysmon"
| where EventID == 1
| extend EventData = parse_xml(EventData).DataItem.EventData.Data
| mv-expand bagexpansion=array EventData
| evaluate bag_unpack(EventData)
| extend Key=tostring(['@Name']), Value=['#text']
| evaluate pivot(Key, any(Value), TimeGenerated, Source, EventLog, Computer, EventLevel, EventLevelName, UserName, RenderedDescription, MG, ManagementGroupName, Type, _ResourceId)
| extend process = split(Image, '\\', -1)[-1]
| where process =~ "Microsoft.IdentityServer.ServiceHost.exe"
| distinct Computer
),
( SecurityEvent
| where TimeGenerated > ago(timeframe+lookback)
| where EventID == 4688 and SubjectLogonId != "0x3e4"
| where ProcessName has "Microsoft.IdentityServer.ServiceHost.exe"
| distinct Computer
),
(WindowsEvent
| where TimeGenerated > ago(timeframe+lookback)
| where EventID == 4688 and EventData has "0x3e4" and EventData has "Microsoft.IdentityServer.ServiceHost.exe"
| extend SubjectLogonId = tostring(EventData.SubjectLogonId)
| where SubjectLogonId != "0x3e4"
| extend ProcessName = tostring(EventData.ProcessName)
| where ProcessName has "Microsoft.IdentityServer.ServiceHost.exe"
| distinct Computer
)
| distinct Computer);
(union isfuzzy=true
(
SecurityEvent
| where EventID == 4688
| where TimeGenerated > ago(timeframe)
| where Computer in~ (ADFS_Servers)
| where ParentProcessName has 'wmiprvse.exe'
// Looking for rundll32.exe is based on intel from the blog linked in the description
// This can be commented out or altered to filter out known internal uses
| where CommandLine has_any ('rundll32')
| project TimeGenerated, TargetAccount, CommandLine, Computer, Account, TargetLogonId
| extend timestamp = TimeGenerated
| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
| extend AccountName = tostring(split(Account, "\\")[0]), AccountNTDomain = tostring(split(Account, "\\")[1])
// Search for recent logons to identify lateral movement
| join kind= inner
(SecurityEvent
| where TimeGenerated > ago(timeframe)
| where EventID == 4624 and LogonType == 3
| where Account !endswith "$"
| project TargetLogonId
) on TargetLogonId
),
(
WindowsEvent
| where EventID == 4688
| where TimeGenerated > ago(timeframe)
| where Computer in~ (ADFS_Servers)
| where EventData has 'wmiprvse.exe' and EventData has_any ('rundll32')
| extend ParentProcessName = tostring(EventData.ParentProcessName)
| where ParentProcessName has 'wmiprvse.exe'
// Looking for rundll32.exe is based on intel from the blog linked in the description
// This can be commented out or altered to filter out known internal uses
| extend CommandLine = tostring(EventData.CommandLine)
| where CommandLine has_any ('rundll32')
| extend TargetAccount = strcat(EventData.TargetDomainName,"\\", EventData.TargetUserName)
| extend Account = strcat(EventData.TargetDomainName,"\\", EventData.TargetUserName)
| extend TargetLogonId = tostring(EventData.TargetLogonId)
| project TimeGenerated, TargetAccount, CommandLine, Computer, Account, TargetLogonId
| extend timestamp = TimeGenerated
| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
| extend AccountName = tostring(split(Account, "\\")[0]), AccountNTDomain = tostring(split(Account, "\\")[1])
// Search for recent logons to identify lateral movement
| join kind= inner
(WindowsEvent
| where TimeGenerated > ago(timeframe)
| where EventID == 4624
| extend LogonType = tostring(EventData.LogonType)
| where LogonType == 3
| extend Account = strcat(EventData.TargetDomainName,"\\", EventData.TargetUserName)
| where Account !endswith "$"
| extend TargetLogonId = tostring(EventData.TargetLogonId)
| project TargetLogonId
) on TargetLogonId
),
(
Event
| where TimeGenerated > ago(timeframe)
| where Source == "Microsoft-Windows-Sysmon"
// Check for WMI Events
| where Computer in~ (ADFS_Servers) and EventID in (19, 20, 21)
| extend EventData = parse_xml(EventData).DataItem.EventData.Data
| mv-expand bagexpansion=array EventData
| evaluate bag_unpack(EventData)
| extend Key=tostring(['@Name']), Value=['#text']
| evaluate pivot(Key, any(Value), TimeGenerated, Source, EventLog, Computer, EventLevel, EventLevelName, UserName, RenderedDescription, MG, ManagementGroupName, Type, _ResourceId)
| project TimeGenerated, EventType, Image, Computer, UserName
| extend timestamp = TimeGenerated
| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
| extend AccountName = tostring(split(UserName, "\\")[0]), AccountNTDomain = tostring(split(UserName, "\\")[1])
)
)
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: UserName
- identifier: Name
columnName: AccountName
- identifier: NTDomain
columnName: AccountNTDomain
- entityType: Host
fieldMappings:
- identifier: FullName
columnName: Computer
- identifier: HostName
columnName: HostName
- identifier: NTDomain
columnName: HostNameDomain
version: 1.1.4
kind: Scheduled
metadata:
source:
kind: Community
author:
name: Microsoft Security Research
support:
tier: Community
categories:
domains: [ "Security - Others", "Identity" ]
Stages and Predicates
Parameters
let timeframe = 1d;
let lookback = 6d;
Let Binding: ADFS_Servers
let ADFS_Servers = ( union isfuzzy=true
( Event
| where TimeGenerated > ago(timeframe+lookback)
| where Source == "Microsoft-Windows-Sysmon"
| where EventID == 1
| extend EventData = parse_xml(EventData).DataItem.EventData.Data
| mv-expand bagexpansion=array EventData
| evaluate bag_unpack(EventData)
| extend Key=tostring(['@Name']), Value=['#text']
| evaluate pivot(Key, any(Value), TimeGenerated, Source, EventLog, Computer, EventLevel, EventLevelName, UserName, RenderedDescription, MG, ManagementGroupName, Type, _ResourceId)
| extend process = split(Image, '\\', -1)[-1]
| where process =~ "Microsoft.IdentityServer.ServiceHost.exe"
| distinct Computer
),
( SecurityEvent
| where TimeGenerated > ago(timeframe+lookback)
| where EventID == 4688 and SubjectLogonId != "0x3e4"
| where ProcessName has "Microsoft.IdentityServer.ServiceHost.exe"
| distinct Computer
),
(WindowsEvent
| where TimeGenerated > ago(timeframe+lookback)
| where EventID == 4688 and EventData has "0x3e4" and EventData has "Microsoft.IdentityServer.ServiceHost.exe"
| extend SubjectLogonId = tostring(EventData.SubjectLogonId)
| where SubjectLogonId != "0x3e4"
| extend ProcessName = tostring(EventData.ProcessName)
| where ProcessName has "Microsoft.IdentityServer.ServiceHost.exe"
| distinct Computer
)
| distinct Computer);
Union isfuzzy=true (3 Sources)
Each leg below queries one source; the rule matches if any leg does. Sources: SecurityEvent, WindowsEvent, Event
Leg 1: SecurityEvent
SecurityEvent
| where EventID == 4688
| where TimeGenerated > ago(timeframe)
| where Computer in~ (ADFS_Servers)
| where ParentProcessName has 'wmiprvse.exe'
| where CommandLine has_any ('rundll32')
| project TimeGenerated, TargetAccount, CommandLine, Computer, Account, TargetLogonId
| extend timestamp = TimeGenerated
| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
| extend AccountName = tostring(split(Account, "\\")[0]), AccountNTDomain = tostring(split(Account, "\\")[1])
| join kind= inner
(SecurityEvent
| where TimeGenerated > ago(timeframe)
| where EventID == 4624 and LogonType == 3
| where Account !endswith "$"
| project TargetLogonId
) on TargetLogonId
Leg 2: WindowsEvent
WindowsEvent
| where EventID == 4688
| where TimeGenerated > ago(timeframe)
| where Computer in~ (ADFS_Servers)
| where EventData has 'wmiprvse.exe' and EventData has_any ('rundll32')
| extend ParentProcessName = tostring(EventData.ParentProcessName)
| where ParentProcessName has 'wmiprvse.exe'
| extend CommandLine = tostring(EventData.CommandLine)
| where CommandLine has_any ('rundll32')
| extend TargetAccount = strcat(EventData.TargetDomainName,"\\", EventData.TargetUserName)
| extend Account = strcat(EventData.TargetDomainName,"\\", EventData.TargetUserName)
| extend TargetLogonId = tostring(EventData.TargetLogonId)
| project TimeGenerated, TargetAccount, CommandLine, Computer, Account, TargetLogonId
| extend timestamp = TimeGenerated
| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
| extend AccountName = tostring(split(Account, "\\")[0]), AccountNTDomain = tostring(split(Account, "\\")[1])
| join kind= inner
(WindowsEvent
| where TimeGenerated > ago(timeframe)
| where EventID == 4624
| extend LogonType = tostring(EventData.LogonType)
| where LogonType == 3
| extend Account = strcat(EventData.TargetDomainName,"\\", EventData.TargetUserName)
| where Account !endswith "$"
| extend TargetLogonId = tostring(EventData.TargetLogonId)
| project TargetLogonId
) on TargetLogonId
Leg 3: Event
Event
| where TimeGenerated > ago(timeframe)
| where Source == "Microsoft-Windows-Sysmon"
| where Computer in~ (ADFS_Servers) and EventID in (19, 20, 21)
| extend EventData = parse_xml(EventData).DataItem.EventData.Data
| mv-expand bagexpansion=array EventData
| evaluate bag_unpack(EventData)
| extend Key=tostring(['@Name']), Value=['#text']
| evaluate pivot(Key, any(Value), TimeGenerated, Source, EventLog, Computer, EventLevel, EventLevelName, UserName, RenderedDescription, MG, ManagementGroupName, Type, _ResourceId)
| project TimeGenerated, EventType, Image, Computer, UserName
| extend timestamp = TimeGenerated
| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
| extend AccountName = tostring(split(UserName, "\\")[0]), AccountNTDomain = tostring(split(UserName, "\\")[1])
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
CommandLine | match |
| field:"CommandLine" kind:match value:"rundll32" |
Computer | in |
| field:"Computer" kind:in value:"ADFS_Servers" |
EventData | match |
| field:"EventData" kind:match |
EventID | eq |
| field:"EventID" kind:eq |
EventID | in |
| field:"EventID" kind:in |
LogonType | eq |
| field:"LogonType" kind:eq value:"3" |
ParentProcessName | match |
| field:"ParentProcessName" kind:match value:"wmiprvse.exe" |
Source | eq |
| field:"Source" kind:eq value:"Microsoft-Windows-Sysmon" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
Account | ends_with | $ | excludes:Account field:"Account" value:"$" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Computer | project |
EventType | project |
Image | project |
TimeGenerated | project |
UserName | project |
timestamp | extend |
DomainIndex | extend |
HostName | extend |
HostNameDomain | extend |
AccountNTDomain | extend |
AccountName | extend |
McAfee ePO - Update failed
#Detects when update failed event occurs on agent.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Stealth | |
| Defense Impairment |
Telemetry Coverage
Rule Body
id: 4f0c91c3-1690-48f0-b538-4282dd5417a4
name: McAfee ePO - Update failed
description: |
'Detects when update failed event occurs on agent.'
severity: Medium
requiredDataConnectors:
- connectorId: SyslogAma
datatypes:
- Syslog
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
- DefenseEvasion
relevantTechniques:
- T1562
- T1070
query: |
McAfeeEPOEvent
| where EventId in ('2402', '1119', '1123')
| extend IPCustomEntity = DvcIpAddr, HostCustomEntity = DvcHostname
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPCustomEntity
- entityType: Host
fieldMappings:
- identifier: FullName
columnName: HostCustomEntity
version: 1.0.2
kind: Scheduled
Stages and Predicates
Stage 1: source
McAfeeEPOEvent
Stage 2: where
| where EventId in ('2402', '1119', '1123')
Stage 3: extend
| extend IPCustomEntity = DvcIpAddr, HostCustomEntity = DvcHostname
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
EventId | in |
| field:"EventId" kind:in |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
HostCustomEntity | extend |
IPCustomEntity | extend |
NGINX - Request to sensitive files
#'Detects request to sensitive files.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Rule Body
id: 2141ef6c-d158-4d44-b739-b145a4c21947
name: NGINX - Request to sensitive files
description: |
'Detects request to sensitive files.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: CustomLogsAma
dataTypes:
- NGINX_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1189
query: |
let forbidden_files = dynamic(['shadow', 'passwd', 'id_rsa']);
NGINXHTTPServer
| extend File = extract(@"(.*\/)?(.*)", 2, tostring(UrlOriginal))
| where File in (forbidden_files)
| extend FileCustomEntity = File, UrlCustomEntity = UrlOriginal
entityMappings:
- entityType: File
fieldMappings:
- identifier: Name
columnName: FileCustomEntity
- entityType: URL
fieldMappings:
- identifier: Url
columnName: UrlCustomEntity
version: 1.0.2
kind: Scheduled
Stages and Predicates
Parameters
let forbidden_files = dynamic(['shadow', 'passwd', 'id_rsa']);
Stage 1: source
NGINXHTTPServer
Stage 2: extend
| extend File = extract(@"(.*\/)?(.*)", 2, tostring(UrlOriginal))
Stage 3: where
| where File in (forbidden_files)
Stage 4: extend
| extend FileCustomEntity = File, UrlCustomEntity = UrlOriginal
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
File | in |
| field:"File" kind:in |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
File | extend |
FileCustomEntity | extend |
UrlCustomEntity | extend |
Oracle - Multiple client errors from single IP
#'Detects multiple client errors from one source in short timeframe'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Rule Body
id: 41775080-5740-11ec-bf63-0242ac130002
name: Oracle - Multiple client errors from single IP
description: |
'Detects multiple client errors from one source in short timeframe'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: CustomLogsAma
dataTypes:
- OracleWebLogicServer_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1190
- T1133
query: |
let threshold = 100;
OracleWebLogicServerEvent
| where HttpStatusCode >= 400 and HttpStatusCode <= 499
| summarize MultipleClientErrors = count() by SrcIpAddr, bin(TimeGenerated, 5m)
| where MultipleClientErrors > threshold
| extend IPCustomEntity = SrcIpAddr
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPCustomEntity
version: 1.0.3
kind: Scheduled
Stages and Predicates
Parameters
let threshold = 100;
Stage 1: source
OracleWebLogicServerEvent
Stage 2: where
| where HttpStatusCode >= 400 and HttpStatusCode <= 499
Stage 3: summarize
| summarize MultipleClientErrors = count() by SrcIpAddr, bin(TimeGenerated, 5m)
Stage 4: where
| where MultipleClientErrors > threshold
Stage 5: extend
| extend IPCustomEntity = SrcIpAddr
Indicators
These rows show field, operator, and value matches.
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
MultipleClientErrors | summarize |
SrcIpAddr | summarize |
IPCustomEntity | extend |
Oracle - Put file and get file from same IP address
#'Detects put or get files from one source in short timeframe'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Rule Body
id: 033e98d2-5740-11ec-bf63-0242ac130002
name: Oracle - Put file and get file from same IP address
description: |
'Detects put or get files from one source in short timeframe'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: CustomLogsAma
dataTypes:
- OracleWebLogicServer_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1190
- T1133
query: |
let p = OracleWebLogicServerEvent
| where HttpRequestMethod in~ ('POST', 'PUT')
| sort by EventStartTime asc
| summarize post_time=min(EventStartTime) by SrcIpAddr, tostring(UrlOriginal);
OracleWebLogicServerEvent
| where HttpRequestMethod =~ 'GET'
| sort by EventStartTime asc
| summarize get_time=min(EventStartTime) by SrcIpAddr, tostring(UrlOriginal)
| join kind=innerunique (p) on UrlOriginal, SrcIpAddr
| extend second = datetime_diff('second',get_time,post_time)
| where second between (1 .. 300)
| project second, post_time, get_time, SrcIpAddr, UrlOriginal
| extend IPCustomEntity = SrcIpAddr, UrlCustomEntity = tostring(UrlOriginal)
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPCustomEntity
- entityType: URL
fieldMappings:
- identifier: Url
columnName: UrlCustomEntity
version: 1.0.2
kind: Scheduled
Stages and Predicates
Let Binding: p
let p = OracleWebLogicServerEvent
| where HttpRequestMethod in~ ('POST', 'PUT')
| sort by EventStartTime asc
| summarize post_time=min(EventStartTime) by SrcIpAddr, tostring(UrlOriginal);
Stage 1: source
OracleWebLogicServerEvent
Stage 2: where
| where HttpRequestMethod =~ 'GET'
Stage 3: sort
| sort by EventStartTime asc
Stage 4: summarize
| summarize get_time=min(EventStartTime) by SrcIpAddr, tostring(UrlOriginal)
Stage 5: join
| join kind=innerunique (p) on UrlOriginal, SrcIpAddr
Stage 6: extend
extend second
Stage 7: where
where second >= 1 and second <= 300
Stage 8: project
project SrcIpAddr, UrlOriginal, get_time, post_time, second
Stage 9: extend
extend IPCustomEntity, UrlCustomEntity
Indicators
These rows show field, operator, and value matches.
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
SrcIpAddr | project |
UrlOriginal | project |
get_time | project |
post_time | project |
second | project |
IPCustomEntity | extend |
UrlCustomEntity | extend |
Protection Group Settings Updated
#Detects when protection group settings are updated in Veeam Backup & Replication. This might indicate configuration changes that require review.
Rule Body
id: e9b9efa5-4e19-4716-840b-51ef2feeaad1
name: Protection Group Settings Updated
description: Detects when protection group settings are updated in Veeam Backup &
Replication. This might indicate configuration changes that require review.
severity: Informational
status: Available
requiredDataConnectors:
- connectorId: Syslog
dataTypes:
- Syslog
- connectorId: SyslogAma
dataTypes:
- Syslog
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
eventGroupingSettings:
aggregationKind: AlertPerResult
tactics: []
relevantTechniques: []
query: "Veeam_GetSecurityEvents\n| where instanceId == 29110\n| project\n Date\
\ = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),\n DataSource = original_host,\n\
\ EventId = instanceId,\n UserName = user,\n MessageDetails = Description,\n\
\ Severity = SeverityDescription"
version: 1.0.1
kind: Scheduled
customDetails:
Date: Date
VbrHostName: DataSource
EventId: EventId
MessageDetails: MessageDetails
Severity: Severity
Stages and Predicates
Stage 1: source
Veeam_GetSecurityEvents
Stage 2: where
| where instanceId == 29110
Stage 3: project
| project
Date = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),
DataSource = original_host,
EventId = instanceId,
UserName = user,
MessageDetails = Description,
Severity = SeverityDescription
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
instanceId | eq |
| field:"instanceId" kind:eq value:"29110" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DataSource | project |
Date | project |
EventId | project |
MessageDetails | project |
Severity | project |
UserName | project |
Red Sift - New email with URL from previously unseen source
#'Detects email forensics events that contain one or more URLs where the sender is using a source IP address not seen in the previous 14 days, which may indicate suspicious infrastructure changes or phishing activity.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Rule Body
id: 6084dfd8-830b-4839-9a9c-5f08cc984729
name: Red Sift - New email with URL from previously unseen source
description: |
'Detects email forensics events that contain one or more URLs where the sender is using a source IP address not seen in the previous 14 days, which may indicate suspicious infrastructure changes or phishing activity.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: RedSiftPush
dataTypes:
- RedSiftEmailForensics_CL
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1566
query: |
let lookback = 14d;
let recentWindow = 1h;
let historicalSources = RedSiftEmailForensics_CL
| extend
EmailFrom = tostring(column_ifexists("EmailFrom", "")),
SrcIp = tostring(column_ifexists("SrcIp", ""))
| where TimeGenerated between (ago(lookback) .. ago(recentWindow))
| where isnotempty(EmailFrom) and isnotempty(SrcIp)
| summarize by EmailFrom, SrcIp;
RedSiftEmailForensics_CL
| extend
EmailFrom = tostring(column_ifexists("EmailFrom", "")),
EmailSubject = tostring(column_ifexists("EmailSubject", "")),
EmailReturnPath = tostring(column_ifexists("EmailReturnPath", "")),
EmailMessageUid = tostring(column_ifexists("EmailMessageUid", "")),
SrcIp = tostring(column_ifexists("SrcIp", "")),
DstHostname = tostring(column_ifexists("DstHostname", "")),
Severity = tostring(column_ifexists("Severity", "")),
Message = tostring(column_ifexists("Message", "")),
CorrelationUid = tostring(column_ifexists("CorrelationUid", "")),
EmailUrls = todynamic(column_ifexists("EmailUrls", "[]"))
| where TimeGenerated >= ago(recentWindow)
| where isnotempty(EmailFrom) and isnotempty(SrcIp)
| extend UrlCount = array_length(EmailUrls)
| where UrlCount > 0
| mv-apply Url = EmailUrls on (
summarize UrlSet = make_set(tostring(Url.url_string), 50)
)
| extend UrlList = strcat_array(UrlSet, ", ")
| join kind=leftanti (historicalSources) on EmailFrom, SrcIp
| project
TimeGenerated,
EmailFrom,
EmailSubject,
EmailReturnPath,
EmailMessageUid,
SrcIp,
DstHostname,
UrlCount,
UrlList,
Severity,
Message,
CorrelationUid
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: EmailFrom
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SrcIp
- entityType: DNS
fieldMappings:
- identifier: DomainName
columnName: DstHostname
customDetails:
EmailSubject: EmailSubject
ReturnPath: EmailReturnPath
UrlCount: UrlCount
UrlList: UrlList
CorrelationUid: CorrelationUid
alertDetailsOverride:
alertDisplayNameFormat: "RedSift - New URL-bearing email source for {{EmailFrom}}"
alertDescriptionFormat: "Email from {{EmailFrom}} contains {{UrlCount}} URL(s) and originated from previously unseen source IP {{SrcIp}}."
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: true
reopenClosedIncident: false
lookbackDuration: P1D
matchingMethod: Selected
groupByEntities:
- Account
- IP
groupByCustomDetails:
- EmailSubject
eventGroupingSettings:
aggregationKind: AlertPerResult
suppressionEnabled: false
suppressionDuration: PT1H
version: 1.0.0
kind: Scheduled
Stages and Predicates
Parameters
let lookback = 14d;
let recentWindow = 1h;
Let Binding: historicalSources
let historicalSources = RedSiftEmailForensics_CL
| extend
EmailFrom = tostring(column_ifexists("EmailFrom", "")),
SrcIp = tostring(column_ifexists("SrcIp", ""))
| where TimeGenerated between (ago(lookback) .. ago(recentWindow))
| where isnotempty(EmailFrom) and isnotempty(SrcIp)
| summarize by EmailFrom, SrcIp;
Stage 1: source
RedSiftEmailForensics_CL
Stage 2: extend
| extend
EmailFrom = tostring(column_ifexists("EmailFrom", "")),
EmailSubject = tostring(column_ifexists("EmailSubject", "")),
EmailReturnPath = tostring(column_ifexists("EmailReturnPath", "")),
EmailMessageUid = tostring(column_ifexists("EmailMessageUid", "")),
SrcIp = tostring(column_ifexists("SrcIp", "")),
DstHostname = tostring(column_ifexists("DstHostname", "")),
Severity = tostring(column_ifexists("Severity", "")),
Message = tostring(column_ifexists("Message", "")),
CorrelationUid = tostring(column_ifexists("CorrelationUid", "")),
EmailUrls = todynamic(column_ifexists("EmailUrls", "[]"))
Stage 3: where
| where TimeGenerated >= ago(recentWindow)
Stage 4: where
| where isnotempty(EmailFrom) and isnotempty(SrcIp)
Stage 5: extend
| extend UrlCount = array_length(EmailUrls)
Stage 6: where
| where UrlCount > 0
Stage 7: kusto:mv-apply
| mv-apply Url = EmailUrls on (
summarize UrlSet = make_set(tostring(Url.url_string), 50)
)
Stage 8: extend
| extend UrlList = strcat_array(UrlSet, ", ")
Stage 9: join (negated)
join kind=leftanti (historicalSources) on EmailFrom, SrcIp
Stage 10: project
project CorrelationUid, DstHostname, EmailFrom, EmailMessageUid, EmailReturnPath, EmailSubject, Message, Severity, SrcIp, TimeGenerated, UrlCount, UrlList
Stage 11: summarize aggregation inside the mv-apply branch
summarize
Stage 12: summarize aggregation inside the join branch
summarize by EmailFrom, SrcIp
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
EmailFrom | is_not_null | field:"EmailFrom" kind:is_not_null | |
SrcIp | is_not_null | field:"SrcIp" kind:is_not_null | |
UrlCount | gt |
| field:"UrlCount" kind:gt value:"0" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
EmailFrom | is_not_null | excludes:EmailFrom | |
SrcIp | is_not_null | excludes:SrcIp |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
EmailFrom | summarize |
SrcIp | summarize |
Request for single resource on domain
#'This will look for connections to a domain where only a single file is requested, this is unusual as most modern web applications require additional recources. This type of activity is often assocaited with malware beaconing or tracking URL's delivered in emails. Developed for Zscaler but applicable to any outbound web logging.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Command & Control |
Rule Body
id: 4d500e6d-c984-43a3-9f39-7edec8dcc04d
name: Request for single resource on domain
description: |
'This will look for connections to a domain where only a single file is requested, this is unusual as most modern web applications require additional recources. This type of activity is often assocaited with malware beaconing or tracking URL's delivered in emails. Developed for Zscaler but applicable to any outbound web logging.'
severity: Low
status: Available
requiredDataConnectors:
- connectorId: CefAma
dataTypes:
- CommonSecurityLog
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- CommandAndControl
relevantTechniques:
- T1102
- T1071
query: |
let scriptExtensions = dynamic([".php", ".aspx", ".asp", ".cfml"]);
//The number of URI's seen to be suspicious, higher = less likely to be suspicious
let uriThreshold = 1;
CommonSecurityLog
// Only look at connections that were allowed through the web proxy
| where DeviceVendor =~ "Zscaler" and DeviceAction =~ "Allowed"
// Only look where some data was exchanged.
| where SentBytes > 0 and ReceivedBytes > 0
// Extract the Domain
| extend Domain = iff(countof(DestinationHostName,'.') >= 2, strcat(split(DestinationHostName,'.')[-2], '.',split(DestinationHostName,'.')[-1]), DestinationHostName)
| extend GetData=iff(RequestURL == "?", 1, 0)
| summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated), makelist(RequestURL), makelist(DestinationIP), makelist(SourceIP), numOfConnections = count(), make_set(RequestMethod), max(GetData), max(RequestContext) by Domain
// Determine the number of URIs that have been visited for the domain
| extend destinationURI = arraylength(list_RequestURL)
| where destinationURI <= uriThreshold
| where tostring(list_RequestURL) has_any(scriptExtensions)
//Remove matches with referer
| where max_RequestContext == ""
//Keep requests where data was trasferred either in a GET with parameters or a POST
| where set_RequestMethod in~ ("POST") or max_GetData == 1
//Defeat email click tracking, may increase FN's while decreasing FP's
| where list_RequestURL !has "click" and set_RequestMethod !has "GET"
| mvexpand list_RequestURL, list_DestinationIP
| extend RequestURL = tostring(list_RequestURL), DestinationIP = tostring(list_DestinationIP), ClientIP = tostring(list_SourceIP)
//Extend custom entitites for incidents
| project-away list_RequestURL, list_DestinationIP, list_SourceIP, destinationURI, Domain, StartTimeUtc, EndTimeUtc, max_GetData, max_RequestContext
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: DestinationIP
version: 1.0.5
kind: Scheduled
Stages and Predicates
Parameters
let scriptExtensions = dynamic([".php", ".aspx", ".asp", ".cfml"]);
let uriThreshold = 1;
Stage 1: source
CommonSecurityLog
Stage 2: where
| where DeviceVendor =~ "Zscaler" and DeviceAction =~ "Allowed"
Stage 3: where
| where SentBytes > 0 and ReceivedBytes > 0
Stage 4: extend
| extend Domain = iff(countof(DestinationHostName,'.') >= 2, strcat(split(DestinationHostName,'.')[-2], '.',split(DestinationHostName,'.')[-1]), DestinationHostName)
Domain =/* macro: (countof(DestinationHostName, '.') >= 2) */strcat(split(DestinationHostName, '.')[(- 2)], '.', split(DestinationHostName, '.')[(- 1)])DestinationHostNameStage 5: extend
| extend GetData=iff(RequestURL == "?", 1, 0)
GetData =RequestURL matches regex "."10Stage 6: summarize
| summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated), makelist(RequestURL), makelist(DestinationIP), makelist(SourceIP), numOfConnections = count(), make_set(RequestMethod), max(GetData), max(RequestContext) by Domain
Stage 7: extend
| extend destinationURI = arraylength(list_RequestURL)
Stage 8: where
| where destinationURI <= uriThreshold
Stage 9: where
| where tostring(list_RequestURL) has_any(scriptExtensions)
Stage 10: where
| where max_RequestContext == ""
Stage 11: where
| where set_RequestMethod in~ ("POST") or max_GetData == 1
Stage 12: where
| where list_RequestURL !has "click" and set_RequestMethod !has "GET"
Stage 13: mv-expand
| mvexpand list_RequestURL, list_DestinationIP
Stage 14: extend
| extend RequestURL = tostring(list_RequestURL), DestinationIP = tostring(list_DestinationIP), ClientIP = tostring(list_SourceIP)
Stage 15: project-away
| project-away list_RequestURL, list_DestinationIP, list_SourceIP, destinationURI, Domain, StartTimeUtc, EndTimeUtc, max_GetData, max_RequestContext
Indicators
These rows show field, operator, and value matches.
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
list_RequestURL | match | click | excludes:list_RequestURL field:"list_RequestURL" value:"click" |
set_RequestMethod | match | GET | excludes:set_RequestMethod field:"set_RequestMethod" value:"GET" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
numOfConnections | summarize |
ClientIP | extend |
DestinationIP | extend |
RequestURL | extend |
UniFi Site Manager: Controller Connection State Change
#Identifies when a UniFi controller (Cloud Key, Dream Machine, etc.) experiences a connection state change, which may indicate a network outage, power loss, or device failure.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Command & Control | |
| Impact |
Rule Body
id: ac1efe0f-654e-264e-07be-c1b60e698343
name: "UniFi Site Manager: Controller Connection State Change"
description: |
Identifies when a UniFi controller (Cloud Key, Dream Machine, etc.) experiences a connection state change, which may indicate a network outage, power loss, or device failure.
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: UniFiSiteManagerConnectorDefinition
dataTypes:
- Unifi_SiteManager_Hosts_CL
queryFrequency: 15m
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- Impact
- CommandAndControl
relevantTechniques:
- T1489
- T1071
subTechniques:
- T1071.001
query: |
// UniFi Controller Connection State Change Detection
// Only fire when LastConnectionStateChange has changed since the prior 15-min cycle
// to avoid 4-5 alerts per actual state change (rule runs every 15m, change visible for 1h).
let prev = Unifi_SiteManager_Hosts_CL
| where TimeGenerated between (ago(30m) .. ago(15m))
| summarize arg_max(TimeGenerated, *) by Id
| project id_s = Id, prevChange = todatetime(LastConnectionStateChange);
Unifi_SiteManager_Hosts_CL
| where TimeGenerated > ago(15m)
| where isnotempty(LastConnectionStateChange)
| summarize arg_max(TimeGenerated, *) by Id
| extend
ConnectionChange = todatetime(LastConnectionStateChange),
id_s = Id
| join kind=leftouter prev on id_s
| where ConnectionChange != prevChange or isnull(prevChange)
| where ConnectionChange > ago(30m)
| extend
HostTypeDisplay = case(
HostType == "ucore", "UniFi OS Console",
HostType == "uck", "Cloud Key",
HostType == "uckp", "Cloud Key+",
HostType == "udm", "Dream Machine",
HostType == "udmpro", "Dream Machine Pro",
HostType == "udmse", "Dream Machine SE",
HostType == "self-hosted", "Self-Hosted",
HostType
)
| project
TimeGenerated,
ConnectionChangeTime = ConnectionChange,
PreviousChangeTime = prevChange,
HostId = Id,
HostType = HostType,
HostTypeDisplay,
IPAddress = IpAddress,
IsBlocked = IsBlocked,
IsOwner = Owner
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPAddress
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: true
reopenClosedIncident: false
lookbackDuration: PT4H
matchingMethod: AllEntities
kind: Scheduled
version: 1.0.1
Stages and Predicates
Let Binding: prev
let prev = Unifi_SiteManager_Hosts_CL
| where TimeGenerated between (ago(30m) .. ago(15m))
| summarize arg_max(TimeGenerated, *) by Id
| project id_s = Id, prevChange = todatetime(LastConnectionStateChange);
Stage 1: source
Unifi_SiteManager_Hosts_CL
Stage 2: where
| where TimeGenerated > ago(15m)
Stage 3: where
| where isnotempty(LastConnectionStateChange)
Stage 4: summarize
| summarize arg_max(TimeGenerated, *) by Id
Stage 5: extend
| extend
ConnectionChange = todatetime(LastConnectionStateChange),
id_s = Id
Stage 6: join
| join kind=leftouter prev on id_s
Stage 7: where
where (ConnectionChange != prevChange or isnull(prevChange))
Stage 8: where
where ConnectionChange > ago(1800s)
Stage 9: extend
extend HostTypeDisplay
HostTypeDisplay =HostType == "ucore""UniFi OS Console"HostType == "uck""Cloud Key"HostType == "uckp""Cloud Key+"HostType == "udm""Dream Machine"HostType == "udmpro""Dream Machine Pro"HostType == "udmse""Dream Machine SE"HostType == "self-hosted""Self-Hosted"HostTypeStage 10: project
project ConnectionChangeTime, HostId, HostType, HostTypeDisplay, IPAddress, IsBlocked, IsOwner, PreviousChangeTime, TimeGenerated
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
LastConnectionStateChange | is_not_null | field:"LastConnectionStateChange" kind:is_not_null | |
prevChange | is_null | field:"prevChange" kind:is_null |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ConnectionChangeTime | project |
HostId | project |
HostType | project |
HostTypeDisplay | project |
IPAddress | project |
IsBlocked | project |
IsOwner | project |
PreviousChangeTime | project |
TimeGenerated | project |
UnPAC the hash
#This query looks for an attack that allows an attacker with a valid TGT token for a certain account, to obtain the NTLM hash for that account. Such an account may either be a user account or a machine account. The TGT can, for example, be obtained by authenticating with a certificate instead of with username and password.
Known False Positives
- No false positives observed.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Credential Access | |
| Lateral Movement |
References
Telemetry Coverage
| Provider | Record / event type |
|---|---|
| Security-Auditing | Event ID 4769: A Kerberos service ticket was requested. |
Rule Body
let timeframe = 2*1h;
let RuleId = "0299";
let DedupFields = dynamic(["TimeGenerated"]);
let forwardable = binary_shift_left(1, 30);
let renewable = binary_shift_left(1, 23);
let renewable_ok = binary_shift_left(1, 4);
let enctik = binary_shift_left(1, 3);
let krbflags = binary_or(forwardable, binary_or(renewable, binary_or(renewable_ok, binary_or(enctik, 0))));
SecurityEvent
| where ingestion_time() >= ago(timeframe)
| where EventID == 4769
| extend ticketOptions = toint(extract("<Data Name=\"TicketOptions\">(.*?)</Data>", 1, EventData))
| extend TargetUserName = extract("<Data Name=\"TargetUserName\">(.*?)@.*?</Data>", 1, EventData)
| extend TargetDomainName = extract("<Data Name=\"TargetDomainName\">(.*?)</Data>", 1, EventData)
| extend ServiceName = extract("<Data Name=\"ServiceName\">(.*?)</Data>", 1, EventData)
| where ServiceName =~ TargetUserName // Requirement for getting the NT hash with U2U. This makes the KDC encrypt the NT hash with the key in the TGT.
| where binary_and(ticketOptions, krbflags) == krbflags
| extend HostName=tostring(split(Computer,".")[0]),DnsDomain=iif(Computer contains ".", substring(Computer, indexof(Computer, ".") + 1, strlen(Computer)),"")
// Begin environment-specific filter.
// End environment-specific filter.
// Begin de-duplication logic.
| extend DedupFieldValues=pack_all()
| mv-apply e=DedupFields to typeof(string) on (
extend DedupValue=DedupFieldValues[tostring(e)]
| order by e // Sorting is required to ensure make_list is deterministic.
| summarize DedupValues=make_list(DedupValue)
)
| extend DedupEntity=strcat_array(DedupValues, "|")
| project-away DedupFieldValues, DedupValues
| join kind=leftanti (
SecurityAlert
| where AlertName has RuleId and ProviderName has "ASI"
| where TimeGenerated >= ago(timeframe)
| extend DedupEntity = tostring(parse_json(tostring(parse_json(ExtendedProperties)["Custom Details"])).DedupEntity[0])
| project DedupEntity
) on DedupEntity
// End de-duplication logic.
Stages and Predicates
Parameters
let timeframe = 2*1h;
let RuleId = "0299";
let DedupFields = dynamic(["TimeGenerated"]);
let forwardable = binary_shift_left(1, 30);
let renewable = binary_shift_left(1, 23);
let renewable_ok = binary_shift_left(1, 4);
let enctik = binary_shift_left(1, 3);
let krbflags = binary_or(forwardable, binary_or(renewable, binary_or(renewable_ok, binary_or(enctik, 0))));
Stage 1: source
SecurityEvent
Stage 2: where
| where ingestion_time() >= ago(timeframe)
Stage 3: where
| where EventID == 4769
Stage 4: extend (4 consecutive steps)
| extend ticketOptions = toint(extract("<Data Name=\"TicketOptions\">(.*?)</Data>", 1, EventData))
| extend TargetUserName = extract("<Data Name=\"TargetUserName\">(.*?)@.*?</Data>", 1, EventData)
| extend TargetDomainName = extract("<Data Name=\"TargetDomainName\">(.*?)</Data>", 1, EventData)
| extend ServiceName = extract("<Data Name=\"ServiceName\">(.*?)</Data>", 1, EventData)
Stage 5: where
| where ServiceName =~ TargetUserName
Stage 6: where
| where binary_and(ticketOptions, krbflags) == krbflags
Stage 7: extend
| extend HostName=tostring(split(Computer,".")[0]),DnsDomain=iif(Computer contains ".", substring(Computer, indexof(Computer, ".") + 1, strlen(Computer)),"")
Stage 8: extend
| extend DedupFieldValues=pack_all()
Stage 9: kusto:mv-apply
| mv-apply e=DedupFields to typeof(string) on (
extend DedupValue=DedupFieldValues[tostring(e)]
| order by e
| summarize DedupValues=make_list(DedupValue)
)
Stage 10: extend
| extend DedupEntity=strcat_array(DedupValues, "|")
Stage 11: project-away
| project-away DedupFieldValues, DedupValues
Stage 12: join (negated)
join kind=leftanti (SecurityAlert) on DedupEntity
Stage 13: summarize aggregation inside the mv-apply branch
summarize
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
EventID | eq |
| field:"EventID" kind:eq value:"4769" |
ticketOptions | eq |
| field:"ticketOptions" kind:eq value:"krbflags" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
AlertName | match | 0299 | excludes:AlertName field:"AlertName" value:"0299" |
ProviderName | match | ASI | excludes:ProviderName field:"ProviderName" value:"ASI" |