Skip to content
Open
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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
18 changes: 16 additions & 2 deletions .github/cst-config-docker.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ commandTests:
- name: "python3 version"
command: "python3"
args: ["--version"]
expectedOutput: ["Python 3.13.*"]
expectedOutput: ["Python 3.14.*"]
- name: "poetry"
command: "which"
args: ["poetry"]
Expand All @@ -19,7 +19,7 @@ commandTests:
- name: "poetry python version"
command: "poetry"
args: ["run", "python3", "--version"]
expectedOutput: ["Python 3.13.*"]
expectedOutput: ["Python 3.14.*"]
- name: "powershell which"
command: "which"
args: ["pwsh"]
Expand All @@ -44,6 +44,14 @@ commandTests:
command: "go"
args: ["version"]
expectedOutput: ["go version go1.23.* linux/*"]
- name: "mingw64 gcc exists"
command: "which"
args: ["x86_64-w64-mingw32-gcc"]
expectedOutput: ["/usr/bin/x86_64-w64-mingw32-gcc"]
- name: "perl exists"
command: "which"
args: ["perl"]
expectedOutput: ["/usr/bin/perl"]
fileExistenceTests:
- name: 'profiles'
path: '/empire/empire/server/data/profiles/'
Expand All @@ -57,3 +65,9 @@ fileExistenceTests:
- name: 'plugin-registries'
path: '/root/.local/share/empire/plugin-registries/BC-SECURITY/main/registry.yaml'
shouldExist: true
- name: 'openssl mingw prefix'
path: '/opt/openssl-mingw64/'
shouldExist: true
- name: 'openssl mingw headers'
path: '/opt/openssl-mingw64/include/openssl/ssl.h'
shouldExist: true
17 changes: 16 additions & 1 deletion .github/install_tests/cst-config-install-base.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ commandTests:
- name: "poetry python version"
command: "poetry"
args: ["run", "python3", "--version"]
expectedOutput: ["Python 3.13.*"]
expectedOutput: ["Python 3.14.*"]
# powershell
- name: "powershell which"
command: "which"
Expand Down Expand Up @@ -54,6 +54,15 @@ commandTests:
command: "which"
args: ["mono"]
expectedOutput: ["/usr/bin/mono"]
# mingw / cross-compile deps
- name: "mingw64 gcc exists"
command: "which"
args: ["x86_64-w64-mingw32-gcc"]
expectedOutput: ["/usr/bin/x86_64-w64-mingw32-gcc"]
- name: "perl exists"
command: "which"
args: ["perl"]
expectedOutput: ["/usr/bin/perl"]
# run
- name: "ps-empire help"
command: "./ps-empire"
Expand All @@ -76,3 +85,9 @@ fileExistenceTests:
- name: 'plugin-registries'
path: '/home/empire/.local/share/empire/plugin-registries/BC-SECURITY/main/registry.yaml'
shouldExist: true
- name: 'openssl mingw prefix'
path: '/opt/openssl-mingw64/'
shouldExist: true
- name: 'openssl mingw headers'
path: '/opt/openssl-mingw64/include/openssl/ssl.h'
shouldExist: true
1 change: 1 addition & 0 deletions .github/workflows/dockerimage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,4 @@ jobs:
platforms: linux/amd64,linux/arm64
push: true
tags: bcsecurity/empire:${{ steps.tag-step.outputs.RELEASE_TAG }}
build-args: COMMIT_SHA=${{ github.sha }}
24 changes: 15 additions & 9 deletions .github/workflows/lint-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ jobs:
if: ${{ startsWith(github.head_ref, 'release/') || contains( github.event.pull_request.labels.*.name,
'run-all-versions') }}
run: |
echo "config={\"python-version\": [\"3.13\"]}" >> $GITHUB_OUTPUT
echo "config={\"python-version\": [\"3.13\", \"3.14\"]}" >> $GITHUB_OUTPUT
- id: not-release
if: ${{ !startsWith(github.head_ref, 'release/') }}
run: |
echo "config={\"python-version\": [\"3.13\"]}" >> $GITHUB_OUTPUT
echo "config={\"python-version\": [\"3.14\"]}" >> $GITHUB_OUTPUT
outputs:
config: ${{ steps.release.outputs.config || steps.not-release.outputs.config
}}
Expand Down Expand Up @@ -70,11 +70,12 @@ jobs:
run: |
poetry env use ${{ matrix.python-version }}
poetry install
sudo apt install -y mono-runtime
sudo apt-get update
sudo apt-get install -y mono-runtime mingw-w64 perl
- name: Run test suite - mysql
run: |
set -o pipefail
if [ "${{ matrix.python-version }}" = "3.13" ]; then
if [ "${{ matrix.python-version }}" = "3.14" ]; then
DATABASE_USE=mysql poetry run pytest -v --runslow --cov=empire/server --junitxml=pytest.xml --cov-report=term-missing:skip-covered . | tee pytest-coverage.txt
else
DATABASE_USE=mysql poetry run pytest -v --runslow .
Expand All @@ -85,7 +86,7 @@ jobs:
run: |
DATABASE_USE=sqlite poetry run pytest . -v --runslow
- name: Pytest coverage comment
if: ${{ matrix.python-version == '3.13' }}
if: ${{ matrix.python-version == '3.14' }}
uses: MishaKav/pytest-coverage-comment@v1.2.0
with:
pytest-coverage-path: ./pytest-coverage.txt
Expand Down Expand Up @@ -125,15 +126,20 @@ jobs:
config: .github/cst-config-docker.yaml
test_install_script:
needs: test
timeout-minutes: 30
timeout-minutes: 45
runs-on: ubuntu-latest
name: Test Install Script
strategy:
matrix:
# Because the box runs out of disk space, we can't run all tests on a single docker compose build.
# Each image builds individually to avoid running out of disk space.
# The OpenSSL cross-compilation for MinGW makes each image too large
# to build multiple in parallel on a single runner.
images:
- ['debian11', 'debian12', 'debian13']
- ['ubuntu2204', 'ubuntu2404']
- ['debian11']
- ['debian12']
- ['debian13']
- ['ubuntu2204']
- ['ubuntu2404']
- ['kalirolling'] # 'parrotrolling'
# Parrot disabled for now because the apt repo is having some slowness issues.
# Install is running up way too many minutes.
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release-private-start.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ jobs:
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: '3.12'
python-version: '3.14'
- name: Setup poetry
run: |
curl -sL https://install.python-poetry.org | python - -y
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release-private-tag.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: '3.12'
python-version: '3.14'
- name: Setup poetry
run: |
curl -sL https://install.python-poetry.org | python - -y
Expand Down
2 changes: 1 addition & 1 deletion .python-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.13
3.14
79 changes: 78 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,88 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [6.5.0] - 2026-03-08
- Updated Starkiller to v3.4.0

