Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions config/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,6 @@ producers:

conn_timeout: 45
max_tcp_payload: 4096

capture_traffic:
enabled: false
3 changes: 3 additions & 0 deletions config/rules.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ rules:
- match: tcp dst port 27017
type: conn_handler
target: mongodb
- match: tcp dst port 9889
type: tcp_proxy
target: 127.0.0.1:9889 # Can use hostip:port for the required destination.
- match: tcp
type: conn_handler
target: tcp
Expand Down
22 changes: 16 additions & 6 deletions glutton.go
Original file line number Diff line number Diff line change
Expand Up @@ -222,12 +222,22 @@ func (g *Glutton) tcpListen() {
g.Logger.Error("Failed to set connection timeout", producer.ErrAttr(err))
}

if hfunc, ok := g.tcpProtocolHandlers[rule.Target]; ok {
go func() {
if err := hfunc(g.ctx, conn, md); err != nil {
g.Logger.Error("Failed to handle TCP connection", producer.ErrAttr(err), slog.String("handler", rule.Target))
}
}()
if rule.Type == "tcp_proxy" {
if hfunc, ok := g.tcpProtocolHandlers[rule.Type]; ok {
go func() {
if err := hfunc(g.ctx, conn, md); err != nil {
g.Logger.Error("Failed to handle TCP passthrough", producer.ErrAttr(err), slog.String("handler", "tcp_proxy"))
}
}()
}
} else {
if hfunc, ok := g.tcpProtocolHandlers[rule.Target]; ok {
go func() {
if err := hfunc(g.ctx, conn, md); err != nil {
g.Logger.Error("Failed to handle TCP connection", producer.ErrAttr(err), slog.String("handler", rule.Target))
}
}()
}
}
}
}
Expand Down
3 changes: 3 additions & 0 deletions protocols/protocols.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ func MapTCPProtocolHandlers(log interfaces.Logger, h interfaces.Honeypot) map[st
protocolHandlers["mongodb"] = func(ctx context.Context, conn net.Conn, md connection.Metadata) error {
return tcp.HandleMongoDB(ctx, conn, md, log, h)
}
protocolHandlers["tcp_proxy"] = func(ctx context.Context, conn net.Conn, md connection.Metadata) error {
return tcp.HandlePassThrough(ctx, conn, md, log, h)
}
protocolHandlers["tcp"] = func(ctx context.Context, conn net.Conn, md connection.Metadata) error {
snip, bufConn, err := Peek(conn, 4)
if err != nil {
Expand Down
200 changes: 200 additions & 0 deletions protocols/tcp/passthrough.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
package tcp

import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"log"
"log/slog"
"net"
"time"

"github.com/mushorg/glutton/connection"
"github.com/mushorg/glutton/producer"
"github.com/mushorg/glutton/protocols/interfaces"
"github.com/spf13/viper"
)

type parsedPassThrough struct {
Direction string `json:"direction,omitempty"`
Payload []byte `json:"payload,omitempty"`
PayloadHash string `json:"payload_hash,omitempty"` // Used for easier identification, can remove
}

type passThroughServer struct {
events []parsedPassThrough
conn net.Conn
target string
source string
}

type loggingWriter struct {
dst net.Conn
server *passThroughServer
logger interfaces.Logger
capture bool
dir string
}

func (lw *loggingWriter) Write(p []byte) (int, error) {
lw.server.logPayload(lw.dir, p, lw.logger)
lw.server.recordEvent(lw.dir, p, lw.capture)
return lw.dst.Write(p)
}

// checks whether the payload can be converted to text, to prevent expensive hex coding.
func (srv *passThroughServer) isLikelyText(data []byte) bool {
if len(data) == 0 {
return false
}

printable := 0
for _, b := range data {
if b >= 32 && b <= 126 || b == '\n' || b == '\r' || b == '\t' {
printable++
}
}

return (printable*100)/len(data) > 80 // threshold value --> 80%
}

// logs the payload hex or payload text.
func (srv *passThroughServer) logPayload(direction string, data []byte, logger interfaces.Logger) {
if len(data) == 0 {
return
}

fields := []any{
slog.String("direction", direction),
slog.Int("length", len(data)),
slog.String("sha256", fmt.Sprintf("%x", sha256.Sum256(data))),
}

if srv.isLikelyText(data) {
fields = append(fields, slog.String("payload", string(data)))
} else {
fields = append(fields, slog.String("hex", hex.EncodeToString(data)))
}

logger.Info("payload_transferred", fields...)
}

// records the events in the server
func (srv *passThroughServer) recordEvent(dir string, buf []byte, capture bool) {
if !capture {
return
}
hash := sha256.Sum256(buf)

payload := append([]byte(nil), buf...) // defensive copy

srv.events = append(srv.events, parsedPassThrough{
Direction: dir,
Payload: payload,
PayloadHash: fmt.Sprintf("%x", hash[:]),
})
}

// pipeBidirectional handles data transfer between the two connections
func pipeBidirectional(src, dst net.Conn, server *passThroughServer, logger interfaces.Logger, capture bool, errChan chan error) {
direction := getDirection(src, dst)
writer := &loggingWriter{dst: dst, server: server, logger: logger, capture: capture, dir: direction}

// source to target
go func() {
_, err := io.Copy(writer, src)
errChan <- err
}()

revDirection := getDirection(dst, src)
revWriter := &loggingWriter{dst: src, server: server, logger: logger, capture: capture, dir: revDirection}

// target to source
go func() {
_, err := io.Copy(revWriter, dst)
errChan <- err
}()
}

// getDirection returns the direction as a string
func getDirection(src, dst net.Conn) string {
srcAddr := src.RemoteAddr().String()
dstAddr := dst.RemoteAddr().String()
return fmt.Sprintf("%s -> %s", srcAddr, dstAddr)
}

// Dial to the source ip, acting as a proxy between the client and real source by piping the data back and forth w/o interfering w it.
func HandlePassThrough(ctx context.Context, conn net.Conn, md connection.Metadata, logger interfaces.Logger, h interfaces.Honeypot) error {
var err error
handler := "tcp_proxy"

srcAddr := conn.RemoteAddr().String()
destAddr := md.Rule.Target
Comment thread
namay26 marked this conversation as resolved.

host, _, err := net.SplitHostPort(destAddr)
if err != nil {
logger.Error("invalid address format", producer.ErrAttr(err))
return nil
}

if ip := net.ParseIP(host); ip == nil {
if _, err := net.LookupHost(host); err != nil {
return fmt.Errorf("invalid host: %w", err)
}
}

server := &passThroughServer{
events: []parsedPassThrough{},
conn: conn,
target: destAddr,
source: srcAddr,
}

var capture bool
if viper.GetBool("capture_traffic.enabled") {
capture = true
}

defer func() {
var events []parsedPassThrough
if capture {
events = server.events
}
if err := h.ProduceTCP("passthrough", conn, md, nil, events); err != nil {
logger.Error("failed to produce passthrough message", producer.ErrAttr(err))
}
if err := conn.Close(); err != nil {
logger.Error("failed to close incoming connection", slog.String("handler", handler), producer.ErrAttr(err))
}
}()

if destAddr == "" {
logger.Error("no target defined", slog.String("handler", handler))
return nil
}

timeout := 5 * time.Second

targetConn, err := net.DialTimeout("tcp", destAddr, timeout)
if err != nil {
logger.Error("failed to connect to the target", slog.String("handler", handler), slog.String("target", string(destAddr)), producer.ErrAttr(err))
return nil
}
defer targetConn.Close()

logger.Info("starting passthrough", slog.String("source", srcAddr), slog.String("target", string(destAddr)), slog.String("handler", handler))

errChan := make(chan error, 2)

go pipeBidirectional(conn, targetConn, server, logger, capture, errChan)

// wait for either side to close
if err := <-errChan; err != nil {
log.Printf("connection closed: %v", err)
}

logger.Info("Passthrough completed successfully")
return nil
}
Loading