Skip to content

Instantly share code, notes, and snippets.

@gabrielstellini
Created March 25, 2026 22:51
Show Gist options
  • Select an option

  • Save gabrielstellini/eb8cfe10f1dc7871d238d6ec56fe70b3 to your computer and use it in GitHub Desktop.

Select an option

Save gabrielstellini/eb8cfe10f1dc7871d238d6ec56fe70b3 to your computer and use it in GitHub Desktop.
Scans your local system for the litellm vulnerability
#!/usr/bin/env bash
# ──────────────────────────────────────────────────────────────────────────────
# LiteLLM Supply Chain Attack Scanner (PYSEC-2026-2) — v3 (merged)
#
# Comprehensive scanner combining thorough package discovery (all Python
# environments, dependency/lock files, caches) with runtime IOC detection
# (persistence artifacts, network indicators, processes, Kubernetes).
#
# Malicious versions: 1.82.7 and 1.82.8 only (published 2026-03-24 by TeamPCP).
#
# Environment variables:
# LITELLM_SCAN_ROOT — override root directory to scan (default: $HOME)
#
# Exit codes:
# 0 — clean (no confirmed issues)
# 1 — compromised (confirmed malicious artifacts found)
#
# References:
# - https://github.com/BerriAI/litellm/issues/24518
# - PYSEC-2026-2
# - https://futuresearch.ai/blog/litellm-pypi-supply-chain-attack/
# ──────────────────────────────────────────────────────────────────────────────
set -uo pipefail
# ── Colors ───────────────────────────────────────────────────────────────────
RED='\033[0;31m'
YELLOW='\033[1;33m'
GREEN='\033[0;32m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m'
# ── IOC Data ─────────────────────────────────────────────────────────────────
MALICIOUS_VERSIONS=("1.82.7" "1.82.8")
PTH_HASH="71e35aef03099cd1f2d6446734273025a163597de93912df321ef118bf135238"
MALICIOUS_DOMAINS=("models.litellm.cloud" "checkmarx.zone")
MALICIOUS_IPS=("46.151.182.203" "83.142.209.11")
KNOWN_PTH_REGEX="^(_virtualenv|_uv_ephemeral_overlay|easy-install|distutils|distutils-precedence|setuptools|wheel|pip|pkg_resources|aeosa|__editable__\..+|coloredlogs|a1_coverage|coverage|debugpy|inject|wrapt)$"
# ── Configuration ────────────────────────────────────────────────────────────
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SCAN_ROOT="${LITELLM_SCAN_ROOT:-$HOME}"
FIXTURE_EXCLUDE=""
if [[ -z "${LITELLM_SCAN_ROOT:-}" ]] && [[ -d "$SCRIPT_DIR/tests/fixtures" ]]; then
FIXTURE_EXCLUDE="$SCRIPT_DIR/tests/fixtures"
fi
if [[ -n "${LITELLM_SCAN_ROOT:-}" ]]; then
BROAD_SCAN_ROOTS=("$LITELLM_SCAN_ROOT")
else
BROAD_SCAN_ROOTS=("$HOME" "/usr/local/lib" "/opt/homebrew/lib" "/Library/Python")
fi
# ── Counters ─────────────────────────────────────────────────────────────────
found_count=0
warn_count=0
# ── Output helpers ───────────────────────────────────────────────────────────
banner() {
echo ""
echo -e "${CYAN}${BOLD}════════════════════════════════════════════════════════════════${NC}"
echo -e "${CYAN}${BOLD} LiteLLM Supply Chain Attack Scanner (PYSEC-2026-2) v3${NC}"
echo -e "${CYAN}${BOLD} Malicious versions: 1.82.7, 1.82.8 (published 2026-03-24)${NC}"
echo -e "${CYAN}${BOLD}════════════════════════════════════════════════════════════════${NC}"
echo ""
}
section() {
echo ""
echo -e "${BOLD}── $1 ──${NC}"
}
found() {
echo -e " ${RED}[FOUND]${NC} $1"
found_count=$((found_count + 1))
}
warn() {
echo -e " ${YELLOW}[WARN]${NC} $1"
warn_count=$((warn_count + 1))
}
clean() {
echo -e " ${GREEN}[CLEAN]${NC} $1"
}
info() {
echo -e " ${CYAN}[INFO]${NC} $1"
}
# ── Helpers ──────────────────────────────────────────────────────────────────
is_malicious() {
local v="$1"
for m in "${MALICIOUS_VERSIONS[@]}"; do
[[ "$v" == "$m" ]] && return 0
done
return 1
}
sha256_of() {
if command -v sha256sum &>/dev/null; then
sha256sum "$1" | awk '{print $1}'
elif command -v shasum &>/dev/null; then
shasum -a 256 "$1" | awk '{print $1}'
else
openssl dgst -sha256 "$1" | awk '{print $NF}'
fi
}
flag_pip_result() {
local result="$1" source="$2"
local ver loc
ver=$(printf '%s' "$result" | grep "^Version:" | awk '{print $2}') || true
loc=$(printf '%s' "$result" | grep "^Location:" | cut -d' ' -f2-) || true
if [[ -z "$ver" ]]; then
warn "Could not parse version from $source output"
return
fi
if is_malicious "$ver"; then
found "INFECTED — litellm==$ver [$source] at $loc"
else
clean "litellm==$ver [$source] at $loc"
fi
}
# ── Find wrappers ────────────────────────────────────────────────────────────
# Build prune args once; reused by all collect_* functions.
PRUNE_PATHS=(-path "*/.git" -o -path "*/node_modules" -o -path "*/__pycache__" -o -path "*/.cache" -o -path "*/Library/Caches")
if [[ -n "$FIXTURE_EXCLUDE" ]]; then
PRUNE_PATHS+=(-o -path "$FIXTURE_EXCLUDE")
fi
collect_dependency_files() {
find "$SCAN_ROOT" -maxdepth 8 \
\( "${PRUNE_PATHS[@]}" \) -prune -o \
\( -name "requirements*.txt" -o -name "pyproject.toml" -o -name "setup.py" \
-o -name "setup.cfg" -o -name "Pipfile" -o -name "poetry.lock" \
-o -name "uv.lock" -o -name "pdm.lock" \) -print 2>/dev/null
return 0
}
collect_venv_dirs() {
find "$SCAN_ROOT" -maxdepth 6 \
\( "${PRUNE_PATHS[@]}" \) -prune -o \
\( -type d \( -name ".venv" -o -name "venv" -o -name "env" \) -print \) 2>/dev/null
return 0
}
collect_broad_hits() {
find "${BROAD_SCAN_ROOTS[@]}" -maxdepth 10 \
\( "${PRUNE_PATHS[@]}" \) -prune -o \
\( -name "litellm_init.pth" -o -name "litellm*.dist-info" -o -name "litellm*.egg-info" \) -print 2>/dev/null
return 0
}
get_site_packages() {
{
for py in python3 python python3.9 python3.10 python3.11 python3.12 python3.13; do
command -v "$py" &>/dev/null || continue
"$py" -c "
import site
pkgs = getattr(site, 'getsitepackages', lambda: [])()
user = getattr(site, 'getusersitepackages', lambda: '')()
for p in pkgs + ([user] if user else []):
print(p)
" 2>/dev/null || true
done
for prefix in /usr/local /opt/homebrew; do
[[ -d "$prefix/lib" ]] && find "$prefix/lib" -maxdepth 4 -name "site-packages" -type d 2>/dev/null
done
local _pyenv_root="${PYENV_ROOT:-$HOME/.pyenv}"
[[ -d "$_pyenv_root/versions" ]] && \
find "$_pyenv_root/versions" -maxdepth 6 -name "site-packages" -type d 2>/dev/null
find /usr/lib /usr/local/lib /Library/Python \
-maxdepth 7 -name "site-packages" -type d 2>/dev/null || true
find /System/Library/Frameworks/Python.framework \
-maxdepth 8 -name "site-packages" -type d 2>/dev/null || true
} | sort -u | grep -v '^$' || true
}
# ══════════════════════════════════════════════════════════════════════════════
# PART A: PACKAGE DISCOVERY
# ══════════════════════════════════════════════════════════════════════════════
# ── 1. Enumerate site-packages ───────────────────────────────────────────────
SITE_PACKAGES=()
enumerate_site_packages() {
section "1/13 Enumerating Python site-packages"
while IFS= read -r p; do
[[ -d "$p" ]] && SITE_PACKAGES+=("$p")
done < <(get_site_packages)
info "Found ${#SITE_PACKAGES[@]} site-packages root(s)"
for sp in "${SITE_PACKAGES[@]+"${SITE_PACKAGES[@]}"}"; do
info " $sp"
done
}
# ── 2. pip global installs ───────────────────────────────────────────────────
check_pip_global() {
section "2/13 pip global installs"
local step_found=false
local seen_pip_paths=()
for cmd in pip pip3 pip3.9 pip3.10 pip3.11 pip3.12 pip3.13; do
command -v "$cmd" &>/dev/null || continue
# Dedup: skip if this resolves to an already-checked binary
local resolved
resolved=$(command -v "$cmd") || true
local already=false
local seen
for seen in "${seen_pip_paths[@]+"${seen_pip_paths[@]}"}"; do
[[ "$resolved" == "$seen" ]] && already=true && break
done
[[ "$already" == true ]] && continue
seen_pip_paths+=("$resolved")
local result
result=$("$cmd" show litellm 2>/dev/null) || true
if [[ -n "$result" ]]; then
flag_pip_result "$result" "$cmd"
step_found=true
fi
done
[[ "$step_found" == false ]] && clean "Not found via any pip"
}
# ── 3. Known site-packages ──────────────────────────────────────────────────
check_site_packages() {
section "3/13 Installed litellm in site-packages"
local step_found=false
for sp in "${SITE_PACKAGES[@]+"${SITE_PACKAGES[@]}"}"; do
[[ -d "$sp/litellm" ]] || continue
local ver=""
local dist_dir
for dist_dir in "$sp"/litellm-*.dist-info; do
[[ -d "$dist_dir" ]] || continue
ver=$(basename "$dist_dir" | sed 's/litellm-\(.*\)\.dist-info/\1/')
break
done
if [[ -n "$ver" ]]; then
if is_malicious "$ver"; then
found "INFECTED — litellm==$ver at $sp"
else
clean "litellm==$ver at $sp"
fi
else
warn "litellm directory at $sp/litellm — no dist-info (source checkout? verify manually)"
fi
step_found=true
done
[[ "$step_found" == false ]] && clean "Not found in known site-packages"
}
# ── 4. conda environments ───────────────────────────────────────────────────
check_conda_envs() {
section "4/13 conda environments"
if ! command -v conda &>/dev/null; then
info "conda not installed, skipping"
return
fi
local step_found=false
local env_name
while IFS= read -r env_name; do
[[ -z "$env_name" ]] && continue
local result
result=$(conda run -n "$env_name" pip show litellm 2>/dev/null) || true
if [[ -n "$result" ]]; then
flag_pip_result "$result" "conda:$env_name"
step_found=true
fi
done < <(conda env list 2>/dev/null | awk '{print $1}' | grep -v "^#" | grep -v "^$" || true)
[[ "$step_found" == false ]] && clean "Not found in any conda env"
}
# ── 5. pyenv versions ───────────────────────────────────────────────────────
check_pyenv() {
section "5/13 pyenv versions"
if ! command -v pyenv &>/dev/null; then
info "pyenv not installed, skipping"
return
fi
local step_found=false
local pyenv_root="${PYENV_ROOT:-$HOME/.pyenv}"
local ver
while IFS= read -r ver; do
[[ -z "$ver" ]] && continue
local pip_path="$pyenv_root/versions/$ver/bin/pip"
[[ -x "$pip_path" ]] || continue
local result
result=$("$pip_path" show litellm 2>/dev/null) || true
if [[ -n "$result" ]]; then
flag_pip_result "$result" "pyenv:$ver"
step_found=true
fi
done < <(pyenv versions --bare 2>/dev/null || true)
[[ "$step_found" == false ]] && clean "Not found in any pyenv version"
}
# ── 6. Virtual environments (.venv / venv / env) ────────────────────────────
check_venvs() {
section "6/13 Virtual environments"
info "Scanning for virtualenvs under $SCAN_ROOT..."
local step_found=false
local venv_path
while IFS= read -r venv_path; do
[[ -z "$venv_path" ]] && continue
# Skip venvs whose site-packages were already covered by step 3
local sp_dir
sp_dir=$(find "$venv_path/lib" -maxdepth 2 -type d -name "site-packages" 2>/dev/null | head -1) || true
if [[ -n "$sp_dir" ]]; then
# Already reported by check_site_packages — skip
local already_seen=false
for seen_sp in "${SITE_PACKAGES[@]+"${SITE_PACKAGES[@]}"}"; do
[[ "$sp_dir" == "$seen_sp" ]] && already_seen=true && break
done
[[ "$already_seen" == true ]] && continue
# Fast pre-filter: skip venvs that don't have litellm installed
[[ ! -d "$sp_dir/litellm" ]] && continue
fi
local pip_bin="$venv_path/bin/pip"
[[ -x "$pip_bin" ]] || continue
local result
result=$("$pip_bin" show litellm 2>/dev/null) || true
if [[ -n "$result" ]]; then
flag_pip_result "$result" "venv:$venv_path"
step_found=true
fi
done < <(collect_venv_dirs)
[[ "$step_found" == false ]] && clean "Not found in any virtual env"
}
# ── 7. uv active environment ────────────────────────────────────────────────
check_uv() {
section "7/13 uv active environment"
if ! command -v uv &>/dev/null; then
info "uv not installed, skipping"
return
fi
local result
result=$(uv pip show litellm 2>/dev/null) || true
if [[ -z "$result" ]]; then
clean "Not found in uv default environment"
return
fi
# Skip if this location was already checked by step 2/3
local uv_loc
uv_loc=$(printf '%s' "$result" | grep "^Location:" | cut -d' ' -f2-) || true
if [[ -n "$uv_loc" ]]; then
local already_seen=false
for seen_sp in "${SITE_PACKAGES[@]+"${SITE_PACKAGES[@]}"}"; do
[[ "$uv_loc" == "$seen_sp" ]] && already_seen=true && break
done
if [[ "$already_seen" == true ]]; then
info "uv environment already covered by earlier checks"
return
fi
fi
flag_pip_result "$result" "uv"
}
# ── 8. Dependency files and lock files ───────────────────────────────────────
check_dependency_files() {
section "8/13 Dependency files and lock files"
local step_found=false
local pat_exact='litellm([^=[:space:]]*)?\s*==\s*["'"'"']?(1\.82\.7|1\.82\.8)'
local pat_range='litellm([^=[:space:]]*)?\s*(>=|~=)\s*["'"'"']?1\.82\.[0-8]'
local f
while IFS= read -r f; do
[[ -z "$f" ]] && continue
# Lock files: look for malicious version string near "litellm"
if [[ "$f" == *.lock ]]; then
if grep -qi "litellm" "$f" 2>/dev/null; then
local lock_ver
lock_ver=$(grep -A5 -i "litellm" "$f" 2>/dev/null \
| grep -oE '"(1\.82\.7|1\.82\.8)"' | head -1 | tr -d '"') || true
if [[ -n "$lock_ver" ]]; then
found "Lock file pins malicious litellm==$lock_ver: $f"
step_found=true
fi
fi
continue
fi
local matches
matches=$(grep -i "litellm" "$f" 2>/dev/null | grep -ivE '^[[:space:]]*#') || true
[[ -z "$matches" ]] && continue
local line
while IFS= read -r line; do
[[ -z "$line" ]] && continue
if echo "$line" | grep -qiE "$pat_exact"; then
local pin_ver
pin_ver=$(echo "$line" | grep -oE "1\.82\.[78]" | head -1) || true
found "Pins malicious litellm==$pin_ver: $f"
info " $line"
elif echo "$line" | grep -qiE "$pat_range"; then
warn "Version range may include malicious versions: $f"
info " $line"
else
info "litellm reference (verify version): $f"
info " $line"
fi
step_found=true
done <<< "$matches"
done < <(collect_dependency_files)
[[ "$step_found" == false ]] && clean "No litellm references in dependency files"
}
# ── 9. Package manager caches (pip, uv, conda) ──────────────────────────────
check_caches() {
section "9/13 Package manager caches"
local step_found=false
# pip cache
local pip_cache_dirs=()
if command -v pip &>/dev/null; then
local pip_cache
pip_cache=$(pip cache dir 2>/dev/null) || true
[[ -n "$pip_cache" && -d "$pip_cache" ]] && pip_cache_dirs+=("$pip_cache")
fi
[[ -d "$HOME/.cache/pip" ]] && pip_cache_dirs+=("$HOME/.cache/pip")
[[ -d "$HOME/Library/Caches/pip" ]] && pip_cache_dirs+=("$HOME/Library/Caches/pip")
local cache_dir
for cache_dir in "${pip_cache_dirs[@]+"${pip_cache_dirs[@]}"}"; do
while IFS= read -r -d '' f; do
found "pip cache: $f"
step_found=true
done < <(find "$cache_dir" -type f \( -name "*litellm*1.82.7*" -o -name "*litellm*1.82.8*" \) -print0 2>/dev/null || true)
done
# uv cache
local uv_cache_dirs=("$HOME/.cache/uv" "$HOME/Library/Caches/uv")
for cache_dir in "${uv_cache_dirs[@]}"; do
[[ -d "$cache_dir" ]] || continue
while IFS= read -r -d '' dist_info; do
local metadata_file="$dist_info/METADATA"
if [[ -f "$metadata_file" ]]; then
local ver
ver=$(grep "^Version:" "$metadata_file" | awk '{print $2}') || true
if is_malicious "${ver:-}"; then
found "uv cache: litellm==$ver at $dist_info"
step_found=true
fi
fi
done < <(find "$cache_dir" -type d -name "litellm-*.dist-info" -print0 2>/dev/null || true)
while IFS= read -r -d '' f; do
found "uv cache: $f"
step_found=true
done < <(find "$cache_dir" -type f \( -name "*litellm*1.82.7*" -o -name "*litellm*1.82.8*" \) -print0 2>/dev/null || true)
done
# conda cache
if [[ -d "$HOME/.conda/pkgs" ]]; then
while IFS= read -r -d '' f; do
found "conda cache: $f"
step_found=true
done < <(find "$HOME/.conda/pkgs" -maxdepth 1 -type d -name "litellm-1.82.[78]*" -print0 2>/dev/null || true)
fi
[[ "$step_found" == false ]] && clean "No compromised packages in caches"
}
# ── 10. dist-info / egg-info broad scan ──────────────────────────────────────
check_dist_info() {
section "10/13 dist-info / egg-info (broad scan)"
local step_found=false
local seen_paths=()
# site-packages first
local sp
for sp in "${SITE_PACKAGES[@]+"${SITE_PACKAGES[@]}"}"; do
local d
for d in "$sp"/litellm-*.dist-info "$sp"/litellm-*.egg-info; do
[[ -e "$d" ]] || continue
seen_paths+=("$d")
local ver
ver=$(basename "$d" | sed -E 's/litellm-(.+)\.(dist-info|egg-info)$/\1/')
if is_malicious "$ver"; then
found "INFECTED dist-info litellm==$ver: $d"
else
clean "dist-info litellm==$ver: $d"
fi
step_found=true
done
done
# Broad scan for anything not already caught
local hit
while IFS= read -r hit; do
[[ -z "$hit" ]] && continue
# Skip already-seen
local already=false
local s
for s in "${seen_paths[@]+"${seen_paths[@]}"}"; do
[[ "$hit" == "$s" ]] && already=true && break
done
[[ "$already" == true ]] && continue
# Only process dist-info/egg-info, not .pth
case "$hit" in
*.dist-info|*.egg-info)
local ver
ver=$(basename "$hit" | sed -E 's/litellm-(.+)\.(dist-info|egg-info)$/\1/')
if is_malicious "$ver"; then
found "INFECTED dist-info litellm==$ver: $hit"
else
clean "dist-info litellm==$ver: $hit"
fi
step_found=true
;;
esac
done < <(collect_broad_hits)
[[ "$step_found" == false ]] && clean "No litellm dist-info/egg-info found"
}
# ══════════════════════════════════════════════════════════════════════════════
# PART B: MALWARE ARTIFACT DETECTION
# ══════════════════════════════════════════════════════════════════════════════
# ── 11. litellm_init.pth + suspicious .pth files ────────────────────────────
check_pth_files() {
section "11/13 Malicious .pth files"
local step_found=false
# --- 11a. litellm_init.pth in known site-packages (fast) ---
local sp
for sp in "${SITE_PACKAGES[@]+"${SITE_PACKAGES[@]}"}"; do
if [[ -f "$sp/litellm_init.pth" ]]; then
local hash
hash=$(sha256_of "$sp/litellm_init.pth")
if [[ "$hash" == "$PTH_HASH" ]]; then
found "EXACT HASH MATCH — litellm_init.pth at $sp (sha256: $hash)"
else
found "litellm_init.pth present (different hash): $sp"
fi
step_found=true
fi
done
# --- 11b. litellm_init.pth broad scan ---
local search_roots=("${BROAD_SCAN_ROOTS[@]}")
[[ -n "${VIRTUAL_ENV:-}" ]] && search_roots+=("$VIRTUAL_ENV")
while IFS= read -r -d '' pth_file; do
# Skip if already reported from site-packages
local already=false
for sp in "${SITE_PACKAGES[@]+"${SITE_PACKAGES[@]}"}"; do
[[ "$pth_file" == "$sp/litellm_init.pth" ]] && already=true && break
done
[[ "$already" == true ]] && continue
local hash
hash=$(sha256_of "$pth_file")
if [[ "$hash" == "$PTH_HASH" ]]; then
found "EXACT HASH MATCH — litellm_init.pth: $pth_file"
else
found "litellm_init.pth present (different hash): $pth_file"
fi
step_found=true
done < <(find "${search_roots[@]}" -name "litellm_init.pth" -type f -print0 2>/dev/null || true)
# --- 11c. Non-standard .pth files in site-packages (allowlist) ---
for sp in "${SITE_PACKAGES[@]+"${SITE_PACKAGES[@]}"}"; do
while IFS= read -r -d '' f; do
local fname
fname=$(basename "$f" .pth)
# Use bash regex instead of forking grep per file
if [[ "$fname" =~ $KNOWN_PTH_REGEX ]]; then
continue
fi
if grep -q "base64" "$f" 2>/dev/null && grep -q "exec" "$f" 2>/dev/null; then
found "Malicious .pth with base64+exec: $f"
else
warn "Unexpected .pth file (review manually): $f"
fi
step_found=true
done < <(find "$sp" -maxdepth 1 -name "*.pth" -type f -print0 2>/dev/null || true)
done
# --- 11d. Broader base64+exec .pth scan ---
while IFS= read -r -d '' pth_file; do
# Skip site-packages (already covered)
local in_sp=false
for sp in "${SITE_PACKAGES[@]+"${SITE_PACKAGES[@]}"}"; do
[[ "$pth_file" == "$sp/"* ]] && in_sp=true && break
done
[[ "$in_sp" == true ]] && continue
if grep -q "base64" "$pth_file" 2>/dev/null && grep -q "exec" "$pth_file" 2>/dev/null; then
found "Suspicious .pth with base64+exec: $pth_file"
step_found=true
fi
done < <(find "${search_roots[@]}" -name "*.pth" -type f -size +1k -print0 2>/dev/null || true)
[[ "$step_found" == false ]] && clean "No malicious .pth files found"
}
# ── 12. Persistence artifacts ────────────────────────────────────────────────
check_persistence() {
section "12/13 Persistence artifacts (backdoor, systemd, LaunchAgents, temp files)"
local step_found=false
# Backdoor script — sysmon.py is the specific IOC, the directory alone is weaker signal
local backdoor_files=(
"$HOME/.config/sysmon/sysmon.py"
)
if [[ "$(uname)" == "Darwin" ]]; then
backdoor_files+=("$HOME/Library/Application Support/sysmon/sysmon.py")
fi
local p
for p in "${backdoor_files[@]}"; do
if [[ -f "$p" ]]; then
found "Backdoor artifact: $p"
if grep -qE "checkmarx\.zone|litellm\.cloud" "$p" 2>/dev/null; then
found " Contains known C2 domain references"
fi
step_found=true
fi
done
# Directory without sysmon.py is a weaker signal (could be legitimate monitoring)
if [[ -d "$HOME/.config/sysmon" ]] && [[ ! -f "$HOME/.config/sysmon/sysmon.py" ]]; then
warn "Directory $HOME/.config/sysmon/ exists without sysmon.py (may be remnant or legitimate)"
fi
# Systemd persistence (Linux)
if [[ -f "$HOME/.config/systemd/user/sysmon.service" ]]; then
found "Systemd persistence: $HOME/.config/systemd/user/sysmon.service"
step_found=true
fi
if command -v systemctl &>/dev/null; then
if systemctl --user is-active sysmon.service &>/dev/null; then
found "Systemd service 'sysmon' is ACTIVE"
step_found=true
fi
fi
# LaunchAgent persistence (macOS)
local launchagent_dir="$HOME/Library/LaunchAgents"
if [[ -d "$launchagent_dir" ]]; then
while IFS= read -r -d '' f; do
# Strong signal: C2 domains unique to this attack
if grep -qE "checkmarx\.zone|litellm\.cloud" "$f" 2>/dev/null; then
found "LaunchAgent references C2 domain: $f"
step_found=true
# Weaker signal: sysmon alone could be legitimate monitoring
elif grep -qF "sysmon.py" "$f" 2>/dev/null; then
warn "LaunchAgent references sysmon.py (verify manually): $f"
step_found=true
fi
done < <(find "$launchagent_dir" -name "*.plist" -type f -print0 2>/dev/null || true)
fi
# Temp file artifacts
local tmp_dir="${TMPDIR:-/tmp}"
local suspect_files=(
"$tmp_dir/c" "$tmp_dir/pglog" "$tmp_dir/.pg_state" "$tmp_dir/tpcp.tar.gz"
)
# Also check /tmp if TMPDIR is different
if [[ "$tmp_dir" != "/tmp" ]]; then
suspect_files+=("/tmp/c" "/tmp/pglog" "/tmp/.pg_state" "/tmp/tpcp.tar.gz")
fi
for p in "${suspect_files[@]}"; do
if [[ -f "$p" ]]; then
local size
size=$(wc -c < "$p" 2>/dev/null || echo "?")
found "Temp artifact: $p (${size} bytes)"
step_found=true
fi
done
# Exfil archive
while IFS= read -r -d '' f; do
found "Exfiltration archive: $f"
step_found=true
done < <(find "$HOME" -maxdepth 3 -name "tpcp.tar.gz" -type f -print0 2>/dev/null || true)
[[ "$step_found" == false ]] && clean "No persistence or temp artifacts found"
}
# ── 13. Network + process + Kubernetes IOCs ──────────────────────────────────
check_runtime_iocs() {
section "13/13 Runtime IOCs (network, processes, Kubernetes)"
local step_found=false
# --- Active network connections ---
local ip
if command -v lsof &>/dev/null; then
for ip in "${MALICIOUS_IPS[@]}"; do
if lsof -i "@$ip" -n 2>/dev/null | grep -qF "$ip"; then
found "Active connection to malicious IP: $ip"
step_found=true
fi
done
elif command -v ss &>/dev/null; then
for ip in "${MALICIOUS_IPS[@]}"; do
if ss -tnp 2>/dev/null | grep -qF "$ip"; then
found "Active connection to malicious IP: $ip"
step_found=true
fi
done
elif command -v netstat &>/dev/null; then
for ip in "${MALICIOUS_IPS[@]}"; do
if netstat -an 2>/dev/null | grep -qF "$ip"; then
found "Active connection to malicious IP: $ip"
step_found=true
fi
done
fi
# --- /etc/hosts ---
local domain
for domain in "${MALICIOUS_DOMAINS[@]}"; do
if [[ -f /etc/hosts ]] && grep -qF "$domain" /etc/hosts 2>/dev/null; then
warn "Domain in /etc/hosts: $domain"
step_found=true
fi
done
# --- Shell history ---
local hf
for hf in "$HOME/.zsh_history" "$HOME/.bash_history"; do
[[ -f "$hf" ]] || continue
for domain in "${MALICIOUS_DOMAINS[@]}"; do
if grep -qF "$domain" "$hf" 2>/dev/null; then
warn "Malicious domain in shell history ($hf): $domain"
step_found=true
fi
done
done
# --- Suspicious processes ---
if pgrep -f "sysmon\.py" &>/dev/null; then
found "Process running sysmon.py (backdoor)"
step_found=true
fi
local python_count
python_count=$(pgrep -c python 2>/dev/null || echo "0")
if [[ "$python_count" -gt 100 ]]; then
found "Abnormal Python process count: $python_count (possible fork bomb)"
step_found=true
elif [[ "$python_count" -gt 50 ]]; then
warn "Elevated Python process count: $python_count"
step_found=true
fi
# Check for curl to C2 domains separately (macOS pgrep doesn't support \| alternation)
for domain in "${MALICIOUS_DOMAINS[@]}"; do
# Escape dots for pgrep regex
local escaped_domain="${domain//./\\.}"
if pgrep -af "curl.*$escaped_domain" 2>/dev/null | grep -vq grep; then
found "Active curl to malicious domain: $domain"
step_found=true
fi
done
# --- Kubernetes ---
if command -v kubectl &>/dev/null && kubectl config current-context &>/dev/null 2>&1; then
# Fetch pod JSON once for all K8s checks
local pods_json
pods_json=$(kubectl get pods -n kube-system -o json 2>/dev/null) || true
if [[ -n "$pods_json" ]]; then
local bad_pods
bad_pods=$(echo "$pods_json" | grep -o '"name":"node-setup-[^"]*"' | cut -d'"' -f4) || true
if [[ -n "$bad_pods" ]]; then
found "Suspicious node-setup pods in kube-system"
info "$bad_pods"
step_found=true
fi
# Check for alpine+hostPath pods (use jq if available, fall back to python3)
local alpine_pods=""
if command -v jq &>/dev/null; then
alpine_pods=$(echo "$pods_json" | \
jq -r '.items[] | select(
(.spec.containers[]?.image | contains("alpine")) and
(.spec.volumes[]? | has("hostPath"))
) | .metadata.name' 2>/dev/null) || true
elif command -v python3 &>/dev/null; then
alpine_pods=$(echo "$pods_json" | \
python3 -c "
import json, sys
data = json.load(sys.stdin)
for pod in data.get('items', []):
containers = pod.get('spec', {}).get('containers', [])
volumes = pod.get('spec', {}).get('volumes', [])
has_alpine = any('alpine' in c.get('image', '') for c in containers)
has_hostpath = any('hostPath' in v for v in volumes)
if has_alpine and has_hostpath:
print(pod['metadata']['name'])
" 2>/dev/null) || true
fi
if [[ -n "$alpine_pods" ]]; then
found "Alpine pods with hostPath mounts in kube-system"
info "$alpine_pods"
step_found=true
fi
fi
else
info "No active Kubernetes cluster, skipping K8s checks"
fi
[[ "$step_found" == false ]] && clean "No runtime IOCs found"
}
# ══════════════════════════════════════════════════════════════════════════════
# SUMMARY
# ══════════════════════════════════════════════════════════════════════════════
summary() {
echo ""
echo -e "${BOLD}════════════════════════════════════════════════════════════════${NC}"
if [[ "$found_count" -gt 0 ]]; then
echo -e "${RED}${BOLD} COMPROMISED: $found_count issue(s), $warn_count warning(s)${NC}"
echo ""
echo -e "${BOLD} Immediate actions:${NC}"
echo " 1. Uninstall: pip uninstall litellm -y (in every affected env)"
echo " 2. Reinstall: pip install litellm==1.82.6"
echo " 3. Caches: pip cache purge && rm -rf ~/.cache/uv/archive-v0/*litellm*"
echo " 4. Persistence: rm -rf ~/.config/sysmon/ ~/.config/systemd/user/sysmon.service"
echo " 5. Temp files: rm -f /tmp/c /tmp/pglog /tmp/.pg_state /tmp/tpcp.tar.gz"
echo " 6. Kubernetes: kubectl delete pods -n kube-system -l app=node-setup"
echo ""
echo -e "${RED}${BOLD} ROTATE ALL CREDENTIALS:${NC}"
echo " - API keys (OpenAI, Anthropic, etc.)"
echo " - SSH keys (~/.ssh/)"
echo " - AWS / GCP / Azure credentials"
echo " - Database passwords"
echo " - Any secrets in .env files or shell history"
echo " - Review connected services for unauthorized access"
echo " - Monitor network for 46.151.182.203 / 83.142.209.11"
elif [[ "$warn_count" -gt 0 ]]; then
echo -e "${YELLOW}${BOLD} WARNINGS: $warn_count warning(s), no confirmed compromises${NC}"
echo ""
echo " Review warnings above. Consider credential rotation as a precaution."
else
echo -e "${GREEN}${BOLD} CLEAN: No indicators of compromise found${NC}"
fi
echo ""
echo -e "${BOLD} References:${NC}"
echo " https://github.com/BerriAI/litellm/issues/24518"
echo " PYSEC-2026-2"
echo -e "${BOLD}════════════════════════════════════════════════════════════════${NC}"
echo ""
}
# ── Main ─────────────────────────────────────────────────────────────────────
main() {
banner
# Part A: Package discovery
enumerate_site_packages
check_pip_global
check_site_packages
check_conda_envs
check_pyenv
check_venvs
check_uv
check_dependency_files
check_caches
check_dist_info
# Part B: Malware artifact detection
check_pth_files
check_persistence
check_runtime_iocs
summary
[[ "$found_count" -gt 0 ]] && exit 1
exit 0
}
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment