pub mod scanner;
pub mod types;
use std::collections::HashMap;
use std::error::Error;
use std::sync::Arc;
use futures::stream::{self, StreamExt};
use log::{debug, info, trace};
use tokio::sync::Semaphore;
use tokio::time::{timeout, Duration};
use crate::args::Options;
use crate::modules::webclient::types::Outcome;
use crate::objects::common::WebClientRunning;
use crate::objects::computer::Computer;
use crate::transport::smb::{connect_ipc, is_reachable, nt_hash_from_str, smb_user, SmbAuth};
const DEFAULT_CONCURRENCY: usize = 10; const DEFAULT_PORT_TIMEOUT_MS: u64 = 1_500; const DEFAULT_HOST_TIMEOUT_MS: u64 = 8_000; const DEFAULT_EXPIRY_DAYS: i64 = 60;
pub async fn run(
args: &Options,
computers: &mut Vec<Computer>,
) -> Result<(), Box<dyn Error>> {
if !args.collection_method.does_web_client() {
debug!("[webclient] collection method does not contact hosts - skipping");
return Ok(());
}
let targets: Vec<(String, String)> = computers
.iter()
.filter(|c| c.is_active(DEFAULT_EXPIRY_DAYS))
.map(|c| (c.properties().name().clone(), c.object_identifier().clone()))
.collect();
info!("[webclient] {} active target(s) after expiry/enabled filter", targets.len());
if targets.is_empty() {
return Ok(());
}
let sem = Arc::new(Semaphore::new(DEFAULT_CONCURRENCY));
let domain = args.domain.clone();
let user = smb_user(args.username.as_deref().unwrap_or_default());
let password = args.password.clone().unwrap_or_default();
let nt_hash = nt_hash_from_str(args.hashes.as_deref().unwrap_or_default());
let kerberos_ccache: Option<String> = if args.kerberos {
std::env::var("KRB5CCNAME").ok()
} else {
None
};
let kdc: String = args
.ldapfqdn
.clone()
.filter(|s| !s.is_empty())
.or_else(|| args.ip.clone())
.unwrap_or_else(|| args.domain.clone());
let results: Vec<(String, Outcome)> = stream::iter(targets)
.map(|(host, oid)| {
let (sem, domain, user, password) =
(sem.clone(), domain.clone(), user.clone(), password.clone());
let nt_hash = nt_hash;
let kerberos_ccache = kerberos_ccache.clone();
let kdc = kdc.clone();
async move {
let _permit = sem.acquire().await.unwrap();
let outcome = probe_host(
&host, &domain, &user, &password,
nt_hash.as_ref(), kerberos_ccache.as_deref(), &kdc,
).await;
(oid, outcome)
}
})
.buffer_unordered(DEFAULT_CONCURRENCY)
.collect()
.await;
let mut running = 0usize;
let mut map: HashMap<String, WebClientRunning> = HashMap::with_capacity(results.len());
for (oid, outcome) in &results {
if outcome.is_running() {
running += 1;
}
if let Some(reason) = outcome.failure_reason() {
debug!("[webclient] {oid}: {reason}");
}
map.insert(oid.clone(), api_result(outcome));
}
for c in computers.iter_mut() {
if let Some(v) = map.remove(c.object_identifier()) {
c.set_is_web_client_running(v);
}
}
info!("[webclient] WebClient running on {running}/{} probed host(s)",results.len());
Ok(())
}
fn api_result(o: &Outcome) -> WebClientRunning {
WebClientRunning {
result: o.is_running(),
collected: o.collected(),
failure_reason: o.failure_reason(),
}
}
#[allow(clippy::too_many_arguments)]
async fn probe_host(
host: &str,
domain: &str,
user: &str,
password: &str,
nt_hash: Option<&[u8; 16]>,
kerberos_ccache: Option<&str>,
kdc: &str,
) -> Outcome {
if !is_reachable(host, DEFAULT_PORT_TIMEOUT_MS).await {
trace!("[{host}] 445/tcp unreachable - skip");
return Outcome::Unreachable(format!("{host}: 445/tcp unreachable"));
}
let work = async {
let mut smb = if let Some(ccache) = kerberos_ccache {
let spn = format!("cifs/{host}");
let (gss_blob, session_key) =
match crate::transport::kerberos::kerberos_material_for(ccache, &spn, kdc).await {
Ok(m) => m,
Err(e) => return Outcome::AuthFailed(format!("{host} krb: {e}")),
};
let auth = SmbAuth::Kerberos { gss_blob: &gss_blob, session_key: &session_key };
match connect_ipc(host, domain, user, auth).await {
Ok(c) => c,
Err(e) => return connect_error(host, &e),
}
} else {
let auth = match nt_hash {
Some(h) => SmbAuth::Hash(h),
None => SmbAuth::Password(password),
};
match connect_ipc(host, domain, user, auth).await {
Ok(c) => c,
Err(e) => return connect_error(host, &e),
}
};
debug!("[{host}] IPC$ ready, probing WebClient pipe");
scanner::probe(&mut smb, host).await
};
match timeout(Duration::from_millis(DEFAULT_HOST_TIMEOUT_MS), work).await {
Ok(outcome) => outcome,
Err(_) => Outcome::Unreachable(format!("{host}: per-host timeout")),
}
}
fn connect_error(host: &str, e: &anyhow::Error) -> Outcome {
let msg = format!("{host}: {e}");
let s = e.to_string();
if s.starts_with("auth") {
Outcome::AuthFailed(msg)
} else if s.starts_with("connect:") {
Outcome::Unreachable(msg)
} else if s.contains("tree connect") {
Outcome::AccessDenied(msg)
} else {
Outcome::Error(msg)
}
}