281 lines
6.0 KiB
Go
281 lines
6.0 KiB
Go
/*
|
|
©AngelaMos | 2026
|
|
server.go
|
|
|
|
Redis RESP protocol honeypot using the redcon library
|
|
|
|
Listens for Redis client connections and dispatches commands through
|
|
the RESP handler. CONFIG SET and SLAVEOF commands are flagged as
|
|
exploit attempts commonly used for cryptominer deployment and
|
|
unauthorized replication attacks. Every connection and command is
|
|
published to the event bus for analysis.
|
|
*/
|
|
|
|
package redisd
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/rs/zerolog"
|
|
"github.com/tidwall/redcon"
|
|
|
|
"github.com/CarterPerez-dev/hive/internal/config"
|
|
"github.com/CarterPerez-dev/hive/internal/event"
|
|
"github.com/CarterPerez-dev/hive/internal/ratelimit"
|
|
"github.com/CarterPerez-dev/hive/internal/session"
|
|
"github.com/CarterPerez-dev/hive/pkg/types"
|
|
)
|
|
|
|
const (
|
|
cmdConfigSet = "CONFIG SET"
|
|
cmdSlaveOf = "SLAVEOF"
|
|
cmdReplicaOf = "REPLICAOF"
|
|
cmdModule = "MODULE"
|
|
)
|
|
|
|
type connState struct {
|
|
sessionID string
|
|
srcIP string
|
|
srcPort int
|
|
keys *safeStore
|
|
}
|
|
|
|
type RedisService struct {
|
|
cfg *config.Config
|
|
bus *event.Bus
|
|
logger zerolog.Logger
|
|
tracker *session.Tracker
|
|
limiter *ratelimit.IPLimiter
|
|
}
|
|
|
|
func New(
|
|
cfg *config.Config,
|
|
bus *event.Bus,
|
|
logger *zerolog.Logger,
|
|
tracker *session.Tracker,
|
|
limiter *ratelimit.IPLimiter,
|
|
) *RedisService {
|
|
return &RedisService{
|
|
cfg: cfg,
|
|
bus: bus,
|
|
logger: logger.With().Str("service", "redis").Logger(),
|
|
tracker: tracker,
|
|
limiter: limiter,
|
|
}
|
|
}
|
|
|
|
func (s *RedisService) Name() string { return "redis" }
|
|
|
|
func (s *RedisService) Start(ctx context.Context) error {
|
|
addr := s.cfg.Addr(s.cfg.Redis.Port)
|
|
|
|
srv := redcon.NewServer(
|
|
addr, s.handleCmd, s.handleAccept, s.handleClose,
|
|
)
|
|
|
|
s.logger.Info().
|
|
Str("addr", addr).
|
|
Msg("redis honeypot listening")
|
|
|
|
go func() {
|
|
<-ctx.Done()
|
|
_ = srv.Close()
|
|
}()
|
|
|
|
return srv.ListenAndServe()
|
|
}
|
|
|
|
func (s *RedisService) handleCmd(
|
|
conn redcon.Conn, cmd redcon.Command,
|
|
) {
|
|
state, ok := conn.Context().(*connState)
|
|
if !ok {
|
|
_ = conn.Close()
|
|
return
|
|
}
|
|
|
|
cmdName := handleCommand(
|
|
conn, cmd, s.cfg.Redis.ServerVersion,
|
|
state.keys,
|
|
)
|
|
|
|
s.publishCommand(state, cmd, cmdName)
|
|
|
|
if isExploitCommand(cmdName) {
|
|
s.publishExploit(state, cmd, cmdName)
|
|
}
|
|
}
|
|
|
|
func (s *RedisService) handleAccept(
|
|
conn redcon.Conn,
|
|
) bool {
|
|
srcIP, srcPort := parseRedconAddr(conn.RemoteAddr())
|
|
|
|
if !s.limiter.Allow(srcIP) {
|
|
return false
|
|
}
|
|
|
|
sess := s.tracker.Start(
|
|
s.cfg.Sensor.ID, types.ServiceRedis,
|
|
srcIP, srcPort, s.cfg.Redis.Port,
|
|
)
|
|
|
|
conn.SetContext(&connState{
|
|
sessionID: sess.ID,
|
|
srcIP: srcIP,
|
|
srcPort: srcPort,
|
|
keys: newSafeStore(),
|
|
})
|
|
|
|
s.publishConnect(sess, srcIP, srcPort)
|
|
return true
|
|
}
|
|
|
|
func (s *RedisService) handleClose(
|
|
conn redcon.Conn, _ error,
|
|
) {
|
|
state, ok := conn.Context().(*connState)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
s.tracker.End(state.sessionID)
|
|
s.publishDisconnect(state)
|
|
}
|
|
|
|
func (s *RedisService) publishConnect(
|
|
sess *types.Session, srcIP string, srcPort int,
|
|
) {
|
|
s.bus.Publish(config.TopicConnect, &types.Event{
|
|
ID: uuid.Must(uuid.NewV7()).String(),
|
|
SessionID: sess.ID,
|
|
SensorID: s.cfg.Sensor.ID,
|
|
Timestamp: time.Now().UTC(),
|
|
ServiceType: types.ServiceRedis,
|
|
EventType: types.EventConnect,
|
|
SourceIP: srcIP,
|
|
SourcePort: srcPort,
|
|
DestPort: s.cfg.Redis.Port,
|
|
Protocol: types.ProtocolTCP,
|
|
SchemaVersion: config.SchemaVersion,
|
|
})
|
|
}
|
|
|
|
func (s *RedisService) publishDisconnect(state *connState) {
|
|
s.bus.Publish(config.TopicDisconnect, &types.Event{
|
|
ID: uuid.Must(uuid.NewV7()).String(),
|
|
SessionID: state.sessionID,
|
|
SensorID: s.cfg.Sensor.ID,
|
|
Timestamp: time.Now().UTC(),
|
|
ServiceType: types.ServiceRedis,
|
|
EventType: types.EventDisconnect,
|
|
SourceIP: state.srcIP,
|
|
SourcePort: state.srcPort,
|
|
DestPort: s.cfg.Redis.Port,
|
|
Protocol: types.ProtocolTCP,
|
|
SchemaVersion: config.SchemaVersion,
|
|
})
|
|
}
|
|
|
|
func (s *RedisService) publishCommand(
|
|
state *connState,
|
|
cmd redcon.Command,
|
|
cmdName string,
|
|
) {
|
|
raw := buildCommandArgs(cmd)
|
|
|
|
serviceData, _ := json.Marshal(map[string]string{
|
|
"command": cmdName,
|
|
"raw": raw,
|
|
})
|
|
|
|
s.bus.Publish(config.TopicCommand, &types.Event{
|
|
ID: uuid.Must(uuid.NewV7()).String(),
|
|
SessionID: state.sessionID,
|
|
SensorID: s.cfg.Sensor.ID,
|
|
Timestamp: time.Now().UTC(),
|
|
ServiceType: types.ServiceRedis,
|
|
EventType: types.EventCommand,
|
|
SourceIP: state.srcIP,
|
|
Protocol: types.ProtocolTCP,
|
|
SchemaVersion: config.SchemaVersion,
|
|
ServiceData: serviceData,
|
|
})
|
|
}
|
|
|
|
func (s *RedisService) publishExploit(
|
|
state *connState,
|
|
cmd redcon.Command,
|
|
cmdName string,
|
|
) {
|
|
raw := buildCommandArgs(cmd)
|
|
|
|
tags := []string{"redis-exploit"}
|
|
switch {
|
|
case cmdName == cmdConfigSet:
|
|
tags = append(
|
|
tags, "redis-rce", "mitre:T1059",
|
|
)
|
|
case cmdName == cmdSlaveOf || cmdName == cmdReplicaOf:
|
|
tags = append(
|
|
tags,
|
|
"unauthorized-replication",
|
|
"mitre:T1021",
|
|
)
|
|
case cmdName == cmdModule:
|
|
tags = append(
|
|
tags, "module-load", "mitre:T1059",
|
|
)
|
|
}
|
|
|
|
serviceData, _ := json.Marshal(map[string]string{
|
|
"command": cmdName,
|
|
"raw": raw,
|
|
})
|
|
|
|
s.bus.Publish(config.TopicExploit, &types.Event{
|
|
ID: uuid.Must(uuid.NewV7()).String(),
|
|
SessionID: state.sessionID,
|
|
SensorID: s.cfg.Sensor.ID,
|
|
Timestamp: time.Now().UTC(),
|
|
ServiceType: types.ServiceRedis,
|
|
EventType: types.EventExploit,
|
|
SourceIP: state.srcIP,
|
|
Protocol: types.ProtocolTCP,
|
|
SchemaVersion: config.SchemaVersion,
|
|
Tags: tags,
|
|
ServiceData: serviceData,
|
|
})
|
|
}
|
|
|
|
func parseRedconAddr(addr string) (string, int) {
|
|
host, portStr, err := net.SplitHostPort(addr)
|
|
if err != nil {
|
|
return addr, 0
|
|
}
|
|
port, _ := strconv.Atoi(portStr)
|
|
return host, port
|
|
}
|
|
|
|
func isExploitCommand(cmdName string) bool {
|
|
switch cmdName {
|
|
case cmdConfigSet, cmdSlaveOf, cmdReplicaOf, cmdModule:
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func buildCommandArgs(cmd redcon.Command) string {
|
|
parts := make([]string, len(cmd.Args))
|
|
for i, arg := range cmd.Args {
|
|
parts[i] = string(arg)
|
|
}
|
|
return strings.Join(parts, " ")
|
|
}
|