From f8f3a02298d1c48c63115f7d5f30f6dbf10d34a0 Mon Sep 17 00:00:00 2001 From: Simone Margaritelli Date: Mon, 18 Dec 2023 14:44:21 +0100 Subject: [PATCH] new: added samba module (ref #28) --- .dockerignore | 3 +- Cargo.lock | 21 +++++ Cargo.toml | 3 + Dockerfile | 6 +- README.md | 2 +- src/main.rs | 2 +- src/options.rs | 3 + src/plugins/mod.rs | 6 +- src/plugins/samba/mod.rs | 152 +++++++++++++++++++++++++++++++++++ src/plugins/samba/options.rs | 13 +++ 10 files changed, 204 insertions(+), 7 deletions(-) create mode 100644 src/plugins/samba/mod.rs create mode 100644 src/plugins/samba/options.rs diff --git a/.dockerignore b/.dockerignore index 158f0b3..c51932c 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,4 +2,5 @@ /.vscode /data /test-servers -/release.py \ No newline at end of file +/release.py +/target \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 23676fc..4b8fec3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -568,6 +568,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + [[package]] name = "chacha20" version = "0.9.1" @@ -2024,6 +2030,7 @@ dependencies = [ "ntlmclient", "num_cpus", "paho-mqtt", + "pavao", "rand 0.8.5", "rdp-rs", "regex", @@ -2686,6 +2693,20 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" +[[package]] +name = "pavao" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bcafb8d680c0b6750c095fbd3c9263fc0b3c315e6055cd1867db038641c1757" +dependencies = [ + "cfg_aliases", + "lazy_static", + "libc", + "log", + "pkg-config", + "thiserror", +] + [[package]] name = "pbkdf2" version = "0.11.0" diff --git a/Cargo.toml b/Cargo.toml index c93afd2..1739042 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -78,6 +78,7 @@ rdp-rs = { version = "0.1.0", optional = true } scylla = { version = "0.10.1", optional = true } paho-mqtt = { version = "0.12.3", optional = true } csv = "1.3.0" +pavao = { version = "0.2.3", optional = true } [dev-dependencies] tempfile = "3.8.0" @@ -107,6 +108,7 @@ default = [ "redis", "scylla", "tcp_ports", + "samba", ] http = ["dep:url", "dep:reqwest", "dep:base64", "dep:ntlmclient"] dns = ["dep:trust-dns-resolver"] @@ -134,6 +136,7 @@ amqp = [] redis = [] scylla = ["dep:scylla"] tcp_ports = [] +samba = ["dep:pavao"] [profile.release] lto = true # Enable link-time optimization diff --git a/Dockerfile b/Dockerfile index 25d8b59..ec27232 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,13 @@ FROM rust:bullseye as builder -RUN apt-get update && apt-get install -y libssl-dev ca-certificates cmake git +RUN apt-get update && apt-get install -y libsmbclient-dev libssl-dev ca-certificates cmake git WORKDIR /app ADD . /app RUN cargo build --release +RUN ls -la /usr/lib/ -FROM gcr.io/distroless/cc-debian11 +FROM debian:bullseye +RUN apt-get update && apt-get install -y libsmbclient libssl-dev ca-certificates COPY --from=builder /app/target/release/legba /usr/bin/legba ENTRYPOINT ["/usr/bin/legba"] \ No newline at end of file diff --git a/README.md b/README.md index 979b8d4..8000d52 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ For the building instructions, usage and the complete list of options [check the ## Supported Protocols/Features: -AMQP (ActiveMQ, RabbitMQ, Qpid, JORAM and Solace), Cassandra/ScyllaDB, DNS subdomain enumeration, FTP, HTTP (basic authentication, NTLMv1, NTLMv2, multipart form, custom requests with CSRF support, files/folders enumeration, virtual host enumeration), IMAP, Kerberos pre-authentication and user enumeration, LDAP, MongoDB, MQTT, Microsoft SQL, MySQL, Oracle, PostgreSQL, POP3, RDP, Redis, SSH / SFTP, SMTP, STOMP (ActiveMQ, RabbitMQ, HornetQ and OpenMQ), TCP port scanning, Telnet, VNC. +AMQP (ActiveMQ, RabbitMQ, Qpid, JORAM and Solace), Cassandra/ScyllaDB, DNS subdomain enumeration, FTP, HTTP (basic authentication, NTLMv1, NTLMv2, multipart form, custom requests with CSRF support, files/folders enumeration, virtual host enumeration), IMAP, Kerberos pre-authentication and user enumeration, LDAP, MongoDB, MQTT, Microsoft SQL, MySQL, Oracle, PostgreSQL, POP3, RDP, Redis, Samba, SSH / SFTP, SMTP, STOMP (ActiveMQ, RabbitMQ, HornetQ and OpenMQ), TCP port scanning, Telnet, VNC. ## Benchmark diff --git a/src/main.rs b/src/main.rs index a51d39b..3ca4f0a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -27,7 +27,7 @@ fn setup() -> Result { if env::var_os("RUST_LOG").is_none() { // set `RUST_LOG=debug` to see debug logs - env::set_var("RUST_LOG", "info,blocking=off"); + env::set_var("RUST_LOG", "info,blocking=off,pavao=off"); } env_logger::builder() diff --git a/src/options.rs b/src/options.rs index 65bdb1d..9bfd82c 100644 --- a/src/options.rs +++ b/src/options.rs @@ -96,6 +96,9 @@ pub(crate) struct Options { #[cfg(feature = "telnet")] #[clap(flatten, next_help_heading = "TELNET")] pub telnet: crate::plugins::telnet::options::Options, + #[cfg(feature = "samba")] + #[clap(flatten, next_help_heading = "SAMBA")] + pub smb: crate::plugins::samba::options::Options, #[cfg(feature = "ssh")] #[clap(flatten, next_help_heading = "SSH")] pub ssh: crate::plugins::ssh::options::Options, diff --git a/src/plugins/mod.rs b/src/plugins/mod.rs index a0c7cb6..ee388c0 100644 --- a/src/plugins/mod.rs +++ b/src/plugins/mod.rs @@ -27,10 +27,10 @@ pub(crate) mod kerberos; pub(crate) mod ldap; #[cfg(feature = "mongodb")] pub(crate) mod mongodb; -#[cfg(feature = "mssql")] -mod mssql; #[cfg(feature = "mqtt")] pub(crate) mod mqtt; +#[cfg(feature = "mssql")] +mod mssql; #[cfg(feature = "oracle")] pub(crate) mod oracle; // optional as it requires libclntsh that's a pain to install and configure #[cfg(feature = "pop3")] @@ -39,6 +39,8 @@ pub(crate) mod pop3; pub(crate) mod rdp; #[cfg(feature = "redis")] pub(crate) mod redis; +#[cfg(feature = "samba")] +pub(crate) mod samba; #[cfg(feature = "scylla")] pub(crate) mod scylla; #[cfg(feature = "smtp")] diff --git a/src/plugins/samba/mod.rs b/src/plugins/samba/mod.rs new file mode 100644 index 0000000..72b2ce1 --- /dev/null +++ b/src/plugins/samba/mod.rs @@ -0,0 +1,152 @@ +use std::collections::HashMap; +use std::time::Duration; + +use async_trait::async_trait; +use ctor::ctor; +use pavao::{SmbClient, SmbCredentials, SmbDirentType, SmbOptions}; +use tokio::sync::Mutex; + +use crate::creds::Credentials; +use crate::session::{Error, Loot}; +use crate::Plugin; +use crate::{utils, Options}; + +use lazy_static::lazy_static; + +pub(crate) mod options; + +lazy_static! { + static ref SHARE_CACHE: Mutex> = Mutex::new(HashMap::new()); + static ref PAVAO_LOCK: Mutex = tokio::sync::Mutex::new(true); +} + +#[ctor] +fn register() { + crate::plugins::manager::register("smb", Box::new(SMB::new())); +} + +#[derive(Clone)] +pub(crate) struct SMB { + share: Option, + workgroup: String, +} + +impl SMB { + pub fn new() -> Self { + SMB { + share: None, + workgroup: String::default(), + } + } + + fn get_samba_client( + &self, + server: &str, + workgroup: &str, + share: &str, + username: &str, + password: &str, + ) -> Result { + SmbClient::new( + SmbCredentials::default() + .server(server) + .share(share) + .username(username) + .password(password) + .workgroup(workgroup), + SmbOptions::default() + .no_auto_anonymous_login(false) + .one_share_per_server(true), + ) + .map_err(|e| format!("error creating client for {}: {}", share, e.to_string())) + } + + async fn get_share_for(&self, target: &str) -> Result { + if let Some(share) = self.share.as_ref() { + // return from arguments + return Ok(share.clone()); + } + + let mut guard = SHARE_CACHE.lock().await; + if let Some(share) = guard.get(target) { + // return from cache + return Ok(share.clone()); + } + + // get from listing + log::info!("searching private share for {} ...", target); + + let server = format!("smb://{}", target); + let root_cli = self.get_samba_client(&server, &self.workgroup, "", "", "")?; + for entry in root_cli.list_dir("").unwrap() { + match entry.get_type() { + SmbDirentType::FileShare | SmbDirentType::Dir => { + let share = format!("/{}", entry.name()); + // if share is private we expect an error + let sub_cli = + self.get_samba_client(&server, &self.workgroup, &share, "", "")?; + let listing = sub_cli.list_dir(""); + if listing.is_err() { + log::info!("{}{} found", &server, &share); + // found a private share, update the cache and return. + guard.insert(target.to_owned(), share.clone()); + return Ok(share); + } + } + _ => {} + } + } + + return Err(format!( + "could not find private share for {}, provide one with --smb-share", + target + )); + } +} + +#[async_trait] +impl Plugin for SMB { + fn description(&self) -> &'static str { + "Samba password authentication." + } + + fn setup(&mut self, opts: &Options) -> Result<(), Error> { + self.share = opts.smb.smb_share.clone(); + self.workgroup = opts.smb.smb_workgroup.clone(); + Ok(()) + } + + async fn attempt(&self, creds: &Credentials, timeout: Duration) -> Result, Error> { + let address = utils::parse_target_address(&creds.target, 445)?; + let server = format!("smb://{}", &address); + let share = tokio::time::timeout(timeout, self.get_share_for(&address)) + .await + .map_err(|e: tokio::time::error::Elapsed| e.to_string())? + .map_err(|e| e.to_string())?; + + // HACK: pavao doesn't seem to be thread safe, so we need to acquire this lock here. + // Sadly this decreases performances, but it appears that there are no alternatives + // for rust :/ + let _guard = PAVAO_LOCK.lock().await; + let client = self.get_samba_client( + &server, + &self.workgroup, + &share, + &creds.username, + &creds.password, + )?; + + return if client.list_dir("/").is_ok() { + Ok(Some(Loot::new( + "smb", + &address, + [ + ("username".to_owned(), creds.username.to_owned()), + ("password".to_owned(), creds.password.to_owned()), + ], + ))) + } else { + Ok(None) + }; + } +} diff --git a/src/plugins/samba/options.rs b/src/plugins/samba/options.rs new file mode 100644 index 0000000..5f372e4 --- /dev/null +++ b/src/plugins/samba/options.rs @@ -0,0 +1,13 @@ +use clap::Parser; +use serde::{Deserialize, Serialize}; + +#[derive(Parser, Debug, Serialize, Deserialize, Clone, Default)] +#[group(skip)] +pub(crate) struct Options { + #[clap(long, default_value = "WORKGROUP", help_heading = "SMB")] + /// Samba workgroup name. + pub smb_workgroup: String, + #[clap(long, help_heading = "SMB")] + /// Expicitly set Samba private share to test. + pub smb_share: Option, +}