mirror of
https://github.com/shadow1ng/fscan.git
synced 2026-09-22 03:10:42 +08:00
471 lines
14 KiB
Go
471 lines
14 KiB
Go
package plugins
|
||
|
||
import (
|
||
"context"
|
||
"testing"
|
||
|
||
"github.com/shadow1ng/fscan/common"
|
||
"github.com/shadow1ng/fscan/common/config"
|
||
)
|
||
|
||
/*
|
||
init_test.go - 插件系统核心逻辑测试
|
||
|
||
测试目标:GenerateCredentials 函数
|
||
价值:这个函数生成所有服务的暴力破解凭据,逻辑错误会导致:
|
||
- 漏掉有效凭据(少生成)
|
||
- 浪费时间测试重复凭据(多生成)
|
||
- {user} 占位符不生效(密码错误)
|
||
|
||
"凭据生成是暴力破解的弹药库。弹药错了,仗就打不赢。"
|
||
*/
|
||
|
||
// =============================================================================
|
||
// GenerateCredentials - 核心凭据生成逻辑
|
||
// =============================================================================
|
||
|
||
func preservePluginRegistry(t *testing.T) {
|
||
t.Helper()
|
||
|
||
mutex.RLock()
|
||
snapshot := make(map[string]*PluginInfo, len(plugins))
|
||
for name, info := range plugins {
|
||
copied := *info
|
||
copied.ports = append([]int(nil), info.ports...)
|
||
copied.types = append([]string(nil), info.types...)
|
||
snapshot[name] = &copied
|
||
}
|
||
mutex.RUnlock()
|
||
|
||
t.Cleanup(func() {
|
||
mutex.Lock()
|
||
plugins = snapshot
|
||
mutex.Unlock()
|
||
})
|
||
}
|
||
|
||
type testPlugin struct {
|
||
BasePlugin
|
||
}
|
||
|
||
func (p testPlugin) Scan(context.Context, *common.HostInfo, *common.ScanSession) *Result {
|
||
return &Result{Type: ResultTypeService, Success: true}
|
||
}
|
||
|
||
func TestPluginRegistryMetadata(t *testing.T) {
|
||
preservePluginRegistry(t)
|
||
|
||
RegisterWithPorts("unit_tcp", func() Plugin {
|
||
return testPlugin{BasePlugin: NewBasePlugin("unit_tcp")}
|
||
}, []int{1234, 5678})
|
||
RegisterUDPWithPorts("unit_udp", func() Plugin {
|
||
return testPlugin{BasePlugin: NewBasePlugin("unit_udp")}
|
||
}, []int{161})
|
||
RegisterWithTypes("unit_local", func() Plugin {
|
||
return testPlugin{BasePlugin: NewBasePlugin("unit_local")}
|
||
}, nil, []string{PluginTypeLocal})
|
||
RegisterUnsafeWithTypes("unit_unsafe_web", func() Plugin {
|
||
return testPlugin{BasePlugin: NewBasePlugin("unit_unsafe_web")}
|
||
}, nil, []string{PluginTypeWeb})
|
||
|
||
if !Exists("unit_tcp") || Exists("missing_plugin") {
|
||
t.Fatal("Exists returned wrong result")
|
||
}
|
||
if got := Get("unit_tcp"); got == nil || got.Name() != "unit_tcp" {
|
||
t.Fatalf("Get(unit_tcp) = %#v", got)
|
||
}
|
||
if got := Get("missing_plugin"); got != nil {
|
||
t.Fatalf("Get(missing_plugin) = %#v, want nil", got)
|
||
}
|
||
if !HasType("unit_tcp", PluginTypeService) || !HasType("unit_local", PluginTypeLocal) {
|
||
t.Fatal("registered plugin types were not recorded")
|
||
}
|
||
if !IsUDP("unit_udp") || IsUDP("unit_tcp") {
|
||
t.Fatal("UDP metadata is wrong")
|
||
}
|
||
if !IsSafe("unit_tcp") || IsSafe("unit_local") || IsSafe("unit_unsafe_web") || IsSafe("missing_plugin") {
|
||
t.Fatal("safe metadata is wrong")
|
||
}
|
||
|
||
ports := GetPluginPorts("unit_tcp")
|
||
if len(ports) != 2 || ports[0] != 1234 || ports[1] != 5678 {
|
||
t.Fatalf("ports = %#v", ports)
|
||
}
|
||
if got := GetPluginPorts("missing_plugin"); len(got) != 0 {
|
||
t.Fatalf("missing plugin ports = %#v, want empty", got)
|
||
}
|
||
if !hasPluginType([]string{PluginTypeWeb, PluginTypeLocal}, PluginTypeLocal) ||
|
||
hasPluginType([]string{PluginTypeWeb}, PluginTypeUDP) {
|
||
t.Fatal("hasPluginType returned wrong result")
|
||
}
|
||
|
||
names := All()
|
||
for _, want := range []string{"unit_tcp", "unit_udp", "unit_local", "unit_unsafe_web"} {
|
||
if !containsPluginName(names, want) {
|
||
t.Fatalf("All() missing %q in %#v", want, names)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestPluginLocalModeHook(t *testing.T) {
|
||
preservePluginRegistry(t)
|
||
|
||
RegisterWithTypes("unit_local_mode", func() Plugin {
|
||
return testPlugin{BasePlugin: NewBasePlugin("unit_local_mode")}
|
||
}, nil, []string{PluginTypeLocal})
|
||
RegisterWithPorts("unit_service_mode", func() Plugin {
|
||
return testPlugin{BasePlugin: NewBasePlugin("unit_service_mode")}
|
||
}, []int{22})
|
||
|
||
if common.IsLocalMode == nil {
|
||
t.Fatal("IsLocalMode hook should be installed")
|
||
}
|
||
if !common.IsLocalMode("unit_local_mode") {
|
||
t.Fatal("single local plugin should be local mode")
|
||
}
|
||
if !common.IsLocalMode("unit_local_mode, unit_local_mode") {
|
||
t.Fatal("local plugin list should be local mode")
|
||
}
|
||
if common.IsLocalMode("") || common.IsLocalMode("all") || common.IsLocalMode("unit_local_mode,unit_service_mode") {
|
||
t.Fatal("non-local modes should not be local mode")
|
||
}
|
||
}
|
||
|
||
func containsPluginName(values []string, want string) bool {
|
||
for _, value := range values {
|
||
if value == want {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
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 正确回退到笛卡尔积")
|
||
}
|
||
|
||
func TestBuildConfigAdditionalPasswordsAreNotShadowedByExactPair(t *testing.T) {
|
||
cfg, _, err := common.BuildConfig(&common.FlagVars{
|
||
Username: "root",
|
||
Password: "primary",
|
||
AddPasswords: "extra",
|
||
}, &common.HostInfo{})
|
||
if err != nil {
|
||
t.Fatalf("BuildConfig error = %v", err)
|
||
}
|
||
|
||
result := GenerateCredentials("ssh", cfg)
|
||
found := map[string]bool{}
|
||
for _, cred := range result {
|
||
found[cred.Username+":"+cred.Password] = true
|
||
}
|
||
|
||
if !found["root:primary"] {
|
||
t.Fatal("missing primary password credential")
|
||
}
|
||
if !found["root:extra"] {
|
||
t.Fatal("additional password was shadowed by exact user/password pair")
|
||
}
|
||
}
|