diff --git a/Dockerfile b/Dockerfile index 6e460fa..b5c3ea1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,4 +24,4 @@ COPY --from=build-env /opt/glutton/bin/server /opt/glutton/bin/server COPY --from=build-env /opt/glutton/config /opt/glutton/config COPY --from=build-env /opt/glutton/rules /opt/glutton/rules -CMD ["./bin/server", "-i", "eth0", "-l", "/var/log/glutton.log", "-d", "true"] +CMD ["./bin/server", "-i", "eth0", "-l", "/var/log/glutton.log", "--debug"] diff --git a/Makefile b/Makefile index 47eef93..2d01e96 100644 --- a/Makefile +++ b/Makefile @@ -33,8 +33,8 @@ run: build sudo bin/server -i eth0 docker: - docker build -t glutton . - docker run --rm --cap-add=NET_ADMIN -it glutton + docker build --progress=plain -t glutton . + docker run --rm --cap-add=NET_ADMIN -it --name glutton glutton test: go test -v ./... diff --git a/glutton.go b/glutton.go index 7f1b26f..e3ca689 100644 --- a/glutton.go +++ b/glutton.go @@ -265,32 +265,34 @@ func (g *Glutton) makeID() error { if err := os.MkdirAll(viper.GetString("var-dir"), 0744); err != nil { return fmt.Errorf("failed to create var-dir: %w", err) } - if _, err := os.Stat(filePath); os.IsNotExist(err) { - g.id = uuid.New() - data, err := g.id.MarshalBinary() - if err != nil { - return fmt.Errorf("failed to marshal UUID: %w", err) - } - if err := os.WriteFile(filePath, data, 0744); err != nil { - return fmt.Errorf("failed to create new PID file: %w", err) - } - } else { - if err != nil { - return fmt.Errorf("failed to access PID file: %w", err) - } - f, err := os.Open(filePath) - if err != nil { - return fmt.Errorf("failed to open PID file: %w", err) - } - buff, err := io.ReadAll(f) - if err != nil { - return fmt.Errorf("failed to read PID file: %w", err) - } - g.id, err = uuid.FromBytes(buff) - if err != nil { - return fmt.Errorf("failed to create UUID from PID filed content: %w", err) + if _, err := os.Stat(filePath); err != nil { + if os.IsNotExist(err) { + g.id = uuid.New() + data, err := g.id.MarshalBinary() + if err != nil { + return fmt.Errorf("failed to marshal UUID: %w", err) + } + if err := os.WriteFile(filePath, data, 0744); err != nil { + return fmt.Errorf("failed to create new PID file: %w", err) + } + return nil } + return fmt.Errorf("failed to access PID file: %w", err) + } + + f, err := os.Open(filePath) + if err != nil { + return fmt.Errorf("failed to open PID file: %w", err) } + buff, err := io.ReadAll(f) + if err != nil { + return fmt.Errorf("failed to read PID file: %w", err) + } + g.id, err = uuid.FromBytes(buff) + if err != nil { + return fmt.Errorf("failed to create UUID from PID filed content: %w", err) + } + return nil } diff --git a/producer/logger.go b/producer/logger.go index 7bf07ca..caf7efd 100644 --- a/producer/logger.go +++ b/producer/logger.go @@ -38,6 +38,14 @@ func NewLogger(id string) *slog.Logger { Compress: true, // disabled by default }, } - handler := slog.NewJSONHandler(writer, &slog.HandlerOptions{}) + handlerOptions := &slog.HandlerOptions{} + debug := viper.GetBool("debug") + switch debug { + case true: + handlerOptions.Level = slog.Leveler(slog.LevelDebug) + default: + handlerOptions.Level = slog.Leveler(slog.LevelInfo) + } + handler := slog.NewJSONHandler(writer, handlerOptions) return slog.New(handler.WithAttrs([]slog.Attr{slog.String("sensorID", id)})) } diff --git a/protocols/helpers/helpers.go b/protocols/helpers/helpers.go index 79e9939..4474116 100644 --- a/protocols/helpers/helpers.go +++ b/protocols/helpers/helpers.go @@ -4,6 +4,7 @@ import ( "crypto/sha256" "encoding/hex" "os" + "path" "path/filepath" ) @@ -15,18 +16,20 @@ func FirstOrEmpty[T any](s []T) T { return t } -func StorePayload(data []byte) (string, error) { - sum := sha256.Sum256(data) - if err := os.MkdirAll("payloads", os.ModePerm); err != nil { +func StorePayload(data []byte, paths ...string) (string, error) { + paths = append([]string{"payloads"}, paths...) + path := path.Join(paths...) + if err := os.MkdirAll(path, os.ModePerm); err != nil { return "", err } + sum := sha256.Sum256(data) sha256Hash := hex.EncodeToString(sum[:]) - path := filepath.Join("payloads", sha256Hash) - if _, err := os.Stat(path); err == nil { + filePath := filepath.Join(path, sha256Hash) + if _, err := os.Stat(filePath); err == nil { // file already exists return "", nil } - out, err := os.Create(path) + out, err := os.Create(filePath) if err != nil { return "", err } diff --git a/protocols/tcp/bittorrent.go b/protocols/tcp/bittorrent.go index 03f32c7..885c36c 100644 --- a/protocols/tcp/bittorrent.go +++ b/protocols/tcp/bittorrent.go @@ -46,7 +46,7 @@ func HandleBittorrent(ctx context.Context, conn net.Conn, md connection.Metadata } }() - buffer := make([]byte, 1024) + buffer := make([]byte, maxBufferSize) for { if err := h.UpdateConnectionTimeout(ctx, conn); err != nil { logger.Debug("Failed to set connection timeout", producer.ErrAttr(err), slog.String("handler", "bittorrent")) diff --git a/protocols/tcp/ftp.go b/protocols/tcp/ftp.go index 5c917a3..4a15823 100644 --- a/protocols/tcp/ftp.go +++ b/protocols/tcp/ftp.go @@ -9,6 +9,7 @@ import ( "net" "strconv" "strings" + "time" "github.com/mushorg/glutton/connection" "github.com/mushorg/glutton/producer" @@ -16,9 +17,27 @@ import ( "github.com/mushorg/glutton/protocols/interfaces" ) +const ( + // 1xx + StatusFileStatusOK = 150 // RFC 959, 4.2.1 + // 2xx + StatusOK = 200 // RFC 959, 4.2.1 + StatusServiceReady = 220 // RFC 959, 4.2.1 + StatusClosingControlConn = 221 // RFC 959, 4.2.1 + StatusClosingDataConn = 226 // RFC 959, 4.2.1 + StatusUserLoggedIn = 230 // RFC 959, 4.2.1 + // 3xx + StatusUserOK = 331 // RFC 959, 4.2.1 + // 4xx + StatusCannotOpenDataConnection = 425 // RFC 959, 4.2.1 + // 5xx + StatusActionNotTaken = 550 // RFC 959, 4.2.1 +) + type parsedFTP struct { - Direction string `json:"direction,omitempty"` - Payload []byte `json:"payload,omitempty"` + Direction string `json:"direction,omitempty"` + Payload []byte `json:"payload,omitempty"` + PayloadHash string `json:"payload_hash,omitempty"` } type ftpServer struct { @@ -26,6 +45,11 @@ type ftpServer struct { conn net.Conn } +type ftpResponse struct { + code int + msg string +} + func (s *ftpServer) read(_ interfaces.Logger, _ interfaces.Honeypot) (string, error) { msg, err := bufio.NewReader(s.conn).ReadString('\n') if err != nil { @@ -38,7 +62,8 @@ func (s *ftpServer) read(_ interfaces.Logger, _ interfaces.Honeypot) (string, er return msg, nil } -func (s *ftpServer) write(msg string) error { +func (s *ftpServer) write(resp ftpResponse) error { + msg := fmt.Sprintf("%d %s\r\n", resp.code, resp.msg) _, err := s.conn.Write([]byte(msg)) if err != nil { return err @@ -51,6 +76,38 @@ func (s *ftpServer) write(msg string) error { } +func (s *ftpServer) ftpSTOR(ip string, port int) (ftpResponse, string) { + dialer := &net.Dialer{Timeout: 2 * time.Second} + conn, err := dialer.Dial("tcp", fmt.Sprintf("%s:%d", ip, port)) + if err != nil { + return ftpResponse{StatusCannotOpenDataConnection, "Failed to establish connection."}, "" + } + if err := conn.SetReadDeadline(time.Now().Add(2 * time.Second)); err != nil { + return ftpResponse{StatusActionNotTaken, "Failed to establish connection."}, "" + } + buffer := make([]byte, maxBufferSize) + n, err := conn.Read(buffer) + if err != nil || n == 0 { + return ftpResponse{StatusActionNotTaken, "Failed to read file."}, "" + } + + fileHash, err := helpers.StorePayload(buffer, "ftp") + if err != nil { + return ftpResponse{StatusActionNotTaken, "Failed to store file."}, "" + } + + s.events = append(s.events, parsedFTP{ + Direction: "read", + PayloadHash: fileHash, + Payload: buffer, + }) + + if err := conn.Close(); err != nil { + return ftpResponse{StatusActionNotTaken, "Failed to close connection."}, "" + } + return ftpResponse{StatusClosingDataConn, "File received."}, fileHash +} + // HandleFTP takes a net.Conn and does basic FTP communication func HandleFTP(ctx context.Context, conn net.Conn, md connection.Metadata, logger interfaces.Logger, h interfaces.Honeypot) error { server := ftpServer{ @@ -65,21 +122,23 @@ func HandleFTP(ctx context.Context, conn net.Conn, md connection.Metadata, logge } }() - host, port, err := net.SplitHostPort(conn.RemoteAddr().String()) + host, srcPort, err := net.SplitHostPort(conn.RemoteAddr().String()) if err != nil { return err } - if _, err := conn.Write([]byte("220 Welcome!\r\n")); err != nil { + if err := server.write(ftpResponse{StatusServiceReady, "Welcome"}); err != nil { return err } + var portParam string +loop: for { if err := h.UpdateConnectionTimeout(ctx, conn); err != nil { logger.Debug("Failed to set connection timeout", slog.String("protocol", "ftp"), producer.ErrAttr(err)) return nil } msg, err := server.read(logger, h) - if err != nil || err != io.EOF { + if err != nil && err != io.EOF { logger.Debug("Failed to read data", slog.String("protocol", "ftp"), producer.ErrAttr(err)) break } @@ -89,22 +148,61 @@ func HandleFTP(ctx context.Context, conn net.Conn, md connection.Metadata, logge cmd := strings.ToUpper(msg[:4]) logger.Info( - "ftp payload received", + "ftp command received", slog.String("dest_port", strconv.Itoa(int(md.TargetPort))), slog.String("src_ip", host), - slog.String("src_port", port), + slog.String("src_port", srcPort), slog.String("message", fmt.Sprintf("%q", msg)), slog.String("handler", "ftp"), ) - var resp string + var ( + resp ftpResponse + filehash string + ) switch cmd { case "USER": - resp = "331 OK.\r\n" + resp = ftpResponse{StatusUserOK, "Ok."} case "PASS": - resp = "230 OK.\r\n" + resp = ftpResponse{StatusUserLoggedIn, "Ok."} + case "STOR": + parts := strings.Split(portParam, ",") + ip := strings.Join(parts[:4], ".") + + portByte1, err := strconv.Atoi(parts[4]) + if err != nil { + return err + } + portByte2, err := strconv.Atoi(strings.TrimSpace(parts[5])) + if err != nil { + return err + } + + port := portByte1<<8 + portByte2 + if err := server.write(ftpResponse{StatusFileStatusOK, fmt.Sprintf("Connecting to port %d\r\n", port)}); err != nil { + return err + } + resp, filehash = server.ftpSTOR(ip, port) + if filehash != "" { + logger.Info( + "FTP payload receievd", + slog.String("dest_port", strconv.Itoa(int(md.TargetPort))), + slog.String("src_ip", host), + slog.String("src_port", srcPort), + slog.String("handler", "ftp"), + slog.String("payload_hash", filehash), + ) + } + case "QUIT": + if err := server.write(ftpResponse{StatusClosingControlConn, "Goodbye."}); err != nil { + return err + } + break loop + case "PORT": + portParam = msg[5:] + resp = ftpResponse{StatusOK, "PORT command successful."} default: - resp = "200 OK.\r\n" + resp = ftpResponse{StatusOK, "Ok."} } if err := server.write(resp); err != nil { return err diff --git a/protocols/tcp/memcache.go b/protocols/tcp/memcache.go index b43a963..986132f 100644 --- a/protocols/tcp/memcache.go +++ b/protocols/tcp/memcache.go @@ -14,7 +14,7 @@ import ( func HandleMemcache(ctx context.Context, conn net.Conn, md connection.Metadata, logger interfaces.Logger, h interfaces.Honeypot) error { var dataMap = map[string]string{} - buffer := make([]byte, 1024) + buffer := make([]byte, maxBufferSize) for { if err := h.UpdateConnectionTimeout(ctx, conn); err != nil { return err diff --git a/protocols/tcp/mqtt.go b/protocols/tcp/mqtt.go index 60ad7dc..b91d0cd 100644 --- a/protocols/tcp/mqtt.go +++ b/protocols/tcp/mqtt.go @@ -34,7 +34,7 @@ func HandleMQTT(ctx context.Context, conn net.Conn, md connection.Metadata, logg logger.Error(fmt.Sprintf("[mqtt ] error: %v", err)) } }() - buffer := make([]byte, 1024) + buffer := make([]byte, maxBufferSize) for { if err := h.UpdateConnectionTimeout(ctx, conn); err != nil { return err diff --git a/protocols/tcp/rdp.go b/protocols/tcp/rdp.go index 4741e0f..1bcdfe5 100644 --- a/protocols/tcp/rdp.go +++ b/protocols/tcp/rdp.go @@ -50,7 +50,7 @@ func HandleRDP(ctx context.Context, conn net.Conn, md connection.Metadata, logge } }() - buffer := make([]byte, 1024) + buffer := make([]byte, maxBufferSize) for { if err := h.UpdateConnectionTimeout(ctx, conn); err != nil { logger.Debug("Failed to set connection timeout", slog.String("protocol", "rdp"), producer.ErrAttr(err)) diff --git a/protocols/tcp/smb.go b/protocols/tcp/smb.go index 1082116..ce82288 100644 --- a/protocols/tcp/smb.go +++ b/protocols/tcp/smb.go @@ -53,7 +53,7 @@ func HandleSMB(ctx context.Context, conn net.Conn, md connection.Metadata, logge } }() - buffer := make([]byte, 4096) + buffer := make([]byte, maxBufferSize) for { if err := h.UpdateConnectionTimeout(ctx, conn); err != nil { logger.Debug("Failed to set connection timeout", slog.String("protocol", "smb"), producer.ErrAttr(err)) @@ -64,7 +64,7 @@ func HandleSMB(ctx context.Context, conn net.Conn, md connection.Metadata, logge logger.Debug("Failed to read data", slog.String("protocol", "smb"), producer.ErrAttr(err)) break } - if n > 0 && n < 4096 { + if n > 0 && n < maxBufferSize { logger.Debug("SMB Payload", slog.String("payload", hex.Dump(buffer[0:n])), slog.String("protocol", "smb")) buffer, err := smb.ValidateData(buffer[0:n]) if err != nil { diff --git a/protocols/tcp/tcp.go b/protocols/tcp/tcp.go index bbbcd29..1f893c8 100644 --- a/protocols/tcp/tcp.go +++ b/protocols/tcp/tcp.go @@ -61,7 +61,7 @@ func HandleTCP(ctx context.Context, conn net.Conn, md connection.Metadata, logge msgLength := 0 data := []byte{} - buffer := make([]byte, 1024) + buffer := make([]byte, maxBufferSize) defer func() { if msgLength > 0 {