mirror of
https://github.com/shadow1ng/fscan.git
synced 2026-09-22 03:10:42 +08:00
Go map 遍历顺序不确定,导致 buildBSON 输出的命令文档中 saslStart/saslContinue 不一定是第一个键,MongoDB 拒绝执行。 引入有序 []mongoKV 类型,SASL 命令改用 orderedDoc() 构造。 同时新增 6 协议集成测试框架(Docker Compose + go test -tags integration)。
718 lines
20 KiB
Go
718 lines
20 KiB
Go
//go:build plugin_mongodb || !plugin_selective
|
|
|
|
package services
|
|
|
|
import (
|
|
"context"
|
|
"crypto/hmac"
|
|
"crypto/md5"
|
|
"crypto/rand"
|
|
"crypto/sha1"
|
|
"encoding/base64"
|
|
"encoding/binary"
|
|
"fmt"
|
|
"io"
|
|
"math"
|
|
"net"
|
|
"strconv"
|
|
"strings"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/shadow1ng/fscan/common"
|
|
"github.com/shadow1ng/fscan/common/i18n"
|
|
"github.com/shadow1ng/fscan/plugins"
|
|
"golang.org/x/crypto/pbkdf2"
|
|
)
|
|
|
|
// MongoDBPlugin MongoDB扫描插件(纯 raw TCP 实现,无重型依赖)
|
|
type MongoDBPlugin struct {
|
|
plugins.BasePlugin
|
|
}
|
|
|
|
func NewMongoDBPlugin() *MongoDBPlugin {
|
|
return &MongoDBPlugin{
|
|
BasePlugin: plugins.NewBasePlugin("mongodb"),
|
|
}
|
|
}
|
|
|
|
func (p *MongoDBPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
|
|
config := session.Config
|
|
state := session.State
|
|
target := info.Target()
|
|
|
|
if config.DisableBrute {
|
|
return p.identifyService(ctx, info, session)
|
|
}
|
|
|
|
isUnauth, err := p.mongodbUnauth(ctx, info, session)
|
|
if err != nil {
|
|
return &ScanResult{Success: false, Service: "mongodb", Error: err}
|
|
}
|
|
|
|
if isUnauth {
|
|
session.LogVuln(i18n.Tr("mongodb_unauth", target))
|
|
return &ScanResult{
|
|
Type: plugins.ResultTypeVuln,
|
|
Success: true,
|
|
Service: "mongodb",
|
|
VulInfo: i18n.GetText("unauthorized_access"),
|
|
}
|
|
}
|
|
|
|
credentials := GenerateCredentials("mongodb", config)
|
|
if len(credentials) == 0 {
|
|
return &ScanResult{
|
|
Success: false,
|
|
Service: "mongodb",
|
|
Error: fmt.Errorf("%s", i18n.GetText("service_no_credentials")),
|
|
}
|
|
}
|
|
|
|
authFn := p.createAuthFunc(info, config, state)
|
|
testConfig := DefaultConcurrentTestConfigWithTarget(config, info)
|
|
|
|
result := TestCredentialsConcurrently(ctx, credentials, authFn, "mongodb", testConfig)
|
|
|
|
if result.Success {
|
|
session.LogVuln(i18n.Tr("mongodb_credential", target, result.Username, result.Password))
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
// ── raw TCP MongoDB SCRAM 认证 ──────────────────────────────────
|
|
|
|
func (p *MongoDBPlugin) doMongoDBAuth(ctx context.Context, info *common.HostInfo, cred Credential, config *common.Config, state *common.State) *AuthResult {
|
|
addr := info.Target()
|
|
timeout := config.ModuleTimeout()
|
|
|
|
conn, err := dialTCP(ctx, addr, timeout)
|
|
if err != nil {
|
|
state.IncrementTCPFailedPacketCount()
|
|
return &AuthResult{Success: false, ErrorType: classifyMongoDBErrorType(err), Error: err}
|
|
}
|
|
defer conn.Close()
|
|
|
|
// Step 1: isMaster 获取服务参数
|
|
isMasterCmd := buildMongoCommand("admin", "isMaster")
|
|
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)
|
|
clientFirstBare := "n=" + cred.Username + ",r=" + nonce
|
|
saslPayload := "n,," + clientFirstBare
|
|
|
|
saslStartCmd := buildMongoCommand("admin", orderedDoc(
|
|
kv("saslStart", 1),
|
|
kv("mechanism", "SCRAM-SHA-1"),
|
|
kv("payload", []byte(saslPayload)),
|
|
kv("autoAuthorize", 1),
|
|
))
|
|
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 {
|
|
state.IncrementTCPFailedPacketCount()
|
|
return &AuthResult{Success: false, ErrorType: ErrorTypeNetwork, Error: err}
|
|
}
|
|
|
|
startReply, err := parseMongoCommandReply(resp)
|
|
if err != nil {
|
|
state.IncrementTCPFailedPacketCount()
|
|
return &AuthResult{Success: false, ErrorType: ErrorTypeNetwork, Error: err}
|
|
}
|
|
if !startReply.ok {
|
|
return &AuthResult{Success: false, ErrorType: ErrorTypeAuth, Error: fmt.Errorf("authentication failed: %s", startReply.errmsg)}
|
|
}
|
|
if !startReply.conversationSet || len(startReply.payload) == 0 {
|
|
return &AuthResult{Success: false, ErrorType: ErrorTypeAuth, Error: fmt.Errorf("invalid saslStart response")}
|
|
}
|
|
|
|
serverFirst := string(startReply.payload)
|
|
clientFinal, err := buildMongoSCRAMClientFinal(cred.Username, cred.Password, clientFirstBare, serverFirst)
|
|
if err != nil {
|
|
return &AuthResult{Success: false, ErrorType: ErrorTypeAuth, Error: err}
|
|
}
|
|
|
|
saslContinueCmd := buildMongoCommand("admin", orderedDoc(
|
|
kv("saslContinue", 1),
|
|
kv("conversationId", int(startReply.conversationID)),
|
|
kv("payload", []byte(clientFinal)),
|
|
))
|
|
if _, err := sendMongoMsg(ctx, conn, saslContinueCmd, timeout); err != nil {
|
|
state.IncrementTCPFailedPacketCount()
|
|
return &AuthResult{Success: false, ErrorType: ErrorTypeNetwork, Error: err}
|
|
}
|
|
resp, err = readMongoMsg(conn, timeout)
|
|
if err != nil {
|
|
state.IncrementTCPFailedPacketCount()
|
|
return &AuthResult{Success: false, ErrorType: ErrorTypeNetwork, Error: err}
|
|
}
|
|
finalReply, err := parseMongoCommandReply(resp)
|
|
if err != nil {
|
|
state.IncrementTCPFailedPacketCount()
|
|
return &AuthResult{Success: false, ErrorType: ErrorTypeNetwork, Error: err}
|
|
}
|
|
if !finalReply.ok {
|
|
return &AuthResult{Success: false, ErrorType: ErrorTypeAuth, Error: fmt.Errorf("authentication failed: %s", finalReply.errmsg)}
|
|
}
|
|
|
|
state.IncrementTCPSuccessPacketCount()
|
|
return &AuthResult{Success: true, ErrorType: ErrorTypeUnknown, Error: nil}
|
|
}
|
|
|
|
// ── MongoDB wire protocol 工具 ──────────────────────────────────
|
|
|
|
const (
|
|
opMsg uint32 = 2013
|
|
opQuery uint32 = 2004
|
|
opReply uint32 = 1
|
|
maxMongoMessageBody = 1024 * 1024
|
|
)
|
|
|
|
var mongoRequestID uint32
|
|
|
|
func nextRequestID() uint32 {
|
|
return atomic.AddUint32(&mongoRequestID, 1)
|
|
}
|
|
|
|
// buildMongoCommand 构建 MongoDB 命令的 OP_MSG body (最小 BSON 实现)
|
|
// MongoDB 要求命令名是 BSON 文档的第一个键,因此使用 orderedDoc 保证顺序。
|
|
func buildMongoCommand(db string, args ...interface{}) []byte {
|
|
var buf []byte
|
|
buf = append(buf, 0, 0, 0, 0) // flags
|
|
buf = append(buf, 0) // section kind 0: body
|
|
|
|
var doc []mongoKV
|
|
|
|
for i := 0; i < len(args); i++ {
|
|
switch v := args[i].(type) {
|
|
case string:
|
|
if i+1 < len(args) {
|
|
doc = append(doc, kv(v, args[i+1]))
|
|
i++
|
|
} else {
|
|
doc = append(doc, kv(v, 1))
|
|
}
|
|
case mongoDoc:
|
|
for k, val := range v {
|
|
doc = append(doc, kv(k, val))
|
|
}
|
|
case []mongoKV:
|
|
doc = append(doc, v...)
|
|
}
|
|
}
|
|
|
|
if db != "" {
|
|
doc = append(doc, kv("$db", db))
|
|
}
|
|
|
|
return append(buf, buildBSON(doc)...)
|
|
}
|
|
|
|
type mongoDoc = map[string]interface{}
|
|
|
|
type mongoKV struct {
|
|
Key string
|
|
Value interface{}
|
|
}
|
|
|
|
func orderedDoc(kvs ...mongoKV) []mongoKV { return kvs }
|
|
func kv(k string, v interface{}) mongoKV { return mongoKV{k, v} }
|
|
|
|
func buildBSON(doc interface{}) []byte {
|
|
var pairs []mongoKV
|
|
switch d := doc.(type) {
|
|
case []mongoKV:
|
|
pairs = d
|
|
case mongoDoc:
|
|
for k, v := range d {
|
|
pairs = append(pairs, mongoKV{k, v})
|
|
}
|
|
default:
|
|
return []byte{5, 0, 0, 0, 0}
|
|
}
|
|
var buf []byte
|
|
for _, p := range pairs {
|
|
k, v := p.Key, p.Value
|
|
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 = binary.LittleEndian.AppendUint32(buf, uint32(len(b)+1))
|
|
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 int64:
|
|
buf = append(buf, 0x12) // type int64
|
|
buf = append(buf, []byte(k)...)
|
|
buf = append(buf, 0x00)
|
|
buf = binary.LittleEndian.AppendUint64(buf, uint64(val))
|
|
case float64:
|
|
buf = append(buf, 0x01) // type double
|
|
buf = append(buf, []byte(k)...)
|
|
buf = append(buf, 0x00)
|
|
buf = binary.LittleEndian.AppendUint64(buf, math.Float64bits(val))
|
|
case mongoDoc:
|
|
buf = append(buf, 0x03) // type document
|
|
buf = append(buf, []byte(k)...)
|
|
buf = append(buf, 0x00)
|
|
buf = append(buf, buildBSON(val)...)
|
|
case []mongoKV:
|
|
buf = append(buf, 0x03) // type document
|
|
buf = append(buf, []byte(k)...)
|
|
buf = append(buf, 0x00)
|
|
buf = append(buf, buildBSON(val)...)
|
|
case []byte:
|
|
buf = append(buf, 0x05) // type binary
|
|
buf = append(buf, []byte(k)...)
|
|
buf = append(buf, 0x00)
|
|
buf = binary.LittleEndian.AppendUint32(buf, uint32(len(val)))
|
|
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...)
|
|
}
|
|
|
|
// 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) {
|
|
if tc, ok := conn.(interface{ SetReadDeadline(time.Time) error }); ok {
|
|
_ = tc.SetReadDeadline(time.Now().Add(timeout))
|
|
}
|
|
// 读取 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 {
|
|
return []byte{}, nil
|
|
}
|
|
if bodyLen > maxMongoMessageBody {
|
|
return nil, fmt.Errorf("mongodb response too large: %d", msgLen)
|
|
}
|
|
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
|
|
}
|
|
|
|
type mongoCommandReply struct {
|
|
ok bool
|
|
conversationID int32
|
|
conversationSet bool
|
|
payload []byte
|
|
done bool
|
|
errmsg string
|
|
}
|
|
|
|
//nolint:gocyclo
|
|
func parseMongoCommandReply(doc []byte) (mongoCommandReply, error) {
|
|
var reply mongoCommandReply
|
|
if len(doc) < 5 {
|
|
return reply, fmt.Errorf("short bson document")
|
|
}
|
|
docLen := int(binary.LittleEndian.Uint32(doc[:4]))
|
|
if docLen < 5 || docLen > len(doc) {
|
|
return reply, fmt.Errorf("invalid bson document length: %d", docLen)
|
|
}
|
|
pos := 4
|
|
for pos < docLen-1 {
|
|
typ := doc[pos]
|
|
pos++
|
|
keyStart := pos
|
|
for pos < docLen && doc[pos] != 0 {
|
|
pos++
|
|
}
|
|
if pos >= docLen {
|
|
return reply, fmt.Errorf("unterminated bson key")
|
|
}
|
|
key := string(doc[keyStart:pos])
|
|
pos++
|
|
|
|
switch typ {
|
|
case 0x01: // double
|
|
if pos+8 > docLen {
|
|
return reply, fmt.Errorf("short bson double")
|
|
}
|
|
if key == "ok" {
|
|
reply.ok = binary.LittleEndian.Uint64(doc[pos:pos+8]) != 0
|
|
}
|
|
pos += 8
|
|
case 0x02: // string
|
|
if pos+4 > docLen {
|
|
return reply, fmt.Errorf("short bson string length")
|
|
}
|
|
n := int(binary.LittleEndian.Uint32(doc[pos : pos+4]))
|
|
pos += 4
|
|
if n <= 0 || pos+n > docLen {
|
|
return reply, fmt.Errorf("invalid bson string length: %d", n)
|
|
}
|
|
value := string(doc[pos : pos+n-1])
|
|
pos += n
|
|
switch key {
|
|
case "errmsg":
|
|
reply.errmsg = value
|
|
case "payload":
|
|
reply.payload = []byte(value)
|
|
}
|
|
case 0x05: // binary
|
|
if pos+5 > docLen {
|
|
return reply, fmt.Errorf("short bson binary")
|
|
}
|
|
n := int(binary.LittleEndian.Uint32(doc[pos : pos+4]))
|
|
pos += 5 // length + subtype
|
|
if n < 0 || pos+n > docLen {
|
|
return reply, fmt.Errorf("invalid bson binary length: %d", n)
|
|
}
|
|
if key == "payload" {
|
|
reply.payload = append([]byte(nil), doc[pos:pos+n]...)
|
|
}
|
|
pos += n
|
|
case 0x03, 0x04: // document, array
|
|
if pos+4 > docLen {
|
|
return reply, fmt.Errorf("short bson embedded document")
|
|
}
|
|
n := int(binary.LittleEndian.Uint32(doc[pos : pos+4]))
|
|
if n < 5 || pos+n > docLen {
|
|
return reply, fmt.Errorf("invalid bson embedded document length: %d", n)
|
|
}
|
|
pos += n
|
|
case 0x07: // objectId
|
|
if pos+12 > docLen {
|
|
return reply, fmt.Errorf("short bson objectId")
|
|
}
|
|
pos += 12
|
|
case 0x08: // bool
|
|
if pos+1 > docLen {
|
|
return reply, fmt.Errorf("short bson bool")
|
|
}
|
|
if key == "done" {
|
|
reply.done = doc[pos] != 0
|
|
}
|
|
if key == "ok" {
|
|
reply.ok = doc[pos] != 0
|
|
}
|
|
pos++
|
|
case 0x10: // int32
|
|
if pos+4 > docLen {
|
|
return reply, fmt.Errorf("short bson int32")
|
|
}
|
|
value := int32(binary.LittleEndian.Uint32(doc[pos : pos+4]))
|
|
if key == "conversationId" {
|
|
reply.conversationID = value
|
|
reply.conversationSet = true
|
|
}
|
|
if key == "ok" {
|
|
reply.ok = value != 0
|
|
}
|
|
pos += 4
|
|
case 0x09, 0x11: // datetime, timestamp
|
|
if pos+8 > docLen {
|
|
return reply, fmt.Errorf("short bson fixed64")
|
|
}
|
|
pos += 8
|
|
case 0x0a, 0x7f, 0xff: // null, maxKey, minKey
|
|
case 0x12: // int64
|
|
if pos+8 > docLen {
|
|
return reply, fmt.Errorf("short bson int64")
|
|
}
|
|
if key == "ok" {
|
|
reply.ok = binary.LittleEndian.Uint64(doc[pos:pos+8]) != 0
|
|
}
|
|
pos += 8
|
|
case 0x13: // decimal128
|
|
if pos+16 > docLen {
|
|
return reply, fmt.Errorf("short bson decimal128")
|
|
}
|
|
pos += 16
|
|
default:
|
|
return reply, fmt.Errorf("unsupported bson type 0x%02x for key %s", typ, key)
|
|
}
|
|
}
|
|
return reply, nil
|
|
}
|
|
|
|
func buildMongoSCRAMClientFinal(username, password, clientFirstBare, serverFirst string) (string, error) {
|
|
attrs := parseSCRAMAttributes(serverFirst)
|
|
serverNonce := attrs["r"]
|
|
saltB64 := attrs["s"]
|
|
iterText := attrs["i"]
|
|
if serverNonce == "" || saltB64 == "" || iterText == "" {
|
|
return "", fmt.Errorf("invalid SCRAM server-first payload")
|
|
}
|
|
clientNonce := scramAttr(clientFirstBare, "r")
|
|
if clientNonce == "" || !strings.HasPrefix(serverNonce, clientNonce) {
|
|
return "", fmt.Errorf("invalid SCRAM nonce")
|
|
}
|
|
salt, err := base64.StdEncoding.DecodeString(saltB64)
|
|
if err != nil {
|
|
return "", fmt.Errorf("invalid SCRAM salt: %w", err)
|
|
}
|
|
iterations, err := strconv.Atoi(iterText)
|
|
if err != nil || iterations <= 0 {
|
|
return "", fmt.Errorf("invalid SCRAM iteration count")
|
|
}
|
|
|
|
clientFinalWithoutProof := "c=biws,r=" + serverNonce
|
|
authMessage := clientFirstBare + "," + serverFirst + "," + clientFinalWithoutProof
|
|
digest := md5.Sum([]byte(username + ":mongo:" + password))
|
|
saltedPassword := pbkdf2.Key([]byte(fmt.Sprintf("%x", digest)), salt, iterations, sha1.Size, sha1.New)
|
|
clientKey := mongoHMAC(saltedPassword, []byte("Client Key"))
|
|
storedKey := sha1.Sum(clientKey)
|
|
clientSignature := mongoHMAC(storedKey[:], []byte(authMessage))
|
|
proof := make([]byte, len(clientKey))
|
|
for i := range clientKey {
|
|
proof[i] = clientKey[i] ^ clientSignature[i]
|
|
}
|
|
return clientFinalWithoutProof + ",p=" + base64.StdEncoding.EncodeToString(proof), nil
|
|
}
|
|
|
|
func parseSCRAMAttributes(payload string) map[string]string {
|
|
attrs := make(map[string]string)
|
|
for _, part := range strings.Split(payload, ",") {
|
|
if len(part) < 3 || part[1] != '=' {
|
|
continue
|
|
}
|
|
attrs[part[:1]] = part[2:]
|
|
}
|
|
return attrs
|
|
}
|
|
|
|
func scramAttr(payload, key string) string {
|
|
return parseSCRAMAttributes(payload)[key]
|
|
}
|
|
|
|
func mongoHMAC(key, data []byte) []byte {
|
|
mac := hmac.New(sha1.New, key)
|
|
_, _ = mac.Write(data)
|
|
return mac.Sum(nil)
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
func classifyMongoDBErrorType(err error) ErrorType {
|
|
if err == nil {
|
|
return ErrorTypeUnknown
|
|
}
|
|
mongoAuthErrors := []string{
|
|
"authentication failed",
|
|
"auth mechanism",
|
|
"unauthorized",
|
|
"scram",
|
|
"credential",
|
|
"bad auth",
|
|
}
|
|
mongoNetworkErrors := append(CommonNetworkErrors,
|
|
"dial tcp",
|
|
"connection closed",
|
|
"eof",
|
|
)
|
|
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}
|
|
}
|
|
|
|
if isUnauth {
|
|
session.LogVuln(i18n.Tr("mongodb_unauth", target))
|
|
return &ScanResult{Type: plugins.ResultTypeVuln, Success: true, Service: "mongodb", VulInfo: i18n.GetText("unauthorized_access")}
|
|
}
|
|
|
|
session.LogSuccess(i18n.Tr("mongodb_auth_required", target))
|
|
return &ScanResult{Type: plugins.ResultTypeService, Success: true, Service: "mongodb", Banner: i18n.GetText("auth_required")}
|
|
}
|
|
|
|
func (p *MongoDBPlugin) mongodbUnauth(ctx context.Context, info *common.HostInfo, session *common.ScanSession) (bool, error) {
|
|
realhost := info.Target()
|
|
|
|
reply, err := p.checkMongoAuth(ctx, realhost, createOpMsgPacket(), session)
|
|
if err != nil {
|
|
reply, err = p.checkMongoAuth(ctx, realhost, createOpQueryPacket(), session)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
}
|
|
|
|
if strings.Contains(reply, "totalLinesWritten") {
|
|
return true, nil
|
|
}
|
|
|
|
if len(reply) > 0 {
|
|
return false, nil
|
|
}
|
|
|
|
return false, fmt.Errorf("%s", i18n.Tr("service_not_identified", "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.ModuleTimeout())
|
|
if err != nil {
|
|
return "", fmt.Errorf(i18n.Tr("service_connection_failed", "%w"), err)
|
|
}
|
|
defer func() { _ = conn.Close() }()
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
return "", ctx.Err()
|
|
default:
|
|
}
|
|
|
|
if deadlineErr := conn.SetDeadline(time.Now().Add(session.Config.ModuleTimeout())); deadlineErr != nil {
|
|
return "", deadlineErr
|
|
}
|
|
|
|
if _, writeErr := conn.Write(packet); writeErr != nil {
|
|
return "", writeErr
|
|
}
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
return "", ctx.Err()
|
|
default:
|
|
}
|
|
|
|
reply := make([]byte, 2048)
|
|
count, err := conn.Read(reply)
|
|
if err != nil && err != io.EOF {
|
|
return "", err
|
|
}
|
|
|
|
if count == 0 {
|
|
return "", fmt.Errorf("%s", i18n.GetText("empty_response_received"))
|
|
}
|
|
|
|
return string(reply[:count]), nil
|
|
}
|
|
|
|
func createOpMsgPacket() []byte {
|
|
return []byte{
|
|
0x69, 0x00, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0xdd, 0x07, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x54, 0x00, 0x00,
|
|
0x00, 0x02, 0x67, 0x65, 0x74, 0x4c, 0x6f, 0x67,
|
|
0x00, 0x10, 0x00, 0x00, 0x00, 0x73, 0x74, 0x61,
|
|
0x72, 0x74, 0x75, 0x70, 0x57, 0x61, 0x72, 0x6e,
|
|
0x69, 0x6e, 0x67, 0x73, 0x00, 0x02, 0x24, 0x64,
|
|
0x62, 0x00, 0x06, 0x00, 0x00, 0x00, 0x61, 0x64,
|
|
0x6d, 0x69, 0x6e, 0x00, 0x03, 0x6c, 0x73, 0x69,
|
|
0x64, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x05, 0x69,
|
|
0x64, 0x00, 0x10, 0x00, 0x00, 0x00, 0x04, 0x6e,
|
|
0x81, 0xf8, 0x8e, 0x37, 0x7b, 0x4c, 0x97, 0x84,
|
|
0x4e, 0x90, 0x62, 0x5a, 0x54, 0x3c, 0x93, 0x00, 0x00,
|
|
}
|
|
}
|
|
|
|
func createOpQueryPacket() []byte {
|
|
return []byte{
|
|
0x48, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0xd4, 0x07, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x61, 0x64, 0x6d, 0x69,
|
|
0x6e, 0x2e, 0x24, 0x63, 0x6d, 0x64, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x21,
|
|
0x00, 0x00, 0x00, 0x02, 0x67, 0x65, 0x74, 0x4c,
|
|
0x6f, 0x67, 0x00, 0x10, 0x00, 0x00, 0x00, 0x73,
|
|
0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x57, 0x61,
|
|
0x72, 0x6e, 0x69, 0x6e, 0x67, 0x73, 0x00, 0x00,
|
|
}
|
|
}
|
|
|
|
func init() {
|
|
RegisterPluginWithPorts("mongodb", func() Plugin {
|
|
return NewMongoDBPlugin()
|
|
}, []int{27017, 27018, 27019})
|
|
}
|