Created
March 25, 2026 09:41
-
-
Save AnmolTomer/4450a64fc96bff15dcb1249f13d47649 to your computer and use it in GitHub Desktop.
detection script that checks all of the following in one pass: installed litellm versions (pip, poetry, uv), the malicious `.pth` file in site-packages and uv cache, TeamPCP persistence artifacts, network IOCs in logs, Kubernetes lateral movement pods, and unpinned litellm in your local dependency files.
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
| #!/usr/bin/env bash | |
| # detect-litellm-compromise.sh | |
| # Checks whether your system was affected by the LiteLLM supply chain attack (March 24, 2026). | |
| # Run this on any machine where Python packages may have been installed during the exposure window. | |
| # | |
| # Usage: bash detect-litellm-compromise.sh | |
| # No arguments, no dependencies beyond Python 3 and standard unix tools. | |
| set -euo pipefail | |
| RED='\033[0;31m' | |
| YELLOW='\033[0;33m' | |
| GREEN='\033[0;32m' | |
| NC='\033[0m' | |
| found_issues=0 | |
| section() { printf "\n${YELLOW}[%s]${NC} %s\n" "$1" "$2"; } | |
| critical() { printf " ${RED}CRITICAL:${NC} %s\n" "$1"; found_issues=1; } | |
| ok() { printf " ${GREEN}OK:${NC} %s\n" "$1"; } | |
| warn() { printf " ${YELLOW}WARN:${NC} %s\n" "$1"; } | |
| # --- Step 1: Check installed litellm version --- | |
| section "1/6" "Checking installed litellm versions" | |
| check_version() { | |
| local ver="$1" src="$2" | |
| case "$ver" in | |
| 1.82.7|1.82.8) critical "litellm==$ver found via $src — system is COMPROMISED" ;; | |
| "") ;; | |
| *) ok "litellm==$ver ($src) — not a compromised version" ;; | |
| esac | |
| } | |
| if command -v pip &>/dev/null; then | |
| ver=$(pip show litellm 2>/dev/null | awk '/^Version:/{print $2}') | |
| check_version "${ver:-}" "pip" | |
| [ -z "$ver" ] && ok "litellm not installed in current pip environment" | |
| fi | |
| if command -v poetry &>/dev/null && [ -f poetry.lock ]; then | |
| ver=$(poetry show litellm 2>/dev/null | awk '/version/{print $3; exit}') | |
| check_version "${ver:-}" "poetry" | |
| fi | |
| if command -v uv &>/dev/null; then | |
| ver=$(uv pip freeze 2>/dev/null | grep -i '^litellm==' | cut -d= -f3) | |
| check_version "${ver:-}" "uv" | |
| fi | |
| # --- Step 2: Search for malicious .pth file --- | |
| section "2/6" "Searching for litellm_init.pth" | |
| pth_found=0 | |
| if command -v python3 &>/dev/null; then | |
| while IFS= read -r line; do | |
| if [ -n "$line" ]; then | |
| critical "$line" | |
| pth_found=1 | |
| fi | |
| done < <(python3 -c " | |
| import site, pathlib | |
| paths = set(site.getsitepackages() + [site.getusersitepackages()]) | |
| for p in paths: | |
| f = pathlib.Path(p) / 'litellm_init.pth' | |
| if f.exists(): | |
| print(f) | |
| " 2>/dev/null || true) | |
| fi | |
| # Check uv cache | |
| if [ -d "$HOME/.cache/uv" ]; then | |
| while IFS= read -r f; do | |
| critical "Found in uv cache: $f" | |
| pth_found=1 | |
| done < <(find "$HOME/.cache/uv" -name "litellm_init.pth" 2>/dev/null) | |
| fi | |
| [ "$pth_found" -eq 0 ] && ok "litellm_init.pth not found" | |
| # --- Step 3: Check for persistence artifacts --- | |
| section "3/6" "Checking for TeamPCP persistence artifacts" | |
| persistence_found=0 | |
| for f in \ | |
| "$HOME/.config/sysmon/sysmon.py" \ | |
| "$HOME/.config/systemd/user/sysmon.service" \ | |
| "/root/.config/sysmon/sysmon.py" \ | |
| "/tmp/pglog" \ | |
| "/tmp/.pg_state"; do | |
| if [ -e "$f" ]; then | |
| critical "Found: $f" | |
| persistence_found=1 | |
| fi | |
| done | |
| if systemctl --user is-active sysmon.service &>/dev/null; then | |
| critical "sysmon.service is running" | |
| persistence_found=1 | |
| fi | |
| [ "$persistence_found" -eq 0 ] && ok "No persistence artifacts found" | |
| # --- Step 4: Check network IOCs in DNS/hosts --- | |
| section "4/6" "Checking for network indicators" | |
| printf " Check your DNS, proxy, and egress logs for:\n" | |
| printf " - models.litellm.cloud (exfiltration endpoint)\n" | |
| printf " - checkmarx.zone (C2 / payload delivery)\n" | |
| for domain in models.litellm.cloud checkmarx.zone; do | |
| if grep -qr "$domain" /var/log/ 2>/dev/null; then | |
| critical "Found $domain in /var/log/" | |
| fi | |
| done | |
| # --- Step 5: Check Kubernetes (if kubectl available) --- | |
| section "5/6" "Checking Kubernetes (if applicable)" | |
| if command -v kubectl &>/dev/null && kubectl cluster-info &>/dev/null 2>&1; then | |
| if kubectl get pods -n kube-system 2>/dev/null | grep -q node-setup; then | |
| critical "Found node-setup pods in kube-system" | |
| else | |
| ok "No node-setup pods in kube-system" | |
| fi | |
| priv_alpine=$(kubectl get pods -n kube-system -o json 2>/dev/null | python3 -c " | |
| import json, sys | |
| data = json.load(sys.stdin) | |
| count = 0 | |
| for pod in data.get('items', []): | |
| for c in pod['spec'].get('containers', []): | |
| if 'alpine' in c.get('image', ''): | |
| sc = c.get('securityContext', {}) | |
| if sc.get('privileged'): | |
| ns = pod['metadata']['namespace'] | |
| name = pod['metadata']['name'] | |
| print(f'{ns}/{name} image={c[\"image\"]} privileged=true') | |
| count += 1 | |
| print(f'__count={count}', file=sys.stderr) | |
| " 2>&1) | |
| if echo "$priv_alpine" | grep -q "privileged=true"; then | |
| while IFS= read -r line; do | |
| [ -n "$line" ] && critical "Privileged alpine pod: $line" | |
| done <<< "$priv_alpine" | |
| else | |
| ok "No privileged alpine pods found" | |
| fi | |
| else | |
| ok "kubectl not available or no cluster connection — skipping" | |
| fi | |
| # --- Step 6: Scan local dependency files --- | |
| section "6/6" "Scanning local dependency files for unpinned litellm" | |
| if [ -d "." ]; then | |
| while IFS= read -r f; do | |
| if grep -qiE 'litellm[^=]|litellm>=' "$f" 2>/dev/null; then | |
| warn "Unpinned or loosely pinned litellm in: $f" | |
| fi | |
| done < <(find . \( \ | |
| -name 'requirements*.txt' \ | |
| -o -name 'pyproject.toml' \ | |
| -o -name 'Pipfile' \ | |
| -o -name 'Dockerfile*' \ | |
| \) -not -path '*/node_modules/*' -not -path '*/.git/*' 2>/dev/null) | |
| fi | |
| # --- Summary --- | |
| printf "\n" | |
| if [ "$found_issues" -eq 1 ]; then | |
| printf "${RED}=== ISSUES FOUND ===${NC}\n" | |
| printf "Review the CRITICAL items above. If litellm 1.82.7 or 1.82.8 was installed,\n" | |
| printf "treat this as a full security incident. See the incident response guide:\n" | |
| printf "<link-to-blog>\n" | |
| else | |
| printf "${GREEN}=== NO ISSUES FOUND ===${NC}\n" | |
| printf "No indicators of compromise detected on this system.\n" | |
| fi | |
| exit $found_issues |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment