Compare commits

..

29 Commits

Author SHA1 Message Date
ZacharyZcR
da981cdbce refactor: 正确组织平台特定插件注册
- 将avdetect从跨平台插件移至Windows特有插件
- 将dcinfo从跨平台插件移至Windows特有插件
- 优化Registry.go插件分类和注释
- 确保平台特定功能在正确的registry文件中注册

插件分布:
- Registry.go: 真正跨平台的服务和本地插件
- registry_windows.go: Windows特有的系统功能和持久化插件
- registry_linux.go: Linux特有的持久化插件
2025-08-12 18:16:07 +08:00
ZacharyZcR
75a056809c refactor: 移除无效的-noredis参数
- 删除DisableRedis字段定义和flag声明
- 移除相关i18n消息
- 更新PARAMETERS.md文档,删除-noredis参数说明
- 该参数已失效且在代码中无任何实际作用

现在如需禁用Redis扫描,应通过扫描模式或端口排除实现
2025-08-12 18:09:44 +08:00
ZacharyZcR
4473d1a28f refactor: 将-fingerprint参数缩短为-fp
- 优化命令行参数长度,提升用户体验
- 更新PARAMETERS.md文档中的参数说明
- 同时清理了已移除的-ne参数文档
2025-08-12 18:02:45 +08:00
ZacharyZcR
074aebfeec refactor: 简化插件利用架构,移除不必要的exploit控制参数
- 移除-ne (DisableExploit)参数及其相关控制逻辑
- 统一服务插件exploit实现为空函数,保持接口一致性
- MySQL/FTP/Cassandra/ActiveMQ插件exploit方法改为空实现
- Redis插件移除-ne控制,利用功能由专用参数控制(-rwp/-rf/-rs)
- VNC插件exploit功能改为空实现,专注于服务检测和弱密码扫描
- 更新插件描述,准确反映实际功能范围

所有exploit功能现在由明确的用户参数控制,无需通用禁用开关
2025-08-12 17:56:06 +08:00
ZacharyZcR
1cb1e8b2aa docs: 更新PARAMETERS.md以反映v2.2.1版本新增功能
- 更新版本信息至v2.2.1
- 添加host:port格式支持的说明和示例
- 新增SOCKS5代理参数到HTTP设置部分
- 增加新增功能改进章节,详细说明智能目标识别功能
- 更新使用示例,包含新的host:port格式用法
- 添加版本更新说明章节,记录v2.2.1的主要改进
2025-08-12 17:23:12 +08:00
ZacharyZcR
2703ddd9ed feat: 增强目标解析功能支持host:port格式
- 修复目标地址验证逻辑,支持host:port格式的直接输入
- 优化端口扫描流程,对明确指定的host:port跳过不必要的端口扫描
- 改进主机地址解析,智能识别和处理host:port格式
- 统一端口扫描结果显示,避免显示不一致问题
- 增强用户体验,支持 -h 127.0.0.1:135 等便捷格式
2025-08-12 17:20:59 +08:00
ZacharyZcR
cc4f55374a fix: 修复Web扫描功能无法正常工作的问题
- 修复URL正则表达式支持IP地址格式
- 修复URL解析后正确设置到HostInfo.Url字段
- 修复Web扫描目标Host和Port字段的提取
- 修复Web插件适用性检查逻辑,允许Web扫描策略直接执行Web插件
- 完善URL到目标信息的转换,包含协议默认端口处理
2025-08-12 17:03:38 +08:00
ZacharyZcR
0eb0a8f14a chore: 更新版本号到v2.2.1
- 版本从2.2.0更新到2.2.1
- 反映新增的内存监控功能
2025-08-12 16:31:31 +08:00
ZacharyZcR
245e3d0a12 feat: 在进度条中添加内存使用量监控
- 添加实时内存使用量显示(MB)
- 实现颜色编码:绿色(<50MB)、黄色(50-100MB)、红色(>100MB)
- 优化性能:限制内存统计更新频率为每秒一次
- 支持无颜色模式兼容性
- 在所有进度条状态下显示内存信息
2025-08-12 16:27:39 +08:00
ZacharyZcR
701306ee5f feat: 增强进度条活跃指示器,防止用户误判程序卡死
- 添加动态活跃指示器:活跃时显示●,长时间无更新时显示旋转动画
- 实现智能更新机制:2秒内有更新显示静态圆点,超时切换到旋转指示
- 独立goroutine处理活跃指示器,500ms更新间隔
- 优化渲染逻辑:避免频繁更新时重复渲染,提升性能
- 完善资源清理:正确停止ticker和关闭channel
- 改善用户体验:明确表明程序正在运行,消除卡死疑虑
2025-08-12 16:16:51 +08:00
ZacharyZcR
defe5b0733 refactor: 清理未使用的死代码函数
- 移除hostinfo_ext.go中7个未使用函数
- 移除target.go中7个未使用函数
- 清理多余空行,提升代码简洁性
- 保持核心功能完整,仅删除确认未被调用的函数
- 消除所有死代码编译警告
2025-08-12 16:12:47 +08:00
ZacharyZcR
792a075172 fix: 修复目标主机重复显示的问题
- 解决updateGlobalVariables中主机列表重复追加的逻辑错误
- 当info.Host已有值时不再重复设置解析结果
- 修复容器管理日志中显示"127.0.0.1,127.0.0.1"的问题
- 保持从文件读取主机时的正常功能
2025-08-12 16:09:06 +08:00
ZacharyZcR
c6bfb5f064 feat: 优化智能Web检测性能,避免重复调用
- 添加检测结果缓存机制,避免同一端口重复HTTP探测
- 使用线程安全的读写锁保护缓存访问
- 显著减少网络请求次数,提升扫描速度
- 支持多主机扫描的缓存键设计
- 保持架构兼容性的同时优化性能表现
2025-08-12 16:05:20 +08:00
ZacharyZcR
b706fb46bb refactor: 统一端口组定义并支持多种端口组扫描
- 新增多个预定义端口组常量到base/Constants.go
- 实现-p web/main/db/service/common/all参数支持
- 删除重复的DefaultPorts常量和database端口组
- 合并GetPortGroups和GetTargetPortGroups函数避免重复
- 统一所有端口组定义到统一位置便于维护
2025-08-12 15:56:09 +08:00
ZacharyZcR
a206e2c16e fix: 从默认端口列表中移除161端口
- 将SNMP端口161从MainPorts常量中移除避免默认扫描
- 保持用户可通过-p 161手动指定扫描SNMP服务
- 默认扫描端口数量从58个减少至57个
- 提升扫描效率减少不必要的SNMP端口探测
2025-08-12 14:55:00 +08:00
ZacharyZcR
b463984e78 refactor: 重构main.go入口文件并引入依赖注入架构
- 创建app包实现依赖注入容器和初始化器模式
- 重构main.go为六阶段清晰的初始化流程
- 新增结构化错误处理替代简陋的os.Exit调用
- 为HostInfo添加辅助函数增强功能但保持向后兼容
- 引入TargetInfo包装器支持上下文和元数据管理
- 优化代码组织提升可维护性和可测试性
2025-08-12 14:37:28 +08:00
ZacharyZcR
338dd60c3e feat: 实现智能Web服务检测系统
- 扩展Web端口识别范围至40+个常见端口
- 新增HTTP/HTTPS协议智能探测机制
- 实现并发协议检测提升性能
- 添加精确错误分析避免误判
- 完善插件过滤确保Web插件仅在检测到Web服务时执行
- 支持端口模式匹配和响应头分析
2025-08-12 14:20:57 +08:00
ZacharyZcR
0cc29afbeb fix: 修复服务扫描插件列表显示本地插件的问题
修复了服务扫描模式下插件列表错误包含本地插件的显示问题:

- 在ServiceScanner的插件过滤逻辑中正确排除本地插件
- 统一新插件架构和传统插件系统的过滤规则
- 保留Web插件的智能检测功能(当检测到Web端口时自动包含)

修复前显示包含:cleaner, keylogger, winschtask, minidump 等本地插件
修复后只显示:ssh, mysql, redis, vnc 等真正的服务插件和适用的Web插件

提升了用户体验的准确性和扫描日志的可读性。
2025-08-12 13:51:26 +08:00
ZacharyZcR
7e4e3dbd1b refactor: 清理死代码函数以提升代码质量
移除了36个静态分析工具识别的未使用函数,减少代码体积534行:

核心模块:
- core/PluginAdapter.go: 移除7个未使用的插件适配器方法
- core/Registry.go: 移除5个未使用的注册器方法
- core/Scanner.go: 移除1个未使用的扫描方法
- core/WebDetection.go: 移除4个未使用的Web检测方法

通用模块:
- common/ConcurrencyMonitor.go: 移除3个未使用的并发监控方法
- common/common.go: 移除1个未使用的插件注册方法
- common/base/Plugin.go: 移除4个未使用的插件管理方法
- common/parsers/Simple.go: 移除1个未使用的端口解析方法
- common/utils/memmonitor.go: 移除9个未使用的内存监控方法
- common/utils/slicepool.go: 移除1个未使用的切片去重方法

插件模块:
- plugins/services/vnc/plugin.go: 移除1个未使用的服务识别方法

所有移除的函数均替换为中文注释标记,保持代码可读性。
2025-08-12 13:22:12 +08:00
ZacharyZcR
4a3f281b6b refactor: 统一Plugins目录大小写为小写
- 将所有Plugins路径重命名为plugins
- 修复Git索引与实际文件系统大小写不一致问题
- 确保跨平台兼容性和路径一致性
2025-08-12 13:08:06 +08:00
ZacharyZcR
cddbb9e7e4 refactor: 统一Core目录大小写为小写 2025-08-12 13:04:14 +08:00
ZacharyZcR
3e4cd2466e feat: 实现完整的VNC插件支持
- 新增VNC远程桌面协议检测和利用插件
- 实现RFB协议连接器支持版本识别和认证
- 支持无认证访问检测和弱密码暴力破解
- 添加VNC服务风险评估和信息收集功能
- 支持标准VNC端口范围(5900-5909)
- 在插件注册系统中集成VNC服务扫描

