diff --git a/implants/imix/Cargo.toml b/implants/imix/Cargo.toml index 6c34718ad..09896bb3a 100644 --- a/implants/imix/Cargo.toml +++ b/implants/imix/Cargo.toml @@ -43,6 +43,11 @@ rust-embed = { workspace = true } console-subscriber = { workspace = true, optional = true } rand = { workspace = true } async-trait = { workspace = true } +host_unique = { workspace = true } +url = { workspace = true } +uuid = { workspace = true, features = ["v4"] } +whoami = { workspace = true } +netdev = { workspace = true } [target.'cfg(target_os = "windows")'.dependencies] windows-service = { workspace = true } @@ -50,5 +55,10 @@ windows-service = { workspace = true } [target.'cfg(target_os = "windows")'.build-dependencies] static_vcruntime = { workspace = true } + + + + + [dev-dependencies] transport = { workspace = true, features = ["mock", "grpc"] } diff --git a/implants/imix/src/agent.rs b/implants/imix/src/agent.rs index 81d04c1ca..84da7e885 100644 --- a/implants/imix/src/agent.rs +++ b/implants/imix/src/agent.rs @@ -1,3 +1,4 @@ +use crate::config::Config; use anyhow::{Context as AnyhowContext, Result}; use eldritch::agent::agent::Agent; use eldritch_agent::Context; @@ -8,7 +9,6 @@ use pb::c2::{ ReportTaskOutputMessage, ShellTaskContext, ShellTaskOutput, TaskContext, TaskOutput, report_output_request, }; -use pb::config::Config; use std::collections::{BTreeMap, BTreeSet}; use std::sync::{Arc, Mutex}; use std::time::Duration; @@ -277,8 +277,10 @@ impl ImixAgent { // 2. Create new transport from config let config = self.get_transport_config().await; - let t = - transport::create_transport(config).context("Failed to create on-demand transport")?; + let active_transport = + extract_transport_from_config(&config).context("Failed to extract transport")?; + let t = transport::create_transport(&active_transport) + .context("Failed to create on-demand transport")?; #[cfg(debug_assertions)] log::debug!("Created on-demand transport for background task"); @@ -791,3 +793,21 @@ impl Agent for ImixAgent { Ok(()) } } + +pub fn extract_transport_from_config( + config: &crate::config::Config, +) -> anyhow::Result { + if let Some(info) = &config.info { + if let Some(available_transports) = &info.available_transports { + let active_idx = available_transports.active_index as usize; + if let Some(transport) = available_transports + .transports + .get(active_idx) + .or_else(|| available_transports.transports.first()) + { + return Ok(transport.clone()); + } + } + } + anyhow::bail!("No available transports found in config"); +} diff --git a/implants/lib/pb/src/config.rs b/implants/imix/src/config/mod.rs similarity index 91% rename from implants/lib/pb/src/config.rs rename to implants/imix/src/config/mod.rs index aa8dbe9a3..6e2e34b65 100644 --- a/implants/lib/pb/src/config.rs +++ b/implants/imix/src/config/mod.rs @@ -3,7 +3,7 @@ use host_unique::HostIDSelector; use url::Url; use uuid::Uuid; -use crate::c2::{AvailableTransports, Transport}; +use pb::c2::{AvailableTransports, Transport}; //TODO: Can this struct be removed? /// Config holds values necessary to configure an Agent. @@ -11,7 +11,7 @@ use crate::c2::{AvailableTransports, Transport}; #[derive(Clone, PartialEq, ::prost::Message)] pub struct Config { #[prost(message, optional, tag = "1")] - pub info: ::core::option::Option, + pub info: ::core::option::Option, #[prost(bool, tag = "2")] pub run_once: bool, } @@ -86,14 +86,14 @@ pub const RUN_ONCE: bool = run_once!(); /* * Helper function to determine transport type from URI scheme */ -fn get_transport_type(uri: &str) -> crate::c2::transport::Type { +fn get_transport_type(uri: &str) -> pb::c2::transport::Type { match uri.split(":").next().unwrap_or("unspecified") { - "dns" => crate::c2::transport::Type::TransportDns, - "http1" => crate::c2::transport::Type::TransportHttp1, - "https1" => crate::c2::transport::Type::TransportHttp1, - "https" => crate::c2::transport::Type::TransportGrpc, - "http" => crate::c2::transport::Type::TransportGrpc, - _ => crate::c2::transport::Type::TransportUnspecified, + "dns" => pb::c2::transport::Type::TransportDns, + "http1" => pb::c2::transport::Type::TransportHttp1, + "https1" => pb::c2::transport::Type::TransportHttp1, + "https" => pb::c2::transport::Type::TransportGrpc, + "http" => pb::c2::transport::Type::TransportGrpc, + _ => pb::c2::transport::Type::TransportUnspecified, } } @@ -200,13 +200,13 @@ fn parse_host_unique_selectors() -> Vec> { */ impl Config { pub fn default_with_imix_version(imix_version: &str) -> Self { - let agent = crate::c2::Agent { + let agent = pb::c2::Agent { identifier: format!("imix-v{}", imix_version), }; let selectors = parse_host_unique_selectors(); - let host = crate::c2::Host { + let host = pb::c2::Host { name: whoami::fallible::hostname().unwrap_or(String::from("")), identifier: host_unique::get_id_with_selectors(selectors).to_string(), platform: get_host_platform() as i32, @@ -226,7 +226,7 @@ impl Config { active_index: 0, }; - let info = crate::c2::Beacon { + let info = pb::c2::Beacon { identifier: beacon_id, principal: whoami::username(), available_transports: Some(available_transports), @@ -268,18 +268,18 @@ impl Config { /* * Returns which Platform imix has been compiled for. */ -fn get_host_platform() -> crate::c2::host::Platform { +fn get_host_platform() -> pb::c2::host::Platform { #[cfg(target_os = "linux")] - return crate::c2::host::Platform::Linux; + return pb::c2::host::Platform::Linux; #[cfg(target_os = "macos")] - return crate::c2::host::Platform::Macos; + return pb::c2::host::Platform::Macos; #[cfg(target_os = "windows")] - return crate::c2::host::Platform::Windows; + return pb::c2::host::Platform::Windows; #[cfg(any(target_os = "freebsd", target_os = "netbsd", target_os = "openbsd"))] - return crate::c2::host::Platform::Bsd; + return pb::c2::host::Platform::Bsd; #[cfg(all( not(target_os = "linux"), @@ -289,7 +289,7 @@ fn get_host_platform() -> crate::c2::host::Platform { not(target_os = "netbsd"), not(target_os = "openbsd"), ))] - return crate::c2::host::Platform::Unspecified; + return pb::c2::host::Platform::Unspecified; } /* @@ -330,42 +330,41 @@ mod tests { assert_eq!(available.transports.len(), 1); assert_eq!(available.active_index, 0); // The URL crate normalizes URIs, potentially adding trailing slashes - assert!(available.transports[0] - .uri - .starts_with("http://127.0.0.1:8000")); + assert!( + available.transports[0] + .uri + .starts_with("http://127.0.0.1:8000") + ); } #[test] fn test_transport_type_detection_grpc() { let grpc_type = get_transport_type("http://example.com"); - assert_eq!(grpc_type, crate::c2::transport::Type::TransportGrpc); + assert_eq!(grpc_type, pb::c2::transport::Type::TransportGrpc); let grpcs_type = get_transport_type("https://example.com"); - assert_eq!(grpcs_type, crate::c2::transport::Type::TransportGrpc); + assert_eq!(grpcs_type, pb::c2::transport::Type::TransportGrpc); } #[test] fn test_transport_type_detection_http1() { let http1_type = get_transport_type("http1://example.com"); - assert_eq!(http1_type, crate::c2::transport::Type::TransportHttp1); + assert_eq!(http1_type, pb::c2::transport::Type::TransportHttp1); let https1_type = get_transport_type("https1://example.com"); - assert_eq!(https1_type, crate::c2::transport::Type::TransportHttp1); + assert_eq!(https1_type, pb::c2::transport::Type::TransportHttp1); } #[test] fn test_transport_type_detection_dns() { let dns_type = get_transport_type("dns://8.8.8.8"); - assert_eq!(dns_type, crate::c2::transport::Type::TransportDns); + assert_eq!(dns_type, pb::c2::transport::Type::TransportDns); } #[test] fn test_transport_type_detection_unspecified() { let unknown_type = get_transport_type("ftp://example.com"); - assert_eq!( - unknown_type, - crate::c2::transport::Type::TransportUnspecified - ); + assert_eq!(unknown_type, pb::c2::transport::Type::TransportUnspecified); } #[test] diff --git a/implants/imix/src/lib.rs b/implants/imix/src/lib.rs index 9713ca7c0..7f2ac742d 100644 --- a/implants/imix/src/lib.rs +++ b/implants/imix/src/lib.rs @@ -2,6 +2,7 @@ extern crate alloc; pub mod agent; pub mod assets; +pub mod config; pub mod portal; pub mod printer; pub mod run; diff --git a/implants/imix/src/main.rs b/implants/imix/src/main.rs index f295c043c..3bdc40d49 100644 --- a/implants/imix/src/main.rs +++ b/implants/imix/src/main.rs @@ -16,11 +16,12 @@ mod win_service; #[cfg(all(debug_assertions, feature = "tokio-console"))] use console_subscriber; -pub use pb::config::Config; +pub use crate::config::Config; pub use transport::Transport; mod agent; mod assets; +pub mod config; mod install; mod portal; mod printer; diff --git a/implants/imix/src/run.rs b/implants/imix/src/run.rs index 668a6cb17..027739a77 100644 --- a/implants/imix/src/run.rs +++ b/implants/imix/src/run.rs @@ -4,9 +4,9 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::time::{Duration, Instant}; use crate::agent::ImixAgent; +use crate::config::Config; use crate::task::TaskRegistry; use crate::version::VERSION; -use pb::config::Config; use transport; pub static SHUTDOWN: AtomicBool = AtomicBool::new(false); @@ -98,8 +98,10 @@ async fn run_agent_cycle(agent: Arc, registry: Arc) { // Create new active transport let config = agent.get_transport_config().await; + let active_transport = + crate::agent::extract_transport_from_config(&config).expect("Failed to extract transport"); - let transport = match transport::create_transport(config) { + let transport = match transport::create_transport(&active_transport) { Ok(t) => t, Err(_e) => { #[cfg(debug_assertions)] diff --git a/implants/imix/src/shell/manager.rs b/implants/imix/src/shell/manager.rs index eab40b8ba..9689e6307 100644 --- a/implants/imix/src/shell/manager.rs +++ b/implants/imix/src/shell/manager.rs @@ -373,9 +373,9 @@ impl ShellManager { #[cfg(test)] mod tests { use super::*; + use crate::config::Config; use crate::task::TaskRegistry; use pb::c2::{ReportOutputResponse, ShellTask}; - use pb::config::Config; use transport::{MockTransport, Transport}; #[tokio::test] diff --git a/implants/imix/src/shell/terminal.rs b/implants/imix/src/shell/terminal.rs index 60c91e465..6d6c3b3ba 100644 --- a/implants/imix/src/shell/terminal.rs +++ b/implants/imix/src/shell/terminal.rs @@ -7,6 +7,7 @@ use eldritch::repl::Repl; use eldritch_agent::Context; use pb::c2::{ReverseShellMessageKind, ReverseShellRequest, reverse_shell_request}; +#[allow(dead_code)] pub struct VtWriter { pub tx: tokio::sync::mpsc::Sender, pub context: Context, @@ -52,6 +53,7 @@ impl std::io::Write for VtWriter { } } +#[allow(dead_code)] pub fn render( stdout: &mut W, repl: &Repl, diff --git a/implants/imix/src/task.rs b/implants/imix/src/task.rs index 91facbaa6..e2a0dabdd 100644 --- a/implants/imix/src/task.rs +++ b/implants/imix/src/task.rs @@ -17,6 +17,7 @@ use tokio::sync::mpsc; use crate::printer::StreamPrinter; struct SubtaskHandle { + #[allow(dead_code)] name: String, _handle: tokio::task::JoinHandle<()>, } @@ -96,6 +97,7 @@ impl TaskRegistry { true } + #[allow(dead_code)] pub fn register_subtask( &self, task_id: i64, diff --git a/implants/imix/src/tests/agent_output_aggregation.rs b/implants/imix/src/tests/agent_output_aggregation.rs index 02e492070..62d697fc2 100644 --- a/implants/imix/src/tests/agent_output_aggregation.rs +++ b/implants/imix/src/tests/agent_output_aggregation.rs @@ -1,10 +1,10 @@ use crate::agent::ImixAgent; +use crate::config::Config; use crate::task::TaskRegistry; use pb::c2::{ ReportOutputRequest, ReportShellTaskOutputMessage, ReportTaskOutputMessage, ShellTaskContext, ShellTaskOutput, TaskContext, TaskOutput, report_output_request, }; -use pb::config::Config; use std::sync::{Arc, Mutex}; use transport::MockTransport; diff --git a/implants/imix/src/tests/agent_tests.rs b/implants/imix/src/tests/agent_tests.rs index b0282ce74..66c60cc9d 100644 --- a/implants/imix/src/tests/agent_tests.rs +++ b/implants/imix/src/tests/agent_tests.rs @@ -1,7 +1,7 @@ use super::super::agent::ImixAgent; use super::super::task::TaskRegistry; +use crate::config::Config; use eldritch::agent::agent::Agent; -use pb::config::Config; use std::sync::Arc; use transport::MockTransport; diff --git a/implants/imix/src/tests/agent_trait_tests.rs b/implants/imix/src/tests/agent_trait_tests.rs index 364f40954..8639dfd71 100644 --- a/implants/imix/src/tests/agent_trait_tests.rs +++ b/implants/imix/src/tests/agent_trait_tests.rs @@ -1,9 +1,9 @@ use super::super::agent::ImixAgent; use super::super::task::TaskRegistry; +use crate::config::Config; use eldritch::agent::agent::Agent; use pb::c2::host::Platform; use pb::c2::{self, Host, report_file_request, report_output_request}; -use pb::config::Config; use std::sync::Arc; use transport::MockTransport; diff --git a/implants/imix/src/tests/callback_interval_test.rs b/implants/imix/src/tests/callback_interval_test.rs index 1877d968f..b2c4ac099 100644 --- a/implants/imix/src/tests/callback_interval_test.rs +++ b/implants/imix/src/tests/callback_interval_test.rs @@ -1,7 +1,7 @@ use super::super::agent::ImixAgent; use super::super::task::TaskRegistry; +use crate::config::Config; use eldritch::agent::agent::Agent; -use pb::config::Config; use std::sync::Arc; use transport::MockTransport; diff --git a/implants/imix/src/tests/report_large_file_test.rs b/implants/imix/src/tests/report_large_file_test.rs index c5fdf6714..4228b738c 100644 --- a/implants/imix/src/tests/report_large_file_test.rs +++ b/implants/imix/src/tests/report_large_file_test.rs @@ -1,4 +1,5 @@ use crate::agent::ImixAgent; +use crate::config::Config; use crate::task::TaskRegistry; use eldritch::report::std::file_impl; use eldritch_agent::Context; @@ -9,7 +10,6 @@ use pb::c2::{ ReportOutputResponse, ReportProcessListRequest, ReportProcessListResponse, ReverseShellRequest, ReverseShellResponse, TaskContext, Transport as C2Transport, }; -use pb::config::Config; use std::sync::mpsc::{Receiver, Sender}; use std::sync::{Arc, Mutex}; use transport::Transport; @@ -33,7 +33,7 @@ impl Transport for FakeTransport { FakeTransport::new() } - fn new(_config: Config) -> anyhow::Result { + fn new(_config: &pb::c2::Transport) -> anyhow::Result { Ok(FakeTransport::new()) } diff --git a/implants/lib/eldritch/eldritch-wasm/src/browser.rs b/implants/lib/eldritch/eldritch-wasm/src/browser.rs index 0a91b6a98..ff9ce77f9 100644 --- a/implants/lib/eldritch/eldritch-wasm/src/browser.rs +++ b/implants/lib/eldritch/eldritch-wasm/src/browser.rs @@ -25,6 +25,7 @@ impl BrowserRepl { #[wasm_bindgen(constructor)] pub fn new() -> BrowserRepl { let printer = Arc::new(BufferPrinter::new()); + #[allow(unused_mut)] let mut interp = Interpreter::new_with_printer(printer); #[cfg(feature = "fake_bindings")] @@ -121,6 +122,7 @@ impl BrowserRepl { let ends_with_colon = trimmed.ends_with(':'); let lines: Vec<&str> = self.buffer.lines().collect(); let line_count = lines.len(); + #[allow(unused_variables)] let last_line_empty = self.buffer.ends_with('\n') && lines.last().map_or(true, |l| l.trim().is_empty()); diff --git a/implants/lib/eldritch/eldritch/src/lib.rs b/implants/lib/eldritch/eldritch/src/lib.rs index 66c24becf..1fc2eb06c 100644 --- a/implants/lib/eldritch/eldritch/src/lib.rs +++ b/implants/lib/eldritch/eldritch/src/lib.rs @@ -185,6 +185,7 @@ impl Interpreter { self } + #[allow(unused_mut)] pub fn with_fake_agent(mut self) -> Self { #[cfg(feature = "fake_agent")] self.inner.register_lib(AgentLibraryFake); diff --git a/implants/lib/eldritch/stdlib/eldritch-libreport/src/std/screenshot_impl.rs b/implants/lib/eldritch/stdlib/eldritch-libreport/src/std/screenshot_impl.rs index 9cfed8202..1894f1f23 100644 --- a/implants/lib/eldritch/stdlib/eldritch-libreport/src/std/screenshot_impl.rs +++ b/implants/lib/eldritch/stdlib/eldritch-libreport/src/std/screenshot_impl.rs @@ -14,6 +14,7 @@ use { xcap::Monitor, }; +#[cfg(not(target_os = "linux"))] #[cfg(all(unix, feature = "stdlib"))] fn get_hostname() -> String { nix::unistd::gethostname() @@ -21,23 +22,26 @@ fn get_hostname() -> String { .unwrap_or_else(|_| "unknown".to_string()) } +#[cfg(not(target_os = "linux"))] #[cfg(all(unix, not(feature = "stdlib")))] fn get_hostname() -> String { std::env::var("HOSTNAME").unwrap_or_else(|_| "unknown".to_string()) } +#[cfg(not(target_os = "linux"))] #[cfg(windows)] fn get_hostname() -> String { std::env::var("COMPUTERNAME").unwrap_or_else(|_| "unknown".to_string()) } +#[cfg(not(target_os = "linux"))] #[cfg(not(any(unix, windows)))] fn get_hostname() -> String { "unknown".to_string() } #[cfg(target_os = "linux")] -pub fn screenshot(agent: Arc, context: Context) -> Result<(), String> { +pub fn screenshot(_agent: Arc, _context: Context) -> Result<(), String> { return Err( "This OS isn't supported by the screenshot function.\nOnly windows and mac systems are supported".to_string() ); diff --git a/implants/lib/pb/src/lib.rs b/implants/lib/pb/src/lib.rs index d32c80814..3e55c8af4 100644 --- a/implants/lib/pb/src/lib.rs +++ b/implants/lib/pb/src/lib.rs @@ -13,5 +13,4 @@ pub mod portal { pub mod trace { include!("generated/trace.rs"); } -pub mod config; pub mod xchacha; diff --git a/implants/lib/transport/src/dns.rs b/implants/lib/transport/src/dns.rs index dfb45f39a..a8208e152 100644 --- a/implants/lib/transport/src/dns.rs +++ b/implants/lib/transport/src/dns.rs @@ -2,9 +2,9 @@ use crate::Transport; use anyhow::{Context, Result}; use hickory_resolver::system_conf::read_system_conf; use pb::c2::*; -use pb::config::Config; use pb::dns::*; use prost::Message; +use std::collections::HashMap; use std::sync::mpsc::{Receiver, Sender}; use tokio::net::UdpSocket; @@ -965,9 +965,15 @@ impl Transport for DNS { } } - fn new(config: Config) -> Result { - // Extract URI (dns_server address) and extra config from config - let uri = crate::transport::extract_uri_from_config(&config)?; + fn new(transport: &pb::c2::Transport) -> Result { + let uri = transport + .uri + .split('?') + .next() + .unwrap_or(&transport.uri) + .to_string(); + let extra_map = + serde_json::from_str::>(&transport.extra).unwrap_or_default(); let dns_server = if uri == "dns://*" { // Use system DNS resolver @@ -995,10 +1001,8 @@ impl Transport for DNS { format!("{}:53", host_port) } }; - let extra = crate::transport::extract_extra_from_config(&config); - // Extract base_domain from extra field (required) - let base_domain = extra + let base_domain = extra_map .get("domain") .ok_or_else(|| anyhow::anyhow!("domain parameter is required in extra config"))? .to_string(); @@ -1008,7 +1012,7 @@ impl Transport for DNS { } // Extract record_type from extra field (default: TXT) - let record_type = extra + let record_type = extra_map .get("type") .map(|v| match v.to_lowercase().as_str() { "a" => DnsRecordType::A, @@ -1275,30 +1279,21 @@ mod tests { // ============================================================ // Helper function to create a test config with DNS URI and extra params - fn create_dns_test_config(uri: &str, extra: &str) -> Config { - use pb::c2::{AvailableTransports, Beacon, Transport}; - Config { - info: Some(Beacon { - available_transports: Some(AvailableTransports { - transports: vec![Transport { - uri: uri.to_string(), - interval: 5, - r#type: TransportType::TransportDns as i32, - extra: extra.to_string(), - jitter: 0.0, - }], - active_index: 0, - }), - ..Default::default() - }), - ..Default::default() + fn create_dns_test_transport(uri: &str, extra: &str) -> pb::c2::Transport { + pb::c2::Transport { + uri: uri.to_string(), + interval: 5, + r#type: TransportType::TransportDns as i32, + extra: extra.to_string(), + jitter: 0.0, } } #[test] fn test_new_single_server() { - let config = create_dns_test_config("dns://8.8.8.8:53", r#"{"domain": "dnsc2.realm.pub"}"#); - let dns = DNS::new(config).expect("should parse"); + let transport = + create_dns_test_transport("dns://8.8.8.8:53", r#"{"domain": "dnsc2.realm.pub"}"#); + let dns = DNS::new(&transport).expect("should parse"); assert_eq!(dns.base_domain, "dnsc2.realm.pub"); assert_eq!(dns.dns_server, "8.8.8.8:53"); @@ -1307,35 +1302,36 @@ mod tests { #[test] fn test_new_record_type_a() { - let config = create_dns_test_config( + let transport = create_dns_test_transport( "dns://8.8.8.8:53", r#"{"domain": "dnsc2.realm.pub", "type": "a"}"#, ); - let dns = DNS::new(config).expect("should parse"); + let dns = DNS::new(&transport).expect("should parse"); assert_eq!(dns.record_type, DnsRecordType::A); } #[test] fn test_new_record_type_aaaa() { - let config = create_dns_test_config( + let transport = create_dns_test_transport( "dns://8.8.8.8:53", r#"{"domain": "dnsc2.realm.pub", "type": "aaaa"}"#, ); - let dns = DNS::new(config).expect("should parse"); + let dns = DNS::new(&transport).expect("should parse"); assert_eq!(dns.record_type, DnsRecordType::AAAA); } #[test] fn test_new_record_type_txt_default() { - let config = create_dns_test_config("dns://8.8.8.8:53", r#"{"domain": "dnsc2.realm.pub"}"#); - let dns = DNS::new(config).expect("should parse"); + let transport = + create_dns_test_transport("dns://8.8.8.8:53", r#"{"domain": "dnsc2.realm.pub"}"#); + let dns = DNS::new(&transport).expect("should parse"); assert_eq!(dns.record_type, DnsRecordType::TXT); } #[test] fn test_new_missing_domain() { - let config = create_dns_test_config("dns://8.8.8.8:53", "{}"); - let result = DNS::new(config); + let transport = create_dns_test_transport("dns://8.8.8.8:53", "{}"); + let result = DNS::new(&transport); assert!(result.is_err()); assert!(result .unwrap_err() diff --git a/implants/lib/transport/src/grpc.rs b/implants/lib/transport/src/grpc.rs index bb6e83461..aece3360e 100644 --- a/implants/lib/transport/src/grpc.rs +++ b/implants/lib/transport/src/grpc.rs @@ -1,7 +1,7 @@ use anyhow::Result; use http::Uri; use pb::c2::*; -use pb::config::Config; +use std::collections::HashMap; use std::str::FromStr; use std::sync::mpsc::{Receiver, Sender}; use tonic::GrpcMethod; @@ -78,10 +78,15 @@ impl Transport for GRPC { GRPC { grpc: None } } - fn new(config: Config) -> Result { - // Extract URI and EXTRA from config using helper functions - let callback = crate::transport::extract_uri_from_config(&config)?; - let extra_map = crate::transport::extract_extra_from_config(&config); + fn new(transport: &pb::c2::Transport) -> Result { + let callback = transport + .uri + .split('?') + .next() + .unwrap_or(&transport.uri) + .to_string(); + let extra_map = + serde_json::from_str::>(&transport.extra).unwrap_or_default(); // Tonic 0.14+ might fail with "Connecting to HTTPS without TLS enabled" if we use https:// scheme // even if we provide a TLS-enabled connector. We workaround this by using http:// scheme diff --git a/implants/lib/transport/src/http.rs b/implants/lib/transport/src/http.rs index 062bdadaa..e5e27e606 100644 --- a/implants/lib/transport/src/http.rs +++ b/implants/lib/transport/src/http.rs @@ -3,8 +3,9 @@ use anyhow::{Context, Result}; use bytes::BytesMut; use hyper_legacy::body::HttpBody; use hyper_legacy::{StatusCode, Uri}; -use pb::{c2::*, config::Config}; +use pb::c2::*; use prost::Message; +use std::collections::HashMap; use std::sync::{ mpsc::{Receiver, Sender}, Arc, @@ -375,13 +376,13 @@ impl Transport for HTTP { } } - fn new(config: Config) -> Result { - // Extract URI and EXTRA from config using helper functions - let c = crate::transport::extract_uri_from_config(&config)?; - let callback = c + fn new(transport: &pb::c2::Transport) -> Result { + let uri = transport.uri.split('?').next().unwrap_or(&transport.uri); + let callback = uri .replace("http1s://", "https://") .replace("http1://", "http://"); - let extra_map = crate::transport::extract_extra_from_config(&config); + let extra_map = + serde_json::from_str::>(&transport.extra).unwrap_or_default(); #[cfg(feature = "doh")] let doh: Option<&String> = extra_map.get("doh"); diff --git a/implants/lib/transport/src/lib.rs b/implants/lib/transport/src/lib.rs index d3e10c004..af948522b 100644 --- a/implants/lib/transport/src/lib.rs +++ b/implants/lib/transport/src/lib.rs @@ -1,7 +1,5 @@ use anyhow::{anyhow, Result}; use pb::c2::transport::Type as TransportType; -use pb::{c2::*, config::Config}; -use std::sync::mpsc::{Receiver, Sender}; #[cfg(any(feature = "grpc", feature = "http1"))] mod tls_utils; @@ -26,38 +24,26 @@ pub use mock::MockTransport; mod transport; pub use transport::Transport; -pub fn create_transport(config: Config) -> Result> { - // Extract transport type from config - let transport_type = config - .info - .as_ref() - .and_then(|info| info.available_transports.as_ref()) - .and_then(|at| { - let active_idx = at.active_index as usize; - at.transports - .get(active_idx) - .or_else(|| at.transports.first()) - }) - .map(|t| t.r#type) - .ok_or_else(|| anyhow!("No transports configured"))?; +pub fn create_transport(transport: &pb::c2::Transport) -> Result> { + let transport_type = transport.r#type; // Match on the transport type enum match TransportType::try_from(transport_type) { Ok(TransportType::TransportGrpc) => { #[cfg(feature = "grpc")] - return Ok(Box::new(grpc::GRPC::new(config)?)); + return Ok(Box::new(grpc::GRPC::new(transport)?)); #[cfg(not(feature = "grpc"))] return Err(anyhow!("gRPC transport not enabled")); } Ok(TransportType::TransportHttp1) => { #[cfg(feature = "http1")] - return Ok(Box::new(http::HTTP::new(config)?)); + return Ok(Box::new(http::HTTP::new(transport)?)); #[cfg(not(feature = "http1"))] return Err(anyhow!("http1 transport not enabled")); } Ok(TransportType::TransportDns) => { #[cfg(feature = "dns")] - return Ok(Box::new(dns::DNS::new(config)?)); + return Ok(Box::new(dns::DNS::new(transport)?)); #[cfg(not(feature = "dns"))] return Err(anyhow!("DNS transport not enabled")); } @@ -68,43 +54,26 @@ pub fn create_transport(config: Config) -> Result Box { - let mut config = Config::default(); - config.info = Some(pb::c2::Beacon { - available_transports: Some(pb::c2::AvailableTransports { - transports: vec![pb::c2::Transport { - uri: "http://127.0.0.1".to_string(), - r#type: TransportType::TransportHttp1 as i32, - ..Default::default() - }], - active_index: 0, - }), + let transport = pb::c2::Transport { + uri: "http://127.0.0.1".to_string(), + r#type: TransportType::TransportHttp1 as i32, ..Default::default() - }); - create_transport(config).expect("Failed to create empty transport") + }; + create_transport(&transport).expect("Failed to create empty transport") } #[cfg(test)] mod tests { use super::*; - use pb::c2::{AvailableTransports, Beacon}; - - // Helper to create a test config with a specific URI, transport type, and extra params - fn create_test_config(uri: &str, transport_type: i32, extra: &str) -> Config { - Config { - info: Some(Beacon { - available_transports: Some(AvailableTransports { - transports: vec![pb::c2::Transport { - uri: uri.to_string(), - interval: 5, - r#type: transport_type, - extra: extra.to_string(), - jitter: 0.0, - }], - active_index: 0, - }), - ..Default::default() - }), - ..Default::default() + + // Helper to create a test transport with a specific URI, transport type, and extra params + fn create_test_transport(uri: &str, transport_type: i32, extra: &str) -> pb::c2::Transport { + pb::c2::Transport { + uri: uri.to_string(), + interval: 5, + r#type: transport_type, + extra: extra.to_string(), + jitter: 0.0, } } @@ -122,8 +91,8 @@ mod tests { ]; for uri in inputs { - let config = create_test_config(uri, TransportType::TransportGrpc as i32, "{}"); - let result = create_transport(config); + let transport = create_test_transport(uri, TransportType::TransportGrpc as i32, "{}"); + let result = create_transport(&transport); // 1. Assert strictly on the Variant type assert!(result.is_ok(), "URI '{}' did not resolve to Grpc", uri); @@ -138,8 +107,8 @@ mod tests { let inputs = vec!["http1://127.0.0.1:8080", "https1://127.0.0.1:8080"]; for uri in inputs { - let config = create_test_config(uri, TransportType::TransportHttp1 as i32, "{}"); - let result = create_transport(config); + let transport = create_test_transport(uri, TransportType::TransportHttp1 as i32, "{}"); + let result = create_transport(&transport); assert!(result.is_ok(), "URI '{}' did not resolve to Http", uri); assert_eq!(result.unwrap().name(), "http"); @@ -160,8 +129,8 @@ mod tests { ]; for (uri, extra) in inputs { - let config = create_test_config(uri, TransportType::TransportDns as i32, extra); - let result = create_transport(config); + let transport = create_test_transport(uri, TransportType::TransportDns as i32, extra); + let result = create_transport(&transport); assert!( result.is_ok(), @@ -179,8 +148,8 @@ mod tests { // If the feature is off, these should error out let inputs = vec!["grpc://foo", "grpcs://foo", "http://foo"]; for uri in inputs { - let config = create_test_config(uri, TransportType::TransportGrpc as i32, "{}"); - let result = create_transport(config); + let transport = create_test_transport(uri, TransportType::TransportGrpc as i32, "{}"); + let result = create_transport(&transport); assert!( result.is_err(), "Expected error for '{}' when gRPC feature is disabled", @@ -192,12 +161,12 @@ mod tests { #[tokio::test] async fn test_unknown_transport_errors() { // Test with unspecified transport type - let config = create_test_config( + let transport = create_test_transport( "ftp://example.com", TransportType::TransportUnspecified as i32, "{}", ); - let result = create_transport(config); + let result = create_transport(&transport); assert!(result.is_err(), "Expected error for unknown transport type"); } } diff --git a/implants/lib/transport/src/mock.rs b/implants/lib/transport/src/mock.rs index 3ebaf98df..c30bee3b6 100644 --- a/implants/lib/transport/src/mock.rs +++ b/implants/lib/transport/src/mock.rs @@ -14,7 +14,7 @@ mock! { fn clone_box(&self) -> Box; fn init() -> Self; - fn new(config: pb::config::Config) -> Result; + fn new(transport: &pb::c2::Transport) -> Result; async fn claim_tasks(&mut self, request: ClaimTasksRequest) -> Result; diff --git a/implants/lib/transport/src/tls_utils.rs b/implants/lib/transport/src/tls_utils.rs index b34db4fd3..e8064e852 100644 --- a/implants/lib/transport/src/tls_utils.rs +++ b/implants/lib/transport/src/tls_utils.rs @@ -1,4 +1,3 @@ -use hyper_legacy::client::HttpConnector; use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}; use rustls::pki_types::{CertificateDer, ServerName, UnixTime}; use rustls::{DigitallySignedStruct, SignatureScheme}; diff --git a/implants/lib/transport/src/transport.rs b/implants/lib/transport/src/transport.rs index 20d507d10..c5af44d8a 100644 --- a/implants/lib/transport/src/transport.rs +++ b/implants/lib/transport/src/transport.rs @@ -1,54 +1,7 @@ use anyhow::Result; -use pb::{c2::*, config::Config}; -use std::collections::HashMap; +use pb::c2::*; use std::sync::mpsc::{Receiver, Sender}; -/// Helper function to extract the active URI from config. -/// Strips query parameters from the URI. -pub fn extract_uri_from_config(config: &Config) -> Result { - if let Some(info) = &config.info { - if let Some(available_transports) = &info.available_transports { - let active_idx = available_transports.active_index as usize; - let transport = available_transports - .transports - .get(active_idx) - .or_else(|| available_transports.transports.first()) - .ok_or_else(|| anyhow::anyhow!("No transports configured"))?; - - let uri = transport - .uri - .split('?') - .next() - .unwrap_or(&transport.uri) - .to_string(); - Ok(uri) - } else { - Err(anyhow::anyhow!("No available_transports in config")) - } - } else { - Err(anyhow::anyhow!("No beacon info in config")) - } -} - -/// Helper function to extract the extra configuration as a HashMap from config. -/// Returns an empty HashMap if parsing fails or no extra is configured. -pub fn extract_extra_from_config(config: &Config) -> HashMap { - if let Some(info) = &config.info { - if let Some(available_transports) = &info.available_transports { - let active_idx = available_transports.active_index as usize; - if let Some(transport) = available_transports - .transports - .get(active_idx) - .or_else(|| available_transports.transports.first()) - { - return serde_json::from_str::>(&transport.extra) - .unwrap_or_else(|_| HashMap::new()); - } - } - } - HashMap::new() -} - #[async_trait::async_trait] pub trait Transport: Send + Sync { fn clone_box(&self) -> Box; @@ -62,7 +15,7 @@ pub trait Transport: Send + Sync { // New will create a new instance of the transport using the Config. // The URI is extracted from config.info.available_transports at the active_index. #[allow(dead_code)] - fn new(config: Config) -> Result + fn new(transport: &pb::c2::Transport) -> Result where Self: Sized;