mirror of
https://github.com/shadow1ng/fscan.git
synced 2025-09-14 05:56:46 +08:00
feat: 实现-nobr/-ne参数和服务识别功能
新增功能: - 添加-ne参数禁用利用攻击,实现弱密码检测和利用攻击的分离控制 - 实现-nobr模式下所有插件的服务识别功能,单数据包获取最大信息 - 修复端口插件匹配逻辑,只调用适配端口的插件提升扫描效率 插件改造: - MySQL: 通过握手包识别获取版本信息 - Redis: INFO命令识别版本,优先检测未授权访问 - SSH: Banner识别获取协议和服务器版本 - ActiveMQ: STOMP协议识别获取版本和认证状态 技术改进: - 新增端口匹配算法确保精准插件调用 - 完善i18n国际化支持所有新功能 - 统一服务识别接口设计便于扩展
This commit is contained in:
parent
ecc79aa9b8
commit
51735c4e25
@ -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"))
|
||||
|
||||
// ═════════════════════════════════════════════════
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 插件注册
|
||||
// =============================================================================
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 插件注册
|
||||
// =============================================================================
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 插件注册
|
||||
// =============================================================================
|
||||
|
Loading…
Reference in New Issue
Block a user