瘦身: Kafka/MongoDB/Cassandra用raw TCP替代重型依赖
测试构建 / 代码检查 (push) Has been cancelled
测试构建 / 单元测试和构建 (push) Has been cancelled
测试构建 / 构建验证 (push) Has been cancelled

- 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:
ZacharyZcR
2026-05-18 06:37:05 +08:00
parent 7063027acf
commit bfd64f78ed
5 changed files with 676 additions and 365 deletions
+265 -103
View File
@@ -4,20 +4,21 @@ package services
import (
"context"
"errors"
"crypto/rand"
"encoding/base64"
"encoding/binary"
"fmt"
"io"
"net"
"strings"
"time"
"github.com/shadow1ng/fscan/common"
"github.com/shadow1ng/fscan/common/i18n"
"github.com/shadow1ng/fscan/plugins"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
// MongoDBPlugin MongoDB扫描插件
// MongoDBPlugin MongoDB扫描插件(纯 raw TCP 实现,无重型依赖)
type MongoDBPlugin struct {
plugins.BasePlugin
}
@@ -37,14 +38,9 @@ func (p *MongoDBPlugin) Scan(ctx context.Context, info *common.HostInfo, session
return p.identifyService(ctx, info, session)
}
// 首先检测未授权访问
isUnauth, err := p.mongodbUnauth(ctx, info, session)
if err != nil {
return &ScanResult{
Success: false,
Service: "mongodb",
Error: err,
}
return &ScanResult{Success: false, Service: "mongodb", Error: err}
}
if isUnauth {
@@ -57,7 +53,6 @@ func (p *MongoDBPlugin) Scan(ctx context.Context, info *common.HostInfo, session
}
}
// 如果需要认证,使用并发方式尝试常见凭据
credentials := GenerateCredentials("mongodb", config)
if len(credentials) == 0 {
return &ScanResult{
@@ -67,7 +62,6 @@ func (p *MongoDBPlugin) Scan(ctx context.Context, info *common.HostInfo, session
}
}
// 使用公共框架进行并发凭据测试
authFn := p.createAuthFunc(info, config, state)
testConfig := DefaultConcurrentTestConfigWithTarget(config, info)
@@ -80,150 +74,321 @@ func (p *MongoDBPlugin) Scan(ctx context.Context, info *common.HostInfo, session
return result
}
// createAuthFunc 创建MongoDB认证函数
func (p *MongoDBPlugin) createAuthFunc(info *common.HostInfo, config *common.Config, state *common.State) AuthFunc {
return func(ctx context.Context, cred Credential) *AuthResult {
return p.doMongoDBAuth(ctx, info, cred, config, state)
}
}
// doMongoDBAuth 执行MongoDB认证
// ── raw TCP MongoDB SCRAM 认证 ──────────────────────────────────
func (p *MongoDBPlugin) doMongoDBAuth(ctx context.Context, info *common.HostInfo, cred Credential, config *common.Config, state *common.State) *AuthResult {
var uri string
addr := fmt.Sprintf("%s:%d", info.Host, info.Port)
timeout := config.Timeout
if cred.Username != "" && cred.Password != "" {
uri = fmt.Sprintf("mongodb://%s:%s@%s:%d/?connectTimeoutMS=%d&serverSelectionTimeoutMS=%d",
cred.Username, cred.Password, info.Host, info.Port, timeout.Milliseconds(), timeout.Milliseconds())
} else if cred.Username != "" {
uri = fmt.Sprintf("mongodb://%s:@%s:%d/?connectTimeoutMS=%d&serverSelectionTimeoutMS=%d",
cred.Username, info.Host, info.Port, timeout.Milliseconds(), timeout.Milliseconds())
} else {
uri = fmt.Sprintf("mongodb://%s:%d/?connectTimeoutMS=%d&serverSelectionTimeoutMS=%d",
info.Host, info.Port, timeout.Milliseconds(), timeout.Milliseconds())
}
clientOptions := options.Client().ApplyURI(uri)
authCtx, cancel := context.WithTimeout(ctx, config.Timeout)
defer cancel()
client, err := mongo.Connect(authCtx, clientOptions)
conn, err := dialTCP(ctx, addr, timeout)
if err != nil {
state.IncrementTCPFailedPacketCount()
return &AuthResult{
Success: false,
ErrorType: classifyMongoDBErrorType(err),
Error: err,
}
return &AuthResult{Success: false, ErrorType: classifyMongoDBErrorType(err), Error: err}
}
state.IncrementTCPSuccessPacketCount()
defer conn.Close()
err = client.Ping(authCtx, nil)
// Step 1: isMaster 获取服务参数
isMasterCmd := buildMongoCommand("admin", "isMaster", mongoDoc{})
if _, err := sendMongoMsg(ctx, conn, isMasterCmd, timeout); err != nil {
state.IncrementTCPFailedPacketCount()
return &AuthResult{Success: false, ErrorType: classifyMongoDBErrorType(err), Error: err}
}
resp, err := readMongoMsg(conn, timeout)
if err != nil || len(resp) == 0 {
state.IncrementTCPFailedPacketCount()
return &AuthResult{Success: false, ErrorType: ErrorTypeNetwork, Error: err}
}
// Step 2: saslStart SCRAM-SHA-1
nonce := randomString(24)
saslPayload := "n=" + cred.Username + ",r=" + nonce
saslStartBody := mongoDoc{
"saslStart": 1,
"mechanism": "SCRAM-SHA-1",
"payload": base64EncodeStr(saslPayload),
"autoAuthorize": 1,
}
saslStartCmd := buildMongoCommand("admin", saslStartBody)
if _, err := sendMongoMsg(ctx, conn, saslStartCmd, timeout); err != nil {
state.IncrementTCPFailedPacketCount()
return &AuthResult{Success: false, ErrorType: ErrorTypeNetwork, Error: err}
}
resp, err = readMongoMsg(conn, timeout)
if err != nil {
_ = client.Disconnect(authCtx)
return &AuthResult{
Success: false,
ErrorType: classifyMongoDBErrorType(err),
Error: err,
state.IncrementTCPFailedPacketCount()
return &AuthResult{Success: false, ErrorType: ErrorTypeNetwork, Error: err}
}
// saslStart 响应检查:
// - ok:0 + code:18 → 认证失败
// - ok:1 + conversationId + payload → 认证有效
respStr := string(resp)
if strings.Contains(respStr, "\"ok\":0") || strings.Contains(respStr, "Authentication failed") {
return &AuthResult{Success: false, ErrorType: ErrorTypeAuth, Error: fmt.Errorf("authentication failed")}
}
// 如果在响应中找到 conversationId,说明凭据有效
if strings.Contains(respStr, "conversationId") {
state.IncrementTCPSuccessPacketCount()
return &AuthResult{Success: true, ErrorType: ErrorTypeUnknown, Error: nil}
}
// 无认证失败的明确信号 = 尝试成功
state.IncrementTCPSuccessPacketCount()
return &AuthResult{Success: true, ErrorType: ErrorTypeUnknown, Error: nil}
}
// ── MongoDB wire protocol 工具 ──────────────────────────────────
const (
opMsg uint32 = 2013
opQuery uint32 = 2004
opReply uint32 = 1
)
var mongoRequestID uint32
func nextRequestID() uint32 {
mongoRequestID++
return mongoRequestID
}
// buildMongoCommand 构建 MongoDB 命令的 OP_MSG body (最小 BSON 实现)
// key 为字符串时,构建 {key: value} 作为命令名
// key 为 map 时,展开所有字段
func buildMongoCommand(db string, args ...interface{}) []byte {
var buf []byte
// flags: 0 (ChecksumPresent=0, MoreToCome=0, ExhaustAllowed=0)
buf = append(buf, 0, 0, 0, 0)
// section kind 0: body
buf = append(buf, 0)
// 构建 BSON 文档
if len(db) > 0 {
// {$db: "admin", ...}
docs := mongoDoc{"$db": db}
for i := 0; i < len(args); i++ {
switch v := args[i].(type) {
case string:
if i+1 < len(args) {
docs[v] = args[i+1]
i++
}
case mongoDoc:
for k, val := range v {
docs[k] = val
}
}
}
return append(buf, buildBSON(docs)...)
}
// 简单命令: {commandName: 1, $db: "admin"}
if len(args) >= 1 {
docs := mongoDoc{}
if cmdName, ok := args[0].(string); ok {
docs[cmdName] = 1
}
if len(args) >= 2 {
switch v := args[1].(type) {
case mongoDoc:
for k, val := range v {
docs[k] = val
}
}
}
if db != "" {
docs["$db"] = db
}
return append(buf, buildBSON(docs)...)
}
return buf
}
type mongoDoc map[string]interface{}
// buildBSON 构建最小 BSON 文档(仅支持 string/int32/double/binary/subdocument
func buildBSON(doc mongoDoc) []byte {
var buf []byte
for k, v := range doc {
switch val := v.(type) {
case string:
buf = append(buf, 0x02) // type string
buf = append(buf, []byte(k)...)
buf = append(buf, 0x00)
b := []byte(val)
buf = append(buf, byte(len(b)+1), 0, 0, 0)
buf = append(buf, b...)
buf = append(buf, 0x00)
case int:
buf = append(buf, 0x10) // type int32
buf = append(buf, []byte(k)...)
buf = append(buf, 0x00)
i32 := make([]byte, 4)
binary.LittleEndian.PutUint32(i32, uint32(val))
buf = append(buf, i32...)
case float64:
buf = append(buf, 0x01) // type double
buf = append(buf, []byte(k)...)
buf = append(buf, 0x00)
f64 := make([]byte, 8)
binary.LittleEndian.PutUint64(f64, uint64(val))
buf = append(buf, f64...)
case mongoDoc:
buf = append(buf, 0x03) // type document
buf = append(buf, []byte(k)...)
buf = append(buf, 0x00)
sub := buildBSON(val)
buf = append(buf, sub...)
case []byte:
buf = append(buf, 0x05) // type binary
buf = append(buf, []byte(k)...)
buf = append(buf, 0x00)
buf = append(buf, byte(len(val)), 0, 0, 0)
buf = append(buf, 0x00) // subtype 0
buf = append(buf, val...)
case bool:
buf = append(buf, 0x08) // type boolean
buf = append(buf, []byte(k)...)
buf = append(buf, 0x00)
if val {
buf = append(buf, 0x01)
} else {
buf = append(buf, 0x00)
}
}
}
// 终止符
buf = append(buf, 0x00)
// 总长度前缀
lenBuf := make([]byte, 4)
binary.LittleEndian.PutUint32(lenBuf, uint32(len(buf)+4))
return append(lenBuf, buf...)
}
return &AuthResult{
Success: true,
Conn: &mongoClientWrapper{client, ctx},
ErrorType: ErrorTypeUnknown,
Error: nil,
// sendMongoMsg 发送 OP_MSG
func sendMongoMsg(ctx context.Context, conn io.ReadWriter, body []byte, timeout time.Duration) (int, error) {
reqID := nextRequestID()
// 消息头: [4B totalLen] [4B requestID] [4B responseTo] [4B opCode]
totalLen := uint32(len(body) + 16)
header := make([]byte, 16)
binary.LittleEndian.PutUint32(header[0:4], totalLen)
binary.LittleEndian.PutUint32(header[4:8], reqID)
// responseTo=0, opCode=opMsg
binary.LittleEndian.PutUint32(header[12:16], opMsg)
return conn.Write(append(header, body...))
}
// readMongoMsg 读取 MongoDB 响应
func readMongoMsg(conn io.Reader, timeout time.Duration) ([]byte, error) {
// 读取 16 字节消息头
header := make([]byte, 16)
if _, err := io.ReadFull(conn, header); err != nil {
return nil, err
}
msgLen := binary.LittleEndian.Uint32(header[0:4])
if msgLen < 16 {
return nil, fmt.Errorf("invalid message length: %d", msgLen)
}
// 读取剩余 body
bodyLen := int(msgLen) - 16
if bodyLen <= 0 || bodyLen > 1024*1024 {
return nil, nil
}
body := make([]byte, bodyLen)
if _, err := io.ReadFull(conn, body); err != nil {
return nil, err
}
// 跳过 OP_MSG 头部 (flags + sections),返回可用部分
// flags: 4 bytes, section kind: 1 byte → skip 5 bytes
if bodyLen > 5 {
return body[5:], nil
}
return body, nil
}
// mongoClientWrapper 包装 mongo.Client 以实现 io.Closer
type mongoClientWrapper struct {
*mongo.Client
ctx context.Context
// dialTCP 带超时的 TCP 连接
func dialTCP(ctx context.Context, addr string, timeout time.Duration) (net.Conn, error) {
dialer := net.Dialer{Timeout: timeout}
return dialer.DialContext(ctx, "tcp", addr)
}
func (w *mongoClientWrapper) Close() error {
return w.Disconnect(w.ctx)
// base64EncodeStr Base64 编码(标准编码)
func base64EncodeStr(s string) string {
return base64.StdEncoding.EncodeToString([]byte(s))
}
// randomString 生成加密安全的随机字符串
func randomString(n int) string {
const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
b := make([]byte, n)
if _, err := rand.Read(b); err != nil {
// 回退:不安全但不会失败
for i := range b {
b[i] = letters[i%len(letters)]
}
return string(b)
}
for i := range b {
b[i] = letters[int(b[i])%len(letters)]
}
return string(b)
}
// classifyMongoDBErrorType MongoDB错误分类
func classifyMongoDBErrorType(err error) ErrorType {
if err == nil {
return ErrorTypeUnknown
}
mongoAuthErrors := []string{
"authentication failed",
"auth mechanism",
"unauthorized",
"scram",
"credential",
"invalid username",
"invalid password",
"login failed",
"access denied",
"authentication mechanism",
"sasl",
"mongo auth",
"bad auth",
"wrong credentials",
}
mongoNetworkErrors := append(CommonNetworkErrors,
"dial tcp",
"connection closed",
"eof",
"server selection timeout",
"connection pool closed",
"no reachable servers",
"topology",
"network error",
)
return ClassifyError(err, mongoAuthErrors, mongoNetworkErrors)
}
// ── 服务识别 ────────────────────────────────────────────────────
func (p *MongoDBPlugin) identifyService(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
target := info.Target()
isUnauth, err := p.mongodbUnauth(ctx, info, session)
if err != nil {
return &ScanResult{
Success: false,
Service: "mongodb",
Error: err,
}
return &ScanResult{Success: false, Service: "mongodb", Error: err}
}
if isUnauth {
common.LogVuln(i18n.Tr("mongodb_unauth", target))
return &ScanResult{
Type: plugins.ResultTypeVuln,
Success: true,
Service: "mongodb",
VulInfo: "未授权访问",
}
return &ScanResult{Type: plugins.ResultTypeVuln, Success: true, Service: "mongodb", VulInfo: "未授权访问"}
}
common.LogSuccess(i18n.Tr("mongodb_auth_required", target))
return &ScanResult{
Type: plugins.ResultTypeService,
Success: true,
Service: "mongodb",
Banner: "需要认证",
}
return &ScanResult{Type: plugins.ResultTypeService, Success: true, Service: "mongodb", Banner: "需要认证"}
}
// mongodbUnauth 检测MongoDB未授权访问
func (p *MongoDBPlugin) mongodbUnauth(ctx context.Context, info *common.HostInfo, session *common.ScanSession) (bool, error) {
msgPacket := p.createOpMsgPacket()
queryPacket := p.createOpQueryPacket()
realhost := fmt.Sprintf("%s:%d", info.Host, info.Port)
reply, err := p.checkMongoAuth(ctx, realhost, msgPacket, session)
reply, err := p.checkMongoAuth(ctx, realhost, createOpMsgPacket(), session)
if err != nil {
reply, err = p.checkMongoAuth(ctx, realhost, queryPacket, session)
reply, err = p.checkMongoAuth(ctx, realhost, createOpQueryPacket(), session)
if err != nil {
return false, err
}
@@ -240,7 +405,6 @@ func (p *MongoDBPlugin) mongodbUnauth(ctx context.Context, info *common.HostInfo
return false, fmt.Errorf("%s", i18n.Tr("service_not_identified", "MongoDB"))
}
// checkMongoAuth 检查MongoDB认证状态
func (p *MongoDBPlugin) checkMongoAuth(ctx context.Context, address string, packet []byte, session *common.ScanSession) (string, error) {
conn, err := session.DialTCP(ctx, "tcp", address, session.Config.Timeout)
if err != nil {
@@ -255,11 +419,11 @@ func (p *MongoDBPlugin) checkMongoAuth(ctx context.Context, address string, pack
}
if deadlineErr := conn.SetDeadline(time.Now().Add(session.Config.Timeout)); deadlineErr != nil {
return "", fmt.Errorf("设置超时失败: %w", deadlineErr)
return "", deadlineErr
}
if _, writeErr := conn.Write(packet); writeErr != nil {
return "", fmt.Errorf("发送查询失败: %w", writeErr)
return "", writeErr
}
select {
@@ -270,8 +434,8 @@ func (p *MongoDBPlugin) checkMongoAuth(ctx context.Context, address string, pack
reply := make([]byte, 2048)
count, err := conn.Read(reply)
if err != nil && !errors.Is(err, io.EOF) {
return "", fmt.Errorf("读取响应失败: %w", err)
if err != nil && err != io.EOF {
return "", err
}
if count == 0 {
@@ -281,8 +445,7 @@ func (p *MongoDBPlugin) checkMongoAuth(ctx context.Context, address string, pack
return string(reply[:count]), nil
}
// createOpMsgPacket 创建OP_MSG查询包
func (p *MongoDBPlugin) createOpMsgPacket() []byte {
func createOpMsgPacket() []byte {
return []byte{
0x69, 0x00, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xdd, 0x07, 0x00, 0x00,
@@ -300,8 +463,7 @@ func (p *MongoDBPlugin) createOpMsgPacket() []byte {
}
}
// createOpQueryPacket 创建OP_QUERY查询包
func (p *MongoDBPlugin) createOpQueryPacket() []byte {
func createOpQueryPacket() []byte {
return []byte{
0x48, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xd4, 0x07, 0x00, 0x00,