Last active
May 19, 2026 13:51
-
-
Save Fazzani/f2627d3f340f16643f41c7cab7a6b772 to your computer and use it in GitHub Desktop.
CTI Advisory #002 — CVE-2026-45321 — TLP:AMBER # Threat actor: TeamPCP (DeadCatx3 / PCPcat / ShellForce / CipherForce) # CVSS 9.6 Critical
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # ============================================================ | |
| # Invoke-MiniShaiHuludCheck.ps1 | |
| # CTI Advisory #002 — CVE-2026-45321 — TLP:AMBER | |
| # Threat actor: TeamPCP (DeadCatx3 / PCPcat / ShellForce / CipherForce) | |
| # CVSS 9.6 Critical | |
| # | |
| # Validates whether REAL artefacts are present on disk — | |
| # not just traces in PowerShell history. | |
| # | |
| # Designed to run via one-liner (Gist): | |
| # Windows (PowerShell): irm <gist_raw_url> | iex | |
| # macOS / Linux: curl -fsSL <gist_raw_url> | pwsh | |
| # | |
| # ⚠ CRITICAL SAFETY ORDER: | |
| # ISOLATE machine → IMAGE → KILL DAEMON → REVOKE secrets → ROTATE | |
| # DO NOT revoke tokens before network isolation. | |
| # The worm watchdog triggers data wipe on token revocation. | |
| # ============================================================ | |
| Set-StrictMode -Version Latest | |
| # ── Helpers ───────────────────────────────────────────────────────────────── | |
| function Write-Header { | |
| param([string]$Text) | |
| Write-Host "`n═══════════════════════════════════════════════════════" -ForegroundColor Cyan | |
| Write-Host " $Text" -ForegroundColor Cyan | |
| Write-Host "═══════════════════════════════════════════════════════" -ForegroundColor Cyan | |
| } | |
| function Write-Section { | |
| param([string]$Text) | |
| Write-Host "`n── $Text ─────────────────────────────────────────────" -ForegroundColor DarkCyan | |
| } | |
| function Write-Hit { | |
| param([string]$Severity, [string]$Label, [string]$Detail) | |
| $color = switch ($Severity) { | |
| 'critical' { 'Red' } | |
| 'high' { 'Yellow' } | |
| default { 'Magenta' } | |
| } | |
| Write-Host " [$(($Severity).ToUpper())] $Label" -ForegroundColor $color | |
| if ($Detail) { | |
| Write-Host " → $Detail" -ForegroundColor DarkGray | |
| } | |
| } | |
| function Write-OK { | |
| param([string]$Text) | |
| Write-Host " [OK] $Text" -ForegroundColor Green | |
| } | |
| # ── IOC Definitions ────────────────────────────────────────────────────────── | |
| $PAYLOAD_FILES = @( | |
| @{ Name = 'setup_bun.js'; Severity = 'critical'; Hash = $null } | |
| @{ Name = 'bun_environment.js'; Severity = 'critical'; Hash = $null } | |
| @{ Name = 'router_init.js'; Severity = 'critical'; Hash = 'ab4fcadaec49c03278063dd269ea5eef82d24f2124a8e15d7b90f2fa8601266c' } | |
| @{ Name = 'router_runtime.js'; Severity = 'critical'; Hash = $null } | |
| @{ Name = 'tanstack_runner.js'; Severity = 'critical'; Hash = '2ec78d556d696e208927cc503d48e4b5eb56b31abc2870c2ed2e98d6be27fc96' } | |
| ) | |
| $PERSISTENCE_PATHS = @( | |
| @{ RelPath = '.claude\settings.json'; Severity = 'critical'; Label = '.claude/settings.json (modified by worm)' } | |
| @{ RelPath = '.claude\setup.mjs'; Severity = 'critical'; Label = '.claude/setup.mjs (worm dropper)' } | |
| @{ RelPath = '.claude.json'; Severity = 'critical'; Label = '.claude.json (harvested by worm)' } | |
| @{ RelPath = '.vscode\tasks.json'; Severity = 'critical'; Label = '.vscode/tasks.json (modified by worm)' } | |
| @{ RelPath = '.vscode\setup.mjs'; Severity = 'critical'; Label = '.vscode/setup.mjs (worm dropper)' } | |
| ) | |
| $SUSPICIOUS_PROCESSES = @( | |
| @{ Pattern = 'gh-token-monitor'; Severity = 'critical'; Label = 'gh-token-monitor daemon (persistence)' } | |
| @{ Pattern = 'tanstack'; Severity = 'critical'; Label = 'tanstack_runner process' } | |
| @{ Pattern = 'router_runtime'; Severity = 'critical'; Label = 'router_runtime process' } | |
| ) | |
| $C2_DOMAINS = @( | |
| 'git-tanstack.com' | |
| 'seed1.getsession.org' | |
| 'zero.masscan.cloud' | |
| ) | |
| $SUSPICIOUS_STRINGS = @( | |
| @{ Pattern = 'A Mini Shai-Hulud has Appeared'; Severity = 'critical' } | |
| @{ Pattern = 'Sha1-Hulud: The Second Coming'; Severity = 'critical' } | |
| @{ Pattern = 'Shai-Hulud: Here We Go Again'; Severity = 'critical' } | |
| @{ Pattern = 'IfYouRevokeThisTokenItWillWipeTheComputerOfTheOwner'; Severity = 'critical' } | |
| @{ Pattern = 'ctf-scramble-v2'; Severity = 'critical' } | |
| @{ Pattern = 'OhNoWhatsGoingOnWithGitHub:'; Severity = 'critical' } | |
| @{ Pattern = 'svksjrhjkcejg'; Severity = 'critical' } | |
| ) | |
| $COMPROMISED_PACKAGES = @( | |
| '@mistralai/mistralai' | |
| '@uipath/apollo-core' | |
| 'intercom-client@7.0.4' | |
| 'mbt@1.2.48' | |
| '@cap-js/db-service' | |
| '@cap-js/sqlite@2.2.2' | |
| '@cap-js/postgres' | |
| ) | |
| # ── State ──────────────────────────────────────────────────────────────────── | |
| $findings = [System.Collections.Generic.List[hashtable]]::new() | |
| function Add-Finding { | |
| param([string]$Severity, [string]$Category, [string]$Label, [string]$Detail = '') | |
| $findings.Add(@{ Severity = $Severity; Category = $Category; Label = $Label; Detail = $Detail }) | |
| Write-Hit -Severity $Severity -Label $Label -Detail $Detail | |
| } | |
| # ── Banner ──────────────────────────────────────────────────────────────────── | |
| Write-Host @" | |
| `n | |
| ╔══════════════════════════════════════════════════════════════╗ | |
| ║ Mini Shai-Hulud — Artefact Verification ║ | |
| ║ CTI Advisory #002 · CVE-2026-45321 · TLP:AMBER ║ | |
| ║ v3 ║ | |
| ║ ⚠ ISOLATE machine BEFORE revoking any token/secret ⚠ ║ | |
| ╚══════════════════════════════════════════════════════════════╝ | |
| "@ -ForegroundColor DarkYellow | |
| $auditDate = (Get-Date -Format 'yyyy-MM-ddTHH:mm:ssZ') | |
| $hostname = $env:COMPUTERNAME | |
| Write-Host " Host : $hostname" -ForegroundColor Gray | |
| Write-Host " Date : $auditDate" -ForegroundColor Gray | |
| Write-Host " User : $($env:USERNAME)`n" -ForegroundColor Gray | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| # 1. PAYLOAD FILES ON DISK | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| Write-Header "1/5 — Payload files on disk" | |
| $searchRoots = @( | |
| $env:USERPROFILE | |
| $env:APPDATA | |
| $env:LOCALAPPDATA | |
| $env:TEMP | |
| [System.IO.Path]::GetTempPath() | |
| 'C:\ProgramData' | |
| ) | Where-Object { $_ -and (Test-Path $_) } | Sort-Object -Unique | |
| foreach ($payload in $PAYLOAD_FILES) { | |
| $found = $false | |
| foreach ($root in $searchRoots) { | |
| try { | |
| $matches_ = Get-ChildItem -Path $root -Recurse -Filter $payload.Name ` | |
| -ErrorAction SilentlyContinue -Force 2>$null | |
| foreach ($f in $matches_) { | |
| $found = $true | |
| $detail = $f.FullName | |
| # SHA-256 verification | |
| if ($payload.Hash) { | |
| try { | |
| $actual = (Get-FileHash $f.FullName -Algorithm SHA256).Hash.ToLower() | |
| if ($actual -eq $payload.Hash) { | |
| $detail = "$($f.FullName) — HASH CONFIRMED malicious ($actual)" | |
| } else { | |
| $detail = "$($f.FullName) — hash mismatch (actual: $actual)" | |
| # File exists but hash differs — still suspicious, treat as high | |
| } | |
| } catch { $detail = "$($f.FullName) — could not hash file" } | |
| } | |
| Add-Finding -Severity $payload.Severity ` | |
| -Category 'filesystem' ` | |
| -Label "$($payload.Name) found on disk" ` | |
| -Detail $detail | |
| } | |
| } catch { <# silently skip inaccessible dirs #> } | |
| } | |
| if (-not $found) { | |
| Write-OK "$($payload.Name) — not found on disk" | |
| } | |
| } | |
| # Special case: Linux payload (running via WSL or cross-platform) | |
| if (Test-Path '/tmp/transformers.pyz' -ErrorAction SilentlyContinue) { | |
| Add-Finding -Severity 'critical' -Category 'filesystem' ` | |
| -Label '/tmp/transformers.pyz (PyPI mistralai payload)' ` | |
| -Detail '/tmp/transformers.pyz' | |
| } else { | |
| Write-OK "/tmp/transformers.pyz — not found" | |
| } | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| # 2. PERSISTENCE ARTEFACTS | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| Write-Header "2/5 — Persistence artefacts" | |
| foreach ($p in $PERSISTENCE_PATHS) { | |
| $fullPath = Join-Path $env:USERPROFILE $p.RelPath | |
| if (Test-Path $fullPath -ErrorAction SilentlyContinue) { | |
| Add-Finding -Severity $p.Severity -Category 'persistence' ` | |
| -Label $p.Label -Detail $fullPath | |
| } else { | |
| Write-OK "$($p.Label) — not found" | |
| } | |
| } | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| # 3. PROCESSES | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| Write-Header "3/5 — Running processes" | |
| foreach ($proc in $SUSPICIOUS_PROCESSES) { | |
| $running = Get-Process -ErrorAction SilentlyContinue | Where-Object { | |
| $_.ProcessName -match $proc.Pattern -or | |
| ($_.MainModule -and $_.MainModule.FileName -match $proc.Pattern) | |
| } | |
| if ($running) { | |
| foreach ($r in $running) { | |
| Add-Finding -Severity $proc.Severity -Category 'process' ` | |
| -Label $proc.Label ` | |
| -Detail "PID=$($r.Id) Name=$($r.ProcessName)" | |
| } | |
| } else { | |
| Write-OK "$($proc.Label) — not running" | |
| } | |
| } | |
| # gh-token-monitor via scheduled task | |
| Write-Section "Scheduled tasks" | |
| try { | |
| $tasks = Get-ScheduledTask -ErrorAction SilentlyContinue | | |
| Where-Object { $_.TaskName -match 'gh-token-monitor|tanstack|router_runtime' } | |
| if ($tasks) { | |
| foreach ($t in $tasks) { | |
| Add-Finding -Severity 'critical' -Category 'persistence' ` | |
| -Label "Suspicious scheduled task: $($t.TaskName)" ` | |
| -Detail $t.TaskPath | |
| } | |
| } else { | |
| Write-OK "No suspicious scheduled tasks found" | |
| } | |
| } catch { | |
| Write-Host " [INFO] Could not query scheduled tasks: $_" -ForegroundColor DarkGray | |
| } | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| # 4. NETWORK — Active connections to C2 domains | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| Write-Header "4/5 — Active network connections to C2" | |
| $tcpConnections = Get-NetTCPConnection -ErrorAction SilentlyContinue | |
| foreach ($domain in $C2_DOMAINS) { | |
| try { | |
| $resolved = [System.Net.Dns]::GetHostAddresses($domain) | Select-Object -ExpandProperty IPAddressToString | |
| } catch { $resolved = @() } | |
| $hit = $tcpConnections | Where-Object { | |
| $resolved -contains $_.RemoteAddress | |
| } | |
| if ($hit) { | |
| foreach ($c in $hit) { | |
| Add-Finding -Severity 'critical' -Category 'network' ` | |
| -Label "Active connection to C2: $domain" ` | |
| -Detail "Local=$($c.LocalAddress):$($c.LocalPort) Remote=$($c.RemoteAddress):$($c.RemotePort) PID=$($c.OwningProcess)" | |
| } | |
| } else { | |
| Write-OK "$domain — no active connection" | |
| } | |
| } | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| # 5. SUSPICIOUS STRINGS in .json / .js / .mjs / .lock / .txt files | |
| # (limited to user profile — not full disk scan) | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| Write-Header "5/5 — Suspicious strings in config / source files" | |
| $stringScanExtensions = @('*.json', '*.js', '*.mjs', '*.lock', '*.txt', '*.yml', '*.yaml') | |
| $stringScanRoots = @( | |
| $env:USERPROFILE | |
| $env:APPDATA | |
| $env:LOCALAPPDATA | |
| ) | Where-Object { $_ -and (Test-Path $_) } | Sort-Object -Unique | |
| # Build one combined regex for efficiency | |
| $combinedPattern = ($SUSPICIOUS_STRINGS | ForEach-Object { [Regex]::Escape($_.Pattern) }) -join '|' | |
| $combinedRegex = [System.Text.RegularExpressions.Regex]::new($combinedPattern, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase) | |
| # Track found strings to avoid duplicate reporting per string | |
| $foundStrings = @{} | |
| foreach ($root in $stringScanRoots) { | |
| foreach ($ext in $stringScanExtensions) { | |
| try { | |
| $files = Get-ChildItem -Path $root -Recurse -Filter $ext ` | |
| -ErrorAction SilentlyContinue -Force 2>$null | | |
| Where-Object { $_.Length -lt 5MB } # skip huge files | |
| foreach ($f in $files) { | |
| try { | |
| $content = [System.IO.File]::ReadAllText($f.FullName, [System.Text.Encoding]::UTF8) | |
| $m = $combinedRegex.Match($content) | |
| if ($m.Success) { | |
| $key = "$($f.FullName)|$($m.Value)" | |
| if (-not $foundStrings.ContainsKey($key)) { | |
| $foundStrings[$key] = $true | |
| $ioc = $SUSPICIOUS_STRINGS | Where-Object { | |
| $content -imatch [Regex]::Escape($_.Pattern) | |
| } | Select-Object -First 1 | |
| Add-Finding -Severity $(if ($ioc) { $ioc.Severity } else { 'critical' }) ` | |
| -Category 'string' ` | |
| -Label "Campaign marker found: `"$($m.Value)`"" ` | |
| -Detail $f.FullName | |
| } | |
| } | |
| } catch { <# skip unreadable files #> } | |
| } | |
| } catch { <# skip inaccessible dirs #> } | |
| } | |
| } | |
| # Check compromised packages in package.json / package-lock.json | |
| Write-Section "Compromised packages in package.json" | |
| $pkgFiles = Get-ChildItem -Path $env:USERPROFILE -Recurse -ErrorAction SilentlyContinue ` | |
| -Include 'package.json','package-lock.json' -Force 2>$null | | |
| Where-Object { $_.Length -lt 2MB -and $_.FullName -notmatch '\\node_modules\\' } | |
| $pkgHits = @{} | |
| foreach ($f in $pkgFiles) { | |
| try { | |
| $raw = [System.IO.File]::ReadAllText($f.FullName) | |
| foreach ($pkg in $COMPROMISED_PACKAGES) { | |
| if ($raw -imatch [Regex]::Escape($pkg)) { | |
| $key = "$($f.FullName)|$pkg" | |
| if (-not $pkgHits.ContainsKey($key)) { | |
| $pkgHits[$key] = $true | |
| Add-Finding -Severity 'critical' -Category 'package' ` | |
| -Label "Compromised package referenced: $pkg" ` | |
| -Detail $f.FullName | |
| } | |
| } | |
| } | |
| } catch { <# skip #> } | |
| } | |
| if ($pkgHits.Count -eq 0) { | |
| Write-OK "No compromised packages found in package.json files" | |
| } | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| # SUMMARY | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| Write-Header "SUMMARY — $hostname — $auditDate" | |
| $criticalCount = @($findings | Where-Object { $_.Severity -eq 'critical' }).Count | |
| $highCount = @($findings | Where-Object { $_.Severity -eq 'high' }).Count | |
| $total = $findings.Count | |
| if ($total -eq 0) { | |
| Write-Host "`n ✔ NO ARTEFACTS FOUND — Likely false positive." -ForegroundColor Green | |
| Write-Host " The previous report matched only command-history traces, not real files." -ForegroundColor Gray | |
| Write-Host " Recommended: keep monitoring, no immediate escalation required.`n" -ForegroundColor Gray | |
| } else { | |
| Write-Host "`n ✘ $total finding(s): $criticalCount CRITICAL, $highCount HIGH" -ForegroundColor Red | |
| Write-Host "" | |
| if ($criticalCount -gt 0) { | |
| Write-Host " MANDATORY INCIDENT RESPONSE ORDER:" -ForegroundColor Red | |
| Write-Host " 1. ISOLATE the machine from the network NOW (do not revoke tokens yet)" -ForegroundColor Yellow | |
| Write-Host " 2. PRESERVE evidence — do not reboot, do not delete files" -ForegroundColor Yellow | |
| Write-Host " 3. IMAGE the disk if possible" -ForegroundColor Yellow | |
| Write-Host " 4. KILL the gh-token-monitor daemon (if running)" -ForegroundColor Yellow | |
| Write-Host " 5. THEN revoke and rotate all exposed secrets" -ForegroundColor Yellow | |
| Write-Host " 6. ESCALATE to CSIRT with this output and the CSV report" -ForegroundColor Yellow | |
| } | |
| Write-Host "`n Findings detail:" -ForegroundColor DarkYellow | |
| foreach ($f in $findings) { | |
| $color = if ($f.Severity -eq 'critical') { 'Red' } elseif ($f.Severity -eq 'high') { 'Yellow' } else { 'Magenta' } | |
| Write-Host " [$($f.Severity.ToUpper())][$($f.Category)] $($f.Label)" -ForegroundColor $color | |
| if ($f.Detail) { | |
| Write-Host " $($f.Detail)" -ForegroundColor DarkGray | |
| } | |
| } | |
| Write-Host "" | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment