diff --git a/Common/Flag.go b/Common/Flag.go index c7b599e..e42b901 100644 --- a/Common/Flag.go +++ b/Common/Flag.go @@ -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) } } diff --git a/Common/i18n/messages/flag.go b/Common/i18n/messages/flag.go index 5144b57..70b24c7 100644 --- a/Common/i18n/messages/flag.go +++ b/Common/i18n/messages/flag.go @@ -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", diff --git a/Plugins/local/downloader/plugin.go b/Plugins/local/downloader/plugin.go new file mode 100644 index 0000000..5f0da09 --- /dev/null +++ b/Plugins/local/downloader/plugin.go @@ -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() +} \ No newline at end of file diff --git a/main.go b/main.go index 1de18b3..c1887f1 100644 --- a/main.go +++ b/main.go @@ -31,6 +31,9 @@ import ( // 监控插件 _ "github.com/shadow1ng/fscan/plugins/local/keylogger" // 跨平台键盘记录 + + // 实用工具插件 + _ "github.com/shadow1ng/fscan/plugins/local/downloader" // 跨平台文件下载 ) func main() {