diff --git a/docs/_docs/user-guide/imix.md b/docs/_docs/user-guide/imix.md index b06b63e66..e0942ebe0 100644 --- a/docs/_docs/user-guide/imix.md +++ b/docs/_docs/user-guide/imix.md @@ -18,6 +18,7 @@ Imix has compile-time configuration, that may be specified using environment var | IMIX_CALLBACK_URI | URI for initial callbacks (must specify a scheme, e.g. `http://`) | `http://127.0.0.1:80` | No | | IMIX_CALLBACK_INTERVAL | Duration between callbacks, in seconds. | `5` | No | | IMIX_RETRY_INTERVAL | Duration to wait before restarting the agent loop if an error occurs, in seconds. | `5` | No | +| IMIX_PROXY_URI | Overide system settings for proxy URI over HTTP(S) (must specify a scheme, e.g. `https://`) | No proxy | No | ## Logging @@ -43,6 +44,16 @@ See the [Eldritch User Guide](/user-guide/eldritch) for more information. Imix can execute up to 127 threads concurrently after that the main imix thread will block behind other threads. Every callback interval imix will query each active thread for new output and rely that back to the c2. This means even long running tasks will report their status as new data comes in. +## Proxy support + +Imix's default `grpc` transport supports http and https proxies for outbound communication. +By default imix will try to determine the systems proxy settings: + +- On Linux reading the environment variables `http_proxy` and then `https_proxy` +- On Windows - we cannot automatically determine the default proxy +- On MacOS - we cannot automatically determine the default proxy +- On FreeBSD - we cannot automatically determine the default proxy + ## Static cross compilation ### Linux diff --git a/implants/imix/src/agent.rs b/implants/imix/src/agent.rs index 6f779903e..7fb7f0081 100644 --- a/implants/imix/src/agent.rs +++ b/implants/imix/src/agent.rs @@ -77,7 +77,7 @@ impl Agent { * Callback once using the configured client to claim new tasks and report available output. */ pub async fn callback(&mut self) -> Result<()> { - let transport = GRPC::new(self.cfg.callback_uri.clone())?; + let transport = GRPC::new(self.cfg.callback_uri.clone(), self.cfg.proxy_uri.clone())?; self.claim_tasks(transport.clone()).await?; self.report(transport.clone()).await?; diff --git a/implants/imix/src/config.rs b/implants/imix/src/config.rs index ae9f3c299..92a0001f9 100644 --- a/implants/imix/src/config.rs +++ b/implants/imix/src/config.rs @@ -15,6 +15,17 @@ macro_rules! callback_uri { } }; } + +/* + * Compile-time constant for the agent proxy URI, derived from the IMIX_PROXY_URI environment variable during compilation. + * Defaults to None if this is unset. + */ +macro_rules! proxy_uri { + () => { + option_env!("IMIX_PROXY_URI") + }; +} + /* * Compile-time constant for the agent callback URI, derived from the IMIX_CALLBACK_URI environment variable during compilation. * Defaults to "http://127.0.0.1:80/grpc" if this is unset. @@ -54,6 +65,7 @@ pub const RETRY_INTERVAL: &str = retry_interval!(); pub struct Config { pub info: pb::c2::Beacon, pub callback_uri: String, + pub proxy_uri: Option, pub retry_interval: u64, } @@ -92,6 +104,7 @@ impl Default for Config { Config { info, callback_uri: String::from(CALLBACK_URI), + proxy_uri: get_system_proxy(), retry_interval: match RETRY_INTERVAL.parse::() { Ok(i) => i, Err(_err) => { @@ -100,13 +113,52 @@ impl Default for Config { "failed to parse retry interval constant, defaulting to 5 seconds: {_err}" ); - 5_u64 + 5 } }, } } } +fn get_system_proxy() -> Option { + let proxy_uri_compile_time_override = proxy_uri!(); + if let Some(proxy_uri) = proxy_uri_compile_time_override { + return Some(proxy_uri.to_string()); + } + + #[cfg(target_os = "linux")] + { + match std::env::var("http_proxy") { + Ok(val) => return Some(val), + Err(_e) => { + #[cfg(debug_assertions)] + log::debug!("Didn't find http_proxy env var: {}", _e); + } + } + + match std::env::var("https_proxy") { + Ok(val) => return Some(val), + Err(_e) => { + #[cfg(debug_assertions)] + log::debug!("Didn't find https_proxy env var: {}", _e); + } + } + None + } + #[cfg(target_os = "windows")] + { + None + } + #[cfg(target_os = "macos")] + { + None + } + #[cfg(target_os = "freebsd")] + { + None + } +} + impl Config { pub fn refresh_primary_ip(&mut self) { let fresh_ip = get_primary_ip(); diff --git a/implants/lib/eldritch/src/file/list_impl.rs b/implants/lib/eldritch/src/file/list_impl.rs index 901dd0b3e..9076aa6b4 100644 --- a/implants/lib/eldritch/src/file/list_impl.rs +++ b/implants/lib/eldritch/src/file/list_impl.rs @@ -1,7 +1,7 @@ use super::super::insert_dict_kv; use super::{File, FileType}; use anyhow::{Context, Result}; -use chrono::{DateTime, NaiveDateTime, Utc}; +use chrono::DateTime; use glob::glob; use starlark::{ collections::SmallMap, @@ -98,7 +98,7 @@ fn create_file_from_pathbuf(path_entry: PathBuf) -> Result { } }; - let naive_datetime = match NaiveDateTime::from_timestamp_opt(timestamp, 0) { + let naive_datetime = match DateTime::from_timestamp(timestamp, 0) { Some(local_naive_datetime) => local_naive_datetime, None => { return Err(anyhow::anyhow!( @@ -107,7 +107,6 @@ fn create_file_from_pathbuf(path_entry: PathBuf) -> Result { )) } }; - let time_modified: DateTime = DateTime::from_naive_utc_and_offset(naive_datetime, Utc); Ok(File { name: file_name, @@ -117,7 +116,7 @@ fn create_file_from_pathbuf(path_entry: PathBuf) -> Result { owner: owner_username, group: group_id.to_string(), permissions: permissions.to_string(), - time_modified: time_modified.to_string(), + time_modified: naive_datetime.to_string(), }) } @@ -247,7 +246,6 @@ mod tests { runtime.finish().await; // Read Messages - let expected_output = format!("{}\n", path); let mut found = false; for msg in runtime.messages() { if let Message::ReportText(m) = msg { @@ -330,7 +328,7 @@ for f in file.list(input_params['path']): .join(file); std::fs::File::create(test_file)?; - // Run Eldritch (until finished) + // Run Eldritch (until finished) /tmp/.tmpabc123/down the/rabbit hole/win let mut runtime = crate::start( 123, Tome { @@ -343,6 +341,7 @@ for f in file.list(input_params['path']): String::from("path"), test_dir .path() + .join(expected_dir) .join("*") .join("win") .to_str() @@ -355,25 +354,16 @@ for f in file.list(input_params['path']): .await; runtime.finish().await; - let expected_output = format!( - "{}\n", - test_dir - .path() - .join(expected_dir) - .join(nested_dir) - .join(file) - .to_str() - .unwrap() - ); let mut found = false; for msg in runtime.messages() { if let Message::ReportText(m) = msg { assert_eq!(123, m.id); - assert_eq!(expected_output, m.text); + assert!(m.text.contains(file)); log::debug!("text: {:?}", m.text); found = true; } } + assert!(found); Ok(()) } diff --git a/implants/lib/transport/Cargo.toml b/implants/lib/transport/Cargo.toml index 4db51f8d8..c64ecf311 100644 --- a/implants/lib/transport/Cargo.toml +++ b/implants/lib/transport/Cargo.toml @@ -13,12 +13,16 @@ pb = { workspace = true } anyhow = { workspace = true } log = { workspace = true } -prost = { workspace = true} +prost = { workspace = true } prost-types = { workspace = true } tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } tokio-stream = { workspace = true } -tonic = { workspace = true, features = ["tls-roots"] } +tonic = { workspace = true, features = ["tls-webpki-roots"] } trait-variant = { workspace = true } +hyper = { version = "0.14", features = [ + "client", +] } # Had to user an older version of hyper to support hyper-proxy +hyper-proxy = "0.9.1" # [feature = mock] -mockall = {workspace = true, optional = true } +mockall = { workspace = true, optional = true } diff --git a/implants/lib/transport/src/grpc.rs b/implants/lib/transport/src/grpc.rs index 577b298de..78fa2b681 100644 --- a/implants/lib/transport/src/grpc.rs +++ b/implants/lib/transport/src/grpc.rs @@ -1,6 +1,8 @@ use crate::Transport; use anyhow::Result; +use hyper::Uri; use pb::c2::*; +use std::str::FromStr; use std::sync::mpsc::{Receiver, Sender}; use tonic::codec::ProstCodec; use tonic::GrpcMethod; @@ -21,12 +23,30 @@ pub struct GRPC { } impl Transport for GRPC { - fn new(callback: String) -> Result { + fn new(callback: String, proxy_uri: Option) -> Result { let endpoint = tonic::transport::Endpoint::from_shared(callback)?; - let channel = endpoint - .rate_limit(1, Duration::from_millis(25)) - .connect_lazy(); + let mut http = hyper::client::HttpConnector::new(); + http.enforce_http(false); + http.set_nodelay(true); + + let channel = match proxy_uri { + Some(proxy_uri_string) => { + let proxy: hyper_proxy::Proxy = hyper_proxy::Proxy::new( + hyper_proxy::Intercept::All, + Uri::from_str(proxy_uri_string.as_str())?, + ); + let mut proxy_connector = hyper_proxy::ProxyConnector::from_proxy(http, proxy)?; + proxy_connector.set_tls(None); + + endpoint + .rate_limit(1, Duration::from_millis(25)) + .connect_with_connector_lazy(proxy_connector) + } + None => endpoint + .rate_limit(1, Duration::from_millis(25)) + .connect_lazy(), + }; let grpc = tonic::client::Grpc::new(channel); Ok(Self { grpc }) diff --git a/implants/lib/transport/src/mock.rs b/implants/lib/transport/src/mock.rs index b429178d6..5f3624ff1 100644 --- a/implants/lib/transport/src/mock.rs +++ b/implants/lib/transport/src/mock.rs @@ -10,7 +10,7 @@ mock! { fn clone(&self) -> Self; } impl super::Transport for Transport { - fn new(uri: String) -> Result; + fn new(uri: String, proxy_uri: Option) -> Result; async fn claim_tasks(&mut self, request: ClaimTasksRequest) -> Result; diff --git a/implants/lib/transport/src/transport.rs b/implants/lib/transport/src/transport.rs index 32577f31f..8271e6402 100644 --- a/implants/lib/transport/src/transport.rs +++ b/implants/lib/transport/src/transport.rs @@ -5,7 +5,7 @@ use std::sync::mpsc::{Receiver, Sender}; #[trait_variant::make(Transport: Send)] pub trait UnsafeTransport: Clone + Send { // New will initialize a new instance of the transport using the provided URI. - fn new(uri: String) -> Result; + fn new(uri: String, proxy_uri: Option) -> Result; /// /// Contact the server for new tasks to execute.