Potential PowerShell Obfuscation via Concatenated Dynamic Command Invocation

edit
IMPORTANT: This documentation is no longer updated. Refer to Elastic's version policy and the latest documentation.

Potential PowerShell Obfuscation via Concatenated Dynamic Command Invocation

edit

Detects PowerShell scripts that builds commands from concatenated string literals inside dynamic invocation constructs like &() or .(). Attackers use concatenated dynamic invocation to obscure execution intent, bypass keyword-based detections, and evade AMSI.

Rule type: esql

Rule indices: None

Severity: high

Risk score: 73

Runs every: 5m

Searches indices from: now-9m (Date Math format, see also Additional look-back time)

Maximum alerts per execution: 100

References: None

Tags:

  • Domain: Endpoint
  • OS: Windows
  • Use Case: Threat Detection
  • Tactic: Defense Evasion
  • Data Source: PowerShell Logs
  • Resources: Investigation Guide

Version: 11

Rule authors:

  • Elastic

Rule license: Elastic License v2

Investigation guide

edit

Triage and analysis

Investigating Potential PowerShell Obfuscation via Concatenated Dynamic Command Invocation

Possible investigation steps

  • What did the alert preserve about the concatenated dynamic invocation?
  • Focus: Esql.script_block_pattern_count, powershell.file.script_block_text, and powershell.file.script_block_id.
  • Hint: use full-alert Esql.script_block_tmp only when you need the match-local slice.
  • Implication: escalate faster when multiple call-operator or dot-sourced matches sit near download, reflection, credential, persistence, or execution logic; lower suspicion only when one short match resolves to a transparent helper and source recovery supports the same recognized workflow.
  • Can you reconstruct the full source 4104 script block before interpreting context?
  • Why: PowerShell can split large script blocks, and this ES|QL alert keeps summary fields that do not replace source-event recovery.
  • Focus: query PowerShell Operational source events with host.id, powershell.file.script_block_id, powershell.sequence, and powershell.total; order fragments and record source process.pid when recovered. !{investigate{"description":"","label":"Script block fragments for the same script","providers":[[{"excluded":false,"field":"powershell.file.script_block_id","queryType":"phrase","value":"{{powershell.file.script_block_id}}","valueType":"string"},{"excluded":false,"field":"host.id","queryType":"phrase","value":"{{host.id}}","valueType":"string"}]],"relativeFrom":"now-1h","relativeTo":"now"}}
  • Implication: incomplete fragments are unresolved, not benign; escalation is stronger when reconstruction exposes hidden stages, fileless delivery, or missing execution context.
  • What command or script does the concatenation resolve to, and does the operator expand impact?
  • Focus: reconstructed powershell.file.script_block_text, surrounding variable assignments, and call-operator versus dot-sourcing use.
  • Implication: escalate when the resolved token hides invocation or LOLBin logic that the surrounding code then executes; lower suspicion when reconstruction leaves one readable helper inside a recognized module or compatibility wrapper.
  • Does the source event show a file-backed or fileless origin that fits this user and host?
  • Focus: recovered file.path, user.id, source-event user.name, source-event user.domain, and host.id.
  • Implication: escalate when the script is fileless or sourced from temp, downloads, profiles, shares, or another user-writable path under an unexpected identity; lower suspicion when the file path and user-host pairing match the same recognized admin module or compatibility workflow.
  • Hint: absent file.path after source recovery means interactive, pasted, or memory-only activity; require stronger corroboration before closure.
  • Can you recover the PowerShell process launch chain?
  • Focus: source process.pid plus same-host process-start telemetry for recovered process.entity_id, process.command_line, and process.parent.executable.
  • Hint: if endpoint process telemetry is unavailable, keep later pivots bounded to host.id plus user.id or user.name in the alert window rather than assuming process scope. !{investigate{"description":"","label":"Process events for the PowerShell instance","providers":[[{"excluded":false,"field":"process.pid","queryType":"phrase","value":"{{process.pid}}","valueType":"string"},{"excluded":false,"field":"host.id","queryType":"phrase","value":"{{host.id}}","valueType":"string"},{"excluded":false,"field":"event.category","queryType":"phrase","value":"process","valueType":"string"}]],"relativeFrom":"now-1h","relativeTo":"now"}}
  • Implication: escalate when PowerShell is launched by Office, a browser, an archive extractor, a LOLBin, an unexpected service, or a remote session; lower suspicion when the launch chain matches the same recognized management tool or scheduled task already supported by source evidence.
  • Does the reconstructed script show layered obfuscation or payload-delivery logic beyond concatenation?
  • Focus: powershell.file.script_block_entropy_bits, powershell.file.script_block_surprisal_stdev, powershell.file.script_block_length, and reconstructed powershell.file.script_block_text; compare powershell.file.script_block_length against Esql.script_block_pattern_count to detect dead-code inflation around few match sites.
  • Implication: escalate when concatenation sits beside encoding, reflection, decoder routines, download strings, hidden payload material, dead-code padding, or Get-Command wildcard resolution.
  • Did the recovered process or host-window activity retrieve, stage, or execute follow-on content?
  • Focus: child starts from recovered process.entity_id, same-PID 4104 blocks, and file, DNS, or connection side effects: file.path, dns.question.name, and destination.ip.
  • Hint: missing file, DNS, or network telemetry is unresolved, not benign; if process.entity_id was not recovered, scope only by host.id plus user.id or user.name in the alert window. !{investigate{"description":"","label":"Child process activity from the PowerShell instance","providers":[[{"excluded":false,"field":"process.parent.pid","queryType":"phrase","value":"{{process.pid}}","valueType":"string"},{"excluded":false,"field":"host.id","queryType":"phrase","value":"{{host.id}}","valueType":"string"},{"excluded":false,"field":"event.category","queryType":"phrase","value":"process","valueType":"string"},{"excluded":false,"field":"event.type","queryType":"phrase","value":"start","valueType":"string"}]],"relativeFrom":"now-1h","relativeTo":"now"}} !{investigate{"description":"","label":"File, network, and DNS events for the PowerShell PID","providers":[[{"excluded":false,"field":"event.category","queryType":"phrase","value":"file","valueType":"string"},{"excluded":false,"field":"host.id","queryType":"phrase","value":"{{host.id}}","valueType":"string"},{"excluded":false,"field":"process.pid","queryType":"phrase","value":"{{process.pid}}","valueType":"string"}],[{"excluded":false,"field":"event.category","queryType":"phrase","value":"network","valueType":"string"},{"excluded":false,"field":"host.id","queryType":"phrase","value":"{{host.id}}","valueType":"string"},{"excluded":false,"field":"process.pid","queryType":"phrase","value":"{{process.pid}}","valueType":"string"}],[{"excluded":false,"field":"event.category","queryType":"phrase","value":"dns","valueType":"string"},{"excluded":false,"field":"host.id","queryType":"phrase","value":"{{host.id}}","valueType":"string"},{"excluded":false,"field":"process.pid","queryType":"phrase","value":"{{process.pid}}","valueType":"string"}]],"relativeFrom":"now-1h","relativeTo":"now"}} !{investigate{"description":"","label":"Script block events for the PowerShell PID","providers":[[{"excluded":false,"field":"event.code","queryType":"phrase","value":"4104","valueType":"string"},{"excluded":false,"field":"host.id","queryType":"phrase","value":"{{host.id}}","valueType":"string"},{"excluded":false,"field":"process.pid","queryType":"phrase","value":"{{process.pid}}","valueType":"string"}]],"relativeFrom":"now-1h","relativeTo":"now"}}
  • Implication: escalate when the same process chain spawns shells, writes scripts or binaries, or reaches rare external destinations.
  • If local findings remain suspicious or unresolved, does related alert history change scope?
  • Focus: related alerts for the same user.id in the last 48 hours, prioritizing repeated obfuscated PowerShell, the same resolved token, or script path. !{investigate{"description":"","label":"Alerts associated with the user","providers":[[{"excluded":false,"field":"event.kind","queryType":"phrase","value":"signal","valueType":"string"},{"excluded":false,"field":"user.id","queryType":"phrase","value":"{{user.id}}","valueType":"string"}]],"relativeFrom":"now-48h/h","relativeTo":"now"}}
  • Hint: if the user view is sparse or shared, pivot to the same host.id in the last 48 hours. !{investigate{"description":"","label":"Alerts associated with the host","providers":[[{"excluded":false,"field":"event.kind","queryType":"phrase","value":"signal","valueType":"string"},{"excluded":false,"field":"host.id","queryType":"phrase","value":"{{host.id}}","valueType":"string"}]],"relativeFrom":"now-48h/h","relativeTo":"now"}}
  • Implication: broaden response when repeated obfuscation, AMSI tampering, encoded commands, download, credential-access, or persistence alerts cluster on the same user or host; keep scope local when the alert is isolated and local evidence resolves to one recognized workflow.
  • Escalate on intentionally hidden PowerShell execution across match details, reconstruction, origin, launch chain, layered obfuscation, or follow-on activity; close only when recovered script, resolved token, origin, user-host context, launch chain, and side-effect telemetry align with one recognized workflow; preserve artifacts and escalate when reconstruction, process recovery, or file/network visibility stays incomplete.