功能特性: 服务识别、安全检测、利用验证、风险评估
2025-08-12 12:08:30 +08:00
ZacharyZcR
f097d2812a refactor: 清理项目死代码和未使用函数
- 移除所有未使用的generateCredentials方法
- 删除插件适配器中的过时函数
- 清理MySQL连接器中的无用方法
- 移除Redis利用器中的未调用函数
- 删除遗留加密函数和基础扫描器无用方法
- 完全移除未注册的VNC插件
- 优化代码结构,提升项目可维护性

清理统计: 移除25+个死代码函数,减少400+行无用代码
2025-08-12 11:51:36 +08:00
ZacharyZcR
b89bf4b0da refactor: 规范本地插件编译标签和注册架构
- 为所有平台特定插件添加正确的编译标签
  - Linux插件添加 //go:build linux 标签
  - Windows插件添加 //go:build windows 标签
- 重构本地插件注册方式
  - 从main.go移至core包统一管理
  - 创建平台特定的注册文件实现条件编译
- 修正参数文档中minidump平台支持描述
- 优化插件注册架构,提升代码组织性
2025-08-11 21:36:14 +08:00
ZacharyZcR
7ca20cbebe docs: 更新SOCKS5代理支持文档
- 明确说明代理支持范围:HTTP代理仅Web扫描,SOCKS5代理支持所有服务
- 添加代理失败自动回退的说明
- 更新使用示例,展示SOCKS5代理在不同场景的应用
- 重新组织代理相关参数的文档结构
2025-08-11 21:17:42 +08:00
ZacharyZcR
60eb65766a fix: 为WrapperTcpWithContext和WrapperTlsWithContext添加SOCKS5代理支持
- 修复核心TCP包装函数,检测并使用SOCKS5代理配置
- 添加TLS连接的代理支持
- 实现优雅降级:代理失败时自动回退到直连
- 添加详细的调试日志记录代理使用情况
- 修复影响PostgreSQL、Cassandra、Telnet、LDAP、IMAP等服务插件的代理功能

解决的问题:
- 之前只有WebScan和MySQL支持SOCKS5代理
- 现在所有使用WrapperTcpWithContext的服务都支持代理
- 保持与现有MySQL自定义实现的兼容性
2025-08-11 21:16:28 +08:00
ZacharyZcR
3b6f2083de feat: 优化SOCKS5参数命名以提升清晰度
- 将 -socks5-port 重命名为 -start-socks5,更明确表达启动服务器的意图
- 修正 -socks5 参数描述为使用SOCKS5代理(客户端模式)
- 添加新的i18n消息 flag_start_socks5_server 区分两种用途
- 更新文档说明两个参数的不同用途和使用示例

参数对比:
  旧方式: -socks5-port 1080 (容易混淆)
  新方式: -start-socks5 1080 (启动代理服务器)
  区别于: -socks5 127.0.0.1:1080 (使用外部代理)
2025-08-11 21:02:47 +08:00
ZacharyZcR
285358772f refactor: 简化本地插件参数设计
- 移除 -localplugin 参数,使用 -local 直接指定插件名称
- 简化用户体验:go run main.go -local cleaner
- 保持相同的验证逻辑和错误提示
- 更新文档和帮助信息
- 减少参数数量:从71个减少到70个

使用示例:
  新方式: go run main.go -local cleaner
  旧方式: go run main.go -local -localplugin cleaner (已移除)
2025-08-11 20:55:50 +08:00
ZacharyZcR
c85370a7d9 fix: 替换硬编码插件列表为动态注册表查询
- 在main.go中添加initLocalPlugins函数,从插件注册表动态获取本地插件列表
- 修改Common/Flag.go使用动态插件列表进行验证
- 消除硬编码插件名称,支持自动发现新插件
- 插件列表按字母顺序排序,保持一致性
- 修复循环导入问题
- 添加cleaner插件自毁脚本到.gitignore
- 创建完整的参数说明文档
2025-08-11 20:42:02 +08:00
181 changed files with 2027 additions and 1770 deletions

5
.gitignore vendored
View File

@ -70,3 +70,8 @@ Todo列表.md
# Claude documentation / Claude文档
.claude_docs/
# Cleaner plugin artifacts / 清理插件产物
cleanup.bat
cleanup.sh
cleanup_script_*

View File

@ -99,28 +99,7 @@ func (m *ConcurrencyMonitor) FinishConnection(pluginName, target string) {
}
}
// 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
}
// 已移除未使用的 GetConnectionStats 方法
// GetTotalActiveConnections 获取总活跃连接数
func (m *ConcurrencyMonitor) GetTotalActiveConnections() int64 {
@ -135,16 +114,7 @@ func (m *ConcurrencyMonitor) GetTotalActiveConnections() int64 {
return total
}
// 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
})
}
// 已移除未使用的 Reset 方法
// GetConcurrencyStatus 获取并发状态字符串
func (m *ConcurrencyMonitor) GetConcurrencyStatus() string {
@ -164,28 +134,4 @@ func (m *ConcurrencyMonitor) GetConcurrencyStatus() string {
i18n.GetText("concurrency_connection"), totalConnections)
}
// 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
}
// 已移除未使用的 GetDetailedStatus 方法

View File

