commit
446a004cc1
648
empire
648
empire
|
@ -1,13 +1,637 @@
|
|||
#!/usr/bin/python
|
||||
|
||||
import sqlite3, argparse
|
||||
import sqlite3, argparse, sys, argparse, logging, json, string, os, re, time, signal
|
||||
from flask import Flask, request, jsonify, make_response, abort
|
||||
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 listeners
|
||||
from lib.common import http
|
||||
from lib.common import packets
|
||||
from lib.common import messages
|
||||
|
||||
|
||||
#####################################################
|
||||
#
|
||||
# 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 as e:
|
||||
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_token=?", [apiToken])
|
||||
|
||||
return apiToken
|
||||
|
||||
|
||||
####################################################################
|
||||
#
|
||||
# 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/empire/api/version return the current Empire version
|
||||
#
|
||||
# GET http://localhost:1337/empire/api/config return the current default config
|
||||
#
|
||||
# GET http://localhost:1337/empire/api/stagers return all current stagers
|
||||
# GET http://localhost:1337/empire/api/stagers/X return the stager with name X
|
||||
# POST http://localhost:1337/empire/api/stagers generate a stager given supplied options (need to implement)
|
||||
#
|
||||
# GET http://localhost:1337/empire/api/modules return all current modules
|
||||
#
|
||||
# GET http://localhost:1337/empire/api/listeners return all current listeners
|
||||
# GET http://localhost:1337/empire/api/listeners/Y return the listener with id Y
|
||||
# GET http://localhost:1337/empire/api/listeners/options return all listener options
|
||||
# POST http://localhost:1337/empire/api/listeners starts a new listener with the specified options
|
||||
# DELETE http://localhost:1337/empire/api/listeners/Y kills listener Y
|
||||
#
|
||||
# GET http://localhost:1337/empire/api/agents return all current agents
|
||||
# GET http://localhost:1337/empire/api/agents/Y return the agent with name Y
|
||||
# GET http://localhost:1337/empire/api/agents/Y/results return tasking results for the agent with name Y
|
||||
# POST http://localhost:1337/empire/api/agents/Y modify or task agent with Y
|
||||
# DELETE http://localhost:1337/empire/api/agents/Y removes agent Y from the database
|
||||
# DELETE http://localhost:1337/empire/api/agents/stale removes stale agents from the database
|
||||
#
|
||||
# GET http://localhost:1337/empire/api/reporting return all logged events
|
||||
# GET http://localhost:1337/empire/api/reporting/agent/X return all logged events for the given agent name X
|
||||
# GET http://localhost:1337/empire/api/reporting/type/Y return all logged events of type Y (checkin, task, result, rename)
|
||||
# GET http://localhost:1337/empire/api/reporting/msg/Z return all logged events matching message Z, wildcards accepted
|
||||
#
|
||||
# GET http://localhost:1337/empire/api/admin/shutdown shutdown the RESTful API
|
||||
#
|
||||
####################################################################
|
||||
|
||||
def start_restful_api(startEmpire=False, suppress=False, port=1337):
|
||||
'''
|
||||
|
||||
'''
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
class Namespace:
|
||||
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)
|
||||
|
||||
conn = database_connect()
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
@app.before_request
|
||||
def check_token():
|
||||
token = request.args.get('token')
|
||||
if (not token) or (not tokenAllowed.match(token)):
|
||||
return make_response('', 403)
|
||||
if token != apiToken:
|
||||
return make_response('', 403)
|
||||
|
||||
|
||||
@app.errorhandler(Exception)
|
||||
def exception_handler(error):
|
||||
return repr(error)
|
||||
|
||||
|
||||
@app.errorhandler(404)
|
||||
def not_found(error):
|
||||
return make_response(jsonify( { 'error': 'Not found' } ), 404)
|
||||
|
||||
|
||||
@app.route('/empire/api/version', methods=['GET'])
|
||||
def get_version():
|
||||
"""
|
||||
Returns the current Empire version.
|
||||
"""
|
||||
return jsonify({'version': empire.VERSION})
|
||||
|
||||
|
||||
@app.route('/empire/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_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_token":api_token}
|
||||
return jsonify({'config': config})
|
||||
|
||||
|
||||
@app.route('/empire/api/stagers', methods=['GET'])
|
||||
def get_stagers():
|
||||
"""
|
||||
Returns JSON describing all stagers.
|
||||
"""
|
||||
stagerInfo = {}
|
||||
for stagerName,stager in main.stagers.stagers.iteritems():
|
||||
info = stager.info
|
||||
info['options'] = stager.options
|
||||
stagerInfo[stagerName] = info
|
||||
|
||||
return jsonify({'stagers': stagerInfo})
|
||||
|
||||
|
||||
@app.route('/empire/api/stagers/<string:stager_name>', methods=['GET'])
|
||||
def get_stagers_name(stager_name):
|
||||
"""
|
||||
Returns JSON describing the specified stager_name passed.
|
||||
"""
|
||||
stagerInfo = {}
|
||||
for stagerName,stager in main.stagers.stagers.iteritems():
|
||||
if(stagerName == stager_name):
|
||||
info = stager.info
|
||||
info['options'] = stager.options
|
||||
stagerInfo[stagerName] = info
|
||||
|
||||
return jsonify({'stagers': stagerInfo})
|
||||
|
||||
|
||||
@app.route('/empire/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 jsonify({'error': 'StagerName invalid'})
|
||||
|
||||
if not main.listeners.is_listener_valid(listener):
|
||||
return jsonify({'error': 'invalid listener ID or name'})
|
||||
|
||||
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 jsonify({'error': 'Invalid option %s, check capitalization.' %(option)})
|
||||
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 jsonify({'error': 'required stager options missing'})
|
||||
|
||||
stagerOut = stager.options
|
||||
stagerOut['Output'] = stager.generate()
|
||||
|
||||
return jsonify({stagerName: stagerOut})
|
||||
|
||||
|
||||
@app.route('/empire/api/modules', methods=['GET'])
|
||||
def get_modules():
|
||||
"""
|
||||
Returns JSON describing all currently loaded modules.
|
||||
"""
|
||||
moduleInfo = {}
|
||||
for moduleName,module in main.modules.modules.iteritems():
|
||||
info = module.info
|
||||
info['options'] = module.options
|
||||
moduleInfo[moduleName] = info
|
||||
|
||||
return jsonify({'modules': moduleInfo})
|
||||
|
||||
|
||||
@app.route('/empire/api/listeners', methods=['GET'])
|
||||
def get_listeners():
|
||||
"""
|
||||
Returns JSON describing all currently registered listeners.
|
||||
"""
|
||||
activeListenersRaw = execute_db_query(conn, 'SELECT * FROM listeners')
|
||||
activeListeners = {}
|
||||
|
||||
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}
|
||||
|
||||
return jsonify({'listeners' : activeListeners})
|
||||
|
||||
|
||||
@app.route('/empire/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 * FROM listeners')
|
||||
activeListeners = {}
|
||||
|
||||
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:
|
||||
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}
|
||||
|
||||
return jsonify({'listeners' : activeListeners})
|
||||
|
||||
|
||||
@app.route('/empire/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 * 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({'result': 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({'result': True})
|
||||
else:
|
||||
return jsonify({'error': 'invalid listener name: %s' %(listener_name)})
|
||||
|
||||
|
||||
@app.route('/empire/api/listeners/options', methods=['GET'])
|
||||
def get_listener_options():
|
||||
"""
|
||||
Returns JSON describing the current listener options.
|
||||
"""
|
||||
return jsonify({'ListenerOptions' : main.listeners.options})
|
||||
|
||||
|
||||
@app.route('/empire/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 jsonify({'error': 'Error setting listener value %s with option %s' %(option, values)})
|
||||
|
||||
valid = main.listeners.validate_listener_options()
|
||||
if not valid:
|
||||
return jsonify({'error': 'Error validating listener options'})
|
||||
|
||||
main.listeners.add_listener_from_config()
|
||||
return jsonify({'result': True})
|
||||
|
||||
|
||||
@app.route('/empire/api/agents', methods=['GET'])
|
||||
def get_agents():
|
||||
"""
|
||||
Returns JSON describing all currently registered agents.
|
||||
"""
|
||||
activeAgentsRaw = execute_db_query(conn, 'SELECT * FROM agents')
|
||||
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[name] = {"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('/empire/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 * 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[name] = {"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('/empire/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.
|
||||
"""
|
||||
|
||||
agentResults = execute_db_query(conn, 'SELECT results FROM agents WHERE name=? OR session_id=?', [agent_name, agent_name])[0]
|
||||
|
||||
if agentResults and agentResults[0] and agentResults[0] != '':
|
||||
out = json.loads(agentResults[0])
|
||||
if(out):
|
||||
agentResults = "\n".join(out)
|
||||
else:
|
||||
agentResults = ''
|
||||
else:
|
||||
agentResults = ''
|
||||
|
||||
execute_db_query(conn, 'UPDATE agents SET results=? WHERE name=? OR session_id=?', ['', agent_name, agent_name])
|
||||
|
||||
return jsonify({agent_name : {'Results': agentResults}})
|
||||
|
||||
|
||||
# TODO: add get /name/results to get/clear results from DB
|
||||
@app.route('/empire/api/agents/<string:agent_name>', methods=['POST'])
|
||||
def modify_agent(agent_name):
|
||||
"""
|
||||
Modifies an agent with name agent_name.
|
||||
Used for tasking, clearing tasking, setting sleep, renaming, and killing.
|
||||
"""
|
||||
|
||||
if 'Task' in request.json.keys():
|
||||
|
||||
if agent_name.lower() == "all":
|
||||
agent_name = '%'
|
||||
|
||||
taskName = request.json['Task']['TaskName']
|
||||
task = request.json['Task']['Task']
|
||||
|
||||
# get existing agent taskings
|
||||
agentTasks = execute_db_query(conn, 'SELECT taskings FROM agents WHERE name like ? OR session_id like ?', [agent_name, agent_name])[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([taskName, task])
|
||||
execute_db_query(conn, "UPDATE agents SET taskings=? WHERE name=? OR session_id=?", [json.dumps(agentTasks), agent_name, agent_name])
|
||||
|
||||
timeStamp = strftime("%Y-%m-%d %H:%M:%S", localtime())
|
||||
execute_db_query(conn, "INSERT INTO reporting (name,event_type,message,time_stamp) VALUES (?,?,?,?)", (agent_name,"task",taskName + " - " + task[0:50], timeStamp ))
|
||||
return jsonify({'AgentName':agent_name, 'TaskType':'Task', 'TaskName':taskName, 'Task':task})
|
||||
|
||||
|
||||
elif 'Clear' in request.json.keys():
|
||||
|
||||
if agent_name.lower() == "all":
|
||||
agent_name = '%'
|
||||
|
||||
execute_db_query(conn, "UPDATE agents SET taskings=? WHERE name like ? OR session_id like ?", ['', agent_name, agent_name])
|
||||
|
||||
return jsonify({'AgentName':agent_name, 'TaskType':'Clear', 'TaskName':'', 'Task':''})
|
||||
|
||||
|
||||
elif 'Rename' in request.json.keys():
|
||||
oldName = request.json['Rename']['OldName']
|
||||
newName = request.json['Rename']['NewName']
|
||||
|
||||
try:
|
||||
main.agents.rename_agent(oldName, newName)
|
||||
return jsonify({'result': True})
|
||||
except:
|
||||
return jsonify({'error': 'error in renaming %s to %s' %(oldName, newName)})
|
||||
|
||||
|
||||
return jsonify({'error':'error in tasking agent %s' % (agent_name)})
|
||||
|
||||
|
||||
@app.route('/empire/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":
|
||||
agent_name = '%'
|
||||
|
||||
agentsRaw = execute_db_query(conn, 'SELECT * FROM agents WHERE name like ? OR session_id like ?', [agent_name, agent_name])
|
||||
removedAgents = {}
|
||||
|
||||
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
|
||||
execute_db_query(conn, "DELETE FROM agents WHERE session_id LIKE ?", [sessionID])
|
||||
|
||||
removedAgents[name] = {"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({'RemovedAgents': removedAgents})
|
||||
|
||||
|
||||
@app.route('/empire/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')
|
||||
removedAgents = {}
|
||||
|
||||
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])
|
||||
|
||||
removedAgents[name] = {"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({'RemovedAgents': removedAgents})
|
||||
|
||||
|
||||
@app.route('/empire/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[ID] = {"ID":ID, "name":name, "event_type":eventType, "message":message, "timestamp":timestamp}
|
||||
|
||||
return jsonify({'reporting' : reportingEvents})
|
||||
|
||||
|
||||
@app.route('/empire/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 * FROM reporting WHERE name=?', [sessionID])
|
||||
reportingEvents = {}
|
||||
|
||||
for reportingEvent in reportingRaw:
|
||||
[ID, name, eventType, message, timestamp] = reportingEvent
|
||||
reportingEvents[ID] = {"ID":ID, "name":name, "event_type":eventType, "message":message, "timestamp":timestamp}
|
||||
|
||||
return jsonify({'reporting' : reportingEvents})
|
||||
|
||||
|
||||
@app.route('/empire/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 * FROM reporting WHERE event_type=?', [event_type])
|
||||
reportingEvents = {}
|
||||
|
||||
for reportingEvent in reportingRaw:
|
||||
[ID, name, eventType, message, timestamp] = reportingEvent
|
||||
reportingEvents[ID] = {"ID":ID, "name":name, "event_type":eventType, "message":message, "timestamp":timestamp}
|
||||
|
||||
return jsonify({'reporting' : reportingEvents})
|
||||
|
||||
|
||||
@app.route('/empire/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 * FROM reporting WHERE message like ?", ['%'+msg+'%'])
|
||||
reportingEvents = {}
|
||||
|
||||
for reportingEvent in reportingRaw:
|
||||
[ID, name, eventType, message, timestamp] = reportingEvent
|
||||
reportingEvents[ID] = {"ID":ID, "name":name, "event_type":eventType, "message":message, "timestamp":timestamp}
|
||||
|
||||
return jsonify({'reporting' : reportingEvents})
|
||||
|
||||
|
||||
@app.route('/empire/api/admin/shutdown', methods=['GET', 'POST', 'PUT'])
|
||||
def shutdown_server():
|
||||
"""
|
||||
Signal a shutdown for the Flask server and any Empire server.
|
||||
"""
|
||||
shutdown_server()
|
||||
return jsonify({'result': 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 server gracefully.
|
||||
"""
|
||||
if suppress:
|
||||
# repair stdout
|
||||
sys.stdout.close()
|
||||
sys.stdout = oldStdout
|
||||
|
||||
print "\n[*]Shutting down Empire RESTful API"
|
||||
|
||||
func = request.environ.get('werkzeug.server.shutdown')
|
||||
if func is not None:
|
||||
func()
|
||||
|
||||
if conn: conn.close()
|
||||
|
||||
if startEmpire:
|
||||
print "Shutting down the Empire instance"
|
||||
main.shutdown()
|
||||
|
||||
|
||||
# override the keyboardinterrupt signal handler so we can gracefully shut everything down
|
||||
def signal_handler(signal, frame):
|
||||
|
||||
with app.test_request_context():
|
||||
shutdown_server()
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
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=port, ssl_context=context, threaded=True)
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
@ -19,11 +643,25 @@ if __name__ == '__main__':
|
|||
parser.add_argument('-o', '--stager-options', nargs='*', help="Supply options to set for a stager in OPTION=VALUE format. Lists options if nothing is specified.")
|
||||
parser.add_argument('-l', '--listener', nargs='?', const="list", help='Display listener options. Displays all listeners if nothing is specified.')
|
||||
parser.add_argument('-v', '--version', action='store_true', help='Display current Empire version.')
|
||||
parser.add_argument('--rest', action='store_true', help='Run the Empire RESTful API.')
|
||||
parser.add_argument('--headless', action='store_true', help='Run Empire and the RESTful API headless without the usual interface.')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.version:
|
||||
print empire.VERSION
|
||||
|
||||
elif args.rest:
|
||||
# start just the RESTful API
|
||||
start_restful_api(startEmpire=False, suppress=False, port=1337)
|
||||
|
||||
elif args.headless:
|
||||
# start an Empire instance and RESTful API and suppress output
|
||||
start_restful_api(startEmpire=True, suppress=True, port=1337)
|
||||
|
||||
else:
|
||||
# normal execution
|
||||
main = empire.MainMenu(args=args)
|
||||
main.cmdloop()
|
||||
|
||||
sys.exit()
|
||||
|
|
|
@ -12,12 +12,7 @@ the response types are handled as appropriate.
|
|||
"""
|
||||
|
||||
from pydispatch import dispatcher
|
||||
import sqlite3
|
||||
import pickle
|
||||
import base64
|
||||
import string
|
||||
import os
|
||||
import iptools
|
||||
import sqlite3, pickle, base64, string, os, iptools, json
|
||||
|
||||
# Empire imports
|
||||
import encryption
|
||||
|
@ -40,31 +35,28 @@ class Agents:
|
|||
self.installPath = self.mainMenu.installPath
|
||||
|
||||
self.args = args
|
||||
|
||||
# internal agent dictionary for the client's session key and tasking/result sets
|
||||
# self.agents[sessionID] = [ clientSessionKey,
|
||||
# [tasking1, tasking2, ...],
|
||||
# [results1, results2, ...],
|
||||
# [tab-completable function names for a script-import],
|
||||
# current URIs,
|
||||
# old URIs
|
||||
# ]
|
||||
|
||||
# internal agent dictionary for the client's session key, funcions, and URI sets
|
||||
# this is done to prevent database reads for extremely common tasks (like checking tasking URI existence)
|
||||
# self.agents[sessionID] = { 'sessionKey' : clientSessionKey,
|
||||
# 'functions' : [tab-completable function names for a script-import],
|
||||
# 'currentURIs' : [current URIs used by the client],
|
||||
# 'oldURIs' : [old URIs used by the client]
|
||||
# }
|
||||
self.agents = {}
|
||||
|
||||
# reinitialize any agents that already exist in the database
|
||||
agentIDs = self.get_agent_ids()
|
||||
for agentID in agentIDs:
|
||||
sessionKey = self.get_agent_session_key(agentID)
|
||||
functions = self.get_agent_functions_database(agentID)
|
||||
self.agents[agentID] = {}
|
||||
self.agents[agentID]['sessionKey'] = self.get_agent_session_key(agentID)
|
||||
self.agents[agentID]['functions'] = self.get_agent_functions_database(agentID)
|
||||
|
||||
# get the current and previous URIs for tasking
|
||||
uris,old_uris = self.get_agent_uris(agentID)
|
||||
|
||||
if not old_uris:
|
||||
old_uris = ""
|
||||
|
||||
# [sessionKey, taskings, results, stored_functions, tasking uris, old uris]
|
||||
self.agents[agentID] = [sessionKey, [], [], functions, uris, old_uris]
|
||||
currentURIs,oldURIs = self.get_agent_uris(agentID)
|
||||
if not oldURIs: oldURIs = ''
|
||||
self.agents[agentID]['currentURIs'] = currentURIs
|
||||
self.agents[agentID]['oldURIs'] = oldURIs
|
||||
|
||||
# pull out common configs from the main menu object in empire.py
|
||||
self.ipWhiteList = self.mainMenu.ipWhiteList
|
||||
|
@ -92,13 +84,13 @@ class Agents:
|
|||
# remove the agent from the internal cache
|
||||
self.agents.pop(sessionID, None)
|
||||
|
||||
# remove an agent from the database
|
||||
# remove the agent from the database
|
||||
cur = self.conn.cursor()
|
||||
cur.execute("DELETE FROM agents WHERE session_id like ?", [sessionID])
|
||||
cur.execute("DELETE FROM agents WHERE session_id LIKE ?", [sessionID])
|
||||
cur.close()
|
||||
|
||||
|
||||
def add_agent(self, sessionID, externalIP, delay, jitter, profile, killDate, workingHours,lostLimit):
|
||||
def add_agent(self, sessionID, externalIP, delay, jitter, profile, killDate, workingHours, lostLimit):
|
||||
"""
|
||||
Add an agent to the internal cache and database.
|
||||
"""
|
||||
|
@ -133,7 +125,8 @@ class Agents:
|
|||
|
||||
# initialize the tasking/result buffers along with the client session key
|
||||
sessionKey = self.get_agent_session_key(sessionID)
|
||||
self.agents[sessionID] = [sessionKey, [],[],[], requestUris, ""]
|
||||
# TODO: should oldURIs be a string or list?
|
||||
self.agents[sessionID] = {'sessionKey':sessionKey, 'functions':[], 'currentURIs':requestUris, 'oldURIs': ''}
|
||||
|
||||
# report the initial checkin in the reporting database
|
||||
cur = self.conn.cursor()
|
||||
|
@ -159,7 +152,7 @@ class Agents:
|
|||
"""
|
||||
|
||||
for option,values in self.agents.iteritems():
|
||||
if resource in values[-1] or resource in values [-2]:
|
||||
if resource in values['currentURIs'] or resource in values['oldURIs']:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
@ -283,7 +276,6 @@ class Agents:
|
|||
#
|
||||
###############################################################
|
||||
|
||||
|
||||
def get_agents(self):
|
||||
"""
|
||||
Return all active agents from the database.
|
||||
|
@ -388,7 +380,7 @@ class Agents:
|
|||
|
||||
if ps_version and ps_version != None:
|
||||
if type(ps_version) is str:
|
||||
return sessionKey
|
||||
return ps_version
|
||||
else:
|
||||
return ps_version[0]
|
||||
|
||||
|
@ -416,7 +408,7 @@ class Agents:
|
|||
|
||||
def get_agent_results(self, sessionID):
|
||||
"""
|
||||
Get the agent's results buffer.
|
||||
Return agent results from the backend database.
|
||||
"""
|
||||
|
||||
agentName = sessionID
|
||||
|
@ -426,11 +418,21 @@ class Agents:
|
|||
if nameid : sessionID = nameid
|
||||
|
||||
if sessionID not in self.agents:
|
||||
print helpers.color("[!] Agent " + str(agentName) + " not active.")
|
||||
print helpers.color("[!] Agent %s not active." %(agentName))
|
||||
else:
|
||||
results = self.agents[sessionID][2]
|
||||
self.agents[sessionID][2] = []
|
||||
return "\n".join(results)
|
||||
cur = self.conn.cursor()
|
||||
cur.execute("SELECT results FROM agents WHERE session_id=?", [sessionID])
|
||||
results = cur.fetchone()
|
||||
|
||||
cur.execute("UPDATE agents SET results = ? WHERE session_id=?", ['',sessionID])
|
||||
|
||||
if results and results[0] and results[0] != '':
|
||||
out = json.loads(results[0])
|
||||
if(out):
|
||||
return "\n".join(out)
|
||||
else:
|
||||
return ''
|
||||
cur.close()
|
||||
|
||||
|
||||
def get_agent_id(self, name):
|
||||
|
@ -474,6 +476,7 @@ class Agents:
|
|||
else:
|
||||
return None
|
||||
|
||||
|
||||
def get_agent_functions(self, sessionID):
|
||||
"""
|
||||
Get the tab-completable functions for an agent.
|
||||
|
@ -484,7 +487,7 @@ class Agents:
|
|||
if nameid : sessionID = nameid
|
||||
|
||||
if sessionID in self.agents:
|
||||
return self.agents[sessionID][3]
|
||||
return self.agents[sessionID]['functions']
|
||||
else:
|
||||
return []
|
||||
|
||||
|
@ -552,6 +555,7 @@ class Agents:
|
|||
except:
|
||||
pass
|
||||
|
||||
|
||||
###############################################################
|
||||
#
|
||||
# Methods to update agent information fields.
|
||||
|
@ -568,7 +572,21 @@ class Agents:
|
|||
if nameid : sessionID = nameid
|
||||
|
||||
if sessionID in self.agents:
|
||||
self.agents[sessionID][2].append(results)
|
||||
cur = self.conn.cursor()
|
||||
|
||||
# get existing agent results
|
||||
cur.execute("SELECT results FROM agents WHERE session_id LIKE ?", [sessionID])
|
||||
agentResults = cur.fetchone()
|
||||
|
||||
if(agentResults and agentResults[0]):
|
||||
agentResults = json.loads(agentResults[0])
|
||||
else:
|
||||
agentResults = []
|
||||
|
||||
agentResults.append(results)
|
||||
|
||||
cur.execute("UPDATE agents SET results = ? WHERE session_id=?", [json.dumps(agentResults),sessionID])
|
||||
cur.close()
|
||||
else:
|
||||
dispatcher.send("[!] Non-existent agent " + str(sessionID) + " returned results", sender="Agents")
|
||||
|
||||
|
@ -615,8 +633,8 @@ class Agents:
|
|||
cur = self.conn.cursor()
|
||||
|
||||
# get the existing URIs from the agent and save them to
|
||||
# the old_uris field, so we can ensure that it can check in
|
||||
# to get the new URI tasking... bootstrapping problem :)
|
||||
# the old_uris field, so we can ensure that it can check in
|
||||
# to get the new URI tasking... bootstrapping problem :)
|
||||
cur.execute("SELECT uris FROM agents WHERE session_id=?", [sessionID])
|
||||
oldURIs = cur.fetchone()[0]
|
||||
|
||||
|
@ -624,9 +642,8 @@ class Agents:
|
|||
print helpers.color("[!] Agent " + agentName + " not active.")
|
||||
else:
|
||||
# update the URIs in the cache
|
||||
self.agents[sessionID][-1] = oldURIs
|
||||
# new URIs
|
||||
self.agents[sessionID][-2] = parts[0]
|
||||
self.agents[sessionID]['oldURIs'] = oldURIs
|
||||
self.agents[sessionID]['currentURIs'] = parts[0]
|
||||
|
||||
# if no additional headers
|
||||
if len(parts) == 2:
|
||||
|
@ -696,7 +713,7 @@ class Agents:
|
|||
if nameid : sessionID = nameid
|
||||
|
||||
if sessionID in self.agents:
|
||||
self.agents[sessionID][3] = functions
|
||||
self.agents[sessionID]['functions'] = functions
|
||||
|
||||
functions = ",".join(functions)
|
||||
|
||||
|
@ -756,8 +773,22 @@ class Agents:
|
|||
print helpers.color("[!] Agent " + str(agentName) + " not active.")
|
||||
else:
|
||||
if sessionID:
|
||||
|
||||
dispatcher.send("[*] Tasked " + str(sessionID) + " to run " + str(taskName), sender="Agents")
|
||||
self.agents[sessionID][1].append([taskName, task])
|
||||
|
||||
# get existing agent taskings
|
||||
cur = self.conn.cursor()
|
||||
cur.execute("SELECT taskings FROM agents WHERE session_id=?", [sessionID])
|
||||
agentTasks = cur.fetchone()
|
||||
|
||||
if(agentTasks and agentTasks[0]):
|
||||
agentTasks = json.loads(agentTasks[0])
|
||||
else:
|
||||
agentTasks = []
|
||||
|
||||
# append our new json-ified task and update the backend
|
||||
agentTasks.append([taskName, task])
|
||||
cur.execute("UPDATE agents SET taskings=? WHERE session_id=?", [json.dumps(agentTasks),sessionID])
|
||||
|
||||
# write out the last tasked script to "LastTask.ps1" if in debug mode
|
||||
if self.args and self.args.debug:
|
||||
|
@ -766,8 +797,7 @@ class Agents:
|
|||
f.close()
|
||||
|
||||
# report the agent tasking in the reporting database
|
||||
cur = self.conn.cursor()
|
||||
cur.execute("INSERT INTO reporting (name,event_type,message,time_stamp) VALUES (?,?,?,?)", (sessionID,"task",taskName + " - " + task[0:30],helpers.get_datetime()))
|
||||
cur.execute("INSERT INTO reporting (name,event_type,message,time_stamp) VALUES (?,?,?,?)", (sessionID,"task",taskName + " - " + task[0:50],helpers.get_datetime()))
|
||||
cur.close()
|
||||
|
||||
|
||||
|
@ -786,47 +816,37 @@ class Agents:
|
|||
print helpers.color("[!] Agent " + str(agentName) + " not active.")
|
||||
return []
|
||||
else:
|
||||
tasks = self.agents[sessionID][1]
|
||||
# clear the taskings out
|
||||
self.agents[sessionID][1] = []
|
||||
|
||||
cur = self.conn.cursor()
|
||||
cur.execute("SELECT taskings FROM agents WHERE session_id=?", [sessionID])
|
||||
tasks = cur.fetchone()
|
||||
|
||||
if(tasks and tasks[0]):
|
||||
tasks = json.loads(tasks[0])
|
||||
|
||||
# clear the taskings out
|
||||
cur.execute("UPDATE agents SET taskings=? WHERE session_id=?", ['', sessionID])
|
||||
else:
|
||||
tasks = []
|
||||
|
||||
cur.close()
|
||||
|
||||
return tasks
|
||||
|
||||
|
||||
def get_agent_task(self, sessionID):
|
||||
"""
|
||||
Pop off the agent's top task.
|
||||
"""
|
||||
|
||||
# see if we were passed a name instead of an ID
|
||||
nameid = self.get_agent_id(sessionID)
|
||||
if nameid : sessionID = nameid
|
||||
|
||||
try:
|
||||
# pop the first task off the front of the stack
|
||||
return self.agents[sessionID][1].pop(0)
|
||||
except:
|
||||
[]
|
||||
|
||||
|
||||
def clear_agent_tasks(self, sessionID):
|
||||
"""
|
||||
Clear out the agent's task buffer.
|
||||
Clear out one (or all) agent's task buffer.
|
||||
"""
|
||||
|
||||
agentName = sessionID
|
||||
|
||||
if sessionID.lower() == "all":
|
||||
for option,values in self.agents.iteritems():
|
||||
self.agents[option][1] = []
|
||||
else:
|
||||
# see if we were passed a name instead of an ID
|
||||
nameid = self.get_agent_id(sessionID)
|
||||
if nameid : sessionID = nameid
|
||||
sessionID = '%'
|
||||
|
||||
if sessionID not in self.agents:
|
||||
print helpers.color("[!] Agent " + agentName + " not active.")
|
||||
else:
|
||||
self.agents[sessionID][1] = []
|
||||
cur = self.conn.cursor()
|
||||
cur.execute("UPDATE agents SET taskings=? WHERE session_id LIKE ?", ['', sessionID])
|
||||
cur.close()
|
||||
|
||||
|
||||
def handle_agent_response(self, sessionID, responseName, data):
|
||||
|
@ -1122,7 +1142,7 @@ class Agents:
|
|||
allTaskPackets += taskPacket
|
||||
|
||||
# get the session key for the agent
|
||||
sessionKey = self.agents[sessionID][0]
|
||||
sessionKey = self.agents[sessionID]['sessionKey']
|
||||
|
||||
# encrypt the tasking packets with the agent's session key
|
||||
encryptedData = encryption.aes_encrypt_then_mac(sessionKey, allTaskPackets)
|
||||
|
@ -1199,7 +1219,7 @@ class Agents:
|
|||
else:
|
||||
|
||||
# extract the agent's session key
|
||||
sessionKey = self.agents[sessionID][0]
|
||||
sessionKey = self.agents[sessionID]['sessionKey']
|
||||
|
||||
try:
|
||||
# verify, decrypt and depad the packet
|
||||
|
@ -1241,7 +1261,7 @@ class Agents:
|
|||
return (404, "")
|
||||
|
||||
except Exception as e:
|
||||
dispatcher.send("[!] Error processing result packet from "+str(sessionID), sender="Agents")
|
||||
dispatcher.send("[!] Error processing result packet from %s : %s" %(str(sessionID),e), sender="Agents")
|
||||
return (404, "")
|
||||
|
||||
|
||||
|
@ -1330,7 +1350,7 @@ class Agents:
|
|||
lostLimit = config[11]
|
||||
|
||||
# get the session key for the agent
|
||||
sessionKey = self.agents[sessionID][0]
|
||||
sessionKey = self.agents[sessionID]['sessionKey']
|
||||
|
||||
try:
|
||||
# decrypt and parse the agent's sysinfo checkin
|
||||
|
|
|
@ -9,7 +9,7 @@ menu loops.
|
|||
"""
|
||||
|
||||
# make version for Empire
|
||||
VERSION = "1.4.4"
|
||||
VERSION = "1.4.6"
|
||||
|
||||
|
||||
from pydispatch import dispatcher
|
||||
|
@ -39,7 +39,7 @@ class NavListeners(Exception): pass
|
|||
|
||||
class MainMenu(cmd.Cmd):
|
||||
|
||||
def __init__(self, args=None):
|
||||
def __init__(self, args=None, restAPI=False):
|
||||
|
||||
cmd.Cmd.__init__(self)
|
||||
|
||||
|
@ -49,42 +49,8 @@ class MainMenu(cmd.Cmd):
|
|||
# empty database object
|
||||
self.conn = self.database_connect()
|
||||
|
||||
# grab the universal install path
|
||||
# TODO: combine these into one query
|
||||
cur = self.conn.cursor()
|
||||
cur.execute("SELECT install_path FROM config")
|
||||
self.installPath = cur.fetchone()[0]
|
||||
cur.close()
|
||||
|
||||
# pull out the stage0 uri
|
||||
cur = self.conn.cursor()
|
||||
cur.execute("SELECT stage0_uri FROM config")
|
||||
self.stage0 = cur.fetchone()[0]
|
||||
cur.close()
|
||||
|
||||
# pull out the stage1 uri
|
||||
cur = self.conn.cursor()
|
||||
cur.execute("SELECT stage1_uri FROM config")
|
||||
self.stage1 = cur.fetchone()[0]
|
||||
cur.close()
|
||||
|
||||
# pull out the stage2 uri
|
||||
cur = self.conn.cursor()
|
||||
cur.execute("SELECT stage2_uri FROM config")
|
||||
self.stage2 = cur.fetchone()[0]
|
||||
cur.close()
|
||||
|
||||
# pull out the IP whitelist and create it, if applicable
|
||||
cur = self.conn.cursor()
|
||||
cur.execute("SELECT ip_whitelist FROM config")
|
||||
self.ipWhiteList = helpers.generate_ip_list(cur.fetchone()[0])
|
||||
cur.close()
|
||||
|
||||
# pull out the IP blacklist and create it, if applicable
|
||||
cur = self.conn.cursor()
|
||||
cur.execute("SELECT ip_blacklist FROM config")
|
||||
self.ipBlackList = helpers.generate_ip_list(cur.fetchone()[0])
|
||||
cur.close()
|
||||
# pull out some common configuration information
|
||||
(self.installPath, self.stage0, self.stage1, self.stage2, self.ipWhiteList, self.ipBlackList) = helpers.get_config('install_path,stage0_uri,stage1_uri,stage2_uri,ip_whitelist,ip_blacklist')
|
||||
|
||||
# instantiate the agents, listeners, and stagers objects
|
||||
self.agents = agents.Agents(self, args=args)
|
||||
|
@ -94,7 +60,6 @@ class MainMenu(cmd.Cmd):
|
|||
self.credentials = credentials.Credentials(self, args=args)
|
||||
|
||||
# make sure all the references are passed after instantiation
|
||||
# TODO: replace these with self?
|
||||
self.agents.listeners = self.listeners
|
||||
self.agents.modules = self.modules
|
||||
self.agents.stagers = self.stagers
|
||||
|
@ -116,8 +81,9 @@ class MainMenu(cmd.Cmd):
|
|||
self.args = args
|
||||
self.handle_args()
|
||||
|
||||
# start everything up normally
|
||||
self.startup()
|
||||
# start everything up normally if the RESTful API isn't being launched
|
||||
if not restAPI:
|
||||
self.startup()
|
||||
|
||||
|
||||
def handle_args(self):
|
||||
|
@ -252,7 +218,7 @@ class MainMenu(cmd.Cmd):
|
|||
else:
|
||||
num_modules = 0
|
||||
|
||||
num_listeners = self.listeners.listeners
|
||||
num_listeners = self.listeners.get_listeners()
|
||||
if(num_listeners):
|
||||
num_listeners = len(num_listeners)
|
||||
else:
|
||||
|
@ -407,6 +373,7 @@ class MainMenu(cmd.Cmd):
|
|||
except Exception as e:
|
||||
raise e
|
||||
|
||||
|
||||
def do_usemodule(self, line):
|
||||
"Use an Empire module."
|
||||
if line not in self.modules.modules:
|
||||
|
@ -418,6 +385,7 @@ class MainMenu(cmd.Cmd):
|
|||
except Exception as e:
|
||||
raise e
|
||||
|
||||
|
||||
def do_searchmodule(self, line):
|
||||
"Search Empire module names/descriptions."
|
||||
self.modules.search_modules(line.strip())
|
||||
|
@ -748,7 +716,7 @@ class AgentsMenu(cmd.Cmd):
|
|||
# traceback.print_stack()
|
||||
|
||||
# print a nicely formatted help menu
|
||||
# stolen/adapted from recon-ng
|
||||
# stolen/adapted from recon-ng
|
||||
def print_topics(self, header, cmds, cmdlen, maxcol):
|
||||
if cmds:
|
||||
self.stdout.write("%s\n"%str(header))
|
||||
|
@ -763,8 +731,13 @@ class AgentsMenu(cmd.Cmd):
|
|||
|
||||
|
||||
def do_back(self, line):
|
||||
"Return back a menu."
|
||||
return True
|
||||
"Go back to the main menu."
|
||||
raise NavMain()
|
||||
|
||||
|
||||
def do_listeners(self, line):
|
||||
"Jump to the listeners menu."
|
||||
raise NavListeners()
|
||||
|
||||
|
||||
def do_main(self, line):
|
||||
|
@ -796,8 +769,6 @@ class AgentsMenu(cmd.Cmd):
|
|||
# name sure we get an old name and new name for the agent
|
||||
if len(parts) == 2:
|
||||
# replace the old name with the new name
|
||||
oldname = parts[0]
|
||||
newname = parts[1]
|
||||
self.mainMenu.agents.rename_agent(parts[0], parts[1])
|
||||
else:
|
||||
print helpers.color("[!] Please enter an agent name and new name")
|
||||
|
@ -1107,11 +1078,6 @@ class AgentsMenu(cmd.Cmd):
|
|||
print helpers.color("[!] Invalid agent name")
|
||||
|
||||
|
||||
def do_listeners(self, line):
|
||||
"Jump to the listeners menu."
|
||||
raise NavListeners()
|
||||
|
||||
|
||||
def do_usestager(self, line):
|
||||
"Use an Empire stager."
|
||||
|
||||
|
@ -1334,9 +1300,9 @@ class AgentMenu(cmd.Cmd):
|
|||
return True
|
||||
|
||||
|
||||
def do_main(self, line):
|
||||
"Go back to the main menu."
|
||||
raise NavMain()
|
||||
def do_agents(self, line):
|
||||
"Jump to the Agents menu."
|
||||
raise NavAgents()
|
||||
|
||||
|
||||
def do_listeners(self, line):
|
||||
|
@ -1344,9 +1310,9 @@ class AgentMenu(cmd.Cmd):
|
|||
raise NavListeners()
|
||||
|
||||
|
||||
def do_agents(self, line):
|
||||
"Jump to the Agents menu."
|
||||
raise NavAgents()
|
||||
def do_main(self, line):
|
||||
"Go back to the main menu."
|
||||
raise NavMain()
|
||||
|
||||
|
||||
def do_help(self, *args):
|
||||
|
@ -1538,7 +1504,6 @@ class AgentMenu(cmd.Cmd):
|
|||
self.mainMenu.agents.save_agent_log(self.sessionID, msg)
|
||||
|
||||
|
||||
|
||||
def do_shell(self, line):
|
||||
"Task an agent to use a shell command."
|
||||
|
||||
|
@ -2042,11 +2007,6 @@ class ListenerMenu(cmd.Cmd):
|
|||
def emptyline(self): pass
|
||||
|
||||
|
||||
def do_exit(self, line):
|
||||
"Exit Empire."
|
||||
raise KeyboardInterrupt
|
||||
|
||||
|
||||
def do_list(self, line):
|
||||
"List all active listeners (or agents)."
|
||||
|
||||
|
@ -2059,8 +2019,13 @@ class ListenerMenu(cmd.Cmd):
|
|||
|
||||
|
||||
def do_back(self, line):
|
||||
"Go back a menu."
|
||||
return True
|
||||
"Go back to the main menu."
|
||||
raise NavMain()
|
||||
|
||||
|
||||
def do_agents(self, line):
|
||||
"Jump to the Agents menu."
|
||||
raise NavAgents()
|
||||
|
||||
|
||||
def do_main(self, line):
|
||||
|
@ -2068,6 +2033,11 @@ class ListenerMenu(cmd.Cmd):
|
|||
raise NavMain()
|
||||
|
||||
|
||||
def do_exit(self, line):
|
||||
"Exit Empire."
|
||||
raise KeyboardInterrupt
|
||||
|
||||
|
||||
def do_set(self, line):
|
||||
"Set a listener option."
|
||||
parts = line.split(" ")
|
||||
|
@ -2158,11 +2128,6 @@ class ListenerMenu(cmd.Cmd):
|
|||
self.do_execute(line)
|
||||
|
||||
|
||||
def do_agents(self, line):
|
||||
"Jump to the Agents menu."
|
||||
raise NavAgents()
|
||||
|
||||
|
||||
def do_usestager(self, line):
|
||||
"Use an Empire stager."
|
||||
|
||||
|
@ -2370,6 +2335,11 @@ class ModuleMenu(cmd.Cmd):
|
|||
self.stdout.write("\n")
|
||||
|
||||
|
||||
def do_back(self, line):
|
||||
"Go back a menu."
|
||||
return True
|
||||
|
||||
|
||||
def do_agents(self, line):
|
||||
"Jump to the Agents menu."
|
||||
raise NavAgents()
|
||||
|
@ -2380,16 +2350,16 @@ class ModuleMenu(cmd.Cmd):
|
|||
raise NavListeners()
|
||||
|
||||
|
||||
def do_main(self, line):
|
||||
"Go back to the main menu."
|
||||
raise NavMain()
|
||||
|
||||
|
||||
def do_exit(self, line):
|
||||
"Exit Empire."
|
||||
raise KeyboardInterrupt
|
||||
|
||||
|
||||
def do_main(self, line):
|
||||
"Return to the main menu."
|
||||
return True
|
||||
|
||||
|
||||
def do_list(self, line):
|
||||
"Lists all active agents (or listeners)."
|
||||
|
||||
|
@ -2422,16 +2392,6 @@ class ModuleMenu(cmd.Cmd):
|
|||
messages.display_module(self.moduleName, self.module)
|
||||
|
||||
|
||||
def do_back(self, line):
|
||||
"Return to the main menu."
|
||||
return True
|
||||
|
||||
|
||||
def do_main(self, line):
|
||||
"Go back to the main menu."
|
||||
raise NavMain()
|
||||
|
||||
|
||||
def do_set(self, line):
|
||||
"Set a module option."
|
||||
|
||||
|
@ -2703,14 +2663,29 @@ class StagerMenu(cmd.Cmd):
|
|||
self.stdout.write("\n")
|
||||
|
||||
|
||||
def do_exit(self, line):
|
||||
"Exit Empire."
|
||||
raise KeyboardInterrupt
|
||||
def do_back(self, line):
|
||||
"Go back a menu."
|
||||
return True
|
||||
|
||||
|
||||
def do_agents(self, line):
|
||||
"Jump to the Agents menu."
|
||||
raise NavAgents()
|
||||
|
||||
|
||||
def do_listeners(self, line):
|
||||
"Jump to the listeners menu."
|
||||
raise NavListeners()
|
||||
|
||||
|
||||
def do_main(self, line):
|
||||
"Return to the main menu."
|
||||
return True
|
||||
"Go back to the main menu."
|
||||
raise NavMain()
|
||||
|
||||
|
||||
def do_exit(self, line):
|
||||
"Exit Empire."
|
||||
raise KeyboardInterrupt
|
||||
|
||||
|
||||
def do_list(self, line):
|
||||
|
@ -2734,16 +2709,6 @@ class StagerMenu(cmd.Cmd):
|
|||
messages.display_stager(self.stagerName, self.stager)
|
||||
|
||||
|
||||
def do_back(self, line):
|
||||
"Return to the main menu."
|
||||
return True
|
||||
|
||||
|
||||
def do_main(self, line):
|
||||
"Go back to the main menu."
|
||||
raise NavMain()
|
||||
|
||||
|
||||
def do_set(self, line):
|
||||
"Set a stager option."
|
||||
|
||||
|
@ -2826,7 +2791,6 @@ class StagerMenu(cmd.Cmd):
|
|||
|
||||
def do_execute(self, line):
|
||||
"Generate/execute the given Empire stager."
|
||||
|
||||
self.do_generate(line)
|
||||
|
||||
|
||||
|
@ -2862,13 +2826,3 @@ class StagerMenu(cmd.Cmd):
|
|||
mline = line.partition(' ')[2]
|
||||
offs = len(mline) - len(text)
|
||||
return [s[offs:] for s in options if s.startswith(mline)]
|
||||
|
||||
|
||||
def do_agents(self, line):
|
||||
"Jump to the Agents menu."
|
||||
raise NavAgents()
|
||||
|
||||
|
||||
def do_listeners(self, line):
|
||||
"Jump to the listeners menu."
|
||||
raise NavListeners()
|
||||
|
|
|
@ -7,18 +7,9 @@ randomized stagers.
|
|||
|
||||
"""
|
||||
|
||||
import re, string, commands, base64, binascii, sys, os, socket, sqlite3, iptools
|
||||
from time import localtime, strftime
|
||||
from Crypto.Random import random
|
||||
import re
|
||||
import string
|
||||
import commands
|
||||
import base64
|
||||
import binascii
|
||||
import sys
|
||||
import os
|
||||
import socket
|
||||
import sqlite3
|
||||
import iptools
|
||||
|
||||
|
||||
###############################################################
|
||||
|
@ -501,7 +492,7 @@ def get_config(fields):
|
|||
conn.isolation_level = None
|
||||
|
||||
cur = conn.cursor()
|
||||
cur.execute("SELECT "+fields+" FROM config")
|
||||
cur.execute("SELECT %s FROM config" %(fields))
|
||||
results = cur.fetchone()
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
@ -622,14 +613,6 @@ def uniquify_tuples(tuples):
|
|||
return [item for item in tuples if "%s%s%s%s"%(item[0],item[1],item[2],item[3]) not in seen and not seen.add("%s%s%s%s"%(item[0],item[1],item[2],item[3]))]
|
||||
|
||||
|
||||
def urldecode(url):
|
||||
"""
|
||||
URL decode a string.
|
||||
"""
|
||||
rex=re.compile('%([0-9a-hA-H][0-9a-hA-H])',re.M)
|
||||
return rex.sub(htc,url)
|
||||
|
||||
|
||||
def decode_base64(data):
|
||||
"""
|
||||
Try to decode a base64 string.
|
||||
|
|
|
@ -111,17 +111,18 @@ class RequestHandler(BaseHTTPRequestHandler):
|
|||
dispatcher.send("[*] Post to "+resource+" from "+str(sessionID)+" at "+clientIP, sender="HttpHandler")
|
||||
|
||||
# read in the length of the POST data
|
||||
length = int(self.headers.getheader('content-length'))
|
||||
postData = self.rfile.read(length)
|
||||
if self.headers.getheader('content-length'):
|
||||
length = int(self.headers.getheader('content-length'))
|
||||
postData = self.rfile.read(length)
|
||||
|
||||
# get the appropriate response for this agent
|
||||
(code, responsedata) = self.server.agents.process_post(self.server.server_port, clientIP, sessionID, resource, postData)
|
||||
# get the appropriate response for this agent
|
||||
(code, responsedata) = self.server.agents.process_post(self.server.server_port, clientIP, sessionID, resource, postData)
|
||||
|
||||
# write the response out
|
||||
self.send_response(code)
|
||||
self.end_headers()
|
||||
self.wfile.write(responsedata)
|
||||
self.wfile.flush()
|
||||
# write the response out
|
||||
self.send_response(code)
|
||||
self.end_headers()
|
||||
self.wfile.write(responsedata)
|
||||
self.wfile.flush()
|
||||
# self.wfile.close() # causes an error with HTTP comms
|
||||
|
||||
# supress all the stupid default stdout/stderr output
|
||||
|
|
|
@ -200,12 +200,15 @@ class Listeners:
|
|||
else:
|
||||
self.options['Port']['Value'] = "80"
|
||||
|
||||
return True
|
||||
|
||||
elif option == "CertPath":
|
||||
self.options[option]['Value'] = value
|
||||
host = self.options["Host"]['Value']
|
||||
# if we're setting a SSL cert path, but the host is specific at http
|
||||
if host.startswith("http:"):
|
||||
self.options["Host"]['Value'] = self.options["Host"]['Value'].replace("http:", "https:")
|
||||
return True
|
||||
|
||||
elif option == "Port":
|
||||
self.options[option]['Value'] = value
|
||||
|
@ -214,11 +217,13 @@ class Listeners:
|
|||
parts = host.split(":")
|
||||
if len(parts) == 2 or len(parts) == 3:
|
||||
self.options["Host"]['Value'] = parts[0] + ":" + parts[1] + ":" + str(value)
|
||||
return True
|
||||
|
||||
elif option == "StagingKey":
|
||||
# if the staging key isn't 32 characters, assume we're md5 hashing it
|
||||
if len(value) != 32:
|
||||
self.options[option]['Value'] = hashlib.md5(value).hexdigest()
|
||||
return True
|
||||
|
||||
elif option in self.options:
|
||||
|
||||
|
@ -228,9 +233,11 @@ class Listeners:
|
|||
# set the profile for hop.php for hop
|
||||
parts = self.options['DefaultProfile']['Value'].split("|")
|
||||
self.options['DefaultProfile']['Value'] = "/hop.php|" + "|".join(parts[1:])
|
||||
return True
|
||||
|
||||
else:
|
||||
print helpers.color("[!] Error: invalid option name")
|
||||
|
||||
return False
|
||||
|
||||
def get_listener_options(self):
|
||||
"""
|
||||
|
@ -404,7 +411,7 @@ class Listeners:
|
|||
|
||||
if(listenerId):
|
||||
cur = self.conn.cursor()
|
||||
cur.execute('SELECT host,port,cert_path,staging_key,default_delay,default_jitter,default_profile,kill_date,working_hours,listener_type,redirect_target,default_lost_limit FROM listeners WHERE id=? or name=? limit 1', [listenerID, listenerID])
|
||||
cur.execute('SELECT host,port,cert_path,staging_key,default_delay,default_jitter,default_profile,kill_date,working_hours,listener_type,redirect_target,default_lost_limit FROM listeners WHERE id=? or name=? limit 1', [listenerId, listenerId])
|
||||
stagingInformation = cur.fetchone()
|
||||
cur.close()
|
||||
|
||||
|
|
|
@ -124,7 +124,7 @@ def agent_print (agents):
|
|||
print " --------- ----------- ------------ --------- ------- ----- --------------------"
|
||||
|
||||
for agent in agents:
|
||||
[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] = agent
|
||||
[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
|
||||
if str(high_integrity) == "1":
|
||||
# add a * to the username if it's high integrity
|
||||
username = "*" + username
|
||||
|
@ -160,7 +160,7 @@ def display_agent(agent):
|
|||
"""
|
||||
|
||||
# extract out database fields.
|
||||
keys = ["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"]
|
||||
keys = ["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", "takings", "results"]
|
||||
|
||||
print helpers.color("\n[*] Agent info:\n")
|
||||
|
||||
|
@ -168,7 +168,7 @@ def display_agent(agent):
|
|||
agentInfo = dict(zip(keys, agent))
|
||||
|
||||
for key in agentInfo:
|
||||
if key != "functions":
|
||||
if key != "functions" and key != "takings" and key != "results":
|
||||
print "\t%s\t%s" % (helpers.color('{0: <16}'.format(key), "blue"), wrap_string(agentInfo[key], width=70))
|
||||
print ""
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ if lsb_release -d | grep -q "Fedora"; then
|
|||
pip install pycrypto
|
||||
pip install iptools
|
||||
pip install pydispatcher
|
||||
pip install flask
|
||||
elif lsb_release -d | grep -q "Kali"; then
|
||||
Release=Kali
|
||||
apt-get install python-dev
|
||||
|
@ -23,6 +24,7 @@ elif lsb_release -d | grep -q "Kali"; then
|
|||
pip install pycrypto
|
||||
pip install iptools
|
||||
pip install pydispatcher
|
||||
pip install flask
|
||||
elif lsb_release -d | grep -q "Ubuntu"; then
|
||||
Release=Ubuntu
|
||||
apt-get install python-dev
|
||||
|
@ -31,6 +33,7 @@ elif lsb_release -d | grep -q "Ubuntu"; then
|
|||
pip install pycrypto
|
||||
pip install iptools
|
||||
pip install pydispatcher
|
||||
pip install flask
|
||||
else
|
||||
echo "Unknown distro - Debian/Ubuntu Fallback"
|
||||
apt-get install python-dev
|
||||
|
@ -39,6 +42,7 @@ else
|
|||
pip install pycrypto
|
||||
pip install iptools
|
||||
pip install pydispatcher
|
||||
pip install flask
|
||||
fi
|
||||
|
||||
# set up the database schema
|
||||
|
|
|
@ -104,11 +104,12 @@ c.execute('''CREATE TABLE config (
|
|||
"ip_blacklist" text,
|
||||
"default_lost_limit" integer,
|
||||
"autorun_command" text,
|
||||
"autorun_data" text
|
||||
"autorun_data" text,
|
||||
"api_token" text
|
||||
)''')
|
||||
|
||||
# kick off the config component of the database
|
||||
c.execute("INSERT INTO config VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)", (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, "", ""))
|
||||
c.execute("INSERT INTO config VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?, ?)", (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, "", "", ""))
|
||||
|
||||
c.execute('''CREATE TABLE "agents" (
|
||||
"id" integer PRIMARY KEY,
|
||||
|
@ -139,7 +140,9 @@ c.execute('''CREATE TABLE "agents" (
|
|||
"kill_date" text,
|
||||
"working_hours" text,
|
||||
"ps_version" text,
|
||||
"lost_limit" integer
|
||||
"lost_limit" integer,
|
||||
"taskings" text,
|
||||
"results" text
|
||||
)''')
|
||||
|
||||
c.execute('''CREATE TABLE "listeners" (
|
||||
|
|
Loading…
Reference in New Issue