mirror of
https://github.com/shadow1ng/fscan.git
synced 2026-09-24 12:11: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,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
|
||||
}
|
||||
Reference in New Issue
Block a user