Files
fscan/plugins/services/mongodb.go
T
ZacharyZcR 42092d8664 fix: 修复实机测试发现的可靠性问题 (v2.2.0-rc.1)
- UDP 插件在 -p 指定端口时被跳过
- Redis exploit 无超时保护 / readReply 吞没非超时错误
- service_probe 连接丢失后静默成功
- SNMP 探测成功但终端无输出
- SSH 爆破不稳定 (并发过高 + 自适应超时过短 + 限流误判)
- 进度条 isActive 竞态

新增 Config.ModuleTimeout() 协议级超时下限 (≥3s)
新增 ErrorTypeThrottle 限流错误分类
2026-06-14 22:14:02 +08:00

716 lines
20 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//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", 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)
clientFirstBare := "n=" + cred.Username + ",r=" + nonce
saslPayload := "n,," + clientFirstBare
saslStartBody := mongoDoc{
"saslStart": 1,
"mechanism": "SCRAM-SHA-1",
"payload": []byte(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 {
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}
}
saslContinueBody := mongoDoc{
"saslContinue": 1,
"conversationId": int(startReply.conversationID),
"payload": []byte(clientFinal),
}
saslContinueCmd := buildMongoCommand("admin", saslContinueBody)
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 实现)
// 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 = 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)
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 = 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})
}