Skip to content

Commit

Permalink
Support Empire for system-wide deployment (#757)
Browse files Browse the repository at this point in the history
  • Loading branch information
D3vil0p3r authored Dec 7, 2024
1 parent 8f41087 commit 54ce418
Show file tree
Hide file tree
Showing 14 changed files with 173 additions and 53 deletions.
3 changes: 0 additions & 3 deletions .github/cst-config-docker.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 0 additions & 3 deletions .github/install_tests/cst-config-install-base.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 1 addition & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions docs/quickstart/configuration/client.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
2 changes: 2 additions & 0 deletions docs/quickstart/configuration/server.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
5 changes: 3 additions & 2 deletions empire.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down
5 changes: 4 additions & 1 deletion empire/client/src/EmpireCliConfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

import yaml

from empire import config_manager

log = logging.getLogger(__name__)


Expand All @@ -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:
Expand Down
122 changes: 122 additions & 0 deletions empire/config_manager.py
Original file line number Diff line number Diff line change
@@ -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
44 changes: 30 additions & 14 deletions empire/server/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import yaml
from pydantic import BaseModel, ConfigDict, Field, field_validator

from empire import config_manager

log = logging.getLogger(__name__)


Expand Down Expand Up @@ -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):
Expand All @@ -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
Expand All @@ -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)
3 changes: 1 addition & 2 deletions empire/server/modules/bof/nanodump.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 3 additions & 15 deletions empire/server/server.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
#!/usr/bin/env python3
import logging
import os
import pathlib
import pwd
import shutil
import signal
Expand Down Expand Up @@ -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():
Expand All @@ -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")
Expand Down Expand Up @@ -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)

Expand Down
8 changes: 1 addition & 7 deletions empire/test/test_zz_reset.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand All @@ -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 = [
Expand Down Expand Up @@ -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)
Expand All @@ -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()

Expand Down
Loading

0 comments on commit 54ce418

Please sign in to comment.