Potential PowerShell Obfuscation via Backtick-Escaped Variable Expansion
Detects PowerShell scripts that use backtick-escaped characters inside ${} variable expansion (multiple backticks
between word characters) to reconstruct strings at runtime. Attackers use variable-expansion obfuscation to split
keywords, hide commands, and evade static analysis and AMSI.
Elastic rule (View on GitHub)
1[metadata]
2creation_date = "2025/04/16"
3integration = ["windows"]
4maturity = "production"
5updated_date = "2026/04/30"
6
7[rule]
8author = ["Elastic"]
9description = """
10Detects PowerShell scripts that use backtick-escaped characters inside `${}` variable expansion (multiple backticks
11between word characters) to reconstruct strings at runtime. Attackers use variable-expansion obfuscation to split
12keywords, hide commands, and evade static analysis and AMSI.
13"""
14from = "now-9m"
15language = "esql"
16license = "Elastic License v2"
17name = "Potential PowerShell Obfuscation via Backtick-Escaped Variable Expansion"
18risk_score = 73
19rule_id = "d43f2b43-02a1-4219-8ce9-10929a32a618"
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"
35
36// Filter out smaller scripts that are unlikely to implement obfuscation using the patterns we are looking for
37| eval Esql.script_block_length = length(powershell.file.script_block_text)
38| where Esql.script_block_length > 500
39
40// replace the patterns we are looking for with the 🔥 emoji to enable counting them
41// The emoji is used because it's unlikely to appear in scripts and has a consistent character length of 1
42| eval Esql.script_block_tmp = replace(powershell.file.script_block_text, """\$\{(\w++`){2,}\w++\}""", "🔥")
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_length,
51 Esql.script_block_tmp,
52 powershell.file.*,
53 file.path,
54 file.name,
55 process.pid,
56 powershell.sequence,
57 powershell.total,
58 _id,
59 _version,
60 _index,
61 host.name,
62 host.id,
63 agent.id,
64 user.id
65
66// Filter for scripts that match the pattern at least once
67| where Esql.script_block_pattern_count >= 1
68'''
69
70note = """## Triage and analysis
71
72### Investigating Potential PowerShell Obfuscation via Backtick-Escaped Variable Expansion
73
74#### Possible investigation steps
75
76- Did you reconstruct the complete alerting script block and confirm the escaped-variable pattern?
77 - Focus: alert-local `Esql.script_block_pattern_count`, `Esql.script_block_tmp`, `Esql.script_block_length`, and reconstructed `powershell.file.script_block_text`. $investigate_2
78 - Hint: query PowerShell Operational events from logs-windows.powershell_operational*, then reconstruct with `powershell.file.script_block_id` + `powershell.sequence` + `powershell.total` on the same `host.id`; confirm fragment count before judging intent.
79 - Implication: escalate when the complete block repeats ${} backtick expansion across command, variable, or string-building logic; lower suspicion when one isolated escaped token sits inside readable build or template code. Missing fragments keep the alert unresolved, not benign.
80
81- What execution-critical text appears when the backticks inside ${} are removed?
82 - Focus: reconstructed `powershell.file.script_block_text`, alert-local `Esql.script_block_tmp`, and the command, variable, or string tokens exposed by removing the backticks.
83 - Implication: escalate when escaped expansions hide cmdlets, invocation operators, download strings, encoded payloads, AMSI or logging bypass names, or variables that feed execution; lower suspicion when they only protect literal template placeholders and no decoded token changes execution.
84
85- Does the script origin and user-host context fit one bounded generation workflow?
86 - Focus: `file.path`, `file.name`, `user.id`, `host.name`, and `host.id`.
87 - Implication: escalate when `file.path` is absent for a long obfuscated block, the path is user-writable, temporary, or delivery-oriented, or the account-host pair does not fit script generation or deployment; lower suspicion only when origin, account, host, and decoded content match one recognized build, packaging, updater, or test workflow.
88
89- If process telemetry is available, how was the PowerShell instance launched?
90 - Focus: alert-preserved `process.pid`, plus recovered `process.executable`, `process.command_line`, `process.parent.executable`, and `process.parent.command_line`.
91 - Hint: recover the matching process via `host.id + process.pid`; around alert `@timestamp`, prefer the closest start event for that `host.id` if PID reuse creates multiple matches. $investigate_3
92 - Implication: escalate when the chain starts from a browser, document process, archive, remote tooling, scheduled task, non-PowerShell host process using System.Management.Automation, or encoded/in-memory command path that does not fit the script purpose; lower suspicion when executable, parent, and command line match the same recognized build, packaging, updater, or test workflow. Missing endpoint process telemetry keeps lineage unresolved, not benign.
93
94- Does the decoded content request a second execution stage?
95 - Focus: decoded execution, download, credential, policy, persistence, or payload-staging commands in `powershell.file.script_block_text`.
96 - Hint: after process recovery via `host.id + process.pid`, review child events where `process.parent.entity_id` equals the recovered PowerShell `process.entity_id`; use `host.id` plus `process.parent.pid` as a weaker tight-window fallback. $investigate_4
97 - Implication: escalate when hidden tokens feed execution operators, downloaded content, child processes, credential access, policy tampering, persistence, or payload staging; lower suspicion when decoded actions stay inside the same recognized generation or update task and no second execution path appears. Missing endpoint process telemetry leaves child-process correlation unresolved, not benign.
98
99- If local evidence remains suspicious or unresolved, does this escaped-variable pattern appear elsewhere?
100 - Focus: related alerts for `user.id` and `host.id` in the last 48 hours, decoded token fragments from `powershell.file.script_block_text`, and the same `file.path` when present.
101 - Hint: start with related alerts for the same `user.id`; if sparse, pivot to the same `host.id`. $investigate_0 $investigate_1
102 - Implication: broaden scope when the same escaped-variable technique or decoded execution strings appear on unrelated hosts, users, or source paths; keep local when recurrence stays inside the same confirmed workflow and local evidence is otherwise clean.
103
104- Escalate on strong unauthorized execution evidence from decoded tokens, fileless or unusual origin, launch chain, second-stage behavior, or repeated scope; close only when telemetry and any needed owner or change confirmation explain every suspicious token as one recognized build, packaging, updater, or test workflow; preserve and escalate when fragments, endpoint process telemetry, or workflow proof are missing for a suspicious script.
105
106### False positive analysis
107
108- Code generation, packaging, build, updater, bootstrap, or test harness workflows can emit backtick-escaped ${} sequences while producing PowerShell text. Confirm only when reconstructed `powershell.file.script_block_text` is limited to template, packaging, installer, updater, or test logic; `file.path` or its absence fits that source; any recovered launch chain supports the same tool; and `user.id` plus `host.id` match the same operating scope. If change records are unavailable, require the same file origin, decoded token family, and user-host scope across prior alerts from this rule.
109- Treat one benign-looking token as insufficient for closure. Do not close when decoded content contains execution, download, defense-evasion, credential, or persistence logic that the named workflow does not require.
110- Before creating an exception, anchor it to stable indexed fields such as `user.id`, `host.id`, and `file.path` or `file.name`, plus the stable decoded token family and recovered parent context when available. Do not use `Esql.script_block_pattern_count` or `Esql.script_block_tmp` in exceptions because they are alert-local summaries, not exception-safe fields.
111
112### Response and remediation
113
114- If confirmed benign, document the evidence that explained the alert first: reconstructed script intent, file origin or fileless source, `user.id`, `host.id`, and the recovered launch context when available. Then reverse any temporary containment and create a narrow exception only after the same workflow pattern is stable across prior alerts.
115- If suspicious but unconfirmed, preserve the alert, reconstructed `powershell.file.script_block_text`, `powershell.file.script_block_id`, ordered 4104 fragments, file origin, host-user scope, recovered launch context when available, and decoded indicators before containment. Apply reversible containment such as heightened monitoring or host isolation only if the host role can tolerate it, then escalate before deleting artifacts or resetting credentials.
116- If confirmed malicious, preserve the same script, source-event, process, and decoded-indicator evidence before destructive action. Isolate the host when the evidence shows unauthorized execution and host criticality allows it, record the recovered process identifier before termination, block confirmed malicious decoded indicators, and remove only the scripts, payloads, startup items, policy changes, or persistence artifacts identified during the investigation. Reset credentials only when the investigation shows account misuse beyond local script execution.
117- Post-incident hardening: retain PowerShell script-block logging, keep endpoint process telemetry sufficient for `host.id + process.pid` recovery, restrict recurring script generation to recognized signed tooling and service scopes, and document the confirmed benign workflow or malicious decoded token family for future triage.
118"""
119
120setup = """## Setup
121
122PowerShell Script Block Logging must be enabled to generate the events used by this rule (e.g., 4104).
123Setup instructions: https://ela.st/powershell-logging-setup
124"""
125
126[rule.investigation_fields]
127field_names = [
128 "@timestamp",
129 "user.id",
130 "powershell.file.script_block_text",
131 "powershell.file.script_block_id",
132 "powershell.sequence",
133 "powershell.total",
134 "file.path",
135 "file.name",
136 "host.name",
137 "host.id",
138 "process.pid",
139 "Esql.script_block_pattern_count",
140 "Esql.script_block_length"
141]
142
143[transform]
144
145[[transform.investigate]]
146label = "Alerts associated with the user"
147description = ""
148providers = [
149 [
150 { excluded = false, field = "event.kind", queryType = "phrase", value = "signal", valueType = "string" },
151 { excluded = false, field = "user.id", queryType = "phrase", value = "{{user.id}}", valueType = "string" }
152 ]
153]
154relativeFrom = "now-48h/h"
155relativeTo = "now"
156
157[[transform.investigate]]
158label = "Alerts associated with the host"
159description = ""
160providers = [
161 [
162 { excluded = false, field = "event.kind", queryType = "phrase", value = "signal", valueType = "string" },
163 { excluded = false, field = "host.id", queryType = "phrase", value = "{{host.id}}", valueType = "string" }
164 ]
165]
166relativeFrom = "now-48h/h"
167relativeTo = "now"
168
169[[transform.investigate]]
170label = "Script block fragments for the same script"
171description = ""
172providers = [
173 [
174 { excluded = false, field = "powershell.file.script_block_id", queryType = "phrase", value = "{{powershell.file.script_block_id}}", valueType = "string" },
175 { excluded = false, field = "host.id", queryType = "phrase", value = "{{host.id}}", valueType = "string" }
176 ]
177]
178relativeFrom = "now-1h"
179relativeTo = "now"
180
181[[transform.investigate]]
182label = "Process events for the PowerShell instance"
183description = ""
184providers = [
185 [
186 { excluded = false, field = "process.pid", queryType = "phrase", value = "{{process.pid}}", valueType = "string" },
187 { excluded = false, field = "host.id", queryType = "phrase", value = "{{host.id}}", valueType = "string" },
188 { excluded = false, field = "event.category", queryType = "phrase", value = "process", valueType = "string" }
189 ]
190]
191relativeFrom = "now-1h"
192relativeTo = "now"
193
194[[transform.investigate]]
195label = "Child process activity from the PowerShell instance"
196description = ""
197providers = [
198 [
199 { excluded = false, field = "process.parent.pid", queryType = "phrase", value = "{{process.pid}}", valueType = "string" },
200 { excluded = false, field = "host.id", queryType = "phrase", value = "{{host.id}}", valueType = "string" },
201 { excluded = false, field = "event.category", queryType = "phrase", value = "process", valueType = "string" },
202 { excluded = false, field = "event.type", queryType = "phrase", value = "start", valueType = "string" }
203 ]
204]
205relativeFrom = "now-1h"
206relativeTo = "now"
207
208[[rule.threat]]
209framework = "MITRE ATT&CK"
210
211[[rule.threat.technique]]
212id = "T1027"
213name = "Obfuscated Files or Information"
214reference = "https://attack.mitre.org/techniques/T1027/"
215
216[[rule.threat.technique.subtechnique]]
217id = "T1027.010"
218name = "Command Obfuscation"
219reference = "https://attack.mitre.org/techniques/T1027/010/"
220
221[[rule.threat.technique]]
222id = "T1140"
223name = "Deobfuscate/Decode Files or Information"
224reference = "https://attack.mitre.org/techniques/T1140/"
225
226[rule.threat.tactic]
227id = "TA0005"
228name = "Defense Evasion"
229reference = "https://attack.mitre.org/tactics/TA0005/"
230
231[[rule.threat]]
232framework = "MITRE ATT&CK"
233
234[[rule.threat.technique]]
235id = "T1059"
236name = "Command and Scripting Interpreter"
237reference = "https://attack.mitre.org/techniques/T1059/"
238
239[[rule.threat.technique.subtechnique]]
240id = "T1059.001"
241name = "PowerShell"
242reference = "https://attack.mitre.org/techniques/T1059/001/"
243
244[rule.threat.tactic]
245id = "TA0002"
246name = "Execution"
247reference = "https://attack.mitre.org/tactics/TA0002/"
Triage and analysis
Investigating Potential PowerShell Obfuscation via Backtick-Escaped Variable Expansion
Possible investigation steps
-
Did you reconstruct the complete alerting script block and confirm the escaped-variable pattern?
- Focus: alert-local
Esql.script_block_pattern_count,Esql.script_block_tmp,Esql.script_block_length, and reconstructedpowershell.file.script_block_text. $investigate_2 - Hint: query PowerShell Operational events from logs-windows.powershell_operational*, then reconstruct with
powershell.file.script_block_id+powershell.sequence+powershell.totalon the samehost.id; confirm fragment count before judging intent. - Implication: escalate when the complete block repeats ${} backtick expansion across command, variable, or string-building logic; lower suspicion when one isolated escaped token sits inside readable build or template code. Missing fragments keep the alert unresolved, not benign.
- Focus: alert-local
-
What execution-critical text appears when the backticks inside ${} are removed?
- Focus: reconstructed
powershell.file.script_block_text, alert-localEsql.script_block_tmp, and the command, variable, or string tokens exposed by removing the backticks. - Implication: escalate when escaped expansions hide cmdlets, invocation operators, download strings, encoded payloads, AMSI or logging bypass names, or variables that feed execution; lower suspicion when they only protect literal template placeholders and no decoded token changes execution.
- Focus: reconstructed
-
Does the script origin and user-host context fit one bounded generation workflow?
- Focus:
file.path,file.name,user.id,host.name, andhost.id. - Implication: escalate when
file.pathis absent for a long obfuscated block, the path is user-writable, temporary, or delivery-oriented, or the account-host pair does not fit script generation or deployment; lower suspicion only when origin, account, host, and decoded content match one recognized build, packaging, updater, or test workflow.
- Focus:
-
If process telemetry is available, how was the PowerShell instance launched?
- Focus: alert-preserved
process.pid, plus recoveredprocess.executable,process.command_line,process.parent.executable, andprocess.parent.command_line. - Hint: recover the matching process via
host.id + process.pid; around alert@timestamp, prefer the closest start event for thathost.idif PID reuse creates multiple matches. $investigate_3 - Implication: escalate when the chain starts from a browser, document process, archive, remote tooling, scheduled task, non-PowerShell host process using System.Management.Automation, or encoded/in-memory command path that does not fit the script purpose; lower suspicion when executable, parent, and command line match the same recognized build, packaging, updater, or test workflow. Missing endpoint process telemetry keeps lineage unresolved, not benign.
- Focus: alert-preserved
-
Does the decoded content request a second execution stage?
- Focus: decoded execution, download, credential, policy, persistence, or payload-staging commands in
powershell.file.script_block_text. - Hint: after process recovery via
host.id + process.pid, review child events whereprocess.parent.entity_idequals the recovered PowerShellprocess.entity_id; usehost.idplusprocess.parent.pidas a weaker tight-window fallback. $investigate_4 - Implication: escalate when hidden tokens feed execution operators, downloaded content, child processes, credential access, policy tampering, persistence, or payload staging; lower suspicion when decoded actions stay inside the same recognized generation or update task and no second execution path appears. Missing endpoint process telemetry leaves child-process correlation unresolved, not benign.
- Focus: decoded execution, download, credential, policy, persistence, or payload-staging commands in
-
If local evidence remains suspicious or unresolved, does this escaped-variable pattern appear elsewhere?
- Focus: related alerts for
user.idandhost.idin the last 48 hours, decoded token fragments frompowershell.file.script_block_text, and the samefile.pathwhen present. - Hint: start with related alerts for the same
user.id; if sparse, pivot to the samehost.id. $investigate_0 $investigate_1 - Implication: broaden scope when the same escaped-variable technique or decoded execution strings appear on unrelated hosts, users, or source paths; keep local when recurrence stays inside the same confirmed workflow and local evidence is otherwise clean.
- Focus: related alerts for
-
Escalate on strong unauthorized execution evidence from decoded tokens, fileless or unusual origin, launch chain, second-stage behavior, or repeated scope; close only when telemetry and any needed owner or change confirmation explain every suspicious token as one recognized build, packaging, updater, or test workflow; preserve and escalate when fragments, endpoint process telemetry, or workflow proof are missing for a suspicious script.
False positive analysis
- Code generation, packaging, build, updater, bootstrap, or test harness workflows can emit backtick-escaped ${} sequences while producing PowerShell text. Confirm only when reconstructed
powershell.file.script_block_textis limited to template, packaging, installer, updater, or test logic;file.pathor its absence fits that source; any recovered launch chain supports the same tool; anduser.idplushost.idmatch the same operating scope. If change records are unavailable, require the same file origin, decoded token family, and user-host scope across prior alerts from this rule. - Treat one benign-looking token as insufficient for closure. Do not close when decoded content contains execution, download, defense-evasion, credential, or persistence logic that the named workflow does not require.
- Before creating an exception, anchor it to stable indexed fields such as
user.id,host.id, andfile.pathorfile.name, plus the stable decoded token family and recovered parent context when available. Do not useEsql.script_block_pattern_countorEsql.script_block_tmpin exceptions because they are alert-local summaries, not exception-safe fields.
Response and remediation
- If confirmed benign, document the evidence that explained the alert first: reconstructed script intent, file origin or fileless source,
user.id,host.id, and the recovered launch context when available. Then reverse any temporary containment and create a narrow exception only after the same workflow pattern is stable across prior alerts. - If suspicious but unconfirmed, preserve the alert, reconstructed
powershell.file.script_block_text,powershell.file.script_block_id, ordered 4104 fragments, file origin, host-user scope, recovered launch context when available, and decoded indicators before containment. Apply reversible containment such as heightened monitoring or host isolation only if the host role can tolerate it, then escalate before deleting artifacts or resetting credentials. - If confirmed malicious, preserve the same script, source-event, process, and decoded-indicator evidence before destructive action. Isolate the host when the evidence shows unauthorized execution and host criticality allows it, record the recovered process identifier before termination, block confirmed malicious decoded indicators, and remove only the scripts, payloads, startup items, policy changes, or persistence artifacts identified during the investigation. Reset credentials only when the investigation shows account misuse beyond local script execution.
- Post-incident hardening: retain PowerShell script-block logging, keep endpoint process telemetry sufficient for
host.id + process.pidrecovery, restrict recurring script generation to recognized signed tooling and service scopes, and document the confirmed benign workflow or malicious decoded token family for future triage.
Related rules
- Potential PowerShell Obfuscation via Character Array Reconstruction
- Potential PowerShell Obfuscation via Concatenated Dynamic Command Invocation
- Potential PowerShell Obfuscation via String Concatenation
- Potential Process Injection via PowerShell
- Deprecated - Potential PowerShell Obfuscated Script