diff --git a/Common/Flag.go b/Common/Flag.go index a715381..e2dbde2 100644 --- a/Common/Flag.go +++ b/Common/Flag.go @@ -297,6 +297,10 @@ func parseCommandLineArgs() { // 检查参数冲突 checkParameterConflicts() + + // 额外的本地插件互斥检查 + // 需要在解析后检查,因为Host是通过Info.Host设置的 + // 这个检查在app/initializer.go中进行 } // parseEnvironmentArgs 安全地解析环境变量中的参数 @@ -386,7 +390,7 @@ func shouldShowHelp(Info *HostInfo) bool { return false } -// checkParameterConflicts 检查参数冲突和兼容性 +// checkParameterConflicts 检查参数冲突和兼容性 func checkParameterConflicts() { // 检查 -ao 和 -m icmp 同时指定的情况(向后兼容提示) if AliveOnly && ScanMode == "icmp" { @@ -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) - } + // 验证本地插件名称 - 使用统一插件系统验证 + // 这里不进行验证,让运行时的插件系统来处理不存在的插件 } } diff --git a/core/BaseScanStrategy.go b/core/BaseScanStrategy.go index 80be35d..21fc8f5 100644 --- a/core/BaseScanStrategy.go +++ b/core/BaseScanStrategy.go @@ -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 diff --git a/core/LocalScanner.go b/core/LocalScanner.go index fbe87d2..87dd7b7 100644 --- a/core/LocalScanner.go +++ b/core/LocalScanner.go @@ -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") diff --git a/core/ServiceScanner.go b/core/ServiceScanner.go index 8126181..508f420 100644 --- a/core/ServiceScanner.go +++ b/core/ServiceScanner.go @@ -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 -} diff --git a/main.go b/main.go index 0c0a230..7d398e0 100644 --- a/main.go +++ b/main.go @@ -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() diff --git a/plugins/init.go b/plugins/init.go index 875f02c..5da694f 100644 --- a/plugins/init.go +++ b/plugins/init.go @@ -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] diff --git a/plugins/local/types.go b/plugins/local/types.go index fa6e0bd..bee3fd1 100644 --- a/plugins/local/types.go +++ b/plugins/local/types.go @@ -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} }) } diff --git a/plugins/services/types.go b/plugins/services/types.go index b3e23ab..50010a8 100644 --- a/plugins/services/types.go +++ b/plugins/services/types.go @@ -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) } // 获取函数 diff --git a/plugins/web/webtitle.go b/plugins/web/webtitle.go index b821d2f..abc0c31 100644 --- a/plugins/web/webtitle.go +++ b/plugins/web/webtitle.go @@ -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)]*>([^<]+)`) matches := titleRe.FindStringSubmatch(html)