feat: 完善插件系统i18n国际化支持

- 修复重复输出问题:适配器层改为debug输出,避免与插件层重复
- 修复格式化错误:修正SaveExploitResult中的端口格式化问题
- 新增利用方法名称i18n:添加GetExploitMethodName函数支持方法名本地化
- 扩展i18n消息模板:新增利用方法执行、MySQL/Redis专用消息模板
- 完善exploiter国际化:所有利用方法和结果消息支持中英文切换
- 优化用户体验:利用方法显示从"information_gathering"变为"信息收集"
This commit is contained in:
ZacharyZcR 2025-08-07 12:30:17 +08:00
parent 43f210ffc6
commit b346e6bdc1
7 changed files with 254 additions and 63 deletions

View File

@ -188,6 +188,19 @@ func GetText(key string, args ...interface{}) string {
return globalManager.GetText(key, args...)
}
// GetExploitMethodName 获取利用方法的本地化名称
func GetExploitMethodName(methodName string) string {
// 尝试获取本地化的方法名称
key := fmt.Sprintf("exploit_method_name_%s", methodName)
localizedName := globalManager.GetText(key)
// 如果没有找到对应的本地化名称,返回原始名称
if localizedName == key {
return methodName
}
return localizedName
}
// =============================================================================================
// 已删除的死代码函数(未使用):
// GetGlobalManager, GetLanguage, AddMessage, GetTextWithLanguage,

View File

@ -39,6 +39,164 @@ var PluginMessages = map[string]map[string]string{
LangEN: "%s exploitation failed: %v",
},
// ========================= 通用成功消息模板 =========================
"plugin_login_success": {
LangZH: "%s弱密码: %s [%s:%s]",
LangEN: "%s weak password: %s [%s:%s]",
},
"plugin_login_success_passwd_only": {
LangZH: "%s弱密码: %s [%s]",
LangEN: "%s weak password: %s [%s]",
},
"plugin_unauthorized_access": {
LangZH: "%s未授权访问: %s",
LangEN: "%s unauthorized access: %s",
},
// ========================= 利用(Exploit)消息模板 =========================
"exploit_weak_password_success": {
LangZH: "%s %s 弱密码利用成功",
LangEN: "%s %s weak password exploit successful",
},
"exploit_unauthorized_success": {
LangZH: "%s %s 未授权访问利用成功",
LangEN: "%s %s unauthorized access exploit successful",
},
"exploit_command_exec_success": {
LangZH: "%s %s 命令执行利用成功",
LangEN: "%s %s command execution exploit successful",
},
"exploit_file_write_success": {
LangZH: "%s %s 文件写入利用成功",
LangEN: "%s %s file write exploit successful",
},
"exploit_sql_injection_success": {
LangZH: "%s %s SQL注入利用成功",
LangEN: "%s %s SQL injection exploit successful",
},
"exploit_data_extraction_success": {
LangZH: "%s %s %s 利用成功",
LangEN: "%s %s %s exploit successful",
},
"exploit_generic_success": {
LangZH: "%s %s %s 利用成功",
LangEN: "%s %s %s exploit successful",
},
"exploit_with_output": {
LangZH: " 输出: %s",
LangEN: " output: %s",
},
"exploit_files_created": {
LangZH: "创建/修改的文件: %v",
LangEN: "Files created/modified: %v",
},
"exploit_shell_obtained": {
LangZH: "获得Shell: %s %s:%d 用户:%s",
LangEN: "Shell obtained: %s %s:%d user:%s",
},
// ========================= 利用方法执行消息 =========================
"exploit_method_trying": {
LangZH: "尝试利用方法: %s",
LangEN: "Trying exploit method: %s",
},
"exploit_method_success": {
LangZH: "利用方法 %s 执行成功",
LangEN: "Exploit method %s executed successfully",
},
"exploit_method_failed": {
LangZH: "利用方法 %s 执行失败: %v",
LangEN: "Exploit method %s failed: %v",
},
"exploit_method_condition_not_met": {
LangZH: "利用方法 %s 前置条件不满足,跳过",
LangEN: "Exploit method %s prerequisites not met, skipping",
},
"exploit_all_methods_failed": {
LangZH: "所有利用方法都失败",
LangEN: "All exploit methods failed",
},
// ========================= MySQL利用方法消息 =========================
"mysql_version_info": {
LangZH: "MySQL版本: %s",
LangEN: "MySQL version: %s",
},
"mysql_current_user": {
LangZH: "当前用户: %s",
LangEN: "Current user: %s",
},
"mysql_current_database": {
LangZH: "当前数据库: %s",
LangEN: "Current database: %s",
},
"mysql_databases_found": {
LangZH: "发现数据库: %s",
LangEN: "Databases found: %s",
},
"mysql_tables_found": {
LangZH: "发现表: %v",
LangEN: "Tables found: %v",
},
"mysql_user_privileges": {
LangZH: "用户权限: %s",
LangEN: "User privileges: %s",
},
"mysql_file_privilege_detected": {
LangZH: "检测到FILE权限可能支持文件操作",
LangEN: "FILE privilege detected, file operations may be supported",
},
"mysql_file_read_success": {
LangZH: "读取文件 %s:\n%s",
LangEN: "File %s read:\n%s",
},
"mysql_file_write_success": {
LangZH: "成功写入文件: %s",
LangEN: "File written successfully: %s",
},
"mysql_no_file_privilege": {
LangZH: "无法读取任何文件可能没有FILE权限",
LangEN: "Cannot read any files, may lack FILE privilege",
},
// ========================= Redis利用方法消息 =========================
"redis_server_info": {
LangZH: "Redis服务器信息: %s",
LangEN: "Redis server info: %s",
},
"redis_config_info": {
LangZH: "Redis配置信息: %s",
LangEN: "Redis config info: %s",
},
"redis_keys_found": {
LangZH: "发现Redis键: %v",
LangEN: "Redis keys found: %v",
},
"redis_backup_created": {
LangZH: "Redis备份创建成功: %s",
LangEN: "Redis backup created: %s",
},
"redis_cron_job_written": {
LangZH: "Cron任务写入成功: %s",
LangEN: "Cron job written successfully: %s",
},
"redis_ssh_key_written": {
LangZH: "SSH密钥写入成功: %s",
LangEN: "SSH key written successfully: %s",
},
"redis_webshell_written": {
LangZH: "Webshell写入成功: %s",
LangEN: "Webshell written successfully: %s",
},
"redis_no_keys_found": {
LangZH: "未发现任何Redis键",
LangEN: "No Redis keys found",
},
"redis_write_failed": {
LangZH: "Redis写入操作失败",
LangEN: "Redis write operation failed",
},
// ========================= 插件架构消息 =========================
"plugin_new_arch_trying": {
LangZH: "尝试使用新插件架构: %s",
@ -181,21 +339,47 @@ var PluginMessages = map[string]map[string]string{
LangEN: "%s vulnerability found: %s - %s",
},
// ========================= 利用方法名称i18n =========================
"exploit_method_name_information_gathering": {
LangZH: "信息收集",
LangEN: "information_gathering",
},
"exploit_method_name_database_enumeration": {
LangZH: "数据库枚举",
LangEN: "database_enumeration",
},
"exploit_method_name_privilege_check": {
LangZH: "权限检查",
LangEN: "privilege_check",
},
"exploit_method_name_file_read": {
LangZH: "文件读取",
LangEN: "file_read",
},
"exploit_method_name_file_write": {
LangZH: "文件写入",
LangEN: "file_write",
},
"exploit_method_name_arbitrary_file_write": {
LangZH: "任意文件写入",
LangEN: "arbitrary_file_write",
},
"exploit_method_name_ssh_key_write": {
LangZH: "SSH密钥写入",
LangEN: "ssh_key_write",
},
"exploit_method_name_crontab_injection": {
LangZH: "定时任务注入",
LangEN: "crontab_injection",
},
"exploit_method_name_data_extraction": {
LangZH: "数据提取",
LangEN: "data_extraction",
},
// ========================= 利用结果消息 =========================
"exploit_result_saved": {
LangZH: "利用结果已保存: %s",
LangEN: "Exploitation result saved: %s",
},
"exploit_method_trying": {
LangZH: "尝试利用方法: %s",
LangEN: "Trying exploitation method: %s",
},
"exploit_method_success": {
LangZH: "利用方法成功: %s",
LangEN: "Exploitation method successful: %s",
},
"exploit_method_failed": {
LangZH: "利用方法失败: %s - %v",
LangEN: "Exploitation method failed: %s - %v",
},
}

View File

@ -57,21 +57,9 @@ func (a *PluginAdapter) AdaptPluginScan(pluginName string, info *common.HostInfo
if result != nil && result.Success {
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
if len(result.Credentials) > 0 {
// 有凭据的情况
cred := result.Credentials[0]
if cred.Username != "" {
common.LogSuccess(fmt.Sprintf("%s successful login: %s [%s:%s]",
pluginName, target, cred.Username, cred.Password))
} else {
// 仅密码的情况如Redis
common.LogSuccess(fmt.Sprintf("%s successful login: %s [%s]",
pluginName, target, cred.Password))
}
} else {
// 未授权访问的情况
common.LogSuccess(fmt.Sprintf("%s unauthorized access: %s", pluginName, target))
}
// 适配器层不输出扫描结果,由插件层负责输出
// 这避免了重复输出的问题
common.LogDebug(fmt.Sprintf("插件 %s 适配成功: %s", pluginName, target))
// 保存结果到文件
a.saveResult(info, result, pluginName)

View File

@ -4,6 +4,7 @@ import (
"context"
"fmt"
"github.com/shadow1ng/fscan/common"
"github.com/shadow1ng/fscan/common/i18n"
"sort"
)
@ -56,28 +57,28 @@ func (e *BaseExploiter) Exploit(ctx context.Context, info *common.HostInfo, cred
for _, method := range e.exploitMethods {
// 检查前置条件
if !e.checkConditions(method.Conditions, info, creds) {
common.LogDebug(fmt.Sprintf("利用方法 %s 前置条件不满足,跳过", method.Name))
common.LogDebug(i18n.GetText("exploit_method_condition_not_met", method.Name))
continue
}
common.LogDebug(fmt.Sprintf("尝试利用方法: %s", method.Name))
common.LogDebug(i18n.GetText("exploit_method_trying", i18n.GetExploitMethodName(method.Name)))
// 执行利用
result, err := method.Handler(ctx, info, creds)
if err != nil {
common.LogError(fmt.Sprintf("利用方法 %s 执行失败: %v", method.Name, err))
common.LogError(i18n.GetText("exploit_method_failed", method.Name, err))
continue
}
if result != nil && result.Success {
common.LogSuccess(fmt.Sprintf("利用方法 %s 执行成功", method.Name))
common.LogSuccess(i18n.GetText("exploit_method_success", i18n.GetExploitMethodName(method.Name)))
result.Type = method.Type
result.Method = method.Name
return result, nil
}
}
return nil, fmt.Errorf("所有利用方法都失败")
return nil, fmt.Errorf(i18n.GetText("exploit_all_methods_failed"))
}
// checkConditions 检查前置条件
@ -167,38 +168,40 @@ func SaveExploitResult(info *common.HostInfo, result *ExploitResult, pluginName
return
}
target := fmt.Sprintf("%s:%d", info.Host, info.Ports)
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
var message string
switch result.Type {
case ExploitWeakPassword:
message = fmt.Sprintf("%s %s 弱密码利用成功", pluginName, target)
message = i18n.GetText("exploit_weak_password_success", pluginName, target)
case ExploitUnauthorized:
message = fmt.Sprintf("%s %s 未授权访问利用成功", pluginName, target)
message = i18n.GetText("exploit_unauthorized_success", pluginName, target)
case ExploitCommandExec:
message = fmt.Sprintf("%s %s 命令执行利用成功", pluginName, target)
message = i18n.GetText("exploit_command_exec_success", pluginName, target)
case ExploitFileWrite:
message = fmt.Sprintf("%s %s 文件写入利用成功", pluginName, target)
message = i18n.GetText("exploit_file_write_success", pluginName, target)
case ExploitSQLInjection:
message = fmt.Sprintf("%s %s SQL注入利用成功", pluginName, target)
message = i18n.GetText("exploit_sql_injection_success", pluginName, target)
case ExploitDataExtraction:
message = i18n.GetText("exploit_data_extraction_success", pluginName, target, i18n.GetExploitMethodName(result.Method))
default:
message = fmt.Sprintf("%s %s %s 利用成功", pluginName, target, result.Type)
message = i18n.GetText("exploit_generic_success", pluginName, target, i18n.GetExploitMethodName(result.Method))
}
if result.Output != "" {
message += fmt.Sprintf(" 输出: %s", result.Output)
message += i18n.GetText("exploit_with_output", result.Output)
}
common.LogSuccess(message)
// 保存文件信息
if len(result.Files) > 0 {
common.LogSuccess(fmt.Sprintf("创建/修改的文件: %v", result.Files))
common.LogSuccess(i18n.GetText("exploit_files_created", result.Files))
}
// 保存Shell信息
if result.Shell != nil {
common.LogSuccess(fmt.Sprintf("获得Shell: %s %s:%d 用户:%s",
common.LogSuccess(i18n.GetText("exploit_shell_obtained",
result.Shell.Type, result.Shell.Host, result.Shell.Port, result.Shell.User))
}
}

View File

@ -5,6 +5,7 @@ import (
"database/sql"
"fmt"
"github.com/shadow1ng/fscan/common"
"github.com/shadow1ng/fscan/common/i18n"
"github.com/shadow1ng/fscan/plugins/base"
"strconv"
"strings"
@ -90,21 +91,21 @@ func (e *MySQLExploiter) exploitInformationGathering(ctx context.Context, info *
// 获取版本信息
version, err := e.getVersion(ctx, db)
if err == nil {
base.AddOutputToResult(result, fmt.Sprintf("MySQL版本: %s", version))
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, fmt.Sprintf("当前用户: %s", user))
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, fmt.Sprintf("当前数据库: %s", database))
base.AddOutputToResult(result, i18n.GetText("mysql_current_database", database))
result.Extra["current_database"] = database
}
@ -124,14 +125,14 @@ func (e *MySQLExploiter) exploitDatabaseEnumeration(ctx context.Context, info *c
// 枚举数据库
databases, err := e.enumerateDatabases(ctx, db)
if err == nil && len(databases) > 0 {
base.AddOutputToResult(result, fmt.Sprintf("发现数据库: %s", strings.Join(databases, ", ")))
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, fmt.Sprintf("发现表: %v", tables))
base.AddOutputToResult(result, i18n.GetText("mysql_tables_found", tables))
result.Extra["tables"] = tables
}
@ -151,7 +152,7 @@ func (e *MySQLExploiter) exploitPrivilegeCheck(ctx context.Context, info *common
// 检查用户权限
privileges, err := e.getUserPrivileges(ctx, db)
if err == nil && len(privileges) > 0 {
base.AddOutputToResult(result, fmt.Sprintf("用户权限: %s", strings.Join(privileges, ", ")))
base.AddOutputToResult(result, i18n.GetText("mysql_user_privileges", strings.Join(privileges, ", ")))
result.Extra["privileges"] = privileges
}
@ -159,7 +160,7 @@ func (e *MySQLExploiter) exploitPrivilegeCheck(ctx context.Context, info *common
hasFilePriv := e.hasFilePrivilege(privileges)
result.Extra["has_file_privilege"] = hasFilePriv
if hasFilePriv {
base.AddOutputToResult(result, "检测到FILE权限可能支持文件操作")
base.AddOutputToResult(result, i18n.GetText("mysql_file_privilege_detected"))
}
return result, nil
@ -187,14 +188,14 @@ func (e *MySQLExploiter) exploitFileRead(ctx context.Context, info *common.HostI
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))
base.AddOutputToResult(result, i18n.GetText("mysql_file_read_success", file, content))
hasRead = true
}
}
if !hasRead {
return base.CreateFailedExploitResult(base.ExploitDataExtraction, "file_read",
fmt.Errorf("无法读取任何文件可能没有FILE权限")), nil
fmt.Errorf(i18n.GetText("mysql_no_file_privilege"))), nil
}
return result, nil
@ -219,7 +220,7 @@ func (e *MySQLExploiter) exploitFileWrite(ctx context.Context, info *common.Host
return base.CreateFailedExploitResult(base.ExploitFileWrite, "file_write", err), nil
}
base.AddOutputToResult(result, fmt.Sprintf("成功写入文件: %s", testFile))
base.AddOutputToResult(result, i18n.GetText("mysql_file_write_success", testFile))
base.AddFileToResult(result, testFile)
return result, nil

View File

@ -5,6 +5,7 @@ import (
"fmt"
"github.com/shadow1ng/fscan/common"
"github.com/shadow1ng/fscan/common/i18n"
"github.com/shadow1ng/fscan/plugins/base"
)
@ -67,10 +68,10 @@ func (p *MySQLPlugin) Scan(ctx context.Context, info *common.HostInfo) (*base.Sc
return result, err // 扫描失败,直接返回
}
// 记录成功的弱密码发现(支持i18n
// 记录成功的弱密码发现(使用i18n
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
cred := result.Credentials[0]
common.LogSuccess(fmt.Sprintf("MySQL scan success: %s with %s:%s", target, cred.Username, cred.Password))
common.LogSuccess(i18n.GetText("mysql_scan_success", target, cred.Username, cred.Password))
// 自动利用功能(可通过-nobr参数禁用
if result.Success && len(result.Credentials) > 0 && !common.DisableBrute {
@ -84,17 +85,17 @@ func (p *MySQLPlugin) Scan(ctx context.Context, info *common.HostInfo) (*base.Sc
// autoExploit 自动利用
func (p *MySQLPlugin) autoExploit(ctx context.Context, info *common.HostInfo, creds *base.Credential) {
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
common.LogDebug(fmt.Sprintf("MySQL exploit starting for %s", target))
common.LogDebug(i18n.GetText("plugin_exploit_start", "MySQL", target))
// 执行利用
result, err := p.exploiter.Exploit(ctx, info, creds)
if err != nil {
common.LogError(fmt.Sprintf("MySQL exploit failed: %v", err))
common.LogError(i18n.GetText("plugin_exploit_failed", "MySQL", err))
return
}
if result != nil && result.Success {
common.LogSuccess(fmt.Sprintf("MySQL exploit success using %s", result.Method))
common.LogSuccess(i18n.GetText("plugin_exploit_success", "MySQL", i18n.GetExploitMethodName(result.Method)))
base.SaveExploitResult(info, result, "MySQL")
}
}

View File

@ -5,6 +5,7 @@ import (
"context"
"fmt"
"github.com/shadow1ng/fscan/common"
"github.com/shadow1ng/fscan/common/i18n"
"github.com/shadow1ng/fscan/plugins/base"
"os"
"path/filepath"
@ -129,7 +130,7 @@ func (e *RedisExploiter) exploitArbitraryFileWrite(ctx context.Context, info *co
fmt.Errorf("写入失败: %s", msg)), nil
}
base.AddOutputToResult(result, fmt.Sprintf("成功写入文件: %s", common.RedisWritePath))
base.AddOutputToResult(result, i18n.GetText("redis_webshell_written", common.RedisWritePath))
base.AddFileToResult(result, common.RedisWritePath)
return result, nil
@ -216,7 +217,7 @@ func (e *RedisExploiter) exploitCrontabInjection(ctx context.Context, info *comm
fmt.Errorf("写入失败: %s", msg)), nil
}
base.AddOutputToResult(result, fmt.Sprintf("成功注入Crontab任务反弹Shell到: %s", common.RedisShell))
base.AddOutputToResult(result, i18n.GetText("redis_cron_job_written", common.RedisShell))
// 创建Shell信息
shellParts := strings.Split(common.RedisShell, ":")
@ -245,7 +246,7 @@ func (e *RedisExploiter) exploitDataExtraction(ctx context.Context, info *common
// 获取所有键
keys, err := e.getAllKeys(redisConn)
if err == nil && len(keys) > 0 {
base.AddOutputToResult(result, fmt.Sprintf("发现 %d 个键: %s", len(keys), strings.Join(keys[:min(10, len(keys))], ", ")))
base.AddOutputToResult(result, i18n.GetText("redis_keys_found", strings.Join(keys[:min(10, len(keys))], ", ")))
result.Extra["keys"] = keys
// 获取部分键值
@ -255,7 +256,7 @@ func (e *RedisExploiter) exploitDataExtraction(ctx context.Context, info *common
}
value, err := e.getKeyValue(redisConn, key)
if err == nil && value != "" {
base.AddOutputToResult(result, fmt.Sprintf("%s = %s", key, value))
base.AddOutputToResult(result, fmt.Sprintf("%s = %s", key, value))
}
}
}
@ -288,8 +289,8 @@ func (e *RedisExploiter) exploitInfoGathering(ctx context.Context, info *common.
}
// 获取配置信息
base.AddOutputToResult(result, fmt.Sprintf("数据库目录: %s", redisConn.config.Dir))
base.AddOutputToResult(result, fmt.Sprintf("数据库文件: %s", redisConn.config.DBFilename))
base.AddOutputToResult(result, i18n.GetText("redis_config_info", fmt.Sprintf("Dir: %s", redisConn.config.Dir)))
base.AddOutputToResult(result, i18n.GetText("redis_config_info", fmt.Sprintf("DBFilename: %s", redisConn.config.DBFilename)))
return result, nil
}