#!/usr/bin/python import sqlite3, argparse, sys, argparse, logging, json, string import os, re, time, signal, copy, base64 from flask import Flask, request, jsonify, make_response, abort, url_for from time import localtime, strftime from OpenSSL import SSL from Crypto.Random import random # Empire imports from lib.common import empire from lib.common import helpers global serverExitCommand serverExitCommand = 'restart' ##################################################### # # Database interaction methods for the RESTful API # ##################################################### def database_connect(): """ Connect with the backend ./empire.db sqlite database and return the connection object. """ try: # set the database connectiont to autocommit w/ isolation level conn = sqlite3.connect('./data/empire.db', check_same_thread=False) conn.text_factory = str conn.isolation_level = None return conn except Exception: print helpers.color("[!] Could not connect to database") print helpers.color("[!] Please run database_setup.py") sys.exit() def execute_db_query(conn, query, args=None): """ Execute the supplied query on the provided db conn object with optional args for a paramaterized query. """ cur = conn.cursor() if args: cur.execute(query, args) else: cur.execute(query) results = cur.fetchall() cur.close() return results def refresh_api_token(conn): """ Generates a randomized RESTful API token and updates the value in the config stored in the backend database. """ # generate a randomized API token apiToken = ''.join(random.choice(string.ascii_lowercase + string.digits) for x in range(40)) execute_db_query(conn, "UPDATE config SET api_current_token=?", [apiToken]) return apiToken def get_permanent_token(conn): """ Returns the permanent API token stored in empire.db. If one doesn't exist, it will generate one and store it before returning. """ permanentToken = execute_db_query(conn, "SELECT api_permanent_token FROM config")[0] if not permanentToken[0]: permanentToken = ''.join(random.choice(string.ascii_lowercase + string.digits) for x in range(40)) execute_db_query(conn, "UPDATE config SET api_permanent_token=?", [permanentToken]) return permanentToken[0] #################################################################### # # The Empire RESTful API. # # Adapted from http://blog.miguelgrinberg.com/post/designing-a-restful-api-with-python-and-flask # example code at https://gist.github.com/miguelgrinberg/5614326 # # Verb URI Action # ---- --- ------ # GET http://localhost:1337/api/version return the current Empire version # # GET http://localhost:1337/api/config return the current default config # # GET http://localhost:1337/api/stagers return all current stagers # GET http://localhost:1337/api/stagers/X return the stager with name X # POST http://localhost:1337/api/stagers generate a stager given supplied options (need to implement) # # GET http://localhost:1337/api/modules return all current modules # GET http://localhost:1337/api/modules/ return the module with the specified name # POST http://localhost:1337/api/modules/ execute the given module with the specified options # POST http://localhost:1337/api/modules/search searches modulesfor a passed term # POST http://localhost:1337/api/modules/search/modulename searches module names for a specific term # POST http://localhost:1337/api/modules/search/description searches module descriptions for a specific term # POST http://localhost:1337/api/modules/search/description searches module comments for a specific term # POST http://localhost:1337/api/modules/search/author searches module authors for a specific term # # GET http://localhost:1337/api/listeners return all current listeners # GET http://localhost:1337/api/listeners/Y return the listener with id Y # GET http://localhost:1337/api/listeners/options return all listener options # POST http://localhost:1337/api/listeners starts a new listener with the specified options # DELETE http://localhost:1337/api/listeners/Y kills listener Y # # GET http://localhost:1337/api/agents return all current agents # GET http://localhost:1337/api/agents/stale return all stale agents # DELETE http://localhost:1337/api/agents/stale removes stale agents from the database # DELETE http://localhost:1337/api/agents/Y removes agent Y from the database # GET http://localhost:1337/api/agents/Y return the agent with name Y # GET http://localhost:1337/api/agents/Y/results return tasking results for the agent with name Y # DELETE http://localhost:1337/api/agents/Y/results deletes the result buffer for agent Y # POST http://localhost:1337/api/agents/Y/shell task agent Y to execute a shell command # POST http://localhost:1337/api/agents/Y/rename rename agent Y # GET/POST http://localhost:1337/api/agents/Y/clear clears the result buffer for agent Y # GET/POST http://localhost:1337/api/agents/Y/kill kill agent Y # # GET http://localhost:1337/api/reporting return all logged events # GET http://localhost:1337/api/reporting/agent/X return all logged events for the given agent name X # GET http://localhost:1337/api/reporting/type/Y return all logged events of type Y (checkin, task, result, rename) # GET http://localhost:1337/api/reporting/msg/Z return all logged events matching message Z, wildcards accepted # # GET http://localhost:1337/api/creds return stored credentials # # GET http://localhost:1337/api/admin/login retrieve the API token given the correct username and password # GET http://localhost:1337/api/admin/permanenttoken retrieve the permanent API token, generating/storing one if it doesn't already exist # GET http://localhost:1337/api/admin/shutdown shutdown the RESTful API # GET http://localhost:1337/api/admin/restart restart the RESTful API # #################################################################### def start_restful_api(startEmpire=False, suppress=False, username=None, password=None, port=1337): """ Kick off the RESTful API with the given parameters. startEmpire - start a complete Empire instance in the backend as well suppress - suppress most console output username - optional username to use for the API, otherwise pulls from the empire.db config password - optional password to use for the API, otherwise pulls from the empire.db config port - port to start the API on, defaults to 1337 ;) """ app = Flask(__name__) conn = database_connect() global serverExitCommand # if a username/password were not supplied, use the creds stored in the db (dbUsername, dbPassword) = execute_db_query(conn, "SELECT api_username, api_password FROM config")[0] if not username: username = dbUsername if not password: password = dbPassword class Namespace: """ Temporary namespace to create the followin base argument object. """ def __init__(self, **kwargs): self.__dict__.update(kwargs) # instantiate an Empire instance in case we need to interact with stagers or listeners args = Namespace(debug=None, listener=None, stager=None, stager_options=None, version=False) print '' if startEmpire: # if we want to start a full-running empire instance print " * Starting a full Empire instance" main = empire.MainMenu(args=args) else: # if we just want the RESTful API, i.e. no listener/etc. startup main = empire.MainMenu(args=args, restAPI=True) print " * Starting Empire RESTful API on port: %s" %(port) # refresh the token for the RESTful API apiToken = refresh_api_token(conn) print " * RESTful API token: %s" %(apiToken) permanentApiToken = get_permanent_token(conn) tokenAllowed = re.compile("^[0-9a-z]{40}") oldStdout = sys.stdout if suppress: # suppress the normal Flask output log = logging.getLogger('werkzeug') log.setLevel(logging.ERROR) # suppress all stdout and don't initiate the main cmdloop sys.stdout = open(os.devnull, 'w') # validate API token before every request except for the login URI @app.before_request def check_token(): """ Before every request, check if a valid token is passed along with the request. """ if request.path != '/api/admin/login': token = request.args.get('token') if (not token) or (not tokenAllowed.match(token)): return make_response('', 401) if (token != apiToken) and (token != permanentApiToken): return make_response('', 401) @app.errorhandler(Exception) def exception_handler(error): """ Generic exception handler. """ return repr(error) @app.errorhandler(404) def not_found(error): """ 404/not found handler. """ return make_response(jsonify({'error': 'Not found'}), 404) @app.route('/api/version', methods=['GET']) def get_version(): """ Returns the current Empire version. """ return jsonify({'version': empire.VERSION}) @app.route('/api/map', methods=['GET']) def list_routes(): """ List all of the current registered routes. """ import urllib output = [] for rule in app.url_map.iter_rules(): options = {} for arg in rule.arguments: options[arg] = "[{0}]".format(arg) methods = ','.join(rule.methods) url = url_for(rule.endpoint, **options) line = urllib.unquote("[ { '" + rule.endpoint + "': [ { 'methods': '" + methods + "', 'url': '" + url + "' } ] } ]") output.append(line) res = '' for line in sorted(output): res = res + '\r\n' + line return jsonify({'Routes':res}) @app.route('/api/config', methods=['GET']) def get_config(): """ Returns JSON of the current Empire config. """ configRaw = execute_db_query(conn, 'SELECT * FROM config') [staging_key, stage0_uri, stage1_uri, stage2_uri, default_delay, default_jitter, default_profile, default_cert_path, default_port, install_path, server_version, ip_whitelist, ip_blacklist, default_lost_limit, autorun_command, autorun_data, api_username, api_password, current_api_token, permanent_api_token] = configRaw[0] config = [{"version":empire.VERSION, "staging_key":staging_key, "stage0_uri":stage0_uri, "stage1_uri":stage1_uri, "stage2_uri":stage2_uri, "default_delay":default_delay, "default_jitter":default_jitter, "default_profile":default_profile, "default_cert_path":default_cert_path, "default_port":default_port, "install_path":install_path, "server_version":server_version, "ip_whitelist":ip_whitelist, "ip_blacklist":ip_blacklist, "default_lost_limit":default_lost_limit, "autorun_command":autorun_command, "autorun_data":autorun_data, "api_username":api_username, "api_password":api_password, "current_api_token":current_api_token, "permanent_api_token":permanent_api_token}] return jsonify({'config': config}) @app.route('/api/stagers', methods=['GET']) def get_stagers(): """ Returns JSON describing all stagers. """ stagers = [] for stagerName, stager in main.stagers.stagers.iteritems(): info = copy.deepcopy(stager.info) info['options'] = stager.options info['Name'] = stagerName stagers.append(info) return jsonify({'stagers': stagers}) @app.route('/api/stagers/', methods=['GET']) def get_stagers_name(stager_name): """ Returns JSON describing the specified stager_name passed. """ if stager_name not in main.stagers.stagers: return make_response(jsonify({'error': 'stager name %s not found' %(stager_name)}), 404) stagers = [] for stagerName, stager in main.stagers.stagers.iteritems(): if stagerName == stager_name: info = copy.deepcopy(stager.info) info['options'] = stager.options info['Name'] = stagerName stagers.append(info) return jsonify({'stagers': stagers}) @app.route('/api/stagers', methods=['POST']) def generate_stager(): """ Generates a stager with the supplied config and returns JSON information describing the generated stager, with 'Output' being the stager output. Required JSON args: StagerName - the stager name to generate Listener - the Listener name to use for the stager """ if not request.json or not 'StagerName' in request.json or not 'Listener' in request.json: abort(400) stagerName = request.json['StagerName'] listener = request.json['Listener'] if stagerName not in main.stagers.stagers: return make_response(jsonify({'error': 'stager name %s not found' %(stagerName)}), 404) if not main.listeners.is_listener_valid(listener): return make_response(jsonify({'error': 'invalid listener ID or name'}), 400) stager = main.stagers.stagers[stagerName] # set all passed options for option, values in request.json.iteritems(): if option != 'StagerName': if option not in stager.options: return make_response(jsonify({'error': 'Invalid option %s, check capitalization.' %(option)}), 400) stager.options[option]['Value'] = values # validate stager options for option, values in stager.options.iteritems(): if values['Required'] and ((not values['Value']) or (values['Value'] == '')): return make_response(jsonify({'error': 'required stager options missing'}), 400) stagerOut = copy.deepcopy(stager.options) if ('OutFile' in stagerOut) and (stagerOut['OutFile']['Value'] != ''): # if the output was intended for a file, return the base64 encoded text stagerOut['Output'] = base64.b64encode(stager.generate()) else: # otherwise return the text of the stager generation stagerOut['Output'] = stager.generate() return jsonify({stagerName: stagerOut}) @app.route('/api/modules', methods=['GET']) def get_modules(): """ Returns JSON describing all currently loaded modules. """ modules = [] for moduleName, module in main.modules.modules.iteritems(): moduleInfo = copy.deepcopy(module.info) moduleInfo['options'] = module.options moduleInfo['Name'] = moduleName modules.append(moduleInfo) return jsonify({'modules': modules}) @app.route('/api/modules/', methods=['GET']) def get_module_name(module_name): """ Returns JSON describing the specified currently module. """ if module_name not in main.modules.modules: return make_response(jsonify({'error': 'module name %s not found' %(module_name)}), 404) modules = [] moduleInfo = copy.deepcopy(main.modules.modules[module_name].info) moduleInfo['options'] = main.modules.modules[module_name].options moduleInfo['Name'] = module_name modules.append(moduleInfo) return jsonify({'modules': modules}) @app.route('/api/modules/', methods=['POST']) def execute_module(module_name): """ Executes a given module name with the specified parameters. """ # ensure the 'Agent' argument is set if not request.json or not 'Agent' in request.json: abort(400) if module_name not in main.modules.modules: return make_response(jsonify({'error': 'module name %s not found' %(module_name)}), 404) module = main.modules.modules[module_name] # set all passed module options for key, value in request.json.iteritems(): if key not in module.options: return make_response(jsonify({'error': 'invalid module option'}), 400) module.options[key]['Value'] = value # validate module options sessionID = module.options['Agent']['Value'] for option, values in module.options.iteritems(): if values['Required'] and ((not values['Value']) or (values['Value'] == '')): return make_response(jsonify({'error': 'required module option missing'}), 400) try: # if we're running this module for all agents, skip this validation if sessionID.lower() != "all" and sessionID.lower() != "autorun": if not main.agents.is_agent_present(sessionID): return make_response(jsonify({'error': 'invalid agent name'}), 400) modulePSVersion = int(module.info['MinPSVersion']) agentPSVersion = int(main.agents.get_language_version(sessionID)) # check if the agent/module PowerShell versions are compatible if modulePSVersion > agentPSVersion: return make_response(jsonify({'error': "module requires PS version "+str(modulePSVersion)+" but agent running PS version "+str(agentPSVersion)}), 400) except Exception as e: return make_response(jsonify({'error': 'exception: %s' %(e)}), 400) # check if the module needs admin privs if module.info['NeedsAdmin']: # if we're running this module for all agents, skip this validation if sessionID.lower() != "all" and sessionID.lower() != "autorun": if not main.agents.is_agent_elevated(sessionID): return make_response(jsonify({'error': 'module needs to run in an elevated context'}), 400) # actually execute the module moduleData = module.generate() if not moduleData or moduleData == "": return make_response(jsonify({'error': 'module produced an empty script'}), 400) try: moduleData.decode('ascii') except UnicodeDecodeError: return make_response(jsonify({'error': 'module source contains non-ascii characters'}), 400) moduleData = helpers.strip_powershell_comments(moduleData) taskCommand = "" # build the appropriate task command and module data blob if str(module.info['Background']).lower() == "true": # if this module should be run in the background extention = module.info['OutputExtension'] if extention and extention != "": # if this module needs to save its file output to the server # format- [15 chars of prefix][5 chars extension][data] saveFilePrefix = module_name.split("/")[-1] moduleData = saveFilePrefix.rjust(15) + extention.rjust(5) + moduleData taskCommand = "TASK_CMD_JOB_SAVE" else: taskCommand = "TASK_CMD_JOB" else: # if this module is run in the foreground extention = module.info['OutputExtension'] if module.info['OutputExtension'] and module.info['OutputExtension'] != "": # if this module needs to save its file output to the server # format- [15 chars of prefix][5 chars extension][data] saveFilePrefix = module_name.split("/")[-1][:15] moduleData = saveFilePrefix.rjust(15) + extention.rjust(5) + moduleData taskCommand = "TASK_CMD_WAIT_SAVE" else: taskCommand = "TASK_CMD_WAIT" if sessionID.lower() == "all": for agent in main.agents.get_agents(): sessionID = agent[1] main.agents.add_agent_task(sessionID, taskCommand, moduleData) msg = "tasked agent %s to run module %s" %(sessionID, module_name) main.agents.save_agent_log(sessionID, msg) msg = "tasked all agents to run module %s" %(module_name) return jsonify({'success': True, 'msg':msg}) else: # set the agent's tasking in the cache main.agents.add_agent_task(sessionID, taskCommand, moduleData) # update the agent log msg = "tasked agent %s to run module %s" %(sessionID, module_name) main.agents.save_agent_log(sessionID, msg) return jsonify({'success': True, 'msg':msg}) @app.route('/api/modules/search', methods=['POST']) def search_modules(): """ Returns JSON describing the the modules matching the passed 'term' search parameter. Module name, description, comments, and author fields are searched. """ if not request.json or not 'term': abort(400) searchTerm = request.json['term'] modules = [] for moduleName, module in main.modules.modules.iteritems(): if (searchTerm.lower() == '') or (searchTerm.lower() in moduleName.lower()) or (searchTerm.lower() in ("".join(module.info['Description'])).lower()) or (searchTerm.lower() in ("".join(module.info['Comments'])).lower()) or (searchTerm.lower() in ("".join(module.info['Author'])).lower()): moduleInfo = copy.deepcopy(main.modules.modules[moduleName].info) moduleInfo['options'] = main.modules.modules[moduleName].options moduleInfo['Name'] = moduleName modules.append(moduleInfo) return jsonify({'modules': modules}) @app.route('/api/modules/search/modulename', methods=['POST']) def search_modules_name(): """ Returns JSON describing the the modules matching the passed 'term' search parameter for the modfule name. """ if not request.json or not 'term': abort(400) searchTerm = request.json['term'] modules = [] for moduleName, module in main.modules.modules.iteritems(): if (searchTerm.lower() == '') or (searchTerm.lower() in moduleName.lower()): moduleInfo = copy.deepcopy(main.modules.modules[moduleName].info) moduleInfo['options'] = main.modules.modules[moduleName].options moduleInfo['Name'] = moduleName modules.append(moduleInfo) return jsonify({'modules': modules}) @app.route('/api/modules/search/description', methods=['POST']) def search_modules_description(): """ Returns JSON describing the the modules matching the passed 'term' search parameter for the 'Description' field. """ if not request.json or not 'term': abort(400) searchTerm = request.json['term'] modules = [] for moduleName, module in main.modules.modules.iteritems(): if (searchTerm.lower() == '') or (searchTerm.lower() in ("".join(module.info['Description'])).lower()): moduleInfo = copy.deepcopy(main.modules.modules[moduleName].info) moduleInfo['options'] = main.modules.modules[moduleName].options moduleInfo['Name'] = moduleName modules.append(moduleInfo) return jsonify({'modules': modules}) @app.route('/api/modules/search/comments', methods=['POST']) def search_modules_comments(): """ Returns JSON describing the the modules matching the passed 'term' search parameter for the 'Comments' field. """ if not request.json or not 'term': abort(400) searchTerm = request.json['term'] modules = [] for moduleName, module in main.modules.modules.iteritems(): if (searchTerm.lower() == '') or (searchTerm.lower() in ("".join(module.info['Comments'])).lower()): moduleInfo = copy.deepcopy(main.modules.modules[moduleName].info) moduleInfo['options'] = main.modules.modules[moduleName].options moduleInfo['Name'] = moduleName modules.append(moduleInfo) return jsonify({'modules': modules}) @app.route('/api/modules/search/author', methods=['POST']) def search_modules_author(): """ Returns JSON describing the the modules matching the passed 'term' search parameter for the 'Author' field. """ if not request.json or not 'term': abort(400) searchTerm = request.json['term'] modules = [] for moduleName, module in main.modules.modules.iteritems(): if (searchTerm.lower() == '') or (searchTerm.lower() in ("".join(module.info['Author'])).lower()): moduleInfo = copy.deepcopy(main.modules.modules[moduleName].info) moduleInfo['options'] = main.modules.modules[moduleName].options moduleInfo['Name'] = moduleName modules.append(moduleInfo) return jsonify({'modules': modules}) @app.route('/api/listeners', methods=['GET']) def get_listeners(): """ Returns JSON describing all currently registered listeners. """ activeListenersRaw = execute_db_query(conn, 'SELECT * FROM listeners') listeners = [] for activeListener in activeListenersRaw: [ID, name, host, port, cert_path, staging_key, default_delay, default_jitter, default_profile, kill_date, working_hours, listener_type, redirect_target, default_lost_limit] = activeListener # activeListeners[name] = {'ID':ID, 'name':name, 'host':host, 'port':port, 'cert_path':cert_path, 'staging_key':staging_key, 'default_delay':default_delay, 'default_jitter':default_jitter, 'default_profile':default_profile, 'kill_date':kill_date, 'working_hours':working_hours, 'listener_type':listener_type, 'redirect_target':redirect_target, 'default_lost_limit':default_lost_limit} listeners.append({'ID':ID, 'name':name, 'host':host, 'port':port, 'cert_path':cert_path, 'staging_key':staging_key, 'default_delay':default_delay, 'default_jitter':default_jitter, 'default_profile':default_profile, 'kill_date':kill_date, 'working_hours':working_hours, 'listener_type':listener_type, 'redirect_target':redirect_target, 'default_lost_limit':default_lost_limit}) return jsonify({'listeners' : listeners}) @app.route('/api/listeners/', methods=['GET']) def get_listener_name(listener_name): """ Returns JSON describing the listener specified by listener_name. """ activeListenersRaw = execute_db_query(conn, 'SELECT * FROM listeners') listeners = [] if listener_name != "" and main.listeners.is_listener_valid(listener_name): for activeListener in activeListenersRaw: [ID, name, host, port, cert_path, staging_key, default_delay, default_jitter, default_profile, kill_date, working_hours, listener_type, redirect_target, default_lost_limit] = activeListener if name == listener_name: listeners.append({'ID':ID, 'name':name, 'host':host, 'port':port, 'cert_path':cert_path, 'staging_key':staging_key, 'default_delay':default_delay, 'default_jitter':default_jitter, 'default_profile':default_profile, 'kill_date':kill_date, 'working_hours':working_hours, 'listener_type':listener_type, 'redirect_target':redirect_target, 'default_lost_limit':default_lost_limit}) return jsonify({'listeners' : listeners}) else: return make_response(jsonify({'error': 'listener name %s not found' %(listener_name)}), 404) @app.route('/api/listeners/', methods=['DELETE']) def kill_listener(listener_name): """ Kills the listener specified by listener_name. """ if listener_name.lower() == "all": activeListenersRaw = execute_db_query(conn, 'SELECT * FROM listeners') for activeListener in activeListenersRaw: [ID, name, host, port, cert_path, staging_key, default_delay, default_jitter, default_profile, kill_date, working_hours, listener_type, redirect_target, default_lost_limit] = activeListener main.listeners.shutdown_listener(name) main.listeners.delete_listener(name) return jsonify({'success': True}) else: if listener_name != "" and main.listeners.is_listener_valid(listener_name): main.listeners.shutdown_listener(listener_name) main.listeners.delete_listener(listener_name) return jsonify({'success': True}) else: return make_response(jsonify({'error': 'listener name %s not found' %(listener_name)}), 404) @app.route('/api/listeners/options', methods=['GET']) def get_listener_options(): """ Returns JSON describing the current listener options. """ return jsonify({'listeneroptions' : [main.listeners.options]}) @app.route('/api/listeners', methods=['POST']) def start_listener(): """ Starts a listener with options supplied in the POST. """ # set all passed options for option, values in request.json.iteritems(): returnVal = main.listeners.set_listener_option(option, values) if not returnVal: return make_response(jsonify({'error': 'error setting listener value %s with option %s' %(option, values)}), 400) valid = main.listeners.validate_listener_options() if not valid: return make_response(jsonify({'error': 'error validating listener options'}), 400) (success, message) = main.listeners.add_listener_from_config() if success: return jsonify({'success': success, 'msg' : "listener '%s' successfully started." %(message)}) else: return jsonify({'success': success, 'msg' : message}) @app.route('/api/agents', methods=['GET']) def get_agents(): """ Returns JSON describing all currently registered agents. """ activeAgentsRaw = execute_db_query(conn, 'SELECT * FROM agents') agents = [] for activeAgent in activeAgentsRaw: [ID, sessionID, listener, name, delay, jitter, external_ip, internal_ip, username, high_integrity, process_name, process_id, hostname, os_details, session_key, checkin_time, lastseen_time, parent, children, servers, uris, old_uris, user_agent, headers, functions, kill_date, working_hours, ps_version, lost_limit, taskings, results] = activeAgent agents.append({"ID":ID, "sessionID":sessionID, "listener":listener, "name":name, "delay":delay, "jitter":jitter, "external_ip":external_ip, "internal_ip":internal_ip, "username":username, "high_integrity":high_integrity, "process_name":process_name, "process_id":process_id, "hostname":hostname, "os_details":os_details, "session_key":session_key, "checkin_time":checkin_time, "lastseen_time":lastseen_time, "parent":parent, "children":children, "servers":servers, "uris":uris, "old_uris":old_uris, "user_agent":user_agent, "headers":headers, "functions":functions, "kill_date":kill_date, "working_hours":working_hours, "ps_version":ps_version, "lost_limit":lost_limit, "taskings":taskings, "results":results}) return jsonify({'agents' : agents}) @app.route('/api/agents/stale', methods=['GET']) def get_agents_stale(): """ Returns JSON describing all stale agents. """ agentsRaw = execute_db_query(conn, 'SELECT * FROM agents') staleAgents = [] for agent in agentsRaw: [ID, sessionID, listener, name, delay, jitter, external_ip, internal_ip, username, high_integrity, process_name, process_id, hostname, os_details, session_key, checkin_time, lastseen_time, parent, children, servers, uris, old_uris, user_agent, headers, functions, kill_date, working_hours, ps_version, lost_limit, taskings, results] = agent intervalMax = (delay + delay * jitter)+30 # get the agent last check in time agentTime = time.mktime(time.strptime(lastseen_time, "%Y-%m-%d %H:%M:%S")) if agentTime < time.mktime(time.localtime()) - intervalMax: staleAgents.append({"ID":ID, "sessionID":sessionID, "listener":listener, "name":name, "delay":delay, "jitter":jitter, "external_ip":external_ip, "internal_ip":internal_ip, "username":username, "high_integrity":high_integrity, "process_name":process_name, "process_id":process_id, "hostname":hostname, "os_details":os_details, "session_key":session_key, "checkin_time":checkin_time, "lastseen_time":lastseen_time, "parent":parent, "children":children, "servers":servers, "uris":uris, "old_uris":old_uris, "user_agent":user_agent, "headers":headers, "functions":functions, "kill_date":kill_date, "working_hours":working_hours, "ps_version":ps_version, "lost_limit":lost_limit, "taskings":taskings, "results":results}) return jsonify({'agents' : staleAgents}) @app.route('/api/agents/stale', methods=['DELETE']) def remove_stale_agent(): """ Removes stale agents from the controller. WARNING: doesn't kill the agent first! Ensure the agent is dead. """ agentsRaw = execute_db_query(conn, 'SELECT * FROM agents') for agent in agentsRaw: [ID, sessionID, listener, name, delay, jitter, external_ip, internal_ip, username, high_integrity, process_name, process_id, hostname, os_details, session_key, checkin_time, lastseen_time, parent, children, servers, uris, old_uris, user_agent, headers, functions, kill_date, working_hours, ps_version, lost_limit, taskings, results] = agent intervalMax = (delay + delay * jitter)+30 # get the agent last check in time agentTime = time.mktime(time.strptime(lastseen_time, "%Y-%m-%d %H:%M:%S")) if agentTime < time.mktime(time.localtime()) - intervalMax: execute_db_query(conn, "DELETE FROM agents WHERE session_id LIKE ?", [sessionID]) return jsonify({'success': True}) @app.route('/api/agents/', methods=['DELETE']) def remove_agent(agent_name): """ Removes an agent from the controller specified by agent_name. WARNING: doesn't kill the agent first! Ensure the agent is dead. """ if agent_name.lower() == "all": # enumerate all target agent sessionIDs agentNameIDs = execute_db_query(conn, "SELECT name,session_id FROM agents WHERE name like '%' OR session_id like '%'") else: agentNameIDs = execute_db_query(conn, 'SELECT name,session_id FROM agents WHERE name like ? OR session_id like ?', [agent_name, agent_name]) if not agentNameIDs or len(agentNameIDs) == 0: return make_response(jsonify({'error': 'agent name %s not found' %(agent_name)}), 404) for agentNameID in agentNameIDs: (agentName, agentSessionID) = agentNameID execute_db_query(conn, "DELETE FROM agents WHERE session_id LIKE ?", [agentSessionID]) return jsonify({'success': True}) @app.route('/api/agents/', methods=['GET']) def get_agents_name(agent_name): """ Returns JSON describing the agent specified by agent_name. """ activeAgentsRaw = execute_db_query(conn, 'SELECT * FROM agents WHERE name=? OR session_id=?', [agent_name, agent_name]) activeAgents = [] for activeAgent in activeAgentsRaw: [ID, sessionID, listener, name, delay, jitter, external_ip, internal_ip, username, high_integrity, process_name, process_id, hostname, os_details, session_key, checkin_time, lastseen_time, parent, children, servers, uris, old_uris, user_agent, headers, functions, kill_date, working_hours, ps_version, lost_limit, taskings, results] = activeAgent activeAgents.append({"ID":ID, "sessionID":sessionID, "listener":listener, "name":name, "delay":delay, "jitter":jitter, "external_ip":external_ip, "internal_ip":internal_ip, "username":username, "high_integrity":high_integrity, "process_name":process_name, "process_id":process_id, "hostname":hostname, "os_details":os_details, "session_key":session_key, "checkin_time":checkin_time, "lastseen_time":lastseen_time, "parent":parent, "children":children, "servers":servers, "uris":uris, "old_uris":old_uris, "user_agent":user_agent, "headers":headers, "functions":functions, "kill_date":kill_date, "working_hours":working_hours, "ps_version":ps_version, "lost_limit":lost_limit, "taskings":taskings, "results":results}) return jsonify({'agents' : activeAgents}) @app.route('/api/agents//results', methods=['GET']) def get_agent_results(agent_name): """ Returns JSON describing the agent's results and removes the result field from the backend database. """ agentTaskResults = [] if agent_name.lower() == "all": # enumerate all target agent sessionIDs agentNameIDs = execute_db_query(conn, "SELECT name,session_id FROM agents WHERE name like '%' OR session_id like '%'") else: agentNameIDs = execute_db_query(conn, 'SELECT name,session_id FROM agents WHERE name like ? OR session_id like ?', [agent_name, agent_name]) for agentNameID in agentNameIDs: (agentName, agentSessionID) = agentNameID agentResults = execute_db_query(conn, 'SELECT results FROM agents WHERE session_id=?', [agentSessionID])[0] if agentResults and agentResults[0] and agentResults[0] != '': out = json.loads(agentResults[0]) if out: agentResults = "\n".join(out) else: agentResults = '' else: agentResults = '' agentTaskResults.append({"agentname":agentName, "results":agentResults}) return jsonify({'results': agentTaskResults}) @app.route('/api/agents//results', methods=['DELETE']) def delete_agent_results(agent_name): """ Removes the specified agent results field from the backend database. """ if agent_name.lower() == "all": # enumerate all target agent sessionIDs execute_db_query(conn, "UPDATE agents SET results='' WHERE name like '%' OR session_id like '%'") else: execute_db_query(conn, "UPDATE agents SET results='' WHERE name like ? OR session_id like ?", [agent_name, agent_name]) return jsonify({'success': True}) @app.route('/api/agents//shell', methods=['POST']) def task_agent_shell(agent_name): """ Tasks an the specified agent_name to execute a shell command. Takes {'command':'shell_command'} """ if agent_name.lower() == "all": # enumerate all target agent sessionIDs agentNameIDs = execute_db_query(conn, "SELECT name,session_id FROM agents WHERE name like '%' OR session_id like '%'") else: agentNameIDs = execute_db_query(conn, 'SELECT name,session_id FROM agents WHERE name like ? OR session_id like ?', [agent_name, agent_name]) if not agentNameIDs or len(agentNameIDs) == 0: return make_response(jsonify({'error': 'agent name %s not found' %(agent_name)}), 404) command = request.json['command'] for agentNameID in agentNameIDs: (agentName, agentSessionID) = agentNameID # get existing agent taskings for each agent agentTasks = execute_db_query(conn, 'SELECT taskings FROM agents WHERE session_id like ?', [agentSessionID])[0] if agentTasks and agentTasks[0]: agentTasks = json.loads(agentTasks[0]) else: agentTasks = [] # append our new json-ified task and update the backend agentTasks.append(['TASK_SHELL', command]) execute_db_query(conn, "UPDATE agents SET taskings=? WHERE session_id=?", [json.dumps(agentTasks), agentSessionID]) timeStamp = strftime("%Y-%m-%d %H:%M:%S", localtime()) execute_db_query(conn, "INSERT INTO reporting (name,event_type,message,time_stamp) VALUES (?,?,?,?)", (agentName, "task", "TASK_SHELL - " + command[0:50], timeStamp)) return jsonify({'success': True}) @app.route('/api/agents//rename', methods=['POST']) def task_agent_rename(agent_name): """ Renames the specified agent. Takes {'newname':'NAME'} """ agentNameID = execute_db_query(conn, 'SELECT name,session_id FROM agents WHERE name like ? OR session_id like ?', [agent_name, agent_name]) if not agentNameID or len(agentNameID) == 0: return make_response(jsonify({'error': 'agent name %s not found' %(agent_name)}), 404) (agentName, agentSessionID) = agentNameID[0] newName = request.json['newname'] try: result = main.agents.rename_agent(agentName, newName) if not result: return make_response(jsonify({'error': 'error in renaming %s to %s, new name may have already been used' %(agentName, newName)}), 400) return jsonify({'success': True}) except Exception: return make_response(jsonify({'error': 'error in renaming %s to %s' %(agentName, newName)}), 400) @app.route('/api/agents//clear', methods=['POST', 'GET']) def task_agent_clear(agent_name): """ Clears the tasking buffer for the specified agent. """ if agent_name.lower() == "all": # enumerate all target agent sessionIDs agentNameIDs = execute_db_query(conn, "SELECT name,session_id FROM agents WHERE name like '%' OR session_id like '%'") else: agentNameIDs = execute_db_query(conn, 'SELECT name,session_id FROM agents WHERE name like ? OR session_id like ?', [agent_name, agent_name]) if not agentNameIDs or len(agentNameIDs) == 0: return make_response(jsonify({'error': 'agent name %s not found' %(agent_name)}), 404) for agentNameID in agentNameIDs: (agentName, agentSessionID) = agentNameID execute_db_query(conn, "UPDATE agents SET taskings=? WHERE session_id=?", ['', agentSessionID]) timeStamp = strftime("%Y-%m-%d %H:%M:%S", localtime()) execute_db_query(conn, "INSERT INTO reporting (name,event_type,message,time_stamp) VALUES (?,?,?,?)", (agentName, "clear", '', timeStamp)) return jsonify({'success': True}) @app.route('/api/agents//kill', methods=['POST', 'GET']) def task_agent_kill(agent_name): """ Tasks the specified agent to exit. """ if agent_name.lower() == "all": # enumerate all target agent sessionIDs agentNameIDs = execute_db_query(conn, "SELECT name,session_id FROM agents WHERE name like '%' OR session_id like '%'") else: agentNameIDs = execute_db_query(conn, 'SELECT name,session_id FROM agents WHERE name like ? OR session_id like ?', [agent_name, agent_name]) if not agentNameIDs or len(agentNameIDs) == 0: return make_response(jsonify({'error': 'agent name %s not found' %(agent_name)}), 404) for agentNameID in agentNameIDs: (agentName, agentSessionID) = agentNameID # get existing agent taskings for each agent agentTasks = execute_db_query(conn, 'SELECT taskings FROM agents WHERE session_id like ?', [agentSessionID])[0] if agentTasks and agentTasks[0]: agentTasks = json.loads(agentTasks[0]) else: agentTasks = [] # append our new json-ified task and update the backend agentTasks.append(['TASK_EXIT', '']) execute_db_query(conn, "UPDATE agents SET taskings=? WHERE session_id=?", [json.dumps(agentTasks), agentSessionID]) timeStamp = strftime("%Y-%m-%d %H:%M:%S", localtime()) execute_db_query(conn, "INSERT INTO reporting (name,event_type,message,time_stamp) VALUES (?,?,?,?)", (agentName, "task", "TASK_EXIT", timeStamp)) return jsonify({'success': True}) @app.route('/api/creds', methods=['GET']) def get_creds(): """ Returns JSON describing the credentials stored in the backend database. """ credsRaw = execute_db_query(conn, 'SELECT * FROM credentials') creds = [] for credRaw in credsRaw: [ID, credtype, domain, username, password, host, sid, notes] = credRaw creds.append({"ID":ID, "credtype":credtype, "domain":domain, "username":username, "password":password, "host":host, "sid":sid, "notes":notes}) return jsonify({'creds' : creds}) @app.route('/api/reporting', methods=['GET']) def get_reporting(): """ Returns JSON describing the reporting events from the backend database. """ reportingRaw = execute_db_query(conn, 'SELECT * FROM reporting') reportingEvents = [] for reportingEvent in reportingRaw: [ID, name, eventType, message, timestamp] = reportingEvent reportingEvents.append({"ID":ID, "agentname":name, "event_type":eventType, "message":message, "timestamp":timestamp}) return jsonify({'reporting' : reportingEvents}) @app.route('/api/reporting/agent/', methods=['GET']) def get_reporting_agent(reporting_agent): """ Returns JSON describing the reporting events from the backend database for the agent specified by reporting_agent. """ # first resolve the supplied name to a sessionID results = execute_db_query(conn, 'SELECT session_id FROM agents WHERE name=?', [reporting_agent]) if results: sessionID = results[0][0] else: return jsonify({'reporting' : ''}) reportingRaw = execute_db_query(conn, 'SELECT * FROM reporting WHERE name=?', [sessionID]) reportingEvents = [] for reportingEvent in reportingRaw: [ID, name, eventType, message, timestamp] = reportingEvent reportingEvents.append({"ID":ID, "agentname":name, "event_type":eventType, "message":message, "timestamp":timestamp}) return jsonify({'reporting' : reportingEvents}) @app.route('/api/reporting/type/', methods=['GET']) def get_reporting_type(event_type): """ Returns JSON describing the reporting events from the backend database for the event type specified by event_type. """ reportingRaw = execute_db_query(conn, 'SELECT * FROM reporting WHERE event_type=?', [event_type]) reportingEvents = [] for reportingEvent in reportingRaw: [ID, name, eventType, message, timestamp] = reportingEvent reportingEvents.append({"ID":ID, "agentname":name, "event_type":eventType, "message":message, "timestamp":timestamp}) return jsonify({'reporting' : reportingEvents}) @app.route('/api/reporting/msg/', methods=['GET']) def get_reporting_msg(msg): """ Returns JSON describing the reporting events from the backend database for the any messages with *msg* specified by msg. """ reportingRaw = execute_db_query(conn, "SELECT * FROM reporting WHERE message like ?", ['%'+msg+'%']) reportingEvents = [] for reportingEvent in reportingRaw: [ID, name, eventType, message, timestamp] = reportingEvent reportingEvents.append({"ID":ID, "agentname":name, "event_type":eventType, "message":message, "timestamp":timestamp}) return jsonify({'reporting' : reportingEvents}) @app.route('/api/admin/login', methods=['POST']) def server_login(): """ Takes a supplied username and password and returns the current API token if authentication is accepted. """ if not request.json or not 'username' in request.json or not 'password' in request.json: abort(400) suppliedUsername = request.json['username'] suppliedPassword = request.json['password'] # try to prevent some basic bruting time.sleep(2) if suppliedUsername == username and suppliedPassword == password: return jsonify({'token': apiToken}) else: return make_response('', 401) @app.route('/api/admin/permanenttoken', methods=['GET']) def get_server_perm_token(): """ Returns the 'permanent' API token for the server. """ permanentToken = get_permanent_token(conn) return jsonify({'token': permanentToken}) @app.route('/api/admin/restart', methods=['GET', 'POST', 'PUT']) def signal_server_restart(): """ Signal a restart for the Flask server and any Empire instance. """ restart_server() return jsonify({'success': True}) @app.route('/api/admin/shutdown', methods=['GET', 'POST', 'PUT']) def signal_server_shutdown(): """ Signal a restart for the Flask server and any Empire instance. """ shutdown_server() return jsonify({'success': True}) if not os.path.exists('./data/empire.pem'): print "[!] Error: cannot find certificate ./data/empire.pem" sys.exit() def shutdown_server(): """ Shut down the Flask server and any Empire instance gracefully. """ global serverExitCommand if suppress: # repair stdout sys.stdout.close() sys.stdout = oldStdout print "\n * Shutting down Empire RESTful API" if conn: conn.close() if startEmpire: print " * Shutting down the Empire instance" main.shutdown() serverExitCommand = 'shutdown' func = request.environ.get('werkzeug.server.shutdown') if func is not None: func() def restart_server(): """ Restart the Flask server and any Empire instance. """ global serverExitCommand shutdown_server() serverExitCommand = 'restart' def signal_handler(signal, frame): """ Overrides the keyboardinterrupt signal handler so we can gracefully shut everything down. """ global serverExitCommand with app.test_request_context(): shutdown_server() serverExitCommand = 'shutdown' # repair the original signal handler import signal signal.signal(signal.SIGINT, signal.default_int_handler) sys.exit() signal.signal(signal.SIGINT, signal_handler) # wrap the Flask connection in SSL and start it context = ('./data/empire.pem', './data/empire.pem') app.run(host='0.0.0.0', port=int(port), ssl_context=context, threaded=True) if __name__ == '__main__': parser = argparse.ArgumentParser() generalGroup = parser.add_argument_group('General Options') generalGroup.add_argument('--debug', nargs='?', const='1', help='Debug level for output (default of 1, 2 for msg display).') generalGroup.add_argument('-v', '--version', action='store_true', help='Display current Empire version.') cliGroup = parser.add_argument_group('CLI Payload Options') cliGroup.add_argument('-l', '--listener', nargs='?', const="list", help='Display listener options. Displays all listeners if nothing is specified.') cliGroup.add_argument('-s', '--stager', nargs='?', const="list", help='Specify a stager to generate. Lists all stagers if none is specified.') cliGroup.add_argument('-o', '--stager-options', nargs='*', help="Supply options to set for a stager in OPTION=VALUE format. Lists options if nothing is specified.") restGroup = parser.add_argument_group('RESTful API Options') launchGroup = restGroup.add_mutually_exclusive_group() launchGroup.add_argument('--rest', action='store_true', help='Run the Empire RESTful API.') launchGroup.add_argument('--headless', action='store_true', help='Run Empire and the RESTful API headless without the usual interface.') restGroup.add_argument('--restport', type=int, nargs=1, help='Port to run the Empire RESTful API on.') restGroup.add_argument('--username', nargs=1, help='Start the RESTful API with the specified username instead of pulling from empire.db') restGroup.add_argument('--password', nargs=1, help='Start the RESTful API with the specified password instead of pulling from empire.db') args = parser.parse_args() if not args.restport: args.restport = '1337' else: args.restport = args.restport[0] if args.version: print empire.VERSION elif args.rest: # start just the RESTful API while serverExitCommand == 'restart': try: start_restful_api(startEmpire=False, suppress=False, username=args.username, password=args.password, port=args.restport) except SystemExit as e: pass elif args.headless: # start an Empire instance and RESTful API and suppress output while serverExitCommand == 'restart': try: start_restful_api(startEmpire=True, suppress=True, username=args.username, password=args.password, port=args.restport) except SystemExit as e: pass else: # normal execution main = empire.MainMenu(args=args) main.cmdloop() sys.exit()