mirror of
https://github.com/shadow1ng/fscan.git
synced 2026-09-25 20:51:52 +08:00
feat: v2.1.0 核心重构与功能增强
## 架构重构
- 全局变量消除,迁移至 Config/State 对象
- SMB 插件融合(smb/smb2/smbghost/smbinfo)
- 服务探测重构,实现 Nmap 风格 fallback 机制
- 输出系统重构,TXT 实时刷盘 + 双写机制
- i18n 框架升级至 go-i18n
## 性能优化
- 正则表达式预编译
- 内存优化 map[string]struct{}
- 并发指纹匹配
- SOCKS5 连接复用
- 滑动窗口调度 + 自适应线程池
## 新功能
- Web 管理界面
- 多格式 POC 适配(xray/afrog)
- 增强指纹库(3139条)
- Favicon hash 指纹识别
- 插件选择性编译(Build Tags)
- fscan-lab 靶场环境
- 默认端口扩展(62→133)
## 构建系统
- 添加 no_local tag 支持排除本地插件
- 多版本构建:fscan/fscan-nolocal/fscan-web
- CI 添加 snapshot 模式支持仅测试构建
## Bug 修复
- 修复 120+ 个问题,包括 RDP panic、批量扫描漏报、
JSON 输出格式、Redis 检测、Context 超时等
## 测试增强
- 单元测试覆盖率 74-100%
- 并发安全测试
- 集成测试(Web/端口/服务/SSH/ICMP)
This commit is contained in:
@@ -0,0 +1,250 @@
|
||||
//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, config *common.Config, state *common.State) *plugins.Result {
|
||||
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()
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user