mirror of
https://github.com/shadow1ng/fscan.git
synced 2025-09-14 14:06:44 +08:00

将所有服务插件移动到plugins/services/目录下,使目录结构更加清晰直观: • 创建plugins/services/目录统一管理服务扫描插件 • 添加init.go提供类型别名和函数导出 • 更新main.go导入路径 • 所有20个服务插件功能验证正常 新的目录结构更便于插件管理和维护。
212 lines
4.9 KiB
Go
212 lines
4.9 KiB
Go
package services
|
||
|
||
import (
|
||
"context"
|
||
"database/sql"
|
||
"fmt"
|
||
"net"
|
||
"regexp"
|
||
"time"
|
||
|
||
_ "github.com/go-sql-driver/mysql"
|
||
"github.com/shadow1ng/fscan/common"
|
||
"github.com/shadow1ng/fscan/common/i18n"
|
||
)
|
||
|
||
// MySQLPlugin MySQL数据库弱密码扫描插件 - 单文件实现
|
||
type MySQLPlugin struct {
|
||
name string
|
||
ports []int
|
||
}
|
||
|
||
// NewMySQLPlugin 创建MySQL插件
|
||
func NewMySQLPlugin() *MySQLPlugin {
|
||
return &MySQLPlugin{
|
||
name: "mysql",
|
||
ports: []int{3306, 3307, 33060},
|
||
}
|
||
}
|
||
|
||
// GetName 实现Plugin接口
|
||
func (p *MySQLPlugin) GetName() string {
|
||
return p.name
|
||
}
|
||
|
||
// GetPorts 实现Plugin接口
|
||
func (p *MySQLPlugin) GetPorts() []int {
|
||
return p.ports
|
||
}
|
||
|
||
// Scan 执行MySQL扫描 - 核心功能实现
|
||
func (p *MySQLPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanResult {
|
||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||
|
||
// 如果禁用暴力破解,只做服务识别
|
||
if common.DisableBrute {
|
||
return p.identifyService(info)
|
||
}
|
||
|
||
// 生成测试凭据
|
||
credentials := GenerateCredentials("mysql")
|
||
if len(credentials) == 0 {
|
||
return &ScanResult{
|
||
Success: false,
|
||
Service: "mysql",
|
||
Error: fmt.Errorf("没有可用的测试凭据"),
|
||
}
|
||
}
|
||
|
||
// 逐个测试凭据
|
||
for _, cred := range credentials {
|
||
// 检查Context是否被取消
|
||
select {
|
||
case <-ctx.Done():
|
||
return &ScanResult{
|
||
Success: false,
|
||
Service: "mysql",
|
||
Error: ctx.Err(),
|
||
}
|
||
default:
|
||
}
|
||
|
||
// 测试凭据
|
||
if p.testCredential(ctx, info, cred) {
|
||
// 弱密码发现成功
|
||
common.LogSuccess(i18n.GetText("mysql_scan_success", target, cred.Username, cred.Password))
|
||
|
||
return &ScanResult{
|
||
Success: true,
|
||
Service: "mysql",
|
||
Username: cred.Username,
|
||
Password: cred.Password,
|
||
}
|
||
}
|
||
}
|
||
|
||
// 所有凭据都失败
|
||
return &ScanResult{
|
||
Success: false,
|
||
Service: "mysql",
|
||
Error: fmt.Errorf("未发现弱密码"),
|
||
}
|
||
}
|
||
|
||
// testCredential 测试单个凭据 - 核心认证逻辑
|
||
func (p *MySQLPlugin) testCredential(ctx context.Context, info *common.HostInfo, cred Credential) bool {
|
||
// 构建连接字符串
|
||
connStr := p.buildConnectionString(info.Host, info.Ports, cred.Username, cred.Password)
|
||
|
||
// 创建数据库连接
|
||
db, err := sql.Open("mysql", connStr)
|
||
if err != nil {
|
||
return false
|
||
}
|
||
defer db.Close()
|
||
|
||
// 设置连接超时
|
||
db.SetConnMaxLifetime(time.Duration(common.Timeout) * time.Second)
|
||
db.SetMaxOpenConns(1)
|
||
db.SetMaxIdleConns(0)
|
||
|
||
// 测试连接 - 使用Context超时控制
|
||
err = db.PingContext(ctx)
|
||
return err == nil
|
||
}
|
||
|
||
// buildConnectionString 构建MySQL连接字符串
|
||
func (p *MySQLPlugin) buildConnectionString(host, port, username, password string) string {
|
||
// 支持SOCKS代理
|
||
if common.Socks5Proxy != "" {
|
||
// 如果使用代理,需要注册自定义拨号器
|
||
p.registerProxyDialer()
|
||
return fmt.Sprintf("%s:%s@tcp-proxy(%s:%s)/mysql?charset=utf8&timeout=%ds",
|
||
username, password, host, port, common.Timeout)
|
||
}
|
||
|
||
// 标准TCP连接
|
||
return fmt.Sprintf("%s:%s@tcp(%s:%s)/mysql?charset=utf8&timeout=%ds",
|
||
username, password, host, port, common.Timeout)
|
||
}
|
||
|
||
// identifyService 服务识别 - 不进行暴力破解时的功能
|
||
func (p *MySQLPlugin) identifyService(info *common.HostInfo) *ScanResult {
|
||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||
|
||
// 尝试连接获取握手包
|
||
conn, err := common.WrapperTcpWithTimeout("tcp", target, time.Duration(common.Timeout)*time.Second)
|
||
if err != nil {
|
||
return &ScanResult{
|
||
Success: false,
|
||
Service: "mysql",
|
||
Error: err,
|
||
}
|
||
}
|
||
defer conn.Close()
|
||
|
||
// 读取MySQL握手包
|
||
if banner := p.readMySQLBanner(conn); banner != "" {
|
||
common.LogSuccess(i18n.GetText("mysql_service_identified", target, banner))
|
||
return &ScanResult{
|
||
Success: true,
|
||
Service: "mysql",
|
||
Banner: banner,
|
||
}
|
||
}
|
||
|
||
return &ScanResult{
|
||
Success: false,
|
||
Service: "mysql",
|
||
Error: fmt.Errorf("无法识别为MySQL服务"),
|
||
}
|
||
}
|
||
|
||
// readMySQLBanner 读取MySQL服务器握手包
|
||
func (p *MySQLPlugin) readMySQLBanner(conn net.Conn) string {
|
||
// 设置读取超时
|
||
conn.SetReadDeadline(time.Now().Add(time.Duration(common.Timeout) * time.Second))
|
||
|
||
// 读取握手包
|
||
handshake := make([]byte, 512)
|
||
n, err := conn.Read(handshake)
|
||
if err != nil || n < 10 {
|
||
return ""
|
||
}
|
||
|
||
// 检查MySQL协议版本(通常是10)
|
||
if handshake[4] != 10 {
|
||
return ""
|
||
}
|
||
|
||
// 提取版本字符串
|
||
versionStart := 5
|
||
versionEnd := versionStart
|
||
for versionEnd < n && handshake[versionEnd] != 0 {
|
||
versionEnd++
|
||
}
|
||
|
||
if versionEnd <= versionStart {
|
||
return ""
|
||
}
|
||
|
||
versionStr := string(handshake[versionStart:versionEnd])
|
||
|
||
// 验证版本字符串格式
|
||
if regexp.MustCompile(`\d+\.\d+`).MatchString(versionStr) {
|
||
return fmt.Sprintf("MySQL %s", versionStr)
|
||
}
|
||
|
||
return ""
|
||
}
|
||
|
||
// registerProxyDialer 注册SOCKS代理拨号器
|
||
func (p *MySQLPlugin) registerProxyDialer() {
|
||
// TODO: 实现代理拨号器注册
|
||
// 这里简化处理,实际需要注册到MySQL驱动
|
||
}
|
||
|
||
// init 自动注册插件
|
||
func init() {
|
||
RegisterPlugin("mysql", func() Plugin {
|
||
return NewMySQLPlugin()
|
||
})
|
||
} |