fscan/WebScan/WebScan.go
ZacharyZcR 43f210ffc6 feat: 实现新一代插件注册系统完全替代传统手动注册模式
- 重构插件注册架构采用现代工厂模式和自动发现机制
- 新增完整的插件元数据管理系统支持版本能力标签等信息
- 实现智能插件适配器提供向后兼容的桥接功能
- 建立MySQL Redis SSH三个标准插件作为新架构参考实现
- 优化插件扫描逻辑支持按端口按类型的智能查询和过滤
- 添加国际化支持和完善的文档体系
- 代码量减少67%维护成本大幅降低扩展性显著提升

新架构特点:
- 零配置插件注册import即用
- 工厂模式延迟初始化和依赖注入
- 丰富元数据系统和能力声明
- 完全解耦的模块化设计
- 面向未来的可扩展架构

测试验证: MySQL和Redis插件功能完整包括弱密码检测未授权访问检测和自动利用攻击
2025-08-07 11:28:34 +08:00

326 lines
7.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package WebScan
import (
"context"
"embed"
"errors"
"fmt"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/shadow1ng/fscan/common"
"github.com/shadow1ng/fscan/common/config"
"github.com/shadow1ng/fscan/webscan/lib"
)
// 常量定义
const (
protocolHTTP = "http://"
protocolHTTPS = "https://"
yamlExt = ".yaml"
ymlExt = ".yml"
defaultTimeout = 30 * time.Second
concurrencyLimit = 10 // 并发加载POC的限制
)
// 错误定义
var (
ErrInvalidURL = errors.New("无效的URL格式")
ErrEmptyTarget = errors.New("目标URL为空")
ErrPocNotFound = errors.New("未找到匹配的POC")
ErrPocLoadFailed = errors.New("POC加载失败")
)
//go:embed pocs
var pocsFS embed.FS
var (
once sync.Once
allPocs []*lib.Poc
)
// WebScan 执行Web漏洞扫描
func WebScan(info *common.HostInfo) {
// 初始化POC
once.Do(initPocs)
// 验证输入
if info == nil {
common.LogError("无效的扫描目标")
return
}
if len(allPocs) == 0 {
common.LogError("POC加载失败无法执行扫描")
return
}
// 构建目标URL
target, err := buildTargetURL(info)
if err != nil {
common.LogError(fmt.Sprintf("构建目标URL失败: %v", err))
return
}
// 使用带超时的上下文
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
defer cancel()
// 根据扫描策略执行POC
if common.Pocinfo.PocName == "" && len(info.Infostr) == 0 {
// 执行所有POC
executePOCs(ctx, config.PocInfo{Target: target})
} else if len(info.Infostr) > 0 {
// 基于指纹信息执行POC
scanByFingerprints(ctx, target, info.Infostr)
} else if common.Pocinfo.PocName != "" {
// 基于指定POC名称执行
executePOCs(ctx, config.PocInfo{Target: target, PocName: common.Pocinfo.PocName})
}
}
// buildTargetURL 构建规范的目标URL
func buildTargetURL(info *common.HostInfo) (string, error) {
// 自动构建URL
if info.Url == "" {
info.Url = fmt.Sprintf("%s%s:%s", protocolHTTP, info.Host, info.Ports)
} else if !hasProtocolPrefix(info.Url) {
info.Url = protocolHTTP + info.Url
}
// 解析URL以提取基础部分
parsedURL, err := url.Parse(info.Url)
if err != nil {
return "", fmt.Errorf("%w: %v", ErrInvalidURL, err)
}
return fmt.Sprintf("%s://%s", parsedURL.Scheme, parsedURL.Host), nil
}
// hasProtocolPrefix 检查URL是否包含协议前缀
func hasProtocolPrefix(urlStr string) bool {
return strings.HasPrefix(urlStr, protocolHTTP) || strings.HasPrefix(urlStr, protocolHTTPS)
}
// scanByFingerprints 根据指纹执行POC
func scanByFingerprints(ctx context.Context, target string, fingerprints []string) {
for _, fingerprint := range fingerprints {
if fingerprint == "" {
continue
}
pocName := lib.CheckInfoPoc(fingerprint)
if pocName == "" {
continue
}
executePOCs(ctx, config.PocInfo{Target: target, PocName: pocName})
}
}
// executePOCs 执行POC检测
func executePOCs(ctx context.Context, pocInfo config.PocInfo) {
// 验证目标
if pocInfo.Target == "" {
common.LogError(ErrEmptyTarget.Error())
return
}
// 确保URL格式正确
if !hasProtocolPrefix(pocInfo.Target) {
pocInfo.Target = protocolHTTP + pocInfo.Target
}
// 验证URL
_, err := url.Parse(pocInfo.Target)
if err != nil {
common.LogError(fmt.Sprintf("%v %s: %v", ErrInvalidURL, pocInfo.Target, err))
return
}
// 创建基础请求
req, err := createBaseRequest(ctx, pocInfo.Target)
if err != nil {
common.LogError(fmt.Sprintf("创建HTTP请求失败: %v", err))
return
}
// 筛选POC
matchedPocs := filterPocs(pocInfo.PocName)
if len(matchedPocs) == 0 {
common.LogDebug(fmt.Sprintf("%v: %s", ErrPocNotFound, pocInfo.PocName))
return
}
// 执行POC检测
lib.CheckMultiPoc(req, matchedPocs, common.PocNum)
}
// createBaseRequest 创建带上下文的HTTP请求
func createBaseRequest(ctx context.Context, target string) (*http.Request, error) {
req, err := http.NewRequestWithContext(ctx, "GET", target, nil)
if err != nil {
return nil, err
}
// 设置请求头
req.Header.Set("User-agent", common.UserAgent)
req.Header.Set("Accept", common.Accept)
req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9")
if common.Cookie != "" {
req.Header.Set("Cookie", common.Cookie)
}
return req, nil
}
// initPocs 初始化并加载POC
func initPocs() {
// 预分配容量避免频繁扩容典型POC数量在100-500之间
allPocs = make([]*lib.Poc, 0, 256)
if common.PocPath == "" {
loadEmbeddedPocs()
} else {
loadExternalPocs(common.PocPath)
}
}
// loadEmbeddedPocs 加载内置POC
func loadEmbeddedPocs() {
entries, err := pocsFS.ReadDir("pocs")
if err != nil {
common.LogError(fmt.Sprintf("加载内置POC目录失败: %v", err))
return
}
// 收集所有POC文件
var pocFiles []string
for _, entry := range entries {
if isPocFile(entry.Name()) {
pocFiles = append(pocFiles, entry.Name())
}
}
// 并发加载POC文件
loadPocsConcurrently(pocFiles, true, "")
}
// loadExternalPocs 从外部路径加载POC
func loadExternalPocs(pocPath string) {
if !directoryExists(pocPath) {
common.LogError(fmt.Sprintf("POC目录不存在: %s", pocPath))
return
}
// 收集所有POC文件路径
var pocFiles []string
err := filepath.Walk(pocPath, func(path string, info os.FileInfo, err error) error {
if err != nil || info == nil || info.IsDir() {
return nil
}
if isPocFile(info.Name()) {
pocFiles = append(pocFiles, path)
}
return nil
})
if err != nil {
common.LogError(fmt.Sprintf("遍历POC目录失败: %v", err))
return
}
// 并发加载POC文件
loadPocsConcurrently(pocFiles, false, pocPath)
}
// loadPocsConcurrently 并发加载POC文件
func loadPocsConcurrently(pocFiles []string, isEmbedded bool, pocPath string) {
pocCount := len(pocFiles)
if pocCount == 0 {
return
}
var wg sync.WaitGroup
var mu sync.Mutex
var successCount, failCount int
// 使用信号量控制并发数
semaphore := make(chan struct{}, concurrencyLimit)
for _, file := range pocFiles {
wg.Add(1)
semaphore <- struct{}{} // 获取信号量
go func(filename string) {
defer func() {
<-semaphore // 释放信号量
wg.Done()
}()
var poc *lib.Poc
var err error
// 根据不同的来源加载POC
if isEmbedded {
poc, err = lib.LoadPoc(filename, pocsFS)
} else {
poc, err = lib.LoadPocbyPath(filename)
}
mu.Lock()
defer mu.Unlock()
if err != nil {
failCount++
return
}
if poc != nil {
allPocs = append(allPocs, poc)
successCount++
}
}(file)
}
wg.Wait()
common.LogBase(fmt.Sprintf("POC加载完成: 总共%d个成功%d个失败%d个",
pocCount, successCount, failCount))
}
// directoryExists 检查目录是否存在
func directoryExists(path string) bool {
info, err := os.Stat(path)
return err == nil && info.IsDir()
}
// isPocFile 检查文件是否为POC文件
func isPocFile(filename string) bool {
lowerName := strings.ToLower(filename)
return strings.HasSuffix(lowerName, yamlExt) || strings.HasSuffix(lowerName, ymlExt)
}
// filterPocs 根据POC名称筛选
func filterPocs(pocName string) []*lib.Poc {
if pocName == "" {
return allPocs
}
// 转换为小写以进行不区分大小写的匹配
searchName := strings.ToLower(pocName)
var matchedPocs []*lib.Poc
for _, poc := range allPocs {
if poc != nil && strings.Contains(strings.ToLower(poc.Name), searchName) {
matchedPocs = append(matchedPocs, poc)
}
}
return matchedPocs
}