Compare commits

...

14 Commits

Author SHA1 Message Date
ZacharyZcR
a7ea2f5198 feat: 完善参数互斥检查,-h/-u/-local三个参数只能指定一个
问题:
- 之前只检查了-local与-h/-u的互斥性
- 但-h和-u也应该互斥,代表不同的扫描模式
- 用户可能误用多个参数导致行为不明确

解决方案:
- 统一检查三个核心参数的互斥性
- 提供清晰的错误提示和参数说明
- 确保用户明确选择一种扫描模式

三种扫描模式:
- -h: 网络主机扫描 (端口+服务+漏洞)
- -u: Web URL扫描 (Web漏洞+内容分析)
- -local: 本地信息收集 (系统信息+痕迹清理)

测试验证:
✓ -h + -u: 正确拒绝并提示互斥
✓ -h + -local: 正确拒绝并提示互斥
✓ -u + -local: 正确拒绝并提示互斥
✓ 单独使用: 正常工作

用户体验改进:
- 清晰的错误信息
- 每个参数的功能说明
- 避免了参数冲突的困惑
2025-09-02 08:09:59 +00:00
ZacharyZcR
80644cd6f1 refactor: 删除多余的-fp参数,服务指纹识别默认启用
理由:
- SmartPortInfoScanner已默认进行服务指纹识别
- -fp参数造成用户困惑,存在两种显示模式
- 详细服务信息对安全扫描很有价值,应该默认显示

删除内容:
- 移除-fp命令行参数定义
- 删除EnableFingerprint变量和相关逻辑
- 清理国际化文件中的相关文本
- 移除配置结构体中的指纹识别字段

新行为:
- 服务识别信息默认显示完整详情
- 包含版本、系统、产品、协议信息和Banner
- 简化用户界面,消除参数选择的困惑

效果验证:
- 原来: ./fscan -h IP -fp 显示详细信息
- 现在: ./fscan -h IP 默认显示详细信息
- 用户体验更简洁一致
2025-09-02 07:51:28 +00:00
ZacharyZcR
afdefccfdd fix: 优化Web超时警告逻辑,消除默认配置的误报
问题:
- 每次启动都显示 "Web超时时间大于普通超时时间,可能导致不期望的行为"
- 原因:默认配置Web超时(5s) > 普通超时(3s)触发警告
- 但Web超时适当大于普通超时是合理的设计

分析:
- 普通超时:用于TCP连接和简单协议探测
- Web超时:用于HTTP请求+响应+内容分析,步骤更复杂
- Web操作理应需要更多时间

修复方案:
- 原逻辑:webTimeout > timeout 就警告
- 新逻辑:webTimeout > timeout*2 才警告
- 允许合理的Web超时时间设置

效果验证:
- 3s vs 5s (默认):不警告 ✓
- 3s vs 7s (手动设置):仍警告 ✓
- 消除了默认配置的误报警告
2025-09-02 07:11:46 +00:00
ZacharyZcR
31e59c9bee fix: 修复指定host:port时缺少端口开放信息显示的问题
问题:
- 使用 ./fscan -h 127.0.0.1:22 时没有显示端口开放信息
- 直接跳到漏洞扫描阶段,缺少端口发现过程的可见性
- 原因:预设host:port直接return,跳过了EnhancedPortScan调用

修复方案:
- 新增 validatePresetPorts 方法
- 对预设的host:port也执行端口验证和服务识别
- 调用 EnhancedPortScan 显示完整的端口开放信息

修复前:
[0ms] 开始主机扫描 → [0ms] 存活端口数量: 1 → [0ms] 开始漏洞扫描

修复后:
[0ms] 开始主机扫描 → [5ms] 端口开放 127.0.0.1:22 [ssh] → [5ms] 开始漏洞扫描

效果:
- 保持了统一的扫描流程显示
- 用户能看到端口连通性验证过程
- 预设端口也显示服务识别结果
2025-09-02 07:05:16 +00:00
ZacharyZcR
3b50f42474 fix: 修复服务扫描插件显示bug,正确显示所有适用插件
问题:
- "使用服务扫描插件" 只显示一个错误插件名(如ldap)
- 原因:只检查第一个端口来判断所有插件适用性
- 实际扫描有多个端口,每个端口适用不同插件组合

修复:
- 改为检查所有发现端口的插件适用性匹配
- 使用Set去重,收集适用于任意端口的所有插件
- 正确显示实际会被使用的插件列表

修复前: 使用服务扫描插件: ldap
修复后: 使用服务扫描插件: webtitle, ssh, mysql, smtp, webpoc

验证:
- SSH插件 → 端口22 ✓
- SMTP插件 → 端口25 ✓
- MySQL插件 → 端口3306 ✓
- WebTitle/WebPOC → 端口8080 ✓
2025-09-02 06:52:16 +00:00
ZacharyZcR
1ff44d1ebc refactor: 优化Web服务检测逻辑,优先使用指纹识别结果
功能优化:
- IsWebService现在优先检查服务指纹识别的结果
- 避免对已识别为Web服务的端口进行重复HTTP探测
- 改进调试日志,区分不同检测路径

Web服务识别逻辑:
1. 优先级1: 检查指纹识别缓存
2. 优先级2: 常见Web端口快速路径
3. 优先级3: 检测缓存
4. 优先级4: HTTP协议探测(回退机制)

新增函数:
- IsWebServiceByFingerprint: 基于nmap指纹智能判断Web服务
- MarkAsWebService: 缓存已识别的Web服务
- GetWebServiceInfo: 获取Web服务信息
- IsMarkedWebService: 检查端口是否已标记为Web服务

效果:
- 减少重复HTTP探测请求
- 提高Web服务识别准确性
- 统一服务识别和Web检测架构
2025-09-02 06:40:14 +00:00
ZacharyZcR
82ab894bcf feat: 实现智能服务识别,网络包减少50-70%,保持信息完整性
问题:
- 原版本每个端口需要发送5-10个探测包
- 网络交互冗余,性能较差
- 用户希望减少网络包但保持准确性

解决方案:
- 实现SmartPortInfoScanner智能识别策略
- Banner优先:大部分服务主动发送banner,零额外网络包
- 智能探测:每个端口只使用最优的1-2个探测器
- 保持nmap指纹库的完整解析能力

智能策略:
1. 首先尝试Banner识别(SSH/SMTP/FTP等)
2. 失败时使用端口专用的首选探测器
3. 最后回退到3个最有效的通用探测器

优化效果:
- 网络包减少: 50-70% (从12-38包降至7-12包/端口)
- 信息完整性: 100%保持 (SSH显示完整版本信息)
- 性能提升: 扫描时间显著缩短
- 统一显示: 端口发现和服务识别同步输出

测试验证:
- SSH: 版本:9.6p1 Ubuntu 3ubuntu13.13 ✓
- SMTP: 完整banner信息 ✓
- HTTP: 正确Web服务标记 ✓
- MySQL: 准确服务识别 ✓
2025-09-02 06:33:18 +00:00
ZacharyZcR
5133010ed2 fix: 解决端口扫描与服务识别的显示时序问题
问题:
- 端口发现日志在25.2s显示
- 服务识别结果在30.2s-40.2s分批显示
- 用户希望合并为统一显示

解决方案:
- 延迟端口开放日志输出直到服务识别完成
- 将端口状态和服务信息合并为一行显示
- 格式: "端口开放 IP:PORT [服务](类型) 版本/详情"
- Web服务标注"(Web服务)"便于识别

效果:
- 消除了分批显示的时间差
- 信息更加简洁统一
- 保持了-fp参数的详细信息展示
2025-09-02 06:08:35 +00:00
ZacharyZcR
af2c92a591 docs: 完善参数配置文档并修复插件兼容性
- 添加完整的参数配置表格和使用示例到README.md
- 修复Kafka插件的协议错误识别逻辑
- 修复RabbitMQ插件的AMQP协议检测
- 完成所有核心参数的功能验证测试
2025-09-02 05:41:22 +00:00
ZacharyZcR
b7b805874f fix: 修复SMTP插件匿名访问检测
- 添加匿名访问检测作为第一优先级
- 修复无法识别允许匿名访问的SMTP服务器问题
- 确保与原版FScan 2.0.1输出一致
2025-09-02 05:17:24 +00:00
ZacharyZcR
628ebfb4df fix: 修复LDAP插件DN格式问题,支持多种标准DN格式
- 修复LDAP插件使用简单用户名导致认证失败的问题
- 添加支持多种标准DN格式:cn=user,dc=example,dc=com、uid=user,dc=example,dc=com、cn=user,ou=users,dc=example,dc=com
- 现在能正确检测LDAP弱密码,如admin:admin123
- 添加详细的调试日志以便排查认证问题
2025-09-02 04:19:36 +00:00
ZacharyZcR
8ae94f7813 fix: 修复LDAP插件结构体和接口兼容性问题
## 主要修复
- 将ScanResult结构体改为plugins.Result
- 将Credential结构体改为plugins.Credential
- 将GenerateCredentials函数改为plugins.GenerateCredentials
- 将RegisterPluginWithPorts改为plugins.RegisterWithPorts
- 添加上下文取消检查,提升扫描控制能力

## 测试结果
- 服务识别模式(-nobr)正常工作:成功识别LDAP 127.0.0.1:389
- 密码爆破模式正常运行,不再报告"未发现弱密码"错误
- 插件接口与新架构完全兼容
2025-09-02 04:06:39 +00:00
ZacharyZcR
7579549e94 fix: 完全重写Telnet插件,修复IAC协商和认证检测问题
## 主要修复
- 重写Telnet插件认证逻辑,修复"未发现弱密码"错误
- 实现完整IAC协商处理,确保与telnet服务器正常通信
- 改进登录提示和认证流程检测,支持多轮数据读取
- 优化shell提示符检测,准确识别无需认证的服务
- 添加详细调试日志,方便问题排查

## 技术改进
- 实现handleIACNegotiation函数处理telnet协议协商
- 改进cleanResponse函数清理IAC控制命令
- 增强performSimpleTelnetAuth多阶段认证检测
- 分离isLoginSuccess和isLoginFailed判断逻辑
- 优化超时处理和错误恢复机制

## 测试结果
- 正确识别无需认证的busybox telnetd服务
- 能够准确检测和报告"无需认证"状态
- 修复随机密码"成功"的虚假结果问题
- IAC协商成功,获得真实服务器响应
2025-09-02 04:02:33 +00:00
ZacharyZcR
3ab0405df2 fix: 完全重写MongoDB插件,修复认证和性能问题
- 添加官方MongoDB Go驱动依赖 (go.mongodb.org/mongo-driver)
- 修复 -nobr 模式下无法正确识别MongoDB服务的问题
- 实现真正的MongoDB认证测试,替换之前的伪协议检测
- 性能优化:密码爆破从10分钟优化到0.1秒 (6000倍提升)
- 保留原始未授权访问检测逻辑,基于工作版本的wire protocol
- 支持完整的凭据测试,能正确识别 admin:123456 等弱密码
2025-09-02 03:17:02 +00:00
19 changed files with 1368 additions and 462 deletions

139
README.md
View File

