Δεν χρειάστηκε να κάνεις import το litellm. Αρκεί να υπάρχει στον δίσκο.
Στις 11:39 UTC της Τρίτης 21ης Οκτωβρίου 2026, ανέβηκε στο PyPI η έκδοση 1.107.0 του LiteLLM — της δημοφιλούς open-source βιβλιοθήκης που λειτουργεί ως ενιαίο API προς εκατοντάδες LLM providers. Μέσα στα wheels (και τα επτά: Linux, macOS, Windows) κρύβονταν τρία επιπλέον αρχεία που δεν υπήρχαν πουθενά στον πηγαίο κώδικα του GitHub: ένα cook.pth δώδεκα bytes, ένα cook.py 22 KB και ένα sitecustomize.py 25 KB. Το πρώτο λέει μια φράση: import cook. Ό,τι έπεται είναι κακόβουλη μηχανική ακριβείας: ένα credential stealer που ψάχνει SSH keys, cloud credentials, CI/CD μυστικά και tokens από AI εργαλεία, και ένα backdoor που κάθε ώρα κατεβάζει και εκτελεί ό,τι κώδικα του πει ο επιτιθέμενος.
Το πιο ανησυχητικό στοιχείο δεν είναι το τι έκλεβαν — είναι το πότε έτρεχαν: κατά την εκκίνηση κάθε Python process. Η Python, όταν ξεκινά, εκτελεί αυτόματα ό,τι αρχεία .pth με εντολές import βρίσκει στα site-packages, και imports το sitecustomize. Δηλαδή: η εφαρμογή σου, το pip σου, ο language server του IDE σου, τα CI jobs σου — όλα έτρεχαν τον κλέφτη, ακόμα κι αν το project τους δεν είχε καμία σχέση με το litellm. Αρκεί το πακέτο να είχε γίνει pip install στο μηχάνημα.
Η ανάλυση της SafeDep που αποκάλυψε το θέμα είναι ανελέητα τεκμηριωμένη: το upstream GitHub repo δεν περιέχει πουθενά τα αρχεία αυτά, το PyPI αφαίρεσε το release γύρω στις 13:40 UTC, και οι εκδόσεις 1.106.2 (ανέβηκε 10:44 UTC — δηλαδή μία ώρα πριν την κακόβουλη) και 1.107.0rc1/rc2 είναι καθαρές. Ο επιτιθέμενος χτύπησε στο σωστό σημείο, την κατάλληλη στιγμή: το παράθυρο ανάμεσα σε δύο νόμιμες εκδόσεις.
Τι είναι το LiteLLM — και γιατί είναι το τέλειο θύμα
Το LiteLLM (BerriAI) είναι δύο πράγματα ταυτόχρονα: μια Python βιβλιοθήκη που δίνει ενιαίο interface σε 100+ LLM providers (OpenAI, Anthropic, Gemini, Azure, Bedrock, Ollama, ό,τι θες), και ένα proxy server που οι εταιρείες στήνουν μπροστά από τα LLM keys τους για routing, rate limiting, budgets και logging. Με άλλα λόγια: είναι ο διαχειριστής των πιο πολύτιμων μυστικών της AI στοίβας σου — τα API keys που πληρώνουν ανά token.
Αυτό κάνει την παραβίασή του διπλά σοβαρή. Πρώτον, επειδή το πακέτο κατεβαίνει από developers, CI pipelines και AI agents σε όλο τον κόσμο — «pip install litellm» είναι σχεδόν το default σε κάθε project που αγγίζει LLMs. Δεύτερον, επειδή ο ίδιος ο backdoor ξέρει ότι το θύμα που τρέχει LiteLLM proxy κρατάει όλα τα LLM provider keys του οργανισμού σε μια βάση: το stealer έχει αποκλειστική μονάδα db_litellm που διαβάζει το DATABASE_URL και αδειάζει τους πίνακες LiteLLM_CredentialsTable και LiteLLM_ProxyModelTable. Ένας διαχειριστής κλειδιών που γίνεται αυτόματος κλέφτης κλειδιών.
.pth: η κρυφή πόρτα της Python
Τα αρχεία .pth μέσα στα site-packages σχεδιάστηκαν για να προσθέτουν paths στο sys.path. Αλλά αν μια γραμμή τους ξεκινά με «import », η Python την εκτελεί κατά την εκκίνηση — σε κάθε process. Το cook.pth με περιεχόμενο import cook είναι η πιο σύντομη κακόβουλη γραμμή κώδικα που θα δεις φέτος: 12 bytes που μετατρέπουν οποιαδήποτε Python εφαρμογή του μηχανήματος σε client του stealer. Για αυτό δεν χρειάζεται ευάλωτη εφαρμογή, δεν χρειάζεται phishing, δεν χρειάζεται import: αρκεί το πακέτο να είναι εγκατεστημένο.
Το χρονολόγιο της 21ης Οκτωβρίου — όλα μέσα σε λίγες ώρες
Η ένεση: μία γραμμή στο pyproject.toml
Πώς φόρτωσαν τα αρχεία μέσα σε wheel που μοιάζει απόλυτα κανονικό; Με την πιο απλή απάντηση: έβαλαν μία γραμμή στο build configuration. Στο sdist του 1.107.0, η ενότητα του maturin (του Rust-Python build tool) έχει ένα data = "cook" που δεν υπάρχει πουθενά στο GitHub:
[tool.maturin]
...
python-source = "."
data = "cook" # <-- η ένεση
python-packages = ["litellm_cli"] # legit — υπάρχει upstream
Η ρύθμιση data λέει στο maturin να αντιγράψει τον φάκελο cook/ μέσα στα purelib του wheel — δηλαδή εκεί όπου το pip κάνει install στα site-packages. Ο φάκελος cook/ περιείχε τα τρία αρχεία-implant. Αλλά το ζουμί είναι πιο βαθύ: η SafeDep σύγκρινε blob-by-blob ολόκληρο το sdist με τα git trees. Το malicious sdist ταιριάζει με το commit a8113b0 του main συν πέντε επεξεργασμένα αρχεία: τέσσερα Cargo.toml με μια αθώα γραμμή readme = "README.md" (την ίδια που έχουν νόμιμα τα releases — θόρυβος που καλύπτει) και το pyproject.toml, όπου εκτός από την ένεση, ο attacker κάνει rollback την έκδοση από 1.108.0 σε 1.107.0 και κλειδώνει τα companion packages στις τιμές του rc2. Αποτέλεσμα: οποιοδήποτε dependency diff ενάντια στην release candidate έδειχνε… τίποτα.
Το release δεν ταιριάζει με το tag
Το tag v1.107.0 δείχνει commit που λέει 1.108.0 — και το sdist λέει 1.107.0 αλλά χτίστηκε από άλλο, νεότερο commit. Αυτή η «σφήνα» ανάμεσα σε tag, branch και artifact είναι το μοναδικό δακτυλικό αποτύπωμα του επιτιθέμενου. Και είναι ακριβώς ο έλεγχος που κάνει η provenance attestation του PyPI αυτόματα — εκτός που το 1.107.0, όπως και οι νόμιμες εκδόσεις της BerriAI, δεν είχε καμία.
Τέλος: τα wheels φτιάχτηκαν με maturin 1.9.4 — την ίδια έκδοση των νόμιμων releases — και έχουν σωστές εγγραφές RECORD, άρα δεν πειράχτηκαν μετά το build. Στο Windows wheel, και τα 2.804 αρχεία .py έχουν CRLF line endings, συμπεριλαμβανομένων των τριών implant files: ένδειξη ότι το build έτρεξε σε Windows machine με git checkout και line-ending conversion — όχι στο κανονικό CI. Η SafeDep έλεγξε όλα τα 539 GitHub Actions runs του παραθύρου 09:30–12:30 UTC: κανένα δεν έχτισε ή δημοσίευσε το release.
cook.py: ο κλέφτης που ξέρει τι να ψάξει
Το cook.py είναι ~61 KB Python κώδικας τυλιγμένος σε ένα στρώμα exec(zlib.decompress(base64.b64decode('...'))). Ο αποκωδικοποιημένος κώδικας ορίζεται από μια ενότητα configuration που τα λέει όλα:
PC_URL = 'http://www.litellm.dev/healthcheck'
PC_TOKEN = 'XWiA8gZNa7NcX2XRoAEb' # X-Token header
PC_PUBKEY = '-----BEGIN PUBLIC KEY----- MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvxRBQMVXDXVskKS47haU ...' # RSA-4096
PC_ONCE_PATH = '/tmp/.pc_once_72129118c59f5636c0bb'
PC_ASYNC = True # daemon thread
PC_MODULES = ['sysinfo','netinfo','browser_pw','browser_hist','ssh_keys',
'shell_hist','cloud_creds','dev_secrets','container_creds',
'cicd_secrets','ai_devtools','env_vars','db_litellm',
'cloud_metadata','gpg_keys','tls_privkey']
Δεκαέξι μονάδες συλλογής, καθεμία στοχευμένη:
- Συστήματος: hostname, χρήστης, OS, IPs, network settings.
- Browser: αποκρυπτογραφημένοι saved passwords Chromium (μέσω DPAPI master key στο Windows) και Firefox logins.json/key4.db, και ιστορικό.
- Developer identity: ~/.ssh/*, bash/zsh history, PowerShell PSReadLine, cmd history.
- Cloud: ~/.aws/credentials, Azure profiles, gcloud, ~/.kube/config, Docker config, GitHub CLI hosts.yml, rclone.conf, .netrc.
- CI/CD: GitLab runner config.toml, Jenkins initialAdminPassword και master.key, .vault-token, Terraform credentials.tfrc.json, Packer.
- AI εργαλεία: tokens από Claude Code, Claude Desktop, Cursor, Windsurf, Copilot, VS Code mcp.json, Gemini CLI, Codex auth.json, Continue, Aider, Goose, opencode, Qwen, Crush, Kiro, Zed, Hugging Face. Η λίστα διαβάζεται σαν κατάλογος του AI developer stack του 2026 — ο επιτιθέμενος ξέρει ακριβώς πού ζουν πλέον τα μυστικά.
- Ειδικά για LiteLLM: db_litellm — σύνδεση στο DATABASE_URL και dump των πινάκων με τα LLM provider keys του proxy.
- Cloud metadata: queries στο 169.254.169.254 (IMDSv2, fallback v1) για κλοπή IAM ρόλων από cloud instances.
- Crypto/TLS: GPG keys και TLS private keys.
Η εξαγωγή έχει επαγγελματική υγιεινή: τα JSON δεδομένα κρυπτογραφούνται με τυχαίο AES-256-CBC key, το key κρυπτογραφείται με RSA-OAEP και το ενσωματωμένο δημόσιο κλειδί των 4096 bit, και το πακέτο (hdr.json, data.bin, key.bin) στέλνεται σε POST στο www.litellm[.]dev/healthcheck — το domain που μιμείται το legit healthcheck endpoint του LiteLLM proxy. Μόλις πετύχει, γράφει το marker /tmp/.pc_once_… ώστε να μην ξαναστείλει από το ίδιο host: μία φορά είναι αρκετή.
sitecustomize.py: ο υπάκουος daemon που περίμενε εντολές
Το δεύτερο αρχείο εκμεταλλεύεται έναν άλλο μηχανισμό της Python: το module sitecustomize φορτώνεται αυτόματα κατά την εκκίνηση, αν υπάρχει στο path. Το implant κάνει fork και setsid (daemon, parent PID 1, working directory /), παίρνει exclusive lock στο /tmp/remote_code_loader.lock ώστε να τρέχει ένα daemon ανά host, και μπαίνει σε ωριαίο βρόχο:
- Κάθε ώρα: διαβάζει το πρώτο domain από
raw.githubusercontent.com/letters9813/pythonclient-test/main/domain.txt. - Ξανατρέχει τον stealer — αυτή τη φορά χωρίς once-per-host marker, δηλαδή συνεχόμενη συλλογή.
- Κατεβάζει και εκτελεί με
exec()ό,τι επιστρέψει τοhttp://<domain>/codepy— second stage πλήρως ελεγχόμενο από τον επιτιθέμενο, με payload που μπορεί να αλλάζει ανά ώρα.
Εδώ η φύση της απειλής αλλάζει ριζικά: ο stealer κλέβει snapshot του παρόντος. Ο daemon παρέχει συνεχή έλεγχο — ο επιτιθέμενος μπορεί ανά ώρα να αλλάξει payload, να στείλει ransomware, miner ή logger. Η αδυναμία του ήταν η υποδομή: στις 13:40 UTC το account letters9813 σβήστηκε από το GitHub και το domain.txt εξαφανίστηκε. Ο daemon πλέον δεν βρίσκει διεύθυνση και δεν κάνει τίποτα. Αλλά τα hosts που έτρεξαν τον daemon μέσα στο παράθυρο των δύο ωρών μπορεί να είχαν ήδη λάβει και εκτελέσει second stage κώδικα — η SafeDep δεν μπόρεσε να ανακτήσει τη διεύθυνση του server, άρα δεν γνωρίζουμε τι έτρεξε εκεί.
Ποιος το έχτισε — το ερώτημα της pipeline
Τα ευρήματα που γεννούν τα περίεργα ερωτήματα: τα wheels θυμίζουν έξοδο της κανονικής BerriAI release pipeline — σωστό maturin 1.9.4, σωστές RECORD εγγραφές, ο bot δημοσίευσε το GitHub Release με την ίδια καθυστέρηση που κάνει στις νόμιμες εκδόσεις. Αλλά κανένα GitHub Actions run δεν έχτισε ή ανέβασε το πακέτο. Πώς τα έφτιαξε και τα ανέβασε; Η SafeDep διατυπώνει τρία σενάρια, με μεσαία έως υψηλή βεβαιότητα για κάποιο από τα δύο πρώτα:
- Παραβιασμένο credential maintainer ή bot (PyPI token, session, release key) — ο attacker χτίζει ο ίδιος από δικό του μηχάνημα (το CRLF pattern του Windows wheel συνάδει) και ανεβάζει ως «maintainer».
- Ιδιωτικό branch ή mirror — η pipeline χτίζει από attacker-controlled πηγή, χωρίς να το ξέρει.
- Κλεμμένο PyPI token από εξωτερικό μηχάνημα — ταιριάζει λιγότερο καλά με τα ευρήματα, αλλά δεν αποκλείεται.
Και εδώ είναι το συμπέρασμα που καίει: μόνο τα logs της BerriAI μπορούν να απαντήσουν οριστικά — και αυτό είναι εξ ορισμού αργά. Το PyPI δεν διαθέτει provenance attestations για το 1.107.0, ούτε για τις νόμιμες εκδόσεις. Δηλαδή: ούτε το registry ούτε εσύ μπορούσες να αποδείξεις ότι το wheel που κατέβασες χτίστηκε από το commit που βλέπεις στο GitHub. Ο attacker χρειάστηκε να παρακάμψει ακριβώς μία στρώση — τη σχέση εμπιστοσύνης ανάμεσα στο repo και το registry — και αυτή η στρώση δεν είχε χτιστεί καθόλου.
Τον Μάρτιο το ξαναζήσαμε. Και ξεχάσαμε.
Αυτό δεν είναι το πρώτο χτύπημα στο LiteLLM — ούτε καν το πρώτο φέτος. Τον Μάρτιο του 2026, οι εκδόσεις 1.82.7 και 1.82.8 στο PyPI παραδόθηκαν με litellm_init.pth — ένα credential stealer με ακριβώς την ίδια τεχνική .pth, στο πλαίσιο της καμπάνιας που οι ερευνητές της Datadog ονόμασαν TeamPCP. Το HN thread για το issue της BerriAI μάζεψε 739 πόντους, ο Karpathy το σχολίασε, το BleepingComputer έγραψε για αυτό… και επτά μήνες μετά, ο ίδιος μηχανισμός ξαναδούλεψε σε νεότερη έκδοση, με πιο εξελιγμένο payload.
Η ιστορία των supply chain attacks είναι μια λίστα επαναλήψεων που αρνούμαστε να διαβάσουμε:
- SolarWinds (2020): κακόβουλος κώδικας μέσα σε υπογεγραμμένες ενημερώσεις — ~18.000 οργανισμοί.
- Codecov (2021): παραποιημένο bash uploader — κλοπή credentials από CI pipelines.
- 3CX (2023): υπογεγραμμένο desktop app μολυσμένο downstream — double supply chain.
- XZ Utils (2024): χρόνια infiltration, backdoor στα liblzma — έπεσε από μία υποψία ενός engineer.
- TeamPCP / litellm Μαρτίου 2026: κλέφτης σε .pth — μια πρώτη δοκιμή του επιτιθέμενου.
- litellm 1.107.0, Οκτωβρίου 2026: η ίδια πόρτα, εμφυτευμένη πλέον στο build configuration, με daemon και C2. Η τεχνική ωρίμασε από δοκιμή σε πλήρη επιχείρηση.
Checklist: αν έκανες pip install litellm σήμερα (ή τρέχεις Python hosts που δεν ελέγχεις)
- Έλεγξε έκδοση:
pip show litellm/pip index versions litellm. Το 1.107.0 έχει αφαιρεθεί από το PyPI — αν το δεις στο environment σου, θεώρησέ το compromised. - Ψάξε τα markers:
cook.pth,cook.py,sitecustomize.pyστα site-packages. Θυμήσου: το sitecustomize.py μπορεί να υπάρχει και για νόμιμους λόγους — δες περιεχόμενο και hash. - Ψάξε τα filesystem σημεία:
/tmp/.pc_once_72129118c59f5636c0bb(ο stealer έστειλε δεδομένα) και/tmp/remote_code_loader.lock(ο daemon έτρεξε). - Ψάξε processes: daemonized Python με parent PID 1, own session, CWD
/. ΨάξεPRIVCOLLECT_ACTIVE=1στο environment. - Ψάξε network logs: requests προς
www.litellm[.]dev, headerX-Token, και access στο169.254.169.254από Python processes. - Rotate τα πάντα — με αυτή τη σειρά: LLM provider keys (αν τρέχεις LiteLLM proxy: rotate ΟΛΑ, όχι μόνο τα ενεργά), cloud credentials (AWS/Azure/GCP), SSH keys, tokens AI εργαλείων (Claude, Cursor, Copilot, HF…), CI/CD secrets (Jenkins, GitLab, Vault), και browser passwords αν υπήρχαν Chromium profiles.
- Rebuild, μην καθαρίσεις: αν επιβεβαίωσες implant, φτιάξε νέα environments/hosts από clean images. Ένας daemon που έτρεξε έστω και μία φορά είναι αρκετός λόγος να μην εμπιστευτείς το host.
- Πήγαινε σε καθαρές εκδόσεις: 1.106.2 ή νεότερη καθαρή έκδοση (το vendor αναμένεται να κυκλοφορήσει hotfix) — και pin σε hash (pip --require-hashes), όχι σε version.
Import, provenance και το τέλος της αφελούς εμπιστοσύνης στο registry
Τρία μαθήματα μένουν. Πρώτον, το γλωσσικό design χρέος: τα .pth files και το sitecustomize είναι μηχανισμοί της δεκαετίας του 2000 που επιτρέπουν κώδικα να τρέχει χωρίς καν να κληθεί. Αν διαχειρίζεσαι Python υποδομές, τα εργαλεία υπάρχουν: pip-audit, έλεγχος για unexpected .pth files, freeze environments με hashes. Και ως κοινότητα, αξίζει η συζήτηση — γιατί κάθε λύση τύπου «τρέξε Python με -S» είναι ένα επίθεμα πάνω στο πρόβλημα, όχι λύση.
Δεύτερον, το provenance χάσμα: το PyPI υποστηρίζει Trusted Publishing και attestations — αλλά είναι προαιρετικά, και τα περισσότερα projects (συμπεριλαμβανομένου ενός core AI dependency όπως το LiteLLM) δεν τα χρησιμοποιούν. Όταν δεν μπορείς να αποδείξεις ότι το artifact προέρχεται από το source που βλέπεις, η διαδρομή repo→registry είναι σκόπιμα εύκολο να πλαστογραφηθεί. Η φόρμουλα είναι γνωστή εδώ και χρόνια: OIDC-based trusted publishing, signed attestations, SLSA levels. Απλώς δεν μπαίνει μέχρι να χτυπήσει το καμπανάκι.
Τρίτον, το AI χρέος: ο stealer είχε εξειδικευμένη μονάδα για Claude Code, Cursor, Copilot, MCP configs, Codex, Aider… Τα AI dev tools του 2026 κρατούν tokens με πρόσβαση σε κώδικα, υποδομές και πιστωτικές κάρτες — και ζουν στα ίδια dotfiles με τα SSH keys. Η AI supply chain δεν είναι πια θεωρία: είναι η λίστα targets που ο επιτιθέμενος σου έχει ήδη γράψει σε config.
FAQ: ό,τι ρωτούν πρώτο μετά από μια τέτοια είδηση
Χρειάστηκε να κάνω import το litellm για να εκτελεστεί ο κακόβουλος κώδικας;
cook.pth περιέχει τη γραμμή import cook και το sitecustomize.py φορτώνεται αυτόματα. Αρκεί το πακέτο να είναι εγκατεστημένο σε ένα environment: κάθε εκκίνηση Python process — εφαρμογή, pip, IDE, CI — έτρεχε τον κώδικα.Ποιες εκδόσεις είναι καθαρές;
Πώς κρύφτηκε το payload μέσα στο wheel;
data = "cook" στο [tool.maturin] του pyproject.toml. Το maturin αντέγραψε τον φάκελο cook/ στα site-packages, χωρίς αντίστοιχο commit στο δημόσιο GitHub repo.Τι ακριβώς έκλεβε;
Πώς καταλάβαμε ότι το πακέτο ήταν κακόβουλο;
Τι κάνω αν εγκατέστησα την 1.107.0;
/tmp/.pc_once_* και /tmp/remote_code_loader.lock, ψάξε κίνηση προς www.litellm[.]dev και κάνε rotate LLM keys, cloud credentials, SSH keys και tokens AI εργαλείων. Αν βρεις implant, rebuild από clean image.Είναι το C2 ακόμα ενεργό;
domain.txt στο GitHub account letters9813 έγινε 404 και το release αφαιρέθηκε, οπότε ο ωριαίος loader δεν βρίσκει πλέον διεύθυνση. Ωστόσο, hosts που έτρεξαν τον daemon μέσα στο δίωρο μπορεί να είχαν ήδη εκτελέσει second-stage payload.Είναι η ίδια επίθεση με τον Μάρτιο του 2026;
litellm_init.pth (TeamPCP). Τώρα χρησιμοποιήθηκε cook.pth, sitecustomize.py και C2 — η ίδια τεχνική .pth, αλλά με πιο εξελιγμένο payload και daemon.