編集部注:本稿は、Black Hat USA 2026で発表された研究内容を詳述する「Breaking the M365 Copilot Sandbox」全4回シリーズの第1回です。
ユーザーがLLMにドキュメントについて質問する場面を想像してください。目に見えない攻撃者がそのチャットセッションに対話型のプロンプトチャネルを確立し、プロンプトを送信してアシスタントの応答を読み取り、次に何を尋ねるかを決められるとしたらどうでしょうか。
私たちはこの攻撃クラスを「Remote Prompt Execution(RPE)」と呼んでいます。単一の初期トリガーの後、AIアシスタントを適応的かつ双方向に制御できる点が特徴です。公に文書化された初のRPEである「ChatMate」は、悪意あるドキュメントがMicrosoft Copilotにコード実行環境を呼び出させ、ネットワーク分離されたサンドボックスから脱出し、攻撃者への人間操作によるプロンプトシェルを確立する様子を示しています。
ChatMateが重要な理由
Microsoft Copilotは非常に多くの企業に導入されており、最近の多くのアシスタントと同様、実際の計算処理を行うためにバックグラウンドでPythonインタープリターを稼働させています。モデルが生成したコードを実行するインタープリターは興味深い攻撃対象であるため、私たちはこれを調査しました。その結果、最終的にはコンテナからホストへの完全なエスケープと、被害者のMicrosoft 365データへの到達経路にまでたどり着き、CVE-2026-32193(CVSS深刻度:High、8.8)として追跡されています。
本シリーズでは、隠されたテキスト命令からホストノード上でのコード実行、そして最終的には被害者のアシスタントコンテキスト内での人間操作によるプロンプトシェルに至るまでの経路をたどります。第1回では、その他すべての土台となる2つの構成要素を扱います。
- Copilotのコード実行安全レイヤーの回避:Copilotは
psやnetstatのような「詮索的な」コードの実行を拒否しますが、私たちはあらゆるコードを確実に実行させる方法を見つけ、アシスタントを汎用のPythonランナーへと変貌させました。 - サンドボックス内でのroot権限へのローカル権限昇格
最も深刻で影響の大きいバグはシリーズの後半で登場します。今回はまず、それを見つけるために必要なツールを紹介します。
Remote Prompt Executionが意味するもの
RPEとは、侵害後の能力を指すもので、攻撃者が被害者の認証済みAIアシスタントに対して繰り返しプロンプトを送信し、出力を受け取り、被害者側のさらなる操作なしに次のプロンプトを調整できる状態を表します。ChatMateでは、これは人間が操作するフルレスポンス型のREPLという形を取ります。攻撃者はアシスタントの完全な回答を確認し、次のプロンプトを入力し、これを繰り返します。
ChatMateは、私たちが把握している中で最も近い関連研究であるVaronisのRepromptとは独立して開発されました。Repromptは、Copilotが本来意図していたインターネット機能を利用して、事前の応答に依存して後続の指示を組み立てるサーバー主導のリクエストチェーンを作り出しました。公開されたレポートでは、継続的かつ動的なデータ流出について説明されていますが、人間が操作する汎用のプロンプトシェルを公に実証してはいません。とはいえ、根底にある制御ループは密接に関連しています。
したがって両者の違いは、適応的な遠隔制御自体が過去に存在しなかったという点にあるのではありません。違いはChatMateがそれをどう実証したかにあります。悪意あるドキュメントがコード実行のトリガーとなり、ネットワーク分離されたサンドボックスから脱出し、脱出後の環境を完全応答型のプロンプトチャネルへと変えるのです。Repromptは本来意図されたインターネット機能を悪用しましたが、ChatMateはアシスタントの実行境界を越えることでそのチャネルを生み出しています。
私たちが目にしているものは何か
大規模言語モデルはテキスト生成には優れていますが、機械的な計算処理はあまり得意ではありません。業界標準的な解決策は、モデルにコードインタープリターを持たせることです。例えば、PDFを生成する場合、モデルはPDFを生成するPythonコードを書き、サンドボックス内でそれを実行し、結果を読み戻します。Microsoft 365 Copilotもまさにこれを行っています。スプレッドシートの処理、グラフの作成、アップロードされたドキュメントの処理を依頼すると、裏側ではPythonコードが生成され、コード実行ツールを通じて実行されます。
このコード実行ツールは、コードを隔離されたサンドボックス環境で実行します。攻撃的なセキュリティ研究に携わっている方なら、この一文にピンとくるはずです。攻撃者が制御するコードを安全に実行するサンドボックスを適切に構築するのは、非常に難しいことです。
Copilotに私たちのコードを実行させる
最初の障害は、Copilotが任意のコードを実行したがらないことです。コード実行ツールは正当なデータ処理タスクのためのものであり、コードが環境を探るような挙動に見える場合、それに抵抗する安全レイヤーが存在します。ps(プロセス一覧)やnetstat(ソケット一覧)の実行を依頼しても、丁寧に拒否されます。
しかし、この安全レイヤーは提示されたコードから意図を判断しているにすぎず、その意図は簡単にごまかせます。Copilotに対し、解凍対象の圧縮バイト列を使ってgzip解凍のベンチマークを取るよう指示すれば、たとえ解凍後のバイト列が先ほど実行を拒否したまさにそのps/netstatコードだったとしても、完全に無害なパフォーマンステストとして読み取られます。
そこで私たちは、任意のPythonソースコードを受け取り、それをgzip圧縮し、無害に見える「gzipbenchmark」プロンプトでラップする小さなヘルパーを書きました。
これに次を与えると:
…ペイロードが16進数のブロブとなったプロンプトが出力されます。
Copilotはそれを無害なベンチマークだと認識し、解凍してexecで実行し、出力をダウンロード可能なファイルとして返してきます。先ほどまで拒否していたpsやnetstatが、今度は何の問題もなく実行されます。以降、「Xを実行した」というのは「このハーネスを介してXを実行した」という意味になります。私たちは事実上、Copilotサンドボックス内にPythonのREPLを手に入れたのです。
ただし稀に、Copilotが不審に思い、ペイロードを実行せずに解凍するだけのコードを書いて、これから実行しようとしているものを確認しようとすることがありました。この場合は実行を拒否されました。より堅牢な回避方法としては、ペイロードだけでなくexec自体も難読化することが考えられますが、研究目的であれば単純な手法で十分でした。
内部からサンドボックスをマッピングする
REPLを手に入れたら、まずは偵察です。Copilotはどのようなサンドボックスでコードを実行しているのでしょうか。
psを実行すると、すぐに環境の全体像がつかめます。
明確な構図が見えてきます。
- PID 1は
/app/entrypoint.shで、rootとして実行されています。これが他のすべてのプロセスを起動します。これは後ほど重要になるので覚えておいてください。 - 私たちのコードは、権限のない
ubuntuユーザーとして、Jupyter/IPythonカーネル内で実行されます(これがipykernel_launcherとjupyter-notebookのプロセスです)。 goclientappはGoで書かれたバイナリで、後のリバースエンジニアリングによれば、外部との通信を担い、実行すべきコードや、アップロード/ダウンロードするファイルなどをカーネルに渡す役割を持っています。httpproxyappはプロキシで、Apache Tikaはドキュメント解析用に配置されています。
netstatと環境変数からは、残りの情報が得られました。いくつか目を引く点がありました。まず、この環境は非常に厳格にネットワークが強化されています。インターネットは使えず、DNSも意図的に壊されており、http_proxy/https_proxy変数は、すべてのリクエストに対して「forbidden」を返すローカルプロキシを指していました。
netstatではまた、私たちのものではない、いくつかのローカルリスナーも表示されました。
ポート53827と53828は、Azure内部サービスである「PodAgent」の一部です。特に興味深いのがポート8578です。これは私たちが何を投げても404を返すHTTPサーバーで、その所有プロセスは見えません(PID欄が「-」になっています)。この謎めいたサービスは、この研究において重要な役割を果たすことになります。
すべてのファイルシステムマウントを一覧表示するコマンドfindmntを実行すると、有望な情報が見つかりました。/mnt/data、/etc/hosts、/etc/resolv.confはいずれも、コンテナのオーバーレイではなく、ホストのディスク(/dev/sda2)によって裏付けられていました。つまり、これらはコンテナ自身のファイルシステムの外部と共有されているということです。
マッピングが完了したサンドボックス。私たちのコードは、インターネットアクセスのないJupyterカーネル内で権限の弱いubuntuユーザーとして実行される一方、entrypoint.shはrootとして実行されており、脇では正体不明の「謎のサービス」が一つ待ち構えています。
壁にぶつかる
私たちは、この権限のない立場からこの箱の外に出る方法を探るのにかなりの時間を費やしましたが、その結果分かったのは、堅牢な対策がしっかり施されているということでした。
- ネットワーク。インターネットも機能するDNSもありません。IMDSや、よく知られたAzureおよびパブリックDNSサーバー十数個への到達を試みましたが、
goclientappが通信していたIPはすべてタイムアウトしました。 - 他のセッション。同一アカウント、および別のアカウントの他のCopilotセッションは分離されたままで、通信することはできませんでした。
- ローカルサービス。
goclientapp、httpproxyapp、Jupyter、Tikaはすべて私たち自身のubuntuユーザーとして実行されています。これらを制圧しても何も得られません。興味深いリスナー(8578番、PodAgentのポート群)は、私たちからは見えないプロセスによって所有されており、同様にネットワーク面で厳重に保護されていると考えられます。
サンドボックスはよく作り込まれているように見え、私たちは行き詰まってしまいました。
先に進むには、例えばトラフィックを傍受するためにも、root権限が必要でした。つまり、サンドボックス内での権限昇格の脆弱性を見つける必要があったのです。
サンドボックス内でroot権限を取得する
最初のpsの出力の1行に戻りましょう。
rootとして実行されているPID 1は、/app/entrypoint.shにあるbashスクリプトです。そして/appは、サンドボックスハーネスのファイルすべてが置かれている場所であり、私たちのubuntuユーザーがずっと読み取りアクセスしてきたのと同じ場所です。
そこで、このスクリプトの権限を確認してみました。この先どうなるか、想像がつくでしょうか。
なんと、私たちに書き込み権限があったのです。環境全体を起動するroot所有のプロセスが、権限のないユーザーでも編集できるシェルスクリプトだったのです。もしこのスクリプトを再実行させることができれば、追記した内容がrootとして実行されることになります。しかしこれはエントリポイントであり、他に誰もこれを実行しません。プロセスを終了させれば環境自体が破棄され、プラットフォームがサンドボックス全体をリセットしてしまいます。
その解決策が「bashスクリプト追記攻撃」です。bashはシェルスクリプトを実行する際、ファイルを一行ずつ読み込みながら実行します。つまり、bashがスクリプトの実行を開始した後であっても、現在実行中の行より下の部分にファイルへの変更を加えれば、その変更は読み込まれ実行されるということです。
今回のケースでは、entrypoint.shスクリプトはおおよそ次のようになっていました。
サンドボックスが起動している間、このスクリプトは最後の行――waitで待機した状態になります。つまり、私たちがやるべきことは次の2つだけです。
wait行の後にバックドアを追記する。entrypoint.shにそのwait行の実行を終わらせ、追記したコードに到達させる。
ステップ1は簡単です。しかしステップ2はどうやって実現するのでしょうか。waitはすべての子プロセスが終了するまでブロックします。つまり、wait行が処理を返し、私たちのバックドアコードが実行されるのは、./goclientapp、./httpproxyapp、./keepAliveJupyterSvc.shのすべてが終了した後だけです。であれば、この3つのプロセスを終了させればバックドアが動く、ということでしょうか。
ほぼ正解ですが、一つ落とし穴があります。./goclientappを終了させると、サンドボックスと通信する唯一のチャネルも失われてしまいます。一瞬だけrootとして動作することにはなりますが、それでは何も得られません。
そこで、もっと注意深いアプローチが必要になります。これらのプロセスを終了させてwaitを解放し、root権限を取得したうえで、プラットフォームが何かが停止したことに気づかないほど速やかに環境を元通りに復元する必要があります。具体的には、追記したペイロードが、エントリポイントが通常起動するすべてのサービスを再起動します。追記後のentrypoint.shは次のようになりました。
理論上は単純です。しかし実際にはなかなかうまくいきませんでした。何をやっても環境がリサイクルされてしまうのです。私たちは多くの誤った仮説を検証した末に、もっと早くやるべきだったこと――オフラインでの再現――にようやく取り掛かりました。
同一バージョンのUbuntu VMを立ち上げ、ハーネスのファイル一式をコピーし、不要な部分をすべて取り除いた上で攻撃を実行してみると、原因が明らかになりました。再起動後、goclientappの起動に失敗していたのです。単純な再起動処理では満たせない起動時の要件があり、そのためこのサービスが復帰していませんでした。その結果、プラットフォームはセッションが死んだと判断していたのです。この点を修正すると、goclientappの再起動は問題なく成功し、VM上、そして続いてCopilot上でも仕組み全体が正しく動作しました。
これで、Copilotサンドボックス内でroot権限を得ることができました。
このrootを一度きりのものではなく実用的なものにするため、ペイロードは小さなデーモンを起動します。これはポート1337でローカルリスナーとして動作し、bashコマンドを受け取ってrootとして実行します。そして、gzipのトリックとまったく同様に、rootのbashコマンド(またはrootのPythonコード)を受け取り、それを通常のサンドボックスコードにパッケージ化してlocalhost:1337と通信する小さなスクリプトを用意しました。外部から見ると、単にCopilotにPythonを実行させているだけのように見えますが、その裏ではrootコマンドが実行されているのです。
言及しておく価値のある一つの行き止まりがあります。それを除外できたこと自体が有益な学びだったからです。古典的なcp /bin/bash /mnt/data/rootbash; chmod +xsというSUIDのトリックはうまくいきませんでした。このマウントにはnosuid/no-new-privilegesの制約があるため、SUIDバイナリを作ってもここでは何も得られません。一方で、コードをrootとして実行すること自体は問題なく機能し、これがポート1337のデーモンによって実現されているのです。
成果:Azure Dynamic Sessions
root権限を得たことで、ようやくトラフィックを観測できるようになりました。tcpdumpは使えなかったので、Pythonでプロミスキャスモードの小さなパケットスニファーを自作しました。
これが捕捉したリクエストの一例です。
Host: ACA-Session-Interpreter。「ACA」はAzure Container Appsを意味します。これとAzureContainerApps-DynamicSessionsという環境変数を組み合わせることで、全体像が一気に明らかになりました。Copilotのコードインタープリターは、信頼できないコードを使い捨てのサンドボックスで実行するための汎用的かつ一般公開されているAzure製品、「Azure Container Apps dynamic sessions」の上に構築されていたのです。
この発見は、私たちの研究の方向性を大きく変えました。もしCopilotのサンドボックスがDynamic Sessionsそのものであるなら、gzipプロンプトもチャットウィンドウも安全レイヤーとの攻防も不要で、明確に文書化されたAPIを通じて、まったく同じ環境をAzureから直接借りることができるということになります。
個人のAzureアカウントでDynamic Sessionsプールを開き、そのREST API経由でコードを実行したところ、まさに同一の環境であることが確認できました。同じ/app、同じサービス、そのままの状態で権限昇格エクスプロイトが機能したのです。
つまり、次のことが言えます。
- 研究が劇的にやりやすくなりました。チャットボットの代わりに通常のPythonスクリプトからセッションを開き、権限昇格を実行し、rootコマンドを操作できるようになったからです。
- ここで発見した内容はCopilot固有のバグにとどまらず、Azure Dynamic Sessions自体にも影響を及ぼすものでした。
まとめ
第1回を終えた時点で、私たちは次のことを達成しました。
- 安全レイヤーにもかかわらず、Copilotサンドボックス内で任意のコードを確実に実行できるようになった。
- そのサンドボックス内でroot権限へ昇格できるようになった。
- サンドボックスの正体がAzure Container Apps dynamic sessionsであることを突き止め、明確でスクリプト操作可能な検証用の複製環境を手に入れた。
それでも、私たちがいるのは依然として完全に自分自身のものであるコンテナの内部にすぎません。ネットワークにつながらない使い捨てサンドボックス内でのroot権限は、それ単体では大きな影響を持ちません。ここから到達できるものはすべて私たち自身の権限で動作しているか、あの優れたネットワーク強化によって遮断されています。唯一残された手がかりは、ポート8578で稼働する正体不明のHTTPサーバーです。何を試しても404を返してきたあのサーバーです。
何を送っても404を返すこのポート8578のサーバーこそ、私が説明できず、突破もできなかった唯一の存在でした。第2回では、この謎に戻っていきます。
Ori LahavはRubrik Zero Labsのセキュリティリサーチャーです。本研究は、新興のAIインフラのセキュリティに関するRubrik Zero Labsの取り組みの一環として実施されました。