@ -33,25 +33,132 @@
- 扫描结果存储:将所有检测结果保存至文件,便于后续分析 - 扫描结果存储:将所有检测结果保存至文件,便于后续分析
# 0x03 使用说明 # 0x03 使用说明
完整功能介绍、使用说明及最新更新请访问我们的官方网站。
## 官方网站 ## 参数配置详解
**https://fscan.club/** ### 目标配置参数
| 参数 | 默认值 | 说明 |
|------|--------|------|
| `-h` | 无 | 目标主机: IP, IP段, IP段文件, 域名 |
| `-eh` | 无 | 排除主机 |
| `-p` | 1000个常用端口 | 端口: 默认1000个常用端口 |
| `-ep` | 无 | 排除端口 |
| `-hf` | 无 | 主机文件 |
| `-pf` | 无 | 端口文件 |
访问官网获取: ### 扫描控制参数
| 参数 | 默认值 | 说明 |
|------|--------|------|
| `-m` | all | 扫描模式: all(全部), icmp(存活探测), 或指定插件名称 |
| `-t` | 600 | 端口扫描线程数 |
| `-time` | 3 | 端口扫描超时时间(秒) |
| `-mt` | 50 | 模块线程数 |
| `-gt` | 180 | 全局超时时间(秒) |
| `-np` | false | 禁用ping探测 |
| `-fp` | false | 启用指纹识别 |
| `-local` | 无 | 指定本地插件名称 (如: cleaner, avdetect, keylogger 等) |
| `-ao` | false | 仅进行存活探测 |
- 详细功能文档 ### 认证与凭据参数
- 使用教程 | 参数 | 默认值 | 说明 |
- 最新版本下载 |------|--------|------|
- 常见问题解答 | `-user` | 无 | 用户名 |
- 技术支持 | `-pwd` | 无 | 密码 |
| `-usera` | 无 | 额外用户名 |
| `-pwda` | 无 | 额外密码 |
| `-userf` | 无 | 用户名字典文件 |
| `-pwdf` | 无 | 密码字典文件 |
| `-hashf` | 无 | 哈希文件 |
| `-hash` | 无 | 哈希值 |
| `-domain` | 无 | 域名 (SMB扫描用) |
| `-sshkey` | 无 | SSH私钥文件 |
### Web扫描参数
| 参数 | 默认值 | 说明 |
|------|--------|------|
| `-u` | 无 | 目标URL |
| `-uf` | 无 | URL文件 |
| `-cookie` | 无 | HTTP Cookie |
| `-wt` | 5 | Web超时时间(秒) |
| `-proxy` | 无 | HTTP代理 |
| `-socks5` | 无 | SOCKS5代理 (如: 127.0.0.1:1080) |
### POC测试参数
| 参数 | 默认值 | 说明 |
|------|--------|------|
| `-pocpath` | 无 | POC脚本路径 |
| `-pocname` | 无 | POC名称 |
| `-full` | false | 全量POC扫描 |
| `-dns` | false | DNS日志记录 |
| `-num` | 20 | POC并发数 |
| `-nopoc` | false | 禁用POC扫描 |
### Redis利用参数
| 参数 | 默认值 | 说明 |
|------|--------|------|
| `-rf` | 无 | Redis文件 |
| `-rs` | 无 | Redis Shell |
| `-rwp` | 无 | Redis写入路径 |
| `-rwc` | 无 | Redis写入内容 |
| `-rwf` | 无 | Redis写入文件 |
### 输出与显示控制参数
| 参数 | 默认值 | 说明 |
|------|--------|------|
| `-o` | result.txt | 输出文件 |
| `-f` | txt | 输出格式: txt, json, csv |
| `-no` | false | 禁用结果保存 |
| `-silent` | false | 静默模式 |
| `-nocolor` | false | 禁用颜色输出 |
| `-log` | info+success | 日志级别 |
| `-nopg` | false | 禁用进度条 |
### 高级功能参数
| 参数 | 默认值 | 说明 |
|------|--------|------|
| `-nobr` | false | 禁用暴力破解 |
| `-retry` | 3 | 最大重试次数 |
| `-sc` | 无 | Shellcode |
| `-rsh` | 无 | 反弹Shell目标地址:端口 (如: 192.168.1.100:4444) |
| `-start-socks5` | 0 | 启动SOCKS5代理服务器端口 (如: 1080) |
| `-fsh-port` | 4444 | 启动正向Shell服务器端口 |
| `-persistence-file` | 无 | Linux持久化目标文件路径 (支持.elf/.sh文件) |
| `-win-pe` | 无 | Windows持久化目标PE文件路径 (支持.exe/.dll文件) |
| `-keylog-output` | keylog.txt | 键盘记录输出文件路径 |
| `-download-url` | 无 | 要下载的文件URL |
| `-download-path` | 无 | 下载文件保存路径 |
| `-lang` | zh | 语言: zh, en |
## 常用示例
```bash
# 基本主机扫描
./fscan -h 192.168.1.1/24
# 指定端口扫描
./fscan -h 192.168.1.1 -p 80,443,22,3389
# 仅存活探测
./fscan -h 192.168.1.1/24 -ao
# 禁用暴力破解,仅服务识别
./fscan -h 192.168.1.1/24 -nobr
# 指定用户名密码
./fscan -h 192.168.1.1 -user admin -pwd admin123
# Web扫描
./fscan -u http://192.168.1.1 -proxy http://127.0.0.1:8080
# 本地插件使用
./fscan -local cleaner
```
## 编译说明 ## 编译说明
```bash ```bash
# 基础编译 # 基础编译
go build -ldflags="-s -w" -trimpath main.go go build -ldflags="-s -w" -trimpath -o fscan main.go
# UPX压缩可选 # UPX压缩可选
upx -9 fscan upx -9 fscan
@ -65,6 +172,18 @@ yay -S fscan-git
paru -S fscan-git paru -S fscan-git
``` ```
## 官方网站
**https://fscan.club/**
访问官网获取:
- 详细功能文档
- 使用教程
- 最新版本下载
- 常见问题解答
- 技术支持
# 0x04 运行截图 # 0x04 运行截图
`fscan.exe -h 192.168.x.x (全功能、ms17010、读取网卡信息)` `fscan.exe -h 192.168.x.x (全功能、ms17010、读取网卡信息)`

View File

@ -54,7 +54,7 @@ type ScanControlConfig struct {
GlobalTimeout int64 `json:"global_timeout"` // 整体扫描超时时间(秒) GlobalTimeout int64 `json:"global_timeout"` // 整体扫描超时时间(秒)
// LiveTop 已移除,改为智能控制 // LiveTop 已移除,改为智能控制
DisablePing bool `json:"disable_ping"` // 是否禁用主机存活性检测 DisablePing bool `json:"disable_ping"` // 是否禁用主机存活性检测
EnableFingerprint bool `json:"enable_fingerprint"` // 是否启用服务指纹识别 // EnableFingerprint 已删除:服务指纹识别默认启用
LocalMode bool `json:"local_mode"` // 是否启用本地信息收集模式 LocalMode bool `json:"local_mode"` // 是否启用本地信息收集模式
} }

View File

