Files
fscan/plugins/services/mysql.go
T
ZacharyZcR 42092d8664 fix: 修复实机测试发现的可靠性问题 (v2.2.0-rc.1)
- UDP 插件在 -p 指定端口时被跳过
- Redis exploit 无超时保护 / readReply 吞没非超时错误
- service_probe 连接丢失后静默成功
- SNMP 探测成功但终端无输出
- SSH 爆破不稳定 (并发过高 + 自适应超时过短 + 限流误判)
- 进度条 isActive 竞态

新增 Config.ModuleTimeout() 协议级超时下限 (≥3s)
新增 ErrorTypeThrottle 限流错误分类
2026-06-14 22:14:02 +08:00

232 lines
5.4 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"
"io"
"log"
"net"
"strconv"
"strings"
"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("%s", 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 {
session.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, err := mySQLConnString(cred.Username, cred.Password, info, config.ModuleTimeout())
if err != nil {
return &AuthResult{
Success: false,
ErrorType: ErrorTypeAuth,
Error: err,
}
}
db, err := sql.Open("mysql", connStr)
if err != nil {
state.IncrementTCPFailedPacketCount()
return &AuthResult{
Success: false,
ErrorType: classifyMySQLErrorType(err),
Error: err,
}
}
db.SetConnMaxLifetime(config.ModuleTimeout())
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,
}
}
func mySQLConnString(username, password string, info *common.HostInfo, timeout time.Duration) (string, error) {
if strings.ContainsAny(username, ":@/") {
return "", fmt.Errorf("mysql username contains unsupported DSN delimiter")
}
cfg := mysql.NewConfig()
cfg.User = username
cfg.Passwd = password
cfg.Net = "tcp"
cfg.Addr = net.JoinHostPort(info.Host, strconv.Itoa(info.Port))
cfg.DBName = "information_schema"
cfg.Params = map[string]string{"charset": "utf8"}
cfg.Timeout = timeout
return cfg.FormatDSN(), 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 != "" {
session.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("%s", i18n.Tr("service_not_identified", "MySQL")),
}
}
func (p *MySQLPlugin) readMySQLBanner(conn net.Conn, config *common.Config) string {
_ = conn.SetReadDeadline(time.Now().Add(config.ModuleTimeout()))
header := make([]byte, 5)
if _, err := io.ReadFull(conn, header); err != nil {
return ""
}
if header[4] != 10 {
return ""
}
version := make([]byte, 0, 64)
var b [1]byte
for len(version) < 250 {
if _, err := io.ReadFull(conn, b[:]); err != nil {
return ""
}
if b[0] == 0 {
break
}
version = append(version, b[0])
}
if len(version) == 0 {
return ""
}
return fmt.Sprintf("MySQL %s", string(version))
}
func init() {
RegisterPluginWithPorts("mysql", func() Plugin {
return NewMySQLPlugin()
}, []int{3306, 3307, 33060})
}