muaddib-scanner 2.10.21 → 2.10.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -11
- package/bin/muaddib.js +365 -39
- package/package.json +1 -1
- package/scripts/benchmark.js +326 -0
- package/src/index.js +34 -3
- package/src/ioc/scraper.js +4 -141
- package/src/ioc/updater.js +26 -7
- package/src/response/playbooks.js +112 -0
- package/src/rules/index.js +324 -1
- package/src/scanner/ast-detectors.js +744 -12
- package/src/scanner/ast.js +62 -1
- package/src/scanner/package.js +70 -0
- package/src/scanner/shell.js +5 -5
- package/src/scoring.js +76 -6
- package/src/shared/constants.js +5 -5
- package/src/utils.js +8 -0
|
@@ -223,9 +223,14 @@ const BLOCKCHAIN_RPC_ENDPOINTS = [
|
|
|
223
223
|
// Solana/Web3 C2 methods — used for dead drop resolver (GlassWorm)
|
|
224
224
|
const SOLANA_C2_METHODS = [
|
|
225
225
|
'getSignaturesForAddress', 'getAccountInfo', 'getTransaction',
|
|
226
|
-
'getConfirmedSignaturesForAddress2', 'getParsedTransaction'
|
|
226
|
+
'getConfirmedSignaturesForAddress2', 'getParsedTransaction',
|
|
227
|
+
// Blue Team v8: extended Ethereum/Web3 C2 methods
|
|
228
|
+
'eth_call', 'getCode', 'getLogs'
|
|
227
229
|
];
|
|
228
230
|
|
|
231
|
+
// Blue Team v8: Ethereum/Web3 package names for compound blockchain C2 detection
|
|
232
|
+
const ETHEREUM_PACKAGES = ['ethers', 'web3', '@ethersproject/providers', '@ethersproject/contracts'];
|
|
233
|
+
|
|
229
234
|
// Solana/Web3 package names
|
|
230
235
|
const SOLANA_PACKAGES = ['@solana/web3.js', 'solana-web3.js', '@solana/web3'];
|
|
231
236
|
|
|
@@ -531,6 +536,7 @@ function handleVariableDeclarator(node, ctx) {
|
|
|
531
536
|
}
|
|
532
537
|
|
|
533
538
|
// Audit v3 B3: const AF = Object.getPrototypeOf(async function(){}).constructor
|
|
539
|
+
// Blue Team v8: Extended to detect nested getPrototypeOf chains (2+ levels deep)
|
|
534
540
|
if (node.init?.type === 'MemberExpression') {
|
|
535
541
|
const initProp = node.init.computed
|
|
536
542
|
? (node.init.property?.type === 'Literal' ? String(node.init.property.value) : null)
|
|
@@ -557,6 +563,33 @@ function handleVariableDeclarator(node, ctx) {
|
|
|
557
563
|
file: ctx.relFile
|
|
558
564
|
});
|
|
559
565
|
}
|
|
566
|
+
// Blue Team v8: Nested getPrototypeOf — Object.getPrototypeOf(Object.getPrototypeOf(...)).constructor
|
|
567
|
+
// Walking up prototype chain 2+ levels to reach Function constructor from any object
|
|
568
|
+
if (arg.type === 'CallExpression' && arg.callee?.type === 'MemberExpression' &&
|
|
569
|
+
arg.callee.property?.name === 'getPrototypeOf') {
|
|
570
|
+
ctx.evalAliases.set(node.id.name, 'Function');
|
|
571
|
+
ctx.hasDynamicExec = true;
|
|
572
|
+
ctx.threats.push({
|
|
573
|
+
type: 'dangerous_constructor',
|
|
574
|
+
severity: 'CRITICAL',
|
|
575
|
+
message: `Nested Object.getPrototypeOf() chain (2+ levels) + .constructor into "${node.id.name}" — deep prototype traversal to reach Function constructor.`,
|
|
576
|
+
file: ctx.relFile
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
// Blue Team v8b (A3): Object.getPrototypeOf(variable).constructor
|
|
580
|
+
// When a variable (possibly holding a prototype chain result) is passed to
|
|
581
|
+
// getPrototypeOf and .constructor is extracted — prototype chain traversal attack.
|
|
582
|
+
// Covers: const C = Object.getPrototypeOf(protoVar).constructor
|
|
583
|
+
if (arg.type === 'Identifier') {
|
|
584
|
+
ctx.evalAliases.set(node.id.name, 'Function');
|
|
585
|
+
ctx.hasDynamicExec = true;
|
|
586
|
+
ctx.threats.push({
|
|
587
|
+
type: 'prototype_chain_constructor',
|
|
588
|
+
severity: 'CRITICAL',
|
|
589
|
+
message: `Object.getPrototypeOf(${arg.name}).constructor extracted into "${node.id.name}" — prototype chain traversal to reach Function constructor.`,
|
|
590
|
+
file: ctx.relFile
|
|
591
|
+
});
|
|
592
|
+
}
|
|
560
593
|
}
|
|
561
594
|
}
|
|
562
595
|
}
|
|
@@ -591,6 +624,21 @@ function handleVariableDeclarator(node, ctx) {
|
|
|
591
624
|
ctx.stringVarValues.set(node.id.name, strVal);
|
|
592
625
|
}
|
|
593
626
|
|
|
627
|
+
// Blue Team v8b (B7): Track path.join() results where last arg is an image/binary filename
|
|
628
|
+
// Enables steganographic payload detection when the variable is used with fs.readFileSync
|
|
629
|
+
if (!strVal && node.init?.type === 'CallExpression') {
|
|
630
|
+
const initCallName = getCallName(node.init);
|
|
631
|
+
if ((initCallName === 'join' || initCallName === 'resolve') &&
|
|
632
|
+
node.init.callee?.type === 'MemberExpression' &&
|
|
633
|
+
node.init.arguments?.length > 0) {
|
|
634
|
+
const lastArg = node.init.arguments[node.init.arguments.length - 1];
|
|
635
|
+
if (lastArg?.type === 'Literal' && typeof lastArg.value === 'string') {
|
|
636
|
+
// Store the last path component so image extension check works later
|
|
637
|
+
ctx.stringVarValues.set(node.id.name, lastArg.value);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
594
642
|
// Track variables assigned from require.cache[...] (module cache references)
|
|
595
643
|
// Used to detect writes to cached module exports (require.cache poisoning)
|
|
596
644
|
if (node.init?.type === 'MemberExpression' && node.init.computed) {
|
|
@@ -890,6 +938,14 @@ function handleCallExpression(node, ctx) {
|
|
|
890
938
|
if (reqStr && SOLANA_PACKAGES.some(pkg => reqStr === pkg)) {
|
|
891
939
|
ctx.hasSolanaImport = true;
|
|
892
940
|
}
|
|
941
|
+
// Blue Team v8: track Ethereum/Web3 imports
|
|
942
|
+
if (reqStr && ETHEREUM_PACKAGES.some(pkg => reqStr === pkg || reqStr.startsWith(pkg))) {
|
|
943
|
+
ctx.hasSolanaImport = true; // reuse flag — both indicate blockchain SDK usage
|
|
944
|
+
}
|
|
945
|
+
// Blue Team v8: track require('ws') for WebSocket C2 detection
|
|
946
|
+
if (reqStr === 'ws' || reqStr === 'websocket') {
|
|
947
|
+
ctx.hasWebSocketNew = true; // ws module provides WebSocket functionality
|
|
948
|
+
}
|
|
893
949
|
}
|
|
894
950
|
|
|
895
951
|
// Detect process.mainModule.require('child_process') — module system bypass
|
|
@@ -921,6 +977,28 @@ function handleCallExpression(node, ctx) {
|
|
|
921
977
|
}
|
|
922
978
|
}
|
|
923
979
|
|
|
980
|
+
// B8: require('process').mainModule.require('child_process') — indirect process access
|
|
981
|
+
// Pattern: require('process').mainModule.require(mod)
|
|
982
|
+
if (node.callee.type === 'MemberExpression' &&
|
|
983
|
+
node.callee.property?.type === 'Identifier' && node.callee.property.name === 'require' &&
|
|
984
|
+
node.callee.object?.type === 'MemberExpression' &&
|
|
985
|
+
node.callee.object.property?.type === 'Identifier' && node.callee.object.property.name === 'mainModule' &&
|
|
986
|
+
node.callee.object.object?.type === 'CallExpression' &&
|
|
987
|
+
getCallName(node.callee.object.object) === 'require' &&
|
|
988
|
+
node.callee.object.object.arguments?.[0]?.type === 'Literal' &&
|
|
989
|
+
node.callee.object.object.arguments[0].value === 'process') {
|
|
990
|
+
const arg = node.arguments?.[0];
|
|
991
|
+
const modName = arg ? extractStringValueDeep(arg) : null;
|
|
992
|
+
const DANGEROUS_MODS = ['child_process', 'fs', 'net', 'dns', 'http', 'https', 'tls'];
|
|
993
|
+
const severity = modName && DANGEROUS_MODS.includes(modName) ? 'CRITICAL' : 'HIGH';
|
|
994
|
+
ctx.threats.push({
|
|
995
|
+
type: 'require_process_mainmodule',
|
|
996
|
+
severity,
|
|
997
|
+
message: `require('process').mainModule.require(${modName ? "'" + modName + "'" : '...'}) — indirect process access bypasses direct process.mainModule detection.`,
|
|
998
|
+
file: ctx.relFile
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
|
|
924
1002
|
// Detect exec/execSync with dangerous shell commands (direct or via MemberExpression)
|
|
925
1003
|
const execName = callName === 'exec' || callName === 'execSync' ? callName : null;
|
|
926
1004
|
const memberExec = !execName && node.callee.type === 'MemberExpression' &&
|
|
@@ -1040,6 +1118,29 @@ function handleCallExpression(node, ctx) {
|
|
|
1040
1118
|
}
|
|
1041
1119
|
}
|
|
1042
1120
|
|
|
1121
|
+
// Blue Team v8b (C10): require('child_process').execSync/exec(variable) — inline require + variable command
|
|
1122
|
+
// When require is literal 'child_process' but the command argument is not resolvable (MemberExpression, Identifier),
|
|
1123
|
+
// this is a hidden exec with runtime-determined command (C2 pattern: cmd from network data)
|
|
1124
|
+
if ((execName || memberExec) && node.callee.type === 'MemberExpression' &&
|
|
1125
|
+
node.callee.object?.type === 'CallExpression') {
|
|
1126
|
+
const innerCall = node.callee.object;
|
|
1127
|
+
const innerName = getCallName(innerCall);
|
|
1128
|
+
if (innerName === 'require' && innerCall.arguments.length > 0 &&
|
|
1129
|
+
innerCall.arguments[0]?.type === 'Literal' && innerCall.arguments[0].value === 'child_process') {
|
|
1130
|
+
const cmdArg = node.arguments[0];
|
|
1131
|
+
if (cmdArg && cmdArg.type !== 'Literal') {
|
|
1132
|
+
// Non-literal command argument with inline require — opaque shell execution
|
|
1133
|
+
ctx.hasDynamicExec = true;
|
|
1134
|
+
ctx.threats.push({
|
|
1135
|
+
type: 'dangerous_exec',
|
|
1136
|
+
severity: 'HIGH',
|
|
1137
|
+
message: `Inline require('child_process').${execName || memberExec}(variable) — hidden import with runtime-determined command. Typical C2 or RCE payload pattern.`,
|
|
1138
|
+
file: ctx.relFile
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1043
1144
|
// Detect sandbox/container evasion
|
|
1044
1145
|
if (node.callee.type === 'MemberExpression' && node.callee.property?.type === 'Identifier') {
|
|
1045
1146
|
const fsMethod = node.callee.property.name;
|
|
@@ -2042,6 +2143,22 @@ function handleCallExpression(node, ctx) {
|
|
|
2042
2143
|
});
|
|
2043
2144
|
}
|
|
2044
2145
|
}
|
|
2146
|
+
|
|
2147
|
+
// B3: (function(){}).constructor('code')() — direct prototype chain Function access
|
|
2148
|
+
// Also: [].constructor.constructor, ''.constructor.constructor, (0).constructor.constructor
|
|
2149
|
+
if (obj?.type === 'FunctionExpression' || obj?.type === 'ArrowFunctionExpression' ||
|
|
2150
|
+
obj?.type === 'ArrayExpression' || obj?.type === 'Literal') {
|
|
2151
|
+
if (!hasOnlyStringLiteralArgs(node)) {
|
|
2152
|
+
ctx.hasEvalInFile = true;
|
|
2153
|
+
ctx.hasDynamicExec = true;
|
|
2154
|
+
ctx.threats.push({
|
|
2155
|
+
type: 'function_prototype_constructor',
|
|
2156
|
+
severity: 'CRITICAL',
|
|
2157
|
+
message: `Function constructor via prototype chain: (${obj.type === 'FunctionExpression' ? 'function(){}' : obj.type === 'ArrayExpression' ? '[]' : 'literal'}).constructor(code) — bypasses Function/eval detection.`,
|
|
2158
|
+
file: ctx.relFile
|
|
2159
|
+
});
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2045
2162
|
}
|
|
2046
2163
|
|
|
2047
2164
|
// SANDWORM_MODE: Track writeFileSync/writeFile to temp paths
|
|
@@ -2164,9 +2281,37 @@ function handleCallExpression(node, ctx) {
|
|
|
2164
2281
|
file: ctx.relFile
|
|
2165
2282
|
});
|
|
2166
2283
|
}
|
|
2284
|
+
// B1: Reflect.apply(require, null, ['child_process']) — bypasses require() call detection
|
|
2285
|
+
if (target.type === 'Identifier' && target.name === 'require') {
|
|
2286
|
+
const argsArray = node.arguments[2];
|
|
2287
|
+
let modName = null;
|
|
2288
|
+
if (argsArray?.type === 'ArrayExpression' && argsArray.elements.length > 0) {
|
|
2289
|
+
modName = extractStringValueDeep(argsArray.elements[0]);
|
|
2290
|
+
}
|
|
2291
|
+
const severity = modName && ['child_process', 'fs', 'net', 'dns', 'http', 'https'].includes(modName)
|
|
2292
|
+
? 'CRITICAL' : 'HIGH';
|
|
2293
|
+
ctx.threats.push({
|
|
2294
|
+
type: 'reflect_apply_require',
|
|
2295
|
+
severity,
|
|
2296
|
+
message: `Reflect.apply(require, null, [${modName ? "'" + modName + "'" : '...'}]) — indirect require() bypasses static call detection.`,
|
|
2297
|
+
file: ctx.relFile
|
|
2298
|
+
});
|
|
2299
|
+
}
|
|
2167
2300
|
}
|
|
2168
2301
|
}
|
|
2169
2302
|
|
|
2303
|
+
// B4: __defineGetter__ / __defineSetter__ — prototype pollution via legacy API
|
|
2304
|
+
if (node.callee.type === 'MemberExpression' &&
|
|
2305
|
+
node.callee.property?.type === 'Identifier' &&
|
|
2306
|
+
(node.callee.property.name === '__defineGetter__' || node.callee.property.name === '__defineSetter__')) {
|
|
2307
|
+
ctx.threats.push({
|
|
2308
|
+
type: 'prototype_pollution',
|
|
2309
|
+
severity: 'HIGH',
|
|
2310
|
+
message: `${node.callee.property.name}() called — legacy prototype pollution API can hijack property access on any object.`,
|
|
2311
|
+
file: ctx.relFile
|
|
2312
|
+
});
|
|
2313
|
+
}
|
|
2314
|
+
|
|
2170
2315
|
// Batch 1: process.binding('spawn_sync'/'fs') / process._linkedBinding(...)
|
|
2171
2316
|
if (node.callee.type === 'MemberExpression' &&
|
|
2172
2317
|
node.callee.object?.type === 'Identifier' && node.callee.object.name === 'process' &&
|
|
@@ -2194,6 +2339,144 @@ function handleCallExpression(node, ctx) {
|
|
|
2194
2339
|
}
|
|
2195
2340
|
}
|
|
2196
2341
|
|
|
2342
|
+
// Blue Team v8: process.dlopen() — direct native module loading bypass
|
|
2343
|
+
if (node.callee.type === 'MemberExpression' &&
|
|
2344
|
+
node.callee.object?.type === 'Identifier' && node.callee.object.name === 'process' &&
|
|
2345
|
+
node.callee.property?.type === 'Identifier' && node.callee.property.name === 'dlopen' &&
|
|
2346
|
+
node.arguments.length >= 1) {
|
|
2347
|
+
ctx.threats.push({
|
|
2348
|
+
type: 'process_binding_abuse',
|
|
2349
|
+
severity: 'CRITICAL',
|
|
2350
|
+
message: 'process.dlopen() — direct native module loading bypasses require() and all module system checks.',
|
|
2351
|
+
file: ctx.relFile
|
|
2352
|
+
});
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
// Blue Team v8: dgram.createSocket() / socket.send() — UDP exfiltration tracking
|
|
2356
|
+
if (node.callee.type === 'MemberExpression' && node.callee.property?.type === 'Identifier') {
|
|
2357
|
+
const dgramMethod = node.callee.property.name;
|
|
2358
|
+
if (dgramMethod === 'createSocket' && ctx.hasDgramImport) {
|
|
2359
|
+
ctx.hasDgramSend = true; // createSocket implies intent to use UDP
|
|
2360
|
+
}
|
|
2361
|
+
if (dgramMethod === 'send' && ctx.hasDgramImport) {
|
|
2362
|
+
ctx.hasDgramSend = true;
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2366
|
+
// Blue Team v8: Crontab/cron write detection — fs.writeFileSync to cron paths
|
|
2367
|
+
if (node.callee.type === 'MemberExpression' && node.callee.property?.type === 'Identifier') {
|
|
2368
|
+
const cronWriteMethod = node.callee.property.name;
|
|
2369
|
+
if (['writeFileSync', 'writeFile', 'appendFileSync'].includes(cronWriteMethod) && node.arguments.length >= 1) {
|
|
2370
|
+
const cronPathArg = node.arguments[0];
|
|
2371
|
+
let cronPathStr = extractStringValueDeep(cronPathArg);
|
|
2372
|
+
if (!cronPathStr && cronPathArg?.type === 'Identifier' && ctx.stringVarValues?.has(cronPathArg.name)) {
|
|
2373
|
+
cronPathStr = ctx.stringVarValues.get(cronPathArg.name);
|
|
2374
|
+
}
|
|
2375
|
+
if (cronPathStr && (/\/etc\/cron/i.test(cronPathStr) || /crontab/i.test(cronPathStr) ||
|
|
2376
|
+
/\/var\/spool\/cron/i.test(cronPathStr))) {
|
|
2377
|
+
ctx.hasCrontabWrite = true;
|
|
2378
|
+
ctx.threats.push({
|
|
2379
|
+
type: 'crontab_systemd_write',
|
|
2380
|
+
severity: 'CRITICAL',
|
|
2381
|
+
message: `${cronWriteMethod}() writes to cron path: "${cronPathStr.substring(0, 80)}" — scheduled task persistence.`,
|
|
2382
|
+
file: ctx.relFile
|
|
2383
|
+
});
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
|
|
2388
|
+
// Blue Team v8: .replace() chain detection — 3+ chained .replace() calls for string mutation obfuscation
|
|
2389
|
+
// Pattern: 'l33t'.replace(/3/g, 'e').replace(/1/g, 'i') to reconstruct dangerous strings
|
|
2390
|
+
if (node.callee.type === 'MemberExpression' && node.callee.property?.type === 'Identifier' &&
|
|
2391
|
+
node.callee.property.name === 'replace' && node.arguments.length >= 2) {
|
|
2392
|
+
// Walk up the chain to count .replace() depth and try to resolve
|
|
2393
|
+
let depth = 1;
|
|
2394
|
+
let baseNode = node.callee.object;
|
|
2395
|
+
while (baseNode?.type === 'CallExpression' &&
|
|
2396
|
+
baseNode.callee?.type === 'MemberExpression' &&
|
|
2397
|
+
baseNode.callee.property?.type === 'Identifier' &&
|
|
2398
|
+
baseNode.callee.property.name === 'replace' &&
|
|
2399
|
+
baseNode.arguments?.length >= 2) {
|
|
2400
|
+
depth++;
|
|
2401
|
+
baseNode = baseNode.callee.object;
|
|
2402
|
+
}
|
|
2403
|
+
if (depth >= 3) {
|
|
2404
|
+
// Try to statically resolve the chain
|
|
2405
|
+
let resolved = null;
|
|
2406
|
+
if (baseNode?.type === 'Literal' && typeof baseNode.value === 'string') {
|
|
2407
|
+
resolved = baseNode.value;
|
|
2408
|
+
} else if (baseNode?.type === 'Identifier' && ctx.stringVarValues?.has(baseNode.name)) {
|
|
2409
|
+
resolved = ctx.stringVarValues.get(baseNode.name);
|
|
2410
|
+
}
|
|
2411
|
+
if (resolved !== null) {
|
|
2412
|
+
// Apply each .replace() in order (walk the chain from inner to outer)
|
|
2413
|
+
const replaceCalls = [];
|
|
2414
|
+
let currentNode = node;
|
|
2415
|
+
while (currentNode?.type === 'CallExpression' &&
|
|
2416
|
+
currentNode.callee?.type === 'MemberExpression' &&
|
|
2417
|
+
currentNode.callee.property?.name === 'replace') {
|
|
2418
|
+
replaceCalls.unshift(currentNode.arguments);
|
|
2419
|
+
currentNode = currentNode.callee.object;
|
|
2420
|
+
}
|
|
2421
|
+
for (const args of replaceCalls) {
|
|
2422
|
+
if (args.length >= 2) {
|
|
2423
|
+
let pattern = null;
|
|
2424
|
+
let replacement = null;
|
|
2425
|
+
// Extract regex or string pattern
|
|
2426
|
+
if (args[0].type === 'Literal' && args[0].regex) {
|
|
2427
|
+
try { pattern = new RegExp(args[0].regex.pattern, args[0].regex.flags); } catch { /* skip */ }
|
|
2428
|
+
} else if (args[0].type === 'Literal' && typeof args[0].value === 'string') {
|
|
2429
|
+
pattern = args[0].value;
|
|
2430
|
+
}
|
|
2431
|
+
if (args[1].type === 'Literal' && typeof args[1].value === 'string') {
|
|
2432
|
+
replacement = args[1].value;
|
|
2433
|
+
}
|
|
2434
|
+
if (pattern !== null && replacement !== null) {
|
|
2435
|
+
resolved = resolved.replace(pattern, replacement);
|
|
2436
|
+
} else {
|
|
2437
|
+
resolved = null;
|
|
2438
|
+
break;
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
const DANGEROUS_REPLACE_KEYWORDS = /\b(child_process|eval|exec|spawn|Function|http|net|dns|require|process)\b/;
|
|
2444
|
+
if (resolved && DANGEROUS_REPLACE_KEYWORDS.test(resolved)) {
|
|
2445
|
+
ctx.threats.push({
|
|
2446
|
+
type: 'string_mutation_obfuscation',
|
|
2447
|
+
severity: 'HIGH',
|
|
2448
|
+
message: `String mutation via ${depth} chained .replace() calls resolves to "${resolved.substring(0, 80)}" — leet-speak/substitution evasion.`,
|
|
2449
|
+
file: ctx.relFile
|
|
2450
|
+
});
|
|
2451
|
+
} else if (depth >= 4) {
|
|
2452
|
+
// 4+ replace chains without resolution is still suspicious
|
|
2453
|
+
ctx.threats.push({
|
|
2454
|
+
type: 'string_mutation_obfuscation',
|
|
2455
|
+
severity: 'MEDIUM',
|
|
2456
|
+
message: `${depth} chained .replace() calls detected — potential string mutation obfuscation (could not fully resolve).`,
|
|
2457
|
+
file: ctx.relFile
|
|
2458
|
+
});
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
|
|
2463
|
+
// Blue Team v8b (A1): X.apply(require, null, [...]) — indirect require via Reflect.apply
|
|
2464
|
+
// Detect Reflect.apply(require, ...) or anyVar.apply(require, ...) pattern
|
|
2465
|
+
// This is an evasion where the attacker uses reflection to invoke require dynamically
|
|
2466
|
+
if (node.callee?.type === 'MemberExpression' &&
|
|
2467
|
+
node.callee.property?.type === 'Identifier' && node.callee.property.name === 'apply' &&
|
|
2468
|
+
node.arguments.length >= 2) {
|
|
2469
|
+
const firstArg = node.arguments[0];
|
|
2470
|
+
if (firstArg?.type === 'Identifier' && firstArg.name === 'require') {
|
|
2471
|
+
ctx.threats.push({
|
|
2472
|
+
type: 'dynamic_require',
|
|
2473
|
+
severity: 'CRITICAL',
|
|
2474
|
+
message: '.apply(require, ...) — indirect require() invocation via Reflect.apply or Function.prototype.apply. Evasion technique to dynamically load modules.',
|
|
2475
|
+
file: ctx.relFile
|
|
2476
|
+
});
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
|
|
2197
2480
|
// Audit v3 bypass fix: process.on('uncaughtException'/'unhandledRejection', handler)
|
|
2198
2481
|
// Error handler hijacking for silent credential exfiltration
|
|
2199
2482
|
if (node.callee?.type === 'MemberExpression' &&
|
|
@@ -2208,6 +2491,116 @@ function handleCallExpression(node, ctx) {
|
|
|
2208
2491
|
}
|
|
2209
2492
|
|
|
2210
2493
|
// SANDWORM_MODE R8: dns.resolve detection moved to walk.ancestor() in ast.js (FIX 5)
|
|
2494
|
+
|
|
2495
|
+
// Blue Team v8b (A7): JSON.parse with reviver that checks __proto__
|
|
2496
|
+
// JSON.parse(str, function(key, value) { if (key === '__proto__') ... })
|
|
2497
|
+
// Note: getCallName returns 'parse' (property only), so check object.name === 'JSON'
|
|
2498
|
+
if (callName === 'parse' && node.callee?.type === 'MemberExpression' &&
|
|
2499
|
+
node.callee.object?.type === 'Identifier' && node.callee.object.name === 'JSON' &&
|
|
2500
|
+
node.arguments.length >= 2) {
|
|
2501
|
+
const reviver = node.arguments[1];
|
|
2502
|
+
if (reviver && (reviver.type === 'FunctionExpression' || reviver.type === 'ArrowFunctionExpression')) {
|
|
2503
|
+
// Check if reviver body contains __proto__ reference
|
|
2504
|
+
const reviverSrc = reviver.start !== undefined && reviver.end !== undefined
|
|
2505
|
+
? ctx._sourceCode?.slice(reviver.start, reviver.end) : '';
|
|
2506
|
+
if (reviverSrc && /__proto__|prototype\s*[.[]/.test(reviverSrc)) {
|
|
2507
|
+
ctx.hasJsonReviverProto = true;
|
|
2508
|
+
const hasRequireInReviver = /\brequire\s*\(/.test(reviverSrc);
|
|
2509
|
+
const hasProtoAssign = /Object\.prototype\s*\./.test(reviverSrc) || /\.__proto__\s*=/.test(reviverSrc);
|
|
2510
|
+
ctx.threats.push({
|
|
2511
|
+
type: 'json_reviver_pollution',
|
|
2512
|
+
severity: (hasRequireInReviver || hasProtoAssign) ? 'CRITICAL' : 'HIGH',
|
|
2513
|
+
message: (hasRequireInReviver || hasProtoAssign)
|
|
2514
|
+
? 'JSON.parse reviver accesses __proto__/prototype with require() or prototype assignment — prototype pollution for code injection.'
|
|
2515
|
+
: 'JSON.parse reviver accesses __proto__/prototype — potential prototype pollution via untrusted JSON input.',
|
|
2516
|
+
file: ctx.relFile
|
|
2517
|
+
});
|
|
2518
|
+
}
|
|
2519
|
+
}
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2522
|
+
// Blue Team v8b (C2): vm.runInContext/runInNewContext/compileFunction with dynamic code
|
|
2523
|
+
// Detect when the code argument is built from Buffer.from/base64/concat, not a string literal
|
|
2524
|
+
if (node.callee?.type === 'MemberExpression' && node.callee.property?.type === 'Identifier') {
|
|
2525
|
+
const vmMethods = ['runInContext', 'runInNewContext', 'runInThisContext', 'compileFunction'];
|
|
2526
|
+
if (vmMethods.includes(node.callee.property.name) && node.arguments.length > 0) {
|
|
2527
|
+
const codeArg = node.arguments[0];
|
|
2528
|
+
// Dynamic code: not a string literal, could be variable, concat, Buffer.from, etc.
|
|
2529
|
+
if (codeArg && codeArg.type !== 'Literal') {
|
|
2530
|
+
const isDynamic = codeArg.type === 'Identifier' || codeArg.type === 'BinaryExpression' ||
|
|
2531
|
+
codeArg.type === 'CallExpression' || codeArg.type === 'TemplateLiteral';
|
|
2532
|
+
if (isDynamic) {
|
|
2533
|
+
ctx.hasVmDynamicExec = true;
|
|
2534
|
+
ctx.hasDynamicExec = true;
|
|
2535
|
+
ctx.threats.push({
|
|
2536
|
+
type: 'vm_dynamic_code',
|
|
2537
|
+
severity: 'CRITICAL',
|
|
2538
|
+
message: `vm.${node.callee.property.name}() with dynamically constructed code — vm sandbox escape via runtime-built code string.`,
|
|
2539
|
+
file: ctx.relFile
|
|
2540
|
+
});
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2545
|
+
|
|
2546
|
+
// Blue Team v8b (C2): vm.createContext() with custom require injection + sensitive modules
|
|
2547
|
+
// Detects sandbox setup that provides module access to untrusted code
|
|
2548
|
+
if (callName === 'createContext' && node.callee?.type === 'MemberExpression' &&
|
|
2549
|
+
node.callee.object?.type === 'Identifier') {
|
|
2550
|
+
const src = ctx._sourceCode || '';
|
|
2551
|
+
// Check if the same file defines a custom require for the sandbox
|
|
2552
|
+
const hasRequireInjection = /\.require\s*=\s*(?:\(|function\b)/.test(src) ||
|
|
2553
|
+
/require\s*:\s*(?:\(|function\b)/.test(src);
|
|
2554
|
+
const hasSensitiveModules = /require\s*\(\s*['"](?:fs|http|https|net|child_process)['"]/.test(src);
|
|
2555
|
+
if (hasRequireInjection && hasSensitiveModules) {
|
|
2556
|
+
ctx.hasDynamicExec = true;
|
|
2557
|
+
ctx.threats.push({
|
|
2558
|
+
type: 'vm_dynamic_code',
|
|
2559
|
+
severity: 'CRITICAL',
|
|
2560
|
+
message: 'vm.createContext() with custom require() injection and access to sensitive modules (fs/http/net) — sandbox that enables untrusted code execution with elevated privileges.',
|
|
2561
|
+
file: ctx.relFile
|
|
2562
|
+
});
|
|
2563
|
+
}
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2566
|
+
// Blue Team v8b (B7): fs.readFileSync on image/binary files (stego pattern)
|
|
2567
|
+
if (node.callee?.type === 'MemberExpression' && node.callee.property?.type === 'Identifier' &&
|
|
2568
|
+
['readFileSync', 'readFile'].includes(node.callee.property.name)) {
|
|
2569
|
+
const fileArg = node.arguments?.[0];
|
|
2570
|
+
let filePath = '';
|
|
2571
|
+
if (fileArg?.type === 'Literal' && typeof fileArg.value === 'string') {
|
|
2572
|
+
filePath = fileArg.value;
|
|
2573
|
+
} else if (fileArg?.type === 'Identifier' && ctx.stringVarValues?.has(fileArg.name)) {
|
|
2574
|
+
filePath = ctx.stringVarValues.get(fileArg.name);
|
|
2575
|
+
}
|
|
2576
|
+
if (/\.(png|jpg|jpeg|gif|bmp|ico|svg)$/i.test(filePath)) {
|
|
2577
|
+
ctx.hasBinaryFileRead = true;
|
|
2578
|
+
}
|
|
2579
|
+
}
|
|
2580
|
+
|
|
2581
|
+
// Blue Team v8b (C10): execSync/exec inside .on('message') or .on('data') callback
|
|
2582
|
+
if (node.callee?.type === 'MemberExpression' && node.callee.property?.type === 'Identifier' &&
|
|
2583
|
+
node.callee.property.name === 'on' && node.arguments.length >= 2) {
|
|
2584
|
+
const eventArg = node.arguments[0];
|
|
2585
|
+
if (eventArg?.type === 'Literal' && ['message', 'data'].includes(eventArg.value)) {
|
|
2586
|
+
const callback = node.arguments[1];
|
|
2587
|
+
if (callback && (callback.type === 'FunctionExpression' || callback.type === 'ArrowFunctionExpression')) {
|
|
2588
|
+
const cbSrc = callback.start !== undefined && callback.end !== undefined
|
|
2589
|
+
? ctx._sourceCode?.slice(callback.start, callback.end) : '';
|
|
2590
|
+
if (cbSrc && /\b(execSync|exec|spawn|spawnSync)\s*\(/.test(cbSrc) &&
|
|
2591
|
+
/\brequire\s*\(\s*['"]child_process['"]\s*\)/.test(cbSrc)) {
|
|
2592
|
+
ctx.hasCallbackExec = true;
|
|
2593
|
+
ctx.hasDynamicExec = true;
|
|
2594
|
+
ctx.threats.push({
|
|
2595
|
+
type: 'callback_exec_rce',
|
|
2596
|
+
severity: 'CRITICAL',
|
|
2597
|
+
message: `exec/spawn inside .on('${eventArg.value}') callback with require('child_process') — remote command execution from network input.`,
|
|
2598
|
+
file: ctx.relFile
|
|
2599
|
+
});
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
}
|
|
2211
2604
|
}
|
|
2212
2605
|
|
|
2213
2606
|
function handleImportExpression(node, ctx) {
|
|
@@ -2232,10 +2625,16 @@ function handleImportExpression(node, ctx) {
|
|
|
2232
2625
|
ctx.hasSolanaImport = true;
|
|
2233
2626
|
}
|
|
2234
2627
|
} else {
|
|
2628
|
+
// Blue Team v8b (C6): Dynamic import with non-literal arg — if it's a variable
|
|
2629
|
+
// built from URL manipulation, this is remote code loading
|
|
2630
|
+
const isCritical = node.source.type === 'Identifier' || node.source.type === 'TemplateLiteral' ||
|
|
2631
|
+
(node.source.type === 'CallExpression' && node.source.callee?.property?.name === 'replace');
|
|
2235
2632
|
ctx.threats.push({
|
|
2236
2633
|
type: 'dynamic_import',
|
|
2237
|
-
severity: 'HIGH',
|
|
2238
|
-
message:
|
|
2634
|
+
severity: isCritical ? 'CRITICAL' : 'HIGH',
|
|
2635
|
+
message: isCritical
|
|
2636
|
+
? 'Dynamic import() with computed URL argument — remote code loading from dynamically constructed URL.'
|
|
2637
|
+
: 'Dynamic import() with computed argument (possible obfuscation).',
|
|
2239
2638
|
file: ctx.relFile
|
|
2240
2639
|
});
|
|
2241
2640
|
}
|
|
@@ -2318,19 +2717,92 @@ function handleNewExpression(node, ctx) {
|
|
|
2318
2717
|
|
|
2319
2718
|
// Batch 2: new Worker(code, { eval: true }) — worker_threads code execution
|
|
2320
2719
|
if (node.callee.type === 'Identifier' && node.callee.name === 'Worker' &&
|
|
2321
|
-
node.arguments.length >=
|
|
2322
|
-
|
|
2323
|
-
if (
|
|
2324
|
-
const
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2720
|
+
node.arguments.length >= 1) {
|
|
2721
|
+
ctx.hasWorkerThread = true;
|
|
2722
|
+
if (node.arguments.length >= 2) {
|
|
2723
|
+
const opts = node.arguments[1];
|
|
2724
|
+
if (opts?.type === 'ObjectExpression') {
|
|
2725
|
+
const evalProp = opts.properties?.find(p =>
|
|
2726
|
+
p.key?.name === 'eval' && p.value?.value === true);
|
|
2727
|
+
if (evalProp) {
|
|
2728
|
+
ctx.hasDynamicExec = true;
|
|
2729
|
+
ctx.threats.push({
|
|
2730
|
+
type: 'worker_thread_exec',
|
|
2731
|
+
severity: 'HIGH',
|
|
2732
|
+
message: 'new Worker() with eval:true — executes arbitrary code in worker thread, bypasses main thread detection.',
|
|
2733
|
+
file: ctx.relFile
|
|
2734
|
+
});
|
|
2735
|
+
}
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
2738
|
+
// Blue Team v8: new Worker('data:...') — data URL code injection into worker
|
|
2739
|
+
const firstArg = node.arguments[0];
|
|
2740
|
+
if (firstArg?.type === 'Literal' && typeof firstArg.value === 'string' &&
|
|
2741
|
+
firstArg.value.startsWith('data:')) {
|
|
2742
|
+
ctx.hasDynamicExec = true;
|
|
2743
|
+
ctx.threats.push({
|
|
2744
|
+
type: 'worker_thread_exec',
|
|
2745
|
+
severity: 'HIGH',
|
|
2746
|
+
message: 'new Worker() with data: URL — inline code injection into worker thread.',
|
|
2747
|
+
file: ctx.relFile
|
|
2748
|
+
});
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
|
|
2752
|
+
// Blue Team v8: new SharedArrayBuffer() — shared memory for covert IPC
|
|
2753
|
+
if (node.callee.type === 'Identifier' && node.callee.name === 'SharedArrayBuffer') {
|
|
2754
|
+
ctx.hasSharedArrayBuffer = true;
|
|
2755
|
+
}
|
|
2756
|
+
|
|
2757
|
+
// Blue Team v8: new WebSocket(url) — track for C2 compound detection
|
|
2758
|
+
if (node.callee.type === 'Identifier' && node.callee.name === 'WebSocket' &&
|
|
2759
|
+
node.arguments.length >= 1) {
|
|
2760
|
+
ctx.hasWebSocketNew = true;
|
|
2761
|
+
// Check if URL points to suspicious domain
|
|
2762
|
+
const wsArg = node.arguments[0];
|
|
2763
|
+
const wsUrl = extractStringValueDeep(wsArg);
|
|
2764
|
+
if (wsUrl) {
|
|
2765
|
+
const wsLower = wsUrl.toLowerCase();
|
|
2766
|
+
const isSuspiciousWs = SUSPICIOUS_DOMAINS_HIGH.some(d => wsLower.includes(d)) ||
|
|
2767
|
+
SUSPICIOUS_DOMAINS_MEDIUM.some(d => wsLower.includes(d));
|
|
2768
|
+
if (isSuspiciousWs) {
|
|
2328
2769
|
ctx.threats.push({
|
|
2329
|
-
type: '
|
|
2770
|
+
type: 'websocket_c2',
|
|
2330
2771
|
severity: 'HIGH',
|
|
2331
|
-
message:
|
|
2772
|
+
message: `new WebSocket() connecting to suspicious domain: "${wsUrl.substring(0, 80)}" — potential C2 channel.`,
|
|
2773
|
+
file: ctx.relFile
|
|
2774
|
+
});
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
}
|
|
2778
|
+
|
|
2779
|
+
// B2: new FinalizationRegistry(callback) — deferred execution after GC
|
|
2780
|
+
// Malicious pattern: callback contains require('child_process') or exec/spawn
|
|
2781
|
+
if (node.callee.type === 'Identifier' && node.callee.name === 'FinalizationRegistry' &&
|
|
2782
|
+
node.arguments.length >= 1) {
|
|
2783
|
+
const callback = node.arguments[0];
|
|
2784
|
+
if (callback) {
|
|
2785
|
+
// Check if callback body contains dangerous patterns
|
|
2786
|
+
let hasDangerousBody = false;
|
|
2787
|
+
const cbSource = callback.start !== undefined && callback.end !== undefined
|
|
2788
|
+
? ctx._sourceCode?.slice(callback.start, callback.end) : null;
|
|
2789
|
+
if (cbSource && /\b(child_process|exec|execSync|spawn|spawnSync)\b/.test(cbSource)) {
|
|
2790
|
+
hasDangerousBody = true;
|
|
2791
|
+
}
|
|
2792
|
+
// Also flag if the callback is a variable known to be dangerous
|
|
2793
|
+
if (callback.type === 'Identifier' && ctx.evalAliases?.has(callback.name)) {
|
|
2794
|
+
hasDangerousBody = true;
|
|
2795
|
+
}
|
|
2796
|
+
if (hasDangerousBody) {
|
|
2797
|
+
ctx.hasDynamicExec = true;
|
|
2798
|
+
ctx.threats.push({
|
|
2799
|
+
type: 'finalization_registry_exec',
|
|
2800
|
+
severity: 'CRITICAL',
|
|
2801
|
+
message: 'new FinalizationRegistry() with dangerous callback — deferred code execution triggered by garbage collection, evades synchronous analysis.',
|
|
2332
2802
|
file: ctx.relFile
|
|
2333
2803
|
});
|
|
2804
|
+
} else {
|
|
2805
|
+
ctx.hasFinalizationRegistry = true;
|
|
2334
2806
|
}
|
|
2335
2807
|
}
|
|
2336
2808
|
}
|
|
@@ -2463,6 +2935,81 @@ function handleAssignmentExpression(node, ctx) {
|
|
|
2463
2935
|
}
|
|
2464
2936
|
}
|
|
2465
2937
|
|
|
2938
|
+
// B6: Symbol property hiding — obj[Symbol(...)] = require('child_process')
|
|
2939
|
+
if (node.left?.type === 'MemberExpression' && node.left.computed &&
|
|
2940
|
+
node.left.property?.type === 'CallExpression' &&
|
|
2941
|
+
node.left.property.callee?.type === 'Identifier' && node.left.property.callee.name === 'Symbol') {
|
|
2942
|
+
// Check if the right side is require('child_process') or similar dangerous module
|
|
2943
|
+
let isDangerous = false;
|
|
2944
|
+
let modName = null;
|
|
2945
|
+
if (node.right?.type === 'CallExpression' && getCallName(node.right) === 'require' &&
|
|
2946
|
+
node.right.arguments?.[0]?.type === 'Literal') {
|
|
2947
|
+
const rawMod = node.right.arguments[0].value;
|
|
2948
|
+
modName = typeof rawMod === 'string' && rawMod.startsWith('node:') ? rawMod.slice(5) : rawMod;
|
|
2949
|
+
if (['child_process', 'fs', 'net', 'dns', 'http', 'https'].includes(modName)) {
|
|
2950
|
+
isDangerous = true;
|
|
2951
|
+
}
|
|
2952
|
+
}
|
|
2953
|
+
// Also detect: obj[Symbol('x')] = eval / Function / exec
|
|
2954
|
+
if (node.right?.type === 'Identifier' && ['eval', 'Function'].includes(node.right.name)) {
|
|
2955
|
+
isDangerous = true;
|
|
2956
|
+
}
|
|
2957
|
+
if (isDangerous) {
|
|
2958
|
+
ctx.threats.push({
|
|
2959
|
+
type: 'symbol_property_hiding',
|
|
2960
|
+
severity: 'HIGH',
|
|
2961
|
+
message: `Dangerous module/function hidden behind Symbol property — obj[Symbol(...)] = ${modName ? "require('" + modName + "')" : node.right?.name || '...'}, evades string-based property enumeration.`,
|
|
2962
|
+
file: ctx.relFile
|
|
2963
|
+
});
|
|
2964
|
+
}
|
|
2965
|
+
}
|
|
2966
|
+
|
|
2967
|
+
// B5: Module.wrap = ... or require('module').wrap = ... — module wrapper override
|
|
2968
|
+
if (node.left?.type === 'MemberExpression' &&
|
|
2969
|
+
node.left.property?.type === 'Identifier' && node.left.property.name === 'wrap') {
|
|
2970
|
+
const obj = node.left.object;
|
|
2971
|
+
// Direct: Module.wrap = ... (where Module was imported via require('module'))
|
|
2972
|
+
const isModuleObj = (obj?.type === 'Identifier' && ctx.moduleAliases?.has(obj.name)) ||
|
|
2973
|
+
(obj?.type === 'Identifier' && obj.name === 'Module');
|
|
2974
|
+
// Inline: require('module').wrap = ...
|
|
2975
|
+
const isInlineRequire = obj?.type === 'CallExpression' && getCallName(obj) === 'require' &&
|
|
2976
|
+
obj.arguments?.[0]?.type === 'Literal' && obj.arguments[0].value === 'module';
|
|
2977
|
+
if (isModuleObj || isInlineRequire) {
|
|
2978
|
+
ctx.threats.push({
|
|
2979
|
+
type: 'module_wrap_override',
|
|
2980
|
+
severity: 'CRITICAL',
|
|
2981
|
+
message: 'Module.wrap overridden — module wrapper function hijacked, allows injecting code into every loaded module.',
|
|
2982
|
+
file: ctx.relFile
|
|
2983
|
+
});
|
|
2984
|
+
}
|
|
2985
|
+
}
|
|
2986
|
+
|
|
2987
|
+
// Blue Team v8b (A4): Module._resolveFilename / _compile / _extensions hijack
|
|
2988
|
+
// Any assignment to these private Module APIs is a supply-chain attack vector
|
|
2989
|
+
if (node.left?.type === 'MemberExpression' && !node.left.computed &&
|
|
2990
|
+
node.left.property?.type === 'Identifier' &&
|
|
2991
|
+
['_resolveFilename', '_compile', '_extensions', '_findPath', '_nodeModulePaths'].includes(node.left.property.name)) {
|
|
2992
|
+
// Check if the object is Module, a module alias, or a constructor chain
|
|
2993
|
+
const obj = node.left.object;
|
|
2994
|
+
const isModuleRef = (obj?.type === 'Identifier' && (ctx.moduleAliases?.has(obj.name) || obj.name === 'Module')) ||
|
|
2995
|
+
// require('module')._resolveFilename = ...
|
|
2996
|
+
(obj?.type === 'CallExpression' && getCallName(obj) === 'require' && obj.arguments?.[0]?.value === 'module') ||
|
|
2997
|
+
// x.constructor._resolveFilename = ... (any .constructor chain)
|
|
2998
|
+
(obj?.type === 'MemberExpression' && obj.property?.type === 'Identifier' && obj.property.name === 'constructor');
|
|
2999
|
+
// Also match: proc.mainModule.constructor._resolveFilename (deeper chain)
|
|
3000
|
+
const isDeepChain = obj?.type === 'MemberExpression' && obj.property?.type === 'Identifier' &&
|
|
3001
|
+
['_resolveFilename', '_compile', '_extensions', '_findPath', '_nodeModulePaths'].includes(node.left.property.name);
|
|
3002
|
+
if (isModuleRef || isDeepChain || (obj?.type === 'MemberExpression' && obj.property?.name === 'constructor')) {
|
|
3003
|
+
ctx.hasModuleInternalsHijack = true;
|
|
3004
|
+
ctx.threats.push({
|
|
3005
|
+
type: 'module_internals_hijack',
|
|
3006
|
+
severity: 'CRITICAL',
|
|
3007
|
+
message: `Assignment to Module.${node.left.property.name} — module system internals hijacked. All subsequent require() calls can be intercepted.`,
|
|
3008
|
+
file: ctx.relFile
|
|
3009
|
+
});
|
|
3010
|
+
}
|
|
3011
|
+
}
|
|
3012
|
+
|
|
2466
3013
|
// Detect object property indirection: obj.exec = require('child_process').exec
|
|
2467
3014
|
// or obj.fn = eval — stashing dangerous functions in object properties
|
|
2468
3015
|
if (node.left?.type === 'MemberExpression' && node.right) {
|
|
@@ -2517,6 +3064,17 @@ function handleAssignmentExpression(node, ctx) {
|
|
|
2517
3064
|
}
|
|
2518
3065
|
}
|
|
2519
3066
|
|
|
3067
|
+
// B4: Prototype pollution — __proto__ assignment
|
|
3068
|
+
if (node.left?.type === 'MemberExpression' && !node.left.computed &&
|
|
3069
|
+
node.left.property?.type === 'Identifier' && node.left.property.name === '__proto__') {
|
|
3070
|
+
ctx.threats.push({
|
|
3071
|
+
type: 'prototype_pollution',
|
|
3072
|
+
severity: 'HIGH',
|
|
3073
|
+
message: `__proto__ assignment on ${node.left.object?.name || 'object'} — prototype pollution can hijack inherited properties across all objects.`,
|
|
3074
|
+
file: ctx.relFile
|
|
3075
|
+
});
|
|
3076
|
+
}
|
|
3077
|
+
|
|
2520
3078
|
if (node.left?.type === 'MemberExpression') {
|
|
2521
3079
|
const left = node.left;
|
|
2522
3080
|
|
|
@@ -3021,6 +3579,160 @@ function handlePostWalk(ctx) {
|
|
|
3021
3579
|
file: ctx.relFile
|
|
3022
3580
|
});
|
|
3023
3581
|
}
|
|
3582
|
+
|
|
3583
|
+
// B2 compound: FinalizationRegistry + exec/network in same file = deferred malicious execution
|
|
3584
|
+
if (ctx.hasFinalizationRegistry && ctx.hasDynamicExec) {
|
|
3585
|
+
ctx.threats.push({
|
|
3586
|
+
type: 'finalization_registry_exec',
|
|
3587
|
+
severity: 'CRITICAL',
|
|
3588
|
+
message: 'FinalizationRegistry + dynamic execution in same file — deferred code execution triggered by garbage collection.',
|
|
3589
|
+
file: ctx.relFile
|
|
3590
|
+
});
|
|
3591
|
+
}
|
|
3592
|
+
|
|
3593
|
+
// Blue Team v8: SharedArrayBuffer + Worker = covert shared memory IPC
|
|
3594
|
+
// Pattern: SharedArrayBuffer enables worker-to-main-thread communication
|
|
3595
|
+
// that bypasses message channel monitoring. Alone = MEDIUM, with exec = HIGH.
|
|
3596
|
+
if (ctx.hasSharedArrayBuffer && ctx.hasWorkerThread) {
|
|
3597
|
+
ctx.threats.push({
|
|
3598
|
+
type: 'shared_memory_ipc',
|
|
3599
|
+
severity: ctx.hasDynamicExec ? 'HIGH' : 'MEDIUM',
|
|
3600
|
+
message: ctx.hasDynamicExec
|
|
3601
|
+
? 'SharedArrayBuffer + Worker + dynamic execution — covert shared memory IPC with code execution.'
|
|
3602
|
+
: 'SharedArrayBuffer + Worker — shared memory IPC channel bypasses standard message monitoring.',
|
|
3603
|
+
file: ctx.relFile
|
|
3604
|
+
});
|
|
3605
|
+
}
|
|
3606
|
+
|
|
3607
|
+
// Blue Team v8: dgram/UDP exfiltration — dgram import + send in same file
|
|
3608
|
+
if (ctx.hasDgramImport && ctx.hasDgramSend) {
|
|
3609
|
+
const hasExfilSignal = ctx.threats.some(t =>
|
|
3610
|
+
t.file === ctx.relFile && (t.type === 'env_access' || t.type === 'sensitive_string')
|
|
3611
|
+
);
|
|
3612
|
+
ctx.threats.push({
|
|
3613
|
+
type: 'udp_exfiltration',
|
|
3614
|
+
severity: hasExfilSignal ? 'CRITICAL' : 'HIGH',
|
|
3615
|
+
message: hasExfilSignal
|
|
3616
|
+
? 'UDP socket (dgram) with credential/env access — data exfiltration via UDP bypasses HTTP-level monitoring.'
|
|
3617
|
+
: 'UDP socket (dgram) createSocket + send — non-HTTP data channel. UDP exfiltration bypasses HTTP firewalls.',
|
|
3618
|
+
file: ctx.relFile
|
|
3619
|
+
});
|
|
3620
|
+
}
|
|
3621
|
+
|
|
3622
|
+
// Blue Team v8: WebSocket C2 compound — WebSocket + exec/spawn in same file
|
|
3623
|
+
if (ctx.hasWebSocketNew && ctx.hasDynamicExec) {
|
|
3624
|
+
// Only emit if no websocket_c2 already emitted (from suspicious domain detection)
|
|
3625
|
+
if (!ctx.threats.some(t => t.type === 'websocket_c2' && t.file === ctx.relFile)) {
|
|
3626
|
+
ctx.threats.push({
|
|
3627
|
+
type: 'websocket_c2',
|
|
3628
|
+
severity: 'HIGH',
|
|
3629
|
+
message: 'WebSocket connection + dynamic execution in same file — potential WebSocket C2 channel for remote command execution.',
|
|
3630
|
+
file: ctx.relFile
|
|
3631
|
+
});
|
|
3632
|
+
}
|
|
3633
|
+
}
|
|
3634
|
+
|
|
3635
|
+
// Blue Team v8: Extended blockchain C2 — Ethereum/Web3 import + C2 methods
|
|
3636
|
+
// Extends GlassWorm detection to cover ethers.js/web3.js patterns
|
|
3637
|
+
if (!ctx.hasSolanaImport && ctx.hasSolanaC2Method) {
|
|
3638
|
+
// C2 method detected without Solana import — check if Ethereum packages are used
|
|
3639
|
+
const hasEthImport = /\brequire\s*\(\s*['"](?:ethers|web3|@ethersproject)/i.test(ctx._sourceCode || '');
|
|
3640
|
+
if (hasEthImport) {
|
|
3641
|
+
ctx.threats.push({
|
|
3642
|
+
type: 'blockchain_c2_resolution',
|
|
3643
|
+
severity: ctx.hasDynamicExec ? 'CRITICAL' : 'HIGH',
|
|
3644
|
+
message: 'Ethereum/Web3 import + blockchain C2 method — ' +
|
|
3645
|
+
(ctx.hasDynamicExec
|
|
3646
|
+
? 'dead drop resolver with dynamic execution. Blockchain C2 pattern confirmed.'
|
|
3647
|
+
: 'potential dead drop resolver. Technique: C2 commands stored in blockchain transactions.'),
|
|
3648
|
+
file: ctx.relFile
|
|
3649
|
+
});
|
|
3650
|
+
}
|
|
3651
|
+
}
|
|
3652
|
+
|
|
3653
|
+
// Blue Team v8b (A6): with statement + Proxy + require/exec in same file = sandbox escape compound
|
|
3654
|
+
// Boost: if with_body_dangerous AND proxy trap detected → ensure minimum CRITICAL score
|
|
3655
|
+
const hasWithDangerous = ctx.threats.some(t => t.type === 'with_body_dangerous' && t.file === ctx.relFile);
|
|
3656
|
+
const hasProxyInFile = ctx.hasProxyTrap || ctx.proxyHandlerVars?.size > 0;
|
|
3657
|
+
if (hasWithDangerous && hasProxyInFile) {
|
|
3658
|
+
// Elevate existing with_body_dangerous to CRITICAL if not already
|
|
3659
|
+
for (const t of ctx.threats) {
|
|
3660
|
+
if (t.type === 'with_body_dangerous' && t.file === ctx.relFile && t.severity !== 'CRITICAL') {
|
|
3661
|
+
t.severity = 'CRITICAL';
|
|
3662
|
+
t.message = 'with() + Proxy compound: Proxy trap intercepts all scope resolution inside with block — complete API hijacking for sandbox escape.';
|
|
3663
|
+
}
|
|
3664
|
+
}
|
|
3665
|
+
}
|
|
3666
|
+
|
|
3667
|
+
// Blue Team v8b (B7): Binary file read + new Function/eval = steganographic payload
|
|
3668
|
+
if (ctx.hasBinaryFileRead && ctx.hasDynamicExec) {
|
|
3669
|
+
ctx.threats.push({
|
|
3670
|
+
type: 'stego_binary_exec',
|
|
3671
|
+
severity: 'CRITICAL',
|
|
3672
|
+
message: 'Binary/image file read + dynamic execution (eval/Function) — steganographic payload extraction and execution.',
|
|
3673
|
+
file: ctx.relFile
|
|
3674
|
+
});
|
|
3675
|
+
} else if (ctx.hasImageFileRef && ctx.hasDynamicExec && ctx.hasWriteFileSyncInContent) {
|
|
3676
|
+
// Fallback: image reference + eval in same file (may not directly readFile the image)
|
|
3677
|
+
ctx.threats.push({
|
|
3678
|
+
type: 'stego_binary_exec',
|
|
3679
|
+
severity: 'HIGH',
|
|
3680
|
+
message: 'Image file reference + dynamic execution + file I/O in same file — potential steganographic payload pattern.',
|
|
3681
|
+
file: ctx.relFile
|
|
3682
|
+
});
|
|
3683
|
+
}
|
|
3684
|
+
|
|
3685
|
+
// Blue Team v8b (C1): AsyncLocalStorage + credential file read + exec/network
|
|
3686
|
+
// Trigger on hasDynamicExec OR when dynamic_require is present (require('child_' + 'process') stored in context)
|
|
3687
|
+
const hasDynReqInFile = ctx.threats.some(t => t.type === 'dynamic_require' && t.file === ctx.relFile);
|
|
3688
|
+
if (ctx.hasAsyncLocalStorage && (ctx.hasDynamicExec || hasDynReqInFile)) {
|
|
3689
|
+
ctx.threats.push({
|
|
3690
|
+
type: 'asynclocal_context_exec',
|
|
3691
|
+
severity: 'HIGH',
|
|
3692
|
+
message: 'AsyncLocalStorage + dynamic execution/require — code execution hidden in async context, evades synchronous call-stack analysis.',
|
|
3693
|
+
file: ctx.relFile
|
|
3694
|
+
});
|
|
3695
|
+
}
|
|
3696
|
+
|
|
3697
|
+
// Blue Team v8b (C10): net.Socket + exec = WebSocket/TCP C2
|
|
3698
|
+
// Trigger on callbackExec (exec in .on('message') callback) OR hasDynamicExec (exec anywhere in file)
|
|
3699
|
+
if (ctx.hasNetSocketCreate && (ctx.hasCallbackExec || ctx.hasDynamicExec)) {
|
|
3700
|
+
if (!ctx.threats.some(t => t.type === 'websocket_c2' && t.file === ctx.relFile)) {
|
|
3701
|
+
ctx.threats.push({
|
|
3702
|
+
type: 'websocket_c2',
|
|
3703
|
+
severity: 'CRITICAL',
|
|
3704
|
+
message: 'net.Socket + exec in same file — persistent TCP/WebSocket C2 with remote command execution.',
|
|
3705
|
+
file: ctx.relFile
|
|
3706
|
+
});
|
|
3707
|
+
}
|
|
3708
|
+
}
|
|
3709
|
+
|
|
3710
|
+
// Blue Team v8b (B2): CI environment fingerprinting probe — 3+ CI provider env vars in same file
|
|
3711
|
+
// Indicates multi-provider CI detection for conditional payload activation
|
|
3712
|
+
if (ctx.ciProviderCount >= 3) {
|
|
3713
|
+
ctx.threats.push({
|
|
3714
|
+
type: 'ci_environment_probe',
|
|
3715
|
+
severity: 'HIGH',
|
|
3716
|
+
message: `File references ${ctx.ciProviderCount} CI provider environment variables (GITHUB_ACTIONS, GITLAB_CI, etc.) — CI environment fingerprinting for targeted execution.`,
|
|
3717
|
+
file: ctx.relFile
|
|
3718
|
+
});
|
|
3719
|
+
}
|
|
3720
|
+
|
|
3721
|
+
// Blue Team v8: Hardcoded contract address (40-char hex) + blockchain import = C2 address
|
|
3722
|
+
if ((ctx.hasSolanaImport || /\brequire\s*\(\s*['"](?:ethers|web3|@ethersproject|@solana)/i.test(ctx._sourceCode || '')) &&
|
|
3723
|
+
/\b0x[0-9a-fA-F]{40}\b/.test(ctx._sourceCode || '')) {
|
|
3724
|
+
const existingBlockchain = ctx.threats.some(t =>
|
|
3725
|
+
t.type === 'blockchain_c2_resolution' && t.file === ctx.relFile
|
|
3726
|
+
);
|
|
3727
|
+
if (!existingBlockchain) {
|
|
3728
|
+
ctx.threats.push({
|
|
3729
|
+
type: 'blockchain_c2_resolution',
|
|
3730
|
+
severity: 'MEDIUM',
|
|
3731
|
+
message: 'Blockchain import + hardcoded contract address (0x...) — potential smart contract C2 endpoint.',
|
|
3732
|
+
file: ctx.relFile
|
|
3733
|
+
});
|
|
3734
|
+
}
|
|
3735
|
+
}
|
|
3024
3736
|
}
|
|
3025
3737
|
|
|
3026
3738
|
function handleWithStatement(node, ctx) {
|
|
@@ -3049,6 +3761,26 @@ function handleWithStatement(node, ctx) {
|
|
|
3049
3761
|
file: ctx.relFile
|
|
3050
3762
|
});
|
|
3051
3763
|
}
|
|
3764
|
+
return; // Already handled as direct with(require(...))
|
|
3765
|
+
}
|
|
3766
|
+
|
|
3767
|
+
// B7: with(obj) { ... require('child_process') ... } — body contains dangerous require/exec
|
|
3768
|
+
// The with statement itself is rare in modern code; combined with dangerous APIs in body = evasion
|
|
3769
|
+
if (node.body) {
|
|
3770
|
+
const bodySource = node.body.start !== undefined && node.body.end !== undefined
|
|
3771
|
+
? ctx._sourceCode?.slice(node.body.start, node.body.end) : null;
|
|
3772
|
+
if (bodySource && /\b(require\s*\(\s*['"]child_process['"]\s*\)|child_process|exec\s*\(|execSync\s*\(|spawn\s*\()/.test(bodySource)) {
|
|
3773
|
+
// Blue Team v8: Elevate to CRITICAL when with() scope object is a known Proxy variable
|
|
3774
|
+
const isProxyScope = node.object?.type === 'Identifier' && ctx.proxyHandlerVars?.has(node.object.name);
|
|
3775
|
+
ctx.threats.push({
|
|
3776
|
+
type: 'with_body_dangerous',
|
|
3777
|
+
severity: isProxyScope ? 'CRITICAL' : 'HIGH',
|
|
3778
|
+
message: isProxyScope
|
|
3779
|
+
? `with(Proxy) + exec/require in body — Proxy trap intercepts all name resolution, enabling complete API hijacking.`
|
|
3780
|
+
: 'with() statement body contains require/exec/spawn — scope injection used to obscure dangerous API calls.',
|
|
3781
|
+
file: ctx.relFile
|
|
3782
|
+
});
|
|
3783
|
+
}
|
|
3052
3784
|
}
|
|
3053
3785
|
}
|
|
3054
3786
|
|