feat: 添加跨平台文件下载插件

- 新增downloader本地插件,支持Windows/Linux/Darwin
- 添加-download-url和-download-path命令行参数
- 支持HTTP/HTTPS协议文件下载
- 实现文件大小限制和超时控制
- 支持自动文件名提取和路径权限检查
- 完整的错误处理和资源清理机制
- 集成多语言支持(中英文)
This commit is contained in:
ZacharyZcR 2025-08-11 10:33:16 +08:00
parent ebe7c631fa
commit 25649467c1
4 changed files with 360 additions and 3 deletions

View File

@ -84,6 +84,10 @@ var (
// 键盘记录相关变量
KeyloggerOutputFile string // 键盘记录输出文件
// 文件下载相关变量
DownloadURL string // 下载文件的URL
DownloadSavePath string // 下载文件保存路径
// Parse.go 使用的变量
HostPort []string
URLs []string
@ -245,6 +249,10 @@ func Flag(Info *HostInfo) {
flag.StringVar(&PersistenceTargetFile, "persistence-file", "", i18n.GetText("flag_persistence_file"))
flag.StringVar(&WinPEFile, "win-pe", "", i18n.GetText("flag_win_pe_file"))
flag.StringVar(&KeyloggerOutputFile, "keylog-output", "keylog.txt", i18n.GetText("flag_keylogger_output"))
// 文件下载插件参数
flag.StringVar(&DownloadURL, "download-url", "", i18n.GetText("flag_download_url"))
flag.StringVar(&DownloadSavePath, "download-path", "", i18n.GetText("flag_download_path"))
flag.StringVar(&Language, "lang", "zh", i18n.GetText("flag_language"))
// 帮助参数
@ -391,12 +399,12 @@ func checkParameterConflicts() {
if LocalMode {
if LocalPlugin == "" {
fmt.Printf("错误: 使用本地扫描模式 (-local) 时必须指定一个本地插件 (-localplugin)\n")
fmt.Printf("可用的本地插件: avdetect, fileinfo, dcinfo, minidump, reverseshell, socks5proxy, forwardshell, ldpreload, shellenv, crontask, systemdservice, winregistry, winstartup, winschtask, winservice, winwmi, keylogger\n")
fmt.Printf("可用的本地插件: avdetect, fileinfo, dcinfo, minidump, reverseshell, socks5proxy, forwardshell, ldpreload, shellenv, crontask, systemdservice, winregistry, winstartup, winschtask, winservice, winwmi, keylogger, downloader\n")
os.Exit(1)
}
// 验证本地插件名称
validPlugins := []string{"avdetect", "fileinfo", "dcinfo", "minidump", "reverseshell", "socks5proxy", "forwardshell", "ldpreload", "shellenv", "crontask", "systemdservice", "winregistry", "winstartup", "winschtask", "winservice", "winwmi", "keylogger"} // 已重构的插件
validPlugins := []string{"avdetect", "fileinfo", "dcinfo", "minidump", "reverseshell", "socks5proxy", "forwardshell", "ldpreload", "shellenv", "crontask", "systemdservice", "winregistry", "winstartup", "winschtask", "winservice", "winwmi", "keylogger", "downloader"} // 已重构的插件
isValid := false
for _, valid := range validPlugins {
if LocalPlugin == valid {
@ -407,7 +415,7 @@ func checkParameterConflicts() {
if !isValid {
fmt.Printf("错误: 无效的本地插件 '%s'\n", LocalPlugin)
fmt.Printf("可用的本地插件: avdetect, fileinfo, dcinfo, minidump, reverseshell, socks5proxy, forwardshell, ldpreload, shellenv, crontask, systemdservice, winregistry, winstartup, winschtask, winservice, winwmi, keylogger\n")
fmt.Printf("可用的本地插件: avdetect, fileinfo, dcinfo, minidump, reverseshell, socks5proxy, forwardshell, ldpreload, shellenv, crontask, systemdservice, winregistry, winstartup, winschtask, winservice, winwmi, keylogger, downloader\n")
os.Exit(1)
}
}

View File

@ -254,6 +254,14 @@ var FlagMessages = map[string]map[string]string{
LangZH: "键盘记录输出文件路径",
LangEN: "Keylogger output file path",
},
"flag_download_url": {
LangZH: "要下载的文件URL",
LangEN: "URL of the file to download",
},
"flag_download_path": {
LangZH: "下载文件保存路径",
LangEN: "Save path for downloaded file",
},
"flag_language": {
LangZH: "语言: zh, en",
LangEN: "Language: zh, en",

View File

@ -0,0 +1,338 @@
package downloader
import (
"context"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"runtime"
"strings"
"time"
"github.com/shadow1ng/fscan/common"
"github.com/shadow1ng/fscan/plugins/base"
"github.com/shadow1ng/fscan/plugins/local"
)
// DownloaderPlugin 文件下载插件 - 跨平台支持
type DownloaderPlugin struct {
*local.BaseLocalPlugin
// 配置选项
downloadURL string
savePath string
downloadTimeout time.Duration
maxFileSize int64
}
// NewDownloaderPlugin 创建文件下载插件
func NewDownloaderPlugin() *DownloaderPlugin {
metadata := &base.PluginMetadata{
Name: "downloader",
Version: "1.0.0",
Author: "fscan-team",
Description: "跨平台文件下载插件支持从指定URL下载文件并保存到本地",
Category: "local",
Tags: []string{"local", "downloader", "file", "cross-platform"},
Protocols: []string{"http", "https"},
}
plugin := &DownloaderPlugin{
BaseLocalPlugin: local.NewBaseLocalPlugin(metadata),
downloadTimeout: 30 * time.Second, // 默认30秒超时
maxFileSize: 100 * 1024 * 1024, // 默认最大100MB
}
// 支持所有主要平台
plugin.SetPlatformSupport([]string{"windows", "linux", "darwin"})
// 需要文件写入权限
plugin.SetRequiresPrivileges(false)
// 从全局参数获取配置
plugin.downloadURL = common.DownloadURL
plugin.savePath = common.DownloadSavePath
return plugin
}
// Initialize 初始化插件
func (p *DownloaderPlugin) Initialize() error {
common.LogInfo(fmt.Sprintf("初始化文件下载插件 - 平台: %s", runtime.GOOS))
// 验证必要参数
if err := p.validateParameters(); err != nil {
return fmt.Errorf("参数验证失败: %v", err)
}
// 检查保存路径权限
if err := p.checkSavePathPermissions(); err != nil {
return fmt.Errorf("保存路径权限检查失败: %v", err)
}
return p.BaseLocalPlugin.Initialize()
}
// Scan 重写扫描方法以确保调用正确的ScanLocal实现
func (p *DownloaderPlugin) Scan(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) {
return p.ScanLocal(ctx, info)
}
// ScanLocal 执行文件下载任务
func (p *DownloaderPlugin) ScanLocal(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) {
common.LogInfo(fmt.Sprintf("开始下载文件: %s", p.downloadURL))
// 执行下载
downloadInfo, err := p.downloadFile(ctx)
if err != nil {
return &base.ScanResult{
Success: false,
Error: err,
}, nil
}
result := &base.ScanResult{
Success: true,
Service: "FileDownloader",
Banner: fmt.Sprintf("文件下载成功: %s -> %s", p.downloadURL, downloadInfo["save_path"]),
Extra: map[string]interface{}{
"download_url": p.downloadURL,
"save_path": downloadInfo["save_path"],
"file_size": downloadInfo["file_size"],
"content_type": downloadInfo["content_type"],
"platform": runtime.GOOS,
"download_time": downloadInfo["download_time"],
},
}
common.LogSuccess(fmt.Sprintf("文件下载完成: %s (大小: %v bytes)",
downloadInfo["save_path"], downloadInfo["file_size"]))
return result, nil
}
// validateParameters 验证输入参数
func (p *DownloaderPlugin) validateParameters() error {
if p.downloadURL == "" {
return fmt.Errorf("下载URL不能为空请使用 -download-url 参数指定")
}
// 验证URL格式
if !strings.HasPrefix(strings.ToLower(p.downloadURL), "http://") &&
!strings.HasPrefix(strings.ToLower(p.downloadURL), "https://") {
return fmt.Errorf("无效的URL格式必须以 http:// 或 https:// 开头")
}
// 如果没有指定保存路径使用URL中的文件名
if p.savePath == "" {
filename := p.extractFilenameFromURL(p.downloadURL)
if filename == "" {
filename = "downloaded_file"
}
p.savePath = filename
common.LogInfo(fmt.Sprintf("未指定保存路径,使用默认文件名: %s", p.savePath))
}
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() error {
// 获取保存目录
saveDir := filepath.Dir(p.savePath)
if saveDir == "." || saveDir == "" {
// 使用当前目录
var err error
saveDir, err = os.Getwd()
if err != nil {
return fmt.Errorf("获取当前目录失败: %v", err)
}
p.savePath = filepath.Join(saveDir, filepath.Base(p.savePath))
}
// 确保目录存在
if err := os.MkdirAll(saveDir, 0755); err != nil {
return fmt.Errorf("创建保存目录失败: %v", err)
}
// 检查写入权限
testFile := filepath.Join(saveDir, ".fscan_write_test")
if file, err := os.Create(testFile); err != nil {
return fmt.Errorf("保存目录无写入权限: %v", err)
} else {
file.Close()
os.Remove(testFile)
}
common.LogInfo(fmt.Sprintf("文件将保存至: %s", p.savePath))
return nil
}
// downloadFile 执行文件下载
func (p *DownloaderPlugin) downloadFile(ctx context.Context) (map[string]interface{}, error) {
startTime := time.Now()
// 创建带超时的HTTP客户端
client := &http.Client{
Timeout: p.downloadTimeout,
}
// 创建请求
req, err := http.NewRequestWithContext(ctx, "GET", p.downloadURL, nil)
if err != nil {
return nil, fmt.Errorf("创建HTTP请求失败: %v", err)
}
// 设置User-Agent
req.Header.Set("User-Agent", "fscan-downloader/1.0")
common.LogInfo("正在连接到服务器...")
// 发送请求
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("HTTP请求失败: %v", err)
}
defer resp.Body.Close()
// 检查HTTP状态码
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("HTTP请求失败状态码: %d %s", resp.StatusCode, resp.Status)
}
// 检查文件大小
contentLength := resp.ContentLength
if contentLength > p.maxFileSize {
return nil, fmt.Errorf("文件过大 (%d bytes),超过最大限制 (%d bytes)",
contentLength, p.maxFileSize)
}
common.LogInfo(fmt.Sprintf("开始下载,文件大小: %d bytes", contentLength))
// 创建保存文件
outFile, err := os.Create(p.savePath)
if err != nil {
return nil, fmt.Errorf("创建保存文件失败: %v", err)
}
defer outFile.Close()
// 使用带限制的Reader防止过大文件
limitedReader := io.LimitReader(resp.Body, p.maxFileSize)
// 复制数据
written, err := io.Copy(outFile, limitedReader)
if err != nil {
// 清理部分下载的文件
os.Remove(p.savePath)
return nil, fmt.Errorf("文件下载失败: %v", err)
}
downloadTime := time.Since(startTime)
// 返回下载信息
downloadInfo := map[string]interface{}{
"save_path": p.savePath,
"file_size": written,
"content_type": resp.Header.Get("Content-Type"),
"download_time": downloadTime,
}
return downloadInfo, nil
}
// GetLocalData 获取下载器本地数据
func (p *DownloaderPlugin) GetLocalData(ctx context.Context) (map[string]interface{}, error) {
data := make(map[string]interface{})
data["plugin_type"] = "downloader"
data["platform"] = runtime.GOOS
data["download_url"] = p.downloadURL
data["save_path"] = p.savePath
data["timeout"] = p.downloadTimeout.String()
data["max_file_size"] = p.maxFileSize
if hostname, err := os.Hostname(); err == nil {
data["hostname"] = hostname
}
if workDir, err := os.Getwd(); err == nil {
data["work_dir"] = workDir
}
return data, nil
}
// ExtractData 提取数据
func (p *DownloaderPlugin) ExtractData(ctx context.Context, info *common.HostInfo, data map[string]interface{}) (*base.ExploitResult, error) {
return &base.ExploitResult{
Success: true,
Output: fmt.Sprintf("文件下载完成: %s -> %s", p.downloadURL, p.savePath),
Data: data,
Extra: map[string]interface{}{
"download_url": p.downloadURL,
"save_path": p.savePath,
"platform": runtime.GOOS,
"status": "completed",
},
}, nil
}
// GetInfo 获取插件信息
func (p *DownloaderPlugin) GetInfo() string {
var info strings.Builder
info.WriteString("跨平台文件下载插件\n")
info.WriteString(fmt.Sprintf("支持平台: %s\n", strings.Join(p.GetPlatformSupport(), ", ")))
info.WriteString(fmt.Sprintf("支持协议: %s\n", strings.Join(p.GetMetadata().Protocols, ", ")))
info.WriteString("功能: 从HTTP/HTTPS URL下载文件到本地\n")
info.WriteString("参数:\n")
info.WriteString(" -download-url: 要下载的文件URL\n")
info.WriteString(" -download-path: 保存路径 (可选默认使用URL中的文件名)\n")
return info.String()
}
// RegisterDownloaderPlugin 注册文件下载插件
func RegisterDownloaderPlugin() {
factory := base.NewSimplePluginFactory(
&base.PluginMetadata{
Name: "downloader",
Version: "1.0.0",
Author: "fscan-team",
Description: "跨平台文件下载插件支持从指定URL下载文件并保存到本地",
Category: "local",
Tags: []string{"downloader", "local", "file", "cross-platform"},
Protocols: []string{"http", "https"},
},
func() base.Plugin {
return NewDownloaderPlugin()
},
)
base.GlobalPluginRegistry.Register("downloader", factory)
}
// init 插件注册函数
func init() {
RegisterDownloaderPlugin()
}

View File

@ -31,6 +31,9 @@ import (
// 监控插件
_ "github.com/shadow1ng/fscan/plugins/local/keylogger" // 跨平台键盘记录
// 实用工具插件
_ "github.com/shadow1ng/fscan/plugins/local/downloader" // 跨平台文件下载
)
func main() {