2025-09-28 06:48:03 +01:00
|
|
|
package app
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bufio"
|
2025-09-28 08:56:11 +01:00
|
|
|
"context"
|
2025-09-28 06:48:03 +01:00
|
|
|
"crypto/rand"
|
|
|
|
|
"crypto/rsa"
|
2025-09-28 08:06:05 +01:00
|
|
|
"crypto/tls"
|
|
|
|
|
"crypto/x509"
|
|
|
|
|
"crypto/x509/pkix"
|
2025-09-28 08:56:11 +01:00
|
|
|
"encoding/pem"
|
2025-09-28 06:48:03 +01:00
|
|
|
"fmt"
|
|
|
|
|
"io"
|
|
|
|
|
"log"
|
2025-09-28 08:06:05 +01:00
|
|
|
"math/big"
|
2025-09-28 06:48:03 +01:00
|
|
|
"net"
|
|
|
|
|
"net/http"
|
2025-09-28 08:06:05 +01:00
|
|
|
"os"
|
2025-09-28 07:18:53 +01:00
|
|
|
"path/filepath"
|
2025-09-28 06:48:03 +01:00
|
|
|
"strconv"
|
|
|
|
|
"strings"
|
|
|
|
|
"sync"
|
|
|
|
|
"time"
|
2025-09-28 08:56:11 +01:00
|
|
|
|
2025-09-28 15:28:39 +01:00
|
|
|
"honeydany/app/dashboard"
|
|
|
|
|
svcs "honeydany/app/services"
|
2025-09-28 21:28:39 +01:00
|
|
|
|
|
|
|
|
"golang.org/x/crypto/ssh"
|
2025-09-28 06:48:03 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// App holds runtime pieces
|
|
|
|
|
type App struct {
|
2025-09-28 21:28:39 +01:00
|
|
|
cfg Config
|
|
|
|
|
logger *Logger
|
|
|
|
|
threatIntel *ThreatIntel
|
|
|
|
|
threatManager *dashboard.ThreatManager
|
|
|
|
|
webTemplateManager *WebTemplateManager
|
|
|
|
|
webServiceManager *WebServiceManager
|
|
|
|
|
ctx context.Context
|
|
|
|
|
cancel context.CancelFunc
|
|
|
|
|
wg sync.WaitGroup
|
2025-09-28 08:56:11 +01:00
|
|
|
// keep references to servers for graceful shutdown
|
|
|
|
|
httpSrvs []*http.Server
|
|
|
|
|
sshSigner ssh.Signer
|
|
|
|
|
// track TCP listeners for graceful shutdown
|
|
|
|
|
mu sync.Mutex
|
|
|
|
|
listeners []net.Listener
|
|
|
|
|
conns map[net.Conn]struct{}
|
2025-09-28 14:47:22 +01:00
|
|
|
// restart channel
|
|
|
|
|
restartCh chan struct{}
|
2025-09-28 07:18:53 +01:00
|
|
|
}
|
|
|
|
|
|
2025-09-28 08:06:05 +01:00
|
|
|
// HTTPS honeypot using self-signed or configured certificate
|
|
|
|
|
func (a *App) startHTTPS(port int) {
|
2025-09-28 08:56:11 +01:00
|
|
|
addr := fmt.Sprintf(":%d", port)
|
|
|
|
|
mux := http.NewServeMux()
|
|
|
|
|
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
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 sni := r.TLS; sni != nil && sni.ServerName != "" {
|
|
|
|
|
details["sni_server_name"] = sni.ServerName
|
|
|
|
|
}
|
2025-09-28 21:28:39 +01:00
|
|
|
|
|
|
|
|
// Handle POST requests (login attempts)
|
|
|
|
|
if r.Method == http.MethodPost {
|
|
|
|
|
if err := r.ParseForm(); err == nil {
|
|
|
|
|
username := strings.TrimSpace(r.FormValue("username"))
|
|
|
|
|
password := r.FormValue("password")
|
|
|
|
|
if username != "" || password != "" {
|
|
|
|
|
details["login_attempt"] = "true"
|
|
|
|
|
details["username"] = username
|
|
|
|
|
details["password"] = password
|
|
|
|
|
bodySnippet = fmt.Sprintf("username=%s&password=%s", username, password)
|
|
|
|
|
|
|
|
|
|
// Log the login attempt
|
|
|
|
|
rec := Record{Timestamp: time.Now().UTC(), RemoteAddr: remoteIP(r.RemoteAddr), RemotePort: remotePort(r.RemoteAddr), Service: "https", Details: details, RawPayload: bodySnippet}
|
|
|
|
|
a.logEvent(rec)
|
|
|
|
|
|
|
|
|
|
// Redirect back with error (honeypot always rejects)
|
|
|
|
|
redirectURL := fmt.Sprintf("%s?error=invalid", r.URL.Path)
|
|
|
|
|
http.Redirect(w, r, redirectURL, http.StatusSeeOther)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-28 08:56:11 +01:00
|
|
|
rec := Record{Timestamp: time.Now().UTC(), RemoteAddr: remoteIP(r.RemoteAddr), RemotePort: remotePort(r.RemoteAddr), Service: "https", Details: details, RawPayload: bodySnippet}
|
|
|
|
|
a.logEvent(rec)
|
|
|
|
|
|
2025-09-28 21:28:39 +01:00
|
|
|
// Check if template is configured
|
|
|
|
|
if a.cfg.Web.HTTPSTemplateName != "" && a.webTemplateManager != nil {
|
|
|
|
|
tmpl, err := a.webTemplateManager.LoadTemplate(a.cfg.Web.HTTPSTemplateName)
|
|
|
|
|
if err == nil {
|
|
|
|
|
// Render template with context data compatible with admin-login.html
|
|
|
|
|
data := map[string]interface{}{
|
|
|
|
|
"ServiceName": "HTTPS Service",
|
|
|
|
|
"LoginPath": r.URL.Path,
|
|
|
|
|
"UseHTTPS": true,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
w.Header().Set("Server", "nginx/1.18.0 (Ubuntu)")
|
|
|
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
|
|
|
w.WriteHeader(200)
|
|
|
|
|
|
|
|
|
|
if err := tmpl.Execute(w, data); err != nil {
|
|
|
|
|
log.Printf("HTTPS template execution error: %v", err)
|
|
|
|
|
_, _ = w.Write([]byte("Welcome (TLS)\n"))
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
} else {
|
|
|
|
|
log.Printf("HTTPS template load error: %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Default response if no template or template failed
|
2025-09-28 08:56:11 +01:00
|
|
|
w.Header().Set("Server", "nginx/1.18.0 (Ubuntu)")
|
|
|
|
|
w.Header().Set("Content-Type", "text/plain")
|
|
|
|
|
w.WriteHeader(200)
|
|
|
|
|
_, _ = w.Write([]byte("Welcome (TLS)\n"))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Load or create certificate
|
|
|
|
|
cert, _, _, err := a.GetOrCreateTLSCertificate()
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Printf("HTTPS certificate error: %v", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
tlsCfg := &tls.Config{Certificates: []tls.Certificate{cert}}
|
|
|
|
|
|
|
|
|
|
srv := &http.Server{Addr: addr, Handler: mux, TLSConfig: tlsCfg}
|
|
|
|
|
a.addHTTPServer(srv)
|
|
|
|
|
log.Printf("HTTPS listening on %s", addr)
|
|
|
|
|
ln, err := net.Listen("tcp", addr)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Printf("https listen failed on %s: %v", addr, err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
// Wrap with TLS and serve
|
|
|
|
|
tlsLn := tls.NewListener(ln, tlsCfg)
|
|
|
|
|
if err := srv.Serve(tlsLn); err != nil && err != http.ErrServerClosed {
|
|
|
|
|
log.Printf("https server error: %v", err)
|
|
|
|
|
}
|
|
|
|
|
log.Printf("https stopped")
|
2025-09-28 08:06:05 +01:00
|
|
|
}
|
|
|
|
|
|
2025-09-28 07:18:53 +01:00
|
|
|
// addHTTPServer registers an HTTP server for graceful shutdown
|
|
|
|
|
func (a *App) addHTTPServer(s *http.Server) {
|
2025-09-28 08:56:11 +01:00
|
|
|
a.mu.Lock()
|
|
|
|
|
a.httpSrvs = append(a.httpSrvs, s)
|
|
|
|
|
a.mu.Unlock()
|
2025-09-28 06:48:03 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// addListener registers a listener for later shutdown
|
|
|
|
|
func (a *App) addListener(l net.Listener) {
|
2025-09-28 08:56:11 +01:00
|
|
|
a.mu.Lock()
|
|
|
|
|
a.listeners = append(a.listeners, l)
|
|
|
|
|
a.mu.Unlock()
|
2025-09-28 06:48:03 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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) {
|
2025-09-28 08:56:11 +01:00
|
|
|
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
|
2025-09-28 06:48:03 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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 {
|
2025-09-28 08:56:11 +01:00
|
|
|
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]
|
2025-09-28 06:48:03 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func trimQuotes(s string) string {
|
2025-09-28 08:56:11 +01:00
|
|
|
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
|
2025-09-28 06:48:03 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func NewApp(cfg Config) (*App, error) {
|
2025-09-28 08:56:11 +01:00
|
|
|
l, err := NewLogger(cfg)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
// Initialize threat intelligence system
|
|
|
|
|
threatIntelPath := "threat_intel.json"
|
|
|
|
|
if cfg.LogPath != "" {
|
|
|
|
|
threatIntelPath = filepath.Join(filepath.Dir(cfg.LogPath), "threat_intel.json")
|
|
|
|
|
}
|
|
|
|
|
ti := NewThreatIntel(threatIntelPath, true)
|
|
|
|
|
|
2025-09-28 15:28:39 +01:00
|
|
|
// Initialize threat manager
|
|
|
|
|
dbPath := "app.db"
|
|
|
|
|
if cfg.LogPath != "" {
|
|
|
|
|
dbPath = filepath.Join(filepath.Dir(cfg.LogPath), "app.db")
|
|
|
|
|
}
|
|
|
|
|
tm, err := dashboard.NewThreatManager(dbPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Printf("Failed to initialize threat manager: %v", err)
|
|
|
|
|
tm = nil // Continue without threat manager if it fails
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-28 21:28:39 +01:00
|
|
|
// Initialize web template manager
|
|
|
|
|
configPath := "config.json" // Default path, should be passed from main
|
|
|
|
|
if LastConfigPath != "" {
|
|
|
|
|
configPath = LastConfigPath
|
|
|
|
|
}
|
|
|
|
|
webTemplateManager := NewWebTemplateManager(configPath)
|
|
|
|
|
|
|
|
|
|
// Initialize web service manager
|
|
|
|
|
webServiceManager := NewWebServiceManager(webTemplateManager, func(record Record) {
|
|
|
|
|
// Use the same logging function as other services
|
|
|
|
|
if l != nil {
|
|
|
|
|
_ = l.Log(record)
|
|
|
|
|
}
|
|
|
|
|
// Also process with threat intelligence
|
|
|
|
|
if ti != nil && record.RemoteAddr != "" && !IsPrivateIP(record.RemoteAddr) {
|
|
|
|
|
ti.RecordActivity(record)
|
|
|
|
|
}
|
|
|
|
|
// Process with threat manager
|
|
|
|
|
if tm != nil && record.RemoteAddr != "" && !IsPrivateIP(record.RemoteAddr) {
|
|
|
|
|
details := make(map[string]interface{})
|
|
|
|
|
for k, v := range record.Details {
|
|
|
|
|
details[k] = v
|
|
|
|
|
}
|
|
|
|
|
tm.ProcessHoneypotRecord(record.Timestamp, record.RemoteAddr, record.RemotePort, record.Service, details, record.RawPayload)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
2025-09-28 08:56:11 +01:00
|
|
|
// Root context for the App used for shutdown signalling
|
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
2025-09-28 21:28:39 +01:00
|
|
|
a := &App{
|
|
|
|
|
cfg: cfg,
|
|
|
|
|
logger: l,
|
|
|
|
|
threatIntel: ti,
|
|
|
|
|
threatManager: tm,
|
|
|
|
|
webTemplateManager: webTemplateManager,
|
|
|
|
|
webServiceManager: webServiceManager,
|
|
|
|
|
ctx: ctx,
|
|
|
|
|
cancel: cancel,
|
|
|
|
|
conns: make(map[net.Conn]struct{}),
|
|
|
|
|
restartCh: make(chan struct{}, 1),
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-28 15:28:39 +01:00
|
|
|
// Start periodic threat analysis tasks
|
|
|
|
|
if tm != nil {
|
2025-09-28 21:28:39 +01:00
|
|
|
tm.RunPeriodicTasks(a.ctx)
|
2025-09-28 15:28:39 +01:00
|
|
|
}
|
2025-09-28 21:28:39 +01:00
|
|
|
|
2025-09-28 08:56:11 +01:00
|
|
|
return a, nil
|
2025-09-28 06:48:03 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (a *App) Run(ctx context.Context) error {
|
2025-09-28 08:56:11 +01:00
|
|
|
// start services according to cfg
|
|
|
|
|
if a.cfg.Services.HTTP {
|
2025-09-28 06:48:03 +01:00
|
|
|
a.wg.Add(1)
|
|
|
|
|
go func() {
|
|
|
|
|
defer a.wg.Done()
|
2025-09-28 08:56:11 +01:00
|
|
|
a.startHTTP(a.cfg.Ports.HTTP)
|
2025-09-28 06:48:03 +01:00
|
|
|
}()
|
|
|
|
|
}
|
2025-09-28 08:56:11 +01:00
|
|
|
if a.cfg.Services.HTTPS {
|
2025-09-28 06:48:03 +01:00
|
|
|
a.wg.Add(1)
|
|
|
|
|
go func() {
|
|
|
|
|
defer a.wg.Done()
|
2025-09-28 08:56:11 +01:00
|
|
|
a.startHTTPS(a.cfg.Ports.HTTPS)
|
2025-09-28 06:48:03 +01:00
|
|
|
}()
|
|
|
|
|
}
|
2025-09-28 08:56:11 +01:00
|
|
|
if a.cfg.Services.SSH {
|
2025-09-28 06:48:03 +01:00
|
|
|
a.wg.Add(1)
|
|
|
|
|
go func() {
|
|
|
|
|
defer a.wg.Done()
|
2025-09-28 08:56:11 +01:00
|
|
|
handler := svcs.NewSSHHandler(a.svcLogger(), a.getSSHSigner)
|
|
|
|
|
a.startTCPService("ssh", a.cfg.Ports.SSH, handler)
|
|
|
|
|
}()
|
|
|
|
|
}
|
|
|
|
|
if a.cfg.Services.FTP {
|
|
|
|
|
a.wg.Add(1)
|
|
|
|
|
go func() {
|
|
|
|
|
defer a.wg.Done()
|
|
|
|
|
a.startTCPService("ftp", a.cfg.Ports.FTP, svcs.NewFTPHandler(a.svcLogger()))
|
|
|
|
|
}()
|
|
|
|
|
}
|
|
|
|
|
if a.cfg.Services.SMTP {
|
|
|
|
|
a.wg.Add(1)
|
|
|
|
|
go func() {
|
|
|
|
|
defer a.wg.Done()
|
|
|
|
|
a.startTCPService("smtp", a.cfg.Ports.SMTP, svcs.NewSMTPHandler(a.svcLogger()))
|
2025-09-28 06:48:03 +01:00
|
|
|
}()
|
|
|
|
|
}
|
|
|
|
|
if a.cfg.Services.IMAP {
|
|
|
|
|
a.wg.Add(1)
|
|
|
|
|
go func() {
|
|
|
|
|
defer a.wg.Done()
|
2025-09-28 08:06:05 +01:00
|
|
|
a.startTCPService("imap", a.cfg.Ports.IMAP, svcs.NewIMAPHandler(a.svcLogger()))
|
2025-09-28 06:48:03 +01:00
|
|
|
}()
|
|
|
|
|
}
|
|
|
|
|
if a.cfg.Services.Telnet {
|
|
|
|
|
a.wg.Add(1)
|
|
|
|
|
go func() {
|
|
|
|
|
defer a.wg.Done()
|
2025-09-28 08:06:05 +01:00
|
|
|
a.startTCPService("telnet", a.cfg.Ports.Telnet, svcs.NewTelnetHandler(a.svcLogger()))
|
2025-09-28 06:48:03 +01:00
|
|
|
}()
|
|
|
|
|
}
|
|
|
|
|
if a.cfg.Services.MySQL {
|
|
|
|
|
a.wg.Add(1)
|
|
|
|
|
go func() {
|
|
|
|
|
defer a.wg.Done()
|
2025-09-28 08:06:05 +01:00
|
|
|
a.startTCPService("mysql", a.cfg.Ports.MySQL, svcs.NewMySQLHandler(a.svcLogger()))
|
2025-09-28 06:48:03 +01:00
|
|
|
}()
|
|
|
|
|
}
|
|
|
|
|
if a.cfg.Services.PostgreSQL {
|
|
|
|
|
a.wg.Add(1)
|
|
|
|
|
go func() {
|
|
|
|
|
defer a.wg.Done()
|
2025-09-28 08:06:05 +01:00
|
|
|
a.startTCPService("postgresql", a.cfg.Ports.PostgreSQL, svcs.NewPostgreSQLHandler(a.svcLogger()))
|
2025-09-28 06:48:03 +01:00
|
|
|
}()
|
|
|
|
|
}
|
|
|
|
|
if a.cfg.Services.MongoDB {
|
|
|
|
|
a.wg.Add(1)
|
|
|
|
|
go func() {
|
|
|
|
|
defer a.wg.Done()
|
2025-09-28 08:06:05 +01:00
|
|
|
a.startTCPService("mongodb", a.cfg.Ports.MongoDB, svcs.NewMongoDBHandler(a.svcLogger()))
|
2025-09-28 06:48:03 +01:00
|
|
|
}()
|
|
|
|
|
}
|
|
|
|
|
if a.cfg.Services.RDP {
|
|
|
|
|
a.wg.Add(1)
|
|
|
|
|
go func() {
|
|
|
|
|
defer a.wg.Done()
|
2025-09-28 08:06:05 +01:00
|
|
|
a.startTCPService("rdp", a.cfg.Ports.RDP, svcs.NewRDPHandler(a.svcLogger()))
|
2025-09-28 06:48:03 +01:00
|
|
|
}()
|
|
|
|
|
}
|
|
|
|
|
if a.cfg.Services.SMB {
|
|
|
|
|
a.wg.Add(1)
|
|
|
|
|
go func() {
|
|
|
|
|
defer a.wg.Done()
|
2025-09-28 08:56:11 +01:00
|
|
|
a.startTCPService("smb", a.cfg.Ports.SMB, svcs.NewSMBHandler(a.svcLogger()))
|
2025-09-28 06:48:03 +01:00
|
|
|
}()
|
|
|
|
|
}
|
|
|
|
|
if a.cfg.Services.VNC {
|
|
|
|
|
a.wg.Add(1)
|
|
|
|
|
go func() {
|
|
|
|
|
defer a.wg.Done()
|
2025-09-28 08:56:11 +01:00
|
|
|
a.startTCPService("vnc", a.cfg.Ports.VNC, svcs.NewVNCHandler(a.svcLogger()))
|
2025-09-28 06:48:03 +01:00
|
|
|
}()
|
|
|
|
|
}
|
2025-09-28 21:28:39 +01:00
|
|
|
|
2025-09-28 06:48:03 +01:00
|
|
|
// start web dashboard if enabled
|
|
|
|
|
if a.cfg.Web.Enabled {
|
|
|
|
|
a.wg.Add(1)
|
|
|
|
|
go func() {
|
|
|
|
|
defer a.wg.Done()
|
|
|
|
|
a.startWeb()
|
|
|
|
|
}()
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-28 21:28:39 +01:00
|
|
|
// Start custom web services (only generic services)
|
|
|
|
|
for _, genericService := range a.cfg.WebServices.Generic {
|
|
|
|
|
if genericService.Enabled {
|
|
|
|
|
if err := a.webServiceManager.StartWebService(genericService); err != nil {
|
|
|
|
|
log.Printf("Failed to start web service %s: %v", genericService.Name, err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-28 06:48:03 +01:00
|
|
|
<-ctx.Done()
|
|
|
|
|
a.Shutdown()
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-28 14:47:22 +01:00
|
|
|
func (a *App) Restart() {
|
|
|
|
|
select {
|
|
|
|
|
case a.restartCh <- struct{}{}:
|
|
|
|
|
default:
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (a *App) RestartChan() <-chan struct{} {
|
|
|
|
|
return a.restartCh
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-28 06:48:03 +01:00
|
|
|
func (a *App) Shutdown() {
|
2025-09-28 08:56:11 +01:00
|
|
|
a.cancel()
|
|
|
|
|
// attempt to close all http servers if running
|
|
|
|
|
a.mu.Lock()
|
|
|
|
|
srvs := a.httpSrvs
|
|
|
|
|
a.httpSrvs = nil
|
|
|
|
|
a.mu.Unlock()
|
|
|
|
|
for _, s := range srvs {
|
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
|
|
|
_ = s.Shutdown(ctx)
|
|
|
|
|
cancel()
|
|
|
|
|
}
|
|
|
|
|
// close all TCP listeners to unblock Accept loops
|
|
|
|
|
a.closeAllListeners()
|
|
|
|
|
// close all tracked connections to unblock handlers
|
|
|
|
|
a.closeAllConns()
|
|
|
|
|
a.wg.Wait()
|
|
|
|
|
|
|
|
|
|
// Save threat intelligence data before shutdown
|
|
|
|
|
if a.threatIntel != nil {
|
|
|
|
|
_ = a.threatIntel.Save()
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-28 15:28:39 +01:00
|
|
|
// Close threat manager
|
|
|
|
|
if a.threatManager != nil {
|
|
|
|
|
_ = a.threatManager.Close()
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-28 08:56:11 +01:00
|
|
|
_ = a.logger.Close()
|
2025-09-28 06:48:03 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// closeAllListeners closes all tracked listeners to unblock Accept()
|
|
|
|
|
func (a *App) closeAllListeners() {
|
2025-09-28 08:56:11 +01:00
|
|
|
a.mu.Lock()
|
|
|
|
|
ls := a.listeners
|
|
|
|
|
a.listeners = nil
|
|
|
|
|
a.mu.Unlock()
|
|
|
|
|
for _, l := range ls {
|
|
|
|
|
_ = l.Close()
|
|
|
|
|
}
|
2025-09-28 06:48:03 +01:00
|
|
|
}
|
|
|
|
|
|
2025-09-28 07:18:53 +01:00
|
|
|
// addConn tracks an active connection for shutdown
|
|
|
|
|
func (a *App) addConn(c net.Conn) {
|
2025-09-28 08:56:11 +01:00
|
|
|
a.mu.Lock()
|
|
|
|
|
if a.conns == nil {
|
|
|
|
|
a.conns = make(map[net.Conn]struct{})
|
|
|
|
|
}
|
|
|
|
|
a.conns[c] = struct{}{}
|
|
|
|
|
a.mu.Unlock()
|
2025-09-28 07:18:53 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// removeConn removes a connection from tracking
|
|
|
|
|
func (a *App) removeConn(c net.Conn) {
|
2025-09-28 08:56:11 +01:00
|
|
|
a.mu.Lock()
|
|
|
|
|
delete(a.conns, c)
|
|
|
|
|
a.mu.Unlock()
|
2025-09-28 07:18:53 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// closeAllConns closes all tracked connections to unblock handlers
|
|
|
|
|
func (a *App) closeAllConns() {
|
2025-09-28 08:56:11 +01:00
|
|
|
a.mu.Lock()
|
|
|
|
|
conns := make([]net.Conn, 0, len(a.conns))
|
|
|
|
|
for c := range a.conns {
|
|
|
|
|
conns = append(conns, c)
|
|
|
|
|
}
|
|
|
|
|
a.conns = make(map[net.Conn]struct{})
|
|
|
|
|
a.mu.Unlock()
|
|
|
|
|
for _, c := range conns {
|
|
|
|
|
_ = c.Close()
|
|
|
|
|
}
|
2025-09-28 07:18:53 +01:00
|
|
|
}
|
|
|
|
|
|
2025-09-28 06:48:03 +01:00
|
|
|
// helpers for logging
|
|
|
|
|
func (a *App) logEvent(r Record) {
|
2025-09-28 08:56:11 +01:00
|
|
|
if a.logger != nil {
|
|
|
|
|
_ = a.logger.Log(r)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Record threat intelligence if IP is not private
|
|
|
|
|
if a.threatIntel != nil && r.RemoteAddr != "" && !IsPrivateIP(r.RemoteAddr) {
|
|
|
|
|
a.threatIntel.RecordActivity(r)
|
|
|
|
|
}
|
2025-09-28 15:28:39 +01:00
|
|
|
|
|
|
|
|
// Process with threat manager for advanced analysis
|
|
|
|
|
if a.threatManager != nil && r.RemoteAddr != "" && !IsPrivateIP(r.RemoteAddr) {
|
|
|
|
|
// Convert map[string]string to map[string]interface{}
|
|
|
|
|
details := make(map[string]interface{})
|
|
|
|
|
for k, v := range r.Details {
|
|
|
|
|
details[k] = v
|
|
|
|
|
}
|
|
|
|
|
a.threatManager.ProcessHoneypotRecord(r.Timestamp, r.RemoteAddr, r.RemotePort, r.Service, details, r.RawPayload)
|
|
|
|
|
}
|
2025-09-28 06:48:03 +01:00
|
|
|
}
|
|
|
|
|
|
2025-09-28 08:06:05 +01:00
|
|
|
// svcLogger adapts services.Record to the app Record and logs it
|
|
|
|
|
func (a *App) svcLogger() func(svcs.Record) {
|
2025-09-28 08:56:11 +01:00
|
|
|
return func(sr svcs.Record) {
|
|
|
|
|
a.logEvent(Record{
|
|
|
|
|
Timestamp: sr.Timestamp,
|
|
|
|
|
RemoteAddr: sr.RemoteAddr,
|
|
|
|
|
RemotePort: sr.RemotePort,
|
|
|
|
|
Service: sr.Service,
|
|
|
|
|
Details: sr.Details,
|
|
|
|
|
RawPayload: sr.RawPayload,
|
|
|
|
|
})
|
|
|
|
|
}
|
2025-09-28 08:06:05 +01:00
|
|
|
}
|
|
|
|
|
|
2025-09-28 06:48:03 +01:00
|
|
|
// HTTP honeypot
|
|
|
|
|
func (a *App) startHTTP(port int) {
|
2025-09-28 08:56:11 +01:00
|
|
|
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
|
|
|
|
|
}
|
2025-09-28 21:28:39 +01:00
|
|
|
|
|
|
|
|
// Handle POST requests (login attempts)
|
|
|
|
|
if r.Method == http.MethodPost {
|
|
|
|
|
if err := r.ParseForm(); err == nil {
|
|
|
|
|
username := strings.TrimSpace(r.FormValue("username"))
|
|
|
|
|
password := r.FormValue("password")
|
|
|
|
|
if username != "" || password != "" {
|
|
|
|
|
details["login_attempt"] = "true"
|
|
|
|
|
details["username"] = username
|
|
|
|
|
details["password"] = password
|
|
|
|
|
bodySnippet = fmt.Sprintf("username=%s&password=%s", username, password)
|
|
|
|
|
|
|
|
|
|
// Log the login attempt
|
|
|
|
|
rec := Record{Timestamp: time.Now().UTC(), RemoteAddr: remoteIP(r.RemoteAddr), RemotePort: remotePort(r.RemoteAddr), Service: "http", Details: details, RawPayload: bodySnippet}
|
|
|
|
|
a.logEvent(rec)
|
|
|
|
|
|
|
|
|
|
// Redirect back with error (honeypot always rejects)
|
|
|
|
|
redirectURL := fmt.Sprintf("%s?error=invalid", r.URL.Path)
|
|
|
|
|
http.Redirect(w, r, redirectURL, http.StatusSeeOther)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-28 08:56:11 +01:00
|
|
|
rec := Record{Timestamp: time.Now().UTC(), RemoteAddr: remoteIP(r.RemoteAddr), RemotePort: remotePort(r.RemoteAddr), Service: "http", Details: details, RawPayload: bodySnippet}
|
|
|
|
|
a.logEvent(rec)
|
|
|
|
|
|
2025-09-28 21:28:39 +01:00
|
|
|
// Check if template is configured
|
|
|
|
|
if a.cfg.Web.HTTPTemplateName != "" && a.webTemplateManager != nil {
|
|
|
|
|
tmpl, err := a.webTemplateManager.LoadTemplate(a.cfg.Web.HTTPTemplateName)
|
|
|
|
|
if err == nil {
|
|
|
|
|
// Render template with context data compatible with admin-login.html
|
|
|
|
|
data := map[string]interface{}{
|
|
|
|
|
"ServiceName": "HTTP Service",
|
|
|
|
|
"LoginPath": r.URL.Path,
|
|
|
|
|
"UseHTTPS": false,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
w.Header().Set("Server", "Apache/2.4.41 (Ubuntu)")
|
|
|
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
|
|
|
w.WriteHeader(200)
|
|
|
|
|
|
|
|
|
|
if err := tmpl.Execute(w, data); err != nil {
|
|
|
|
|
log.Printf("HTTP template execution error: %v", err)
|
|
|
|
|
_, _ = w.Write([]byte("Welcome\n"))
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
} else {
|
|
|
|
|
log.Printf("HTTP template load error: %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Default response if no template or template failed
|
2025-09-28 08:56:11 +01:00
|
|
|
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.addHTTPServer(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")
|
2025-09-28 06:48:03 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// remote helpers
|
|
|
|
|
func remoteIP(addr string) string {
|
2025-09-28 08:56:11 +01:00
|
|
|
h, _, _ := net.SplitHostPort(addr)
|
|
|
|
|
return h
|
2025-09-28 06:48:03 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func remotePort(addr string) string {
|
2025-09-28 08:56:11 +01:00
|
|
|
_, p, _ := net.SplitHostPort(addr)
|
|
|
|
|
return p
|
2025-09-28 06:48:03 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Generic TCP listener starter
|
|
|
|
|
func (a *App) startTCPService(name string, port int, handler func(net.Conn)) {
|
2025-09-28 08:56:11 +01:00
|
|
|
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) {
|
|
|
|
|
// track and ensure cleanup
|
|
|
|
|
a.addConn(c)
|
|
|
|
|
defer func() {
|
|
|
|
|
a.removeConn(c)
|
|
|
|
|
_ = 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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-09-28 06:48:03 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// sshHandler implements a real SSH server handshake and logs password auth attempts
|
|
|
|
|
func (a *App) sshHandler(conn net.Conn) {
|
2025-09-28 08:56:11 +01:00
|
|
|
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()
|
2025-09-28 06:48:03 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// getSSHSigner returns or generates an RSA host key signer
|
|
|
|
|
func (a *App) getSSHSigner() (ssh.Signer, error) {
|
2025-09-28 08:56:11 +01:00
|
|
|
if a.sshSigner != nil {
|
|
|
|
|
return a.sshSigner, nil
|
|
|
|
|
}
|
|
|
|
|
// Determine host key path: config override or alongside LogPath
|
|
|
|
|
var hostKeyPath string
|
|
|
|
|
if a.cfg.Certificates.SSHHostKeyPath != "" {
|
|
|
|
|
hostKeyPath = a.cfg.Certificates.SSHHostKeyPath
|
|
|
|
|
} else {
|
|
|
|
|
baseDir := a.dataDir()
|
|
|
|
|
hostKeyPath = filepath.Join(baseDir, "ssh_host_key.pem")
|
|
|
|
|
}
|
|
|
|
|
signer, err := a.loadOrCreateSSHHostKey(hostKeyPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
a.sshSigner = signer
|
|
|
|
|
return signer, nil
|
2025-09-28 06:48:03 +01:00
|
|
|
}
|
|
|
|
|
|
2025-09-28 08:06:05 +01:00
|
|
|
// dataDir returns the directory where persistent data should live (same dir as LogPath)
|
|
|
|
|
func (a *App) dataDir() string {
|
2025-09-28 08:56:11 +01:00
|
|
|
if a.cfg.LogPath == "" {
|
|
|
|
|
return "."
|
|
|
|
|
}
|
|
|
|
|
return filepath.Dir(a.cfg.LogPath)
|
2025-09-28 08:06:05 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// loadOrCreateSSHHostKey loads an RSA private key from PEM or generates and saves one
|
|
|
|
|
func (a *App) loadOrCreateSSHHostKey(path string) (ssh.Signer, error) {
|
2025-09-28 08:56:11 +01:00
|
|
|
if b, err := os.ReadFile(path); err == nil {
|
|
|
|
|
// Try parse PEM private key
|
|
|
|
|
block, _ := pem.Decode(b)
|
|
|
|
|
if block != nil && strings.Contains(block.Type, "PRIVATE KEY") {
|
|
|
|
|
if key, err := x509.ParsePKCS1PrivateKey(block.Bytes); err == nil {
|
|
|
|
|
return ssh.NewSignerFromKey(key)
|
|
|
|
|
}
|
|
|
|
|
if k2, err := x509.ParsePKCS8PrivateKey(block.Bytes); err == nil {
|
|
|
|
|
return ssh.NewSignerFromKey(k2)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// If parse failed, fall through to generate new and overwrite
|
|
|
|
|
}
|
|
|
|
|
// Generate new RSA key and persist to PEM
|
|
|
|
|
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
pemBlock := &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}
|
|
|
|
|
if err := os.WriteFile(path, pem.EncodeToMemory(pemBlock), 0600); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return ssh.NewSignerFromKey(key)
|
2025-09-28 08:06:05 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetOrCreateTLSCertificate loads a TLS cert/key if provided or generates a self-signed pair
|
|
|
|
|
// in the data directory (next to LogPath) and returns the tls.Certificate and the file paths used.
|
|
|
|
|
func (a *App) GetOrCreateTLSCertificate() (tls.Certificate, string, string, error) {
|
2025-09-28 08:56:11 +01:00
|
|
|
// If custom cert/key provided, try load them
|
|
|
|
|
if a.cfg.Certificates.TLSCertPath != "" && a.cfg.Certificates.TLSKeyPath != "" {
|
|
|
|
|
cert, err := tls.LoadX509KeyPair(a.cfg.Certificates.TLSCertPath, a.cfg.Certificates.TLSKeyPath)
|
|
|
|
|
return cert, a.cfg.Certificates.TLSCertPath, a.cfg.Certificates.TLSKeyPath, err
|
|
|
|
|
}
|
|
|
|
|
// Else generate or reuse self-signed in data dir
|
|
|
|
|
dir := a.dataDir()
|
|
|
|
|
certPath := filepath.Join(dir, "tls_cert.pem")
|
|
|
|
|
keyPath := filepath.Join(dir, "tls_key.pem")
|
|
|
|
|
if _, err := os.Stat(certPath); err == nil {
|
|
|
|
|
if _, err2 := os.Stat(keyPath); err2 == nil {
|
|
|
|
|
cert, err3 := tls.LoadX509KeyPair(certPath, keyPath)
|
|
|
|
|
return cert, certPath, keyPath, err3
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// create self-signed cert
|
|
|
|
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
|
|
|
|
return tls.Certificate{}, "", "", err
|
|
|
|
|
}
|
|
|
|
|
priv, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return tls.Certificate{}, "", "", err
|
|
|
|
|
}
|
|
|
|
|
serial, _ := rand.Int(rand.Reader, big.NewInt(1<<62))
|
|
|
|
|
tmpl := x509.Certificate{
|
|
|
|
|
SerialNumber: serial,
|
|
|
|
|
Subject: pkix.Name{CommonName: "honeypot.local"},
|
|
|
|
|
NotBefore: time.Now().Add(-time.Hour),
|
|
|
|
|
NotAfter: time.Now().AddDate(5, 0, 0),
|
|
|
|
|
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
|
|
|
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
|
|
|
|
BasicConstraintsValid: true,
|
|
|
|
|
}
|
|
|
|
|
der, err := x509.CreateCertificate(rand.Reader, &tmpl, &tmpl, &priv.PublicKey, priv)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return tls.Certificate{}, "", "", err
|
|
|
|
|
}
|
|
|
|
|
certOut := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})
|
|
|
|
|
keyOut := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)})
|
|
|
|
|
if err := os.WriteFile(certPath, certOut, 0600); err != nil {
|
|
|
|
|
return tls.Certificate{}, "", "", err
|
|
|
|
|
}
|
|
|
|
|
if err := os.WriteFile(keyPath, keyOut, 0600); err != nil {
|
|
|
|
|
return tls.Certificate{}, "", "", err
|
|
|
|
|
}
|
|
|
|
|
cert, err := tls.X509KeyPair(certOut, keyOut)
|
|
|
|
|
return cert, certPath, keyPath, err
|
2025-09-28 08:06:05 +01:00
|
|
|
}
|
|
|
|
|
|
2025-09-28 06:48:03 +01:00
|
|
|
// logSSHEvent is a helper to log SSH-specific events with consistent metadata
|
|
|
|
|
func (a *App) logSSHEvent(sessionID, remote, eventType string, details map[string]string) {
|
2025-09-28 08:56:11 +01:00
|
|
|
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),
|
|
|
|
|
})
|
2025-09-28 06:48:03 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// genericBannerHandler returns a handler that writes a banner then reads data
|
|
|
|
|
func (a *App) genericBannerHandler(banner string) func(net.Conn) {
|
2025-09-28 08:56:11 +01:00
|
|
|
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})
|
|
|
|
|
}
|
2025-09-28 06:48:03 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (a *App) sipHandler(c net.Conn) {
|
2025-09-28 08:56:11 +01:00
|
|
|
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})
|
2025-09-28 06:48:03 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (a *App) genericEchoHandler(c net.Conn) {
|
2025-09-28 08:56:11 +01:00
|
|
|
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()})
|
2025-09-28 06:48:03 +01:00
|
|
|
}
|