From 25d99c265d5d6d00d25c9cf0da8287d03736c0db Mon Sep 17 00:00:00 2001 From: Evan Hosinski Date: Sun, 12 Oct 2025 20:58:53 -0400 Subject: [PATCH] Add elimination API handler, update browser logic for process tracking, and refine UI animations and modal handling. --- .../pkg/hunt/detect/common/directories.go | 200 ++++++++------ .../pkg/hunt/detect/directory/directories.go | 59 +++-- internal/web/browser.go | 176 ++++++++++-- internal/web/templates/index.html | 250 +++++++++++++++++- internal/web/webserver.go | 245 ++++++++++++++++- 5 files changed, 799 insertions(+), 131 deletions(-) diff --git a/internal/pkg/hunt/detect/common/directories.go b/internal/pkg/hunt/detect/common/directories.go index d5cab91..05eea07 100644 --- a/internal/pkg/hunt/detect/common/directories.go +++ b/internal/pkg/hunt/detect/common/directories.go @@ -1,78 +1,128 @@ package common -var CommonDirectories = []string{ - `C:\Program Files (x86)%\mRemoteNG`, - `C:\\Program Files (x86)\Sysprogs`, - `C:\\Program Files (x86)\Sysprogs\SmarTTY`, - `C:\AlpemixSrvc`, - `C:\Downloads\SuperPuTTY`, - `C:\Program Files (x86)\Almageste\DragonDisk`, - `C:\Program Files (x86)\AnyDesk`, - `C:\Program Files (x86)\AnyViewer`, - `C:\Program Files (x86)\Atera Networks`, - `C:\Program Files (x86)\Bitvise SSH Client`, - `C:\Program Files (x86)\Bluetrait Agent`, - `C:\Program Files (x86)\DesktopCentral_Agent`, - `C:\Program Files (x86)\DesktopCentral_Agent\bin`, - `C:\Program Files (x86)\GoTo Opener`, - `C:\Program Files (x86)\GoToMyPC`, - `C:\Program Files (x86)\Google\Chrome Remote Desktop`, - `C:\Program Files (x86)\ISL Online`, - `C:\Program Files (x86)\Kaseya`, - `C:\Program Files (x86)\LANDesk`, - `C:\Program Files (x86)\OnionShare`, - `C:\Program Files (x86)\NetSarang`, - `C:\Program Files (x86)\NetSarang\xShell`, - `C:\Program Files (x86)\PJ Technologies`, - `C:\Program Files (x86)\PJ Technologies\GOVsrv`, - `C:\Program Files (x86)\Radmin Viewer 3`, - `C:\Program Files (x86)\RemotePC`, - `C:\Program Files (x86)\S3 Browser`, - `C:\Program Files (x86)\ScreenConnect Client (`, // C:\Program Files (x86)\ScreenConnect Client () - `C:\Program Files (x86)\SmartFTP Client`, - `C:\Program Files (x86)\Splashtop`, - `C:\Program Files (x86)\TeamViewer`, - `C:\Program Files (x86)\UltraViewer`, - `C:\Program Files (x86)\Xpra`, - `C:\Program Files (x86)\Yandex`, - `C:\Program Files (x86)\mRemoteNG`, - `C:\Program Files\ATERA NETWORKS`, - `C:\Program Files\ATERA NETWORKS\AteraAgent`, - `C:\Program Files\AnyDesk`, - `C:\Program Files\Bitvise SSH Server`, - `C:\Program Files\Danware Data\NetOp Packn Deploy`, - `C:\Program Files\Level`, - `C:\\Program Files\\LiteManager Pro`, - `C:\\Program Files\\LiteManager Pro \u2013 Viewer`, - `C:\Program Files\ManageEngine\ManageEngine Free Tools`, - `C:\Program Files\ManageEngine\ManageEngine Free Tools\Launcher`, - `C:\Program Files\RealVNC`, - `C:\Program Files\RealVNC\VNC Serve`, - `C:\Program Files\Remote Utilities`, - `C:\Program Files\Remote Utilities\Agent`, - `C:\Program Files\Solar-Putty-v4`, - `C:\Program Files\SolarWinds\Dameware Mini Remote Control`, - `C:\Program Files\SysAidServer`, - `C:\Program Files\TeamViewer`, - `C:\Program Files\TightVNC`, - `C:\Program Files\ZOC8`, - `C:\Program Files\uvnc bvba`, - `C:\Program Files\uvnc bvba\UltraVNC`, - `C:\ProgramData\Kaseya`, - `C:\ProgramData\Total Software Deployment`, - `C:\ProgramFiles\GoTo Machine Installer`, - `C:\ProgramFiles (x86)\GoTo Machine Installer`, - `{{APPDATA}}\Local\Google\Chrome\User Data\Default\Extensions\iodihamcpbpeioajjeobimgagajmlibd`, - `{{APPDATA}}\Local\MEGAsync`, - `{{APPDATA}}\Roaming\Mikogo`, - `{{APPDATA}}\Roaming\SyncTrayzor`, - `C:\Users\IEUser\Downloads\WinSCP-5.21.6-Portable`, - `C:\Users\USERNAME\AppData\Roaming\Insync`, - `C:\Users\USERNAME\AppData\Roaming\Insync\App`, - `C:\Windows\Action1`, - `C:\Windows\SysWOW64\rserver30`, - `C:\Windows\SysWOW64\rserver30\FamItrfc`, - `C:\Windows\SysWOW64\rserver30\FamItrf2`, - `C:\Windows\dwrcs`, - `C:\ProgramData\AMMYY`, +// KnownRMMDirectories contains known directory names/paths +// These will be searched in common installation locations defined in SearchBasePaths +var KnownRMMDirectories = []string{ + // A + `Action1`, + `Almageste\DragonDisk`, + `AlpemixSrvc`, + `AMMYY`, + `AnyDesk`, + `AnyViewer`, + `Atera Networks`, + `ATERA NETWORKS`, + `ATERA NETWORKS\AteraAgent`, + + // B + `Bitvise SSH Client`, + `Bitvise SSH Server`, + `Bluetrait Agent`, + + // D + `Danware Data\NetOp Packn Deploy`, + `DesktopCentral_Agent`, + `DesktopCentral_Agent\bin`, + + // G + `GoTo Opener`, + `GoTo Machine Installer`, + `GoToMyPC`, + `Google\Chrome Remote Desktop`, + `Google\Chrome\User Data\Default\Extensions\iodihamcpbpeioajjeobimgagajmlibd`, + + // I + `Insync`, + `Insync\App`, + `ISL Online`, + + // K + `Kaseya`, + + // L + `LANDesk`, + `Level`, + `LiteManager Pro`, + `LiteManager Pro – Viewer`, + + // M + `ManageEngine\ManageEngine Free Tools`, + `ManageEngine\ManageEngine Free Tools\Launcher`, + `MEGAsync`, + `Mikogo`, + `mRemoteNG`, + + // N + `NetSarang`, + `NetSarang\xShell`, + + // O + `OnionShare`, + + // P + `PJ Technologies`, + `PJ Technologies\GOVsrv`, + + // R + `Radmin Viewer 3`, + `RealVNC`, + `RealVNC\VNC Serve`, + `Remote Utilities`, + `Remote Utilities\Agent`, + `RemotePC`, + `RustDesk`, + + // S + `S3 Browser`, + `ScreenConnect Client (`, // Prefix pattern for ScreenConnect Client () + `SmartFTP Client`, + `Solar-Putty-v4`, + `SolarWinds\Dameware Mini Remote Control`, + `Splashtop`, + `SuperPuTTY`, + `SyncTrayzor`, + `Sysprogs`, + `Sysprogs\SmarTTY`, + `SysAidServer`, + `SysWOW64\rserver30`, + `SysWOW64\rserver30\FamItrfc`, + `SysWOW64\rserver30\FamItrf2`, + + // T + `TeamViewer`, + `TightVNC`, + `Total Software Deployment`, + + // U + `UltraViewer`, + `uvnc bvba`, + `uvnc bvba\UltraVNC`, + + // W + `WinSCP-5.21.6-Portable`, + `dwrcs`, + + // X + `Xpra`, + + // Y + `Yandex`, + + // Z + `ZOC8`, +} + +// SearchBasePaths defines the base directories to search within +var SearchBasePaths = []string{ + `C:\Program Files`, + `C:\Program Files (x86)`, + `C:\ProgramData`, + `C:\ProgramFiles`, // Installers variant 1 + `C:\ProgramFiles (x86)`, // Installers variant 2 + `C:\Windows`, + `{{APPDATA}}\Local`, + `{{APPDATA}}\Roaming`, + `{{USERPROFILE}}\Downloads`, + `C:\Downloads`, // Standard downloads location + `C:\`, // Root for edge cases (AlpemixSrvc) } diff --git a/internal/pkg/hunt/detect/directory/directories.go b/internal/pkg/hunt/detect/directory/directories.go index 41c7f3b..a0ed870 100644 --- a/internal/pkg/hunt/detect/directory/directories.go +++ b/internal/pkg/hunt/detect/directory/directories.go @@ -10,49 +10,56 @@ import ( ) var appData = os.Getenv("APPDATA") +var userProfile = os.Getenv("USERPROFILE") func Detect() []Directory { var suspiciousDirectories []Directory seen := make(map[string]bool) // Prevent duplicates fmt.Printf("[*] Enumerating Suspicious Directories \n") - // Check for common directories - for _, dir := range common.CommonDirectories { - dir = replaceAppData(dir) - // Check if this is a prefix pattern (ends with incomplete path such as Screen Connect "C:\Program Files (x86)\ScreenConnect Client (") - if isPrefix(dir) { - // Find all directories matching this prefix - matches := findPrefixMatches(dir) - for _, match := range matches { - if !seen[match] { - fmt.Printf(" [?] Found %s\n", match) - suspiciousDirectories = append(suspiciousDirectories, Directory{Path: match}) - seen[match] = true + // For each known RMM directory, check in all base paths + for _, rmmDir := range common.KnownRMMDirectories { + for _, basePath := range common.SearchBasePaths { + // Replace environment variables + basePath = replaceEnvVars(basePath) + + // Construct full path + fullPath := filepath.Join(basePath, rmmDir) + + // Check if this is a prefix pattern (ends with incomplete path like "ScreenConnect Client (") + if isPrefix(rmmDir) { + // Find all directories matching this prefix + matches := findPrefixMatches(fullPath) + for _, match := range matches { + if !seen[match] { + fmt.Printf(" [?] Found %s\n", match) + suspiciousDirectories = append(suspiciousDirectories, Directory{Path: match}) + seen[match] = true + } } - } - } else { - // Exact match - if _, err := os.Stat(dir); err == nil { - if !seen[dir] { - fmt.Printf(" [?] Found %s\n", dir) - suspiciousDirectories = append(suspiciousDirectories, Directory{Path: dir}) - seen[dir] = true + } else { + // Exact match + if _, err := os.Stat(fullPath); err == nil { + if !seen[fullPath] { + fmt.Printf(" [?] Found %s\n", fullPath) + suspiciousDirectories = append(suspiciousDirectories, Directory{Path: fullPath}) + seen[fullPath] = true + } } } } } + fmt.Printf("[+] Found %d Suspicious Directories\n", len(suspiciousDirectories)) return suspiciousDirectories } -// replaceAppData replaces {{APPDATA}} with the actual APPDATA path -func replaceAppData(path string) string { - if strings.Contains(path, "{{APPDATA}}") { - p := strings.Replace(path, "{{APPDATA}}", "", -1) - return filepath.Join(appData, p) - } +// replaceEnvVars replaces environment variable placeholders with actual paths +func replaceEnvVars(path string) string { + path = strings.ReplaceAll(path, "{{APPDATA}}", appData) + path = strings.ReplaceAll(path, "{{USERPROFILE}}", userProfile) return path } diff --git a/internal/web/browser.go b/internal/web/browser.go index 670a9d7..6759c73 100644 --- a/internal/web/browser.go +++ b/internal/web/browser.go @@ -4,42 +4,178 @@ import ( "fmt" "syscall" "unsafe" + + "golang.org/x/sys/windows" ) var ( - shell32 = syscall.NewLazyDLL("shell32.dll") - shellExecuteW = shell32.NewProc("ShellExecuteW") + shell32 = syscall.NewLazyDLL("shell32.dll") + shellExecuteExW = shell32.NewProc("ShellExecuteExW") ) +const ( + SEE_MASK_NOCLOSEPROCESS = 0x00000040 + SW_SHOW = 5 +) + +// SHELLEXECUTEINFO structure for ShellExecuteEx +type shellExecuteInfo struct { + cbSize uint32 + fMask uint32 + hwnd uintptr + lpVerb *uint16 + lpFile *uint16 + lpParameters *uint16 + lpDirectory *uint16 + nShow int32 + hInstApp uintptr + lpIDList uintptr + lpClass *uint16 + hkeyClass uintptr + dwHotKey uint32 + hIconOrMonitor uintptr + hProcess windows.Handle +} + +// BrowserHandle represents a handle to the opened browser process +type BrowserHandle struct { + ProcessID uint32 + Handle windows.Handle +} + // OpenBrowser opens the default browser to the specified URL using Windows ShellExecute API -func OpenBrowser(url string) error { +// Returns a handle to the browser process that can be used to close it later +func OpenBrowser(url string) (*BrowserHandle, error) { // Convert strings to UTF16 pointers operation, err := syscall.UTF16PtrFromString("open") if err != nil { - return fmt.Errorf("failed to convert operation string: %w", err) + return nil, 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) + return nil, 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) + // Initialize SHELLEXECUTEINFO structure + sei := shellExecuteInfo{ + cbSize: uint32(unsafe.Sizeof(shellExecuteInfo{})), + fMask: SEE_MASK_NOCLOSEPROCESS, // Request process handle + hwnd: 0, + lpVerb: operation, + lpFile: urlPtr, + lpParameters: nil, + lpDirectory: nil, + nShow: SW_SHOW, + hInstApp: 0, } - fmt.Printf("[web] Browser opened successfully (return code: %d)\n", ret) + // Call ShellExecuteExW + ret, _, err := shellExecuteExW.Call(uintptr(unsafe.Pointer(&sei))) + if ret == 0 { + return nil, fmt.Errorf("ShellExecuteExW failed: %w", err) + } + + if sei.hInstApp <= 32 { + return nil, fmt.Errorf("ShellExecuteExW failed with code: %d", sei.hInstApp) + } + + // Get process ID from handle + var processID uint32 + if sei.hProcess != 0 { + processID, err = windows.GetProcessId(sei.hProcess) + if err != nil { + // If we can't get PID, still return the handle + processID = 0 + } + } + + fmt.Printf("[web] Browser opened successfully (PID: %d)\n", processID) + + return &BrowserHandle{ + ProcessID: processID, + Handle: sei.hProcess, + }, nil +} + +// Close terminates the browser process and all child processes +func (bh *BrowserHandle) Close() error { + if bh == nil { + return nil + } + + // First try to kill the direct process if we have a handle + if bh.Handle != 0 { + windows.CloseHandle(bh.Handle) + } + + // Kill all browser processes that might have our URL open + // This is more reliable than trying to track the exact process tree + killed := killBrowserProcesses() + + fmt.Printf("[web] Terminated %d browser process(es)\n", killed) return nil } + +// killBrowserProcesses finds and kills common browser processes +func killBrowserProcesses() int { + browserExes := []string{ + "chrome.exe", + "msedge.exe", + "firefox.exe", + "brave.exe", + "opera.exe", + "iexplore.exe", + } + + killed := 0 + for _, exeName := range browserExes { + count := killProcessByName(exeName) + killed += count + } + + return killed +} + +// killProcessByName kills all processes with the given executable name +func killProcessByName(exeName string) int { + snapshot, err := windows.CreateToolhelp32Snapshot(windows.TH32CS_SNAPPROCESS, 0) + if err != nil { + return 0 + } + defer windows.CloseHandle(snapshot) + + var procEntry windows.ProcessEntry32 + procEntry.Size = uint32(unsafe.Sizeof(procEntry)) + + err = windows.Process32First(snapshot, &procEntry) + if err != nil { + return 0 + } + + killed := 0 + for { + // Convert the process name from [260]uint16 to string + processName := syscall.UTF16ToString(procEntry.ExeFile[:]) + + if processName == exeName { + // Open process with terminate rights + handle, err := windows.OpenProcess(windows.PROCESS_TERMINATE, false, procEntry.ProcessID) + if err == nil { + err = windows.TerminateProcess(handle, 0) + if err == nil { + killed++ + fmt.Printf("[web] Killed %s (PID: %d)\n", exeName, procEntry.ProcessID) + } + windows.CloseHandle(handle) + } + } + + err = windows.Process32Next(snapshot, &procEntry) + if err != nil { + break + } + } + + return killed +} diff --git a/internal/web/templates/index.html b/internal/web/templates/index.html index 294d087..a4eee25 100644 --- a/internal/web/templates/index.html +++ b/internal/web/templates/index.html @@ -14,6 +14,7 @@ --bg:#0b0f0c; --bg2:#111612; --panel:#0f1511; --accent:#17e46e; --accent2:#0eea5a; --muted:#a7b5a9; --text:#e6f4ea; --danger:#ff5c7a; --warn:#ffd166; } *{box-sizing:border-box} + html{background:var(--bg)} body{margin:0;font-family:Inter,system-ui,Segoe UI,Roboto,Helvetica,Arial,sans-serif;background:linear-gradient(180deg,var(--bg),#070a08);color:var(--text);min-height:100vh;display:flex;flex-direction:column} /* Scrollbar styling */ @@ -66,11 +67,15 @@ .tree-node{cursor:pointer;padding:8px 10px;border-radius:6px;margin:4px 0;user-select:none;font-weight:600;color:var(--text);border:1px solid transparent} .tree-node:hover{background:#0e1a13;border-color:#124b2b} .tree-node.expanded{background:#0e1a13;border-color:#124b2b} - .tree-child{cursor:pointer;padding:6px 10px 6px 12px;border-radius:6px;margin:2px 0;font-size:13px;color:var(--muted);border:1px solid transparent;display:flex;align-items:center;gap:8px;transition:all 0.15s} + .tree-child{cursor:pointer;padding:6px 10px 6px 12px;border-radius:6px;margin:2px 0;font-size:13px;color:var(--muted);border:1px solid transparent;display:flex;align-items:center;gap:8px;transition:all 0.15s;position:relative;overflow:hidden} .tree-child:hover{background:#0a2a18;color:var(--text);border-color:#124b2b} .tree-child.selected{background:#103e24;color:var(--accent);border-color:#1d7e4a} .tree-child::before{content:'→';color:var(--accent);opacity:0;transition:opacity 0.15s} .tree-child:hover::before,.tree-child.selected::before{opacity:1} + .tree-child.eliminating{animation:slideOutRight 0.6s ease-in-out forwards} + .tree-child.eliminating::after{content:'';position:absolute;top:0;left:0;right:0;bottom:0;background:linear-gradient(90deg,transparent,rgba(23,228,110,.3),transparent);animation:shimmer 0.6s ease-in-out} + @keyframes slideOutRight{0%{transform:translateX(0);opacity:1}50%{opacity:0.5}100%{transform:translateX(100%);opacity:0;height:0;margin:0;padding:0}} + @keyframes shimmer{0%{transform:translateX(-100%)}100%{transform:translateX(100%)}} .detail-field{margin:12px 0;padding:10px;background:#050805;border:1px solid #0c2819;border-radius:8px} .detail-label{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:0.5px;margin-bottom:4px} .detail-value{color:var(--text);font-size:14px;word-break:break-all} @@ -102,6 +107,35 @@ .splash-loader{width:200px;height:3px;background:rgba(23,228,110,.1);border-radius:3px;overflow:hidden;position:relative} .splash-loader::after{content:'';position:absolute;top:0;left:0;height:100%;width:40%;background:linear-gradient(90deg,transparent,var(--accent),transparent);animation:loading 1.5s ease-in-out infinite} @keyframes loading{0%{left:-40%}100%{left:100%}} + + /* Shutdown Screen */ + #shutdownScreen{position:fixed;top:0;left:0;right:0;bottom:0;background:radial-gradient(circle at center,#0f1511,#0b0f0c,#000);z-index:10000;display:none;flex-direction:column;align-items:center;justify-content:center} + #shutdownScreen.active{display:flex} + .spinner{width:80px;height:80px;border:4px solid rgba(23,228,110,.1);border-top:4px solid var(--accent);border-radius:50%;animation:spin 1s linear infinite;margin-bottom:30px} + .spinner.hide{display:none} + @keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}} + .shutdown-title{font-size:28px;font-weight:700;color:var(--accent);margin-bottom:12px;letter-spacing:1.5px} + .shutdown-message{font-size:16px;color:var(--muted);margin-bottom:20px} + .shutdown-success{font-size:18px;color:var(--accent);font-weight:600;display:none} + .shutdown-success.show{display:block} + + /* Modal */ + .modal{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.8);z-index:10001;display:none;align-items:center;justify-content:center} + .modal.active{display:flex} + .modal-content{background:var(--panel);border:1px solid #133422;border-radius:12px;padding:30px;max-width:500px;width:90%;box-shadow:0 20px 60px rgba(0,0,0,.5)} + .modal-icon{font-size:60px;text-align:center;margin-bottom:20px} + .modal-icon.success{color:var(--accent);animation:checkPop 0.5s ease-out} + .modal-icon.error{color:#ff5c7a} + @keyframes checkPop{0%{transform:scale(0)}50%{transform:scale(1.2)}100%{transform:scale(1)}} + .modal-title{font-size:24px;font-weight:700;text-align:center;margin-bottom:12px} + .modal-title.success{color:var(--accent)} + .modal-title.error{color:#ff5c7a} + .modal-message{text-align:center;color:var(--muted);margin-bottom:24px;line-height:1.6} + .modal-btn{width:100%;padding:14px;border:none;border-radius:8px;font-weight:600;font-size:15px;cursor:pointer;transition:all 0.2s} + .modal-btn.success{background:var(--accent);color:#000} + .modal-btn.success:hover{filter:brightness(1.2)} + .modal-btn.error{background:rgba(255,92,122,.2);color:#ff5c7a;border:1px solid #ff5c7a} + .modal-btn.error:hover{background:rgba(255,92,122,.3)} @@ -113,6 +147,24 @@
+ +
+
+
Shutting Down
+
RMM Hunter is closing...
+
✓ You can now close this browser tab
+
+ + + +