feat: 实现-nobr/-ne参数和服务识别功能

新增功能:
- 添加-ne参数禁用利用攻击,实现弱密码检测和利用攻击的分离控制
- 实现-nobr模式下所有插件的服务识别功能,单数据包获取最大信息
- 修复端口插件匹配逻辑,只调用适配端口的插件提升扫描效率

插件改造:
- MySQL: 通过握手包识别获取版本信息
- Redis: INFO命令识别版本,优先检测未授权访问
- SSH: Banner识别获取协议和服务器版本
- ActiveMQ: STOMP协议识别获取版本和认证状态

技术改进:
- 新增端口匹配算法确保精准插件调用
- 完善i18n国际化支持所有新功能
- 统一服务识别接口设计便于扩展
This commit is contained in:
ZacharyZcR 2025-08-08 03:32:00 +08:00
parent ecc79aa9b8
commit 51735c4e25
8 changed files with 417 additions and 22 deletions

View File

@ -54,6 +54,7 @@ var (
RedisWriteFile string
DisableBrute bool
DisableExploit bool
MaxRetries int
DisableSave bool
@ -198,6 +199,7 @@ func Flag(Info *HostInfo) {
// 暴力破解控制参数
// ═════════════════════════════════════════════════
flag.BoolVar(&DisableBrute, "nobr", false, i18n.GetText("flag_disable_brute"))
flag.BoolVar(&DisableExploit, "ne", false, i18n.GetText("flag_disable_exploit"))
flag.IntVar(&MaxRetries, "retry", 3, i18n.GetText("flag_max_retries"))
// ═════════════════════════════════════════════════

View File

@ -194,6 +194,10 @@ var FlagMessages = map[string]map[string]string{
LangZH: "禁用暴力破解",
LangEN: "Disable brute force",
},
"flag_disable_exploit": {
LangZH: "禁用利用攻击",
LangEN: "Disable exploit attacks",
},
"flag_max_retries": {
LangZH: "最大重试次数",
LangEN: "Maximum retries",

View File

@ -224,6 +224,10 @@ var PluginMessages = map[string]map[string]string{
LangZH: "MySQL弱密码扫描成功: %s [%s:%s]",
LangEN: "MySQL weak password scan successful: %s [%s:%s]",
},
"mysql_service_identified": {
LangZH: "MySQL服务识别成功: %s - %s",
LangEN: "MySQL service identified: %s - %s",
},
"mysql_connection_failed": {
LangZH: "MySQL连接失败: %v",
LangEN: "MySQL connection failed: %v",
@ -262,6 +266,10 @@ var PluginMessages = map[string]map[string]string{
LangZH: "Redis弱密码扫描成功: %s [%s]",
LangEN: "Redis weak password scan successful: %s [%s]",
},
"redis_service_identified": {
LangZH: "Redis服务识别成功: %s - %s",
LangEN: "Redis service identified: %s - %s",
},
"redis_connection_failed": {
LangZH: "Redis连接失败: %v",
LangEN: "Redis connection failed: %v",
@ -300,6 +308,10 @@ var PluginMessages = map[string]map[string]string{
LangZH: "SSH密码认证成功: %s [%s:%s]",
LangEN: "SSH password authentication successful: %s [%s:%s]",
},
"ssh_service_identified": {
LangZH: "SSH服务识别成功: %s - %s",
LangEN: "SSH service identified: %s - %s",
},
"ssh_connection_failed": {
LangZH: "SSH连接失败: %v",
LangEN: "SSH connection failed: %v",
@ -422,6 +434,10 @@ var PluginMessages = map[string]map[string]string{
LangZH: "ActiveMQ弱密码扫描成功(STOMP): %s [%s:%s]",
LangEN: "ActiveMQ weak password scan successful(STOMP): %s [%s:%s]",
},
"activemq_service_identified": {
LangZH: "ActiveMQ服务识别成功: %s (%s) - %s",
LangEN: "ActiveMQ service identified: %s (%s) - %s",
},
"activemq_stomp_auth_success": {
LangZH: "ActiveMQ STOMP认证成功: %s@%s:%d",
LangEN: "ActiveMQ STOMP authentication successful: %s@%s:%d",

View File

@ -109,7 +109,11 @@ func (s *ServiceScanStrategy) LogVulnerabilityPluginInfo(targets []common.HostIn
for _, pluginName := range allPlugins {
// 首先检查新插件架构
if factory := base.GlobalPluginRegistry.GetFactory(pluginName); factory != nil {
servicePlugins = append(servicePlugins, pluginName)
// 获取插件元数据检查端口匹配
metadata := factory.GetMetadata()
if s.isNewPluginApplicableToAnyPort(metadata, portSet, isCustomMode) {
servicePlugins = append(servicePlugins, pluginName)
}
continue
}
@ -159,3 +163,27 @@ func (s *ServiceScanStrategy) isPluginApplicableToAnyPort(plugin common.ScanPlug
return false
}
// isNewPluginApplicableToAnyPort 检查新插件架构的插件是否对任何端口适用
func (s *ServiceScanStrategy) isNewPluginApplicableToAnyPort(metadata *base.PluginMetadata, portSet map[int]bool, isCustomMode bool) bool {
// 自定义模式下运行所有明确指定的插件
if isCustomMode {
return true
}
// 无端口限制的插件适用于所有端口
if len(metadata.Ports) == 0 {
return true
}
// 有端口限制的插件:检查是否匹配任何目标端口
for port := range portSet {
for _, pluginPort := range metadata.Ports {
if pluginPort == port {
return true
}
}
}
return false
}

View File

@ -3,6 +3,9 @@ package activemq
import (
"context"
"fmt"
"net"
"strings"
"time"
"github.com/shadow1ng/fscan/common"
"github.com/shadow1ng/fscan/common/i18n"
@ -61,6 +64,13 @@ func NewActiveMQPlugin() *ActiveMQPlugin {
// Scan 执行ActiveMQ服务的完整安全扫描
// 重写基础扫描方法,集成弱密码检测和自动利用功能
func (p *ActiveMQPlugin) Scan(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) {
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
// 如果禁用暴力破解,则进行基础服务识别
if common.DisableBrute {
return p.performServiceIdentification(ctx, info)
}
// 调用基础服务插件进行弱密码扫描
result, err := p.ServicePlugin.Scan(ctx, info)
if err != nil || !result.Success {
@ -68,14 +78,13 @@ func (p *ActiveMQPlugin) Scan(ctx context.Context, info *common.HostInfo) (*base
}
// 记录成功的弱密码发现使用i18n根据端口显示不同协议
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
cred := result.Credentials[0]
// 专注于STOMP协议的成功消息
common.LogSuccess(i18n.GetText("activemq_stomp_scan_success", target, cred.Username, cred.Password))
// 自动利用功能(可通过-nobr参数禁用)
if result.Success && len(result.Credentials) > 0 && !common.DisableBrute {
// 自动利用功能(可通过-ne参数禁用)
if result.Success && len(result.Credentials) > 0 && !common.DisableExploit {
// 同步执行利用攻击,确保显示结果
p.autoExploit(context.Background(), info, result.Credentials[0])
}
@ -173,6 +182,89 @@ func (p *ActiveMQPlugin) generateCredentials() []*base.Credential {
return unique
}
// performServiceIdentification 执行服务识别(-nobr模式
func (p *ActiveMQPlugin) performServiceIdentification(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) {
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
// 尝试连接到ActiveMQ服务进行基础识别
conn, err := common.WrapperTcpWithTimeout("tcp", target, time.Duration(common.Timeout)*time.Second)
if err != nil {
return &base.ScanResult{
Success: false,
Error: err,
}, nil
}
defer conn.Close()
// 尝试STOMP协议识别
stompInfo, isActiveMQ := p.identifySTOMPService(conn)
if isActiveMQ {
// 记录服务识别成功
common.LogSuccess(i18n.GetText("activemq_service_identified", target, "STOMP", stompInfo))
return &base.ScanResult{
Success: true,
Service: "ActiveMQ",
Banner: stompInfo,
Extra: map[string]interface{}{
"service": "ActiveMQ",
"protocol": "STOMP",
"port": info.Ports,
"info": stompInfo,
},
}, nil
}
// 如果无法识别为ActiveMQ返回一般服务信息
return &base.ScanResult{
Success: false,
Error: fmt.Errorf("无法识别为ActiveMQ服务"),
}, nil
}
// identifySTOMPService 识别STOMP协议服务
func (p *ActiveMQPlugin) identifySTOMPService(conn net.Conn) (string, bool) {
// 发送STOMP CONNECT帧进行协议识别不提供凭据
connectFrame := "CONNECT\naccept-version:1.0,1.1,1.2\nhost:localhost\n\n\x00"
conn.SetWriteDeadline(time.Now().Add(time.Duration(common.Timeout) * time.Second))
if _, err := conn.Write([]byte(connectFrame)); err != nil {
return "", false
}
// 读取响应
conn.SetReadDeadline(time.Now().Add(time.Duration(common.Timeout) * time.Second))
response := make([]byte, 1024)
n, err := conn.Read(response)
if err != nil {
return "", false
}
responseStr := string(response[:n])
// 检查是否为STOMP协议响应
if strings.Contains(responseStr, "CONNECTED") {
// 提取版本信息
version := "unknown"
if strings.Contains(responseStr, "version:") {
lines := strings.Split(responseStr, "\n")
for _, line := range lines {
if strings.HasPrefix(line, "version:") {
version = strings.TrimPrefix(line, "version:")
break
}
}
}
return fmt.Sprintf("STOMP协议版本: %s", version), true
} else if strings.Contains(responseStr, "ERROR") {
// 即使返回错误但能识别STOMP协议格式
return "STOMP协议需要认证", true
}
return "", false
}
// GetServiceName 获取服务名称
func (p *ActiveMQPlugin) GetServiceName() string {
return "ActiveMQ"

View File

@ -3,6 +3,9 @@ package mysql
import (
"context"
"fmt"
"net"
"regexp"
"time"
"github.com/shadow1ng/fscan/common"
"github.com/shadow1ng/fscan/common/i18n"
@ -62,6 +65,13 @@ func NewMySQLPlugin() *MySQLPlugin {
// Scan 执行MySQL服务的完整安全扫描
// 重写基础扫描方法,集成弱密码检测和自动利用功能
func (p *MySQLPlugin) Scan(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) {
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
// 如果禁用暴力破解,则进行基础服务识别
if common.DisableBrute {
return p.performServiceIdentification(ctx, info)
}
// 调用基础服务插件进行弱密码扫描
result, err := p.ServicePlugin.Scan(ctx, info)
if err != nil || !result.Success {
@ -69,12 +79,11 @@ func (p *MySQLPlugin) Scan(ctx context.Context, info *common.HostInfo) (*base.Sc
}
// 记录成功的弱密码发现使用i18n
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
cred := result.Credentials[0]
common.LogSuccess(i18n.GetText("mysql_scan_success", target, cred.Username, cred.Password))
// 自动利用功能(可通过-nobr参数禁用)
if result.Success && len(result.Credentials) > 0 && !common.DisableBrute {
// 自动利用功能(可通过-ne参数禁用)
if result.Success && len(result.Credentials) > 0 && !common.DisableExploit {
// 异步执行利用攻击,避免阻塞扫描进程
go p.autoExploit(context.Background(), info, result.Credentials[0])
}
@ -127,6 +136,84 @@ func (p *MySQLPlugin) generateCredentials() []*base.Credential {
return base.GenerateCredentials(usernames, common.Passwords)
}
// performServiceIdentification 执行MySQL服务识别-nobr模式
func (p *MySQLPlugin) performServiceIdentification(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) {
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
// 尝试连接到MySQL服务获取握手包
conn, err := common.WrapperTcpWithTimeout("tcp", target, time.Duration(common.Timeout)*time.Second)
if err != nil {
return &base.ScanResult{
Success: false,
Error: err,
}, nil
}
defer conn.Close()
// 读取MySQL握手包
mysqlInfo, isMySQL := p.identifyMySQLService(conn)
if isMySQL {
// 记录服务识别成功
common.LogSuccess(i18n.GetText("mysql_service_identified", target, mysqlInfo))
return &base.ScanResult{
Success: true,
Service: "MySQL",
Banner: mysqlInfo,
Extra: map[string]interface{}{
"service": "MySQL",
"port": info.Ports,
"info": mysqlInfo,
},
}, nil
}
// 如果无法识别为MySQL返回失败
return &base.ScanResult{
Success: false,
Error: fmt.Errorf("无法识别为MySQL服务"),
}, nil
}
// identifyMySQLService 通过握手包识别MySQL服务
func (p *MySQLPlugin) identifyMySQLService(conn net.Conn) (string, bool) {
// 设置读取超时
conn.SetReadDeadline(time.Now().Add(time.Duration(common.Timeout) * time.Second))
// MySQL服务器在连接后会主动发送握手包
handshake := make([]byte, 1024)
n, err := conn.Read(handshake)
if err != nil || n < 10 {
return "", false
}
// 检查MySQL握手包格式
// MySQL握手包开始: 包长度(3字节) + 序号(1字节) + 协议版本(1字节)
if handshake[4] != 10 { // MySQL 协议版本通常是10
return "", false
}
// 提取版本字符串从第5字节开始到第一个0结束
versionStart := 5
versionEnd := versionStart
for versionEnd < n && handshake[versionEnd] != 0 {
versionEnd++
}
if versionEnd <= versionStart {
return "", false
}
versionStr := string(handshake[versionStart:versionEnd])
// 验证版本字符串是否包含MySQL标识
if len(versionStr) > 0 && (regexp.MustCompile(`\d+\.\d+`).MatchString(versionStr)) {
return fmt.Sprintf("MySQL版本: %s", versionStr), true
}
return "", false
}
// =============================================================================
// 插件注册
// =============================================================================

View File

@ -3,6 +3,9 @@ package redis
import (
"context"
"fmt"
"net"
"strings"
"time"
"github.com/shadow1ng/fscan/common"
"github.com/shadow1ng/fscan/common/i18n"
@ -67,19 +70,16 @@ func (p *RedisPlugin) Scan(ctx context.Context, info *common.HostInfo) (*base.Sc
common.LogSuccess(i18n.GetText("redis_unauth_success", target))
// 如果启用了利用功能,执行自动利用
if !common.DisableBrute { // 使用DisableBrute作为替代用户可以通过-nobr禁用利用功能
if !common.DisableExploit { // 使用DisableExploit控制利用功能
go p.autoExploit(context.Background(), info, nil) // 未授权访问不需要凭据
}
return unauthorizedResult, nil
}
// 如果未授权访问失败,尝试暴力破解
// 如果未授权访问失败,在-nobr模式下进行基础服务识别
if common.DisableBrute {
return &base.ScanResult{
Success: false,
Error: fmt.Errorf("暴力破解已禁用且无未授权访问"),
}, nil
return p.performServiceIdentification(ctx, info)
}
// 执行基础的暴力破解扫描
@ -92,7 +92,7 @@ func (p *RedisPlugin) Scan(ctx context.Context, info *common.HostInfo) (*base.Sc
target, result.Credentials[0].Password))
// 如果扫描成功并且启用了利用功能,执行自动利用
if result.Success && len(result.Credentials) > 0 && !common.DisableBrute {
if result.Success && len(result.Credentials) > 0 && !common.DisableExploit {
go p.autoExploit(context.Background(), info, result.Credentials[0])
}
@ -170,6 +170,87 @@ func (p *RedisPlugin) generateCredentials() []*base.Credential {
return base.GeneratePasswordOnlyCredentials(common.Passwords)
}
// performServiceIdentification 执行Redis服务识别-nobr模式
func (p *RedisPlugin) performServiceIdentification(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) {
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
// 尝试连接到Redis服务
conn, err := common.WrapperTcpWithTimeout("tcp", target, time.Duration(common.Timeout)*time.Second)
if err != nil {
return &base.ScanResult{
Success: false,
Error: err,
}, nil
}
defer conn.Close()
// 发送INFO命令获取Redis服务器信息
redisInfo, isRedis := p.identifyRedisService(conn)
if isRedis {
// 记录服务识别成功
common.LogSuccess(i18n.GetText("redis_service_identified", target, redisInfo))
return &base.ScanResult{
Success: true,
Service: "Redis",
Banner: redisInfo,
Extra: map[string]interface{}{
"service": "Redis",
"port": info.Ports,
"info": redisInfo,
},
}, nil
}
// 如果无法识别为Redis返回失败
return &base.ScanResult{
Success: false,
Error: fmt.Errorf("无法识别为Redis服务"),
}, nil
}
// identifyRedisService 通过INFO命令识别Redis服务
func (p *RedisPlugin) identifyRedisService(conn net.Conn) (string, bool) {
// 发送INFO命令
infoCmd := "INFO server\r\n"
conn.SetWriteDeadline(time.Now().Add(time.Duration(common.Timeout) * time.Second))
if _, err := conn.Write([]byte(infoCmd)); err != nil {
return "", false
}
// 读取响应
conn.SetReadDeadline(time.Now().Add(time.Duration(common.Timeout) * time.Second))
response := make([]byte, 2048)
n, err := conn.Read(response)
if err != nil || n < 10 {
return "", false
}
responseStr := string(response[:n])
// 检查是否为Redis响应
if strings.Contains(responseStr, "redis_version:") {
// 提取Redis版本信息
lines := strings.Split(responseStr, "\r\n")
for _, line := range lines {
if strings.HasPrefix(line, "redis_version:") {
version := strings.TrimPrefix(line, "redis_version:")
return fmt.Sprintf("Redis版本: %s", version), true
}
}
return "Redis服务版本未知", true
} else if strings.Contains(responseStr, "-NOAUTH") {
// 需要认证的Redis
return "Redis服务需要认证", true
} else if strings.Contains(responseStr, "+PONG") || strings.Contains(responseStr, "$") {
// 通过RESP协议特征识别
return "Redis服务", true
}
return "", false
}
// =============================================================================
// 插件注册
// =============================================================================

View File

@ -3,11 +3,16 @@ package ssh
import (
"context"
"fmt"
"io/ioutil"
"net"
"regexp"
"strings"
"time"
"github.com/shadow1ng/fscan/common"
"github.com/shadow1ng/fscan/common/i18n"
"github.com/shadow1ng/fscan/plugins/base"
"golang.org/x/crypto/ssh"
"io/ioutil"
)
// SSHConnector SSH连接器实现
@ -151,7 +156,7 @@ func (p *SSHPlugin) Scan(ctx context.Context, info *common.HostInfo) (*base.Scan
common.LogSuccess(i18n.GetText("ssh_key_auth_success", target, result.Credentials[0].Username))
// 自动利用功能 - 同步执行以确保及时显示结果
if !common.DisableBrute {
if !common.DisableExploit {
p.autoExploit(context.Background(), info, result.Credentials[0])
}
@ -161,10 +166,7 @@ func (p *SSHPlugin) Scan(ctx context.Context, info *common.HostInfo) (*base.Scan
// 执行基础的密码扫描
if common.DisableBrute {
return &base.ScanResult{
Success: false,
Error: fmt.Errorf("暴力破解已禁用"),
}, nil
return p.performServiceIdentification(ctx, info)
}
result, err := p.ServicePlugin.Scan(ctx, info)
@ -177,8 +179,8 @@ func (p *SSHPlugin) Scan(ctx context.Context, info *common.HostInfo) (*base.Scan
cred := result.Credentials[0]
common.LogSuccess(i18n.GetText("ssh_pwd_auth_success", target, cred.Username, cred.Password))
// 自动利用功能(可通过-nobr参数禁用)- 同步执行以确保及时显示结果
if result.Success && len(result.Credentials) > 0 && !common.DisableBrute {
// 自动利用功能(可通过-ne参数禁用)- 同步执行以确保及时显示结果
if result.Success && len(result.Credentials) > 0 && !common.DisableExploit {
p.autoExploit(context.Background(), info, result.Credentials[0])
}
@ -261,6 +263,89 @@ func (p *SSHPlugin) IsExploitSupported(method base.ExploitType) bool {
return p.exploiter.IsExploitSupported(method)
}
// performServiceIdentification 执行SSH服务识别-nobr模式
func (p *SSHPlugin) performServiceIdentification(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) {
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
// 尝试连接到SSH服务获取Banner
conn, err := common.WrapperTcpWithTimeout("tcp", target, time.Duration(common.Timeout)*time.Second)
if err != nil {
return &base.ScanResult{
Success: false,
Error: err,
}, nil
}
defer conn.Close()
// 读取SSH Banner
sshInfo, isSSH := p.identifySSHService(conn)
if isSSH {
// 记录服务识别成功
common.LogSuccess(i18n.GetText("ssh_service_identified", target, sshInfo))
return &base.ScanResult{
Success: true,
Service: "SSH",
Banner: sshInfo,
Extra: map[string]interface{}{
"service": "SSH",
"port": info.Ports,
"info": sshInfo,
},
}, nil
}
// 如果无法识别为SSH返回失败
return &base.ScanResult{
Success: false,
Error: fmt.Errorf("无法识别为SSH服务"),
}, nil
}
// identifySSHService 通过Banner识别SSH服务
func (p *SSHPlugin) identifySSHService(conn net.Conn) (string, bool) {
// 设置读取超时
conn.SetReadDeadline(time.Now().Add(time.Duration(common.Timeout) * time.Second))
// SSH服务器在连接后会发送Banner
banner := make([]byte, 512)
n, err := conn.Read(banner)
if err != nil || n < 4 {
return "", false
}
bannerStr := strings.TrimSpace(string(banner[:n]))
// 检查SSH协议标识
if strings.HasPrefix(bannerStr, "SSH-") {
// 提取SSH版本信息
parts := strings.Fields(bannerStr)
if len(parts) > 0 {
// 提取协议版本和服务器标识
versionPart := parts[0]
serverInfo := ""
if len(parts) > 1 {
serverInfo = strings.Join(parts[1:], " ")
}
// 使用正则表达式提取更详细信息
if matched := regexp.MustCompile(`SSH-([0-9.]+)-(.+)`).FindStringSubmatch(versionPart); len(matched) >= 3 {
protocolVersion := matched[1]
serverVersion := matched[2]
if serverInfo != "" {
return fmt.Sprintf("SSH %s (%s) %s", protocolVersion, serverVersion, serverInfo), true
}
return fmt.Sprintf("SSH %s (%s)", protocolVersion, serverVersion), true
}
return fmt.Sprintf("SSH服务: %s", bannerStr), true
}
return "SSH服务", true
}
return "", false
}
// =============================================================================
// 插件注册
// =============================================================================