False positive analysis

  • Internal compatibility wrappers, module loaders, code-protected vendor scripts, or administrative scripts may concatenate or dot-source helper names. Confirm recovered powershell.file.script_block_text, resolved token, file.path or stable helper path, recovered parent executable, dot-sourced location, and user.id plus host.id all align with one recognized workflow, with child process, file, DNS, and network effects contained to it. If external records are unavailable, require the same file.path or helper path, resolved token, parent executable, and user-host pairing to recur across prior alerts from this rule.
  • Before creating an exception, anchor it on stable file.path, resolved token, recovered parent executable, and relevant host.id or user.id scope. Avoid exceptions on Esql.script_block_pattern_count, Esql.script_block_tmp, user.name, or powershell.file.script_block_text alone.

Response and remediation

  • If confirmed benign, reverse any temporary containment and document the evidence that proved one recognized workflow: resolved token, recovered file.path, launch chain, and user.id plus host.id scope. Create an exception only after the same pattern recurs consistently.
  • If suspicious but unconfirmed, preserve the alert, reconstructed script fragments, recovered process identifiers, launch chain, staged file paths, DNS names, destination IPs, and case timeline before containment or cleanup.
  • Apply reversible containment first: heightened monitoring, temporary outbound restrictions, or PowerShell restrictions on the affected host.id. Escalate to host isolation only when launch-chain or follow-on evidence indicates likely payload execution, lateral movement, or active command-and-control.
  • If confirmed malicious, isolate the endpoint or contain the account based on the identity, launch-chain, file, and network evidence. Before suspending or terminating PowerShell, record the recovered process entity ID, command line, parent chain, resolved token, reconstructed script fragments, and staged file or network indicators.
  • Review related hosts and users for the same resolved token, stable file path, parent executable, and destination indicators before removing artifacts so scoping completes before evidence is destroyed.
  • Remove only the unauthorized scripts, dropped payloads, and persistence artifacts identified during the investigation, then remediate the delivery path or administrative-control gap that allowed obfuscated PowerShell execution.
  • Post-incident hardening: retain Script Block Logging and endpoint process/file/network telemetry, restrict PowerShell where it is not required, and document the resolved token, script path, launch chain, and side-effect pattern that distinguished benign workflow from abuse.

