├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── go.mod ├── imaphoney ├── client_test.go └── honey.go └── smtphoney ├── client_test.go └── honey.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | build/ 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 yvesago 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | VERSION=$(shell git describe --abbrev=0 --tags) 3 | 4 | BUILD=$(shell git rev-parse --short HEAD) 5 | DATE=$(shell date +%FT%T%z) 6 | 7 | # Binaries to be build 8 | PLATFORMS = linux/imaphoney linux/smtphoney 9 | BINS = $(wildcard build/*/*) 10 | 11 | # functions 12 | temp = $(subst /, ,$@) 13 | os = $(word 1, $(temp)) 14 | target = $(word 2, $(temp)) 15 | 16 | # Setup the -ldflags option for go building, interpolate the variable values 17 | LDFLAGS=-trimpath -ldflags "-w -s -X 'main.Version=${VERSION}, git: ${BUILD}, build: ${DATE}'" 18 | 19 | # Build binaries 20 | # first build : linux/imaphoney 21 | $(PLATFORMS): 22 | @mkdir -p build/${os} 23 | CGO_ENABLED=0 GOARCH=386 GOOS=${os} go build ${LDFLAGS} -o build/$@ ${target}/honey.go 24 | @echo " => bin builded: build/$@" 25 | 26 | build: $(PLATFORMS) 27 | 28 | # List binaries 29 | $(BINS): 30 | @sha256sum $@ 31 | 32 | sha: $(BINS) 33 | 34 | # Cleans our project: deletes binaries 35 | clean: 36 | rm -rf build/ 37 | @echo "Build cleaned" 38 | 39 | test: 40 | go test ./... 41 | 42 | all: build 43 | 44 | .PHONY: clean build sha $(BINS) $(PLATFORMS) 45 | 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # imap-honey 2 | 3 | Simple IMAP or SMTP honeypot written in Golang with log to console or syslog 4 | 5 | ## Quick start 6 | 7 | ``` 8 | $ go run imaphoney/honey.go & 9 | 10 | $ telnet localhost 1993 11 | Trying ::1... 12 | Connected to localhost. 13 | Escape character is '^]'. 14 | OK IMAP4 15 | a0001 CAPABILITY 16 | * CAPABILITY ACL ID IDLE IMAP4rev1 AUTH=PLAIN 17 | a0001 OK CAPABILITY 18 | a0002 LOGOUT 19 | * BYE localhost 20 | a0002 OK LOGOUT 21 | Connection closed by foreign host. 22 | ``` 23 | 24 | ``` 25 | $ go run smtphoney/honey.go & 26 | 27 | $ telnet localhost 1993 28 | Trying ::1... 29 | Connected to localhost. 30 | Escape character is '^]'. 31 | 220 localhost ESMTP ready 32 | EHLO honey 33 | 250-localhost 34 | 250-PIPELINING 35 | 250-SIZE 5242880 36 | 250-ETRN 37 | 250 8BITMIME 38 | 250 DSN 39 | MAIL FROM: test@example.org 40 | 250 Recipient ok 41 | QUIT 42 | 221 2.0.0 Bye 43 | Connection closed by foreign host. 44 | ``` 45 | 46 | ## IMAPS/SMTPS support 47 | 48 | 1) Create public/private keys via: 49 | 50 | ``` 51 | openssl genrsa -out server.key 2048 52 | openssl req -new -x509 -sha256 -key server.key -out server.pem -days 3650 53 | ``` 54 | 55 | 2) Run and test 56 | 57 | ``` 58 | $ make all 59 | $ ./build/linux/imaphoney -cert server.pem -key server.key -addr :9443 -server my.syslog.server:514 & 60 | 61 | $ openssl s_client -connect localhost:9443 -quiet 62 | ... 63 | ``` 64 | 65 | 66 | ## Full usage 67 | 68 | ``` 69 | Usage of ./build/linux/imaphoney: 70 | -addr string 71 | ipaddr:port (default ":1993") 72 | -cap string 73 | imap CAPABILITY (default "ACL ID IDLE IMAP4rev1 AUTH=PLAIN") 74 | -cert string 75 | cert file 76 | -d debug 77 | -hostname string 78 | hostname (default "localhost") 79 | -key string 80 | cert file 81 | -q quiet - no msg in console 82 | -server string 83 | syslog remote server 84 | ``` 85 | 86 | ``` 87 | Usage of ./build/linux/smtphoney: 88 | -addr string 89 | ipaddr:port (default ":1993") 90 | -aok 91 | auth ok 92 | -cap string 93 | smtp CAPABILITY (default "250-localhost;250-PIPELINING;250-SIZE 5242880;250-ETRN;250 8BITMIME;250 DSN;") 94 | -cert string 95 | cert file 96 | -d debug 97 | -hostname string 98 | hostname (default "localhost") 99 | -key string 100 | cert file 101 | -la 102 | log auth 103 | -ld 104 | log data 105 | -q quiet - no msg in console 106 | -server string 107 | syslog remote server 108 | ``` 109 | 110 | # AUTHORS 111 | 112 | Yves Agostini, `` 113 | 114 | # LICENSE AND COPYRIGHT 115 | 116 | License : MIT 117 | 118 | Copyright 2022 - Yves Agostini 119 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module imap-honey 2 | 3 | go 1.18 4 | -------------------------------------------------------------------------------- /imaphoney/client_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "net" 7 | "os" 8 | "strings" 9 | "testing" 10 | ) 11 | 12 | type Client struct { 13 | socket net.Conn 14 | } 15 | 16 | func (client *Client) Send(msg string) { 17 | client.socket.Write([]byte(msg + "\r\n")) 18 | } 19 | 20 | func (client *Client) Read() string { 21 | reader := bufio.NewReader(client.socket) 22 | for { 23 | message, err := reader.ReadString('\n') 24 | if err != nil { 25 | client.socket.Close() 26 | return "" 27 | } 28 | return string(message) 29 | } 30 | } 31 | 32 | func NewClient(addr string) (*Client, string) { 33 | connection, err := net.Dial("tcp", addr) 34 | if err != nil { 35 | fmt.Println(err) 36 | os.Exit(1) 37 | } 38 | client := &Client{socket: connection} 39 | hello := client.Read() 40 | return client, hello 41 | } 42 | 43 | func TestMail(t *testing.T) { 44 | var listTests = []struct { 45 | message string // input 46 | response string // expected result 47 | }{ 48 | //{"A01 CAPABILITY", "* CAPABILITY IMAP4rev1 AUTH=PLAIN"}, 49 | {"A02 NOOP", "A02 OK"}, 50 | {"A03 LOGIN joe password", "A03 NO LOGIN failed"}, 51 | {"A04 CLOSE", ""}, 52 | } 53 | 54 | s := NewServer("localhost", ":1992", 55 | "", "", false) 56 | //s.SetDebug(true) 57 | //s.SetQuiet(false) 58 | 59 | //u := strings.ReplaceAll(capFlag, ";", "\r\n") 60 | //s.SetCapability(u) 61 | 62 | e := Listen(s) 63 | if e != nil { 64 | fmt.Printf("Listen() ERROR: %v\n", e) 65 | return 66 | } 67 | 68 | go Serve(s) 69 | 70 | client, hello := NewClient("localhost:1992") 71 | println("hello from server : ", hello) 72 | client.Send("A01 CAPABILITY") 73 | r1 := client.Read() 74 | if strings.TrimSuffix(r1,"\r\n") != "* CAPABILITY IMAP4rev1 AUTH=PLAIN" { 75 | t.Errorf("send: \"A01 CAPABILITY\"\n wait: \"* CAPABILITY IMAP4rev1 AUTH=PLAIN\"\n receive: \"%s\"\n", r1) 76 | } 77 | r1 = client.Read() 78 | if strings.TrimSuffix(r1,"\r\n") != "A01 OK CAPABILITY" { 79 | t.Errorf(" wait: \"A01 OK CAPABILITY\"\n receive: \"%s\"\n", r1) 80 | } 81 | 82 | for _, tt := range listTests { 83 | println("write to server: ", tt.message) 84 | 85 | client.Send(tt.message) 86 | reply := client.Read() 87 | 88 | println(" wait from server: ", tt.response) 89 | println("reply from server: ", reply) 90 | 91 | if strings.TrimSuffix(reply,"\r\n") != tt.response { 92 | t.Errorf("send: \"%s\"\n wait: \"%s\"\n receive: \"%s\"\n", tt.message, tt.response, reply) 93 | } 94 | } 95 | 96 | } 97 | 98 | -------------------------------------------------------------------------------- /imaphoney/honey.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "crypto/tls" 7 | "encoding/base64" 8 | "flag" 9 | "fmt" 10 | "log" 11 | "log/syslog" 12 | "net" 13 | "strconv" 14 | "strings" 15 | "time" 16 | ) 17 | 18 | var Version string 19 | 20 | type Server struct { 21 | debug bool 22 | quiet bool // don't write to console 23 | addr string 24 | hostname string 25 | capability string 26 | listener net.Listener 27 | closed bool 28 | withTLS bool 29 | tlsConfig *tls.Config 30 | } 31 | 32 | func (server *Server) IsDebug() bool { 33 | return server.debug 34 | } 35 | func (server *Server) SetDebug(d bool) { 36 | server.debug = d 37 | } 38 | func (server *Server) IsQuiet() bool { 39 | return server.quiet 40 | } 41 | func (server *Server) SetQuiet(q bool) { 42 | server.quiet = q 43 | } 44 | func (server *Server) SetCapability(s string) { 45 | server.capability = s 46 | } 47 | func (server *Server) Closed() bool { return server.closed } 48 | func (server *Server) Close() { 49 | server.closed = true 50 | server.listener.Close() 51 | } 52 | func NewServer(hostname string, addr string, certPath string, keyPath string, withTLS bool) *Server { 53 | var tlsConfig *tls.Config 54 | if withTLS { 55 | cert, err := tls.LoadX509KeyPair(certPath, keyPath) 56 | if err != nil { 57 | fmt.Errorf("%v", err) 58 | return nil 59 | } 60 | tlsConfig = &tls.Config{Certificates: []tls.Certificate{cert}} 61 | 62 | } 63 | server := &Server{false, false, addr, hostname, "IMAP4rev1 AUTH=PLAIN", nil, false, withTLS, tlsConfig} 64 | return server 65 | } 66 | 67 | // Session 68 | 69 | type Session struct { 70 | server *Server 71 | conn net.Conn 72 | reader *bufio.Reader 73 | writer *bufio.Writer 74 | // Stateful stuff 75 | state int 76 | username string 77 | } 78 | 79 | func NewSession( 80 | server *Server, conn net.Conn, 81 | reader *bufio.Reader, writer *bufio.Writer, 82 | ) *Session { 83 | s := &Session{server, conn, reader, writer, 0, ""} 84 | return s 85 | } 86 | func (sess *Session) Sendf(format string, args ...interface{}) { 87 | fmt.Fprintf(sess.writer, format, args...) 88 | sess.writer.Flush() 89 | } 90 | func (sess *Session) Readline() (string, error) { 91 | s, e := sess.reader.ReadString('\n') 92 | return s, e 93 | } 94 | func (sess *Session) SetUsername(username string) { 95 | sess.username = username 96 | } 97 | func (sess *Session) RemoteIP() string { 98 | s := sess.conn.RemoteAddr().String() 99 | ip, _, _ := net.SplitHostPort(s) 100 | return ip 101 | } 102 | func (sess *Session) Log(s string) { 103 | log.Print(s) // syslog 104 | if !sess.server.IsQuiet() { 105 | fmt.Printf("%s - %s\n", time.Now().Format(time.RFC3339), s) // console 106 | } 107 | } 108 | 109 | // Command 110 | 111 | type Command struct { 112 | Tag string 113 | Command string 114 | Arguments string 115 | } 116 | 117 | func ParseCommand(s string) (*Command, error) { 118 | var tag, com, args string = "", "", "" 119 | 120 | sp := strings.Split(s, " ") 121 | 122 | switch len(sp) { 123 | case 1: 124 | args = sp[0] 125 | if len(args) == 0 { 126 | return nil, fmt.Errorf("Missing tag in command %q", s) 127 | } 128 | 129 | if len(args) <= 200 { 130 | // auth plain base64 encoding 131 | d, err := base64.StdEncoding.DecodeString(args) 132 | if err == nil { 133 | d = bytes.Replace(d, []byte("\x00"), []byte(" "), -1) 134 | args = strconv.QuoteToASCII(string(d[:])) 135 | com = "LOGIN" 136 | } else { 137 | return nil, fmt.Errorf("Missing tag in command %q", s) 138 | } 139 | } 140 | case 2: 141 | tag = sp[0] 142 | com = strings.ToUpper(strings.TrimSpace(sp[1])) 143 | case 3: 144 | tag = sp[0] 145 | com = strings.ToUpper(strings.TrimSpace(sp[1])) 146 | args = sp[2] 147 | case 4: 148 | tag = sp[0] 149 | com = strings.ToUpper(strings.TrimSpace(sp[1])) 150 | args = sp[2] + " " + sp[3] 151 | } 152 | 153 | //fmt.Printf("tag %s, com %s, args %s\n", tag,com,args) 154 | 155 | command := &Command{tag, com, args} 156 | return command, nil 157 | } 158 | 159 | func handle_session(sess *Session) error { 160 | timeout := time.Duration(3) * time.Minute 161 | sess.conn.SetReadDeadline(time.Now().Add(timeout)) 162 | 163 | if sess.server.IsDebug() { 164 | sess.Log(fmt.Sprintf("IP: %s, OPENED %p", sess.RemoteIP(), sess)) 165 | } 166 | 167 | // Send greeting 168 | // sess.Sendf("OK %s IMAP4rev1\r\n", sess.server.hostname) 169 | sess.Sendf("OK IMAP4\r\n") 170 | 171 | var command *Command 172 | memtag := "" // for AUTH=PLAIN 173 | 174 | command: 175 | s, e := sess.Readline() 176 | if e != nil { 177 | goto err 178 | } 179 | s = strings.TrimRight(s, "\r\n") 180 | if sess.server.IsDebug() { 181 | sess.Log(fmt.Sprintf("IP: %s, COMMAND: %s", sess.RemoteIP(), s)) 182 | } 183 | 184 | command, e = ParseCommand(s) 185 | if e != nil { 186 | goto err 187 | } 188 | 189 | // Handle commands 190 | 191 | switch command.Command { 192 | case "CAPABILITY": 193 | //sess.Sendf("* CAPABILITY ACL ID IDLE IMAP4rev1 AUTH=PLAIN\r\n") 194 | sess.Sendf("* CAPABILITY %s\r\n", sess.server.capability) 195 | sess.Sendf("%s OK CAPABILITY\r\n", command.Tag) 196 | goto command 197 | case "NOOP": 198 | sess.Sendf("%s OK\r\n", command.Tag) 199 | goto command 200 | case "AUTHENTICATE": 201 | sess.Sendf("+\r\n") 202 | memtag = command.Tag 203 | goto command 204 | case "LOGIN": 205 | sess.Log(fmt.Sprintf("IP: %s, LOGIN: %s", sess.RemoteIP(), command.Arguments)) 206 | tag := command.Tag 207 | time.Sleep(3 * time.Second) 208 | if command.Tag == "" { 209 | tag = memtag 210 | } 211 | sess.Sendf("%s NO LOGIN failed\r\n", tag) 212 | goto close 213 | case "LOGOUT": 214 | sess.Sendf("* BYE %s\r\n", sess.server.hostname) 215 | sess.Sendf("%s OK LOGOUT\r\n", command.Tag) 216 | goto close 217 | default: 218 | sess.Sendf("%s BAD invalid command\r\n", command.Tag) 219 | goto command 220 | } 221 | 222 | close: 223 | sess.conn.Close() 224 | if sess.server.IsDebug() { 225 | sess.Log(fmt.Sprintf("CLOSED %p\n", sess)) 226 | } 227 | return nil 228 | 229 | err: 230 | sess.conn.Close() 231 | return fmt.Errorf("handle_session: %v", e) 232 | } 233 | 234 | // Server 235 | 236 | func Listen(server *Server) error { 237 | var ln net.Listener 238 | var e error 239 | 240 | if server.withTLS { 241 | ln, e = tls.Listen("tcp", server.addr, server.tlsConfig) 242 | } else { 243 | ln, e = net.Listen("tcp", server.addr) 244 | } 245 | if e != nil { 246 | return e 247 | } else { 248 | server.listener = ln 249 | } 250 | return nil 251 | } 252 | 253 | func Serve(server *Server) error { 254 | for { 255 | conn, e := server.listener.Accept() 256 | if e != nil { 257 | if server.Closed() { 258 | break 259 | } 260 | fmt.Printf("accept error: %v\n", e) 261 | return e 262 | } 263 | go func(conn_pointer *net.Conn) { 264 | conn := *conn_pointer 265 | sess := NewSession( 266 | server, conn, 267 | bufio.NewReader(conn), bufio.NewWriter(conn), 268 | ) 269 | 270 | e = handle_session(sess) 271 | if e != nil { 272 | fmt.Printf("Serve() ERROR: %v\n", e) 273 | return 274 | } 275 | 276 | }(&conn) //goroutine 277 | } 278 | 279 | return nil 280 | } 281 | 282 | /** 283 | 284 | USAGE 285 | 286 | openssl genrsa -out server.key 2048 287 | openssl req -new -x509 -sha256 -key server.key -out server.pem -days 3650 288 | 289 | ./honey -d -cert server.pem -key server.key -addr :9443 -server server:514 290 | 291 | **/ 292 | func main() { 293 | 294 | fmt.Printf("Version: %s\n", Version) 295 | syslogServerFlag := flag.String("server", "", "syslog remote server") 296 | hostnameFlag := flag.String("hostname", "localhost", "hostname") 297 | addressFlag := flag.String("addr", ":1993", "ipaddr:port") 298 | certFlag := flag.String("cert", "", "cert file") 299 | keyFlag := flag.String("key", "", "cert file") 300 | capFlag := flag.String("cap", "ACL ID IDLE IMAP4rev1 AUTH=PLAIN", "imap CAPABILITY") 301 | debugFlag := flag.Bool("d", false, "debug") 302 | quietFlag := flag.Bool("q", false, "quiet - no msg in console") 303 | flag.Parse() 304 | 305 | withTls := false 306 | if *certFlag != "" && *keyFlag != "" { 307 | withTls = true 308 | } 309 | 310 | log.SetFlags(0) // remove useless timestamp for syslog 311 | if *syslogServerFlag == "" { 312 | logwriter, err := syslog.New(syslog.LOG_NOTICE, "imaphoney") 313 | if err == nil { 314 | log.SetOutput(logwriter) 315 | } 316 | } else { 317 | logwriter, err := syslog.Dial("udp", *syslogServerFlag, syslog.LOG_NOTICE, "imaphoney") 318 | if err == nil { 319 | log.SetOutput(logwriter) 320 | } else { 321 | fmt.Printf("syslog.Dial() ERROR: %v\n", err) 322 | } 323 | } 324 | 325 | s := NewServer(*hostnameFlag, *addressFlag, 326 | *certFlag, *keyFlag, withTls) 327 | s.SetDebug(*debugFlag) 328 | s.SetQuiet(*quietFlag) 329 | 330 | s.SetCapability(*capFlag) 331 | 332 | e := Listen(s) 333 | if e != nil { 334 | fmt.Printf("Listen() ERROR: %v\n", e) 335 | return 336 | } 337 | 338 | Serve(s) 339 | } 340 | -------------------------------------------------------------------------------- /smtphoney/client_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "net" 7 | "os" 8 | "strings" 9 | "testing" 10 | ) 11 | 12 | type Client struct { 13 | socket net.Conn 14 | } 15 | 16 | func (client *Client) Send(msg string) { 17 | client.socket.Write([]byte(msg + "\r\n")) 18 | } 19 | 20 | func (client *Client) Read() string { 21 | reader := bufio.NewReader(client.socket) 22 | for { 23 | message, err := reader.ReadString('\n') 24 | if err != nil { 25 | client.socket.Close() 26 | return "" 27 | } 28 | return string(message) 29 | } 30 | } 31 | 32 | func NewClient(addr string) (*Client, string) { 33 | connection, err := net.Dial("tcp", addr) 34 | if err != nil { 35 | fmt.Println(err) 36 | os.Exit(1) 37 | } 38 | client := &Client{socket: connection} 39 | hello := client.Read() 40 | return client, hello 41 | } 42 | 43 | func TestMail(t *testing.T) { 44 | var listTests = []struct { 45 | message string // input 46 | response string // expected result 47 | }{ 48 | {"EHLO truc", "250-localhost"}, 49 | {"MAIL FROM: ", "250 Recipient ok"}, 50 | {"RCPT TO: Some One ", "550 ... Denied due to spam list"}, 51 | } 52 | 53 | s := NewServer("localhost", ":1993", 54 | "", "", false, 55 | false, false, false) 56 | //s.SetDebug(true) 57 | //s.SetQuiet(false) 58 | 59 | //u := strings.ReplaceAll(capFlag, ";", "\r\n") 60 | //s.SetCapability(u) 61 | 62 | e := Listen(s) 63 | if e != nil { 64 | fmt.Printf("Listen() ERROR: %v\n", e) 65 | return 66 | } 67 | 68 | go Serve(s) 69 | 70 | client, hello := NewClient("localhost:1993") 71 | println("hello from server : ", hello) 72 | 73 | for _, tt := range listTests { 74 | println("write to server: ", tt.message) 75 | 76 | client.Send(tt.message) 77 | reply := client.Read() 78 | 79 | println(" wait from server: ", tt.response) 80 | println("reply from server: ", reply) 81 | 82 | if strings.TrimSuffix(reply, "\r\n") != tt.response { 83 | t.Errorf("send: \"%s\"\n wait: \"%s\"\n receive: \"%s\"\n", tt.message, tt.response, reply) 84 | } 85 | } 86 | 87 | } 88 | 89 | func TestAuth(t *testing.T) { 90 | var listTests = []struct { 91 | message string // input 92 | response string // expected result 93 | }{ 94 | {"EHLO truc", "250-localhost"}, 95 | {"AUTH LOGIN", "334 VXNlcm5hbWU6"}, 96 | {"YWRtaW4=", "334 UGFzc3dvcmQ6"}, 97 | {"YWRtaW4=", "535 5.7.0 Error: authentication failed"}, 98 | } 99 | 100 | s := NewServer("localhost", ":1994", 101 | "", "", false, 102 | true, false, false) 103 | //s.SetDebug(true) 104 | //s.SetQuiet(false) 105 | 106 | //u := strings.ReplaceAll(capFlag, ";", "\r\n") 107 | //s.SetCapability(u) 108 | 109 | e := Listen(s) 110 | if e != nil { 111 | fmt.Printf("Listen() ERROR: %v\n", e) 112 | return 113 | } 114 | 115 | go Serve(s) 116 | 117 | client, hello := NewClient("localhost:1994") 118 | println("hello from server : ", hello) 119 | 120 | for _, tt := range listTests { 121 | println("write to server: ", tt.message) 122 | 123 | client.Send(tt.message) 124 | reply := client.Read() 125 | 126 | println(" wait from server: ", tt.response) 127 | println("reply from server: ", reply) 128 | 129 | if strings.TrimSuffix(reply, "\r\n") != tt.response { 130 | t.Errorf("send: \"%s\"\n wait: \"%s\"\n receive: \"%s\"\n", tt.message, tt.response, reply) 131 | } 132 | } 133 | 134 | } 135 | 136 | func TestErrors(t *testing.T) { 137 | var listTests = []struct { 138 | message string // input 139 | response string // expected result 140 | }{ 141 | {"HELO", "250-localhost"}, 142 | {"EHLO", "250-localhost"}, 143 | {"MAIL FROM: ", "250 Recipient ok"}, 144 | {"RCPT TO: ", "550 <>... Denied due to spam list"}, 145 | } 146 | 147 | s := NewServer("localhost", ":1995", 148 | "", "", false, 149 | false, false, false) 150 | //s.SetDebug(true) 151 | //s.SetQuiet(false) 152 | 153 | //u := strings.ReplaceAll(capFlag, ";", "\r\n") 154 | //s.SetCapability(u) 155 | 156 | e := Listen(s) 157 | if e != nil { 158 | fmt.Printf("Listen() ERROR: %v\n", e) 159 | return 160 | } 161 | 162 | go Serve(s) 163 | 164 | client, hello := NewClient("localhost:1995") 165 | println("hello from server : ", hello) 166 | 167 | for _, tt := range listTests { 168 | println("write to server: ", tt.message) 169 | 170 | client.Send(tt.message) 171 | reply := client.Read() 172 | 173 | println(" wait from server: ", tt.response) 174 | println("reply from server: ", reply) 175 | 176 | if strings.TrimSuffix(reply, "\r\n") != tt.response { 177 | t.Errorf("send: \"%s\"\n wait: \"%s\"\n receive: \"%s\"\n", tt.message, tt.response, reply) 178 | } 179 | } 180 | 181 | } 182 | func TestErrors2(t *testing.T) { 183 | var listTests = []struct { 184 | message string // input 185 | response string // expected result 186 | }{ 187 | {"HELO", "250-localhost"}, 188 | {"RCPT TO ", "221 2.0.0 Bye"}, 189 | {"MAIL FROM", ""}, 190 | } 191 | 192 | s := NewServer("localhost", ":1996", 193 | "", "", false, 194 | false, false, false) 195 | //s.SetDebug(true) 196 | //s.SetQuiet(false) 197 | 198 | //u := strings.ReplaceAll(capFlag, ";", "\r\n") 199 | //s.SetCapability(u) 200 | 201 | e := Listen(s) 202 | if e != nil { 203 | fmt.Printf("Listen() ERROR: %v\n", e) 204 | return 205 | } 206 | 207 | go Serve(s) 208 | 209 | client, hello := NewClient("localhost:1996") 210 | println("hello from server : ", hello) 211 | 212 | for _, tt := range listTests { 213 | println("write to server: ", tt.message) 214 | 215 | client.Send(tt.message) 216 | reply := client.Read() 217 | 218 | println(" wait from server: ", tt.response) 219 | println("reply from server: ", reply) 220 | 221 | if strings.TrimSuffix(reply, "\r\n") != tt.response { 222 | t.Errorf("send: \"%s\"\n wait: \"%s\"\n receive: \"%s\"\n", tt.message, tt.response, reply) 223 | } 224 | } 225 | 226 | } 227 | -------------------------------------------------------------------------------- /smtphoney/honey.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | //"bytes" 6 | "crypto/tls" 7 | "encoding/base64" 8 | "flag" 9 | "fmt" 10 | "log" 11 | "log/syslog" 12 | "net" 13 | "net/mail" 14 | //"strconv" 15 | "regexp" 16 | "strings" 17 | "time" 18 | ) 19 | 20 | var Version string 21 | 22 | type Server struct { 23 | debug bool 24 | quiet bool // don't write to console 25 | addr string 26 | hostname string 27 | capability string 28 | listener net.Listener 29 | closed bool 30 | withTLS bool 31 | logAuth bool 32 | logData bool 33 | authOK bool 34 | tlsConfig *tls.Config 35 | } 36 | 37 | func (server *Server) IsDebug() bool { 38 | return server.debug 39 | } 40 | func (server *Server) SetDebug(d bool) { 41 | server.debug = d 42 | } 43 | func (server *Server) IsQuiet() bool { 44 | return server.quiet 45 | } 46 | func (server *Server) SetQuiet(q bool) { 47 | server.quiet = q 48 | } 49 | 50 | func (server *Server) SetCapability(s string) { 51 | server.capability = s 52 | } 53 | func (server *Server) Closed() bool { return server.closed } 54 | func (server *Server) Close() { 55 | server.closed = true 56 | server.listener.Close() 57 | } 58 | func NewServer(hostname string, addr string, certPath string, keyPath string, withTLS bool, logAuth bool, logData bool, authOK bool) *Server { 59 | var tlsConfig *tls.Config 60 | if withTLS { 61 | cert, err := tls.LoadX509KeyPair(certPath, keyPath) 62 | if err != nil { 63 | fmt.Errorf(err.Error()) 64 | return nil 65 | } 66 | tlsConfig = &tls.Config{Certificates: []tls.Certificate{cert}} 67 | 68 | } 69 | 70 | //server := &Server{false, false, addr, hostname, "250-localhost\r\n", nil, false, withTLS, logAuth, logData, authOK, tlsConfig} 71 | server := &Server{ 72 | debug: false, 73 | quiet: false, // don't write to console 74 | addr: addr, 75 | hostname: hostname, 76 | capability: "250-localhost\r\n", 77 | listener: nil, 78 | closed: false, 79 | withTLS: withTLS, 80 | logAuth: logAuth, 81 | logData: logData, 82 | authOK: authOK, 83 | tlsConfig: tlsConfig, 84 | } 85 | return server 86 | } 87 | 88 | // Session 89 | 90 | type Session struct { 91 | server *Server 92 | conn net.Conn 93 | reader *bufio.Reader 94 | writer *bufio.Writer 95 | // Stateful stuff 96 | state int 97 | username string 98 | } 99 | 100 | func NewSession( 101 | server *Server, conn net.Conn, 102 | reader *bufio.Reader, writer *bufio.Writer, 103 | ) *Session { 104 | s := &Session{server, conn, reader, writer, 0, ""} 105 | return s 106 | } 107 | func (sess *Session) Sendf(format string, args ...interface{}) { 108 | fmt.Fprintf(sess.writer, format, args...) 109 | sess.writer.Flush() 110 | } 111 | func (sess *Session) Readline() (string, error) { 112 | s, e := sess.reader.ReadString('\n') 113 | return s, e 114 | } 115 | func (sess *Session) SetUsername(username string) { 116 | sess.username = username 117 | } 118 | func (sess *Session) GetUsername() string { 119 | return sess.username 120 | } 121 | func (sess *Session) RemoteIP() string { 122 | s := sess.conn.RemoteAddr().String() 123 | ip, _, _ := net.SplitHostPort(s) 124 | return ip 125 | } 126 | func (sess *Session) Log(s string) { 127 | log.Print(s) // syslog 128 | if !sess.server.IsQuiet() { 129 | fmt.Printf("%s - %s\n", time.Now().Format(time.RFC3339), s) // console 130 | } 131 | } 132 | 133 | // Command 134 | 135 | type Command struct { 136 | Command string 137 | Arguments string 138 | } 139 | 140 | func cleanMail(mails string) string { 141 | emails, _ := mail.ParseAddressList(mails) 142 | var u []string 143 | for _, v := range emails { 144 | u = append(u, v.Address) 145 | } 146 | return strings.Join(u, ", ") 147 | } 148 | 149 | func ParseCommand(s string) (*Command, error) { 150 | 151 | command := &Command{"", ""} 152 | 153 | matched, _ := regexp.MatchString(`^\.$`, s) 154 | switch { 155 | case strings.Contains(s, "EHLO"): 156 | sp := strings.Split(s, " ") 157 | command.Command = "EHLO" 158 | if len(sp) > 1 { 159 | command.Arguments = sp[1] 160 | } 161 | case strings.Contains(s, "HELO"): 162 | sp := strings.Split(s, " ") 163 | command.Command = "HELO" 164 | if len(sp) > 1 { 165 | command.Arguments = sp[1] 166 | } 167 | case strings.Contains(s, "DATA"): 168 | command.Command = "DATA" 169 | case matched: 170 | command.Command = "END" 171 | case strings.Contains(s, "QUIT"): 172 | command.Command = "QUIT" 173 | case strings.Contains(s, "MAIL FROM"): 174 | sp := strings.Split(s, ":") 175 | command.Command = "QUIT" 176 | if len(sp) > 1 { 177 | command.Command = "MAIL" 178 | command.Arguments = cleanMail(sp[1]) 179 | } 180 | case strings.Contains(s, "RCPT TO"): 181 | sp := strings.Split(s, ":") 182 | command.Command = "QUIT" 183 | if len(sp) > 1 { 184 | command.Command = "TO" 185 | command.Arguments = cleanMail(sp[1]) 186 | } 187 | case strings.Contains(s, "AUTH LOGIN"): 188 | command.Command = "AUTH" 189 | case strings.Contains(s, "STARTTLS"): 190 | command.Command = "STARTTLS" 191 | default: 192 | command.Command = s 193 | } 194 | return command, nil 195 | } 196 | 197 | func handle_session(sess *Session) error { 198 | timeout := time.Duration(3) * time.Minute 199 | sess.conn.SetReadDeadline(time.Now().Add(timeout)) 200 | 201 | if sess.server.IsDebug() { 202 | sess.Log(fmt.Sprintf("IP: %s, OPENED %p", sess.RemoteIP(), sess)) 203 | } 204 | 205 | // Send greeting 206 | sess.Sendf("220 %s ESMTP ready\r\n", sess.server.hostname) 207 | 208 | var command *Command 209 | 210 | command: 211 | s, e := sess.Readline() 212 | 213 | if e != nil { 214 | goto err 215 | } 216 | s = strings.TrimRight(s, "\r\n") 217 | if sess.server.IsDebug() { 218 | sess.Log(fmt.Sprintf("IP: %s, COMMAND: %s", sess.RemoteIP(), s)) 219 | } 220 | 221 | command, e = ParseCommand(s) 222 | if e != nil { 223 | goto err 224 | } 225 | 226 | // Handle commands 227 | 228 | switch command.Command { 229 | case "HELO": 230 | sp := strings.Split(sess.server.capability, "\r\n") 231 | sess.Sendf("%s\r\n", sp[0]) 232 | goto command 233 | case "EHLO": 234 | sess.Sendf(sess.server.capability) 235 | goto command 236 | case "TO": 237 | if sess.server.logData { 238 | sess.Sendf("250 Sender ok\r\n") 239 | goto command 240 | } 241 | sess.Sendf("550 <%s>... Denied due to spam list\r\n", command.Arguments) 242 | goto close 243 | case "MAIL": 244 | sess.Sendf("250 Recipient ok\r\n") 245 | goto command 246 | case "DATA": 247 | sess.Sendf("354 Enter mail, end with \".\" on a line by itself\r\n") 248 | goto command 249 | case "END": 250 | sess.Sendf("250 Ok\r\n") 251 | goto command 252 | case "RSET": 253 | sess.Sendf("250 Ok\r\n") 254 | goto command 255 | case "QUIT": 256 | sess.Sendf("221 2.0.0 Bye\r\n") 257 | goto close 258 | case "AUTH": 259 | if sess.server.logAuth { 260 | sess.Sendf("334 VXNlcm5hbWU6\r\n") 261 | goto command 262 | } 263 | sess.Sendf("503 5.5.1 Error: authentication not enabled\r\n") 264 | goto close 265 | case "STARTTLS": 266 | //sess.Sendf("502 5.5.2 Error: command not recognized\r\n") 267 | //goto close 268 | sess.Sendf("454 TLS not available due to temporary reason\r\n") 269 | goto close 270 | default: 271 | rawDecodedText, err := base64.StdEncoding.DecodeString(command.Command) 272 | if err == nil { 273 | login := sess.GetUsername() 274 | if login == "" { 275 | sess.SetUsername(string(rawDecodedText)) 276 | sess.Sendf("334 UGFzc3dvcmQ6\r\n") 277 | } else { 278 | sess.Log(fmt.Sprintf("IP: %s, LOGIN: \"%s\", PASS: \"%s\"", sess.RemoteIP(), login, rawDecodedText)) 279 | time.Sleep(3 * time.Second) 280 | if sess.server.authOK { 281 | sess.Sendf("2.7.0 Authentication successful\r\n") 282 | } else { 283 | sess.Sendf("535 5.7.0 Error: authentication failed\r\n") 284 | goto close 285 | } 286 | } 287 | } else { 288 | sess.Sendf("502 5.5.2 Error: command not recognized\r\n") 289 | goto close 290 | } 291 | 292 | //sess.Sendf("BAD invalid command\r\n") 293 | goto command 294 | } 295 | 296 | close: 297 | sess.conn.Close() 298 | if sess.server.IsDebug() { 299 | sess.Log(fmt.Sprintf("CLOSED %p\n", sess)) 300 | } 301 | return nil 302 | 303 | err: 304 | sess.conn.Close() 305 | return fmt.Errorf("handle_session: %v", e) 306 | } 307 | 308 | // Server 309 | 310 | func Listen(server *Server) error { 311 | var ln net.Listener 312 | var e error 313 | 314 | if server.withTLS { 315 | ln, e = tls.Listen("tcp", server.addr, server.tlsConfig) 316 | } else { 317 | ln, e = net.Listen("tcp", server.addr) 318 | } 319 | if e != nil { 320 | return e 321 | } else { 322 | server.listener = ln 323 | } 324 | return nil 325 | } 326 | 327 | func Serve(server *Server) error { 328 | for { 329 | conn, e := server.listener.Accept() 330 | if e != nil { 331 | if server.Closed() { 332 | break 333 | } 334 | fmt.Printf("accept error: %v\n", e) 335 | return e 336 | } 337 | go func(conn_pointer *net.Conn) { 338 | conn := *conn_pointer 339 | sess := NewSession( 340 | server, conn, 341 | bufio.NewReader(conn), bufio.NewWriter(conn), 342 | ) 343 | 344 | e = handle_session(sess) 345 | if e != nil { 346 | fmt.Printf("Serve() ERROR: %v\n", e) 347 | return 348 | } 349 | 350 | }(&conn) //goroutine 351 | } 352 | 353 | return nil 354 | } 355 | 356 | /** 357 | 358 | USAGE 359 | 360 | openssl genrsa -out server.key 2048 361 | openssl req -new -x509 -sha256 -key server.key -out server.pem -days 3650 362 | 363 | ./honey -d -cert server.pem -key server.key -addr :9443 -server server:514 364 | 365 | **/ 366 | func main() { 367 | 368 | fmt.Printf("Version: %s\n", Version) 369 | syslogServerFlag := flag.String("server", "", "syslog remote server") 370 | hostnameFlag := flag.String("hostname", "localhost", "hostname") 371 | addressFlag := flag.String("addr", ":1993", "ipaddr:port") 372 | certFlag := flag.String("cert", "", "cert file") 373 | keyFlag := flag.String("key", "", "cert file") 374 | var capFlag string 375 | flag.StringVar(&capFlag, "cap", "250-localhost;250-PIPELINING;250-SIZE 5242880;250-ETRN;250 8BITMIME;250 DSN;", "smtp CAPABILITY") 376 | logAuthFlag := flag.Bool("la", false, "log auth") 377 | logDataFlag := flag.Bool("ld", false, "log data") 378 | authOk := flag.Bool("aok", false, "auth ok") 379 | debugFlag := flag.Bool("d", false, "debug") 380 | quietFlag := flag.Bool("q", false, "quiet - no msg in console") 381 | flag.Parse() 382 | 383 | withTls := false 384 | if *certFlag != "" && *keyFlag != "" { 385 | withTls = true 386 | } 387 | 388 | log.SetFlags(0) // remove useless timestamp for syslog 389 | if *syslogServerFlag == "" { 390 | logwriter, err := syslog.New(syslog.LOG_NOTICE, "smtphoney") 391 | if err == nil { 392 | log.SetOutput(logwriter) 393 | } 394 | } else { 395 | logwriter, err := syslog.Dial("udp", *syslogServerFlag, syslog.LOG_NOTICE, "smtphoney") 396 | if err == nil { 397 | log.SetOutput(logwriter) 398 | } else { 399 | fmt.Printf("syslog.Dial() ERROR: %v\n", err) 400 | } 401 | } 402 | 403 | s := NewServer(*hostnameFlag, *addressFlag, 404 | *certFlag, *keyFlag, withTls, 405 | *logAuthFlag, *logDataFlag, *authOk) 406 | s.SetDebug(*debugFlag) 407 | s.SetQuiet(*quietFlag) 408 | 409 | u := strings.ReplaceAll(capFlag, ";", "\r\n") 410 | s.SetCapability(u) 411 | 412 | e := Listen(s) 413 | if e != nil { 414 | fmt.Printf("Listen() ERROR: %v\n", e) 415 | return 416 | } 417 | 418 | Serve(s) 419 | } 420 | --------------------------------------------------------------------------------