Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d2f7e7595b | |||
| 3935efb6ed | |||
| 88c5afba9c | |||
| 852b061b40 | |||
| b4db6c19b9 | |||
| 3f50f20892 | |||
| 25d99c265d | |||
| 0b09092973 | |||
| adcad167df | |||
| 15fb9eb510 | |||
| 01113551fb | |||
| e6f91d0bc7 | |||
| 615c129376 | |||
| bf63fb83bf | |||
| 976b45043b | |||
| e578a8fd97 | |||
| 58b521987d | |||
| 69657e4c46 | |||
| d349b38047 | |||
| 3e498365a3 | |||
| b47351f4ee |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 4.2 MiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 2.6 MiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 832 KiB |
@@ -1,22 +1,27 @@
|
||||
MIT License
|
||||
KrakenTech Proprietary License
|
||||
|
||||
Copyright (c) 2025 KrakenTech LLC
|
||||
Copyright (c) 2025 KrakenTech LLC. All rights reserved.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
Permission is hereby granted to view and use this software for personal,
|
||||
educational, or internal business purposes only, subject to the following
|
||||
restrictions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
1. You may NOT distribute, sublicense, publish, sell, resell, or otherwise
|
||||
make this software available to any third party, in whole or in part,
|
||||
in source or binary form.
|
||||
|
||||
2. You may NOT create derivative works for distribution or commercial use.
|
||||
|
||||
3. You may NOT use this software to provide a competing product or service.
|
||||
|
||||
4. Modifications for personal or internal use are permitted, provided they
|
||||
are not distributed.
|
||||
|
||||
5. This copyright notice and license must be included in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
IMPLIED. IN NO EVENT SHALL KRAKENTECH LLC BE LIABLE FOR ANY CLAIM, DAMAGES
|
||||
OR OTHER LIABILITY ARISING FROM THE USE OF THIS SOFTWARE.
|
||||
|
||||
For commercial licensing, redistribution rights, or other inquiries,
|
||||
contact: licensing@krkn.tech
|
||||
|
||||
@@ -1,15 +1,35 @@
|
||||
# RMM-Hunter
|
||||
|
||||
A comprehensive Windows security tool designed to detect and analyze Remote Monitoring and Management (RMM) software deployments across enterprise environments.
|
||||
A comprehensive Windows security tool designed to detect and analyze Remote Monitoring and Management (RMM) software deployments.
|
||||
|
||||

|
||||

