Azure’s Blind Spot: How VM RunCommand Evades Activity Logs – and How Elastic ES|QL Reveals the Truth + Video

Listen to this Post

Featured Image

Introduction:

Azure Activity Logs are the go‑to source for tracking control plane operations, but they have a critical gap: when you execute a script via VM RunCommand, the activity log records that a RunCommand operation occurred, but not the actual command string or its output. On the target Windows VM, all spawned processes run under `NT AUTHORITY\SYSTEM` (SID S-1-5-18) or `root` on Linux, with zero user attribution. This blind spot allows lateral movement or persistence to go undetected if you only monitor Azure audit trails. Using Elastic’s ES|QL, however, you can correlate Endpoint security data (process creation events) with Azure Active Directory (AAD) sign‑in logs to bridge that gap – identifying exactly which user triggered which command, even when Azure itself stays silent.

Learning Objectives:

  • Understand the logging limitations of Azure VM RunCommand and why traditional Activity Logs are insufficient for forensics.
  • Learn to correlate Elastic Endpoint process data with AAD audit logs using ES|QL queries.
  • Implement detection rules and cloud hardening measures to identify and block unauthorized VM RunCommand executions.

You Should Know:

1. Reproducing the Azure RunCommand Logging Gap

Azure VM RunCommand is a powerful feature that lets you execute scripts on a VM without needing direct RDP or SSH. However, the Azure Activity Log only captures metadata (e.g., “Microsoft.Compute/virtualMachines/runCommand/action”), not the actual command payload. To see this firsthand:

Step‑by‑step guide:

  1. From Azure CLI, run a command on a Windows VM:
    az vm run-command invoke --command-id RunPowerShellScript --name MyVM -g MyRG --scripts "whoami > C:\temp\whoami.txt"
    
  2. Navigate to Azure Monitor → Activity Log. Filter by operation “Run Command”. You’ll see the event, but no `–scripts` content.
  3. Log into the VM and check processes spawned at that time (PowerShell):
    Get-WinEvent -FilterHashtable @{LogName='Security'; ID=4688} | Where-Object {$_.Message -like "whoami"} | Format-List
    

    On Linux (using auditd or Elastic endpoint), you’ll see the command running as `root` with no link back to the Azure user who invoked it.

Key takeaway: The Activity Log tells you that something ran, but not what ran or who (in terms of Azure AD identity) actually triggered it. The local process runs as SYSTEM/root – a classic attribution gap.

2. Bridging the Gap with Elastic ES|QL Correlation

Elastic’s ES|QL (Elasticsearch Query Language) allows you to join data from multiple sources in a single, piped query. Here we correlate Endpoint process events (capturing the actual command line and parent process) with Azure AD audit logs (capturing which user called the RunCommand API).

Step‑by‑step guide:

1. Ensure both data streams are in Elastic:

  • Elastic Agent with Endpoint Integration (process event event.action="ProcessRollup2").
  • Azure AD logs via the Azure Active Directory Logs integration (event event.dataset="azure.audit").
  1. Use this ES|QL query to match RunCommand API calls with the resulting child processes:
    FROM logs-endpoint.events.process, logs-azure.audit
    | WHERE @timestamp > NOW() - 1 hour
    | EVAL vm_name = CASE( // extract VM name from Azure activity target
    event.dataset == "azure.audit" AND operation_name == "Run Command",
    to_string(target_resources[bash].resource_name),
    ""
    )
    | WHERE event.dataset == "azure.audit" AND operation_name == "Run Command"
    | SORT @timestamp
    | KEEP @timestamp, initiated_by.user.user_principal_name, vm_name, operation_name
    | JOIN type=left
    (FROM logs-endpoint.events.process
    | WHERE host.os.type == "windows" AND user.id == "S-1-5-18"
    | RENAME host.name AS vm_name)
    ON vm_name
    | WHERE _query2.@timestamp > (@timestamp - 5 seconds) AND _query2.@timestamp < (@timestamp + 5 seconds)
    
  2. The result will show: Azure user `[email protected]` called RunCommand on `VM-web-01` at 10:32:01, and at 10:32:03 a `powershell.exe` process as SYSTEM created whoami.txt. This closes the attribution gap.

Pro tip: Adjust the time window (5 seconds) based on observed latency between API call and process spawn.

3. Building a Detection Rule in Elastic Security

Once you can correlate, you can detect. Create a rule that triggers when a SYSTEM/root process appears immediately after a RunCommand API call from a sensitive Azure role.

Step‑by‑step rule creation:

  1. In Elastic Security → Rules → Create new rule → “ES|QL” type.

2. Paste this detection query:

FROM logs-azure.audit
| WHERE operation_name == "Run Command"
| KEEP @timestamp, initiated_by.user.user_principal_name, target_resources
| JOIN type=inner
(FROM logs-endpoint.events.process
| WHERE host.os.type == "windows" AND user.id == "S-1-5-18"
| KILL @timestamp AS proc_time, process.command_line, host.name)
ON host.name = target_resources[bash].resource_name
| WHERE proc_time BETWEEN @timestamp AND @timestamp + interval(5 seconds)
| WHERE initiated_by.user.user_principal_name != "system_account"

3. Set rule action to “Critical” and add a notification to your SecOps channel.
4. Test by executing a benign command – the rule should fire within seconds.

Why this works: You’re no longer trusting Azure’s log to be complete. Instead, you’re combining control plane (AAD) and data plane (Endpoint) telemetry to reconstruct the full kill chain.

4. Hardening Azure Against RunCommand Abuse

Prevention beats detection. Implement these controls to restrict who can call RunCommand and from where.

