From e6736530f018d3d193ebe9e3ca03c201788205cd Mon Sep 17 00:00:00 2001 From: s-b-repo Date: Sat, 13 Jun 2026 14:47:50 +0200 Subject: [PATCH] creds bruteforce: run built-in defaults when no wordlist is set MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit creds_helper required a password wordlist (cfg_prompt_existing_file with no default), so in a mass scan with no 'setg password_wordlist' every host errored 'Missing required prompt key' before the module's built-in DEFAULTS were ever tried — making bruteforce sweeps look stuck/broken (the RTSP report). Wordlists are now OPTIONAL when the module ships defaults: a missing wordlist falls back to defaults-only instead of erroring. Modules with no defaults (postgres/mysql/snmp) still require a wordlist. Verified: batch-mode elasticsearch_bruteforce with no wordlist runs its 6 defaults and completes (was: error). --- src/utils/creds_helper.rs | 51 +++++++++++++++++++++++++++++++-------- 1 file changed, 41 insertions(+), 10 deletions(-) diff --git a/src/utils/creds_helper.rs b/src/utils/creds_helper.rs index abe6ef7..ebcebc1 100644 --- a/src/utils/creds_helper.rs +++ b/src/utils/creds_helper.rs @@ -101,13 +101,32 @@ where // Prompt for wordlists. cfg_prompt_* answers are cached across the // batch via the prompt cache, so a /16 scan asks once. - let username_path = if cfg.password_only { + // Wordlists are OPTIONAL when the module ships built-in defaults. A missing + // wordlist (e.g. an unattended mass scan with no `setg password_wordlist`) + // falls back to defaults-only instead of erroring on every host — which is + // what made bruteforce mass-scans look stuck/broken. If the module has no + // defaults, a wordlist is still required. + let have_defaults = !cfg.defaults.is_empty(); + let username_path: Option = if cfg.password_only { None } else { - Some(cfg_prompt_existing_file("username_wordlist", "Username wordlist").await?) + match cfg_prompt_existing_file("username_wordlist", "Username wordlist").await { + Ok(p) => Some(p), + Err(e) if have_defaults => { + tracing::debug!("{}: no username wordlist ({e}); using built-in defaults only", cfg.service_name); + None + } + Err(e) => return Err(e), + } + }; + let password_path: Option = match cfg_prompt_existing_file("password_wordlist", "Password wordlist").await { + Ok(p) => Some(p), + Err(e) if have_defaults => { + tracing::debug!("{}: no password wordlist ({e}); using built-in defaults only", cfg.service_name); + None + } + Err(e) => return Err(e), }; - let password_path = - cfg_prompt_existing_file("password_wordlist", "Password wordlist").await?; // `cartesian` = full cross-product (every user × every password); // `paired` = user[i] with pass[i]. (cred-file mode is not implemented here.) let combo_input = cfg_prompt_default( @@ -133,21 +152,33 @@ where // `MAX_FILE_SIZE` cap turns a big rockyou-class list into a hard error. // Instead, route them into the streaming brute-force engine, which reads // the file in bounded batches. Small files keep the exact eager path. - let stream_passwords = file_size(&password_path) > STREAMING_THRESHOLD; + let stream_passwords = password_path + .as_deref() + .map(|p| file_size(p) > STREAMING_THRESHOLD) + .unwrap_or(false); // For the small-file path we load eagerly so we can validate emptiness and - // preserve the historical "defaults first" combo ordering. + // preserve the historical "defaults first" combo ordering. No password + // wordlist (defaults-only mode) yields an empty list — the defaults below + // still run. let passwords: Vec = if stream_passwords { Vec::new() } else { - load_lines(&password_path)? + match password_path.as_deref() { + Some(p) => load_lines(p)?, + None => Vec::new(), + } }; if !cfg.password_only && usernames.is_empty() { return Err(anyhow!("Username wordlist is empty")); } - if !stream_passwords && passwords.is_empty() { - return Err(anyhow!("Password wordlist is empty")); + // An empty password set is only an error when there are no built-in defaults + // to fall back on; otherwise defaults-only is a valid run. + if !stream_passwords && passwords.is_empty() && !have_defaults { + return Err(anyhow!( + "Password wordlist is empty and module has no built-in defaults" + )); } // Defaults are the highest-yield rows. In the eager path they go first; in @@ -194,7 +225,7 @@ where run_bruteforce_streaming( &bf_config, usernames, - Some(password_path.as_str()), + password_path.as_deref(), passwords, mode, defaults,