Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,12 +97,13 @@ The `docs/_docs/` folder contains three guides:

## Eldritch Standard Library

The Eldritch DSL provides 12 modules:
The Eldritch DSL provides 13 modules:

| Module | Purpose |
|-----------|--------------------------------------|
| `agent` | Agent metadata and control |
| `assets` | Embedded file access |
| `chain` | Multi-agent chaining |
| `crypto` | Encryption, decryption, hashing |
| `file` | File system operations |
| `http` | HTTP/HTTPS requests |
Expand Down
31 changes: 31 additions & 0 deletions docs/_docs/user-guide/eldritch.md
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,37 @@ The **assets.read** method returns a UTF-8 string representation of the asset fi

---

## Chain

The `chain` library enables multi-agent chaining by allowing one agent (Agent A) to proxy C2 traffic for another agent (Agent B). This is useful for establishing communication through intermediary agents in restricted networks.

### chain.tcp

`chain.tcp(addr: str) -> int`

The **chain.tcp** method establishes a chain proxy over TCP, allowing Agent A to forward C2 messages to/from Agent B. Agent A connects to Agent B's bind TCP transport listener at the specified address and proxies gRPC traffic over HTTP/2.

**Parameters:**
- `addr`: The address and port where Agent B is listening for chain connections (e.g., `"192.168.1.100:8443"`)

**Returns:**
- `0` on successful initialization (the proxy runs asynchronously in the background)

**Example:**

```python
# Agent A connects to Agent B's bind TCP listener and starts proxying traffic
chain.tcp("192.168.1.100:8443")

# Now Agent B's C2 messages flow through Agent A to Tavern
```

**Usage Pattern:**

Agent A must have one of the standard transports (grpc, http1, dns) configured for its upstream connection to Tavern. Agent B is configured with a TCP bind transport to accept connections from Agent A on a TCP port.

---

## Crypto

The `crypto` library offers functionalities to encrypt, decrypt, and hash data. It includes support for algorithms like AES, MD5, SHA1, and SHA256, as well as helpers for base64 encoding and JSON parsing.
Expand Down
31 changes: 31 additions & 0 deletions docs/_docs/user-guide/imix.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,37 @@ This transport doesn't support eldritch functions that require bi-directional st

*Note*: TXT records provide the best performance.

### tcp_bind

The TCP Bind transport inverts the traditional C2 communication model: instead of the agent connecting outbound to the server, the agent binds to a local TCP port and waits for an upstream agent (or redirector) to connect inward.

**Use Cases:**
- Agent chaining: An upstream agent (Agent A) connects to a downstream agent's (Agent B) TCP bind port to proxy its C2 traffic
- Network egress restrictions: When agents can't initiate outbound connections but can receive inbound connections
- Multi-stage deployments: Establishing secure communication tunnels between agent stages

**Configuration:**

```yaml
transports:
- type: tcp_bind
uri: tcp://0.0.0.0:8443 # Bind address and port
```

**Parameters:**
- `uri`: The local address and port to bind on (e.g., `tcp://0.0.0.0:8443`). Use `0.0.0.0` to listen on all interfaces, or specify a specific IP for local-only binding.

**Important Notes:**

- **Inverted Nature**: The agent binds and listens; upstream agents or redirectors must initiate the connection. This reverses the typical agent-to-server model.
- **Secure Channel**: TCP Bind is treated as a trusted local channel. Messages are sent as plain protobuf over the TCP connection; encryption (ChaCha20) is applied by the upstream agent when forwarding to Tavern.
- **Agent Chaining**: Use with `chain.tcp()` in Eldritch to have one agent proxy traffic for another. For example:
- Agent B binds on `tcp://0.0.0.0:8443` with TCP Bind transport
- Agent A uses Eldritch to call `chain.tcp("192.168.1.100:8443")` to connect to Agent B
- Agent A proxies all of Agent B's C2 traffic upstream to Tavern
- **Connection Persistence**: The TCP connection is maintained and reused across multiple C2 cycles. If the connection drops, a new upstream connection must be initiated.
- **Not Suitable for Wide-Area Networks**: This transport is designed for local or trusted network chaining. For remote communication, use standard grpc, http1, or dns transports.

