package core import ( "context" "fmt" "net" "strconv" "strings" "sync" "github.com/shadow1ng/fscan/common" "github.com/shadow1ng/fscan/common/i18n" "github.com/shadow1ng/fscan/common/parsers" "github.com/shadow1ng/fscan/plugins" ) // ServiceScanStrategy 服务扫描策略 type ServiceScanStrategy struct { *BaseScanStrategy } // NewServiceScanStrategy 创建新的服务扫描策略 func NewServiceScanStrategy() *ServiceScanStrategy { return &ServiceScanStrategy{ BaseScanStrategy: NewBaseScanStrategy(i18n.GetText("scan_strategy_service_name"), FilterService), } } // LogPluginInfo 重写以提供基于端口的插件过滤 func (s *ServiceScanStrategy) LogPluginInfo(config *common.Config, session *common.ScanSession) { // 需要从命令行参数获取端口信息来进行过滤 // 如果没有指定端口,使用默认端口进行过滤显示 ports := config.Target.Ports if ports == "" || ports == "all" { // 默认端口扫描:显示所有插件 s.BaseScanStrategy.LogPluginInfo(config, session) } else { // 指定端口扫描:只显示匹配的插件 s.showPluginsForSpecifiedPorts(config, session) } } // showPluginsForSpecifiedPorts 显示指定端口的匹配插件 func (s *ServiceScanStrategy) showPluginsForSpecifiedPorts(config *common.Config, session *common.ScanSession) { allPlugins, isCustomMode := s.GetPlugins(config) // 解析端口 ports := s.parsePortList(config.Target.Ports) if len(ports) == 0 { s.BaseScanStrategy.LogPluginInfo(config, session) return } // 收集所有匹配的插件(去重) pluginSet := make(map[string]struct{}, len(allPlugins)) for _, port := range ports { for _, pluginName := range allPlugins { if s.pluginExists(pluginName) { if s.isPluginApplicableToPort(pluginName, port) && s.isPluginPassesFilterType(pluginName, isCustomMode, config) { pluginSet[pluginName] = struct{}{} } } } } // 转换为列表 var applicablePlugins []string for pluginName := range pluginSet { applicablePlugins = append(applicablePlugins, pluginName) } // 输出结果(仅在有匹配插件时显示,避免因预检端口不完整而输出误导性的"无可用插件") if len(applicablePlugins) > 0 { pluginStr := formatPluginList(applicablePlugins) if isCustomMode { session.LogInfo(i18n.Tr("service_plugin_custom", pluginStr)) } else { session.LogInfo(i18n.Tr("service_plugin_info", pluginStr)) } } } // parsePortList 解析端口列表 func (s *ServiceScanStrategy) parsePortList(portStr string) []int { if portStr == "" || portStr == "all" { return []int{} } ports := parsers.ParsePort(portStr) if ports == nil { return []int{} } return ports } // Name 返回策略名称 func (s *ServiceScanStrategy) Name() string { return i18n.GetText("scan_strategy_service_name") } // Description 返回策略描述 func (s *ServiceScanStrategy) Description() string { return i18n.GetText("scan_strategy_service_desc") } // Execute 执行服务扫描策略 func (s *ServiceScanStrategy) Execute(ctx context.Context, session *common.ScanSession, info common.HostInfo, ch chan struct{}, wg *sync.WaitGroup) { config := session.Config // 验证扫描目标(需要同时检查 -h 和 -hf 参数) if info.Host == "" && session.Params.HostsFile == "" { session.LogError(i18n.GetText("parse_error_target_empty")) return } // 输出扫描开始信息 s.LogScanStart(session) // 验证插件配置 if err := s.ValidateConfiguration(); err != nil { session.LogError(err.Error()) return } // 输出插件信息(重写以提供端口过滤) s.LogPluginInfo(config, session) // 执行主机扫描流程 s.performHostScan(ctx, session, info, ch, wg) } // performHostScan 执行主机扫描的完整流程 // pipeline 模式:端口扫描和插件执行并行,扫到开放端口立即开始跑插件 func (s *ServiceScanStrategy) performHostScan(ctx context.Context, session *common.ScanSession, info common.HostInfo, ch chan struct{}, wg *sync.WaitGroup) { config := session.Config state := session.State excludes, err := loadHostExcludes(session.Params) if err != nil { session.LogError(fmt.Sprintf("%s: %v", i18n.GetText("parse_target_failed"), err)) return } iter, err := parsers.NewHostIterator(info.Host, session.Params.HostsFile, excludes...) if err != nil { session.LogError(fmt.Sprintf("%s: %v", i18n.GetText("parse_target_failed"), err)) return } defer func() { _ = iter.Close() }() pluginsToRun, isCustomMode := s.GetPlugins(config) totalAlive := 0 sawHosts := false performedLiveness := false envProfiled := false // 系统能力探测(不需要网络目标) sysProfile := ProbeSystem() for { hosts, err := iter.NextBatch(ctx, targetHostBatchSize(config)) if err != nil { if ctx.Err() != nil { session.LogError(i18n.Tr("global_timeout_exceeded", int(config.GlobalTimeout.Seconds()))) return } session.LogError(fmt.Sprintf("%s: %v", i18n.GetText("parse_target_failed"), err)) return } if len(hosts) == 0 { break } sawHosts = true if s.shouldPerformLivenessCheck(hosts, config) { performedLiveness = true hosts = CheckLive(ctx, hosts, false, session) } totalAlive += len(hosts) if len(hosts) == 0 { continue } // 首批 alive hosts 出来后做网络探测,调整后续所有参数 if !envProfiled { envProfiled = true netProfile := ProbeNetwork(ctx, hosts, session) ep := &EnvironmentProfile{Net: *netProfile, System: sysProfile} ep.TuneConfig(config, session) } // UDP 插件调度:默认端口模式全量调度,用户指定 -p 时只调度端口有交集的 UDP 插件 s.dispatchUDPPlugins(ctx, session, hosts, info, config, ch, wg) s.scanHostBatch(ctx, session, hosts, info, pluginsToRun, isCustomMode, ch, wg) } if sawHosts && performedLiveness { session.LogInfo(i18n.Tr("alive_hosts_count_info", totalAlive)) } if !sawHosts && len(state.GetHostPorts()) == 0 { return } // 合并预设的 host:port hostPorts := state.GetHostPorts() if len(hostPorts) > 0 { merged := mergeHostPorts(nil, hostPorts) targets := s.convertToTargetInfos(merged, info) for _, target := range targets { for _, pluginName := range pluginsToRun { if s.IsPluginApplicableByName(pluginName, target.Host, target.Port, isCustomMode, config) { executeScanTask(ctx, session, pluginName, target, ch, wg) } } } state.ClearHostPorts() } } func (s *ServiceScanStrategy) scanHostBatch(ctx context.Context, session *common.ScanSession, hosts []string, info common.HostInfo, pluginsToRun []string, isCustomMode bool, ch chan struct{}, wg *sync.WaitGroup) { config := session.Config stream := make(chan string, 64) go EnhancedPortScan(ctx, hosts, config.Target.Ports, int64(config.Timeout.Seconds()), session, stream) cancelled := false for addr := range stream { if cancelled { continue } select { case <-ctx.Done(): cancelled = true continue default: } infos := s.convertToTargetInfos([]string{addr}, info) for _, target := range infos { for _, pluginName := range pluginsToRun { if s.IsPluginApplicableByName(pluginName, target.Host, target.Port, isCustomMode, config) { executeScanTask(ctx, session, pluginName, target, ch, wg) } } } } } // dispatchUDPPlugins 分发UDP协议插件,跳过TCP端口扫描链路 func (s *ServiceScanStrategy) dispatchUDPPlugins(ctx context.Context, session *common.ScanSession, hosts []string, baseInfo common.HostInfo, config *common.Config, ch chan struct{}, wg *sync.WaitGroup) { _, isCustomMode := s.GetPlugins(config) var udpPlugins []string if isCustomMode { // custom mode: 只跑用户指定的 UDP 插件 requested, _ := s.GetPlugins(config) for _, name := range requested { if plugins.IsUDP(name) { udpPlugins = append(udpPlugins, name) } } } else { // auto mode: 跑所有已注册的安全 UDP 插件 for _, name := range plugins.All() { if plugins.IsUDP(name) && plugins.IsSafe(name) { udpPlugins = append(udpPlugins, name) } } } if len(udpPlugins) == 0 { return } // 用户指定 -p 时,只调度端口有交集的 UDP 插件 var userPorts map[int]bool if config.Target.Ports != "" && config.Target.Ports != "all" { parsed := parsers.ParsePort(config.Target.Ports) userPorts = make(map[int]bool, len(parsed)) for _, p := range parsed { userPorts[p] = true } } for _, host := range hosts { for _, pluginName := range udpPlugins { for _, port := range plugins.GetPluginPorts(pluginName) { if userPorts != nil && !userPorts[port] { continue } target := baseInfo target.Host = host target.Port = port executeScanTask(ctx, session, pluginName, target, ch, wg) } } } } // PrepareTargets 准备目标信息 func (s *ServiceScanStrategy) PrepareTargets(info common.HostInfo, session *common.ScanSession) []common.HostInfo { // 发现目标主机和端口 targetInfos, err := s.discoverTargets(context.Background(), info.Host, info, session) if err != nil { session.LogError(err.Error()) return nil } return targetInfos } // LogVulnerabilityPluginInfo 输出服务扫描插件信息 func (s *ServiceScanStrategy) LogVulnerabilityPluginInfo(targets []common.HostInfo, config *common.Config) { allPlugins, isCustomMode := s.GetPlugins(config) // 获取实际会被使用的插件列表 servicePluginSet := make(map[string]struct{}, len(allPlugins)) for _, pluginName := range allPlugins { // 使用统一插件系统检查插件存在性 if !s.pluginExists(pluginName) { continue } // 检查插件是否通过过滤器类型检查 if !s.isPluginPassesFilterType(pluginName, isCustomMode, config) { continue } // 检查插件是否适用于任意一个目标 for _, target := range targets { if target.Port == 0 { continue } // 使用 host:port 信息检查插件适用性(Web插件需要host信息) if s.isPluginApplicableToPortWithHost(pluginName, target.Host, target.Port) { servicePluginSet[pluginName] = struct{}{} break // 只要适用于一个目标就添加 } } } // 转换为切片 var servicePlugins []string for pluginName := range servicePluginSet { servicePlugins = append(servicePlugins, pluginName) } if len(servicePlugins) > 0 { common.LogInfo(i18n.Tr("service_plugin_info", strings.Join(servicePlugins, ", "))) } } // ============================================================================= // 端口发现功能(从 PortDiscoveryService 合并) // ============================================================================= // discoverTargets 发现目标主机和端口 func (s *ServiceScanStrategy) discoverTargets(ctx context.Context, hostInput string, baseInfo common.HostInfo, session *common.ScanSession) ([]common.HostInfo, error) { config := session.Config state := session.State // 标准流程:解析目标主机 excludes, err := loadHostExcludes(session.Params) if err != nil { return nil, fmt.Errorf("%s: %w", i18n.GetText("parse_target_failed"), err) } hosts, err := parsers.ParseIP(hostInput, session.Params.HostsFile, excludes...) if err != nil { return nil, fmt.Errorf("%s: %w", i18n.GetText("parse_target_failed"), err) } var targetInfos []common.HostInfo // 主机存活性检测和端口扫描 if len(hosts) > 0 || len(state.GetHostPorts()) > 0 { // 主机存活检测 if s.shouldPerformLivenessCheck(hosts, config) { hosts = CheckLive(ctx, hosts, false, session) session.LogInfo(i18n.Tr("alive_hosts_count_info", len(hosts))) } // 端口扫描 alivePorts := s.discoverAlivePorts(ctx, hosts, session) if len(alivePorts) > 0 { targetInfos = s.convertToTargetInfos(alivePorts, baseInfo) } } return targetInfos, nil } // shouldPerformLivenessCheck 判断是否需要执行存活性检测 func (s *ServiceScanStrategy) shouldPerformLivenessCheck(hosts []string, config *common.Config) bool { return !config.DisablePing && len(hosts) > 1 } // discoverAlivePorts 发现存活的端口 // 执行正常端口扫描后,合并预设的 host:port(来自项目缓存或 CLI),确保不遗漏 func (s *ServiceScanStrategy) discoverAlivePorts(ctx context.Context, hosts []string, session *common.ScanSession) []string { config := session.Config state := session.State var alivePorts []string // 正常端口扫描 if len(hosts) > 0 { alivePorts = EnhancedPortScan(ctx, hosts, config.Target.Ports, int64(config.Timeout.Seconds()), session, nil) } // 合并预设的 host:port(项目缓存 / CLI 注入) hostPorts := state.GetHostPorts() if len(hostPorts) > 0 { alivePorts = mergeHostPorts(alivePorts, hostPorts) session.LogInfo(i18n.Tr("alive_ports_count", len(alivePorts))) state.ClearHostPorts() } return alivePorts } // mergeHostPorts 合并两个 host:port 列表并去重 func mergeHostPorts(a, b []string) []string { seen := make(map[string]struct{}, len(a)+len(b)) for _, s := range a { seen[s] = struct{}{} } for _, s := range b { seen[s] = struct{}{} } result := make([]string, 0, len(seen)) for s := range seen { result = append(result, s) } return result } // convertToTargetInfos 将端口列表转换为目标信息 func (s *ServiceScanStrategy) convertToTargetInfos(ports []string, baseInfo common.HostInfo) []common.HostInfo { var infos []common.HostInfo for _, targetIP := range ports { targetIP = strings.TrimSpace(targetIP) host, portStr, err := net.SplitHostPort(targetIP) if err != nil && strings.Count(targetIP, ":") == 1 { parts := strings.SplitN(targetIP, ":", 2) host, portStr = parts[0], parts[1] err = nil } if err != nil { common.LogError(i18n.Tr("invalid_target_format", targetIP)) continue } // 去除空格并过滤空值 host = strings.TrimSpace(host) portStr = strings.TrimSpace(portStr) if host == "" || portStr == "" { common.LogError(i18n.Tr("invalid_target_format", targetIP)) continue } // 验证端口范围(与scanner.go中parsePort保持一致) port, err := strconv.Atoi(portStr) if err != nil { common.LogError(i18n.Tr("host_port_invalid", host, portStr)) continue } if port < 1 || port > 65535 { common.LogError(i18n.Tr("host_port_out_of_range", host, port)) continue } info := baseInfo info.Host = host info.Port = port // 深拷贝Info避免多个target共享slice底层数组 if len(baseInfo.Info) > 0 { info.Info = append([]string(nil), baseInfo.Info...) } infos = append(infos, info) } return infos }