### Added

- Log Empire version and git commit SHA at startup for easier production diagnostics; commit SHA is baked into the Docker image at build time via `--build-arg`
- Added C stager for lightweight stage0 shellcode injection via Fibers
- Added `shellcode_compiler` utility for compiling position-independent C stagers into raw x64 shellcode for BOF process injection
- Added `clipboard_window_inject_list` BOF module for enumerating processes with clipboard window class
- Added PIC shellcode C template and linker script for MinGW-based shellcode compilation
- Added unit tests for `shellcode_compiler` and rewrote `test_bof_packer` to cover the new `Packer` class API
- Added a runtime `Background` option to C# modules, allowing operators to override background/foreground execution at task time
- Added C# PatchETW module for in-process ETW patching via ntdll!EtwEventWrite
- Added C# PatchlessAMSI module for patchless AMSI bypass using hardware breakpoints and vectored exception handling
- Added PowerShell Invoke-VSSExtract module for NTDS.dit and SYSTEM hive extraction via Volume Shadow Copy
- Added PowerShell Invoke-RDPHijack module for RDP session hijacking via tscon.exe
- Added Python linux_keyring module for credential extraction from the Linux kernel keyring subsystem
- Added Python aws_imds module for AWS IAM role credential theft via EC2 Instance Metadata Service
- Added BOF `spawn` module for EarlyBird process hollowing with suspended process creation, shellcode injection, and APC thread hijacking

