iTerm2を使っているなら、`cat readme.txt` さえ安全ではない
(blog.calif.io)- SSH integration はリモートシェルと通信するためにターミナルの escape sequence を使用しており、この構造のため通常のターミナル出力も conductor プロトコルのように解釈されうる
- 中核の問題は 信頼の破綻 にあり、実際のリモート conductor ではない 悪意あるファイル・バナー・MOTD・サーバー応答 も、偽装された
DCS 2000pとOSC 135を通じて conductor のように動作できる cat readme.txtを実行するだけでも、偽の conductor transcript がレンダリングされると iTerm2 がgetshell・pythonversion・run(...)の流れを自ら進め、攻撃側の出力は応答だけを装えばよい- エクスプロイトは、PTY に書き込まれた base64 コマンドが実際の SSH conductor が存在しないとき ローカルシェルの平文入力 に落ちる混線を利用し、最後のチャンクが
ace/c+aliFIoパスとして解釈されると実行可能になる - 修正は 3 月 31 日のコミット
a9e745993c2e2cbb30b884a16617cd5495899f86に反映されたが、公開時点では stable release 未収録 の状態で、パッチ普及前の公開によって保護の空白期間が発生した
iTerm2 の SSH 統合の背景
- iTerm2 SSH integration はリモートセッションをより豊かに理解するための機能で、リモートシェル上に小さなヘルパースクリプトである conductor を配置して動作する構造
it2sshを通じて SSH 統合を開始- 既存の SSH セッションを通じてリモート bootstrap スクリプトである conductor を送信
- このリモートスクリプトが iTerm2 プロトコルの相手役を務める
- iTerm2 とリモート conductor は一般的なネットワークサービスではなく、ターミナル I/O 上で escape sequence をやり取りする方式
- ログインシェルの検出
- Python の有無の確認
- ディレクトリ変更
- ファイルアップロード
- コマンド実行
PTY の動作方式
- 現代のターミナルエミュレータは昔のハードウェア端末のソフトウェア版であり、画面出力・キーボード入力・ターミナル制御シーケンスの解釈を担う
- シェルとコマンドラインプログラムは今でも実際の端末のように見えるデバイスを前提とするため、OS が PTY を提供する構造になっている
- PTY はターミナルエミュレータとフォアグラウンドプロセスの間に位置する pseudoterminal
- 一般的な SSH セッションでは、iTerm2 がバイト列を PTY に書き込み、フォアグラウンドプロセスである
sshがそれをリモートマシンへ転送し、リモート conductor が stdin として読み取る流れになる - iTerm2 がリモート conductor にコマンドを送るとき、ローカルでは最終的に PTY にバイト列を書き込む 方式になる
conductor プロトコル
- SSH 統合プロトコルの伝送手段には ターミナル escape sequence が使われる
- 中核要素は 2 つ
DCS 2000pは SSH conductor を hook する用途OSC 135は pre-framer conductor メッセージ用途
- ソースコードレベルでは
DCS 2000pによって iTerm2 が conductor parser を生成し、その後 parser がOSC 135メッセージを処理する構造begin <id>- command output lines
end <id> <status> runhook
- 正常なリモート conductor は ターミナル出力だけで iTerm2 と通信できる状態にある
中核となる脆弱性
- 脆弱性の本質は 信頼の破綻 であり、実際の信頼された conductor セッションではないターミナル出力も iTerm2 が SSH conductor プロトコルとして受け取ってしまう点にある
- その結果、信頼されていないターミナル出力が リモート conductor を装える 状態になる
- 悪意あるファイル
- サーバー応答
- バナー
- MOTD
- 攻撃入力は偽の
DCS 2000phook と偽のOSC 135応答を出力でき、この場合 iTerm2 は実際に SSH integration のやり取りが進行中であるかのように動作する
エクスプロイトの動作方式
- エクスプロイト用ファイルは 偽の conductor transcript を含む形になっている
- ユーザーが
cat readme.txtを実行すると iTerm2 はファイルをレンダリングするが、そのファイルには単なるテキストではなく次の要素が含まれている- 偽の conductor セッションを知らせる 偽の
DCS 2000p行 - iTerm2 の要求に応答する 偽の
OSC 135メッセージ
- 偽の conductor セッションを知らせる 偽の
- hook が受理されると iTerm2 は通常の conductor ワークフローを開始し、上流ソースでは
Conductor.start()が即座にgetshell()を送信し、成功するとpythonversion()を送信する - 攻撃側はこれらの要求を注入する必要がなく、iTerm2 が自ら要求を発行 し、悪意ある出力は応答だけを装えばよい構造になっている
状態マシンの進行過程
- 偽の
OSC 135メッセージは最小限だが正確な順序で構成されているgetshellに対する command body の開始- シェル検出出力のように見える行を返す
- そのコマンドの成功終了
pythonversionに対する command body の開始- そのコマンドの失敗終了
unhook
- この流れだけでも iTerm2 は通常の fallback 経路に入り、その後 SSH integration ワークフローが十分に完了したと判断して次の段階へ進む
- 次の段階は
run(...)コマンドを構成して送信 する過程
sshargs の役割
- 偽装された
DCS 2000phook には複数のフィールドが含まれ、その中に 攻撃者が制御するsshargsが存在する - この値は後に iTerm2 が conductor の
run ...要求を構成するとき コマンドの材料 として使われる値 - エクスプロイトでは、iTerm2 が次のデータを base64 エンコードするとき
run <padding><magic-bytes>
- 最後の 128 バイトチャンクが
ace/c+aliFIoになるようsshargsを選ぶ - この文字列は任意の値ではなく、次の 2 条件を同時に満たすよう選ばれた値
- conductor エンコード経路の有効な出力
- 有効な相対パス名
エクスプロイトを可能にする PTY の混線
- 通常の SSH integration セッションでは、iTerm2 が base64 エンコードされた conductor コマンドを PTY に書き込み、
sshがそれをリモート conductor へ転送する構造 - エクスプロイト時も iTerm2 は同様に PTY にコマンドを書き込むが、実際の SSH conductor が存在しないため ローカルシェルがそれを平文入力として受け取る 点が異なる
- 記録されたセッションでは次のような形が観察される
getshellが base64 形式で現れるpythonversionが base64 形式で現れる- 続いて長い base64 エンコード済みの
run ...payload が現れる - 最後のチャンクは
ace/c+aliFIo
- 先行するチャンクは意味のないコマンドとして失敗し、最後のチャンクはそのパスがローカルに存在して実行可能である場合に動作する構造
再現手順
- 元のファイルベース PoC は
genpoc.pyで再現できるpython3 genpoc.pyunzip poc.zipcat readme.txt
- この手順で次の 2 ファイルが生成される
ace/c+aliFIoという実行可能なヘルパースクリプト- 悪意ある
DCS 2000pおよびOSC 135シーケンスを含むreadme.txt
- 1 つ目のファイルは iTerm2 が偽の conductor と通信するよう誘導し、2 つ目のファイルは最後のチャンク到達時にシェルが実際に実行する対象を提供する
- エクスプロイトを成功させるには、
cat readme.txtをace/c+aliFIoがあるディレクトリで実行 する必要があり、そうすることで最後の攻撃者制御チャンクが実際の実行可能パスとして解釈される条件が整う
公開とパッチのスケジュール
- 3 月 30 日に iTerm2 にバグ報告
- 3 月 31 日のコミット
a9e745993c2e2cbb30b884a16617cd5495899f86で修正完了 - 執筆時点では修正はまだ stable release に含まれていない状態
- パッチコミット反映後、パッチだけを基にエクスプロイトをゼロから再構成する試みが進められた
- その過程のプロンプトは
prompts.md - 生成物は
genpoc2.py genpoc.pyと非常によく似た動作をする
- その過程のプロンプトは
公開時期に関する問題提起
- 修正が stable release に到達する前に公開が行われたことで、大多数のユーザーが実質的に保護されにくい状態 のまま脆弱性が知られる窓口が生まれた
- こうした公開時期のトレードオフには 明確な正当化 が必要
- 2 週間という期間は意味ある普及を期待するには短く、早期公開で対応を強制すべきだと正当化するにも短い
- 結果として、脆弱性は広く知られた一方で、修正版は現実に必要なユーザーへまだ提供されていない 公開の空白期間 が生じた
- より良い選択肢としては、修正版が実際にユーザーの手に届くまで待つか、早期公開がなぜ必要だったのかを明確に示すことができたはずだが、そのどちらも満たされていない
1件のコメント
Hacker Newsのコメント
安定版にパッチがまだ出ていないのに、なぜ今公開したのか気になった。アップストリームに報告されてからまだ18日しか経っておらず、公開されたコミットよりブログ記事のほうがはるかに詳細で、実際の悪用可能性を高めているように感じた。筆者がアップストリームのコミットだけでもLLMを使ってエクスプロイトを作れた点は確認したが、それでもこの文章が脆弱性の可視性をさらに高めたと思う
この作業は見事だが、そこまで驚きではなかった。機能豊富なターミナルアプリで繰り返し起きてきた問題であり、過去15年間にも似た脆弱性が何度も公開されている。lessやvimのようなツールも例外ではなく、こうした問題のかなりの部分はメモリ安全性というよりロジックバグに近いので、Rustで書き直しても自動的には解決しないと思う。一方でOSレベルの道具には単純で予測可能であってほしいと思いながら、他方では美しい色やアニメーション、無限のカスタマイズも求めるという緊張関係がある。今やAIエージェントまで入ってきており、悪意あるテキストファイルが「以前の指示を無視しろ」のような文言だけを含めばよい時代になったとも言える
PDP-10時代の話を思い出した。ある同僚が、バックスペースを押し続けるとターミナルハンドラがバッファ先頭側の文字まで消してしまうことを見つけ、さらに1行全体を消すescape文字を使うとOSが吹き飛んでしまった
6年前にもほぼ同じiTerm2のセキュリティ問題があった
私はiTerm2の作者だ。この問題はエクスプロイトチェーンの一部として使われる可能性はあるが、タイトルのように単独で非常に危険であるかのように語るのは誇張だと思う。今は家族旅行中で、戻ったら修正版をリリースする予定だ
bootstrapスクリプト、リモートのconductorエージェント、escape sequenceなどを活用する複雑なシステムで微妙なバグが起きたのは驚きではなかった。本来意図されていない形で構成要素を組み合わせると、こうした問題は起きやすい。テキストファイルやサーバーバナーのように画面へ出力される信頼できない出力に特殊コードが含まれていると、出所を検証せずに処理してしまう構造だと理解している
これは前にも見た話のように感じた。iTerm2のSSH integrationがCVEの原因になったことがあり、CVE-2025-22275も思い出す。以前の事例もあり、このスレッドで言及されている昔の問題はtmux integration関連だった。こうした統合機能はもう少し控えめに入れるほうがよいのではないかと思う
タイトルが刺激的すぎる。問題はcatではなく、iTermのSSH integrationであり、データストリームと分離されていない制御チャネル構造が危険に見える。この機能を使わず通常のSSHだけを使うなら、概ね問題ないと思う
昔のターミナルエミュレータはescape codeでキーボード再バインドまで許していた。だから信頼できないファイルは
catせず、lessのようなツールで開けというのはほとんど常識だった記事の表現が不正確だ。第2段落は「iTerm2を使うと安全ではない」と読めるが、正確には任意のShell Integration機能を使うと問題が起きうる、という程度だと思う。この機能がデフォルトで無効なら影響範囲は限定的だと理解している。間違っていたら訂正してほしい