## Logging

At runtime, you may use the `IMIX_LOG` environment variable to control log levels and verbosity. See [these docs](https://docs.rs/pretty_env_logger/latest/pretty_env_logger/) for more information. **When building a release version of imix, logging is disabled** and is not included in the released binary.
Expand Down
6 changes: 6 additions & 0 deletions implants/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ members = [
"lib/eldritch/eldritch",
"lib/eldritch/eldritch-wasm",
"lib/portals/portal-stream", "lib/eldritch/testutils/eldritch-mockagent",
"lib/eldritch/stdlib/eldritch-libchain",
]
exclude = [
"lib/eldritch/stdlib/tests", # Excluded to prevent fake_bindings from polluting workspace builds
Expand Down Expand Up @@ -59,6 +60,11 @@ eldritch-libsys = {path = "lib/eldritch/stdlib/eldritch-libsys",default-features
eldritch-libtime = {path = "lib/eldritch/stdlib/eldritch-libtime",default-features = false }
portal-stream = { path = "lib/portals/portal-stream" }

http-body-util = "0.1"
hyper-util = { version = "0.1", features = ["full"] }

eldritch-libchain = {path = "lib/eldritch/stdlib/eldritch-libchain",default-features = false }

aes = "0.8.3"
allocative = "0.3.2"
allocative_derive = "0.3.2"
Expand Down
2 changes: 1 addition & 1 deletion implants/golem/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,10 @@ pub struct ParsedTome {
// Build a new runtime
fn new_runtime(assetlib: impl ForeignValue + 'static) -> Interpreter {
// Maybe change the printer here?
let mut interp = Interpreter::new_with_printer(Arc::new(StdoutPrinter)).with_default_libs();
// Register the libraries that we need. Basically the same as interp.with_task_context but
// with our custom assets library
let agent = Arc::new(AgentFake {});
let mut interp = Interpreter::new_with_printer(Arc::new(StdoutPrinter)).with_default_libs();
let task_context = TaskContext {
task_id: 0,
jwt: String::new(),
Expand Down
4 changes: 3 additions & 1 deletion implants/imix/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ edition = "2024"
crate-type = ["cdylib"]

[features]
default = ["install", "grpc", "http1", "dns", "doh"]
default = ["install", "grpc", "http1", "dns", "doh", "tcp-bind"]
grpc = ["transport/grpc"]
http1 = ["transport/http1"]
dns = ["transport/dns"]
doh = ["transport/doh"]
tcp-bind = ["transport/tcp-bind"]
win_service = []
install = []
tokio-console = ["dep:console-subscriber", "tokio/tracing"]
Expand Down Expand Up @@ -43,6 +44,7 @@ rust-embed = { workspace = true }
console-subscriber = { workspace = true, optional = true }
rand = { workspace = true }
async-trait = { workspace = true }
eldritch-libchain = { workspace = true, features = ["stdlib"] }

[target.'cfg(target_os = "windows")'.dependencies]
windows-service = { workspace = true }
Expand Down
47 changes: 47 additions & 0 deletions implants/imix/src/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@ pub struct ImixAgent {
pub process_list_tx: std::sync::mpsc::SyncSender<c2::ReportProcessListRequest>,
pub process_list_rx: Arc<Mutex<std::sync::mpsc::Receiver<c2::ReportProcessListRequest>>>,
pub shell_manager_tx: tokio::sync::mpsc::Sender<ShellManagerMessage>,
pub pending_forwards: Arc<
tokio::sync::Mutex<
Vec<(
String,
tokio::sync::mpsc::Receiver<Vec<u8>>,
tokio::sync::mpsc::Sender<Vec<u8>>,
)>,
>,
>,
}

impl ImixAgent {
Expand All @@ -57,6 +66,7 @@ impl ImixAgent {
process_list_tx,
process_list_rx: Arc::new(Mutex::new(process_list_rx)),
shell_manager_tx,
pending_forwards: Arc::new(tokio::sync::Mutex::new(Vec::new())),
}
}

Expand Down Expand Up @@ -326,6 +336,31 @@ impl ImixAgent {
}

pub async fn process_job_request(&self) -> Result<()> {
// Dispatch any pending forward_raw requests before checking in.
// Each forward is spawned so the beacon cycle is not blocked by long-running
// streaming calls (e.g. ReportFile).
let pending: Vec<_> = {
let mut forwards = self.pending_forwards.lock().await;
forwards.drain(..).collect()
};
for (path, rx, tx) in pending {
let agent = self.clone();
self.runtime_handle.spawn(async move {
if let Ok(mut t) = agent.get_usable_transport().await {
if let Err(_e) = t.forward_raw(path.clone(), rx, tx).await {
#[cfg(debug_assertions)]
log::error!("Deferred forward_raw to {} failed: {}", path, _e);
}
} else {
#[cfg(debug_assertions)]
log::error!(
"Failed to get transport for deferred forward_raw to {}",
path
);
}
});
}

let resp = self.claim_tasks().await?;

let mut has_work = false;
Expand Down Expand Up @@ -415,6 +450,7 @@ impl ImixAgent {
}

// Implement the Eldritch Agent Trait
#[async_trait::async_trait]
impl Agent for ImixAgent {
fn fetch_asset(&self, req: c2::FetchAssetRequest) -> Result<Vec<u8>, String> {
self.with_transport(|mut t| async move {
Expand Down Expand Up @@ -502,6 +538,17 @@ impl Agent for ImixAgent {
self.with_transport(|mut t| async move { t.claim_tasks(req).await })
}

async fn forward_raw(
&self,
path: String,
rx: tokio::sync::mpsc::Receiver<Vec<u8>>,
tx: tokio::sync::mpsc::Sender<Vec<u8>>,
) -> Result<(), String> {
let mut forwards = self.pending_forwards.lock().await;
forwards.push((path, rx, tx));
Ok(())
}

fn get_config(&self) -> Result<BTreeMap<String, String>, String> {
let mut map = BTreeMap::new();
// Blocks on read, but it's fast
Expand Down
19 changes: 10 additions & 9 deletions implants/imix/src/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,15 +96,16 @@ async fn run_agent_cycle(agent: Arc<ImixAgent>, registry: Arc<TaskRegistry>) {
// Create new active transport
let config = agent.get_transport_config().await;

let transport = match transport::create_transport(config) {
Ok(t) => t,
Err(_e) => {
#[cfg(debug_assertions)]
log::error!("Failed to create transport: {_e:#}");
agent.rotate_callback_uri().await;
return;
}
};
let transport: Box<dyn transport::Transport + Send + Sync> =
match transport::create_transport(config) {
Ok(t) => t,
Err(_e) => {
#[cfg(debug_assertions)]
log::error!("Failed to create transport: {_e:#}");
agent.rotate_callback_uri().await;
return;
}
};

// Set transport
agent.update_transport(transport).await;
Expand Down
2 changes: 1 addition & 1 deletion implants/imix/src/shell/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ impl ShellManager {
let mut interpreter = Interpreter::new_with_printer(printer)
.with_default_libs()
.with_context(
agent.clone(),
agent.clone() as std::sync::Arc<dyn eldritch_agent::Agent>,
Context::ShellTask(shell_task_context),
Vec::new(),
backend,
Expand Down
7 changes: 6 additions & 1 deletion implants/imix/src/shell/repl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,12 @@ pub async fn run_repl_reverse_shell(
let backend = Arc::new(EmptyAssets {});
let mut interpreter = Interpreter::new_with_printer(printer)
.with_default_libs()
.with_context(Arc::new(agent), context.clone(), Vec::new(), backend); // Changed to with_context
.with_context(
Arc::new(agent.clone()) as Arc<dyn eldritch::agent::agent::Agent>,
context.clone(),
Vec::new(),
backend,
);

let mut repl = Repl::new();
let stdout = VtWriter {
Expand Down
2 changes: 1 addition & 1 deletion implants/imix/src/task.rs
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ fn setup_interpreter(
// Support embedded assets behind remote asset filenames
let backend = Arc::new(EmbeddedAssets::<crate::assets::Asset>::new());
// Register Task Context (Agent, Report, Assets)
interp = interp.with_context(agent, context, remote_assets, backend);
interp = interp.with_context(agent.clone(), context, remote_assets, backend);

// Inject input_params
let params_map: BTreeMap<String, String> = tome
Expand Down
9 changes: 9 additions & 0 deletions implants/imix/src/tests/report_large_file_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,15 @@ impl Transport for FakeTransport {
Ok(())
}

async fn forward_raw(
&mut self,
_path: String,
_rx: tokio::sync::mpsc::Receiver<Vec<u8>>,
_tx: tokio::sync::mpsc::Sender<Vec<u8>>,
) -> anyhow::Result<()> {
Ok(())
}

fn get_type(&mut self) -> pb::c2::transport::Type {
pb::c2::transport::Type::TransportUnspecified
}
Expand Down
12 changes: 11 additions & 1 deletion implants/imix/src/tests/task_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ impl MockAgent {
}
}

#[async_trait::async_trait]
impl Agent for MockAgent {
fn fetch_asset(&self, _req: c2::FetchAssetRequest) -> Result<Vec<u8>, String> {
Ok(vec![])
Expand Down Expand Up @@ -108,7 +109,16 @@ impl Agent for MockAgent {
fn add_callback_uri(&self, _uri: String) -> std::result::Result<(), String> {
Ok(())
}
fn remove_callback_uri(&self, _uri: String) -> std::result::Result<(), String> {
fn remove_callback_uri(&self, _uri: String) -> Result<(), String> {
Ok(())
}

async fn forward_raw(
&self,
_path: String,
_rx: tokio::sync::mpsc::Receiver<Vec<u8>>,
_tx: tokio::sync::mpsc::Sender<Vec<u8>>,
) -> Result<(), String> {
Ok(())
}
}
Expand Down
2 changes: 2 additions & 0 deletions implants/lib/eldritch/eldritch-agent/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ edition = "2021"

[dependencies]
pb = { workspace = true }
tokio = { workspace = true, features = ["sync"] }
async-trait = { workspace = true }
9 changes: 9 additions & 0 deletions implants/lib/eldritch/eldritch-agent/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub enum Context {
ShellTask(ShellTaskContext),
}

#[async_trait::async_trait]
pub trait Agent: Send + Sync {
// Interactivity
fn fetch_asset(&self, req: c2::FetchAssetRequest) -> Result<Vec<u8>, String>;
Expand Down Expand Up @@ -53,4 +54,12 @@ pub trait Agent: Send + Sync {
// Task Management
fn list_tasks(&self) -> Result<Vec<c2::Task>, String>;
fn stop_task(&self, task_id: i64) -> Result<(), String>;

// Chained transport forwarding
async fn forward_raw(
&self,
path: String,
rx: tokio::sync::mpsc::Receiver<Vec<u8>>,
tx: tokio::sync::mpsc::Sender<Vec<u8>>,
) -> Result<(), String>;
}
2 changes: 2 additions & 0 deletions implants/lib/eldritch/eldritch/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ eldritch-libregex = { workspace = true, default-features = false }
eldritch-libreport = { workspace = true, default-features = false }
eldritch-libsys = { workspace = true, default-features = false }
eldritch-libtime = { workspace = true, default-features = false }
eldritch-libchain = { workspace = true, default-features = false }
pb = { workspace = true, optional = true }
eldritch-repl = { workspace = true, default-features = false }

Expand Down Expand Up @@ -70,6 +71,7 @@ stdlib = [
"eldritch-libreport/stdlib",
"eldritch-libsys/stdlib",
"eldritch-libtime/stdlib",
"eldritch-libchain/stdlib",
]

[dev-dependencies]
Expand Down
2 changes: 1 addition & 1 deletion implants/lib/eldritch/eldritch/src/bindings_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ fn create_interp() -> Interpreter {
}
#[cfg(not(feature = "stdlib"))]
{
Interpreter::new().with_default_libs()
Interpreter::new()
}
}

Expand Down
Loading
Loading