278 lines
5.7 KiB
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,
|
|
})
|
|
}
|