- SOPS+age credential vault created (59 encrypted files, separate repo) - Updated CLAUDE.md credential access to reference SOPS vault - Updated memory for ACG-5070 (Windows 11, replaces CachyOS) - SC-Syncro sync script: enriched 410 SC sessions with company/device data - Syncro scripts: SC property updater, SC deployer, rogue SC killer - Session log with full details Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
440 lines
18 KiB
PowerShell
440 lines
18 KiB
PowerShell
# 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."
|
|
}
|