Files
fscan/plugins/services/mysql.go
T
ZacharyZcR 5ebdbd3aa2 feat: 优化日志颜色方案,区分漏洞和普通信息
- 新增 LogVuln 级别(红色),用于漏洞和重要发现
- 密码爆破成功、未授权访问、POC漏洞等改用红色显示
- 普通信息(扫描统计等)改为白色
- Web指纹保持绿色
2026-01-17 13:21:43 +08:00

212 lines
4.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.
//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, config *common.Config, state *common.State) *ScanResult {
if config.DisableBrute {
return p.identifyService(info, config)
}
credentials := GenerateCredentials("mysql", config)
if len(credentials) == 0 {
return &ScanResult{
Success: false,
Service: "mysql",
Error: fmt.Errorf("没有可用的测试凭据"),
}
}
target := info.Target()
// 使用公共框架进行并发凭据测试
authFn := p.createAuthFunc(info, config, state)
testConfig := DefaultConcurrentTestConfig(config)
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()
// MySQL 使用 sql.DB,包装为 io.Closer
return &AuthResult{
Success: true,
Conn: &sqlDBWrapper{db},
ErrorType: ErrorTypeUnknown,
Error: nil,
}
}
// sqlDBWrapper 包装 sql.DB 以实现 io.Closer
type sqlDBWrapper struct {
*sql.DB
}
func (w *sqlDBWrapper) Close() error {
return w.DB.Close()
}
// 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(info *common.HostInfo, config *common.Config) *ScanResult {
target := info.Target()
conn, err := common.SafeTCPDial(target, config.Timeout)
if err != nil {
return &ScanResult{
Success: false,
Service: "mysql",
Error: err,
}
}
defer func() { _ = conn.Close() }()
if banner := p.readMySQLBanner(conn, 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("无法识别为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})
}