Detection rules › Kusto

Hunt for public remotly exploitable devices (with high EPSS)

Group by
DeviceId, DeviceName
Author
Robbe Van den Daele
Source
github.com/HybridBrothers/Hunting-Queries-Detection-Rules

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

TacticTechniques
Initial AccessT1190 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.

FieldKindValues
ActionTypecontains
  • InboundConnection
CveIdin
  • flagged_cves transforms: cased
CvssVectorcontains
  • /AV:N
  • /UI:N
DeviceIdin
  • flagged_devices transforms: cased
EpssScorege
  • 0.1 transforms: cased
ExposureScoreeq
  • High transforms: cased
InitiatingProcessFileNamene
  • controlcenter transforms: cased
  • rapportd transforms: cased
IsExploitAvailablene
  • 0 transforms: cased
PrivEscExploitLevelsmatch
  • ExploitIsInKit
  • ExploitIsPublic
  • ExploitIsVerified
RceExploitLevelsmatch
  • ExploitIsInKit
  • ExploitIsPublic
  • ExploitIsVerified
RemoteIPTypeeq
  • Public transforms: cased
VulnerabilitySeverityLevelin
  • Critical transforms: cased
  • High transforms: cased
typeeq
  • DeviceInventoryId transforms: cased

Output fields

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

FieldSource
CveListsummarize
DeviceIdsummarize
DeviceNamesummarize
CveCountextend