Files
fscan/core/service_scanner.go
T
ZacharyZcR d7749c4766 refactor: context 穿透扫描生命周期,修复长驻插件阻塞和 Web Stop 无效
- RunScan 接受 context.Context,创建可取消上下文并穿透到所有策略和插件
- 长驻插件(forwardshell/socks5proxy/reverseshell)不再进入 scan WaitGroup,
  通过 ctx.Done() 管理生命周期,解除 wg.Wait() 死锁
- Web Stop API 从 stopChan 改为 context.CancelFunc,取消信号真正传播到扫描链路
- ExecuteScanTasks 和 executeScanTask 支持 context 取消检查,停止分发新任务
- CLI 模式传 context.Background(),行为完全不变
2026-04-27 19:04:26 +08:00

314 lines
9.2 KiB
Go

package core
import (
"context"
"fmt"
"strconv"
"strings"
"sync"
"github.com/shadow1ng/fscan/common"
"github.com/shadow1ng/fscan/common/i18n"
"github.com/shadow1ng/fscan/common/parsers"
)
// ServiceScanStrategy 服务扫描策略
type ServiceScanStrategy struct {
*BaseScanStrategy
}
// NewServiceScanStrategy 创建新的服务扫描策略
func NewServiceScanStrategy() *ServiceScanStrategy {
return &ServiceScanStrategy{
BaseScanStrategy: NewBaseScanStrategy("服务扫描", FilterService),
}
}
// LogPluginInfo 重写以提供基于端口的插件过滤
func (s *ServiceScanStrategy) LogPluginInfo(config *common.Config) {
// 需要从命令行参数获取端口信息来进行过滤
// 如果没有指定端口,使用默认端口进行过滤显示
ports := common.GetFlagVars().Ports
if ports == "" || ports == "all" {
// 默认端口扫描:显示所有插件
s.BaseScanStrategy.LogPluginInfo(config)
} else {
// 指定端口扫描:只显示匹配的插件
s.showPluginsForSpecifiedPorts(config)
}
}
// showPluginsForSpecifiedPorts 显示指定端口的匹配插件
func (s *ServiceScanStrategy) showPluginsForSpecifiedPorts(config *common.Config) {
allPlugins, isCustomMode := s.GetPlugins(config)
// 解析端口
ports := s.parsePortList(common.GetFlagVars().Ports)
if len(ports) == 0 {
s.BaseScanStrategy.LogPluginInfo(config)
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 {
common.LogInfo(i18n.Tr("service_plugin_custom", pluginStr))
} else {
common.LogInfo(i18n.Tr("service_plugin_info", pluginStr))
}
} else {
common.LogInfo(i18n.GetText("service_plugin_none"))
}
}
// parsePortList 解析端口列表
func (s *ServiceScanStrategy) parsePortList(portStr string) []int {
if portStr == "" || portStr == "all" {
return []int{}
}
ports := []int{} // 初始化为空切片而非nil
parts := strings.Split(portStr, ",")
for _, part := range parts {
part = strings.TrimSpace(part)
if port, err := strconv.Atoi(part); err == nil {
// 验证端口范围 1-65535(与 scanner.go 的 parsePort 保持一致)
if port >= 1 && port <= 65535 {
ports = append(ports, port)
} else {
common.LogError(i18n.Tr("port_out_of_range", port))
}
}
}
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, config *common.Config, state *common.State, info common.HostInfo, ch chan struct{}, wg *sync.WaitGroup) {
// 验证扫描目标(需要同时检查 -h 和 -hf 参数)
fv := common.GetFlagVars()
if info.Host == "" && fv.HostsFile == "" {
common.LogError(i18n.GetText("parse_error_target_empty"))
return
}
// 输出扫描开始信息
s.LogScanStart()
// 验证插件配置
if err := s.ValidateConfiguration(); err != nil {
common.LogError(err.Error())
return
}
// 输出插件信息(重写以提供端口过滤)
s.LogPluginInfo(config)
// 执行主机扫描流程
s.performHostScan(ctx, config, state, info, ch, wg)
}
// performHostScan 执行主机扫描的完整流程
func (s *ServiceScanStrategy) performHostScan(ctx context.Context, config *common.Config, state *common.State, info common.HostInfo, ch chan struct{}, wg *sync.WaitGroup) {
// 发现目标主机和端口
targetInfos, err := s.discoverTargets(info.Host, info, config, state)
if err != nil {
common.LogError(err.Error())
return
}
// 执行漏洞扫描
if len(targetInfos) > 0 {
ExecuteScanTasks(ctx, config, state, targetInfos, s, ch, wg)
}
}
// PrepareTargets 准备目标信息
func (s *ServiceScanStrategy) PrepareTargets(info common.HostInfo, config *common.Config, state *common.State) []common.HostInfo {
// 发现目标主机和端口
targetInfos, err := s.discoverTargets(info.Host, info, config, state)
if err != nil {
common.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, ", ")))
} else {
common.LogInfo(i18n.GetText("scan_no_service_plugins"))
}
}
// =============================================================================
// 端口发现功能(从 PortDiscoveryService 合并)
// =============================================================================
// discoverTargets 发现目标主机和端口
func (s *ServiceScanStrategy) discoverTargets(hostInput string, baseInfo common.HostInfo, config *common.Config, state *common.State) ([]common.HostInfo, error) {
// 标准流程:解析目标主机
fv := common.GetFlagVars()
hosts, err := parsers.ParseIP(hostInput, fv.HostsFile, fv.ExcludeHosts)
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(hosts, false, config, state)
common.LogInfo(i18n.Tr("alive_hosts_count_info", len(hosts)))
}
// 端口扫描
alivePorts := s.discoverAlivePorts(hosts, config, state)
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 发现存活的端口
func (s *ServiceScanStrategy) discoverAlivePorts(hosts []string, config *common.Config, state *common.State) []string {
var alivePorts []string
// 如果已经有明确指定的host:port,直接使用(让后续SmartIdentify统一验证和识别)
hostPorts := state.GetHostPorts()
if len(hostPorts) > 0 {
alivePorts = hostPorts
common.LogInfo(i18n.Tr("alive_ports_count", len(alivePorts)))
state.ClearHostPorts()
return alivePorts
}
// 根据扫描模式选择端口扫描方式
if len(hosts) > 0 {
alivePorts = EnhancedPortScan(hosts, config.Target.Ports, int64(config.Timeout.Seconds()), config, state)
}
return alivePorts
}
// convertToTargetInfos 将端口列表转换为目标信息
func (s *ServiceScanStrategy) convertToTargetInfos(ports []string, baseInfo common.HostInfo) []common.HostInfo {
var infos []common.HostInfo
for _, targetIP := range ports {
hostParts := strings.Split(targetIP, ":")
if len(hostParts) != 2 {
common.LogError(i18n.Tr("invalid_target_format", targetIP))
continue
}
// 去除空格并过滤空值
host := strings.TrimSpace(hostParts[0])
portStr := strings.TrimSpace(hostParts[1])
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
}