Files
honeydany/app/services/mongodb.go
T

203 lines
7.6 KiB
Go

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] = "<double>"
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] = "<int64>"
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] = "<double>"
i += 8
default:
// Stop on types we don't parse to avoid desync
return out
}
}
return out
}