diff --git a/.gitignore b/.gitignore index cfc5793a6..dae071c20 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,5 @@ empire/client/generated-stagers empire/server/v2/api/static/* starkiller-dist.tar.gz + +empire/test/downloads diff --git a/empire/server/common/agents.py b/empire/server/common/agents.py index 2b657cd30..b16f8230c 100644 --- a/empire/server/common/agents.py +++ b/empire/server/common/agents.py @@ -65,7 +65,7 @@ from builtins import object, str from datetime import datetime, timezone -from pydispatch import dispatcher +# from pydispatch import dispatcher from sqlalchemy import and_, func, or_, update from sqlalchemy.orm import Session, undefer from zlib_wrapper import decompress @@ -317,6 +317,8 @@ def save_file( sessionID = nameid lang = self.get_language_db(sessionID) + + # todo this doesn't work for non-windows. All files are stored flat. parts = path.split("\\") # construct the appropriate save path @@ -1156,7 +1158,7 @@ def handle_agent_staging( sessionID ) signal = json.dumps({"print": True, "message": message}) - dispatcher.send(signal, sender="agents/{}".format(sessionID)) + # dispatcher.send(signal, sender="agents/{}".format(sessionID)) return "ERROR: Invalid PowerShell public key" elif language.lower() == "python": @@ -1165,7 +1167,7 @@ def handle_agent_staging( sessionID ) signal = json.dumps({"print": True, "message": message}) - dispatcher.send(signal, sender="agents/{}".format(sessionID)) + # dispatcher.send(signal, sender="agents/{}".format(sessionID)) return "Error: Invalid Python key post format from %s" % (sessionID) else: try: @@ -1175,7 +1177,7 @@ def handle_agent_staging( sessionID ) signal = json.dumps({"print": True, "message": message}) - dispatcher.send(signal, sender="agents/{}".format(sessionID)) + # dispatcher.send(signal, sender="agents/{}".format(sessionID)) return "Error: Invalid Python key post format from {}".format( sessionID ) @@ -1192,7 +1194,7 @@ def handle_agent_staging( sessionID, clientIP ) signal = json.dumps({"print": True, "message": message}) - dispatcher.send(signal, sender="agents/{}".format(sessionID)) + # dispatcher.send(signal, sender="agents/{}".format(sessionID)) delay = listenerOptions["DefaultDelay"]["Value"] jitter = listenerOptions["DefaultJitter"]["Value"] @@ -1228,7 +1230,7 @@ def handle_agent_staging( sessionID, clientIP, language ) signal = json.dumps({"print": True, "message": message}) - dispatcher.send(signal, sender="agents/{}".format(sessionID)) + # dispatcher.send(signal, sender="agents/{}".format(sessionID)) return "ERROR: invalid language: {}".format(language) elif meta == "STAGE2": @@ -1249,7 +1251,7 @@ def handle_agent_staging( ) ) signal = json.dumps({"print": True, "message": message}) - dispatcher.send(signal, sender="agents/{}".format(sessionID)) + # dispatcher.send(signal, sender="agents/{}".format(sessionID)) # remove the agent from the cache/database self.mainMenu.agents.remove_agent_db(sessionID) return ( @@ -1263,7 +1265,7 @@ def handle_agent_staging( ): message = "[!] Invalid nonce returned from {}".format(sessionID) signal = json.dumps({"print": True, "message": message}) - dispatcher.send(signal, sender="agents/{}".format(sessionID)) + # dispatcher.send(signal, sender="agents/{}".format(sessionID)) # remove the agent from the cache/database self.mainMenu.agents.remove_agent_db(sessionID) return "ERROR: Invalid nonce returned from %s" % (sessionID) @@ -1272,7 +1274,7 @@ def handle_agent_staging( sessionID, message ) signal = json.dumps({"print": False, "message": message}) - dispatcher.send(signal, sender="agents/{}".format(sessionID)) + # dispatcher.send(signal, sender="agents/{}".format(sessionID)) listener = str(parts[1], "utf-8") domainname = str(parts[2], "utf-8") @@ -1299,7 +1301,7 @@ def handle_agent_staging( ) ) signal = json.dumps({"print": True, "message": message}) - dispatcher.send(signal, sender="agents/{}".format(sessionID)) + # dispatcher.send(signal, sender="agents/{}".format(sessionID)) # remove the agent from the cache/database self.mainMenu.agents.remove_agent_db(sessionID) return ( @@ -1348,7 +1350,7 @@ def handle_agent_staging( sessionID, clientIP ) signal = json.dumps({"print": True, "message": message}) - dispatcher.send(signal, sender="agents/{}".format(sessionID)) + # dispatcher.send(signal, sender="agents/{}".format(sessionID)) agent = self.mainMenu.agents.get_agent_for_socket(sessionID) hooks.run_hooks( @@ -1390,7 +1392,7 @@ def handle_agent_staging( sessionID, clientIP, meta ) signal = json.dumps({"print": True, "message": message}) - dispatcher.send(signal, sender="agents/{}".format(sessionID)) + # dispatcher.send(signal, sender="agents/{}".format(sessionID)) def handle_agent_data( self, @@ -1411,7 +1413,7 @@ def handle_agent_data( len(routingPacket) ) signal = json.dumps({"print": False, "message": message}) - dispatcher.send(signal, sender="empire") + # dispatcher.send(signal, sender="empire") return None if isinstance(routingPacket, str): @@ -1431,7 +1433,7 @@ def handle_agent_data( ) ) signal = json.dumps({"print": False, "message": message}) - dispatcher.send(signal, sender="agents/{}".format(sessionID)) + # dispatcher.send(signal, sender="agents/{}".format(sessionID)) dataToReturn.append( ( language, @@ -1453,7 +1455,7 @@ def handle_agent_data( sessionID ) signal = json.dumps({"print": False, "message": message}) - dispatcher.send(signal, sender="agents/{}".format(sessionID)) + # dispatcher.send(signal, sender="agents/{}".format(sessionID)) dataToReturn.append( ("", "ERROR: sessionID %s not in cache!" % (sessionID)) ) @@ -1463,7 +1465,7 @@ def handle_agent_data( sessionID ) signal = json.dumps({"print": False, "message": message}) - dispatcher.send(signal, sender="agents/{}".format(sessionID)) + # dispatcher.send(signal, sender="agents/{}".format(sessionID)) dataToReturn.append( ( language, @@ -1478,7 +1480,7 @@ def handle_agent_data( ) ) signal = json.dumps({"print": False, "message": message}) - dispatcher.send(signal, sender="agents/{}".format(sessionID)) + # dispatcher.send(signal, sender="agents/{}".format(sessionID)) dataToReturn.append( ( language, @@ -1912,7 +1914,9 @@ def process_agent_packet( # attach file to tasking download = models.Download( - location=final_save_path, size=os.path.getsize(final_save_path) + location=final_save_path, + filename=final_save_path.split("/")[-1], + size=os.path.getsize(final_save_path), ) db.add(download) db.flush() @@ -1930,7 +1934,7 @@ def process_agent_packet( ) ) signal = json.dumps({"print": True, "message": message}) - dispatcher.send(signal, sender="agents/{}".format(self.sessionID)) + # dispatcher.send(signal, sender="agents/{}".format(self.sessionID)) return with open(savePath, "a+") as f: @@ -2061,7 +2065,7 @@ def process_agent_packet( self.save_agent_log(session_id, data) message = "[+] Updated comms for {} to {}".format(session_id, listener_name) signal = json.dumps({"print": False, "message": message}) - dispatcher.send(signal, sender="agents/{}".format(session_id)) + # dispatcher.send(signal, sender="agents/{}".format(session_id)) elif response_name == "TASK_UPDATE_LISTENERNAME": # The agent listener name variable has been updated agent side diff --git a/empire/server/common/empire.py b/empire/server/common/empire.py index 176085a47..7e078c762 100755 --- a/empire/server/common/empire.py +++ b/empire/server/common/empire.py @@ -20,7 +20,8 @@ from prompt_toolkit import HTML, PromptSession from prompt_toolkit.patch_stdout import patch_stdout -from pydispatch import dispatcher + +# from pydispatch import dispatcher from sqlalchemy import and_, func, or_ from empire.server.common import hooks_internal @@ -447,7 +448,7 @@ def preobfuscate_modules(self, obfuscation_command, reobfuscate=False): "obfuscated_file": os.path.basename(file), } ) - dispatcher.send(signal, sender="empire") + # dispatcher.send(signal, sender="empire") else: print( helpers.color( diff --git a/empire/server/common/events.py b/empire/server/common/events.py index 582e9ef73..886e516a9 100644 --- a/empire/server/common/events.py +++ b/empire/server/common/events.py @@ -7,11 +7,12 @@ import json -from pydispatch import dispatcher - from empire.server.database import models from empire.server.database.base import SessionLocal +# from pydispatch import dispatcher + + # from empire.server.common import db # used in the disabled TODO below ################################################################################ diff --git a/empire/server/common/helpers.py b/empire/server/common/helpers.py index 1e33af4a2..a0ed4038c 100644 --- a/empire/server/common/helpers.py +++ b/empire/server/common/helpers.py @@ -40,16 +40,13 @@ """ from __future__ import division, print_function -import json - -from future import standard_library - -standard_library.install_aliases() import base64 import binascii import datetime import fnmatch import hashlib +import json +import numbers import os import random import re @@ -66,7 +63,6 @@ import iptools import netifaces -from past.utils import old_div ############################################################### # @@ -658,6 +654,17 @@ def get_file_datetime(): return datetime.now().strftime("%Y-%m-%d_%H-%M-%S") +def old_div(a, b): + """ + Equivalent to ``a / b`` on Python 2 without ``from __future__ import + division``. + """ + if isinstance(a, numbers.Integral) and isinstance(b, numbers.Integral): + return a // b + else: + return a / b + + def get_file_size(file): """ Returns a string with the file size and highest rating. diff --git a/empire/server/common/http.py b/empire/server/common/http.py index 363317f13..2b01ec94e 100644 --- a/empire/server/common/http.py +++ b/empire/server/common/http.py @@ -24,11 +24,11 @@ import threading from http.server import BaseHTTPRequestHandler -from pydispatch import dispatcher - # Empire imports from . import helpers +# from pydispatch import dispatcher + def default_page(path_to_html_file="empty"): if path_to_html_file == "empty": diff --git a/empire/server/common/packets.py b/empire/server/common/packets.py index 06d6b761e..11b3954a6 100644 --- a/empire/server/common/packets.py +++ b/empire/server/common/packets.py @@ -66,11 +66,12 @@ import struct import sys -from pydispatch import dispatcher - # Empire imports from . import encryption +# from pydispatch import dispatcher + + # 0 -> error # 1-99 -> standard functionality # 100-199 -> dynamic functionality diff --git a/empire/server/common/pylnk.py b/empire/server/common/pylnk.py index e533f172a..a93b319d9 100644 --- a/empire/server/common/pylnk.py +++ b/empire/server/common/pylnk.py @@ -23,9 +23,6 @@ from __future__ import print_function -from future import standard_library - -standard_library.install_aliases() import re import time from builtins import chr, object, range, str diff --git a/empire/server/common/stagers.py b/empire/server/common/stagers.py index 3132b5229..824023a56 100755 --- a/empire/server/common/stagers.py +++ b/empire/server/common/stagers.py @@ -26,12 +26,12 @@ import donut import macholib.MachO -from past.utils import old_div from empire.server.database import models from empire.server.database.base import SessionLocal from . import helpers +from .helpers import old_div class Stagers(object): diff --git a/empire/server/database/models.py b/empire/server/database/models.py index c56177e8c..7e65b5e36 100644 --- a/empire/server/database/models.py +++ b/empire/server/database/models.py @@ -52,6 +52,13 @@ Column("download_id", Integer, ForeignKey("downloads.id")), ) +# this doesn't actually join to anything atm, but is used for the filtering in api/v2/downloads +upload_download_assc = Table( + "upload_download_assc", + Base.metadata, + Column("download_id", Integer, ForeignKey("downloads.id")), +) + class User(Base): __tablename__ = "users" diff --git a/empire/server/listeners/dbx.py b/empire/server/listeners/dbx.py index ebfe7c4ae..d31aae562 100755 --- a/empire/server/listeners/dbx.py +++ b/empire/server/listeners/dbx.py @@ -9,7 +9,6 @@ from typing import List, Optional, Tuple import dropbox -from pydispatch import dispatcher from empire.server.common import encryption, helpers, templating from empire.server.database import models @@ -17,6 +16,8 @@ from empire.server.utils import data_util from empire.server.utils.module_util import handle_validate_message +# from pydispatch import dispatcher + class Listener(object): def __init__(self, mainMenu, params=[]): @@ -967,9 +968,9 @@ def download_file(dbx, path): listenerName = self.options["Name"]["Value"] message = "[!] Error downloading data from '{}' : {}".format(path, err) signal = json.dumps({"print": True, "message": message}) - dispatcher.send( - signal, sender="listeners/dropbox/{}".format(listenerName) - ) + # dispatcher.send( + # signal, sender="listeners/dropbox/{}".format(listenerName) + # ) return None return res.content @@ -982,9 +983,9 @@ def upload_file(dbx, path, data): listenerName = self.options["Name"]["Value"] message = "[!] Error uploading data to '{}'".format(path) signal = json.dumps({"print": True, "message": message}) - dispatcher.send( - signal, sender="listeners/dropbox/{}".format(listenerName) - ) + # dispatcher.send( + # signal, sender="listeners/dropbox/{}".format(listenerName) + # ) def delete_file(dbx, path): # helper to delete a file at the given path @@ -994,9 +995,9 @@ def delete_file(dbx, path): listenerName = self.options["Name"]["Value"] message = "[!] Error deleting data at '{}'".format(path) signal = json.dumps({"print": True, "message": message}) - dispatcher.send( - signal, sender="listeners/dropbox/{}".format(listenerName) - ) + # dispatcher.send( + # signal, sender="listeners/dropbox/{}".format(listenerName) + # ) # make a copy of the currently set listener options for later stager/agent generation listenerOptions = copy.deepcopy(listenerOptions) @@ -1039,21 +1040,21 @@ def delete_file(dbx, path): listenerName = self.options["Name"]["Value"] message = "[*] Dropbox folder '{}' already exists".format(stagingFolder) signal = json.dumps({"print": False, "message": message}) - dispatcher.send(signal, sender="listeners/dropbox/{}".format(listenerName)) + # dispatcher.send(signal, sender="listeners/dropbox/{}".format(listenerName)) try: dbx.files_create_folder(taskingsFolder) except dropbox.exceptions.ApiError: listenerName = self.options["Name"]["Value"] message = "[*] Dropbox folder '{}' already exists".format(taskingsFolder) signal = json.dumps({"print": False, "message": message}) - dispatcher.send(signal, sender="listeners/dropbox/{}".format(listenerName)) + # dispatcher.send(signal, sender="listeners/dropbox/{}".format(listenerName)) try: dbx.files_create_folder(resultsFolder) except dropbox.exceptions.ApiError: listenerName = self.options["Name"]["Value"] message = "[*] Dropbox folder '{}' already exists".format(resultsFolder) signal = json.dumps({"print": False, "message": message}) - dispatcher.send(signal, sender="listeners/dropbox/{}".format(listenerName)) + # dispatcher.send(signal, sender="listeners/dropbox/{}".format(listenerName)) # upload the stager.ps1 code stagerCodeps = self.generate_stager( @@ -1099,10 +1100,10 @@ def delete_file(dbx, path): ) ) signal = json.dumps({"print": True, "message": message}) - dispatcher.send( - signal, - sender="listeners/dropbox/{}".format(listenerName), - ) + # dispatcher.send( + # signal, + # sender="listeners/dropbox/{}".format(listenerName), + # ) continue stageData = res.content @@ -1122,12 +1123,12 @@ def delete_file(dbx, path): signal = json.dumps( {"print": True, "message": message} ) - dispatcher.send( - signal, - sender="listeners/dropbox/{}".format( - listenerName - ), - ) + # dispatcher.send( + # signal, + # sender="listeners/dropbox/{}".format( + # listenerName + # ), + # ) try: stageName = "%s/%s_2.txt" % ( stagingFolder, @@ -1140,12 +1141,12 @@ def delete_file(dbx, path): signal = json.dumps( {"print": True, "message": message} ) - dispatcher.send( - signal, - sender="listeners/dropbox/{}".format( - listenerName - ), - ) + # dispatcher.send( + # signal, + # sender="listeners/dropbox/{}".format( + # listenerName + # ), + # ) dbx.files_upload(results, stageName) except dropbox.exceptions.ApiError: listenerName = self.options["Name"]["Value"] @@ -1155,12 +1156,12 @@ def delete_file(dbx, path): signal = json.dumps( {"print": True, "message": message} ) - dispatcher.send( - signal, - sender="listeners/dropbox/{}".format( - listenerName - ), - ) + # dispatcher.send( + # signal, + # sender="listeners/dropbox/{}".format( + # listenerName + # ), + # ) if stage == "3": try: @@ -1173,10 +1174,10 @@ def delete_file(dbx, path): ) ) signal = json.dumps({"print": True, "message": message}) - dispatcher.send( - signal, - sender="listeners/dropbox/{}".format(listenerName), - ) + # dispatcher.send( + # signal, + # sender="listeners/dropbox/{}".format(listenerName), + # ) continue stageData = res.content @@ -1197,12 +1198,12 @@ def delete_file(dbx, path): signal = json.dumps( {"print": True, "message": message} ) - dispatcher.send( - signal, - sender="listeners/dropbox/{}".format( - listenerName - ), - ) + # dispatcher.send( + # signal, + # sender="listeners/dropbox/{}".format( + # listenerName + # ), + # ) try: dbx.files_delete(fileName) @@ -1216,12 +1217,12 @@ def delete_file(dbx, path): signal = json.dumps( {"print": True, "message": message} ) - dispatcher.send( - signal, - sender="listeners/dropbox/{}".format( - listenerName - ), - ) + # dispatcher.send( + # signal, + # sender="listeners/dropbox/{}".format( + # listenerName + # ), + # ) try: fileName2 = fileName.replace( diff --git a/empire/server/listeners/http.py b/empire/server/listeners/http.py index beb37a16f..8762be0d2 100755 --- a/empire/server/listeners/http.py +++ b/empire/server/listeners/http.py @@ -14,7 +14,8 @@ from typing import List, Optional, Tuple from flask import Flask, make_response, request, send_from_directory -from pydispatch import dispatcher + +# from pydispatch import dispatcher from werkzeug.serving import WSGIRequestHandler from empire.server.common import encryption, helpers, packets, templating @@ -1237,7 +1238,7 @@ def check_ip(): request.remote_addr ) signal = json.dumps({"print": True, "message": message}) - dispatcher.send(signal, sender="listeners/http/{}".format(listenerName)) + # dispatcher.send(signal, sender="listeners/http/{}".format(listenerName)) return make_response(self.default_response(), 404) @app.after_request @@ -1297,7 +1298,7 @@ def handle_get(request_uri): request.host, request_uri, clientIP ) signal = json.dumps({"print": False, "message": message}) - dispatcher.send(signal, sender="listeners/http/{}".format(listenerName)) + # dispatcher.send(signal, sender="listeners/http/{}".format(listenerName)) routingPacket = None cookie = request.headers.get("Cookie") @@ -1312,9 +1313,9 @@ def handle_get(request_uri): clientIP, cookie ) signal = json.dumps({"print": False, "message": message}) - dispatcher.send( - signal, sender="listeners/http/{}".format(listenerName) - ) + # dispatcher.send( + # signal, sender="listeners/http/{}".format(listenerName) + # ) cookieParts = cookie.split(";") for part in cookieParts: if part.startswith(self.session_cookie): @@ -1346,10 +1347,10 @@ def handle_get(request_uri): ) ) signal = json.dumps({"print": True, "message": message}) - dispatcher.send( - signal, - sender="listeners/http/{}".format(listenerName), - ) + # dispatcher.send( + # signal, + # sender="listeners/http/{}".format(listenerName), + # ) stage = self.generate_stager( language=language, listenerOptions=listenerOptions, @@ -1364,10 +1365,10 @@ def handle_get(request_uri): request_uri, clientIP, results ) signal = json.dumps({"print": True, "message": message}) - dispatcher.send( - signal, - sender="listeners/http/{}".format(listenerName), - ) + # dispatcher.send( + # signal, + # sender="listeners/http/{}".format(listenerName), + # ) if b"not in cache" in results: # signal the client to restage @@ -1390,10 +1391,10 @@ def handle_get(request_uri): signal = json.dumps( {"print": False, "message": message} ) - dispatcher.send( - signal, - sender="listeners/http/{}".format(listenerName), - ) + # dispatcher.send( + # signal, + # sender="listeners/http/{}".format(listenerName), + # ) return make_response(results, 200) else: # dispatcher.send("[!] Results are None...", sender='listeners/http') @@ -1407,7 +1408,7 @@ def handle_get(request_uri): request_uri, clientIP ) signal = json.dumps({"print": True, "message": message}) - dispatcher.send(signal, sender="listeners/http/{}".format(listenerName)) + # dispatcher.send(signal, sender="listeners/http/{}".format(listenerName)) return make_response(self.default_response(), 404) @app.route("/", methods=["POST"]) @@ -1425,7 +1426,7 @@ def handle_post(request_uri): clientIP, len(requestData) ) signal = json.dumps({"print": False, "message": message}) - dispatcher.send(signal, sender="listeners/http/{}".format(listenerName)) + # dispatcher.send(signal, sender="listeners/http/{}".format(listenerName)) # the routing packet should be at the front of the binary request.data # NOTE: this can also go into a cookie/etc. @@ -1452,9 +1453,9 @@ def handle_post(request_uri): sessionID, clientIP ) signal = json.dumps({"print": True, "message": message}) - dispatcher.send( - signal, sender="listeners/http/{}".format(listenerName) - ) + # dispatcher.send( + # signal, sender="listeners/http/{}".format(listenerName) + # ) hopListenerName = request.headers.get("Hop-Name") @@ -1506,9 +1507,9 @@ def handle_post(request_uri): ) ) signal = json.dumps({"print": True, "message": message}) - dispatcher.send( - signal, sender="listeners/http/{}".format(listenerName) - ) + # dispatcher.send( + # signal, sender="listeners/http/{}".format(listenerName) + # ) return make_response(self.default_response(), 404) elif results.startswith(b"VALID"): listenerName = self.options["Name"]["Value"] @@ -1516,9 +1517,9 @@ def handle_post(request_uri): clientIP ) signal = json.dumps({"print": False, "message": message}) - dispatcher.send( - signal, sender="listeners/http/{}".format(listenerName) - ) + # dispatcher.send( + # signal, sender="listeners/http/{}".format(listenerName) + # ) return make_response(self.default_response(), 200) else: return make_response(results, 200) @@ -1573,7 +1574,7 @@ def handle_post(request_uri): listenerName = self.options["Name"]["Value"] message = "[!] Listener startup on port {} failed: {}".format(port, e) signal = json.dumps({"print": True, "message": message}) - dispatcher.send(signal, sender="listeners/http/{}".format(listenerName)) + # dispatcher.send(signal, sender="listeners/http/{}".format(listenerName)) def start(self, name=""): """ diff --git a/empire/server/listeners/http_com.py b/empire/server/listeners/http_com.py index f751adb39..6b554005b 100755 --- a/empire/server/listeners/http_com.py +++ b/empire/server/listeners/http_com.py @@ -13,7 +13,8 @@ from typing import List, Optional, Tuple from flask import Flask, make_response, request, send_from_directory -from pydispatch import dispatcher + +# from pydispatch import dispatcher from werkzeug.serving import WSGIRequestHandler from empire.server.common import encryption, helpers, packets @@ -779,9 +780,9 @@ def check_ip(): request.remote_addr ) signal = json.dumps({"print": True, "message": message}) - dispatcher.send( - signal, sender="listeners/http_com/{}".format(listenerName) - ) + # dispatcher.send( + # signal, sender="listeners/http_com/{}".format(listenerName) + # ) return make_response(self.default_response(), 404) @app.after_request @@ -841,7 +842,7 @@ def handle_get(request_uri): request.host, request_uri, clientIP ) signal = json.dumps({"print": False, "message": message}) - dispatcher.send(signal, sender="listeners/http_com/{}".format(listenerName)) + # dispatcher.send(signal, sender="listeners/http_com/{}".format(listenerName)) routingPacket = None reqHeader = request.headers.get(listenerOptions["RequestHeader"]["Value"]) @@ -880,10 +881,10 @@ def handle_get(request_uri): ) ) signal = json.dumps({"print": True, "message": message}) - dispatcher.send( - signal, - sender="listeners/http_com/{}".format(listenerName), - ) + # dispatcher.send( + # signal, + # sender="listeners/http_com/{}".format(listenerName), + # ) stage = self.generate_stager( language=language, listenerOptions=listenerOptions, @@ -898,10 +899,10 @@ def handle_get(request_uri): request_uri, clientIP, results ) signal = json.dumps({"print": True, "message": message}) - dispatcher.send( - signal, - sender="listeners/http_com/{}".format(listenerName), - ) + # dispatcher.send( + # signal, + # sender="listeners/http_com/{}".format(listenerName), + # ) if "not in cache" in results: # signal the client to restage @@ -924,10 +925,10 @@ def handle_get(request_uri): signal = json.dumps( {"print": False, "message": message} ) - dispatcher.send( - signal, - sender="listeners/http_com/{}".format(listenerName), - ) + # dispatcher.send( + # signal, + # sender="listeners/http_com/{}".format(listenerName), + # ) return make_response(base64.b64encode(results), 200) else: # dispatcher.send("[!] Results are None...", sender='listeners/http_com') @@ -941,9 +942,9 @@ def handle_get(request_uri): request_uri, clientIP ) signal = json.dumps({"print": True, "message": message}) - dispatcher.send( - signal, sender="listeners/http_com/{}".format(listenerName) - ) + # dispatcher.send( + # signal, sender="listeners/http_com/{}".format(listenerName) + # ) return make_response(self.default_response(), 404) @app.route("/", methods=["POST"]) @@ -982,10 +983,10 @@ def handle_post(request_uri): sessionID, clientIP ) signal = json.dumps({"print": True, "message": message}) - dispatcher.send( - signal, - sender="listeners/http_com/{}".format(listenerName), - ) + # dispatcher.send( + # signal, + # sender="listeners/http_com/{}".format(listenerName), + # ) # step 6 of negotiation -> server sends patched agent.ps1/agent.py agentCode = self.generate_agent( @@ -1011,19 +1012,19 @@ def handle_post(request_uri): ) ) signal = json.dumps({"print": True, "message": message}) - dispatcher.send( - signal, - sender="listeners/http_com/{}".format(listenerName), - ) + # dispatcher.send( + # signal, + # sender="listeners/http_com/{}".format(listenerName), + # ) return make_response(self.default_response(), 200) elif results == b"VALID": listenerName = self.options["Name"]["Value"] message = "[*] Valid results return by {}".format(clientIP) signal = json.dumps({"print": False, "message": message}) - dispatcher.send( - signal, - sender="listeners/http_com/{}".format(listenerName), - ) + # dispatcher.send( + # signal, + # sender="listeners/http_com/{}".format(listenerName), + # ) return make_response(self.default_response(), 200) else: return make_response(base64.b64encode(results), 200) @@ -1073,7 +1074,7 @@ def handle_post(request_uri): message = "[!] Listener startup on port {} failed: {}".format(port, e) message += "[!] Ensure the folder specified in CertPath exists and contains your pem and private key file." signal = json.dumps({"print": True, "message": message}) - dispatcher.send(signal, sender="listeners/http_com/{}".format(listenerName)) + # dispatcher.send(signal, sender="listeners/http_com/{}".format(listenerName)) def start(self, name=""): """ diff --git a/empire/server/listeners/http_malleable.py b/empire/server/listeners/http_malleable.py index d309203fc..ba09c5ff9 100644 --- a/empire/server/listeners/http_malleable.py +++ b/empire/server/listeners/http_malleable.py @@ -15,7 +15,6 @@ from typing import List, Optional, Tuple from flask import Flask, Response, make_response, request -from pydispatch import dispatcher from empire.server.common import encryption, helpers, malleable, packets, templating from empire.server.database import models @@ -23,6 +22,8 @@ from empire.server.utils import data_util from empire.server.utils.module_util import handle_validate_message +# from pydispatch import dispatcher + class Listener(object): def __init__(self, mainMenu, params=[]): @@ -1623,9 +1624,9 @@ def handle_request(request_uri="", tempListenerOptions=None): len(request.data), ) signal = json.dumps({"print": False, "message": message}) - dispatcher.send( - signal, sender="listeners/http_malleable/{}".format(listenerName) - ) + # dispatcher.send( + # signal, sender="listeners/http_malleable/{}".format(listenerName) + # ) try: # build malleable request from flask request @@ -1696,12 +1697,12 @@ def handle_request(request_uri="", tempListenerOptions=None): signal = json.dumps( {"print": True, "message": message} ) - dispatcher.send( - signal, - sender="listeners/http_malleable/{}".format( - listenerName - ), - ) + # dispatcher.send( + # signal, + # sender="listeners/http_malleable/{}".format( + # listenerName + # ), + # ) # build stager (stage 1) stager = self.generate_stager( @@ -1742,12 +1743,12 @@ def handle_request(request_uri="", tempListenerOptions=None): signal = json.dumps( {"print": True, "message": message} ) - dispatcher.send( - signal, - sender="listeners/http_malleable/{}".format( - listenerName - ), - ) + # dispatcher.send( + # signal, + # sender="listeners/http_malleable/{}".format( + # listenerName + # ), + # ) # TODO: handle this with malleable?? tempListenerOptions = None @@ -1822,12 +1823,12 @@ def handle_request(request_uri="", tempListenerOptions=None): signal = json.dumps( {"print": True, "message": message} ) - dispatcher.send( - signal, - sender="listeners/http_malleable/{}".format( - listenerName - ), - ) + # dispatcher.send( + # signal, + # sender="listeners/http_malleable/{}".format( + # listenerName + # ), + # ) return Response(self.default_response(), 404) @@ -1839,12 +1840,12 @@ def handle_request(request_uri="", tempListenerOptions=None): signal = json.dumps( {"print": True, "message": message} ) - dispatcher.send( - signal, - sender="listeners/http_malleable/{}".format( - listenerName - ), - ) + # dispatcher.send( + # signal, + # sender="listeners/http_malleable/{}".format( + # listenerName + # ), + # ) if b"not in cache" in results: # signal the client to restage @@ -1868,12 +1869,12 @@ def handle_request(request_uri="", tempListenerOptions=None): signal = json.dumps( {"print": False, "message": message} ) - dispatcher.send( - signal, - sender="listeners/http/{}".format( - listenerName - ), - ) + # dispatcher.send( + # signal, + # sender="listeners/http/{}".format( + # listenerName + # ), + # ) malleableResponse = ( implementation.construct_server("") @@ -1897,12 +1898,12 @@ def handle_request(request_uri="", tempListenerOptions=None): signal = json.dumps( {"print": True, "message": message} ) - dispatcher.send( - signal, - sender="listeners/http_malleable/{}".format( - listenerName - ), - ) + # dispatcher.send( + # signal, + # sender="listeners/http_malleable/{}".format( + # listenerName + # ), + # ) # note: stage 1 negotiation comms are hard coded, so we can't use malleable return Response( @@ -1919,12 +1920,12 @@ def handle_request(request_uri="", tempListenerOptions=None): signal = json.dumps( {"print": False, "message": message} ) - dispatcher.send( - signal, - sender="listeners/http_malleable/{}".format( - listenerName - ), - ) + # dispatcher.send( + # signal, + # sender="listeners/http_malleable/{}".format( + # listenerName + # ), + # ) # build malleable response with results malleableResponse = ( @@ -1952,12 +1953,12 @@ def handle_request(request_uri="", tempListenerOptions=None): signal = json.dumps( {"print": False, "message": message} ) - dispatcher.send( - signal, - sender="listeners/http_malleable/{}".format( - listenerName - ), - ) + # dispatcher.send( + # signal, + # sender="listeners/http_malleable/{}".format( + # listenerName + # ), + # ) # build malleable response with no results malleableResponse = implementation.construct_server( @@ -1976,22 +1977,22 @@ def handle_request(request_uri="", tempListenerOptions=None): ) ) signal = json.dumps({"print": True, "message": message}) - dispatcher.send( - signal, - sender="listeners/http_malleable/{}".format( - listenerName - ), - ) + # dispatcher.send( + # signal, + # sender="listeners/http_malleable/{}".format( + # listenerName + # ), + # ) # log invalid request message = "[!] /{} requested by {} with no routing packet.".format( request_uri, clientIP ) signal = json.dumps({"print": True, "message": message}) - dispatcher.send( - signal, - sender="listeners/http_malleable/{}".format(listenerName), - ) + # dispatcher.send( + # signal, + # sender="listeners/http_malleable/{}".format(listenerName), + # ) else: # log invalid uri @@ -1999,10 +2000,10 @@ def handle_request(request_uri="", tempListenerOptions=None): request_uri, clientIP ) signal = json.dumps({"print": True, "message": message}) - dispatcher.send( - signal, - sender="listeners/http_malleable/{}".format(listenerName), - ) + # dispatcher.send( + # signal, + # sender="listeners/http_malleable/{}".format(listenerName), + # ) except malleable.MalleableError as e: # probably an issue with the malleable library, please report it :) @@ -2052,9 +2053,9 @@ def handle_request(request_uri="", tempListenerOptions=None): port, e.__class__.__name__, str(e) ) signal = json.dumps({"print": True, "message": message}) - dispatcher.send( - signal, sender="listeners/http_malleable/{}".format(listenerName) - ) + # dispatcher.send( + # signal, sender="listeners/http_malleable/{}".format(listenerName) + # ) def start(self, name=""): """ diff --git a/empire/server/listeners/onedrive.py b/empire/server/listeners/onedrive.py index f993b4f5e..552100ea8 100755 --- a/empire/server/listeners/onedrive.py +++ b/empire/server/listeners/onedrive.py @@ -10,7 +10,7 @@ from builtins import object, str from typing import List, Optional, Tuple -from pydispatch import dispatcher +# from pydispatch import dispatcher from requests import Request, Session from empire.server.common import encryption, helpers @@ -687,9 +687,9 @@ def setup_folders(): else: message = "[*] {} folder already exists".format(base_folder) signal = json.dumps({"print": True, "message": message}) - dispatcher.send( - signal, sender="listeners/onedrive/{}".format(listener_name) - ) + # dispatcher.send( + # signal, sender="listeners/onedrive/{}".format(listener_name) + # ) for item in [staging_folder, taskings_folder, results_folder]: item_object = s.get( @@ -712,9 +712,9 @@ def setup_folders(): else: message = "[*] {}/{} already exists".format(base_folder, item) signal = json.dumps({"print": True, "message": message}) - dispatcher.send( - signal, sender="listeners/onedrive/{}".format(listener_name) - ) + # dispatcher.send( + # signal, sender="listeners/onedrive/{}".format(listener_name) + # ) def upload_launcher(): ps_launcher = self.mainMenu.stagers.generate_launcher( @@ -775,9 +775,9 @@ def upload_stager(): print(helpers.color("[!] Something went wrong uploading stager")) message = r.content signal = json.dumps({"print": True, "message": message}) - dispatcher.send( - signal, sender="listeners/onedrive/{}".format(listener_name) - ) + # dispatcher.send( + # signal, sender="listeners/onedrive/{}".format(listener_name) + # ) listener_options = copy.deepcopy(listenerOptions) @@ -801,9 +801,9 @@ def upload_stager(): token = renew_token(client_id, client_secret, refresh_token) message = "[*] Refreshed auth token" signal = json.dumps({"print": True, "message": message}) - dispatcher.send( - signal, sender="listeners/onedrive/{}".format(listener_name) - ) + # dispatcher.send( + # signal, sender="listeners/onedrive/{}".format(listener_name) + # ) else: try: token = get_token(client_id, client_secret, auth_code) @@ -813,7 +813,7 @@ def upload_stager(): message = "[*] Got new auth token" signal = json.dumps({"print": True, "message": message}) - dispatcher.send(signal, sender="listeners/onedrive") + # dispatcher.send(signal, sender="listeners/onedrive") s.headers["Authorization"] = "Bearer " + token["access_token"] @@ -843,9 +843,9 @@ def upload_stager(): s.headers["Authorization"] = "Bearer " + token["access_token"] message = "[*] Refreshed auth token" signal = json.dumps({"print": True, "message": message}) - dispatcher.send( - signal, sender="listeners/onedrive/{}".format(listener_name) - ) + # dispatcher.send( + # signal, sender="listeners/onedrive/{}".format(listener_name) + # ) upload_stager() if token["update"]: with SessionLocal.begin() as db: @@ -874,10 +874,10 @@ def upload_stager(): base_folder, staging_folder, item["name"], item["size"] ) signal = json.dumps({"print": False, "message": message}) - dispatcher.send( - signal, - sender="listeners/onedrive/{}".format(listener_name), - ) + # dispatcher.send( + # signal, + # sender="listeners/onedrive/{}".format(listener_name), + # ) content = s.get( item["@microsoft.graph.downloadUrl"] ).content @@ -891,10 +891,10 @@ def upload_stager(): str(len(return_val)), ) signal = json.dumps({"print": False, "message": message}) - dispatcher.send( - signal, - sender="listeners/onedrive/{}".format(listener_name), - ) + # dispatcher.send( + # signal, + # sender="listeners/onedrive/{}".format(listener_name), + # ) s.put( "%s/drive/root:/%s/%s/%s_2.txt:/content" % (base_url, base_folder, staging_folder, agent_name), @@ -904,10 +904,10 @@ def upload_stager(): base_folder, staging_folder, item["name"] ) signal = json.dumps({"print": False, "message": message}) - dispatcher.send( - signal, - sender="listeners/onedrive/{}".format(listener_name), - ) + # dispatcher.send( + # signal, + # sender="listeners/onedrive/{}".format(listener_name), + # ) s.delete("%s/drive/items/%s" % (base_url, item["id"])) if ( @@ -917,10 +917,10 @@ def upload_stager(): base_folder, staging_folder, item["name"], item["size"] ) signal = json.dumps({"print": False, "message": message}) - dispatcher.send( - signal, - sender="listeners/onedrive/{}".format(listener_name), - ) + # dispatcher.send( + # signal, + # sender="listeners/onedrive/{}".format(listener_name), + # ) content = s.get( item["@microsoft.graph.downloadUrl"] ).content @@ -956,10 +956,10 @@ def upload_stager(): str(len(enc_code)), ) signal = json.dumps({"print": False, "message": message}) - dispatcher.send( - signal, - sender="listeners/onedrive/{}".format(listener_name), - ) + # dispatcher.send( + # signal, + # sender="listeners/onedrive/{}".format(listener_name), + # ) s.put( "%s/drive/root:/%s/%s/%s_4.txt:/content" % (base_url, base_folder, staging_folder, agent_name), @@ -969,10 +969,10 @@ def upload_stager(): base_folder, staging_folder, item["name"] ) signal = json.dumps({"print": False, "message": message}) - dispatcher.send( - signal, - sender="listeners/onedrive/{}".format(listener_name), - ) + # dispatcher.send( + # signal, + # sender="listeners/onedrive/{}".format(listener_name), + # ) s.delete("%s/drive/items/%s" % (base_url, item["id"])) except Exception as e: @@ -984,9 +984,9 @@ def upload_stager(): ) message = traceback.format_exc() signal = json.dumps({"print": False, "message": message}) - dispatcher.send( - signal, sender="listeners/onedrive/{}".format(listener_name) - ) + # dispatcher.send( + # signal, sender="listeners/onedrive/{}".format(listener_name) + # ) agent_ids = self.mainMenu.agents.get_agents_for_listener(listener_name) @@ -1013,10 +1013,10 @@ def upload_stager(): ) ) signal = json.dumps({"print": False, "message": message}) - dispatcher.send( - signal, - sender="listeners/onedrive/{}".format(listener_name), - ) + # dispatcher.send( + # signal, + # sender="listeners/onedrive/{}".format(listener_name), + # ) r = s.put( "%s/drive/root:/%s/%s/%s.txt:/content" @@ -1030,10 +1030,10 @@ def upload_stager(): ) ) signal = json.dumps({"print": False, "message": message}) - dispatcher.send( - signal, - sender="listeners/onedrive/{}".format(listener_name), - ) + # dispatcher.send( + # signal, + # sender="listeners/onedrive/{}".format(listener_name), + # ) search = s.get( "%s/drive/root:/%s/%s?expand=children" @@ -1075,10 +1075,10 @@ def upload_stager(): ) ) signal = json.dumps({"print": False, "message": message}) - dispatcher.send( - signal, - sender="listeners/onedrive/{}".format(listener_name), - ) + # dispatcher.send( + # signal, + # sender="listeners/onedrive/{}".format(listener_name), + # ) r = s.get(item["@microsoft.graph.downloadUrl"]) self.mainMenu.agents.handle_agent_data( staging_key, @@ -1090,19 +1090,19 @@ def upload_stager(): results_folder, item["name"] ) signal = json.dumps({"print": False, "message": message}) - dispatcher.send( - signal, - sender="listeners/onedrive/{}".format(listener_name), - ) + # dispatcher.send( + # signal, + # sender="listeners/onedrive/{}".format(listener_name), + # ) s.delete("%s/drive/items/%s" % (base_url, item["id"])) except Exception as e: message = "[!] Error handling agent results for {}, {}".format( item["name"], e ) signal = json.dumps({"print": False, "message": message}) - dispatcher.send( - signal, sender="listeners/onedrive/{}".format(listener_name) - ) + # dispatcher.send( + # signal, sender="listeners/onedrive/{}".format(listener_name) + # ) except Exception as e: print( @@ -1113,9 +1113,9 @@ def upload_stager(): ) message = traceback.format_exc() signal = json.dumps({"print": False, "message": message}) - dispatcher.send( - signal, sender="listeners/onedrive/{}".format(listener_name) - ) + # dispatcher.send( + # signal, sender="listeners/onedrive/{}".format(listener_name) + # ) s.close() diff --git a/empire/server/server.py b/empire/server/server.py index 95dfcc932..4d06d7c01 100755 --- a/empire/server/server.py +++ b/empire/server/server.py @@ -1,22 +1,17 @@ #!/usr/bin/env python3 import os -import signal -import ssl import subprocess import sys import time from time import sleep import urllib3 -from flask import Flask, jsonify, make_response, request +from flask import jsonify, make_response, request # Empire imports from empire.arguments import args from empire.server.common import empire, helpers from empire.server.common.config import empire_config -from empire.server.common.empire import MainMenu -from empire.server.database import models -from empire.server.database.base import SessionLocal from empire.server.v2.api import v2App # Disable http warnings @@ -24,21 +19,12 @@ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) -def start_restful_api(empireMenu: MainMenu, suppress=False, ip="0.0.0.0", port=1337): - app = Flask(__name__) - main = empireMenu - +def start_restful_api(): # todo vr: can we remove the global obfuscate flag and if not, how should it be handled in v2? - @app.route("/api/admin/options", methods=["POST"]) def set_admin_options(): """ Admin menu options for obfuscation """ - if not request.json: - return make_response( - jsonify({"error": "request body must be valid JSON"}), 400 - ) - # Set global obfuscation if "obfuscate" in request.json: if request.json["obfuscate"].lower() == "true": @@ -64,102 +50,8 @@ def set_admin_options(): return make_response( jsonify({"error": "JSON body must include key valid admin option"}), 400 ) - - print(helpers.color(msg)) return jsonify({"success": True}) - def shutdown_server(): - """ - Shut down the Flask server and any Empire instance gracefully. - """ - global serverExitCommand - - print(helpers.color("[*] Shutting down Empire RESTful API")) - - if suppress: - print(helpers.color("[*] Shutting down the Empire instance")) - main.shutdown() - - serverExitCommand = "shutdown" - - func = request.environ.get("werkzeug.server.shutdown") - if func is not None: - func() - - 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() - - try: - signal.signal(signal.SIGINT, signal_handler) - except ValueError: - pass - - # wrap the Flask connection in SSL and start it - cert_path = os.path.abspath("./empire/server/data/") - - proto = ssl.PROTOCOL_TLS - context = ssl.SSLContext(proto) - context.load_cert_chain( - "%s/empire-chain.pem" % cert_path, "%s/empire-priv.key" % cert_path - ) - app.run(host=ip, port=int(port), ssl_context=context, threaded=True) - - # def server_startup_validator(): - # print(helpers.color('[*] Testing APIs')) - # rng = random.SystemRandom() - # username = 'test-' + ''.join(rng.choice(string.ascii_lowercase) for i in range(4)) - # password = ''.join(rng.choice(string.ascii_lowercase) for i in range(10)) - # main.users.add_new_user(username, password) - # response = requests.post(url=f'https://{args.restip}:{args.restport}/api/admin/login', - # json={'username': username, 'password': password}, - # verify=False) - # if response: - # print(helpers.color('[+] Empire RESTful API successfully started')) - # - # try: - # sio = socketio.Client(ssl_verify=False) - # sio.connect(f'wss://{args.restip}:{args.socketport}?token={response.json()["token"]}') - # print(helpers.color('[+] Empire SocketIO successfully started')) - # except Exception as e: - # print(e) - # print(helpers.color('[!] Empire SocketIO failed to start')) - # sys.exit() - # finally: - # cleanup_test_user(username) - # sio.disconnect() - # - # else: - # print(helpers.color('[!] Empire RESTful API failed to start')) - # cleanup_test_user(password) - # sys.exit() - - -def cleanup_test_user(username: str): - print(helpers.color("[*] Cleaning up test user")) - user = ( - SessionLocal() - .query(models.User) - .filter(models.User.username == username) - .first() - ) - SessionLocal().delete(user) - SessionLocal().commit() - main = empire.MainMenu(args=args) @@ -188,8 +80,6 @@ def run(args): subprocess.call("./setup/cert.sh") time.sleep(3) - # start an Empire instance and RESTful API with the teamserver interface - def thread_v2_api(): v2App.initialize() @@ -200,7 +90,6 @@ def thread_v2_api(): thread3.start() sleep(2) - # server_startup_validator() main.teamserver() sys.exit() diff --git a/empire/server/stagers/multi/war.py b/empire/server/stagers/multi/war.py index d5f04ee27..f62c3bbd6 100644 --- a/empire/server/stagers/multi/war.py +++ b/empire/server/stagers/multi/war.py @@ -1,8 +1,5 @@ from __future__ import print_function -from future import standard_library - -standard_library.install_aliases() import io import zipfile from builtins import object, str diff --git a/empire/server/v2/api/download/download_dto.py b/empire/server/v2/api/download/download_dto.py new file mode 100644 index 000000000..3703ff266 --- /dev/null +++ b/empire/server/v2/api/download/download_dto.py @@ -0,0 +1,48 @@ +from datetime import datetime +from enum import Enum +from typing import List + +from pydantic import BaseModel + + +def domain_to_dto_download(download): + return Download( + id=download.id, + location=download.location, + filename=download.filename, + size=download.size, + created_at=download.created_at, + updated_at=download.updated_at, + ) + + +class DownloadSourceFilter(str, Enum): + upload = "upload" + stager = "stager" + agent_file = "agent_file" + agent_task = "agent_task" + + +class DownloadOrderOptions(str, Enum): + filename = "filename" + location = "location" + size = "size" + created_at = "created_at" + updated_at = "updated_at" + + +class Download(BaseModel): + id: int + location: str + filename: str + size: int + created_at: datetime + updated_at: datetime + + +class Downloads(BaseModel): + records: List[Download] + limit: int + page: int + total_pages: int + total: int diff --git a/empire/server/v2/api/download/downloadv2.py b/empire/server/v2/api/download/downloadv2.py index c35d4fb19..31cc00eb8 100644 --- a/empire/server/v2/api/download/downloadv2.py +++ b/empire/server/v2/api/download/downloadv2.py @@ -1,12 +1,22 @@ -from fastapi import Depends, File, HTTPException, UploadFile +import math +from typing import List, Optional + +from fastapi import Depends, File, HTTPException, Query, UploadFile from sqlalchemy.orm import Session from starlette.responses import FileResponse from empire.server.database import models from empire.server.server import main +from empire.server.v2.api.download.download_dto import ( + DownloadOrderOptions, + Downloads, + DownloadSourceFilter, + domain_to_dto_download, +) from empire.server.v2.api.EmpireApiRouter import APIRouter from empire.server.v2.api.jwt_auth import get_current_active_user from empire.server.v2.api.shared_dependencies import get_db +from empire.server.v2.api.shared_dto import OrderDirection download_service = main.downloadsv2 @@ -26,12 +36,8 @@ async def get_download(uid: int, db: Session = Depends(get_db)): raise HTTPException(404, f"Download not found for id {uid}") -@router.get( - "/{uid}", - response_class=FileResponse, - dependencies=[Depends(get_current_active_user)], -) -async def read_download( +@router.get("/{uid}/download", response_class=FileResponse) +async def download_download( uid: int, db: Session = Depends(get_db), db_download: models.Download = Depends(get_download), @@ -44,15 +50,56 @@ async def read_download( return FileResponse(db_download.location, filename=filename) -# todo At the moment downloads don't have a backref to their joined objects. -# maybe that's fine? -# todo remove the install path from the location? -# todo { records: [] } +@router.get( + "/{uid}", + dependencies=[Depends(get_current_active_user)], +) +async def read_download( + uid: int, + db: Session = Depends(get_db), + db_download: models.Download = Depends(get_download), +): + return domain_to_dto_download(db_download) + + +# todo basically everything should go to downloads which means the path should start after downloads. @router.get("/", dependencies=[Depends(get_current_active_user)]) -async def read_downloads(db: Session = Depends(get_db), query: str = None): - return download_service.get_all(db, query) +async def read_downloads( + db: Session = Depends(get_db), + limit: int = -1, + page: int = 1, + order_direction: OrderDirection = OrderDirection.desc, + order_by: DownloadOrderOptions = DownloadOrderOptions.updated_at, + query: str = None, + sources: Optional[List[DownloadSourceFilter]] = Query(None), +): + downloads, total = download_service.get_all( + db=db, + download_types=sources, + q=query, + limit=limit, + offset=(page - 1) * limit, + order_by=order_by, + order_direction=order_direction, + ) + + downloads_converted = list(map(lambda x: domain_to_dto_download(x), downloads)) + return Downloads( + records=downloads_converted, + page=page, + total_pages=math.ceil(total / limit) + if limit > 0 + else page, # TODO this probably needs to be fixed on taskv2 + limit=limit, + total=total, + ) -@router.post("/", dependencies=[Depends(get_current_active_user)]) -async def create_download(db: Session = Depends(get_db), file: UploadFile = File(...)): - return download_service.create_download(db, file) + +@router.post("/", status_code=201) +async def create_download( + db: Session = Depends(get_db), + user: models.User = Depends(get_current_active_user), + file: UploadFile = File(...), +): + return domain_to_dto_download(download_service.create_download(db, user, file)) diff --git a/empire/server/v2/api/shared_dto.py b/empire/server/v2/api/shared_dto.py index fe881f1a3..da010ca1f 100644 --- a/empire/server/v2/api/shared_dto.py +++ b/empire/server/v2/api/shared_dto.py @@ -43,8 +43,11 @@ def domain_to_dto_download_description(download: models.Download): else: filename = download.location.split("/")[-1] + # todo file_name -> filename return DownloadDescription( - id=download.id, file_name=filename, link=f"/api/v2beta/downloads/{download.id}" + id=download.id, + file_name=filename, + link=f"/api/v2beta/downloads/{download.id}/download", ) diff --git a/empire/server/v2/api/v2App.py b/empire/server/v2/api/v2App.py index 29bb38a3a..23e1e7b1a 100644 --- a/empire/server/v2/api/v2App.py +++ b/empire/server/v2/api/v2App.py @@ -1,4 +1,5 @@ import json +import os from datetime import datetime from json import JSONEncoder @@ -31,6 +32,8 @@ def default(self, o): if isinstance(o, bytes): return o.decode("latin-1") + # todo TaskingStatus not serializing + # Object of type User is not JSON Serializable return super(MyJsonEncoder, self).default(o) @@ -111,4 +114,16 @@ def initialize(): except Exception as e: pass - uvicorn.run(v2App, host="0.0.0.0", port=8000) + cert_path = os.path.abspath("./empire/server/data/") + + # todo this gets the cert working, but ajax requests are not working, since browsers + # do not like self signed certs. + # todo if the server fails to start we should exit. + uvicorn.run( + v2App, + host="0.0.0.0", + port=1337, + # ssl_keyfile="%s/empire-priv.key" % cert_path, + # ssl_certfile="%s/empire-chain.pem" % cert_path, + # log_level="info", + ) diff --git a/empire/server/v2/core/download_service.py b/empire/server/v2/core/download_service.py index e8da8cbf4..8c3d3ac11 100644 --- a/empire/server/v2/core/download_service.py +++ b/empire/server/v2/core/download_service.py @@ -1,11 +1,17 @@ import os import shutil +from typing import List, Optional, Tuple from fastapi import UploadFile -from sqlalchemy import or_ +from sqlalchemy import func, or_ from sqlalchemy.orm import Session from empire.server.database import models +from empire.server.v2.api.download.download_dto import ( + DownloadOrderOptions, + DownloadSourceFilter, +) +from empire.server.v2.api.shared_dto import OrderDirection class DownloadService(object): @@ -17,8 +23,51 @@ def get_by_id(db: Session, uid: int): return db.query(models.Download).filter(models.Download.id == uid).first() @staticmethod - def get_all(db: Session, q: str): - query = db.query(models.Download) + def get_all( + db: Session, + download_types: Optional[List[DownloadSourceFilter]], + q: str, + limit: int = -1, + offset: int = 0, + order_by: DownloadOrderOptions = DownloadOrderOptions.updated_at, + order_direction: OrderDirection = OrderDirection.desc, + ) -> Tuple[List[models.Download], int]: + query = db.query( + models.Download, func.count(models.Download.id).over().label("total") + ) + + download_types = download_types or [] + sub = [] + if DownloadSourceFilter.agent_task in download_types: + sub.append( + db.query( + models.tasking_download_assc.c.download_id.label("download_id") + ) + ) + if DownloadSourceFilter.agent_file in download_types: + sub.append( + db.query( + models.agent_file_download_assc.c.download_id.label("download_id") + ) + ) + if DownloadSourceFilter.stager in download_types: + sub.append( + db.query(models.stager_download_assc.c.download_id.label("download_id")) + ) + if DownloadSourceFilter.upload in download_types: + sub.append( + db.query(models.upload_download_assc.c.download_id.label("download_id")) + ) + + subquery = None + if len(sub) > 0: + subquery = sub[0] + if len(sub) > 1: + subquery = subquery.union(*sub[1:]) + subquery = subquery.subquery() + + if subquery is not None: + query = query.join(subquery, subquery.c.download_id == models.Download.id) if q: query = query.filter( @@ -27,23 +76,65 @@ def get_all(db: Session, q: str): models.Download.location.like(f"%{q}%"), ) ) - return query.all() - def create_download(self, db: Session, file: UploadFile): + if order_by == DownloadOrderOptions.filename: + order_by_prop = func.lower(models.Download.filename) + elif order_by == DownloadOrderOptions.location: + order_by_prop = func.lower(models.Download.location) + elif order_by == DownloadOrderOptions.size: + order_by_prop = models.Download.size + elif order_by == DownloadOrderOptions.created_at: + order_by_prop = models.Download.created_at + else: + order_by_prop = models.Download.updated_at + + if order_direction == OrderDirection.asc: + query = query.order_by(order_by_prop.asc()) + else: + query = query.order_by(order_by_prop.desc()) + + if limit > 0: + query = query.limit(limit).offset(offset) + + results = query.all() + + total = 0 if len(results) == 0 else results[0].total + results = list(map(lambda x: x[0], results)) + + return results, total + + def create_download(self, db: Session, user: models.User, file: UploadFile): """ Upload the file to the downloads directory and save a reference to the db. :param db: + :param user: :param file: :return: """ # TODO VR should this should be pulled from empire_config instead of main_menu - location = f"{self.main_menu.directory['downloads']}{file.filename}" + filename = file.filename + location = ( + f"{self.main_menu.directory['downloads']}uploads/{user.username}/{filename}" + ) + os.makedirs(os.path.dirname(location), exist_ok=True) + + # append number to filename if it already exists + filename, file_extension = os.path.splitext(filename) + i = 1 + while os.path.isfile(location): + temp_name = f"{filename}({i}){file_extension}" + location = f"{self.main_menu.directory['downloads']}uploads/{user.username}/{temp_name}" + i += 1 + filename = os.path.basename(location) + with open(location, "wb") as buffer: shutil.copyfileobj(file.file, buffer) download = models.Download( - location=location, filename=file.filename, size=os.path.getsize(location) + location=location, filename=filename, size=os.path.getsize(location) ) db.add(download) + db.flush() + db.execute(models.upload_download_assc.insert().values(download_id=download.id)) return download diff --git a/empire/server/v2/core/listener_service.py b/empire/server/v2/core/listener_service.py index 261246a2b..b20fa315d 100644 --- a/empire/server/v2/core/listener_service.py +++ b/empire/server/v2/core/listener_service.py @@ -3,7 +3,7 @@ import json from typing import Dict, List, Optional, Tuple -from pydispatch import dispatcher +# from pydispatch import dispatcher from sqlalchemy.orm import Session from empire.server.common import helpers @@ -119,7 +119,7 @@ def delete_listener(self, db: Session, db_listener: models.Listener): db.delete(db_listener) def shutdown_listeners(self): - for key, listener in self._active_listeners: + for key, listener in self._active_listeners.items(): listener.shutdown() def start_existing_listener(self, db: Session, listener: models.Listener): diff --git a/empire/server/v2/core/plugin_service.py b/empire/server/v2/core/plugin_service.py index fedb1b557..929bdabe0 100644 --- a/empire/server/v2/core/plugin_service.py +++ b/empire/server/v2/core/plugin_service.py @@ -89,5 +89,5 @@ def get_by_id(self, uid: str): return self.loaded_plugins[uid] def shutdown(self): - for plugin in self.loaded_plugins: + for plugin in self.loaded_plugins.values(): plugin.shutdown() diff --git a/empire/server/v2/core/profile_service.py b/empire/server/v2/core/profile_service.py index 8dde83fac..d5b47edf9 100644 --- a/empire/server/v2/core/profile_service.py +++ b/empire/server/v2/core/profile_service.py @@ -2,7 +2,7 @@ import json import os -from pydispatch import dispatcher +# from pydispatch import dispatcher from sqlalchemy.orm import Session from empire.server.common import helpers diff --git a/empire/server/v2/core/stager_service.py b/empire/server/v2/core/stager_service.py index e1794766b..c4f3fc2ee 100644 --- a/empire/server/v2/core/stager_service.py +++ b/empire/server/v2/core/stager_service.py @@ -1,7 +1,5 @@ import copy import os -import random -import string import uuid from typing import Dict, Optional, Tuple @@ -117,7 +115,7 @@ def create_stager(self, db: Session, stager_req, save: bool, user_id: int): download = models.Download( location=generated, - filename=None if db_stager else self.get_one_liner_filename(db_stager), + filename=generated.split("/")[-1], size=os.path.getsize(generated), ) db.add(download) @@ -159,7 +157,7 @@ def update_stager(self, db: Session, db_stager: models.Stager, stager_req): download = models.Download( location=generated, - filename=None if db_stager else self.get_one_liner_filename(db_stager), + filename=generated.split("/")[-1], size=os.path.getsize(generated), ) db.add(download) @@ -178,10 +176,13 @@ def generate_stager(self, template_instance): out_file = template_instance.options.get("OutFile", {}).get("Value") if out_file and len(out_file) > 0: file_name = template_instance.options["OutFile"]["Value"].split("/")[-1] - else: + else: # todo use a better default name file_name = f"{uuid.uuid4()}.txt" - file_name = f"{self.main_menu.installPath}/data/generated-stagers/{file_name}" + # TODO VR should this should be pulled from empire_config instead of main_menu + file_name = ( + f"{self.main_menu.directory['downloads']}generated-stagers/{file_name}" + ) os.makedirs(os.path.dirname(file_name), exist_ok=True) mode = "w" if type(resp) == str else "wb" with open(file_name, mode) as f: @@ -192,8 +193,3 @@ def generate_stager(self, template_instance): @staticmethod def delete_stager(db: Session, stager: models.Stager): db.delete(stager) - - @staticmethod - def get_one_liner_filename(db_stager: models.Stager): - random_chars = "".join(random.choice(string.ascii_letters) for i in range(10)) - return f"stager_{db_stager.module}_{db_stager.id if db_stager.id > 0 else random_chars}.txt" diff --git a/empire/test/conftest.py b/empire/test/conftest.py index a26e3a46e..f14e3b199 100644 --- a/empire/test/conftest.py +++ b/empire/test/conftest.py @@ -7,8 +7,10 @@ @pytest.fixture(scope="session") def client(): + # todo could make test_config a bit more dynamic so we can generate random db names + # can we do the pathing in a way that we can run tests from any directory? + # test bootstrapping should clear the files dir and test db. sys.argv = ["", "server", "--config", "empire/test/test_config.yaml"] - from empire.server.v2.api.agent import agentfilev2, agentv2, taskv2 from empire.server.v2.api.bypass import bypassv2 from empire.server.v2.api.credential import credentialv2 @@ -47,7 +49,9 @@ def client(): from empire.server.database.base import engine from empire.server.database.models import Base + from empire.server.server import main + main.shutdown() Base.metadata.drop_all(engine) @@ -63,9 +67,6 @@ def admin_auth_token(client): }, ) - # todo this is not working. need to manually set tokens :( - client.headers["Authorization"] = f'Bearer: {response.json()["access_token"]}' - yield response.json()["access_token"] diff --git a/empire/test/test_config.yaml b/empire/test/test_config.yaml index c4595edbf..639ec1e7c 100644 --- a/empire/test/test_config.yaml +++ b/empire/test/test_config.yaml @@ -20,6 +20,7 @@ database: modules: retain-last-value: false directories: - downloads: empire/server/downloads/ + # todo should test directory be embedded under server? + downloads: empire/test/downloads/ module_source: empire/server/data/module_source/ obfuscated_module_source: empire/server/data/obfuscated_module_source/ \ No newline at end of file diff --git a/empire/test/test_download_api.py b/empire/test/test_download_api.py index 4c7f02a6e..a6242808b 100644 --- a/empire/test/test_download_api.py +++ b/empire/test/test_download_api.py @@ -1,22 +1,99 @@ -def test_get_download_not_found(): - assert 0 == 1 +import urllib.parse -def test_get_download_with_filename(): - assert 0 == 1 +def test_get_download_not_found(client, admin_auth_header): + response = client.get("/api/v2beta/downloads/9999", headers=admin_auth_header) + assert response.status_code == 404 + assert response.json()["detail"] == "Download not found for id 9999" -def test_get_download_no_filename(): - assert 0 == 1 +def test_create_download(client, admin_auth_header): + response = client.post( + "/api/v2beta/downloads", + headers=admin_auth_header, + files={ + "file": ( + "test-upload.yaml", + open("./empire/test/test-upload.yaml", "r").read(), + ) + }, + ) -def test_get_downloads(): - assert 0 == 1 + assert response.status_code == 201 + assert response.json()["id"] == 1 -def test_get_downloads_with_query(): - assert 0 == 1 +def test_create_download_appends_number_if_already_exists(client, admin_auth_header): + response = client.post( + "/api/v2beta/downloads", + headers=admin_auth_header, + files={ + "file": ( + "test-upload.yaml", + open("./empire/test/test-upload.yaml", "r").read(), + ) + }, + ) + assert response.status_code == 201 + assert response.json()["id"] > 0 -def test_create_download(): - assert 0 == 1 + response = client.post( + "/api/v2beta/downloads", + headers=admin_auth_header, + files={ + "file": ( + "test-upload.yaml", + open("./empire/test/test-upload.yaml", "r").read(), + ) + }, + ) + + assert response.status_code == 201 + assert response.json()["id"] > 0 + assert response.json()["location"].endswith("(2).yaml") + assert response.json()["filename"].endswith("(2).yaml") + + +def test_get_download(client, admin_auth_header): + response = client.get("/api/v2beta/downloads/1", headers=admin_auth_header) + + assert response.status_code == 200 + assert response.json()["id"] == 1 + assert response.json()["filename"] == "test-upload.yaml" + + +def test_download_download(client, admin_auth_header): + response = client.get("/api/v2beta/downloads/1/download", headers=admin_auth_header) + + assert response.status_code == 200 + assert ( + response.headers.get("content-disposition") + == 'attachment; filename="test-upload.yaml"' + ) + + +def test_get_downloads(client, admin_auth_header): + response = client.get("/api/v2beta/downloads", headers=admin_auth_header) + + assert response.status_code == 200 + assert response.json()["total"] == 3 + assert response.json()["records"][0]["id"] == 1 + + +def test_get_downloads_with_query(client, admin_auth_header): + response = client.get( + "/api/v2beta/downloads?query=gobblygook", headers=admin_auth_header + ) + + assert response.status_code == 200 + assert response.json()["total"] == 0 + assert response.json()["records"] == [] + + q = urllib.parse.urlencode({"query": "test-upload(2)"}) + response = client.get(f"/api/v2beta/downloads?{q}", headers=admin_auth_header) + + assert response.status_code == 200 + assert response.json()["total"] == 1 + assert response.json()["records"][0]["id"] == 3 diff --git a/poetry.lock b/poetry.lock index a378a9e16..50fb90275 100644 --- a/poetry.lock +++ b/poetry.lock @@ -577,14 +577,6 @@ typing-extensions = ">=3.7.4.3" dotenv = ["python-dotenv (>=0.10.4)"] email = ["email-validator (>=1.0.3)"] -[[package]] -name = "pydispatcher" -version = "2.0.5" -description = "Multi-producer-multi-consumer signal dispatching mechanism" -category = "main" -optional = false -python-versions = "*" - [[package]] name = "pyflakes" version = "2.3.1" @@ -1031,7 +1023,7 @@ python-versions = "*" [metadata] lock-version = "1.1" python-versions = "^3.8" -content-hash = "0bf62038a9125f40dab48ee9317b9184588e624fbe584e57339f76fdedc8673b" +content-hash = "f31b0632cff0ebb17f19fa2a51e68a33ca5731f305dd0eb2c1a60786ee5f56f0" [metadata.files] aiofiles = [ @@ -1593,10 +1585,6 @@ pydantic = [ {file = "pydantic-1.9.0-py3-none-any.whl", hash = "sha256:085ca1de245782e9b46cefcf99deecc67d418737a1fd3f6a4f511344b613a5b3"}, {file = "pydantic-1.9.0.tar.gz", hash = "sha256:742645059757a56ecd886faf4ed2441b9c0cd406079c2b4bee51bcc3fbcd510a"}, ] -pydispatcher = [ - {file = "PyDispatcher-2.0.5.tar.gz", hash = "sha256:5570069e1b1769af1fe481de6dd1d3a388492acddd2cdad7a3bde145615d5caf"}, - {file = "PyDispatcher-2.0.5.zip", hash = "sha256:5be4a8be12805ef7d712dd9a93284fb8bc53f309867e573f653a72e5fd10e433"}, -] pyflakes = [ {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"}, {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, diff --git a/pyproject.toml b/pyproject.toml index f4837364f..fd9317eee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,9 +16,7 @@ packages = [ python = "^3.8" urllib3 = "*" requests = "^2.24.0" -setuptools = "*" iptools = "*" -pydispatcher = "*" flask = "^1.1.2" macholib = "*" dropbox = "*" @@ -28,7 +26,6 @@ zlib_wrapper = "*" netifaces = "*" jinja2 = "*" xlutils = "*" -pefile = "*" pyparsing = "*" PyMySQL = "^0.10.1" SQLAlchemy = "^1.3.20"