Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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: passthrough
Comment thread
namay26 marked this conversation as resolved.
Outdated
target: 127.0.0.1:9889 # Can use hostip:port for the required destination.
- match: tcp
type: conn_handler
target: tcp
Expand Down
20 changes: 15 additions & 5 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 == "passthrough" {
Comment thread
namay26 marked this conversation as resolved.
Outdated
if hfunc, ok := g.tcpProtocolHandlers["passthrough"]; ok {
Comment thread
namay26 marked this conversation as resolved.
Outdated
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", "Passthrough"))
}
}()
} 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["passthrough"] = 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
184 changes: 184 additions & 0 deletions protocols/tcp/passthrough.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
package tcp

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

"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
}

// 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(ctx context.Context, src, dst net.Conn, server *passThroughServer, logger interfaces.Logger, capture bool, errChan chan error) {
buf := make([]byte, 4096)
direction := getDirection(src, dst)
for {
select {
case <-ctx.Done():
errChan <- ctx.Err()
return
default:
n, err := src.Read(buf)
if err != nil {
errChan <- err
return
}

if n > 0 {
server.logPayload(direction, buf[:n], logger)
server.recordEvent(direction, buf[:n], capture)

if _, err := dst.Write(buf[:n]); err != nil {
errChan <- err
return
}
}
}
}
}

// 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)
}

Comment thread
glaslos marked this conversation as resolved.
// 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

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

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", "passthrough"), producer.ErrAttr(err))
}
}()

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

targetConn, err := net.Dial("tcp", destAddr)
Comment thread
namay26 marked this conversation as resolved.
Outdated
if err != nil {
logger.Error("failed to connect to the target", slog.String("handler", "passthrough"), slog.String("target", string(destAddr)), producer.ErrAttr(err))
Comment thread
namay26 marked this conversation as resolved.
Outdated
return nil
}
defer targetConn.Close()

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

errChan := make(chan error, 2)

go pipeBidirectional(ctx, conn, targetConn, server, logger, capture, errChan) // source to target
Comment thread
namay26 marked this conversation as resolved.
Outdated
go pipeBidirectional(ctx, targetConn, conn, server, logger, capture, errChan) // target to source

select {
case err := <-errChan:
if err != nil && err != io.EOF {
logger.Error("transfer error", producer.ErrAttr(err))
return err
}
case <-ctx.Done():
logger.Info("context cancelled")
return ctx.Err()
}

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