Skip to content

fix(core): 修复 stdout 爆破零输出卡死(53a80b5 回归)+ 单 host --concurrency 限流#66

Open
wuchulonly wants to merge 4 commits into
chainreactors:masterfrom
wuchulonly:fix/host-threads-rate-limit
Open

fix(core): 修复 stdout 爆破零输出卡死(53a80b5 回归)+ 单 host --concurrency 限流#66
wuchulonly wants to merge 4 commits into
chainreactors:masterfrom
wuchulonly:fix/host-threads-rate-limit

Conversation

@wuchulonly

@wuchulonly wuchulonly commented Jun 13, 2026

Copy link
Copy Markdown

概述

本分支两处相关的 core 改动:

  1. 修复:stdout 输出模式下爆破卡死、零输出(f84a081)—— 这是已在 master的回归。
  2. 特性:单 host 并发限流 --concurrency(f12fa385a642fa)—— 消除爆破"时成时不成"的非确定性漏报。

1. OutputHandler 卡死(影响当前 master)

53a80b5(已在 master)把 OutputHandler goroutine 的启动门控错绑在 OutFunc != nil 上,而 OutFunc 只在指定了输出文件(-f)时才赋值(options.go: if opt.OutputFile != "")。OutputHandler无缓冲 OutputCh 的唯一读者,同时也是唯一的 console 打印者——因此当结果输出到 stdout 且未给 -f 时(例如 -o jl,即 SDK/自动化常用路径),handler 不会启动:第一个 Output()OutputCh <- res 处永久阻塞,整个爆破卡死且零输出。

修复:用显式的 RunnerOption.ManualDrain 标志(默认 false)替代 OutFunc 门控。默认/CLI 调用都会启动内建 handler;自行消费 OutputCh 的 SDK 调用方可通过 ManualDrain = true opt-out。outlock 的 Add/Wait 计数同步绑定该标志以保持配对。

master 上复现:zombie -i <redis> -s redis(stdout)卡死/无输出;加上 -f out.txt 即正常。

2. 单 host 并发限流

默认 -t 100 下,爆破会朝同一目标同时发起多达 100 条 pre-auth 连接。sshd MaxStartups(默认 10:30:100)随即随机丢弃超过软上限的连接;zombie 把这类丢连当成"口令错误",于是承载正确口令的那次连接被随机丢弃时就被静默漏掉——典型的"同一条命令一次成、一次不成"。

--concurrency(别名 --host-threads,默认 8,0 = 不限)通过一个 ctx 感知的信号量,按 ip:port 把在飞连接数封顶(命中后队列排空时不再爆连)。8 落在 sshd 的"必然接受"区间内。

验证

  • go test ./... 全绿
  • 真实靶场(redis / ssh / mysql / postgres):stdout 模式爆破现在正常命中(修复前:卡死、success:0);-f 文件输出不变
  • ssh 并发 A/B(默认 MaxStartups,各 20 次):不限 → 18/20 命中(10% 漏报),--concurrency 8 → 20/20(0% 漏报)

🤖 Generated with Claude Code

wuchulonly and others added 4 commits May 29, 2026 05:20
Brute results were non-deterministic: the same command would find the
password on one run and miss it on another.

Root cause: there was no per-host concurrency cap, so with the default
-t 100 the brute fired up to 100 simultaneous pre-auth connections at a
single SSH target. sshd MaxStartups (default 10:30:100) then randomly
dropped connections above the soft limit. zombie treats such network
drops as "wrong password", so whenever the attempt carrying the correct
password happened to be dropped, it was silently missed — and which
attempt got dropped was random, hence the run-to-run flakiness.

Fix: add a per-host semaphore (hostLimiter) keyed by ip:port that bounds
in-flight connections per target.
- acquire is context-aware: if the target is cancelled while waiting for
  a slot (e.g. another worker already found the password), it returns
  without dialing, so the post-first-success queue drain does not burst
  connections.
- Skip already-cancelled tasks at the top of the worker.
- The gate sits in the outer worker before the timeout select, so time
  spent queuing for a slot is not counted against the per-task timeout;
  released via defer for panic safety.
- New flag --host-threads (default 8) controls the cap; 0 = unlimited.
  8 stays inside sshd's default always-accept zone (MaxStartups starts
  dropping at 10), while 10 sits exactly on the knee.

Verified on an SSH target with default MaxStartups, 40-password dict with
the correct password mid-list, repeated runs:
  unlimited: 46% miss | --host-threads 10: 13% | 8: 0% | 2: 0%
In-flight dials now strictly equal --host-threads.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
CLI 参数 --host-threads 改名为 --concurrency(纯长参,避开 -c/--cidr 短参冲突);
字段 HostThreads -> Concurrency,默认值与 hostLimiter 行为不变。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
master 已把 go.mod bump 到 `go 1.22.0` + `toolchain go1.24.3`,
但 ci.yml 仍写死 go-version 1.20.x,Go 1.20 无法解析 1.22.0 版本格式
与 toolchain 指令,导致 `go mod download` 解析 go.mod 失败、CI 全红。
改用 go-version-file: go.mod 让 CI 跟随 go.mod 单一事实源。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
53a80b5 gated the OutputHandler goroutine on `OutFunc != nil`, but
OutFunc is only set when an output file (-f) is given (options.go:
`if opt.OutputFile != ""`). OutputHandler is the sole reader of the
unbuffered OutputCh *and* the sole console printer, so whenever
results go to stdout with no -f (e.g. `-o jl`, the common case and how
SDK/automation callers invoke it) the handler never started: the first
Output() blocked forever on `OutputCh <- res` and the whole brute hung
with zero output. The host-threads/concurrency work on this branch was
silently unusable for the same reason.

Replace the OutFunc gate with an explicit ManualDrain flag (default
false): the built-in handler runs for every CLI/default invocation,
and SDK consumers that drain OutputCh themselves opt out via
ManualDrain=true. The outlock Add/Wait accounting is gated on the same
flag so it stays balanced with the handler.

Verified against live redis/ssh/mysql/postgres targets: stdout-mode
brute now cracks (was: hang, success:0); -f file output unchanged;
go test ./... green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@wuchulonly wuchulonly changed the title fix(core): unbreak stdout brute output (53a80b5 regression) + per-host --concurrency limit fix(core): 修复 stdout 爆破零输出卡死(53a80b5 回归)+ 单 host --concurrency 限流 Jun 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant