first commit
This commit is contained in:
+143
@@ -0,0 +1,143 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config contains runtime configuration for the honeypot
|
||||||
|
type Config struct {
|
||||||
|
LogMode string `json:"log_mode"` // "file" | "stdout" | "sqlite"
|
||||||
|
LogPath string `json:"log_path"`
|
||||||
|
|
||||||
|
Web struct {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
Bind string `json:"bind"`
|
||||||
|
Port int `json:"port"`
|
||||||
|
} `json:"web"`
|
||||||
|
|
||||||
|
Services struct {
|
||||||
|
HTTP bool `json:"http"`
|
||||||
|
SSH bool `json:"ssh"`
|
||||||
|
FTP bool `json:"ftp"`
|
||||||
|
SMTP bool `json:"smtp"`
|
||||||
|
POP3 bool `json:"pop3"`
|
||||||
|
IMAP bool `json:"imap"`
|
||||||
|
Telnet bool `json:"telnet"`
|
||||||
|
MySQL bool `json:"mysql"`
|
||||||
|
PostgreSQL bool `json:"postgresql"`
|
||||||
|
Redis bool `json:"redis"`
|
||||||
|
MongoDB bool `json:"mongodb"`
|
||||||
|
RDP bool `json:"rdp"`
|
||||||
|
SMB bool `json:"smb"`
|
||||||
|
SIP bool `json:"sip"`
|
||||||
|
VNC bool `json:"vnc"`
|
||||||
|
DNS bool `json:"dns"`
|
||||||
|
SNMP bool `json:"snmp"`
|
||||||
|
LDAP bool `json:"ldap"`
|
||||||
|
Generic []int `json:"generic"`
|
||||||
|
} `json:"services"`
|
||||||
|
|
||||||
|
Ports struct {
|
||||||
|
HTTP int `json:"http"`
|
||||||
|
SSH int `json:"ssh"`
|
||||||
|
FTP int `json:"ftp"`
|
||||||
|
SMTP int `json:"smtp"`
|
||||||
|
POP3 int `json:"pop3"`
|
||||||
|
IMAP int `json:"imap"`
|
||||||
|
Telnet int `json:"telnet"`
|
||||||
|
MySQL int `json:"mysql"`
|
||||||
|
PostgreSQL int `json:"postgresql"`
|
||||||
|
Redis int `json:"redis"`
|
||||||
|
MongoDB int `json:"mongodb"`
|
||||||
|
RDP int `json:"rdp"`
|
||||||
|
SMB int `json:"smb"`
|
||||||
|
SIP int `json:"sip"`
|
||||||
|
VNC int `json:"vnc"`
|
||||||
|
DNS int `json:"dns"`
|
||||||
|
SNMP int `json:"snmp"`
|
||||||
|
LDAP int `json:"ldap"`
|
||||||
|
} `json:"ports"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnsureConfig writes a default config file if the given path doesn't exist
|
||||||
|
func EnsureConfig(path string) error {
|
||||||
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||||
|
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
||||||
|
return fmt.Errorf("create config dir: %w", err)
|
||||||
|
}
|
||||||
|
def := defaultConfig()
|
||||||
|
b, _ := json.MarshalIndent(def, "", " ")
|
||||||
|
if err := os.WriteFile(path, b, 0644); err != nil {
|
||||||
|
return fmt.Errorf("write default config: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadConfig loads JSON config from path
|
||||||
|
func LoadConfig(path string) (Config, error) {
|
||||||
|
var cfg Config
|
||||||
|
b, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return cfg, err
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(b, &cfg); err != nil {
|
||||||
|
return cfg, err
|
||||||
|
}
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultConfig() Config {
|
||||||
|
var c Config
|
||||||
|
c.LogMode = "file"
|
||||||
|
c.LogPath = "honeypot.log"
|
||||||
|
c.Web.Enabled = true
|
||||||
|
c.Web.Bind = "127.0.0.1"
|
||||||
|
c.Web.Port = 6333
|
||||||
|
|
||||||
|
// Enable common services by default
|
||||||
|
c.Services.HTTP = true
|
||||||
|
c.Services.SSH = true
|
||||||
|
c.Services.FTP = true
|
||||||
|
c.Services.SMTP = true
|
||||||
|
c.Services.Telnet = true
|
||||||
|
c.Services.MySQL = false
|
||||||
|
c.Services.PostgreSQL = false
|
||||||
|
c.Services.Redis = false
|
||||||
|
c.Services.MongoDB = false
|
||||||
|
c.Services.POP3 = false
|
||||||
|
c.Services.IMAP = false
|
||||||
|
c.Services.RDP = false
|
||||||
|
c.Services.SMB = false
|
||||||
|
c.Services.SIP = false
|
||||||
|
c.Services.VNC = false
|
||||||
|
c.Services.DNS = false
|
||||||
|
c.Services.SNMP = false
|
||||||
|
c.Services.LDAP = false
|
||||||
|
c.Services.Generic = []int{}
|
||||||
|
|
||||||
|
// Standard ports
|
||||||
|
c.Ports.HTTP = 8080
|
||||||
|
c.Ports.SSH = 2222
|
||||||
|
c.Ports.FTP = 2121
|
||||||
|
c.Ports.SMTP = 2525
|
||||||
|
c.Ports.POP3 = 1110
|
||||||
|
c.Ports.IMAP = 1143
|
||||||
|
c.Ports.Telnet = 2323
|
||||||
|
c.Ports.MySQL = 3306
|
||||||
|
c.Ports.PostgreSQL = 5432
|
||||||
|
c.Ports.Redis = 6379
|
||||||
|
c.Ports.MongoDB = 27017
|
||||||
|
c.Ports.RDP = 3389
|
||||||
|
c.Ports.SMB = 4450
|
||||||
|
c.Ports.SIP = 5060
|
||||||
|
c.Ports.VNC = 5900
|
||||||
|
c.Ports.DNS = 5353
|
||||||
|
c.Ports.SNMP = 1161
|
||||||
|
c.Ports.LDAP = 3890
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
+123
@@ -0,0 +1,123 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
_ "github.com/mattn/go-sqlite3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Record represents a captured event
|
||||||
|
type Record struct {
|
||||||
|
Timestamp time.Time `json:"timestamp"`
|
||||||
|
RemoteAddr string `json:"remote_addr"`
|
||||||
|
RemotePort string `json:"remote_port"`
|
||||||
|
Service string `json:"service"`
|
||||||
|
Details map[string]string `json:"details,omitempty"`
|
||||||
|
RawPayload string `json:"raw_payload,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logger handles output to file / stdout / sqlite
|
||||||
|
type Logger struct {
|
||||||
|
mode string
|
||||||
|
mu sync.Mutex
|
||||||
|
f *os.File
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLogger creates and initializes a Logger
|
||||||
|
func NewLogger(cfg Config) (*Logger, error) {
|
||||||
|
l := &Logger{mode: cfg.LogMode}
|
||||||
|
switch cfg.LogMode {
|
||||||
|
case "stdout":
|
||||||
|
// nothing to open
|
||||||
|
case "sqlite":
|
||||||
|
if err := os.MkdirAll(filepath.Dir(cfg.LogPath), 0755); err != nil {
|
||||||
|
return nil, fmt.Errorf("create db dir: %w", err)
|
||||||
|
}
|
||||||
|
db, err := sql.Open("sqlite3", cfg.LogPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("open sqlite: %w", err)
|
||||||
|
}
|
||||||
|
l.db = db
|
||||||
|
if err := l.ensureSQLiteSchema(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
default: // file
|
||||||
|
if err := os.MkdirAll(filepath.Dir(cfg.LogPath), 0700); err != nil {
|
||||||
|
return nil, fmt.Errorf("create log dir: %w", err)
|
||||||
|
}
|
||||||
|
f, err := os.OpenFile(cfg.LogPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("open log file: %w", err)
|
||||||
|
}
|
||||||
|
l.f = f
|
||||||
|
}
|
||||||
|
return l, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) ensureSQLiteSchema() error {
|
||||||
|
if l.db == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
q := `CREATE TABLE IF NOT EXISTS logs (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
timestamp TEXT,
|
||||||
|
remote_addr TEXT,
|
||||||
|
remote_port TEXT,
|
||||||
|
service TEXT,
|
||||||
|
details TEXT,
|
||||||
|
raw_payload TEXT
|
||||||
|
)`
|
||||||
|
_, err := l.db.Exec(q)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes any underlying resources
|
||||||
|
func (l *Logger) Close() error {
|
||||||
|
l.mu.Lock()
|
||||||
|
defer l.mu.Unlock()
|
||||||
|
if l.f != nil {
|
||||||
|
_ = l.f.Sync()
|
||||||
|
if err := l.f.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if l.db != nil {
|
||||||
|
return l.db.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log writes a Record according to the configured backend
|
||||||
|
func (l *Logger) Log(r Record) error {
|
||||||
|
l.mu.Lock()
|
||||||
|
defer l.mu.Unlock()
|
||||||
|
switch l.mode {
|
||||||
|
case "stdout":
|
||||||
|
b, _ := json.Marshal(r)
|
||||||
|
fmt.Println(string(b))
|
||||||
|
return nil
|
||||||
|
case "sqlite":
|
||||||
|
if l.db == nil {
|
||||||
|
return fmt.Errorf("sqlite DB not open")
|
||||||
|
}
|
||||||
|
detailsB, _ := json.Marshal(r.Details)
|
||||||
|
_, err := l.db.Exec(`INSERT INTO logs (timestamp, remote_addr, remote_port, service, details, raw_payload) VALUES (?, ?, ?, ?, ?, ?)`, r.Timestamp.UTC().Format(time.RFC3339Nano), r.RemoteAddr, r.RemotePort, r.Service, string(detailsB), r.RawPayload)
|
||||||
|
return err
|
||||||
|
default: // file
|
||||||
|
if l.f == nil {
|
||||||
|
return fmt.Errorf("file logger not initialized")
|
||||||
|
}
|
||||||
|
enc := json.NewEncoder(l.f)
|
||||||
|
if err := enc.Encode(r); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return l.f.Sync()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,619 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FTP Handler - implements basic FTP protocol with authentication logging
|
||||||
|
func (a *App) ftpHandler(conn net.Conn) {
|
||||||
|
defer conn.Close()
|
||||||
|
remote := conn.RemoteAddr().String()
|
||||||
|
sessionID := fmt.Sprintf("ftp_%x", time.Now().UnixNano())
|
||||||
|
|
||||||
|
a.logServiceEvent(sessionID, remote, "ftp", "connection_start", nil)
|
||||||
|
|
||||||
|
// Send FTP welcome banner
|
||||||
|
_, _ = conn.Write([]byte("220 Welcome to FTP Server\r\n"))
|
||||||
|
|
||||||
|
conn.SetDeadline(time.Now().Add(5 * time.Minute))
|
||||||
|
scanner := bufio.NewScanner(conn)
|
||||||
|
|
||||||
|
var username, password string
|
||||||
|
authenticated := false
|
||||||
|
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := strings.TrimSpace(scanner.Text())
|
||||||
|
if line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.SplitN(line, " ", 2)
|
||||||
|
if len(parts) < 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := strings.ToUpper(parts[0])
|
||||||
|
arg := ""
|
||||||
|
if len(parts) > 1 {
|
||||||
|
arg = parts[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
switch cmd {
|
||||||
|
case "USER":
|
||||||
|
username = arg
|
||||||
|
a.logServiceEvent(sessionID, remote, "ftp", "username_attempt", map[string]string{"username": username})
|
||||||
|
_, _ = conn.Write([]byte("331 Password required for " + username + "\r\n"))
|
||||||
|
case "PASS":
|
||||||
|
password = arg
|
||||||
|
a.logServiceEvent(sessionID, remote, "ftp", "password_attempt", map[string]string{
|
||||||
|
"username": username,
|
||||||
|
"password": password,
|
||||||
|
})
|
||||||
|
_, _ = conn.Write([]byte("530 Login incorrect\r\n"))
|
||||||
|
case "QUIT":
|
||||||
|
_, _ = conn.Write([]byte("221 Goodbye\r\n"))
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
if !authenticated {
|
||||||
|
_, _ = conn.Write([]byte("530 Please login with USER and PASS\r\n"))
|
||||||
|
} else {
|
||||||
|
_, _ = conn.Write([]byte("502 Command not implemented\r\n"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SMTP Handler - implements basic SMTP protocol
|
||||||
|
func (a *App) smtpHandler(conn net.Conn) {
|
||||||
|
defer conn.Close()
|
||||||
|
remote := conn.RemoteAddr().String()
|
||||||
|
sessionID := fmt.Sprintf("smtp_%x", time.Now().UnixNano())
|
||||||
|
|
||||||
|
a.logServiceEvent(sessionID, remote, "smtp", "connection_start", nil)
|
||||||
|
|
||||||
|
// Send SMTP welcome banner
|
||||||
|
_, _ = conn.Write([]byte("220 mail.example.com ESMTP Postfix\r\n"))
|
||||||
|
|
||||||
|
conn.SetDeadline(time.Now().Add(5 * time.Minute))
|
||||||
|
scanner := bufio.NewScanner(conn)
|
||||||
|
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := strings.TrimSpace(scanner.Text())
|
||||||
|
if line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.SplitN(line, " ", 2)
|
||||||
|
if len(parts) < 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := strings.ToUpper(parts[0])
|
||||||
|
arg := ""
|
||||||
|
if len(parts) > 1 {
|
||||||
|
arg = parts[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
switch cmd {
|
||||||
|
case "HELO", "EHLO":
|
||||||
|
a.logServiceEvent(sessionID, remote, "smtp", "helo", map[string]string{"hostname": arg})
|
||||||
|
if cmd == "EHLO" {
|
||||||
|
_, _ = conn.Write([]byte("250-mail.example.com\r\n250-AUTH PLAIN LOGIN\r\n250 OK\r\n"))
|
||||||
|
} else {
|
||||||
|
_, _ = conn.Write([]byte("250 mail.example.com\r\n"))
|
||||||
|
}
|
||||||
|
case "AUTH":
|
||||||
|
a.handleSMTPAuth(conn, sessionID, remote, arg)
|
||||||
|
case "MAIL":
|
||||||
|
_, _ = conn.Write([]byte("250 OK\r\n"))
|
||||||
|
case "RCPT":
|
||||||
|
_, _ = conn.Write([]byte("250 OK\r\n"))
|
||||||
|
case "DATA":
|
||||||
|
_, _ = conn.Write([]byte("354 End data with <CR><LF>.<CR><LF>\r\n"))
|
||||||
|
case "QUIT":
|
||||||
|
_, _ = conn.Write([]byte("221 Bye\r\n"))
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
_, _ = conn.Write([]byte("502 Command not implemented\r\n"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) handleSMTPAuth(conn net.Conn, sessionID, remote, authLine string) {
|
||||||
|
parts := strings.Fields(authLine)
|
||||||
|
if len(parts) < 1 {
|
||||||
|
_, _ = conn.Write([]byte("501 Syntax error\r\n"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
method := strings.ToUpper(parts[0])
|
||||||
|
switch method {
|
||||||
|
case "PLAIN":
|
||||||
|
if len(parts) > 1 {
|
||||||
|
// Decode base64 auth
|
||||||
|
decoded, err := base64.StdEncoding.DecodeString(parts[1])
|
||||||
|
if err == nil {
|
||||||
|
authParts := strings.Split(string(decoded), "\x00")
|
||||||
|
if len(authParts) >= 3 {
|
||||||
|
username := authParts[1]
|
||||||
|
password := authParts[2]
|
||||||
|
a.logServiceEvent(sessionID, remote, "smtp", "auth_attempt", map[string]string{
|
||||||
|
"method": "PLAIN",
|
||||||
|
"username": username,
|
||||||
|
"password": password,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_, _ = conn.Write([]byte("334 \r\n"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, _ = conn.Write([]byte("535 Authentication failed\r\n"))
|
||||||
|
case "LOGIN":
|
||||||
|
_, _ = conn.Write([]byte("334 VXNlcm5hbWU6\r\n")) // "Username:" in base64
|
||||||
|
default:
|
||||||
|
_, _ = conn.Write([]byte("504 Authentication method not supported\r\n"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Telnet Handler
|
||||||
|
func (a *App) telnetHandler(conn net.Conn) {
|
||||||
|
defer conn.Close()
|
||||||
|
remote := conn.RemoteAddr().String()
|
||||||
|
sessionID := fmt.Sprintf("telnet_%x", time.Now().UnixNano())
|
||||||
|
|
||||||
|
a.logServiceEvent(sessionID, remote, "telnet", "connection_start", nil)
|
||||||
|
|
||||||
|
// Send telnet login prompt
|
||||||
|
_, _ = conn.Write([]byte("\r\nUbuntu 20.04.3 LTS\r\n\r\nlogin: "))
|
||||||
|
|
||||||
|
conn.SetDeadline(time.Now().Add(2 * time.Minute))
|
||||||
|
scanner := bufio.NewScanner(conn)
|
||||||
|
|
||||||
|
var username string
|
||||||
|
expectingPassword := false
|
||||||
|
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := strings.TrimSpace(scanner.Text())
|
||||||
|
|
||||||
|
if !expectingPassword {
|
||||||
|
username = line
|
||||||
|
a.logServiceEvent(sessionID, remote, "telnet", "username_attempt", map[string]string{"username": username})
|
||||||
|
_, _ = conn.Write([]byte("Password: "))
|
||||||
|
expectingPassword = true
|
||||||
|
} else {
|
||||||
|
password := line
|
||||||
|
a.logServiceEvent(sessionID, remote, "telnet", "password_attempt", map[string]string{
|
||||||
|
"username": username,
|
||||||
|
"password": password,
|
||||||
|
})
|
||||||
|
_, _ = conn.Write([]byte("\r\nLogin incorrect\r\nlogin: "))
|
||||||
|
expectingPassword = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MySQL Handler
|
||||||
|
func (a *App) mysqlHandler(conn net.Conn) {
|
||||||
|
defer conn.Close()
|
||||||
|
remote := conn.RemoteAddr().String()
|
||||||
|
sessionID := fmt.Sprintf("mysql_%x", time.Now().UnixNano())
|
||||||
|
|
||||||
|
a.logServiceEvent(sessionID, remote, "mysql", "connection_start", nil)
|
||||||
|
|
||||||
|
// Send MySQL handshake packet
|
||||||
|
handshake := []byte{
|
||||||
|
0x4a, 0x00, 0x00, 0x00, // packet length + sequence
|
||||||
|
0x0a, // protocol version
|
||||||
|
}
|
||||||
|
handshake = append(handshake, []byte("5.7.34-0ubuntu0.18.04.1\x00")...) // server version
|
||||||
|
handshake = append(handshake, []byte{
|
||||||
|
0x01, 0x00, 0x00, 0x00, // connection id
|
||||||
|
0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, // auth plugin data part 1
|
||||||
|
0x00, // filler
|
||||||
|
0xff, 0xf7, // capability flags lower 2 bytes
|
||||||
|
0x08, // character set
|
||||||
|
0x02, 0x00, // status flags
|
||||||
|
0x0f, 0x80, // capability flags upper 2 bytes
|
||||||
|
0x15, // auth plugin data length
|
||||||
|
}...)
|
||||||
|
|
||||||
|
_, _ = conn.Write(handshake)
|
||||||
|
|
||||||
|
conn.SetDeadline(time.Now().Add(30 * time.Second))
|
||||||
|
buf := make([]byte, 1024)
|
||||||
|
n, err := conn.Read(buf)
|
||||||
|
if err == nil && n > 4 {
|
||||||
|
// Parse login packet (simplified)
|
||||||
|
payload := buf[4:n] // skip packet header
|
||||||
|
if len(payload) > 32 {
|
||||||
|
// Extract username (simplified parsing)
|
||||||
|
usernameStart := 32
|
||||||
|
usernameEnd := usernameStart
|
||||||
|
for usernameEnd < len(payload) && payload[usernameEnd] != 0 {
|
||||||
|
usernameEnd++
|
||||||
|
}
|
||||||
|
if usernameEnd < len(payload) {
|
||||||
|
username := string(payload[usernameStart:usernameEnd])
|
||||||
|
a.logServiceEvent(sessionID, remote, "mysql", "auth_attempt", map[string]string{
|
||||||
|
"username": username,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send error packet
|
||||||
|
errorPacket := []byte{
|
||||||
|
0x24, 0x00, 0x00, 0x01, // packet header
|
||||||
|
0xff, // error packet marker
|
||||||
|
0x10, 0x04, // error code
|
||||||
|
0x23, 0x48, 0x59, 0x30, 0x30, 0x30, // SQL state
|
||||||
|
}
|
||||||
|
errorPacket = append(errorPacket, []byte("Access denied for user")...)
|
||||||
|
_, _ = conn.Write(errorPacket)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PostgreSQL Handler
|
||||||
|
func (a *App) postgresqlHandler(conn net.Conn) {
|
||||||
|
defer conn.Close()
|
||||||
|
remote := conn.RemoteAddr().String()
|
||||||
|
sessionID := fmt.Sprintf("postgresql_%x", time.Now().UnixNano())
|
||||||
|
|
||||||
|
a.logServiceEvent(sessionID, remote, "postgresql", "connection_start", nil)
|
||||||
|
|
||||||
|
conn.SetDeadline(time.Now().Add(30 * time.Second))
|
||||||
|
buf := make([]byte, 1024)
|
||||||
|
n, err := conn.Read(buf)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if n >= 8 {
|
||||||
|
// Parse startup message
|
||||||
|
length := int(buf[0])<<24 | int(buf[1])<<16 | int(buf[2])<<8 | int(buf[3])
|
||||||
|
if length > 8 && n >= length {
|
||||||
|
params := string(buf[8:length])
|
||||||
|
// Extract username from parameters
|
||||||
|
if strings.Contains(params, "user") {
|
||||||
|
re := regexp.MustCompile(`user\x00([^\x00]+)`)
|
||||||
|
matches := re.FindStringSubmatch(params)
|
||||||
|
if len(matches) > 1 {
|
||||||
|
username := matches[1]
|
||||||
|
a.logServiceEvent(sessionID, remote, "postgresql", "auth_attempt", map[string]string{
|
||||||
|
"username": username,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send authentication request
|
||||||
|
authRequest := []byte{
|
||||||
|
0x52, // Authentication message type
|
||||||
|
0x00, 0x00, 0x00, 0x08, // message length
|
||||||
|
0x00, 0x00, 0x00, 0x03, // auth type (cleartext password)
|
||||||
|
}
|
||||||
|
_, _ = conn.Write(authRequest)
|
||||||
|
|
||||||
|
// Read password response
|
||||||
|
n, err = conn.Read(buf)
|
||||||
|
if err == nil && n > 5 && buf[0] == 0x70 { // password message
|
||||||
|
length := int(buf[1])<<24 | int(buf[2])<<16 | int(buf[3])<<8 | int(buf[4])
|
||||||
|
if length > 5 && n >= length {
|
||||||
|
password := string(buf[5 : length-1]) // exclude null terminator
|
||||||
|
a.logServiceEvent(sessionID, remote, "postgresql", "password_attempt", map[string]string{
|
||||||
|
"password": password,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send error response
|
||||||
|
errorMsg := []byte{
|
||||||
|
0x45, // Error message type
|
||||||
|
0x00, 0x00, 0x00, 0x26, // message length
|
||||||
|
}
|
||||||
|
errorMsg = append(errorMsg, []byte("SFATAL\x00C28P01\x00Mpassword authentication failed\x00\x00")...)
|
||||||
|
_, _ = conn.Write(errorMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redis Handler
|
||||||
|
func (a *App) redisHandler(conn net.Conn) {
|
||||||
|
defer conn.Close()
|
||||||
|
remote := conn.RemoteAddr().String()
|
||||||
|
sessionID := fmt.Sprintf("redis_%x", time.Now().UnixNano())
|
||||||
|
|
||||||
|
a.logServiceEvent(sessionID, remote, "redis", "connection_start", nil)
|
||||||
|
|
||||||
|
conn.SetDeadline(time.Now().Add(2 * time.Minute))
|
||||||
|
scanner := bufio.NewScanner(conn)
|
||||||
|
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := strings.TrimSpace(scanner.Text())
|
||||||
|
if line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse Redis protocol
|
||||||
|
if strings.HasPrefix(line, "*") {
|
||||||
|
// Multi-bulk request
|
||||||
|
continue
|
||||||
|
} else if strings.HasPrefix(line, "$") {
|
||||||
|
// Bulk string length
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
// Command
|
||||||
|
cmd := strings.ToUpper(line)
|
||||||
|
switch cmd {
|
||||||
|
case "AUTH":
|
||||||
|
// Next line should be password
|
||||||
|
if scanner.Scan() {
|
||||||
|
password := strings.TrimSpace(scanner.Text())
|
||||||
|
a.logServiceEvent(sessionID, remote, "redis", "auth_attempt", map[string]string{
|
||||||
|
"password": password,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_, _ = conn.Write([]byte("-ERR invalid password\r\n"))
|
||||||
|
case "PING":
|
||||||
|
_, _ = conn.Write([]byte("+PONG\r\n"))
|
||||||
|
case "INFO":
|
||||||
|
_, _ = conn.Write([]byte("+redis_version:6.2.6\r\n"))
|
||||||
|
case "QUIT":
|
||||||
|
_, _ = conn.Write([]byte("+OK\r\n"))
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
_, _ = conn.Write([]byte("-ERR unknown command\r\n"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MongoDB Handler
|
||||||
|
func (a *App) mongodbHandler(conn net.Conn) {
|
||||||
|
defer conn.Close()
|
||||||
|
remote := conn.RemoteAddr().String()
|
||||||
|
sessionID := fmt.Sprintf("mongodb_%x", time.Now().UnixNano())
|
||||||
|
|
||||||
|
a.logServiceEvent(sessionID, remote, "mongodb", "connection_start", nil)
|
||||||
|
|
||||||
|
conn.SetDeadline(time.Now().Add(30 * time.Second))
|
||||||
|
buf := make([]byte, 1024)
|
||||||
|
n, err := conn.Read(buf)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if n >= 16 {
|
||||||
|
// Parse MongoDB wire protocol message
|
||||||
|
// This is a simplified implementation
|
||||||
|
a.logServiceEvent(sessionID, remote, "mongodb", "protocol_attempt", map[string]string{
|
||||||
|
"bytes_received": strconv.Itoa(n),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send connection error
|
||||||
|
_, _ = conn.Write([]byte("connection refused"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// POP3 Handler
|
||||||
|
func (a *App) pop3Handler(conn net.Conn) {
|
||||||
|
defer conn.Close()
|
||||||
|
remote := conn.RemoteAddr().String()
|
||||||
|
sessionID := fmt.Sprintf("pop3_%x", time.Now().UnixNano())
|
||||||
|
|
||||||
|
a.logServiceEvent(sessionID, remote, "pop3", "connection_start", nil)
|
||||||
|
|
||||||
|
_, _ = conn.Write([]byte("+OK POP3 server ready\r\n"))
|
||||||
|
|
||||||
|
conn.SetDeadline(time.Now().Add(2 * time.Minute))
|
||||||
|
scanner := bufio.NewScanner(conn)
|
||||||
|
|
||||||
|
var username string
|
||||||
|
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := strings.TrimSpace(scanner.Text())
|
||||||
|
if line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.SplitN(line, " ", 2)
|
||||||
|
if len(parts) < 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := strings.ToUpper(parts[0])
|
||||||
|
arg := ""
|
||||||
|
if len(parts) > 1 {
|
||||||
|
arg = parts[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
switch cmd {
|
||||||
|
case "USER":
|
||||||
|
username = arg
|
||||||
|
a.logServiceEvent(sessionID, remote, "pop3", "username_attempt", map[string]string{"username": username})
|
||||||
|
_, _ = conn.Write([]byte("+OK\r\n"))
|
||||||
|
case "PASS":
|
||||||
|
password := arg
|
||||||
|
a.logServiceEvent(sessionID, remote, "pop3", "password_attempt", map[string]string{
|
||||||
|
"username": username,
|
||||||
|
"password": password,
|
||||||
|
})
|
||||||
|
_, _ = conn.Write([]byte("-ERR Authentication failed\r\n"))
|
||||||
|
case "QUIT":
|
||||||
|
_, _ = conn.Write([]byte("+OK Bye\r\n"))
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
_, _ = conn.Write([]byte("-ERR Unknown command\r\n"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IMAP Handler
|
||||||
|
func (a *App) imapHandler(conn net.Conn) {
|
||||||
|
defer conn.Close()
|
||||||
|
remote := conn.RemoteAddr().String()
|
||||||
|
sessionID := fmt.Sprintf("imap_%x", time.Now().UnixNano())
|
||||||
|
|
||||||
|
a.logServiceEvent(sessionID, remote, "imap", "connection_start", nil)
|
||||||
|
|
||||||
|
_, _ = conn.Write([]byte("* OK IMAP4rev1 Service Ready\r\n"))
|
||||||
|
|
||||||
|
conn.SetDeadline(time.Now().Add(2 * time.Minute))
|
||||||
|
scanner := bufio.NewScanner(conn)
|
||||||
|
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := strings.TrimSpace(scanner.Text())
|
||||||
|
if line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.Fields(line)
|
||||||
|
if len(parts) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
tag := parts[0]
|
||||||
|
cmd := strings.ToUpper(parts[1])
|
||||||
|
|
||||||
|
switch cmd {
|
||||||
|
case "LOGIN":
|
||||||
|
if len(parts) >= 4 {
|
||||||
|
username := strings.Trim(parts[2], "\"")
|
||||||
|
password := strings.Trim(parts[3], "\"")
|
||||||
|
a.logServiceEvent(sessionID, remote, "imap", "login_attempt", map[string]string{
|
||||||
|
"username": username,
|
||||||
|
"password": password,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_, _ = conn.Write([]byte(tag + " NO LOGIN failed\r\n"))
|
||||||
|
case "CAPABILITY":
|
||||||
|
_, _ = conn.Write([]byte("* CAPABILITY IMAP4rev1 AUTH=PLAIN\r\n"))
|
||||||
|
_, _ = conn.Write([]byte(tag + " OK CAPABILITY completed\r\n"))
|
||||||
|
case "LOGOUT":
|
||||||
|
_, _ = conn.Write([]byte("* BYE IMAP4rev1 Server logging out\r\n"))
|
||||||
|
_, _ = conn.Write([]byte(tag + " OK LOGOUT completed\r\n"))
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
_, _ = conn.Write([]byte(tag + " BAD Command not recognized\r\n"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RDP Handler (simplified)
|
||||||
|
func (a *App) rdpHandler(conn net.Conn) {
|
||||||
|
defer conn.Close()
|
||||||
|
remote := conn.RemoteAddr().String()
|
||||||
|
sessionID := fmt.Sprintf("rdp_%x", time.Now().UnixNano())
|
||||||
|
|
||||||
|
a.logServiceEvent(sessionID, remote, "rdp", "connection_start", nil)
|
||||||
|
|
||||||
|
conn.SetDeadline(time.Now().Add(30 * time.Second))
|
||||||
|
buf := make([]byte, 1024)
|
||||||
|
n, err := conn.Read(buf)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if n > 0 {
|
||||||
|
a.logServiceEvent(sessionID, remote, "rdp", "protocol_attempt", map[string]string{
|
||||||
|
"bytes_received": strconv.Itoa(n),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send RDP connection failure
|
||||||
|
_, _ = conn.Write([]byte{0x03, 0x00, 0x00, 0x0b, 0x02, 0xf0, 0x80, 0x04, 0x01, 0x00, 0x01})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DNS Handler (UDP)
|
||||||
|
func (a *App) dnsHandler(conn net.Conn) {
|
||||||
|
defer conn.Close()
|
||||||
|
remote := conn.RemoteAddr().String()
|
||||||
|
sessionID := fmt.Sprintf("dns_%x", time.Now().UnixNano())
|
||||||
|
|
||||||
|
a.logServiceEvent(sessionID, remote, "dns", "query_attempt", nil)
|
||||||
|
|
||||||
|
conn.SetDeadline(time.Now().Add(10 * time.Second))
|
||||||
|
buf := make([]byte, 512)
|
||||||
|
n, err := conn.Read(buf)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if n > 12 {
|
||||||
|
// Parse DNS query (simplified)
|
||||||
|
a.logServiceEvent(sessionID, remote, "dns", "query_received", map[string]string{
|
||||||
|
"query_size": strconv.Itoa(n),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SNMP Handler (UDP)
|
||||||
|
func (a *App) snmpHandler(conn net.Conn) {
|
||||||
|
defer conn.Close()
|
||||||
|
remote := conn.RemoteAddr().String()
|
||||||
|
sessionID := fmt.Sprintf("snmp_%x", time.Now().UnixNano())
|
||||||
|
|
||||||
|
a.logServiceEvent(sessionID, remote, "snmp", "request_attempt", nil)
|
||||||
|
|
||||||
|
conn.SetDeadline(time.Now().Add(10 * time.Second))
|
||||||
|
buf := make([]byte, 1024)
|
||||||
|
n, err := conn.Read(buf)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if n > 0 {
|
||||||
|
a.logServiceEvent(sessionID, remote, "snmp", "request_received", map[string]string{
|
||||||
|
"request_size": strconv.Itoa(n),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LDAP Handler
|
||||||
|
func (a *App) ldapHandler(conn net.Conn) {
|
||||||
|
defer conn.Close()
|
||||||
|
remote := conn.RemoteAddr().String()
|
||||||
|
sessionID := fmt.Sprintf("ldap_%x", time.Now().UnixNano())
|
||||||
|
|
||||||
|
a.logServiceEvent(sessionID, remote, "ldap", "connection_start", nil)
|
||||||
|
|
||||||
|
conn.SetDeadline(time.Now().Add(30 * time.Second))
|
||||||
|
buf := make([]byte, 1024)
|
||||||
|
n, err := conn.Read(buf)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if n > 0 {
|
||||||
|
a.logServiceEvent(sessionID, remote, "ldap", "bind_attempt", map[string]string{
|
||||||
|
"request_size": strconv.Itoa(n),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send LDAP bind failure
|
||||||
|
_, _ = conn.Write([]byte{0x30, 0x0c, 0x02, 0x01, 0x01, 0x61, 0x07, 0x0a, 0x01, 0x31, 0x04, 0x00, 0x04, 0x00})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to log service events
|
||||||
|
func (a *App) logServiceEvent(sessionID, remote, service, eventType string, details map[string]string) {
|
||||||
|
if details == nil {
|
||||||
|
details = make(map[string]string)
|
||||||
|
}
|
||||||
|
details["session_id"] = sessionID
|
||||||
|
details["event_type"] = eventType
|
||||||
|
|
||||||
|
a.logEvent(Record{
|
||||||
|
Timestamp: time.Now().UTC(),
|
||||||
|
RemoteAddr: remoteIP(remote),
|
||||||
|
RemotePort: remotePort(remote),
|
||||||
|
Service: service,
|
||||||
|
Details: details,
|
||||||
|
RawPayload: fmt.Sprintf("%s: %s", eventType, sessionID),
|
||||||
|
})
|
||||||
|
}
|
||||||
+556
@@ -0,0 +1,556 @@
|
|||||||
|
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()})
|
||||||
|
}
|
||||||
@@ -0,0 +1,488 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ThreatIntel tracks malicious IPs and their activities
|
||||||
|
type ThreatIntel struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
maliciousIPs map[string]*IPThreatInfo
|
||||||
|
configPath string
|
||||||
|
autoSave bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// IPThreatInfo contains information about a potentially malicious IP
|
||||||
|
type IPThreatInfo struct {
|
||||||
|
IP string `json:"ip"`
|
||||||
|
FirstSeen time.Time `json:"first_seen"`
|
||||||
|
LastSeen time.Time `json:"last_seen"`
|
||||||
|
TotalConnections int `json:"total_connections"`
|
||||||
|
Services map[string]int `json:"services"` // service -> connection count
|
||||||
|
AuthAttempts int `json:"auth_attempts"` // total authentication attempts
|
||||||
|
UniqueUsernames map[string]int `json:"unique_usernames"` // username -> attempt count
|
||||||
|
UniquePasswords map[string]int `json:"unique_passwords"` // password -> attempt count
|
||||||
|
Countries map[string]int `json:"countries"` // country -> count (if GeoIP available)
|
||||||
|
ThreatScore int `json:"threat_score"` // calculated threat score
|
||||||
|
IsBlacklisted bool `json:"is_blacklisted"`
|
||||||
|
Notes []string `json:"notes"`
|
||||||
|
RawPayloads []string `json:"raw_payloads,omitempty"` // sample payloads
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewThreatIntel creates a new threat intelligence tracker
|
||||||
|
func NewThreatIntel(configPath string, autoSave bool) *ThreatIntel {
|
||||||
|
ti := &ThreatIntel{
|
||||||
|
maliciousIPs: make(map[string]*IPThreatInfo),
|
||||||
|
configPath: configPath,
|
||||||
|
autoSave: autoSave,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to load existing data
|
||||||
|
ti.Load()
|
||||||
|
|
||||||
|
// Start auto-save routine if enabled
|
||||||
|
if autoSave {
|
||||||
|
go ti.autoSaveRoutine()
|
||||||
|
}
|
||||||
|
|
||||||
|
return ti
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecordActivity records activity from an IP address
|
||||||
|
func (ti *ThreatIntel) RecordActivity(record Record) {
|
||||||
|
ti.mu.Lock()
|
||||||
|
defer ti.mu.Unlock()
|
||||||
|
|
||||||
|
ip := record.RemoteAddr
|
||||||
|
if ip == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get or create IP info
|
||||||
|
info, exists := ti.maliciousIPs[ip]
|
||||||
|
if !exists {
|
||||||
|
info = &IPThreatInfo{
|
||||||
|
IP: ip,
|
||||||
|
FirstSeen: record.Timestamp,
|
||||||
|
Services: make(map[string]int),
|
||||||
|
UniqueUsernames: make(map[string]int),
|
||||||
|
UniquePasswords: make(map[string]int),
|
||||||
|
Countries: make(map[string]int),
|
||||||
|
Notes: []string{},
|
||||||
|
RawPayloads: []string{},
|
||||||
|
}
|
||||||
|
ti.maliciousIPs[ip] = info
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update basic info
|
||||||
|
info.LastSeen = record.Timestamp
|
||||||
|
info.TotalConnections++
|
||||||
|
info.Services[record.Service]++
|
||||||
|
|
||||||
|
// Process authentication attempts
|
||||||
|
if record.Details != nil {
|
||||||
|
if username, ok := record.Details["username"]; ok && username != "" {
|
||||||
|
info.UniqueUsernames[username]++
|
||||||
|
info.AuthAttempts++
|
||||||
|
}
|
||||||
|
if password, ok := record.Details["password"]; ok && password != "" {
|
||||||
|
info.UniquePasswords[password]++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for specific attack patterns
|
||||||
|
ti.analyzeAttackPatterns(info, record)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store sample payloads (limit to 10)
|
||||||
|
if record.RawPayload != "" && len(info.RawPayloads) < 10 {
|
||||||
|
info.RawPayloads = append(info.RawPayloads, record.RawPayload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recalculate threat score
|
||||||
|
info.ThreatScore = ti.calculateThreatScore(info)
|
||||||
|
|
||||||
|
// Auto-blacklist based on threat score
|
||||||
|
if info.ThreatScore >= 100 && !info.IsBlacklisted {
|
||||||
|
info.IsBlacklisted = true
|
||||||
|
info.Notes = append(info.Notes, fmt.Sprintf("Auto-blacklisted at %s (threat score: %d)", time.Now().Format(time.RFC3339), info.ThreatScore))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// analyzeAttackPatterns looks for specific attack patterns and adds notes
|
||||||
|
func (ti *ThreatIntel) analyzeAttackPatterns(info *IPThreatInfo, record Record) {
|
||||||
|
// Check for brute force patterns
|
||||||
|
if info.AuthAttempts > 10 {
|
||||||
|
if len(info.Notes) == 0 || info.Notes[len(info.Notes)-1] != "Brute force attack detected" {
|
||||||
|
info.Notes = append(info.Notes, "Brute force attack detected")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for credential stuffing (many unique usernames)
|
||||||
|
if len(info.UniqueUsernames) > 20 {
|
||||||
|
if len(info.Notes) == 0 || info.Notes[len(info.Notes)-1] != "Credential stuffing attack detected" {
|
||||||
|
info.Notes = append(info.Notes, "Credential stuffing attack detected")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for service scanning (multiple services)
|
||||||
|
if len(info.Services) > 5 {
|
||||||
|
if len(info.Notes) == 0 || info.Notes[len(info.Notes)-1] != "Port/service scanning detected" {
|
||||||
|
info.Notes = append(info.Notes, "Port/service scanning detected")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for common attack usernames
|
||||||
|
commonAttackUsernames := []string{"admin", "root", "administrator", "user", "test", "guest", "oracle", "postgres", "mysql"}
|
||||||
|
if username, ok := record.Details["username"]; ok {
|
||||||
|
for _, attackUser := range commonAttackUsernames {
|
||||||
|
if username == attackUser {
|
||||||
|
info.Notes = append(info.Notes, fmt.Sprintf("Used common attack username: %s", username))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for common attack passwords
|
||||||
|
commonAttackPasswords := []string{"password", "123456", "admin", "root", "toor", "password123", "qwerty"}
|
||||||
|
if password, ok := record.Details["password"]; ok {
|
||||||
|
for _, attackPass := range commonAttackPasswords {
|
||||||
|
if password == attackPass {
|
||||||
|
info.Notes = append(info.Notes, fmt.Sprintf("Used common attack password: %s", password))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculateThreatScore calculates a threat score based on various factors
|
||||||
|
func (ti *ThreatIntel) calculateThreatScore(info *IPThreatInfo) int {
|
||||||
|
score := 0
|
||||||
|
|
||||||
|
// Base score for any connection
|
||||||
|
score += 1
|
||||||
|
|
||||||
|
// Score based on number of connections
|
||||||
|
if info.TotalConnections > 100 {
|
||||||
|
score += 50
|
||||||
|
} else if info.TotalConnections > 50 {
|
||||||
|
score += 30
|
||||||
|
} else if info.TotalConnections > 10 {
|
||||||
|
score += 15
|
||||||
|
} else if info.TotalConnections > 5 {
|
||||||
|
score += 5
|
||||||
|
}
|
||||||
|
|
||||||
|
// Score based on authentication attempts
|
||||||
|
if info.AuthAttempts > 50 {
|
||||||
|
score += 40
|
||||||
|
} else if info.AuthAttempts > 20 {
|
||||||
|
score += 25
|
||||||
|
} else if info.AuthAttempts > 10 {
|
||||||
|
score += 15
|
||||||
|
} else if info.AuthAttempts > 5 {
|
||||||
|
score += 10
|
||||||
|
}
|
||||||
|
|
||||||
|
// Score based on unique usernames (credential stuffing)
|
||||||
|
if len(info.UniqueUsernames) > 50 {
|
||||||
|
score += 30
|
||||||
|
} else if len(info.UniqueUsernames) > 20 {
|
||||||
|
score += 20
|
||||||
|
} else if len(info.UniqueUsernames) > 10 {
|
||||||
|
score += 10
|
||||||
|
}
|
||||||
|
|
||||||
|
// Score based on service diversity (scanning)
|
||||||
|
if len(info.Services) > 10 {
|
||||||
|
score += 25
|
||||||
|
} else if len(info.Services) > 5 {
|
||||||
|
score += 15
|
||||||
|
} else if len(info.Services) > 3 {
|
||||||
|
score += 10
|
||||||
|
}
|
||||||
|
|
||||||
|
// Score based on time span (persistent attacker)
|
||||||
|
timeSpan := info.LastSeen.Sub(info.FirstSeen)
|
||||||
|
if timeSpan > 24*time.Hour {
|
||||||
|
score += 20
|
||||||
|
} else if timeSpan > 12*time.Hour {
|
||||||
|
score += 15
|
||||||
|
} else if timeSpan > 6*time.Hour {
|
||||||
|
score += 10
|
||||||
|
} else if timeSpan > 1*time.Hour {
|
||||||
|
score += 5
|
||||||
|
}
|
||||||
|
|
||||||
|
return score
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsBlacklisted checks if an IP is blacklisted
|
||||||
|
func (ti *ThreatIntel) IsBlacklisted(ip string) bool {
|
||||||
|
ti.mu.RLock()
|
||||||
|
defer ti.mu.RUnlock()
|
||||||
|
|
||||||
|
if info, exists := ti.maliciousIPs[ip]; exists {
|
||||||
|
return info.IsBlacklisted
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetThreatInfo returns threat information for an IP
|
||||||
|
func (ti *ThreatIntel) GetThreatInfo(ip string) (*IPThreatInfo, bool) {
|
||||||
|
ti.mu.RLock()
|
||||||
|
defer ti.mu.RUnlock()
|
||||||
|
|
||||||
|
info, exists := ti.maliciousIPs[ip]
|
||||||
|
if !exists {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return a copy to avoid race conditions
|
||||||
|
infoCopy := *info
|
||||||
|
infoCopy.Services = make(map[string]int)
|
||||||
|
infoCopy.UniqueUsernames = make(map[string]int)
|
||||||
|
infoCopy.UniquePasswords = make(map[string]int)
|
||||||
|
infoCopy.Countries = make(map[string]int)
|
||||||
|
|
||||||
|
for k, v := range info.Services {
|
||||||
|
infoCopy.Services[k] = v
|
||||||
|
}
|
||||||
|
for k, v := range info.UniqueUsernames {
|
||||||
|
infoCopy.UniqueUsernames[k] = v
|
||||||
|
}
|
||||||
|
for k, v := range info.UniquePasswords {
|
||||||
|
infoCopy.UniquePasswords[k] = v
|
||||||
|
}
|
||||||
|
for k, v := range info.Countries {
|
||||||
|
infoCopy.Countries[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
infoCopy.Notes = make([]string, len(info.Notes))
|
||||||
|
copy(infoCopy.Notes, info.Notes)
|
||||||
|
|
||||||
|
infoCopy.RawPayloads = make([]string, len(info.RawPayloads))
|
||||||
|
copy(infoCopy.RawPayloads, info.RawPayloads)
|
||||||
|
|
||||||
|
return &infoCopy, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTopThreats returns the top N threats by score
|
||||||
|
func (ti *ThreatIntel) GetTopThreats(n int) []*IPThreatInfo {
|
||||||
|
ti.mu.RLock()
|
||||||
|
defer ti.mu.RUnlock()
|
||||||
|
|
||||||
|
var threats []*IPThreatInfo
|
||||||
|
for _, info := range ti.maliciousIPs {
|
||||||
|
threats = append(threats, info)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by threat score (simple bubble sort for small datasets)
|
||||||
|
for i := 0; i < len(threats)-1; i++ {
|
||||||
|
for j := 0; j < len(threats)-i-1; j++ {
|
||||||
|
if threats[j].ThreatScore < threats[j+1].ThreatScore {
|
||||||
|
threats[j], threats[j+1] = threats[j+1], threats[j]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if n > len(threats) {
|
||||||
|
n = len(threats)
|
||||||
|
}
|
||||||
|
|
||||||
|
return threats[:n]
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBlacklistedIPs returns all blacklisted IPs
|
||||||
|
func (ti *ThreatIntel) GetBlacklistedIPs() []string {
|
||||||
|
ti.mu.RLock()
|
||||||
|
defer ti.mu.RUnlock()
|
||||||
|
|
||||||
|
var blacklisted []string
|
||||||
|
for ip, info := range ti.maliciousIPs {
|
||||||
|
if info.IsBlacklisted {
|
||||||
|
blacklisted = append(blacklisted, ip)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return blacklisted
|
||||||
|
}
|
||||||
|
|
||||||
|
// ManualBlacklist manually blacklists an IP
|
||||||
|
func (ti *ThreatIntel) ManualBlacklist(ip string, reason string) {
|
||||||
|
ti.mu.Lock()
|
||||||
|
defer ti.mu.Unlock()
|
||||||
|
|
||||||
|
info, exists := ti.maliciousIPs[ip]
|
||||||
|
if !exists {
|
||||||
|
info = &IPThreatInfo{
|
||||||
|
IP: ip,
|
||||||
|
FirstSeen: time.Now(),
|
||||||
|
Services: make(map[string]int),
|
||||||
|
UniqueUsernames: make(map[string]int),
|
||||||
|
UniquePasswords: make(map[string]int),
|
||||||
|
Countries: make(map[string]int),
|
||||||
|
Notes: []string{},
|
||||||
|
RawPayloads: []string{},
|
||||||
|
}
|
||||||
|
ti.maliciousIPs[ip] = info
|
||||||
|
}
|
||||||
|
|
||||||
|
info.IsBlacklisted = true
|
||||||
|
info.LastSeen = time.Now()
|
||||||
|
note := fmt.Sprintf("Manually blacklisted at %s", time.Now().Format(time.RFC3339))
|
||||||
|
if reason != "" {
|
||||||
|
note += fmt.Sprintf(" - Reason: %s", reason)
|
||||||
|
}
|
||||||
|
info.Notes = append(info.Notes, note)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveFromBlacklist removes an IP from the blacklist
|
||||||
|
func (ti *ThreatIntel) RemoveFromBlacklist(ip string) {
|
||||||
|
ti.mu.Lock()
|
||||||
|
defer ti.mu.Unlock()
|
||||||
|
|
||||||
|
if info, exists := ti.maliciousIPs[ip]; exists {
|
||||||
|
info.IsBlacklisted = false
|
||||||
|
info.Notes = append(info.Notes, fmt.Sprintf("Removed from blacklist at %s", time.Now().Format(time.RFC3339)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save saves threat intelligence data to file
|
||||||
|
func (ti *ThreatIntel) Save() error {
|
||||||
|
ti.mu.RLock()
|
||||||
|
defer ti.mu.RUnlock()
|
||||||
|
|
||||||
|
if ti.configPath == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create directory if it doesn't exist
|
||||||
|
if err := os.MkdirAll(filepath.Dir(ti.configPath), 0755); err != nil {
|
||||||
|
return fmt.Errorf("create threat intel dir: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.MarshalIndent(ti.maliciousIPs, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("marshal threat intel data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return os.WriteFile(ti.configPath, data, 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load loads threat intelligence data from file
|
||||||
|
func (ti *ThreatIntel) Load() error {
|
||||||
|
ti.mu.Lock()
|
||||||
|
defer ti.mu.Unlock()
|
||||||
|
|
||||||
|
if ti.configPath == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(ti.configPath)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil // File doesn't exist yet, that's okay
|
||||||
|
}
|
||||||
|
return fmt.Errorf("read threat intel file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Unmarshal(data, &ti.maliciousIPs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// autoSaveRoutine periodically saves threat intelligence data
|
||||||
|
func (ti *ThreatIntel) autoSaveRoutine() {
|
||||||
|
ticker := time.NewTicker(5 * time.Minute)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for range ticker.C {
|
||||||
|
if err := ti.Save(); err != nil {
|
||||||
|
// Log error but don't stop the routine
|
||||||
|
fmt.Printf("Error auto-saving threat intel: %v\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStats returns overall statistics
|
||||||
|
func (ti *ThreatIntel) GetStats() map[string]interface{} {
|
||||||
|
ti.mu.RLock()
|
||||||
|
defer ti.mu.RUnlock()
|
||||||
|
|
||||||
|
totalIPs := len(ti.maliciousIPs)
|
||||||
|
blacklistedIPs := 0
|
||||||
|
totalConnections := 0
|
||||||
|
totalAuthAttempts := 0
|
||||||
|
serviceStats := make(map[string]int)
|
||||||
|
|
||||||
|
for _, info := range ti.maliciousIPs {
|
||||||
|
if info.IsBlacklisted {
|
||||||
|
blacklistedIPs++
|
||||||
|
}
|
||||||
|
totalConnections += info.TotalConnections
|
||||||
|
totalAuthAttempts += info.AuthAttempts
|
||||||
|
|
||||||
|
for service, count := range info.Services {
|
||||||
|
serviceStats[service] += count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return map[string]interface{}{
|
||||||
|
"total_ips": totalIPs,
|
||||||
|
"blacklisted_ips": blacklistedIPs,
|
||||||
|
"total_connections": totalConnections,
|
||||||
|
"total_auth_attempts": totalAuthAttempts,
|
||||||
|
"service_stats": serviceStats,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShouldBlock determines if a connection should be blocked based on IP reputation
|
||||||
|
func (ti *ThreatIntel) ShouldBlock(ip string) bool {
|
||||||
|
// Check if IP is blacklisted
|
||||||
|
if ti.IsBlacklisted(ip) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Additional checks could be added here:
|
||||||
|
// - Rate limiting
|
||||||
|
// - Temporary blocks for high-frequency connections
|
||||||
|
// - Integration with external threat feeds
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsPrivateIP checks if an IP address is private/internal
|
||||||
|
func IsPrivateIP(ip string) bool {
|
||||||
|
parsedIP := net.ParseIP(ip)
|
||||||
|
if parsedIP == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for private IP ranges
|
||||||
|
privateRanges := []string{
|
||||||
|
"10.0.0.0/8",
|
||||||
|
"172.16.0.0/12",
|
||||||
|
"192.168.0.0/16",
|
||||||
|
"127.0.0.0/8",
|
||||||
|
"169.254.0.0/16",
|
||||||
|
"::1/128",
|
||||||
|
"fc00::/7",
|
||||||
|
"fe80::/10",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, cidr := range privateRanges {
|
||||||
|
_, network, err := net.ParseCIDR(cidr)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if network.Contains(parsedIP) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
+101
@@ -0,0 +1,101 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var tpl = template.Must(template.New("base").Parse(`
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head><meta charset="utf-8"><title>Honeypot Dashboard</title></head>
|
||||||
|
<body>
|
||||||
|
<h1>Honeypot Dashboard</h1>
|
||||||
|
<p><a href="/logs">Logs</a></p>
|
||||||
|
{{ .Body }}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`))
|
||||||
|
|
||||||
|
func (a *App) startWeb() {
|
||||||
|
bind := a.cfg.Web.Bind
|
||||||
|
port := a.cfg.Web.Port
|
||||||
|
addr := fmt.Sprintf("%s:%d", bind, port)
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
tpl.Execute(w, map[string]interface{}{"Body": template.HTML("<p><a href=\"/logs\">View logs</a></p>")})
|
||||||
|
})
|
||||||
|
mux.HandleFunc("/logs", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// display last 200 logs
|
||||||
|
var rows []Record
|
||||||
|
if a.logger != nil && a.logger.mode == "sqlite" && a.logger.db != nil {
|
||||||
|
// query sqlite
|
||||||
|
q := `SELECT timestamp, remote_addr, remote_port, service, details, raw_payload FROM logs ORDER BY id DESC LIMIT 200`
|
||||||
|
rs, err := a.logger.db.Query(q)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "db query failed", 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer rs.Close()
|
||||||
|
for rs.Next() {
|
||||||
|
var ts, ra, rp, svc, detailsS, raw string
|
||||||
|
if err := rs.Scan(&ts, &ra, &rp, &svc, &detailsS, &raw); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var det map[string]string
|
||||||
|
_ = json.Unmarshal([]byte(detailsS), &det)
|
||||||
|
rows = append(rows, Record{Timestamp: parseTime(ts), RemoteAddr: ra, RemotePort: rp, Service: svc, Details: det, RawPayload: raw})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// try to read file based JSON-lines
|
||||||
|
path := a.cfg.LogPath
|
||||||
|
b, err := os.ReadFile(path)
|
||||||
|
if err == nil {
|
||||||
|
lines := strings.Split(string(b), "\n")
|
||||||
|
for i := len(lines) - 1; i >= 0 && len(rows) < 200; i-- {
|
||||||
|
line := strings.TrimSpace(lines[i])
|
||||||
|
if line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var rec Record
|
||||||
|
if err := json.Unmarshal([]byte(line), &rec); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rows = append(rows, rec)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// render simple table
|
||||||
|
var sb strings.Builder
|
||||||
|
sb.WriteString("<table border=1 cellpadding=4><tr><th>Time</th><th>Remote</th><th>Service</th><th>Details</th><th>Payload</th></tr>")
|
||||||
|
for _, r := range rows {
|
||||||
|
detB, _ := json.Marshal(r.Details)
|
||||||
|
sb.WriteString(fmt.Sprintf("<tr><td>%s</td><td>%s:%s</td><td>%s</td><td>%s</td><td><pre>%s</pre></td></tr>", r.Timestamp.Format("2006-01-02 15:04:05"), r.RemoteAddr, r.RemotePort, r.Service, template.HTMLEscapeString(string(detB)), template.HTMLEscapeString(r.RawPayload)))
|
||||||
|
}
|
||||||
|
sb.WriteString("</table>")
|
||||||
|
tpl.Execute(w, map[string]interface{}{"Body": template.HTML(sb.String())})
|
||||||
|
})
|
||||||
|
|
||||||
|
srv := &http.Server{Addr: addr, Handler: mux}
|
||||||
|
a.httpSrv = srv
|
||||||
|
log.Printf("Dashboard listening on http://%s", addr)
|
||||||
|
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
|
log.Printf("dashboard error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseTime(s string) (t time.Time) {
|
||||||
|
t, _ = time.Parse(time.RFC3339Nano, s)
|
||||||
|
if t.IsZero() {
|
||||||
|
// fallback current time
|
||||||
|
t = time.Now()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"log_mode": "file",
|
||||||
|
"log_path": "honeypot.log",
|
||||||
|
"web": {
|
||||||
|
"enabled": true,
|
||||||
|
"bind": "127.0.0.1",
|
||||||
|
"port": 6333
|
||||||
|
},
|
||||||
|
"services": {
|
||||||
|
"http": true,
|
||||||
|
"ssh": true,
|
||||||
|
"smb": false,
|
||||||
|
"sip": false,
|
||||||
|
"vnc": false,
|
||||||
|
"generic": []
|
||||||
|
},
|
||||||
|
"ports": {
|
||||||
|
"http": 8080,
|
||||||
|
"ssh": 2222,
|
||||||
|
"smb": 4450,
|
||||||
|
"sip": 5060,
|
||||||
|
"vnc": 5900
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
module honeydany
|
||||||
|
|
||||||
|
go 1.24.5
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.32
|
||||||
|
golang.org/x/crypto v0.42.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require golang.org/x/sys v0.36.0 // indirect
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
|
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
|
||||||
|
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
|
||||||
|
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||||
|
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
|
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
|
||||||
|
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{"timestamp":"2025-09-27T21:33:32.955904584Z","remote_addr":"127.0.0.1","remote_port":"44558","service":"ssh","details":{"auth_attempts":"0","duration_sec":"0.09","error":"read tcp 127.0.0.1:2222-\u003e127.0.0.1:44558: read: connection reset by peer","last_password":"","last_username":"","session_id":"18694132175c68ec"},"raw_payload":"session_end: map[auth_attempts:0 duration_sec:0.09 error:read tcp 127.0.0.1:2222-\u003e127.0.0.1:44558: read: connection reset by peer last_password: last_username: session_id:18694132175c68ec]"}
|
||||||
|
{"timestamp":"2025-09-27T21:34:21.343267479Z","remote_addr":"127.0.0.1","remote_port":"59260","service":"ssh","details":{"attempt":"1","client":"SSH-2.0-OpenSSH_9.6p1 Ubuntu-3ubuntu13.14","password":"asxcbvc","session_id":"1869413c69661475","username":"bob"},"raw_payload":"auth_attempt: map[attempt:1 client:SSH-2.0-OpenSSH_9.6p1 Ubuntu-3ubuntu13.14 password:asxcbvc session_id:1869413c69661475 username:bob]"}
|
||||||
|
{"timestamp":"2025-09-27T21:34:24.503353375Z","remote_addr":"127.0.0.1","remote_port":"59260","service":"ssh","details":{"attempt":"2","client":"SSH-2.0-OpenSSH_9.6p1 Ubuntu-3ubuntu13.14","password":"ascojmnrhe[pom","session_id":"1869413c69661475","username":"bob"},"raw_payload":"auth_attempt: map[attempt:2 client:SSH-2.0-OpenSSH_9.6p1 Ubuntu-3ubuntu13.14 password:ascojmnrhe[pom session_id:1869413c69661475 username:bob]"}
|
||||||
|
{"timestamp":"2025-09-27T21:34:30.534724871Z","remote_addr":"127.0.0.1","remote_port":"59260","service":"ssh","details":{"auth_attempts":"2","duration_sec":"13.35","error":"[ssh: no auth passed yet, permission denied, permission denied]","last_password":"ascojmnrhe[pom","last_username":"bob","session_id":"1869413c69661475"},"raw_payload":"session_end: map[auth_attempts:2 duration_sec:13.35 error:[ssh: no auth passed yet, permission denied, permission denied] last_password:ascojmnrhe[pom last_username:bob session_id:1869413c69661475]"}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"honeydany/app"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cfgPath := flag.String("config", "config.json", "path to config.json")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
// ensure config exists (auto-generate default if missing)
|
||||||
|
if err := app.EnsureConfig(*cfgPath); err != nil {
|
||||||
|
log.Fatalf("ensure config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, err := app.LoadConfig(*cfgPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("load config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
a, err := app.NewApp(cfg)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("create app: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// handle signals
|
||||||
|
sigCh := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
go func() {
|
||||||
|
<-sigCh
|
||||||
|
fmt.Println("signal received, shutting down")
|
||||||
|
cancel()
|
||||||
|
a.Shutdown()
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err := a.Run(ctx); err != nil {
|
||||||
|
log.Fatalf("app run: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("honeypot stopped")
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
- web dashboard default to **127.0.0.1:6333** (configurable in `config.json`).
|
||||||
|
- Logging supports `file`, `stdout` and `sqlite` (enable `sqlite` by setting `log_mode: "sqlite"` and `log_path: "honeypot/honeypot.db"`).
|
||||||
|
- To build/run:
|
||||||
|
1. `cd honeypot`
|
||||||
|
2. `go mod tidy` (to fetch `github.com/mattn/go-sqlite3` if you enable sqlite)
|
||||||
|
3. `go run ./...` or `go build ./...`
|
||||||
|
|
||||||
|
If you want, I can now:
|
||||||
|
- Add a `Dockerfile` and `systemd` unit; or
|
||||||
|
- Harden the SSH/SMB emulation to behave more like OpenCanary (fake banners/commands); or
|
||||||
|
- Add authentication-capture heuristics to parse username/password pairs more reliably.
|
||||||
Reference in New Issue
Block a user