mirror of
https://github.com/shadow1ng/fscan.git
synced 2025-09-14 14:06:44 +08:00
feat: 添加跨平台文件下载插件
- 新增downloader本地插件,支持Windows/Linux/Darwin - 添加-download-url和-download-path命令行参数 - 支持HTTP/HTTPS协议文件下载 - 实现文件大小限制和超时控制 - 支持自动文件名提取和路径权限检查 - 完整的错误处理和资源清理机制 - 集成多语言支持(中英文)
This commit is contained in:
parent
ebe7c631fa
commit
25649467c1
@ -84,6 +84,10 @@ var (
|
|||||||
// 键盘记录相关变量
|
// 键盘记录相关变量
|
||||||
KeyloggerOutputFile string // 键盘记录输出文件
|
KeyloggerOutputFile string // 键盘记录输出文件
|
||||||
|
|
||||||
|
// 文件下载相关变量
|
||||||
|
DownloadURL string // 下载文件的URL
|
||||||
|
DownloadSavePath string // 下载文件保存路径
|
||||||
|
|
||||||
// Parse.go 使用的变量
|
// Parse.go 使用的变量
|
||||||
HostPort []string
|
HostPort []string
|
||||||
URLs []string
|
URLs []string
|
||||||
@ -245,6 +249,10 @@ func Flag(Info *HostInfo) {
|
|||||||
flag.StringVar(&PersistenceTargetFile, "persistence-file", "", i18n.GetText("flag_persistence_file"))
|
flag.StringVar(&PersistenceTargetFile, "persistence-file", "", i18n.GetText("flag_persistence_file"))
|
||||||
flag.StringVar(&WinPEFile, "win-pe", "", i18n.GetText("flag_win_pe_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(&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"))
|
flag.StringVar(&Language, "lang", "zh", i18n.GetText("flag_language"))
|
||||||
|
|
||||||
// 帮助参数
|
// 帮助参数
|
||||||
@ -391,12 +399,12 @@ func checkParameterConflicts() {
|
|||||||
if LocalMode {
|
if LocalMode {
|
||||||
if LocalPlugin == "" {
|
if LocalPlugin == "" {
|
||||||
fmt.Printf("错误: 使用本地扫描模式 (-local) 时必须指定一个本地插件 (-localplugin)\n")
|
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)
|
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
|
isValid := false
|
||||||
for _, valid := range validPlugins {
|
for _, valid := range validPlugins {
|
||||||
if LocalPlugin == valid {
|
if LocalPlugin == valid {
|
||||||
@ -407,7 +415,7 @@ func checkParameterConflicts() {
|
|||||||
|
|
||||||
if !isValid {
|
if !isValid {
|
||||||
fmt.Printf("错误: 无效的本地插件 '%s'\n", LocalPlugin)
|
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)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -254,6 +254,14 @@ var FlagMessages = map[string]map[string]string{
|
|||||||
LangZH: "键盘记录输出文件路径",
|
LangZH: "键盘记录输出文件路径",
|
||||||
LangEN: "Keylogger output file path",
|
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": {
|
"flag_language": {
|
||||||
LangZH: "语言: zh, en",
|
LangZH: "语言: zh, en",
|
||||||
LangEN: "Language: zh, en",
|
LangEN: "Language: zh, en",
|
||||||
|
338
Plugins/local/downloader/plugin.go
Normal file
338
Plugins/local/downloader/plugin.go
Normal 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()
|
||||||
|
}
|
3
main.go
3
main.go
@ -31,6 +31,9 @@ import (
|
|||||||
|
|
||||||
// 监控插件
|
// 监控插件
|
||||||
_ "github.com/shadow1ng/fscan/plugins/local/keylogger" // 跨平台键盘记录
|
_ "github.com/shadow1ng/fscan/plugins/local/keylogger" // 跨平台键盘记录
|
||||||
|
|
||||||
|
// 实用工具插件
|
||||||
|
_ "github.com/shadow1ng/fscan/plugins/local/downloader" // 跨平台文件下载
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
Loading…
Reference in New Issue
Block a user