LiteLLM says anyone who ran an unpinned pip install litellm between 11:39 and 13:23 UTC on October 21, or used the Docker tags v1.107.0, latest, main-latest or main-stable between 11:46 and 14:20 UTC, is affected by the malicious litellm 1.107.0. That version contained compiled malware which "steals credentials and loads remote-code execution tools," and the files it adds run every time Python starts in the affected environment. The project describes itself as an open-source AI gateway that gives applications one API for more than 100 language models.
LiteLLM's own pipeline built and published the release, so it was no typosquat: an attacker holding stolen GitHub credentials had changed the release workflow to build from a lookalike repository.
Two windows, four Docker tags and one safe pin
LiteLLM lists all seven 1.107.0 wheels on PyPI as compromised. The four Docker tags were served from both Docker Hub and GitHub's container registry (GHCR); the GHCR tags were made unavailable by 13:51 UTC, while Docker Hub kept pointing at the bad image until the end of the window. The compromised Docker Hub image index carries the digest sha256:1bbcb1443bc962e45f5aad53fe3d2c30600e0379715fb6c664cdd3ac126c339e, the string to look for in pull and deployment logs.
Three of those four tags float. Endor Labs says a deployment that pulls latest or main-stable could have received the malicious image without anyone selecting version 1.107.0. Customers on LiteLLM's private enterprise release were not affected.
LiteLLM asks users to pin 1.106.2 or any explicit earlier version, and PyPI again lists 1.106.2 as the current release, with 1.107.0 gone from the index. A team pinned that way, or pinned to the Docker image by a pre-incident digest, would not have received the bad build from PyPI or the official images. Mirrors are the exception. StepSecurity says some PyPI mirrors kept serving cached 1.107.0 wheels after PyPI removed the release, so an internal package cache may still resolve it until those files are purged.
Cleanup starts outside the environment, and rotation covers whatever it could read
LiteLLM tells users not to begin with pip: any Python start in the affected environment, pip included, runs cook.pth and sitecustomize.py again. Datadog adds that deleting the litellm package directory leaves the dropped files in place. Inspecting and cleaning from outside the environment, or rebuilding it, is the safer path.
- Requests to
www.litellm.dev/healthcheckin proxy or egress logs, the endpoint the stealer posts to. - The files
litellm-1.107.0.dist-info,cook.pthandsitecustomize.py, which LiteLLM lists as filesystem evidence. /tmp/.pc_once_*, which Aikido says means the exfiltration has fired, and/tmp/remote_code_loader.lock, which Aikido says means the loader ran or is still running.
Finding one of the three files LiteLLM lists confirms exposure, the company says, while not finding one does not prove the environment is clean. The /tmp marker is weaker still. Aikido says it keeps the credential collector to one run per machine, but StepSecurity says container restarts and /tmp cleanup remove it. SafeDep says the daemon runs an embedded copy of the stealer with no once-per-host marker at all. On Windows, the marker path likely fails, according to StepSecurity, so the stealer may run again on every Python start.
For proxy operators, LiteLLM says to rotate everything the host could reach, in particular four groups of secrets:
- the instance or task IAM role
- every provider key stored in the LiteLLM proxy database, plus the master and virtual keys
- the password in
DATABASE_URL - any secret in the environment of any process on the host
Developer laptops and CI runners hold a different set. Datadog's list of targeted data includes SSH, GPG and TLS keys, Git, npm and PyPI tokens, .env files and shell history. Socket names the AI-tool files the stealer reads, among them ~/.claude/.credentials.json, ~/.codex/auth.json, ~/.gemini/oauth_creds.json and ~/.cursor/mcp.json. Upwind cautions that the backdoor's presence does not by itself prove every credential on a machine was stolen, but says organizations should assume credentials accessible to the compromised process may have been exposed until investigation establishes otherwise.
Wiz says stolen credentials were put to use within hours. Between 16:20 and 17:01 UTC, about 4.5 hours after exfiltration, it observed the attacker validating AWS instance-role credentials harvested from compromised CI runners and Kubernetes nodes. Each was checked with sts:GetCallerIdentity and then bedrock:ListFoundationModels, a sequence Wiz calls consistent with LLMjacking, and in a few environments the attacker also enumerated IAM.
For AWS, LiteLLM's log check starts in CloudTrail: GetCallerIdentity calls from the instance role around install time, then any use of the role's credentials from outside the organization's own infrastructure. The two addresses Wiz reported, 168.100.8.57 and 165.227.201.8, belong in that search. LiteLLM gives the check for AWS only, though the stealer also collects GCP and Azure metadata tokens.
How far the bad build could travel
LiteLLM can arrive without anyone choosing it: DSPy, for one, declares litellm>=1.65.8 with only a floor, and deps.dev counts 3,158 packages that depend on LiteLLM, so a project that resolved its dependencies fresh during the PyPI window could have pulled 1.107.0 through such a chain.
PyPI Stats puts LiteLLM at about 22.8 million downloads in the past week, and Wiz cites ClickPy data for about 49,000 downloads of 1.107.0 itself, a count that probably overstates the number of affected organizations because downloads include CI runs, mirrors and repeat fetches.
Three files that run whenever Python starts
Socket says its scanner flagged the seven wheels at 11:46 UTC, seven minutes after publication. Upwind found that they place cook.pth, cook.py and sitecustomize.py directly in the site-packages root. The .pth file holds a single line, import cook, and Python processes it at every interpreter start. Datadog says that means any Python process in the environment runs the malware, even if LiteLLM is never imported. Socket describes sitecustomize.py as a second startup entry point, this one for a background loader.
The stealer calls itself privcollect and runs 16 collection modules, per Wiz. StepSecurity says it takes browser passwords, SSH and GPG keys, cloud credentials, the environment of every running process and the configs of more than 20 AI coding tools. It also validates AWS role credentials with STS GetCallerIdentity and collects GCP and Azure metadata tokens, according to Upwind.
For anyone running the LiteLLM proxy, one module matters more than the rest. When DATABASE_URL is set, Upwind says, the stealer spawns prisma and dumps the LiteLLM_CredentialsTable and LiteLLM_ProxyModelTable tables, which means "every upstream provider key the proxy stores." Socket adds that the code does not demonstrate successful access to any particular deployment or decryption of every returned credential value, and that on ordinary macOS installations the module's Linux /proc inspection fails before it launches the dump.
Everything collected is encrypted and posted to www.litellm.dev/healthcheck. Upwind says litellm.dev was registered on October 13 and is not the project's domain, which is litellm.ai.
An hourly loader whose dead drop never held a live server address
Socket says the loader in sitecustomize.py takes a lock on /tmp/remote_code_loader.lock, forks a detached process and repeatedly fetches domain.txt from the GitHub repository letters9813/pythonclient-test. It then downloads Python from http://<domain>/codepy and runs it with exec, with no signature or content-hash check, and waits 3,600 seconds between cycles.
That file never pointed anywhere real. Upwind says the dead drop "has never held a real C2," and StepSecurity gives its history: localhost:18909 on October 18, test.test.com:18909 on October 19, then empty from October 20. SafeDep says the account and repository returned 404 at 13:40 UTC.
SafeDep says it could not recover a server address. StepSecurity raises a later risk: the channel is tied to the deleted username, so anyone who registered letters9813 again and recreated the file could get code execution on every host still infected.
On Windows hosts the loader fails silently, because the fcntl and os.fork calls it relies on do not exist there, StepSecurity says. The stealer still runs.
The pipeline signed what a one-hyphen lookalike repository supplied
Wiz says the attacker pushed ten commits between 08:56 and 10:46 UTC to Berri-AI/litellm, one hyphen away from the real BerriAI organization. The first nine carry GitHub's Verified badge, which StepSecurity says only shows they were made in GitHub's web editor, since the editor signs everything it creates.
The packaging change was one line. Socket dates it to a 09:48 UTC commit that added data = "cook" to the Maturin build configuration, the setting Wiz says told the build tool to bundle the malware into the wheel. A 10:46 commit added COPY cook ./cook to the Dockerfiles. The tagged pyproject.toml lacks that setting, so reviewing the tag alone would miss it.
LiteLLM says the attacker first tried to get the workflow change merged and failed on approvals, then used the organization owner's token to bypass and disable the repository's restrictions. A push of the change was rejected at 10:08:29 UTC, and the override that applied it came five seconds later, at 10:08:34. At 10:46 the modified workflow started an official staging run from Berri-AI/litellm, and at 11:11 a second maintainer's token satisfied the production approval gate.
Around 11:54 UTC, after the release, a third maintainer's token deleted the production and staging run records, which limited the workflow logs available for investigation. The attacker then pushed a revert of the workflow change past branch protection at 12:09 and force-pushed main back at 12:24, so that main showed neither the change nor the revert. Both commits are still retrievable by SHA, and the organization audit log recorded each override.
Publishing normally required an account locked to a specific laptop in LiteLLM's office, and that restriction was relaxed earlier that week so any two engineers could trigger and approve a release. Under that rule, the attacker used one stolen token to trigger the release and another to approve it. LiteLLM believes the split was meant to evade detection in noisy audit logs.
After a March compromise of versions 1.82.7 and 1.82.8, LiteLLM says it moved releases to a private repository, created them through a strict set of workflows using Trusted GitHub Publishers, made its Docker release tags immutable and began signing images. StepSecurity says none of that helped, because the attacker did not bypass the pipeline: it changed the trusted release workflow itself, and the legitimate pipeline built, signed and published the malware.
LiteLLM traces the entry to a May key that was never revoked
LiteLLM says a configuration file committed to the public LiteLLM-Labs/lite-harness repository on May 27 contained the master key to a deployment of the LiteLLM Agent Platform, an open source tool it uses to orchestrate agents. The commit was force-pushed over and the key was identified as needing revocation, but in the company's words, "key revocation did not occur."
The Agent Platform was configured with a GitHub CLI OAuth token tied to the GitHub organization owner's account, and the token was neither a service account's nor scoped down. GitHub CLI tokens are post-2FA and are not subject to the re-authentication GitHub requires for sensitive organization or repository actions, LiteLLM adds.
On October 10 at 21:27 UTC, a request made with the master key returned the configuration of every managed agent, and with it the owner's token. At 22:11 a workflow on a throwaway branch in BerriAI/litellm posted six repository secrets to an attacker-controlled webhook.site endpoint, and through such workflows the attacker acquired similar tokens for two engineers with maintainer privileges. The GitHub audit log records a hash of the token behind each action, and LiteLLM says the hash of the GH_TOKEN taken that night matches the token that approved the October 21 release. If that match holds, the release was the use of access the attacker had held for about eleven days, not a same-day intrusion.
Socket reached the October 10 activity on its own. It says it recovered the secret-exfiltration workflow that ran in BerriAI/litellm that day and a second one on October 12 that took 14 further secrets. Socket does not claim the October 10 theft produced the access used on October 21; that connection rests on LiteLLM's audit log.
The exfiltration infrastructure arrived three days after the token theft. StepSecurity says litellm.dev was registered at 00:45 UTC on October 13 and the letters9813 GitHub account was created six hours later.
LiteLLM's first account suspected the tokens were taken from the logs of an internal LiteLLM deployment exposed to the internet. The request logs, it later found, show the first token came from a different service. The company now says some earlier assumptions "were later proven to be inaccurate or incomplete" and calls its final account the canonical source of truth wherever earlier status updates disagree.
LiteLLM says it removed every member from its GitHub organization except one account temporarily promoted to owner, and is working through every authentication token across the organization to revoke or rotate it. On October 22 at 02:19 UTC, its registries remained locked to new releases and regular releases were paused. On October 23, the company said it was confident it could safely issue a release by the end of that day; by early October 24 UTC, PyPI and GitHub still listed 1.106.2 as the newest release.
The Docker image LiteLLM called unaffected in March carried the payload in October
For the March compromise, LiteLLM said it believed the attack originated from the Trivy dependency in its CI security scanning workflow, and that customers running the official LiteLLM Proxy Docker image were not impacted. Datadog links the March incident to TeamPCP and says no connection between the two compromises has been established. Socket told The Hacker News that the embedded RSA keys, the exfiltration code and the backdoor behavior differ, though it says that does not rule out the same operator using different tooling.
Those images also carry the record that named the lookalike. Socket says the build provenance attached to the 1.107.0 Linux amd64 image identifies the private BerriAI/project-releaser workflow and records Berri-AI as the source owner, while the provenance for 1.106.2 records the legitimate BerriAI/litellm. Socket cautions that provenance is evidence of recorded build inputs, not proof those inputs were trustworthy.













