Files
honeydany/app/services.go
T

557 lines
15 KiB
Go
Raw Normal View History

2025-09-28 06:48:03 +01:00
package app
import (
"bufio"
"crypto/rand"
"crypto/rsa"
"context"
"fmt"
"io"
"log"
"net"
"net/http"
"strconv"
"strings"
"sync"
"time"
"golang.org/x/crypto/ssh"
)
// App holds runtime pieces
type App struct {
cfg Config
logger *Logger
threatIntel *ThreatIntel
ctx context.Context
cancel context.CancelFunc
wg sync.WaitGroup
// keep references to servers for graceful shutdown
httpSrv *http.Server
sshSigner ssh.Signer
// track TCP listeners for graceful shutdown
mu sync.Mutex
listeners []net.Listener
}
// addListener registers a listener for later shutdown
func (a *App) addListener(l net.Listener) {
a.mu.Lock()
a.listeners = append(a.listeners, l)
a.mu.Unlock()
}
// parseCreds attempts to extract a username and password from arbitrary SSH-like payloads.
// This is heuristic-based and targets common patterns like:
// - "login: root\npassword: toor"
// - "user=root pass=toor"
// - "username root password toor"
func parseCreds(data []byte) (string, string) {
sOrig := string(data)
sLower := strings.ToLower(sOrig)
// Try common keys in decreasing specificity
userKeys := []string{"username", "user", "login"}
passKeys := []string{"password", "passwd", "pass", "pwd"}
var user string
var pass string
for _, k := range userKeys {
if user == "" {
user = extractAfter(sLower, sOrig, k)
}
}
for _, k := range passKeys {
if pass == "" {
pass = extractAfter(sLower, sOrig, k)
}
}
// Handle a very common two-line prompt format
if user == "" && strings.Contains(sLower, "login:") {
user = extractAfter(sLower, sOrig, "login:")
}
if pass == "" && strings.Contains(sLower, "password:") {
pass = extractAfter(sLower, sOrig, "password:")
}
// Trim quotes and whitespace
user = trimQuotes(strings.TrimSpace(user))
pass = trimQuotes(strings.TrimSpace(pass))
return user, pass
}
// extractAfter finds the token in the lowercased string, then returns the next word-like token
// from the original-cased string.
func extractAfter(sLower, sOrig, tokenLower string) string {
idx := strings.Index(sLower, tokenLower)
if idx == -1 {
return ""
}
i := idx + len(tokenLower)
// Skip separators
for i < len(sLower) {
c := sLower[i]
if c == ' ' || c == '\t' || c == ':' || c == '=' || c == '-' { // common separators
i++
continue
}
break
}
// Capture until whitespace or line break
j := i
for j < len(sLower) {
c := sLower[j]
if c == ' ' || c == '\t' || c == '\n' || c == '\r' { // stop at separators
break
}
j++
}
if i >= len(sOrig) || j > len(sOrig) || i >= j {
return ""
}
return sOrig[i:j]
}
func trimQuotes(s string) string {
if len(s) >= 2 {
if (s[0] == '\'' && s[len(s)-1] == '\'') || (s[0] == '"' && s[len(s)-1] == '"') {
return s[1 : len(s)-1]
}
}
return s
}
func NewApp(cfg Config) (*App, error) {
l, err := NewLogger(cfg)
if err != nil {
return nil, err
}
ctx, cancel := context.WithCancel(context.Background())
a := &App{cfg: cfg, logger: l, ctx: ctx, cancel: cancel}
return a, nil
}
func (a *App) Run(ctx context.Context) error {
// start services according to cfg
if a.cfg.Services.HTTP {
a.wg.Add(1)
go func() {
defer a.wg.Done()
a.startHTTP(a.cfg.Ports.HTTP)
}()
}
if a.cfg.Services.SSH {
a.wg.Add(1)
go func() {
defer a.wg.Done()
a.startTCPService("ssh", a.cfg.Ports.SSH, a.sshHandler)
}()
}
if a.cfg.Services.FTP {
a.wg.Add(1)
go func() {
defer a.wg.Done()
a.startTCPService("ftp", a.cfg.Ports.FTP, a.ftpHandler)
}()
}
if a.cfg.Services.SMTP {
a.wg.Add(1)
go func() {
defer a.wg.Done()
a.startTCPService("smtp", a.cfg.Ports.SMTP, a.smtpHandler)
}()
}
if a.cfg.Services.POP3 {
a.wg.Add(1)
go func() {
defer a.wg.Done()
a.startTCPService("pop3", a.cfg.Ports.POP3, a.pop3Handler)
}()
}
if a.cfg.Services.IMAP {
a.wg.Add(1)
go func() {
defer a.wg.Done()
a.startTCPService("imap", a.cfg.Ports.IMAP, a.imapHandler)
}()
}
if a.cfg.Services.Telnet {
a.wg.Add(1)
go func() {
defer a.wg.Done()
a.startTCPService("telnet", a.cfg.Ports.Telnet, a.telnetHandler)
}()
}
if a.cfg.Services.MySQL {
a.wg.Add(1)
go func() {
defer a.wg.Done()
a.startTCPService("mysql", a.cfg.Ports.MySQL, a.mysqlHandler)
}()
}
if a.cfg.Services.PostgreSQL {
a.wg.Add(1)
go func() {
defer a.wg.Done()
a.startTCPService("postgresql", a.cfg.Ports.PostgreSQL, a.postgresqlHandler)
}()
}
if a.cfg.Services.Redis {
a.wg.Add(1)
go func() {
defer a.wg.Done()
a.startTCPService("redis", a.cfg.Ports.Redis, a.redisHandler)
}()
}
if a.cfg.Services.MongoDB {
a.wg.Add(1)
go func() {
defer a.wg.Done()
a.startTCPService("mongodb", a.cfg.Ports.MongoDB, a.mongodbHandler)
}()
}
if a.cfg.Services.RDP {
a.wg.Add(1)
go func() {
defer a.wg.Done()
a.startTCPService("rdp", a.cfg.Ports.RDP, a.rdpHandler)
}()
}
if a.cfg.Services.SMB {
a.wg.Add(1)
go func() {
defer a.wg.Done()
a.startTCPService("smb", a.cfg.Ports.SMB, a.genericBannerHandler("SMB-Server-1.0"))
}()
}
if a.cfg.Services.VNC {
a.wg.Add(1)
go func() {
defer a.wg.Done()
a.startTCPService("vnc", a.cfg.Ports.VNC, a.genericBannerHandler("RFB 003.008"))
}()
}
if a.cfg.Services.SIP {
a.wg.Add(1)
go func() {
defer a.wg.Done()
a.startTCPService("sip", a.cfg.Ports.SIP, a.sipHandler)
}()
}
if a.cfg.Services.DNS {
a.wg.Add(1)
go func() {
defer a.wg.Done()
a.startTCPService("dns", a.cfg.Ports.DNS, a.dnsHandler)
}()
}
if a.cfg.Services.SNMP {
a.wg.Add(1)
go func() {
defer a.wg.Done()
a.startTCPService("snmp", a.cfg.Ports.SNMP, a.snmpHandler)
}()
}
if a.cfg.Services.LDAP {
a.wg.Add(1)
go func() {
defer a.wg.Done()
a.startTCPService("ldap", a.cfg.Ports.LDAP, a.ldapHandler)
}()
}
for _, p := range a.cfg.Services.Generic {
port := p
a.wg.Add(1)
go func() {
defer a.wg.Done()
a.startTCPService(fmt.Sprintf("generic-%d", port), port, a.genericEchoHandler)
}()
}
// start web dashboard if enabled
if a.cfg.Web.Enabled {
a.wg.Add(1)
go func() {
defer a.wg.Done()
a.startWeb()
}()
}
<-ctx.Done()
a.Shutdown()
return nil
}
func (a *App) Shutdown() {
a.cancel()
// attempt to close http server if running
if a.httpSrv != nil {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_ = a.httpSrv.Shutdown(ctx)
}
// close all TCP listeners to unblock Accept loops
a.closeAllListeners()
a.wg.Wait()
_ = a.logger.Close()
}
// closeAllListeners closes all tracked listeners to unblock Accept()
func (a *App) closeAllListeners() {
a.mu.Lock()
ls := a.listeners
a.listeners = nil
a.mu.Unlock()
for _, l := range ls {
_ = l.Close()
}
}
// helpers for logging
func (a *App) logEvent(r Record) {
if a.logger == nil {
return
}
_ = a.logger.Log(r)
}
// HTTP honeypot
func (a *App) startHTTP(port int) {
addr := fmt.Sprintf(":%d", port)
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// read bounded body
var bodySnippet string
if r.Body != nil {
b, _ := io.ReadAll(io.LimitReader(r.Body, 1024*64))
bodySnippet = string(b)
}
details := map[string]string{"method": r.Method, "url": r.URL.String(), "proto": r.Proto}
if ua := r.Header.Get("User-Agent"); ua != "" {
details["user_agent"] = ua
}
if auth := r.Header.Get("Authorization"); auth != "" {
details["authorization"] = auth
}
rec := Record{Timestamp: time.Now().UTC(), RemoteAddr: remoteIP(r.RemoteAddr), RemotePort: remotePort(r.RemoteAddr), Service: "http", Details: details, RawPayload: bodySnippet}
a.logEvent(rec)
w.Header().Set("Server", "Apache/2.4.41 (Ubuntu)")
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(200)
_, _ = w.Write([]byte("Welcome\n"))
})
srv := &http.Server{Addr: addr, Handler: mux}
a.httpSrv = srv
log.Printf("HTTP listening on %s", addr)
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Printf("http server error: %v", err)
}
log.Printf("http stopped")
}
// remote helpers
func remoteIP(addr string) string {
h, _, _ := net.SplitHostPort(addr)
return h
}
func remotePort(addr string) string {
_, p, _ := net.SplitHostPort(addr)
return p
}
// Generic TCP listener starter
func (a *App) startTCPService(name string, port int, handler func(net.Conn)) {
addr := fmt.Sprintf(":%d", port)
ln, err := net.Listen("tcp", addr)
if err != nil {
log.Printf("%s listen failed on %s: %v", name, addr, err)
return
}
log.Printf("%s listening on %s", name, addr)
a.addListener(ln)
defer func() {
ln.Close()
log.Printf("%s stopped", name)
}()
// Use a channel to coordinate shutdown
acceptCh := make(chan net.Conn)
errCh := make(chan error)
go func() {
for {
conn, err := ln.Accept()
if err != nil {
errCh <- err
return
}
acceptCh <- conn
}
}()
for {
select {
case <-a.ctx.Done():
return
case conn := <-acceptCh:
go func(c net.Conn) {
defer c.Close()
// Check if context is cancelled before processing
select {
case <-a.ctx.Done():
return
default:
handler(c)
}
}(conn)
case err := <-errCh:
select {
case <-a.ctx.Done():
return
default:
log.Printf("%s accept err: %v", name, err)
return // Exit on persistent errors
}
}
}
}
// sshHandler implements a real SSH server handshake and logs password auth attempts
func (a *App) sshHandler(conn net.Conn) {
defer conn.Close()
remote := conn.RemoteAddr().String()
sessionID := fmt.Sprintf("%x", time.Now().UnixNano())
start := time.Now()
log.Printf("[SSH] New connection from %s (session: %s)", remote, sessionID)
signer, err := a.getSSHSigner()
if err != nil {
log.Printf("[SSH] host key error: %v", err)
return
}
var authAttempts int
var lastUser, lastPass string
cfg := &ssh.ServerConfig{
NoClientAuth: false,
ServerVersion: "SSH-2.0-OpenSSH_7.9p1 Ubuntu-10",
PasswordCallback: func(c ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) {
authAttempts++
lastUser = c.User()
lastPass = string(pass)
a.logSSHEvent(sessionID, remote, "auth_attempt", map[string]string{
"attempt": strconv.Itoa(authAttempts),
"username": lastUser,
"password": lastPass,
"client": string(c.ClientVersion()),
})
return nil, fmt.Errorf("permission denied")
},
}
cfg.AddHostKey(signer)
_ = conn.SetDeadline(time.Now().Add(2 * time.Minute))
sc, chans, reqs, err := ssh.NewServerConn(conn, cfg)
if err != nil {
a.logSSHEvent(sessionID, remote, "session_end", map[string]string{
"auth_attempts": strconv.Itoa(authAttempts),
"duration_sec": fmt.Sprintf("%.2f", time.Since(start).Seconds()),
"last_username": lastUser,
"last_password": lastPass,
"error": err.Error(),
})
return
}
go ssh.DiscardRequests(reqs)
for ch := range chans {
_ = ch.Reject(ssh.Prohibited, "not allowed")
}
_ = sc.Close()
}
// getSSHSigner returns or generates an RSA host key signer
func (a *App) getSSHSigner() (ssh.Signer, error) {
if a.sshSigner != nil {
return a.sshSigner, nil
}
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, err
}
signer, err := ssh.NewSignerFromKey(key)
if err != nil {
return nil, err
}
a.sshSigner = signer
return signer, nil
}
// logSSHEvent is a helper to log SSH-specific events with consistent metadata
func (a *App) logSSHEvent(sessionID, remote, eventType string, details map[string]string) {
if details == nil {
details = make(map[string]string)
}
details["session_id"] = sessionID
a.logEvent(Record{
Timestamp: time.Now().UTC(),
RemoteAddr: remoteIP(remote),
RemotePort: remotePort(remote),
Service: "ssh",
Details: details,
RawPayload: fmt.Sprintf("%s: %v", eventType, details),
})
}
// genericBannerHandler returns a handler that writes a banner then reads data
func (a *App) genericBannerHandler(banner string) func(net.Conn) {
return func(c net.Conn) {
defer c.Close()
remote := c.RemoteAddr().String()
log.Printf("conn to %s from %s", banner, remote)
_, _ = c.Write([]byte(banner + "\r\n"))
c.SetDeadline(time.Now().Add(10 * time.Second))
buf := make([]byte, 4096)
n, _ := c.Read(buf)
payload := strings.TrimSpace(string(buf[:n]))
a.logEvent(Record{Timestamp: time.Now().UTC(), RemoteAddr: remoteIP(remote), RemotePort: remotePort(remote), Service: banner, Details: map[string]string{"read_bytes": strconv.Itoa(n)}, RawPayload: payload})
}
}
func (a *App) sipHandler(c net.Conn) {
defer c.Close()
remote := c.RemoteAddr().String()
log.Printf("sip conn from %s", remote)
c.SetDeadline(time.Now().Add(8 * time.Second))
r := bufio.NewReader(c)
line, _ := r.ReadString('\n')
if line == "" {
return
}
a.logEvent(Record{Timestamp: time.Now().UTC(), RemoteAddr: remoteIP(remote), RemotePort: remotePort(remote), Service: "sip", Details: map[string]string{"first_line": strings.TrimSpace(line)}, RawPayload: line})
}
func (a *App) genericEchoHandler(c net.Conn) {
defer c.Close()
remote := c.RemoteAddr().String()
log.Printf("generic conn from %s", remote)
c.SetDeadline(time.Now().Add(10 * time.Second))
_, _ = c.Write([]byte("220 Welcome to service\r\n"))
r := bufio.NewReader(c)
var b strings.Builder
for i := 0; i < 10; i++ {
line, err := r.ReadString('\n')
if err != nil {
break
}
b.WriteString(line)
_, _ = c.Write([]byte("ACK\r\n"))
}
a.logEvent(Record{Timestamp: time.Now().UTC(), RemoteAddr: remoteIP(remote), RemotePort: remotePort(remote), Service: "generic", Details: map[string]string{"lines": strconv.Itoa(strings.Count(b.String(), "\n"))}, RawPayload: b.String()})
}