Add SMTP Honeypot
This commit is contained in:
parent
ed66cd5492
commit
4912eef00a
15
apiary.toml
15
apiary.toml
@ -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
|
||||||
|
@ -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(),
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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
2
go.mod
@ -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
5
go.sum
@ -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
12
honeypot/smtp/mail.go
Normal 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
30
honeypot/smtp/metrics.go
Normal 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
112
honeypot/smtp/server.go
Normal 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
12
honeypot/smtp/store.go
Normal 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
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user