|
||||
|
||||
## Overview
|
||||
|
||||
RMM-Hunter is a forensic analysis tool that identifies potentially malicious or unauthorized Remote Monitoring and Management software on Windows systems. Built on a proprietary detection framework called **Scurvy** (private repository), RMM-Hunter provides security teams with comprehensive visibility into RMM installations that may pose security risks or compliance concerns.
|
||||
RMM-Hunter is an analysis tool that identifies potentially malicious or unauthorized Remote Monitoring and Management software/connections on Windows systems. Built on **Scurvy**, a custom low-level OS exploitation repository (private), RMM-Hunter provides security teams with comprehensive visibility into RMM installations that may pose security risks or compliance concerns.
|
||||
|
||||
## Features
|
||||
|
||||
### Web Interface
|
||||
|
||||
RMM-Hunter now includes a modern web-based interface for both hunting and elimination operations. Simply double-click the executable to launch the web server, which automatically:
|
||||
|
||||
- **Starts a local web server** on port 80 (http://rmm-hunter)
|
||||
- **Adds a DNS entry** to your Windows hosts file for easy access via `http://rmm-hunter`
|
||||
- **Requests UAC elevation** if administrator privileges are not already granted
|
||||
- **Opens your default browser** automatically to the web interface
|
||||
- **Cleans up the hosts entry** when the application exits
|
||||
|
||||

|
||||
|
||||
The web interface provides:
|
||||
- Real-time hunt execution with live log streaming via WebSockets
|
||||
- Interactive elimination interface with visual feedback
|
||||
- Previous hunt report browsing and analysis
|
||||
- Modern, responsive UI accessible from any browser on the local machine
|
||||
|
||||

|
||||
|
||||
### Hunt Module
|
||||
|
||||
The hunt module performs deep system analysis across multiple detection vectors:
|
||||
@@ -21,8 +41,10 @@ The hunt module performs deep system analysis across multiple detection vectors:
|
||||
- **Network Connection Monitoring** - Identifies active outbound connections to known RMM infrastructure
|
||||
- **Scheduled Task Detection** - Discovers RMM-related scheduled tasks used for persistence
|
||||
- **Directory Scanning** - Searches for RMM installation directories and artifacts
|
||||
- **AutoRun Analysis** - Searches for RMM persistence via Windows AutoRuns utilzing COM Services and Registry Keys
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
### Detection Capabilities
|
||||
|
||||
@@ -42,15 +64,13 @@ RMM-Hunter generates comprehensive reports in multiple formats:
|
||||
- **JSON** - Machine-readable format for integration with SIEM and automation platforms
|
||||
- **HTML** - Interactive web-based report with filtering and search capabilities
|
||||
|
||||

|
||||
|
||||
The HTML report includes:
|
||||
- Executive summary with detection statistics
|
||||
- Detailed findings across all detection categories
|
||||
- Metadata including detection time and system information
|
||||
- Built-in search and filter functionality for large result sets
|
||||
|
||||

|
||||

|
||||
|
||||
## Installation
|
||||
|
||||
@@ -58,6 +78,7 @@ The HTML report includes:
|
||||
|
||||
- Windows Operating System (Windows 10/11 or Windows Server 2016+)
|
||||
- Administrator privileges (required for service and process enumeration)
|
||||
- The application will automatically request UAC elevation if not running as administrator
|
||||
- Go 1.24+ (for building from source)
|
||||
|
||||
### Binary Download
|
||||
@@ -74,9 +95,31 @@ The Scurvy Library is not publicly accessible making building this tool from sou
|
||||
|
||||
## Usage
|
||||
|
||||
### Hunt Mode
|
||||
### Web Interface (Recommended)
|
||||
|
||||
Execute a comprehensive system scan:
|
||||
Launch the web interface by simply running the executable without arguments:
|
||||
|
||||
```powershell
|
||||
.\rmm-hunter.exe
|
||||
```
|
||||
|
||||
This will:
|
||||
1. Check for administrator privileges and request UAC elevation if needed
|
||||
2. Start a web server on port 80
|
||||
3. Add `rmm-hunter` to your hosts file (pointing to 127.0.0.1)
|
||||
4. Automatically open your browser to `http://rmm-hunter`
|
||||
|
||||
From the web interface, you can:
|
||||
- Execute hunts with real-time progress monitoring
|
||||
- View and analyze previous hunt reports
|
||||
- Perform elimination operations on detected RMM software
|
||||
- Access all functionality through an intuitive browser-based UI
|
||||
|
||||
The hosts file entry is automatically cleaned up when you exit the application.
|
||||
|
||||
### Hunt Mode (CLI)
|
||||
|
||||
Execute a comprehensive system scan from the command line:
|
||||
|
||||
```powershell
|
||||
powershell .\rmm-hunter.exe hunt
|
||||
@@ -86,7 +129,7 @@ With custom output file:
|
||||
|
||||
```powershell
|
||||
powershell .\rmm-hunter.exe hunt --output custom-report.json
|
||||
```
|
||||
```
|
||||
|
||||
Exclude specific RMM tools from detection:
|
||||
```powershell
|
||||
@@ -95,29 +138,92 @@ powershell .\rmm-hunter.exe hunt --exclude TeamViewer,AnyDesk
|
||||
|
||||
### Eliminate Mode
|
||||
|
||||
**Status: Under Construction**
|
||||
The elimination module provides both web-based and command-line interfaces for removing detected RMM installations from your system. Both interfaces automatically request UAC elevation if administrator privileges are required.
|
||||
|
||||
The elimination module is currently under active development. This functionality will provide automated remediation capabilities for detected RMM installations.
|
||||
#### Web Interface
|
||||
|
||||
Planned features:
|
||||
- Service termination and removal
|
||||
- Process termination
|
||||
- Binary deletion
|
||||
- Registry cleanup
|
||||
- Scheduled task removal
|
||||
- Backup and rollback capabilities
|
||||
The web interface provides a modern, browser-based elimination experience:
|
||||
|
||||
```powershell
|
||||
.\rmm-hunter.exe
|
||||
```
|
||||
|
||||
Or explicitly launch the web-based elimination interface:
|
||||
|
||||
```powershell
|
||||
powershell .\rmm-hunter.exe eliminate --web
|
||||
```
|
||||
|
||||
The web interface offers:
|
||||
- Visual representation of all detected RMM components
|
||||
- Real-time elimination with progress feedback
|
||||
- Dependency checking to prevent system instability
|
||||
- Interactive browsing of previous hunt reports
|
||||
- Live log streaming during operations
|
||||
|
||||
#### CLI Component
|
||||
|
||||
Launch the interactive CLI elimination interface:
|
||||
|
||||
```powershell
|
||||
powershell .\rmm-hunter.exe eliminate --cli
|
||||
```
|
||||
|
||||
The CLI component operates through a multi-stage interactive workflow designed to provide granular control over the elimination process. When launched, the interface guides you through the following stages:
|
||||
|
||||
**Stage 1: Report Selection**
|
||||
|
||||
The interface scans the current directory for JSON hunt reports and presents them in a navigable list. You can browse available reports using arrow keys and select one by pressing Enter. The file picker automatically filters for valid JSON files generated by previous hunt operations.
|
||||
|
||||
**Stage 2: Category Selection**
|
||||
|
||||
After loading a report, you are presented with seven elimination categories corresponding to the detection vectors from the hunt module. Each category is accessible via numeric keys (1-7):
|
||||
|
||||
1. AutoRuns - Registry-based persistence mechanisms
|
||||
2. Binaries - Executable files on disk
|
||||
3. Connections - Active network connections
|
||||
4. Directories - Installation directories
|
||||
5. Processes - Running processes
|
||||
6. Scheduled Tasks - Task Scheduler entries
|
||||
7. Services - Windows services
|
||||
|
||||
**Stage 3: Item List View**
|
||||
|
||||
Upon selecting a category, the interface displays all detected items within that category. Each item shows relevant identifying information such as process names, file paths, service names, or connection details. Items that have already been eliminated are marked with a checkmark and displayed in green to provide visual feedback on remediation progress. You can navigate through the list using arrow keys and select an item for detailed inspection by pressing Enter.
|
||||
|
||||
**Stage 4: Detail View and Elimination**
|
||||
|
||||
The detail view presents comprehensive information about the selected item, including all metadata collected during the hunt phase. For each item type, the interface displays specific details:
|
||||
|
||||
For processes, you see the process name, PID, parent PID, command-line arguments, creation time, and executable path. For services, the display includes service name, display name, service type, start type, binary path, start account, and description. For autoruns, you see the entry name, launch string, registry location, image path, arguments, and file hashes (MD5, SHA1, SHA256). For binaries and directories, the full path is shown. For network connections, local and remote addresses, remote hostname, connection state, associated PID, and process name are displayed. For scheduled tasks, the name, author, state, enabled status, last result, next run time, last run time, and task path are presented.
|
||||
|
||||
From the detail view, pressing the exclamation mark (!) key initiates the elimination process for that specific item. The system performs intelligent dependency checking before elimination to prevent system instability.
|
||||
|
||||
**Dependency Validation**
|
||||
|
||||
Before eliminating binaries or directories, the system checks whether any active processes or enabled services are currently using those resources. If a dependency is detected, a warning modal appears explaining the conflict and suggesting the proper elimination order. For example, if you attempt to delete a binary that is currently in use by a running process, the system will warn you to eliminate the process first. Similarly, if a directory contains binaries used by active services, you must stop and remove those services before the directory can be deleted.
|
||||
|
||||

|
||||
|
||||
**Elimination Actions**
|
||||
|
||||
Each category type performs specific elimination operations:
|
||||
|
||||
Processes are terminated using their PID. Services are stopped and then deleted from the service control manager. Binaries are removed from the filesystem. Directories are recursively deleted along with all contents. AutoRun entries are removed from their respective registry locations. Scheduled tasks are disabled and then deleted from the Task Scheduler. Network connections result in the creation of Windows Firewall outbound block rules for the remote host, preventing future connections to that destination.
|
||||
|
||||

|
||||
|
||||
**State Persistence**
|
||||
|
||||
After each successful elimination, the system updates the JSON report file to mark the item as eliminated. This ensures that if you exit and restart the elimination interface, previously eliminated items remain marked and visually distinguished. The persistent state allows you to work through large result sets across multiple sessions without losing track of your progress.
|
||||
|
||||
**Navigation**
|
||||
|
||||
Throughout the interface, you can navigate backward using the left arrow key to return to the previous screen. Pressing 'q', 'Esc', or 'Ctrl+C' at any point will exit the application. The interface provides contextual help at each stage, displaying available keyboard shortcuts and actions.
|
||||
|
||||
## Architecture
|
||||
|
||||
RMM-Hunter is built on **Scurvy**, a proprietary Windows system analysis framework (private repository). Scurvy provides the core capabilities for:
|
||||
|
||||
- Low-level Windows API interactions
|
||||
- Process and service management
|
||||
- Registry operations
|
||||
- Network connection enumeration
|
||||
- WMI query execution
|
||||
|
||||
The modular architecture allows for extensible detection capabilities while maintaining performance and stability.
|
||||
RMM-Hunter is built on **Scurvy**, a custom low-level OS exploitation repository (private). Scurvy provides the core capabilities for low-level Windows API interactions, process and service management, registry operations, network connection enumeration, and WMI query execution. The modular architecture allows for extensible detection capabilities while maintaining performance and stability.
|
||||
|
||||
## Output Formats
|
||||
|
||||
@@ -144,17 +250,11 @@ RMM-Hunter employs multiple detection strategies:
|
||||
|
||||
## Limitations
|
||||
|
||||
- Requires administrative privileges for complete system visibility
|
||||
- May generate false positives in environments with legitimate RMM deployments
|
||||
- Network detection requires active connections at scan time
|
||||
- Elimination functionality not yet available
|
||||
Requires administrative privileges for complete system visibility (UAC elevation prompt will appear if needed). May generate false positives in environments with legitimate RMM deployments. Network detection requires active connections at scan time. The web server requires port 80 to be available on the local machine.
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome. Please submit pull requests with:
|
||||
- Detailed description of changes
|
||||
- Test coverage for new detection signatures
|
||||
- Documentation updates
|
||||
Contributions are welcome. Please submit pull requests with detailed descriptions of changes, test coverage for new detection signatures, and documentation updates.
|
||||
|
||||
## License
|
||||
|
||||
@@ -180,7 +280,9 @@ This tool is intended for authorized security assessments and forensic analysis
|
||||
|
||||
For issues, questions, or feature requests, please open an issue on the GitHub repository.
|
||||
|
||||
---
|
||||
**Note**: The underlying Scurvy repository is a custom low-level OS exploitation framework that is not publicly accessible and is maintained privately.
|
||||
|
||||
**Note**: The underlying Scurvy framework is not publicly accessible and is maintained in a private repository.
|
||||
## Any.Run Submission
|
||||
v1.2.0: https://app.any.run/tasks/03b6afcd-308c-4056-bafc-e6514185d922
|
||||

|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"rmm-hunter/internal/pkg"
|
||||
"rmm-hunter/internal/pkg/hunter"
|
||||
"rmm-hunter/internal/tui"
|
||||
"rmm-hunter/internal/web"
|
||||
|
||||
scurvy "github.com/Kraken-OffSec/Scurvy"
|
||||
"github.com/Kraken-OffSec/Scurvy/core/escalator"
|
||||
@@ -145,9 +146,8 @@ func runHunt() {
|
||||
|
||||
func runEliminate() {
|
||||
if webUI {
|
||||
// Launch the web UI for elimination flow
|
||||
// TODO: Launch web UI
|
||||
fmt.Println("Web UI not implemented yet")
|
||||
fmt.Println("Starting Web UI on http://127.0.0.1:8080 ...")
|
||||
web.StartWebServer()
|
||||
return
|
||||
} else if cliUI {
|
||||
// Launch the TUI for elimination flow
|
||||
|
||||
@@ -30,6 +30,7 @@ require (
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||
github.com/ethereum/go-ethereum v1.14.12 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.1 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
@@ -49,6 +50,7 @@ require (
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
golang.org/x/crypto v0.31.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa // indirect
|
||||
golang.org/x/net v0.27.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||
howett.net/plist v1.0.1 // indirect
|
||||
|
||||
@@ -57,6 +57,8 @@ github.com/ethereum/go-ethereum v1.14.12 h1:8hl57x77HSUo+cXExrURjU/w1VhL+ShCTJrT
|
||||
github.com/ethereum/go-ethereum v1.14.12/go.mod h1:RAC2gVMWJ6FkxSPESfbshrcKpIokgQKsVKmAuqdekDY=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
||||
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
@@ -102,6 +104,8 @@ golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ=
|
||||
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE=
|
||||
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
||||
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
|
||||
@@ -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 (<string ID>)
|
||||
`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 (<string ID>)
|
||||
`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)
|
||||
}
|
||||
|
||||
@@ -7,52 +7,106 @@ import (
|
||||
"rmm-hunter/internal/pkg/hunt/detect/common"
|
||||
. "rmm-hunter/internal/suspicious"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var appData = os.Getenv("APPDATA")
|
||||
var userProfile = os.Getenv("USERPROFILE")
|
||||
|
||||
const numWorkers = 5
|
||||
|
||||
type searchJob struct {
|
||||
basePath string
|
||||
rmmDir string
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
// Create channels
|
||||
jobs := make(chan searchJob, 100)
|
||||
results := make(chan Directory, 100)
|
||||
|
||||
// WaitGroup to track workers
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// Start worker pool
|
||||
for i := 0; i < numWorkers; i++ {
|
||||
wg.Add(1)
|
||||
go worker(jobs, results, &wg)
|
||||
}
|
||||
|
||||
// Start result collector goroutine
|
||||
var suspiciousDirectories []Directory
|
||||
seen := make(map[string]bool)
|
||||
var resultWg sync.WaitGroup
|
||||
resultWg.Add(1)
|
||||
|
||||
go func() {
|
||||
defer resultWg.Done()
|
||||
for dir := range results {
|
||||
if !seen[dir.Path] {
|
||||
fmt.Printf(" [?] Found %s\n", dir.Path)
|
||||
suspiciousDirectories = append(suspiciousDirectories, dir)
|
||||
seen[dir.Path] = 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
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Send jobs to workers
|
||||
for _, rmmDir := range common.KnownRMMDirectories {
|
||||
for _, basePath := range common.SearchBasePaths {
|
||||
jobs <- searchJob{
|
||||
basePath: basePath,
|
||||
rmmDir: rmmDir,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Close jobs channel and wait for workers to finish
|
||||
close(jobs)
|
||||
wg.Wait()
|
||||
|
||||
// Close results channel and wait for collector to finish
|
||||
close(results)
|
||||
resultWg.Wait()
|
||||
|
||||
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)
|
||||
// worker processes search jobs from the jobs channel
|
||||
func worker(jobs <-chan searchJob, results chan<- Directory, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
|
||||
for job := range jobs {
|
||||
// Replace environment variables
|
||||
basePath := replaceEnvVars(job.basePath)
|
||||
|
||||
// Construct full path
|
||||
fullPath := filepath.Join(basePath, job.rmmDir)
|
||||
|
||||
// Check if this is a prefix pattern (ends with incomplete path like "ScreenConnect Client (")
|
||||
if isPrefix(job.rmmDir) {
|
||||
// Find all directories matching this prefix
|
||||
matches := findPrefixMatches(fullPath)
|
||||
for _, match := range matches {
|
||||
results <- Directory{Path: match}
|
||||
}
|
||||
} else {
|
||||
// Exact match
|
||||
if _, err := os.Stat(fullPath); err == nil {
|
||||
results <- Directory{Path: fullPath}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -10,11 +10,33 @@ import (
|
||||
// EliminateAutoRun removes an autorun entry from the system
|
||||
func EliminateAutoRun(ar AutoRun) error {
|
||||
all := scurvy.ListAutoruns()
|
||||
|
||||
// Try to find by MD5 first
|
||||
for _, a := range all {
|
||||
if a.MD5 == ar.MD5 {
|
||||
// Found it, delete it
|
||||
if a.MD5 == ar.MD5 && a.MD5 != "" {
|
||||
return scurvy.DeleteAutorun(a)
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("%s | %s not found", ar.Location, ar.Entry)
|
||||
|
||||
// If not found by MD5, try to find by location (for registry entries)
|
||||
for _, a := range all {
|
||||
if a.Location == ar.Location && ar.Location != "" {
|
||||
return scurvy.DeleteAutorun(a)
|
||||
}
|
||||
}
|
||||
|
||||
// Build a descriptive error message
|
||||
location := ar.Location
|
||||
if location == "" {
|
||||
location = "unknown location"
|
||||
}
|
||||
entry := ar.Entry
|
||||
if entry == "" {
|
||||
entry = ar.ImageName
|
||||
}
|
||||
if entry == "" {
|
||||
entry = "unknown entry"
|
||||
}
|
||||
|
||||
return fmt.Errorf("autorun entry not found at %s (%s) - it may have already been removed", location, entry)
|
||||
}
|
||||
|
||||
@@ -185,7 +185,7 @@ func (m AppModel) View() string {
|
||||
}
|
||||
}
|
||||
|
||||
// performEliminate routes to placeholder eliminate functions without removing items from data
|
||||
// performEliminate routes to eliminate functions without removing items from data
|
||||
func (m *AppModel) performEliminate(typeKey string, idx int) error {
|
||||
switch typeKey {
|
||||
case "autoruns":
|
||||
|
||||
@@ -108,7 +108,7 @@ func (m DetailViewModel) renderDetails() string {
|
||||
return "Item no longer available"
|
||||
}
|
||||
c := m.data.OutboundConnections[m.index]
|
||||
return fmt.Sprintf("Local: %s\nRemote: %s\nHost: %s\nState: %s\nPID: %s\nProcess: %s\nAction: add firewall block (placeholder)", c.LocalAddr, c.RemoteAddr, c.RemoteHost, c.State, c.PID, c.Process)
|
||||
return fmt.Sprintf("Local: %s\nRemote: %s\nHost: %s\nState: %s\nPID: %s\nProcess: %s\nAction: add firewall block", c.LocalAddr, c.RemoteAddr, c.RemoteHost, c.State, c.PID, c.Process)
|
||||
case "directories":
|
||||
if m.index >= len(m.data.Directories) {
|
||||
return "Item no longer available"
|
||||
@@ -120,19 +120,19 @@ func (m DetailViewModel) renderDetails() string {
|
||||
return "Item no longer available"
|
||||
}
|
||||
p := m.data.Processes[m.index]
|
||||
return fmt.Sprintf("Name: %s\nPID: %d\nPPID: %d\nParent: %s\nArgs: %s\nCreated: %s\nPath: %s\nAction: stop then delete (placeholder)", p.Name, p.PID, p.PPID, p.Parent, p.Args, p.Created, p.Path)
|
||||
return fmt.Sprintf("Name: %s\nPID: %d\nPPID: %d\nParent: %s\nArgs: %s\nCreated: %s\nPath: %s\nAction: stop then delete", p.Name, p.PID, p.PPID, p.Parent, p.Args, p.Created, p.Path)
|
||||
case "scheduledTasks":
|
||||
if m.index >= len(m.data.ScheduledTasks) {
|
||||
return "Item no longer available"
|
||||
}
|
||||
t := m.data.ScheduledTasks[m.index]
|
||||
return fmt.Sprintf("Name: %s\nAuthor: %s\nState: %s\nEnabled: %v\nLastResult: %s\nNextRun: %s\nLastRun: %s\nPath: %s\nAction: disable then delete (placeholder)", t.Name, t.Author, t.State, t.Enabled, t.LastResult, t.NextRun, t.LastRun, t.Path)
|
||||
return fmt.Sprintf("Name: %s\nAuthor: %s\nState: %s\nEnabled: %v\nLastResult: %s\nNextRun: %s\nLastRun: %s\nPath: %s\nAction: disable then delete", t.Name, t.Author, t.State, t.Enabled, t.LastResult, t.NextRun, t.LastRun, t.Path)
|
||||
case "services":
|
||||
if m.index >= len(m.data.Services) {
|
||||
return "Item no longer available"
|
||||
}
|
||||
s := m.data.Services[m.index]
|
||||
return fmt.Sprintf("Name: %s\nDisplay: %s\nType: %s\nStartType: %s\nBinPath: %s\nStartName: %s\nDescription: %s\nAction: stop then delete (placeholder)", s.Name, s.DisplayName, s.ServiceType, s.StartType, s.BinaryPathName, s.ServiceStartName, s.Description)
|
||||
return fmt.Sprintf("Name: %s\nDisplay: %s\nType: %s\nStartType: %s\nBinPath: %s\nStartName: %s\nDescription: %s\nAction: stop then delete", s.Name, s.DisplayName, s.ServiceType, s.StartType, s.BinaryPathName, s.ServiceStartName, s.Description)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
var (
|
||||
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
|
||||
// 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 nil, fmt.Errorf("failed to convert operation string: %w", err)
|
||||
}
|
||||
|
||||
urlPtr, err := syscall.UTF16PtrFromString(url)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert URL string: %w", err)
|
||||
}
|
||||
|
||||
// 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,
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 469 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1 @@
|
||||
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
|
||||
@@ -1,5 +1,574 @@
|
||||
package web
|
||||
|
||||
func StartWebServer() {
|
||||
// TODO: Start web server
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"rmm-hunter/internal/suspicious"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"rmm-hunter/internal/pkg"
|
||||
"rmm-hunter/internal/pkg/hunt/eliminate"
|
||||
"rmm-hunter/internal/pkg/hunter"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
//go:embed templates/*
|
||||
var contentFS embed.FS
|
||||
|
||||
// broadcaster for hunt logs
|
||||
type wsHub struct {
|
||||
mu sync.Mutex
|
||||
conns map[*websocket.Conn]struct{}
|
||||
}
|
||||
|
||||
func newHub() *wsHub { return &wsHub{conns: make(map[*websocket.Conn]struct{})} }
|
||||
func (h *wsHub) add(c *websocket.Conn) { h.mu.Lock(); h.conns[c] = struct{}{}; h.mu.Unlock() }
|
||||
func (h *wsHub) rm(c *websocket.Conn) { h.mu.Lock(); delete(h.conns, c); h.mu.Unlock() }
|
||||
func (h *wsHub) send(msg string) {
|
||||
h.mu.Lock()
|
||||
for c := range h.conns {
|
||||
_ = c.WriteMessage(websocket.TextMessage, []byte(msg))
|
||||
}
|
||||
h.mu.Unlock()
|
||||
}
|
||||
|
||||
// JSONReportMeta is a lightweight descriptor for previous hunts
|
||||
type JSONReportMeta struct {
|
||||
File string `json:"file"`
|
||||
ReportName string `json:"reportName"`
|
||||
GeneratedAt string `json:"generatedAt"`
|
||||
}
|
||||
|
||||
type server struct {
|
||||
hub *wsHub
|
||||
http *http.Server
|
||||
quitCh chan struct{}
|
||||
}
|
||||
|
||||
func StartWebServer() {
|
||||
var hostAdded bool
|
||||
h := newHub()
|
||||
s := &server{hub: h, quitCh: make(chan struct{})}
|
||||
|
||||
// Add hosts file entry for rmm-hunter
|
||||
if err := AddHostsEntry(); err != nil {
|
||||
log.Printf("[web] Warning: Failed to add hosts entry: %v\n", err)
|
||||
} else {
|
||||
hostAdded = true
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/", s.handleIndex)
|
||||
mux.HandleFunc("/logo", s.handleLogo)
|
||||
mux.HandleFunc("/favicon.ico", s.handleFavicon)
|
||||
mux.HandleFunc("/favicon-32x32.png", s.handleFavicon)
|
||||
mux.HandleFunc("/favicon-16x16.png", s.handleFavicon)
|
||||
mux.HandleFunc("/apple-touch-icon.png", s.handleFavicon)
|
||||
mux.HandleFunc("/site.webmanifest", s.handleManifest)
|
||||
mux.HandleFunc("/api/hunts", s.handleListHunts)
|
||||
mux.HandleFunc("/api/hunt/start", s.handleStartHunt)
|
||||
mux.HandleFunc("/api/report", s.handleGetReport)
|
||||
mux.HandleFunc("/api/eliminate", s.handleEliminate)
|
||||
mux.HandleFunc("/api/quit", s.handleQuit)
|
||||
mux.HandleFunc("/ws/hunt", s.handleWS)
|
||||
|
||||
s.http = &http.Server{Addr: ":80", Handler: logRequests(mux)}
|
||||
|
||||
// Determine which URL to open in browser
|
||||
browserURL := "http://rmm-hunter"
|
||||
if !hostAdded {
|
||||
browserURL = "http://127.0.0.1"
|
||||
}
|
||||
|
||||
// Channel to signal when server is ready
|
||||
serverReady := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
// Signal that we're about to start listening
|
||||
close(serverReady)
|
||||
|
||||
if err := s.http.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatalf("listen: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for server to start, then open browser
|
||||
<-serverReady
|
||||
time.Sleep(500 * time.Millisecond) // Give server a moment to fully initialize
|
||||
log.Printf("[web] Opening browser to %s...\n", browserURL)
|
||||
_, err := OpenBrowser(browserURL)
|
||||
if err != nil {
|
||||
log.Printf("[web] Warning: Failed to open browser: %v\n", err)
|
||||
if !hostAdded {
|
||||
log.Printf("[web] Please open your browser and navigate to http://127.0.0.1\n")
|
||||
}
|
||||
log.Printf("[web] Please open your browser and navigate to http://rmm-hunter\n")
|
||||
}
|
||||
|
||||
// block until quit
|
||||
<-s.quitCh
|
||||
|
||||
// Clean up hosts entry on exit
|
||||
log.Printf("[web] Cleaning up hosts entry...\n")
|
||||
if err := RemoveHostsEntry(); err != nil {
|
||||
log.Printf("[web] Warning: Failed to remove hosts entry: %v\n", err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
_ = s.http.Shutdown(ctx)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func logRequests(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("%s %s", r.Method, r.URL.Path)
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *server) handleIndex(w http.ResponseWriter, r *http.Request) {
|
||||
b, err := contentFS.ReadFile("templates/index.html")
|
||||
if err != nil {
|
||||
http.Error(w, "template missing", 500)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
_, _ = w.Write(b)
|
||||
}
|
||||
|
||||
// serve logo from repo .img; fallback to 404
|
||||
func (s *server) handleLogo(w http.ResponseWriter, r *http.Request) {
|
||||
path := filepath.Join(".img", "rmm-hunter.png")
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
w.Header().Set("Content-Type", "image/png")
|
||||
http.ServeContent(w, r, "rmm-hunter.png", time.Now(), f)
|
||||
}
|
||||
|
||||
// serve favicon files from embedded templates folder
|
||||
func (s *server) handleFavicon(w http.ResponseWriter, r *http.Request) {
|
||||
filename := filepath.Base(r.URL.Path)
|
||||
b, err := contentFS.ReadFile("templates/" + filename)
|
||||
if err != nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Set appropriate content type
|
||||
contentType := "image/x-icon"
|
||||
if filepath.Ext(filename) == ".png" {
|
||||
contentType = "image/png"
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
w.Header().Set("Cache-Control", "public, max-age=31536000")
|
||||
w.Write(b)
|
||||
}
|
||||
|
||||
// serve site.webmanifest from embedded templates folder
|
||||
func (s *server) handleManifest(w http.ResponseWriter, r *http.Request) {
|
||||
b, err := contentFS.ReadFile("templates/site.webmanifest")
|
||||
if err != nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/manifest+json")
|
||||
w.Header().Set("Cache-Control", "public, max-age=31536000")
|
||||
w.Write(b)
|
||||
}
|
||||
|
||||
func (s *server) handleListHunts(w http.ResponseWriter, r *http.Request) {
|
||||
files, _ := filepath.Glob("*.json")
|
||||
var out []JSONReportMeta
|
||||
for _, f := range files {
|
||||
// read small head of file to verify
|
||||
b, err := os.ReadFile(f)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
var env struct {
|
||||
ReportName string `json:"reportName"`
|
||||
GeneratedAt string `json:"generatedAt"`
|
||||
}
|
||||
if json.Unmarshal(b, &env) == nil && (env.ReportName != "" || strings.Contains(string(b), "\"findings\"")) {
|
||||
out = append(out, JSONReportMeta{File: f, ReportName: env.ReportName, GeneratedAt: env.GeneratedAt})
|
||||
}
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(out)
|
||||
}
|
||||
|
||||
func (s *server) handleGetReport(w http.ResponseWriter, r *http.Request) {
|
||||
f := r.URL.Query().Get("file")
|
||||
if f == "" || strings.Contains(f, "..") {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": "bad file"})
|
||||
return
|
||||
}
|
||||
b, err := os.ReadFile(f)
|
||||
if err != nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": "not found"})
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(b)
|
||||
}
|
||||
|
||||
func (s *server) handleStartHunt(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": "use POST"})
|
||||
return
|
||||
}
|
||||
name := fmt.Sprintf("hunt-%s", time.Now().Format("20060102-150405"))
|
||||
go s.runHunt(name)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]string{"reportName": name})
|
||||
}
|
||||
|
||||
func (s *server) runHunt(name string) {
|
||||
// redirect stdout to our pipe
|
||||
oldStdout := os.Stdout
|
||||
pr, pw, _ := os.Pipe()
|
||||
os.Stdout = pw
|
||||
// also mirror stderr
|
||||
oldStderr := os.Stderr
|
||||
pr2, pw2, _ := os.Pipe()
|
||||
os.Stderr = pw2
|
||||
|
||||
// reader goroutines
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
sc := bufio.NewScanner(pr)
|
||||
for sc.Scan() {
|
||||
s.hub.send(sc.Text())
|
||||
}
|
||||
done <- struct{}{}
|
||||
}()
|
||||
go func() {
|
||||
sc := bufio.NewScanner(pr2)
|
||||
for sc.Scan() {
|
||||
s.hub.send(sc.Text())
|
||||
}
|
||||
done <- struct{}{}
|
||||
}()
|
||||
|
||||
// run hunter
|
||||
hunter.Start(pkg.RunOptions{Name: name})
|
||||
|
||||
// close writers and restore
|
||||
_ = pw.Close()
|
||||
_ = pw2.Close()
|
||||
<-done
|
||||
<-done
|
||||
os.Stdout = oldStdout
|
||||
os.Stderr = oldStderr
|
||||
s.hub.send("[+] Hunt complete")
|
||||
}
|
||||
|
||||
func (s *server) handleWS(w http.ResponseWriter, r *http.Request) {
|
||||
up := websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }}
|
||||
c, err := up.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
s.hub.add(c)
|
||||
defer func() { s.hub.rm(c); _ = c.Close() }()
|
||||
for { // keep alive until client closes
|
||||
if _, _, err := c.ReadMessage(); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *server) handleEliminate(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": "method not allowed"})
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
ReportFile string `json:"reportFile"`
|
||||
Type string `json:"type"`
|
||||
Index int `json:"index"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": "invalid request"})
|
||||
return
|
||||
}
|
||||
|
||||
// Load the report file
|
||||
reportFile := req.ReportFile
|
||||
if !strings.HasSuffix(reportFile, ".json") {
|
||||
reportFile += ".json"
|
||||
}
|
||||
reportPath := filepath.Join(".", reportFile)
|
||||
data, err := os.ReadFile(reportPath)
|
||||
if err != nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": fmt.Sprintf("failed to read report: %v", err)})
|
||||
return
|
||||
}
|
||||
|
||||
// Parse the full report structure with findings wrapper
|
||||
var fullReport struct {
|
||||
ReportName string `json:"reportName"`
|
||||
GeneratedAt string `json:"generatedAt"`
|
||||
RiskRating interface{} `json:"riskRating"`
|
||||
Findings suspicious.Suspicious `json:"findings"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &fullReport); err != nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": fmt.Sprintf("failed to parse report: %v", err)})
|
||||
return
|
||||
}
|
||||
|
||||
// Perform elimination based on type
|
||||
if err := performElimination(&fullReport.Findings, req.Type, req.Index); err != nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Save updated report
|
||||
updatedData, err := json.MarshalIndent(fullReport, "", " ")
|
||||
if err != nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": fmt.Sprintf("failed to marshal report: %v", err)})
|
||||
return
|
||||
}
|
||||
|
||||
if err := os.WriteFile(reportPath, updatedData, 0644); err != nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": fmt.Sprintf("failed to save report: %v", err)})
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]string{"status": "success"})
|
||||
}
|
||||
|
||||
func (s *server) handleQuit(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": "use POST"})
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"ok":true}`))
|
||||
go func() { time.Sleep(200 * time.Millisecond); s.quitCh <- struct{}{} }()
|
||||
}
|
||||
|
||||
// performElimination executes the elimination logic for a specific finding type and index
|
||||
func performElimination(report *suspicious.Suspicious, typeKey string, idx int) error {
|
||||
switch typeKey {
|
||||
case "connections":
|
||||
if idx < 0 || idx >= len(report.OutboundConnections) {
|
||||
return fmt.Errorf("invalid index: %d (array length: %d)", idx, len(report.OutboundConnections))
|
||||
}
|
||||
conn := report.OutboundConnections[idx]
|
||||
if err := eliminate.EliminateConnection(conn.RemoteHost); err != nil {
|
||||
return err
|
||||
}
|
||||
report.OutboundConnections[idx].Eliminated = true
|
||||
|
||||
case "processes":
|
||||
if idx < 0 || idx >= len(report.Processes) {
|
||||
return fmt.Errorf("invalid index: %d (array length: %d)", idx, len(report.Processes))
|
||||
}
|
||||
proc := report.Processes[idx]
|
||||
if err := eliminate.EliminateProcess(proc); err != nil {
|
||||
return err
|
||||
}
|
||||
report.Processes[idx].Eliminated = true
|
||||
|
||||
case "services":
|
||||
if idx < 0 || idx >= len(report.Services) {
|
||||
return fmt.Errorf("invalid index: %d (array length: %d)", idx, len(report.Services))
|
||||
}
|
||||
svc := report.Services[idx]
|
||||
if svc == nil {
|
||||
return fmt.Errorf("service is nil")
|
||||
}
|
||||
if err := eliminate.EliminateService(*svc); err != nil {
|
||||
return err
|
||||
}
|
||||
report.Services[idx].Eliminated = true
|
||||
|
||||
case "tasks":
|
||||
if idx < 0 || idx >= len(report.ScheduledTasks) {
|
||||
return fmt.Errorf("invalid index: %d (array length: %d)", idx, len(report.ScheduledTasks))
|
||||
}
|
||||
task := report.ScheduledTasks[idx]
|
||||
if task == nil {
|
||||
return fmt.Errorf("task is nil")
|
||||
}
|
||||
if err := eliminate.EliminateScheduledTask(*task); err != nil {
|
||||
return err
|
||||
}
|
||||
report.ScheduledTasks[idx].Eliminated = true
|
||||
|
||||
case "autoruns":
|
||||
if idx < 0 || idx >= len(report.AutoRuns) {
|
||||
return fmt.Errorf("invalid index: %d (array length: %d)", idx, len(report.AutoRuns))
|
||||
}
|
||||
ar := report.AutoRuns[idx]
|
||||
if err := eliminate.EliminateAutoRun(ar); err != nil {
|
||||
return err
|
||||
}
|
||||
report.AutoRuns[idx].Eliminated = true
|
||||
|
||||
case "binaries":
|
||||
if idx < 0 || idx >= len(report.Binaries) {
|
||||
return fmt.Errorf("invalid index: %d (array length: %d)", idx, len(report.Binaries))
|
||||
}
|
||||
bin := report.Binaries[idx]
|
||||
// Check if binary is blocked by active processes/services
|
||||
if err := checkBinaryBlocked(bin.Path, *report); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := eliminate.EliminateBinary(bin.Path); err != nil {
|
||||
return err
|
||||
}
|
||||
report.Binaries[idx].Eliminated = true
|
||||
|
||||
case "directories":
|
||||
if idx < 0 || idx >= len(report.Directories) {
|
||||
return fmt.Errorf("invalid index: %d (array length: %d)", idx, len(report.Directories))
|
||||
}
|
||||
dir := report.Directories[idx]
|
||||
// Check if directory is blocked by active processes/services
|
||||
if err := checkDirectoryBlocked(dir.Path, *report); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := eliminate.EliminateDirectory(dir.Path); err != nil {
|
||||
return err
|
||||
}
|
||||
report.Directories[idx].Eliminated = true
|
||||
|
||||
default:
|
||||
return fmt.Errorf("unknown type: %s", typeKey)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkBinaryBlocked checks if a binary is in use by active processes or services
|
||||
func checkBinaryBlocked(path string, data suspicious.Suspicious) error {
|
||||
normPath := func(p string) string {
|
||||
return strings.ToLower(filepath.Clean(p))
|
||||
}
|
||||
|
||||
np := normPath(path)
|
||||
|
||||
// Check active processes
|
||||
for _, p := range data.Processes {
|
||||
if p.Eliminated {
|
||||
continue
|
||||
}
|
||||
if normPath(p.Path) == np {
|
||||
return fmt.Errorf("binary in use by running process %s (PID %d). Eliminate the process first", p.Name, p.PID)
|
||||
}
|
||||
}
|
||||
|
||||
// Check enabled services
|
||||
for _, s := range data.Services {
|
||||
if s == nil || s.Eliminated {
|
||||
continue
|
||||
}
|
||||
sp := normPath(s.BinaryPathName)
|
||||
if sp == np && !strings.EqualFold(strings.TrimSpace(s.StartType), "disabled") {
|
||||
// Check if service has a running process
|
||||
for _, p := range data.Processes {
|
||||
if p.Eliminated {
|
||||
continue
|
||||
}
|
||||
if normPath(p.Path) == sp {
|
||||
return fmt.Errorf("binary used by active and enabled service %s. Stop/delete the service first", s.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkDirectoryBlocked checks if a directory contains binaries used by active processes or services
|
||||
func checkDirectoryBlocked(dir string, data suspicious.Suspicious) error {
|
||||
normPath := func(p string) string {
|
||||
return strings.ToLower(filepath.Clean(p))
|
||||
}
|
||||
|
||||
dn := normPath(dir)
|
||||
if !strings.HasSuffix(dn, string(filepath.Separator)) {
|
||||
dn += string(filepath.Separator)
|
||||
}
|
||||
|
||||
inDir := func(p string) bool {
|
||||
pp := normPath(p)
|
||||
if pp == "" {
|
||||
return false
|
||||
}
|
||||
return strings.HasPrefix(pp, dn)
|
||||
}
|
||||
|
||||
// Check processes
|
||||
for _, p := range data.Processes {
|
||||
if p.Eliminated {
|
||||
continue
|
||||
}
|
||||
if inDir(p.Path) {
|
||||
return fmt.Errorf("directory contains active process %s (PID %d). Eliminate the process first", p.Name, p.PID)
|
||||
}
|
||||
}
|
||||
|
||||
// Check services
|
||||
for _, s := range data.Services {
|
||||
if s == nil || s.Eliminated {
|
||||
continue
|
||||
}
|
||||
if inDir(s.BinaryPathName) && !strings.EqualFold(strings.TrimSpace(s.StartType), "disabled") {
|
||||
// Check if service has a running process
|
||||
for _, p := range data.Processes {
|
||||
if p.Eliminated {
|
||||
continue
|
||||
}
|
||||
if normPath(p.Path) == normPath(s.BinaryPathName) {
|
||||
return fmt.Errorf("directory contains active and enabled service binary for %s. Stop/delete the service first", s.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,9 +1,23 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"rmm-hunter/cmd"
|
||||
"rmm-hunter/internal/web"
|
||||
|
||||
scurvy "github.com/Kraken-OffSec/Scurvy"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) == 1 {
|
||||
escErr := scurvy.CheckAndEscalateBinary()
|
||||
if escErr != nil {
|
||||
fmt.Printf("Failed to elevate: %v\n", escErr)
|
||||
os.Exit(1)
|
||||
}
|
||||
web.StartWebServer()
|
||||
return
|
||||
}
|
||||
cmd.Execute()
|
||||
}
|
||||
|
||||