Step‑by‑step hardening:

  1. Azure Policy – Deny RunCommand for all VMs except those in a dedicated “management” resource group:
    {
    "if": {
    "allOf": [
    { "field": "type", "equals": "Microsoft.Compute/virtualMachines/runCommands" },
    { "not": { "field": "resourceGroup", "equals": "Mgmt-RG" } }
    ]
    },
    "then": { "effect": "deny" }
    }
    

Assign via: `az policy assignment create –policy `

  1. RBAC – Create a custom role that explicitly denies `Microsoft.Compute/virtualMachines/runCommand/action` for all non‑break‑glass users:
    az role definition create --role-definition '{
    "Name": "No RunCommand",
    "Actions": [""],
    "NotActions": [],
    "DataActions": [],
    "NotDataActions": [],
    "AssignableScopes": ["/subscriptions/..."]
    }'
    
  2. Just‑in‑Time (JIT) VM access – Force approval workflows for any command execution. Use Azure Security Center’s JIT policy or a custom Logic App that reviews RunCommand API calls.

5. Incident Response: Investigating a Suspicious RunCommand Execution

When your Elastic rule fires, follow this IR workflow.

Step‑by‑step response:

  1. Collect evidence – From Elastic, retrieve the ES|QL correlation output: Azure user, timestamp, VM name, and full command line.
  2. Isolate the VM – Disable network security group rules or use Azure Bastion’s “disconnect”:
    Deny all outbound from the VM’s NSG
    az network nsg rule create --nsg-name VM-NSG -g MyRG --name Block-Outbound --priority 100 --direction Outbound --access Deny --protocol '' --destination-port-ranges ''
    
  3. Capture memory and disk – Use Azure VM Backup or create a snapshot:
    az snapshot create --source MyVM --name MyVM-snapshot --resource-group IR-RG
    
  4. Analyze laterally – Query Elastic Endpoint for any other VMs where the same Azure user executed a RunCommand in the past 7 days:
    FROM logs-azure.audit
    | WHERE initiated_by.user.user_principal_name == "<suspected-user>"
    | STATS executions = COUNT() BY target_resources[bash].resource_name
    
  5. Reset credentials – Force a password reset for the compromised Azure AD user and revoke refresh tokens.

  6. Alternative Mitigation: Capture Commands via Azure Monitor Agent
    If you can’t deploy Elastic, use Azure Monitor Agent with custom logs to record RunCommand payloads.

Step‑by‑step:

1. Deploy Azure Monitor Agent to the VM.

  1. Create a Data Collection Rule (DCR) that collects Windows Event Log “Microsoft-Windows-Sysmon/Operational” (process creation) or Linux audit logs.

3. Send to Log Analytics workspace.

  1. Query using KQL to find processes with parent `C:\Windows\System32\RunCommand.exe` (approximate) or command line containing “Invoke-WebRequest”.
  2. Alert using Azure Sentinel (now Microsoft Sentinel). However, note this still won’t give you the Azure user attribution – you’d need to join with AAD logs separately, similar to the Elastic method.

What Undercode Say:

  • Key Takeaway 1: Azure’s native Activity Log is deliberately incomplete for data‑plane actions like RunCommand. Relying solely on it for security monitoring creates a dangerous blind spot that attackers can easily abuse.
  • Key Takeaway 2: Elastic ES|QL is not just another query language – its ability to perform time‑based joins across completely different data sources (AAD identity logs + Endpoint process telemetry) transforms a “missing log” problem into a solvable correlation puzzle.

Analysis: The post highlights a systemic issue in cloud logging: control plane logs (Azure Activity Log) and data plane logs (VM OS) are rarely stitched together out of the box. This gap is especially critical for RunCommand because it’s a common pivoting technique (e.g., running Mimikatz or C2 beacons as SYSTEM). The proposed solution using Elastic ES|QL is elegant – it avoids writing custom parsers or shipping logs to external SIEMs with complex correlation rules. However, it requires that you already have Elastic Endpoint deployed on every VM, which may not be feasible in multi‑cloud or legacy environments. A complementary approach would be to enforce that RunCommand scripts are always logged to Azure Storage via a custom script extension that writes to a diagnostic storage account – but that’s reactive. The proactive win here is detection engineering: treat every RunCommand call as suspicious and immediately look for child processes that shouldn’t exist under SYSTEM. Organizations lacking Elastic can approximate this with Azure Log Analytics and KQL, but the performance and ease of ES|QL’s `JOIN` operation gives Elastic a real edge.

Prediction:

As cloud providers like Azure and AWS continue to separate control plane (CloudTrail/Activity Log) from data plane telemetry, attackers will increasingly exploit these cracks for operational stealth. Over the next 12–18 months, expect to see a surge in adversary techniques that leverage RunCommand‑like APIs (AWS SSM Run Command, GCP OS Config) specifically because they bypass traditional detection. In response, SIEM vendors will embed native cross‑plane correlation engines – Elastic’s ES|QL is a preview of this trend. Moreover, Microsoft will likely be pressured to enhance Activity Logs to include command hashes or truncate command lines, but due to performance and cost constraints, they’ll only offer it as a premium diagnostic setting. The long‑term solution will be a new cloud log category, “Command Execution Logs”, that merges identity, API call, and child process data into a single event. Until then, security teams must build their own bridges – using tools like Elastic, or risk flying blind.

▶️ Related Video (74% Match):

🎯Let’s Practice For Free:

IT/Security Reporter URL:

Reported By: Samir B – Hackers Feeds
Extra Hub: Undercode MoN
Basic Verification: Pass ✅

🔐JOIN OUR CYBER WORLD [ CVE News • HackMonitor • UndercodeNews ]

💬 Whatsapp | 💬 Telegram

📢 Follow UndercodeTesting & Stay Tuned:

𝕏 formerly Twitter 🐦 | @ Threads | 🔗 Linkedin | 🦋BlueSky