diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 596b4a3..2abb3e7 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -46,10 +46,14 @@ jobs: run: sudo apt-get update -y && sudo apt-get install -y libdbus-1-dev - name: Lint agent code - run: cargo clippy --color always --all-features --all-targets -- -D warnings + run: | + cargo build -p genconfig + cargo clippy --color always --all-features --all-targets -- -D warnings - name: Check agent code formatting - run: cargo fmt --all -- --color always --check + run: | + cargo build -p genconfig + cargo fmt --all -- --color always --check mythic: name: Mythic Code diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b6d9328..191fa41 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -49,7 +49,9 @@ jobs: run: sudo apt-get update -y && sudo apt-get install -y libdbus-1-dev - name: Run tests - run: cargo test --color always --workspace --exclude genconfig --all-features + run: | + cargo build -p genconfig + cargo test --color always --workspace --exclude genconfig --all-features mythic: name: Mythic Tests diff --git a/Payload_Type/thanatos/.mythic.code-workspace b/Payload_Type/thanatos/.mythic.code-workspace index cff33de..5b6a0e3 100644 --- a/Payload_Type/thanatos/.mythic.code-workspace +++ b/Payload_Type/thanatos/.mythic.code-workspace @@ -1,10 +1,15 @@ { "folders": [ { + "name": "mythic", "path": "mythic" }, { + "name": "agent", "path": "agent" + }, + { + "path": "../../.github" } ], "settings": { diff --git a/Payload_Type/thanatos/agent/.env.sh b/Payload_Type/thanatos/agent/.env.sh deleted file mode 100644 index e4d739d..0000000 --- a/Payload_Type/thanatos/agent/.env.sh +++ /dev/null @@ -1,2 +0,0 @@ -export SRC_CONFIG=$(pwd)/.config -export CONFIG=$(pwd)/.config.bin diff --git a/Payload_Type/thanatos/agent/.gitignore b/Payload_Type/thanatos/agent/.gitignore index 2ddc608..76c8195 100644 --- a/Payload_Type/thanatos/agent/.gitignore +++ b/Payload_Type/thanatos/agent/.gitignore @@ -1,5 +1,5 @@ target build.ps1 tags -.config +.config.json .config.bin diff --git a/Payload_Type/thanatos/agent/Cargo.lock b/Payload_Type/thanatos/agent/Cargo.lock index b978a2f..a9ea918 100644 --- a/Payload_Type/thanatos/agent/Cargo.lock +++ b/Payload_Type/thanatos/agent/Cargo.lock @@ -2,6 +2,21 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -10,9 +25,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "base64" -version = "0.21.5" +version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] name = "bitflags" @@ -26,9 +41,15 @@ version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ - "generic-array", + "generic-array 0.14.7", ] +[[package]] +name = "bumpalo" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" + [[package]] name = "byteorder" version = "1.5.0" @@ -50,18 +71,37 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bc015644b92d5890fab7489e49d21f879d5c990186827d42ec511919404f38b" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets", +] + [[package]] name = "config" version = "0.2.0" dependencies = [ - "rmp", - "rmp-serde", - "serde", - "serde_bytes", - "serde_repr", + "cfg-if", + "errors", + "pb-rs", + "quick-protobuf", "utils", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + [[package]] name = "cpufeatures" version = "0.2.11" @@ -77,7 +117,7 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ - "generic-array", + "generic-array 0.14.7", "typenum", ] @@ -126,7 +166,7 @@ name = "ffiwrappers" version = "0.2.0" dependencies = [ "errors", - "generic-array", + "generic-array 1.0.0", "hex-literal", "libc", "windows", @@ -152,12 +192,11 @@ name = "genconfig" version = "0.2.0" dependencies = [ "base64", - "lazy_static", - "rmp", - "rmp-serde", + "chrono", + "config", + "quick-protobuf", "serde", "serde_json", - "serde_repr", "sha2", "utils", ] @@ -172,12 +211,44 @@ dependencies = [ "version_check", ] +[[package]] +name = "generic-array" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe739944a5406424e080edccb6add95685130b9f160d5407c639c7df0c5836b0" +dependencies = [ + "typenum", +] + [[package]] name = "hex-literal" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "itoa" version = "1.0.10" @@ -185,10 +256,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" [[package]] -name = "lazy_static" -version = "1.4.0" +name = "js-sys" +version = "0.3.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "406cda4b368d531c842222cf9d2600a9a4acce8d29423695379c6868a143a9ee" +dependencies = [ + "wasm-bindgen", +] [[package]] name = "libc" @@ -205,11 +279,39 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "memchr" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "num-traits" -version = "0.2.17" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" dependencies = [ "autocfg", ] @@ -259,10 +361,14 @@ dependencies = [ ] [[package]] -name = "paste" -version = "1.0.14" +name = "pb-rs" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" +checksum = "354a34df9c65b596152598001c0fe3393379ec2db03ae30b9985659422e2607e" +dependencies = [ + "log", + "nom", +] [[package]] name = "pkg-config" @@ -280,34 +386,21 @@ dependencies = [ ] [[package]] -name = "quote" -version = "1.0.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "rmp" -version = "0.8.12" +name = "quick-protobuf" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f9860a6cc38ed1da53456442089b4dfa35e7cedaa326df63017af88385e6b20" +checksum = "9d6da84cc204722a989e01ba2f6e1e276e190f22263d0cb6ce8526fcdb0d2e1f" dependencies = [ "byteorder", - "num-traits", - "paste", ] [[package]] -name = "rmp-serde" -version = "1.1.2" +name = "quote" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bffea85eea980d8a74453e5d02a8d93028f3c34725de143085a844ebe953258a" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ - "byteorder", - "rmp", - "serde", + "proc-macro2", ] [[package]] @@ -325,15 +418,6 @@ dependencies = [ "serde_derive", ] -[[package]] -name = "serde_bytes" -version = "0.11.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bb1879ea93538b78549031e2d54da3e901fd7e75f2e4dc758d760937b123d10" -dependencies = [ - "serde", -] - [[package]] name = "serde_derive" version = "1.0.193" @@ -347,26 +431,15 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.108" +version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" +checksum = "cb0652c533506ad7a2e353cce269330d6afd8bdfb6d75e0ace5b35aacbd7b9e9" dependencies = [ "itoa", "ryu", "serde", ] -[[package]] -name = "serde_repr" -version = "0.1.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3081f5ffbb02284dda55132aa26daecedd7372a42417bbbab6f14ab7d6bb9145" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "sha2" version = "0.10.8" @@ -390,10 +463,9 @@ dependencies = [ ] [[package]] -name = "thanatos_core" +name = "thanatos" version = "0.2.0" dependencies = [ - "base64", "cfg-if", "config", "cryptolib", @@ -401,37 +473,21 @@ dependencies = [ "errors", "ffiwrappers", "hex-literal", - "rmp", - "rmp-serde", - "serde", - "serde_json", - "serde_repr", - "sha2", "utils", ] [[package]] -name = "thanatos_http" -version = "0.2.0" -dependencies = [ - "config", - "rmp", - "rmp-serde", - "thanatos_core", -] - -[[package]] -name = "thanatos_http_binary" +name = "thanatos_binary" version = "0.2.0" dependencies = [ - "thanatos_http", + "thanatos", ] [[package]] -name = "thanatos_http_cdylib" +name = "thanatos_cdylib" version = "0.2.0" dependencies = [ - "thanatos_http", + "thanatos", ] [[package]] @@ -449,10 +505,6 @@ checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "utils" version = "0.2.0" -dependencies = [ - "serde", - "serde_bytes", -] [[package]] name = "vcpkg" @@ -466,6 +518,60 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "wasm-bindgen" +version = "0.2.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1e124130aee3fb58c5bdd6b639a0509486b0338acaaae0c84a5124b0f588b7f" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e7e1900c352b609c8488ad12639a311045f40a35491fb69ba8c12f758af70b" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b30af9e2d358182b5c7449424f017eba305ed32a7010509ede96cdc4696c46ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f186bd2dcf04330886ce82d6f33dd75a7bfcf69ecf5763b89fcde53b6ac9838" + [[package]] name = "winapi" version = "0.3.9" diff --git a/Payload_Type/thanatos/agent/Cargo.toml b/Payload_Type/thanatos/agent/Cargo.toml index 35b8bd8..ae7f051 100644 --- a/Payload_Type/thanatos/agent/Cargo.toml +++ b/Payload_Type/thanatos/agent/Cargo.toml @@ -4,21 +4,18 @@ resolver = "2" members = [ "config", "ffiwrappers", - "genconfig", - "thanatos_core", - "thanatos_http", - "thanatos_http/cdylib", - "thanatos_http/binary", + "thanatos_agent", + "thanatos_agent/cdylib", + "thanatos_agent/binary", "utils", "cryptolib", "errors", + "genconfig", ] -default-members = ["thanatos_core"] +default-members = ["thanatos_agent"] -exclude = [ - "shellcode-builder", -] +exclude = ["shellcode-builder"] [workspace.package] authors = ["@M_alphaaa"] @@ -32,12 +29,10 @@ version = "0.2.0" [workspace.dependencies] base64 = "0.21.5" cfg-if = "1" +chrono = "0.4.34" hex-literal = "0.4.1" -rmp = "0.8.12" -rmp-serde = "1.1.2" +quick-protobuf = "0.8.1" serde_json = "1" -serde_repr = "0.1" -serde_bytes = "0.11" sha2 = "0.10.8" [workspace.dependencies.dbus] @@ -47,6 +42,10 @@ default-features = false [workspace.dependencies.libc] version = "0.2" +[workspace.dependencies.pb-rs] +version = "0.10.0" +default-features = false + [workspace.dependencies.serde] version = "1" features = ["derive"] diff --git a/Payload_Type/thanatos/agent/config/Cargo.toml b/Payload_Type/thanatos/agent/config/Cargo.toml index 2368748..b00bdc4 100644 --- a/Payload_Type/thanatos/agent/config/Cargo.toml +++ b/Payload_Type/thanatos/agent/config/Cargo.toml @@ -7,14 +7,19 @@ license.workspace = true version.workspace = true -[features] - [dependencies] -rmp.workspace = true -rmp-serde.workspace = true -serde.workspace = true -serde_repr.workspace = true -serde_bytes.workspace = true +quick-protobuf.workspace = true +cfg-if.workspace = true + +[dependencies.errors] +path = "../errors" [dependencies.utils] path = "../utils" + +[build-dependencies] +pb-rs.workspace = true + +[features] +default = ["full"] +full = [] diff --git a/Payload_Type/thanatos/agent/config/build.rs b/Payload_Type/thanatos/agent/config/build.rs new file mode 100644 index 0000000..7fb8372 --- /dev/null +++ b/Payload_Type/thanatos/agent/config/build.rs @@ -0,0 +1,58 @@ +use pb_rs::{types::FileDescriptor, ConfigBuilder}; +use std::path::{Path, PathBuf}; + +fn main() { + let config_proto = Path::new(&std::env::var("CARGO_MANIFEST_DIR").unwrap()) + .parent() + .unwrap() + .parent() + .unwrap() + .join("mythic") + .join("protos") + .join("config.proto"); + + let out_proto = Path::new(&std::env::var("CARGO_MANIFEST_DIR").unwrap()) + .join("src") + .join("proto"); + + if !config_proto.exists() && !out_proto.exists() { + panic!("Failed to generate protos"); + } + + if config_proto.exists() { + println!("cargo:rerun-if-changed={}", config_proto.to_str().unwrap()); + } + + if out_proto.exists() { + std::fs::remove_dir_all(&out_proto).unwrap(); + } + + std::fs::DirBuilder::new().create(&out_proto).unwrap(); + + let builder = ConfigBuilder::new(&[config_proto], Some(&out_proto), None, &[]) + .unwrap() + .single_module(true); + FileDescriptor::run(&builder.build()).unwrap(); + + if std::env::var("CARGO_FEATURE_FULL").is_ok() { + let config_bin = match std::env::var("CONFIG") { + Ok(c) => Some(PathBuf::from(c)), + Err(_) => { + let c = Path::new(&std::env::var("CARGO_MANIFEST_DIR").unwrap()) + .parent() + .unwrap() + .join(".config.bin"); + if c.exists() { + Some(c.canonicalize().unwrap()) + } else { + None + } + } + } + .expect("Failed to find 'CONFIG' or '.config.bin'"); + + let out_config_path = Path::new(&std::env::var("OUT_DIR").unwrap()).join("config.bin"); + std::fs::copy(&config_bin, out_config_path).expect("Failed to copy config.bin"); + println!("cargo:rerun-if-changed={}", config_bin.to_str().unwrap()); + } +} diff --git a/Payload_Type/thanatos/agent/config/src/lib.rs b/Payload_Type/thanatos/agent/config/src/lib.rs index c632312..846f8b2 100644 --- a/Payload_Type/thanatos/agent/config/src/lib.rs +++ b/Payload_Type/thanatos/agent/config/src/lib.rs @@ -1,5 +1,75 @@ //! Contains the configuration values for agent -mod structs; +cfg_if::cfg_if! { + if #[cfg(feature = "full")] { + mod proto; + use errors::ThanatosError; + use utils::uuid::Uuid; -pub use structs::*; + const CONFIG: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/config.bin")); + + pub struct ConfigVars<'a>(proto::config::Config<'a>); + + impl<'a> ConfigVars<'a> { + pub fn parse() -> Result, ThanatosError> { + Ok(ConfigVars(quick_protobuf::deserialize_from_slice(CONFIG).map_err(|_| ThanatosError::ConfigParseError)?)) + } + + pub fn uuid(&self) -> Result { + Uuid::try_from(self.0.uuid.as_ref()).map_err(|_| ThanatosError::ConfigParseError) + } + + pub fn working_hours_start(&self) -> i64 { + self.0.working_hours_start + } + + pub fn working_hours_end(&self) -> i64 { + self.0.working_hours_end + } + + pub fn connection_retries(&self) -> u32 { + self.0.connection_retries + } + + pub fn domains(&self) -> Result, ThanatosError> { + if self.0.domains.len() % 32 != 0 { + return Err(ThanatosError::ConfigParseError); + } + + self.0.domains.chunks(32).map(|c| { + c[..32].try_into().map_err(|_| ThanatosError::ConfigParseError) + }).collect::, ThanatosError>>() + } + + pub fn hostnames(&self) -> Result, ThanatosError> { + if self.0.hostnames.len() % 32 != 0 { + return Err(ThanatosError::ConfigParseError); + } + + self.0.hostnames.chunks(32).map(|c| { + c[..32].try_into().map_err(|_| ThanatosError::ConfigParseError) + }).collect::, ThanatosError>>() + } + + pub fn usernames(&self) -> Result, ThanatosError> { + if self.0.usernames.len() % 32 != 0 { + return Err(ThanatosError::ConfigParseError); + } + + self.0.usernames.chunks(32).map(|c| { + c[..32].try_into().map_err(|_| ThanatosError::ConfigParseError) + }).collect::, ThanatosError>>() + } + + pub fn spawn_to(&self) -> &str { + self.0.spawn_to.as_ref() + } + + pub fn http(&self) -> Option<&proto::config::HttpConfig> { + self.0.http.as_ref() + } + } + } else { + pub mod proto; + } +} diff --git a/Payload_Type/thanatos/agent/config/src/proto/config.rs b/Payload_Type/thanatos/agent/config/src/proto/config.rs new file mode 100644 index 0000000..0656344 --- /dev/null +++ b/Payload_Type/thanatos/agent/config/src/proto/config.rs @@ -0,0 +1,197 @@ +// Automatically generated rust module for 'config.proto' file + +#![allow(non_snake_case)] +#![allow(non_upper_case_globals)] +#![allow(non_camel_case_types)] +#![allow(unused_imports)] +#![allow(unknown_lints)] +#![allow(clippy::all)] +#![cfg_attr(rustfmt, rustfmt_skip)] + + +use std::borrow::Cow; +use std::collections::HashMap; +type KVMap = HashMap; +use quick_protobuf::{MessageInfo, MessageRead, MessageWrite, BytesReader, Writer, WriterBackend, Result}; +use quick_protobuf::sizeofs::*; +use super::*; + +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Debug, Default, PartialEq, Clone)] +pub struct Config<'a> { + pub uuid: Cow<'a, [u8]>, + pub working_hours_start: i64, + pub working_hours_end: i64, + pub connection_retries: u32, + pub domains: Cow<'a, [u8]>, + pub hostnames: Cow<'a, [u8]>, + pub usernames: Cow<'a, [u8]>, + pub spawn_to: Cow<'a, str>, + pub http: Option>, +} + +impl<'a> MessageRead<'a> for Config<'a> { + fn from_reader(r: &mut BytesReader, bytes: &'a [u8]) -> Result { + let mut msg = Self::default(); + while !r.is_eof() { + match r.next_tag(bytes) { + Ok(10) => msg.uuid = r.read_bytes(bytes).map(Cow::Borrowed)?, + Ok(16) => msg.working_hours_start = r.read_int64(bytes)?, + Ok(24) => msg.working_hours_end = r.read_int64(bytes)?, + Ok(32) => msg.connection_retries = r.read_uint32(bytes)?, + Ok(42) => msg.domains = r.read_bytes(bytes).map(Cow::Borrowed)?, + Ok(50) => msg.hostnames = r.read_bytes(bytes).map(Cow::Borrowed)?, + Ok(58) => msg.usernames = r.read_bytes(bytes).map(Cow::Borrowed)?, + Ok(66) => msg.spawn_to = r.read_string(bytes).map(Cow::Borrowed)?, + Ok(74) => msg.http = Some(r.read_message::(bytes)?), + Ok(t) => { r.read_unknown(bytes, t)?; } + Err(e) => return Err(e), + } + } + Ok(msg) + } +} + +impl<'a> MessageWrite for Config<'a> { + fn get_size(&self) -> usize { + 0 + + if self.uuid == Cow::Borrowed(b"") { 0 } else { 1 + sizeof_len((&self.uuid).len()) } + + if self.working_hours_start == 0i64 { 0 } else { 1 + sizeof_varint(*(&self.working_hours_start) as u64) } + + if self.working_hours_end == 0i64 { 0 } else { 1 + sizeof_varint(*(&self.working_hours_end) as u64) } + + if self.connection_retries == 0u32 { 0 } else { 1 + sizeof_varint(*(&self.connection_retries) as u64) } + + if self.domains == Cow::Borrowed(b"") { 0 } else { 1 + sizeof_len((&self.domains).len()) } + + if self.hostnames == Cow::Borrowed(b"") { 0 } else { 1 + sizeof_len((&self.hostnames).len()) } + + if self.usernames == Cow::Borrowed(b"") { 0 } else { 1 + sizeof_len((&self.usernames).len()) } + + if self.spawn_to == "" { 0 } else { 1 + sizeof_len((&self.spawn_to).len()) } + + self.http.as_ref().map_or(0, |m| 1 + sizeof_len((m).get_size())) + } + + fn write_message(&self, w: &mut Writer) -> Result<()> { + if self.uuid != Cow::Borrowed(b"") { w.write_with_tag(10, |w| w.write_bytes(&**&self.uuid))?; } + if self.working_hours_start != 0i64 { w.write_with_tag(16, |w| w.write_int64(*&self.working_hours_start))?; } + if self.working_hours_end != 0i64 { w.write_with_tag(24, |w| w.write_int64(*&self.working_hours_end))?; } + if self.connection_retries != 0u32 { w.write_with_tag(32, |w| w.write_uint32(*&self.connection_retries))?; } + if self.domains != Cow::Borrowed(b"") { w.write_with_tag(42, |w| w.write_bytes(&**&self.domains))?; } + if self.hostnames != Cow::Borrowed(b"") { w.write_with_tag(50, |w| w.write_bytes(&**&self.hostnames))?; } + if self.usernames != Cow::Borrowed(b"") { w.write_with_tag(58, |w| w.write_bytes(&**&self.usernames))?; } + if self.spawn_to != "" { w.write_with_tag(66, |w| w.write_string(&**&self.spawn_to))?; } + if let Some(ref s) = self.http { w.write_with_tag(74, |w| w.write_message(s))?; } + Ok(()) + } +} + +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Debug, Default, PartialEq, Clone)] +pub struct HttpConfig<'a> { + pub callback_port: u32, + pub killdate: u64, + pub callback_jitter: u32, + pub headers: KVMap, Cow<'a, str>>, + pub aes_key: Cow<'a, [u8]>, + pub callback_host: Cow<'a, str>, + pub get_uri: Cow<'a, str>, + pub post_uri: Cow<'a, str>, + pub query_path_name: Cow<'a, str>, + pub proxy: Option>, + pub callback_interval: u32, +} + +impl<'a> MessageRead<'a> for HttpConfig<'a> { + fn from_reader(r: &mut BytesReader, bytes: &'a [u8]) -> Result { + let mut msg = Self::default(); + while !r.is_eof() { + match r.next_tag(bytes) { + Ok(8) => msg.callback_port = r.read_uint32(bytes)?, + Ok(16) => msg.killdate = r.read_uint64(bytes)?, + Ok(24) => msg.callback_jitter = r.read_uint32(bytes)?, + Ok(34) => { + let (key, value) = r.read_map(bytes, |r, bytes| Ok(r.read_string(bytes).map(Cow::Borrowed)?), |r, bytes| Ok(r.read_string(bytes).map(Cow::Borrowed)?))?; + msg.headers.insert(key, value); + } + Ok(42) => msg.aes_key = r.read_bytes(bytes).map(Cow::Borrowed)?, + Ok(50) => msg.callback_host = r.read_string(bytes).map(Cow::Borrowed)?, + Ok(58) => msg.get_uri = r.read_string(bytes).map(Cow::Borrowed)?, + Ok(66) => msg.post_uri = r.read_string(bytes).map(Cow::Borrowed)?, + Ok(74) => msg.query_path_name = r.read_string(bytes).map(Cow::Borrowed)?, + Ok(82) => msg.proxy = Some(r.read_message::(bytes)?), + Ok(88) => msg.callback_interval = r.read_uint32(bytes)?, + Ok(t) => { r.read_unknown(bytes, t)?; } + Err(e) => return Err(e), + } + } + Ok(msg) + } +} + +impl<'a> MessageWrite for HttpConfig<'a> { + fn get_size(&self) -> usize { + 0 + + if self.callback_port == 0u32 { 0 } else { 1 + sizeof_varint(*(&self.callback_port) as u64) } + + if self.killdate == 0u64 { 0 } else { 1 + sizeof_varint(*(&self.killdate) as u64) } + + if self.callback_jitter == 0u32 { 0 } else { 1 + sizeof_varint(*(&self.callback_jitter) as u64) } + + self.headers.iter().map(|(k, v)| 1 + sizeof_len(2 + sizeof_len((k).len()) + sizeof_len((v).len()))).sum::() + + if self.aes_key == Cow::Borrowed(b"") { 0 } else { 1 + sizeof_len((&self.aes_key).len()) } + + if self.callback_host == "" { 0 } else { 1 + sizeof_len((&self.callback_host).len()) } + + if self.get_uri == "" { 0 } else { 1 + sizeof_len((&self.get_uri).len()) } + + if self.post_uri == "" { 0 } else { 1 + sizeof_len((&self.post_uri).len()) } + + if self.query_path_name == "" { 0 } else { 1 + sizeof_len((&self.query_path_name).len()) } + + self.proxy.as_ref().map_or(0, |m| 1 + sizeof_len((m).get_size())) + + if self.callback_interval == 0u32 { 0 } else { 1 + sizeof_varint(*(&self.callback_interval) as u64) } + } + + fn write_message(&self, w: &mut Writer) -> Result<()> { + if self.callback_port != 0u32 { w.write_with_tag(8, |w| w.write_uint32(*&self.callback_port))?; } + if self.killdate != 0u64 { w.write_with_tag(16, |w| w.write_uint64(*&self.killdate))?; } + if self.callback_jitter != 0u32 { w.write_with_tag(24, |w| w.write_uint32(*&self.callback_jitter))?; } + for (k, v) in self.headers.iter() { w.write_with_tag(34, |w| w.write_map(2 + sizeof_len((k).len()) + sizeof_len((v).len()), 10, |w| w.write_string(&**k), 18, |w| w.write_string(&**v)))?; } + if self.aes_key != Cow::Borrowed(b"") { w.write_with_tag(42, |w| w.write_bytes(&**&self.aes_key))?; } + if self.callback_host != "" { w.write_with_tag(50, |w| w.write_string(&**&self.callback_host))?; } + if self.get_uri != "" { w.write_with_tag(58, |w| w.write_string(&**&self.get_uri))?; } + if self.post_uri != "" { w.write_with_tag(66, |w| w.write_string(&**&self.post_uri))?; } + if self.query_path_name != "" { w.write_with_tag(74, |w| w.write_string(&**&self.query_path_name))?; } + if let Some(ref s) = self.proxy { w.write_with_tag(82, |w| w.write_message(s))?; } + if self.callback_interval != 0u32 { w.write_with_tag(88, |w| w.write_uint32(*&self.callback_interval))?; } + Ok(()) + } +} + +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Debug, Default, PartialEq, Clone)] +pub struct ProxyInfo<'a> { + pub host: Cow<'a, str>, + pub port: u32, + pub pass: Cow<'a, str>, +} + +impl<'a> MessageRead<'a> for ProxyInfo<'a> { + fn from_reader(r: &mut BytesReader, bytes: &'a [u8]) -> Result { + let mut msg = Self::default(); + while !r.is_eof() { + match r.next_tag(bytes) { + Ok(10) => msg.host = r.read_string(bytes).map(Cow::Borrowed)?, + Ok(16) => msg.port = r.read_uint32(bytes)?, + Ok(26) => msg.pass = r.read_string(bytes).map(Cow::Borrowed)?, + Ok(t) => { r.read_unknown(bytes, t)?; } + Err(e) => return Err(e), + } + } + Ok(msg) + } +} + +impl<'a> MessageWrite for ProxyInfo<'a> { + fn get_size(&self) -> usize { + 0 + + if self.host == "" { 0 } else { 1 + sizeof_len((&self.host).len()) } + + if self.port == 0u32 { 0 } else { 1 + sizeof_varint(*(&self.port) as u64) } + + if self.pass == "" { 0 } else { 1 + sizeof_len((&self.pass).len()) } + } + + fn write_message(&self, w: &mut Writer) -> Result<()> { + if self.host != "" { w.write_with_tag(10, |w| w.write_string(&**&self.host))?; } + if self.port != 0u32 { w.write_with_tag(16, |w| w.write_uint32(*&self.port))?; } + if self.pass != "" { w.write_with_tag(26, |w| w.write_string(&**&self.pass))?; } + Ok(()) + } +} + diff --git a/Payload_Type/thanatos/agent/config/src/proto/mod.rs b/Payload_Type/thanatos/agent/config/src/proto/mod.rs new file mode 100644 index 0000000..d81a7d8 --- /dev/null +++ b/Payload_Type/thanatos/agent/config/src/proto/mod.rs @@ -0,0 +1,2 @@ +// Automatically generated mod.rs +pub mod config; diff --git a/Payload_Type/thanatos/agent/config/src/structs.rs b/Payload_Type/thanatos/agent/config/src/structs.rs deleted file mode 100644 index 769caf9..0000000 --- a/Payload_Type/thanatos/agent/config/src/structs.rs +++ /dev/null @@ -1,97 +0,0 @@ -use std::collections::HashMap; - -use serde::{Deserialize, Serialize}; -use serde_repr::{Deserialize_repr, Serialize_repr}; -use utils::uuid::Uuid; - -/// Configuration option for the initial payload execution -#[derive(Serialize_repr, Deserialize_repr, Copy, Clone, Debug)] -#[repr(u8)] -pub enum InitOption { - /// Payload should not do anything special when executed - None = 0, - - /// Payload should run in a new thread - Thread = 1, - - /// Payload should fork to the background - #[cfg(target_os = "linux")] - Fork = 2, -} - -/// HTTP C2 profile proxy information -#[derive(Serialize, Deserialize, Debug)] -pub struct ProxyInfo<'a> { - host: &'a str, - port: u16, - user: &'a str, - pass: &'a str, -} - -/// HTTP Profile crypto info -#[derive(Serialize, Deserialize, Debug)] -pub enum CryptoInfo { - /// AES256 encryption type - #[serde(rename = "aes256_hmac")] - Aes256Hmac { - /// AES key - key: [u8; 16], - }, -} - -/// HTTP profile configuration variables -#[derive(Serialize, Deserialize, Debug, Default)] -pub struct HttpConfigVars<'a> { - callback_host: &'a str, - callback_interval: u32, - callback_jitter: u16, - callback_port: u16, - killdate: u64, - encrypted_exchange_check: bool, - crypto_info: Option, - headers: HashMap<&'a str, &'a str>, - get_uri: &'a str, - post_uri: &'a str, - query_path_name: &'a str, - proxy_info: Option>, -} - -/// Payload configuration variables -#[derive(Serialize, Deserialize, Debug)] -pub struct ConfigVars<'a> { - uuid: Uuid, - init_option: InitOption, - working_hours_start: u64, - working_hours_end: u64, - connection_retries: u32, - domains: Vec<[u8; 32]>, - hostnames: Vec<[u8; 32]>, - usernames: Vec<[u8; 32]>, - tlsuntrusted: bool, - spawn_to: &'a str, - http_profile: Option>, -} - -impl ConfigVars<'_> { - /// Returns the payload uuid - pub fn uuid(&self) -> &Uuid { - &self.uuid - } - - /// Returns the payload init options - pub fn init_option(&self) -> InitOption { - self.init_option - } - - pub fn domains(&self) -> &[[u8; 32]] { - &self.domains - } - - pub fn hostnames(&self) -> &[[u8; 32]] { - &self.hostnames - } - - pub fn usernames(&self) -> &[[u8; 32]] { - &self.usernames - } -} diff --git a/Payload_Type/thanatos/agent/cryptolib/src/hash/system/windows.rs b/Payload_Type/thanatos/agent/cryptolib/src/hash/system/windows.rs index e11000a..15b0afb 100644 --- a/Payload_Type/thanatos/agent/cryptolib/src/hash/system/windows.rs +++ b/Payload_Type/thanatos/agent/cryptolib/src/hash/system/windows.rs @@ -1,11 +1,10 @@ -use ffiwrappers::windows::bcrypt::{algorithms, BCryptAlgHandle, BCryptHashHandle}; +use ffiwrappers::windows::bcrypt::{algorithms, BCryptHashHandle}; pub struct Sha256(BCryptHashHandle); impl Sha256 { pub fn new() -> Sha256 { - let mut alg_handle = BCryptAlgHandle::::new(); - Sha256(alg_handle.create_hash()) + Sha256(BCryptHashHandle::::new()) } pub fn update(&mut self, input: &[u8]) { diff --git a/Payload_Type/thanatos/agent/errors/src/lib.rs b/Payload_Type/thanatos/agent/errors/src/lib.rs index a358b1c..d77839e 100644 --- a/Payload_Type/thanatos/agent/errors/src/lib.rs +++ b/Payload_Type/thanatos/agent/errors/src/lib.rs @@ -8,6 +8,8 @@ pub enum ThanatosError { FFIError(FFIError), NotDomainJoined, + ConfigParseError, + #[cfg(target_os = "linux")] DbusError(dbus::Error), } diff --git a/Payload_Type/thanatos/agent/ffiwrappers/Cargo.toml b/Payload_Type/thanatos/agent/ffiwrappers/Cargo.toml index de25ecd..3c38b09 100644 --- a/Payload_Type/thanatos/agent/ffiwrappers/Cargo.toml +++ b/Payload_Type/thanatos/agent/ffiwrappers/Cargo.toml @@ -8,10 +8,10 @@ version.workspace = true [dependencies] -generic-array = "0.14.7" windows.workspace = true libc.workspace = true hex-literal.workspace = true +generic-array = "1" [dependencies.errors] path = "../errors" diff --git a/Payload_Type/thanatos/agent/ffiwrappers/src/windows/bcrypt/hash.rs b/Payload_Type/thanatos/agent/ffiwrappers/src/windows/bcrypt/hash.rs index cc1bf36..b458ded 100644 --- a/Payload_Type/thanatos/agent/ffiwrappers/src/windows/bcrypt/hash.rs +++ b/Payload_Type/thanatos/agent/ffiwrappers/src/windows/bcrypt/hash.rs @@ -1,11 +1,11 @@ use std::marker::PhantomData; -use generic_array::{sequence::GenericSequence, GenericArray}; +use generic_array::GenericArray; use windows::Win32::Security::Cryptography::{ BCryptDestroyHash, BCryptFinishHash, BCryptHashData, BCRYPT_HASH_HANDLE, }; -use super::traits::HashAlgorithm; +use super::{traits::HashAlgorithm, BCryptAlgHandle}; #[repr(transparent)] pub struct BCryptHashHandle { @@ -15,6 +15,11 @@ pub struct BCryptHashHandle { } impl BCryptHashHandle { + pub fn new() -> BCryptHashHandle { + let mut alg_handle = BCryptAlgHandle::::new(); + alg_handle.create_hash() + } + pub fn hash_data(&mut self, data: &[u8]) { // Possible return/error values are documented here: https://learn.microsoft.com/en-us/windows/win32/api/bcrypt/nf-bcrypt-bcrypthashdata#return-value // Error assertions: @@ -28,11 +33,11 @@ impl BCryptHashHandle { // where this is called with an invalid handle. // // SAFETY: Error assertions are defined above. - unsafe { BCryptHashData(self.handle, data, 0).ok().unwrap_unchecked() } + let _ = unsafe { BCryptHashData(self.handle, data, 0) }; } pub fn finish_hash(self) -> GenericArray { - let mut output = GenericArray::::generate(|v| v as u8); + let mut output: GenericArray = Default::default(); // Possible return/error values are documented here: https://learn.microsoft.com/en-us/windows/win32/api/bcrypt/nf-bcrypt-bcryptfinishhash // Error assertions: @@ -46,16 +51,18 @@ impl BCryptHashHandle { // error code will never be returned. // // SAFETY: Error assertions are defined above. - unsafe { - BCryptFinishHash(self.handle, output.as_mut_slice(), 0) - .ok() - .unwrap_unchecked() - } + let _ = unsafe { BCryptFinishHash(self.handle, output.as_mut_slice(), 0) }; output } } +impl Default for BCryptHashHandle { + fn default() -> Self { + Self::new() + } +} + impl Drop for BCryptHashHandle { fn drop(&mut self) { let _ = unsafe { BCryptDestroyHash(self.handle) }; @@ -66,18 +73,28 @@ impl Drop for BCryptHashHandle { mod tests { use crate::windows::bcrypt::{algorithms::Sha256, BCryptAlgHandle}; - #[test] - fn sha256_test() { - let w = "hello"; + use super::BCryptHashHandle; - let expected = - hex_literal::hex!("2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"); + const WORD: &'static str = "hello"; + const EXPECTED: [u8; 32] = + hex_literal::hex!("2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"); + + #[test] + fn sha256_alg_test() { let mut alg = BCryptAlgHandle::::new(); let mut h = alg.create_hash(); - h.hash_data(w.as_bytes()); + h.hash_data(WORD.as_bytes()); + + let output: [u8; 32] = h.finish_hash().into(); + assert_eq!(output, EXPECTED); + } + #[test] + fn sha256_new_test() { + let mut h = BCryptHashHandle::::new(); + h.hash_data(WORD.as_bytes()); let output: [u8; 32] = h.finish_hash().into(); - assert_eq!(output, expected); + assert_eq!(output, EXPECTED); } } diff --git a/Payload_Type/thanatos/agent/ffiwrappers/src/windows/bcrypt/mod.rs b/Payload_Type/thanatos/agent/ffiwrappers/src/windows/bcrypt/mod.rs index 1c6f9be..8d2659d 100644 --- a/Payload_Type/thanatos/agent/ffiwrappers/src/windows/bcrypt/mod.rs +++ b/Payload_Type/thanatos/agent/ffiwrappers/src/windows/bcrypt/mod.rs @@ -15,6 +15,6 @@ pub mod traits { } pub trait HashAlgorithm: Algorithm { - type LEN: ArrayLength; + type LEN: ArrayLength; } } diff --git a/Payload_Type/thanatos/agent/genconfig/Cargo.toml b/Payload_Type/thanatos/agent/genconfig/Cargo.toml index 5e826cd..e30256b 100644 --- a/Payload_Type/thanatos/agent/genconfig/Cargo.toml +++ b/Payload_Type/thanatos/agent/genconfig/Cargo.toml @@ -6,14 +6,17 @@ homepage.workspace = true license.workspace = true version.workspace = true - [build-dependencies] base64.workspace = true -rmp.workspace = true -rmp-serde.workspace = true +chrono.workspace = true +quick-protobuf.workspace = true serde.workspace = true serde_json.workspace = true -serde_repr.workspace = true -utils = { path = "../utils" } sha2.workspace = true -lazy_static = "1.4" + +[build-dependencies.utils] +path = "../utils" + +[build-dependencies.config] +path = "../config" +default-features = false diff --git a/Payload_Type/thanatos/agent/genconfig/build.rs b/Payload_Type/thanatos/agent/genconfig/build.rs index 841e1ee..48e3db9 100644 --- a/Payload_Type/thanatos/agent/genconfig/build.rs +++ b/Payload_Type/thanatos/agent/genconfig/build.rs @@ -1,101 +1,370 @@ -lazy_static::lazy_static! { -static ref SRC_CONFIG_PATH: &'static str = option_env!("SRC_CONFIG").unwrap_or("../.config"); -static ref CONFIG_PATH: &'static str = option_env!("CONFIG").unwrap_or("../.config.bin"); -} +use base64::{engine::general_purpose, Engine as _}; +use chrono::{DateTime, Utc}; +use quick_protobuf::MessageWrite; +use serde::Deserialize; +use sha2::Digest; +use std::{borrow::Cow, collections::HashMap, io::Write, path::Path, str::FromStr}; +use utils::uuid::Uuid; -const DEFAULT_CONFIG: &'static str = r#"UUID=00000000-0000-0000-0000-000000000000 -INIT_OPTION=none -WORKING_HOURS_START=0 -WORKING_HOURS_END=86400 -CONNECTION_RETRIES=1 -TLS_SELF_SIGNED=false -HTTP_CALLBACK_HOST=http://mythic -HTTP_CALLBACK_INTERVAL=10 -HTTP_CALLBACK_JITTER=23 -HTTP_CALLBACK_PORT=80 -HTTP_KILLDATE=4070908800 -HTTP_ENCRYPTED_EXCHANGE_CHECK=true -HTTP_HEADERS=eyJVc2VyLUFnZW50IjoidGVzdCJ9 -HTTP_GET_URI=index -HTTP_POST_URI=data -HTTP_QUERY_PATH_NAME=q +use config::proto::config::{Config, HttpConfig, ProxyInfo}; + +const DEFAULT_CONFIG: &'static str = r#"{ + "uuid": "00000000-0000-0000-0000-000000000000", + "description": "", + "payload_type": "thanatos", + "c2_profiles": [ + { + "c2_profile": "http", + "c2_profile_parameters": { + "AESPSK": { + "dec_key": null, + "enc_key": null, + "value": "none" + }, + "callback_host": "http://mythic", + "callback_interval": 1, + "callback_jitter": 0, + "callback_port": 80, + "encrypted_exchange_check": false, + "get_uri": "index", + "headers": { + "User-Agent": "default" + }, + "killdate": "2099-01-01", + "post_uri": "data", + "proxy_host": "", + "proxy_pass": "", + "proxy_port": "", + "proxy_user": "", + "query_path_name": "q" + } + } + ], + "build_parameters": [ + { + "name": "usernames", + "value": [] + }, + { + "name": "spawnto", + "value": "" + }, + { + "name": "connection_retries", + "value": "1" + }, + { + "name": "libexport", + "value": "init" + }, + { + "name": "tlsuntrusted", + "value": "false" + }, + { + "name": "architecture", + "value": "amd64" + }, + { + "name": "working_hours", + "value": "00:00-23:59" + }, + { + "name": "static", + "value": [] + }, + { + "name": "output", + "value": "executable" + }, + { + "name": "initoptions", + "value": "none" + }, + { + "name": "cryptolib", + "value": "system (Windows CNG/Linux OpenSSL)" + }, + { + "name": "domains", + "value": [] + }, + { + "name": "hostnames", + "value": [] + } + ], + "commands": [ + "exit", + "sleep" + ], + "selected_os": "Linux", + "filename": "thanatos", + "wrapped_payload": "" +} "#; -use sha2::Digest; -use std::{io::Write, str::FromStr}; +#[derive(Deserialize, Debug)] +struct InputConfig<'a> { + uuid: String, + c2_profiles: Vec>, + build_parameters: Vec, +} -include!("../config/src/structs.rs"); +#[derive(Deserialize, Debug)] +#[serde(tag = "name", content = "value")] +#[serde(rename_all = "lowercase")] +enum BuildParameter { + Usernames(Vec), + SpawnTo(String), -fn hash_string_list(s: &str) -> Vec<[u8; 32]> { - s.split(",") - .skip_while(|value| value.is_empty()) - .map(|value| { - let mut h = sha2::Sha256::new(); - h.update(value.to_lowercase().as_bytes()); - h.finalize().into() - }) - .collect() + #[serde(rename = "connection_retries")] + ConnectionRetries(String), + + #[serde(rename = "working_hours", with = "working_hours_format")] + WorkingHours((i64, i64)), + Domains(Vec), + Hostnames(Vec), + + #[serde(untagged)] + Unused(serde_json::Value), } -fn main() { - let config_data = match std::fs::read_to_string(*SRC_CONFIG_PATH) { - Ok(src) => src, - Err(_) => { - let mut w = std::fs::File::create(*SRC_CONFIG_PATH) - .expect("Failed to create new 'SRC_CONFIG' file"); +#[derive(Deserialize, Debug, PartialEq, Eq)] +#[serde(tag = "c2_profile", content = "c2_profile_parameters")] +enum C2Profile<'a> { + #[serde(rename = "http")] + Http(HttpC2ProfileParameters<'a>), +} + +#[derive(Deserialize, Debug, PartialEq, Eq)] +struct HttpC2ProfileParameters<'a> { + #[serde(rename = "AESPSK")] + aes_psk: AesPsk, - w.write_all(DEFAULT_CONFIG.as_bytes()) - .expect("Failed to write default 'SRC_CONFIG' file"); + callback_host: String, + callback_interval: u32, + callback_jitter: u32, + callback_port: u32, + encrypted_exchange_check: bool, + get_uri: String, + headers: HashMap, Cow<'a, str>>, - DEFAULT_CONFIG.to_owned() + #[serde(with = "killdate_format")] + killdate: DateTime, + post_uri: String, + proxy_host: String, + proxy_pass: String, + proxy_port: String, + proxy_user: String, + query_path_name: String, +} + +#[derive(Deserialize, Debug, PartialEq, Eq)] +struct AesPsk { + dec_key: Option, + enc_key: Option, + value: String, +} + +mod killdate_format { + use chrono::{DateTime, NaiveDate, NaiveDateTime, Utc}; + use serde::{Deserialize, Deserializer}; + + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + let naive_date = + NaiveDate::parse_from_str(&s, "%Y-%m-%d").map_err(serde::de::Error::custom)?; + Ok(DateTime::::from_naive_utc_and_offset( + NaiveDateTime::from(naive_date), + Utc, + )) + } +} + +mod working_hours_format { + use chrono::NaiveTime; + use serde::{Deserialize, Deserializer}; + + pub fn deserialize<'de, D>(deserializer: D) -> Result<(i64, i64), D::Error> + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + let mut split = s.split('-'); + let start = split + .next() + .ok_or(serde::de::Error::custom("Missing start interval"))?; + + let start = NaiveTime::parse_from_str(start, "%H:%M").map_err(serde::de::Error::custom)?; + let start = start.signed_duration_since(NaiveTime::default()); + + let end = split + .next() + .ok_or(serde::de::Error::custom("Missing end interval"))?; + + let end = NaiveTime::parse_from_str(end, "%H:%M").map_err(serde::de::Error::custom)?; + let end = end.signed_duration_since(NaiveTime::default()); + + if end <= start { + return Err(serde::de::Error::custom( + "End is less than or equal to start", + )); } + + Ok((start.num_seconds(), end.num_seconds())) + } +} + +macro_rules! get_build_param { + ($data:ident, $variant:pat_param, $inner:ident) => { + $data.build_parameters.iter().find_map(|v| match v { + $variant => Some($inner), + _ => None, + }) }; - let mut config_map: HashMap<&str, &str> = config_data - .lines() - .map(|line| { - let mut line_split = line.split("="); - let key = line_split.next().expect("Failed to get key"); - let value = line_split.next().expect("Failed to get value"); + ($data:ident, $variant:pat_param, $b:block) => { + $data.build_parameters.iter().find_map(|v| match v { + $variant => $b, + _ => None, + }) + }; - (key, value) + ($data:ident, $variant:pat_param, $e:expr) => { + $data.build_parameters.iter().find_map(|v| match v { + $variant => Some($e), + _ => None, }) - .collect(); - - let config_parsed = ConfigVars { - init_option: match config_map["INIT_OPTION"] { - "thread" => InitOption::Thread, - "fork" => InitOption::Fork, - "none" | &_ => InitOption::None, - }, - - connection_retries: config_map["CONNECTION_RETRIES"].parse().unwrap(), - uuid: Uuid::from_str(config_map["UUID"]).unwrap(), - working_hours_end: config_map["WORKING_HOURS_END"].parse().unwrap(), - working_hours_start: config_map["WORKING_HOURS_START"].parse().unwrap(), - spawn_to: config_map.remove("SPAWN_TO").unwrap_or_default(), - tlsuntrusted: config_map - .remove("TLS_UNTRUSTED") - .map(|v| v.parse().ok()) - .flatten() - .unwrap_or(false), - domains: hash_string_list(config_map.remove("DOMAIN_LIST").unwrap_or_default()), - hostnames: hash_string_list(config_map.remove("HOSETNAME_LIST").unwrap_or_default()), - usernames: hash_string_list(config_map.remove("USERNAME_LIST").unwrap_or_default()), - http_profile: None, }; +} + +fn main() { + let config_path = Path::new(&std::env::var("CARGO_MANIFEST_DIR").unwrap()) + .parent() + .unwrap() + .join(".config.json"); + + let json_config: InputConfig = if config_path.exists() { + let f = std::fs::File::open(&config_path).expect("Failed to open .config.json file"); + serde_json::from_reader(f).expect("Failed to parse .config.json file") + } else { + let mut f = + std::fs::File::create(&config_path).expect("Failed to create .config.json file"); + f.write_all(DEFAULT_CONFIG.as_bytes()) + .expect("Failed to write .config.json file"); + + serde_json::from_str(DEFAULT_CONFIG).expect("Failed to parse default config") + }; + + let uuid = Uuid::from_str(&json_config.uuid).expect("Failed to parse uuid"); + + let working_hours = get_build_param!(json_config, BuildParameter::WorkingHours(w), w) + .expect("Failed to find 'working_hours'"); - let serialized_config = rmp_serde::to_vec(&config_parsed).expect("Failed to serialize config"); + let connection_retries = get_build_param!(json_config, BuildParameter::ConnectionRetries(v), v) + .expect("Failed to get 'connection_retries") + .parse() + .expect("Failed to parse 'connection_retries'"); + + let domains = get_build_param!(json_config, BuildParameter::Domains(d), { + Some(hash_guard_list(d)) + }) + .unwrap_or_default(); + + let hostnames = get_build_param!(json_config, BuildParameter::Hostnames(h), { + Some(hash_guard_list(h)) + }) + .unwrap_or_default(); + + let usernames = get_build_param!(json_config, BuildParameter::Usernames(u), { + Some(hash_guard_list(u)) + }) + .unwrap_or_default(); + + let spawn_to = + get_build_param!(json_config, BuildParameter::SpawnTo(s), s.clone()).unwrap_or_default(); + + let mut config = Config { + uuid: Cow::Borrowed(uuid.as_slice()), + working_hours_start: working_hours.0, + working_hours_end: working_hours + .1 + .checked_add(60) + .expect("Working hours end is too large"), + connection_retries, + domains: Cow::Borrowed(&domains), + hostnames: Cow::Borrowed(&hostnames), + usernames: Cow::Borrowed(&usernames), + spawn_to: Cow::Borrowed(&spawn_to), + ..Default::default() + }; + + if let Some(C2Profile::Http(profile)) = json_config.c2_profiles.into_iter().find(|p| match p { + C2Profile::Http(_) => true, + }) { + let aes_key = profile.aes_psk.enc_key.map(|key| { + general_purpose::STANDARD + .decode(key.as_bytes()) + .expect("Failed to decode AES key") + }); + + config.http = Some(HttpConfig { + callback_host: Cow::Owned(profile.callback_host), + killdate: profile + .killdate + .timestamp() + .try_into() + .expect("Killdate is less than 1970"), + callback_jitter: profile.callback_jitter, + headers: profile.headers, + aes_key: Cow::Owned(aes_key.unwrap_or_default()), + get_uri: Cow::Owned(profile.get_uri), + post_uri: Cow::Owned(profile.post_uri), + query_path_name: Cow::Owned(profile.query_path_name), + proxy: { + if !profile.proxy_host.is_empty() { + Some(ProxyInfo { + host: Cow::Owned(profile.proxy_host), + port: profile + .proxy_port + .parse() + .expect("Failed to parse proxy port"), + pass: Cow::Owned(profile.proxy_pass), + }) + } else { + None + } + }, + callback_interval: profile.callback_interval, + callback_port: profile.callback_port, + }); + } + + let output_file = Path::new(&std::env::var("CARGO_MANIFEST_DIR").unwrap()) + .parent() + .unwrap() + .join(".config.bin"); + + config + .write_file(output_file) + .expect("Failed to write serialized config"); + + println!("cargo:rerun-if-changed={}", config_path.to_str().unwrap()); +} - let mut f = std::fs::OpenOptions::new() - .write(true) - .create(true) - .open(*CONFIG_PATH) - .expect("Failed to open destination config"); +fn hash_guard_list(guard_list: &[String]) -> Vec { + let mut hashlist = Vec::new(); - f.write_all(&serialized_config) - .expect("Failed to write out config"); + for val in guard_list { + let mut h = sha2::Sha256::new(); + h.update(val.as_bytes()); + let hashed: [u8; 32] = h.finalize().into(); + hashlist.extend_from_slice(&hashed); + } - println!("cargo:rerun-if-changed={}", *SRC_CONFIG_PATH); - println!("cargo:rerun-if-changed={}", *CONFIG_PATH); + hashlist } diff --git a/Payload_Type/thanatos/agent/thanatos_core/Cargo.toml b/Payload_Type/thanatos/agent/thanatos_agent/Cargo.toml similarity index 70% rename from Payload_Type/thanatos/agent/thanatos_core/Cargo.toml rename to Payload_Type/thanatos/agent/thanatos_agent/Cargo.toml index 1d64de4..11de3d6 100644 --- a/Payload_Type/thanatos/agent/thanatos_core/Cargo.toml +++ b/Payload_Type/thanatos/agent/thanatos_agent/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "thanatos_core" +name = "thanatos" authors.workspace = true edition.workspace = true homepage.workspace = true @@ -8,22 +8,15 @@ version.workspace = true [dependencies] -base64.workspace = true cfg-if.workspace = true -rmp.workspace = true -rmp-serde.workspace = true -serde.workspace = true -serde_json.workspace = true -serde_repr.workspace = true -sha2.workspace = true hex-literal.workspace = true - [dependencies.config] path = "../config" [dependencies.cryptolib] path = "../cryptolib" +optional = true [dependencies.errors] path = "../errors" @@ -38,4 +31,14 @@ path = "../utils" dbus.workspace = true [features] +default = ["crypto-internal"] crypto-system = ["cryptolib/system"] +crypto-internal = ["cryptolib/internal"] +domaincheck = [] +hostnamecheck = [] +usernamecheck = [] +init-fork = [] +init-thread = [] +tlsuntrusted = [] +http = [] +tcp = [] diff --git a/Payload_Type/thanatos/agent/thanatos_http/binary/Cargo.toml b/Payload_Type/thanatos/agent/thanatos_agent/binary/Cargo.toml similarity index 71% rename from Payload_Type/thanatos/agent/thanatos_http/binary/Cargo.toml rename to Payload_Type/thanatos/agent/thanatos_agent/binary/Cargo.toml index 7f3f45b..e774ec7 100644 --- a/Payload_Type/thanatos/agent/thanatos_http/binary/Cargo.toml +++ b/Payload_Type/thanatos/agent/thanatos_agent/binary/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "thanatos_http_binary" +name = "thanatos_binary" authors.workspace = true edition.workspace = true homepage.workspace = true @@ -7,4 +7,4 @@ license.workspace = true version.workspace = true [dependencies] -thanatos_http = { path = ".." } +thanatos = { path = ".." } diff --git a/Payload_Type/thanatos/agent/thanatos_http/binary/src/main.rs b/Payload_Type/thanatos/agent/thanatos_agent/binary/src/main.rs similarity index 59% rename from Payload_Type/thanatos/agent/thanatos_http/binary/src/main.rs rename to Payload_Type/thanatos/agent/thanatos_agent/binary/src/main.rs index 4339b87..b422873 100644 --- a/Payload_Type/thanatos/agent/thanatos_http/binary/src/main.rs +++ b/Payload_Type/thanatos/agent/thanatos_agent/binary/src/main.rs @@ -4,8 +4,6 @@ windows_subsystem = "windows" )] -const CONFIG: &[u8] = include_bytes!(env!("CONFIG")); - fn main() { - thanatos_http::entrypoint(CONFIG); + thanatos::entrypoint(); } diff --git a/Payload_Type/thanatos/agent/thanatos_http/cdylib/Cargo.toml b/Payload_Type/thanatos/agent/thanatos_agent/cdylib/Cargo.toml similarity index 74% rename from Payload_Type/thanatos/agent/thanatos_http/cdylib/Cargo.toml rename to Payload_Type/thanatos/agent/thanatos_agent/cdylib/Cargo.toml index 0679057..098affa 100644 --- a/Payload_Type/thanatos/agent/thanatos_http/cdylib/Cargo.toml +++ b/Payload_Type/thanatos/agent/thanatos_agent/cdylib/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "thanatos_http_cdylib" +name = "thanatos_cdylib" authors.workspace = true edition.workspace = true homepage.workspace = true @@ -10,4 +10,4 @@ version.workspace = true crate-type = ["cdylib"] [dependencies] -thanatos_http = { path = ".." } +thanatos = { path = ".." } diff --git a/Payload_Type/thanatos/agent/thanatos_agent/cdylib/src/lib.rs b/Payload_Type/thanatos/agent/thanatos_agent/cdylib/src/lib.rs new file mode 100644 index 0000000..57d768b --- /dev/null +++ b/Payload_Type/thanatos/agent/thanatos_agent/cdylib/src/lib.rs @@ -0,0 +1,3 @@ +pub fn foo() { + thanatos::entrypoint(); +} diff --git a/Payload_Type/thanatos/agent/thanatos_core/src/guardrails.rs b/Payload_Type/thanatos/agent/thanatos_agent/src/guardrails.rs similarity index 77% rename from Payload_Type/thanatos/agent/thanatos_core/src/guardrails.rs rename to Payload_Type/thanatos/agent/thanatos_agent/src/guardrails.rs index ae05606..2334233 100644 --- a/Payload_Type/thanatos/agent/thanatos_core/src/guardrails.rs +++ b/Payload_Type/thanatos/agent/thanatos_agent/src/guardrails.rs @@ -1,18 +1,30 @@ -#[cfg(target_os = "linux")] +#[cfg(all( + target_os = "linux", + any( + feature = "domaincheck", + feature = "usernamecheck", + feature = "hostnamecheck", + ) +))] use crate::native::linux::system; -#[cfg(target_os = "windows")] +#[cfg(all( + target_os = "windows", + any( + feature = "domaincheck", + feature = "usernamecheck", + feature = "hostnamecheck", + ) +))] use crate::native::windows::system; -cfg_if::cfg_if! { - if #[cfg(feature = "crypto-system")] { - use cryptolib::hash::system::Sha256; - } else { - use cryptolib::hash::internal::Sha256; - } -} +#[cfg(feature = "crypto-system")] +use cryptolib::hash::system::Sha256; + +#[cfg(not(feature = "crypto-system"))] +use cryptolib::hash::internal::Sha256; -#[cfg(target_os = "linux")] +#[cfg(all(target_os = "linux", feature = "domaincheck"))] pub fn check_domain(domains: &[[u8; 32]]) -> bool { let check_domains = match system::domains() { Ok(check_domains) => check_domains, @@ -24,7 +36,7 @@ pub fn check_domain(domains: &[[u8; 32]]) -> bool { .any(|check_domain| check_hashlist_with(domains, &check_domain)) } -#[cfg(target_os = "windows")] +#[cfg(all(target_os = "windows", feature = "domaincheck"))] pub fn check_domain(domains: &[[u8; 32]]) -> bool { let domain = match system::domain() { Ok(domain) => domain, @@ -34,6 +46,7 @@ pub fn check_domain(domains: &[[u8; 32]]) -> bool { check_hashlist_with(domains, &domain) } +#[cfg(feature = "hostnamecheck")] pub fn check_hostname(hostnames: &[[u8; 32]]) -> bool { let hostname = match system::hostname() { Ok(hostname) => hostname, @@ -43,6 +56,7 @@ pub fn check_hostname(hostnames: &[[u8; 32]]) -> bool { check_hashlist_with(hostnames, &hostname) } +#[cfg(feature = "usernamecheck")] pub fn check_username(usernames: &[[u8; 32]]) -> bool { let username = match system::username() { Ok(username) => username, @@ -52,6 +66,12 @@ pub fn check_username(usernames: &[[u8; 32]]) -> bool { check_hashlist_with(usernames, &username) } +#[cfg(any( + feature = "usernamecheck", + feature = "hostnamecheck", + feature = "domaincheck", + test +))] fn check_hashlist_with(hlist: &[[u8; 32]], value: &str) -> bool { let value = value.to_lowercase(); diff --git a/Payload_Type/thanatos/agent/thanatos_agent/src/lib.rs b/Payload_Type/thanatos/agent/thanatos_agent/src/lib.rs new file mode 100644 index 0000000..d9d43dc --- /dev/null +++ b/Payload_Type/thanatos/agent/thanatos_agent/src/lib.rs @@ -0,0 +1,60 @@ +#![forbid(unsafe_code)] + +use config::ConfigVars; + +#[cfg(any( + feature = "usernamecheck", + feature = "hostnamecheck", + feature = "domaincheck", + test +))] +mod guardrails; + +pub mod logging; +pub mod native; + +pub fn entrypoint() { + let agent_config = if let Ok(c) = ConfigVars::parse() { + c + } else { + return; + }; + + #[cfg(feature = "usernamecheck")] + if let Ok(usernames) = agent_config.usernames() { + if !guardrails::check_username(&usernames) { + return; + } + } else { + return; + } + + #[cfg(feature = "hostnamecheck")] + if let Ok(hostnames) = agent_config.hostnames() { + if !guardrails::check_hostname(&hostnames) { + return; + } + } else { + return; + } + + #[cfg(feature = "domaincheck")] + if let Ok(domains) = agent_config.domains() { + if !guardrails::check_domain(&domains) { + return; + } + } else { + return; + } + + #[cfg(feature = "init-thread")] + std::thread::spawn(|| run_agent(agent_config)); + + #[cfg(all(target_os = "linux", feature = "init-fork"))] + native::linux::fork(|| run_agent(agent_config)); + + #[cfg(not(any(feature = "init-thread", feature = "init-fork")))] + run_agent(agent_config); +} + +fn run_agent(_agent_config: ConfigVars) {} diff --git a/Payload_Type/thanatos/agent/thanatos_core/src/logging.rs b/Payload_Type/thanatos/agent/thanatos_agent/src/logging.rs similarity index 100% rename from Payload_Type/thanatos/agent/thanatos_core/src/logging.rs rename to Payload_Type/thanatos/agent/thanatos_agent/src/logging.rs diff --git a/Payload_Type/thanatos/agent/thanatos_core/src/native/linux/mod.rs b/Payload_Type/thanatos/agent/thanatos_agent/src/native/linux/mod.rs similarity index 100% rename from Payload_Type/thanatos/agent/thanatos_core/src/native/linux/mod.rs rename to Payload_Type/thanatos/agent/thanatos_agent/src/native/linux/mod.rs diff --git a/Payload_Type/thanatos/agent/thanatos_core/src/native/linux/system.rs b/Payload_Type/thanatos/agent/thanatos_agent/src/native/linux/system.rs similarity index 100% rename from Payload_Type/thanatos/agent/thanatos_core/src/native/linux/system.rs rename to Payload_Type/thanatos/agent/thanatos_agent/src/native/linux/system.rs diff --git a/Payload_Type/thanatos/agent/thanatos_core/src/native/mod.rs b/Payload_Type/thanatos/agent/thanatos_agent/src/native/mod.rs similarity index 100% rename from Payload_Type/thanatos/agent/thanatos_core/src/native/mod.rs rename to Payload_Type/thanatos/agent/thanatos_agent/src/native/mod.rs diff --git a/Payload_Type/thanatos/agent/thanatos_core/src/native/windows/mod.rs b/Payload_Type/thanatos/agent/thanatos_agent/src/native/windows/mod.rs similarity index 100% rename from Payload_Type/thanatos/agent/thanatos_core/src/native/windows/mod.rs rename to Payload_Type/thanatos/agent/thanatos_agent/src/native/windows/mod.rs diff --git a/Payload_Type/thanatos/agent/thanatos_core/src/lib.rs b/Payload_Type/thanatos/agent/thanatos_core/src/lib.rs deleted file mode 100644 index f572758..0000000 --- a/Payload_Type/thanatos/agent/thanatos_core/src/lib.rs +++ /dev/null @@ -1,38 +0,0 @@ -#![forbid(unsafe_code)] - -use config::{ConfigVars, InitOption}; - -mod guardrails; -pub mod logging; -pub mod native; - -pub fn initialize_agent(f: F, config: ConfigVars<'static>) -where - F: Fn(ConfigVars<'static>) + Send + Sync + 'static, -{ - let domains = config.domains(); - if !domains.is_empty() && !guardrails::check_domain(domains) { - return; - } - - let hostnames = config.hostnames(); - if !hostnames.is_empty() && !guardrails::check_hostname(hostnames) { - return; - } - - let usernames = config.usernames(); - if !usernames.is_empty() && !guardrails::check_username(usernames) { - return; - } - - match config.init_option() { - InitOption::Thread => { - std::thread::spawn(move || f(config)); - } - #[cfg(target_os = "linux")] - InitOption::Fork => todo!(), - InitOption::None => { - f(config); - } - } -} diff --git a/Payload_Type/thanatos/agent/thanatos_http/Cargo.toml b/Payload_Type/thanatos/agent/thanatos_http/Cargo.toml deleted file mode 100644 index 3fbb1dd..0000000 --- a/Payload_Type/thanatos/agent/thanatos_http/Cargo.toml +++ /dev/null @@ -1,17 +0,0 @@ -[package] -name = "thanatos_http" -authors.workspace = true -edition.workspace = true -homepage.workspace = true -license.workspace = true -version.workspace = true - -[dependencies] -rmp.workspace = true -rmp-serde.workspace = true - -[dependencies.config] -path = "../config" - -[dependencies.thanatos_core] -path = "../thanatos_core" diff --git a/Payload_Type/thanatos/agent/thanatos_http/cdylib/src/lib.rs b/Payload_Type/thanatos/agent/thanatos_http/cdylib/src/lib.rs deleted file mode 100644 index c302449..0000000 --- a/Payload_Type/thanatos/agent/thanatos_http/cdylib/src/lib.rs +++ /dev/null @@ -1,7 +0,0 @@ -#![cfg(not(test))] - -const CONFIG: &[u8] = include_bytes!(env!("CONFIG")); - -pub fn foo() { - thanatos_http::entrypoint(CONFIG); -} diff --git a/Payload_Type/thanatos/agent/thanatos_http/src/lib.rs b/Payload_Type/thanatos/agent/thanatos_http/src/lib.rs deleted file mode 100644 index 0cffc52..0000000 --- a/Payload_Type/thanatos/agent/thanatos_http/src/lib.rs +++ /dev/null @@ -1,16 +0,0 @@ -#![forbid(unsafe_code)] -#![cfg_attr( - all(not(debug_assertions), target_os = "windows"), - windows_subsystem = "windows" -)] - -use config::ConfigVars; - -pub fn entrypoint(config_bytes: &'static [u8]) { - let config = rmp_serde::from_slice(config_bytes).unwrap(); - thanatos_core::initialize_agent(run_agent, config); -} - -fn run_agent(config: ConfigVars) { - thanatos_core::debug!(config); -} diff --git a/Payload_Type/thanatos/agent/utils/Cargo.toml b/Payload_Type/thanatos/agent/utils/Cargo.toml index 1d7f89e..888e2bc 100644 --- a/Payload_Type/thanatos/agent/utils/Cargo.toml +++ b/Payload_Type/thanatos/agent/utils/Cargo.toml @@ -8,5 +8,3 @@ version.workspace = true [dependencies] -serde.workspace = true -serde_bytes.workspace = true diff --git a/Payload_Type/thanatos/agent/utils/src/uuid.rs b/Payload_Type/thanatos/agent/utils/src/uuid.rs index 5d7bd9a..1abc461 100644 --- a/Payload_Type/thanatos/agent/utils/src/uuid.rs +++ b/Payload_Type/thanatos/agent/utils/src/uuid.rs @@ -1,13 +1,10 @@ //! Module for handling uuids in string and binary form use std::str::FromStr; -use serde::{Deserialize, Serialize}; -use serde_bytes::ByteArray; - /// Holds a Uuid #[repr(transparent)] -#[derive(Debug, Serialize, Deserialize)] -pub struct Uuid(#[serde(with = "serde_bytes")] ByteArray<16>); +#[derive(Debug)] +pub struct Uuid([u8; 16]); /// Error types when parsing Uuids #[derive(Debug)] @@ -21,7 +18,17 @@ pub enum UuidError { impl From<[u8; 16]> for Uuid { fn from(value: [u8; 16]) -> Self { - Self(ByteArray::new(value)) + Self(value) + } +} + +impl TryFrom<&[u8]> for Uuid { + type Error = UuidError; + + fn try_from(value: &[u8]) -> Result { + Ok(Uuid::from( + <[u8; 16]>::try_from(value).map_err(|_| UuidError::InvalidLength)?, + )) } } @@ -58,7 +65,7 @@ impl FromStr for Uuid { u[uidx] |= lsb; } - Ok(Self(ByteArray::new(u))) + Ok(Self(u)) } } @@ -133,7 +140,11 @@ impl AsRef<[u8; 16]> for Uuid { impl Uuid { /// Consumes the Uuid and returns the underlying data pub fn into_bytes(self) -> [u8; 16] { - self.0.into_array() + self.0 + } + + pub fn as_slice(&self) -> &[u8] { + self.0.as_slice() } } diff --git a/Payload_Type/thanatos/mythic/Makefile b/Payload_Type/thanatos/mythic/Makefile index 2c696ec..16a86ae 100644 --- a/Payload_Type/thanatos/mythic/Makefile +++ b/Payload_Type/thanatos/mythic/Makefile @@ -1,38 +1,2 @@ -TARGET := thanatos -SOURCES := $(wildcard ./**.go) -MODULE ?= ./... -GOARGS ?= -TEST ?= - -.PHONY: help -help: # Print out this help info - @awk 'match($$0, /^([a-zA-Z-]+):.*(#.*)/, m) { print m[1] ":", m[2] }' Makefile - -.PHONY: list -list: # List all the targets - @awk 'match($$0, /^([a-zA-Z-]+):/, m) { print m[1] }' Makefile - -.PHONY: clean -clean: # Remove the built server code - rm -vf $(TARGET) - -build: $(TARGET) # Build the Mythic server code - -$(TARGET): $(SOURCES) - go build -o $@ - -.PHONY: test -test: # Run the Go tests. Set 'GOARGS' to add additional "go test" arguments. Set 'MODULE' to the path of the package to test - go test $(GOARGS) $(MODULE) - -.PHONY: test-verbose -test-verbose: # Run the Go tests but in "verbose" mode - @$(MAKE) GOARGS="-v" test - -.PHONY: test-mockbuild -test-mockbuild: # Run all of the build tests with mock handlers - @$(MAKE) GOARGS="$(GOARGS) -run \"^TestPayloadMockBuild/$(TEST)\$\"" MODULE=./builder test - -.PHONY: test-fullbuild -test-fullbuild: # Run all of the build tests - @$(MAKE) GOARGS="$(GOARGS) -run \"^TestPayloadFullBuild/$(TEST)\$\"" MODULE=./builder test +protos: + protoc --proto_path=protos --go_out=. protos/config.proto diff --git a/Payload_Type/thanatos/mythic/builder/buildcommand.go b/Payload_Type/thanatos/mythic/builder/buildcommand.go index 121edf2..00f711d 100644 --- a/Payload_Type/thanatos/mythic/builder/buildcommand.go +++ b/Payload_Type/thanatos/mythic/builder/buildcommand.go @@ -1,31 +1,70 @@ -// Handles taking the parsed build parameters and converting it into the command used to -// build the payload package builder import ( "fmt" + "strings" ) -// Take the parsed build parameter command config and return a string containing the command to build -// the payload -func FormulateBuildCommand(configPath string, target string, payloadConfig ParsedPayloadParameters) string { - - profile := "" - if payloadConfig.C2Profiles.HttpProfile != nil { - profile = "http" - } else { - panic("Unimplemented build profile") - } - +func FormulateBuildCommand(target string, configPath string, config ParsedPayloadParameters) string { output := "" - switch payloadConfig.PayloadBuildParameters.Output { + switch config.BuildParameters.Output { case PayloadBuildParameterOutputFormatExecutable: output = "binary" default: panic("Unimplemented output") } - buildPackage := fmt.Sprintf("thanatos_%s_%s", profile, output) - cargoCommand := fmt.Sprintf("env CONFIG=%s cargo build -p %s --target %s --release", configPath, buildPackage, target) + buildPackage := fmt.Sprintf("thanatos_%s", output) + + features := []string{} + features = append(features, config.Commands...) + + if config.BuildParameters.CryptoLib == PayloadBuildParameterCryptoLibrarySystem { + features = append(features, "crypto-system") + } else { + features = append(features, "crypto-internal") + } + + switch config.BuildParameters.InitOptions { + case PayloadBuildParameterInitOptionFork: + features = append(features, "init-fork") + case PayloadBuildParameterInitOptionSpawnThread: + features = append(features, "init-thread") + } + + if len(config.BuildParameters.DomainList) > 0 { + features = append(features, "domaincheck") + } + + if len(config.BuildParameters.HostnameList) > 0 { + features = append(features, "hostnamecheck") + } + + if len(config.BuildParameters.UsernameList) > 0 { + features = append(features, "usernamecheck") + } + + if config.BuildParameters.TlsUntrusted { + features = append(features, "tlsuntrusted") + } + + if config.C2Profiles.HttpC2Profile != nil { + features = append(features, "http") + } + + if config.C2Profiles.TcpC2Profile != nil { + features = append(features, "tcp") + } + + featureString := strings.Join(features, ",") + + cargoCommand := fmt.Sprintf( + "env CONFIG=%s cargo build -p %s --target %s --features %s --release", + configPath, + buildPackage, + target, + featureString, + ) + return cargoCommand } diff --git a/Payload_Type/thanatos/mythic/builder/builder.go b/Payload_Type/thanatos/mythic/builder/builder.go index 711f9b3..41e3989 100644 --- a/Payload_Type/thanatos/mythic/builder/builder.go +++ b/Payload_Type/thanatos/mythic/builder/builder.go @@ -2,15 +2,12 @@ package builder import ( - "encoding/hex" "errors" - "fmt" "os" "path/filepath" thanatoserror "thanatos/errors" agentstructs "github.com/MythicMeta/MythicContainer/agent_structs" - "github.com/google/uuid" ) // Metadata defining the Mythic payload type @@ -214,96 +211,15 @@ var payloadDefinition = agentstructs.PayloadType{ BuildSteps: []agentstructs.BuildStep{}, } -// Stores all of the parsed payload build parameters. This includes both the payload -// parameters and the C2 profile parameters -type ParsedPayloadParameters struct { - // UUID of the agent - Uuid uuid.UUID - - // Selected OS - SelectedOS string - - // The payload parameters - PayloadBuildParameters ParsedBuildParameters - - // The configured C2 profile parameters - C2Profiles struct { - - // The parameters for the HTTP C2 profile - HttpProfile *HttpC2ProfileParameters - } -} - -func (p *ParsedPayloadParameters) String() string { - output := fmt.Sprintf("UUID=%s\n", p.Uuid.String()) - output += p.PayloadBuildParameters.String() - if p.C2Profiles.HttpProfile != nil { - output += p.C2Profiles.HttpProfile.String() - } - return output -} - -// Parses the user supplied build parameters -func parsePayloadParameters(buildMessage agentstructs.PayloadBuildMessage) (ParsedPayloadParameters, error) { - payloadUuid, err := uuid.Parse(buildMessage.PayloadUUID) - if err != nil { - return ParsedPayloadParameters{}, thanatoserror.Errorf("failed to parse the payload UUID: %v", err) - } - - payloadParameters := ParsedPayloadParameters{ - Uuid: payloadUuid, - SelectedOS: buildMessage.SelectedOS, - } - - buildParameters, err := parsePayloadBuildParameters(buildMessage) - if err != nil { - return payloadParameters, errors.Join(thanatoserror.New("failed to parse to payload build parameters"), err) - } - - payloadParameters.PayloadBuildParameters = buildParameters - - payloadParameters.C2Profiles.HttpProfile = nil - - for _, profileParameter := range buildMessage.C2Profiles { - if profileParameter.Name == "http" { - httpProfile, err := parseHttpProfileParameters(profileParameter) - if err != nil { - return payloadParameters, errors.Join(thanatoserror.New("failed to parse the profile parameters for the HTTP C2 profile"), err) - } - - payloadParameters.C2Profiles.HttpProfile = httpProfile - } - } - - return payloadParameters, nil -} - -// Converts the selected os and architecture from the build parameters to a formatted Rust -// target -func getRustTriple(os string, arch PayloadBuildParameterArchitecture) string { - target := "" - - switch arch { - case PayloadBuildParameterArchitectureAmd64: - target += "x86_64-" - case PayloadBuildParameterArchitectureX86: - target += "i686-" - } - - switch os { - case agentstructs.SUPPORTED_OS_LINUX: - target += "unknown-linux-gnu" - case agentstructs.SUPPORTED_OS_WINDOWS: - target += "pc-windows-gnu" - } - - return target +var builtinCommands = []string{ + "sleep", + "exit", } // Secondary entrypoint for the payload builder. This takes in the payload build message // and a handler which consists of a set of routines for doing long-running tasks and // Mythic RPC calls -func buildPayload(payloadBuildMsg agentstructs.PayloadBuildMessage, handler BuildHandler) agentstructs.PayloadBuildResponse { +func BuildPayload(handler BuildHandler, payloadBuildMsg agentstructs.PayloadBuildMessage) agentstructs.PayloadBuildResponse { // Create the build response payloadBuildResponse := agentstructs.PayloadBuildResponse{ PayloadUUID: payloadBuildMsg.PayloadUUID, @@ -312,28 +228,13 @@ func buildPayload(payloadBuildMsg agentstructs.PayloadBuildMessage, handler Buil } // Parse all of the payload parameters - payloadConfig, err := parsePayloadParameters(payloadBuildMsg) + parameters, err := parsePayloadParameters(payloadBuildMsg) if err != nil { payloadBuildResponse.BuildStdErr = errors.Join(thanatoserror.New("failed to parse the payload parameters"), err).Error() return payloadBuildResponse } - // Get the Rust target for the payload build - rustTarget := getRustTriple(payloadBuildMsg.SelectedOS, payloadConfig.PayloadBuildParameters.Architecture) - - // Print out the payload config - payloadBuildResponse.BuildMessage = "Payload Configuration:\n" - payloadBuildResponse.BuildMessage += payloadConfig.String() + "\n" - - // Serialize the payload config - serializedConfig, err := payloadConfig.Serialize() - if err != nil { - payloadBuildResponse.BuildStdErr = thanatoserror.Errorf("failed to serialize payload config: %s", err.Error()).Error() - return payloadBuildResponse - } - - payloadBuildResponse.BuildMessage += "Serialized Payload Configuration:\n" - payloadBuildResponse.BuildMessage += hex.Dump(serializedConfig) + payloadConfig := createConfig(parameters) configFile, err := os.CreateTemp("", "thanatos-config*") if err != nil { @@ -342,18 +243,33 @@ func buildPayload(payloadBuildMsg agentstructs.PayloadBuildMessage, handler Buil } defer os.Remove(configFile.Name()) - if _, err := configFile.Write(serializedConfig); err != nil { + if _, err := configFile.Write([]byte(payloadConfig.String())); err != nil { payloadBuildResponse.BuildStdErr = thanatoserror.Errorf("failed to write config to config file: %s", err.Error()).Error() return payloadBuildResponse } - buildCommand := FormulateBuildCommand(configFile.Name(), rustTarget, payloadConfig) + target := "" + switch parameters.BuildParameters.Architecture { + case PayloadBuildParameterArchitectureAmd64: + target = "x86_64-" + case PayloadBuildParameterArchitectureX86: + target = "i686-" + } + + switch payloadBuildMsg.SelectedOS { + case agentstructs.SUPPORTED_OS_LINUX: + target += "unknown-linux-gnu" + case agentstructs.SUPPORTED_OS_WINDOWS: + target += "pc-windows-gnu" + } + + buildCommand := FormulateBuildCommand(target, configFile.Name(), parameters) - payloadBuildResponse.BuildStdOut += "Build Command:\n" - payloadBuildResponse.BuildStdOut += buildCommand + payloadBuildResponse.BuildMessage += "Build Command:\n" + payloadBuildResponse.BuildMessage += buildCommand // Build the payload - payload, err := handler.Build(rustTarget, payloadConfig, buildCommand) + payload, err := handler.Build(target, parameters, buildCommand) if err != nil { payloadBuildResponse.BuildStdErr = errors.Join(thanatoserror.New("failed to build the payload"), err).Error() return payloadBuildResponse @@ -367,8 +283,7 @@ func buildPayload(payloadBuildMsg agentstructs.PayloadBuildMessage, handler Buil // Main entrypoint when Mythic executes the payload builder func mythicBuildPayloadFunction(payloadBuildMsg agentstructs.PayloadBuildMessage) agentstructs.PayloadBuildResponse { - handler := MythicPayloadHandler{} - return buildPayload(payloadBuildMsg, &handler) + return BuildPayload(MythicPayloadHandler{}, payloadBuildMsg) } // Initializes the payload build routines in Mythic diff --git a/Payload_Type/thanatos/mythic/builder/builder_test.go b/Payload_Type/thanatos/mythic/builder/builder_test.go index 613de53..3c8d401 100644 --- a/Payload_Type/thanatos/mythic/builder/builder_test.go +++ b/Payload_Type/thanatos/mythic/builder/builder_test.go @@ -2,352 +2,167 @@ package builder import ( - "encoding/json" - "fmt" - "io/fs" - "os" - "reflect" - "regexp" - "strings" "testing" agentstructs "github.com/MythicMeta/MythicContainer/agent_structs" - "github.com/MythicMeta/MythicContainer/mythicrpc" - "github.com/google/uuid" ) -const testDataPath string = "tests/buildtests" - -// Options for when testing if the build succeeded. This is for checking if the result -// BuildStdout, BuildStderr and BuildMessage contain correct values -type expectCompareOptions struct { - // Option to check if the build result contains a specified string - Contains *string `json:"contains"` - - // Option to check if the build result matches a regex pattern - Regex *string `json:"regex"` - - // Option to check if the build result matches a specified string exactly - Is *string `json:"is"` - - // Modifier signifying that the comparison should be case insensitive - Insensitive *bool `json:"case_insensitive"` -} - -// Expected BuildStdout, BuildStderr and BuildMessage values from a payload build -type expectValues struct { - // Whether the build should be successful or not - Success bool `json:"success"` - - // Expected build message - Message *expectCompareOptions `json:"message"` - - // Expected build stdout - Stdout *expectCompareOptions `json:"stdout"` - - // Expected build stderr - Stderr *expectCompareOptions `json:"stderr"` - - // Expected new filename - Filename *expectCompareOptions `json:"filename"` -} - -// Definition for a new test. This contains the build parameters for the build along with -// a set of expected results -type testSpec struct { - // Input filename for the build - Filename string `json:"filename"` - - // List of commands for the build - CommandList []string `json:"commands"` - - // Selected OS for the build - SelectedOS string `json:"selected_os"` - - // Payload parameters for the build - BuildParameters map[string]interface{} `json:"build_parameters"` - - // C2 profile parameters for the build - C2Profiles []agentstructs.PayloadBuildC2Profile `json:"c2profiles"` - - // Expected build results - Expect expectValues `json:"expect"` -} - -// Type which contains the mock implementations of the handler routines. This will -// essentially "no-op" expensive function or Mythic RPC calls -type MockBuildPayloadHandler struct{} - -// Mock implementation for the payload build -func (handler MockBuildPayloadHandler) Build(target string, config ParsedPayloadParameters, command string) ([]byte, error) { - return []byte{}, nil -} - -// Mock implementation for updating a build step in Mythic -func (handler MockBuildPayloadHandler) UpdateBuildStep(input mythicrpc.MythicRPCPayloadUpdateBuildStepMessage) (*mythicrpc.MythicRPCPayloadUpdateBuildStepMessageResponse, error) { - response := mythicrpc.MythicRPCPayloadUpdateBuildStepMessageResponse{ - Success: true, - Error: "", - } - return &response, nil -} - -// Type which contains the full implementations for building a payload. This will build -// the payload and install the required Rust tool chain. This will mock the Mythic RPC -// calls -type FullBuildPayloadHandler struct{} - -// Runs the real build command for the build handler -func (handler FullBuildPayloadHandler) Build(target string, config ParsedPayloadParameters, command string) ([]byte, error) { - return MythicPayloadHandler{}.Build(target, config, command) -} - -// Runs the mock Mythic RPC function -func (handler FullBuildPayloadHandler) UpdateBuildStep(input mythicrpc.MythicRPCPayloadUpdateBuildStepMessage) (*mythicrpc.MythicRPCPayloadUpdateBuildStepMessageResponse, error) { - return MockBuildPayloadHandler{}.UpdateBuildStep(input) -} - -// Prints out a set of data using the testing logger -func testLogPrintData(t *testing.T, data ...any) { - for _, v := range data { - p, err := json.MarshalIndent(v, "", " ") - if err != nil { - continue - } - - typeName := reflect.TypeOf(v).String() - t.Logf("%s:\n%s", typeName, string(p)) - } -} - -// Checks the payload build results with the expected results -func checkResults(t *testing.T, payloadUUID string, buildResult agentstructs.PayloadBuildResponse, testData testSpec) { - if buildResult.PayloadUUID != payloadUUID { - testLogPrintData(t, testData, buildResult) - t.Fatalf("Resulting payload UUID did not match expected UUID. Found '%s' expected '%s'", buildResult.PayloadUUID, payloadUUID) - } - - if buildResult.Success != testData.Expect.Success { - testLogPrintData(t, testData, buildResult) - t.Logf("(buildResult.Success = %t) != (testData.Expect.Success = %t)", buildResult.Success, testData.Expect.Success) - if buildResult.Success { - t.Fatal("Payload build was successful but the test expected it to fail") - } else { - t.Fatal("Payload build was unsuccessful but the test expected it to succeed") - } - } - - logMsgBuffer := []string{} - - if testData.Expect.Message != nil { - compareData := testData.Expect.Message - value := buildResult.BuildMessage - - if compareData.Contains != nil { - if !strings.Contains(value, *compareData.Contains) { - logMsgBuffer = append(logMsgBuffer, fmt.Sprintf("buildResult.BuildMessage:\n%s\n", buildResult.BuildMessage)) - logMsgBuffer = append(logMsgBuffer, fmt.Sprintf("testData.Expec.Message.Contains = %s", *compareData.Contains)) - logMsgBuffer = append(logMsgBuffer, "Expected build message does not match returned build message") - } - } else if compareData.Is != nil { - if value != *compareData.Is { - logMsgBuffer = append(logMsgBuffer, fmt.Sprintf("buildResult.BuildMessage:\n%s\n", buildResult.BuildMessage)) - logMsgBuffer = append(logMsgBuffer, fmt.Sprintf("testData.Expect.Message.Is = %s", *compareData.Is)) - logMsgBuffer = append(logMsgBuffer, "Expected build message does not match returned build message") - } - } else if compareData.Regex != nil { - re := regexp.MustCompile(*compareData.Regex) - if re.FindStringIndex(value) == nil { - logMsgBuffer = append(logMsgBuffer, fmt.Sprintf("buildResult.BuildMessage:\n%s\n", buildResult.BuildMessage)) - logMsgBuffer = append(logMsgBuffer, fmt.Sprintf("testData.Expect.Message.Regex = %s", *compareData.Regex)) - logMsgBuffer = append(logMsgBuffer, "Expected build message does not match returned build message") - } - } - } - - if testData.Expect.Stdout != nil { - compareData := testData.Expect.Stdout - value := buildResult.BuildStdOut - - if compareData.Contains != nil { - if !strings.Contains(value, *compareData.Contains) { - logMsgBuffer = append(logMsgBuffer, fmt.Sprintf("buildResult.BuildStdOut:\n%s\n", buildResult.BuildStdOut)) - logMsgBuffer = append(logMsgBuffer, fmt.Sprintf("testData.Expect.Stdout.Contains = %s", *compareData.Contains)) - logMsgBuffer = append(logMsgBuffer, "Expected build stdout does not match returned build stdout") - } - } else if compareData.Is != nil { - if value != *compareData.Is { - logMsgBuffer = append(logMsgBuffer, fmt.Sprintf("buildResult.BuildStdOut:\n%s\n", buildResult.BuildStdOut)) - logMsgBuffer = append(logMsgBuffer, fmt.Sprintf("testData.Expect.StdOut.Is = %s", *compareData.Is)) - logMsgBuffer = append(logMsgBuffer, "Expected build stdout does not match returned build stdout") - } - } else if compareData.Regex != nil { - re := regexp.MustCompile(*compareData.Regex) - if re.FindStringIndex(value) == nil { - logMsgBuffer = append(logMsgBuffer, fmt.Sprintf("buildResult.BuildStdOut:\n%s\n", buildResult.BuildStdOut)) - logMsgBuffer = append(logMsgBuffer, fmt.Sprintf("testData.Expect.StdOut.Regex = %s", *compareData.Regex)) - logMsgBuffer = append(logMsgBuffer, "Expected build stdout does not match returned build stdout") - } - } - } - - if testData.Expect.Stderr != nil { - compareData := testData.Expect.Stderr - value := buildResult.BuildStdErr - - if compareData.Contains != nil { - if !strings.Contains(value, *compareData.Contains) { - logMsgBuffer = append(logMsgBuffer, fmt.Sprintf("buildResult.BuildStdErr:\n%s\n", buildResult.BuildStdErr)) - logMsgBuffer = append(logMsgBuffer, fmt.Sprintf("testData.Expect.Stderr.Contains = %s", *compareData.Contains)) - logMsgBuffer = append(logMsgBuffer, "Expected build stderr does not match returned build stderr") - } - } else if compareData.Is != nil { - if value != *compareData.Is { - logMsgBuffer = append(logMsgBuffer, fmt.Sprintf("buildResult.BuildStdErr:\n%s\n", buildResult.BuildStdErr)) - logMsgBuffer = append(logMsgBuffer, fmt.Sprintf("testData.Expect.Stderr.Is = %s", *compareData.Is)) - logMsgBuffer = append(logMsgBuffer, "Expected build stderr does not match returned build stderr") - } - } else if compareData.Regex != nil { - re := regexp.MustCompile(*compareData.Regex) - if re.FindStringIndex(value) == nil { - logMsgBuffer = append(logMsgBuffer, fmt.Sprintf("buildResult.BuildStdErr:\n%s\n", buildResult.BuildStdErr)) - logMsgBuffer = append(logMsgBuffer, fmt.Sprintf("testData.Expect.Stderr.Regex = %s", *compareData.Regex)) - logMsgBuffer = append(logMsgBuffer, "Expected build stderr does not match returned build stderr") - } - } - } - - if testData.Expect.Filename != nil { - if buildResult.UpdatedFilename != nil { - compareData := testData.Expect.Filename - value := *buildResult.UpdatedFilename - - if compareData.Contains != nil { - if !strings.Contains(value, *compareData.Contains) { - logMsgBuffer = append(logMsgBuffer, fmt.Sprintf("buildResult.UpdatedFilename = %s", *buildResult.UpdatedFilename)) - logMsgBuffer = append(logMsgBuffer, fmt.Sprintf("testData.Expect.Filename.Contains = %s", *compareData.Contains)) - logMsgBuffer = append(logMsgBuffer, "Expected updated filename does not match returned filename") - } - } else if compareData.Is != nil { - if value != *compareData.Is { - logMsgBuffer = append(logMsgBuffer, fmt.Sprintf("buildResult.UpdatedFilename = %s", *buildResult.UpdatedFilename)) - logMsgBuffer = append(logMsgBuffer, fmt.Sprintf("testData.Expect.Filename.Is = %s", *compareData.Is)) - logMsgBuffer = append(logMsgBuffer, "Expected updated filename does not match returned filename") - } - } else if compareData.Regex != nil { - re := regexp.MustCompile(*compareData.Regex) - if re.FindStringIndex(value) == nil { - logMsgBuffer = append(logMsgBuffer, fmt.Sprintf("buildResult.UpdatedFilename = %s", *buildResult.UpdatedFilename)) - logMsgBuffer = append(logMsgBuffer, fmt.Sprintf("testData.Expect.Filename.Regex = %s", *compareData.Regex)) - logMsgBuffer = append(logMsgBuffer, "Expected updated filename does not match returned filename") - } - } - } else { - logMsgFormatStr := "%s = %s" - logMsg := "" - if testData.Expect.Filename.Contains != nil { - logMsg = fmt.Sprintf(logMsgFormatStr, "testData.Expect.Filename.Contains", testData.Expect.Filename.Contains) - } else if testData.Expect.Filename.Is != nil { - logMsg = fmt.Sprintf(logMsgFormatStr, "testData.Expect.Filename.Is", testData.Expect.Filename.Is) - } else if testData.Expect.Filename.Regex != nil { - logMsg = fmt.Sprintf(logMsgFormatStr, "testData.Expect.Filename.Regex", testData.Expect.Filename.Regex) - } - - if testData.Expect.Filename.Insensitive != nil { - logMsg = fmt.Sprintf("%s, case_insensitive = %t", logMsg, *testData.Expect.Filename.Insensitive) - } - - logMsgBuffer = append(logMsgBuffer, "buildResult.UpdatedFilename = nil") - logMsgBuffer = append(logMsgBuffer, logMsg) - logMsgBuffer = append(logMsgBuffer, "Build result did not return an updated filename but expected it to be present") - } - } - - if len(logMsgBuffer) > 0 { - testLogPrintData(t, testData, buildResult) - for _, m := range logMsgBuffer { - t.Log(m) - } - - t.Logf("Test '%s' failed", t.Name()) - t.Fail() - } -} - -// Function which runs all of the tests with a specified handler -func runTest(t *testing.T, handler BuildHandler, testData testSpec) { - testLogPrintData(t, testData) - - payloadUUID := uuid.NewString() - payloadFileUUID := uuid.NewString() - - payloadBuildMsg := agentstructs.PayloadBuildMessage{ - PayloadType: "thanatos", - Filename: testData.Filename, - CommandList: testData.CommandList, - SelectedOS: testData.SelectedOS, - BuildParameters: agentstructs.PayloadBuildArguments{ - Parameters: testData.BuildParameters, +var testCases = []TestCase{ + { + Name: "linux_amd64_basic", + Filename: "thanatos", + SelectedOS: agentstructs.SUPPORTED_OS_LINUX, + CommandList: []string{ + "exit", + "sleep", }, - C2Profiles: testData.C2Profiles, - WrappedPayload: nil, - WrappedPayloadUUID: nil, - PayloadUUID: payloadUUID, - PayloadFileUUID: payloadFileUUID, - } - - buildResult := buildPayload(payloadBuildMsg, handler) - t.Logf("[BUILD MESSAGE]: %s", buildResult.BuildMessage) - t.Logf("[BUILD STDOUT]: %s", buildResult.BuildStdOut) - t.Logf("[BUILD STDERR]: %s", buildResult.BuildStdErr) - checkResults(t, payloadUUID, buildResult, testData) -} - -func setupTests(t *testing.T, handler BuildHandler) { - if err := os.Chdir(".."); err != nil { - t.Fatal(err) - } - - testDir := os.DirFS(testDataPath).(fs.ReadDirFS) - testSpecs, err := testDir.ReadDir(".") - if err != nil { - t.Fatal(err) - } - - for _, specPath := range testSpecs { - if specPath.IsDir() { - continue - } - - testRawData, err := fs.ReadFile(testDir, specPath.Name()) - if err != nil { - t.Fatal(err) - } - - testData := testSpec{} - if err := json.Unmarshal(testRawData, &testData); err != nil { - t.Fatal(err) - } - - testName := strings.TrimSuffix(specPath.Name(), ".json") - t.Logf("%s\n", testName) - - t.Run(testName, func(t *testing.T) { - t.Parallel() - runTest(t, handler, testData) - }) - - } + BuildParameters: map[string]interface{}{ + "architecture": string(PayloadBuildParameterArchitectureAmd64), + "initoptions": string(PayloadBuildParameterInitOptionNone), + "connection_retries": 1, + "cryptolib": string(PayloadBuildParameterCryptoLibraryInternal), + "working_hours": "00:00-23:59", + "output": string(PayloadBuildParameterOutputFormatExecutable), + }, + C2Profiles: []agentstructs.PayloadBuildC2Profile{ + { + Name: "http", + IsP2P: false, + Parameters: map[string]interface{}{ + "callback_port": 80, + "killdate": "2099-01-01", + "encrypted_exchange_check": true, + "callback_jitter": 23, + "headers": map[string]interface{}{ + "User-Agent": "test", + }, + "AESPSK": map[string]string{ + "value": "none", + }, + "callback_host": "http://mythic", + "get_uri": "index", + "post_uri": "data", + "query_path_name": "q", + "proxy_host": "", + "proxy_user": "", + "proxy_port": "", + "proxy_pass": "", + "callback_interval": 10, + }, + }, + }, + Expect: TestCaseResult{ + Success: true, + BuildCommand: "cargo build -p thanatos_binary --target x86_64-unknown-linux-gnu --features crypto-internal,http --release", + }, + }, + { + Name: "linux_amd64_system_crypto", + Filename: "thanatos", + SelectedOS: agentstructs.SUPPORTED_OS_LINUX, + CommandList: []string{ + "exit", + "sleep", + }, + BuildParameters: map[string]interface{}{ + "architecture": string(PayloadBuildParameterArchitectureAmd64), + "initoptions": string(PayloadBuildParameterInitOptionNone), + "connection_retries": 1, + "cryptolib": string(PayloadBuildParameterCryptoLibrarySystem), + "working_hours": "00:00-23:59", + "output": string(PayloadBuildParameterOutputFormatExecutable), + }, + C2Profiles: []agentstructs.PayloadBuildC2Profile{ + { + Name: "http", + IsP2P: false, + Parameters: map[string]interface{}{ + "callback_port": 80, + "killdate": "2099-01-01", + "encrypted_exchange_check": true, + "callback_jitter": 23, + "headers": map[string]interface{}{ + "User-Agent": "test", + }, + "AESPSK": map[string]string{ + "value": "none", + }, + "callback_host": "http://mythic", + "get_uri": "index", + "post_uri": "data", + "query_path_name": "q", + "proxy_host": "", + "proxy_user": "", + "proxy_port": "", + "proxy_pass": "", + "callback_interval": 10, + }, + }, + }, + Expect: TestCaseResult{ + Success: true, + BuildCommand: "cargo build -p thanatos_binary --target x86_64-unknown-linux-gnu --features crypto-system,http --release", + }, + }, + { + Name: "linux_amd64_hostnames", + Filename: "thanatos", + SelectedOS: agentstructs.SUPPORTED_OS_LINUX, + CommandList: []string{ + "exit", + "sleep", + }, + BuildParameters: map[string]interface{}{ + "architecture": string(PayloadBuildParameterArchitectureAmd64), + "initoptions": string(PayloadBuildParameterInitOptionNone), + "connection_retries": 1, + "cryptolib": string(PayloadBuildParameterCryptoLibraryInternal), + "working_hours": "00:00-23:59", + "output": string(PayloadBuildParameterOutputFormatExecutable), + "hostnames": []interface{}{ + "myhost", + }, + }, + C2Profiles: []agentstructs.PayloadBuildC2Profile{ + { + Name: "http", + IsP2P: false, + Parameters: map[string]interface{}{ + "callback_port": 80, + "killdate": "2099-01-01", + "encrypted_exchange_check": true, + "callback_jitter": 23, + "headers": map[string]interface{}{ + "User-Agent": "test", + }, + "AESPSK": map[string]string{ + "value": "none", + }, + "callback_host": "http://mythic", + "get_uri": "index", + "post_uri": "data", + "query_path_name": "q", + "proxy_host": "", + "proxy_user": "", + "proxy_port": "", + "proxy_pass": "", + "callback_interval": 10, + }, + }, + }, + Expect: TestCaseResult{ + Success: true, + BuildCommand: "cargo build -p thanatos_binary --target x86_64-unknown-linux-gnu --features crypto-internal,hostnamecheck,http --release", + }, + }, } // Test function which mocks all of the payload building and tests the build responses func TestPayloadMockBuild(t *testing.T) { - handler := MockBuildPayloadHandler{} - setupTests(t, handler) + RunTestCases(t, testCases, MockBuildPayloadHandler{}) } // Test function which will build the payload in the test func TestPayloadFullBuild(t *testing.T) { - handler := FullBuildPayloadHandler{} - setupTests(t, handler) + RunTestCases(t, testCases, FullBuildPayloadHandler{}) } diff --git a/Payload_Type/thanatos/mythic/builder/buildparameters.go b/Payload_Type/thanatos/mythic/builder/buildparameters.go new file mode 100644 index 0000000..e2c008f --- /dev/null +++ b/Payload_Type/thanatos/mythic/builder/buildparameters.go @@ -0,0 +1,164 @@ +// Handles parsing all of the payload parameters +package builder + +import ( + "errors" + thanatoserror "thanatos/errors" + + agentstructs "github.com/MythicMeta/MythicContainer/agent_structs" +) + +// Strongly type struct containing all of the build parameters from Mythic +type ParsedBuildParameters struct { + // Supported architectures of the agent + Architecture PayloadBuildParameterArchitecture + + // Agent's initial exection parameters + InitOptions PayloadBuildParameterInitOptions + + // Number of tries to reconnect to Mythic on failed connections + ConnectionRetries uint32 + + // Library for doing crypto + CryptoLib PayloadBuildParameterCryptoLibrary + + // Working hours + WorkingHours ParsedWorkingHours + + // List of domains for execution guardrails + DomainList []string + + // List of hostnames for execution guardrails + HostnameList []string + + // List of usernames for execution guardrails + UsernameList []string + + // Options for static linking + StaticOptions []PayloadBuildParameterStaticOption + + // Whether the agent should connect to self signed TLS certificates + TlsUntrusted bool + + // Initial spawnto value + SpawnTo string + + // Export name for shared libraries + LibExportName string + + // Output format + Output PayloadBuildParameterOutputFormat +} + +// Parses the build parameters from Mythic to a strongly typed structure +func parsePayloadBuildParameters(buildMessage agentstructs.PayloadBuildMessage) (ParsedBuildParameters, error) { + const errorFormatStr string = "failed to get the '%s' value from the payload build parameters: %s" + + parameters := buildMessage.BuildParameters + + parsedParameters := ParsedBuildParameters{} + + architecture, err := parameters.GetStringArg("architecture") + if err != nil { + return parsedParameters, thanatoserror.Errorf(errorFormatStr, "architecture", err.Error()) + } + + parsedParameters.Architecture = PayloadBuildParameterArchitecture(architecture) + + initOptions, err := parameters.GetStringArg("initoptions") + if err != nil { + return parsedParameters, thanatoserror.Errorf(errorFormatStr, "initoptions", err.Error()) + } + + if initOptions == string(PayloadBuildParameterInitOptionFork) && buildMessage.SelectedOS == agentstructs.SUPPORTED_OS_WINDOWS { + return parsedParameters, thanatoserror.New("cannot build a Windows payload with the fork initial execution option") + } + + parsedParameters.InitOptions = PayloadBuildParameterInitOptions(initOptions) + + connectionRetries, err := parameters.GetNumberArg("connection_retries") + if err != nil { + return parsedParameters, thanatoserror.Errorf(errorFormatStr, "connection_retries", err.Error()) + } + + if connectionRetries <= 0 { + return parsedParameters, thanatoserror.New("connection_retries value is <= 0") + } + + parsedParameters.ConnectionRetries = uint32(connectionRetries) + + cryptoLib, err := parameters.GetStringArg("cryptolib") + if err != nil { + return parsedParameters, thanatoserror.Errorf(errorFormatStr, "cryptolib", err.Error()) + } + + parsedParameters.CryptoLib = PayloadBuildParameterCryptoLibrary(cryptoLib) + + workingHoursStr, err := parameters.GetStringArg("working_hours") + if err != nil { + return parsedParameters, thanatoserror.Errorf(errorFormatStr, "working_hours", err.Error()) + } + + workingHours, err := parseWorkingHours(workingHoursStr) + if err != nil { + return parsedParameters, errors.Join(thanatoserror.New("failed to parse the payload's working hours"), err) + } + + if workingHours.StartTime >= workingHours.EndTime { + return parsedParameters, thanatoserror.New("working hours start time is after the working hours end time") + } + + parsedParameters.WorkingHours = workingHours + + if domainsList, err := parameters.GetArrayArg("domains"); err == nil { + parsedParameters.DomainList = domainsList + } else { + parsedParameters.DomainList = []string{} + } + + if hostnamesList, err := parameters.GetArrayArg("hostnames"); err == nil { + parsedParameters.HostnameList = hostnamesList + } else { + parsedParameters.HostnameList = []string{} + } + + if usernamesList, err := parameters.GetArrayArg("usernames"); err == nil { + parsedParameters.UsernameList = usernamesList + } else { + parsedParameters.UsernameList = []string{} + } + + staticOptions, err := parameters.GetArrayArg("static") + if err == nil { + for _, option := range staticOptions { + parsedParameters.StaticOptions = append(parsedParameters.StaticOptions, PayloadBuildParameterStaticOption(option)) + } + } else { + parsedParameters.StaticOptions = []PayloadBuildParameterStaticOption{} + } + + tlsuntrusted, err := parameters.GetBooleanArg("tlsuntrusted") + if err == nil { + parsedParameters.TlsUntrusted = tlsuntrusted + } else { + parsedParameters.TlsUntrusted = false + } + + spawnto, err := parameters.GetStringArg("spawnto") + if err == nil { + parsedParameters.SpawnTo = spawnto + } else { + parsedParameters.SpawnTo = "" + } + + parsedParameters.LibExportName, _ = parameters.GetStringArg("libexport") + + output, err := parameters.GetStringArg("output") + if err != nil { + return parsedParameters, thanatoserror.Errorf(errorFormatStr, "output", err.Error()) + } + + parsedParameters.Output = PayloadBuildParameterOutputFormat(output) + + return parsedParameters, nil +} diff --git a/Payload_Type/thanatos/mythic/builder/buildparameters_parser.go b/Payload_Type/thanatos/mythic/builder/buildparameters_parser.go deleted file mode 100644 index 5bb8ed5..0000000 --- a/Payload_Type/thanatos/mythic/builder/buildparameters_parser.go +++ /dev/null @@ -1,332 +0,0 @@ -// Handles parsing all of the payload parameters -package builder - -import ( - "bytes" - "crypto/sha256" - "errors" - "fmt" - "strings" - thanatoserror "thanatos/errors" - - agentstructs "github.com/MythicMeta/MythicContainer/agent_structs" - "github.com/vmihailenco/msgpack/v5" -) - -// Strongly type struct containing all of the build parameters from Mythic -type ParsedBuildParameters struct { - // Supported architectures of the agent - Architecture PayloadBuildParameterArchitecture - - // Agent's initial exection parameters - InitOptions PayloadBuildParameterInitOptions - - // Number of tries to reconnect to Mythic on failed connections - ConnectionRetries int - - // Library for doing crypto - CryptoLib PayloadBuildParameterCryptoLibrary - - // Working hours - WorkingHours ParsedWorkingHours - - // List of domains for execution guardrails - DomainList []string - - // List of hostnames for execution guardrails - HostnameList []string - - // List of usernames for execution guardrails - UsernameList []string - - // Options for static linking - StaticOptions []PayloadBuildParameterStaticOption - - // Whether the agent should connect to self signed TLS certificates - TlsUntrusted bool - - // Initial spawnto value - SpawnTo string - - // Output format for the agent - Output PayloadBuildParameterOutputFormat -} - -/* -/// Configuration option for the initial payload execution -#[derive(Serialize_repr, Deserialize_repr, Copy, Clone, Debug)] -#[repr(u8)] -pub enum InitOption { - /// Payload should not do anything special when executed - None = 0, - - /// Payload should run in a new thread - Thread = 1, - - /// Payload should fork to the background - Daemonize = 2, -} -*/ - -type SerializedConfigInitOption byte - -const ( - SerializedConfigInitOptionNone SerializedConfigInitOption = 0 - SerializedConfigInitOptionThread SerializedConfigInitOption = 1 - SerializedConfigInitOptionFork SerializedConfigInitOption = 2 -) - -/* -/// Holds a Uuid -#[repr(transparent)] -#[derive(Debug, Serialize, Deserialize)] -pub struct Uuid([u8; 16]); - - -/// Payload configuration variables -#[derive(Serialize, Deserialize, Debug)] -pub struct ConfigVars<'a> { - uuid: Uuid, - init_option: InitOption, - working_hours_start: u64, - working_hours_end: u64, - connection_retries: u32, - domains: Vec<[u8; 32]>, - hostnames: Vec<[u8; 32]>, - usernames: Vec<[u8; 32]>, - tlsuntrusted: bool, - spawn_to: &'a str, - http_profile: Option>, -} -*/ - -// Format for the serialized payload config -type SerializedBuildParameterFormat struct { - Uuid [16]byte `msgpack:"uuid"` - InitOption SerializedConfigInitOption `msgpack:"init_option"` - WorkingHoursStart uint64 `msgpack:"working_hours_start"` - WorkingHoursEnd uint64 `msgpack:"working_hours_end"` - ConnectionRetries uint32 `msgpack:"connection_retries"` - Domains [][32]byte `msgpack:"domains"` - Hostnames [][32]byte `msgpack:"hostnames"` - Usernames [][32]byte `msgpack:"usernames"` - TlsUntrusted bool `msgpack:"tlsuntrusted"` - SpawnTo string `msgpack:"spawn_to"` - HttpProfile *HttpC2ProfileParameters `msgpack:"http_profile,omitempty"` -} - -func (p *ParsedPayloadParameters) Serialize() ([]byte, error) { - uuidBytes, err := p.Uuid.MarshalBinary() - fmt.Printf("%x\n", uuidBytes) - if err != nil { - return []byte{}, thanatoserror.Errorf("failed to marshal payload UUID: %s", err.Error()) - } - - initOption := SerializedConfigInitOptionNone - - switch p.PayloadBuildParameters.InitOptions { - case PayloadBuildParameterInitOptionNone: - initOption = SerializedConfigInitOptionNone - case PayloadBuildParameterInitOptionSpawnThread: - initOption = SerializedConfigInitOptionThread - case PayloadBuildParameterInitOptionFork: - initOption = SerializedConfigInitOptionFork - } - - domains := [][32]byte{} - for _, domain := range p.PayloadBuildParameters.DomainList { - domain = strings.ToLower(domain) - domains = append(domains, sha256.Sum256([]byte(domain))) - } - - hostnames := [][32]byte{} - for _, hostname := range p.PayloadBuildParameters.HostnameList { - hostname = strings.ToLower(hostname) - hostnames = append(hostnames, sha256.Sum256([]byte(hostname))) - } - - usernames := [][32]byte{} - for _, username := range p.PayloadBuildParameters.UsernameList { - username = strings.ToLower(username) - usernames = append(usernames, sha256.Sum256([]byte(username))) - } - - serializedFormat := SerializedBuildParameterFormat{ - Uuid: [16]byte(uuidBytes), - InitOption: initOption, - WorkingHoursStart: uint64(p.PayloadBuildParameters.WorkingHours.StartTime.Seconds()), - WorkingHoursEnd: uint64(p.PayloadBuildParameters.WorkingHours.EndTime.Seconds()), - ConnectionRetries: uint32(p.PayloadBuildParameters.ConnectionRetries), - Domains: domains, - Hostnames: hostnames, - Usernames: usernames, - TlsUntrusted: p.PayloadBuildParameters.TlsUntrusted, - SpawnTo: p.PayloadBuildParameters.SpawnTo, - } - - if p.C2Profiles.HttpProfile != nil { - serializedFormat.HttpProfile = p.C2Profiles.HttpProfile - } - - var buffer bytes.Buffer - encoder := msgpack.NewEncoder(&buffer) - encoder.UseArrayEncodedStructs(true) - encoder.UseCompactFloats(true) - encoder.UseCompactInts(true) - encoder.UseInternedStrings(true) - - if err := encoder.Encode(&serializedFormat); err != nil { - return []byte{}, thanatoserror.Errorf("failed to serialize payload config: %s", err.Error()) - } - - return buffer.Bytes(), nil -} - -func (p *ParsedBuildParameters) String() string { - output := "" - - initOption := "none" - - switch p.InitOptions { - case PayloadBuildParameterInitOptionFork: - initOption = "fork" - case PayloadBuildParameterInitOptionSpawnThread: - initOption = "thread" - } - - output += fmt.Sprintf("INIT_OPTION=%s\n", initOption) - - outputFormat := "WORKING_HOURS_START=%0.0f\n" + - "WORKING_HOURS_END=%0.0f\n" + - "CONNECTION_RETRIES=%d\n" - - output += fmt.Sprintf(outputFormat, p.WorkingHours.StartTime.Seconds(), p.WorkingHours.EndTime.Seconds(), p.ConnectionRetries) - - if len(p.DomainList) > 0 { - output += fmt.Sprintf("DOMAIN_LIST=%s\n", strings.Join(p.DomainList, ",")) - } - - if len(p.HostnameList) > 0 { - output += fmt.Sprintf("HOSTNAME_LIST=%s\n", strings.Join(p.HostnameList, ",")) - } - - if len(p.UsernameList) > 0 { - output += fmt.Sprintf("USERNAME_LIST=%s\n", strings.Join(p.UsernameList, ",")) - } - - output += fmt.Sprintf("TLS_UNTRUSTED=%t\n", p.TlsUntrusted) - if len(p.SpawnTo) > 0 { - output += fmt.Sprintf("SPAWN_TO=%s\n", p.SpawnTo) - } - - return output -} - -// Parses the build parameters from Mythic to a strongly typed structure -func parsePayloadBuildParameters(buildMessage agentstructs.PayloadBuildMessage) (ParsedBuildParameters, error) { - const errorFormatStr string = "failed to get the '%s' value from the payload build parameters: %s" - - configuredOS := buildMessage.SelectedOS - _ = configuredOS - parameters := buildMessage.BuildParameters - - parsedParameters := ParsedBuildParameters{} - - architecture, err := parameters.GetStringArg("architecture") - if err != nil { - return parsedParameters, thanatoserror.Errorf(errorFormatStr, "architecture", err.Error()) - } - - parsedParameters.Architecture = PayloadBuildParameterArchitecture(architecture) - - initOptions, err := parameters.GetStringArg("initoptions") - if err != nil { - return parsedParameters, thanatoserror.Errorf(errorFormatStr, "initoptions", err.Error()) - } - - parsedParameters.InitOptions = PayloadBuildParameterInitOptions(initOptions) - - connectionRetries, err := parameters.GetNumberArg("connection_retries") - if err != nil { - return parsedParameters, thanatoserror.Errorf(errorFormatStr, "connection_retries", err.Error()) - } - - if connectionRetries <= 0 { - return parsedParameters, thanatoserror.New("connection_retries value is <= 0") - } - - parsedParameters.ConnectionRetries = int(connectionRetries) - - cryptoLib, err := parameters.GetStringArg("cryptolib") - if err != nil { - return parsedParameters, thanatoserror.Errorf(errorFormatStr, "cryptolib", err.Error()) - } - - parsedParameters.CryptoLib = PayloadBuildParameterCryptoLibrary(cryptoLib) - - workingHoursStr, err := parameters.GetStringArg("working_hours") - if err != nil { - return parsedParameters, thanatoserror.Errorf(errorFormatStr, "working_hours", err.Error()) - } - - workingHours, err := parseWorkingHours(workingHoursStr) - if err != nil { - return parsedParameters, errors.Join(thanatoserror.New("failed to parse the payload's working hours"), err) - } - - if workingHours.StartTime >= workingHours.EndTime { - return parsedParameters, thanatoserror.New("working hours start time is after the working hours end time") - } - - parsedParameters.WorkingHours = workingHours - - if domainsList, err := parameters.GetArrayArg("domains"); err == nil { - parsedParameters.DomainList = domainsList - } else { - parsedParameters.DomainList = []string{} - } - - if hostnamesList, err := parameters.GetArrayArg("hostnames"); err == nil { - parsedParameters.HostnameList = hostnamesList - } else { - parsedParameters.HostnameList = []string{} - } - - if usernamesList, err := parameters.GetArrayArg("usernames"); err == nil { - parsedParameters.UsernameList = usernamesList - } else { - parsedParameters.UsernameList = []string{} - } - - staticOptions, err := parameters.GetArrayArg("static") - if err == nil { - for _, option := range staticOptions { - parsedParameters.StaticOptions = append(parsedParameters.StaticOptions, PayloadBuildParameterStaticOption(option)) - } - } else { - parsedParameters.StaticOptions = []PayloadBuildParameterStaticOption{} - } - - tlsuntrusted, err := parameters.GetBooleanArg("tlsuntrusted") - if err == nil { - parsedParameters.TlsUntrusted = tlsuntrusted - } else { - parsedParameters.TlsUntrusted = false - } - - spawnto, err := parameters.GetStringArg("spawnto") - if err == nil { - parsedParameters.SpawnTo = spawnto - } else { - parsedParameters.SpawnTo = "" - } - - output, err := parameters.GetStringArg("output") - if err != nil { - return parsedParameters, thanatoserror.Errorf(errorFormatStr, "output", err.Error()) - } - - parsedParameters.Output = PayloadBuildParameterOutputFormat(output) - - return parsedParameters, nil -} diff --git a/Payload_Type/thanatos/mythic/builder/config.go b/Payload_Type/thanatos/mythic/builder/config.go new file mode 100644 index 0000000..f0186aa --- /dev/null +++ b/Payload_Type/thanatos/mythic/builder/config.go @@ -0,0 +1,79 @@ +package builder + +import ( + "crypto/sha256" + "thanatos/pb/config" +) + +func createConfig(payloadParameters ParsedPayloadParameters) *config.Config { + payloadConfig := config.Config{} + + payloadConfig.Uuid = payloadParameters.Uuid[:] + + payloadConfig.WorkingHoursStart = int64(payloadParameters.BuildParameters.WorkingHours.StartTime.Seconds()) + payloadConfig.WorkingHoursEnd = int64(payloadParameters.BuildParameters.WorkingHours.EndTime.Seconds()) + + payloadConfig.ConnectionRetries = payloadParameters.BuildParameters.ConnectionRetries + payloadConfig.SpawnTo = payloadParameters.BuildParameters.SpawnTo + + if len(payloadParameters.BuildParameters.DomainList) > 0 { + hashedDomains := make([]byte, len(payloadParameters.BuildParameters.DomainList)*32) + for _, domain := range payloadParameters.BuildParameters.DomainList { + h := sha256.New() + h.Write([]byte(domain)) + hashedDomains = append(hashedDomains, h.Sum(nil)...) + } + + payloadConfig.Domains = hashedDomains + } + + if len(payloadParameters.BuildParameters.HostnameList) > 0 { + hashedHostnames := make([]byte, len(payloadParameters.BuildParameters.HostnameList)*32) + for _, domain := range payloadParameters.BuildParameters.HostnameList { + h := sha256.New() + h.Write([]byte(domain)) + hashedHostnames = append(hashedHostnames, h.Sum(nil)...) + } + + payloadConfig.Hostnames = hashedHostnames + } + + if len(payloadParameters.BuildParameters.UsernameList) > 0 { + hashedUsernames := make([]byte, len(payloadParameters.BuildParameters.UsernameList)*32) + for _, domain := range payloadParameters.BuildParameters.UsernameList { + h := sha256.New() + h.Write([]byte(domain)) + hashedUsernames = append(hashedUsernames, h.Sum(nil)...) + } + + payloadConfig.Usernames = hashedUsernames + } + + if payloadParameters.C2Profiles.HttpC2Profile != nil { + payloadConfig.Http = &config.HttpConfig{ + CallbackPort: uint32(payloadParameters.C2Profiles.HttpC2Profile.CallbackPort), + Killdate: payloadParameters.C2Profiles.HttpC2Profile.Killdate, + CallbackJitter: uint32(payloadParameters.C2Profiles.HttpC2Profile.CallbackJitter), + Headers: payloadParameters.C2Profiles.HttpC2Profile.Headers, + CallbackHost: payloadParameters.C2Profiles.HttpC2Profile.CallbackHost, + GetUri: payloadParameters.C2Profiles.HttpC2Profile.GetUri, + PostUri: payloadParameters.C2Profiles.HttpC2Profile.PostUri, + QueryPathName: payloadParameters.C2Profiles.HttpC2Profile.QueryPathName, + CallbackInterval: uint32(payloadParameters.C2Profiles.HttpC2Profile.CallbackInterval), + } + + if payloadParameters.C2Profiles.HttpC2Profile.CryptoInfo != nil { + payloadConfig.Http.AesKey = payloadParameters.C2Profiles.HttpC2Profile.CryptoInfo.Key[:] + } + + if payloadParameters.C2Profiles.HttpC2Profile.ProxyInfo != nil { + payloadConfig.Http.Proxy = &config.ProxyInfo{ + Host: payloadParameters.C2Profiles.HttpC2Profile.ProxyInfo.Host, + Port: uint32(payloadParameters.C2Profiles.HttpC2Profile.ProxyInfo.Port), + Pass: payloadParameters.C2Profiles.HttpC2Profile.ProxyInfo.Pass, + } + } + } + + return &payloadConfig +} diff --git a/Payload_Type/thanatos/mythic/builder/errors/errors.go b/Payload_Type/thanatos/mythic/builder/errors/errors.go deleted file mode 100644 index 800082f..0000000 --- a/Payload_Type/thanatos/mythic/builder/errors/errors.go +++ /dev/null @@ -1,45 +0,0 @@ -// Error types for the payload build. Adds filenames and line numbers to the error message -package errors - -import ( - "fmt" - "runtime" - "strings" -) - -// The type for the build errors -type BuildError struct { - // Line number where the error occured - lineno int - - // Filename where the error occured - fname string - msg string -} - -// Construct a new build error -func New(msg string) error { - _, fname, lineno, _ := runtime.Caller(1) - fnameSplit := strings.Split(fname, "/") - return &BuildError{ - msg: msg, - lineno: lineno, - fname: fnameSplit[len(fnameSplit)-1], - } -} - -// Construct a new build error with a format string -func Errorf(format string, a ...any) error { - _, fname, lineno, _ := runtime.Caller(1) - fnameSplit := strings.Split(fname, "/") - return &BuildError{ - msg: fmt.Sprintf(format, a...), - fname: fnameSplit[len(fnameSplit)-1], - lineno: lineno, - } -} - -// Return the string version of the error -func (e *BuildError) Error() string { - return fmt.Sprintf("[%s:%d]: %s", e.fname, e.lineno, e.msg) -} diff --git a/Payload_Type/thanatos/mythic/builder/handlers.go b/Payload_Type/thanatos/mythic/builder/handlers.go index ff9c66b..4fb8f78 100644 --- a/Payload_Type/thanatos/mythic/builder/handlers.go +++ b/Payload_Type/thanatos/mythic/builder/handlers.go @@ -37,25 +37,25 @@ func (handler MythicPayloadHandler) Build(target string, config ParsedPayloadPar outpath := fmt.Sprintf("%s/target/%s/release", agentCodePath, target) - profile := "" - if config.C2Profiles.HttpProfile != nil { - profile = "http" + profileType := "" + if config.IsP2P { + profileType = "p2p" } else { - panic("Unimplemented build profile") + profileType = "egress" } filename := "" if config.SelectedOS == agentstructs.SUPPORTED_OS_LINUX { - if config.PayloadBuildParameters.Output != PayloadBuildParameterOutputFormatExecutable { - filename = fmt.Sprintf("libthanatos_%s_cdylib.so", profile) + if config.BuildParameters.Output != PayloadBuildParameterOutputFormatExecutable { + filename = fmt.Sprintf("libthanatos_%s_cdylib.so", profileType) } else { - filename = fmt.Sprintf("thanatos_%s_binary", profile) + filename = fmt.Sprintf("thanatos_%s_binary", profileType) } } else if config.SelectedOS == agentstructs.SUPPORTED_OS_WINDOWS { - if config.PayloadBuildParameters.Output == PayloadBuildParameterOutputFormatExecutable { - filename = fmt.Sprintf("thanatos_%s_binary.exe", profile) + if config.BuildParameters.Output == PayloadBuildParameterOutputFormatExecutable { + filename = fmt.Sprintf("thanatos_%s_binary.exe", profileType) } else { - filename = fmt.Sprintf("thanatos_%s_cdylib.dll", profile) + filename = fmt.Sprintf("thanatos_%s_cdylib.dll", profileType) } } diff --git a/Payload_Type/thanatos/mythic/builder/httpparameters_parser.go b/Payload_Type/thanatos/mythic/builder/httpparameters.go similarity index 81% rename from Payload_Type/thanatos/mythic/builder/httpparameters_parser.go rename to Payload_Type/thanatos/mythic/builder/httpparameters.go index 3c4596e..9edee51 100644 --- a/Payload_Type/thanatos/mythic/builder/httpparameters_parser.go +++ b/Payload_Type/thanatos/mythic/builder/httpparameters.go @@ -13,105 +13,57 @@ import ( agentstructs "github.com/MythicMeta/MythicContainer/agent_structs" ) -/* -#[derive(Serialize, Deserialize, Debug)] -pub struct ProxyInfo<'a> { - host: &'a str, - port: u16, - user: &'a str, - pass: &'a str, -} -*/ - // Data type with the HTTP proxy parameters type HttpC2ProfileProxyParameters struct { - Host string `msgpack:"host"` - Port uint16 `msgpack:"port"` - User string `msgpack:"user"` - Pass string `msgpack:"pass"` -} - -/* -{ - "crypto_type": { - // TODO: Change type to an integer value rather than a string (serde_repr) - "type": "aes256_hmac", - "key": [1, 2, 3, 4, 5, ...] - } + Host string + Port uint16 + User string + Pass string } -#[derive(Serialize, Deserialize, Debug)] -#[serde(tag = "type")] -pub enum CryptoInfo { - #[serde(rename = "aes256_hmac")] - Aes256Hmac { - key: [u8; 16], - } -} -*/ - // HTTP C2 profile crypto information type HttpC2ProfileCryptoInfo struct { - Type string `msgpack:"type"` - Key [16]byte `msgpack:"key"` -} - -/* -/// HTTP profile configuration variables -#[derive(Serialize, Deserialize, Debug)] -pub struct HttpConfigVars<'a> { - callback_host: &'a str, - callback_interval: u32, - callback_jitter: u16, - callback_port: u16, - killdate: u64, - encrypted_exchange_check: bool, - crypto_info: Option, - headers: HashMap<&'a str, &'a str>, - get_uri: &'a str, - post_uri: &'a str, - query_path_name: &'a str, - proxy_info: Option, + Type string + Key [16]byte } -*/ // Contains the HTTP C2 profile parameters type HttpC2ProfileParameters struct { // Host for making HTTP connections to - CallbackHost string `msgpack:"callback_host"` + CallbackHost string // Interval for HTTP connections - CallbackInterval uint32 `msgpack:"callback_interval"` + CallbackInterval uint32 // Callback jitter for the payload - CallbackJitter uint16 `msgpack:"callback_jitter"` + CallbackJitter uint16 // Callback port for the payload to connect to - CallbackPort uint16 `msgpack:"callback_port"` + CallbackPort uint16 // Killdate of the payload - Killdate uint64 `msgpack:"killdate"` + Killdate uint64 // Whether the payload should do a key exchange - EncryptedExchangeCheck bool `msgpack:"encrypted_exchange_check"` + EncryptedExchangeCheck bool // Information for encryption - CryptoInfo *HttpC2ProfileCryptoInfo `msgpack:"crypto_info,omitempty"` + CryptoInfo *HttpC2ProfileCryptoInfo // HTTP headers for making HTTP requests - Headers map[string]string `msgpack:"headers"` + Headers map[string]string // The GET uri for any GET requests - GetUri string `msgpack:"get_uri"` + GetUri string // The POST uri for any POST requests - PostUri string `msgpack:"post_uri"` + PostUri string // The query path for GET requests - QueryPathName string `msgpack:"query_path_name"` + QueryPathName string // HTTP proxy information - ProxyInfo *HttpC2ProfileProxyParameters `msgpack:"proxy_info,omitempty"` + ProxyInfo *HttpC2ProfileProxyParameters } func (p *HttpC2ProfileParameters) String() string { diff --git a/Payload_Type/thanatos/mythic/builder/payloadparameters.go b/Payload_Type/thanatos/mythic/builder/payloadparameters.go new file mode 100644 index 0000000..0ce430b --- /dev/null +++ b/Payload_Type/thanatos/mythic/builder/payloadparameters.go @@ -0,0 +1,69 @@ +package builder + +import ( + "slices" + thanatoserror "thanatos/errors" + + agentstructs "github.com/MythicMeta/MythicContainer/agent_structs" + "github.com/google/uuid" +) + +type ParsedPayloadParameters struct { + Uuid uuid.UUID + SelectedOS string + Filename string + Commands []string + BuildParameters ParsedBuildParameters + IsP2P bool + C2Profiles struct { + HttpC2Profile *HttpC2ProfileParameters + TcpC2Profile *string + } +} + +func parsePayloadParameters(buildMessage agentstructs.PayloadBuildMessage) (ParsedPayloadParameters, error) { + payloadUUID, err := uuid.Parse(buildMessage.PayloadUUID) + if err != nil { + return ParsedPayloadParameters{}, thanatoserror.Errorf("failed to parse payload UUID: %s", err.Error()) + } + + buildParameters, err := parsePayloadBuildParameters(buildMessage) + if err != nil { + return ParsedPayloadParameters{}, thanatoserror.Errorf("failed to parse build parameters: %s", err.Error()) + } + + commands := slices.DeleteFunc(buildMessage.CommandList, func(s string) bool { + return slices.Contains(builtinCommands, s) + }) + + parameters := ParsedPayloadParameters{ + Uuid: payloadUUID, + SelectedOS: buildMessage.SelectedOS, + Filename: buildMessage.Filename, + Commands: commands, + BuildParameters: buildParameters, + } + + for _, profile := range buildMessage.C2Profiles { + switch profile.Name { + case "http": + httpProfile, err := parseHttpProfileParameters(profile) + if err != nil { + return ParsedPayloadParameters{}, thanatoserror.Errorf("failed to parse HTTP C2 profile parameters: %s", err.Error()) + } + parameters.C2Profiles.HttpC2Profile = httpProfile + } + } + + if parameters.C2Profiles.HttpC2Profile != nil && parameters.C2Profiles.TcpC2Profile != nil { + return parameters, thanatoserror.New("cannot mix egress and p2p C2 profiles") + } + + if parameters.C2Profiles.HttpC2Profile != nil { + parameters.IsP2P = false + } else { + parameters.IsP2P = true + } + + return parameters, nil +} diff --git a/Payload_Type/thanatos/mythic/builder/testing.go b/Payload_Type/thanatos/mythic/builder/testing.go new file mode 100644 index 0000000..814fff1 --- /dev/null +++ b/Payload_Type/thanatos/mythic/builder/testing.go @@ -0,0 +1,67 @@ +package builder + +import ( + "strings" + "testing" + + agentstructs "github.com/MythicMeta/MythicContainer/agent_structs" + "github.com/google/uuid" +) + +func RunTestCases(t *testing.T, testCases []TestCase, handler BuildHandler) { + for _, testCase := range testCases { + t.Run(testCase.Name, func(t *testing.T) { + input := createBuildInput(testCase) + buildResult := BuildPayload(handler, input) + + t.Logf("[BUILD MESSAGE]: %s", buildResult.BuildMessage) + t.Logf("[BUILD STDOUT]: %s", buildResult.BuildStdOut) + t.Logf("[BUILD STDERR]: %s", buildResult.BuildStdErr) + + checkResults(t, buildResult, input, testCase) + }) + } +} + +func createBuildInput(testCase TestCase) agentstructs.PayloadBuildMessage { + payloadUUID := uuid.NewString() + payloadFileUUID := uuid.NewString() + + inputPayloadBuildMsg := agentstructs.PayloadBuildMessage{ + PayloadUUID: payloadUUID, + PayloadFileUUID: payloadFileUUID, + + PayloadType: "thanatos", + Filename: testCase.Filename, + SelectedOS: testCase.SelectedOS, + CommandList: testCase.CommandList, + BuildParameters: agentstructs.PayloadBuildArguments{ + Parameters: testCase.BuildParameters, + }, + C2Profiles: testCase.C2Profiles, + } + + return inputPayloadBuildMsg +} + +func checkResults(t *testing.T, result agentstructs.PayloadBuildResponse, input agentstructs.PayloadBuildMessage, testCase TestCase) { + if result.Success && !testCase.Expect.Success { + t.Fatalf("test case returned a success but was expected to fail") + } + + if !result.Success && testCase.Expect.Success { + t.Fatalf("test case returned a failure but was expected to succeed") + } + + if result.PayloadUUID != input.PayloadUUID { + t.Logf("[input.PayloadUUID]: %s", input.PayloadUUID) + t.Logf("[result.PayloadUUID]: %s", result.PayloadUUID) + t.Fatalf("input payload UUID does not match resulting payload UUID") + } + + if !strings.Contains(result.BuildMessage, testCase.Expect.BuildCommand) { + t.Logf("[result.BuildMessage]:\n%s", result.BuildMessage) + t.Logf("[testCase.Expect.BuildCommand]: %s", testCase.Expect.BuildCommand) + t.Fatalf("expected build command does not match resulting build command") + } +} diff --git a/Payload_Type/thanatos/mythic/builder/testing_types.go b/Payload_Type/thanatos/mythic/builder/testing_types.go new file mode 100644 index 0000000..837fdec --- /dev/null +++ b/Payload_Type/thanatos/mythic/builder/testing_types.go @@ -0,0 +1,55 @@ +package builder + +import ( + agentstructs "github.com/MythicMeta/MythicContainer/agent_structs" + "github.com/MythicMeta/MythicContainer/mythicrpc" +) + +type TestCase struct { + Name string + Filename string + SelectedOS string + CommandList []string + BuildParameters map[string]interface{} + C2Profiles []agentstructs.PayloadBuildC2Profile + + Expect TestCaseResult +} + +type TestCaseResult struct { + Success bool + BuildCommand string +} + +// Type which contains the mock implementations of the handler routines. This will +// essentially "no-op" expensive function or Mythic RPC calls +type MockBuildPayloadHandler struct{} + +// Mock implementation for the payload build +func (handler MockBuildPayloadHandler) Build(target string, config ParsedPayloadParameters, command string) ([]byte, error) { + return []byte{}, nil +} + +// Mock implementation for updating a build step in Mythic +func (handler MockBuildPayloadHandler) UpdateBuildStep(input mythicrpc.MythicRPCPayloadUpdateBuildStepMessage) (*mythicrpc.MythicRPCPayloadUpdateBuildStepMessageResponse, error) { + response := mythicrpc.MythicRPCPayloadUpdateBuildStepMessageResponse{ + Success: true, + Error: "", + } + return &response, nil +} + +// Type which contains the full implementations for building a payload. This will build +// the payload and install the required Rust tool chain. This will mock the Mythic RPC +// calls +type FullBuildPayloadHandler struct{} + +// Runs the real build command for the build handler +func (handler FullBuildPayloadHandler) Build(target string, config ParsedPayloadParameters, command string) ([]byte, error) { + return MythicPayloadHandler{}.Build(target, config, command) +} + +// Runs the mock Mythic RPC function +func (handler FullBuildPayloadHandler) UpdateBuildStep(input mythicrpc.MythicRPCPayloadUpdateBuildStepMessage) (*mythicrpc.MythicRPCPayloadUpdateBuildStepMessageResponse, error) { + return MockBuildPayloadHandler{}.UpdateBuildStep(input) +} diff --git a/Payload_Type/thanatos/mythic/builder/types.go b/Payload_Type/thanatos/mythic/builder/types.go index 79548d9..12709d6 100644 --- a/Payload_Type/thanatos/mythic/builder/types.go +++ b/Payload_Type/thanatos/mythic/builder/types.go @@ -5,6 +5,13 @@ import ( "github.com/MythicMeta/MythicContainer/mythicrpc" ) +type SupportedCommand string + +const ( + ExitCommand SupportedCommand = "exit" + SleepCommand SupportedCommand = "sleep" +) + // Type for the payload architecture parameter type PayloadBuildParameterArchitecture string diff --git a/Payload_Type/thanatos/mythic/builder/workinghours_parser.go b/Payload_Type/thanatos/mythic/builder/workinghours.go similarity index 100% rename from Payload_Type/thanatos/mythic/builder/workinghours_parser.go rename to Payload_Type/thanatos/mythic/builder/workinghours.go diff --git a/Payload_Type/thanatos/mythic/pb/config/config.pb.go b/Payload_Type/thanatos/mythic/pb/config/config.pb.go new file mode 100644 index 0000000..9f21650 --- /dev/null +++ b/Payload_Type/thanatos/mythic/pb/config/config.pb.go @@ -0,0 +1,484 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.32.0 +// protoc v4.25.2 +// source: config.proto + +package config + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Config struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Uuid []byte `protobuf:"bytes,1,opt,name=uuid,proto3" json:"uuid,omitempty"` + WorkingHoursStart int64 `protobuf:"varint,2,opt,name=working_hours_start,json=workingHoursStart,proto3" json:"working_hours_start,omitempty"` + WorkingHoursEnd int64 `protobuf:"varint,3,opt,name=working_hours_end,json=workingHoursEnd,proto3" json:"working_hours_end,omitempty"` + ConnectionRetries uint32 `protobuf:"varint,4,opt,name=connection_retries,json=connectionRetries,proto3" json:"connection_retries,omitempty"` + Domains []byte `protobuf:"bytes,5,opt,name=domains,proto3,oneof" json:"domains,omitempty"` + Hostnames []byte `protobuf:"bytes,6,opt,name=hostnames,proto3,oneof" json:"hostnames,omitempty"` + Usernames []byte `protobuf:"bytes,7,opt,name=usernames,proto3,oneof" json:"usernames,omitempty"` + SpawnTo string `protobuf:"bytes,8,opt,name=spawn_to,json=spawnTo,proto3" json:"spawn_to,omitempty"` + Http *HttpConfig `protobuf:"bytes,9,opt,name=http,proto3,oneof" json:"http,omitempty"` +} + +func (x *Config) Reset() { + *x = Config{} + if protoimpl.UnsafeEnabled { + mi := &file_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Config) ProtoMessage() {} + +func (x *Config) ProtoReflect() protoreflect.Message { + mi := &file_config_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Config) GetUuid() []byte { + if x != nil { + return x.Uuid + } + return nil +} + +func (x *Config) GetWorkingHoursStart() int64 { + if x != nil { + return x.WorkingHoursStart + } + return 0 +} + +func (x *Config) GetWorkingHoursEnd() int64 { + if x != nil { + return x.WorkingHoursEnd + } + return 0 +} + +func (x *Config) GetConnectionRetries() uint32 { + if x != nil { + return x.ConnectionRetries + } + return 0 +} + +func (x *Config) GetDomains() []byte { + if x != nil { + return x.Domains + } + return nil +} + +func (x *Config) GetHostnames() []byte { + if x != nil { + return x.Hostnames + } + return nil +} + +func (x *Config) GetUsernames() []byte { + if x != nil { + return x.Usernames + } + return nil +} + +func (x *Config) GetSpawnTo() string { + if x != nil { + return x.SpawnTo + } + return "" +} + +func (x *Config) GetHttp() *HttpConfig { + if x != nil { + return x.Http + } + return nil +} + +type HttpConfig struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + CallbackPort uint32 `protobuf:"varint,1,opt,name=callback_port,json=callbackPort,proto3" json:"callback_port,omitempty"` + Killdate uint64 `protobuf:"varint,2,opt,name=killdate,proto3" json:"killdate,omitempty"` + CallbackJitter uint32 `protobuf:"varint,3,opt,name=callback_jitter,json=callbackJitter,proto3" json:"callback_jitter,omitempty"` + Headers map[string]string `protobuf:"bytes,4,rep,name=headers,proto3" json:"headers,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + AesKey []byte `protobuf:"bytes,5,opt,name=aes_key,json=aesKey,proto3,oneof" json:"aes_key,omitempty"` + CallbackHost string `protobuf:"bytes,6,opt,name=callback_host,json=callbackHost,proto3" json:"callback_host,omitempty"` + GetUri string `protobuf:"bytes,7,opt,name=get_uri,json=getUri,proto3" json:"get_uri,omitempty"` + PostUri string `protobuf:"bytes,8,opt,name=post_uri,json=postUri,proto3" json:"post_uri,omitempty"` + QueryPathName string `protobuf:"bytes,9,opt,name=query_path_name,json=queryPathName,proto3" json:"query_path_name,omitempty"` + Proxy *ProxyInfo `protobuf:"bytes,10,opt,name=proxy,proto3,oneof" json:"proxy,omitempty"` + CallbackInterval uint32 `protobuf:"varint,11,opt,name=callback_interval,json=callbackInterval,proto3" json:"callback_interval,omitempty"` +} + +func (x *HttpConfig) Reset() { + *x = HttpConfig{} + if protoimpl.UnsafeEnabled { + mi := &file_config_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *HttpConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HttpConfig) ProtoMessage() {} + +func (x *HttpConfig) ProtoReflect() protoreflect.Message { + mi := &file_config_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HttpConfig.ProtoReflect.Descriptor instead. +func (*HttpConfig) Descriptor() ([]byte, []int) { + return file_config_proto_rawDescGZIP(), []int{1} +} + +func (x *HttpConfig) GetCallbackPort() uint32 { + if x != nil { + return x.CallbackPort + } + return 0 +} + +func (x *HttpConfig) GetKilldate() uint64 { + if x != nil { + return x.Killdate + } + return 0 +} + +func (x *HttpConfig) GetCallbackJitter() uint32 { + if x != nil { + return x.CallbackJitter + } + return 0 +} + +func (x *HttpConfig) GetHeaders() map[string]string { + if x != nil { + return x.Headers + } + return nil +} + +func (x *HttpConfig) GetAesKey() []byte { + if x != nil { + return x.AesKey + } + return nil +} + +func (x *HttpConfig) GetCallbackHost() string { + if x != nil { + return x.CallbackHost + } + return "" +} + +func (x *HttpConfig) GetGetUri() string { + if x != nil { + return x.GetUri + } + return "" +} + +func (x *HttpConfig) GetPostUri() string { + if x != nil { + return x.PostUri + } + return "" +} + +func (x *HttpConfig) GetQueryPathName() string { + if x != nil { + return x.QueryPathName + } + return "" +} + +func (x *HttpConfig) GetProxy() *ProxyInfo { + if x != nil { + return x.Proxy + } + return nil +} + +func (x *HttpConfig) GetCallbackInterval() uint32 { + if x != nil { + return x.CallbackInterval + } + return 0 +} + +type ProxyInfo struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Host string `protobuf:"bytes,1,opt,name=host,proto3" json:"host,omitempty"` + Port uint32 `protobuf:"varint,2,opt,name=port,proto3" json:"port,omitempty"` + Pass string `protobuf:"bytes,3,opt,name=pass,proto3" json:"pass,omitempty"` +} + +func (x *ProxyInfo) Reset() { + *x = ProxyInfo{} + if protoimpl.UnsafeEnabled { + mi := &file_config_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ProxyInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProxyInfo) ProtoMessage() {} + +func (x *ProxyInfo) ProtoReflect() protoreflect.Message { + mi := &file_config_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProxyInfo.ProtoReflect.Descriptor instead. +func (*ProxyInfo) Descriptor() ([]byte, []int) { + return file_config_proto_rawDescGZIP(), []int{2} +} + +func (x *ProxyInfo) GetHost() string { + if x != nil { + return x.Host + } + return "" +} + +func (x *ProxyInfo) GetPort() uint32 { + if x != nil { + return x.Port + } + return 0 +} + +func (x *ProxyInfo) GetPass() string { + if x != nil { + return x.Pass + } + return "" +} + +var File_config_proto protoreflect.FileDescriptor + +var file_config_proto_rawDesc = []byte{ + 0x0a, 0x0c, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xfe, + 0x02, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x75, 0x69, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x75, 0x75, 0x69, 0x64, 0x12, 0x2e, 0x0a, + 0x13, 0x77, 0x6f, 0x72, 0x6b, 0x69, 0x6e, 0x67, 0x5f, 0x68, 0x6f, 0x75, 0x72, 0x73, 0x5f, 0x73, + 0x74, 0x61, 0x72, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x11, 0x77, 0x6f, 0x72, 0x6b, + 0x69, 0x6e, 0x67, 0x48, 0x6f, 0x75, 0x72, 0x73, 0x53, 0x74, 0x61, 0x72, 0x74, 0x12, 0x2a, 0x0a, + 0x11, 0x77, 0x6f, 0x72, 0x6b, 0x69, 0x6e, 0x67, 0x5f, 0x68, 0x6f, 0x75, 0x72, 0x73, 0x5f, 0x65, + 0x6e, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x77, 0x6f, 0x72, 0x6b, 0x69, 0x6e, + 0x67, 0x48, 0x6f, 0x75, 0x72, 0x73, 0x45, 0x6e, 0x64, 0x12, 0x2d, 0x0a, 0x12, 0x63, 0x6f, 0x6e, + 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x72, 0x65, 0x74, 0x72, 0x69, 0x65, 0x73, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x11, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x52, 0x65, 0x74, 0x72, 0x69, 0x65, 0x73, 0x12, 0x1d, 0x0a, 0x07, 0x64, 0x6f, 0x6d, 0x61, + 0x69, 0x6e, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x00, 0x52, 0x07, 0x64, 0x6f, 0x6d, + 0x61, 0x69, 0x6e, 0x73, 0x88, 0x01, 0x01, 0x12, 0x21, 0x0a, 0x09, 0x68, 0x6f, 0x73, 0x74, 0x6e, + 0x61, 0x6d, 0x65, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x01, 0x52, 0x09, 0x68, 0x6f, + 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x88, 0x01, 0x01, 0x12, 0x21, 0x0a, 0x09, 0x75, 0x73, + 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x02, 0x52, + 0x09, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x88, 0x01, 0x01, 0x12, 0x19, 0x0a, + 0x08, 0x73, 0x70, 0x61, 0x77, 0x6e, 0x5f, 0x74, 0x6f, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x07, 0x73, 0x70, 0x61, 0x77, 0x6e, 0x54, 0x6f, 0x12, 0x24, 0x0a, 0x04, 0x68, 0x74, 0x74, 0x70, + 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x48, 0x74, 0x74, 0x70, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x48, 0x03, 0x52, 0x04, 0x68, 0x74, 0x74, 0x70, 0x88, 0x01, 0x01, 0x42, 0x0a, + 0x0a, 0x08, 0x5f, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x68, + 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x75, 0x73, 0x65, + 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x42, 0x07, 0x0a, 0x05, 0x5f, 0x68, 0x74, 0x74, 0x70, 0x22, + 0xef, 0x03, 0x0a, 0x0a, 0x48, 0x74, 0x74, 0x70, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x23, + 0x0a, 0x0d, 0x63, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0c, 0x63, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x50, + 0x6f, 0x72, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x6b, 0x69, 0x6c, 0x6c, 0x64, 0x61, 0x74, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x08, 0x6b, 0x69, 0x6c, 0x6c, 0x64, 0x61, 0x74, 0x65, 0x12, + 0x27, 0x0a, 0x0f, 0x63, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x5f, 0x6a, 0x69, 0x74, 0x74, + 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0e, 0x63, 0x61, 0x6c, 0x6c, 0x62, 0x61, + 0x63, 0x6b, 0x4a, 0x69, 0x74, 0x74, 0x65, 0x72, 0x12, 0x32, 0x0a, 0x07, 0x68, 0x65, 0x61, 0x64, + 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x48, 0x74, 0x74, 0x70, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x45, 0x6e, + 0x74, 0x72, 0x79, 0x52, 0x07, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x12, 0x1c, 0x0a, 0x07, + 0x61, 0x65, 0x73, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x00, 0x52, + 0x06, 0x61, 0x65, 0x73, 0x4b, 0x65, 0x79, 0x88, 0x01, 0x01, 0x12, 0x23, 0x0a, 0x0d, 0x63, 0x61, + 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x5f, 0x68, 0x6f, 0x73, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0c, 0x63, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x48, 0x6f, 0x73, 0x74, 0x12, + 0x17, 0x0a, 0x07, 0x67, 0x65, 0x74, 0x5f, 0x75, 0x72, 0x69, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x06, 0x67, 0x65, 0x74, 0x55, 0x72, 0x69, 0x12, 0x19, 0x0a, 0x08, 0x70, 0x6f, 0x73, 0x74, + 0x5f, 0x75, 0x72, 0x69, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x70, 0x6f, 0x73, 0x74, + 0x55, 0x72, 0x69, 0x12, 0x26, 0x0a, 0x0f, 0x71, 0x75, 0x65, 0x72, 0x79, 0x5f, 0x70, 0x61, 0x74, + 0x68, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x71, 0x75, + 0x65, 0x72, 0x79, 0x50, 0x61, 0x74, 0x68, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x25, 0x0a, 0x05, 0x70, + 0x72, 0x6f, 0x78, 0x79, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0a, 0x2e, 0x50, 0x72, 0x6f, + 0x78, 0x79, 0x49, 0x6e, 0x66, 0x6f, 0x48, 0x01, 0x52, 0x05, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x88, + 0x01, 0x01, 0x12, 0x2b, 0x0a, 0x11, 0x63, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x5f, 0x69, + 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x10, 0x63, + 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x1a, + 0x3a, 0x0a, 0x0c, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, + 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, + 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x0a, 0x0a, 0x08, 0x5f, + 0x61, 0x65, 0x73, 0x5f, 0x6b, 0x65, 0x79, 0x42, 0x08, 0x0a, 0x06, 0x5f, 0x70, 0x72, 0x6f, 0x78, + 0x79, 0x22, 0x47, 0x0a, 0x09, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x12, + 0x0a, 0x04, 0x68, 0x6f, 0x73, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x6f, + 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, + 0x52, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x73, 0x73, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x73, 0x73, 0x42, 0x0b, 0x5a, 0x09, 0x70, 0x62, + 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_config_proto_rawDescOnce sync.Once + file_config_proto_rawDescData = file_config_proto_rawDesc +) + +func file_config_proto_rawDescGZIP() []byte { + file_config_proto_rawDescOnce.Do(func() { + file_config_proto_rawDescData = protoimpl.X.CompressGZIP(file_config_proto_rawDescData) + }) + return file_config_proto_rawDescData +} + +var file_config_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_config_proto_goTypes = []interface{}{ + (*Config)(nil), // 0: Config + (*HttpConfig)(nil), // 1: HttpConfig + (*ProxyInfo)(nil), // 2: ProxyInfo + nil, // 3: HttpConfig.HeadersEntry +} +var file_config_proto_depIdxs = []int32{ + 1, // 0: Config.http:type_name -> HttpConfig + 3, // 1: HttpConfig.headers:type_name -> HttpConfig.HeadersEntry + 2, // 2: HttpConfig.proxy:type_name -> ProxyInfo + 3, // [3:3] is the sub-list for method output_type + 3, // [3:3] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name +} + +func init() { file_config_proto_init() } +func file_config_proto_init() { + if File_config_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_config_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Config); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_config_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*HttpConfig); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_config_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ProxyInfo); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + file_config_proto_msgTypes[0].OneofWrappers = []interface{}{} + file_config_proto_msgTypes[1].OneofWrappers = []interface{}{} + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_config_proto_rawDesc, + NumEnums: 0, + NumMessages: 4, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_config_proto_goTypes, + DependencyIndexes: file_config_proto_depIdxs, + MessageInfos: file_config_proto_msgTypes, + }.Build() + File_config_proto = out.File + file_config_proto_rawDesc = nil + file_config_proto_goTypes = nil + file_config_proto_depIdxs = nil +} diff --git a/Payload_Type/thanatos/mythic/protos/config.proto b/Payload_Type/thanatos/mythic/protos/config.proto new file mode 100644 index 0000000..cf850f4 --- /dev/null +++ b/Payload_Type/thanatos/mythic/protos/config.proto @@ -0,0 +1,35 @@ +syntax = "proto3"; + +option go_package = "pb/config"; + +message Config { + bytes uuid = 1; + int64 working_hours_start = 2; + int64 working_hours_end = 3; + uint32 connection_retries = 4; + optional bytes domains = 5; + optional bytes hostnames = 6; + optional bytes usernames = 7; + string spawn_to = 8; + optional HttpConfig http = 9; +} + +message HttpConfig { + uint32 callback_port = 1; + uint64 killdate = 2; + uint32 callback_jitter = 3; + map headers = 4; + optional bytes aes_key = 5; + string callback_host = 6; + string get_uri = 7; + string post_uri = 8; + string query_path_name = 9; + optional ProxyInfo proxy = 10; + uint32 callback_interval = 11; +} + +message ProxyInfo { + string host = 1; + uint32 port = 2; + string pass = 3; +} diff --git a/Payload_Type/thanatos/mythic/tests/buildtests/amd64_linux_simple.json b/Payload_Type/thanatos/mythic/tests/buildtests/amd64_linux_simple.json deleted file mode 100644 index c89acb2..0000000 --- a/Payload_Type/thanatos/mythic/tests/buildtests/amd64_linux_simple.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "$schema": "../schemas/payload_build_spec.schema.json", - "filename": "thanatos", - "commands": [ - "exit" - ], - "selected_os": "Linux", - "build_parameters": { - "architecture": "amd64", - "initoptions": "none", - "connection_retries": 1, - "cryptolib": "system (Windows CNG/Linux OpenSSL)", - "working_hours": "00:00-23:59", - "output": "executable" - }, - - "c2profiles": [ - { - "name": "http", - "is_p2p": false, - "parameters": { - "callback_port": 80, - "killdate": "2099-01-01", - "encrypted_exchange_check": true, - "callback_jitter": 23, - "headers": { - "User-Agent": "test" - }, - "AESPSK": { - "value": "none", - "enc_key": "", - "dec_key": "" - }, - "callback_host": "http://mythic", - "get_uri": "index", - "post_uri": "data", - "query_path_name": "q", - "proxy_host": "", - "proxy_port": "", - "proxy_user": "", - "proxy_pass": "", - "callback_interval": 10 - } - } - ], - - "expect": { - "success": true - } - -} diff --git a/Payload_Type/thanatos/mythic/tests/buildtests/amd64_windows_simple.json b/Payload_Type/thanatos/mythic/tests/buildtests/amd64_windows_simple.json deleted file mode 100644 index 043a077..0000000 --- a/Payload_Type/thanatos/mythic/tests/buildtests/amd64_windows_simple.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "$schema": "../schemas/payload_build_spec.schema.json", - "filename": "thanatos", - "commands": [ - "exit" - ], - "selected_os": "Windows", - "build_parameters": { - "architecture": "amd64", - "initoptions": "none", - "connection_retries": 1, - "cryptolib": "system (wincrypto-ng/openssl)", - "working_hours": "00:00-23:59", - "output": "executable" - }, - - "c2profiles": [ - { - "name": "http", - "is_p2p": false, - "parameters": { - "callback_port": 80, - "killdate": "2099-01-01", - "encrypted_exchange_check": true, - "callback_jitter": 23, - "headers": { - "User-Agent": "test" - }, - "AESPSK": { - "value": "none", - "enc_key": "", - "dec_key": "" - }, - "callback_host": "http://mythic", - "get_uri": "index", - "post_uri": "data", - "query_path_name": "q", - "proxy_host": "", - "proxy_port": "", - "proxy_user": "", - "proxy_pass": "", - "callback_interval": 10 - } - } - ], - - "expect": { - "success": true - } - -} diff --git a/Payload_Type/thanatos/mythic/tests/schemas/payload_build_spec.schema.json b/Payload_Type/thanatos/mythic/tests/schemas/payload_build_spec.schema.json deleted file mode 100644 index b47048c..0000000 --- a/Payload_Type/thanatos/mythic/tests/schemas/payload_build_spec.schema.json +++ /dev/null @@ -1,462 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft-07/schema", - "$id": "https://raw.githubusercontent.com/MythicAgents/thanatos/main/Payload_Type/thanatos/mythic/testartifacts/schemas/payload_build_spec.schema.json", - "title": "Payload Build Test Spec", - "description": "Definition for the payload build test spec file", - "type": "object", - "additionalProperties": false, - "properties": { - "$schema": { - "type": "string" - }, - "filename": { - "description": "Output filename", - "type": "string" - }, - "commands": { - "description": "List of commands to include in the agent", - "type": "array" - }, - "selected_os": { - "description": "Operating system to build the agent for", - "type": "string", - "enum": [ - "Linux", - "Windows" - ] - }, - "build_parameters": { - "description": "Values for the payload build parameters", - "type": "object", - "additionalProperties": false, - "required": [ - "architecture", - "initoptions", - "connection_retries", - "cryptolib", - "working_hours", - "output" - ], - "properties": { - "architecture": { - "description": "Payload architecture", - "type": "string", - "enum": [ - "amd64", - "x86" - ] - }, - "initoptions": { - "description": "Initial execution option", - "type": "string", - "enum": [ - "none", - "Spawn Thread (Windows Only)", - "Daemonize (Linux Only)" - ] - }, - "connection_retries": { - "description": "Number of times to try and reconnect to Mythic", - "type": "number" - }, - "cryptolib": { - "description": "Library to use for doing crypto routines", - "type": "string", - "enum": [ - "internal (RustCrypto)", - "system (Windows CNG/Linux OpenSSL)" - ] - }, - "working_hours": { - "description": "Working hours for the agent (use 24 hour time)", - "type": "string" - }, - "domains": { - "description": "Limit payload execution to machines joined to one of the following domains", - "type": "array", - "items": { - "type": "string" - } - }, - "hostnames": { - "description": "Limit payload execution to machines with one of the specified hostnames", - "type": "array", - "items": { - "type": "string" - } - }, - "usernames": { - "description": "Limit payload execution to users with one of the specified usernames", - "type": "array", - "items": { - "type": "string" - } - }, - "static": { - "description": "Libraries to statically link to (Linux only)", - "type": "string", - "items": { - "type": "string", - "enum": [ - "openssl", - "libcurl" - ] - } - }, - "tlsselfsigned": { - "description": "Allow HTTPs connections to self-signed TLS certificates", - "type": "boolean" - }, - "spawnto": { - "description": "Initial spawnto value", - "type": "string" - }, - "output": { - "description": "Payload output format", - "type": "string", - "enum": [ - "executable", - "Shared Library (Run on load)", - "Shared Library (.dll/.so with export name 'init')", - "Windows Shellcode" - ] - } - } - }, - "c2profiles": { - "description": "Configured C2 profiles", - "uniqueItems": true, - "type": "array", - "items": { - "type": "object", - "additionalProperties": false, - "required": [ - "name", - "is_p2p", - "parameters" - ], - "properties": { - "name": { - "description": "C2 profile name", - "type": "string", - "enum": [ - "http" - ] - }, - "is_p2p": { - "description": "Whether the C2 profile is p2p", - "type": "boolean" - }, - "parameters": { - "type": "object" - } - }, - "allOf": [ - { - "if": { - "properties": { - "name": { - "const": "http" - } - } - }, - "then": { - "properties": { - "parameters": { - "description": "C2 profile parameters", - "type": "object", - "additionalProperties": false, - "required": [ - "callback_host" - ], - "properties": { - "callback_port": { - "description": "Callback Port", - "type": "number", - "minimum": 0, - "maximum": 65535 - }, - "killdate": { - "description": "Kill Date", - "type": "string", - "pattern": "\\d{4}-\\d{2}-\\d{2}" - }, - "encrypted_exchange_check": { - "description": "Perform Key Exchange", - "type": "boolean" - }, - "callback_jitter": { - "description": "Callback jitter in percent", - "type": "number" - }, - "headers": { - "description": "HTTP Headers", - "type": "object" - }, - "AESPSK": { - "description": "Encryption Type", - "type": "object", - "additionalProperties": false, - "required": [ - "value" - ], - "properties": { - "value": { - "description": "Value for the crypto type", - "type": "string", - "enum": [ - "aes256_hmac", - "none" - ] - }, - "enc_key": { - "description": "Base64 encoded encryption key", - "type": "string" - }, - "dec_key": { - "description": "Base64 encoded decryption key", - "type": "string" - } - } - }, - "callback_host": { - "description": "Callback Host", - "type": "string", - "pattern": "^(http|https)://.*[^/]$" - }, - "get_uri": { - "description": "GET request URI (don't include leading /)", - "type": "string" - }, - "post_uri": { - "description": "POST request URI (don't include leading /)", - "type": "string" - }, - "query_path_name": { - "description": "Name of the query parameter for GET requests", - "type": "string", - "pattern": "^[^/]" - }, - "proxy_host": { - "description": "Proxy Host", - "type": "string", - "pattern": "^$|^(http|https)://[a-zA-Z0-9]+" - }, - "proxy_port": { - "description": "Proxy Port", - "type": "string", - "pattern": "^$|^[0-9]+$" - }, - "proxy_user": { - "description": "Proxy Username", - "type": "string" - }, - "proxy_pass": { - "description": "Proxy Password", - "type": "string" - }, - "callback_interval": { - "description": "Callback Interval in seconds", - "type": "number", - "minimum": 0 - } - } - } - } - } - } - ] - } - }, - "expect": { - "description": "Expected result from the payload build", - "type": "object", - "additionalProperties": false, - "required": [ - "success" - ], - "properties": { - "success": { - "description": "Should the build be successful", - "type": "boolean" - }, - "stdout": { - "description": "Expected stdout value from the resulting payload build", - "type": "object", - "additionalProperties": false, - "properties": { - "contains": { - "description": "Text the stdout value should contain", - "type": "string" - }, - - "is": { - "description": "Value the full stdout message should exactly match", - "type": "string" - }, - - "regex": { - "description": "Regular expression for validating the stdout value", - "type": "string" - }, - - "case_insensitive": { - "description": "Whether the comparison should be case insensitive", - "type": "boolean" - } - }, - "oneOf": [ - { - "required": [ - "contains" - ] - }, - { - "required": [ - "regex" - ] - }, - { - "required": [ - "is" - ] - } - ] - }, - "message": { - "description": "Expected build message from the resulting payload build", - "type": "object", - "additionalProperties": false, - "properties": { - "contains": { - "description": "Text the build message should contain", - "type": "string" - }, - - "regex": { - "description": "Regular expression for validating the build message", - "type": "string" - }, - - "is": { - "description": "Value the full build message should exactly match", - "type": "string" - }, - - "case_insensitive": { - "description": "Whether the comparison should be case insensitive", - "type": "boolean" - } - }, - "oneOf": [ - { - "required": [ - "contains" - ] - }, - { - "required": [ - "regex" - ] - }, - { - "required": [ - "is" - ] - } - ] - }, - "stderr": { - "description": "Expected stderr value from the payload build", - "type": "object", - "additionalProperties": false, - "properties": { - "contains": { - "description": "Text the stderr value should contain", - "type": "string" - }, - - "regex": { - "description": "Regular expression for validating the stderr value", - "type": "string" - }, - - "is": { - "description": "Value the stderr value should exactly match", - "type": "string" - }, - - "case_insensitive": { - "description": "Whether the comparison should be case insensitive", - "type": "boolean" - } - }, - "oneOf": [ - { - "required": [ - "contains" - ] - }, - { - "required": [ - "regex" - ] - }, - { - "required": [ - "is" - ] - } - ] - }, - "filename": { - "description": "Expected new filename from the payload build results", - "type": "object", - "additionalProperties": false, - "properties": { - "contains": { - "description": "Text the filename should contain", - "type": "string" - }, - - "regex": { - "description": "Regular expression for validating the filename", - "type": "string" - }, - - "is": { - "description": "Value the filename should exactly match", - "type": "string" - }, - - "case_insensitive": { - "description": "Whether the comparison should be case insensitive", - "type": "boolean" - } - }, - "oneOf": [ - { - "required": [ - "contains" - ] - }, - { - "required": [ - "regex" - ] - }, - { - "required": [ - "is" - ] - } - ] - }, - "commands": { - "description": "List of expected updated commands", - "type": "array" - } - } - } - } -}