From 51735c4e258f5af7068f043bb7d70c17796e29ce Mon Sep 17 00:00:00 2001 From: ZacharyZcR Date: Fri, 8 Aug 2025 03:32:00 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0-nobr/-ne=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E5=92=8C=E6=9C=8D=E5=8A=A1=E8=AF=86=E5=88=AB=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增功能: - 添加-ne参数禁用利用攻击,实现弱密码检测和利用攻击的分离控制 - 实现-nobr模式下所有插件的服务识别功能,单数据包获取最大信息 - 修复端口插件匹配逻辑,只调用适配端口的插件提升扫描效率 插件改造: - MySQL: 通过握手包识别获取版本信息 - Redis: INFO命令识别版本,优先检测未授权访问 - SSH: Banner识别获取协议和服务器版本 - ActiveMQ: STOMP协议识别获取版本和认证状态 技术改进: - 新增端口匹配算法确保精准插件调用 - 完善i18n国际化支持所有新功能 - 统一服务识别接口设计便于扩展 --- Common/Flag.go | 2 + Common/i18n/messages/flag.go | 4 ++ Common/i18n/messages/plugins.go | 16 +++++ Core/ServiceScanner.go | 30 ++++++++- Plugins/services/activemq/plugin.go | 98 ++++++++++++++++++++++++++- Plugins/services/mysql/plugin.go | 93 ++++++++++++++++++++++++- Plugins/services/redis/plugin.go | 95 ++++++++++++++++++++++++-- Plugins/services/ssh/plugin.go | 101 +++++++++++++++++++++++++--- 8 files changed, 417 insertions(+), 22 deletions(-) diff --git a/Common/Flag.go b/Common/Flag.go index 393b8f5..df5e44b 100644 --- a/Common/Flag.go +++ b/Common/Flag.go @@ -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")) // ═════════════════════════════════════════════════ diff --git a/Common/i18n/messages/flag.go b/Common/i18n/messages/flag.go index 482b7a3..f72c0b5 100644 --- a/Common/i18n/messages/flag.go +++ b/Common/i18n/messages/flag.go @@ -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", diff --git a/Common/i18n/messages/plugins.go b/Common/i18n/messages/plugins.go index 62733b9..90602ba 100644 --- a/Common/i18n/messages/plugins.go +++ b/Common/i18n/messages/plugins.go @@ -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", diff --git a/Core/ServiceScanner.go b/Core/ServiceScanner.go index e068c31..9947e6d 100644 --- a/Core/ServiceScanner.go +++ b/Core/ServiceScanner.go @@ -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 +} diff --git a/Plugins/services/activemq/plugin.go b/Plugins/services/activemq/plugin.go index c62b239..1f22dec 100644 --- a/Plugins/services/activemq/plugin.go +++ b/Plugins/services/activemq/plugin.go @@ -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" diff --git a/Plugins/services/mysql/plugin.go b/Plugins/services/mysql/plugin.go index a5da3f9..74849b0 100644 --- a/Plugins/services/mysql/plugin.go +++ b/Plugins/services/mysql/plugin.go @@ -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 +} + // ============================================================================= // 插件注册 // ============================================================================= diff --git a/Plugins/services/redis/plugin.go b/Plugins/services/redis/plugin.go index 7876d95..32f752f 100644 --- a/Plugins/services/redis/plugin.go +++ b/Plugins/services/redis/plugin.go @@ -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 +} + // ============================================================================= // 插件注册 // ============================================================================= diff --git a/Plugins/services/ssh/plugin.go b/Plugins/services/ssh/plugin.go index efe50c6..4e3b49b 100644 --- a/Plugins/services/ssh/plugin.go +++ b/Plugins/services/ssh/plugin.go @@ -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 +} + // ============================================================================= // 插件注册 // =============================================================================