Setup

edit

Setup

PowerShell Script Block Logging must be enabled to generate the events used by this rule (e.g., 4104). Setup instructions: https://ela.st/powershell-logging-setup

Rule query

edit
from logs-windows.powershell_operational* metadata _id, _version, _index
| where event.code == "4104" and powershell.file.script_block_text like "*+*"

// replace the patterns we are looking for with the 🔥 emoji to enable counting them
// The emoji is used because it's unlikely to appear in scripts and has a consistent character length of 1
| eval Esql.script_block_tmp = replace(
    powershell.file.script_block_text,
    """[.&]\(\s*(['"][A-Za-z0-9.-]+['"]\s*\+\s*)+['"][A-Za-z0-9.-]+['"]\s*\)""",
    "🔥"
)

// count how many patterns were detected by calculating the number of 🔥 characters inserted
| eval Esql.script_block_pattern_count = length(Esql.script_block_tmp) - length(replace(Esql.script_block_tmp, "🔥", ""))

// keep the fields relevant to the query, although this is not needed as the alert is populated using _id
| keep
    Esql.script_block_pattern_count,
    Esql.script_block_tmp,
    powershell.file.*,
    file.path,
    process.pid,
    powershell.sequence,
    powershell.total,
    _id,
    _version,
    _index,
    host.name,
    host.id,
    agent.id,
    user.id

// Filter for scripts that match the pattern at least once
| where Esql.script_block_pattern_count >= 1

Framework: MITRE ATT&CKTM