# Syncro Script: Find and Remove Rogue ScreenConnect Instances # # Detects ScreenConnect/ConnectWise Control services that do NOT connect # to our instance (computerguru.screenconnect.com). Collects forensic # evidence, creates a security ticket with attachments, then removes # the unauthorized instance. # # Syncro Platform Variables (set in script editor): # $OrgName - platform - {{customer_business_name_or_customer_name}} # # File Type: PowerShell | Run as: System | Max Run Time: 10 minutes Import-Module $env:SyncroModule $OurHost = "computerguru.screenconnect.com" $Found = 0 $Killed = 0 $EvidenceDir = "$env:TEMP\RogueSC_$(Get-Date -Format 'yyyyMMdd_HHmmss')" $RogueInstances = @() Write-Host "=== Rogue ScreenConnect Detection ===" Write-Host "Legitimate host: $OurHost" Write-Host "" # ============================================================================ # Find all ScreenConnect services # ============================================================================ $scServices = Get-Service -Name "ScreenConnect Client*" -ErrorAction SilentlyContinue if (-not $scServices) { Write-Host "No ScreenConnect services found." exit 0 } foreach ($svc in $scServices) { $serviceName = $svc.Name Write-Host "Found service: $serviceName (Status: $($svc.Status))" # Get the ImagePath from registry to find the install location $regPath = "HKLM:\SYSTEM\CurrentControlSet\Services\$serviceName" $imagePath = (Get-ItemProperty -Path $regPath -ErrorAction SilentlyContinue).ImagePath if (-not $imagePath) { Write-Host " [WARNING] Could not read ImagePath - skipping" continue } # Clean up the image path (remove quotes, get directory) $exePath = ($imagePath -replace '"', '').Split(' ')[0] $installDir = Split-Path -Parent $exePath # Check the service launch parameters for the host $connectedHost = "unknown" # Method 1: Check registry ImagePath for host parameter if ($imagePath -match 'h=([^&\s"]+)') { $connectedHost = $Matches[1] } # Method 2: Check config/xml files in install directory if ($connectedHost -eq "unknown" -and (Test-Path $installDir)) { foreach ($pattern in @("*.xml", "*.config")) { Get-ChildItem -Path $installDir -Filter $pattern -ErrorAction SilentlyContinue | ForEach-Object { $content = Get-Content $_.FullName -Raw -ErrorAction SilentlyContinue if ($content -match 'h=([^&\s<"]+)') { $connectedHost = $Matches[1] } } if ($connectedHost -ne "unknown") { break } } } # Method 3: Check binary strings if ($connectedHost -eq "unknown" -and (Test-Path $exePath)) { try { $content = [System.IO.File]::ReadAllText($exePath) if ($content -match 'h=([a-zA-Z0-9\.\-]+\.screenconnect\.com)') { $connectedHost = $Matches[1] } elseif ($content -match 'h=([a-zA-Z0-9\.\-]+)') { $connectedHost = $Matches[1] } } catch {} } Write-Host " Connected to: $connectedHost" # Determine if this is ours if ($connectedHost -like "*$OurHost*" -or $connectedHost -eq $OurHost) { Write-Host " [OK] This is our instance - keeping." continue } # ====================================================================== # ROGUE INSTANCE DETECTED - Collect forensic evidence before removal # ====================================================================== $Found++ Write-Host " [ROGUE] Unauthorized ScreenConnect instance!" Write-Host " Collecting forensic evidence..." # Create evidence directory if (-not (Test-Path $EvidenceDir)) { New-Item -ItemType Directory -Path $EvidenceDir -Force | Out-Null } $instanceDir = Join-Path $EvidenceDir "instance_$Found" New-Item -ItemType Directory -Path $instanceDir -Force | Out-Null # --- Evidence Collection --- $evidence = [ordered]@{ Timestamp = (Get-Date -Format "yyyy-MM-dd HH:mm:ss UTC" -AsUTC) ComputerName = $env:COMPUTERNAME ServiceName = $serviceName ServiceStatus = $svc.Status.ToString() ServiceStartType = $svc.StartType.ToString() ConnectedHost = $connectedHost ImagePath = $imagePath InstallDirectory = $installDir ExePath = $exePath } # Service account info $svcWmi = Get-CimInstance Win32_Service -Filter "Name='$serviceName'" -ErrorAction SilentlyContinue if ($svcWmi) { $evidence["ServiceAccount"] = $svcWmi.StartName $evidence["ServicePID"] = $svcWmi.ProcessId $evidence["ServicePath"] = $svcWmi.PathName } # File details if (Test-Path $exePath) { $fileInfo = Get-Item $exePath -ErrorAction SilentlyContinue $evidence["ExeSize"] = "$([math]::Round($fileInfo.Length/1KB, 2)) KB" $evidence["ExeCreated"] = $fileInfo.CreationTimeUtc.ToString("yyyy-MM-dd HH:mm:ss UTC") $evidence["ExeModified"] = $fileInfo.LastWriteTimeUtc.ToString("yyyy-MM-dd HH:mm:ss UTC") # Digital signature $sig = Get-AuthenticodeSignature $exePath -ErrorAction SilentlyContinue if ($sig) { $evidence["ExeSignatureStatus"] = $sig.Status.ToString() $evidence["ExeSignerSubject"] = $sig.SignerCertificate.Subject $evidence["ExeSignerIssuer"] = $sig.SignerCertificate.Issuer $evidence["ExeSignerThumbprint"] = $sig.SignerCertificate.Thumbprint } # File hash $hash = Get-FileHash $exePath -Algorithm SHA256 -ErrorAction SilentlyContinue if ($hash) { $evidence["ExeSHA256"] = $hash.Hash } } # Install date from registry $uninstallPaths = @( "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall", "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall" ) foreach ($uPath in $uninstallPaths) { if (-not (Test-Path $uPath)) { continue } Get-ChildItem $uPath -ErrorAction SilentlyContinue | ForEach-Object { $props = Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue if ($props.DisplayName -match "ScreenConnect|ConnectWise Control" -and $props.UninstallString -notmatch [regex]::Escape($OurHost)) { $evidence["InstallerDisplayName"] = $props.DisplayName $evidence["InstallerVersion"] = $props.DisplayVersion $evidence["InstallerPublisher"] = $props.Publisher $evidence["InstallerInstallDate"] = $props.InstallDate $evidence["InstallerProductCode"] = $_.PSChildName $evidence["InstallerUninstallCmd"] = $props.UninstallString } } } # Save evidence summary $evidenceTxt = ($evidence.GetEnumerator() | ForEach-Object { "$($_.Key): $($_.Value)" }) -join "`r`n" $evidenceTxt | Out-File (Join-Path $instanceDir "evidence-summary.txt") -Encoding UTF8 # Copy install directory contents (configs, logs -- not the binary itself to save space) if (Test-Path $installDir) { $artifactDir = Join-Path $instanceDir "install_files" New-Item -ItemType Directory -Path $artifactDir -Force | Out-Null Get-ChildItem -Path $installDir -ErrorAction SilentlyContinue | ForEach-Object { if ($_.Length -lt 5MB) { Copy-Item $_.FullName -Destination $artifactDir -Force -ErrorAction SilentlyContinue } else { # For large files, just log metadata "SKIPPED (too large): $($_.Name) - $([math]::Round($_.Length/1MB,2)) MB" | Out-File (Join-Path $artifactDir "_skipped_files.txt") -Append -Encoding UTF8 } } } # Export relevant Windows Event Logs Write-Host " Collecting event logs..." # Application log - SC install/service events try { $appEvents = Get-WinEvent -FilterHashtable @{ LogName = 'Application' StartTime = (Get-Date).AddDays(-30) } -ErrorAction SilentlyContinue | Where-Object { $_.Message -match "ScreenConnect|ConnectWise Control" -or $_.ProviderName -match "MsiInstaller" } | Select-Object TimeCreated, Id, ProviderName, LevelDisplayName, Message -First 100 if ($appEvents) { $appEvents | Export-Csv (Join-Path $instanceDir "eventlog-application.csv") -NoTypeInformation -Encoding UTF8 } } catch {} # System log - service start/stop events try { $sysEvents = Get-WinEvent -FilterHashtable @{ LogName = 'System' Id = @(7034, 7035, 7036, 7040, 7045) StartTime = (Get-Date).AddDays(-30) } -ErrorAction SilentlyContinue | Where-Object { $_.Message -match "ScreenConnect" } | Select-Object TimeCreated, Id, ProviderName, LevelDisplayName, Message -First 100 if ($sysEvents) { $sysEvents | Export-Csv (Join-Path $instanceDir "eventlog-system.csv") -NoTypeInformation -Encoding UTF8 } } catch {} # Security log - logon events around install time if ($evidence["InstallerInstallDate"]) { try { $installDate = [DateTime]::ParseExact($evidence["InstallerInstallDate"], "yyyyMMdd", $null) $secEvents = Get-WinEvent -FilterHashtable @{ LogName = 'Security' Id = @(4624, 4625, 4648, 4672) StartTime = $installDate.AddHours(-2) EndTime = $installDate.AddHours(2) } -ErrorAction SilentlyContinue | Select-Object TimeCreated, Id, LevelDisplayName, Message -First 200 if ($secEvents) { $secEvents | Export-Csv (Join-Path $instanceDir "eventlog-security-around-install.csv") -NoTypeInformation -Encoding UTF8 } } catch {} } # Active network connections at time of detection try { $netConns = Get-NetTCPConnection -State Established -ErrorAction SilentlyContinue | Select-Object LocalAddress, LocalPort, RemoteAddress, RemotePort, OwningProcess, @{N='ProcessName';E={(Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue).ProcessName}} $netConns | Export-Csv (Join-Path $instanceDir "active-connections.csv") -NoTypeInformation -Encoding UTF8 } catch {} # Running processes at time of detection try { Get-Process | Select-Object Id, ProcessName, Path, StartTime, Company | Export-Csv (Join-Path $instanceDir "running-processes.csv") -NoTypeInformation -Encoding UTF8 } catch {} # Recent user logins try { $loginEvents = Get-WinEvent -FilterHashtable @{ LogName = 'Security' Id = @(4624) StartTime = (Get-Date).AddDays(-7) } -MaxEvents 50 -ErrorAction SilentlyContinue | Select-Object TimeCreated, @{N='LogonType';E={$_.Properties[8].Value}}, @{N='User';E={"$($_.Properties[6].Value)\$($_.Properties[5].Value)"}}, @{N='SourceIP';E={$_.Properties[18].Value}} $loginEvents | Export-Csv (Join-Path $instanceDir "recent-logins.csv") -NoTypeInformation -Encoding UTF8 } catch {} # Scheduled tasks (attackers sometimes add persistence) try { Get-ScheduledTask -ErrorAction SilentlyContinue | Where-Object { $_.Actions.Execute -match "ScreenConnect|ConnectWise" -or $_.TaskPath -match "ScreenConnect|ConnectWise" } | Select-Object TaskName, TaskPath, State, @{N='Action';E={$_.Actions.Execute}}, @{N='Arguments';E={$_.Actions.Arguments}} | Export-Csv (Join-Path $instanceDir "scheduled-tasks.csv") -NoTypeInformation -Encoding UTF8 } catch {} # Collect the full service registry export try { reg export "HKLM\SYSTEM\CurrentControlSet\Services\$serviceName" (Join-Path $instanceDir "service-registry.reg") /y 2>&1 | Out-Null } catch {} # Add to rogue list for ticket $RogueInstances += $evidence # ====================================================================== # REMOVE the rogue instance # ====================================================================== Write-Host " Stopping and removing rogue instance..." # Stop the service try { Stop-Service -Name $serviceName -Force -ErrorAction Stop } catch { Get-Process -Name "ScreenConnect.ClientService" -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue } # Disable it Set-Service -Name $serviceName -StartupType Disabled -ErrorAction SilentlyContinue # Uninstall via MSI $uninstalled = $false foreach ($uPath in $uninstallPaths) { if (-not (Test-Path $uPath)) { continue } Get-ChildItem $uPath -ErrorAction SilentlyContinue | ForEach-Object { $props = Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue if ($props.DisplayName -match "ScreenConnect|ConnectWise Control" -and $props.UninstallString -and $props.UninstallString -notmatch [regex]::Escape($OurHost)) { if ($props.UninstallString -match "\{[0-9A-Fa-f\-]+\}") { $productCode = $Matches[0] Start-Process msiexec.exe -ArgumentList "/x $productCode /qn /norestart" -Wait | Out-Null $uninstalled = $true } } } } # Fallback: nuke install directory if (-not $uninstalled -and (Test-Path $installDir)) { Remove-Item -Path $installDir -Recurse -Force -ErrorAction SilentlyContinue } # Remove service registration sc.exe delete $serviceName 2>&1 | Out-Null $Killed++ Write-Host " [REMOVED] Rogue instance cleaned up." Write-Host "" } # ============================================================================ # Package evidence and create ticket if rogue instances found # ============================================================================ if ($Found -gt 0) { Write-Host "=== Packaging Evidence ===" # Build ticket body $ticketBody = @" ## SECURITY INCIDENT: Rogue ScreenConnect Detected **Computer:** $env:COMPUTERNAME **Detection Time:** $(Get-Date -Format "yyyy-MM-dd HH:mm:ss") **Customer:** $OrgName **Instances Found:** $Found **Instances Removed:** $Killed ### Rogue Instance Details "@ foreach ($inst in $RogueInstances) { $ticketBody += @" --- **Service:** $($inst.ServiceName) **Connected To:** $($inst.ConnectedHost) **Install Directory:** $($inst.InstallDirectory) **Exe Created:** $($inst.ExeCreated) **Exe SHA256:** $($inst.ExeSHA256) **Signature:** $($inst.ExeSignatureStatus) ($($inst.ExeSignerSubject)) **Installer Date:** $($inst.InstallerInstallDate) **Installer Product:** $($inst.InstallerDisplayName) v$($inst.InstallerVersion) **Service Account:** $($inst.ServiceAccount) "@ } $ticketBody += @" ### Actions Taken - Forensic evidence collected (event logs, network connections, processes, install files) - Service stopped and disabled - Software uninstalled / install directory removed - Service registration deleted - Evidence package attached to this ticket ### Recommended Follow-Up 1. Review the attached evidence package for indicators of compromise 2. Check the security event logs around the install date for unauthorized access 3. Verify no other persistence mechanisms remain (scheduled tasks, startup items) 4. Consider password resets for accounts on this machine 5. Check other machines at this customer for similar rogue instances 6. Determine if this was an authorized install by another vendor or an actual breach "@ # Create zip of evidence $zipPath = "$EvidenceDir.zip" try { Compress-Archive -Path "$EvidenceDir\*" -DestinationPath $zipPath -Force Write-Host "Evidence packaged: $zipPath" } catch { Write-Host "[WARNING] Could not create zip: $_" } # Create Syncro ticket Write-Host "Creating security ticket..." $ticketResult = Create-Syncro-Ticket -Subject "SECURITY: Rogue ScreenConnect on $env:COMPUTERNAME" -IssueType "Security" -Status "New" -Body $ticketBody # Upload evidence zip to the ticket if ($ticketResult -and (Test-Path $zipPath)) { try { Upload-File -FilePath $zipPath -TicketIdOrNumber $ticketResult.ticket.id Write-Host "Evidence attached to ticket." } catch { Write-Host "[WARNING] Could not attach evidence to ticket: $_" # Fallback: upload to asset files try { Upload-File -FilePath $zipPath Write-Host "Evidence uploaded to asset files instead." } catch { Write-Host "[WARNING] Could not upload evidence at all. Local copy at: $zipPath" } } } # RMM Alert Rmm-Alert -Category "Security" -Body "ROGUE SCREENCONNECT on $env:COMPUTERNAME - $Found instance(s) connecting to: $(($RogueInstances | ForEach-Object { $_.ConnectedHost }) -join ', '). Evidence collected. Ticket created. Instances removed." # Activity log Log-Activity -Message "SECURITY: Removed $Killed rogue ScreenConnect instance(s). Ticket created with forensic evidence." -EventName "Security" Write-Host "" Write-Host "[ALERT] Security ticket created with full evidence package." # Cleanup local evidence (zip is uploaded, raw files no longer needed) Remove-Item -Path $EvidenceDir -Recurse -Force -ErrorAction SilentlyContinue } else { Write-Host "" Write-Host "=== Summary ===" Write-Host "Total SC services found: $($scServices.Count)" Write-Host "All clear - no rogue instances." }