mirror of
https://github.com/shadow1ng/fscan.git
synced 2025-09-14 05:56:46 +08:00
Compare commits
14 Commits
0cc843dc97
...
a7ea2f5198
Author | SHA1 | Date | |
---|---|---|---|
![]() |
a7ea2f5198 | ||
![]() |
80644cd6f1 | ||
![]() |
afdefccfdd | ||
![]() |
31e59c9bee | ||
![]() |
3b50f42474 | ||
![]() |
1ff44d1ebc | ||
![]() |
82ab894bcf | ||
![]() |
5133010ed2 | ||
![]() |
af2c92a591 | ||
![]() |
b7b805874f | ||
![]() |
628ebfb4df | ||
![]() |
8ae94f7813 | ||
![]() |
7579549e94 | ||
![]() |
3ab0405df2 |
139
README.md
139
README.md
@ -33,25 +33,132 @@
|
||||
- 扫描结果存储:将所有检测结果保存至文件,便于后续分析
|
||||
|
||||
# 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
|
||||
# 基础编译
|
||||
go build -ldflags="-s -w" -trimpath main.go
|
||||
go build -ldflags="-s -w" -trimpath -o fscan main.go
|
||||
|
||||
# UPX压缩(可选)
|
||||
upx -9 fscan
|
||||
@ -65,6 +172,18 @@ yay -S fscan-git
|
||||
paru -S fscan-git
|
||||
```
|
||||
|
||||
## 官方网站
|
||||
|
||||
**https://fscan.club/**
|
||||
|
||||
访问官网获取:
|
||||
|
||||
- 详细功能文档
|
||||
- 使用教程
|
||||
- 最新版本下载
|
||||
- 常见问题解答
|
||||
- 技术支持
|
||||
|
||||
# 0x04 运行截图
|
||||
|
||||
`fscan.exe -h 192.168.x.x (全功能、ms17010、读取网卡信息)`
|
||||
|
@ -54,7 +54,7 @@ type ScanControlConfig struct {
|
||||
GlobalTimeout int64 `json:"global_timeout"` // 整体扫描超时时间(秒)
|
||||
// LiveTop 已移除,改为智能控制
|
||||
DisablePing bool `json:"disable_ping"` // 是否禁用主机存活性检测
|
||||
EnableFingerprint bool `json:"enable_fingerprint"` // 是否启用服务指纹识别
|
||||
// EnableFingerprint 已删除:服务指纹识别默认启用
|
||||
LocalMode bool `json:"local_mode"` // 是否启用本地信息收集模式
|
||||
}
|
||||
|
||||
|
@ -21,7 +21,7 @@ var (
|
||||
|
||||
ModuleThreadNum int
|
||||
GlobalTimeout int64
|
||||
EnableFingerprint bool
|
||||
// EnableFingerprint 已删除:服务指纹识别默认启用
|
||||
|
||||
AddUsers string
|
||||
AddPasswords string
|
||||
@ -146,7 +146,7 @@ func Flag(Info *HostInfo) {
|
||||
flag.Int64Var(&GlobalTimeout, "gt", 180, i18n.GetText("flag_global_timeout"))
|
||||
// LiveTop 参数已移除,改为智能控制
|
||||
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.BoolVar(&AliveOnly, "ao", false, i18n.GetText("flag_alive_only"))
|
||||
|
||||
|
@ -62,10 +62,7 @@ var FlagMessages = map[string]map[string]string{
|
||||
LangZH: "禁用ping探测",
|
||||
LangEN: "Disable ping detection",
|
||||
},
|
||||
"flag_enable_fingerprint": {
|
||||
LangZH: "启用指纹识别",
|
||||
LangEN: "Enable fingerprinting",
|
||||
},
|
||||
// "flag_enable_fingerprint" 已删除:服务指纹识别默认启用
|
||||
"flag_local_mode": {
|
||||
LangZH: "本地扫描模式",
|
||||
LangEN: "Local scan mode",
|
||||
|
@ -206,8 +206,9 @@ func (np *NetworkParser) parseTimeouts(timeout, webTimeout int64) (time.Duration
|
||||
finalWebTimeout = time.Duration(webTimeout) * time.Second
|
||||
}
|
||||
|
||||
// 验证超时配置合理性
|
||||
if finalWebTimeout > finalTimeout {
|
||||
// 验证超时配置合理性:只有在Web超时显著大于普通超时时才警告
|
||||
// Web超时适当大于普通超时是合理的,因为Web请求包含更多步骤
|
||||
if finalWebTimeout > finalTimeout*2 {
|
||||
warnings = append(warnings, i18n.GetText("config_web_timeout_warning"))
|
||||
}
|
||||
|
||||
|
@ -60,9 +60,9 @@ func (p *PortDiscoveryService) shouldPerformLivenessCheck(hosts []string) bool {
|
||||
func (p *PortDiscoveryService) discoverAlivePorts(hosts []string) []string {
|
||||
var alivePorts []string
|
||||
|
||||
// 如果已经有明确指定的host:port,则优先使用并跳过常规端口扫描
|
||||
// 如果已经有明确指定的host:port,验证连通性并显示端口信息
|
||||
if len(common.HostPort) > 0 {
|
||||
alivePorts = append(alivePorts, common.HostPort...)
|
||||
alivePorts = p.validatePresetPorts(common.HostPort)
|
||||
alivePorts = common.RemoveDuplicate(alivePorts)
|
||||
common.LogBase(i18n.GetText("scan_alive_ports_count", len(alivePorts)))
|
||||
common.HostPort = nil
|
||||
@ -131,3 +131,30 @@ func (p *PortDiscoveryService) handleUDPPorts(hosts []string) []string {
|
||||
|
||||
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
182
core/PortInfoSmart.go
Normal 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适用于HTTP,GenericLines适用于大多数文本协议
|
||||
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
|
||||
}
|
@ -84,26 +84,25 @@ func EnhancedPortScan(hosts []string, ports string, timeout int64) []string {
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// 记录开放端口
|
||||
// 记录开放端口(先记录到内存,延迟输出直到服务识别完成)
|
||||
atomic.AddInt64(&count, 1)
|
||||
aliveMap.Store(addr, struct{}{})
|
||||
common.LogInfo("端口开放 " + addr)
|
||||
common.SaveResult(&output.ScanResult{
|
||||
Time: time.Now(), Type: output.TypePort, Target: host,
|
||||
Status: "open", Details: map[string]interface{}{"port": port},
|
||||
})
|
||||
|
||||
// 服务识别
|
||||
if common.EnableFingerprint {
|
||||
if info, err := NewPortInfoScanner(host, port, conn, to).Identify(); err == nil {
|
||||
// 智能服务识别:保持准确性,优化网络交互
|
||||
serviceInfo, err := NewSmartPortInfoScanner(host, port, conn, to).SmartIdentify()
|
||||
if err == nil {
|
||||
// 构建结果详情
|
||||
details := map[string]interface{}{"port": port, "service": info.Name}
|
||||
if info.Version != "" {
|
||||
details["version"] = info.Version
|
||||
details := map[string]interface{}{"port": port, "service": serviceInfo.Name}
|
||||
if serviceInfo.Version != "" {
|
||||
details["version"] = serviceInfo.Version
|
||||
}
|
||||
|
||||
// 处理额外信息
|
||||
for k, v := range info.Extras {
|
||||
for k, v := range serviceInfo.Extras {
|
||||
if v == "" {
|
||||
continue
|
||||
}
|
||||
@ -114,8 +113,16 @@ func EnhancedPortScan(hosts []string, ports string, timeout int64) []string {
|
||||
details[k] = v
|
||||
}
|
||||
}
|
||||
if len(info.Banner) > 0 {
|
||||
details["banner"] = strings.TrimSpace(info.Banner)
|
||||
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)
|
||||
}
|
||||
|
||||
// 保存服务结果
|
||||
@ -124,17 +131,23 @@ func EnhancedPortScan(hosts []string, ports string, timeout int64) []string {
|
||||
Status: "identified", Details: details,
|
||||
})
|
||||
|
||||
// 记录服务信息
|
||||
// 构建统一的服务信息日志
|
||||
var sb strings.Builder
|
||||
sb.WriteString("服务识别 " + addr + " => ")
|
||||
if info.Name != "unknown" {
|
||||
sb.WriteString("[" + info.Name + "]")
|
||||
sb.WriteString("端口开放 " + addr)
|
||||
|
||||
if serviceInfo.Name != "unknown" {
|
||||
sb.WriteString(" [" + serviceInfo.Name + "]")
|
||||
if isWeb {
|
||||
sb.WriteString("(Web服务)")
|
||||
}
|
||||
if info.Version != "" {
|
||||
sb.WriteString(" 版本:" + info.Version)
|
||||
}
|
||||
|
||||
for k, v := range info.Extras {
|
||||
if serviceInfo.Version != "" {
|
||||
sb.WriteString(" 版本:" + serviceInfo.Version)
|
||||
}
|
||||
|
||||
// 添加详细服务信息(默认启用)
|
||||
for k, v := range serviceInfo.Extras {
|
||||
if v == "" {
|
||||
continue
|
||||
}
|
||||
@ -148,12 +161,16 @@ func EnhancedPortScan(hosts []string, ports string, timeout int64) []string {
|
||||
}
|
||||
}
|
||||
|
||||
if len(info.Banner) > 0 && len(info.Banner) < 100 {
|
||||
sb.WriteString(" Banner:[" + strings.TrimSpace(info.Banner) + "]")
|
||||
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
|
||||
|
@ -173,24 +173,33 @@ func (s *ServiceScanStrategy) LogVulnerabilityPluginInfo(targets []common.HostIn
|
||||
}
|
||||
}
|
||||
|
||||
// 获取实际会被使用的插件列表(考虑端口匹配)
|
||||
var servicePlugins []string
|
||||
// 获取实际会被使用的插件列表(考虑所有发现端口的匹配)
|
||||
servicePluginSet := make(map[string]bool)
|
||||
|
||||
// 提取第一个目标端口用于匹配检查
|
||||
var firstTargetPort int
|
||||
if len(targets) > 0 && targets[0].Ports != "" {
|
||||
firstTargetPort, _ = strconv.Atoi(targets[0].Ports)
|
||||
// 提取所有目标端口用于插件适用性检查
|
||||
var allPorts []int
|
||||
for port := range portSet {
|
||||
allPorts = append(allPorts, port)
|
||||
}
|
||||
|
||||
for _, pluginName := range allPlugins {
|
||||
// 使用统一插件系统检查插件存在性
|
||||
if s.pluginExists(pluginName) {
|
||||
// 检查插件是否适用于目标端口
|
||||
if s.IsPluginApplicableByName(pluginName, "127.0.0.1", firstTargetPort, isCustomMode) {
|
||||
// 检查插件是否适用于任意一个发现的端口
|
||||
for _, port := range allPorts {
|
||||
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 {
|
||||
|
@ -102,8 +102,15 @@ func newWebPortDetector() *WebPortDetector {
|
||||
}
|
||||
}
|
||||
|
||||
// IsWebService 智能检测端口是否运行Web服务
|
||||
// IsWebService 智能检测端口是否运行Web服务(优化版:优先使用指纹识别结果)
|
||||
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
|
||||
if w.IsCommonWebPort(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()
|
||||
|
||||
// 3. 智能路径:对非常见端口进行HTTP协议探测
|
||||
common.LogDebug(fmt.Sprintf("对端口 %d 进行智能Web检测", port))
|
||||
// 3. 回退路径:对非常见端口进行HTTP协议探测(仅在服务指纹识别失败时使用)
|
||||
common.LogDebug(fmt.Sprintf("端口 %d 未通过服务指纹识别为Web服务,回退到HTTP协议探测", port))
|
||||
result := w.detectHTTPService(host, port)
|
||||
|
||||
// 4. 缓存结果
|
||||
@ -129,7 +136,7 @@ func (w *WebPortDetector) IsWebService(host string, port int) bool {
|
||||
w.detectionCache[cacheKey] = result
|
||||
w.cacheMutex.Unlock()
|
||||
|
||||
common.LogDebug(fmt.Sprintf("端口 %d 智能检测结果: %v", port, result))
|
||||
common.LogDebug(fmt.Sprintf("端口 %d HTTP协议探测结果: %v", port, result))
|
||||
return result
|
||||
}
|
||||
|
||||
@ -420,4 +427,106 @@ func min(a, b int) int {
|
||||
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
7
go.mod
@ -15,6 +15,8 @@ require (
|
||||
github.com/satori/go.uuid v1.2.0
|
||||
github.com/schollz/progressbar/v3 v3.13.1
|
||||
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/net v0.32.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-runewidth v0.0.14 // 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/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/rogpeppe/go-internal v1.13.1 // 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/text v0.21.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
|
19
go.sum
19
go.sum
@ -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/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
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/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
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/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/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
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/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/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
|
||||
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/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
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.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/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/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-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
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.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.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
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.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=
|
||||
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/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||
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=
|
||||
|
35
main.go
35
main.go
@ -22,13 +22,36 @@ func main() {
|
||||
var info common.HostInfo
|
||||
common.Flag(&info)
|
||||
|
||||
// 检查-local与-h -u的互斥性
|
||||
if common.LocalPlugin != "" && info.Host != "" {
|
||||
fmt.Printf("错误: -local参数与-h参数互斥,本地插件只能在本机运行\n")
|
||||
os.Exit(1)
|
||||
// 检查核心参数的互斥性:-h、-u、-local 只能指定一个
|
||||
paramCount := 0
|
||||
var activeParam string
|
||||
|
||||
if info.Host != "" {
|
||||
paramCount++
|
||||
activeParam = "-h"
|
||||
}
|
||||
if common.LocalPlugin != "" && common.TargetURL != "" {
|
||||
fmt.Printf("错误: -local参数与-u参数互斥,本地插件不需要URL目标\n")
|
||||
if common.TargetURL != "" {
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -3,6 +3,7 @@ package services
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"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)
|
||||
|
||||
if common.DisableBrute {
|
||||
return p.identifyService(ctx, info)
|
||||
}
|
||||
|
||||
credentials := GenerateCredentials("kafka")
|
||||
credentials := plugins.GenerateCredentials("kafka")
|
||||
if len(credentials) == 0 {
|
||||
return &ScanResult{
|
||||
return &plugins.Result{
|
||||
Success: false,
|
||||
Service: "kafka",
|
||||
Error: fmt.Errorf("没有可用的测试凭据"),
|
||||
@ -39,10 +40,21 @@ func (p *KafkaPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanResu
|
||||
}
|
||||
|
||||
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 {
|
||||
client.Close()
|
||||
common.LogSuccess(fmt.Sprintf("Kafka %s %s:%s", target, cred.Username, cred.Password))
|
||||
return &ScanResult{
|
||||
return &plugins.Result{
|
||||
Success: true,
|
||||
Service: "kafka",
|
||||
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,
|
||||
Service: "kafka",
|
||||
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)
|
||||
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)
|
||||
|
||||
emptyCred := Credential{Username: "", Password: ""}
|
||||
// 尝试无认证连接
|
||||
emptyCred := plugins.Credential{Username: "", Password: ""}
|
||||
client := p.testCredential(ctx, info, emptyCred)
|
||||
if client == nil {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "kafka",
|
||||
Error: fmt.Errorf("无法连接到Kafka服务"),
|
||||
}
|
||||
}
|
||||
if client != nil {
|
||||
defer client.Close()
|
||||
|
||||
banner := "Kafka"
|
||||
banner := "Kafka (无认证)"
|
||||
common.LogSuccess(fmt.Sprintf("Kafka %s %s", target, banner))
|
||||
return &ScanResult{
|
||||
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,
|
||||
Service: "kafka",
|
||||
Error: fmt.Errorf("无法识别为Kafka服务: %v", err),
|
||||
}
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
banner := "Kafka"
|
||||
common.LogSuccess(fmt.Sprintf("Kafka %s %s", target, banner))
|
||||
return &plugins.Result{
|
||||
Success: true,
|
||||
Service: "kafka",
|
||||
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() {
|
||||
// 使用高效注册方式:直接传递端口信息,避免实例创建
|
||||
RegisterPluginWithPorts("kafka", func() Plugin {
|
||||
plugins.RegisterWithPorts("kafka", func() plugins.Plugin {
|
||||
return NewKafkaPlugin()
|
||||
}, []int{9092, 9093, 9094})
|
||||
}
|
@ -19,28 +19,30 @@ func NewLDAPPlugin() *LDAPPlugin {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
func (p *LDAPPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanResult {
|
||||
func (p *LDAPPlugin) Scan(ctx context.Context, info *common.HostInfo) *plugins.Result {
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
|
||||
if common.DisableBrute {
|
||||
return p.identifyService(ctx, info)
|
||||
}
|
||||
|
||||
credentials := GenerateCredentials("ldap")
|
||||
if len(credentials) == 0 {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "ldap",
|
||||
Error: fmt.Errorf("没有可用的测试凭据"),
|
||||
}
|
||||
}
|
||||
credentials := plugins.GenerateCredentials("ldap")
|
||||
|
||||
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) {
|
||||
common.LogSuccess(fmt.Sprintf("LDAP %s %s:%s", target, cred.Username, cred.Password))
|
||||
return &ScanResult{
|
||||
return &plugins.Result{
|
||||
Success: true,
|
||||
Service: "ldap",
|
||||
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,
|
||||
Service: "ldap",
|
||||
Error: fmt.Errorf("未发现弱密码"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func (p *LDAPPlugin) testCredential(ctx context.Context, info *common.HostInfo, cred Credential) bool {
|
||||
func (p *LDAPPlugin) testCredential(ctx context.Context, info *common.HostInfo, cred plugins.Credential) bool {
|
||||
conn, err := p.connectLDAP(ctx, info, cred)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// 简单的绑定测试
|
||||
if err := conn.Bind(cred.Username, cred.Password); err == nil {
|
||||
// 尝试多种DN格式进行绑定测试
|
||||
dnFormats := []string{
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
if info.Ports == "636" {
|
||||
@ -80,17 +92,12 @@ func (p *LDAPPlugin) connectLDAP(ctx context.Context, info *common.HostInfo, cre
|
||||
return ldaplib.Dial("tcp", target)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
func (p *LDAPPlugin) identifyService(ctx context.Context, info *common.HostInfo) *ScanResult {
|
||||
func (p *LDAPPlugin) identifyService(ctx context.Context, info *common.HostInfo) *plugins.Result {
|
||||
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 {
|
||||
return &ScanResult{
|
||||
return &plugins.Result{
|
||||
Success: false,
|
||||
Service: "ldap",
|
||||
Error: err,
|
||||
@ -100,7 +107,7 @@ func (p *LDAPPlugin) identifyService(ctx context.Context, info *common.HostInfo)
|
||||
|
||||
banner := "LDAP"
|
||||
common.LogSuccess(fmt.Sprintf("LDAP %s %s", target, banner))
|
||||
return &ScanResult{
|
||||
return &plugins.Result{
|
||||
Success: true,
|
||||
Service: "ldap",
|
||||
Banner: banner,
|
||||
@ -108,8 +115,7 @@ func (p *LDAPPlugin) identifyService(ctx context.Context, info *common.HostInfo)
|
||||
}
|
||||
|
||||
func init() {
|
||||
// 使用高效注册方式:直接传递端口信息,避免实例创建
|
||||
RegisterPluginWithPorts("ldap", func() Plugin {
|
||||
plugins.RegisterWithPorts("ldap", func() plugins.Plugin {
|
||||
return NewLDAPPlugin()
|
||||
}, []int{389, 636, 3268, 3269})
|
||||
}
|
@ -3,10 +3,13 @@ package services
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
)
|
||||
@ -21,170 +24,239 @@ func NewMongoDBPlugin() *MongoDBPlugin {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
func (p *MongoDBPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanResult {
|
||||
func (p *MongoDBPlugin) Scan(ctx context.Context, info *common.HostInfo) *plugins.Result {
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
|
||||
if common.DisableBrute {
|
||||
return p.identifyService(ctx, info)
|
||||
}
|
||||
|
||||
credentials := GenerateCredentials("mongodb")
|
||||
if len(credentials) == 0 {
|
||||
return &ScanResult{
|
||||
// 首先检测未授权访问
|
||||
isUnauth, err := p.mongodbUnauth(ctx, info)
|
||||
if err != nil {
|
||||
return &plugins.Result{
|
||||
Success: false,
|
||||
Service: "mongodb",
|
||||
Error: fmt.Errorf("没有可用的测试凭据"),
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
// 优化:只测试一次连接,检查是否允许无认证访问
|
||||
if p.testUnauthenticatedAccess(ctx, info) {
|
||||
common.LogSuccess(fmt.Sprintf("MongoDB %s 无认证访问", target))
|
||||
return &ScanResult{
|
||||
if isUnauth {
|
||||
common.LogSuccess(fmt.Sprintf("MongoDB %s 未授权访问", target))
|
||||
return &plugins.Result{
|
||||
Success: true,
|
||||
Service: "mongodb",
|
||||
Username: "",
|
||||
Password: "",
|
||||
Banner: "无认证访问",
|
||||
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,
|
||||
Service: "mongodb",
|
||||
Error: fmt.Errorf("未发现弱密码"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
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 {
|
||||
func (p *MongoDBPlugin) identifyService(ctx context.Context, info *common.HostInfo) *plugins.Result {
|
||||
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 {
|
||||
return false
|
||||
}
|
||||
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{
|
||||
return &plugins.Result{
|
||||
Success: false,
|
||||
Service: "mongodb",
|
||||
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()
|
||||
|
||||
conn.SetDeadline(time.Now().Add(timeout))
|
||||
// 检查上下文是否已取消
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return "", ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
// 简化识别逻辑:先尝试基础查询,失败则使用端口推断
|
||||
if p.testBasicQuery(conn) {
|
||||
banner := "MongoDB"
|
||||
common.LogSuccess(fmt.Sprintf("MongoDB %s %s", target, banner))
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
Service: "mongodb",
|
||||
Banner: banner,
|
||||
// 设置读写超时
|
||||
if err := conn.SetDeadline(time.Now().Add(time.Duration(common.Timeout) * time.Second)); err != nil {
|
||||
return "", fmt.Errorf("设置超时失败: %v", err)
|
||||
}
|
||||
|
||||
// 发送查询包
|
||||
if _, err := conn.Write(packet); err != nil {
|
||||
return "", fmt.Errorf("发送查询失败: %v", err)
|
||||
}
|
||||
|
||||
// 再次检查上下文是否已取消
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
// 如果查询失败但能建立TCP连接,且是MongoDB默认端口,推断为MongoDB
|
||||
if info.Ports == "27017" || info.Ports == "27018" || info.Ports == "27019" {
|
||||
banner := "MongoDB (端口推断)"
|
||||
common.LogSuccess(fmt.Sprintf("MongoDB %s %s", target, banner))
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
Service: "mongodb",
|
||||
Banner: banner,
|
||||
// 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,
|
||||
}
|
||||
}
|
||||
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "mongodb",
|
||||
Error: fmt.Errorf("无法识别为MongoDB服务: 连接成功但协议查询失败"),
|
||||
// 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() {
|
||||
// 使用高效注册方式:直接传递端口信息,避免实例创建
|
||||
RegisterPluginWithPorts("mongodb", func() Plugin {
|
||||
plugins.RegisterWithPorts("mongodb", func() plugins.Plugin {
|
||||
return NewMongoDBPlugin()
|
||||
}, []int{27017, 27018, 27019})
|
||||
}
|
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"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)
|
||||
|
||||
if common.DisableBrute {
|
||||
return p.identifyService(ctx, info)
|
||||
}
|
||||
|
||||
// 对于AMQP端口,首先识别服务
|
||||
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 {
|
||||
return &ScanResult{
|
||||
return &plugins.Result{
|
||||
Success: false,
|
||||
Service: "rabbitmq",
|
||||
Error: fmt.Errorf("没有可用的测试凭据"),
|
||||
@ -45,10 +56,20 @@ func (p *RabbitMQPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanR
|
||||
}
|
||||
|
||||
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) {
|
||||
common.LogSuccess(fmt.Sprintf("RabbitMQ %s %s:%s", target, cred.Username, cred.Password))
|
||||
|
||||
return &ScanResult{
|
||||
return &plugins.Result{
|
||||
Success: true,
|
||||
Service: "rabbitmq",
|
||||
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,
|
||||
Service: "rabbitmq",
|
||||
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 {
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
// testAMQPProtocol 检测AMQP协议
|
||||
func (p *RabbitMQPlugin) testAMQPProtocol(ctx context.Context, info *common.HostInfo) *plugins.Result {
|
||||
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",
|
||||
Banner: "RabbitMQ AMQP",
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *RabbitMQPlugin) testCredential(ctx context.Context, info *common.HostInfo, cred Credential) bool {
|
||||
// 读取服务器响应
|
||||
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",
|
||||
Error: fmt.Errorf("非AMQP协议响应"),
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
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 {
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
|
||||
func (p *RabbitMQPlugin) identifyService(ctx context.Context, info *common.HostInfo) *plugins.Result {
|
||||
// 对于AMQP端口,检测AMQP协议
|
||||
if info.Ports == "5672" || info.Ports == "5671" {
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
Service: "rabbitmq",
|
||||
Banner: "RabbitMQ AMQP",
|
||||
}
|
||||
return p.testAMQPProtocol(ctx, info)
|
||||
}
|
||||
|
||||
// 对于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)
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
return &ScanResult{
|
||||
return &plugins.Result{
|
||||
Success: false,
|
||||
Service: "rabbitmq",
|
||||
Error: err,
|
||||
@ -132,7 +234,7 @@ func (p *RabbitMQPlugin) identifyService(ctx context.Context, info *common.HostI
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return &ScanResult{
|
||||
return &plugins.Result{
|
||||
Success: false,
|
||||
Service: "rabbitmq",
|
||||
Error: err,
|
||||
@ -146,33 +248,29 @@ func (p *RabbitMQPlugin) identifyService(ctx context.Context, info *common.HostI
|
||||
bodyStr := strings.ToLower(string(body))
|
||||
|
||||
if strings.Contains(bodyStr, "rabbitmq") {
|
||||
banner = "RabbitMQ"
|
||||
banner = "RabbitMQ Management"
|
||||
} else if strings.Contains(bodyStr, "management") {
|
||||
banner = "RabbitMQ"
|
||||
banner = "RabbitMQ Management"
|
||||
} 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 {
|
||||
return &ScanResult{
|
||||
return &plugins.Result{
|
||||
Success: false,
|
||||
Service: "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() {
|
||||
// 使用高效注册方式:直接传递端口信息,避免实例创建
|
||||
RegisterPluginWithPorts("rabbitmq", func() Plugin {
|
||||
plugins.RegisterWithPorts("rabbitmq", func() plugins.Plugin {
|
||||
return NewRabbitMQPlugin()
|
||||
}, []int{5672, 15672, 5671})
|
||||
}
|
@ -2,7 +2,6 @@ package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"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)
|
||||
|
||||
if common.DisableBrute {
|
||||
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 {
|
||||
common.LogSuccess(fmt.Sprintf("SMTP %s 开放中继", target))
|
||||
return result
|
||||
}
|
||||
|
||||
credentials := GenerateCredentials("smtp")
|
||||
credentials := plugins.GenerateCredentials("smtp")
|
||||
if len(credentials) == 0 {
|
||||
return &ScanResult{
|
||||
return &plugins.Result{
|
||||
Success: false,
|
||||
Service: "smtp",
|
||||
Error: fmt.Errorf("没有可用的测试凭据"),
|
||||
@ -47,10 +57,20 @@ func (p *SMTPPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanResul
|
||||
}
|
||||
|
||||
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) {
|
||||
common.LogSuccess(fmt.Sprintf("SMTP %s %s:%s", target, cred.Username, cred.Password))
|
||||
|
||||
return &ScanResult{
|
||||
return &plugins.Result{
|
||||
Success: true,
|
||||
Service: "smtp",
|
||||
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,
|
||||
Service: "smtp",
|
||||
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)
|
||||
|
||||
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 {
|
||||
return nil
|
||||
}
|
||||
@ -88,52 +119,61 @@ func (p *SMTPPlugin) testOpenRelay(ctx context.Context, info *common.HostInfo) *
|
||||
return nil
|
||||
}
|
||||
|
||||
return &ScanResult{
|
||||
return &plugins.Result{
|
||||
Success: true,
|
||||
Service: "smtp",
|
||||
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)
|
||||
timeout := time.Duration(common.Timeout) * time.Second
|
||||
|
||||
var client *smtp.Client
|
||||
var err error
|
||||
common.LogDebug(fmt.Sprintf("SMTP测试凭据: %s:%s", cred.Username, cred.Password))
|
||||
|
||||
if info.Ports == "465" {
|
||||
conn, err := tls.Dial("tcp", target, &tls.Config{InsecureSkipVerify: true})
|
||||
// 设置连接超时
|
||||
dialer := &net.Dialer{
|
||||
Timeout: timeout,
|
||||
}
|
||||
|
||||
conn, err := dialer.DialContext(ctx, "tcp", target)
|
||||
if err != nil {
|
||||
common.LogDebug(fmt.Sprintf("SMTP连接失败: %v", err))
|
||||
return false
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
client, err = smtp.NewClient(conn, info.Host)
|
||||
// 设置读写超时
|
||||
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
|
||||
}
|
||||
} else {
|
||||
client, err = smtp.Dial(target)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
}
|
||||
defer client.Quit()
|
||||
|
||||
if ok, _ := client.Extension("STARTTLS"); ok {
|
||||
if err := client.StartTLS(&tls.Config{InsecureSkipVerify: true}); err != nil {
|
||||
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试发送邮件测试权限(仿照原版)
|
||||
if err := client.Mail("test@test.com"); err != nil {
|
||||
common.LogDebug(fmt.Sprintf("SMTP Mail命令失败 %s:%s - %v", cred.Username, cred.Password, err))
|
||||
return false
|
||||
}
|
||||
|
||||
common.LogDebug(fmt.Sprintf("SMTP认证成功: %s:%s", cred.Username, cred.Password))
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
func (p *SMTPPlugin) getServerInfo(ctx context.Context, info *common.HostInfo) string {
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
serverInfo := p.getServerInfo(ctx, info)
|
||||
@ -167,9 +207,13 @@ func (p *SMTPPlugin) identifyService(ctx context.Context, info *common.HostInfo)
|
||||
if serverInfo != "" {
|
||||
banner = fmt.Sprintf("SMTP邮件服务 (%s)", serverInfo)
|
||||
} 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 {
|
||||
return &ScanResult{
|
||||
return &plugins.Result{
|
||||
Success: false,
|
||||
Service: "smtp",
|
||||
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))
|
||||
|
||||
return &ScanResult{
|
||||
return &plugins.Result{
|
||||
Success: true,
|
||||
Service: "smtp",
|
||||
Banner: banner,
|
||||
@ -189,8 +233,7 @@ func (p *SMTPPlugin) identifyService(ctx context.Context, info *common.HostInfo)
|
||||
}
|
||||
|
||||
func init() {
|
||||
// 使用高效注册方式:直接传递端口信息,避免实例创建
|
||||
RegisterPluginWithPorts("smtp", func() Plugin {
|
||||
plugins.RegisterWithPorts("smtp", func() plugins.Plugin {
|
||||
return NewSMTPPlugin()
|
||||
}, []int{25, 465, 587, 2525})
|
||||
}
|
||||
|
@ -21,34 +21,31 @@ func NewTelnetPlugin() *TelnetPlugin {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
func (p *TelnetPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanResult {
|
||||
func (p *TelnetPlugin) Scan(ctx context.Context, info *common.HostInfo) *plugins.Result {
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
|
||||
if common.DisableBrute {
|
||||
return p.identifyService(ctx, info)
|
||||
}
|
||||
|
||||
if result := p.testUnauthAccess(ctx, info); result != nil && result.Success {
|
||||
common.LogSuccess(fmt.Sprintf("Telnet %s 未授权访问", target))
|
||||
return result
|
||||
}
|
||||
|
||||
credentials := GenerateCredentials("telnet")
|
||||
if len(credentials) == 0 {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "telnet",
|
||||
Error: fmt.Errorf("没有可用的测试凭据"),
|
||||
}
|
||||
}
|
||||
// 构建凭据列表
|
||||
credentials := plugins.GenerateCredentials("telnet")
|
||||
|
||||
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,
|
||||
Service: "telnet",
|
||||
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,
|
||||
Service: "telnet",
|
||||
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", target, time.Duration(common.Timeout)*time.Second)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
conn.SetDeadline(time.Now().Add(time.Duration(common.Timeout) * time.Second))
|
||||
|
||||
buffer := make([]byte, 1024)
|
||||
n, err := conn.Read(buffer)
|
||||
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 {
|
||||
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", address, 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))
|
||||
// 设置超时
|
||||
deadline := time.Now().Add(time.Duration(common.Timeout) * time.Second)
|
||||
conn.SetDeadline(deadline)
|
||||
|
||||
// 简单的telnet认证流程
|
||||
return p.performSimpleTelnetAuth(conn, cred.Username, cred.Password)
|
||||
}
|
||||
|
||||
// performSimpleTelnetAuth 执行简单的telnet认证
|
||||
func (p *TelnetPlugin) performSimpleTelnetAuth(conn net.Conn, username, password string) bool {
|
||||
buffer := make([]byte, 1024)
|
||||
|
||||
// 处理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 {
|
||||
return false
|
||||
}
|
||||
|
||||
data := string(buffer[:n])
|
||||
data = p.cleanTelnetData(data)
|
||||
|
||||
if strings.Contains(strings.ToLower(data), "login") || strings.Contains(strings.ToLower(data), "username") {
|
||||
conn.Write([]byte(cred.Username + "\r\n"))
|
||||
// 等待密码提示
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
passwordPromptReceived := false
|
||||
attempts = 0
|
||||
|
||||
n, err = conn.Read(buffer)
|
||||
for attempts < 5 && !passwordPromptReceived {
|
||||
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 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
|
||||
}
|
||||
|
||||
data = string(buffer[:n])
|
||||
if strings.Contains(strings.ToLower(data), "password") {
|
||||
conn.Write([]byte(cred.Password + "\r\n"))
|
||||
time.Sleep(1 * time.Second)
|
||||
// 检查登录结果
|
||||
time.Sleep(1000 * time.Millisecond)
|
||||
attempts = 0
|
||||
|
||||
n, err = conn.Read(buffer)
|
||||
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
|
||||
}
|
||||
|
||||
result := string(buffer[:n])
|
||||
result = p.cleanTelnetData(result)
|
||||
|
||||
return p.isLoginSuccessful(result)
|
||||
}
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
}
|
||||
|
||||
common.LogDebug(fmt.Sprintf("无法确定登录结果"))
|
||||
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++ {
|
||||
b := data[i]
|
||||
// 跳过IAC命令序列
|
||||
if b == 255 && i+2 < len(data) {
|
||||
i += 2
|
||||
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)
|
||||
|
||||
successIndicators := []string{"$", "#", ">", "welcome", "last login"}
|
||||
for _, indicator := range successIndicators {
|
||||
if strings.Contains(data, indicator) {
|
||||
// 检查成功标识
|
||||
if strings.Contains(data, "$") ||
|
||||
strings.Contains(data, "#") ||
|
||||
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
|
||||
}
|
||||
|
||||
// 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) *ScanResult {
|
||||
func (p *TelnetPlugin) identifyService(ctx context.Context, info *common.HostInfo) *plugins.Result {
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
|
||||
conn, err := net.DialTimeout("tcp", target, time.Duration(common.Timeout)*time.Second)
|
||||
if err != nil {
|
||||
return &ScanResult{
|
||||
return &plugins.Result{
|
||||
Success: false,
|
||||
Service: "telnet",
|
||||
Error: err,
|
||||
@ -198,7 +319,7 @@ func (p *TelnetPlugin) identifyService(ctx context.Context, info *common.HostInf
|
||||
buffer := make([]byte, 1024)
|
||||
n, err := conn.Read(buffer)
|
||||
if err != nil {
|
||||
return &ScanResult{
|
||||
return &plugins.Result{
|
||||
Success: false,
|
||||
Service: "telnet",
|
||||
Error: err,
|
||||
@ -206,7 +327,21 @@ func (p *TelnetPlugin) identifyService(ctx context.Context, info *common.HostInf
|
||||
}
|
||||
|
||||
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
|
||||
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))
|
||||
|
||||
return &ScanResult{
|
||||
return &plugins.Result{
|
||||
Success: true,
|
||||
Service: "telnet",
|
||||
Banner: banner,
|
||||
@ -227,8 +362,7 @@ func (p *TelnetPlugin) identifyService(ctx context.Context, info *common.HostInf
|
||||
}
|
||||
|
||||
func init() {
|
||||
// 使用高效注册方式:直接传递端口信息,避免实例创建
|
||||
RegisterPluginWithPorts("telnet", func() Plugin {
|
||||
plugins.RegisterWithPorts("telnet", func() plugins.Plugin {
|
||||
return NewTelnetPlugin()
|
||||
}, []int{23, 2323})
|
||||
}
|
Loading…
Reference in New Issue
Block a user