2025-10-10 15:35:17 -04:00
|
|
|
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) {
|
2025-10-10 16:06:48 -04:00
|
|
|
fmt.Printf(" [?] Found %s\n", conn.RemoteHost)
|
2025-10-10 15:35:17 -04:00
|
|
|
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
|
|
|
|
|
}
|