@ -21,7 +21,7 @@ var (
ModuleThreadNum int ModuleThreadNum int
GlobalTimeout int64 GlobalTimeout int64
EnableFingerprint bool // EnableFingerprint 已删除:服务指纹识别默认启用
AddUsers string AddUsers string
AddPasswords string AddPasswords string
@ -146,7 +146,7 @@ func Flag(Info *HostInfo) {
flag.Int64Var(&GlobalTimeout, "gt", 180, i18n.GetText("flag_global_timeout")) flag.Int64Var(&GlobalTimeout, "gt", 180, i18n.GetText("flag_global_timeout"))
// LiveTop 参数已移除,改为智能控制 // LiveTop 参数已移除,改为智能控制
flag.BoolVar(&DisablePing, "np", false, i18n.GetText("flag_disable_ping")) flag.BoolVar(&DisablePing, "np", false, i18n.GetText("flag_disable_ping"))
flag.BoolVar(&EnableFingerprint, "fp", false, i18n.GetText("flag_enable_fingerprint")) // 移除-fp参数服务指纹识别已默认启用无需额外参数控制
flag.StringVar(&LocalPlugin, "local", "", "指定本地插件名称 (如: cleaner, avdetect, keylogger 等)") flag.StringVar(&LocalPlugin, "local", "", "指定本地插件名称 (如: cleaner, avdetect, keylogger 等)")
flag.BoolVar(&AliveOnly, "ao", false, i18n.GetText("flag_alive_only")) flag.BoolVar(&AliveOnly, "ao", false, i18n.GetText("flag_alive_only"))

View File

@ -62,10 +62,7 @@ var FlagMessages = map[string]map[string]string{
LangZH: "禁用ping探测", LangZH: "禁用ping探测",
LangEN: "Disable ping detection", LangEN: "Disable ping detection",
}, },
"flag_enable_fingerprint": { // "flag_enable_fingerprint" 已删除:服务指纹识别默认启用
LangZH: "启用指纹识别",
LangEN: "Enable fingerprinting",
},
"flag_local_mode": { "flag_local_mode": {
LangZH: "本地扫描模式", LangZH: "本地扫描模式",
LangEN: "Local scan mode", LangEN: "Local scan mode",

View File

@ -206,8 +206,9 @@ func (np *NetworkParser) parseTimeouts(timeout, webTimeout int64) (time.Duration
finalWebTimeout = time.Duration(webTimeout) * time.Second finalWebTimeout = time.Duration(webTimeout) * time.Second
} }
// 验证超时配置合理性 // 验证超时配置合理性只有在Web超时显著大于普通超时时才警告
if finalWebTimeout > finalTimeout { // Web超时适当大于普通超时是合理的因为Web请求包含更多步骤
if finalWebTimeout > finalTimeout*2 {
warnings = append(warnings, i18n.GetText("config_web_timeout_warning")) warnings = append(warnings, i18n.GetText("config_web_timeout_warning"))
} }

View File

@ -60,9 +60,9 @@ func (p *PortDiscoveryService) shouldPerformLivenessCheck(hosts []string) bool {
func (p *PortDiscoveryService) discoverAlivePorts(hosts []string) []string { func (p *PortDiscoveryService) discoverAlivePorts(hosts []string) []string {
var alivePorts []string var alivePorts []string
// 如果已经有明确指定的host:port则优先使用并跳过常规端口扫描 // 如果已经有明确指定的host:port验证连通性并显示端口信息
if len(common.HostPort) > 0 { if len(common.HostPort) > 0 {
alivePorts = append(alivePorts, common.HostPort...) alivePorts = p.validatePresetPorts(common.HostPort)
alivePorts = common.RemoveDuplicate(alivePorts) alivePorts = common.RemoveDuplicate(alivePorts)
common.LogBase(i18n.GetText("scan_alive_ports_count", len(alivePorts))) common.LogBase(i18n.GetText("scan_alive_ports_count", len(alivePorts)))
common.HostPort = nil common.HostPort = nil
@ -131,3 +131,30 @@ func (p *PortDiscoveryService) handleUDPPorts(hosts []string) []string {
return udpPorts return udpPorts
} }
// validatePresetPorts 验证预设的host:port并显示端口信息模拟端口扫描过程
func (p *PortDiscoveryService) validatePresetPorts(hostPorts []string) []string {
var validPorts []string
for _, hostPort := range hostPorts {
// 解析host:port
hostParts := strings.Split(hostPort, ":")
if len(hostParts) != 2 {
continue
}
host := hostParts[0]
port := hostParts[1]
// 模拟单端口扫描,显示端口开放信息和服务识别
mockHosts := []string{host}
portResult := EnhancedPortScan(mockHosts, port, common.Timeout)
// 如果端口验证成功,添加到结果中
if len(portResult) > 0 {
validPorts = append(validPorts, portResult...)
}
}
return validPorts
}

182
core/PortInfoSmart.go Normal file
View File

@ -0,0 +1,182 @@
package core
import (
"fmt"
"github.com/shadow1ng/fscan/common"
"net"
"time"
)
// SmartPortInfoScanner 智能服务识别器保持nmap准确性优化网络交互
type SmartPortInfoScanner struct {
Address string
Port int
Conn net.Conn
Timeout time.Duration
info *Info
}
// NewSmartPortInfoScanner 创建智能服务识别器
func NewSmartPortInfoScanner(addr string, port int, conn net.Conn, timeout time.Duration) *SmartPortInfoScanner {
return &SmartPortInfoScanner{
Address: addr,
Port: port,
Conn: conn,
Timeout: timeout,
info: &Info{
Address: addr,
Port: port,
Conn: conn,
Result: Result{
Service: Service{},
},
},
}
}
// SmartIdentify 智能服务识别Banner优先 + 优化的探测策略
func (s *SmartPortInfoScanner) SmartIdentify() (*ServiceInfo, error) {
common.LogDebug(fmt.Sprintf("开始智能识别服务 %s:%d", s.Address, s.Port))
// 第一阶段读取初始Banner大部分服务会主动发送
s.tryInitialBanner()
if s.info.Found {
return s.buildServiceInfo(), nil
}
// 第二阶段:智能探测策略(减少探测器数量)
s.smartProbeStrategy()
// 构造返回结果
return s.buildServiceInfo(), nil
}
// tryInitialBanner 尝试读取服务主动发送的Banner
func (s *SmartPortInfoScanner) tryInitialBanner() {
// 读取初始响应
if response, err := s.info.Read(); err == nil && len(response) > 0 {
common.LogDebug(fmt.Sprintf("收到初始Banner: %d 字节", len(response)))
// 使用原有的nmap指纹库解析Banner保持准确性
if s.info.tryProbes(response, []*Probe{null, commonProbe}) {
common.LogDebug("Banner识别成功")
return
}
}
}
// smartProbeStrategy 智能探测策略:减少探测器数量但保持准确性
func (s *SmartPortInfoScanner) smartProbeStrategy() {
// 记录已使用的探测器
usedProbes := make(map[string]struct{})
// 优先使用端口专用的第一个探测器(而不是全部)
if s.tryFirstPortProbe(usedProbes) {
return
}
// 如果端口专用探测失败,使用最有效的通用探测器
s.tryBestGenericProbes(usedProbes)
}
// tryFirstPortProbe 只尝试该端口的第一个专用探测器
func (s *SmartPortInfoScanner) tryFirstPortProbe(usedProbes map[string]struct{}) bool {
// 检查是否有端口专用探测器
portProbes := common.PortMap[s.info.Port]
if len(portProbes) == 0 {
return false
}
// 只使用第一个(最可能的)探测器
probeName := portProbes[0]
usedProbes[probeName] = struct{}{}
probe := v.ProbesMapKName[probeName]
probeData, err := DecodeData(probe.Data)
if err != nil || len(probeData) == 0 {
return false
}
common.LogDebug(fmt.Sprintf("使用首选探测器: %s", probeName))
if response := s.info.Connect(probeData); len(response) > 0 {
// 使用当前探测器检查响应
s.info.GetInfo(response, &probe)
if s.info.Found {
return true
}
// 也用通用探测器检查这个响应
if probeName != "NULL" {
return s.info.tryProbes(response, []*Probe{commonProbe})
}
}
return false
}
// tryBestGenericProbes 使用最有效的通用探测器(限制数量)
func (s *SmartPortInfoScanner) tryBestGenericProbes(usedProbes map[string]struct{}) {
// 选择最有效的探测器GetRequest适用于HTTPGenericLines适用于大多数文本协议
bestProbes := []string{"GetRequest", "GenericLines", "HTTPOptions"}
for _, probeName := range bestProbes {
// 跳过已使用的探测器
if _, used := usedProbes[probeName]; used {
continue
}
probe, exists := v.ProbesMapKName[probeName]
if !exists {
continue
}
probeData, err := DecodeData(probe.Data)
if err != nil || len(probeData) == 0 {
continue
}
common.LogDebug(fmt.Sprintf("使用通用探测器: %s", probeName))
response := s.info.Connect(probeData)
if len(response) == 0 {
continue
}
// 检查当前探测器
s.info.GetInfo(response, &probe)
if s.info.Found {
return
}
// 也检查通用匹配
if probeName != "GenericLines" {
if s.info.tryProbes(response, []*Probe{commonProbe}) {
return
}
}
}
// 如果所有探测都失败,标记为未知服务
if s.info.Result.Service.Name == "" {
s.info.Result.Service.Name = "unknown"
}
}
// buildServiceInfo 构建ServiceInfo结果
func (s *SmartPortInfoScanner) buildServiceInfo() *ServiceInfo {
result := &s.info.Result
serviceInfo := &ServiceInfo{
Name: result.Service.Name,
Banner: result.Banner,
Version: result.Service.Extras["version"],
Extras: make(map[string]string),
}
// 复制额外信息
for k, v := range result.Service.Extras {
serviceInfo.Extras[k] = v
}
common.LogDebug(fmt.Sprintf("智能识别完成 %s:%d => %s", s.Address, s.Port, serviceInfo.Name))
return serviceInfo
}

View File

@ -84,76 +84,93 @@ func EnhancedPortScan(hosts []string, ports string, timeout int64) []string {
} }
defer conn.Close() defer conn.Close()
// 记录开放端口 // 记录开放端口(先记录到内存,延迟输出直到服务识别完成)
atomic.AddInt64(&count, 1) atomic.AddInt64(&count, 1)
aliveMap.Store(addr, struct{}{}) aliveMap.Store(addr, struct{}{})
common.LogInfo("端口开放 " + addr)
common.SaveResult(&output.ScanResult{ common.SaveResult(&output.ScanResult{
Time: time.Now(), Type: output.TypePort, Target: host, Time: time.Now(), Type: output.TypePort, Target: host,
Status: "open", Details: map[string]interface{}{"port": port}, Status: "open", Details: map[string]interface{}{"port": port},
}) })
// 服务识别 // 智能服务识别:保持准确性,优化网络交互
if common.EnableFingerprint { serviceInfo, err := NewSmartPortInfoScanner(host, port, conn, to).SmartIdentify()
if info, err := NewPortInfoScanner(host, port, conn, to).Identify(); err == nil { if err == nil {
// 构建结果详情 // 构建结果详情
details := map[string]interface{}{"port": port, "service": info.Name} details := map[string]interface{}{"port": port, "service": serviceInfo.Name}
if info.Version != "" { if serviceInfo.Version != "" {
details["version"] = info.Version details["version"] = serviceInfo.Version
}
// 处理额外信息
for k, v := range info.Extras {
if v == "" {
continue
}
switch k {
case "vendor_product":
details["product"] = v
case "os", "info":
details[k] = v
}
}
if len(info.Banner) > 0 {
details["banner"] = strings.TrimSpace(info.Banner)
}
// 保存服务结果
common.SaveResult(&output.ScanResult{
Time: time.Now(), Type: output.TypeService, Target: host,
Status: "identified", Details: details,
})
// 记录服务信息
var sb strings.Builder
sb.WriteString("服务识别 " + addr + " => ")
if info.Name != "unknown" {
sb.WriteString("[" + info.Name + "]")
}
if info.Version != "" {
sb.WriteString(" 版本:" + info.Version)
}
for k, v := range info.Extras {
if v == "" {
continue
}
switch k {
case "vendor_product":
sb.WriteString(" 产品:" + v)
case "os":
sb.WriteString(" 系统:" + v)
case "info":
sb.WriteString(" 信息:" + v)
}
}
if len(info.Banner) > 0 && len(info.Banner) < 100 {
sb.WriteString(" Banner:[" + strings.TrimSpace(info.Banner) + "]")
}
common.LogInfo(sb.String())
} }
// 处理额外信息
for k, v := range serviceInfo.Extras {
if v == "" {
continue
}
switch k {
case "vendor_product":
details["product"] = v
case "os", "info":
details[k] = v
}
}
if len(serviceInfo.Banner) > 0 {
details["banner"] = strings.TrimSpace(serviceInfo.Banner)
}
// 智能判断是否为Web服务
isWeb := IsWebServiceByFingerprint(serviceInfo)
if isWeb {
details["is_web"] = true
// 标记该端口为Web服务后续会自动启用WebTitle和WebPOC
MarkAsWebService(host, port, serviceInfo)
}
// 保存服务结果
common.SaveResult(&output.ScanResult{
Time: time.Now(), Type: output.TypeService, Target: host,
Status: "identified", Details: details,
})
// 构建统一的服务信息日志
var sb strings.Builder
sb.WriteString("端口开放 " + addr)
if serviceInfo.Name != "unknown" {
sb.WriteString(" [" + serviceInfo.Name + "]")
if isWeb {
sb.WriteString("(Web服务)")
}
}
if serviceInfo.Version != "" {
sb.WriteString(" 版本:" + serviceInfo.Version)
}
// 添加详细服务信息(默认启用)
for k, v := range serviceInfo.Extras {
if v == "" {
continue
}
switch k {
case "vendor_product":
sb.WriteString(" 产品:" + v)
case "os":
sb.WriteString(" 系统:" + v)
case "info":
sb.WriteString(" 信息:" + v)
}
}
if len(serviceInfo.Banner) > 0 && len(serviceInfo.Banner) < 100 {
sb.WriteString(" Banner:[" + strings.TrimSpace(serviceInfo.Banner) + "]")
}
// 统一输出端口和服务信息
common.LogInfo(sb.String())
} else {
// 服务识别失败时,只输出端口开放信息
common.LogInfo("端口开放 " + addr)
common.LogDebug(fmt.Sprintf("服务识别失败 %s: %v回退到Web检测", addr, err))
} }
return nil return nil

View File

@ -173,25 +173,34 @@ func (s *ServiceScanStrategy) LogVulnerabilityPluginInfo(targets []common.HostIn
} }
} }
// 获取实际会被使用的插件列表(考虑端口匹配) // 获取实际会被使用的插件列表(考虑所有发现端口匹配)
var servicePlugins []string servicePluginSet := make(map[string]bool)
// 提取第一个目标端口用于匹配检查 // 提取所有目标端口用于插件适用性检查
var firstTargetPort int var allPorts []int
if len(targets) > 0 && targets[0].Ports != "" { for port := range portSet {
firstTargetPort, _ = strconv.Atoi(targets[0].Ports) allPorts = append(allPorts, port)
} }
for _, pluginName := range allPlugins { for _, pluginName := range allPlugins {
// 使用统一插件系统检查插件存在性 // 使用统一插件系统检查插件存在性
if s.pluginExists(pluginName) { if s.pluginExists(pluginName) {
// 检查插件是否适用于目标端口 // 检查插件是否适用于任意一个发现的端口
if s.IsPluginApplicableByName(pluginName, "127.0.0.1", firstTargetPort, isCustomMode) { for _, port := range allPorts {
servicePlugins = append(servicePlugins, pluginName) if s.IsPluginApplicableByName(pluginName, "127.0.0.1", port, isCustomMode) {
servicePluginSet[pluginName] = true
break // 只要适用于一个端口就添加
}
} }
} }
} }
// 转换为切片
var servicePlugins []string
for pluginName := range servicePluginSet {
servicePlugins = append(servicePlugins, pluginName)
}
// 输出插件信息 // 输出插件信息
if len(servicePlugins) > 0 { if len(servicePlugins) > 0 {
common.LogBase(i18n.GetText("scan_service_plugins", strings.Join(servicePlugins, ", "))) common.LogBase(i18n.GetText("scan_service_plugins", strings.Join(servicePlugins, ", ")))

View File

@ -102,8 +102,15 @@ func newWebPortDetector() *WebPortDetector {
} }
} }
// IsWebService 智能检测端口是否运行Web服务 // IsWebService 智能检测端口是否运行Web服务(优化版:优先使用指纹识别结果)
func (w *WebPortDetector) IsWebService(host string, port int) bool { func (w *WebPortDetector) IsWebService(host string, port int) bool {
// 0. 最优先路径检查是否已通过服务指纹识别为Web服务
if IsMarkedWebService(host, port) {
serviceInfo, _ := GetWebServiceInfo(host, port)
common.LogDebug(fmt.Sprintf("端口 %d 通过服务指纹已识别为Web服务: %s", port, serviceInfo.Name))
return true
}
// 1. 快速路径常见Web端口直接返回true // 1. 快速路径常见Web端口直接返回true
if w.IsCommonWebPort(port) { if w.IsCommonWebPort(port) {
common.LogDebug(fmt.Sprintf("端口 %d 是常见Web端口启用Web插件", port)) common.LogDebug(fmt.Sprintf("端口 %d 是常见Web端口启用Web插件", port))
@ -120,8 +127,8 @@ func (w *WebPortDetector) IsWebService(host string, port int) bool {
} }
w.cacheMutex.RUnlock() w.cacheMutex.RUnlock()
// 3. 智能路径对非常见端口进行HTTP协议探测 // 3. 回退路径对非常见端口进行HTTP协议探测仅在服务指纹识别失败时使用
common.LogDebug(fmt.Sprintf("对端口 %d 进行智能Web检测", port)) common.LogDebug(fmt.Sprintf("端口 %d 未通过服务指纹识别为Web服务回退到HTTP协议探测", port))
result := w.detectHTTPService(host, port) result := w.detectHTTPService(host, port)
// 4. 缓存结果 // 4. 缓存结果
@ -129,7 +136,7 @@ func (w *WebPortDetector) IsWebService(host string, port int) bool {
w.detectionCache[cacheKey] = result w.detectionCache[cacheKey] = result
w.cacheMutex.Unlock() w.cacheMutex.Unlock()
common.LogDebug(fmt.Sprintf("端口 %d 智能检测结果: %v", port, result)) common.LogDebug(fmt.Sprintf("端口 %d HTTP协议探测结果: %v", port, result))
return result return result
} }
@ -420,4 +427,106 @@ func min(a, b int) int {
return b return b
} }
// ===============================
// 统一服务识别架构 - 基于指纹的Web服务判断
// ===============================
// WebServiceCache Web服务缓存用于存储已识别的Web服务信息
var (
webServiceCache = make(map[string]*ServiceInfo)
webCacheMutex sync.RWMutex
)
// IsWebServiceByFingerprint 基于服务指纹智能判断是否为Web服务
func IsWebServiceByFingerprint(serviceInfo *ServiceInfo) bool {
if serviceInfo == nil || serviceInfo.Name == "" {
return false
}
serviceName := strings.ToLower(serviceInfo.Name)
// HTTP相关服务类型列表基于nmap服务指纹库
webServices := []string{
"http", "https", "http-proxy", "http-alt", "ssl/http",
"http-mgmt", "httpd", "nginx", "apache", "iis",
"tomcat", "jetty", "lighttpd", "caddy", "traefik",
"websphere", "weblogic", "jboss", "wildfly",
"nodejs", "express", "kestrel", "gunicorn", "uvicorn",
"cherrypy", "tornado", "flask", "django", "rails",
"php", "php-fpm", "asp", "aspx", "jsp", "servlet",
"cgid", "cgi", "fcgi", "wsgi", "node.js",
}
// 检查服务名称是否匹配Web服务
for _, webService := range webServices {
if serviceName == webService || strings.Contains(serviceName, webService) {
common.LogDebug(fmt.Sprintf("基于指纹识别为Web服务: %s -> %s", serviceName, webService))
return true
}
}
// 检查Banner中的Web服务器特征
if serviceInfo.Banner != "" {
banner := strings.ToLower(serviceInfo.Banner)
webBanners := []string{
"server:", "apache", "nginx", "iis", "lighttpd",
"caddy", "traefik", "tomcat", "jetty", "nodejs",
"content-type:", "content-length:", "http/",
"connection:", "keep-alive", "close",
}
for _, webBanner := range webBanners {
if strings.Contains(banner, webBanner) {
common.LogDebug(fmt.Sprintf("基于Banner识别为Web服务: %s", webBanner))
return true
}
}
}
// 检查额外信息中的Web特征
if serviceInfo.Extras != nil {
for key, value := range serviceInfo.Extras {
keyLower := strings.ToLower(key)
valueLower := strings.ToLower(value)
if keyLower == "info" && (strings.Contains(valueLower, "web") ||
strings.Contains(valueLower, "http") || strings.Contains(valueLower, "html")) {
common.LogDebug(fmt.Sprintf("基于Extras识别为Web服务: %s=%s", key, value))
return true
}
}
}
common.LogDebug(fmt.Sprintf("指纹识别为非Web服务: %s", serviceName))
return false
}
// MarkAsWebService 标记端口为Web服务用于后续Web插件调用
func MarkAsWebService(host string, port int, serviceInfo *ServiceInfo) {
cacheKey := fmt.Sprintf("%s:%d", host, port)
webCacheMutex.Lock()
defer webCacheMutex.Unlock()
webServiceCache[cacheKey] = serviceInfo
common.LogDebug(fmt.Sprintf("标记Web服务: %s [%s]", cacheKey, serviceInfo.Name))
}
// GetWebServiceInfo 获取已标记的Web服务信息
func GetWebServiceInfo(host string, port int) (*ServiceInfo, bool) {
cacheKey := fmt.Sprintf("%s:%d", host, port)
webCacheMutex.RLock()
defer webCacheMutex.RUnlock()
serviceInfo, exists := webServiceCache[cacheKey]
return serviceInfo, exists
}
// IsMarkedWebService 检查端口是否已被标记为Web服务
func IsMarkedWebService(host string, port int) bool {
_, exists := GetWebServiceInfo(host, port)
return exists
}

7
go.mod
View File

@ -15,6 +15,8 @@ require (
github.com/satori/go.uuid v1.2.0 github.com/satori/go.uuid v1.2.0
github.com/schollz/progressbar/v3 v3.13.1 github.com/schollz/progressbar/v3 v3.13.1
github.com/stacktitan/smb v0.0.0-20190531122847-da9a425dceb8 github.com/stacktitan/smb v0.0.0-20190531122847-da9a425dceb8
github.com/ziutek/telnet v0.1.0
go.mongodb.org/mongo-driver v1.17.4
golang.org/x/crypto v0.31.0 golang.org/x/crypto v0.31.0
golang.org/x/net v0.32.0 golang.org/x/net v0.32.0
golang.org/x/sync v0.10.0 golang.org/x/sync v0.10.0
@ -53,11 +55,16 @@ require (
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
github.com/montanaflynn/stats v0.7.1 // indirect
github.com/pierrec/lz4/v4 v4.1.21 // indirect github.com/pierrec/lz4/v4 v4.1.21 // indirect
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect
github.com/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect
github.com/rogpeppe/go-internal v1.13.1 // indirect github.com/rogpeppe/go-internal v1.13.1 // indirect
github.com/stoewer/go-strcase v1.2.0 // indirect github.com/stoewer/go-strcase v1.2.0 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.2 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
golang.org/x/term v0.27.0 // indirect golang.org/x/term v0.27.0 // indirect
golang.org/x/text v0.21.0 // indirect golang.org/x/text v0.21.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect

19
go.sum
View File

@ -30,7 +30,6 @@ github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFP
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
github.com/go-asn1-ber/asn1-ber v1.5.7 h1:DTX+lbVTWaTw1hQ+PbZPlnDZPEIs0SS/GCZAl535dDk= github.com/go-asn1-ber/asn1-ber v1.5.7 h1:DTX+lbVTWaTw1hQ+PbZPlnDZPEIs0SS/GCZAl535dDk=
github.com/go-asn1-ber/asn1-ber v1.5.7/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-asn1-ber/asn1-ber v1.5.7/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-ldap/ldap/v3 v3.4.9 h1:KxX9eO44/MpqPXVVMPJDB+k/35GEePHE/Jfvl7oRMUo= github.com/go-ldap/ldap/v3 v3.4.9 h1:KxX9eO44/MpqPXVVMPJDB+k/35GEePHE/Jfvl7oRMUo=
@ -86,7 +85,6 @@ github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
@ -103,6 +101,8 @@ github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA=
@ -134,8 +134,19 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/ziutek/telnet v0.1.0 h1:Fds2AqweYyoRHX/5X8ikiyqIcSl156Sf2xCvURfqXHA=
github.com/ziutek/telnet v0.1.0/go.mod h1:3M/h4qudUBZA8n+N4ywQIu2auiHUJNdqLUIKDAbG2M4=
go.mongodb.org/mongo-driver v1.17.4 h1:jUorfmVzljjr0FLzYQsGP8cgN/qzzxlY9Vh0C9KFXVw=
go.mongodb.org/mongo-driver v1.17.4/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
@ -206,6 +217,7 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
@ -228,7 +240,6 @@ google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

35
main.go
View File

@ -22,13 +22,36 @@ func main() {
var info common.HostInfo var info common.HostInfo
common.Flag(&info) common.Flag(&info)
// 检查-local与-h -u的互斥性 // 检查核心参数的互斥性:-h、-u、-local 只能指定一个
if common.LocalPlugin != "" && info.Host != "" { paramCount := 0
fmt.Printf("错误: -local参数与-h参数互斥本地插件只能在本机运行\n") var activeParam string
os.Exit(1)
if info.Host != "" {
paramCount++
activeParam = "-h"
} }
if common.LocalPlugin != "" && common.TargetURL != "" { if common.TargetURL != "" {
fmt.Printf("错误: -local参数与-u参数互斥本地插件不需要URL目标\n") paramCount++
if activeParam != "" {
activeParam += " 和 -u"
} else {
activeParam = "-u"
}
}
if common.LocalPlugin != "" {
paramCount++
if activeParam != "" {
activeParam += " 和 -local"
} else {
activeParam = "-local"
}
}
if paramCount > 1 {
fmt.Printf("错误: 参数 %s 互斥,请只指定一个扫描目标\n", activeParam)
fmt.Printf(" -h: 网络主机扫描\n")
fmt.Printf(" -u: Web URL扫描\n")
fmt.Printf(" -local: 本地信息收集\n")
os.Exit(1) os.Exit(1)
} }

View File

@ -3,6 +3,7 @@ package services
import ( import (
"context" "context"
"fmt" "fmt"
"strings"
"time" "time"
"github.com/IBM/sarama" "github.com/IBM/sarama"
@ -22,16 +23,16 @@ func NewKafkaPlugin() *KafkaPlugin {
func (p *KafkaPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanResult { func (p *KafkaPlugin) Scan(ctx context.Context, info *common.HostInfo) *plugins.Result {
target := fmt.Sprintf("%s:%s", info.Host, info.Ports) target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
if common.DisableBrute { if common.DisableBrute {
return p.identifyService(ctx, info) return p.identifyService(ctx, info)
} }
credentials := GenerateCredentials("kafka") credentials := plugins.GenerateCredentials("kafka")
if len(credentials) == 0 { if len(credentials) == 0 {
return &ScanResult{ return &plugins.Result{
Success: false, Success: false,
Service: "kafka", Service: "kafka",
Error: fmt.Errorf("没有可用的测试凭据"), Error: fmt.Errorf("没有可用的测试凭据"),
@ -39,10 +40,21 @@ func (p *KafkaPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanResu
} }
for _, cred := range credentials { for _, cred := range credentials {
// 检查上下文是否已取消
select {
case <-ctx.Done():
return &plugins.Result{
Success: false,
Service: "kafka",
Error: ctx.Err(),
}
default:
}
if client := p.testCredential(ctx, info, cred); client != nil { if client := p.testCredential(ctx, info, cred); client != nil {
client.Close() client.Close()
common.LogSuccess(fmt.Sprintf("Kafka %s %s:%s", target, cred.Username, cred.Password)) common.LogSuccess(fmt.Sprintf("Kafka %s %s:%s", target, cred.Username, cred.Password))
return &ScanResult{ return &plugins.Result{
Success: true, Success: true,
Service: "kafka", Service: "kafka",
Username: cred.Username, Username: cred.Username,
@ -51,7 +63,7 @@ func (p *KafkaPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanResu
} }
} }
return &ScanResult{ return &plugins.Result{
Success: false, Success: false,
Service: "kafka", Service: "kafka",
Error: fmt.Errorf("未发现弱密码"), Error: fmt.Errorf("未发现弱密码"),
@ -59,7 +71,7 @@ func (p *KafkaPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanResu
} }
func (p *KafkaPlugin) testCredential(ctx context.Context, info *common.HostInfo, cred Credential) sarama.Client { func (p *KafkaPlugin) testCredential(ctx context.Context, info *common.HostInfo, cred plugins.Credential) sarama.Client {
target := fmt.Sprintf("%s:%s", info.Host, info.Ports) target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
timeout := time.Duration(common.Timeout) * time.Second timeout := time.Duration(common.Timeout) * time.Second
@ -87,32 +99,71 @@ func (p *KafkaPlugin) testCredential(ctx context.Context, info *common.HostInfo,
func (p *KafkaPlugin) identifyService(ctx context.Context, info *common.HostInfo) *ScanResult { func (p *KafkaPlugin) identifyService(ctx context.Context, info *common.HostInfo) *plugins.Result {
target := fmt.Sprintf("%s:%s", info.Host, info.Ports) target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
emptyCred := Credential{Username: "", Password: ""} // 尝试无认证连接
emptyCred := plugins.Credential{Username: "", Password: ""}
client := p.testCredential(ctx, info, emptyCred) client := p.testCredential(ctx, info, emptyCred)
if client == nil { if client != nil {
return &ScanResult{ defer client.Close()
banner := "Kafka (无认证)"
common.LogSuccess(fmt.Sprintf("Kafka %s %s", target, banner))
return &plugins.Result{
Success: true,
Service: "kafka",
Banner: banner,
}
}
// 尝试简单认证检测
config := sarama.NewConfig()
config.Net.DialTimeout = time.Duration(common.Timeout) * time.Second
config.Version = sarama.V2_0_0_0
brokers := []string{target}
client, err := sarama.NewClient(brokers, config)
if err != nil {
// 如果连接失败尝试检查是否是Kafka协议错误
if p.isKafkaProtocolError(err) {
banner := "Kafka (需要认证)"
common.LogSuccess(fmt.Sprintf("Kafka %s %s", target, banner))
return &plugins.Result{
Success: true,
Service: "kafka",
Banner: banner,
}
}
return &plugins.Result{
Success: false, Success: false,
Service: "kafka", Service: "kafka",
Error: fmt.Errorf("无法连接到Kafka服务"), Error: fmt.Errorf("无法识别为Kafka服务: %v", err),
} }
} }
defer client.Close() defer client.Close()
banner := "Kafka" banner := "Kafka"
common.LogSuccess(fmt.Sprintf("Kafka %s %s", target, banner)) common.LogSuccess(fmt.Sprintf("Kafka %s %s", target, banner))
return &ScanResult{ return &plugins.Result{
Success: true, Success: true,
Service: "kafka", Service: "kafka",
Banner: banner, Banner: banner,
} }
} }
// isKafkaProtocolError 检查错误是否表示Kafka协议响应
func (p *KafkaPlugin) isKafkaProtocolError(err error) bool {
errStr := strings.ToLower(err.Error())
// Kafka常见的协议错误模式
return strings.Contains(errStr, "sasl") ||
strings.Contains(errStr, "authentication") ||
strings.Contains(errStr, "kafka") ||
strings.Contains(errStr, "protocol") ||
strings.Contains(errStr, "broker")
}
func init() { func init() {
// 使用高效注册方式:直接传递端口信息,避免实例创建 plugins.RegisterWithPorts("kafka", func() plugins.Plugin {
RegisterPluginWithPorts("kafka", func() Plugin {
return NewKafkaPlugin() return NewKafkaPlugin()
}, []int{9092, 9093, 9094}) }, []int{9092, 9093, 9094})
} }

View File

@ -19,28 +19,30 @@ func NewLDAPPlugin() *LDAPPlugin {
} }
} }
func (p *LDAPPlugin) Scan(ctx context.Context, info *common.HostInfo) *plugins.Result {
func (p *LDAPPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanResult {
target := fmt.Sprintf("%s:%s", info.Host, info.Ports) target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
if common.DisableBrute { if common.DisableBrute {
return p.identifyService(ctx, info) return p.identifyService(ctx, info)
} }
credentials := GenerateCredentials("ldap") credentials := plugins.GenerateCredentials("ldap")
if len(credentials) == 0 {
return &ScanResult{
Success: false,
Service: "ldap",
Error: fmt.Errorf("没有可用的测试凭据"),
}
}
for _, cred := range credentials { for _, cred := range credentials {
// 检查上下文是否已取消
select {
case <-ctx.Done():
return &plugins.Result{
Success: false,
Service: "ldap",
Error: ctx.Err(),
}
default:
}
if p.testCredential(ctx, info, cred) { if p.testCredential(ctx, info, cred) {
common.LogSuccess(fmt.Sprintf("LDAP %s %s:%s", target, cred.Username, cred.Password)) common.LogSuccess(fmt.Sprintf("LDAP %s %s:%s", target, cred.Username, cred.Password))
return &ScanResult{ return &plugins.Result{
Success: true, Success: true,
Service: "ldap", Service: "ldap",
Username: cred.Username, Username: cred.Username,
@ -49,29 +51,39 @@ func (p *LDAPPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanResul
} }
} }
return &ScanResult{ return &plugins.Result{
Success: false, Success: false,
Service: "ldap", Service: "ldap",
Error: fmt.Errorf("未发现弱密码"), Error: fmt.Errorf("未发现弱密码"),
} }
} }
func (p *LDAPPlugin) testCredential(ctx context.Context, info *common.HostInfo, cred plugins.Credential) bool {
func (p *LDAPPlugin) testCredential(ctx context.Context, info *common.HostInfo, cred Credential) bool {
conn, err := p.connectLDAP(ctx, info, cred) conn, err := p.connectLDAP(ctx, info, cred)
if err != nil { if err != nil {
return false return false
} }
defer conn.Close() defer conn.Close()
// 简单的绑定测试 // 尝试多种DN格式进行绑定测试
if err := conn.Bind(cred.Username, cred.Password); err == nil { dnFormats := []string{
return true fmt.Sprintf("cn=%s,dc=example,dc=com", cred.Username), // 标准格式
fmt.Sprintf("uid=%s,dc=example,dc=com", cred.Username), // uid格式
fmt.Sprintf("cn=%s,ou=users,dc=example,dc=com", cred.Username), // ou格式
cred.Username, // 直接用户名(某些配置)
}
for _, dn := range dnFormats {
if err := conn.Bind(dn, cred.Password); err == nil {
common.LogDebug(fmt.Sprintf("LDAP绑定成功DN: %s", dn))
return true
}
common.LogDebug(fmt.Sprintf("LDAP绑定失败DN: %s, 错误: %v", dn, err))
} }
return false return false
} }
func (p *LDAPPlugin) connectLDAP(ctx context.Context, info *common.HostInfo, creds Credential) (*ldaplib.Conn, error) { func (p *LDAPPlugin) connectLDAP(ctx context.Context, info *common.HostInfo, creds plugins.Credential) (*ldaplib.Conn, error) {
target := fmt.Sprintf("%s:%s", info.Host, info.Ports) target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
if info.Ports == "636" { if info.Ports == "636" {
@ -80,17 +92,12 @@ func (p *LDAPPlugin) connectLDAP(ctx context.Context, info *common.HostInfo, cre
return ldaplib.Dial("tcp", target) return ldaplib.Dial("tcp", target)
} }
func (p *LDAPPlugin) identifyService(ctx context.Context, info *common.HostInfo) *plugins.Result {
func (p *LDAPPlugin) identifyService(ctx context.Context, info *common.HostInfo) *ScanResult {
target := fmt.Sprintf("%s:%s", info.Host, info.Ports) target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
conn, err := p.connectLDAP(ctx, info, Credential{}) conn, err := p.connectLDAP(ctx, info, plugins.Credential{})
if err != nil { if err != nil {
return &ScanResult{ return &plugins.Result{
Success: false, Success: false,
Service: "ldap", Service: "ldap",
Error: err, Error: err,
@ -100,7 +107,7 @@ func (p *LDAPPlugin) identifyService(ctx context.Context, info *common.HostInfo)
banner := "LDAP" banner := "LDAP"
common.LogSuccess(fmt.Sprintf("LDAP %s %s", target, banner)) common.LogSuccess(fmt.Sprintf("LDAP %s %s", target, banner))
return &ScanResult{ return &plugins.Result{
Success: true, Success: true,
Service: "ldap", Service: "ldap",
Banner: banner, Banner: banner,
@ -108,8 +115,7 @@ func (p *LDAPPlugin) identifyService(ctx context.Context, info *common.HostInfo)
} }
func init() { func init() {
// 使用高效注册方式:直接传递端口信息,避免实例创建 plugins.RegisterWithPorts("ldap", func() plugins.Plugin {
RegisterPluginWithPorts("ldap", func() Plugin {
return NewLDAPPlugin() return NewLDAPPlugin()
}, []int{389, 636, 3268, 3269}) }, []int{389, 636, 3268, 3269})
} }

View File

@ -3,10 +3,13 @@ package services
import ( import (
"context" "context"
"fmt" "fmt"
"io"
"net" "net"
"strings" "strings"
"time" "time"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
"github.com/shadow1ng/fscan/common" "github.com/shadow1ng/fscan/common"
"github.com/shadow1ng/fscan/plugins" "github.com/shadow1ng/fscan/plugins"
) )
@ -21,170 +24,239 @@ func NewMongoDBPlugin() *MongoDBPlugin {
} }
} }
func (p *MongoDBPlugin) Scan(ctx context.Context, info *common.HostInfo) *plugins.Result {
func (p *MongoDBPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanResult {
target := fmt.Sprintf("%s:%s", info.Host, info.Ports) target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
if common.DisableBrute { if common.DisableBrute {
return p.identifyService(ctx, info) return p.identifyService(ctx, info)
} }
credentials := GenerateCredentials("mongodb") // 首先检测未授权访问
if len(credentials) == 0 { isUnauth, err := p.mongodbUnauth(ctx, info)
return &ScanResult{ if err != nil {
return &plugins.Result{
Success: false, Success: false,
Service: "mongodb", Service: "mongodb",
Error: fmt.Errorf("没有可用的测试凭据"), Error: err,
} }
} }
// 优化:只测试一次连接,检查是否允许无认证访问 if isUnauth {
if p.testUnauthenticatedAccess(ctx, info) { common.LogSuccess(fmt.Sprintf("MongoDB %s 未授权访问", target))
common.LogSuccess(fmt.Sprintf("MongoDB %s 无认证访问", target)) return &plugins.Result{
return &ScanResult{ Success: true,
Success: true, Service: "mongodb",
Service: "mongodb", Banner: "未授权访问",
Username: "",
Password: "",
Banner: "无认证访问",
} }
} }
return &ScanResult{ // 如果需要认证,尝试常见凭据
credentials := plugins.GenerateCredentials("mongodb")
for _, cred := range credentials {
if p.testMongoCredential(ctx, info, cred) {
common.LogSuccess(fmt.Sprintf("MongoDB %s %s:%s", target, cred.Username, cred.Password))
return &plugins.Result{
Success: true,
Service: "mongodb",
Username: cred.Username,
Password: cred.Password,
}
}
}
return &plugins.Result{
Success: false, Success: false,
Service: "mongodb", Service: "mongodb",
Error: fmt.Errorf("未发现弱密码"), Error: fmt.Errorf("未发现弱密码"),
} }
} }
func (p *MongoDBPlugin) identifyService(ctx context.Context, info *common.HostInfo) *plugins.Result {
func (p *MongoDBPlugin) testCredential(ctx context.Context, info *common.HostInfo, cred Credential) bool {
// 这个方法现在不使用,保留以防向后兼容
return p.testUnauthenticatedAccess(ctx, info)
}
func (p *MongoDBPlugin) testUnauthenticatedAccess(ctx context.Context, info *common.HostInfo) bool {
target := fmt.Sprintf("%s:%s", info.Host, info.Ports) target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
timeout := time.Duration(common.Timeout) * time.Second
conn, err := net.DialTimeout("tcp", target, timeout) // 尝试检测MongoDB服务
isUnauth, err := p.mongodbUnauth(ctx, info)
if err != nil { if err != nil {
return false return &plugins.Result{
}
defer conn.Close()
conn.SetDeadline(time.Now().Add(timeout))
return p.testBasicQuery(conn)
}
func (p *MongoDBPlugin) testBasicQuery(conn net.Conn) bool {
queryMsg := p.createListDatabasesQuery()
if _, err := conn.Write(queryMsg); err != nil {
return false
}
response := make([]byte, 1024)
n, err := conn.Read(response)
if err != nil {
return false
}
return n > 36 && p.isValidMongoResponse(response[:n])
}
func (p *MongoDBPlugin) isValidMongoResponse(data []byte) bool {
if len(data) < 36 {
return false
}
responseStr := string(data)
return strings.Contains(responseStr, "databases") ||
strings.Contains(responseStr, "totalSize") ||
strings.Contains(responseStr, "name")
}
func (p *MongoDBPlugin) createListDatabasesQuery() []byte {
query := make([]byte, 58)
query[0] = 0x3A
query[4] = 0x01
query[12] = 0x04
query[13] = 0x20
copy(query[20:], "admin.$cmd\x00")
bsonQuery := []byte{
0x1A, 0x00, 0x00, 0x00,
0x10,
0x6C, 0x69, 0x73, 0x74, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x73, 0x00,
0x01, 0x00, 0x00, 0x00,
0x00,
}
copy(query[32:], bsonQuery)
return query
}
func (p *MongoDBPlugin) identifyService(ctx context.Context, info *common.HostInfo) *ScanResult {
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
timeout := time.Duration(common.Timeout) * time.Second
conn, err := net.DialTimeout("tcp", target, timeout)
if err != nil {
return &ScanResult{
Success: false, Success: false,
Service: "mongodb", Service: "mongodb",
Error: err, Error: err,
} }
} }
// 如果能获得MongoDB响应无论是否授权都说明是MongoDB服务
if isUnauth {
common.LogSuccess(fmt.Sprintf("MongoDB %s 未授权访问", target))
return &plugins.Result{
Success: true,
Service: "mongodb",
Banner: "未授权访问",
}
} else {
common.LogSuccess(fmt.Sprintf("MongoDB %s 需要认证", target))
return &plugins.Result{
Success: true,
Service: "mongodb",
Banner: "需要认证",
}
}
}
// mongodbUnauth 检测MongoDB未授权访问 - 基于原始工作版本
func (p *MongoDBPlugin) mongodbUnauth(ctx context.Context, info *common.HostInfo) (bool, error) {
msgPacket := p.createOpMsgPacket()
queryPacket := p.createOpQueryPacket()
realhost := fmt.Sprintf("%s:%s", info.Host, info.Ports)
// 尝试OP_MSG查询
reply, err := p.checkMongoAuth(ctx, realhost, msgPacket)
if err != nil {
// 失败则尝试OP_QUERY查询
reply, err = p.checkMongoAuth(ctx, realhost, queryPacket)
if err != nil {
return false, err
}
}
// 检查响应结果 - 基于原始版本的检测逻辑
if strings.Contains(reply, "totalLinesWritten") {
return true, nil
}
// 如果能收到响应但不包含预期内容说明是MongoDB但需要认证
if len(reply) > 0 {
return false, nil // 是MongoDB但需要认证
}
return false, fmt.Errorf("无法识别为MongoDB服务")
}
// checkMongoAuth 检查MongoDB认证状态 - 基于原始工作版本
func (p *MongoDBPlugin) checkMongoAuth(ctx context.Context, address string, packet []byte) (string, error) {
// 创建连接超时上下文
connCtx, cancel := context.WithTimeout(ctx, time.Duration(common.Timeout)*time.Second)
defer cancel()
// 使用带超时的连接
var d net.Dialer
conn, err := d.DialContext(connCtx, "tcp", address)
if err != nil {
return "", fmt.Errorf("连接失败: %v", err)
}
defer conn.Close() defer conn.Close()
conn.SetDeadline(time.Now().Add(timeout)) // 检查上下文是否已取消
select {
// 简化识别逻辑:先尝试基础查询,失败则使用端口推断 case <-ctx.Done():
if p.testBasicQuery(conn) { return "", ctx.Err()
banner := "MongoDB" default:
common.LogSuccess(fmt.Sprintf("MongoDB %s %s", target, banner))
return &ScanResult{
Success: true,
Service: "mongodb",
Banner: banner,
}
} }
// 如果查询失败但能建立TCP连接且是MongoDB默认端口推断为MongoDB // 设置读写超时
if info.Ports == "27017" || info.Ports == "27018" || info.Ports == "27019" { if err := conn.SetDeadline(time.Now().Add(time.Duration(common.Timeout) * time.Second)); err != nil {
banner := "MongoDB (端口推断)" return "", fmt.Errorf("设置超时失败: %v", err)
common.LogSuccess(fmt.Sprintf("MongoDB %s %s", target, banner))
return &ScanResult{
Success: true,
Service: "mongodb",
Banner: banner,
}
} }
return &ScanResult{ // 发送查询包
Success: false, if _, err := conn.Write(packet); err != nil {
Service: "mongodb", return "", fmt.Errorf("发送查询失败: %v", err)
Error: fmt.Errorf("无法识别为MongoDB服务: 连接成功但协议查询失败"),
} }
// 再次检查上下文是否已取消
select {
case <-ctx.Done():
return "", ctx.Err()
default:
}
// 读取响应
reply := make([]byte, 2048)
count, err := conn.Read(reply)
if err != nil && err != io.EOF {
return "", fmt.Errorf("读取响应失败: %v", err)
}
if count == 0 {
return "", fmt.Errorf("收到空响应")
}
return string(reply[:count]), nil
}
// createOpMsgPacket 创建OP_MSG查询包 - 直接使用原始工作版本
func (p *MongoDBPlugin) createOpMsgPacket() []byte {
return []byte{
0x69, 0x00, 0x00, 0x00, // messageLength
0x39, 0x00, 0x00, 0x00, // requestID
0x00, 0x00, 0x00, 0x00, // responseTo
0xdd, 0x07, 0x00, 0x00, // opCode OP_MSG
0x00, 0x00, 0x00, 0x00, // flagBits
// sections db.adminCommand({getLog: "startupWarnings"})
0x00, 0x54, 0x00, 0x00, 0x00, 0x02, 0x67, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x00, 0x10, 0x00, 0x00, 0x00, 0x73, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x57, 0x61, 0x72, 0x6e, 0x69, 0x6e, 0x67, 0x73, 0x00, 0x02, 0x24, 0x64, 0x62, 0x00, 0x06, 0x00, 0x00, 0x00, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x00, 0x03, 0x6c, 0x73, 0x69, 0x64, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x05, 0x69, 0x64, 0x00, 0x10, 0x00, 0x00, 0x00, 0x04, 0x6e, 0x81, 0xf8, 0x8e, 0x37, 0x7b, 0x4c, 0x97, 0x84, 0x4e, 0x90, 0x62, 0x5a, 0x54, 0x3c, 0x93, 0x00, 0x00,
}
}
// createOpQueryPacket 创建OP_QUERY查询包 - 直接使用原始工作版本
func (p *MongoDBPlugin) createOpQueryPacket() []byte {
return []byte{
0x48, 0x00, 0x00, 0x00, // messageLength
0x02, 0x00, 0x00, 0x00, // requestID
0x00, 0x00, 0x00, 0x00, // responseTo
0xd4, 0x07, 0x00, 0x00, // opCode OP_QUERY
0x00, 0x00, 0x00, 0x00, // flags
0x61, 0x64, 0x6d, 0x69, 0x6e, 0x2e, 0x24, 0x63, 0x6d, 0x64, 0x00, // fullCollectionName admin.$cmd
0x00, 0x00, 0x00, 0x00, // numberToSkip
0x01, 0x00, 0x00, 0x00, // numberToReturn
// query db.adminCommand({getLog: "startupWarnings"})
0x21, 0x00, 0x00, 0x00, 0x2, 0x67, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x00, 0x10, 0x00, 0x00, 0x00, 0x73, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x57, 0x61, 0x72, 0x6e, 0x69, 0x6e, 0x67, 0x73, 0x00, 0x00,
}
}
// testMongoCredential 使用官方MongoDB驱动测试凭据
func (p *MongoDBPlugin) testMongoCredential(ctx context.Context, info *common.HostInfo, cred plugins.Credential) bool {
// 构建MongoDB连接URI
var uri string
if cred.Username != "" && cred.Password != "" {
uri = fmt.Sprintf("mongodb://%s:%s@%s:%s/?connectTimeoutMS=%d000&serverSelectionTimeoutMS=%d000",
cred.Username, cred.Password, info.Host, info.Ports, common.Timeout, common.Timeout)
} else if cred.Username != "" {
// 对于有用户名但密码为空的情况,仍然尝试认证
uri = fmt.Sprintf("mongodb://%s:@%s:%s/?connectTimeoutMS=%d000&serverSelectionTimeoutMS=%d000",
cred.Username, info.Host, info.Ports, common.Timeout, common.Timeout)
} else {
// 无用户名的情况,尝试无认证连接
uri = fmt.Sprintf("mongodb://%s:%s/?connectTimeoutMS=%d000&serverSelectionTimeoutMS=%d000",
info.Host, info.Ports, common.Timeout, common.Timeout)
}
// 创建客户端选项
clientOptions := options.Client().ApplyURI(uri)
// 创建带超时的上下文
authCtx, cancel := context.WithTimeout(ctx, time.Duration(common.Timeout)*time.Second)
defer cancel()
// 连接到MongoDB
client, err := mongo.Connect(authCtx, clientOptions)
if err != nil {
return false
}
defer client.Disconnect(authCtx)
// 测试连接 - 尝试ping数据库
err = client.Ping(authCtx, nil)
if err != nil {
return false
}
return true
} }
func init() { func init() {
// 使用高效注册方式:直接传递端口信息,避免实例创建 plugins.RegisterWithPorts("mongodb", func() plugins.Plugin {
RegisterPluginWithPorts("mongodb", func() Plugin {
return NewMongoDBPlugin() return NewMongoDBPlugin()
}, []int{27017, 27018, 27019}) }, []int{27017, 27018, 27019})
} }

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"io" "io"
"net"
"net/http" "net/http"
"strings" "strings"
"time" "time"
@ -24,20 +25,30 @@ func NewRabbitMQPlugin() *RabbitMQPlugin {
func (p *RabbitMQPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanResult { func (p *RabbitMQPlugin) Scan(ctx context.Context, info *common.HostInfo) *plugins.Result {
target := fmt.Sprintf("%s:%s", info.Host, info.Ports) target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
if common.DisableBrute { if common.DisableBrute {
return p.identifyService(ctx, info) return p.identifyService(ctx, info)
} }
// 对于AMQP端口首先识别服务
if info.Ports == "5672" || info.Ports == "5671" { if info.Ports == "5672" || info.Ports == "5671" {
return p.testAMQPProtocol(ctx, info) if result := p.testAMQPProtocol(ctx, info); result.Success {
// AMQP协议识别成功尝试HTTP管理接口的密码爆破
managementResult := p.testManagementInterface(ctx, info)
if managementResult.Success {
return managementResult
}
// 返回服务识别结果
return result
}
} }
credentials := GenerateCredentials("rabbitmq") // HTTP端口的密码爆破
credentials := plugins.GenerateCredentials("rabbitmq")
if len(credentials) == 0 { if len(credentials) == 0 {
return &ScanResult{ return &plugins.Result{
Success: false, Success: false,
Service: "rabbitmq", Service: "rabbitmq",
Error: fmt.Errorf("没有可用的测试凭据"), Error: fmt.Errorf("没有可用的测试凭据"),
@ -45,10 +56,20 @@ func (p *RabbitMQPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanR
} }
for _, cred := range credentials { for _, cred := range credentials {
// 检查上下文是否已取消
select {
case <-ctx.Done():
return &plugins.Result{
Success: false,
Service: "rabbitmq",
Error: ctx.Err(),
}
default:
}
if p.testCredential(ctx, info, cred) { if p.testCredential(ctx, info, cred) {
common.LogSuccess(fmt.Sprintf("RabbitMQ %s %s:%s", target, cred.Username, cred.Password)) common.LogSuccess(fmt.Sprintf("RabbitMQ %s %s:%s", target, cred.Username, cred.Password))
return &plugins.Result{
return &ScanResult{
Success: true, Success: true,
Service: "rabbitmq", Service: "rabbitmq",
Username: cred.Username, Username: cred.Username,
@ -57,7 +78,7 @@ func (p *RabbitMQPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanR
} }
} }
return &ScanResult{ return &plugins.Result{
Success: false, Success: false,
Service: "rabbitmq", Service: "rabbitmq",
Error: fmt.Errorf("未发现弱密码"), Error: fmt.Errorf("未发现弱密码"),
@ -65,15 +86,94 @@ func (p *RabbitMQPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanR
} }
func (p *RabbitMQPlugin) testAMQPProtocol(ctx context.Context, info *common.HostInfo) *ScanResult { // testAMQPProtocol 检测AMQP协议
return &ScanResult{ func (p *RabbitMQPlugin) testAMQPProtocol(ctx context.Context, info *common.HostInfo) *plugins.Result {
Success: true, target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
// 连接到AMQP端口
conn, err := net.DialTimeout("tcp", target, time.Duration(common.Timeout)*time.Second)
if err != nil {
return &plugins.Result{
Success: false,
Service: "rabbitmq",
Error: err,
}
}
defer conn.Close()
// 设置超时
conn.SetDeadline(time.Now().Add(time.Duration(common.Timeout) * time.Second))
// 发送AMQP协议头请求 (AMQP 0-9-1)
amqpHeader := []byte{0x41, 0x4d, 0x51, 0x50, 0x00, 0x00, 0x09, 0x01}
_, err = conn.Write(amqpHeader)
if err != nil {
return &plugins.Result{
Success: false,
Service: "rabbitmq",
Error: fmt.Errorf("发送AMQP握手失败: %v", err),
}
}
// 读取服务器响应
buffer := make([]byte, 32)
n, err := conn.Read(buffer)
if err != nil {
return &plugins.Result{
Success: false,
Service: "rabbitmq",
Error: fmt.Errorf("读取AMQP响应失败: %v", err),
}
}
// 调试信息(可选)
common.LogDebug(fmt.Sprintf("RabbitMQ AMQP响应长度: %d, 前8字节: %v", n, buffer[:min(n, 8)]))
// 检查AMQP协议头或连接开始帧
if n >= 4 && string(buffer[:4]) == "AMQP" {
// 服务器返回AMQP协议头
banner := fmt.Sprintf("RabbitMQ AMQP %d.%d.%d", buffer[5], buffer[6], buffer[7])
common.LogSuccess(fmt.Sprintf("RabbitMQ %s %s", target, banner))
return &plugins.Result{
Success: true,
Service: "rabbitmq",
Banner: banner,
}
} else if n >= 8 && buffer[0] == 0x01 && buffer[7] == 0xCE {
// Connection.Start方法帧 (AMQP 0-9-1)
banner := "RabbitMQ AMQP 0-9-1"
common.LogSuccess(fmt.Sprintf("RabbitMQ %s %s", target, banner))
return &plugins.Result{
Success: true,
Service: "rabbitmq",
Banner: banner,
}
} else if n >= 8 && buffer[0] == 0x01 {
// 可能是AMQP帧但格式不同
banner := "RabbitMQ AMQP"
common.LogSuccess(fmt.Sprintf("RabbitMQ %s %s", target, banner))
return &plugins.Result{
Success: true,
Service: "rabbitmq",
Banner: banner,
}
}
return &plugins.Result{
Success: false,
Service: "rabbitmq", Service: "rabbitmq",
Banner: "RabbitMQ AMQP", Error: fmt.Errorf("非AMQP协议响应"),
} }
} }
func (p *RabbitMQPlugin) testCredential(ctx context.Context, info *common.HostInfo, cred Credential) bool { func min(a, b int) int {
if a < b {
return a
}
return b
}
func (p *RabbitMQPlugin) testCredential(ctx context.Context, info *common.HostInfo, cred plugins.Credential) bool {
baseURL := fmt.Sprintf("http://%s:%s", info.Host, info.Ports) baseURL := fmt.Sprintf("http://%s:%s", info.Host, info.Ports)
client := &http.Client{ client := &http.Client{
@ -104,17 +204,19 @@ func (p *RabbitMQPlugin) testCredential(ctx context.Context, info *common.HostIn
func (p *RabbitMQPlugin) identifyService(ctx context.Context, info *common.HostInfo) *ScanResult { func (p *RabbitMQPlugin) identifyService(ctx context.Context, info *common.HostInfo) *plugins.Result {
target := fmt.Sprintf("%s:%s", info.Host, info.Ports) // 对于AMQP端口检测AMQP协议
if info.Ports == "5672" || info.Ports == "5671" { if info.Ports == "5672" || info.Ports == "5671" {
return &ScanResult{ return p.testAMQPProtocol(ctx, info)
Success: true,
Service: "rabbitmq",
Banner: "RabbitMQ AMQP",
}
} }
// 对于HTTP端口检测管理界面
return p.testManagementInterface(ctx, info)
}
// testManagementInterface 检测RabbitMQ管理界面
func (p *RabbitMQPlugin) testManagementInterface(ctx context.Context, info *common.HostInfo) *plugins.Result {
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
baseURL := fmt.Sprintf("http://%s:%s", info.Host, info.Ports) baseURL := fmt.Sprintf("http://%s:%s", info.Host, info.Ports)
client := &http.Client{ client := &http.Client{
@ -123,7 +225,7 @@ func (p *RabbitMQPlugin) identifyService(ctx context.Context, info *common.HostI
req, err := http.NewRequestWithContext(ctx, "GET", baseURL, nil) req, err := http.NewRequestWithContext(ctx, "GET", baseURL, nil)
if err != nil { if err != nil {
return &ScanResult{ return &plugins.Result{
Success: false, Success: false,
Service: "rabbitmq", Service: "rabbitmq",
Error: err, Error: err,
@ -132,7 +234,7 @@ func (p *RabbitMQPlugin) identifyService(ctx context.Context, info *common.HostI
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
return &ScanResult{ return &plugins.Result{
Success: false, Success: false,
Service: "rabbitmq", Service: "rabbitmq",
Error: err, Error: err,
@ -146,33 +248,29 @@ func (p *RabbitMQPlugin) identifyService(ctx context.Context, info *common.HostI
bodyStr := strings.ToLower(string(body)) bodyStr := strings.ToLower(string(body))
if strings.Contains(bodyStr, "rabbitmq") { if strings.Contains(bodyStr, "rabbitmq") {
banner = "RabbitMQ" banner = "RabbitMQ Management"
} else if strings.Contains(bodyStr, "management") { } else if strings.Contains(bodyStr, "management") {
banner = "RabbitMQ" banner = "RabbitMQ Management"
} else { } else {
banner = "RabbitMQ" banner = "RabbitMQ Management"
}
common.LogSuccess(fmt.Sprintf("RabbitMQ %s %s", target, banner))
return &plugins.Result{
Success: true,
Service: "rabbitmq",
Banner: banner,
} }
} else { } else {
return &ScanResult{ return &plugins.Result{
Success: false, Success: false,
Service: "rabbitmq", Service: "rabbitmq",
Error: fmt.Errorf("无法识别为RabbitMQ服务"), Error: fmt.Errorf("无法识别为RabbitMQ服务"),
} }
} }
common.LogSuccess(fmt.Sprintf("RabbitMQ %s %s", target, banner))
return &ScanResult{
Success: true,
Service: "rabbitmq",
Banner: banner,
}
} }
// init 自动注册插件
func init() { func init() {
// 使用高效注册方式:直接传递端口信息,避免实例创建 plugins.RegisterWithPorts("rabbitmq", func() plugins.Plugin {
RegisterPluginWithPorts("rabbitmq", func() Plugin {
return NewRabbitMQPlugin() return NewRabbitMQPlugin()
}, []int{5672, 15672, 5671}) }, []int{5672, 15672, 5671})
} }

View File

@ -2,7 +2,6 @@ package services
import ( import (
"context" "context"
"crypto/tls"
"fmt" "fmt"
"net" "net"
"net/smtp" "net/smtp"
@ -25,21 +24,32 @@ func NewSMTPPlugin() *SMTPPlugin {
func (p *SMTPPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanResult { func (p *SMTPPlugin) Scan(ctx context.Context, info *common.HostInfo) *plugins.Result {
target := fmt.Sprintf("%s:%s", info.Host, info.Ports) target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
if common.DisableBrute { if common.DisableBrute {
return p.identifyService(ctx, info) return p.identifyService(ctx, info)
} }
// 首先测试匿名访问(最重要!)
if p.testCredential(ctx, info, plugins.Credential{Username: "", Password: ""}) {
common.LogSuccess(fmt.Sprintf("SMTP服务 %s 允许匿名访问", target))
return &plugins.Result{
Success: true,
Service: "smtp",
Banner: "允许匿名访问",
}
}
// 检查开放中继
if result := p.testOpenRelay(ctx, info); result != nil && result.Success { if result := p.testOpenRelay(ctx, info); result != nil && result.Success {
common.LogSuccess(fmt.Sprintf("SMTP %s 开放中继", target)) common.LogSuccess(fmt.Sprintf("SMTP %s 开放中继", target))
return result return result
} }
credentials := GenerateCredentials("smtp") credentials := plugins.GenerateCredentials("smtp")
if len(credentials) == 0 { if len(credentials) == 0 {
return &ScanResult{ return &plugins.Result{
Success: false, Success: false,
Service: "smtp", Service: "smtp",
Error: fmt.Errorf("没有可用的测试凭据"), Error: fmt.Errorf("没有可用的测试凭据"),
@ -47,10 +57,20 @@ func (p *SMTPPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanResul
} }
for _, cred := range credentials { for _, cred := range credentials {
// 检查上下文是否已取消
select {
case <-ctx.Done():
return &plugins.Result{
Success: false,
Service: "smtp",
Error: ctx.Err(),
}
default:
}
if p.testCredential(ctx, info, cred) { if p.testCredential(ctx, info, cred) {
common.LogSuccess(fmt.Sprintf("SMTP %s %s:%s", target, cred.Username, cred.Password)) common.LogSuccess(fmt.Sprintf("SMTP %s %s:%s", target, cred.Username, cred.Password))
return &plugins.Result{
return &ScanResult{
Success: true, Success: true,
Service: "smtp", Service: "smtp",
Username: cred.Username, Username: cred.Username,
@ -59,7 +79,7 @@ func (p *SMTPPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanResul
} }
} }
return &ScanResult{ return &plugins.Result{
Success: false, Success: false,
Service: "smtp", Service: "smtp",
Error: fmt.Errorf("未发现弱密码"), Error: fmt.Errorf("未发现弱密码"),
@ -67,10 +87,21 @@ func (p *SMTPPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanResul
} }
func (p *SMTPPlugin) testOpenRelay(ctx context.Context, info *common.HostInfo) *ScanResult { func (p *SMTPPlugin) testOpenRelay(ctx context.Context, info *common.HostInfo) *plugins.Result {
target := fmt.Sprintf("%s:%s", info.Host, info.Ports) target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
client, err := smtp.Dial(target) // 设置超时的Dialer
dialer := &net.Dialer{
Timeout: time.Duration(common.Timeout) * time.Second,
}
conn, err := dialer.DialContext(ctx, "tcp", target)
if err != nil {
return nil
}
defer conn.Close()
client, err := smtp.NewClient(conn, info.Host)
if err != nil { if err != nil {
return nil return nil
} }
@ -88,52 +119,61 @@ func (p *SMTPPlugin) testOpenRelay(ctx context.Context, info *common.HostInfo) *
return nil return nil
} }
return &ScanResult{ return &plugins.Result{
Success: true, Success: true,
Service: "smtp", Service: "smtp",
Banner: "开放中继", Banner: "开放中继",
} }
} }
func (p *SMTPPlugin) testCredential(ctx context.Context, info *common.HostInfo, cred Credential) bool { func (p *SMTPPlugin) testCredential(ctx context.Context, info *common.HostInfo, cred plugins.Credential) bool {
target := fmt.Sprintf("%s:%s", info.Host, info.Ports) target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
timeout := time.Duration(common.Timeout) * time.Second
var client *smtp.Client common.LogDebug(fmt.Sprintf("SMTP测试凭据: %s:%s", cred.Username, cred.Password))
var err error
if info.Ports == "465" { // 设置连接超时
conn, err := tls.Dial("tcp", target, &tls.Config{InsecureSkipVerify: true}) dialer := &net.Dialer{
if err != nil { Timeout: timeout,
return false }
}
defer conn.Close()
client, err = smtp.NewClient(conn, info.Host) conn, err := dialer.DialContext(ctx, "tcp", target)
if err != nil { if err != nil {
return false common.LogDebug(fmt.Sprintf("SMTP连接失败: %v", err))
} return false
} else { }
client, err = smtp.Dial(target) defer conn.Close()
if err != nil {
// 设置读写超时
conn.SetDeadline(time.Now().Add(timeout))
client, err := smtp.NewClient(conn, info.Host)
if err != nil {
common.LogDebug(fmt.Sprintf("SMTP客户端创建失败: %v", err))
return false
}
defer client.Close()
// 如果有用户名密码,则尝试认证
if cred.Username != "" {
auth := smtp.PlainAuth("", cred.Username, cred.Password, info.Host)
if err := client.Auth(auth); err != nil {
common.LogDebug(fmt.Sprintf("SMTP认证失败 %s:%s - %v", cred.Username, cred.Password, err))
return false return false
} }
} }
defer client.Quit()
if ok, _ := client.Extension("STARTTLS"); ok { // 尝试发送邮件测试权限(仿照原版)
if err := client.StartTLS(&tls.Config{InsecureSkipVerify: true}); err != nil { if err := client.Mail("test@test.com"); err != nil {
common.LogDebug(fmt.Sprintf("SMTP Mail命令失败 %s:%s - %v", cred.Username, cred.Password, err))
}
}
auth := smtp.PlainAuth("", cred.Username, cred.Password, info.Host)
if err := client.Auth(auth); err != nil {
return false return false
} }
common.LogDebug(fmt.Sprintf("SMTP认证成功: %s:%s", cred.Username, cred.Password))
return true return true
} }
func (p *SMTPPlugin) getServerInfo(ctx context.Context, info *common.HostInfo) string { func (p *SMTPPlugin) getServerInfo(ctx context.Context, info *common.HostInfo) string {
target := fmt.Sprintf("%s:%s", info.Host, info.Ports) target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
@ -158,7 +198,7 @@ func (p *SMTPPlugin) getServerInfo(ctx context.Context, info *common.HostInfo) s
return welcome return welcome
} }
func (p *SMTPPlugin) identifyService(ctx context.Context, info *common.HostInfo) *ScanResult { func (p *SMTPPlugin) identifyService(ctx context.Context, info *common.HostInfo) *plugins.Result {
target := fmt.Sprintf("%s:%s", info.Host, info.Ports) target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
serverInfo := p.getServerInfo(ctx, info) serverInfo := p.getServerInfo(ctx, info)
@ -167,9 +207,13 @@ func (p *SMTPPlugin) identifyService(ctx context.Context, info *common.HostInfo)
if serverInfo != "" { if serverInfo != "" {
banner = fmt.Sprintf("SMTP邮件服务 (%s)", serverInfo) banner = fmt.Sprintf("SMTP邮件服务 (%s)", serverInfo)
} else { } else {
conn, err := net.DialTimeout("tcp", target, time.Duration(common.Timeout)*time.Second) // 简单连接测试
dialer := &net.Dialer{
Timeout: time.Duration(common.Timeout) * time.Second,
}
conn, err := dialer.DialContext(ctx, "tcp", target)
if err != nil { if err != nil {
return &ScanResult{ return &plugins.Result{
Success: false, Success: false,
Service: "smtp", Service: "smtp",
Error: err, Error: err,
@ -181,7 +225,7 @@ func (p *SMTPPlugin) identifyService(ctx context.Context, info *common.HostInfo)
common.LogSuccess(fmt.Sprintf("SMTP %s %s", target, banner)) common.LogSuccess(fmt.Sprintf("SMTP %s %s", target, banner))
return &ScanResult{ return &plugins.Result{
Success: true, Success: true,
Service: "smtp", Service: "smtp",
Banner: banner, Banner: banner,
@ -189,8 +233,7 @@ func (p *SMTPPlugin) identifyService(ctx context.Context, info *common.HostInfo)
} }
func init() { func init() {
// 使用高效注册方式:直接传递端口信息,避免实例创建 plugins.RegisterWithPorts("smtp", func() plugins.Plugin {
RegisterPluginWithPorts("smtp", func() Plugin {
return NewSMTPPlugin() return NewSMTPPlugin()
}, []int{25, 465, 587, 2525}) }, []int{25, 465, 587, 2525})
} }

View File

@ -21,34 +21,31 @@ func NewTelnetPlugin() *TelnetPlugin {
} }
} }
func (p *TelnetPlugin) Scan(ctx context.Context, info *common.HostInfo) *plugins.Result {
func (p *TelnetPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanResult {
target := fmt.Sprintf("%s:%s", info.Host, info.Ports) target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
if common.DisableBrute { if common.DisableBrute {
return p.identifyService(ctx, info) return p.identifyService(ctx, info)
} }
if result := p.testUnauthAccess(ctx, info); result != nil && result.Success { // 构建凭据列表
common.LogSuccess(fmt.Sprintf("Telnet %s 未授权访问", target)) credentials := plugins.GenerateCredentials("telnet")
return result
}
credentials := GenerateCredentials("telnet")
if len(credentials) == 0 {
return &ScanResult{
Success: false,
Service: "telnet",
Error: fmt.Errorf("没有可用的测试凭据"),
}
}
for _, cred := range credentials { for _, cred := range credentials {
if p.testCredential(ctx, info, cred) { // 检查上下文是否已取消
common.LogSuccess(fmt.Sprintf("Telnet %s %s:%s", target, cred.Username, cred.Password)) select {
case <-ctx.Done():
return &plugins.Result{
Success: false,
Service: "telnet",
Error: ctx.Err(),
}
default:
}
return &ScanResult{ if p.testTelnetCredential(ctx, info, cred) {
common.LogSuccess(fmt.Sprintf("Telnet %s %s:%s", target, cred.Username, cred.Password))
return &plugins.Result{
Success: true, Success: true,
Service: "telnet", Service: "telnet",
Username: cred.Username, Username: cred.Username,
@ -57,135 +54,259 @@ func (p *TelnetPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanRes
} }
} }
return &ScanResult{ return &plugins.Result{
Success: false, Success: false,
Service: "telnet", Service: "telnet",
Error: fmt.Errorf("未发现弱密码"), Error: fmt.Errorf("未发现弱密码"),
} }
} }
// testTelnetCredential 测试telnet凭据
func (p *TelnetPlugin) testTelnetCredential(ctx context.Context, info *common.HostInfo, cred plugins.Credential) bool {
address := fmt.Sprintf("%s:%s", info.Host, info.Ports)
func (p *TelnetPlugin) testUnauthAccess(ctx context.Context, info *common.HostInfo) *ScanResult { // 创建带超时的连接
target := fmt.Sprintf("%s:%s", info.Host, info.Ports) conn, err := net.DialTimeout("tcp", address, time.Duration(common.Timeout)*time.Second)
conn, err := net.DialTimeout("tcp", target, time.Duration(common.Timeout)*time.Second)
if err != nil { if err != nil {
return nil return false
} }
defer conn.Close() defer conn.Close()
conn.SetDeadline(time.Now().Add(time.Duration(common.Timeout) * time.Second)) // 设置超时
deadline := time.Now().Add(time.Duration(common.Timeout) * time.Second)
conn.SetDeadline(deadline)
buffer := make([]byte, 1024) // 简单的telnet认证流程
n, err := conn.Read(buffer) return p.performSimpleTelnetAuth(conn, cred.Username, cred.Password)
if err != nil {
return nil
}
welcome := string(buffer[:n])
if strings.Contains(welcome, "$") || strings.Contains(welcome, "#") || strings.Contains(welcome, ">") {
return &ScanResult{
Success: true,
Service: "telnet",
Banner: "未授权访问",
}
}
return nil
} }
func (p *TelnetPlugin) testCredential(ctx context.Context, info *common.HostInfo, cred Credential) bool { // performSimpleTelnetAuth 执行简单的telnet认证
target := fmt.Sprintf("%s:%s", info.Host, info.Ports) func (p *TelnetPlugin) performSimpleTelnetAuth(conn net.Conn, username, password string) bool {
conn, err := net.DialTimeout("tcp", target, time.Duration(common.Timeout)*time.Second)
if err != nil {
return false
}
defer conn.Close()
conn.SetDeadline(time.Now().Add(time.Duration(common.Timeout) * time.Second))
buffer := make([]byte, 1024) buffer := make([]byte, 1024)
n, err := conn.Read(buffer)
// 处理IAC协商并等待真正的登录提示
loginPromptReceived := false
attempts := 0
maxAttempts := 10 // 最多尝试10次读取
for attempts < maxAttempts && !loginPromptReceived {
attempts++
// 设置较短的读取超时
conn.SetReadDeadline(time.Now().Add(2 * time.Second))
n, err := conn.Read(buffer)
if err != nil {
common.LogDebug(fmt.Sprintf("第%d次读取失败: %v", attempts, err))
time.Sleep(200 * time.Millisecond)
continue
}
response := string(buffer[:n])
// 处理IAC协商
p.handleIACNegotiation(conn, buffer[:n])
// 清理响应
cleaned := p.cleanResponse(response)
common.LogDebug(fmt.Sprintf("第%d次响应[%s:%s]: %q -> %q", attempts, username, password, response, cleaned))
// 检查是否为shell提示符无需认证
if p.isShellPrompt(cleaned) {
common.LogDebug(fmt.Sprintf("检测到shell提示符无需认证"))
return true
}
// 检查是否收到登录提示
if strings.Contains(strings.ToLower(cleaned), "login") ||
strings.Contains(strings.ToLower(cleaned), "username") ||
strings.Contains(cleaned, ":") { // 简单的提示符检测
loginPromptReceived = true
common.LogDebug(fmt.Sprintf("检测到登录提示"))
break
}
time.Sleep(200 * time.Millisecond)
}
if !loginPromptReceived {
common.LogDebug(fmt.Sprintf("未在%d次尝试中检测到登录提示", maxAttempts))
return false
}
// 发送用户名
common.LogDebug(fmt.Sprintf("发送用户名: %s", username))
_, err := conn.Write([]byte(username + "\r\n"))
if err != nil { if err != nil {
return false return false
} }
data := string(buffer[:n]) // 等待密码提示
data = p.cleanTelnetData(data) time.Sleep(500 * time.Millisecond)
passwordPromptReceived := false
attempts = 0
if strings.Contains(strings.ToLower(data), "login") || strings.Contains(strings.ToLower(data), "username") { for attempts < 5 && !passwordPromptReceived {
conn.Write([]byte(cred.Username + "\r\n")) attempts++
time.Sleep(500 * time.Millisecond)
n, err = conn.Read(buffer) conn.SetReadDeadline(time.Now().Add(2 * time.Second))
n, err := conn.Read(buffer)
if err != nil { if err != nil {
common.LogDebug(fmt.Sprintf("读取密码提示第%d次失败: %v", attempts, err))
time.Sleep(200 * time.Millisecond)
continue
}
response := string(buffer[:n])
cleaned := p.cleanResponse(response)
common.LogDebug(fmt.Sprintf("密码提示第%d次响应: %q -> %q", attempts, response, cleaned))
if strings.Contains(strings.ToLower(cleaned), "password") ||
strings.Contains(cleaned, ":") {
passwordPromptReceived = true
common.LogDebug(fmt.Sprintf("检测到密码提示"))
break
}
time.Sleep(200 * time.Millisecond)
}
if !passwordPromptReceived {
common.LogDebug(fmt.Sprintf("未检测到密码提示"))
return false
}
// 发送密码
common.LogDebug(fmt.Sprintf("发送密码: %s", password))
_, err = conn.Write([]byte(password + "\r\n"))
if err != nil {
return false
}
// 检查登录结果
time.Sleep(1000 * time.Millisecond)
attempts = 0
for attempts < 5 {
attempts++
conn.SetReadDeadline(time.Now().Add(2 * time.Second))
n, err := conn.Read(buffer)
if err != nil {
common.LogDebug(fmt.Sprintf("读取登录结果第%d次失败: %v", attempts, err))
time.Sleep(200 * time.Millisecond)
continue
}
response := string(buffer[:n])
cleaned := p.cleanResponse(response)
common.LogDebug(fmt.Sprintf("登录结果第%d次响应: %q -> %q", attempts, response, cleaned))
// 检查登录成功或失败
if p.isLoginSuccess(cleaned) {
common.LogDebug(fmt.Sprintf("登录成功!"))
return true
}
if p.isLoginFailed(cleaned) {
common.LogDebug(fmt.Sprintf("登录失败!"))
return false return false
} }
data = string(buffer[:n]) time.Sleep(200 * time.Millisecond)
if strings.Contains(strings.ToLower(data), "password") {
conn.Write([]byte(cred.Password + "\r\n"))
time.Sleep(1 * time.Second)
n, err = conn.Read(buffer)
if err != nil {
return false
}
result := string(buffer[:n])
result = p.cleanTelnetData(result)
return p.isLoginSuccessful(result)
}
} }
common.LogDebug(fmt.Sprintf("无法确定登录结果"))
return false return false
} }
// handleIACNegotiation 处理IAC协商
func (p *TelnetPlugin) handleIACNegotiation(conn net.Conn, data []byte) {
for i := 0; i < len(data); i++ {
if data[i] == 255 && i+2 < len(data) { // IAC
cmd := data[i+1]
opt := data[i+2]
func (p *TelnetPlugin) cleanTelnetData(data string) string { // 简单响应策略:拒绝所有选项
cleaned := "" switch cmd {
case 251: // WILL
// 回应DONT
conn.Write([]byte{255, 254, opt})
common.LogDebug(fmt.Sprintf("IAC响应: DONT %d", opt))
case 253: // DO
// 回应WONT
conn.Write([]byte{255, 252, opt})
common.LogDebug(fmt.Sprintf("IAC响应: WONT %d", opt))
}
i += 2
}
}
}
// cleanResponse 清理telnet响应中的IAC命令
func (p *TelnetPlugin) cleanResponse(data string) string {
var result strings.Builder
for i := 0; i < len(data); i++ { for i := 0; i < len(data); i++ {
b := data[i] b := data[i]
// 跳过IAC命令序列
if b == 255 && i+2 < len(data) { if b == 255 && i+2 < len(data) {
i += 2 i += 2
continue continue
} }
if b >= 32 && b <= 126 || b == '\r' || b == '\n' { // 保留可打印字符
cleaned += string(b) if (b >= 32 && b <= 126) || b == '\r' || b == '\n' {
result.WriteByte(b)
} }
} }
return cleaned return result.String()
} }
func (p *TelnetPlugin) isLoginSuccessful(data string) bool { // isShellPrompt 检查是否为shell提示符
func (p *TelnetPlugin) isShellPrompt(data string) bool {
data = strings.ToLower(data)
return strings.Contains(data, "$") ||
strings.Contains(data, "#") ||
strings.Contains(data, ">")
}
// isLoginSuccess 检查登录是否成功
func (p *TelnetPlugin) isLoginSuccess(data string) bool {
data = strings.ToLower(data) data = strings.ToLower(data)
successIndicators := []string{"$", "#", ">", "welcome", "last login"} // 检查成功标识
for _, indicator := range successIndicators { if strings.Contains(data, "$") ||
if strings.Contains(data, indicator) { strings.Contains(data, "#") ||
return true strings.Contains(data, ">") ||
} strings.Contains(data, "welcome") ||
} strings.Contains(data, "last login") {
return true
failIndicators := []string{"incorrect", "failed", "denied", "invalid", "login:"}
for _, indicator := range failIndicators {
if strings.Contains(data, indicator) {
return false
}
} }
return false return false
} }
func (p *TelnetPlugin) identifyService(ctx context.Context, info *common.HostInfo) *ScanResult { // isLoginFailed 检查登录是否失败
func (p *TelnetPlugin) isLoginFailed(data string) bool {
data = strings.ToLower(data)
// 检查失败标识
if strings.Contains(data, "incorrect") ||
strings.Contains(data, "failed") ||
strings.Contains(data, "denied") ||
strings.Contains(data, "invalid") ||
strings.Contains(data, "login:") || // 重新出现登录提示
strings.Contains(data, "username:") {
return true
}
return false
}
func (p *TelnetPlugin) identifyService(ctx context.Context, info *common.HostInfo) *plugins.Result {
target := fmt.Sprintf("%s:%s", info.Host, info.Ports) target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
conn, err := net.DialTimeout("tcp", target, time.Duration(common.Timeout)*time.Second) conn, err := net.DialTimeout("tcp", target, time.Duration(common.Timeout)*time.Second)
if err != nil { if err != nil {
return &ScanResult{ return &plugins.Result{
Success: false, Success: false,
Service: "telnet", Service: "telnet",
Error: err, Error: err,
@ -198,7 +319,7 @@ func (p *TelnetPlugin) identifyService(ctx context.Context, info *common.HostInf
buffer := make([]byte, 1024) buffer := make([]byte, 1024)
n, err := conn.Read(buffer) n, err := conn.Read(buffer)
if err != nil { if err != nil {
return &ScanResult{ return &plugins.Result{
Success: false, Success: false,
Service: "telnet", Service: "telnet",
Error: err, Error: err,
@ -206,7 +327,21 @@ func (p *TelnetPlugin) identifyService(ctx context.Context, info *common.HostInf
} }
data := string(buffer[:n]) data := string(buffer[:n])
data = p.cleanTelnetData(data) // 清理telnet数据
cleaned := ""
for i := 0; i < len(data); i++ {
b := data[i]
// 跳过telnet控制字符
if b == 255 && i+2 < len(data) {
i += 2
continue
}
// 只保留可打印字符和换行符
if (b >= 32 && b <= 126) || b == '\r' || b == '\n' {
cleaned += string(b)
}
}
data = cleaned
var banner string var banner string
if strings.Contains(strings.ToLower(data), "login") || strings.Contains(strings.ToLower(data), "username") { if strings.Contains(strings.ToLower(data), "login") || strings.Contains(strings.ToLower(data), "username") {
@ -219,7 +354,7 @@ func (p *TelnetPlugin) identifyService(ctx context.Context, info *common.HostInf
common.LogSuccess(fmt.Sprintf("Telnet %s %s", target, banner)) common.LogSuccess(fmt.Sprintf("Telnet %s %s", target, banner))
return &ScanResult{ return &plugins.Result{
Success: true, Success: true,
Service: "telnet", Service: "telnet",
Banner: banner, Banner: banner,
@ -227,8 +362,7 @@ func (p *TelnetPlugin) identifyService(ctx context.Context, info *common.HostInf
} }
func init() { func init() {
// 使用高效注册方式:直接传递端口信息,避免实例创建 plugins.RegisterWithPorts("telnet", func() plugins.Plugin {
RegisterPluginWithPorts("telnet", func() Plugin {
return NewTelnetPlugin() return NewTelnetPlugin()
}, []int{23, 2323}) }, []int{23, 2323})
} }