Add SMTP Honeypot

This commit is contained in:
Torjus Håkestad 2021-11-06 14:04:11 +01:00
parent ed66cd5492
commit 4912eef00a
9 changed files with 231 additions and 1 deletions

View File

@ -63,7 +63,7 @@ RedirectHTTP = true
[Ports] [Ports]
# Enable the port listener. # Enable the port listener.
# Default: false # Default: false
Enable = true Enable = false
# Which address to listen on. # Which address to listen on.
# Default: "" (listen to all addresses) # Default: "" (listen to all addresses)
@ -76,3 +76,16 @@ TCPPorts = ["25"]
# Which UDP ports to listen to. # Which UDP ports to listen to.
# Default: [] # Default: []
UDPPorts = ["25"] UDPPorts = ["25"]
[SMTP]
# Enable the port listener.
# Default: false
Enable = true
# Which address and port to listen on.
# Default: ":25"
Addr = ":25"
# Enable collecting prometheus metrics
# Default: false
EnableMetrics = true

View File

@ -15,6 +15,7 @@ import (
"github.uio.no/torjus/apiary" "github.uio.no/torjus/apiary"
"github.uio.no/torjus/apiary/config" "github.uio.no/torjus/apiary/config"
"github.uio.no/torjus/apiary/honeypot/ports" "github.uio.no/torjus/apiary/honeypot/ports"
"github.uio.no/torjus/apiary/honeypot/smtp"
"github.uio.no/torjus/apiary/honeypot/ssh" "github.uio.no/torjus/apiary/honeypot/ssh"
"github.uio.no/torjus/apiary/honeypot/ssh/store" "github.uio.no/torjus/apiary/honeypot/ssh/store"
"github.uio.no/torjus/apiary/web" "github.uio.no/torjus/apiary/web"
@ -144,6 +145,39 @@ func ActionServe(c *cli.Context) error {
}() }()
} }
// Setup smtp honeypot if enabled
if cfg.SMTP.Enable {
honeypot, err := smtp.NewSMTPHoneypot()
if err != nil {
loggers.rootLogger.Warnw("Error seting up SMTP honeypot", "error", err)
}
honeypot.Addr = cfg.SMTP.Addr
honeypot.Logger = loggers.smtpLogger
if cfg.SMTP.EnableMetrics {
honeypot.Store = smtp.NewMetricsStore(&smtp.DiscardStore{})
} else {
honeypot.Store = &smtp.DiscardStore{}
}
// Start smtp honeypot
go func() {
loggers.rootLogger.Info("Starting SMTP server")
if err := honeypot.ListenAndServe(); err != nil {
loggers.rootLogger.Warnw("SMTP server returned error", "error", err)
}
}()
// Wait for smtp shutdown
go func() {
<-serversCtx.Done()
loggers.rootLogger.Info("SMTP server shutdown started")
if err := honeypot.Shutdown(); err != nil {
loggers.rootLogger.Errorw("Error shutting down SMTP server", "error", err)
}
loggers.rootLogger.Info("SMTP server shutdown complete")
}()
}
// Handle interrupt // Handle interrupt
go func() { go func() {
<-interruptChan <-interruptChan
@ -250,6 +284,7 @@ type loggerCollection struct {
webAccessLogger *zap.SugaredLogger webAccessLogger *zap.SugaredLogger
webServerLogger *zap.SugaredLogger webServerLogger *zap.SugaredLogger
portsLogger *zap.SugaredLogger portsLogger *zap.SugaredLogger
smtpLogger *zap.SugaredLogger
} }
func setupLoggers(cfg config.Config) *loggerCollection { func setupLoggers(cfg config.Config) *loggerCollection {
@ -290,6 +325,7 @@ func setupLoggers(cfg config.Config) *loggerCollection {
webAccessLogger: rootLogger.Named("ACC").Sugar(), webAccessLogger: rootLogger.Named("ACC").Sugar(),
webServerLogger: rootLogger.Named("WEB").Sugar(), webServerLogger: rootLogger.Named("WEB").Sugar(),
portsLogger: rootLogger.Named("PRT").Sugar(), portsLogger: rootLogger.Named("PRT").Sugar(),
smtpLogger: rootLogger.Named("SMT").Sugar(),
} }
} }

View File

@ -14,6 +14,7 @@ type Config struct {
Honeypot HoneypotConfig `toml:"Honeypot"` Honeypot HoneypotConfig `toml:"Honeypot"`
Frontend FrontendConfig `toml:"Frontend"` Frontend FrontendConfig `toml:"Frontend"`
Ports PortsConfig `toml:"Ports"` Ports PortsConfig `toml:"Ports"`
SMTP SMTPConfig `toml:"SMTP"`
} }
type StoreConfig struct { type StoreConfig struct {
Type string `toml:"Type"` Type string `toml:"Type"`
@ -54,6 +55,12 @@ type PortsConfig struct {
UDPPorts []string `toml:"UDPPorts"` UDPPorts []string `toml:"UDPPorts"`
} }
type SMTPConfig struct {
Enable bool `toml:"Enable"`
Addr string `toml:"Addr"`
EnableMetrics bool `toml:"EnableMetrics"`
}
func FromReader(r io.Reader) (Config, error) { func FromReader(r io.Reader) (Config, error) {
var c Config var c Config
@ -63,6 +70,7 @@ func FromReader(r io.Reader) (Config, error) {
c.Frontend.ListenAddr = ":8080" c.Frontend.ListenAddr = ":8080"
c.Frontend.LogLevel = "INFO" c.Frontend.LogLevel = "INFO"
c.Frontend.AccessLogEnable = true c.Frontend.AccessLogEnable = true
c.SMTP.Addr = ":25"
// Read from config // Read from config
decoder := toml.NewDecoder(r) decoder := toml.NewDecoder(r)

2
go.mod
View File

@ -6,6 +6,8 @@ require (
github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect
github.com/emersion/go-sasl v0.0.0-20211008083017-0b9dcfb154ac // indirect
github.com/emersion/go-smtp v0.15.0
github.com/fujiwara/shapeio v1.0.0 github.com/fujiwara/shapeio v1.0.0
github.com/gliderlabs/ssh v0.3.3 github.com/gliderlabs/ssh v0.3.3
github.com/go-chi/chi/v5 v5.0.4 github.com/go-chi/chi/v5 v5.0.4

5
go.sum
View File

@ -72,6 +72,11 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-sasl v0.0.0-20211008083017-0b9dcfb154ac h1:tn/OQ2PmwQ0XFVgAHfjlLyqMewry25Rz7jWnVoh4Ggs=
github.com/emersion/go-sasl v0.0.0-20211008083017-0b9dcfb154ac/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-smtp v0.15.0 h1:3+hMGMGrqP/lqd7qoxZc1hTU8LY8gHV9RFGWlqSDmP8=
github.com/emersion/go-smtp v0.15.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=

12
honeypot/smtp/mail.go Normal file
View File

@ -0,0 +1,12 @@
package smtp
import "time"
type MailData struct {
From string
Recipients []string
Data string
RemoteAddr string
Timestamp time.Time
}

30
honeypot/smtp/metrics.go Normal file
View File

@ -0,0 +1,30 @@
package smtp
import "github.com/prometheus/client_golang/prometheus"
type MetricsStore struct {
backend SMTPStore
mailTotal *prometheus.CounterVec
}
func NewMetricsStore(backend SMTPStore) *MetricsStore {
store := &MetricsStore{backend: backend}
store.mailTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "apiary_smtp_received_total",
Help: "Total count of received emails.",
ConstLabels: prometheus.Labels{"service": "honeypot_smtp"},
},
[]string{"from"},
)
prometheus.MustRegister(store.mailTotal)
return store
}
func (s *MetricsStore) AddMailData(data *MailData) error {
s.mailTotal.WithLabelValues(data.From).Inc()
return s.backend.AddMailData(data)
}

112
honeypot/smtp/server.go Normal file
View File

@ -0,0 +1,112 @@
package smtp
import (
"bytes"
"io"
"time"
smtplib "github.com/emersion/go-smtp"
"go.uber.org/zap"
)
type backend struct {
honeypot *SMTPHoneypot
}
func (b *backend) Login(state *smtplib.ConnectionState, username, password string) (smtplib.Session, error) {
sess := &session{
honeypot: b.honeypot,
loginUsername: username,
loginPassword: password,
remoteAddr: state.RemoteAddr.String(),
}
return sess, nil
}
func (b *backend) AnonymousLogin(state *smtplib.ConnectionState) (smtplib.Session, error) {
sess := &session{
loginAnonymous: true,
remoteAddr: state.RemoteAddr.String(),
}
return sess, nil
}
type session struct {
honeypot *SMTPHoneypot
loginUsername string
loginPassword string
loginAnonymous bool
remoteAddr string
mail MailData
}
func (sess *session) Reset() {
}
func (sess *session) Logout() error {
return nil
}
func (sess *session) Mail(from string, opts smtplib.MailOptions) error {
sess.mail.From = from
return nil
}
func (sess *session) Rcpt(to string) error {
sess.mail.Recipients = append(sess.mail.Recipients, to)
return nil
}
func (sess *session) Data(r io.Reader) error {
lr := io.LimitReader(r, 1024*1024)
var buf bytes.Buffer
n, err := io.Copy(&buf, lr)
if err != nil {
sess.honeypot.Logger.Debugw("Error reading email data from client.", "error", err, "remote_addr", sess.remoteAddr)
}
if n == 1024*1024 {
// There is probably more data, read the rest from the client
// We don't really care what happens to this, or if it errors
_, _ = io.Copy(io.Discard, r)
}
sess.mail.Data = buf.String()
if err := sess.honeypot.Store.AddMailData(&sess.mail); err != nil {
sess.honeypot.Logger.Warnw("Error storing maildata.", "error", err, "remote_addr", sess.remoteAddr)
return smtplib.ErrDataTooLarge
}
sess.honeypot.Logger.Infow("Mail received", "from", sess.mail.From, "remote_addr", sess.remoteAddr, "size", len(sess.mail.Data))
return nil
}
type SMTPHoneypot struct {
Addr string
Logger *zap.SugaredLogger
Store SMTPStore
server *smtplib.Server
}
func NewSMTPHoneypot() (*SMTPHoneypot, error) {
honeypot := &SMTPHoneypot{
Logger: zap.NewNop().Sugar(),
}
return honeypot, nil
}
func (sh *SMTPHoneypot) ListenAndServe() error {
server := smtplib.NewServer(&backend{
honeypot: sh,
})
server.Addr = sh.Addr
server.Domain = "apiary.t-juice.club"
server.AllowInsecureAuth = true
server.ReadTimeout = 10 * time.Second
server.WriteTimeout = 10 * time.Second
sh.server = server
return server.ListenAndServe()
}
func (sh *SMTPHoneypot) Shutdown() error {
return sh.server.Close()
}

12
honeypot/smtp/store.go Normal file
View File

@ -0,0 +1,12 @@
package smtp
type SMTPStore interface {
AddMailData(mail *MailData) error
}
type DiscardStore struct {
}
func (s *DiscardStore) AddMailData(mail *MailData) error {
return nil
}