From 54ce4187635365d7285fb4cc84669b65bfe60ea5 Mon Sep 17 00:00:00 2001 From: Antonio Date: Sat, 7 Dec 2024 23:20:23 +0100 Subject: [PATCH] Support Empire for system-wide deployment (#757) --- .github/cst-config-docker.yaml | 3 - .../cst-config-install-base.yaml | 3 - CHANGELOG.md | 4 + Dockerfile | 4 +- docs/quickstart/configuration/client.md | 2 + docs/quickstart/configuration/server.md | 2 + empire.py | 5 +- empire/client/src/EmpireCliConfig.py | 5 +- empire/config_manager.py | 122 ++++++++++++++++++ empire/server/core/config.py | 44 +++++-- empire/server/modules/bof/nanodump.py | 3 +- empire/server/server.py | 18 +-- empire/test/test_zz_reset.py | 8 +- setup/install.sh | 3 - 14 files changed, 173 insertions(+), 53 deletions(-) create mode 100644 empire/config_manager.py diff --git a/.github/cst-config-docker.yaml b/.github/cst-config-docker.yaml index 195bdef20..7f268c26f 100644 --- a/.github/cst-config-docker.yaml +++ b/.github/cst-config-docker.yaml @@ -48,9 +48,6 @@ fileExistenceTests: - name: 'profiles' path: '/empire/empire/server/data/profiles/' shouldExist: true - - name: 'invoke obfuscation' - path: '/usr/local/share/powershell/Modules/Invoke-Obfuscation/' - shouldExist: true - name: 'sharpire' path: '/empire/empire/server/csharp/Covenant/Data/ReferenceSourceLibraries/Sharpire' shouldExist: true diff --git a/.github/install_tests/cst-config-install-base.yaml b/.github/install_tests/cst-config-install-base.yaml index d79acc82a..61bcbb77d 100644 --- a/.github/install_tests/cst-config-install-base.yaml +++ b/.github/install_tests/cst-config-install-base.yaml @@ -71,9 +71,6 @@ fileExistenceTests: - name: 'profiles' path: '/empire/empire/server/data/profiles/' shouldExist: true - - name: 'invoke obfuscation' - path: '/usr/local/share/powershell/Modules/Invoke-Obfuscation/' - shouldExist: true - name: 'sharpire' path: '/empire/empire/server/csharp/Covenant/Data/ReferenceSourceLibraries/Sharpire' shouldExist: true diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d592319d..9ec54468e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- Support Empire for system-wide deployment (@D3vil0p3r) +- Paths specified in config.yaml where user does not have write permission will be fallback to ~/.empire directory and config.yaml updated as well (@D3vil0p3r) +- Invoke-Obfuscation is no longer copied to /usr/local/share + ## [5.11.7] - 2024-11-11 - Fix arm installs by installing dotnet and powershell manually diff --git a/Dockerfile b/Dockerfile index 61fdf3e43..65fd89ca9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -59,9 +59,7 @@ RUN poetry config virtualenvs.create false && \ COPY . /empire -RUN mkdir -p /usr/local/share/powershell/Modules && \ - cp -r ./empire/server/data/Invoke-Obfuscation /usr/local/share/powershell/Modules && \ - rm -rf /empire/empire/server/data/empire* +RUN rm -rf /empire/empire/server/data/empire* RUN sed -i 's/use: mysql/use: sqlite/g' empire/server/config.yaml && \ sed -i 's/auto_update: true/auto_update: false/g' empire/server/config.yaml diff --git a/docs/quickstart/configuration/client.md b/docs/quickstart/configuration/client.md index 22d0f294f..393f10ee9 100644 --- a/docs/quickstart/configuration/client.md +++ b/docs/quickstart/configuration/client.md @@ -5,6 +5,8 @@ The Client configuration is managed via [empire/client/config.yaml](https://github.com/BC-SECURITY/Empire/blob/master/empire/client/config.yaml). +Once launched, Empire checks for user write permissions on paths specified in `config.yaml`. If the current user does not have write permissions on these paths, `~/.empire` will be set as fallback parent directory and the configuration file will be updated as well. + * **servers** - The servers block is meant to give the user the ability to set up frequently used Empire servers. If a server is listed in this block then when connecting to the server they need only type: `connect -c localhost`. diff --git a/docs/quickstart/configuration/server.md b/docs/quickstart/configuration/server.md index e6c0a9c44..4e00381ee 100644 --- a/docs/quickstart/configuration/server.md +++ b/docs/quickstart/configuration/server.md @@ -2,6 +2,8 @@ The Server configuration is managed via [empire/server/config.yaml](https://github.com/BC-SECURITY/Empire/blob/master/empire/client/config.yaml). +Once launched, Empire checks for user write permissions on paths specified in `config.yaml`. If the current user does not have write permissions on these paths, `~/.empire` will be set as fallback parent directory and the configuration file will be updated as well. + * **suppress-self-cert-warning** - Suppress the http warnings when launching an Empire instance that uses a self-signed cert. * **api** - Configure the RESTful API. This includes the port to run the API on, as well as the path for the SSL certificates. If `empire-priv.key` and `empire-chain.pem` are not found in this directory, self-signed certs will be generated. diff --git a/empire.py b/empire.py index a9687dec4..9c199f719 100644 --- a/empire.py +++ b/empire.py @@ -2,10 +2,11 @@ import sys -from empire import arguments +from empire import arguments, config_manager if __name__ == "__main__": args = arguments.args + config_manager.config_init() if args.subparser_name == "server": from empire.server import server @@ -16,7 +17,7 @@ from empire.scripts.sync_starkiller import sync_starkiller - with open("empire/server/config.yaml") as f: + with open(config_manager.CONFIG_SERVER_PATH) as f: config = yaml.safe_load(f) sync_starkiller(config) diff --git a/empire/client/src/EmpireCliConfig.py b/empire/client/src/EmpireCliConfig.py index 10efeaa28..9029ebb71 100644 --- a/empire/client/src/EmpireCliConfig.py +++ b/empire/client/src/EmpireCliConfig.py @@ -3,6 +3,8 @@ import yaml +from empire import config_manager + log = logging.getLogger(__name__) @@ -15,7 +17,8 @@ def __init__(self): self.set_yaml(location) if len(self.yaml.items()) == 0: log.info("Loading default config") - self.set_yaml("./empire/client/config.yaml") + self.set_yaml(config_manager.CONFIG_CLIENT_PATH) + config_manager.check_config_permission(self.yaml, "client") def set_yaml(self, location: str): try: diff --git a/empire/config_manager.py b/empire/config_manager.py new file mode 100644 index 000000000..5f172133c --- /dev/null +++ b/empire/config_manager.py @@ -0,0 +1,122 @@ +import logging +import os +import shutil +from pathlib import Path + +import yaml + +log = logging.getLogger(__name__) + +user_home = Path.home() +SOURCE_CONFIG_CLIENT = Path("empire/client/config.yaml") +SOURCE_CONFIG_SERVER = Path("empire/server/config.yaml") +CONFIG_DIR = user_home / ".empire" +CONFIG_CLIENT_PATH = CONFIG_DIR / "client" / "config.yaml" +CONFIG_SERVER_PATH = CONFIG_DIR / "server" / "config.yaml" + + +def config_init(): + CONFIG_CLIENT_PATH.parent.mkdir(parents=True, exist_ok=True) + CONFIG_SERVER_PATH.parent.mkdir(parents=True, exist_ok=True) + + if not CONFIG_CLIENT_PATH.exists(): + shutil.copy(SOURCE_CONFIG_CLIENT, CONFIG_CLIENT_PATH) + log.info(f"Copied {SOURCE_CONFIG_CLIENT} to {CONFIG_CLIENT_PATH}") + else: + log.info(f"{CONFIG_CLIENT_PATH} already exists.") + + if not CONFIG_SERVER_PATH.exists(): + shutil.copy(SOURCE_CONFIG_SERVER, CONFIG_SERVER_PATH) + log.info(f"Copied {SOURCE_CONFIG_SERVER} to {CONFIG_SERVER_PATH}") + else: + log.info(f"{CONFIG_SERVER_PATH} already exists.") + + +def check_config_permission(config_dict: dict, config_type: str): + """ + Check if the specified directories in config.yaml are writable. If not, switches to a fallback directory. + Handles both server and client configurations. + + Args: + config_dict (dict): The configuration dictionary loaded from YAML. + config_type (str): The type of configuration ("server" or "client"). + """ + # Define paths to check based on config type + if config_type == "server": + paths_to_check = { + ("api", "cert_path"): config_dict.get("api", {}).get("cert_path"), + ("database", "sqlite", "location"): config_dict.get("database", {}) + .get("sqlite", {}) + .get("location"), + ("starkiller", "directory"): config_dict.get("starkiller", {}).get( + "directory" + ), + ("logging", "directory"): config_dict.get("logging", {}).get("directory"), + ("debug", "last_task", "file"): config_dict.get("debug", {}) + .get("last_task", {}) + .get("file"), + ("directories", "downloads"): config_dict.get("directories", {}).get( + "downloads" + ), + } + config_path = CONFIG_SERVER_PATH # Use the server config path + + elif config_type == "client": + paths_to_check = { + ("logging", "directory"): config_dict.get("logging", {}).get("directory"), + ("directories", "downloads"): config_dict.get("directories", {}).get( + "downloads" + ), + ("directories", "generated-stagers"): config_dict.get( + "directories", {} + ).get("generated-stagers"), + } + config_path = CONFIG_CLIENT_PATH # Use the client config path + + else: + raise ValueError("Invalid config_type. Expected 'server' or 'client'.") + + # Check permissions and update paths as needed + for keys, dir_path in paths_to_check.items(): + if dir_path is None: + continue + + current_dir = dir_path + while current_dir and not os.path.exists(current_dir): + current_dir = os.path.dirname(current_dir) + + if not os.access(current_dir, os.W_OK): + log.info( + "No write permission for %s. Switching to fallback directory.", + current_dir, + ) + user_home = Path.home() + fallback_dir = os.path.join( + user_home, ".empire", str(current_dir).removeprefix("empire/") + ) + + # Update the directory in config_dict + target = config_dict # target is a reference to config_dict + for key in keys[:-1]: + target = target[key] + target[keys[-1]] = fallback_dir + + log.info( + "Updated %s to fallback directory: %s", "->".join(keys), fallback_dir + ) + + # Write the updated configuration back to the correct YAML file + with open(config_path, "w") as config_file: + yaml.safe_dump(paths2str(config_dict), config_file) + + return config_dict + + +def paths2str(data): + if isinstance(data, dict): + return {key: paths2str(value) for key, value in data.items()} + if isinstance(data, list): + return [paths2str(item) for item in data] + if isinstance(data, Path): + return str(data) + return data diff --git a/empire/server/core/config.py b/empire/server/core/config.py index 539c56c87..46eea3f51 100644 --- a/empire/server/core/config.py +++ b/empire/server/core/config.py @@ -5,6 +5,8 @@ import yaml from pydantic import BaseModel, ConfigDict, Field, field_validator +from empire import config_manager + log = logging.getLogger(__name__) @@ -74,9 +76,9 @@ def __getitem__(self, key): class DirectoriesConfig(EmpireBaseModel): - downloads: Path - module_source: Path - obfuscated_module_source: Path + downloads: Path = Path("empire/server/downloads") + module_source: Path = Path("empire/server/modules") + obfuscated_module_source: Path = Path("empire/server/data/obfuscated_module_source") class LoggingConfig(EmpireBaseModel): @@ -99,17 +101,26 @@ class EmpireConfig(EmpireBaseModel): alias="supress-self-cert-warning", default=True ) api: ApiConfig | None = ApiConfig() - starkiller: StarkillerConfig - submodules: SubmodulesConfig - database: DatabaseConfig + starkiller: StarkillerConfig = StarkillerConfig() + submodules: SubmodulesConfig = SubmodulesConfig() + database: DatabaseConfig = DatabaseConfig( + sqlite=SQLiteDatabaseConfig(), + mysql=MySQLDatabaseConfig(), + defaults=DatabaseDefaultsConfig(), + ) plugins: dict[str, dict[str, str]] = {} - directories: DirectoriesConfig - logging: LoggingConfig - debug: DebugConfig + directories: DirectoriesConfig = DirectoriesConfig() + logging: LoggingConfig = LoggingConfig() + debug: DebugConfig = DebugConfig(last_task=LastTaskConfig()) model_config = ConfigDict(extra="allow") - def __init__(self, config_dict: dict): + def __init__(self, config_dict: dict | None = None): + if config_dict is None: + config_dict = {} + if not isinstance(config_dict, dict): + raise ValueError("config_dict must be a dictionary") + super().__init__(**config_dict) # For backwards compatibility self.yaml = config_dict @@ -126,13 +137,18 @@ def set_yaml(location: str): log.warning(exc) -config_dict = {} +config_dict = EmpireConfig().model_dump() if "--config" in sys.argv: location = sys.argv[sys.argv.index("--config") + 1] log.info(f"Loading config from {location}") - config_dict = set_yaml(location) -if len(config_dict.items()) == 0: + loaded_config = set_yaml(location) + if loaded_config: + config_dict = loaded_config +elif config_manager.CONFIG_SERVER_PATH.exists(): log.info("Loading default config") - config_dict = set_yaml("./empire/server/config.yaml") + loaded_config = set_yaml(config_manager.CONFIG_SERVER_PATH) + if loaded_config: + config_dict = loaded_config + config_dict = config_manager.check_config_permission(config_dict, "server") empire_config = EmpireConfig(config_dict) diff --git a/empire/server/modules/bof/nanodump.py b/empire/server/modules/bof/nanodump.py index 30d635115..c214234b8 100644 --- a/empire/server/modules/bof/nanodump.py +++ b/empire/server/modules/bof/nanodump.py @@ -16,8 +16,7 @@ def generate( module=module, params=params, obfuscate=obfuscate ) - for name in params: - value = params[name] + for name, value in params.items(): if name == "write": if value != "": dump_path = value diff --git a/empire/server/server.py b/empire/server/server.py index bfeb69cd2..01287e692 100755 --- a/empire/server/server.py +++ b/empire/server/server.py @@ -1,7 +1,6 @@ #!/usr/bin/env python3 import logging import os -import pathlib import pwd import shutil import signal @@ -67,10 +66,6 @@ def setup_logging(args): CSHARP_DIR_BASE = os.path.join(os.path.dirname(__file__), "csharp/Covenant") -INVOKE_OBFS_SRC_DIR_BASE = os.path.join( - os.path.dirname(__file__), "data/Invoke-Obfuscation" -) -INVOKE_OBFS_DST_DIR_BASE = "/usr/local/share/powershell/Modules/Invoke-Obfuscation" def reset(): @@ -94,16 +89,6 @@ def reset(): if os.path.exists(empire_config.starkiller.directory): shutil.rmtree(empire_config.starkiller.directory) - # invoke obfuscation - if os.path.exists(f"{INVOKE_OBFS_DST_DIR_BASE}"): - shutil.rmtree(INVOKE_OBFS_DST_DIR_BASE) - pathlib.Path(pathlib.Path(INVOKE_OBFS_SRC_DIR_BASE).parent).mkdir( - parents=True, exist_ok=True - ) - shutil.copytree( - INVOKE_OBFS_SRC_DIR_BASE, INVOKE_OBFS_DST_DIR_BASE, dirs_exist_ok=True - ) - file_util.remove_file("data/sessions.csv") file_util.remove_file("data/credentials.csv") file_util.remove_file("data/master.log") @@ -144,6 +129,9 @@ def check_submodules(): def fetch_submodules(): + if not os.path.exists(Path(".git")): + log.info("No .git directory found. Skipping submodule fetch.") + return command = ["git", "submodule", "update", "--init", "--recursive"] run_as_user(command) diff --git a/empire/test/test_zz_reset.py b/empire/test/test_zz_reset.py index a3c7dbfe1..da047377d 100644 --- a/empire/test/test_zz_reset.py +++ b/empire/test/test_zz_reset.py @@ -39,8 +39,6 @@ def test_reset_server(monkeypatch, tmp_path, default_argv, server_config_dict): 1. Deletes the sqlite db. Don't need to test mysql atm. 2. Deletes the downloads dir contents 3. Deletes the csharp generated files - 4. Deletes the obfuscated modules - 5. Deletes / Copies invoke obfuscation """ monkeypatch.setattr("builtins.input", lambda _: "y") sys.argv = [*default_argv.copy(), "--reset"] @@ -64,9 +62,8 @@ def test_reset_server(monkeypatch, tmp_path, default_argv, server_config_dict): for f in download_files: assert Path(downloads_dir + f[0]).exists() - # Change the csharp and Invoke-Obfuscation dir so we don't delete real files. + # Change the csharp dir so we don't delete real files. csharp_dir = tmp_path / "empire/server/data/csharp" - invoke_obfs_dir = tmp_path / "powershell/Modules/Invoke-Obfuscation" # Write files to csharp_dir csharp_files = [ @@ -105,7 +102,6 @@ def test_reset_server(monkeypatch, tmp_path, default_argv, server_config_dict): assert Path(server_config_dict["database"]["location"]).exists() server.CSHARP_DIR_BASE = csharp_dir - server.INVOKE_OBFS_DST_DIR_BASE = invoke_obfs_dir with pytest.raises(SystemExit): server.run(args) @@ -126,8 +122,6 @@ def test_reset_server(monkeypatch, tmp_path, default_argv, server_config_dict): csharp_dir / "Data/Tasks/CSharp/Compiled/netcoreapp3.0" / f[0] ).exists() - assert Path(invoke_obfs_dir / "Invoke-Obfuscation.ps1").exists() - if server_config_dict.get("database", {}).get("type") == "sqlite": assert not Path(server_config_dict["database"]["location"]).exists() diff --git a/setup/install.sh b/setup/install.sh index 7eeeace3c..28c3c4c6a 100755 --- a/setup/install.sh +++ b/setup/install.sh @@ -40,9 +40,6 @@ function install_powershell() { sudo tar zxf /tmp/powershell.tar.gz -C /opt/microsoft/powershell/7 sudo chmod +x /opt/microsoft/powershell/7/pwsh sudo ln -s /opt/microsoft/powershell/7/pwsh /usr/bin/pwsh - - sudo mkdir -p /usr/local/share/powershell/Modules - sudo cp -r "$PARENT_PATH"/empire/server/data/Invoke-Obfuscation /usr/local/share/powershell/Modules } function install_mysql() {