Files
RMM-Hunter/internal/pkg/hunt/detect/connections/outbound.go
T

193 lines
4.6 KiB
Go

package connections
import (
"bufio"
"fmt"
"net"
"os/exec"
"regexp"
"rmm-hunter/internal/pkg/hunt/detect/common"
. "rmm-hunter/internal/suspicious"
"strings"
)
func DetectOutboundConnections() []NetworkConnection {
var connections []NetworkConnection
fmt.Printf("[*] Enumerating Outbound Connections...\n")
// Get active connections via netstat
netstatConnections := getNetstatConnections()
connections = append(connections, netstatConnections...)
// Get DNS cache entries for hostname resolution
dnsCache := getDNSCache()
// Resolve hostnames for IP addresses
for i := range connections {
if hostname, exists := dnsCache[connections[i].RemoteAddr]; exists {
connections[i].RemoteHost = hostname
} else {
connections[i].RemoteHost = resolveHostname(connections[i].RemoteAddr)
}
}
fmt.Printf(" [>] Dispositioning %d Outbound Connections\n", len(connections))
return compareConnections(connections)
}
func compareConnections(connections []NetworkConnection) []NetworkConnection {
var suspiciousConnections []NetworkConnection
for _, conn := range connections {
remote := conn.RemoteHost
for _, dns := range common.CommonDNS {
if matchesDNSPattern(remote, dns) {
fmt.Printf(" [?] Found %s\n", conn.RemoteHost)
suspiciousConnections = append(suspiciousConnections, conn)
break
}
}
}
fmt.Printf("[+] Found %d Suspicious Outbound Connections\n", len(suspiciousConnections))
return suspiciousConnections
}
// matchesDNSPattern converts DNS pattern to regex and matches hostname
func matchesDNSPattern(hostname, pattern string) bool {
// Convert to lowercase for case-insensitive matching
pattern = strings.ToLower(pattern)
// Remove leading dot if present
if strings.HasPrefix(pattern, ".") {
pattern = pattern[1:]
}
// Escape special regex characters except * and .
pattern = regexp.QuoteMeta(pattern)
// Convert wildcards back to regex
pattern = strings.ReplaceAll(pattern, `\*`, `[^.]*`)
pattern = strings.ReplaceAll(pattern, `\.`, `\.`)
// Anchor the pattern to match end of hostname
pattern = `(^|\.)` + pattern + `$`
regex, err := regexp.Compile(pattern)
if err != nil {
return false
}
return regex.MatchString(hostname)
}
func getNetstatConnections() []NetworkConnection {
var connections []NetworkConnection
cmd := exec.Command("netstat", "-ano")
output, err := cmd.Output()
if err != nil {
return connections
}
scanner := bufio.NewScanner(strings.NewReader(string(output)))
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if strings.Contains(line, "TCP") && strings.Contains(line, "ESTABLISHED") {
fields := strings.Fields(line)
if len(fields) >= 5 {
localAddr := fields[1]
remoteAddr := fields[2]
state := fields[3]
pid := fields[4]
// Filter for outbound connections (exclude localhost)
if !strings.HasPrefix(remoteAddr, "127.0.0.1") &&
!strings.HasPrefix(remoteAddr, "::1") {
connections = append(connections, NetworkConnection{
LocalAddr: localAddr,
RemoteAddr: extractIP(remoteAddr),
State: state,
PID: pid,
})
}
}
}
}
return connections
}
func getDNSCache() map[string]string {
cache := make(map[string]string)
cmd := exec.Command("ipconfig", "/displaydns")
output, err := cmd.Output()
if err != nil {
return cache
}
lines := strings.Split(string(output), "\n")
var currentHost string
for _, line := range lines {
line = strings.TrimSpace(line)
if strings.Contains(line, "Record Name") {
parts := strings.Split(line, ":")
if len(parts) > 1 {
currentHost = strings.TrimSpace(parts[1])
}
}
if strings.Contains(line, "A (Host) Record") && currentHost != "" {
// Look for IP in next few lines
continue
}
if currentHost != "" && net.ParseIP(strings.TrimSpace(line)) != nil {
cache[strings.TrimSpace(line)] = currentHost
currentHost = ""
}
}
return cache
}
func extractIP(addr string) string {
if idx := strings.LastIndex(addr, ":"); idx != -1 {
return addr[:idx]
}
return addr
}
func resolveHostname(ip string) string {
names, err := net.LookupAddr(ip)
if err != nil || len(names) == 0 {
return ip
}
return strings.TrimSuffix(names[0], ".")
}
// GetHTTPHostnames extracts unique hostnames from outbound connections
func GetHTTPHostnames() []string {
connections := DetectOutboundConnections()
hostnameMap := make(map[string]bool)
var hostnames []string
for _, conn := range connections {
if conn.RemoteHost != "" && conn.RemoteHost != conn.RemoteAddr {
if !hostnameMap[conn.RemoteHost] {
hostnameMap[conn.RemoteHost] = true
hostnames = append(hostnames, conn.RemoteHost)
}
}
}
return hostnames
}