Files
fscan/plugins/services/mysql.go
T

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.Timeout)
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.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,
}
}
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.Timeout))
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})
}