feat: 实现Apache Cassandra数据库服务专业扫描插件

• 新增Cassandra插件支持Native Protocol (9042端口)
• 实现弱密码检测和未授权访问检测
• 支持自动利用:信息收集、Keyspace枚举、数据提取
• 集成-nobr模式服务识别和-ne自动利用功能
• 完整的Context超时机制和代理支持
• 添加中英文国际化消息支持
• 基于新插件架构实现模块化设计

功能特性:
- 支持gocql驱动的CQL查询和认证
- 智能识别Cassandra Native Protocol v4
- 三种利用方法:系统信息、数据库结构、敏感数据
- 完整的错误处理和超时控制机制
This commit is contained in:
ZacharyZcR 2025-08-08 04:34:32 +08:00
parent 51735c4e25
commit 516225a11f
3 changed files with 783 additions and 0 deletions

View File

@ -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
}

View File

@ -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
}

View File

@ -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()
}