Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Socketio notifications #335

Merged
merged 11 commits into from
Oct 13, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,17 @@ The following operating systems have been tested for Empire compatibility. We wi

__Note:__ Newer versions of Kali require you to run ```sudo``` before starting Empire.

Beginning with Empire 3.5.0, we recommend the use of [Poetry](https://python-poetry.org/docs/) or the Docker images to run Empire. Poetry is a dependency and virtual environment management tool.
This is highly recommended if using the SocketIO notification feature introduced in 3.5.0. To install Poetry, please follow the installation guide in the documentation or run `sudo pip3 install poetry`.

```sh
git clone https://github.com/BC-SECURITY/Empire.git
cd Empire
sudo ./setup/install.sh
sudo poetry install
sudo poetry run python empire --rest
```

### Kali

You can install the latest version of Empire by running the following:
Expand Down
80 changes: 63 additions & 17 deletions empire
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,11 @@ from time import sleep

from flask import Flask, request, jsonify, make_response, abort, url_for, g
from flask.json import JSONEncoder
from flask_socketio import SocketIO, emit

# Empire imports
from lib.common import empire, helpers, users
from lib.common.empire import MainMenu

# Check if running Python 3
if sys.version[0] == '2':
Expand Down Expand Up @@ -183,8 +185,7 @@ def execute_db_query(conn, query, args=None):
# PUT http://localhost:1337/api/users/Y/updatepassword update password for user Y
#
####################################################################

def start_restful_api(empireMenu, suppress=False, username=None, password=None, port=1337):
def start_restful_api(empireMenu: MainMenu, suppress=False, username=None, password=None, port=1337):
"""
Kick off the RESTful API with the given parameters.

Expand All @@ -202,18 +203,16 @@ def start_restful_api(empireMenu, suppress=False, username=None, password=None,

main = empireMenu

u = users.Users(main)

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 username:
u.update_username(1, username[0])
main.users.update_username(1, username[0])

if password:
u.update_password(1, password[0])
main.users.update_password(1, password[0])

print('')
print(" * Starting Empire RESTful API on port: %s" % (port))
Expand All @@ -237,7 +236,7 @@ def start_restful_api(empireMenu, suppress=False, username=None, password=None,
if request.path != '/api/admin/login':
token = request.args.get('token')
if token and len(token) > 0:
user = u.get_user_from_token(token)
user = main.users.get_user_from_token(token)
if user:
g.user = user
else:
Expand Down Expand Up @@ -1345,7 +1344,7 @@ def start_restful_api(empireMenu, suppress=False, username=None, password=None,

# try to prevent some basic bruting
time.sleep(2)
token = u.user_login(suppliedUsername, suppliedPassword)
token = main.users.user_login(suppliedUsername, suppliedPassword)

if token:
return jsonify({'token': token})
Expand All @@ -1357,7 +1356,7 @@ def start_restful_api(empireMenu, suppress=False, username=None, password=None,
"""
Logs out current user
"""
u.user_logout(g.user['id'])
main.users.user_logout(g.user['id'])
return jsonify({'success': True})

@app.route('/api/admin/restart', methods=['GET', 'POST', 'PUT'])
Expand Down Expand Up @@ -1420,10 +1419,10 @@ def start_restful_api(empireMenu, suppress=False, username=None, password=None,
abort(400)

# Check if user is an admin
if not u.is_admin(g.user['id']):
if not main.users.is_admin(g.user['id']):
abort(403)

status = u.add_new_user(request.json['username'], request.json['password'])
status = main.users.add_new_user(request.json['username'], request.json['password'])
return jsonify({'success': status})

@app.route('/api/users/<int:uid>/disable', methods=['PUT'])
Expand All @@ -1434,10 +1433,10 @@ def start_restful_api(empireMenu, suppress=False, username=None, password=None,

# User performing the action should be an admin.
# User being updated should not be an admin.
if not u.is_admin(g.user['id']) or u.is_admin(uid):
if not main.users.is_admin(g.user['id']) or main.users.is_admin(uid):
abort(403)

status = u.disable_user(uid, request.json['disable'])
status = main.users.disable_user(uid, request.json['disable'])
return jsonify({'success': status})

@app.route('/api/users/<int:uid>/updatepassword', methods=['PUT'])
Expand All @@ -1446,10 +1445,10 @@ def start_restful_api(empireMenu, suppress=False, username=None, password=None,
abort(400)

# Must be an admin or updating self.
if not (u.is_admin(g.user['id']) or uid == g.user['id']):
if not (main.users.is_admin(g.user['id']) or uid == g.user['id']):
abort(403)

status = u.update_password(uid, request.json['password'])
status = main.users.update_password(uid, request.json['password'])
return jsonify({'success': status})

@app.route('/api/plugin/<string:plugin_name>', methods=['GET'])
Expand Down Expand Up @@ -1563,8 +1562,37 @@ def start_restful_api(empireMenu, suppress=False, username=None, password=None,
app.run(host='0.0.0.0', port=int(port), ssl_context=context, threaded=True)


if __name__ == '__main__':
def start_sockets(empire_menu: MainMenu, port: int = 5000):
app = Flask(__name__)
socketio = SocketIO(app, cors_allowed_origins="*")
empire_menu.socketio = socketio

@socketio.on('connect')
def connect():
token = request.args.get('token', '')
if len(token) > 0:
user = empire_menu.users.get_user_from_token(token)
if user:
print(f"{user['username']} connected to socketio")
return

raise ConnectionRefusedError('unauthorized!')

@socketio.on('disconnect')
def test_disconnect():
print('Client disconnected from socketio')

print('')
print(" * Starting Empire SocketIO on port: {}".format(port))

cert_path = os.path.abspath("./data/")
proto = ssl.PROTOCOL_TLS
context = ssl.SSLContext(proto)
context.load_cert_chain("{}/empire-chain.pem".format(cert_path), "{}/empire-priv.key".format(cert_path))
socketio.run(app, host='0.0.0.0', port=port, ssl_context=context)


if __name__ == '__main__':
parser = argparse.ArgumentParser()

generalGroup = parser.add_argument_group('General Options')
Expand All @@ -1582,7 +1610,8 @@ if __name__ == '__main__':
launchGroup = restGroup.add_mutually_exclusive_group()
launchGroup.add_argument('--rest', action='store_true', help='Run Empire and the 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('--restport', type=int, nargs=1, help='Port to run the Empire RESTful API on. Defaults to 1337')
restGroup.add_argument('--socketport', type=int, nargs=1, help='Port to run socketio on. Defaults to 5000')
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')

Expand All @@ -1594,6 +1623,11 @@ if __name__ == '__main__':
else:
args.restport = args.restport[0]

if not args.socketport:
args.socketport = '5000'
else:
args.socketport = args.socketport[0]

if args.version:
print(empire.VERSION)

Expand All @@ -1619,6 +1653,18 @@ if __name__ == '__main__':
thread.daemon = True
thread.start()
sleep(2)

def thread_websocket(empire_menu):
try:
start_sockets(empire_menu=empire_menu, port=int(args.socketport))
except SystemExit as e:
pass

thread2 = helpers.KThread(target=thread_websocket, args=(main,))
thread2.daemon = True
thread2.start()
sleep(2)

main.cmdloop()

elif args.headless:
Expand Down
30 changes: 29 additions & 1 deletion lib/common/agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@
from builtins import object
# -*- encoding: utf-8 -*-
from builtins import str
from datetime import datetime, timezone

from pydispatch import dispatcher
from zlib_wrapper import decompress

Expand Down Expand Up @@ -179,6 +181,17 @@ def add_agent(self, sessionID, externalIP, delay, jitter, profile, killDate, wor
finally:
self.lock.release()

def get_agent_for_socket(self, session_id):
agent = self.get_agent_db(session_id)

lastseen_time = datetime.fromisoformat(agent['lastseen_time']).astimezone(timezone.utc)
stale = helpers.is_stale(lastseen_time, agent['delay'], agent['jitter'])
agent['stale'] = stale

if isinstance(agent['session_key'], bytes):
agent['session_key'] = agent['session_key'].decode('latin-1').encode('utf-8')

return agent

def remove_agent_db(self, sessionID):
"""
Expand Down Expand Up @@ -1148,6 +1161,7 @@ def add_agent_task_db(self, sessionID, taskName, task='', moduleName=None, uid=N
pk = (pk + 1) % 65536
cur.execute("INSERT INTO taskings (id, agent, data, user_id, timestamp, module_name) VALUES(?,?,?,?,?,?)",
[pk, sessionID, task[:100], uid, timestamp, moduleName])
# self.mainMenu.socketio.emit('agent/task', {'sessionID': sessionID, 'taskID': pk, 'data': task[:100]})

# Create result for data when it arrives
cur.execute("INSERT INTO results (id, agent, user_id) VALUES (?,?,?)", (pk, sessionID, uid))
Expand Down Expand Up @@ -1294,7 +1308,6 @@ def clear_agent_tasks_db(self, sessionID):
def handle_agent_staging(self, sessionID, language, meta, additional, encData, stagingKey, listenerOptions, clientIP='0.0.0.0'):
"""
Handles agent staging/key-negotiation.

TODO: does this function need self.lock?
"""

Expand Down Expand Up @@ -1362,6 +1375,10 @@ def handle_agent_staging(self, sessionID, language, meta, additional, encData, s
# add the agent to the database now that it's "checked in"
self.mainMenu.agents.add_agent(sessionID, clientIP, delay, jitter, profile, killDate, workingHours, lostLimit, nonce=nonce, listener=listenerName)

if self.mainMenu.socketio:
self.mainMenu.socketio.emit('agents/new', self.get_agent_for_socket(sessionID),
broadcast=True)

clientSessionKey = self.mainMenu.agents.get_agent_session_key_db(sessionID)
data = "%s%s" % (nonce, clientSessionKey)

Expand Down Expand Up @@ -1428,6 +1445,10 @@ def handle_agent_staging(self, sessionID, language, meta, additional, encData, s
# add the agent to the database now that it's "checked in"
self.mainMenu.agents.add_agent(sessionID, clientIP, delay, jitter, profile, killDate, workingHours, lostLimit, sessionKey=serverPub.key, nonce=nonce, listener=listenerName)

if self.mainMenu.socketio:
self.mainMenu.socketio.emit('agents/new', self.get_agent_for_socket(sessionID),
broadcast=True)

# step 4 of negotiation -> server returns HMAC(AESn(nonce+PUBs))
data = "%s%s" % (nonce, serverPub.publicKey)
encryptedMsg = encryption.aes_encrypt_then_hmac(stagingKey, data)
Expand Down Expand Up @@ -1536,6 +1557,12 @@ def handle_agent_staging(self, sessionID, language, meta, additional, encData, s

# save the initial sysinfo information in the agent log
agent = self.mainMenu.agents.get_agent_db(sessionID)

lastseen_time = datetime.fromisoformat(agent['lastseen_time']).astimezone(timezone.utc)
stale = helpers.is_stale(lastseen_time, agent['delay'], agent['jitter'])
agent['stale'] = stale
self.mainMenu.socketio.emit('agents/stage2', agent, broadcast=True)

output = messages.display_agent(agent, returnAsString=True)
output += "\n[+] Agent %s now active:\n" % (sessionID)
self.mainMenu.agents.save_agent_log(sessionID, output)
Expand Down Expand Up @@ -1782,6 +1809,7 @@ def process_agent_packet(self, sessionID, responseName, taskID, data):
if taskID != 0 and responseName not in ["TASK_DOWNLOAD", "TASK_CMD_JOB_SAVE", "TASK_CMD_WAIT_SAVE"] and data != None:
# Update result with data
cur.execute("UPDATE results SET data=? WHERE id=? AND agent=?", (data, taskID, sessionID))
# self.mainMenu.socketio.emit('agents/task', {'sessionID': sessionID, 'taskID': taskID, 'data': data})

try:
keyLogTaskID = cur.execute("SELECT id FROM taskings WHERE agent=? AND id=? AND data LIKE \"function Get-Keystrokes%\"", [sessionID, taskID]).fetchone()[0]
Expand Down
7 changes: 7 additions & 0 deletions lib/common/empire.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
from builtins import input
from builtins import range
from builtins import str
from typing import Optional

from flask_socketio import SocketIO

VERSION = "3.4.0 BC Security Fork"

Expand Down Expand Up @@ -41,6 +44,7 @@
from . import modules
from . import stagers
from . import credentials
from . import users
from . import plugins
from .events import log_event
from zlib_wrapper import compress
Expand Down Expand Up @@ -117,6 +121,8 @@ def __init__(self, args=None):
self.stagers = stagers.Stagers(self, args=args)
self.modules = modules.Modules(self, args=args)
self.listeners = listeners.Listeners(self, args=args)
self.users = users.Users(self)
self.socketio: Optional[SocketIO] = None
self.resourceQueue = []
#A hashtable of autruns based on agent language
self.autoRuns = {}
Expand Down Expand Up @@ -328,6 +334,7 @@ def shutdown(self):

# enumerate all active servers/listeners and shut them down
self.listeners.shutdown_listener('all')

message = "[*] Shutting down plugins..."
signal = json.dumps({
'print': True,
Expand Down
21 changes: 18 additions & 3 deletions lib/common/listeners.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,11 @@ class Listeners(object):
Listener handling class.
"""

def __init__(self, MainMenu, args):
def __init__(self, main_menu, args):

self.mainMenu = MainMenu
self.mainMenu = main_menu
self.args = args
self.conn = MainMenu.conn
self.conn = main_menu.conn

# loaded listener format:
# {"listenerModuleName": moduleInstance, ...}
Expand Down Expand Up @@ -240,6 +240,10 @@ def start_listener(self, moduleName, listenerObject):
'listener_options': listenerOptions
})
dispatcher.send(signal, sender="listeners/{}/{}".format(moduleName, name))
self.activeListeners[name]['name'] = name

if self.mainMenu.socketio:
self.mainMenu.socketio.emit('listeners/new', self.get_listener_for_socket(name), broadcast=True)
else:
print(helpers.color('[!] Listener failed to start!'))

Expand All @@ -248,6 +252,17 @@ def start_listener(self, moduleName, listenerObject):
del self.activeListeners[name]
print(helpers.color("[!] Error starting listener: %s" % (e)))

def get_listener_for_socket(self, name):
cur = self.conn.cursor()
cur.execute('''
SELECT id, name, module, listener_type, listener_category, options, created_at
FROM listeners WHERE name = ?
''', [name])
listener = cur.fetchone()
[ID, name, module, listener_type, listener_category, options, created_at] = listener
return {'ID': ID, 'name': name, 'module': module, 'listener_type': listener_type,
'listener_category': listener_category, 'options': pickle.loads(options),
'created_at': created_at}

def start_existing_listeners(self):
"""
Expand Down
2 changes: 1 addition & 1 deletion lib/common/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from pydispatch import dispatcher


class Users():
class Users(object):
def __init__(self, mainMenu):
self.mainMenu = mainMenu

Expand Down
Loading