Potential PowerShell Obfuscation via String Concatenation

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

Potential PowerShell Obfuscation via String Concatenation

edit

Detects PowerShell scripts that repeatedly concatenate multiple quoted string literals with + to assemble commands or tokens at runtime. Attackers use string concatenation to fragment keywords or URLs and evade static analysis and 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: 12

Rule authors:

  • Elastic

Rule license: Elastic License v2

Investigation guide

edit

Triage and analysis

Investigating Potential PowerShell Obfuscation via String Concatenation

Possible investigation steps

  • What does the alert-local summary show about the concatenation pattern and where it appears in the preserved script block?
  • Focus: alert-local Esql.script_block_pattern_count, Esql.script_block_length, Esql.script_block_tmp, and powershell.file.script_block_text.
  • Implication: escalate sooner when repeated matches sit near execution, download, decode, or persistence logic; lower suspicion when they resolve to inert configuration or output text, but do not close until the full script block and origin are checked.
  • Is the full script block reconstructed before interpretation?
  • Focus: source 4104 events in logs-windows.powershell_operational* for host.id and powershell.file.script_block_id, ordered by powershell.sequence against powershell.total. !{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: escalate when fragments add hidden stages, payload material, or omitted execution context; missing sequence fragments are unresolved because omitted text may contain the decisive string use.
  • What do the concatenated strings reconstruct to, and do they feed execution?
  • Focus: reconstructed powershell.file.script_block_text, quoted fragments around each match, and statistical cues from powershell.file.script_block_entropy_bits and powershell.file.script_block_surprisal_stdev.
  • Implication: escalate when strings reveal fragmented keywords, URLs or domains, paths, registry keys, encoded blobs, .NET reflection names, or decode inputs feeding invocation, download, file write, or persistence; lower suspicion when they remain inert data construction inside one stable script pattern.
  • Does the source event and origin context explain who ran the script and from where?
  • Focus: recovered file.path, user.id, and host.id, plus whether file.path is absent.
  • Hint: absent file.path after source-event recovery reduces origin provenance because the script may be interactive, pasted, or memory-only; require stronger corroboration before benign closure.
  • Implication: escalate when execution is fileless or from temp, downloads, profiles, mounted shares, or other user-writable locations under an unexpected identity; lower suspicion only when origin, user, host, and string use match one recognized automation or build workflow.
  • If endpoint process telemetry is available, does launch context support benign automation or abuse?
  • Focus: recover process.pid from the 4104 event, then match host.id and the alert window in endpoint process-start events for process.entity_id, process.command_line, and process.parent.executable. !{investigate{"description":"","label":"Process start events for the PowerShell PID","providers":[[{"excluded":false,"field":"event.category","queryType":"phrase","value":"process","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"}}
  • Hint: anchor process starts to @timestamp; if PID reuse creates multiple or distant matches, keep launch context unresolved. Without endpoint process telemetry, bound downstream checks to host.id, user.id or user.name, and the alert window; missing launch telemetry is unresolved, not benign.
  • Implication: escalate when PowerShell starts from Office, a browser, an archive extractor, a LOLBin, a remote context, or a service context with encoded or fileless delivery; lower suspicion when the launch chain and command line match the same recognized automation workflow.
  • Did the recovered process or host-window activity retrieve, stage, or execute follow-on content?
  • Focus: child process starts from the PowerShell PID and file, network, or DNS events for the same PID. !{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"}}
  • Hint: if PID recovery failed in the prior step, scope follow-on review manually with host.id, user.id, and a tight alert window; that fallback is broader, so prioritize timestamps and script-linked paths or destinations. Missing file or network telemetry is unresolved, not benign.
  • Implication: escalate when scoped activity spawns shells, writes scripts or binaries, reaches rare destinations, or stages persistence; lower suspicion when telemetry shows no effects outside the same recognized script workflow.
  • If local findings remain suspicious or unresolved, is this part of broader obfuscated PowerShell activity?
  • Focus: related alerts for user.id in the last 48 hours to test whether this obfuscation follows the actor. !{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 host.id related alerts in the last 48 hours to test whether obfuscated PowerShell, encoded command, download, or persistence alerts stay localized to the asset. !{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: escalate scope when the same user or host shows repeated obfuscated PowerShell or adjacent suspicious behavior; keep scope local when the alert is isolated and local evidence resolves to one recognized workflow.
  • Based on script reconstruction, string use, origin, launch context, effects, and broader scope, what disposition is supported?
  • Escalate on string use supporting hidden execution, download, payload handling, persistence, or unresolved high-risk fragments. Close only when reconstruction, origin, launch/effects if available, and related-alert scope bind the alert to one exact recognized workflow; with mixed or incomplete evidence, preserve artifacts and escalate.

False positive analysis

  • Internal templating, packaging, or configuration-generation scripts may concatenate many string literals to build arguments, paths, or output text. Confirm recovered powershell.file.script_block_text, reconstructed string family, file.path, user.id plus host.id scope, and any launch context align with one recognized repository, build, deployment, or administrative workflow. Without repository or change context, do not rely on recurrence alone; close only when local telemetry proves the exact benign workflow.
  • Compatibility wrappers or vendor-protected administrative scripts may fragment helper names, module paths, or destination strings at runtime. Confirm reconstructed strings map to recognized internal domains, script paths, module functions, or vendor helpers and side effects stay inside that workflow. Without vendor notes or admin records, do not rely on recurrence alone; close only when local telemetry proves the exact helper workflow.
  • Before creating an exception, anchor it on stable file.path, reconstructed string family, host.id or user.id, and recovered launch context when available. Avoid exceptions on alert-local Esql.script_block_pattern_count, Esql.script_block_length, Esql.script_block_tmp, user.name, or powershell.file.script_block_text alone.

Response and remediation

  • If confirmed benign, record the evidence that proved the workflow: reconstructed strings, recovered file.path, user-host scope, launch context when available, and lack of contradictory side effects. Then reverse temporary containment. Create an exception only when that evidence pattern is narrow enough to avoid suppressing lookalike obfuscation; recurrence strengthens the case but is not required when local proof is complete.
  • If suspicious but unconfirmed, preserve the alert record, source 4104 events, full powershell.file.script_block_text, powershell.file.script_block_id, powershell.sequence, powershell.total, reconstructed strings, file.path, host.id, user.id, and any recovered process, file, DNS, or destination artifacts before containment or cleanup.
  • If suspicious but unconfirmed, apply reversible containment tied to the findings, such as heightened monitoring, outbound restrictions, or temporary PowerShell controls on the affected host or account. Escalate to host isolation only when launch context or follow-on activity indicates likely payload execution or spread.
  • If confirmed malicious, record recovered process identifiers, command lines, parent context, script fragments, reconstructed strings, staged files, and destination indicators before isolation, process termination, or suspension. Then isolate the endpoint when host role allows and restrict the affected account if identity misuse is evident.
  • Review related hosts and users for the same reconstructed string family, file.path, origin pattern, 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 the obfuscated PowerShell execution.
  • Post-incident hardening: retain Script Block Logging and endpoint telemetry that enabled reconstruction, restrict PowerShell where it is not required, and record any telemetry gaps that limited reconstruction or containment.

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"

// Filter out smaller scripts that are unlikely to implement obfuscation using the patterns we are looking for
| eval Esql.script_block_length = length(powershell.file.script_block_text)
| where Esql.script_block_length > 500

// 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,
    """['"][A-Za-z0-9.]+['"](\s?\+\s?['"][A-Za-z0-9.,\-\s]+['"]){2,}""",
    "🔥"
)

// 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_length,
    Esql.script_block_tmp,
    powershell.file.*,
    file.path,
    powershell.sequence,
    powershell.total,
    _id,
    _version,
    _index,
    host.name,
    host.id,
    agent.id,
    user.id,
    process.pid

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

Framework: MITRE ATT&CKTM