Files
fscan/plugins/services/ssh.go
ZacharyZcR b86f39d2cc fix(credential): 修复凭据测试结果不一致的问题
问题原因:
1. 未知错误类型不重试,导致服务端限流时跳过正确密码
2. SSH 错误分类不够准确,某些临时错误未被识别

修复内容:
1. 未知错误改为可重试(可能是临时问题)
2. 增加 SSH 特有的网络错误识别(handshake failed, disconnect 等)
2026-01-20 13:29:00 +08:00

289 lines
7.3 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//go:build plugin_ssh || !plugin_selective
package services
import (
"context"
"fmt"
"io"
"net"
"os"
"regexp"
"strings"
"time"
"github.com/shadow1ng/fscan/common"
"github.com/shadow1ng/fscan/common/i18n"
"github.com/shadow1ng/fscan/plugins"
"golang.org/x/crypto/ssh"
)
// 预编译正则表达式
var sshBannerRegex = regexp.MustCompile(`SSH-([0-9.]+)-(.+)`)
// SSHPlugin SSH扫描插件
type SSHPlugin struct {
plugins.BasePlugin
}
// NewSSHPlugin 创建SSH插件
func NewSSHPlugin() *SSHPlugin {
return &SSHPlugin{
BasePlugin: plugins.NewBasePlugin("ssh"),
}
}
// Scan 执行SSH扫描
func (p *SSHPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
target := info.Target()
// 如果指定了SSH密钥优先使用密钥认证
if config.Credentials.SSHKeyPath != "" {
if result := p.scanWithKey(ctx, info, config, state); result != nil && result.Success {
common.LogVuln(i18n.Tr("ssh_key_auth_success", target, result.Username)) //nolint:govet
return result
}
}
// 如果禁用暴力破解,只做服务识别
if config.DisableBrute {
return p.identifyService(info, config, state)
}
// 生成测试凭据
credentials := GenerateCredentials("ssh", config)
if len(credentials) == 0 {
credentials = []Credential{
{Username: "root", Password: ""},
{Username: "root", Password: "root"},
{Username: "root", Password: "toor"},
{Username: "admin", Password: "admin"},
{Username: "admin", Password: ""},
}
}
// 使用公共框架进行并发凭据测试
authFn := p.createAuthFunc(info, config, state)
testConfig := DefaultConcurrentTestConfig(config)
result := TestCredentialsConcurrently(ctx, credentials, authFn, "ssh", testConfig)
// 记录成功
if result.Success {
common.LogVuln(i18n.Tr("ssh_pwd_auth_success", target, result.Username, result.Password)) //nolint:govet
}
return result
}
// createAuthFunc 创建SSH认证函数
func (p *SSHPlugin) createAuthFunc(info *common.HostInfo, config *common.Config, state *common.State) AuthFunc {
return func(ctx context.Context, cred Credential) *AuthResult {
return p.doSSHAuth(ctx, info, cred, config, state)
}
}
// doSSHAuth 执行SSH认证
func (p *SSHPlugin) doSSHAuth(ctx context.Context, info *common.HostInfo, cred Credential, config *common.Config, state *common.State) *AuthResult {
target := info.Target()
// 创建SSH配置
sshConfig := &ssh.ClientConfig{
User: cred.Username,
Timeout: config.Timeout,
//nolint:gosec // G106: 扫描工具需要忽略主机密钥验证以连接未知主机
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
// 设置认证方法
if len(cred.KeyData) > 0 {
signer, err := ssh.ParsePrivateKey(cred.KeyData)
if err != nil {
return &AuthResult{
Success: false,
ErrorType: ErrorTypeAuth,
Error: err,
}
}
sshConfig.Auth = []ssh.AuthMethod{ssh.PublicKeys(signer)}
} else {
sshConfig.Auth = []ssh.AuthMethod{ssh.Password(cred.Password)}
}
// 建立TCP连接
conn, err := common.WrapperTcpWithTimeout("tcp", target, config.Timeout)
if err != nil {
state.IncrementTCPFailedPacketCount()
return &AuthResult{
Success: false,
ErrorType: classifySSHErrorType(err),
Error: err,
}
}
// 在TCP连接上创建SSH客户端
sshConn, chans, reqs, err := ssh.NewClientConn(conn, target, sshConfig)
if err != nil {
_ = conn.Close()
state.IncrementTCPFailedPacketCount()
return &AuthResult{
Success: false,
ErrorType: classifySSHErrorType(err),
Error: err,
}
}
// 创建SSH客户端
client := ssh.NewClient(sshConn, chans, reqs)
state.IncrementTCPSuccessPacketCount()
return &AuthResult{
Success: true,
Conn: &sshClientWrapper{client},
ErrorType: ErrorTypeUnknown,
Error: nil,
}
}
// sshClientWrapper 包装 ssh.Client 以实现 io.Closer
type sshClientWrapper struct {
*ssh.Client
}
func (w *sshClientWrapper) Close() error {
return w.Client.Close()
}
// classifySSHErrorType SSH错误分类
func classifySSHErrorType(err error) ErrorType {
if err == nil {
return ErrorTypeUnknown
}
// SSH 特有的认证错误(密码错误)
sshAuthErrors := append(CommonAuthErrors,
"unable to authenticate",
"no supported methods remain",
)
// SSH 特有的网络/临时错误(需要重试)
sshNetworkErrors := append(CommonNetworkErrors,
"handshake failed", // 握手失败,可能是服务端限流
"ssh: disconnect", // SSH 主动断开
"connection closed", // 连接被关闭
"max startups", // SSH MaxStartups 限制
"too many authentication", // 认证次数过多
)
return ClassifyError(err, sshAuthErrors, sshNetworkErrors)
}
// scanWithKey 使用SSH私钥扫描
func (p *SSHPlugin) scanWithKey(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
keyData, err := os.ReadFile(config.Credentials.SSHKeyPath)
if err != nil {
common.LogError(i18n.Tr("ssh_key_read_failed", err)) //nolint:govet
return nil
}
usernames := config.Credentials.Userdict["ssh"]
if len(usernames) == 0 {
usernames = []string{"root", "admin", "ubuntu", "centos", "user", "git", "www-data"}
}
// 逐个测试用户名
for _, username := range usernames {
select {
case <-ctx.Done():
return nil
default:
}
cred := Credential{
Username: username,
KeyData: keyData,
}
result := p.doSSHAuth(ctx, info, cred, config, state)
if result.Success {
if result.Conn != nil {
_ = result.Conn.Close()
}
return &ScanResult{
Type: plugins.ResultTypeCredential,
Success: true,
Service: "ssh",
Username: username,
}
}
}
return nil
}
// identifyService 服务识别
func (p *SSHPlugin) identifyService(info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
target := info.Target()
conn, err := common.SafeTCPDial(target, config.Timeout)
if err != nil {
state.IncrementTCPFailedPacketCount()
return &ScanResult{
Success: false,
Service: "ssh",
Error: err,
}
}
defer func() { _ = conn.Close() }()
if banner := p.readSSHBanner(conn, config); banner != "" {
state.IncrementTCPSuccessPacketCount()
common.LogSuccess(i18n.Tr("ssh_service_identified", target, banner)) //nolint:govet
return &ScanResult{
Type: plugins.ResultTypeService,
Success: true,
Service: "ssh",
Banner: banner,
}
}
state.IncrementTCPFailedPacketCount()
return &ScanResult{
Success: false,
Service: "ssh",
Error: fmt.Errorf("无法识别为SSH服务"),
}
}
// readSSHBanner 读取SSH服务器Banner
func (p *SSHPlugin) readSSHBanner(conn net.Conn, config *common.Config) string {
_ = conn.SetReadDeadline(time.Now().Add(config.Timeout))
banner := make([]byte, 256)
n, err := conn.Read(banner)
if err != nil || n < 4 {
return ""
}
bannerStr := strings.TrimSpace(string(banner[:n]))
if strings.HasPrefix(bannerStr, "SSH-") {
if matched := sshBannerRegex.FindStringSubmatch(bannerStr); len(matched) >= 3 {
return fmt.Sprintf("SSH %s (%s)", matched[1], matched[2])
}
return fmt.Sprintf("SSH服务: %s", bannerStr)
}
return ""
}
// init 自动注册插件
func init() {
RegisterPluginWithPorts("ssh", func() Plugin {
return NewSSHPlugin()
}, []int{22, 2222, 2200, 22222})
}
// 确保实现了 io.Closer 接口
var _ io.Closer = (*sshClientWrapper)(nil)