Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Empire for system-wide deployment #757

Merged
merged 4 commits into from
Dec 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
D3vil0p3r marked this conversation as resolved.
Show resolved Hide resolved
- 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):
vinnybod marked this conversation as resolved.
Show resolved Hide resolved
"""
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)
vinnybod marked this conversation as resolved.
Show resolved Hide resolved

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"
vinnybod marked this conversation as resolved.
Show resolved Hide resolved


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
Loading