Created
March 25, 2026 00:30
-
-
Save aldehir/faad916112d97aa54ca30ea4688d823e to your computer and use it in GitHub Desktop.
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
| <# | |
| .SYNOPSIS | |
| Scans for Python virtual environments and checks for litellm installation, | |
| flagging versions affected by the TeamPCP supply chain attack (v1.82.7, v1.82.8). | |
| .DESCRIPTION | |
| On 2026-03-24, malicious litellm versions 1.82.7 and 1.82.8 were published to PyPI | |
| via stolen credentials. The payload: | |
| - Harvests SSH keys, cloud creds, API keys, crypto wallets, shell history | |
| - Exfiltrates data (AES-256 encrypted) to models.litellm[.]cloud | |
| - Deploys privileged Kubernetes pods for lateral movement | |
| - Installs persistent backdoor via systemd (sysmon.service) | |
| - v1.82.8 uses a .pth file that runs on EVERY Python process startup | |
| This script recursively finds virtualenvs and conda envs, checks for litellm, | |
| reports the version, and flags compromised versions + indicators of compromise. | |
| .EXAMPLE | |
| .\Find-LiteLLM.ps1 | |
| .\Find-LiteLLM.ps1 -Path "D:\Projects" | |
| .\Find-LiteLLM.ps1 -CheckIOCs | |
| #> | |
| [CmdletBinding()] | |
| param( | |
| [Parameter(Position = 0)] | |
| [string]$Path = (Get-Location).Path, | |
| [switch]$CheckIOCs | |
| ) | |
| $CompromisedVersions = @("1.82.7", "1.82.8") | |
| $MaliciousPthHash = "71e35aef03099cd1f2d6446734273025a163597de93912df321ef118bf135238" | |
| $MaliciousProxyHash = "a0d229be8efcb2f9135e2ad55ba275b76ddcfeb55fa4370e0a522a5bdee0120b" | |
| $found = 0 | |
| $compromised = 0 | |
| function Write-Banner { | |
| Write-Host "" | |
| Write-Host "========================================================" -ForegroundColor Cyan | |
| Write-Host " litellm Supply Chain Attack Scanner" -ForegroundColor Cyan | |
| Write-Host " Affected versions: 1.82.7, 1.82.8 (TeamPCP campaign)" -ForegroundColor Cyan | |
| Write-Host "========================================================" -ForegroundColor Cyan | |
| Write-Host " Scanning: $Path" -ForegroundColor Gray | |
| Write-Host "" | |
| } | |
| function Get-LiteLLMFromMetadata { | |
| param([string]$SitePackages) | |
| # Check dist-info metadata (pip-installed packages) | |
| $distInfoDirs = Get-ChildItem -Path $SitePackages -Directory -Filter "litellm-*.dist-info" -ErrorAction SilentlyContinue | |
| foreach ($dir in $distInfoDirs) { | |
| $metaFile = Join-Path $dir.FullName "METADATA" | |
| if (Test-Path $metaFile) { | |
| $versionLine = Select-String -Path $metaFile -Pattern "^Version:\s*(.+)$" | Select-Object -First 1 | |
| if ($versionLine) { | |
| return $versionLine.Matches[0].Groups[1].Value.Trim() | |
| } | |
| } | |
| } | |
| # Check egg-info metadata | |
| $eggDirs = Get-ChildItem -Path $SitePackages -Directory -Filter "litellm-*.egg-info" -ErrorAction SilentlyContinue | |
| foreach ($dir in $eggDirs) { | |
| $pkgInfo = Join-Path $dir.FullName "PKG-INFO" | |
| if (Test-Path $pkgInfo) { | |
| $versionLine = Select-String -Path $pkgInfo -Pattern "^Version:\s*(.+)$" | Select-Object -First 1 | |
| if ($versionLine) { | |
| return $versionLine.Matches[0].Groups[1].Value.Trim() | |
| } | |
| } | |
| } | |
| return $null | |
| } | |
| function Test-MaliciousPth { | |
| param([string]$SitePackages) | |
| $pthFile = Join-Path $SitePackages "litellm_init.pth" | |
| if (Test-Path $pthFile) { | |
| $hash = (Get-FileHash -Path $pthFile -Algorithm SHA256).Hash.ToLower() | |
| return @{ | |
| Found = $true | |
| Path = $pthFile | |
| Hash = $hash | |
| KnownMalicious = ($hash -eq $MaliciousPthHash) | |
| } | |
| } | |
| return @{ Found = $false } | |
| } | |
| function Find-VirtualEnvs { | |
| param([string]$SearchPath) | |
| $envs = @() | |
| # Find venv/virtualenv: look for pyvenv.cfg or Scripts/python.exe / bin/python | |
| $cfgFiles = Get-ChildItem -Path $SearchPath -Recurse -Filter "pyvenv.cfg" -ErrorAction SilentlyContinue | |
| foreach ($cfg in $cfgFiles) { | |
| $envRoot = $cfg.DirectoryName | |
| # Windows venvs have Scripts\python.exe | |
| $pythonExe = Join-Path $envRoot "Scripts\python.exe" | |
| if (-not (Test-Path $pythonExe)) { | |
| # Unix-style venvs have bin/python | |
| $pythonExe = Join-Path $envRoot "bin/python" | |
| } | |
| $sitePackages = Get-ChildItem -Path $envRoot -Recurse -Directory -Filter "site-packages" -ErrorAction SilentlyContinue | Select-Object -First 1 | |
| if ($sitePackages) { | |
| $envs += @{ | |
| Type = "venv/virtualenv" | |
| Root = $envRoot | |
| SitePackages = $sitePackages.FullName | |
| } | |
| } | |
| } | |
| # Find conda envs: look for conda-meta directories | |
| $condaMeta = Get-ChildItem -Path $SearchPath -Recurse -Directory -Filter "conda-meta" -ErrorAction SilentlyContinue | |
| foreach ($meta in $condaMeta) { | |
| $envRoot = $meta.Parent.FullName | |
| # Avoid duplicates if already found via pyvenv.cfg | |
| if ($envs.Root -contains $envRoot) { continue } | |
| $sitePackages = Get-ChildItem -Path $envRoot -Recurse -Directory -Filter "site-packages" -ErrorAction SilentlyContinue | Select-Object -First 1 | |
| if ($sitePackages) { | |
| $envs += @{ | |
| Type = "conda" | |
| Root = $envRoot | |
| SitePackages = $sitePackages.FullName | |
| } | |
| } | |
| } | |
| return $envs | |
| } | |
| function Test-IOCs { | |
| Write-Host "" | |
| Write-Host "--- Checking System-Wide Indicators of Compromise ---" -ForegroundColor Yellow | |
| # Persistence: sysmon backdoor | |
| $sysmonPath = Join-Path $env:USERPROFILE ".config/sysmon/sysmon.py" | |
| if (Test-Path $sysmonPath) { | |
| Write-Host " [CRITICAL] Backdoor found: $sysmonPath" -ForegroundColor Red | |
| } else { | |
| Write-Host " [OK] No sysmon backdoor at ~/.config/sysmon/" -ForegroundColor Green | |
| } | |
| # Persistence: systemd service (relevant on WSL) | |
| $systemdService = Join-Path $env:USERPROFILE ".config/systemd/user/sysmon.service" | |
| if (Test-Path $systemdService) { | |
| Write-Host " [CRITICAL] Malicious systemd service found: $systemdService" -ForegroundColor Red | |
| } else { | |
| Write-Host " [OK] No sysmon systemd service" -ForegroundColor Green | |
| } | |
| # Check pip cache for compromised packages | |
| $pipCacheDirs = @( | |
| (Join-Path $env:LOCALAPPDATA "pip\cache"), | |
| (Join-Path $env:USERPROFILE ".cache\pip"), | |
| (Join-Path $env:USERPROFILE ".cache\uv") | |
| ) | |
| foreach ($cacheDir in $pipCacheDirs) { | |
| if (Test-Path $cacheDir) { | |
| $maliciousCached = Get-ChildItem -Path $cacheDir -Recurse -Filter "litellm_init.pth" -ErrorAction SilentlyContinue | |
| if ($maliciousCached) { | |
| Write-Host " [WARNING] Malicious .pth file in cache: $($maliciousCached.FullName)" -ForegroundColor Red | |
| } | |
| } | |
| } | |
| # Check hosts file or DNS for C2 domains (informational) | |
| Write-Host " [INFO] C2 domains to block/monitor:" -ForegroundColor Yellow | |
| Write-Host " - models.litellm[.]cloud (exfiltration)" -ForegroundColor Yellow | |
| Write-Host " - checkmarx[.]zone (C2 polling)" -ForegroundColor Yellow | |
| Write-Host "" | |
| } | |
| # --- Main --- | |
| Write-Banner | |
| Write-Host "Searching for Python virtual environments..." -ForegroundColor Gray | |
| Write-Host "" | |
| $envs = Find-VirtualEnvs -SearchPath $Path | |
| if ($envs.Count -eq 0) { | |
| Write-Host "No Python virtual environments found under $Path" -ForegroundColor Yellow | |
| } else { | |
| Write-Host "Found $($envs.Count) virtual environment(s):" -ForegroundColor White | |
| Write-Host "" | |
| foreach ($env in $envs) { | |
| $version = Get-LiteLLMFromMetadata -SitePackages $env.SitePackages | |
| $pthResult = Test-MaliciousPth -SitePackages $env.SitePackages | |
| $relPath = $env.Root | |
| if ($relPath.StartsWith($Path)) { | |
| $relPath = $relPath.Substring($Path.Length).TrimStart('\', '/') | |
| if (-not $relPath) { $relPath = "." } | |
| } | |
| Write-Host " [$($env.Type)] $relPath" -ForegroundColor White | |
| if ($version) { | |
| $found++ | |
| $isCompromised = $CompromisedVersions -contains $version | |
| if ($isCompromised) { | |
| $compromised++ | |
| Write-Host " litellm $version" -ForegroundColor Red -NoNewline | |
| Write-Host " *** COMPROMISED VERSION ***" -ForegroundColor Red | |
| Write-Host " ACTION REQUIRED:" -ForegroundColor Red | |
| Write-Host " 1. Uninstall immediately: pip uninstall litellm" -ForegroundColor Red | |
| Write-Host " 2. Rotate ALL credentials on this machine" -ForegroundColor Red | |
| Write-Host " 3. Check for lateral movement (K8s pods named node-setup-*)" -ForegroundColor Red | |
| Write-Host " 4. Purge pip/uv caches: pip cache purge" -ForegroundColor Red | |
| } else { | |
| Write-Host " litellm $version" -ForegroundColor Green -NoNewline | |
| Write-Host " (not affected)" -ForegroundColor Green | |
| } | |
| } else { | |
| Write-Host " litellm not installed" -ForegroundColor Gray | |
| } | |
| if ($pthResult.Found) { | |
| Write-Host " [CRITICAL] litellm_init.pth FOUND: $($pthResult.Path)" -ForegroundColor Red | |
| if ($pthResult.KnownMalicious) { | |
| Write-Host " SHA256 matches known malicious payload!" -ForegroundColor Red | |
| } else { | |
| Write-Host " SHA256: $($pthResult.Hash) (unknown — investigate manually)" -ForegroundColor Yellow | |
| } | |
| } | |
| Write-Host "" | |
| } | |
| } | |
| if ($CheckIOCs) { | |
| Test-IOCs | |
| } | |
| # --- Summary --- | |
| Write-Host "--- Summary ---" -ForegroundColor Cyan | |
| Write-Host " Environments scanned : $($envs.Count)" | |
| Write-Host " litellm installations: $found" | |
| if ($compromised -gt 0) { | |
| Write-Host " COMPROMISED : $compromised" -ForegroundColor Red | |
| Write-Host "" | |
| Write-Host " !!! $compromised environment(s) have a compromised litellm version !!!" -ForegroundColor Red | |
| Write-Host " Treat affected systems as FULLY COMPROMISED." -ForegroundColor Red | |
| Write-Host " Run with -CheckIOCs for persistence/backdoor checks." -ForegroundColor Red | |
| exit 1 | |
| } else { | |
| if ($found -gt 0) { | |
| Write-Host " COMPROMISED : 0" -ForegroundColor Green | |
| } | |
| Write-Host "" | |
| Write-Host " No compromised litellm versions detected." -ForegroundColor Green | |
| exit 0 | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment