12 Commits
Author SHA1 Message Date
s-b-repo e180afdcc4 creds/exploits: fix the protocol-level audit backlog (VNC, SSH, FortiOS CVE, m365, h3c, engine)
VNC (false positive): negotiate the RFB version instead of forcing 003.008. An
RFB 3.3 server desynced under the forced 3.8 handshake and a misaligned 4-byte
SecurityResult read of 0 was reported as a valid password. Now reply min(server,
3.8), and handle 3.3's single 4-byte security-type vs 3.7+'s count+list+select.

SSH (false negative): ssh_bruteforce + ssh_spray collapsed EVERY ssh2
userauth_password error to AuthFailed, so a transient/transport error mid-auth
became a definitive 'wrong password' (valid creds skipped, never retried). Now
only libssh2 -18/-19 (AUTHENTICATION_FAILED/PUBLICKEY_UNVERIFIED) is AuthFailed;
everything else propagates as a retryable error. ssh_spray also now resolves
hostnames (was SocketAddr::parse, IP-only → hostname targets never sprayed).

ssh_user_enum (noise): replaced the fixed 300ms absolute timing threshold with a
baseline mean+stddev one-sided cutoff (k sigma, median per user, MIN_ABS_DELTA
floor), time ONLY the auth exchange (not connect+KEX), and send a long password
so the server KDF dominates for valid users. threshold setg is now a sigma
multiplier.

FortiOS CVE-2018-13382 (false positive): the ordinary ret=1/redir= login response
was treated as a successful magic-token reset → FindingKind::Vulnerable on PATCHED
hosts. Now the Vulnerable finding is gated on a confirming login with the new
password (verify_login tightened to require the real ret=1 token); fingerprint no
longer calls any 200/401 host a FortiGate; reset POST also sends 'credential'.

Engine: run_bruteforce_streaming's stop-on-first-success early return leaked the
spawn_blocking wordlist reader, which kept scanning a multi-GB file into a dropped
channel. New utils::load_lines_batched_until lets the reader stop the instant the
receiver is gone. bruteforce_retries: unset→1, explicit 0→none, clamped to 10
(was: 0 coerced to 1, huge values unbounded).

m365_activesync_spray: classify valid-but-flagged accounts via X-MS-Diagnostics
ESTS codes (50055 expired / 50057 disabled / 50079/50076/50074/53004 MFA / 50158
CA) as credential hits instead of keying success only on HTTP 200; add 429/503
throttle backoff between rounds.

h3c_redfish_session_spray: require X-Auth-Token on 201 (was: stored a
'(header-missing)' placeholder credential); 400/404/422 → Denied not Error.
h3c_oem_kvm_bruteforce: require an actual token VALUE (header or parsed), not the
bare 'X-Auth-Token' field-name substring + '(present)' placeholder.

Build: 0 errors, 0 warnings. Remaining strict-audit hits in these files are
pre-existing (VNC DES, PG MD5 / MySQL SHA1 auth, #[cfg(test)] asserts, and
run-loop idioms) — none introduced here.
2026-06-13 20:57:27 +02:00
s-b-repo 52767ab45f creds: kill false-positive logins + RTSP-class lockout misclassification across 9 probes
Audit of every bruteforce module surfaced two recurring bug classes. Fixed the
verified, contained ones here.

FALSE POSITIVES (a wrong cred was being stored as loot):
- snmp: success was 'response contains byte 0xa2 anywhere' — matched request-id /
  length / payload bytes and accepted error/Report PDUs. Now structurally parses
  the datagram and requires a GetResponse PDU (0xa2) at its proper position.
- proxy: HTTP CONNECT/forward success was resp.contains("200") — matched
  'Content-Length: 200', Date, Via, etc. Now parses the status line. SOCKS5 now
  also checks the RFC1929 version byte before trusting the status.
- elasticsearch: 200 = success, but an unsecured ES node returns 200 to EVERY
  request → every password a false positive. Now confirms an unauthenticated GET
  is 401 before accepting the creds.
- couchdb: 200 = success without inspecting the body. Now requires {"ok":true}.
- memcached: trusted the status field without checking the response was a SASL-Auth
  reply. Now validates magic 0x81 + opcode 0x21 first.
- l2tp: any UDP reply with the top 2 bits set = success. Now also requires the
  L2TPv2 version nibble.
- http_basic: a redirect to a non-login page counted as success even when the
  endpoint didn't enforce auth. Now gated on the baseline-401 check.
- sample_cred_check: a registered template that stored admin:admin on any host
  whose /login returns 200. Now baselines the unauthenticated request first.

RTSP-CLASS LOCKOUT (a definitive negative from a RESPONDING server was returned as
a retryable Error, burning retries + tripping the consecutive-error give-up):
- ftp: post-handshake login rejections with unusual wording (430/532/localized)
  were Unknown->retryable; now treated as AuthFailed (connection drops stay
  retryable).
- mysql: ERR packets with codes other than 1044/1045 were retryable:true; the
  server clearly responded, so now never retryable, and the common auth-denial
  codes (1130/1226/1227/1698/1862) map to AuthFailed.
- http_basic: definitive 4xx (400/404/405/406/410/422) now AuthFailed instead of
  Unknown->retryable; 429/5xx stay retryable.

FALSE NEGATIVE:
- postgres: connected to database=<user>; a valid superuser whose own DB doesn't
  exist failed startup with 3D000 and was read as a bad login. Now uses the
  'postgres' maintenance DB.

Build: 0 errors, 0 warnings. (Pre-existing strict-audit hits in mysql/postgres are
the protocol-mandated SHA1/MD5 auth — not introduced here.)
2026-06-13 20:07:46 +02:00
s-b-repo 82f72079fb rtsp_bruteforce: classify any RTSP status reply as a result, not an error
A real RTSP camera commonly answers DESCRIBE with 404 Not Found when the stream
PATH is wrong (e.g. default '/'), not the credentials. The probe was treating
any non-200/401/403 status line as LoginResult::Error, which (a) misreported
clean negatives as 'Errors' and (b) — because the engine's lockout/give-up
heuristic counts CONSECUTIVE errors — made a live, responding host look dead and
trip a 30s pause + eventual give-up.

Now: parse the RTSP status code; 200 = Success, any other well-formed RTSP code
(401/403 reject, 404 wrong path, 3xx/5xx) = AuthFailed (a definitive negative for
that credential — no retry, resets the consecutive-error counter). Only a reply
that isn't an RTSP status line at all stays a (non-retryable) Error. 404s are
logged at debug ('setg rtsp_path' is the likely fix). Also decode the banner with
from_utf8_lossy + parse the first line instead of truncating to 64 bytes.
2026-06-13 19:28:07 +02:00
s-b-repo 3183e5d0cc bruteforce: expose connection retries (medusa -r) via setg bruteforce_retries
creds_helper previously hardcoded BruteforceConfig.max_retries = 1. 'setg
bruteforce_retries N' now sets the per-combo retry count for retryable
(transient/connection) errors; unset or 0 keeps the historical default of 1.
show options: + bruteforce_retries. Build: 0 errors, 0 warnings.
2026-06-13 17:06:10 +02:00
s-b-repo 40b2659092 bruteforce: hydra -x mask brute + resumable streamed wordlists
- bruteforce_mask (hydra -x): 'setg bruteforce_mask MIN:MAX:CHARSET' enumerates
  candidate passwords. CHARSET placeholders mirror hydra: a->a-z, A->A-Z, 1->0-9;
  any other char is literal. e.g. '1:4:a1' = 1-4 chars of [a-z0-9]. Generated via
  an odometer counter, hard-capped at MAX_COMBOS with a warning. Mask candidates
  run AFTER the wordlist (lowest yield); skipped in credential_file_only mode.
  New: utils::bruteforce::generate_mask_passwords + creds_helper wiring.

- bruteforce_resume (batch-level): 'setg bruteforce_resume y' makes a streamed
  large-wordlist run record the last fully-completed 500k batch, keyed by
  target+port+wordlist+size+mode, under ~/.rustsploit/checkpoints/<key>.bfr. An
  interrupted run skips already-tried batches on restart and clears the marker on
  clean completion / first success. New checkpoint markers: read/write/clear_
  bruteforce_marker, mirroring the existing seq-marker pattern.

show options: + bruteforce_mask / bruteforce_resume. Build: 0 errors, 0 warnings.
2026-06-13 16:54:57 +02:00
s-b-repo d0e07e8d97 bruteforce: cross-batch give-up, combo-file exclusive mode, stop-mode (host/user/all)
- Cross-batch give-up: run_bruteforce split into a wrapper + run_bruteforce_with_abort
  taking a shared abort flag; the streaming driver reuses one flag so a host the
  engine gives up on stops the WHOLE run instead of re-pausing every 500k batch.
- credential_file_only: 'setg credential_file_only y' makes the combo file the
  ONLY credential source (exact hydra -C; ignores user/pass wordlists + streaming).
- cred_stop_mode (host|user|all): host = stop whole host on first success
  (default); user = stop a username once its password is found, keep other users
  (medusa); all = find every valid credential (hydra default). Engine reads the
  setg (no BruteforceConfig field change -> no ripple across 14 callers).

show options: + credential_file_only / cred_stop_mode. Build: 0 errors, 0 warnings.
2026-06-13 16:41:58 +02:00
s-b-repo c3d0556f77 bruteforce: cap per-host concurrency in batch, wire combo-file (-C), expose delay/jitter, medusa host give-up
1. Batch concurrency cap: in a mass-scan fan-out, per-host bruteforce concurrency
   is capped (4) so it no longer multiplies with scheduler host-concurrency into
   an RLIMIT_NOFILE-exhausting socket count.
2. Combo-file (hydra -C): 'setg credential_file <user:pass file>' is loaded via
   the engine's load_credential_file and tried alongside defaults. Verified:
   2-pair file -> +2 attempts.
3. Delay/jitter (hydra -w): 'setg bruteforce_delay_ms' + 'setg bruteforce_jitter_ms'
   now feed BruteforceConfig.delay_ms/jitter_ms (were hardcoded 0).
4. Host give-up (medusa): run_bruteforce now aborts a host after MAX_LOCKOUT_PAUSES
   (3) consecutive-error lockout pauses with no success, instead of pausing-and-
   grinding forever. run_subnet_bruteforce already had give-up.

Also surfaced credential_file/bruteforce_delay_ms/bruteforce_jitter_ms in
'show options'. Build: 0 errors, 0 warnings.
Known limitation: streaming-path give-up is per-batch (not cross-batch).
2026-06-13 16:16:45 +02:00
s-b-repo 5968d25b58 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.
2026-06-13 15:55:28 +02:00
s-b-repo e6736530f0 creds bruteforce: run built-in defaults when no wordlist is set
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).
2026-06-13 14:47:50 +02:00
s-b-repo bc104901d9 shell: don't show a scary error when the history file doesn't exist yet
First run (or a clean ~/.rustsploit) has no history file; a NotFound on
rl.load_history is normal and was printing '[!] Failed to load history: No such
file or directory'. Now NotFound is logged at debug; only real failures
(permissions, corruption) are surfaced loudly.
2026-06-13 02:34:43 +02:00
s-b-repo 94899781d3 Framework hardening: retry-then-continue, panic fixes, WS/MCP bug fixes, no-swallow sweep + loud error surfacing; docs + release notes
Hardening (non-module framework files):
- Retry-then-continue: bounded per-host retry on transient failures across all 4
  mass-scan fan-outs; '10 errors -> abort sweep' softened to warn-and-continue
- Crash fixes: shell completer char-boundary guard; unreachable! -> bail!
- WS oversize-frame desync fixed (was bricking the PQ AEAD ratchet); MCP tenant
  job list/kill, out-of-range port, non-string option now correct/errored
- No silent error swallowing: swept framework files, every dropped error now
  bound + surfaced (warn for logged-only/data-loss, debug for already-propagated
  or aggregated per-host); removed _ => {} and Err(_)/|_| discards

Docs + release: README + docs/ updated for the release; RELEASE_NOTES.txt
section 6d added; new RELEASE_GITHUB.txt (GitHub release body).

Build: 0 errors, 0 warnings, 40/40 targeted tests green.
2026-06-13 02:29:12 +02:00
s-b-repo 05cfdc6d22 Ports (rmcp MCP SDK, Recog, JARM/JA3, SecLists) + mass-scan fixes + HTTP pooling + per-run output auto-save
Ports:
- MCP server migrated to official rmcp SDK (v1.7); all 29 tools/7 resources preserved
- Recog (Rapid7) fingerprint engine + DBs, wired into service_scanner
- JARM + JA3/JA3S TLS fingerprinting (tls_fingerprint) + jarm_scan module
- SecLists checksum-pinned wordlist catalog

Fixes / features:
- Full-internet sweep host-cap consistency
- Pre-config confirm-before-harvest + ModuleCtx.prompt_only mode; service_scanner
  batch-mode output gating
- HTTP client connection-pool reuse (cached) + bounded pool_idle_timeout
- show options: +scan_order/exclusions/target_rps/module_rps
- NEW: per-run output auto-save to ~/.rustsploit/loot/<module> <time> results.txt
  (append mode, all stdout+stderr; src/results_sink.rs)
- docs: drop stale check()/CheckResult + build.rs references
- assorted module additions + tommy interactive guide

Build: restore Cargo.lock (clean clone was unbuildable without it: transitive
cookie/time conflict). Working dir: clean build (0 warnings), 40/40 tests green.
2026-06-13 01:28:52 +02:00