creds bruteforce: hydra-style -e nsr (null/same/reversed) + expose wordlist/cred_extras options

Adds the highest-yield hydra feature to the shared creds_helper: for each
username, optionally also try an empty password (null), the username as its own
password (same), and the reversed username. Opt-in via 'setg cred_extras' with a
subset of n/s/r (e.g. nsr); default off. Appended to the defaults-first rows so
they run before the wordlist; the engine's existing combo dedup avoids retrying
a pair already in defaults. Skipped for password-only services.

Also surfaced username_wordlist/password_wordlist/cred_extras in 'show options'.

Verified: elasticsearch_bruteforce + cred_extras=nsr + 1 username -> 6 defaults +
null + reversed (same deduped against the admin/admin default) = 8 attempts.
This commit is contained in:
s-b-repo
2026-06-13 15:55:28 +02:00
parent e6736530f0
commit 5968d25b58
2 changed files with 61 additions and 1 deletions
+3
View File
@@ -1308,6 +1308,9 @@ async fn display_all_options(ctx: &ShellContext) {
("exclusions", "Extra networks to skip in mass scans (CIDR,CIDR,...)", ""),
("target_rps", "Per-target rate limit (req/s, 0 = unlimited)", ""),
("module_rps", "Global module rate limit (req/s, 0 = unlimited)", ""),
("username_wordlist", "Bruteforce username wordlist path", ""),
("password_wordlist", "Bruteforce password wordlist path", ""),
("cred_extras", "Bruteforce extra creds: hydra-style n/s/r (null/same/reversed), e.g. nsr", ""),
];
let mut displayed = std::collections::HashSet::new();
+58 -1
View File
@@ -184,12 +184,37 @@ where
// Defaults are the highest-yield rows. In the eager path they go first; in
// the streaming path they're passed as `extra_combos` (the engine runs them
// after the streamed batches).
let defaults: Vec<(String, String)> = cfg
let mut defaults: Vec<(String, String)> = cfg
.defaults
.iter()
.map(|(u, p)| (u.to_string(), p.to_string()))
.collect();
// Hydra-style `-e nsr`: for each username also try an empty password (null),
// the username as its own password (same), and the reversed username. Opt in
// via `setg cred_extras <subset of n/s/r>` (e.g. "nsr"); default off. Appended
// to the high-yield "defaults first" rows so they run before the wordlist.
// Skipped for password-only services (no username to derive from).
if !cfg.password_only {
let extras = cred_extras();
if extras.any() {
for u in &usernames {
if u.is_empty() {
continue;
}
if extras.null {
defaults.push((u.clone(), String::new()));
}
if extras.same {
defaults.push((u.clone(), u.clone()));
}
if extras.reversed {
defaults.push((u.clone(), u.chars().rev().collect::<String>()));
}
}
}
}
let bf_config = BruteforceConfig {
target: host.clone(),
port,
@@ -339,3 +364,35 @@ async fn is_port_open(host: &str, port: u16) -> bool {
};
crate::utils::tcp_port_open(ip, port, Duration::from_secs(2)).await
}
/// Hydra-style `-e` extra-credential flags: null password, password == login
/// (same), and reversed login.
struct CredExtras {
null: bool,
same: bool,
reversed: bool,
}
impl CredExtras {
fn any(&self) -> bool {
self.null || self.same || self.reversed
}
}
/// Parse `setg cred_extras` (a hydra-style subset of `n`/`s`/`r`, e.g. "nsr").
/// Empty / "none" / "off" / "no" disables it (the default when unset).
fn cred_extras() -> CredExtras {
let none = CredExtras { null: false, same: false, reversed: false };
let raw = match crate::tenant::resolve().global_options().try_get("cred_extras") {
Some(s) => s.to_lowercase(),
None => return none,
};
if matches!(raw.trim(), "" | "none" | "off" | "no" | "0") {
return none;
}
CredExtras {
null: raw.contains('n'),
same: raw.contains('s'),
reversed: raw.contains('r'),
}
}