@ -20,6 +20,9 @@ var (
HostsFile string
PortsFile string
// 本地插件列表(由外部初始化)
LocalPluginsList []string
ModuleThreadNum int
GlobalTimeout int64
EnableFingerprint bool
@ -48,13 +51,11 @@ var (
RedisFile string
RedisShell string
DisableRedis bool
RedisWritePath string
RedisWriteContent string
RedisWriteFile string
DisableBrute bool
DisableExploit bool
MaxRetries int
DisableSave bool
@ -172,9 +173,8 @@ func Flag(Info *HostInfo) {
flag.Int64Var(&GlobalTimeout, "gt", 180, i18n.GetText("flag_global_timeout"))
// LiveTop 参数已移除,改为智能控制
flag.BoolVar(&DisablePing, "np", false, i18n.GetText("flag_disable_ping"))
flag.BoolVar(&EnableFingerprint, "fingerprint", false, i18n.GetText("flag_enable_fingerprint"))
flag.BoolVar(&LocalMode, "local", false, i18n.GetText("flag_local_mode"))
flag.StringVar(&LocalPlugin, "localplugin", "", i18n.GetText("flag_local_plugin"))
flag.BoolVar(&EnableFingerprint, "fp", false, i18n.GetText("flag_enable_fingerprint"))
flag.StringVar(&LocalPlugin, "local", "", "指定本地插件名称 (如: cleaner, avdetect, keylogger 等)")
flag.BoolVar(&AliveOnly, "ao", false, i18n.GetText("flag_alive_only"))
// ═════════════════════════════════════════════════
@ -216,7 +216,6 @@ func Flag(Info *HostInfo) {
// ═════════════════════════════════════════════════
flag.StringVar(&RedisFile, "rf", "", i18n.GetText("flag_redis_file"))
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(&RedisWriteContent, "rwc", "", i18n.GetText("flag_redis_write_content"))
flag.StringVar(&RedisWriteFile, "rwf", "", i18n.GetText("flag_redis_write_file"))
@ -225,7 +224,6 @@ func Flag(Info *HostInfo) {
// 暴力破解控制参数
// ═════════════════════════════════════════════════
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"))
// ═════════════════════════════════════════════════
@ -244,7 +242,7 @@ func Flag(Info *HostInfo) {
// ═════════════════════════════════════════════════
flag.StringVar(&Shellcode, "sc", "", i18n.GetText("flag_shellcode"))
flag.StringVar(&ReverseShellTarget, "rsh", "", i18n.GetText("flag_reverse_shell_target"))
flag.IntVar(&Socks5ProxyPort, "socks5-port", 0, i18n.GetText("flag_socks5_proxy"))
flag.IntVar(&Socks5ProxyPort, "start-socks5", 0, i18n.GetText("flag_start_socks5_server"))
flag.IntVar(&ForwardShellPort, "fsh-port", 4444, i18n.GetText("flag_forward_shell_port"))
flag.StringVar(&PersistenceTargetFile, "persistence-file", "", i18n.GetText("flag_persistence_file"))
flag.StringVar(&WinPEFile, "win-pe", "", i18n.GetText("flag_win_pe_file"))
@ -395,18 +393,14 @@ func checkParameterConflicts() {
LogBase(i18n.GetText("param_conflict_ao_icmp_both"))
}
// 检查本地模式和本地插件参数
if LocalMode {
if LocalPlugin == "" {
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)
}
// 检查本地插件参数
if LocalPlugin != "" {
// 自动启用本地模式
LocalMode = true
// 验证本地插件名称
validPlugins := []string{"avdetect", "fileinfo", "dcinfo", "minidump", "reverseshell", "socks5proxy", "forwardshell", "ldpreload", "shellenv", "crontask", "systemdservice", "winregistry", "winstartup", "winschtask", "winservice", "winwmi", "keylogger", "downloader", "cleaner"} // 已重构的插件
isValid := false
for _, valid := range validPlugins {
for _, valid := range LocalPluginsList {
if LocalPlugin == valid {
isValid = true
break
@ -415,14 +409,10 @@ func checkParameterConflicts() {
if !isValid {
fmt.Printf("错误: 无效的本地插件 '%s'\n", LocalPlugin)
fmt.Printf("可用的本地插件: avdetect, fileinfo, dcinfo, minidump, reverseshell, socks5proxy, forwardshell, ldpreload, shellenv, crontask, systemdservice, winregistry, winstartup, winschtask, winservice, winwmi, keylogger, downloader, cleaner\n")
if len(LocalPluginsList) > 0 {
fmt.Printf("可用的本地插件: %s\n", strings.Join(LocalPluginsList, ", "))
}
os.Exit(1)
}
}
// 如果指定了本地插件但未启用本地模式
if !LocalMode && LocalPlugin != "" {
fmt.Printf("错误: 指定本地插件 (-localplugin) 时必须启用本地模式 (-local)\n")
os.Exit(1)
}
}

View File

@ -263,15 +263,19 @@ func updateGlobalVariables(config *parsers.ParsedConfig, info *HostInfo) error {
// 更新目标相关全局变量
if config.Targets != nil {
if len(config.Targets.Hosts) > 0 {
// 如果info.Host已经有值说明解析结果来自info.Host不需要重复设置
// 只有当info.Host为空时才设置如从文件读取的情况
if info.Host == "" {
info.Host = joinStrings(config.Targets.Hosts, ",")
} else {
info.Host += "," + joinStrings(config.Targets.Hosts, ",")
}
}
if len(config.Targets.URLs) > 0 {
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 {

View File

@ -8,8 +8,12 @@ Ports.go - 端口常量(向后兼容层)
此文件保持向后兼容实际常量定义已迁移到Core/Constants.go
*/
// 向后兼容的端口常量 - 引用Core包中的定义
// 向后兼容的端口常量 - 引用base包中的定义
var (
WebPorts = base.WebPorts // Web服务端口组
MainPorts = base.MainPorts // 主要服务端口组
WebPorts = base.WebPorts // Web服务端口组
MainPorts = base.MainPorts // 主要服务端口组
DbPorts = base.DbPorts // 数据库端口组
ServicePorts = base.ServicePorts // 服务端口组
CommonPorts = base.CommonPorts // 常用端口组
AllPorts = base.AllPorts // 全部端口
)

View File

@ -3,6 +3,7 @@ package common
import (
"fmt"
"os"
"runtime"
"sync"
"time"
@ -31,11 +32,27 @@ type ProgressManager struct {
// 输出缓冲相关
outputMutex sync.Mutex
// 活跃指示器相关
spinnerIndex int
lastActivity time.Time
activityTicker *time.Ticker
stopActivityChan chan struct{}
// 内存监控相关
lastMemUpdate time.Time
memStats runtime.MemStats
}
var (
globalProgressManager *ProgressManager
progressMutex sync.Mutex
// 活跃指示器字符序列(旋转动画)
spinnerChars = []string{"|", "/", "-", "\\"}
// 活跃指示器更新间隔
activityUpdateInterval = 500 * time.Millisecond
)
// GetProgressManager 获取全局进度条管理器
@ -69,10 +86,16 @@ func (pm *ProgressManager) InitProgress(total int64, description string) {
pm.startTime = time.Now()
pm.isActive = true
pm.enabled = true
pm.lastActivity = time.Now()
pm.spinnerIndex = 0
pm.lastMemUpdate = time.Now().Add(-2 * time.Second) // 强制首次更新内存
// 为进度条保留空间
pm.setupProgressSpace()
// 启动活跃指示器
pm.startActivityIndicator()
// 初始显示进度条
pm.renderProgress()
}
@ -91,6 +114,9 @@ func (pm *ProgressManager) UpdateProgress(increment int64) {
pm.current = pm.total
}
// 更新活跃时间
pm.lastActivity = time.Now()
pm.renderProgress()
}
@ -110,6 +136,9 @@ func (pm *ProgressManager) FinishProgress() {
pm.current = pm.total
pm.renderProgress()
// 停止活跃指示器
pm.stopActivityIndicator()
// 显示完成信息
pm.showCompletionInfo()
@ -140,7 +169,9 @@ func (pm *ProgressManager) renderProgress() {
// generateProgressBar 生成进度条字符串
func (pm *ProgressManager) generateProgressBar() string {
if pm.total == 0 {
return fmt.Sprintf("%s: 等待中...", pm.description)
spinner := pm.getActivityIndicator()
memInfo := pm.getMemoryInfo()
return fmt.Sprintf("%s %s 等待中... %s", pm.description, spinner, memInfo)
}
percentage := float64(pm.current) / float64(pm.total) * 100
@ -197,16 +228,22 @@ func (pm *ProgressManager) generateProgressBar() string {
bar += "|"
}
// 生成活跃指示器
spinner := pm.getActivityIndicator()
// 构建基础进度条
baseProgress := fmt.Sprintf("%s %6.1f%% %s (%d/%d)%s%s",
pm.description, percentage, bar, pm.current, pm.total, speedStr, eta)
baseProgress := fmt.Sprintf("%s %s %6.1f%% %s (%d/%d)%s%s",
pm.description, spinner, percentage, bar, pm.current, pm.total, speedStr, eta)
// 添加内存信息
memInfo := pm.getMemoryInfo()
// 添加并发状态
if concurrencyStatus != "" {
return fmt.Sprintf("%s [%s]", baseProgress, concurrencyStatus)
return fmt.Sprintf("%s [%s] %s", baseProgress, concurrencyStatus, memInfo)
}
return baseProgress
return fmt.Sprintf("%s %s", baseProgress, memInfo)
}
// showCompletionInfo 显示完成信息
@ -326,4 +363,95 @@ func (pm *ProgressManager) renderProgressUnsafe() {
// 刷新输出
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)
}

View File

@ -12,8 +12,12 @@ Constants.go - 核心常量定义
// 预定义端口组常量
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"
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"
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"
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"
)
// =============================================================================

View File

@ -1,7 +1,6 @@
package base
import (
"fmt"
"sync"
)
@ -126,41 +125,7 @@ func (p *ScanPlugin) GetInfo() map[string]interface{} {
// 插件管理器方法
// =============================================================================
// 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
}
// 已移除未使用的 RegisterPlugin 方法
// ========================================================================================
// 未使用的插件管理器方法已删除(死代码清理)
@ -172,89 +137,11 @@ func (pm *PluginManager) RegisterPlugin(plugin *ScanPlugin) error {
// 全局插件管理函数 (保持向后兼容)
// =============================================================================
// RegisterPlugin 注册插件到全局管理器
func RegisterPlugin(name string, plugin ScanPlugin) error {
// 转换为新的插件结构
newPlugin := &ScanPlugin{
Name: name,
Ports: plugin.Ports,
Types: plugin.Types,
Enabled: true,
ScanFunc: plugin.ScanFunc,
}
// 注册到新的插件管理器
err := globalPluginManager.RegisterPlugin(newPlugin)
if err != nil {
return err
}
// 同时更新Legacy管理器以保持向后兼容
LegacyPluginManager[name] = plugin
return nil
}
// 已移除未使用的 RegisterPlugin 方法
// 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)
}
}
// 已移除未使用的 Clear 方法
// 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
}
}
}
// 已移除未使用的 ClearPluginsByType 方法
// GetGlobalPluginManager 方法已删除(死代码清理)

View File

@ -18,6 +18,7 @@ import (
"github.com/shadow1ng/fscan/common/base"
"github.com/shadow1ng/fscan/common/logging"
"github.com/shadow1ng/fscan/common/output"
"github.com/shadow1ng/fscan/common/proxy"
)
// =============================================================================
@ -29,12 +30,8 @@ type ScanPlugin = base.ScanPlugin
// 插件类型常量
const (
PluginTypeService = base.PluginTypeService
PluginTypeWeb = base.PluginTypeWeb
PluginTypeLocal = base.PluginTypeLocal
PluginTypeBrute = base.PluginTypeBrute
PluginTypePoc = base.PluginTypePoc
PluginTypeScan = base.PluginTypeScan
PluginTypeWeb = base.PluginTypeWeb
PluginTypeLocal = base.PluginTypeLocal
)
// 全局插件管理器
@ -44,12 +41,7 @@ var PluginManager = base.LegacyPluginManager
// 核心功能导出 - 直接调用对应模块
// =============================================================================
// 插件系统
func RegisterPlugin(name string, plugin ScanPlugin) {
if err := base.RegisterPlugin(name, plugin); err != nil {
LogError("Failed to register plugin " + name + ": " + err.Error())
}
}
// 已移除未使用的 RegisterPlugin 方法
// GetGlobalPluginManager 函数已删除(死代码清理)
@ -63,22 +55,22 @@ var loggerMutex sync.Mutex
func getGlobalLogger() *logging.Logger {
loggerMutex.Lock()
defer loggerMutex.Unlock()
if globalLogger == nil {
level := getLogLevelFromString(LogLevel)
config := &logging.LoggerConfig{
Level: level,
EnableColor: !NoColor,
SlowOutput: false,
ShowProgress: ShowProgress,
StartTime: StartTime,
Level: level,
EnableColor: !NoColor,
SlowOutput: false,
ShowProgress: ShowProgress,
StartTime: StartTime,
}
globalLogger = logging.NewLogger(config)
if ProgressBar != nil {
globalLogger.SetProgressBar(ProgressBar)
}
globalLogger.SetOutputMutex(&OutputMutex)
// 设置协调输出函数使用LogWithProgress
globalLogger.SetCoordinatedOutput(LogWithProgress)
}
@ -109,18 +101,18 @@ func getLogLevelFromString(levelStr string) logging.LogLevel {
}
// 日志函数
func InitLogger() {
func InitLogger() {
loggerMutex.Lock()
globalLogger = nil
loggerMutex.Unlock()
getGlobalLogger().Initialize()
getGlobalLogger().Initialize()
}
func LogDebug(msg string) { getGlobalLogger().Debug(msg) }
func LogBase(msg string) { getGlobalLogger().Base(msg) }
func LogInfo(msg string) { getGlobalLogger().Info(msg) }
func LogSuccess(result string) { getGlobalLogger().Success(result) }
func LogError(errMsg string) { getGlobalLogger().Error(errMsg) }
func LogDebug(msg string) { getGlobalLogger().Debug(msg) }
func LogBase(msg string) { getGlobalLogger().Base(msg) }
func LogInfo(msg string) { getGlobalLogger().Info(msg) }
func LogSuccess(result string) { getGlobalLogger().Success(result) }
func LogError(errMsg string) { getGlobalLogger().Error(errMsg) }
// =============================================================================
// 输出系统简化接口
@ -132,7 +124,7 @@ func InitOutput() error {
if Outputfile == "" {
return fmt.Errorf("output file not specified")
}
var format output.OutputFormat
switch OutputFormat {
case "txt":
@ -177,14 +169,58 @@ func WrapperTcpWithTimeout(network, address string, timeout time.Duration) (net.
return net.DialTimeout(network, address, timeout)
}
// WrapperTcpWithContext TCP连接包装器带上下文
// WrapperTcpWithContext TCP连接包装器带上下文和代理支持
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
return d.DialContext(ctx, network, address)
}
LogDebug(fmt.Sprintf("使用SOCKS5代理连接: %s -> %s", Socks5Proxy, address))
return dialer.DialContext(ctx, network, address)
}
// 没有配置代理,使用直连
var d net.Dialer
return d.DialContext(ctx, network, address)
}
// WrapperTlsWithContext TLS连接包装器带上下文
// WrapperTlsWithContext TLS连接包装器带上下文和代理支持
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}
return d.DialContext(ctx, network, address)
}
@ -196,4 +232,4 @@ func WrapperTlsWithContext(ctx context.Context, network, address string, config
// CheckErrs 检查单个错误 - 简化版本
func CheckErrs(err error) error {
return err
}
}

View File

@ -86,7 +86,6 @@ type VulnExploitConfig struct {
// Redis利用
RedisFile string `json:"redis_file"` // Redis利用目标文件
RedisShell string `json:"redis_shell"` // Redis反弹Shell命令
DisableRedis bool `json:"disable_redis"` // 是否禁用Redis利用测试
RedisWritePath string `json:"redis_write_path"` // Redis文件写入路径
RedisWriteContent string `json:"redis_write_content"` // Redis文件写入内容
RedisWriteFile string `json:"redis_write_file"` // Redis写入的源文件

View File

@ -20,7 +20,7 @@ globals.go - 全局变量定义
// 版本信息
// =============================================================================
var version = "2.2.0"
var version = "2.2.1"
// =============================================================================
// 简化的全局状态管理(仅保留必要的同步机制)

58
Common/hostinfo_ext.go Normal file
View File

@ -0,0 +1,58 @@
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
}

View File

@ -170,10 +170,6 @@ var FlagMessages = map[string]map[string]string{
LangZH: "Redis Shell",
LangEN: "Redis Shell",
},
"flag_disable_redis": {
LangZH: "禁用Redis扫描",
LangEN: "Disable Redis scan",
},
"flag_redis_write_path": {
LangZH: "Redis写入路径",
LangEN: "Redis write path",
@ -190,10 +186,6 @@ var FlagMessages = map[string]map[string]string{
LangZH: "禁用暴力破解",
LangEN: "Disable brute force",
},
"flag_disable_exploit": {
LangZH: "禁用利用攻击",
LangEN: "Disable exploit attacks",
},
"flag_max_retries": {
LangZH: "最大重试次数",
LangEN: "Maximum retries",
@ -235,6 +227,10 @@ var FlagMessages = map[string]map[string]string{
LangEN: "Reverse shell target address:port (e.g.: 192.168.1.100:4444)",
},
"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)",
LangEN: "Start SOCKS5 proxy server on port (e.g.: 1080)",
},

View File

@ -108,11 +108,7 @@ func ParsePort(ports string) []int {
return result
}
// ParsePortsFromString 从字符串解析端口列表
// 保持与 ParsePortsFromString 的接口兼容性
func ParsePortsFromString(portsStr string) []int {
return ParsePort(portsStr)
}
// 已移除未使用的 ParsePortsFromString 方法
// =============================================================================
// 辅助函数

View File

@ -30,7 +30,6 @@ type TargetParserOptions struct {
ValidateURLs bool `json:"validate_urls"`
ResolveDomains bool `json:"resolve_domains"`
EnableStatistics bool `json:"enable_statistics"`
DefaultPorts string `json:"default_ports"`
}
// DefaultTargetParserOptions 默认目标解析器选项
@ -43,7 +42,6 @@ func DefaultTargetParserOptions() *TargetParserOptions {
ValidateURLs: DefaultValidateURLs,
ResolveDomains: DefaultResolveDomains,
EnableStatistics: DefaultTargetEnableStatistics,
DefaultPorts: DefaultPorts,
}
}
@ -199,17 +197,42 @@ func (tp *TargetParser) parseHosts(input *TargetInput) ([]string, []error, []str
}
}
// 去重和验证
// 去重和验证同时分离host:port格式
hosts = tp.removeDuplicateStrings(hosts)
validHosts := make([]string, 0, len(hosts))
hostPorts := make([]string, 0)
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 {
validHosts = append(validHosts, host)
} else if err != nil {
warnings = append(warnings, fmt.Sprintf("无效主机: %s - %s", host, err.Error()))
}
}
// 将找到的hostPorts合并到输入结果中通过修改input结构
if len(hostPorts) > 0 {
input.HostPort = append(input.HostPort, hostPorts...)
}
// 检查目标数量限制
if len(validHosts) > tp.options.MaxTargets {
@ -406,8 +429,26 @@ func (tp *TargetParser) parseHostList(hostStr string) ([]string, error) {
}
hosts = append(hosts, rangeHosts...)
default:
// 单个IP或域名
hosts = append(hosts, item)
// 检查是否为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或域名
hosts = append(hosts, item)
}
}
}
@ -697,6 +738,19 @@ func (tp *TargetParser) validateHost(host string) (bool, error) {
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地址
if ip := net.ParseIP(host); ip != nil {
return tp.validateIP(ip)

View File

@ -3,6 +3,8 @@ package parsers
import (
"regexp"
"time"
"github.com/shadow1ng/fscan/common/base"
)
/*
@ -134,12 +136,11 @@ const (
DefaultValidateURLs = true
DefaultResolveDomains = false
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}$`
PortRangeRegexPattern = `^(\d+)(-(\d+))?$`
URLValidationRegexPattern = `^https?://[^\s/$.?#].[^\s]*$`
URLValidationRegexPattern = `^https?://[^\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])?)*$`
CookieRegexPattern = `^[^=;\s]+(=[^;\s]*)?(\s*;\s*[^=;\s]+(=[^;\s]*)?)*$`
@ -192,25 +193,21 @@ const (
SamplingMaxHost = 253
)
// GetPortGroups 获取预定义端口组映射
// GetPortGroups 获取预定义端口组映射(统一的端口组定义)
func GetPortGroups() map[string]string {
return map[string]string{
"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": "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",
"database": "1433,1521,3306,5432,6379,11211,27017",
"common": "21,22,23,25,53,80,110,135,139,143,443,445,993,995,1723,3389,5060,5985,5986",
"web": base.WebPorts, // 使用实际的WebPorts常量
"main": base.MainPorts, // 使用实际的MainPorts常量
"db": base.DbPorts, // 使用实际的DbPorts常量
"service": base.ServicePorts, // 使用实际的ServicePorts常量
"common": base.CommonPorts, // 使用实际的CommonPorts常量
"all": base.AllPorts, // 使用实际的AllPorts常量
}
}
// GetTargetPortGroups 获取目标解析器端口组映射
// GetTargetPortGroups 获取目标解析器端口组映射(向后兼容,调用统一函数)
func GetTargetPortGroups() map[string]string {
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",
}
return GetPortGroups()
}
// =============================================================================

62
Common/target.go Normal file
View File

@ -0,0 +1,62 @@
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
}

View File

@ -1,8 +1,6 @@
package utils
import (
"log"
"runtime"
"time"
)
@ -25,102 +23,19 @@ func NewMemoryMonitor(maxHeapMB uint64, maxGoroutines int, checkInterval time.Du
}
}
// Start 启动内存监控
func (mm *MemoryMonitor) Start() {
if mm.running {
return
}
mm.running = true
go mm.monitor()
}
// 已移除未使用的 Start 方法
// Stop 停止内存监控
func (mm *MemoryMonitor) Stop() {
if !mm.running {
return
}
mm.running = false
select {
case mm.stopChan <- true:
default:
}
}
// 已移除未使用的 Stop 方法
// monitor 监控循环
func (mm *MemoryMonitor) monitor() {
ticker := time.NewTicker(mm.checkInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
mm.checkMemory()
case <-mm.stopChan:
return
}
}
}
// 已移除未使用的 monitor 方法
// 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)
}
}
// 已移除未使用的 checkMemory 方法
// 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)),
}
}
// 已移除未使用的 GetMemoryStats 方法
// 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)
}
// 已移除未使用的 ForceGC 方法
// getHeapSize 获取当前堆大小
func (mm *MemoryMonitor) getHeapSize() uint64 {
var m runtime.MemStats
runtime.ReadMemStats(&m)
return m.HeapInuse
}
// 已移除未使用的 getHeapSize 方法
// 默认内存监控器实例
var DefaultMemMonitor = NewMemoryMonitor(
@ -129,12 +44,6 @@ var DefaultMemMonitor = NewMemoryMonitor(
30*time.Second, // 30秒检查一次
)
// StartDefaultMonitor 启动默认内存监控器
func StartDefaultMonitor() {
DefaultMemMonitor.Start()
}
// 已移除未使用的 StartDefaultMonitor 方法
// StopDefaultMonitor 停止默认内存监控器
func StopDefaultMonitor() {
DefaultMemMonitor.Stop()
}
// 已移除未使用的 StopDefaultMonitor 方法

View File

@ -2,25 +2,7 @@ package utils
// 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
}
// 已移除未使用的 DeduplicateStrings 方法

View File

@ -1,140 +0,0 @@
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")
}

View File

@ -1,145 +0,0 @@
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
}

262
PARAMETERS.md Normal file
View File

@ -0,0 +1,262 @@
# 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

View File

@ -1,165 +0,0 @@
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
}

View File

@ -1,111 +0,0 @@
package vnc
import (
"context"
"fmt"
"net"
"time"
"github.com/mitchellh/go-vnc"
"github.com/shadow1ng/fscan/common"
"github.com/shadow1ng/fscan/common/i18n"
"github.com/shadow1ng/fscan/plugins/base"
)
// VNCConnector VNC服务连接器
type VNCConnector struct{}
// NewVNCConnector 创建新的VNC连接器
func NewVNCConnector() *VNCConnector {
return &VNCConnector{}
}
// Connect 连接到VNC服务
func (c *VNCConnector) Connect(ctx context.Context, info *common.HostInfo) (interface{}, error) {
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
timeout := time.Duration(common.Timeout) * time.Second
// 使用带上下文的TCP连接
conn, err := common.WrapperTcpWithTimeout("tcp", target, timeout)
if err != nil {
return nil, fmt.Errorf(i18n.GetText("vnc_connection_failed"), err)
}
// 设置读写超时
if err := conn.SetDeadline(time.Now().Add(timeout)); err != nil {
conn.Close()
return nil, fmt.Errorf("failed to set connection deadline: %v", err)
}
return conn, nil
}
// Authenticate 认证VNC服务
func (c *VNCConnector) Authenticate(ctx context.Context, conn interface{}, cred *base.Credential) error {
netConn, ok := conn.(net.Conn)
if !ok {
return fmt.Errorf("invalid connection type")
}
// 检查上下文是否已取消
select {
case <-ctx.Done():
return ctx.Err()
default:
}
// VNC只使用密码认证忽略用户名
password := cred.Password
if password == "" && cred.Username != "" {
// 如果密码为空但用户名不为空,尝试使用用户名作为密码
password = cred.Username
}
// 创建完成通道
doneChan := make(chan error, 1)
// 在协程中处理VNC认证
go func() {
// 配置VNC客户端
config := &vnc.ClientConfig{
Auth: []vnc.ClientAuth{
&vnc.PasswordAuth{
Password: password,
},
},
}
// 尝试VNC认证
client, err := vnc.Client(netConn, config)
if err != nil {
select {
case <-ctx.Done():
case doneChan <- err:
}
return
}
// 认证成功,立即关闭客户端
client.Close()
select {
case <-ctx.Done():
case doneChan <- nil:
}
}()
// 等待认证结果或上下文取消
select {
case err := <-doneChan:
return err
case <-ctx.Done():
return ctx.Err()
}
}
// Close 关闭连接
func (c *VNCConnector) Close(conn interface{}) error {
if closer, ok := conn.(interface{ Close() error }); ok && closer != nil {
return closer.Close()
}
return nil
}

View File

@ -1,25 +0,0 @@
package vnc
import (
"context"
"github.com/shadow1ng/fscan/common"
"github.com/shadow1ng/fscan/plugins/base"
)
// VNCExploiter VNC服务利用器
// 遵循新架构设计模式,当前为空实现
type VNCExploiter struct{}
// NewVNCExploiter 创建新的VNC利用器
func NewVNCExploiter() *VNCExploiter {
return &VNCExploiter{}
}
// Exploit 执行VNC服务利用
// 当前为空实现,遵循其他插件的一致性设计
func (e *VNCExploiter) Exploit(ctx context.Context, info *common.HostInfo, creds *base.Credential) (*base.ExploitResult, error) {
// 空实现 - 遵循新架构中其他服务插件的模式
// 主要功能集中在连接器和插件主体中实现
return nil, nil
}

View File

@ -1,187 +0,0 @@
package vnc
import (
"context"
"fmt"
"github.com/shadow1ng/fscan/common"
"github.com/shadow1ng/fscan/common/i18n"
"github.com/shadow1ng/fscan/plugins/base"
)
// VNCPlugin VNC服务插件
type VNCPlugin struct {
*base.ServicePlugin
exploiter *VNCExploiter
}
// NewVNCPlugin 创建VNC插件
func NewVNCPlugin() *VNCPlugin {
// 插件元数据
metadata := &base.PluginMetadata{
Name: "vnc",
Version: "2.0.0",
Author: "fscan-team",
Description: "VNC远程桌面协议服务检测和弱口令扫描",
Category: "service",
Ports: []int{5900, 5901, 5902, 5903}, // VNC常用端口
Protocols: []string{"tcp"},
Tags: []string{"vnc", "remote-desktop", "weak-password"},
}
// 创建连接器和服务插件
connector := NewVNCConnector()
servicePlugin := base.NewServicePlugin(metadata, connector)
// 创建VNC插件
plugin := &VNCPlugin{
ServicePlugin: servicePlugin,
exploiter: NewVNCExploiter(),
}
// 设置能力
plugin.SetCapabilities([]base.Capability{
base.CapWeakPassword,
})
return plugin
}
// init 自动注册VNC插件
func init() {
// 创建插件工厂
metadata := &base.PluginMetadata{
Name: "vnc",
Version: "2.0.0",
Author: "fscan-team",
Description: "VNC远程桌面协议服务检测和弱口令扫描",
Category: "service",
Ports: []int{5900, 5901, 5902, 5903},
Protocols: []string{"tcp"},
Tags: []string{"vnc", "remote-desktop", "weak-password"},
}
factory := base.NewSimplePluginFactory(metadata, func() base.Plugin {
return NewVNCPlugin()
})
base.GlobalPluginRegistry.Register("vnc", factory)
}
// Scan 重写扫描方法进行VNC服务扫描
func (p *VNCPlugin) Scan(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) {
// 如果禁用了暴力破解,只进行服务识别
if common.DisableBrute {
return p.performServiceIdentification(ctx, info)
}
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
// 生成凭据进行暴力破解
credentials := p.generateCredentials()
if len(credentials) == 0 {
return &base.ScanResult{
Success: false,
Error: fmt.Errorf("no credentials available"),
}, nil
}
// 遍历凭据进行测试
for _, cred := range credentials {
result, err := p.ScanCredential(ctx, info, cred)
if err == nil && result.Success {
// 认证成功
common.LogSuccess(i18n.GetText("vnc_weak_password_success", target, cred.Password))
return &base.ScanResult{
Success: true,
Service: "VNC",
Credentials: []*base.Credential{cred},
Banner: result.Banner,
Extra: map[string]interface{}{
"service": "VNC",
"port": info.Ports,
"password": cred.Password,
"type": "weak-password",
},
}, nil
}
}
// 没有找到有效凭据
return &base.ScanResult{
Success: false,
Service: "VNC",
Error: fmt.Errorf("authentication failed for all credentials"),
}, nil
}
// performServiceIdentification 执行服务识别
func (p *VNCPlugin) performServiceIdentification(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) {
// 尝试连接到服务进行基本识别
conn, err := p.GetServiceConnector().Connect(ctx, info)
if err != nil {
return &base.ScanResult{
Success: false,
Error: err,
}, nil
}
defer p.GetServiceConnector().Close(conn)
// 服务识别成功
return &base.ScanResult{
Success: true,
Service: "VNC",
Banner: "VNC service detected",
Extra: map[string]interface{}{
"service": "VNC",
"port": info.Ports,
"type": "service-identification",
},
}, nil
}
// generateCredentials 生成VNC认证凭据
func (p *VNCPlugin) generateCredentials() []*base.Credential {
var credentials []*base.Credential
// VNC只使用密码认证不需要用户名
passwords := common.Passwords
if len(passwords) == 0 {
// 使用默认VNC密码
passwords = []string{"", "123456", "password", "vnc", "admin", "root", "888888", "123123"}
}
// 生成密码凭据VNC不使用用户名
for _, password := range passwords {
credentials = append(credentials, &base.Credential{
Username: "", // VNC不需要用户名
Password: password,
})
}
// 额外尝试常见的VNC密码组合
commonVNCPasswords := []string{
"vnc", "password", "123456", "admin", "root", "guest",
"1234", "12345", "qwerty", "abc123", "888888", "000000",
}
for _, password := range commonVNCPasswords {
// 避免重复添加
found := false
for _, existing := range credentials {
if existing.Password == password {
found = true
break
}
}
if !found {
credentials = append(credentials, &base.Credential{
Username: "",
Password: password,
})
}
}
return credentials
}

107
app/container.go Normal file
View File

@ -0,0 +1,107 @@
package app
import (
"context"
"fmt"
"sync"
"github.com/shadow1ng/fscan/common"
"github.com/shadow1ng/fscan/core"
)
// Container 依赖注入容器
type Container struct {
services map[string]interface{}
initializers []Initializer
mu sync.RWMutex
initialized bool
}
// NewContainer 创建新的容器
func NewContainer() *Container {
container := &Container{
services: make(map[string]interface{}),
}
// 注册默认初始化器
container.AddInitializer(&PluginInitializer{})
return container
}
// AddInitializer 添加初始化器
func (c *Container) AddInitializer(init Initializer) {
c.initializers = append(c.initializers, init)
}
// Register 注册服务
func (c *Container) Register(name string, service interface{}) {
c.mu.Lock()
defer c.mu.Unlock()
c.services[name] = service
}
// Get 获取服务
func (c *Container) Get(name string) (interface{}, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
service, exists := c.services[name]
return service, exists
}
// Initialize 初始化容器和所有服务
func (c *Container) Initialize() error {
if c.initialized {
return nil
}
// 执行所有初始化器
for _, initializer := range c.initializers {
if err := initializer.Initialize(); err != nil {
return WrapError(ErrInitFailed, err)
}
}
c.initialized = true
return nil
}
// RunScan 执行扫描包装现有的core.RunScan
func (c *Container) RunScan(ctx context.Context, info common.HostInfo) error {
// 使用新的验证函数
if err := common.ValidateHostInfo(&info); err != nil {
return WrapError(ErrScanFailed, err)
}
// 创建目标信息(展示新功能,但保持兼容)
target := common.NewTargetInfo(info)
target.WithContext(ctx)
target.SetMetadata("container_managed", true)
target.SetMetadata("validation_passed", true)
// 记录扫描信息
c.logScanInfo(target)
// 调用现有的扫描逻辑
core.RunScan(info)
return nil
}
// logScanInfo 记录扫描信息
func (c *Container) logScanInfo(target *common.TargetInfo) {
targetStr := target.String()
if targetStr != "" {
common.LogDebug(fmt.Sprintf("容器管理的扫描目标: %s", targetStr))
}
if target.HasMetadata("validation_passed") {
common.LogDebug("目标验证通过")
}
}
// Cleanup 清理资源
func (c *Container) Cleanup() {
// 清理输出资源
common.CloseOutput()
}

43
app/errors.go Normal file
View File

@ -0,0 +1,43 @@
package app
import "fmt"
// AppError 应用程序错误类型
type AppError struct {
Code int
Message string
Cause error
}
func (e *AppError) Error() string {
if e.Cause != nil {
return fmt.Sprintf("[%d] %s: %v", e.Code, e.Message, e.Cause)
}
return fmt.Sprintf("[%d] %s", e.Code, e.Message)
}
// 预定义错误类型
var (
ErrInitFailed = &AppError{Code: 1, Message: "初始化失败"}
ErrParseFailed = &AppError{Code: 2, Message: "参数解析失败"}
ErrOutputFailed = &AppError{Code: 3, Message: "输出初始化失败"}
ErrScanFailed = &AppError{Code: 4, Message: "扫描执行失败"}
)
// NewAppError 创建新的应用程序错误
func NewAppError(code int, message string, cause error) *AppError {
return &AppError{
Code: code,
Message: message,
Cause: cause,
}
}
// WrapError 包装错误为应用程序错误
func WrapError(baseErr *AppError, cause error) *AppError {
return &AppError{
Code: baseErr.Code,
Message: baseErr.Message,
Cause: cause,
}
}

66
app/initializer.go Normal file
View File

@ -0,0 +1,66 @@
package app
import (
"sort"
"github.com/shadow1ng/fscan/common"
"github.com/shadow1ng/fscan/plugins/base"
)
// Initializer 初始化器接口
type Initializer interface {
Initialize() error
Name() string
}
// PluginInitializer 插件初始化器
type PluginInitializer struct{}
func (p *PluginInitializer) Name() string {
return "PluginInitializer"
}
func (p *PluginInitializer) Initialize() error {
var localPlugins []string
// 获取所有注册的插件
allPlugins := base.GlobalPluginRegistry.GetAll()
for _, pluginName := range allPlugins {
metadata := base.GlobalPluginRegistry.GetMetadata(pluginName)
if metadata != nil && metadata.Category == "local" {
localPlugins = append(localPlugins, pluginName)
}
}
// 排序以保持一致性
sort.Strings(localPlugins)
// 设置全局变量
common.LocalPluginsList = localPlugins
return nil
}
// LoggerInitializer 日志初始化器
type LoggerInitializer struct{}
func (l *LoggerInitializer) Name() string {
return "LoggerInitializer"
}
func (l *LoggerInitializer) Initialize() error {
common.InitLogger()
return nil
}
// OutputInitializer 输出初始化器
type OutputInitializer struct{}
func (o *OutputInitializer) Name() string {
return "OutputInitializer"
}
func (o *OutputInitializer) Initialize() error {
return common.InitOutput()
}

View File

@ -108,7 +108,8 @@ func (b *BaseScanStrategy) isPluginTypeMatchedByName(pluginName string) bool {
case FilterLocal:
return metadata.Category == "local"
case FilterService:
return metadata.Category == "service"
// 服务扫描允许service类型以及通过智能检测的web类型在上层逻辑中处理
return metadata.Category == "service" || metadata.Category == "web"
case FilterWeb:
return metadata.Category == "web"
default:
@ -122,6 +123,7 @@ func (b *BaseScanStrategy) isPluginTypeMatched(plugin common.ScanPlugin) bool {
case FilterLocal:
return plugin.HasType(common.PluginTypeLocal)
case FilterService:
// 服务扫描排除本地插件允许服务和Web插件
return !plugin.HasType(common.PluginTypeLocal)
case FilterWeb:
return plugin.HasType(common.PluginTypeWeb)
@ -131,7 +133,7 @@ func (b *BaseScanStrategy) isPluginTypeMatched(plugin common.ScanPlugin) bool {
}
// IsPluginApplicableByName 根据插件名称判断是否适用(新方法)
func (b *BaseScanStrategy) IsPluginApplicableByName(pluginName string, targetPort int, isCustomMode bool) bool {
func (b *BaseScanStrategy) IsPluginApplicableByName(pluginName string, targetHost string, targetPort int, isCustomMode bool) bool {
// 自定义模式下运行所有明确指定的插件
if isCustomMode {
return true
@ -143,7 +145,7 @@ func (b *BaseScanStrategy) IsPluginApplicableByName(pluginName string, targetPor
}
// 智能Web插件检测如果是Web插件且检测到Web服务则包含Web插件
if b.shouldIncludeWebPlugin(metadata, targetPort) {
if b.shouldIncludeWebPlugin(metadata, targetHost, targetPort) {
return true
}
@ -162,11 +164,21 @@ func (b *BaseScanStrategy) IsPluginApplicableByName(pluginName string, targetPor
return false
}
// 对于Web插件的特殊处理
if metadata.Category == "web" {
// Web扫描策略下直接允许Web插件执行用户明确指定了Web目标
if b.filterType == FilterWeb {
return true
}
// 其他策略下必须通过智能检测才能执行
return false
}
return true
}
// shouldIncludeWebPlugin 判断是否应该包含Web插件智能检测
func (b *BaseScanStrategy) shouldIncludeWebPlugin(metadata *base.PluginMetadata, targetPort int) bool {
func (b *BaseScanStrategy) shouldIncludeWebPlugin(metadata *base.PluginMetadata, targetHost string, targetPort int) bool {
// 只对服务扫描策略启用Web插件智能检测
if b.filterType != FilterService {
return false
@ -187,14 +199,14 @@ func (b *BaseScanStrategy) shouldIncludeWebPlugin(metadata *base.PluginMetadata,
globalWebDetector = NewWebPortDetector()
}
// 检测是否为Web服务这里暂时只检查常见端口避免每次都进行HTTP探测)
return globalWebDetector.IsCommonWebPort(targetPort)
// 检测是否为Web服务使用完整的智能检测包括HTTP协议探测)
return globalWebDetector.IsWebService(targetHost, targetPort)
}
// globalWebDetector Web检测器全局实例
var globalWebDetector *WebPortDetector
// IsPluginApplicable 判断插件是否适用(通用实现
// IsPluginApplicable 判断插件是否适用(通用实现,传统插件系统
func (b *BaseScanStrategy) IsPluginApplicable(plugin common.ScanPlugin, targetPort int, isCustomMode bool) bool {
// 自定义模式下运行所有明确指定的插件
if isCustomMode {
@ -206,6 +218,21 @@ func (b *BaseScanStrategy) IsPluginApplicable(plugin common.ScanPlugin, targetPo
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 {
// 无端口限制的插件适用于所有端口

78
core/PluginAdapter.go Normal file
View File

@ -0,0 +1,78 @@
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 方法
// 已移除未使用的 GetPluginsByPort 方法
// 已移除未使用的 GetPluginsByType 方法
// 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 方法
// 已移除未使用的 GetServicePlugins 方法
// 已移除未使用的 GetWebPlugins 方法
// 已移除未使用的 GetLocalPlugins 方法

View File

@ -60,6 +60,15 @@ func (p *PortDiscoveryService) shouldPerformLivenessCheck(hosts []string) bool {
func (p *PortDiscoveryService) discoverAlivePorts(hosts []string) []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 {
alivePorts = EnhancedPortScan(hosts, common.Ports, common.Timeout)
@ -73,14 +82,6 @@ func (p *PortDiscoveryService) discoverAlivePorts(hosts []string) []string {
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
}

View File

@ -5,7 +5,7 @@ import (
"github.com/shadow1ng/fscan/common"
"github.com/shadow1ng/fscan/plugins/base"
// 导入新架构插件,触发自动注册
// 导入跨平台服务插件(可在所有平台上运行)
_ "github.com/shadow1ng/fscan/plugins/services/activemq"
_ "github.com/shadow1ng/fscan/plugins/services/cassandra"
_ "github.com/shadow1ng/fscan/plugins/services/ftp"
@ -28,20 +28,30 @@ import (
_ "github.com/shadow1ng/fscan/plugins/services/snmp"
_ "github.com/shadow1ng/fscan/plugins/services/ssh"
_ "github.com/shadow1ng/fscan/plugins/services/telnet"
_ "github.com/shadow1ng/fscan/plugins/services/vnc"
// 导入Legacy插件适配器
_ "github.com/shadow1ng/fscan/plugins/legacy/netbios"
_ "github.com/shadow1ng/fscan/plugins/legacy/ms17010"
_ "github.com/shadow1ng/fscan/plugins/legacy/smb"
_ "github.com/shadow1ng/fscan/plugins/legacy/smb2"
_ "github.com/shadow1ng/fscan/plugins/legacy/smbghost"
_ "github.com/shadow1ng/fscan/plugins/legacy/rdp"
_ "github.com/shadow1ng/fscan/plugins/legacy/elasticsearch"
_ "github.com/shadow1ng/fscan/plugins/legacy/findnet"
// 导入跨平台Legacy插件
_ "github.com/shadow1ng/fscan/plugins/legacy/elasticsearch" // 跨平台服务
_ "github.com/shadow1ng/fscan/plugins/legacy/findnet" // 网络发现
_ "github.com/shadow1ng/fscan/plugins/legacy/ms17010" // Windows SMB漏洞但扫描器可跨平台
_ "github.com/shadow1ng/fscan/plugins/legacy/netbios" // NetBIOS协议主要Windows但可跨平台扫描
_ "github.com/shadow1ng/fscan/plugins/legacy/rdp" // RDP协议扫描可跨平台
_ "github.com/shadow1ng/fscan/plugins/legacy/smb" // SMB协议扫描可跨平台
_ "github.com/shadow1ng/fscan/plugins/legacy/smb2" // SMBv2协议扫描可跨平台
_ "github.com/shadow1ng/fscan/plugins/legacy/smbghost" // SMBGhost漏洞扫描可跨平台
// 导入Web插件适配器
_ "github.com/shadow1ng/fscan/plugins/legacy/webtitle"
_ "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代理
)
// =============================================================================
@ -73,49 +83,15 @@ func InitializePluginSystem() error {
return nil
}
// GetAllPlugins 获取所有已注册插件名称
func GetAllPlugins() []string {
return base.GlobalPluginRegistry.GetAll()
}
// 已移除未使用的 GetAllPlugins 方法
// GetPluginMetadata 获取插件元数据
func GetPluginMetadata(name string) *base.PluginMetadata {
return base.GlobalPluginRegistry.GetMetadata(name)
}
// 已移除未使用的 GetPluginMetadata 方法
// CreatePlugin 创建插件实例
func CreatePlugin(name string) (base.Plugin, error) {
return base.GlobalPluginRegistry.Create(name)
}
// 已移除未使用的 CreatePlugin 方法
// 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
}
// 已移除未使用的 GetPluginsByCategory 方法
// 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
}
// 已移除未使用的 GetPluginsByPort 方法
// init 自动初始化插件系统
func init() {

View File

@ -19,7 +19,7 @@ type ScanStrategy interface {
// 插件管理方法
GetPlugins() ([]string, bool)
IsPluginApplicable(plugin common.ScanPlugin, targetPort int, isCustomMode bool) bool
IsPluginApplicableByName(pluginName string, targetPort int, isCustomMode bool) bool
IsPluginApplicableByName(pluginName string, targetHost string, targetPort int, isCustomMode bool) bool
}
// selectStrategy 根据扫描配置选择适当的扫描策略
@ -125,7 +125,7 @@ func ExecuteScanTasks(targets []common.HostInfo, strategy ScanStrategy, ch *chan
}
// 检查插件是否适用于当前目标
if strategy.IsPluginApplicableByName(pluginName, targetPort, isCustomMode) {
if strategy.IsPluginApplicableByName(pluginName, target.Host, targetPort, isCustomMode) {
executeScanTask(pluginName, target, ch, wg)
}
}
@ -143,7 +143,7 @@ func countApplicableTasks(targets []common.HostInfo, pluginsToRun []string, isCu
for _, pluginName := range pluginsToRun {
if GlobalPluginAdapter.PluginExists(pluginName) &&
strategy.IsPluginApplicableByName(pluginName, targetPort, isCustomMode) {
strategy.IsPluginApplicableByName(pluginName, target.Host, targetPort, isCustomMode) {
count++
}
}
@ -187,7 +187,4 @@ func executeScanTask(pluginName string, target common.HostInfo, ch *chan struct{
}
// Scan 入口函数,向后兼容旧的调用方式
func Scan(info common.HostInfo) {
RunScan(info)
}
// 已移除未使用的 Scan 方法

View File

@ -144,7 +144,7 @@ func (s *ServiceScanStrategy) isPluginApplicableToAnyPort(plugin common.ScanPlug
return true
}
// 非自定义模式下,排除本地插件
// 服务扫描排除本地插件但保留Web插件有智能检测
if plugin.HasType(common.PluginTypeLocal) {
return false
}
@ -166,6 +166,11 @@ func (s *ServiceScanStrategy) isPluginApplicableToAnyPort(plugin common.ScanPlug
// isNewPluginApplicableToAnyPort 检查新插件架构的插件是否对任何端口适用
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 {
return true

377
core/WebDetection.go Normal file
View File

@ -0,0 +1,377 @@
package core
import (
"context"
"crypto/tls"
"fmt"
"net"
"net/http"
"regexp"
"strconv"
"strings"
"sync"
"time"
"github.com/shadow1ng/fscan/common"
)
// WebPortDetector Web端口检测器
type WebPortDetector struct {
// 常见Web端口列表
commonWebPorts map[int]bool
// HTTP检测超时时间
httpTimeout time.Duration
// HTTP客户端
httpClient *http.Client
// HTTPS客户端
httpsClient *http.Client
// 检测结果缓存(避免重复检测)
detectionCache map[string]bool
// 缓存互斥锁
cacheMutex sync.RWMutex
}
// NewWebPortDetector 创建Web端口检测器
func NewWebPortDetector() *WebPortDetector {
timeout := 3 * time.Second
// 定义常见Web端口扩展列表
commonPorts := map[int]bool{
// 标准Web端口
80: true, // HTTP
443: true, // HTTPS
8080: true, // HTTP alternate
8443: true, // HTTPS alternate
// 开发服务器端口
3000: true, // Node.js dev server
4000: true, // Ruby dev server
5000: true, // Python dev server
8000: true, // Development server
8888: true, // Common dev port
9000: true, // Common dev port
9090: true, // Common dev port
// Web服务器alternate端口
8081: true, 8082: true, 8083: true, 8084: true, 8085: true,
8086: true, 8087: true, 8088: true, 8089: true,
// 企业级Web端口
9080: true, // WebSphere
9443: true, // WebSphere SSL
7001: true, // WebLogic
7002: true, // WebLogic SSL
// 应用服务器端口
8180: true, // Tomcat alternate
8181: true, // Tomcat alternate
8282: true, // Common alternate
8383: true, // Common alternate
8484: true, // Common alternate
8585: true, // Common alternate
// 代理和负载均衡端口
3128: true, // Squid proxy
8008: true, // HTTP proxy
8118: true, // Privoxy
// 监控和管理界面
9200: true, // Elasticsearch
5601: true, // Kibana
3001: true, // Grafana alternate
9091: true, // Prometheus
}
// 创建HTTP客户端
httpClient := &http.Client{
Timeout: timeout,
Transport: &http.Transport{
DialContext: (&net.Dialer{
Timeout: timeout,
}).DialContext,
MaxIdleConns: 10,
IdleConnTimeout: 30 * time.Second,
DisableKeepAlives: true,
MaxConnsPerHost: 5,
},
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse // 不跟随重定向,减少检测时间
},
}
// 创建HTTPS客户端跳过证书验证
httpsClient := &http.Client{
Timeout: timeout,
Transport: &http.Transport{
DialContext: (&net.Dialer{
Timeout: timeout,
}).DialContext,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
MaxIdleConns: 10,
IdleConnTimeout: 30 * time.Second,
DisableKeepAlives: true,
MaxConnsPerHost: 5,
},
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
return &WebPortDetector{
commonWebPorts: commonPorts,
httpTimeout: timeout,
httpClient: httpClient,
httpsClient: httpsClient,
detectionCache: make(map[string]bool),
}
}
// IsWebService 智能检测端口是否运行Web服务
func (w *WebPortDetector) IsWebService(host string, port int) bool {
// 1. 快速路径常见Web端口直接返回true
if w.IsCommonWebPort(port) {
common.LogDebug(fmt.Sprintf("端口 %d 是常见Web端口启用Web插件", port))
return true
}
// 2. 缓存检查:避免重复检测同一端口
cacheKey := fmt.Sprintf("%s:%d", host, port)
w.cacheMutex.RLock()
if cached, exists := w.detectionCache[cacheKey]; exists {
w.cacheMutex.RUnlock()
common.LogDebug(fmt.Sprintf("端口 %d 使用缓存结果: %v", port, cached))
return cached
}
w.cacheMutex.RUnlock()
// 3. 智能路径对非常见端口进行HTTP协议探测
common.LogDebug(fmt.Sprintf("对端口 %d 进行智能Web检测", port))
result := w.detectHTTPService(host, port)
// 4. 缓存结果
w.cacheMutex.Lock()
w.detectionCache[cacheKey] = result
w.cacheMutex.Unlock()
common.LogDebug(fmt.Sprintf("端口 %d 智能检测结果: %v", port, result))
return result
}
// 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和HTTPS
httpChan := make(chan bool, 1)
httpsChan := make(chan bool, 1)
// 检测HTTP
go func() {
httpChan <- w.tryHTTPConnection(ctx, host, port, "http")
}()
// 检测HTTPS对于高端口常见
go func() {
httpsChan <- w.tryHTTPConnection(ctx, host, port, "https")
}()
// 等待任一协议检测成功
select {
case result := <-httpChan:
if result {
common.LogDebug(fmt.Sprintf("端口 %d 检测到HTTP服务", port))
return true
}
case <-ctx.Done():
return false
}
select {
case result := <-httpsChan:
if result {
common.LogDebug(fmt.Sprintf("端口 %d 检测到HTTPS服务", port))
return true
}
case <-ctx.Done():
return false
}
return false
}
// tryHTTPConnection 尝试HTTP连接
func (w *WebPortDetector) tryHTTPConnection(ctx context.Context, host string, port int, protocol string) bool {
// 构造URL
var url string
if (port == 80 && protocol == "http") || (port == 443 && protocol == "https") {
url = fmt.Sprintf("%s://%s", protocol, host)
} else {
url = fmt.Sprintf("%s://%s:%d", protocol, host, port)
}
// 选择客户端
var client *http.Client
if protocol == "https" {
client = w.httpsClient
} else {
client = w.httpClient
}
// 发送HEAD请求最小化网络开销
req, err := http.NewRequestWithContext(ctx, "HEAD", url, nil)
if err != nil {
return false
}
req.Header.Set("User-Agent", "fscan-web-detector/2.2")
req.Header.Set("Accept", "*/*")
resp, err := client.Do(req)
if err != nil {
// 检查错误类型某些错误也表明是HTTP服务
return w.analyzeHTTPError(err)
}
defer resp.Body.Close()
// 分析HTTP响应
return w.analyzeHTTPResponse(resp, url)
}
// analyzeHTTPError 分析HTTP错误判断是否表明存在HTTP服务
func (w *WebPortDetector) analyzeHTTPError(err error) bool {
errStr := strings.ToLower(err.Error())
// 先检查明确的非Web服务错误
nonWebErrors := []string{
"connection refused",
"no such host",
"network unreachable",
"timeout",
"deadline exceeded",
"connection reset",
"eof",
}
for _, nonWebErr := range nonWebErrors {
if strings.Contains(errStr, nonWebErr) {
return false
}
}
// 这些错误表明连接到了HTTP服务器只是协议或其他问题
webIndicators := []string{
"malformed http response",
"unexpected http response",
"ssl handshake",
"certificate",
"tls",
"bad request",
"method not allowed",
"server gave http response",
}
for _, indicator := range webIndicators {
if strings.Contains(errStr, indicator) {
return true
}
}
return false
}
// analyzeHTTPResponse 分析HTTP响应判断是否为Web服务
func (w *WebPortDetector) analyzeHTTPResponse(resp *http.Response, url string) bool {
// 任何HTTP状态码都表明这是Web服务
if resp.StatusCode > 0 {
// 分析响应头获取更多信息
serverHeader := resp.Header.Get("Server")
contentType := resp.Header.Get("Content-Type")
// 记录检测到的Web服务器信息
if serverHeader != "" {
common.LogDebug(fmt.Sprintf("检测到Web服务器: %s (Server: %s)", url, serverHeader))
}
// 特殊处理某些非Web服务也会返回HTTP响应
if w.isNonWebHTTPService(serverHeader, contentType) {
common.LogDebug(fmt.Sprintf("端口返回HTTP响应但可能不是Web服务: %s", url))
return false
}
return true
}
return false
}
// isNonWebHTTPService 判断是否为非Web的HTTP服务
func (w *WebPortDetector) isNonWebHTTPService(serverHeader, contentType string) bool {
// 某些服务使用HTTP协议但不是传统Web服务
nonWebServices := []string{
"docker",
"kubernetes",
"consul",
"etcd",
"vault",
"nomad",
}
serverLower := strings.ToLower(serverHeader)
for _, service := range nonWebServices {
if strings.Contains(serverLower, service) {
return true
}
}
return false
}
// GetWebPortRange 获取可能的Web端口范围用于高级扫描
func (w *WebPortDetector) GetWebPortRange() []int {
// 返回扩展的Web端口列表用于目标发现
var ports []int
for port := range w.commonWebPorts {
ports = append(ports, port)
}
// 添加一些常见的自定义端口范围
customRanges := []int{
8090, 8091, 8092, 8093, 8094, 8095, 8096, 8097, 8098, 8099,
9001, 9002, 9003, 9004, 9005, 9006, 9007, 9008, 9009,
10000, 10001, 10002, 10003, 10004, 10005,
}
ports = append(ports, customRanges...)
return ports
}
// IsWebPortByPattern 基于端口模式判断是否可能是Web端口
func (w *WebPortDetector) IsWebPortByPattern(port int) bool {
portStr := strconv.Itoa(port)
// Web端口的常见模式
webPatterns := []*regexp.Regexp{
regexp.MustCompile(`^80\d{2}$`), // 80xx
regexp.MustCompile(`^90\d{2}$`), // 90xx
regexp.MustCompile(`^300\d$`), // 300x
regexp.MustCompile(`^[3-9]000$`), // x000
regexp.MustCompile(`^1[0-9]000$`), // 1x000
}
for _, pattern := range webPatterns {
if pattern.MatchString(portStr) {
return true
}
}
return false
}

View File

@ -1,6 +1,8 @@
package core
import (
"fmt"
"net/url"
"github.com/shadow1ng/fscan/common"
"github.com/shadow1ng/fscan/common/i18n"
"strings"
@ -54,16 +56,55 @@ func (s *WebScanStrategy) Execute(info common.HostInfo, ch *chan struct{}, wg *s
func (s *WebScanStrategy) PrepareTargets(baseInfo common.HostInfo) []common.HostInfo {
var targetInfos []common.HostInfo
for _, url := range common.URLs {
urlInfo := baseInfo
// 确保URL包含协议头
if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
url = "http://" + url
// 首先从common.URLs获取目标
for _, urlStr := range common.URLs {
urlInfo := s.createTargetFromURL(baseInfo, urlStr)
if urlInfo != nil {
targetInfos = append(targetInfos, *urlInfo)
}
}
// 如果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
}
// 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
}

11
core/registry_linux.go Normal file
View File

@ -0,0 +1,11 @@
//go:build linux
package core
import (
// Linux持久化插件
_ "github.com/shadow1ng/fscan/plugins/local/ldpreload" // Linux LD_PRELOAD持久化
_ "github.com/shadow1ng/fscan/plugins/local/shellenv" // Linux Shell环境变量持久化
_ "github.com/shadow1ng/fscan/plugins/local/crontask" // Linux Cron计划任务持久化
_ "github.com/shadow1ng/fscan/plugins/local/systemdservice" // Linux Systemd服务持久化
)

17
core/registry_windows.go Normal file
View File

@ -0,0 +1,17 @@
//go:build windows
package core
import (
// Windows特有系统功能插件
_ "github.com/shadow1ng/fscan/plugins/local/avdetect" // Windows 杀毒软件检测
_ "github.com/shadow1ng/fscan/plugins/local/dcinfo" // Windows 域控信息收集
_ "github.com/shadow1ng/fscan/plugins/local/minidump" // Windows 内存转储
// Windows持久化插件
_ "github.com/shadow1ng/fscan/plugins/local/winregistry" // Windows 注册表持久化
_ "github.com/shadow1ng/fscan/plugins/local/winstartup" // Windows 启动文件夹持久化
_ "github.com/shadow1ng/fscan/plugins/local/winschtask" // Windows 计划任务持久化
_ "github.com/shadow1ng/fscan/plugins/local/winservice" // Windows 服务持久化
_ "github.com/shadow1ng/fscan/plugins/local/winwmi" // Windows WMI事件订阅持久化
)

97
main.go
View File

@ -1,61 +1,62 @@
package main
import (
"context"
"fmt"
"os"
"github.com/shadow1ng/fscan/app"
"github.com/shadow1ng/fscan/common"
"github.com/shadow1ng/fscan/core"
// 引入本地插件以触发注册
_ "github.com/shadow1ng/fscan/plugins/local/fileinfo" // 已重构,可用
_ "github.com/shadow1ng/fscan/plugins/local/dcinfo" // 已重构,可用
_ "github.com/shadow1ng/fscan/plugins/local/minidump" // 已重构,可用
_ "github.com/shadow1ng/fscan/plugins/local/reverseshell" // 已重构,可用
_ "github.com/shadow1ng/fscan/plugins/local/socks5proxy" // 已重构,可用
_ "github.com/shadow1ng/fscan/plugins/local/avdetect" // 已重构,可用
_ "github.com/shadow1ng/fscan/plugins/local/forwardshell" // 新增,可用
// Linux持久化插件
_ "github.com/shadow1ng/fscan/plugins/local/ldpreload" // Linux LD_PRELOAD持久化
_ "github.com/shadow1ng/fscan/plugins/local/shellenv" // Linux Shell环境变量持久化
_ "github.com/shadow1ng/fscan/plugins/local/crontask" // Linux Cron计划任务持久化
_ "github.com/shadow1ng/fscan/plugins/local/systemdservice" // Linux Systemd服务持久化
// Windows持久化插件
_ "github.com/shadow1ng/fscan/plugins/local/winregistry" // Windows 注册表持久化
_ "github.com/shadow1ng/fscan/plugins/local/winstartup" // Windows 启动文件夹持久化
_ "github.com/shadow1ng/fscan/plugins/local/winschtask" // Windows 计划任务持久化
_ "github.com/shadow1ng/fscan/plugins/local/winservice" // Windows 服务持久化
_ "github.com/shadow1ng/fscan/plugins/local/winwmi" // Windows WMI事件订阅持久化
// 监控插件
_ "github.com/shadow1ng/fscan/plugins/local/keylogger" // 跨平台键盘记录
// 实用工具插件
_ "github.com/shadow1ng/fscan/plugins/local/downloader" // 跨平台文件下载
_ "github.com/shadow1ng/fscan/plugins/local/cleaner" // 跨平台系统痕迹清理
)
func main() {
var Info common.HostInfo
common.Flag(&Info)
// 在flag解析后初始化logger确保LogLevel参数生效
common.InitLogger()
// 解析 CLI 参数
if err := common.Parse(&Info); err != nil {
os.Exit(1)
// 创建应用容器
container := app.NewContainer()
// 第一阶段:基础初始化(插件系统)
if err := container.Initialize(); err != nil {
handleError("基础初始化失败", err)
}
// 初始化输出系统,如果失败则直接退出
if err := common.InitOutput(); err != nil {
common.LogError(fmt.Sprintf("初始化输出系统失败: %v", err))
os.Exit(1)
defer container.Cleanup()
// 第二阶段:解析配置
var info common.HostInfo
common.Flag(&info)
// 第三阶段日志初始化依赖于flag解析
logInit := &app.LoggerInitializer{}
if err := logInit.Initialize(); err != nil {
handleError("日志初始化失败", err)
}
// 第四阶段:参数解析和验证
if err := common.Parse(&info); err != nil {
handleError("参数解析失败", err)
}
// 第五阶段:输出系统初始化
outputInit := &app.OutputInitializer{}
if err := outputInit.Initialize(); err != nil {
handleError("输出初始化失败", err)
}
// 第六阶段:执行扫描
ctx := context.Background()
if err := container.RunScan(ctx, info); err != nil {
handleError("扫描失败", err)
}
}
func handleError(msg string, err error) {
// 检查是否是应用程序错误
if appErr, ok := err.(*app.AppError); ok {
common.LogError(fmt.Sprintf("%s: %s", msg, appErr.Message))
if appErr.Cause != nil {
common.LogError(fmt.Sprintf("详细错误: %v", appErr.Cause))
}
os.Exit(appErr.Code)
} else {
common.LogError(fmt.Sprintf("%s: %v", msg, err))
os.Exit(1)
}
defer common.CloseOutput()
// 执行 CLI 扫描逻辑
core.RunScan(Info)
}

View File

@ -0,0 +1,7 @@
// Package adapters provides plugin compatibility layers.
// This package contains legacy adapter code that was part of a transition architecture.
// The adapter functions were not being used in the current codebase.
package adapters
// Legacy plugin adapter functionality has been removed as it was unused.
// This file is preserved for future compatibility needs if required.

View File

@ -264,46 +264,4 @@ func GenerateCredentials(usernames []string, passwords []string) []*Credential {
return credentials
}
// 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等函数
}
}
// 已移除未使用的 GeneratePasswordOnlyCredentials 方法

View File

@ -1,7 +1,6 @@
package Plugins
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"encoding/base64"
@ -44,32 +43,6 @@ func ReadBytes(conn net.Conn) ([]byte, error) {
// 默认AES加密密钥
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模式解密字符串
func AesDecrypt(crypted string, key string) (string, error) {
@ -104,12 +77,6 @@ func AesDecrypt(crypted string, key string) (string, error) {
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填充
func PKCS7UnPadding(data []byte) ([]byte, error) {

View File

@ -1,3 +1,5 @@
//go:build linux
package crontask
import (

Some files were not shown because too many files have changed in this diff Show More