diff --git a/internal/pkg/hunt/detect/services/services.go b/internal/pkg/hunt/detect/services/services.go index 13e0b7a..cba30b8 100644 --- a/internal/pkg/hunt/detect/services/services.go +++ b/internal/pkg/hunt/detect/services/services.go @@ -7,7 +7,6 @@ import ( "strings" "github.com/Kraken-OffSec/Scurvy/core/service" - "golang.org/x/sys/windows" ) // Whitelist for our own tool and legitimate system components @@ -35,7 +34,8 @@ func Detect() []*Service { fmt.Printf("[-] Error getting Service Manager: %s\n", err.Error()) return []*Service{} } - defer windows.Close(scm.Handle) + // Note: The service manager handle is managed by the Scurvy library + // and should not be manually closed here to avoid invalid handle errors services, err := scm.ListServices() if err != nil { @@ -57,6 +57,8 @@ func compareServices(serviceStrings []string, scm *service.Mgr) []*Service { fmt.Printf(" [>-] Error opening service %s: %s\n", serviceString, err.Error()) continue } + // Note: Individual service handles are also managed by Scurvy library + config, err := svc.Config() if err != nil { fmt.Printf(" [>-] Error getting service config %s: %s\n", serviceString, err.Error()) diff --git a/internal/web/browser.go b/internal/web/browser.go new file mode 100644 index 0000000..670a9d7 --- /dev/null +++ b/internal/web/browser.go @@ -0,0 +1,45 @@ +package web + +import ( + "fmt" + "syscall" + "unsafe" +) + +var ( + shell32 = syscall.NewLazyDLL("shell32.dll") + shellExecuteW = shell32.NewProc("ShellExecuteW") +) + +// OpenBrowser opens the default browser to the specified URL using Windows ShellExecute API +func OpenBrowser(url string) error { + // Convert strings to UTF16 pointers + operation, err := syscall.UTF16PtrFromString("open") + if err != nil { + return fmt.Errorf("failed to convert operation string: %w", err) + } + + urlPtr, err := syscall.UTF16PtrFromString(url) + if err != nil { + return fmt.Errorf("failed to convert URL string: %w", err) + } + + // ShellExecuteW(hwnd, operation, file, parameters, directory, showCmd) + // SW_SHOWNORMAL = 1, SW_SHOW = 5 + ret, _, callErr := shellExecuteW.Call( + 0, // hwnd (NULL) + uintptr(unsafe.Pointer(operation)), // operation ("open") + uintptr(unsafe.Pointer(urlPtr)), // file (URL) + 0, // parameters (NULL) + 0, // directory (NULL) + 5, // showCmd (SW_SHOW) + ) + + // ShellExecute returns a value > 32 on success + if ret <= 32 { + return fmt.Errorf("ShellExecute failed with code: %d (error: %v)", ret, callErr) + } + + fmt.Printf("[web] Browser opened successfully (return code: %d)\n", ret) + return nil +} diff --git a/internal/web/hosts.go b/internal/web/hosts.go new file mode 100644 index 0000000..5c2688d --- /dev/null +++ b/internal/web/hosts.go @@ -0,0 +1,129 @@ +package web + +import ( + "bufio" + "fmt" + "os" + "path/filepath" + "strings" +) + +const ( + hostsEntry = "127.0.0.1 rmm-hunter" + marker = "# RMM-Hunter entry" +) + +// AddHostsEntry adds the rmm-hunter DNS entry to the Windows hosts file +// Requires administrator privileges +func AddHostsEntry() error { + hostsPath := getHostsPath() + + // Check if entry already exists + exists, err := hostsEntryExists(hostsPath) + if err != nil { + return fmt.Errorf("failed to check hosts file: %w", err) + } + + if exists { + fmt.Println("[+] rmm-hunter hosts entry already exists") + return nil + } + + // Read existing hosts file + content, err := os.ReadFile(hostsPath) + if err != nil { + return fmt.Errorf("failed to read hosts file: %w", err) + } + + // Append our entry + newContent := string(content) + if !strings.HasSuffix(newContent, "\n") { + newContent += "\n" + } + newContent += fmt.Sprintf("\n%s\n%s\n", marker, hostsEntry) + + // Write back to hosts file + err = os.WriteFile(hostsPath, []byte(newContent), 0644) + if err != nil { + return fmt.Errorf("failed to write hosts file: %w", err) + } + + fmt.Println("[+] Added rmm-hunter to hosts file") + fmt.Println("[+] You can now access the web UI at: http://rmm-hunter:8080") + return nil +} + +// RemoveHostsEntry removes the rmm-hunter DNS entry from the Windows hosts file +func RemoveHostsEntry() error { + hostsPath := getHostsPath() + + // Read existing hosts file + file, err := os.Open(hostsPath) + if err != nil { + return fmt.Errorf("failed to open hosts file: %w", err) + } + defer file.Close() + + var newLines []string + scanner := bufio.NewScanner(file) + skipNext := false + + for scanner.Scan() { + line := scanner.Text() + + // Skip the marker line and the next line (our entry) + if strings.Contains(line, marker) { + skipNext = true + continue + } + + if skipNext && strings.Contains(line, "rmm-hunter") { + skipNext = false + continue + } + + newLines = append(newLines, line) + } + + if err := scanner.Err(); err != nil { + return fmt.Errorf("failed to read hosts file: %w", err) + } + + // Write back to hosts file + newContent := strings.Join(newLines, "\n") + err = os.WriteFile(hostsPath, []byte(newContent), 0644) + if err != nil { + return fmt.Errorf("failed to write hosts file: %w", err) + } + + fmt.Println("[+] Removed rmm-hunter from hosts file") + return nil +} + +// hostsEntryExists checks if the rmm-hunter entry already exists in the hosts file +func hostsEntryExists(hostsPath string) (bool, error) { + file, err := os.Open(hostsPath) + if err != nil { + return false, err + } + defer file.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if strings.Contains(line, "rmm-hunter") && strings.Contains(line, "127.0.0.1") { + return true, nil + } + } + + return false, scanner.Err() +} + +// getHostsPath returns the path to the Windows hosts file +func getHostsPath() string { + systemRoot := os.Getenv("SystemRoot") + if systemRoot == "" { + systemRoot = "C:\\Windows" + } + return filepath.Join(systemRoot, "System32", "drivers", "etc", "hosts") +} diff --git a/internal/web/templates/android-chrome-192x192.png b/internal/web/templates/android-chrome-192x192.png new file mode 100644 index 0000000..9f9fff0 Binary files /dev/null and b/internal/web/templates/android-chrome-192x192.png differ diff --git a/internal/web/templates/android-chrome-512x512.png b/internal/web/templates/android-chrome-512x512.png new file mode 100644 index 0000000..e5605af Binary files /dev/null and b/internal/web/templates/android-chrome-512x512.png differ diff --git a/internal/web/templates/apple-touch-icon.png b/internal/web/templates/apple-touch-icon.png new file mode 100644 index 0000000..4039879 Binary files /dev/null and b/internal/web/templates/apple-touch-icon.png differ diff --git a/internal/web/templates/favicon-16x16.png b/internal/web/templates/favicon-16x16.png new file mode 100644 index 0000000..cfffb9f Binary files /dev/null and b/internal/web/templates/favicon-16x16.png differ diff --git a/internal/web/templates/favicon-32x32.png b/internal/web/templates/favicon-32x32.png new file mode 100644 index 0000000..bb3d71e Binary files /dev/null and b/internal/web/templates/favicon-32x32.png differ diff --git a/internal/web/templates/favicon.ico b/internal/web/templates/favicon.ico new file mode 100644 index 0000000..5da1b38 Binary files /dev/null and b/internal/web/templates/favicon.ico differ diff --git a/internal/web/templates/index.html b/internal/web/templates/index.html index a32fee7..abeb6ed 100644 --- a/internal/web/templates/index.html +++ b/internal/web/templates/index.html @@ -4,48 +4,122 @@ RMM Hunter Web UI + + + + + + +
+ +
RMM HUNTER
+
POWERED BY KRAKENTECH
+
+
+
@@ -72,27 +146,63 @@
- Proceed to Eliminate + Proceed to Eliminate
-