Detection rules › Kusto
Hunt for public remotly exploitable devices (with high EPSS)
This query searches for devices that comply with the following criteria: - Incomming connections from public IP addresses in last 7 days (internet exposed) - High or Critical severity CVE's - CVE's must have known exploits - CVE's are remotely exploitable over the network - No user interaction is required to exploit the CVE's - EPSS score of CVE must by above 10% (likelihood of exploitation) > If devices are placed behind a proxy, they will not be returned in this query by default
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access | T1190 Exploit Public-Facing Application |
Rule body yaml
// Flag remotly exploitable, no user interaction, CVE's with a EPSS score above a certain threshold (likelyhood of exploitation)
// For devices with incomming public connections
// See efficiency research on https://www.first.org/epss/model
let epss_threshold = 0.1;
let exploit_statusses = dynamic(["ExploitIsPublic","ExploitIsInKit","ExploitIsVerified"]);
// Xspm base query we materialize since we need these results multiple times
let xspm_base = materialize (
ExposureGraphNodes
// Get device nodes with their inventory ID
| mv-expand EntityIds
| where EntityIds.type == "DeviceInventoryId"
// Get first important properties
| extend DeviceId = tostring(parse_json(EntityIds)["id"]),
ExposureScore = tostring(parse_json(NodeProperties)["rawData"]["exposureScore"]),
HasHighOrCriticalCve = tostring(parse_json(NodeProperties)["rawData"]["highRiskVulnerabilityInsights"]["hasHighOrCritical"])
// Focus on devices with high exposure
| where ExposureScore == "High"
// Get vulnerability exploit information
| extend RceExploitLevels = parse_json(NodeProperties)["rawData"]["highRiskVulnerabilityInsights"]["vulnerableToRemoteCodeExecution"]["explotabilityLevels"]
| extend PrivEscExploitLevels = parse_json(NodeProperties)["rawData"]["highRiskVulnerabilityInsights"]["vulnerableToPrivilegeEscalation"]["explotabilityLevels"]
// Focus on devices where cve has known epxloits
| where RceExploitLevels has_any (exploit_statusses) or PrivEscExploitLevels has_any (exploit_statusses)
// Focus on devices that are public exposed
| join kind=inner (
DeviceNetworkEvents
| where TimeGenerated > ago(7d)
| where ActionType contains "InboundConnection"
| where RemoteIPType == "Public"
// Exclude MacOS Rapportd and ControlCenter
| where InitiatingProcessFileName != "rapportd" and InitiatingProcessFileName != "controlcenter"
| distinct DeviceName, DeviceId, LocalPort, InitiatingProcessFolderPath, InitiatingProcessVersionInfoProductName, InitiatingProcessFileName
) on $left.DeviceId == $right.DeviceId
// Save all the open ports and their process in a JSON
| extend OpenPortJson = bag_pack_columns(LocalPort, InitiatingProcessFolderPath, InitiatingProcessFileName)
// Save open ports by Device ID
| summarize PublicOpenPortList = make_set(OpenPortJson) by DeviceId
);
// Save flagged device IDs in list to limit results of CVE's we need to search later
let flagged_devices = toscalar(
xspm_base
| summarize make_set(DeviceId)
);
// CVE base query we materialize since we need these results multiple times
let cve_base = materialize (
DeviceTvmSoftwareVulnerabilities
| where VulnerabilitySeverityLevel in ("High", "Critical")
| where DeviceId in ( flagged_devices )
);
// Save flagged CVE IDs in list to limit results of CVE database we need to search later
let flagged_cves = toscalar(
cve_base
| summarize make_set(CveId)
);
// Query the CVE's of the flagged devices
cve_base
// Enrich the CVE data with their EPSS and CVSS Score
| join kind=inner (
DeviceTvmSoftwareVulnerabilitiesKB
// Focus on flagged CVE's
| where CveId in ( flagged_cves )
// Focus on CVE's tagged with Attack Vector being over the Network
// 'Vulnerabilities with this rating are remotely exploitable, from one or more hops away, up to and including remote exploitation over the Internet.'
// 'Does not require user interaction'
| where CvssVector contains "/AV:N" and CvssVector contains "/UI:N"
// Focus on CVE's where an exploit is available
| where IsExploitAvailable != 0
| distinct CveId, EpssScore, CvssScore, CvssVector, IsExploitAvailable, AffectedSoftwareList=tostring(AffectedSoftware)
) on CveId
// Continue with only relevant data
| project DeviceId, DeviceName, OSPlatform, OSVersion, OSArchitecture, SoftwareName, SoftwareVendor, SoftwareVersion, CveId, VulnerabilitySeverityLevel, EpssScore, CvssScore, CvssVector, IsExploitAvailable, AffectedSoftwareList
// Now flag CVE's with a EPSS score above a certain threshold
// See efficiency research on https://www.first.org/epss/model
| where EpssScore >= epss_threshold
// Save all the CVE data in a JSON column
| extend CveJson = bag_pack_columns(SoftwareName, SoftwareVendor, SoftwareVersion, CveId, EpssScore, CvssScore, CvssVector, IsExploitAvailable, AffectedSoftwareList)
// Group the CVE data for each device per device
| summarize CveList = make_list(CveJson) by DeviceId, DeviceName
// Add xspm data again
| join kind=inner xspm_base on DeviceId
| project-away DeviceId1
// Sort by CVE amount
| extend CveCount = array_length(CveList)
| sort by CveCount desc
Stages and Predicates
Parameters
let epss_threshold = 0.1;
let exploit_statusses = dynamic(["ExploitIsPublic","ExploitIsInKit","ExploitIsVerified"]);
Let binding: xspm_base
let xspm_base = materialize (
ExposureGraphNodes
| mv-expand EntityIds
| where EntityIds.type == "DeviceInventoryId"
| extend DeviceId = tostring(parse_json(EntityIds)["id"]),
ExposureScore = tostring(parse_json(NodeProperties)["rawData"]["exposureScore"]),
HasHighOrCriticalCve = tostring(parse_json(NodeProperties)["rawData"]["highRiskVulnerabilityInsights"]["hasHighOrCritical"])
| where ExposureScore == "High"
| extend RceExploitLevels = parse_json(NodeProperties)["rawData"]["highRiskVulnerabilityInsights"]["vulnerableToRemoteCodeExecution"]["explotabilityLevels"]
| extend PrivEscExploitLevels = parse_json(NodeProperties)["rawData"]["highRiskVulnerabilityInsights"]["vulnerableToPrivilegeEscalation"]["explotabilityLevels"]
| where RceExploitLevels has_any (exploit_statusses) or PrivEscExploitLevels has_any (exploit_statusses)
| join kind=inner (
DeviceNetworkEvents
| where TimeGenerated > ago(7d)
| where ActionType contains "InboundConnection"
| where RemoteIPType == "Public"
| where InitiatingProcessFileName != "rapportd" and InitiatingProcessFileName != "controlcenter"
| distinct DeviceName, DeviceId, LocalPort, InitiatingProcessFolderPath, InitiatingProcessVersionInfoProductName, InitiatingProcessFileName
) on $left.DeviceId == $right.DeviceId
| extend OpenPortJson = bag_pack_columns(LocalPort, InitiatingProcessFolderPath, InitiatingProcessFileName)
| summarize PublicOpenPortList = make_set(OpenPortJson) by DeviceId
);
Derived from exploit_statusses.
Let binding: flagged_devices
let flagged_devices = toscalar(
xspm_base
| summarize make_set(DeviceId)
);
Derived from xspm_base.
Let binding: flagged_cves
let flagged_cves = toscalar(
cve_base
| summarize make_set(CveId)
);
Derived from cve_base.
The stages below define let cve_base (the rule's main pipeline source).
Stage 1: source
DeviceTvmSoftwareVulnerabilities
Stage 2: where
| where VulnerabilitySeverityLevel in ("High", "Critical")
Stage 3: where
| where DeviceId in ( flagged_devices )
References flagged_devices (defined above).
The stages below run on cve_base (the outer pipeline).
Stage 4: join
cve_base
| join kind=inner (
DeviceTvmSoftwareVulnerabilitiesKB
| where CveId in ( flagged_cves )
| where CvssVector contains "/AV:N" and CvssVector contains "/UI:N"
| where IsExploitAvailable != 0
| distinct CveId, EpssScore, CvssScore, CvssVector, IsExploitAvailable, AffectedSoftwareList=tostring(AffectedSoftware)
) on CveId
Stage 5: project
| project DeviceId, DeviceName, OSPlatform, OSVersion, OSArchitecture, SoftwareName, SoftwareVendor, SoftwareVersion, CveId, VulnerabilitySeverityLevel, EpssScore, CvssScore, CvssVector, IsExploitAvailable, AffectedSoftwareList
Stage 6: where
| where EpssScore >= epss_threshold
Stage 7: extend
| extend CveJson = bag_pack_columns(SoftwareName, SoftwareVendor, SoftwareVersion, CveId, EpssScore, CvssScore, CvssVector, IsExploitAvailable, AffectedSoftwareList)
Stage 8: summarize
| summarize CveList = make_list(CveJson) by DeviceId, DeviceName
Stage 9: join
| join kind=inner xspm_base on DeviceId
Stage 10: project-away
| project-away DeviceId1
Stage 11: extend
| extend CveCount = array_length(CveList)
Stage 12: sort
| sort by CveCount desc
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.
| Field | Kind | Values |
|---|---|---|
ActionType | contains |
|
CveId | in |
|
CvssVector | contains |
|
DeviceId | in |
|
EpssScore | ge |
|
ExposureScore | eq |
|
InitiatingProcessFileName | ne |
|
IsExploitAvailable | ne |
|
PrivEscExploitLevels | match |
|
RceExploitLevels | match |
|
RemoteIPType | eq |
|
VulnerabilitySeverityLevel | in |
|
type | eq |
|
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.
| Field | Source |
|---|---|
CveList | summarize |
DeviceId | summarize |
DeviceName | summarize |
CveCount | extend |