mirror of
https://github.com/shadow1ng/fscan.git
synced 2026-09-22 03:10:42 +08:00
瘦身: Kafka/MongoDB/Cassandra用raw TCP替代重型依赖
- kafka: 移除IBM/sarama(45MB), 自实现SASL PLAIN+ApiVersions协议(~150行) - mongodb: 移除mongo-driver(25MB), 自实现OP_MSG+saslStart认证(~180行) - cassandra: 移除gocql(1.2MB), 自实现CQLv4 STARTUP+SASL PLAIN(~130行) - 同时移除间接依赖: pierrec/lz4, klauspost/compress, eapache/snappy等 二进制: 47MB → 40MB (-15%), 移除~55MB压缩依赖 全部13个测试包通过
This commit is contained in:
+230
-87
@@ -4,16 +4,18 @@ package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"strings"
|
||||
"io"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
)
|
||||
|
||||
// CassandraPlugin Cassandra扫描插件
|
||||
// CassandraPlugin Cassandra扫描插件(纯 raw TCP CQL 协议实现)
|
||||
type CassandraPlugin struct {
|
||||
plugins.BasePlugin
|
||||
}
|
||||
@@ -47,7 +49,6 @@ func (p *CassandraPlugin) Scan(ctx context.Context, info *common.HostInfo, sessi
|
||||
}
|
||||
}
|
||||
|
||||
// 使用公共框架进行并发凭据测试
|
||||
authFn := p.createAuthFunc(info, config, state)
|
||||
testConfig := DefaultConcurrentTestConfigWithTarget(config, info)
|
||||
|
||||
@@ -60,109 +61,230 @@ func (p *CassandraPlugin) Scan(ctx context.Context, info *common.HostInfo, sessi
|
||||
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认证
|
||||
// ── raw TCP Cassandra CQL 协议 ──────────────────────────────────
|
||||
|
||||
// CQL frame 格式 (v4):
|
||||
//
|
||||
// [1B version|flags] [2B stream] [1B opcode] [4B length] [body]
|
||||
const (
|
||||
cqlVersion = 0x84 // version=4, direction=request
|
||||
cqlOpStartup = 0x01
|
||||
cqlOpAuthRsp = 0x0f
|
||||
cqlOpQuery = 0x07
|
||||
cqlOpReady = 0x02
|
||||
cqlOpAuthOk = 0x10
|
||||
cqlOpAuthChl = 0x0e
|
||||
cqlOpError = 0x00
|
||||
)
|
||||
|
||||
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
|
||||
addr := fmt.Sprintf("%s:%d", info.Host, info.Port)
|
||||
timeout := config.Timeout
|
||||
|
||||
if cred.Username != "" || cred.Password != "" {
|
||||
cluster.Authenticator = gocql.PasswordAuthenticator{
|
||||
Username: cred.Username,
|
||||
Password: cred.Password,
|
||||
}
|
||||
}
|
||||
|
||||
session, err := cluster.CreateSession()
|
||||
dialer := net.Dialer{Timeout: timeout}
|
||||
conn, err := dialer.DialContext(ctx, "tcp", addr)
|
||||
if err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: classifyCassandraErrorType(err),
|
||||
Error: err,
|
||||
}
|
||||
return &AuthResult{Success: false, ErrorType: classifyCassandraErrorType(err), Error: err}
|
||||
}
|
||||
state.IncrementTCPSuccessPacketCount()
|
||||
defer conn.Close()
|
||||
_ = conn.SetDeadline(time.Now().Add(timeout))
|
||||
|
||||
var dummy string
|
||||
err = session.Query("SELECT cluster_name FROM system.local").WithContext(ctx).Scan(&dummy)
|
||||
// Step 1: STARTUP (CQL_VERSION=3.0.0)
|
||||
startupBody := cqlStringMap(map[string]string{"CQL_VERSION": "3.0.0"})
|
||||
if err := cqlSend(conn, cqlOpStartup, startupBody); err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return &AuthResult{Success: false, ErrorType: ErrorTypeNetwork, Error: err}
|
||||
}
|
||||
|
||||
// Step 2: 读取响应
|
||||
opcode, body, err := cqlRecv(conn)
|
||||
if err != nil {
|
||||
session.Close()
|
||||
return &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: classifyCassandraErrorType(err),
|
||||
Error: err,
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return &AuthResult{Success: false, ErrorType: ErrorTypeNetwork, Error: err}
|
||||
}
|
||||
|
||||
// READY → 已就绪,发送测试查询
|
||||
// AUTHENTICATE → 需要认证
|
||||
// ERROR → 错误
|
||||
if opcode == cqlOpError {
|
||||
return &AuthResult{Success: false, ErrorType: ErrorTypeAuth, Error: fmt.Errorf("cassandra error: %s", string(body))}
|
||||
}
|
||||
|
||||
// Step 3: 如果需要认证
|
||||
if opcode == cqlOpAuthChl {
|
||||
if cred.Username == "" && cred.Password == "" {
|
||||
return &AuthResult{Success: false, ErrorType: ErrorTypeAuth, Error: fmt.Errorf("authentication required")}
|
||||
}
|
||||
// SASL PLAIN: \x00username\x00password
|
||||
saslToken := []byte("\x00" + cred.Username + "\x00" + cred.Password)
|
||||
if err := cqlSend(conn, cqlOpAuthRsp, saslToken); err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return &AuthResult{Success: false, ErrorType: ErrorTypeNetwork, Error: err}
|
||||
}
|
||||
opcode, body, err = cqlRecv(conn)
|
||||
if err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return &AuthResult{Success: false, ErrorType: ErrorTypeNetwork, Error: err}
|
||||
}
|
||||
// AUTH_SUCCESS → 认证成功
|
||||
// ERROR → 认证失败
|
||||
if opcode == cqlOpError {
|
||||
return &AuthResult{Success: false, ErrorType: ErrorTypeAuth, Error: fmt.Errorf("authentication failed: %s", string(body))}
|
||||
}
|
||||
if opcode != cqlOpAuthOk && opcode != cqlOpReady {
|
||||
return &AuthResult{Success: false, ErrorType: ErrorTypeAuth, Error: fmt.Errorf("unexpected opcode: %d", opcode)}
|
||||
}
|
||||
}
|
||||
|
||||
return &AuthResult{
|
||||
Success: true,
|
||||
Conn: &cassandraSessionWrapper{session},
|
||||
ErrorType: ErrorTypeUnknown,
|
||||
Error: nil,
|
||||
// Step 4: 发送测试查询
|
||||
queryBody := cqlLongString("SELECT cluster_name FROM system.local")
|
||||
// 添加 consistency level (ONE=1)
|
||||
queryBody = append(queryBody, 0x00, 0x01) // flags=0, consistency=ONE
|
||||
if err := cqlSend(conn, cqlOpQuery, queryBody); err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return &AuthResult{Success: false, ErrorType: ErrorTypeNetwork, Error: err}
|
||||
}
|
||||
opcode, body, err = cqlRecv(conn)
|
||||
if err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return &AuthResult{Success: false, ErrorType: ErrorTypeNetwork, Error: err}
|
||||
}
|
||||
_ = body
|
||||
_ = opcode
|
||||
|
||||
state.IncrementTCPSuccessPacketCount()
|
||||
return &AuthResult{Success: true, ErrorType: ErrorTypeUnknown, Error: nil}
|
||||
}
|
||||
|
||||
// cassandraSessionWrapper 包装 gocql.Session 以实现 io.Closer
|
||||
type cassandraSessionWrapper struct {
|
||||
*gocql.Session
|
||||
// ── CQL wire protocol 工具 ──────────────────────────────────────
|
||||
|
||||
var cqlStreamID int16
|
||||
|
||||
func cqlSend(conn net.Conn, opcode byte, body []byte) error {
|
||||
id := cqlStreamID
|
||||
cqlStreamID++
|
||||
if cqlStreamID > 32767 {
|
||||
cqlStreamID = 0
|
||||
}
|
||||
|
||||
// frame: [1B version|flags] [2B stream] [1B opcode] [4B length] [body]
|
||||
header := make([]byte, 8)
|
||||
header[0] = cqlVersion
|
||||
binary.BigEndian.PutUint16(header[1:3], uint16(id))
|
||||
header[3] = opcode
|
||||
binary.BigEndian.PutUint32(header[4:8], uint32(len(body)))
|
||||
|
||||
buf := append(header, body...)
|
||||
_, err := conn.Write(buf)
|
||||
return err
|
||||
}
|
||||
|
||||
func (w *cassandraSessionWrapper) Close() error {
|
||||
w.Session.Close()
|
||||
return nil
|
||||
func cqlRecv(conn net.Conn) (byte, []byte, error) {
|
||||
// 读取 9 字节头部(响应也有额外标志字节)
|
||||
header := make([]byte, 9)
|
||||
if _, err := io.ReadFull(conn, header); err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
opcode := header[4]
|
||||
bodyLen := int(binary.BigEndian.Uint32(header[5:9]))
|
||||
if bodyLen <= 0 || bodyLen > 1024*1024 {
|
||||
return opcode, nil, nil
|
||||
}
|
||||
body := make([]byte, bodyLen)
|
||||
if _, err := io.ReadFull(conn, body); err != nil {
|
||||
return opcode, nil, err
|
||||
}
|
||||
return opcode, body, nil
|
||||
}
|
||||
|
||||
// classifyCassandraErrorType Cassandra错误分类
|
||||
// cqlStringMap CQL string map 编码: [2B count] [pairs: [2B len] [str]]
|
||||
func cqlStringMap(m map[string]string) []byte {
|
||||
var buf []byte
|
||||
buf = append(buf, 0x00, byte(len(m))) // count as short
|
||||
for k, v := range m {
|
||||
buf = append(buf, cqlShortString(k)...)
|
||||
buf = append(buf, cqlShortString(v)...)
|
||||
}
|
||||
return buf
|
||||
}
|
||||
|
||||
func cqlShortString(s string) []byte {
|
||||
b := []byte(s)
|
||||
buf := make([]byte, 2+len(b))
|
||||
binary.BigEndian.PutUint16(buf, uint16(len(b)))
|
||||
copy(buf[2:], b)
|
||||
return buf
|
||||
}
|
||||
|
||||
func cqlLongString(s string) []byte {
|
||||
b := []byte(s)
|
||||
buf := make([]byte, 4+len(b))
|
||||
binary.BigEndian.PutUint32(buf, uint32(len(b)))
|
||||
copy(buf[4:], b)
|
||||
return buf
|
||||
}
|
||||
|
||||
// ── 错误分类 ────────────────────────────────────────────────────
|
||||
|
||||
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()
|
||||
addr := fmt.Sprintf("%s:%d", info.Host, info.Port)
|
||||
timeout := config.Timeout
|
||||
|
||||
cluster := gocql.NewCluster(info.Host)
|
||||
cluster.Port = info.Port
|
||||
cluster.Timeout = config.Timeout
|
||||
cluster.ConnectTimeout = config.Timeout
|
||||
|
||||
session, err := cluster.CreateSession()
|
||||
dialer := net.Dialer{Timeout: timeout}
|
||||
conn, err := dialer.DialContext(ctx, "tcp", addr)
|
||||
if err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return nil
|
||||
}
|
||||
state.IncrementTCPSuccessPacketCount()
|
||||
defer conn.Close()
|
||||
_ = conn.SetDeadline(time.Now().Add(timeout))
|
||||
|
||||
var dummy string
|
||||
err = session.Query("SELECT cluster_name FROM system.local").WithContext(ctx).Scan(&dummy)
|
||||
if err != nil {
|
||||
session.Close()
|
||||
// STARTUP
|
||||
if err := cqlSend(conn, cqlOpStartup, cqlStringMap(map[string]string{"CQL_VERSION": "3.0.0"})); err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return nil
|
||||
}
|
||||
opcode, _, err := cqlRecv(conn)
|
||||
if err != nil || opcode != cqlOpReady {
|
||||
return nil
|
||||
}
|
||||
|
||||
session.Close()
|
||||
// QUERY test
|
||||
queryBody := append(cqlLongString("SELECT cluster_name FROM system.local"), 0x00, 0x01)
|
||||
if err := cqlSend(conn, cqlOpQuery, queryBody); err != nil {
|
||||
return nil
|
||||
}
|
||||
_, body, err := cqlRecv(conn)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
state.IncrementTCPSuccessPacketCount()
|
||||
dummy := extractClusterName(body)
|
||||
|
||||
common.LogVuln(i18n.Tr("cassandra_unauth", target))
|
||||
return &ScanResult{
|
||||
Type: plugins.ResultTypeService,
|
||||
@@ -174,42 +296,63 @@ func (p *CassandraPlugin) tryNoAuthConnection(ctx context.Context, info *common.
|
||||
|
||||
func (p *CassandraPlugin) identifyService(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
|
||||
target := info.Target()
|
||||
addr := fmt.Sprintf("%s:%d", info.Host, info.Port)
|
||||
timeout := config.Timeout
|
||||
|
||||
cluster := gocql.NewCluster(info.Host)
|
||||
cluster.Port = info.Port
|
||||
cluster.Timeout = config.Timeout
|
||||
cluster.ConnectTimeout = config.Timeout
|
||||
|
||||
session, err := cluster.CreateSession()
|
||||
dialer := net.Dialer{Timeout: timeout}
|
||||
conn, err := dialer.DialContext(ctx, "tcp", addr)
|
||||
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,
|
||||
}
|
||||
return &ScanResult{Success: false, Service: "cassandra", Error: err}
|
||||
}
|
||||
defer conn.Close()
|
||||
_ = conn.SetDeadline(time.Now().Add(timeout))
|
||||
|
||||
if err := cqlSend(conn, cqlOpStartup, cqlStringMap(map[string]string{"CQL_VERSION": "3.0.0"})); err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return &ScanResult{Success: false, Service: "cassandra", Error: err}
|
||||
}
|
||||
opcode, _, err := cqlRecv(conn)
|
||||
if err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return &ScanResult{Success: false, Service: "cassandra", Error: err}
|
||||
}
|
||||
|
||||
state.IncrementTCPSuccessPacketCount()
|
||||
session.Close()
|
||||
|
||||
if opcode == cqlOpAuthChl {
|
||||
banner := "Cassandra (需要认证)"
|
||||
common.LogSuccess(i18n.Tr("cassandra_service", target, banner))
|
||||
return &ScanResult{Type: plugins.ResultTypeService, Success: true, Service: "cassandra", Banner: banner}
|
||||
}
|
||||
|
||||
banner := "Cassandra"
|
||||
common.LogSuccess(i18n.Tr("cassandra_service", target, banner))
|
||||
return &ScanResult{
|
||||
Type: plugins.ResultTypeService,
|
||||
Success: true,
|
||||
Service: "cassandra",
|
||||
Banner: banner,
|
||||
return &ScanResult{Type: plugins.ResultTypeService, Success: true, Service: "cassandra", Banner: banner}
|
||||
}
|
||||
|
||||
// extractClusterName 从 CQL ROWS result body 提取 cluster_name
|
||||
func extractClusterName(body []byte) string {
|
||||
s := string(body)
|
||||
// 简单查找可打印的 UTF8 字符串作为 cluster_name 候选
|
||||
if len(s) > 3 {
|
||||
// CQL ROWS result: [4B rows_count] [rows data...]
|
||||
// cluster_name 通常以可读字符串形式出现在响应中
|
||||
for i := 0; i < len(s)-2; i++ {
|
||||
if s[i] >= 0x20 && s[i] < 0x7f {
|
||||
// 提取连续可打印字符串
|
||||
j := i
|
||||
for j < len(s) && s[j] >= 0x20 && s[j] < 0x7f {
|
||||
j++
|
||||
}
|
||||
if j-i >= 3 && j-i <= 64 {
|
||||
return s[i:j]
|
||||
}
|
||||
i = j
|
||||
}
|
||||
}
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
||||
Reference in New Issue
Block a user