Skip to content

fix: set conn deadline in smtp testAnonymousAccess/testOpenRelay to avoid hang on silent servers - #608

Merged
shadow1ng merged 1 commit into
shadow1ng:mainfrom
NOPTrace:fix/smtp-deadline
Sep 17, 2026
Merged

shadow1ng merged 1 commit into
shadow1ng:mainfrom
NOPTrace:fix/smtp-deadline

Conversation

@NOPTrace

Copy link
Copy Markdown
Contributor

现象

扫描包含 SMTP 服务(25/465/587/2525)的网段时,若目标服务器接受 TCP 连接但永不发送 220 banner
(蜜罐/tarpit/防反制设备常见行为),fscan 进程会永久假死:CPU 归零、日志停止输出、
连接停留在 ESTABLISHED,持续 1 小时以上不会自行退出,只能强杀。

根因

plugins/services/smtp.go 中,testAnonymousAccess 与 testOpenRelay 两个函数的
goroutine 在 DialTCP 之后未调用 conn.SetDeadline 就直接 smtp.NewClient:

  • smtp.NewClient 第一步是读取服务器的 220 greeting,该读没有超时 → goroutine 永久阻塞;
  • 外层 select { case <-resultChan; case <-ctx.Done() }:默认 -gt 0 时 ctx 无 deadline,
    永远不会触发 → Scan() 永不返回;
  • 最终 core.RunScan 的 wg.Wait()(core/scanner.go:129)永久等待,整个进程假死。

同文件其余函数(doSMTPAuth、testVRFYCommand、testEXPNCommand、getServerInfo)
均已正确设置 deadline,仅这两处遗漏。默认参数(-gt 0)下必现。

修复(共 6 行,两个函数各 3 行)

在两处 DialTCP 之后、smtp.NewClient 之前,补上与其他函数一致的 deadline:

​go _ = conn.SetDeadline(time.Now().Add(session.Config.ModuleTimeout())) ​

完整 diff:
​```diff
--- a/plugins/services/smtp.go
+++ b/plugins/services/smtp.go
@@ -243,6 +243,9 @@ func (p *SMTPPlugin) testAnonymousAccess(...) {
}
defer func() { _ = conn.Close() }()

  •   // 修复: 设置读写超时, 防止对只accept不发送banner的服务器(tarpit)永久阻塞
    
  •   _ = conn.SetDeadline(time.Now().Add(session.Config.ModuleTimeout()))
    
  •   client, err := smtp.NewClient(conn, info.Host)
    

@@ -295,6 +298,9 @@ func (p *SMTPPlugin) testOpenRelay(...) {
}
defer func() { _ = conn.Close() }()

  •   // 修复: 设置读写超时, 防止对只accept不发送banner的服务器(tarpit)永久阻塞
    
  •   _ = conn.SetDeadline(time.Now().Add(session.Config.ModuleTimeout()))
    
  •   client, err := smtp.NewClient(conn, info.Host)
    

​```

修复验证(同一 commit 源码树 A/B 构建,仅差这 6 行)

沉默服务器(accept 不发 banner) 正常应答的 SMTP 服务器
未修复版 75s+ 挂死,SIGQUIT dump 栈见下 3s 正常完成
修复版 92s 完整跑完 smtp 插件全链(匿名/中继/VRFY/EXPN/爆破),正常退出 3s 正常完成,检出能力一致

即:修复消除挂死,且对正常服务器的检测能力零回归。

复现步骤

# 1. 启动 tarpit(只 accept、只收、永不发送数据;2525 属于 smtp 插件注册端口)
cat > tarpit.py <<'EOF'
import socket, threading
s = socket.socket(); s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(("127.0.0.1", 2525)); s.listen(64)
def handle(c):
    try:
        while True: c.recv(4096)   # 只收不发,模拟蜜罐/tarpit
    except Exception: pass
while True:
    c,_ = s.accept(); threading.Thread(target=handle,args=(c,),daemon=True).start()
EOF
python3 tarpit.py &

# 2. 默认参数扫描(-nobr 可绕过,不加则必现)
./fscan -h 127.0.0.1 -p 2525 -np -nopoc -t 5
# 预期:进程永不退出(本机实测 75s+ 仍挂死)

# 3. 抓取 goroutine 栈定位挂点
kill -QUIT <fscan的pid>    # Go runtime 将 dump 全部 goroutine 到 stderr

# 4. 对照:加 -nobr 后 smtp 插件短路,可正常完成(旁证挂点在未授权检测)
./fscan -h 127.0.0.1 -p 2525 -np -nopoc -nobr -t 5   # 正常完成,输出服务识别结果

…void hang on silent servers

testAnonymousAccess and testOpenRelay dial the target and immediately
hand the connection to smtp.NewClient without setting any deadline.
smtp.NewClient reads the 220 greeting as its first operation, so a
server that accepts TCP but never sends data (honeypot/tarpit) blocks
the goroutine forever. The outer select waits on resultChan or
ctx.Done(); with the default -gt 0 the context has no deadline, so
Scan never returns and RunScan's wg.Wait() freezes the whole process.

Set the same ModuleTimeout deadline used by the other functions in
this file (doSMTPAuth, testVRFYCommand, testEXPNCommand, getServerInfo).

Verified against a live accept-but-silent SMTP endpoint:
- unpatched: process hangs (31 goroutines, stack at smtp.go:276)
- patched: full plugin chain completes in ~92s, no regression on
  normally-responding servers (detection output identical)
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.

2 participants