mirror of
https://github.com/shadow1ng/fscan.git
synced 2026-09-22 19:21:52 +08:00
## 架构重构
- 全局变量消除,迁移至 Config/State 对象
- SMB 插件融合(smb/smb2/smbghost/smbinfo)
- 服务探测重构,实现 Nmap 风格 fallback 机制
- 输出系统重构,TXT 实时刷盘 + 双写机制
- i18n 框架升级至 go-i18n
## 性能优化
- 正则表达式预编译
- 内存优化 map[string]struct{}
- 并发指纹匹配
- SOCKS5 连接复用
- 滑动窗口调度 + 自适应线程池
## 新功能
- Web 管理界面
- 多格式 POC 适配(xray/afrog)
- 增强指纹库(3139条)
- Favicon hash 指纹识别
- 插件选择性编译(Build Tags)
- fscan-lab 靶场环境
- 默认端口扩展(62→133)
## 构建系统
- 添加 no_local tag 支持排除本地插件
- 多版本构建:fscan/fscan-nolocal/fscan-web
- CI 添加 snapshot 模式支持仅测试构建
## Bug 修复
- 修复 120+ 个问题,包括 RDP panic、批量扫描漏报、
JSON 输出格式、Redis 检测、Context 超时等
## 测试增强
- 单元测试覆盖率 74-100%
- 并发安全测试
- 集成测试(Web/端口/服务/SSH/ICMP)
328 lines
9.2 KiB
Go
328 lines
9.2 KiB
Go
package core
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/shadow1ng/fscan/common"
|
|
"github.com/shadow1ng/fscan/common/i18n"
|
|
"github.com/shadow1ng/fscan/plugins"
|
|
)
|
|
|
|
// PluginFilterType 插件过滤类型
|
|
type PluginFilterType int
|
|
|
|
const (
|
|
// FilterNone 不过滤
|
|
FilterNone PluginFilterType = iota
|
|
// FilterLocal 仅本地插件
|
|
FilterLocal
|
|
// FilterService 仅服务插件(排除本地)
|
|
FilterService
|
|
// FilterWeb 仅Web插件
|
|
FilterWeb
|
|
)
|
|
|
|
// BaseScanStrategy 扫描策略基础类
|
|
type BaseScanStrategy struct {
|
|
strategyName string
|
|
filterType PluginFilterType
|
|
}
|
|
|
|
// NewBaseScanStrategy 创建基础扫描策略
|
|
func NewBaseScanStrategy(name string, filterType PluginFilterType) *BaseScanStrategy {
|
|
return &BaseScanStrategy{
|
|
strategyName: name,
|
|
filterType: filterType,
|
|
}
|
|
}
|
|
|
|
// GetPlugins 获取插件列表
|
|
func (b *BaseScanStrategy) GetPlugins(config *common.Config) ([]string, bool) {
|
|
scanMode := config.Mode
|
|
// 如果指定了特定插件且不是"all"
|
|
if scanMode != "" && scanMode != "all" {
|
|
requestedPlugins := parsePluginList(scanMode)
|
|
if len(requestedPlugins) == 0 {
|
|
requestedPlugins = []string{scanMode}
|
|
}
|
|
|
|
// 验证插件是否存在
|
|
var validPlugins []string
|
|
var missingPlugins []string
|
|
for _, name := range requestedPlugins {
|
|
if b.pluginExists(name) {
|
|
validPlugins = append(validPlugins, name)
|
|
} else {
|
|
missingPlugins = append(missingPlugins, name)
|
|
}
|
|
}
|
|
|
|
// 警告用户显式指定的插件不存在
|
|
// 注意:使用fmt.Fprintf直接输出到stderr,确保错误消息不会被日志级别过滤
|
|
for _, name := range missingPlugins {
|
|
errMsg := i18n.Tr("scan_plugin_not_found", name)
|
|
fmt.Fprintf(os.Stderr, "[ERROR] %s\n", errMsg)
|
|
}
|
|
|
|
return validPlugins, true
|
|
}
|
|
|
|
// 未指定或使用"all":根据策略类型获取对应插件
|
|
return b.getPluginsByFilterType(), false
|
|
}
|
|
|
|
// IsPluginApplicableByName 根据插件名称判断是否适用
|
|
func (b *BaseScanStrategy) IsPluginApplicableByName(pluginName string, targetHost string, targetPort int, isCustomMode bool, config *common.Config) bool {
|
|
// 首先检查插件是否存在
|
|
if !b.pluginExists(pluginName) {
|
|
return false
|
|
}
|
|
|
|
// 检查端口匹配和过滤器类型
|
|
return b.isPluginApplicableToPortWithHost(pluginName, targetHost, targetPort) && b.isPluginPassesFilterType(pluginName, isCustomMode, config)
|
|
}
|
|
|
|
func (b *BaseScanStrategy) pluginExists(pluginName string) bool {
|
|
return plugins.Exists(pluginName)
|
|
}
|
|
|
|
func (b *BaseScanStrategy) getPluginPorts(pluginName string) []int {
|
|
return plugins.GetPluginPorts(pluginName)
|
|
}
|
|
|
|
func (b *BaseScanStrategy) isWebPlugin(pluginName string) bool {
|
|
return plugins.HasType(pluginName, plugins.PluginTypeWeb)
|
|
}
|
|
|
|
func (b *BaseScanStrategy) isLocalPlugin(pluginName string) bool {
|
|
return plugins.HasType(pluginName, plugins.PluginTypeLocal)
|
|
}
|
|
|
|
func (b *BaseScanStrategy) isLocalPluginExplicitlySpecified(pluginName string, config *common.Config) bool {
|
|
return config.LocalPlugin == pluginName
|
|
}
|
|
|
|
// isPluginApplicableToPortWithHost 检查插件是否适用于指定端口
|
|
func (b *BaseScanStrategy) isPluginApplicableToPortWithHost(pluginName string, targetHost string, targetPort int) bool {
|
|
if b.isWebPlugin(pluginName) {
|
|
return IsMarkedWebService(targetHost, targetPort)
|
|
}
|
|
|
|
pluginPorts := b.getPluginPorts(pluginName)
|
|
|
|
// 无端口限制的插件适用于所有端口
|
|
if len(pluginPorts) == 0 {
|
|
return true
|
|
}
|
|
|
|
// 有端口限制的插件:检查端口匹配
|
|
if targetPort > 0 {
|
|
for _, port := range pluginPorts {
|
|
if port == targetPort {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func (b *BaseScanStrategy) isPluginApplicableToPort(pluginName string, targetPort int) bool {
|
|
return b.isPluginApplicableToPortWithHost(pluginName, "", targetPort)
|
|
}
|
|
|
|
// isPluginPassesFilterType 检查插件是否通过过滤器类型检查
|
|
func (b *BaseScanStrategy) isPluginPassesFilterType(pluginName string, isCustomMode bool, config *common.Config) bool {
|
|
// 自定义模式下强制运行所有明确指定的插件
|
|
if isCustomMode {
|
|
return true
|
|
}
|
|
|
|
// 应用过滤器类型检查
|
|
switch b.filterType {
|
|
case FilterLocal:
|
|
// 本地扫描策略:只允许本地插件且必须通过-local参数明确指定
|
|
if b.isLocalPlugin(pluginName) {
|
|
return b.isLocalPluginExplicitlySpecified(pluginName, config)
|
|
}
|
|
return false
|
|
case FilterService:
|
|
// 服务扫描策略:排除本地插件
|
|
return !b.isLocalPlugin(pluginName)
|
|
case FilterWeb:
|
|
// Web扫描策略:只允许Web插件
|
|
return b.isWebPlugin(pluginName)
|
|
default:
|
|
// 无过滤器:本地插件需要明确指定,其他插件都允许
|
|
if b.isLocalPlugin(pluginName) {
|
|
return b.isLocalPluginExplicitlySpecified(pluginName, config)
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
|
|
// LogPluginInfo 输出插件信息
|
|
func (b *BaseScanStrategy) LogPluginInfo(config *common.Config) {
|
|
allPlugins, isCustomMode := b.GetPlugins(config)
|
|
|
|
var prefix string
|
|
switch b.filterType {
|
|
case FilterLocal:
|
|
prefix = i18n.GetText("concurrency_local_plugin")
|
|
case FilterService:
|
|
prefix = i18n.GetText("concurrency_service_plugin")
|
|
case FilterWeb:
|
|
prefix = i18n.GetText("concurrency_web_plugin")
|
|
default:
|
|
prefix = i18n.GetText("concurrency_plugin")
|
|
}
|
|
|
|
if len(allPlugins) > 0 {
|
|
pluginStr := formatPluginList(allPlugins)
|
|
if isCustomMode {
|
|
common.LogBase(i18n.Tr("plugins_custom_specified", prefix, pluginStr))
|
|
} else {
|
|
common.LogBase(i18n.Tr("plugins_info", prefix, pluginStr))
|
|
}
|
|
} else {
|
|
common.LogBase(i18n.Tr("plugins_none", prefix))
|
|
}
|
|
}
|
|
|
|
// formatPluginList 格式化插件列表(超过5个时精简显示)
|
|
func formatPluginList(plugins []string) string {
|
|
if len(plugins) <= 5 {
|
|
return strings.Join(plugins, ", ")
|
|
}
|
|
return fmt.Sprintf("%s ... 等%d个", strings.Join(plugins[:5], ", "), len(plugins))
|
|
}
|
|
|
|
// LogPluginInfoWithPort 带端口信息的插件显示
|
|
func (b *BaseScanStrategy) LogPluginInfoWithPort(targetHost string, targetPort int, config *common.Config) {
|
|
allPlugins, isCustomMode := b.GetPlugins(config)
|
|
|
|
var prefix string
|
|
switch b.filterType {
|
|
case FilterLocal:
|
|
prefix = i18n.GetText("concurrency_local_plugin")
|
|
case FilterService:
|
|
prefix = i18n.GetText("concurrency_service_plugin")
|
|
case FilterWeb:
|
|
prefix = i18n.GetText("concurrency_web_plugin")
|
|
default:
|
|
prefix = i18n.GetText("concurrency_plugin")
|
|
}
|
|
|
|
// 过滤适用的插件
|
|
var applicablePlugins []string
|
|
for _, pluginName := range allPlugins {
|
|
if b.pluginExists(pluginName) {
|
|
if b.IsPluginApplicableByName(pluginName, targetHost, targetPort, isCustomMode, config) {
|
|
applicablePlugins = append(applicablePlugins, pluginName)
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(applicablePlugins) > 0 {
|
|
pluginStr := formatPluginList(applicablePlugins)
|
|
if isCustomMode {
|
|
common.LogBase(i18n.Tr("plugins_custom_specified", prefix, pluginStr))
|
|
} else {
|
|
common.LogBase(i18n.Tr("plugins_info", prefix, pluginStr))
|
|
}
|
|
} else {
|
|
common.LogBase(i18n.Tr("plugins_none", prefix))
|
|
}
|
|
}
|
|
|
|
// ValidateConfiguration 验证扫描配置
|
|
func (b *BaseScanStrategy) ValidateConfiguration() error {
|
|
return nil
|
|
}
|
|
|
|
// LogScanStart 输出扫描开始信息(已精简,仅在非服务扫描模式下显示)
|
|
func (b *BaseScanStrategy) LogScanStart() {
|
|
// 服务扫描模式下不显示(插件信息已足够说明)
|
|
// 仅在本地/Web等特殊模式下显示
|
|
switch b.filterType {
|
|
case FilterLocal:
|
|
common.LogBase(i18n.GetText("start_local_scan"))
|
|
case FilterWeb:
|
|
common.LogBase(i18n.GetText("start_web_scan"))
|
|
}
|
|
}
|
|
|
|
// getPluginsByFilterType 根据过滤器类型获取插件列表
|
|
func (b *BaseScanStrategy) getPluginsByFilterType() []string {
|
|
allPlugins := plugins.All()
|
|
var filteredPlugins []string
|
|
|
|
switch b.filterType {
|
|
case FilterLocal:
|
|
// 本地扫描策略:只返回本地插件
|
|
for _, pluginName := range allPlugins {
|
|
if b.isLocalPlugin(pluginName) {
|
|
filteredPlugins = append(filteredPlugins, pluginName)
|
|
}
|
|
}
|
|
case FilterService:
|
|
// 服务扫描策略:排除本地插件和纯Web插件,保留服务插件
|
|
for _, pluginName := range allPlugins {
|
|
if !b.isLocalPlugin(pluginName) {
|
|
filteredPlugins = append(filteredPlugins, pluginName)
|
|
}
|
|
}
|
|
case FilterWeb:
|
|
// Web扫描策略:只返回Web插件
|
|
for _, pluginName := range allPlugins {
|
|
if b.isWebPlugin(pluginName) {
|
|
filteredPlugins = append(filteredPlugins, pluginName)
|
|
}
|
|
}
|
|
// 确保 webtitle 在 webpoc 之前执行,避免指纹识别竞态
|
|
sort.Slice(filteredPlugins, func(i, j int) bool {
|
|
// webtitle 必须在 webpoc 之前
|
|
if filteredPlugins[i] == "webtitle" {
|
|
return true
|
|
}
|
|
if filteredPlugins[j] == "webtitle" {
|
|
return false
|
|
}
|
|
if filteredPlugins[i] == "webpoc" {
|
|
return false
|
|
}
|
|
if filteredPlugins[j] == "webpoc" {
|
|
return true
|
|
}
|
|
// 其他插件保持字母顺序
|
|
return filteredPlugins[i] < filteredPlugins[j]
|
|
})
|
|
default:
|
|
// 无过滤器:返回所有插件
|
|
filteredPlugins = allPlugins
|
|
}
|
|
|
|
return filteredPlugins
|
|
}
|
|
|
|
// parsePluginList 解析插件列表字符串
|
|
func parsePluginList(pluginStr string) []string {
|
|
if pluginStr == "" {
|
|
return []string{}
|
|
}
|
|
|
|
// 支持逗号分隔的插件列表
|
|
plugins := strings.Split(pluginStr, ",")
|
|
result := []string{} // 初始化为空切片而非nil
|
|
for _, plugin := range plugins {
|
|
plugin = strings.TrimSpace(plugin)
|
|
if plugin != "" {
|
|
result = append(result, plugin)
|
|
}
|
|
}
|
|
return result
|
|
}
|