Cybersecurity-Projects/PROJECTS/advanced/honeypot-network/internal/redisd/server.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, " ")
}