use aes::cipher::{BlockDecryptMut, KeyIvInit};
use anyhow::Result;
use std::time::Duration;
use tokio::process::Command;
use tokio::time::timeout;
use crate::types::{Finding, ModuleResult, Severity, StageTimer};
use crate::ui;
#[allow(dead_code)]
type Aes256CbcDec = cbc::Decryptor<aes::Aes256>;
#[allow(dead_code)]
pub async fn run_null_session(target: &str) -> Result<ModuleResult> {
ui::section("SMB NULL SESSION");
let timer = StageTimer::start();
let spin = ui::spinner("SMB-NULL");
let mut result = ModuleResult::new("smb-null");
spin.set_message("testing anonymous access...");
let out = timeout(
Duration::from_secs(15),
Command::new("smbclient")
.args(["-N", "-L", target, "--no-pass"])
.output(),
)
.await;
match out {
Ok(Ok(output)) => {
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
let combined = format!("{}\n{}", stdout, stderr);
ui::verbose_output("smbclient", &combined);
if combined.contains("Sharename") || combined.contains("IPC$") {
ui::success("Null session accepted — share listing available");
let shares = parse_smbclient_shares(&combined);
if !shares.is_empty() {
ui::info(&format!("{} share(s) visible:", shares.len()));
for (name, stype, comment) in &shares {
ui::kv(&format!(" {}", name), &format!("{} — {}", stype, comment));
}
}
let finding = Finding::new(
"smb",
"SMB-001",
Severity::Medium,
"SMB null session permitted",
)
.with_description("Anonymous SMB access is allowed, enabling share enumeration")
.with_recommendation("Disable null session access via RestrictAnonymous registry key")
.with_mitre("T1021.002");
result.findings.push(finding);
} else if combined.to_lowercase().contains("nt_status_access_denied")
|| combined.to_lowercase().contains("nt_status_logon_failure")
{
ui::info("Null session rejected (expected)");
} else {
ui::info(&format!("SMB response: {}", combined.lines().next().unwrap_or("unknown")));
}
}
Ok(Err(e)) => {
ui::warning(&format!("smbclient not available: {}", e));
}
Err(_) => {
ui::warning("SMB null session test timed out");
}
}
spin.set_message("checking SMB signing...");
check_smb_signing(target, &mut result).await;
ui::finish_spinner(&spin, "null session check complete");
ui::stage_done("SMB NULL", "done", &timer.elapsed_pretty());
result = result.success(timer.elapsed());
Ok(result)
}
pub async fn run_authenticated(
target: &str,
domain: &str,
username: &str,
password: &str,
ntlm: Option<&str>,
tags: &[String],
) -> Result<ModuleResult> {
ui::section("SMB AUTHENTICATED");
let timer = StageTimer::start();
let spin = ui::spinner("SMB-AUTH");
let mut result = ModuleResult::new("smb-auth");
let should_run = |tag: &str| -> bool {
tags.is_empty() || tags.iter().any(|t| t.eq_ignore_ascii_case(tag))
};
spin.set_message("listing shares...");
let shares = list_shares_authenticated(target, domain, username, password, ntlm).await;
match shares {
Ok(share_list) => {
if share_list.is_empty() {
ui::info("No shares visible");
} else {
ui::success(&format!("{} share(s) accessible:", share_list.len()));
for (name, stype, comment) in &share_list {
ui::kv(&format!(" {}", name), &format!("{} — {}", stype, comment));
}
let interesting = ["ADMIN$", "C$", "NETLOGON", "SYSVOL"];
let admin_shares: Vec<_> = share_list
.iter()
.filter(|(n, _, _)| interesting.iter().any(|i| n.eq_ignore_ascii_case(i)))
.collect();
if admin_shares.iter().any(|(n, _, _)| n.eq_ignore_ascii_case("ADMIN$") || n == "C$") {
let finding = Finding::new(
"smb",
"SMB-002",
Severity::High,
"Administrative share access (ADMIN$/C$)",
)
.with_description("Current credentials have access to administrative shares, indicating local admin privileges")
.with_recommendation("Restrict local admin access; implement LAPS and tiered admin model")
.with_mitre("T1021.002");
result.findings.push(finding);
ui::warning("Administrative share access detected (ADMIN$/C$)!");
}
}
}
Err(e) => {
ui::warning(&format!("Share enumeration failed: {}", e));
}
}
if should_run("gpp") || should_run("sysvol") {
spin.set_message("checking SYSVOL for GPP passwords...");
enumerate_sysvol_gpp(target, domain, username, password, ntlm, &mut result).await;
}
ui::finish_spinner(&spin, "authenticated enumeration complete");
ui::stage_done("SMB AUTH", &format!("{} findings", result.findings.len()), &timer.elapsed_pretty());
result = result.success(timer.elapsed());
Ok(result)
}
async fn list_shares_authenticated(
target: &str,
domain: &str,
username: &str,
password: &str,
ntlm: Option<&str>,
) -> Result<Vec<(String, String, String)>> {
let user_arg = format!("{}\\{}", domain.split('.').next().unwrap_or(domain), username);
let mut args = vec!["-gL".to_string(), target.to_string()];
if let Some(hash) = ntlm {
args.extend(["--pw-nt-hash".to_string(), "-U".to_string(), format!("{}%{}", user_arg, hash)]);
} else {
args.extend(["-U".to_string(), format!("{}%{}", user_arg, password)]);
}
let output = timeout(
Duration::from_secs(20),
Command::new("smbclient").args(&args).output(),
)
.await??;
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
ui::verbose_output("smbclient", &stdout);
ui::verbose_output("smbclient", &stderr);
Ok(parse_smbclient_shares(&stdout))
}
fn parse_smbclient_shares(output: &str) -> Vec<(String, String, String)> {
let mut shares = Vec::new();
for line in output.lines() {
let parts: Vec<&str> = line.split('|').collect();
if parts.len() >= 2 {
let stype = parts[0].trim().to_string();
let name = parts[1].trim().to_string();
let comment = if parts.len() > 2 {
parts[2].trim().to_string()
} else {
String::new()
};
if stype == "Disk" || stype == "IPC" || stype == "Printer" {
shares.push((name, stype, comment));
}
}
}
shares
}
async fn enumerate_sysvol_gpp(
target: &str,
domain: &str,
username: &str,
password: &str,
ntlm: Option<&str>,
result: &mut ModuleResult,
) {
let user_arg = format!(
"{}\\{}",
domain.split('.').next().unwrap_or(domain),
username
);
let unc = format!("\\\\{}\\SYSVOL", target);
let mut args = vec![unc.clone(), "-c".to_string(), "recurse; ls".to_string()];
if let Some(hash) = ntlm {
args.extend(["--pw-nt-hash".to_string(), "-U".to_string(), format!("{}%{}", user_arg, hash)]);
} else {
args.extend(["-U".to_string(), format!("{}%{}", user_arg, password)]);
}
let output = timeout(
Duration::from_secs(30),
Command::new("smbclient").args(&args).output(),
)
.await;
let Ok(Ok(out)) = output else { return };
let stdout = String::from_utf8_lossy(&out.stdout);
ui::verbose_output("smbclient-sysvol", &stdout);
let gpp_files = [
"Groups.xml",
"Services.xml",
"Scheduledtasks.xml",
"DataSources.xml",
"Drives.xml",
];
let mut found_gpp = false;
for line in stdout.lines() {
for gpp_file in &gpp_files {
if line.to_ascii_lowercase().contains(&gpp_file.to_ascii_lowercase()) {
ui::warning(&format!("GPP file found: {}", line.trim()));
found_gpp = true;
}
}
}
if found_gpp {
let finding = Finding::new(
"smb",
"GPP-001",
Severity::High,
"Group Policy Preference XML files found in SYSVOL",
)
.with_description("GPP XML files may contain cpassword values that can be decrypted with the publicly known AES key (MS14-025)")
.with_recommendation("Remove GPP XML files with embedded credentials; use LAPS instead")
.with_mitre("T1552.006");
result.findings.push(finding);
}
}
#[allow(dead_code)]
pub fn decrypt_gpp_password(cpassword: &str) -> Option<String> {
let key: [u8; 32] = [
0x4e, 0x99, 0x06, 0xe8, 0xfc, 0xb6, 0x6c, 0xc9, 0xfa, 0xf4, 0x93, 0x10, 0x62, 0x0f,
0xfe, 0xe8, 0xf4, 0x96, 0xe8, 0x06, 0xcc, 0x05, 0x79, 0x90, 0x20, 0x9b, 0x09, 0xa4,
0x33, 0xb6, 0x6c, 0x1b,
];
let iv = [0u8; 16];
let mut padded = cpassword.replace('-', "+").replace('_', "/");
while padded.len() % 4 != 0 {
padded.push('=');
}
let ciphertext = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, &padded).ok()?;
if ciphertext.is_empty() || ciphertext.len() % 16 != 0 {
return None;
}
let mut buf = ciphertext.clone();
let decryptor = Aes256CbcDec::new_from_slices(&key, &iv).ok()?;
let decrypted = decryptor.decrypt_padded_mut::<aes::cipher::block_padding::Pkcs7>(&mut buf).ok()?;
String::from_utf8(decrypted.to_vec()).ok()
}
async fn check_smb_signing(target: &str, result: &mut ModuleResult) {
let tools = ["nxc", "netexec", "crackmapexec"];
for tool in tools {
let out = timeout(
Duration::from_secs(10),
Command::new(tool)
.args(["smb", target])
.output(),
)
.await;
if let Ok(Ok(output)) = out {
let stdout = String::from_utf8_lossy(&output.stdout);
let combined = format!("{}\n{}", stdout, String::from_utf8_lossy(&output.stderr));
ui::verbose_output(tool, &combined);
if combined.to_lowercase().contains("signing:false")
|| combined.to_lowercase().contains("signing: false")
|| combined.contains("signing:False")
{
ui::warning("SMB signing is NOT enforced");
let finding = Finding::new(
"smb",
"SMB-003",
Severity::High,
"SMB signing not enforced",
)
.with_description("SMB signing is not required, making the host vulnerable to NTLM relay attacks")
.with_recommendation("Enable and require SMB signing via Group Policy")
.with_mitre("T1557.001");
result.findings.push(finding);
} else if combined.to_lowercase().contains("signing:true")
|| combined.to_lowercase().contains("signing: true")
{
ui::success("SMB signing is enforced");
}
return;
}
}
ui::info("SMB signing check: no compatible tool found (nxc/netexec/crackmapexec)");
}