diff --git a/Cargo.lock b/Cargo.lock index 9f308e6..30f6062 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2054,6 +2054,7 @@ dependencies = [ "scylla", "serde", "serde_json", + "shell-words", "sibyl", "sqlx", "tempfile", @@ -3805,6 +3806,12 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "shell-words" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" + [[package]] name = "sibyl" version = "0.6.16" diff --git a/Cargo.toml b/Cargo.toml index f5bd190..6be99d9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -80,6 +80,7 @@ paho-mqtt = { version = "0.12.3", optional = true } csv = "1.3.0" pavao = { version = "0.2.3", optional = true } fast-socks5 = { version = "0.9.2", optional = true } +shell-words = "1.1.0" [dev-dependencies] tempfile = "3.8.0" diff --git a/src/options.rs b/src/options.rs index abf32e4..40f5719 100644 --- a/src/options.rs +++ b/src/options.rs @@ -84,6 +84,8 @@ pub(crate) struct Options { #[clap(short = 'Q', long, default_value_t = false)] pub quiet: bool, + #[clap(flatten, next_help_heading = "COMMAND (CMD)")] + pub cmd: crate::plugins::cmd::options::Options, #[cfg(feature = "amqp")] #[clap(flatten, next_help_heading = "AMQP")] pub amqp: crate::plugins::amqp::options::Options, @@ -97,7 +99,7 @@ pub(crate) struct Options { #[clap(flatten, next_help_heading = "TELNET")] pub telnet: crate::plugins::telnet::options::Options, #[cfg(feature = "samba")] - #[clap(flatten, next_help_heading = "SAMBA")] + #[clap(flatten, next_help_heading = "SAMBA (SMB)")] pub smb: crate::plugins::samba::options::Options, #[cfg(feature = "ssh")] #[clap(flatten, next_help_heading = "SSH")] diff --git a/src/plugins/cmd/mod.rs b/src/plugins/cmd/mod.rs new file mode 100644 index 0000000..400d7a9 --- /dev/null +++ b/src/plugins/cmd/mod.rs @@ -0,0 +1,110 @@ +use std::process::Stdio; +use std::time::Duration; + +use async_trait::async_trait; +use ctor::ctor; + +use crate::session::{Error, Loot}; +use crate::Plugin; +use crate::{utils, Options}; + +use crate::creds::Credentials; + +pub(crate) mod options; + +#[ctor] +fn register() { + crate::plugins::manager::register("cmd", Box::new(Command::new())); +} + +#[derive(Clone)] +pub(crate) struct Command { + opts: options::Options, +} + +impl Command { + pub fn new() -> Self { + Command { + opts: options::Options::default(), + } + } + + async fn run(&self, creds: &Credentials) -> Result { + let (target, port) = utils::parse_target(&creds.target, 0)?; + let args = shell_words::split( + &self + .opts + .cmd_args + .replace("{USERNAME}", &creds.username) + .replace("{PASSWORD}", &creds.password) + .replace("{TARGET}", &target) + .replace("{PORT}", &format!("{}", port)), + ) + .unwrap(); + + log::debug!("{} {}", &self.opts.cmd_binary, args.join(" ")); + + let child = std::process::Command::new(&self.opts.cmd_binary) + .args(&args) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn() + .map_err(|e| e.to_string())?; + + child.wait_with_output().map_err(|e| e.to_string()) + } +} + +#[async_trait] +impl Plugin for Command { + fn description(&self) -> &'static str { + "Command execution." + } + + fn setup(&mut self, opts: &Options) -> Result<(), Error> { + self.opts = opts.cmd.clone(); + Ok(()) + } + + async fn attempt(&self, creds: &Credentials, timeout: Duration) -> Result, Error> { + let output = tokio::time::timeout(timeout, self.run(creds)) + .await + .map_err(|e| e.to_string())?; + + if let Ok(out) = output { + let stdout = String::from_utf8_lossy(&out.stdout); + let stderr = String::from_utf8_lossy(&out.stderr); + if !stderr.is_empty() { + log::error!("{}", stderr); + } + + log::debug!("{}", &stdout); + + // check exit code first + if out.status.code().unwrap_or(-1) == self.opts.cmd_success_exit_code { + // then output if needed + let ok = if let Some(pattern) = &self.opts.cmd_success_match { + stdout.contains(pattern) + } else { + true + }; + + if ok { + return Ok(Some(Loot::new( + "command", + &creds.target, + [ + ("username".to_owned(), creds.username.to_owned()), + ("password".to_owned(), creds.password.to_owned()), + ], + ))); + } + } + + return Ok(None); + } else { + return Err(output.err().unwrap().to_string()); + } + } +} diff --git a/src/plugins/cmd/options.rs b/src/plugins/cmd/options.rs new file mode 100644 index 0000000..8bcfbfd --- /dev/null +++ b/src/plugins/cmd/options.rs @@ -0,0 +1,22 @@ +use clap::Parser; +use serde::{Deserialize, Serialize}; + +#[derive(Parser, Debug, Serialize, Deserialize, Clone, Default)] +#[group(skip)] +pub(crate) struct Options { + #[clap(long)] + /// Command binary path. + pub cmd_binary: String, + + #[clap(long, default_value = "")] + /// Command arguments. {USERNAME}, {PASSWORD}, {TARGET} and {PORT} can be used as placeholders. + pub cmd_args: String, + + #[clap(long, default_value_t = 0)] + /// Process exit code to be considered as a positive match. + pub cmd_success_exit_code: i32, + + #[clap(long)] + /// String to look for in the process standard output to be considered as a positive match. + pub cmd_success_match: Option, +} diff --git a/src/plugins/mod.rs b/src/plugins/mod.rs index 1ae31fa..99a317b 100644 --- a/src/plugins/mod.rs +++ b/src/plugins/mod.rs @@ -6,9 +6,10 @@ pub(crate) use plugin::Plugin; // TODO: AFP // TODO: SNMP -// TODO: SMB // TODO: network discovery +pub(crate) mod cmd; + #[cfg(feature = "amqp")] pub(crate) mod amqp; #[cfg(feature = "cassandra")] diff --git a/src/report.rs b/src/report.rs index 39e436f..02d263b 100644 --- a/src/report.rs +++ b/src/report.rs @@ -6,7 +6,7 @@ use memory_stats::memory_stats; use crate::Session; -pub(crate) async fn statistics(session: Arc) { +pub(crate) fn statistics(session: Arc) { let one_sec = time::Duration::from_millis(1000); while !session.is_stop() { std::thread::sleep(one_sec);