diff --git a/Plugins/services/cassandra/connector.go b/Plugins/services/cassandra/connector.go new file mode 100644 index 0000000..0a6cf6e --- /dev/null +++ b/Plugins/services/cassandra/connector.go @@ -0,0 +1,169 @@ +package cassandra + +import ( + "context" + "fmt" + "net" + "strconv" + "time" + + "github.com/gocql/gocql" + "github.com/shadow1ng/fscan/common" + "github.com/shadow1ng/fscan/plugins/base" +) + +// CassandraConnector Cassandra连接器实现 +type CassandraConnector struct { + host string + port string +} + +// CassandraProxyDialer 实现gocql.Dialer接口,支持代理连接 +type CassandraProxyDialer struct { + timeout time.Duration +} + +// DialContext 实现代理拨号 +func (d *CassandraProxyDialer) DialContext(ctx context.Context, network, addr string) (net.Conn, error) { + host, port, err := net.SplitHostPort(addr) + if err != nil { + return nil, err + } + return common.WrapperTcpWithContext(ctx, network, fmt.Sprintf("%s:%s", host, port)) +} + +// NewCassandraConnector 创建Cassandra连接器 +func NewCassandraConnector() *CassandraConnector { + return &CassandraConnector{} +} + +// Connect 连接到Cassandra服务 +func (c *CassandraConnector) Connect(ctx context.Context, info *common.HostInfo) (interface{}, error) { + c.host = info.Host + c.port = info.Ports + + // 创建Cassandra集群配置 + cluster := gocql.NewCluster(c.host) + + // 解析端口 + port, err := strconv.Atoi(c.port) + if err != nil { + return nil, fmt.Errorf("无效的端口号: %s", c.port) + } + cluster.Port = port + + // 设置连接参数 + timeout := time.Duration(common.Timeout) * time.Second + cluster.Timeout = timeout + cluster.ConnectTimeout = timeout + cluster.ProtoVersion = 4 + cluster.Consistency = gocql.One + + // 如果配置了代理,设置自定义Dialer + if common.Socks5Proxy != "" { + cluster.Dialer = &CassandraProxyDialer{ + timeout: timeout, + } + } + + // 设置重试策略 + cluster.RetryPolicy = &gocql.SimpleRetryPolicy{NumRetries: 3} + + return cluster, nil +} + +// Authenticate 认证 +func (c *CassandraConnector) Authenticate(ctx context.Context, conn interface{}, cred *base.Credential) error { + cluster, ok := conn.(*gocql.ClusterConfig) + if !ok { + return fmt.Errorf("无效的连接类型") + } + + // 创建集群配置副本 + authCluster := *cluster + + // 设置认证信息 + if cred.Username != "" || cred.Password != "" { + authCluster.Authenticator = gocql.PasswordAuthenticator{ + Username: cred.Username, + Password: cred.Password, + } + } + + // 创建会话通道以支持Context超时 + sessionChan := make(chan struct { + session *gocql.Session + err error + }, 1) + + // 在goroutine中创建会话,以便可以通过Context取消 + go func() { + session, err := authCluster.CreateSession() + select { + case <-ctx.Done(): + if session != nil { + session.Close() + } + case sessionChan <- struct { + session *gocql.Session + err error + }{session, err}: + } + }() + + // 等待会话创建或Context取消 + var session *gocql.Session + var err error + select { + case result := <-sessionChan: + session, err = result.session, result.err + if err != nil { + return fmt.Errorf("Cassandra认证失败: %v", err) + } + case <-ctx.Done(): + return fmt.Errorf("Cassandra连接超时: %v", ctx.Err()) + } + + defer session.Close() + + // 尝试执行查询验证连接 + resultChan := make(chan struct { + success bool + err error + }, 1) + + go func() { + var err error + + // 尝试两种查询,确保至少一种成功 + err = session.Query("SELECT peer FROM system.peers").WithContext(ctx).Scan(nil) + if err != nil { + err = session.Query("SELECT now() FROM system.local").WithContext(ctx).Scan(nil) + } + + select { + case <-ctx.Done(): + case resultChan <- struct { + success bool + err error + }{err == nil, err}: + } + }() + + // 等待查询结果或Context取消 + select { + case result := <-resultChan: + if !result.success && result.err != nil { + return fmt.Errorf("Cassandra查询验证失败: %v", result.err) + } + return nil + case <-ctx.Done(): + return fmt.Errorf("Cassandra查询超时: %v", ctx.Err()) + } +} + +// Close 关闭连接 +func (c *CassandraConnector) Close(conn interface{}) error { + // Cassandra集群配置无需显式关闭 + return nil +} \ No newline at end of file diff --git a/Plugins/services/cassandra/exploiter.go b/Plugins/services/cassandra/exploiter.go new file mode 100644 index 0000000..931d872 --- /dev/null +++ b/Plugins/services/cassandra/exploiter.go @@ -0,0 +1,333 @@ +package cassandra + +import ( + "context" + "fmt" + "strings" + + "github.com/gocql/gocql" + "github.com/shadow1ng/fscan/common" + "github.com/shadow1ng/fscan/common/i18n" + "github.com/shadow1ng/fscan/plugins/base" +) + +// CassandraExploiter Cassandra利用器 +type CassandraExploiter struct { + *base.BaseExploiter + connector *CassandraConnector +} + +// NewCassandraExploiter 创建Cassandra利用器 +func NewCassandraExploiter() *CassandraExploiter { + exploiter := &CassandraExploiter{ + BaseExploiter: base.NewBaseExploiter("cassandra"), + connector: NewCassandraConnector(), + } + + // 添加利用方法 + exploiter.setupExploitMethods() + + return exploiter +} + +// setupExploitMethods 设置利用方法 +func (e *CassandraExploiter) setupExploitMethods() { + // 1. 信息收集 + infoMethod := base.NewExploitMethod(base.ExploitDataExtraction, "information_gathering"). + WithDescription(i18n.GetText("exploit_method_name_information_gathering")). + WithPriority(8). + WithConditions("has_credentials"). + WithHandler(e.exploitInformationGathering). + Build() + e.AddExploitMethod(infoMethod) + + // 2. Keyspace枚举 + enumMethod := base.NewExploitMethod(base.ExploitDataExtraction, "keyspace_enumeration"). + WithDescription(i18n.GetText("exploit_method_name_database_enumeration")). + WithPriority(9). + WithConditions("has_credentials"). + WithHandler(e.exploitKeyspaceEnumeration). + Build() + e.AddExploitMethod(enumMethod) + + // 3. 数据提取 + dataMethod := base.NewExploitMethod(base.ExploitDataExtraction, "data_extraction"). + WithDescription(i18n.GetText("exploit_method_name_data_extraction")). + WithPriority(7). + WithConditions("has_credentials"). + WithHandler(e.exploitDataExtraction). + Build() + e.AddExploitMethod(dataMethod) +} + +// exploitInformationGathering 信息收集 +func (e *CassandraExploiter) exploitInformationGathering(ctx context.Context, info *common.HostInfo, creds *base.Credential) (*base.ExploitResult, error) { + return e.executeWithSession(ctx, info, creds, "information_gathering", e.informationGathering) +} + +// exploitKeyspaceEnumeration keyspace枚举 +func (e *CassandraExploiter) exploitKeyspaceEnumeration(ctx context.Context, info *common.HostInfo, creds *base.Credential) (*base.ExploitResult, error) { + return e.executeWithSession(ctx, info, creds, "keyspace_enumeration", e.keyspaceEnumeration) +} + +// exploitDataExtraction 数据提取 +func (e *CassandraExploiter) exploitDataExtraction(ctx context.Context, info *common.HostInfo, creds *base.Credential) (*base.ExploitResult, error) { + return e.executeWithSession(ctx, info, creds, "data_extraction", e.dataExtraction) +} + +// executeWithSession 使用Cassandra会话执行利用方法 +func (e *CassandraExploiter) executeWithSession(ctx context.Context, info *common.HostInfo, creds *base.Credential, methodName string, method func(context.Context, interface{}, string) ([]string, error)) (*base.ExploitResult, error) { + target := fmt.Sprintf("%s:%s", info.Host, info.Ports) + + // 建立连接 + conn, err := e.connector.Connect(ctx, info) + if err != nil { + return nil, fmt.Errorf("连接失败: %v", err) + } + + // 认证 + err = e.connector.Authenticate(ctx, conn, creds) + if err != nil { + return nil, fmt.Errorf("认证失败: %v", err) + } + + // 创建会话用于利用 + cluster := conn.(*gocql.ClusterConfig) + if creds.Username != "" || creds.Password != "" { + cluster.Authenticator = gocql.PasswordAuthenticator{ + Username: creds.Username, + Password: creds.Password, + } + } + + session, err := cluster.CreateSession() + if err != nil { + return nil, fmt.Errorf("创建会话失败: %v", err) + } + defer session.Close() + + // 执行方法 + output, err := method(ctx, session, target) + if err != nil { + return &base.ExploitResult{ + Success: false, + Error: err, + Type: base.ExploitDataExtraction, + Method: methodName, + Output: fmt.Sprintf("执行失败: %v", err), + }, nil + } + + return &base.ExploitResult{ + Success: true, + Type: base.ExploitDataExtraction, + Method: methodName, + Output: strings.Join(output, "\n"), + }, nil +} + +// informationGathering 信息收集 +func (e *CassandraExploiter) informationGathering(ctx context.Context, session interface{}, target string) ([]string, error) { + cassandraSession := session.(*gocql.Session) + var output []string + + // 获取Cassandra版本信息 + var releaseVersion string + err := cassandraSession.Query("SELECT release_version FROM system.local").WithContext(ctx).Scan(&releaseVersion) + if err == nil { + output = append(output, fmt.Sprintf("Cassandra版本: %s", releaseVersion)) + } + + // 获取集群名称 + var clusterName string + err = cassandraSession.Query("SELECT cluster_name FROM system.local").WithContext(ctx).Scan(&clusterName) + if err == nil { + output = append(output, fmt.Sprintf("集群名称: %s", clusterName)) + } + + // 获取数据中心信息 + var datacenter string + err = cassandraSession.Query("SELECT data_center FROM system.local").WithContext(ctx).Scan(&datacenter) + if err == nil { + output = append(output, fmt.Sprintf("数据中心: %s", datacenter)) + } + + // 获取机架信息 + var rack string + err = cassandraSession.Query("SELECT rack FROM system.local").WithContext(ctx).Scan(&rack) + if err == nil { + output = append(output, fmt.Sprintf("机架: %s", rack)) + } + + // 获取节点ID + var hostId string + err = cassandraSession.Query("SELECT host_id FROM system.local").WithContext(ctx).Scan(&hostId) + if err == nil { + output = append(output, fmt.Sprintf("节点ID: %s", hostId)) + } + + if len(output) == 0 { + return nil, fmt.Errorf("无法获取Cassandra信息") + } + + return output, nil +} + +// keyspaceEnumeration keyspace枚举 +func (e *CassandraExploiter) keyspaceEnumeration(ctx context.Context, session interface{}, target string) ([]string, error) { + cassandraSession := session.(*gocql.Session) + var output []string + + // 查询所有keyspace + iter := cassandraSession.Query("SELECT keyspace_name FROM system_schema.keyspaces").WithContext(ctx).Iter() + defer iter.Close() + + var keyspaceName string + var keyspaces []string + + for iter.Scan(&keyspaceName) { + keyspaces = append(keyspaces, keyspaceName) + } + + if err := iter.Close(); err != nil { + return nil, fmt.Errorf("查询keyspace失败: %v", err) + } + + if len(keyspaces) > 0 { + output = append(output, fmt.Sprintf("发现Keyspaces: %s", strings.Join(keyspaces, ", "))) + + // 对每个非系统keyspace获取表信息 + for _, ks := range keyspaces { + if !strings.HasPrefix(ks, "system") { + tables := e.getTablesInKeyspace(ctx, cassandraSession, ks) + if len(tables) > 0 { + output = append(output, fmt.Sprintf("Keyspace '%s' 中的表: %s", ks, strings.Join(tables, ", "))) + } + } + } + } else { + return nil, fmt.Errorf("未发现任何keyspace") + } + + return output, nil +} + +// getTablesInKeyspace 获取keyspace中的表 +func (e *CassandraExploiter) getTablesInKeyspace(ctx context.Context, session *gocql.Session, keyspace string) []string { + iter := session.Query("SELECT table_name FROM system_schema.tables WHERE keyspace_name = ?", keyspace).WithContext(ctx).Iter() + defer iter.Close() + + var tableName string + var tables []string + + for iter.Scan(&tableName) { + tables = append(tables, tableName) + } + + return tables +} + +// dataExtraction 数据提取 +func (e *CassandraExploiter) dataExtraction(ctx context.Context, session interface{}, target string) ([]string, error) { + cassandraSession := session.(*gocql.Session) + var output []string + + // 尝试读取一些系统表中的敏感信息 + + // 获取所有用户角色信息(如果存在) + if roles := e.extractRoles(ctx, cassandraSession); len(roles) > 0 { + output = append(output, roles...) + } + + // 获取peer节点信息 + if peers := e.extractPeers(ctx, cassandraSession); len(peers) > 0 { + output = append(output, peers...) + } + + // 尝试获取一些配置信息 + if configs := e.extractConfigs(ctx, cassandraSession); len(configs) > 0 { + output = append(output, configs...) + } + + if len(output) == 0 { + return []string{"未能提取到敏感数据"}, nil + } + + return output, nil +} + +// extractRoles 提取角色信息 +func (e *CassandraExploiter) extractRoles(ctx context.Context, session *gocql.Session) []string { + var output []string + + // 尝试查询角色表(Cassandra 2.2+) + iter := session.Query("SELECT role FROM system_auth.roles").WithContext(ctx).Iter() + defer iter.Close() + + var role string + var roles []string + + for iter.Scan(&role) { + roles = append(roles, role) + } + + if len(roles) > 0 { + output = append(output, fmt.Sprintf("发现角色: %s", strings.Join(roles, ", "))) + } + + return output +} + +// extractPeers 提取peer节点信息 +func (e *CassandraExploiter) extractPeers(ctx context.Context, session *gocql.Session) []string { + var output []string + + iter := session.Query("SELECT peer, data_center, rack, release_version FROM system.peers").WithContext(ctx).Iter() + defer iter.Close() + + var peer, datacenter, rack, version string + var peerCount int + + for iter.Scan(&peer, &datacenter, &rack, &version) { + peerCount++ + output = append(output, fmt.Sprintf("Peer节点 %d: %s (数据中心: %s, 机架: %s, 版本: %s)", + peerCount, peer, datacenter, rack, version)) + } + + if peerCount == 0 { + output = append(output, "未发现其他peer节点(单节点集群)") + } + + return output +} + +// extractConfigs 提取配置信息 +func (e *CassandraExploiter) extractConfigs(ctx context.Context, session *gocql.Session) []string { + var output []string + + // 获取token信息 + var tokens string + err := session.Query("SELECT tokens FROM system.local").WithContext(ctx).Scan(&tokens) + if err == nil && tokens != "" { + // 只显示token数量,不显示完整token(太长) + tokenList := strings.Split(strings.Trim(tokens, "{}"), ",") + output = append(output, fmt.Sprintf("节点tokens数量: %d", len(tokenList))) + } + + // 获取监听地址 + var listenAddress string + err = session.Query("SELECT listen_address FROM system.local").WithContext(ctx).Scan(&listenAddress) + if err == nil && listenAddress != "" { + output = append(output, fmt.Sprintf("监听地址: %s", listenAddress)) + } + + // 获取广播地址 + var broadcastAddress string + err = session.Query("SELECT broadcast_address FROM system.local").WithContext(ctx).Scan(&broadcastAddress) + if err == nil && broadcastAddress != "" { + output = append(output, fmt.Sprintf("广播地址: %s", broadcastAddress)) + } + + return output +} + diff --git a/Plugins/services/cassandra/plugin.go b/Plugins/services/cassandra/plugin.go new file mode 100644 index 0000000..718345f --- /dev/null +++ b/Plugins/services/cassandra/plugin.go @@ -0,0 +1,281 @@ +package cassandra + +import ( + "context" + "fmt" + "net" + "strings" + "time" + + "github.com/shadow1ng/fscan/common" + "github.com/shadow1ng/fscan/common/i18n" + "github.com/shadow1ng/fscan/plugins/base" +) + +// CassandraPlugin Cassandra插件实现 +type CassandraPlugin struct { + *base.ServicePlugin + exploiter *CassandraExploiter +} + +// NewCassandraPlugin 创建Cassandra插件 +func NewCassandraPlugin() *CassandraPlugin { + // 插件元数据 + metadata := &base.PluginMetadata{ + Name: "cassandra", + Version: "2.0.0", + Author: "fscan-team", + Description: "Apache Cassandra服务扫描和利用插件", + Category: "service", + Ports: []int{9042}, // Cassandra Native Protocol + Protocols: []string{"tcp"}, + Tags: []string{"cassandra", "nosql", "database", "bruteforce"}, + } + + // 创建连接器和服务插件 + connector := NewCassandraConnector() + servicePlugin := base.NewServicePlugin(metadata, connector) + + // 创建Cassandra插件 + plugin := &CassandraPlugin{ + ServicePlugin: servicePlugin, + exploiter: NewCassandraExploiter(), + } + + // 设置能力 + plugin.SetCapabilities([]base.Capability{ + base.CapWeakPassword, + base.CapDataExtraction, + base.CapInformationLeak, + }) + + return plugin +} + +// Scan 重写扫描方法以支持自动利用 +func (p *CassandraPlugin) Scan(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) { + // 如果禁用暴力破解,只进行服务识别 + if common.DisableBrute { + return p.performServiceIdentification(ctx, info) + } + + // 执行基础的密码扫描 + result, err := p.ServicePlugin.Scan(ctx, info) + if err != nil || !result.Success { + return result, err + } + + // 记录成功的弱密码/未授权访问发现 + target := fmt.Sprintf("%s:%s", info.Host, info.Ports) + cred := result.Credentials[0] + + if cred.Username == "" && cred.Password == "" { + // 未授权访问 + common.LogSuccess(i18n.GetText("plugin_unauthorized_access", "Cassandra", target)) + } else { + // 弱密码 + common.LogSuccess(i18n.GetText("plugin_login_success", "Cassandra", target, cred.Username, cred.Password)) + } + + // 自动利用功能(可通过-ne参数禁用) + if result.Success && len(result.Credentials) > 0 && !common.DisableExploit { + p.autoExploit(context.Background(), info, result.Credentials[0]) + } + + return result, nil +} + +// generateCredentials 生成Cassandra凭据 +func (p *CassandraPlugin) generateCredentials() []*base.Credential { + // 获取Cassandra专用的用户名字典 + usernames := common.Userdict["cassandra"] + if len(usernames) == 0 { + // 默认Cassandra用户名(包含空用户名用于测试未授权访问) + usernames = []string{"", "cassandra", "admin", "root", "user"} + } + + // 生成凭据组合,包括空密码测试未授权访问 + var credentials []*base.Credential + + // 首先测试未授权访问(空用户名和密码) + credentials = append(credentials, &base.Credential{ + Username: "", + Password: "", + }) + + // 然后生成常规用户名密码组合 + regularCreds := base.GenerateCredentials(usernames, common.Passwords) + credentials = append(credentials, regularCreds...) + + return credentials +} + +// autoExploit 自动利用功能 +func (p *CassandraPlugin) autoExploit(ctx context.Context, info *common.HostInfo, creds *base.Credential) { + target := fmt.Sprintf("%s:%s", info.Host, info.Ports) + common.LogDebug(i18n.GetText("plugin_exploit_start", "Cassandra", target)) + + // 执行利用操作 + result, err := p.exploiter.Exploit(ctx, info, creds) + if err != nil { + common.LogError(i18n.GetText("plugin_exploit_failed", "Cassandra", err)) + return + } + + // 处理利用结果 + if result != nil && result.Success { + // SaveExploitResult会自动使用LogSuccess显示红色利用成功消息 + base.SaveExploitResult(info, result, "Cassandra") + } +} + +// Exploit 使用exploiter执行利用 +func (p *CassandraPlugin) Exploit(ctx context.Context, info *common.HostInfo, creds *base.Credential) (*base.ExploitResult, error) { + return p.exploiter.Exploit(ctx, info, creds) +} + +// GetExploitMethods 获取利用方法 +func (p *CassandraPlugin) GetExploitMethods() []base.ExploitMethod { + return p.exploiter.GetExploitMethods() +} + +// IsExploitSupported 检查利用支持 +func (p *CassandraPlugin) IsExploitSupported(method base.ExploitType) bool { + return p.exploiter.IsExploitSupported(method) +} + +// performServiceIdentification 执行Cassandra服务识别(-nobr模式) +func (p *CassandraPlugin) performServiceIdentification(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) { + target := fmt.Sprintf("%s:%s", info.Host, info.Ports) + + // 尝试连接到Cassandra服务进行识别 + cassandraInfo, isCassandra := p.identifyCassandraService(ctx, info) + if isCassandra { + // 记录服务识别成功 + common.LogSuccess(i18n.GetText("cassandra_service_identified", target, cassandraInfo)) + + return &base.ScanResult{ + Success: true, + Service: "Cassandra", + Banner: cassandraInfo, + Extra: map[string]interface{}{ + "service": "Cassandra", + "port": info.Ports, + "info": cassandraInfo, + }, + }, nil + } + + // 如果无法识别为Cassandra,返回失败 + return &base.ScanResult{ + Success: false, + Error: fmt.Errorf("无法识别为Cassandra服务"), + }, nil +} + +// identifyCassandraService 通过连接识别Cassandra服务 +func (p *CassandraPlugin) identifyCassandraService(ctx context.Context, info *common.HostInfo) (string, bool) { + // 尝试建立简单的TCP连接 + target := fmt.Sprintf("%s:%s", info.Host, info.Ports) + conn, err := common.WrapperTcpWithTimeout("tcp", target, time.Duration(common.Timeout)*time.Second) + if err != nil { + return "", false + } + defer conn.Close() + + // 对于Cassandra native protocol (9042),尝试发送OPTIONS frame + if info.Ports == "9042" { + return p.identifyNativeProtocol(conn) + } + + // 通用端口检测(其他端口) + return p.identifyGenericCassandra(conn) +} + +// identifyNativeProtocol 识别Cassandra native protocol +func (p *CassandraPlugin) identifyNativeProtocol(conn net.Conn) (string, bool) { + // 设置读写超时 + conn.SetDeadline(time.Now().Add(time.Duration(common.Timeout) * time.Second)) + + // Cassandra native protocol OPTIONS frame + // Frame format: version(1) + flags(1) + stream(2) + opcode(1) + length(4) + body + optionsFrame := []byte{ + 0x04, // Version 4 + 0x00, // Flags + 0x00, 0x00, // Stream ID + 0x05, // Opcode: OPTIONS + 0x00, 0x00, 0x00, 0x00, // Body length: 0 + } + + // 发送OPTIONS请求 + _, err := conn.Write(optionsFrame) + if err != nil { + return "", false + } + + // 读取响应 + response := make([]byte, 1024) + n, err := conn.Read(response) + if err != nil || n < 8 { + return "", false + } + + // 检查响应是否为有效的Cassandra协议响应 + if n >= 8 && response[0] == 0x84 { // Response version + // 简单解析响应以获取支持的版本信息 + return "Cassandra Native Protocol v4", true + } + + return "", false +} + +// identifyGenericCassandra 通用Cassandra识别 +func (p *CassandraPlugin) identifyGenericCassandra(conn net.Conn) (string, bool) { + // 设置超时 + conn.SetDeadline(time.Now().Add(time.Duration(common.Timeout) * time.Second)) + + // 尝试读取任何初始数据 + response := make([]byte, 512) + n, err := conn.Read(response) + + if err == nil && n > 0 { + responseStr := string(response[:n]) + // 检查响应中是否包含Cassandra相关信息 + if strings.Contains(strings.ToLower(responseStr), "cassandra") { + return fmt.Sprintf("Cassandra服务: %s", strings.TrimSpace(responseStr)), true + } + } + + // 如果端口开放但没有明确标识,仍然认为可能是Cassandra + return "Cassandra服务", true +} + +// ============================================================================= +// 插件注册 +// ============================================================================= + +// RegisterCassandraPlugin 注册Cassandra插件 +func RegisterCassandraPlugin() { + factory := base.NewSimplePluginFactory( + &base.PluginMetadata{ + Name: "cassandra", + Version: "2.0.0", + Author: "fscan-team", + Description: "Apache Cassandra服务扫描和利用插件", + Category: "service", + Ports: []int{9042}, // Cassandra Native Protocol + Protocols: []string{"tcp"}, + Tags: []string{"cassandra", "nosql", "database", "bruteforce"}, + }, + func() base.Plugin { + return NewCassandraPlugin() + }, + ) + + base.GlobalPluginRegistry.Register("cassandra", factory) +} + +// 自动注册 +func init() { + RegisterCassandraPlugin() +} \ No newline at end of file