package services import ( "encoding/binary" "net" "strconv" "strings" "time" ) func NewMongoDBHandler(log LoggerFunc) Handler { return func(conn net.Conn) { defer conn.Close() remote := conn.RemoteAddr().String() conn.SetDeadline(time.Now().Add(30 * time.Second)) buf := make([]byte, 4096) n, err := conn.Read(buf) if err != nil { return } details := map[string]string{"event":"protocol_attempt","bytes_received":strconv.Itoa(n)} if n >= 16 { msgLen := int32(binary.LittleEndian.Uint32(buf[0:4])) // reqID := int32(binary.LittleEndian.Uint32(buf[4:8])) // respTo := int32(binary.LittleEndian.Uint32(buf[8:12])) opCode := int32(binary.LittleEndian.Uint32(buf[12:16])) details["opcode"] = strconv.Itoa(int(opCode)) details["msg_len"] = strconv.Itoa(int(msgLen)) if opCode == 2004 { // OP_QUERY // flags (4) + cstring ns starting at offset 20 // skip flags i := 20 // extract cstring namespace end := i for end < n && buf[end] != 0 { end++ } ns := string(buf[i:end]) details["namespace"] = ns } else if opCode == 2013 { // OP_MSG (modern commands like hello) // Structure: flags (4) + sections... payload := buf[16:n] if len(payload) >= 5 { // first section kind := payload[4] if kind == 0 && len(payload) > 5 { // body BSON document doc := payload[5:] out := map[string]string{} // Flatten with prefix, cap strings to 64, cap total fields to 32 parseBSONFlat(doc, 64, "", out, 32) // Extract common command indicators for _, k := range []string{"hello","isMaster","ismaster","saslStart","saslContinue","client","mechanism"} { if v, ok := out[k]; ok { details["op_msg_"+k] = v } } if _, ok := out["hello"]; ok { details["op_msg_hint"] = "hello" } if _, ok := out["isMaster"]; ok { details["op_msg_hint"] = "isMaster" } if _, ok := out["ismaster"]; ok { details["op_msg_hint"] = "ismaster" } if _, ok := out["saslStart"]; ok { details["op_msg_hint"] = "saslStart" } } } } } log(Record{Timestamp: Now(), RemoteAddr: remoteIP(remote), RemotePort: remotePort(remote), Service: "mongodb", Details: details}) // Send a minimal isMaster/hello-like JSON to keep client talking reply := `{"ok":1,"ismaster":true,"maxWireVersion":13,"minWireVersion":0,"helloOk":true}` _, _ = conn.Write([]byte(reply)) } } // parseBSONFlat flattens a BSON document or array into out with key prefixing. // - maxStr: maximum string length to record // - prefix: current key prefix (e.g., "client.") // - out: destination map // - maxFields: stop after this many fields to avoid floods func parseBSONFlat(b []byte, maxStr int, prefix string, out map[string]string, maxFields int) { if len(out) >= maxFields || len(b) < 5 { return } // First 4 bytes are int32 length; be lenient but keep bounds i := 4 for i < len(b) && len(out) < maxFields { t := b[i] if t == 0x00 { // EOO break } i++ // cstring key ks := i for i < len(b) && b[i] != 0 { i++ } if i >= len(b) { return } key := string(b[ks:i]) if prefix != "" { key = prefix + key } i++ // skip NUL switch t { case 0x01: // double if i+8 > len(b) { return } out[key] = "" i += 8 case 0x02: // string if i+4 > len(b) { return } sl := int(int32(binary.LittleEndian.Uint32(b[i : i+4]))) i += 4 if sl <= 0 || i+sl > len(b) { return } val := string(b[i : i+sl-1]) // exclude trailing NUL if maxStr > 0 && len(val) > maxStr { val = val[:maxStr] } out[key] = val i += sl case 0x03: // embedded document if i+4 > len(b) { return } // read declared length to bound sub-doc subLen := int(int32(binary.LittleEndian.Uint32(b[i : i+4]))) end := i + subLen if subLen <= 4 || end > len(b) { return } parseBSONFlat(b[i:end], maxStr, key+".", out, maxFields) i = end case 0x04: // array if i+4 > len(b) { return } subLen := int(int32(binary.LittleEndian.Uint32(b[i : i+4]))) end := i + subLen if subLen <= 4 || end > len(b) { return } parseBSONFlat(b[i:end], maxStr, key+".", out, maxFields) i = end case 0x08: // bool if i >= len(b) { return } if b[i] == 0x01 { out[key] = "true" } else { out[key] = "false" } i++ case 0x10: // int32 if i+4 > len(b) { return } v := int32(binary.LittleEndian.Uint32(b[i : i+4])) out[key] = strconv.FormatInt(int64(v), 10) i += 4 case 0x12: // int64 if i+8 > len(b) { return } out[key] = "" i += 8 default: // skip unknown types conservatively: stop parsing to avoid desync return } } } // containsAny reports whether s contains any of the substrings in list. func containsAny(s string, list []string) bool { ls := strings.ToLower(s) for _, t := range list { if t == "" { continue } if strings.Contains(ls, strings.ToLower(t)) { return true } } return false } // parseBSONStrings extracts top-level string/boolean/int32/double markers from a BSON document buffer. // It is intentionally minimal and bounded for honeypot logging. func parseBSONStrings(b []byte, maxStr int) map[string]string { out := map[string]string{} if len(b) < 5 { return out } i := 4 // skip doc length for i < len(b) { t := b[i] if t == 0x00 { // terminator break } i++ // read cstring key ks := i for i < len(b) && b[i] != 0 { i++ } if i >= len(b) { break } key := string(b[ks:i]) i++ // skip NUL switch t { case 0x02: // string if i+4 > len(b) { return out } sl := int(int32(binary.LittleEndian.Uint32(b[i : i+4]))) i += 4 if sl <= 0 || i+sl > len(b) { return out } val := string(b[i : i+sl-1]) // exclude trailing NUL if maxStr > 0 && len(val) > maxStr { val = val[:maxStr] } out[key] = val i += sl case 0x08: // boolean if i >= len(b) { return out } if b[i] == 0x01 { out[key] = "true" } else { out[key] = "false" } i++ case 0x10: // int32 if i+4 > len(b) { return out } v := int32(binary.LittleEndian.Uint32(b[i : i+4])) out[key] = strconv.FormatInt(int64(v), 10) i += 4 case 0x01: // double if i+8 > len(b) { return out } out[key] = "" i += 8 default: // Stop on types we don't parse to avoid desync return out } } return out }