Best practices for writing and extending brute-force / credential-checking modules.
New credential modules should use the shared harness in src/utils/creds_helper.rs
instead of hand-rolling prompt/engine/loot logic. It handles: target parsing, TCP
precheck, wordlist prompts, brute-force engine wiring, loot persistence, workspace
tracking, and Finding emission — all in one call.
use crate::utils::creds_helper::{self, CredsRun};
use crate::utils::LoginResult;
pub async fn run(ctx: &ModuleCtx) -> Result<ModuleOutcome> {
let target = ctx.target.as_single().context("module requires a single-host target")?;
creds_helper::run(
target,
CredsRun {
service_name: "myproto",
default_port: 1234,
source_module: "creds/generic/myproto_bruteforce",
defaults: &[("admin", "admin"), ("root", "")],
password_only: false,
},
|host, port, user, pass, timeout| async move {
probe(&host, port, &user, &pass, timeout).await
},
)
.await
}The probe closure receives five arguments: (String, u16, String, String, Duration).
The Duration is the user-configured timeout (from setg timeout N or the prompt),
passed through so inner probe functions honour it — no hardcoded timeouts.
For password-only protocols (VNC, SNMP), set password_only: true and drop(user)
in the closure:
|host, port, user, pass, timeout| async move {
drop(user);
probe(&host, port, &pass, timeout).await
}Return LoginResult from probes:
async fn probe(host: &str, port: u16, pass: &str, timeout: Duration) -> LoginResult {
// ... attempt login ...
LoginResult::Success // valid credential
LoginResult::AuthFailed // wrong password, keep going
LoginResult::Error { // transient or permanent error
message: format!("connect: {e}"),
retryable: true,
}
}| Function | Description |
|---|---|
connect_with_timeout(addr, deadline) |
Flattened async TCP connect → io::Result<TcpStream> |
read_exact_with_timeout(reader, buf, deadline) |
Flattened exact-read with timeout |
parse_host_port(target, default_port) |
Public host/port splitter for reuse |
For modules that don't use creds_helper::run (e.g. ftp_bruteforce with its FTPS
fallback and subnet branch), prompt interactively for:
- Port number
- Username wordlist path
- Password wordlist path
- Concurrency limit (threads / semaphore slots)
- Stop-on-success toggle
- Output file path
- Verbose logging toggle
Use the shared cfg_prompt_* helpers from crate::utils, which respect the priority chain (API custom_prompts > global options > interactive stdin):
use crate::utils::{cfg_prompt_required, cfg_prompt_default, cfg_prompt_yes_no, cfg_prompt_port};- Trim wordlist entries and skip blank lines
- Early exit if a wordlist is empty
- Validate paths — no
.., usecanonicalize() - Stream large files — for password files >150 MB, use streaming mode (see RDP module)
All bruteforce modules use the shared engine (crate::modules::creds::utils):
| Function | Use Case |
|---|---|
run_bruteforce() / run_bruteforce_streaming() |
Single-target credential testing with concurrency, progress, retry |
run_subnet_bruteforce() |
CIDR subnet scanning with per-host credential testing |
generate_combos() |
Generate user/password pairs (combo or linear mode) |
Avoid custom concurrency — always use the engine which handles semaphores, progress reporting, lockout detection, and credential storage.
Use format_addr to wrap IPv6 addresses in brackets and handle port suffixes:
// Good
let addr = format_addr(&ip, port); // "[::1]:22"Implement specific error types for better debugging and reporting:
enum CredsError {
ConnectionFailed(String),
AuthenticationFailed,
CertificateError,
Timeout,
NetworkError(String),
ProtocolError(String),
ToolNotFound,
}All TCP/UDP connections must go through the framework's network wrappers so
setg source_port <port> is honoured universally:
| Wrapper | Use case |
|---|---|
tcp_connect_str(addr, timeout) |
Async TCP from "host:port" string |
tcp_connect_addr(addr, timeout) |
Async TCP from SocketAddr |
blocking_tcp_connect(addr, timeout) |
Sync TCP for spawn_blocking contexts |
udp_bind(Some(ip)) |
UDP socket with correct address family |
Third-party library pattern: Libraries like suppaftp and telnet that
normally create their own TCP connections must receive a pre-connected stream
from the framework wrapper:
// FTP: connect through wrapper, then hand stream to suppaftp
let tcp_stream = tcp_connect_str(&addr, timeout).await?;
let mut ftp = AsyncFtpStream::connect_with_stream(tcp_stream).await?;
// Telnet: connect through wrapper, then hand stream to telnet crate
let tcp_stream = blocking_tcp_connect(&socket_addr, timeout)?;
let telnet = Telnet::from_stream(Box::new(tcp_stream), 500);Never call TcpStream::connect(), UdpSocket::bind("0.0.0.0:0"), or
library-level connect functions directly — they bypass source port binding.
Accept invalid certificates for offensive tooling convenience (e.g., danger_accept_invalid_certs(true) in reqwest / native-tls), but document this clearly in module comments and output.
Offer to write host -> user:pass pairs to a local file (default ./results.txt):
if let Some(ref path) = output_file {
let line = format!("{} -> {}:{}\n", target, user, pass);
fs::OpenOptions::new().create(true).append(true).open(path)?.write_all(line.as_bytes())?;
}| Module | Port(s) | Auth Method | Features |
|---|---|---|---|
ssh_bruteforce |
22 | libssh2 password auth | Default creds, combo mode, streaming wordlists |
ssh_spray |
22 | Password spray | One password across many targets |
ssh_user_enum |
22 | Timing attack | CVE-2018-15473 style user enumeration |
telnet_bruteforce |
23, 2323 | IAC negotiation + prompt detection | Multi-port, 55+ IoT defaults, shell verification, streaming |
telnet_hose |
23, 2323, 23231 | Default creds mass scan | 500 concurrent, multi-port per host |
rdp_bruteforce |
3389 | Native CredSSP/NTLM | NLA/TLS/RDP/Negotiate security levels |
vnc_bruteforce |
5900 | DES challenge-response (RFB) | Password-only, bit-reversed DES key |
ftp_bruteforce |
21 | FTP/FTPS LOGIN | TLS fallback, error classification |
ftp_anonymous |
21 | Anonymous login check | FTPS fallback, LIST verification |
| Module | Port(s) | Auth Method | Features |
|---|---|---|---|
smtp_bruteforce |
25, 465, 587 | SMTP AUTH (PLAIN/LOGIN/CRAM-MD5) | STARTTLS support |
pop3_bruteforce |
110, 995 | POP3 USER/PASS | TLS/STLS support |
imap_bruteforce |
143, 993 | IMAP LOGIN | IMAPS (implicit TLS), RFC 3501 escaping |
| Module | Port(s) | Auth Method | Features |
|---|---|---|---|
mysql_bruteforce |
3306 | Native wire protocol (SHA1 handshake) | HandshakeV10 parsing, salt extraction |
postgres_bruteforce |
5432 | MD5 or cleartext auth | Wire protocol, md5(md5(pass+user)+salt) |
redis_bruteforce |
6379 | AUTH command (legacy + ACL) | Redis 6+ ACL support, INFO version detection |
elasticsearch_bruteforce |
9200 | HTTP Basic Auth | Cluster detection, open-access check |
couchdb_bruteforce |
5984 | Session auth + Basic fallback | /_session POST, /_all_dbs verification |
memcached_bruteforce |
11211 | SASL PLAIN (binary protocol) | Open memcached detection, version check |
| Module | Port(s) | Auth Method | Features |
|---|---|---|---|
http_basic_bruteforce |
80, 443 | HTTP Basic Authentication | HTTPS, custom paths, redirect detection |
fortinet_bruteforce |
443 | FortiOS web login | CSRF token extraction, realm support |
| Module | Port(s) | Auth Method | Features |
|---|---|---|---|
snmp_bruteforce |
161 (UDP) | SNMPv1/v2c community strings | Custom SNMP packet, BER parsing |
| Module | Port(s) | Auth Method | Features |
|---|---|---|---|
mqtt_bruteforce |
1883, 8883 | MQTT 3.1.1 CONNECT | TLS/SSL, anonymous detection, client ID |
rtsp_bruteforce |
554 | RTSP Basic Auth | Path brute-forcing, custom headers |
| Module | Port(s) | Auth Method | Features |
|---|---|---|---|
l2tp_bruteforce |
1701 (UDP) | L2TPv2 SCCRQ detection | Gateway reachability probe (real L2TP credential brute-force needs full PPP+CHAP stack — out of scope) |
| Module | Description |
|---|---|
enablebruteforce |
Raise file descriptor limits (ulimit) for high-concurrency scans |
sample_cred_check |
Template/example credential check module |
acti_camera_default |
Multi-protocol default credential check (FTP/SSH/Telnet/HTTP) |
camxploit |
Mass camera scanner with port + path + credential testing |
All 28 credential modules support mass scanning via the framework's multi-target dispatcher. The framework automatically handles:
- Random targets (
random,0.0.0.0/0) — generates random public IPs with exclusion-set enforcement - CIDR ranges (e.g.,
192.168.1.0/24) — expands and iterates all hosts - File-based targets — reads one target per line from a file path
- Comma-separated targets — splits and runs against each target
Mass scan is handled entirely at the framework level by scheduler::run. Modules
do not need is_mass_scan_target() checks or run_mass_scan() calls — the
scheduler fans out every target type and invokes modules with Target::Single.