fscan/Core/Scanner.go
2025-04-14 02:36:16 +08:00

627 lines
16 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package Core
import (
"fmt"
"github.com/schollz/progressbar/v3"
"github.com/shadow1ng/fscan/Common"
"github.com/shadow1ng/fscan/WebScan/lib"
"sort"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
)
// 全局状态
var (
LocalScan bool // 本地扫描模式标识
WebScan bool // Web扫描模式标识
Mutex = &sync.Mutex{} // 用于保护共享资源
)
// ScanTask 表示单个扫描任务
type ScanTask struct {
pluginName string // 插件名称
target Common.HostInfo // 目标信息
}
// 添加一个本地插件集合,用于识别哪些插件是本地信息收集插件
var localPlugins = map[string]bool{
"localinfo": true,
"dcinfo": true,
"minidump": true,
}
// -----------------------------------------------------------------------------
// 主扫描流程
// -----------------------------------------------------------------------------
// Scan 执行整体扫描流程的入口函数
func Scan(info Common.HostInfo) {
Common.LogInfo("开始信息扫描")
lib.Inithttp()
// 并发控制初始化
ch := make(chan struct{}, Common.ThreadNum)
wg := sync.WaitGroup{}
// 选择并执行扫描模式
selectScanMode(info, &ch, &wg)
// 等待所有扫描完成
wg.Wait()
finishScan()
}
// 根据配置选择扫描模式
func selectScanMode(info Common.HostInfo, ch *chan struct{}, wg *sync.WaitGroup) {
switch {
case Common.LocalMode:
LocalScan = true
executeLocalScan(info, ch, wg)
case len(Common.URLs) > 0:
WebScan = true
executeWebScan(info, ch, wg)
default:
executeHostScan(info, ch, wg)
}
}
// 完成扫描并输出结果
func finishScan() {
if Common.ProgressBar != nil {
Common.ProgressBar.Finish()
fmt.Println()
}
Common.LogSuccess(fmt.Sprintf("扫描已完成: %v/%v", Common.End, Common.Num))
}
// -----------------------------------------------------------------------------
// 三种扫描模式实现
// -----------------------------------------------------------------------------
// 执行本地信息收集
func executeLocalScan(info Common.HostInfo, ch *chan struct{}, wg *sync.WaitGroup) {
Common.LogInfo("执行本地信息收集")
// 验证插件配置
if err := validateScanPlugins(); err != nil {
Common.LogError(err.Error())
return
}
// 输出插件信息
logPluginInfo()
// 执行扫描任务
executeScanTasks([]Common.HostInfo{info}, ch, wg)
}
// 执行Web扫描
func executeWebScan(info Common.HostInfo, ch *chan struct{}, wg *sync.WaitGroup) {
Common.LogInfo("开始Web扫描")
// 验证插件配置
if err := validateScanPlugins(); err != nil {
Common.LogError(err.Error())
return
}
// 准备URL目标
targetInfos := prepareURLTargets(info)
// 输出插件信息
logPluginInfo()
// 执行扫描任务
executeScanTasks(targetInfos, ch, wg)
}
// 执行主机扫描
func executeHostScan(info Common.HostInfo, ch *chan struct{}, wg *sync.WaitGroup) {
// 验证扫描目标
if info.Host == "" {
Common.LogError("未指定扫描目标")
return
}
// 验证插件配置
if err := validateScanPlugins(); err != nil {
Common.LogError(err.Error())
return
}
// 解析目标主机
hosts, err := Common.ParseIP(info.Host, Common.HostsFile, Common.ExcludeHosts)
if err != nil {
Common.LogError(fmt.Sprintf("解析主机错误: %v", err))
return
}
Common.LogInfo("开始主机扫描")
// 输出插件信息
logPluginInfo()
// 执行主机扫描
performHostScan(hosts, info, ch, wg)
}
// -----------------------------------------------------------------------------
// 主机扫描流程详细实现
// -----------------------------------------------------------------------------
// 执行主机扫描的完整流程
func performHostScan(hosts []string, info Common.HostInfo, ch *chan struct{}, wg *sync.WaitGroup) {
var targetInfos []Common.HostInfo
// 主机存活性检测和端口扫描
if len(hosts) > 0 || len(Common.HostPort) > 0 {
// 主机存活检测
if shouldPerformLivenessCheck(hosts) {
hosts = CheckLive(hosts, Common.UsePing)
Common.LogInfo(fmt.Sprintf("存活主机数量: %d", len(hosts)))
}
// 端口扫描
targetInfos = scanPortsAndPrepareTargets(hosts, info)
}
// 添加URL目标
targetInfos = appendURLTargets(targetInfos, info)
// 执行漏洞扫描
if len(targetInfos) > 0 {
Common.LogInfo("开始漏洞扫描")
executeScanTasks(targetInfos, ch, wg)
}
}
// 判断是否需要执行存活性检测
func shouldPerformLivenessCheck(hosts []string) bool {
return Common.DisablePing == false && len(hosts) > 1
}
// 扫描端口并准备目标信息
func scanPortsAndPrepareTargets(hosts []string, info Common.HostInfo) []Common.HostInfo {
// 扫描存活端口
alivePorts := discoverAlivePorts(hosts)
if len(alivePorts) == 0 {
return nil
}
// 转换为目标信息
return convertToTargetInfos(alivePorts, info)
}
// 发现存活的端口
func discoverAlivePorts(hosts []string) []string {
var alivePorts []string
// 根据扫描模式选择端口扫描方式
if WebScan || len(Common.URLs) > 0 {
alivePorts = NoPortScan(hosts, Common.Ports)
} else if len(hosts) > 0 {
alivePorts = PortScan(hosts, Common.Ports, Common.Timeout)
Common.LogInfo(fmt.Sprintf("存活端口数量: %d", len(alivePorts)))
}
// 合并额外指定的端口
if len(Common.HostPort) > 0 {
alivePorts = append(alivePorts, Common.HostPort...)
alivePorts = Common.RemoveDuplicate(alivePorts)
Common.HostPort = nil
Common.LogInfo(fmt.Sprintf("存活端口数量: %d", len(alivePorts)))
}
return alivePorts
}
// -----------------------------------------------------------------------------
// 插件管理和解析
// -----------------------------------------------------------------------------
// getAllLocalPlugins 返回所有本地插件的名称列表
func getAllLocalPlugins() []string {
var localPluginList []string
for plugin := range localPlugins {
localPluginList = append(localPluginList, plugin)
}
sort.Strings(localPluginList)
return localPluginList
}
// parsePluginList 解析逗号分隔的插件列表
// pluginStr: 逗号分隔的插件字符串,如 "ssh,ftp,telnet"
// 返回: 插件名称的字符串切片
func parsePluginList(pluginStr string) []string {
if pluginStr == "" {
return nil
}
// 按逗号分割并去除每个插件名称两端的空白
plugins := strings.Split(pluginStr, ",")
for i, p := range plugins {
plugins[i] = strings.TrimSpace(p)
}
// 过滤空字符串
var result []string
for _, p := range plugins {
if p != "" {
result = append(result, p)
}
}
return result
}
// validateScanPlugins 验证扫描插件的有效性
// 返回: 错误信息
func validateScanPlugins() error {
// 如果未指定扫描模式或使用All模式则无需验证
if Common.ScanMode == "" || Common.ScanMode == "All" {
return nil
}
// 解析插件列表
plugins := parsePluginList(Common.ScanMode)
if len(plugins) == 0 {
plugins = []string{Common.ScanMode}
}
// 验证每个插件是否有效
var invalidPlugins []string
for _, plugin := range plugins {
if _, exists := Common.PluginManager[plugin]; !exists {
invalidPlugins = append(invalidPlugins, plugin)
}
}
if len(invalidPlugins) > 0 {
return fmt.Errorf("无效的插件: %s", strings.Join(invalidPlugins, ", "))
}
// 如果是本地模式,验证是否包含非本地插件
if Common.LocalMode {
var nonLocalPlugins []string
for _, plugin := range plugins {
if !isLocalPlugin(plugin) {
nonLocalPlugins = append(nonLocalPlugins, plugin)
}
}
if len(nonLocalPlugins) > 0 {
Common.LogInfo(fmt.Sprintf("本地模式下,以下非本地插件将被忽略: %s", strings.Join(nonLocalPlugins, ", ")))
}
}
return nil
}
// isLocalPlugin 判断插件是否为本地信息收集插件
func isLocalPlugin(pluginName string) bool {
return localPlugins[pluginName]
}
// getPluginsToRun 获取要执行的插件列表
// 返回: 插件列表和是否为自定义插件模式
func getPluginsToRun() ([]string, bool) {
// 本地模式处理
if Common.LocalMode {
// 在本地模式下只执行本地插件
// 如果指定了特定插件(单个或多个)
if Common.ScanMode != "" && Common.ScanMode != "All" {
requestedPlugins := parsePluginList(Common.ScanMode)
if len(requestedPlugins) == 0 {
requestedPlugins = []string{Common.ScanMode}
}
// 过滤出本地插件
var localPluginsToRun []string
for _, plugin := range requestedPlugins {
if isLocalPlugin(plugin) {
localPluginsToRun = append(localPluginsToRun, plugin)
}
}
return localPluginsToRun, true
}
// 如果是All模式或未指定则返回所有本地插件
return getAllLocalPlugins(), true
}
// 非本地模式处理(保持原有行为)
// 如果指定了插件列表(逗号分隔)
if Common.ScanMode != "" && Common.ScanMode != "All" {
plugins := parsePluginList(Common.ScanMode)
if len(plugins) > 0 {
return plugins, true
}
return []string{Common.ScanMode}, true
}
// 默认情况:使用所有非本地插件
allPlugins := GetAllPlugins()
filteredPlugins := make([]string, 0, len(allPlugins))
for _, plugin := range allPlugins {
if !isLocalPlugin(plugin) {
filteredPlugins = append(filteredPlugins, plugin)
}
}
return filteredPlugins, false
}
// logPluginInfo 输出插件信息
func logPluginInfo() {
if Common.LocalMode {
if Common.ScanMode == "" || Common.ScanMode == "All" {
Common.LogInfo("本地模式: 使用所有本地信息收集插件")
} else {
plugins := parsePluginList(Common.ScanMode)
if len(plugins) == 0 {
plugins = []string{Common.ScanMode}
}
// 过滤出本地插件
var localPluginsToRun []string
for _, plugin := range plugins {
if isLocalPlugin(plugin) {
localPluginsToRun = append(localPluginsToRun, plugin)
}
}
if len(localPluginsToRun) > 1 {
Common.LogInfo(fmt.Sprintf("本地模式: 使用本地插件: %s", strings.Join(localPluginsToRun, ", ")))
} else if len(localPluginsToRun) == 1 {
Common.LogInfo(fmt.Sprintf("本地模式: 使用本地插件: %s", localPluginsToRun[0]))
} else {
Common.LogInfo("本地模式: 未指定有效的本地插件,将不执行任何扫描")
}
}
return
}
// 非本地模式的原有逻辑
if Common.ScanMode == "" || Common.ScanMode == "All" {
Common.LogInfo("使用所有可用插件(已排除本地敏感插件)")
} else {
plugins := parsePluginList(Common.ScanMode)
if len(plugins) > 1 {
Common.LogInfo(fmt.Sprintf("使用插件: %s", strings.Join(plugins, ", ")))
} else {
Common.LogInfo(fmt.Sprintf("使用插件: %s", Common.ScanMode))
}
}
}
// -----------------------------------------------------------------------------
// 目标准备
// -----------------------------------------------------------------------------
// 准备URL目标列表
func prepareURLTargets(baseInfo Common.HostInfo) []Common.HostInfo {
var targetInfos []Common.HostInfo
for _, url := range Common.URLs {
urlInfo := baseInfo
// 确保URL包含协议头
if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
url = "http://" + url
}
urlInfo.Url = url
targetInfos = append(targetInfos, urlInfo)
}
return targetInfos
}
// 将端口列表转换为目标信息
func 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(fmt.Sprintf("无效的目标地址格式: %s", targetIP))
continue
}
info := baseInfo
info.Host = hostParts[0]
info.Ports = hostParts[1]
infos = append(infos, info)
}
return infos
}
// 添加URL扫描目标
func appendURLTargets(targetInfos []Common.HostInfo, baseInfo Common.HostInfo) []Common.HostInfo {
for _, url := range Common.URLs {
urlInfo := baseInfo
urlInfo.Url = url
targetInfos = append(targetInfos, urlInfo)
}
return targetInfos
}
// -----------------------------------------------------------------------------
// 任务执行
// -----------------------------------------------------------------------------
// 执行扫描任务集合
func executeScanTasks(targets []Common.HostInfo, ch *chan struct{}, wg *sync.WaitGroup) {
// 获取要执行的插件
pluginsToRun, isCustomMode := getPluginsToRun()
// 准备扫描任务
tasks := prepareScanTasks(targets, pluginsToRun, isCustomMode)
// 输出扫描计划
if Common.ShowScanPlan && len(tasks) > 0 {
logScanPlan(tasks)
}
// 初始化进度条
if len(tasks) > 0 && Common.ShowProgress {
initProgressBar(len(tasks))
}
// 执行所有任务
for _, task := range tasks {
scheduleScanTask(task.pluginName, task.target, ch, wg)
}
}
// logScanPlan 输出扫描计划信息
func logScanPlan(tasks []ScanTask) {
// 统计每个插件的目标数量
pluginCounts := make(map[string]int)
for _, task := range tasks {
pluginCounts[task.pluginName]++
}
// 构建扫描计划信息
var planInfo strings.Builder
planInfo.WriteString("扫描计划:\n")
for plugin, count := range pluginCounts {
planInfo.WriteString(fmt.Sprintf(" - %s: %d 个目标\n", plugin, count))
}
Common.LogInfo(planInfo.String())
}
// 准备扫描任务列表
func prepareScanTasks(targets []Common.HostInfo, pluginsToRun []string, isCustomMode bool) []ScanTask {
var tasks []ScanTask
for _, target := range targets {
targetPort, _ := strconv.Atoi(target.Ports)
for _, pluginName := range pluginsToRun {
plugin, exists := Common.PluginManager[pluginName]
if !exists {
continue
}
// 检查插件是否适用于当前目标
if isPluginApplicable(plugin, targetPort, isCustomMode, pluginName) {
tasks = append(tasks, ScanTask{
pluginName: pluginName,
target: target,
})
}
}
}
return tasks
}
// isPluginApplicable 判断插件是否适用于目标
func isPluginApplicable(plugin Common.ScanPlugin, targetPort int, isCustomMode bool, pluginName string) bool {
// 本地模式下,只执行本地插件
if LocalScan {
return isLocalPlugin(pluginName)
}
// 非本地模式下,本地插件特殊处理
if isLocalPlugin(pluginName) {
// 只有在自定义模式下明确指定时才执行本地插件
return isCustomMode
}
// 特殊扫描模式下的处理
if WebScan || isCustomMode {
return true
}
// 端口匹配检查
// 无端口限制的插件或端口匹配的插件
return len(plugin.Ports) == 0 || plugin.HasPort(targetPort)
}
// 初始化进度条
func initProgressBar(totalTasks int) {
Common.ProgressBar = progressbar.NewOptions(totalTasks,
progressbar.OptionEnableColorCodes(true),
progressbar.OptionShowCount(),
progressbar.OptionSetWidth(15),
progressbar.OptionSetDescription("[cyan]扫描进度:[reset]"),
progressbar.OptionSetTheme(progressbar.Theme{
Saucer: "[green]=[reset]",
SaucerHead: "[green]>[reset]",
SaucerPadding: " ",
BarStart: "[",
BarEnd: "]",
}),
progressbar.OptionThrottle(65*time.Millisecond),
progressbar.OptionUseANSICodes(true),
progressbar.OptionSetRenderBlankState(true),
)
}
// 调度单个扫描任务
func scheduleScanTask(pluginName string, target Common.HostInfo, ch *chan struct{}, wg *sync.WaitGroup) {
wg.Add(1)
*ch <- struct{}{} // 获取并发槽位
go func() {
startTime := time.Now()
defer func() {
// 捕获并记录任何可能的panic
if r := recover(); r != nil {
Common.LogError(fmt.Sprintf("[PANIC] 插件 %s 扫描 %s:%s 时崩溃: %v",
pluginName, target.Host, target.Ports, r))
}
// 完成任务,释放资源
duration := time.Since(startTime)
if Common.ShowScanPlan {
Common.LogInfo(fmt.Sprintf("完成 %s 扫描 %s:%s (耗时: %.2fs)",
pluginName, target.Host, target.Ports, duration.Seconds()))
}
wg.Done()
<-*ch // 释放并发槽位
}()
atomic.AddInt64(&Common.Num, 1)
executeSingleScan(pluginName, target)
updateProgress()
}()
}
// 执行单个扫描
func executeSingleScan(pluginName string, info Common.HostInfo) {
plugin, exists := Common.PluginManager[pluginName]
if !exists {
Common.LogInfo(fmt.Sprintf("扫描类型 %v 无对应插件,已跳过", pluginName))
return
}
if err := plugin.ScanFunc(&info); err != nil {
Common.LogError(fmt.Sprintf("扫描错误 %v:%v - %v", info.Host, info.Ports, err))
}
}
// 更新扫描进度
func updateProgress() {
Common.OutputMutex.Lock()
defer Common.OutputMutex.Unlock()
atomic.AddInt64(&Common.End, 1)
if Common.ProgressBar != nil {
fmt.Print("\033[2K\r")
Common.ProgressBar.Add(1)
}
}