Potential PowerShell Obfuscation via Concatenated Dynamic Command Invocation

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.

Elastic rule (View on GitHub)

  1[metadata]
  2creation_date = "2025/04/15"
  3integration = ["windows"]
  4maturity = "production"
  5updated_date = "2026/04/30"
  6
  7[rule]
  8author = ["Elastic"]
  9description = """
 10Detects PowerShell scripts that builds commands from concatenated string literals inside dynamic invocation constructs
 11like &() or .(). Attackers use concatenated dynamic invocation to obscure execution intent, bypass keyword-based
 12detections, and evade AMSI.
 13"""
 14from = "now-9m"
 15language = "esql"
 16license = "Elastic License v2"
 17name = "Potential PowerShell Obfuscation via Concatenated Dynamic Command Invocation"
 18risk_score = 73
 19rule_id = "083383af-b9a4-42b7-a463-29c40efe7797"
 20severity = "high"
 21tags = [
 22    "Domain: Endpoint",
 23    "OS: Windows",
 24    "Use Case: Threat Detection",
 25    "Tactic: Defense Evasion",
 26    "Data Source: PowerShell Logs",
 27    "Resources: Investigation Guide",
 28]
 29timestamp_override = "event.ingested"
 30type = "esql"
 31
 32query = '''
 33from logs-windows.powershell_operational* metadata _id, _version, _index
 34| where event.code == "4104" and powershell.file.script_block_text like "*+*"
 35
 36// replace the patterns we are looking for with the 🔥 emoji to enable counting them
 37// The emoji is used because it's unlikely to appear in scripts and has a consistent character length of 1
 38| eval Esql.script_block_tmp = replace(
 39    powershell.file.script_block_text,
 40    """[.&]\(\s*(['"][A-Za-z0-9.-]+['"]\s*\+\s*)+['"][A-Za-z0-9.-]+['"]\s*\)""",
 41    "🔥"
 42)
 43
 44// count how many patterns were detected by calculating the number of 🔥 characters inserted
 45| eval Esql.script_block_pattern_count = length(Esql.script_block_tmp) - length(replace(Esql.script_block_tmp, "🔥", ""))
 46
 47// keep the fields relevant to the query, although this is not needed as the alert is populated using _id
 48| keep
 49    Esql.script_block_pattern_count,
 50    Esql.script_block_tmp,
 51    powershell.file.*,
 52    file.path,
 53    process.pid,
 54    powershell.sequence,
 55    powershell.total,
 56    _id,
 57    _version,
 58    _index,
 59    host.name,
 60    host.id,
 61    agent.id,
 62    user.id
 63
 64// Filter for scripts that match the pattern at least once
 65| where Esql.script_block_pattern_count >= 1
 66'''
 67
 68note = """## Triage and analysis
 69
 70### Investigating Potential PowerShell Obfuscation via Concatenated Dynamic Command Invocation
 71
 72#### Possible investigation steps
 73
 74- What did the alert preserve about the concatenated dynamic invocation?
 75  - Focus: `Esql.script_block_pattern_count`, `powershell.file.script_block_text`, and `powershell.file.script_block_id`.
 76  - Hint: use full-alert `Esql.script_block_tmp` only when you need the match-local slice.
 77  - 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.
 78- Can you reconstruct the full source 4104 script block before interpreting context?
 79  - Why: PowerShell can split large script blocks, and this ES|QL alert keeps summary fields that do not replace source-event recovery.
 80  - 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_2
 81  - Implication: incomplete fragments are unresolved, not benign; escalation is stronger when reconstruction exposes hidden stages, fileless delivery, or missing execution context.
 82- What command or script does the concatenation resolve to, and does the operator expand impact?
 83  - Focus: reconstructed `powershell.file.script_block_text`, surrounding variable assignments, and call-operator versus dot-sourcing use.
 84  - 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.
 85- Does the source event show a file-backed or fileless origin that fits this user and host?
 86  - Focus: recovered `file.path`, `user.id`, source-event `user.name`, source-event `user.domain`, and `host.id`.
 87  - 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.
 88  - Hint: absent `file.path` after source recovery means interactive, pasted, or memory-only activity; require stronger corroboration before closure.
 89- Can you recover the PowerShell process launch chain?
 90  - Focus: source `process.pid` plus same-host process-start telemetry for recovered `process.entity_id`, `process.command_line`, and `process.parent.executable`.
 91  - 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_3
 92  - 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.
 93- Does the reconstructed script show layered obfuscation or payload-delivery logic beyond concatenation?
 94  - 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.
 95  - Implication: escalate when concatenation sits beside encoding, reflection, decoder routines, download strings, hidden payload material, dead-code padding, or Get-Command wildcard resolution.
 96- Did the recovered process or host-window activity retrieve, stage, or execute follow-on content?
 97  - 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`.
 98  - 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_4 $investigate_5 $investigate_6
 99  - Implication: escalate when the same process chain spawns shells, writes scripts or binaries, or reaches rare external destinations.
100- If local findings remain suspicious or unresolved, does related alert history change scope?
101  - 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_0
102  - Hint: if the user view is sparse or shared, pivot to the same `host.id` in the last 48 hours. $investigate_1
103  - 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.
104
105- 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.
106
107### False positive analysis
108
109- 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.
110- 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.
111
112### Response and remediation
113
114- 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.
115- 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.
116- 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.
117- 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.
118- 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.
119- 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.
120- 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."""
121
122setup = """## Setup
123
124PowerShell Script Block Logging must be enabled to generate the events used by this rule (e.g., 4104).
125Setup instructions: https://ela.st/powershell-logging-setup
126"""
127
128[rule.investigation_fields]
129field_names = [
130    "@timestamp",
131    "host.name",
132    "host.id",
133    "user.id",
134    "file.path",
135    "process.pid",
136    "powershell.file.script_block_text",
137    "powershell.file.script_block_id",
138    "powershell.sequence",
139    "powershell.total",
140    "powershell.file.script_block_entropy_bits",
141    "powershell.file.script_block_surprisal_stdev",
142    "powershell.file.script_block_unique_symbols",
143    "powershell.file.script_block_length",
144    "Esql.script_block_pattern_count"
145]
146
147[transform]
148
149[[transform.investigate]]
150label = "Alerts associated with the user"
151description = ""
152providers = [
153  [
154    { excluded = false, field = "event.kind", queryType = "phrase", value = "signal", valueType = "string" },
155    { excluded = false, field = "user.id", queryType = "phrase", value = "{{user.id}}", valueType = "string" }
156  ]
157]
158relativeFrom = "now-48h/h"
159relativeTo = "now"
160
161[[transform.investigate]]
162label = "Alerts associated with the host"
163description = ""
164providers = [
165  [
166    { excluded = false, field = "event.kind", queryType = "phrase", value = "signal", valueType = "string" },
167    { excluded = false, field = "host.id", queryType = "phrase", value = "{{host.id}}", valueType = "string" }
168  ]
169]
170relativeFrom = "now-48h/h"
171relativeTo = "now"
172
173[[transform.investigate]]
174label = "Script block fragments for the same script"
175description = ""
176providers = [
177  [
178    { excluded = false, field = "powershell.file.script_block_id", queryType = "phrase", value = "{{powershell.file.script_block_id}}", valueType = "string" },
179    { excluded = false, field = "host.id", queryType = "phrase", value = "{{host.id}}", valueType = "string" }
180  ]
181]
182relativeFrom = "now-1h"
183relativeTo = "now"
184
185[[transform.investigate]]
186label = "Process events for the PowerShell instance"
187description = ""
188providers = [
189  [
190    { excluded = false, field = "process.pid", queryType = "phrase", value = "{{process.pid}}", valueType = "string" },
191    { excluded = false, field = "host.id", queryType = "phrase", value = "{{host.id}}", valueType = "string" },
192    { excluded = false, field = "event.category", queryType = "phrase", value = "process", valueType = "string" }
193  ]
194]
195relativeFrom = "now-1h"
196relativeTo = "now"
197
198[[transform.investigate]]
199label = "Child process activity from the PowerShell instance"
200description = ""
201providers = [
202  [
203    { excluded = false, field = "process.parent.pid", queryType = "phrase", value = "{{process.pid}}", valueType = "string" },
204    { excluded = false, field = "host.id", queryType = "phrase", value = "{{host.id}}", valueType = "string" },
205    { excluded = false, field = "event.category", queryType = "phrase", value = "process", valueType = "string" },
206    { excluded = false, field = "event.type", queryType = "phrase", value = "start", valueType = "string" }
207  ]
208]
209relativeFrom = "now-1h"
210relativeTo = "now"
211
212[[transform.investigate]]
213label = "File, network, and DNS events for the PowerShell PID"
214description = ""
215providers = [
216  [
217    { excluded = false, field = "event.category", queryType = "phrase", value = "file", valueType = "string" },
218    { excluded = false, field = "host.id", queryType = "phrase", value = "{{host.id}}", valueType = "string" },
219    { excluded = false, field = "process.pid", queryType = "phrase", value = "{{process.pid}}", valueType = "string" }
220  ],
221  [
222    { excluded = false, field = "event.category", queryType = "phrase", value = "network", valueType = "string" },
223    { excluded = false, field = "host.id", queryType = "phrase", value = "{{host.id}}", valueType = "string" },
224    { excluded = false, field = "process.pid", queryType = "phrase", value = "{{process.pid}}", valueType = "string" }
225  ],
226  [
227    { excluded = false, field = "event.category", queryType = "phrase", value = "dns", valueType = "string" },
228    { excluded = false, field = "host.id", queryType = "phrase", value = "{{host.id}}", valueType = "string" },
229    { excluded = false, field = "process.pid", queryType = "phrase", value = "{{process.pid}}", valueType = "string" }
230  ]
231]
232relativeFrom = "now-1h"
233relativeTo = "now"
234
235[[transform.investigate]]
236label = "Script block events for the PowerShell PID"
237description = ""
238providers = [
239  [
240    { excluded = false, field = "event.code", queryType = "phrase", value = "4104", valueType = "string" },
241    { excluded = false, field = "host.id", queryType = "phrase", value = "{{host.id}}", valueType = "string" },
242    { excluded = false, field = "process.pid", queryType = "phrase", value = "{{process.pid}}", valueType = "string" }
243  ]
244]
245relativeFrom = "now-1h"
246relativeTo = "now"
247
248[[rule.threat]]
249framework = "MITRE ATT&CK"
250
251[[rule.threat.technique]]
252id = "T1027"
253name = "Obfuscated Files or Information"
254reference = "https://attack.mitre.org/techniques/T1027/"
255
256[[rule.threat.technique.subtechnique]]
257id = "T1027.010"
258name = "Command Obfuscation"
259reference = "https://attack.mitre.org/techniques/T1027/010/"
260
261[[rule.threat.technique]]
262id = "T1140"
263name = "Deobfuscate/Decode Files or Information"
264reference = "https://attack.mitre.org/techniques/T1140/"
265
266[rule.threat.tactic]
267id = "TA0005"
268name = "Defense Evasion"
269reference = "https://attack.mitre.org/tactics/TA0005/"
270
271[[rule.threat]]
272framework = "MITRE ATT&CK"
273
274[[rule.threat.technique]]
275id = "T1059"
276name = "Command and Scripting Interpreter"
277reference = "https://attack.mitre.org/techniques/T1059/"
278
279[[rule.threat.technique.subtechnique]]
280id = "T1059.001"
281name = "PowerShell"
282reference = "https://attack.mitre.org/techniques/T1059/001/"
283
284[rule.threat.tactic]
285id = "TA0002"
286name = "Execution"
287reference = "https://attack.mitre.org/tactics/TA0002/"

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_2
    • 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_3
    • 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_4 $investigate_5 $investigate_6
    • 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_0
    • Hint: if the user view is sparse or shared, pivot to the same host.id in the last 48 hours. $investigate_1
    • 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.

Related rules

to-top