Files
fscan/plugins/services/mysql.go
T
ZacharyZcR 77827bef66 修复凭证测试器计数器、消除panic、补齐i18n
- credential_tester: testCredentialWithRetry返回ErrorType,修复网络错误计数器永久不递增的bug
- scanner: os.Exit(1)改为return,defer Cleanup可正常执行
- probe_parser: 5处panic改为error返回,调用链透传到init()
- common库: parsers/initialize/network/session共17处硬编码中文改用i18n
- services插件: 18个文件115处硬编码中文改用i18n
- locale: 补齐service/parser/network相关~25个中英文键
2026-05-18 04:55:20 +08:00

204 lines
4.8 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.
//go:build plugin_mysql || !plugin_selective
package services
import (
"context"
"database/sql"
"fmt"
"log"
"net"
"time"
"github.com/go-sql-driver/mysql"
"github.com/shadow1ng/fscan/common"
"github.com/shadow1ng/fscan/common/i18n"
"github.com/shadow1ng/fscan/plugins"
)
type nullWriter struct{}
func (nullWriter) Write(p []byte) (int, error) { return len(p), nil }
func init() {
// 禁用mysql驱动的错误日志(如unexpected EOF
_ = mysql.SetLogger(log.New(&nullWriter{}, "", 0))
}
// MySQLPlugin MySQL数据库扫描插件
type MySQLPlugin struct {
plugins.BasePlugin
}
func NewMySQLPlugin() *MySQLPlugin {
return &MySQLPlugin{
BasePlugin: plugins.NewBasePlugin("mysql"),
}
}
func (p *MySQLPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
config := session.Config
state := session.State
if config.DisableBrute {
return p.identifyService(ctx, info, session)
}
credentials := GenerateCredentials("mysql", config)
if len(credentials) == 0 {
return &ScanResult{
Success: false,
Service: "mysql",
Error: fmt.Errorf(i18n.GetText("service_no_credentials")),
}
}
target := info.Target()
// 使用公共框架进行并发凭据测试
authFn := p.createAuthFunc(info, config, state)
testConfig := DefaultConcurrentTestConfigWithTarget(config, info)
result := TestCredentialsConcurrently(ctx, credentials, authFn, "mysql", testConfig)
if result.Success {
common.LogVuln(i18n.Tr("mysql_credential", target, result.Username, result.Password))
}
return result
}
// createAuthFunc 创建MySQL认证函数
func (p *MySQLPlugin) createAuthFunc(info *common.HostInfo, config *common.Config, state *common.State) AuthFunc {
return func(ctx context.Context, cred Credential) *AuthResult {
return p.doMySQLAuth(ctx, info, cred, config, state)
}
}
// doMySQLAuth 执行MySQL认证
func (p *MySQLPlugin) doMySQLAuth(ctx context.Context, info *common.HostInfo, cred Credential, config *common.Config, state *common.State) *AuthResult {
connStr := fmt.Sprintf("%s:%s@tcp(%s:%d)/information_schema?charset=utf8&timeout=%ds",
cred.Username, cred.Password, info.Host, info.Port, int64(config.Timeout.Seconds()))
db, err := sql.Open("mysql", connStr)
if err != nil {
state.IncrementTCPFailedPacketCount()
return &AuthResult{
Success: false,
ErrorType: classifyMySQLErrorType(err),
Error: err,
}
}
db.SetConnMaxLifetime(config.Timeout)
db.SetMaxOpenConns(1)
db.SetMaxIdleConns(0)
err = db.PingContext(ctx)
if err != nil {
_ = db.Close()
state.IncrementTCPFailedPacketCount()
return &AuthResult{
Success: false,
ErrorType: classifyMySQLErrorType(err),
Error: err,
}
}
state.IncrementTCPSuccessPacketCount()
return &AuthResult{
Success: true,
Conn: &SQLDBWrapper{db},
ErrorType: ErrorTypeUnknown,
Error: nil,
}
}
// classifyMySQLErrorType MySQL错误分类
func classifyMySQLErrorType(err error) ErrorType {
if err == nil {
return ErrorTypeUnknown
}
mysqlAuthErrors := []string{
"access denied for user",
"unknown database",
"host is not allowed",
"authentication failed",
"permission denied",
"user does not exist",
}
mysqlNetworkErrors := append(CommonNetworkErrors,
"too many connections",
"can't connect to mysql server",
"lost connection to mysql server",
"mysql server has gone away",
)
return ClassifyError(err, mysqlAuthErrors, mysqlNetworkErrors)
}
func (p *MySQLPlugin) identifyService(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
target := info.Target()
conn, err := session.DialTCP(ctx, "tcp", target, session.Config.Timeout)
if err != nil {
return &ScanResult{
Success: false,
Service: "mysql",
Error: err,
}
}
defer func() { _ = conn.Close() }()
if banner := p.readMySQLBanner(conn, session.Config); banner != "" {
common.LogSuccess(i18n.Tr("mysql_service", target, banner))
return &ScanResult{
Type: plugins.ResultTypeService,
Success: true,
Service: "mysql",
Banner: banner,
}
}
return &ScanResult{
Success: false,
Service: "mysql",
Error: fmt.Errorf(i18n.Tr("service_not_identified", "MySQL")),
}
}
func (p *MySQLPlugin) readMySQLBanner(conn net.Conn, config *common.Config) string {
_ = conn.SetReadDeadline(time.Now().Add(config.Timeout))
handshake := make([]byte, 256)
n, err := conn.Read(handshake)
if err != nil || n < 10 {
return ""
}
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])
return fmt.Sprintf("MySQL %s", versionStr)
}
func init() {
RegisterPluginWithPorts("mysql", func() Plugin {
return NewMySQLPlugin()
}, []int{3306, 3307, 33060})
}