feat: 完善本地插件控制机制和参数验证

- 实现本地插件严格单个指定控制,拒绝多插件分隔符
- 修复本地插件自动调用问题,避免不必要的插件实例创建
- 添加-local与-h/-u参数的互斥性检查
- 优化插件存在性检查,使用pluginExists()替代plugins.Get()
- 完善统一插件系统的端口信息管理
- 增强Web插件的协议智能检测功能

主要变更:
* 本地插件现在只能通过-local参数明确指定单个插件运行
* 插件适用性检查不再创建不必要的插件实例,提升性能
* 本地扫描与网络扫描参数完全隔离,避免配置冲突
This commit is contained in:
ZacharyZcR 2025-08-26 19:34:14 +08:00
parent d570be1f50
commit 43ddb3630d
9 changed files with 308 additions and 120 deletions

View File

@ -297,6 +297,10 @@ func parseCommandLineArgs() {
// 检查参数冲突
checkParameterConflicts()
// 额外的本地插件互斥检查
// 需要在解析后检查因为Host是通过Info.Host设置的
// 这个检查在app/initializer.go中进行
}
// parseEnvironmentArgs 安全地解析环境变量中的参数
@ -395,24 +399,21 @@ func checkParameterConflicts() {
// 检查本地插件参数
if LocalPlugin != "" {
// 检查是否包含分隔符(确保只能指定单个插件)
invalidChars := []string{",", ";", " ", "|", "&"}
for _, char := range invalidChars {
if strings.Contains(LocalPlugin, char) {
fmt.Printf("错误: 本地插件只能指定单个插件,不支持使用 '%s' 分隔的多个插件\n", char)
LogError(fmt.Sprintf("本地插件只能指定单个插件,不支持使用 '%s' 分隔的多个插件", char))
os.Exit(1)
}
}
// 自动启用本地模式
LocalMode = true
// 验证本地插件名称
isValid := false
for _, valid := range LocalPluginsList {
if LocalPlugin == valid {
isValid = true
break
}
}
if !isValid {
fmt.Printf("错误: 无效的本地插件 '%s'\n", LocalPlugin)
if len(LocalPluginsList) > 0 {
fmt.Printf("可用的本地插件: %s\n", strings.Join(LocalPluginsList, ", "))
}
os.Exit(1)
}
// 验证本地插件名称 - 使用统一插件系统验证
// 这里不进行验证,让运行时的插件系统来处理不存在的插件
}
}

View File

@ -3,7 +3,7 @@ package core
import (
"fmt"
"github.com/shadow1ng/fscan/common"
"github.com/shadow1ng/fscan/plugins/services"
"github.com/shadow1ng/fscan/plugins"
"strings"
)
@ -43,7 +43,7 @@ func (b *BaseScanStrategy) GetPlugins() ([]string, bool) {
// 验证插件是否存在
var validPlugins []string
for _, name := range requestedPlugins {
if services.GetPlugin(name) != nil {
if b.pluginExists(name) {
validPlugins = append(validPlugins, name)
}
}
@ -52,7 +52,7 @@ func (b *BaseScanStrategy) GetPlugins() ([]string, bool) {
}
// 未指定或使用"all":获取所有插件
return services.GetAllPlugins(), false
return plugins.All(), false
}
// IsPluginApplicable 判断插件是否适用(传统接口兼容)
@ -78,39 +78,112 @@ func (b *BaseScanStrategy) IsPluginApplicable(plugin common.ScanPlugin, targetPo
// IsPluginApplicableByName 根据插件名称判断是否适用(新接口)
func (b *BaseScanStrategy) IsPluginApplicableByName(pluginName string, targetHost string, targetPort int, isCustomMode bool) bool {
// 自定义模式下运行所有明确指定的插件
// 首先检查插件是否存在,但不创建实例
if !b.pluginExists(pluginName) {
return false
}
// 自定义模式下强制运行所有明确指定的插件n*m调用
if isCustomMode {
return true
}
// 获取插件实例
plugin := services.GetPlugin(pluginName)
if plugin == nil {
return false
// 本地插件特殊处理:优先检查,避免不必要的端口获取
if b.isLocalPlugin(pluginName) {
result := b.isLocalPluginExplicitlySpecified(pluginName)
common.LogDebug(fmt.Sprintf("本地插件 %s 检查结果: %v (LocalPlugin='%s')", pluginName, result, common.LocalPlugin))
return result
}
// 检查端口匹配(如果指定了端口)
// 检查插件端口匹配(特殊端口自动调用具体插件)
pluginPorts := b.getPluginPorts(pluginName)
// Web插件特殊处理使用智能HTTP检测
if len(pluginPorts) == 0 && b.isWebPlugin(pluginName) {
return b.isWebServicePort(targetHost, targetPort)
}
// 无端口限制的其他插件适用于所有端口
if len(pluginPorts) == 0 {
return true
}
// 有端口限制的插件:检查端口匹配
if targetPort > 0 {
pluginPorts := plugin.GetPorts()
if len(pluginPorts) > 0 {
found := false
for _, port := range pluginPorts {
if port == targetPort {
found = true
break
}
}
if !found {
return false
for _, port := range pluginPorts {
if port == targetPort {
return true
}
}
}
return true
return false
}
// LogPluginInfo 输出插件信息(简化版)
// pluginExists 检查插件是否存在,不创建实例
func (b *BaseScanStrategy) pluginExists(pluginName string) bool {
// 使用All()获取所有注册插件名称避免调用Get()创建实例
allPlugins := plugins.All()
for _, name := range allPlugins {
if name == pluginName {
return true
}
}
return false
}
// getPluginPorts 获取插件端口列表
func (b *BaseScanStrategy) getPluginPorts(pluginName string) []int {
// 使用统一插件系统获取端口信息
return plugins.GetPluginPorts(pluginName)
}
// isWebPlugin 判断是否为Web插件
func (b *BaseScanStrategy) isWebPlugin(pluginName string) bool {
// 已知的Web插件列表
webPlugins := []string{"webtitle", "webpoc"}
for _, webPlugin := range webPlugins {
if pluginName == webPlugin {
return true
}
}
return false
}
// isLocalPlugin 判断是否为本地插件
func (b *BaseScanStrategy) isLocalPlugin(pluginName string) bool {
// 已知的本地插件列表从RegisterLocalPlugin调用中获取
localPlugins := []string{
"avdetect", "crontask", "cleaner", "dcinfo", "envinfo", "forwardshell",
"minidump", "socks5proxy", "shellenv", "downloader", "reverseshell",
"systemdservice", "fileinfo", "keylogger", "winwmi", "winstartup",
"winschtask", "systeminfo", "winregistry", "ldpreload", "winservice",
}
for _, localPlugin := range localPlugins {
if pluginName == localPlugin {
return true
}
}
return false
}
// isWebServicePort 使用智能检测判断端口是否运行Web服务
func (b *BaseScanStrategy) isWebServicePort(host string, port int) bool {
// 创建Web端口检测器实例
detector := NewWebPortDetector()
return detector.IsWebService(host, port)
}
// isLocalPluginExplicitlySpecified 检查本地插件是否明确通过-local参数指定
func (b *BaseScanStrategy) isLocalPluginExplicitlySpecified(pluginName string) bool {
// 只有通过-local参数明确指定的单个插件才能调用
return common.LocalPlugin == pluginName
}
// LogPluginInfo 输出插件信息简化版将被各Strategy重写
func (b *BaseScanStrategy) LogPluginInfo() {
// 基础实现:显示所有插件(无端口过滤)
// 各个具体Strategy应该重写这个方法以提供更精确的显示
allPlugins, isCustomMode := b.GetPlugins()
var prefix string
@ -136,6 +209,43 @@ func (b *BaseScanStrategy) LogPluginInfo() {
}
}
// LogPluginInfoWithPort 带端口信息的插件显示(供子类使用)
func (b *BaseScanStrategy) LogPluginInfoWithPort(targetPort int) {
allPlugins, isCustomMode := b.GetPlugins()
var prefix string
switch b.filterType {
case FilterLocal:
prefix = "本地插件"
case FilterService:
prefix = "服务插件"
case FilterWeb:
prefix = "Web插件"
default:
prefix = "插件"
}
// 过滤适用的插件
var applicablePlugins []string
for _, pluginName := range allPlugins {
if b.pluginExists(pluginName) {
if b.IsPluginApplicableByName(pluginName, "127.0.0.1", targetPort, isCustomMode) {
applicablePlugins = append(applicablePlugins, pluginName)
}
}
}
if len(applicablePlugins) > 0 {
if isCustomMode {
common.LogBase(fmt.Sprintf("%s: 自定义指定 (%s)", prefix, strings.Join(applicablePlugins, ", ")))
} else {
common.LogBase(fmt.Sprintf("%s: %s", prefix, strings.Join(applicablePlugins, ", ")))
}
} else {
common.LogBase(fmt.Sprintf("%s: 无可用插件", prefix))
}
}
// ValidateConfiguration 验证扫描配置
func (b *BaseScanStrategy) ValidateConfiguration() error {
return nil

View File

@ -1,6 +1,7 @@
package core
import (
"fmt"
"github.com/shadow1ng/fscan/common"
"github.com/shadow1ng/fscan/common/i18n"
"sync"
@ -18,6 +19,15 @@ func NewLocalScanStrategy() *LocalScanStrategy {
}
}
// LogPluginInfo 重写以只显示通过-local指定的插件
func (s *LocalScanStrategy) LogPluginInfo() {
if common.LocalPlugin != "" {
common.LogBase(fmt.Sprintf("本地插件: %s", common.LocalPlugin))
} else {
common.LogBase("本地插件: 未指定")
}
}
// Name 返回策略名称
func (s *LocalScanStrategy) Name() string {
return i18n.GetText("scan_strategy_local_name")

View File

@ -1,9 +1,9 @@
package core
import (
"fmt"
"github.com/shadow1ng/fscan/common"
"github.com/shadow1ng/fscan/common/i18n"
"github.com/shadow1ng/fscan/plugins/services"
"strconv"
"strings"
"sync"
@ -23,6 +23,77 @@ func NewServiceScanStrategy() *ServiceScanStrategy {
}
}
// LogPluginInfo 重写以提供基于端口的插件过滤
func (s *ServiceScanStrategy) LogPluginInfo() {
// 需要从命令行参数获取端口信息来进行过滤
// 如果没有指定端口,使用默认端口进行过滤显示
if common.Ports == "" || common.Ports == "all" {
// 默认端口扫描:显示所有插件
s.BaseScanStrategy.LogPluginInfo()
} else {
// 指定端口扫描:只显示匹配的插件
s.showPluginsForSpecifiedPorts()
}
}
// showPluginsForSpecifiedPorts 显示指定端口的匹配插件
func (s *ServiceScanStrategy) showPluginsForSpecifiedPorts() {
allPlugins, isCustomMode := s.GetPlugins()
// 解析端口
ports := s.parsePortList(common.Ports)
if len(ports) == 0 {
s.BaseScanStrategy.LogPluginInfo()
return
}
// 收集所有匹配的插件(去重)
pluginSet := make(map[string]bool)
for _, port := range ports {
for _, pluginName := range allPlugins {
if s.pluginExists(pluginName) {
if s.IsPluginApplicableByName(pluginName, "127.0.0.1", port, isCustomMode) {
pluginSet[pluginName] = true
}
}
}
}
// 转换为列表
var applicablePlugins []string
for pluginName := range pluginSet {
applicablePlugins = append(applicablePlugins, pluginName)
}
// 输出结果
if len(applicablePlugins) > 0 {
if isCustomMode {
common.LogBase(fmt.Sprintf("服务插件: 自定义指定 (%s)", strings.Join(applicablePlugins, ", ")))
} else {
common.LogBase(fmt.Sprintf("服务插件: %s", strings.Join(applicablePlugins, ", ")))
}
} else {
common.LogBase("服务插件: 无可用插件")
}
}
// parsePortList 解析端口列表
func (s *ServiceScanStrategy) parsePortList(portStr string) []int {
if portStr == "" || portStr == "all" {
return []int{}
}
var ports []int
parts := strings.Split(portStr, ",")
for _, part := range parts {
part = strings.TrimSpace(part)
if port, err := strconv.Atoi(part); err == nil {
ports = append(ports, port)
}
}
return ports
}
// Name 返回策略名称
func (s *ServiceScanStrategy) Name() string {
return i18n.GetText("scan_strategy_service_name")
@ -52,7 +123,7 @@ func (s *ServiceScanStrategy) Execute(info common.HostInfo, ch *chan struct{}, w
common.LogBase(i18n.GetText("scan_host_start"))
// 输出插件信息
// 输出插件信息(重写以提供端口过滤)
s.LogPluginInfo()
// 执行主机扫描流程
@ -102,29 +173,22 @@ func (s *ServiceScanStrategy) LogVulnerabilityPluginInfo(targets []common.HostIn
}
}
// 获取实际会被使用的插件列表(包括新插件架构和传统插件
// 获取实际会被使用的插件列表(考虑端口匹配
var servicePlugins []string
// 检查新插件架构
// 提取第一个目标端口用于匹配检查
var firstTargetPort int
if len(targets) > 0 && targets[0].Ports != "" {
firstTargetPort, _ = strconv.Atoi(targets[0].Ports)
}
for _, pluginName := range allPlugins {
// 首先检查新插件架构
if plugin := services.GetPlugin(pluginName); plugin != nil {
// 检查端口匹配
if s.isNewPluginApplicableToAnyPort(plugin, portSet, isCustomMode) {
// 使用统一插件系统检查插件存在性
if s.pluginExists(pluginName) {
// 检查插件是否适用于目标端口
if s.IsPluginApplicableByName(pluginName, "127.0.0.1", firstTargetPort, isCustomMode) {
servicePlugins = append(servicePlugins, pluginName)
}
continue
}
// 然后检查传统插件系统
plugin, exists := common.PluginManager[pluginName]
if !exists {
continue
}
// 检查传统插件是否对任何目标端口适用
if s.isPluginApplicableToAnyPort(plugin, portSet, isCustomMode) {
servicePlugins = append(servicePlugins, pluginName)
}
}
@ -136,56 +200,3 @@ func (s *ServiceScanStrategy) LogVulnerabilityPluginInfo(targets []common.HostIn
}
}
// isPluginApplicableToAnyPort 检查插件是否对任何端口适用(性能优化)
func (s *ServiceScanStrategy) isPluginApplicableToAnyPort(plugin common.ScanPlugin, portSet map[int]bool, isCustomMode bool) bool {
// 自定义模式下运行所有明确指定的插件
if isCustomMode {
return true
}
// 服务扫描排除本地插件但保留Web插件有智能检测
if plugin.HasType(common.PluginTypeLocal) {
return false
}
// 无端口限制的插件适用于所有端口
if len(plugin.Ports) == 0 {
return true
}
// 有端口限制的插件:检查是否匹配任何目标端口
for port := range portSet {
if plugin.HasPort(port) {
return true
}
}
return false
}
// isNewPluginApplicableToAnyPort 检查新插件架构的插件是否对任何端口适用
func (s *ServiceScanStrategy) isNewPluginApplicableToAnyPort(plugin services.Plugin, portSet map[int]bool, isCustomMode bool) bool {
// 自定义模式下运行所有明确指定的插件
if isCustomMode {
return true
}
// 获取插件支持的端口
pluginPorts := plugin.GetPorts()
// 无端口限制的插件适用于所有端口
if len(pluginPorts) == 0 {
return true
}
// 有端口限制的插件:检查是否匹配任何目标端口
for port := range portSet {
for _, pluginPort := range pluginPorts {
if pluginPort == port {
return true
}
}
}
return false
}

10
main.go
View File

@ -18,6 +18,16 @@ func main() {
var info common.HostInfo
common.Flag(&info)
// 检查-local与-h -u的互斥性
if common.LocalPlugin != "" && info.Host != "" {
fmt.Printf("错误: -local参数与-h参数互斥本地插件只能在本机运行\n")
os.Exit(1)
}
if common.LocalPlugin != "" && common.TargetURL != "" {
fmt.Printf("错误: -local参数与-u参数互斥本地插件不需要URL目标\n")
os.Exit(1)
}
// 初始化日志
common.InitLogger()

View File

@ -58,6 +58,7 @@ type Credential struct {
// 全局插件注册表 - 一个数据结构解决所有问题
var (
plugins = make(map[string]func() Plugin)
pluginPorts = make(map[string][]int) // 存储插件端口信息
mutex sync.RWMutex
)
@ -68,6 +69,14 @@ func Register(name string, factory func() Plugin) {
plugins[name] = factory
}
// RegisterWithPorts 注册带端口信息的插件
func RegisterWithPorts(name string, factory func() Plugin, ports []int) {
mutex.Lock()
defer mutex.Unlock()
plugins[name] = factory
pluginPorts[name] = ports
}
// Get 获取插件实例
func Get(name string) Plugin {
mutex.RLock()
@ -91,6 +100,17 @@ func All() []string {
return names
}
// GetPluginPorts 获取插件端口列表
func GetPluginPorts(name string) []int {
mutex.RLock()
defer mutex.RUnlock()
if ports, exists := pluginPorts[name]; exists {
return ports
}
return []int{} // 返回空列表表示适用于所有端口
}
// GenerateCredentials 生成测试凭据 - 从services包移到这里统一管理
func GenerateCredentials(service string) []Credential {
users := common.Userdict[service]

View File

@ -21,7 +21,8 @@ type ScanResult = plugins.Result
// 向后兼容的函数
func RegisterLocalPlugin(name string, creator func() Plugin) {
plugins.Register(name, func() plugins.Plugin {
return &localPluginAdapter{creator()}
localPlugin := creator()
return &localPluginAdapter{localPlugin}
})
}

View File

@ -21,9 +21,13 @@ type Credential = plugins.Credential
// 注册函数:适配旧插件到新系统
func RegisterPlugin(name string, factory func() Plugin) {
plugins.Register(name, func() plugins.Plugin {
// 获取插件端口信息
plugin := factory()
ports := plugin.GetPorts()
plugins.RegisterWithPorts(name, func() plugins.Plugin {
return &servicePluginAdapter{factory()}
})
}, ports)
}
// 获取函数

View File

@ -70,11 +70,8 @@ func (p *WebTitlePlugin) Scan(ctx context.Context, info *common.HostInfo) *WebSc
func (p *WebTitlePlugin) getWebTitle(ctx context.Context, info *common.HostInfo) (string, int, string, error) {
protocol := "http"
if info.Ports == "443" || info.Ports == "8443" {
protocol = "https"
}
// 智能协议检测
protocol := p.detectProtocol(info)
url := fmt.Sprintf("%s://%s:%s", protocol, info.Host, info.Ports)
client := &http.Client{
@ -107,6 +104,30 @@ func (p *WebTitlePlugin) getWebTitle(ctx context.Context, info *common.HostInfo)
return title, resp.StatusCode, resp.Header.Get("Server"), nil
}
// detectProtocol 智能检测HTTP/HTTPS协议
func (p *WebTitlePlugin) detectProtocol(info *common.HostInfo) string {
port := info.Ports
// 已知的HTTPS端口
httpsPorts := []string{"443", "8443", "9443"}
for _, httpsPort := range httpsPorts {
if port == httpsPort {
return "https"
}
}
// 常见HTTP端口优先使用HTTP
httpPorts := []string{"80", "8080", "8000", "8888", "8090", "3000", "5000", "9000"}
for _, httpPort := range httpPorts {
if port == httpPort {
return "http"
}
}
// 对于其他端口优先尝试HTTP
return "http"
}
func (p *WebTitlePlugin) extractTitle(html string) string {
titleRe := regexp.MustCompile(`(?i)<title[^>]*>([^<]+)</title>`)
matches := titleRe.FindStringSubmatch(html)