-
Notifications
You must be signed in to change notification settings - Fork 671
[New Rule] Unusual Azure VM Extension Installed; Suspicious Child Process via Azure VM CustomScript Extension #6277
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
terrancedejesus
wants to merge
7
commits into
main
Choose a base branch
from
new-rule/azure-vm-unusual-vm-extension-installed
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
270d5a6
[New Rule] Unusual Azure VM Extension Installed
terrancedejesus 7c9cbcb
adjusting rule name
terrancedejesus e2c8e1a
adding Suspicious Child Process via Azure VM CustomScript Extension s…
terrancedejesus 29c62c3
Merge branch 'main' into new-rule/azure-vm-unusual-vm-extension-insta…
terrancedejesus 9ecb638
Potential fix for pull request finding
terrancedejesus f9c0065
Potential fix for pull request finding
terrancedejesus 6ab47ce
Merge branch 'main' into new-rule/azure-vm-unusual-vm-extension-insta…
terrancedejesus File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
164 changes: 164 additions & 0 deletions
164
rules/integrations/azure/execution_azure_vm_extension_unusual_for_host.toml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,164 @@ | ||
| [metadata] | ||
| creation_date = "2026/06/15" | ||
| integration = ["azure"] | ||
| maturity = "production" | ||
| updated_date = "2026/06/15" | ||
|
|
||
| [rule] | ||
| author = ["Elastic"] | ||
| description = """ | ||
| Identifies the first time a given VM extension name is created or updated on an Azure virtual machine or VM scale set | ||
| historically. VM extensions run with high privilege on the guest (SYSTEM on Windows, root on Linux) and are a | ||
| common code-execution and persistence primitive. The extension instance name is attacker-controlled and the Azure | ||
| activity log records only that name, not the publisher or type, so the control plane cannot reliably identify the | ||
| extension family (for example CustomScript). This rule therefore takes a type-agnostic ES|QL new-terms approach: it | ||
| derives the host and the extension instance name from `azure.resource.name` and alerts the first time a given | ||
| (host, extension name) pair is observed in the window, surfacing novel extension deployments while suppressing names a | ||
| host routinely uses. | ||
| """ | ||
| false_positives = [ | ||
| """ | ||
| Legitimate provisioning, patching, and configuration-management automation may deploy a CustomScript extension to a | ||
| host for the first time. The first occurrence per host will alert. Baseline expected automation principals and hosts | ||
| and exclude verified-benign ones. | ||
|
terrancedejesus marked this conversation as resolved.
|
||
| """, | ||
| ] | ||
| from = "now-7d" | ||
| interval = "5m" | ||
| language = "esql" | ||
| license = "Elastic License v2" | ||
| name = "Unusual Azure VM Extension Detected" | ||
| note = """## Triage and analysis | ||
|
|
||
| ### Investigating Unusual Azure VM Extension Detected | ||
|
|
||
| Identifies the first time a given VM extension name is created or updated on an Azure virtual machine or VM scale set | ||
| historically. VM extensions run with high privilege on the guest (SYSTEM on Windows, root on Linux) and are a | ||
| common code-execution and persistence primitive. The extension instance name is attacker-controlled and the Azure | ||
|
terrancedejesus marked this conversation as resolved.
|
||
| activity log records only that name, not the publisher or type, so the control plane cannot reliably identify the | ||
| extension family (for example CustomScript). This rule therefore takes a type-agnostic ES|QL new-terms approach: it | ||
| derives the host and the extension instance name from `azure.resource.name` and alerts the first time a given | ||
| (host, extension name) pair is observed in the window, surfacing novel extension deployments while suppressing names a | ||
| host routinely uses. | ||
|
|
||
| ### Possible investigation steps | ||
|
|
||
| - Identify the host (`Esql.vm_name`) and the full extension resource (`azure.resource.name` / `azure.resource.id`). | ||
| - Identify the acting principal: `Esql.principal_id_values`, `Esql.principal_type_values` (User vs ServicePrincipal), | ||
| `Esql.appid_values`. Service principal or managed identity deployment is more suspicious than a known admin user. | ||
| - Review the source: `Esql.source_ip_values`, `Esql.source_as_number_values`, `Esql.source_country_values`. Cloud | ||
| hosting, VPS, or anonymizing networks are more suspicious than known corporate egress. | ||
| - Was this preceded by a Run Command invocation, role assignment, or other VM operations by the same principal? | ||
| - Correlate with endpoint telemetry on the host: process activity parented by the Azure guest agent | ||
| (`WaAppAgent.exe` / `walinuxagent`) within ~120 seconds of the deployment. | ||
| - Review the principal's Entra ID sign-in logs and RBAC role assignments on the subscription, resource group, and VM. | ||
| - Retrieve the extension settings/protected settings from the VM (the activity log does not contain the script/settings | ||
| body) to assess intent. | ||
| - Pivot on the VM for credential access, new local accounts, or outbound C2 connections following the deployment. | ||
|
|
||
| ### False positive analysis | ||
|
|
||
| - This is a broad first-seen net: the first deployment of any extension name to a host alerts, so benign monitoring, | ||
| antimalware (Defender/MDE), AKS, DSC, or configuration-management extensions deployed by routine automation will | ||
| trigger. Baseline expected automation principals (`Esql.appid_values`) and extension names, and exclude verified ones. | ||
| - Automation that generates a unique extension instance name per deployment produces a new (host, name) pair every time | ||
| and will recur; if benign, exclude by the deploying principal/appid or the known naming pattern rather than per host. | ||
| - Newly provisioned VMs receiving their initial extension set are expected. Corroborate the deploying principal and | ||
| source before escalating, and treat deployments from known corporate egress by approved automation as lower confidence. | ||
|
|
||
| ### Response and remediation | ||
|
|
||
| - If unauthorized, remove the extension, isolate the VM, rotate credentials reachable from it, and review RBAC on the | ||
| affected scope. | ||
| - Collect endpoint and activity log artifacts per incident procedures. | ||
| """ | ||
| references = [ | ||
| "https://www.netspi.com/blog/technical-blog/adversary-simulation/7-ways-to-execute-command-on-azure-virtual-machine-virtual-machine-scale-sets/", | ||
| "https://learn.microsoft.com/en-us/azure/virtual-machines/extensions/custom-script-windows", | ||
| "https://learn.microsoft.com/en-us/azure/virtual-machines/extensions/overview", | ||
| "https://blog.pwnedlabs.io/diving-deep-into-azure-vm-attack-vectors", | ||
| "https://www.sysdig.com/blog/the-expendable-extension-name-azure-vmaccess-naming-chaos-password-resets-and-a-detection-gap", | ||
| ] | ||
| risk_score = 47 | ||
| rule_id = "d29b0a67-178d-4381-92c5-02e9fd9a6ef6" | ||
| severity = "medium" | ||
| tags = [ | ||
| "Domain: Cloud", | ||
| "Data Source: Azure", | ||
| "Data Source: Azure Activity Logs", | ||
| "Use Case: Threat Detection", | ||
| "Tactic: Execution", | ||
| "Tactic: Persistence", | ||
| "Resources: Investigation Guide", | ||
| ] | ||
| timestamp_override = "event.ingested" | ||
| type = "esql" | ||
| query = ''' | ||
| FROM logs-azure.activitylogs-* | ||
| | WHERE event.dataset == "azure.activitylogs" | ||
| AND event.action IN ( | ||
| "MICROSOFT.COMPUTE/VIRTUALMACHINES/EXTENSIONS/WRITE", | ||
| "MICROSOFT.COMPUTE/VIRTUALMACHINESCALESETS/EXTENSIONS/WRITE" | ||
| ) | ||
| AND event.outcome IN ("success", "Success") | ||
| // azure.resource.name is "<host>/EXTENSIONS/<instance-name>"; the instance name is attacker-controlled, | ||
| // so key on the host (first path element) rather than the spoofable extension name | ||
| | EVAL Esql.vm_name = MV_FIRST(SPLIT(azure.resource.name, "/")) | ||
| | EVAL Esql.extension_name = MV_LAST(SPLIT(azure.resource.name, "/")) | ||
| | STATS Esql.first_time_seen = MIN(@timestamp), | ||
| Esql.last_time_seen = MAX(@timestamp), | ||
| Esql.event_count = COUNT(*), | ||
| Esql.resource_name_values = VALUES(azure.resource.name), | ||
| Esql.resource_id_values = VALUES(azure.resource.id), | ||
| Esql.principal_id_values = VALUES(azure.activitylogs.identity.authorization.evidence.principal_id), | ||
| Esql.principal_type_values = VALUES(azure.activitylogs.identity.authorization.evidence.principal_type), | ||
| Esql.appid_values = VALUES(azure.activitylogs.identity.claims.appid), | ||
| Esql.source_ip_values = VALUES(source.ip), | ||
| Esql.source_as_number_values = VALUES(source.`as`.number), | ||
| Esql.source_country_values = VALUES(source.geo.country_name), | ||
| Esql.subscription_id_values = VALUES(azure.subscription_id) | ||
| BY Esql.vm_name, Esql.extension_name | ||
| // new terms emulation: fire only when the (host, extension name) pair is the single occurrence in the | ||
| // 7-day window (event_count == 1) and it is recent (within the schedule interval + ingest-lag buffer) | ||
| | EVAL Esql.recent_minutes = DATE_DIFF("minute", Esql.first_time_seen, NOW()) | ||
| | WHERE Esql.recent_minutes <= 10 AND Esql.event_count == 1 | ||
| // surface real fields for the analyst and rule exceptions | ||
| | EVAL azure.resource.name = MV_FIRST(Esql.resource_name_values), | ||
| azure.resource.id = MV_FIRST(Esql.resource_id_values), | ||
| source.ip = MV_FIRST(Esql.source_ip_values) | ||
| | KEEP azure.resource.name, azure.resource.id, source.ip, Esql.* | ||
| ''' | ||
|
|
||
| [rule.alert_suppression] | ||
| group_by = ["azure.resource.name"] | ||
| missing_fields_strategy = "suppress" | ||
|
|
||
| [rule.alert_suppression.duration] | ||
| unit = "m" | ||
| value = 60 | ||
|
|
||
| [[rule.threat]] | ||
| framework = "MITRE ATT&CK" | ||
|
|
||
| [[rule.threat.technique]] | ||
| id = "T1651" | ||
| name = "Cloud Administration Command" | ||
| reference = "https://attack.mitre.org/techniques/T1651/" | ||
|
|
||
| [rule.threat.tactic] | ||
| id = "TA0002" | ||
| name = "Execution" | ||
| reference = "https://attack.mitre.org/tactics/TA0002/" | ||
|
|
||
| [[rule.threat]] | ||
| framework = "MITRE ATT&CK" | ||
|
|
||
| [[rule.threat.technique]] | ||
| id = "T1037" | ||
| name = "Boot or Logon Initialization Scripts" | ||
| reference = "https://attack.mitre.org/techniques/T1037/" | ||
|
|
||
| [rule.threat.tactic] | ||
| id = "TA0003" | ||
| name = "Persistence" | ||
| reference = "https://attack.mitre.org/tactics/TA0003/" | ||
146 changes: 146 additions & 0 deletions
146
rules/windows/execution_azure_customscript_extension_suspicious_descendant.toml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,146 @@ | ||
| [metadata] | ||
| creation_date = "2026/06/17" | ||
| integration = ["endpoint"] | ||
| maturity = "production" | ||
| updated_date = "2026/06/17" | ||
|
|
||
| [rule] | ||
| author = ["Elastic"] | ||
| description = """ | ||
| Identifies a suspicious process executing as a descendant of the Azure VM CustomScript extension handler | ||
| (CustomScriptHandler.exe) on a Windows host. The Azure CustomScript extension runs an attacker-supplied script with high | ||
| privilege (SYSTEM) via the guest agent, and is a common cloud-to-host code-execution and persistence primitive. Because | ||
|
terrancedejesus marked this conversation as resolved.
|
||
| the extension's resource name is attacker-controlled and absent from on-host telemetry, this rule anchors on the | ||
| type-bearing handler binary ('Microsoft.Compute.CustomScriptExtension\\...\\CustomScriptHandler.exe') rather than the | ||
| spoofable extension name, making it resistant to renaming. CustomScript legitimately launches PowerShell and cmd, so the | ||
| rule fires only when the descendant is an execution-proxy, download, or discovery LOLBin, or PowerShell exhibiting | ||
| suspicious tradecraft. | ||
|
terrancedejesus marked this conversation as resolved.
|
||
| """ | ||
| from = "now-9m" | ||
| index = ["logs-endpoint.events.process-*"] | ||
| language = "eql" | ||
| license = "Elastic License v2" | ||
| name = "Suspicious Child Process via Azure VM CustomScript Extension" | ||
| note = """## Triage and analysis | ||
|
|
||
| ### Investigating Suspicious Child Process via Azure VM CustomScript Extension | ||
|
|
||
| The Azure CustomScript extension executes a script as SYSTEM via the guest agent. The extension's resource name is | ||
| attacker-controlled and not present on the host, so this rule anchors on the handler binary path | ||
| (`Microsoft.Compute.CustomScriptExtension\\...\\CustomScriptHandler.exe`), which is rename-proof, and alerts when a | ||
| LOLBin or suspicious PowerShell runs anywhere in its process tree. | ||
|
terrancedejesus marked this conversation as resolved.
|
||
|
|
||
| ### Possible investigation steps | ||
|
|
||
| - Review the full process tree from `CustomScriptHandler.exe` to the alerting process, including `process.command_line` | ||
| and `process.args`. | ||
| - Identify the descendant: execution proxies (`mshta`, `regsvr32`, `rundll32`, `installutil`, `msbuild`), download tools | ||
| (`certutil`, `bitsadmin`), script hosts (`wscript`, `cscript`), or discovery utilities (`whoami`, `net`, `nltest`, | ||
| `wmic`) are not expected children of a benign CustomScript payload. | ||
|
terrancedejesus marked this conversation as resolved.
|
||
| - Correlate with the control-plane event: a `MICROSOFT.COMPUTE/VIRTUALMACHINES/EXTENSIONS/WRITE` in | ||
| `logs-azure.activitylogs-*` for this host around the same time, and the acting principal/source behind it. | ||
| - Retrieve the extension's settings/protectedSettings from the VM (the activity log does not contain the script body) to | ||
| assess intent. | ||
| - Pivot on the host for credential access, new local accounts, persistence, or outbound C2 following the execution. | ||
| - Review who deployed the extension (Entra sign-in logs and RBAC for the principal in the correlated activity log event). | ||
|
|
||
| ### False positive analysis | ||
|
|
||
| - Infrastructure-as-code and configuration-management scripts deployed via CustomScript may legitimately run discovery | ||
| utilities (`whoami`, `net`, `nltest`, `systeminfo`, `wmic`, `tasklist`, `arp`) for bootstrap or inventory. If the | ||
| activity recurs from known automation, baseline it and exclude by `process.command_line`/`process.args`. | ||
| - Software installation and bootstrapping via CustomScript can invoke `msbuild`, `installutil`, `regsvr32`, `regasm`, | ||
| `regsvcs`, `certutil`, or `bitsadmin` to build, register, or download legitimate components. Verify the target | ||
| file/URL and, if benign, scope the exclusion to the specific command or signed binary rather than the whole LOLBin. | ||
| - Legitimate setup scripts (DSC bootstrap, agent installers) may use PowerShell download cradles | ||
| (`Invoke-WebRequest`, `DownloadString`, `-EncodedCommand`) against trusted internal or Microsoft endpoints. Confirm | ||
| the destination host and content before excluding, and exclude by the specific command line, not by host. | ||
| - A known automation principal deploying the extension from expected corporate egress (corroborated by the correlated | ||
| `MICROSOFT.COMPUTE/VIRTUALMACHINES/EXTENSIONS/WRITE` and an approved change) lowers confidence, but still review the | ||
| executed content. Prefer narrow, command- or argument-scoped exclusions over broad host or LOLBin exclusions, since | ||
| the same execution chain is exactly what an attacker abuses. | ||
|
|
||
| ### Response and remediation | ||
|
|
||
| - If unauthorized, remove the extension, isolate the host, rotate credentials reachable from it, and review RBAC on the | ||
| affected subscription/resource group. | ||
| """ | ||
| references = [ | ||
| "https://blog.pwnedlabs.io/diving-deep-into-azure-vm-attack-vectors", | ||
| "https://www.sysdig.com/blog/the-expendable-extension-name-azure-vmaccess-naming-chaos-password-resets-and-a-detection-gap", | ||
| "https://learn.microsoft.com/en-us/azure/virtual-machines/extensions/custom-script-windows", | ||
| ] | ||
| risk_score = 47 | ||
| rule_id = "b4d4f0fb-908e-4cd1-ac8f-795c0433db0a" | ||
| severity = "medium" | ||
| tags = [ | ||
| "Domain: Endpoint", | ||
| "OS: Windows", | ||
| "Use Case: Threat Detection", | ||
| "Tactic: Execution", | ||
| "Tactic: Defense Evasion", | ||
| "Data Source: Elastic Defend", | ||
| "Resources: Investigation Guide", | ||
| ] | ||
| timestamp_override = "event.ingested" | ||
| type = "eql" | ||
| query = ''' | ||
| sequence by host.id with maxspan=1m | ||
| /* Azure CustomScript extension handler */ | ||
| [process where host.os.type == "windows" and event.type == "start" and | ||
| (process.name : "CustomScriptHandler.exe" or | ||
| process.executable : "?:\\Packages\\Plugins\\*CustomScript*\\*\\CustomScriptHandler.exe")] by process.entity_id | ||
| /* Abused LOLBin / suspicious PowerShell anywhere in its tree */ | ||
| [process where host.os.type == "windows" and event.type == "start" and | ||
| ( | ||
| process.name : ("mshta.exe", "regsvr32.exe", "rundll32.exe", "installutil.exe", "msbuild.exe", "regasm.exe", | ||
| "regsvcs.exe", "wscript.exe", "cscript.exe", "bitsadmin.exe", "nltest.exe", "whoami.exe", | ||
| "net.exe", "net1.exe", "wmic.exe", "systeminfo.exe", "quser.exe", "arp.exe", "tasklist.exe") or | ||
| (process.name : "certutil.exe" and process.args : ("*urlcache*", "*-decode*", "*-encode*")) or | ||
| (process.name : ("powershell.exe", "pwsh.exe") and | ||
| process.command_line : ("*-enc*", "*EncodedCommand*", "*FromBase64String*", "*DownloadString*", "*DownloadFile*", | ||
| "*Invoke-Expression*", "*IEX *", "* -w hidden*", "*WindowStyle Hidden*", "*Net.WebClient*", | ||
| "*Invoke-WebRequest*", "*Start-BitsTransfer*")) | ||
| )] by process.Ext.ancestry | ||
|
terrancedejesus marked this conversation as resolved.
|
||
| ''' | ||
|
|
||
| [[rule.threat]] | ||
| framework = "MITRE ATT&CK" | ||
|
|
||
| [[rule.threat.technique]] | ||
| id = "T1651" | ||
| name = "Cloud Administration Command" | ||
| reference = "https://attack.mitre.org/techniques/T1651/" | ||
|
|
||
| [[rule.threat.technique]] | ||
| id = "T1059" | ||
| name = "Command and Scripting Interpreter" | ||
| reference = "https://attack.mitre.org/techniques/T1059/" | ||
|
|
||
| [[rule.threat.technique.subtechnique]] | ||
| id = "T1059.001" | ||
| name = "PowerShell" | ||
| reference = "https://attack.mitre.org/techniques/T1059/001/" | ||
|
|
||
| [[rule.threat.technique.subtechnique]] | ||
| id = "T1059.003" | ||
| name = "Windows Command Shell" | ||
| reference = "https://attack.mitre.org/techniques/T1059/003/" | ||
|
|
||
| [rule.threat.tactic] | ||
| id = "TA0002" | ||
| name = "Execution" | ||
| reference = "https://attack.mitre.org/tactics/TA0002/" | ||
|
|
||
| [[rule.threat]] | ||
| framework = "MITRE ATT&CK" | ||
|
|
||
| [[rule.threat.technique]] | ||
| id = "T1218" | ||
| name = "System Binary Proxy Execution" | ||
| reference = "https://attack.mitre.org/techniques/T1218/" | ||
|
|
||
| [rule.threat.tactic] | ||
| id = "TA0005" | ||
| name = "Defense Evasion" | ||
| reference = "https://attack.mitre.org/tactics/TA0005/" | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.