### Changed

- Added Python 3.14 support (supports 3.13 and 3.14); Dockerfile now uses `python:3.14.3-trixie`
- Replace `os.path` with `pathlib` in core code and enforce `PTH` lint rule for all core files
- Switch `stager_generation_service` from deprecated `installPath` (str) to `install_path` (Path)
- Optimized test suite for faster CI and local runs
- Modernize Python patterns in core code: use `setdefault()`, truthiness checks, `click.style()` for terminal colors, and remove redundant operations
- Reduced test fixture boilerplate with a shared `make_agent()` factory and deduplicated `plugin_task` fixture across test files
- Removed `autouse` from test fixtures that don't need it, making test dependencies explicit
- Added unit tests for encryption, packet handling, helpers, malleable transformations, and listener utilities
- Migrate remaining `installPath` usages to `install_path` (Path) in core services
- Use `Path.read_text(encoding="utf-8")` instead of `read_bytes().decode()` in stager generation
- Replace `os.system()` calls with `subprocess.run()` in stager JAR generation
- Upgraded all Python dependencies to latest versions (Feb 2026)
- Replace deprecated `handle_error_message` with raised `ModuleValidationException` in all modules (#716)
- Convert 51 modules to use `@auto_get_source` and `@auto_finalize` decorators, eliminating boilerplate (#716)
- Replace unmaintained `terminaltables` dependency with `prettytable` (#809)
- Refactored `bof_packer` from standalone functions to a `Packer` class with granular packing methods (`addbytes`, `addstr`, `addWstr`, `addbool`, `adduint32`, `addint`, `addshort`)
- Rewrote `clipboard_window_inject` BOF module to use PIC shellcode instead of PowerShell launcher-based shellcode generation
- Simplified `clipboard_window_inject` module options by removing unnecessary launcher parameters and corrected BOF format string
- Bumped Empire Compiler from v0.4.3 to v0.4.4

### Removed

- Removed `secinject` BOF module and its pre-compiled binary

### Fixed

- Fixed SQLAlchemy connection pool exhaustion caused by async hooks receiving the caller's committed session. `run_hooks` now wraps async hooks in `_run_async_hook`, which opens a fresh `SessionLocal` session for each hook and closes it cleanly after the hook returns. ORM objects are re-attached via `session.merge()` so lazy-loaded relationships resolve correctly.
- Fixed SQLAlchemy connection pool exhaustion during agent check-ins by releasing the DB session before expensive file I/O, encryption, and packet building in `handle_agent_request()`
- Fixed custom-generate BOF modules (`clipboard_window_inject`, `spawn`, `clipboard_window_inject_list`) returning .NET-only `file|,json` format for Go agents, causing BOF execution to fail on the Go agent's COFF loader
- Added `format_bof_output()` to `ModuleService` to centralize BOF output formatting for Go and .NET agents
- Pass `agent_language` to custom-generate modules so they can produce agent-appropriate output
- Fixed malleable HTTP listener stagers failing after server restart due to random URI regeneration in `Stager._defaults()`
- Fix null-safety bug in `_process_agent_packet` when `save_module_file` returns None on skywalker exploit detection
- Fixed stop-job handlers in PowerShell and Python agents crashing when the target job doesn't exist
- Fixed the `docs/quickstart/installation/README.md` file to specify a previously missing reference to Ubuntu
- Fixed 9 malformed MITRE ATT&CK technique IDs across PowerShell, Python, and C# modules
- Fixed 2 malformed tactic fields that used space-separated strings instead of YAML lists
- Replaced 7 deprecated or revoked ATT&CK techniques with current equivalents
- Added missing `software` field for known ATT&CK tools (Rubeus, BloodHound, Mimikatz)
- Added missing `tactics` field to 82 Python modules that had none
- Fixed 74 technique-to-tactic inconsistencies across all module languages
- Replaced 27 additional deprecated technique IDs predating ATT&CK v10 with current equivalents across Python and template modules
- Removed incorrect T1482 (Domain Trust Discovery) from 32 modules that perform user, group, or computer enumeration
- Removed incorrect T1615 (Group Policy Discovery) from 24 modules unrelated to GPO enumeration
- Replaced T1106 (Native API) with T1059.006 (Python) on 5 DCOS REST API modules
- Added missing `techniques` field to 3 session enumeration modules
- Corrected 3 macOS LaunchAgent persistence modules from T1055 (Process Injection) to T1543.001 (Launch Agent)
- Corrected macOS screensaver credential prompt module from T1113 (Screen Capture) to T1056.002 (GUI Input Capture)
- Corrected Invoke-DownloadFile from T1041 (Exfiltration Over C2) to T1105 (Ingress Tool Transfer)
- Upgraded 3 keylogger modules from parent T1056 to specific T1056.001 (Keylogging) sub-technique
- Upgraded macOS email search module from T1114 to T1114.001 (Local Email Collection) sub-technique
- Upgraded macOS LoginHook persistence from T1037 to T1037.002 (Login Hook) sub-technique
- Added T1105 (Ingress Tool Transfer) to 12 lateral movement modules that deploy stagers to remote hosts
- Added 10 new ATT&CK technique IDs across 51 modules to improve coverage from 181 to 190 unique techniques
- Added T1005 (Data from Local System) to 8 macOS and Linux credential and collection modules
- Added T1550.002 (Pass the Hash) to PsExec, SMBExec, and WMI lateral movement modules
- Added T1562.001 (Impair Defenses) to AMSI bypass, ETW patching, and Outlook security modules
- Fixed duplicate technique entries in RevertToSelf and NetRipper modules
- Fixed PSRansom module `name` field incorrectly set to `Invoke-Script` instead of `PSRansom`
- Fixed misc_skeleton_key Invoke-Mimikatz call

## [6.4.1] - 2026-02-15
- Fixed the `docs/quickstart/installation/README.md` file to specify a previously missing reference to Ubuntu
Expand Down Expand Up @@ -1256,7 +1331,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/v6.4.1...HEAD
[Unreleased]: https://github.com/BC-SECURITY/Empire-Sponsors/compare/v6.5.0...HEAD

[6.5.0]: https://github.com/BC-SECURITY/Empire-Sponsors/compare/v6.4.1...v6.5.0

[6.4.1]: https://github.com/BC-SECURITY/Empire-Sponsors/compare/v6.4.0...v6.4.1

Expand Down
27 changes: 26 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,29 +9,36 @@
# 2) create volume storage: `docker create -v /empire --name data bcsecurity/empire`
# 3) run out container: `docker run -it --volumes-from data bcsecurity/empire /bin/bash`

FROM python:3.13.6-bullseye
FROM python:3.14.3-trixie

LABEL maintainer="bc-security"
LABEL description="Dockerfile for Empire. https://bc-security.gitbook.io/empire-wiki/quickstart/installation#docker"

ENV DEBIAN_FRONTEND=noninteractive DOTNET_CLI_TELEMETRY_OPTOUT=1

ARG TARGETARCH
ARG COMMIT_SHA=""
ENV EMPIRE_COMMIT_SHA=$COMMIT_SHA

SHELL ["/bin/bash", "-c"]

RUN apt-get update && \
apt-get install -qq \
--no-install-recommends \
apt-transport-https \
ca-certificates \
libicu-dev \
sudo \
zip \
curl \
wget \
git \
openssh-client \
default-jdk \
mono-runtime \
build-essential \
mingw-w64 \
perl \
&& rm -rf /var/lib/apt/lists/*

RUN if [ "$TARGETARCH" = "amd64" ]; then \
Expand All @@ -56,6 +63,24 @@ RUN curl -L -o /tmp/go.tar.gz https://go.dev/dl/go1.23.2.linux-${TARGETARCH}.tar
ln -s /opt/go/bin/go /usr/bin/go && \
rm /tmp/go.tar.gz

# Build OpenSSL for MinGW cross-compilation (for Windows C stagers)
RUN set -eux; \
if [ ! -d /opt/openssl-mingw64/include/openssl ]; then \
OPENSSL_VERSION="3.5.4"; \
mkdir -p /tmp/openssl-build; \
cd /tmp/openssl-build; \
wget -q "https://www.openssl.org/source/openssl-${OPENSSL_VERSION}.tar.gz" -O "openssl-${OPENSSL_VERSION}.tar.gz"; \
tar -xzf "openssl-${OPENSSL_VERSION}.tar.gz"; \
cd "openssl-${OPENSSL_VERSION}"; \
./Configure mingw64 no-apps no-async no-docs no-shared no-tests \
--cross-compile-prefix=x86_64-w64-mingw32- \
--prefix=/opt/openssl-mingw64; \
make -j"$(nproc)"; \
make install_dev; \
cd /; \
rm -rf /tmp/openssl-build; \
fi

WORKDIR /empire

COPY pyproject.toml poetry.lock /empire/
Expand Down
17 changes: 15 additions & 2 deletions docs/modules/module-development/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ authors:
- name: John Doe
handle: '@johndoe'
description: A sample module demonstrating Empire module structure.
tactics: []
software: ''
tactics:
- TA0002
techniques:
- T1234
- T1059
background: true
output_extension: ps1
needs_admin: false
Expand All @@ -32,6 +34,17 @@ options:
strict: true
```

## MITRE ATT&CK Fields

Every module should include proper MITRE ATT&CK metadata. The fields are:

- **`tactics`**: A list of ATT&CK tactic IDs (e.g., `TA0001` through `TA0043`). Every module should have at least one tactic — do not leave this as an empty list.
- **`techniques`**: A list of ATT&CK technique or sub-technique IDs. Use the format `T####` for techniques (e.g., `T1059`) or `T####.###` for sub-techniques (e.g., `T1059.001`).
- **`software`**: If the module wraps a known ATT&CK software entry, set this to its ID (e.g., `S0002` for Mimikatz, `S1071` for Rubeus). Leave as `''` if the tool is not cataloged in ATT&CK.

Refer to the [MITRE ATT&CK Enterprise Matrix](https://attack.mitre.org/matrices/enterprise/) for valid tactic, technique, and software IDs.


## Special Options

Empire reserves certain option names that receive special handling during module execution. These are filtered out of the parameters passed to the module's script and instead control how the task is dispatched or processed.
Expand Down
16 changes: 9 additions & 7 deletions empire/server/api/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,20 @@ def __init__(
allow_headers: typing.Sequence[str] = (),
allow_credentials: bool = False,
allow_origin_regex: str | None = None,
allow_private_network: bool = False,
expose_headers: typing.Sequence[str] = (),
max_age: int = 600,
) -> None:
super().__init__(
app,
allow_origins,
allow_methods,
allow_headers,
allow_credentials,
allow_origin_regex,
expose_headers,
max_age,
allow_origins=allow_origins,
allow_methods=allow_methods,
allow_headers=allow_headers,
allow_credentials=allow_credentials,
allow_origin_regex=allow_origin_regex,
allow_private_network=allow_private_network,
expose_headers=expose_headers,
max_age=max_age,
)

async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
Expand Down
Loading