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() {
|
||||
|
||||
+170
-114
@@ -4,16 +4,18 @@ package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"strings"
|
||||
"io"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/IBM/sarama"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
)
|
||||
|
||||
// KafkaPlugin Kafka扫描插件
|
||||
// KafkaPlugin Kafka扫描插件(纯 raw TCP 实现,无重型依赖)
|
||||
type KafkaPlugin struct {
|
||||
plugins.BasePlugin
|
||||
}
|
||||
@@ -42,7 +44,6 @@ func (p *KafkaPlugin) Scan(ctx context.Context, info *common.HostInfo, session *
|
||||
}
|
||||
}
|
||||
|
||||
// 使用公共框架进行并发凭据测试
|
||||
authFn := p.createAuthFunc(info, config, state)
|
||||
testConfig := DefaultConcurrentTestConfigWithTarget(config, info)
|
||||
|
||||
@@ -55,168 +56,223 @@ func (p *KafkaPlugin) Scan(ctx context.Context, info *common.HostInfo, session *
|
||||
return result
|
||||
}
|
||||
|
||||
// createAuthFunc 创建Kafka认证函数
|
||||
func (p *KafkaPlugin) createAuthFunc(info *common.HostInfo, config *common.Config, state *common.State) AuthFunc {
|
||||
return func(ctx context.Context, cred Credential) *AuthResult {
|
||||
return p.doKafkaAuth(ctx, info, cred, config, state)
|
||||
}
|
||||
}
|
||||
|
||||
// doKafkaAuth 执行Kafka认证
|
||||
// ── raw TCP Kafka 实现 ──────────────────────────────────────────
|
||||
|
||||
func (p *KafkaPlugin) doKafkaAuth(ctx context.Context, info *common.HostInfo, cred Credential, config *common.Config, state *common.State) *AuthResult {
|
||||
target := info.Target()
|
||||
target := fmt.Sprintf("%s:%d", info.Host, info.Port)
|
||||
timeout := config.Timeout
|
||||
|
||||
kafkaConfig := sarama.NewConfig()
|
||||
kafkaConfig.Net.DialTimeout = config.Timeout
|
||||
kafkaConfig.Net.ReadTimeout = config.Timeout
|
||||
kafkaConfig.Net.WriteTimeout = config.Timeout
|
||||
kafkaConfig.Version = sarama.V2_0_0_0
|
||||
|
||||
if cred.Username != "" || cred.Password != "" {
|
||||
kafkaConfig.Net.SASL.Enable = true
|
||||
kafkaConfig.Net.SASL.Mechanism = sarama.SASLTypePlaintext
|
||||
kafkaConfig.Net.SASL.User = cred.Username
|
||||
kafkaConfig.Net.SASL.Password = cred.Password
|
||||
kafkaConfig.Net.SASL.Handshake = true
|
||||
}
|
||||
|
||||
type kafkaResult struct {
|
||||
client sarama.Client
|
||||
err error
|
||||
}
|
||||
|
||||
resultChan := make(chan kafkaResult, 1)
|
||||
go func() {
|
||||
client, err := sarama.NewClient([]string{target}, kafkaConfig)
|
||||
resultChan <- kafkaResult{client: client, err: err}
|
||||
}()
|
||||
|
||||
select {
|
||||
case result := <-resultChan:
|
||||
if result.err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: classifyKafkaErrorType(result.err),
|
||||
Error: result.err,
|
||||
}
|
||||
}
|
||||
state.IncrementTCPSuccessPacketCount()
|
||||
return &AuthResult{
|
||||
Success: true,
|
||||
Conn: &kafkaClientWrapper{result.client},
|
||||
ErrorType: ErrorTypeUnknown,
|
||||
Error: nil,
|
||||
}
|
||||
case <-ctx.Done():
|
||||
// context 被取消,启动清理协程等待并关闭可能创建的 client
|
||||
go func() {
|
||||
result := <-resultChan
|
||||
if result.client != nil {
|
||||
_ = result.client.Close()
|
||||
}
|
||||
}()
|
||||
dialer := net.Dialer{Timeout: timeout}
|
||||
conn, err := dialer.DialContext(ctx, "tcp", target)
|
||||
if err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return &AuthResult{
|
||||
Success: false,
|
||||
ErrorType: ErrorTypeNetwork,
|
||||
Error: ctx.Err(),
|
||||
ErrorType: classifyKafkaErrorType(err),
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
defer conn.Close()
|
||||
_ = conn.SetDeadline(time.Now().Add(timeout))
|
||||
|
||||
// Step 1: ApiVersions 握手 (api_key=18, api_version=0)
|
||||
if err := kafkaSend(conn, 18, 0, nil); err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return &AuthResult{Success: false, ErrorType: ErrorTypeNetwork, Error: err}
|
||||
}
|
||||
_, err = kafkaRecv(conn)
|
||||
if err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return &AuthResult{Success: false, ErrorType: ErrorTypeNetwork, Error: err}
|
||||
}
|
||||
|
||||
// Step 2: SASL/PLAIN 认证 (如果需要)
|
||||
if cred.Username != "" || cred.Password != "" {
|
||||
// SaslHandshake: mechanism=PLAIN (api_key=17, api_version=0)
|
||||
body := kafkaString("PLAIN")
|
||||
if err := kafkaSend(conn, 17, 0, body); err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return &AuthResult{Success: false, ErrorType: ErrorTypeNetwork, Error: err}
|
||||
}
|
||||
resp, err := kafkaRecv(conn)
|
||||
if err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return &AuthResult{Success: false, ErrorType: classifyKafkaErrorType(err), Error: err}
|
||||
}
|
||||
// SaslHandshake 响应: [4B error_code] + [mechanisms array]
|
||||
if len(resp) >= 2 {
|
||||
code := int16(binary.BigEndian.Uint16(resp[:2]))
|
||||
if code != 0 {
|
||||
return &AuthResult{Success: false, ErrorType: ErrorTypeAuth, Error: fmt.Errorf("SASL handshake error: %d", code)}
|
||||
}
|
||||
}
|
||||
|
||||
// SaslAuthenticate: PLAIN token = \x00user\x00pass (api_key=36, api_version=0)
|
||||
token := []byte("\x00" + cred.Username + "\x00" + cred.Password)
|
||||
authBody := kafkaBytes(token)
|
||||
if err := kafkaSend(conn, 36, 0, authBody); err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return &AuthResult{Success: false, ErrorType: ErrorTypeNetwork, Error: err}
|
||||
}
|
||||
resp, err = kafkaRecv(conn)
|
||||
if err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return &AuthResult{Success: false, ErrorType: classifyKafkaErrorType(err), Error: err}
|
||||
}
|
||||
if len(resp) >= 2 {
|
||||
code := int16(binary.BigEndian.Uint16(resp[:2]))
|
||||
if code != 0 {
|
||||
return &AuthResult{Success: false, ErrorType: ErrorTypeAuth, Error: fmt.Errorf("SASL authenticate error: %d", code)}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: Metadata 请求验证连接 (api_key=3, api_version=0)
|
||||
// body: [topics_array] -> empty array = request all topics
|
||||
metaBody := []byte{0x00, 0x00, 0x00, 0x00} // empty topics array + allow_auto_topic_creation=false
|
||||
if err := kafkaSend(conn, 3, 0, metaBody); err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return &AuthResult{Success: false, ErrorType: ErrorTypeNetwork, Error: err}
|
||||
}
|
||||
_, err = kafkaRecv(conn)
|
||||
if err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return &AuthResult{Success: false, ErrorType: ErrorTypeNetwork, Error: err}
|
||||
}
|
||||
|
||||
state.IncrementTCPSuccessPacketCount()
|
||||
return &AuthResult{Success: true, ErrorType: ErrorTypeUnknown, Error: nil}
|
||||
}
|
||||
|
||||
// kafkaClientWrapper 包装 sarama.Client 以实现 io.Closer
|
||||
type kafkaClientWrapper struct {
|
||||
sarama.Client
|
||||
// ── Kafka 协议编解码 ────────────────────────────────────────────
|
||||
|
||||
var kafkaCorrelationID int32
|
||||
|
||||
func kafkaSend(conn net.Conn, apiKey, apiVersion int16, body []byte) error {
|
||||
corrID := kafkaCorrelationID
|
||||
kafkaCorrelationID++
|
||||
|
||||
// 请求格式: [4B len] [2B api_key] [2B api_version] [4B corr_id] [2B client_id_len] [client_id] [body]
|
||||
clientID := "fscan"
|
||||
totalLen := 2 + 2 + 4 + 2 + len(clientID) + len(body)
|
||||
buf := make([]byte, 4+totalLen)
|
||||
binary.BigEndian.PutUint32(buf[0:4], uint32(totalLen))
|
||||
binary.BigEndian.PutUint16(buf[4:6], uint16(apiKey))
|
||||
binary.BigEndian.PutUint16(buf[6:8], uint16(apiVersion))
|
||||
binary.BigEndian.PutUint32(buf[8:12], uint32(corrID))
|
||||
binary.BigEndian.PutUint16(buf[12:14], uint16(len(clientID)))
|
||||
copy(buf[14:], clientID)
|
||||
copy(buf[14+len(clientID):], body)
|
||||
|
||||
_, err := conn.Write(buf)
|
||||
return err
|
||||
}
|
||||
|
||||
func (w *kafkaClientWrapper) Close() error {
|
||||
return w.Client.Close()
|
||||
func kafkaRecv(conn net.Conn) ([]byte, error) {
|
||||
// 读取 4 字节长度
|
||||
lenBuf := make([]byte, 4)
|
||||
if _, err := io.ReadFull(conn, lenBuf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
msgLen := int(binary.BigEndian.Uint32(lenBuf))
|
||||
// 读取消息体
|
||||
msg := make([]byte, msgLen)
|
||||
if _, err := io.ReadFull(conn, msg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 跳过 correlation_id (4B),返回 body
|
||||
if len(msg) >= 4 {
|
||||
return msg[4:], nil
|
||||
}
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
// classifyKafkaErrorType Kafka错误分类
|
||||
func kafkaString(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 kafkaBytes(b []byte) []byte {
|
||||
buf := make([]byte, 4+len(b))
|
||||
binary.BigEndian.PutUint32(buf, uint32(len(b)))
|
||||
copy(buf[4:], b)
|
||||
return buf
|
||||
}
|
||||
|
||||
// ── 错误分类 ────────────────────────────────────────────────────
|
||||
|
||||
func classifyKafkaErrorType(err error) ErrorType {
|
||||
if err == nil {
|
||||
return ErrorTypeUnknown
|
||||
}
|
||||
|
||||
kafkaAuthErrors := []string{
|
||||
"sasl authentication failed",
|
||||
"authentication failed",
|
||||
"invalid credentials",
|
||||
"unauthorized",
|
||||
"sasl/plain authentication failed",
|
||||
}
|
||||
|
||||
kafkaNetworkErrors := append(CommonNetworkErrors,
|
||||
"kafka: client has run out of available brokers",
|
||||
"broker not available",
|
||||
"no available brokers",
|
||||
)
|
||||
|
||||
return ClassifyError(err, kafkaAuthErrors, kafkaNetworkErrors)
|
||||
}
|
||||
|
||||
// ── 服务识别 ────────────────────────────────────────────────────
|
||||
|
||||
func (p *KafkaPlugin) identifyService(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
|
||||
target := info.Target()
|
||||
timeout := config.Timeout
|
||||
|
||||
// 尝试无认证连接
|
||||
emptyCred := Credential{Username: "", Password: ""}
|
||||
result := p.doKafkaAuth(ctx, info, emptyCred, config, state)
|
||||
if result.Success && result.Conn != nil {
|
||||
_ = result.Conn.Close()
|
||||
banner := "Kafka (无认证)"
|
||||
common.LogSuccess(i18n.Tr("kafka_service", target, banner))
|
||||
return &ScanResult{
|
||||
Type: plugins.ResultTypeService,
|
||||
Success: true,
|
||||
Service: "kafka",
|
||||
Banner: banner,
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试检测协议
|
||||
kafkaConfig := sarama.NewConfig()
|
||||
kafkaConfig.Net.DialTimeout = config.Timeout
|
||||
kafkaConfig.Version = sarama.V2_0_0_0
|
||||
|
||||
client, err := sarama.NewClient([]string{target}, kafkaConfig)
|
||||
dialer := net.Dialer{Timeout: timeout}
|
||||
conn, err := dialer.DialContext(ctx, "tcp", target)
|
||||
if err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
if p.isKafkaProtocolError(err) {
|
||||
banner := "Kafka (需要认证)"
|
||||
common.LogSuccess(i18n.Tr("kafka_service", target, banner))
|
||||
return &ScanResult{
|
||||
Type: plugins.ResultTypeService,
|
||||
Success: true,
|
||||
Service: "kafka",
|
||||
Banner: banner,
|
||||
}
|
||||
}
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "kafka",
|
||||
Error: fmt.Errorf("%s", i18n.Tr("service_not_identified", "Kafka")),
|
||||
}
|
||||
}
|
||||
defer conn.Close()
|
||||
_ = conn.SetDeadline(time.Now().Add(timeout))
|
||||
|
||||
if err := kafkaSend(conn, 18, 0, nil); err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return &ScanResult{Success: false, Service: "kafka", Error: err}
|
||||
}
|
||||
_, err = kafkaRecv(conn)
|
||||
if err != nil {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
if p.isKafkaError(err) {
|
||||
banner := "Kafka (需要认证)"
|
||||
common.LogSuccess(i18n.Tr("kafka_service", target, banner))
|
||||
return &ScanResult{Type: plugins.ResultTypeService, Success: true, Service: "kafka", Banner: banner}
|
||||
}
|
||||
return &ScanResult{Success: false, Service: "kafka", Error: fmt.Errorf("%s", i18n.Tr("service_not_identified", "Kafka"))}
|
||||
}
|
||||
state.IncrementTCPSuccessPacketCount()
|
||||
_ = client.Close()
|
||||
|
||||
banner := "Kafka"
|
||||
common.LogSuccess(i18n.Tr("kafka_service", target, banner))
|
||||
return &ScanResult{
|
||||
Type: plugins.ResultTypeService,
|
||||
Success: true,
|
||||
Service: "kafka",
|
||||
Banner: banner,
|
||||
}
|
||||
return &ScanResult{Type: plugins.ResultTypeService, Success: true, Service: "kafka", Banner: banner}
|
||||
}
|
||||
|
||||
func (p *KafkaPlugin) isKafkaProtocolError(err error) bool {
|
||||
errStr := strings.ToLower(err.Error())
|
||||
return strings.Contains(errStr, "sasl") ||
|
||||
strings.Contains(errStr, "authentication") ||
|
||||
strings.Contains(errStr, "kafka") ||
|
||||
strings.Contains(errStr, "broker")
|
||||
func (p *KafkaPlugin) isKafkaError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
// 连接成功后读不到数据 -> 需要认证的 Kafka
|
||||
return true
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
||||
+265
-103
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user