Cybersecurity-Projects/PROJECTS/advanced/honeypot-network/internal/ftpd/server.go

278 lines
5.7 KiB
Go

/*
©AngelaMos | 2026
server.go
FTP honeypot service accepting file transfer client connections
Emulates a ProFTPD 1.3.8b server with accept-all authentication.
Logs every credential attempt, command, and uploaded file to the
event bus. PASV mode data channels are opened on demand for
directory listings and upload capture.
*/
package ftpd
import (
"context"
"encoding/json"
"fmt"
"net"
"time"
"github.com/google/uuid"
"github.com/rs/zerolog"
"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"
)
type FTPService 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,
) *FTPService {
return &FTPService{
cfg: cfg,
bus: bus,
logger: logger.With().Str("service", "ftp").Logger(),
tracker: tracker,
limiter: limiter,
}
}
func (s *FTPService) Name() string { return "ftp" }
func (s *FTPService) Start(
ctx context.Context,
) error {
addr := s.cfg.Addr(s.cfg.FTP.Port)
listener, err := net.Listen("tcp", addr)
if err != nil {
return fmt.Errorf(
"ftp listen %s: %w", addr, err,
)
}
s.logger.Info().
Str("addr", addr).
Msg("ftp honeypot listening")
go func() {
<-ctx.Done()
_ = listener.Close()
}()
for ctx.Err() == nil {
conn, err := listener.Accept()
if err != nil {
s.logger.Debug().
Err(err).Msg("accept failed")
continue
}
go s.handleConnection(ctx, conn)
}
return nil
}
func (s *FTPService) handleConnection(
ctx context.Context, conn net.Conn,
) {
defer func() { _ = conn.Close() }()
srcIP, srcPort := types.RemoteAddr(conn)
if !s.limiter.Allow(srcIP) {
return
}
sess := s.tracker.Start(
s.cfg.Sensor.ID, types.ServiceFTP,
srcIP, srcPort, s.cfg.FTP.Port,
)
defer s.tracker.End(sess.ID)
s.publishConnect(sess, srcIP, srcPort)
defer s.publishDisconnect(sess, srcIP, srcPort)
fc := newFTPConn(conn)
defer fc.close()
fmt.Fprintf(
fc.ctrl, "%s\r\n", s.cfg.FTP.Banner,
)
for {
if ctx.Err() != nil {
return
}
cmd, arg, err := fc.readLine()
if err != nil {
return
}
result := fc.dispatch(cmd, arg)
s.publishCommand(
sess.ID, srcIP, cmd, arg,
)
if result.password != "" {
s.publishAuth(
sess.ID, srcIP,
fc.username, result.password,
)
s.tracker.SetLogin(
sess.ID, true, fc.username, "",
)
}
if result.upload != nil {
s.publishUpload(
sess.ID, srcIP,
result.filename, result.upload,
)
}
if result.quit {
return
}
}
}
func (s *FTPService) 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.ServiceFTP,
EventType: types.EventConnect,
SourceIP: srcIP,
SourcePort: srcPort,
DestPort: s.cfg.FTP.Port,
Protocol: types.ProtocolTCP,
SchemaVersion: config.SchemaVersion,
})
}
func (s *FTPService) publishDisconnect(
sess *types.Session,
srcIP string,
srcPort int,
) {
s.bus.Publish(config.TopicDisconnect, &types.Event{
ID: uuid.Must(uuid.NewV7()).String(),
SessionID: sess.ID,
SensorID: s.cfg.Sensor.ID,
Timestamp: time.Now().UTC(),
ServiceType: types.ServiceFTP,
EventType: types.EventDisconnect,
SourceIP: srcIP,
SourcePort: srcPort,
DestPort: s.cfg.FTP.Port,
Protocol: types.ProtocolTCP,
SchemaVersion: config.SchemaVersion,
})
}
func (s *FTPService) publishAuth(
sessionID string,
srcIP string,
username string,
password string,
) {
serviceData, _ := json.Marshal(map[string]string{
"username": username,
"password": password,
"auth_method": "password",
})
s.bus.Publish(config.TopicAuth, &types.Event{
ID: uuid.Must(uuid.NewV7()).String(),
SessionID: sessionID,
SensorID: s.cfg.Sensor.ID,
Timestamp: time.Now().UTC(),
ServiceType: types.ServiceFTP,
EventType: types.EventLoginSuccess,
SourceIP: srcIP,
Protocol: types.ProtocolTCP,
SchemaVersion: config.SchemaVersion,
ServiceData: serviceData,
})
}
func (s *FTPService) publishCommand(
sessionID string,
srcIP string,
cmd string,
arg string,
) {
serviceData, _ := json.Marshal(map[string]string{
"command": cmd,
"arg": arg,
})
s.bus.Publish(config.TopicCommand, &types.Event{
ID: uuid.Must(uuid.NewV7()).String(),
SessionID: sessionID,
SensorID: s.cfg.Sensor.ID,
Timestamp: time.Now().UTC(),
ServiceType: types.ServiceFTP,
EventType: types.EventCommand,
SourceIP: srcIP,
Protocol: types.ProtocolTCP,
SchemaVersion: config.SchemaVersion,
ServiceData: serviceData,
})
}
func (s *FTPService) publishUpload(
sessionID string,
srcIP string,
filename string,
data []byte,
) {
serviceData, _ := json.Marshal(
map[string]interface{}{
"filename": filename,
"size": len(data),
},
)
s.bus.Publish(config.TopicFile, &types.Event{
ID: uuid.Must(uuid.NewV7()).String(),
SessionID: sessionID,
SensorID: s.cfg.Sensor.ID,
Timestamp: time.Now().UTC(),
ServiceType: types.ServiceFTP,
EventType: types.EventFileUpload,
SourceIP: srcIP,
Protocol: types.ProtocolTCP,
SchemaVersion: config.SchemaVersion,
Tags: []string{
"file-upload", "mitre:T1105",
},
ServiceData: serviceData,
})
}