mirror of
https://github.com/shadow1ng/fscan.git
synced 2025-09-14 05:56:46 +08:00

- 新增common/network.go统一网络操作包装器 - 重构MySQL/FTP/SSH/SNMP插件使用统一包装器 - 简化发包控制逻辑,避免重复代码 - 为未来代理、重试等功能扩展奠定基础
272 lines
6.5 KiB
Go
272 lines
6.5 KiB
Go
package services
|
||
|
||
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"
|
||
"github.com/shadow1ng/fscan/plugins"
|
||
)
|
||
|
||
// 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) *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("未发现弱密码"),
|
||
}
|
||
}
|
||
|
||
|
||
// 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
|
||
}
|
||
|
||
// 检查发包限制
|
||
if canSend, reason := common.CanSendPacket(); !canSend {
|
||
common.LogError(fmt.Sprintf("SSH连接 %s 受限: %s", target, reason))
|
||
return nil
|
||
}
|
||
|
||
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 {
|
||
// 计数TCP连接失败包
|
||
common.IncrementTCPFailedPacketCount()
|
||
return nil
|
||
}
|
||
// 计数TCP连接成功包
|
||
common.IncrementTCPSuccessPacketCount()
|
||
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)
|
||
|
||
// 使用统一TCP包装器获取SSH Banner
|
||
conn, err := common.SafeTCPDial(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() {
|
||
// 使用高效注册方式:直接传递端口信息,避免实例创建
|
||
RegisterPluginWithPorts("ssh", func() Plugin {
|
||
return NewSSHPlugin()
|
||
}, []int{22, 2222, 2200, 22222})
|
||
} |