Cacti: 認証されていないリモートコード実行

6 読了時間

Stefan Schiller photo

Stefan Schiller

Vulnerability Researcher

TL;DR 概要

  • Sonarのセキュリティ研究者は、広く展開されているネットワーク監視フレームワークCactiにおいて、認証されていないリモートコード実行の脆弱性を発見しました。これにより、攻撃者は有効な資格情報なしで任意のコマンドを実行できます。
  • この脆弱性はデータソース選択を処理するHTTPエンドポイントに存在し、ユーザーが提供した入力がシステムコマンドに無加工で渡されることで、典型的なコマンドインジェクションのシナリオが生じます。
  • この脆弱性は公開されたCactiインスタンスを運用している組織に影響を与え、悪用されるとインフラ全体の侵害につながる可能性があります。
  • Cactiを使用している組織は、利用可能なパッチを直ちに適用し、Cactiへのアクセスを信頼できる内部ネットワークに制限し、悪用の兆候を監査するべきです。

Cactiはオープンソースのウェブベースの監視ソリューションで、2001年の最初のリリース以来の長い歴史があります。現在では、広く確立され、積極的にメンテナンスされ、世界中で展開されています。Shodanのクイック検索では、何千もの組織がインターネットにインスタンスを公開していることがわかります。

私たちのコード品質ソリューションの技術を継続的に改善するために、オープンソースプロジェクトを定期的にスキャンし、結果を評価しています。Cactiの場合、私たちのエンジンは有望なコマンドインジェクションの脆弱性を報告しました。この発見を分析すると、認証されていない攻撃者が認証バイパスを利用して脆弱性を悪用できることが明らかになりました。

この記事では、発見された脆弱性の影響を概説し、技術的な詳細を深掘りします。さらに、脆弱性の根本原因を特定し、適用されたパッチがどのようにそれらを軽減するかを説明します。

影響

この脆弱性はCactiバージョン1.2.22およびそれ以前に影響を与え、CVE-2022-46169として追跡され、CVSSスコアは9.8です。認証されていない攻撃者は、特定のデータソースを使用する監視デバイスがある場合、脆弱なCactiインスタンスを悪用できます。悪用により、攻撃者はウェブサーバープロセスと同じユーザー権限で任意のコマンドを実行できます。

以下のビデオは、脆弱なバージョンのCactiを実行しているサーバーの悪用を示しています:

セキュリティアドバイザリには、システム管理者がCactiバージョン1.2.22およびそれ以前に手動で適用する必要があるパッチが含まれています。このパッチはバージョン1.2.23および1.3.0の一部としてリリースされます。

提供されたパッチを適用し、新しいバージョンが利用可能になったら更新することを強くお勧めします。

技術的詳細

このセクションでは、SonarQube Cloudによって報告された脆弱性を調査し、攻撃者がどのようにそれを悪用できるかを確認します。私たちが示す攻撃は、2つの異なるコード脆弱性で構成されています:

  1. 認証バイパス: Cactiのほとんどのインストールでホスト名ベースの認可チェックが安全に実装されていません
  2. コマンドインジェクション: 無加工のユーザー入力が外部コマンドを実行するために使用される文字列に伝播されます

認証バイパス

スクリプトremote_agent.phpは、認可されたクライアントのみがアクセスできるようにする必要があります。このため、ファイルの冒頭に認可チェックがあります:

cacti/remote_agent.php

クリップボードにコピー

<?php
// ...
if (!remote_client_authorized()) {
  print 'FATAL: You are not authorized to use this service';
  exit;
}

関数remote_client_authorizedはクライアントのIPアドレス($client_addr)を取得し、それを対応するホスト名($client_name)に解決し、pollerテーブルにこのホスト名のエントリがあるかどうかを確認します:

cacti/lib/html_utility.php

クリップボードにコピー

<?php
// ...
function remote_client_authorized() {
  // ...
  $client_addr = get_client_addr();
  // ...
  $client_name = gethostbyaddr($client_addr);
  // ...
  $pollers = db_fetch_assoc('SELECT * FROM poller', true, $poller_db_cnn_id);
  foreach($pollers as $poller) {
      if (remote_agent_strip_domain($poller['hostname']) == $client_name) {
        return true;
      // ...

上記のコードスニペットは、関数get_client_addrがクライアントのIPアドレスを取得することを示しています。この関数は、IPアドレスを決定する際に攻撃者が制御可能なさまざまなHTTPヘッダーを考慮に入れます:

cacti/lib/functions.php

クリップボードにコピー

<?php
// ...
function get_client_addr($client_addr = false) {
  $http_addr_headers = array(
      // ...
      'HTTP_X_FORWARDED',
      'HTTP_X_FORWARDED_FOR',
      'HTTP_X_CLUSTER_CLIENT_IP',
      'HTTP_FORWARDED_FOR',
      'HTTP_FORWARDED',
      'HTTP_CLIENT_IP',
      'REMOTE_ADDR',
  );

  $client_addr = false;
  foreach ($http_addr_headers as $header) {
      // ...
      $header_ips = explode(',', $_SERVER[$header]);
      foreach ($header_ips as $header_ip) {
        // ...
        $client_addr = $header_ip;
        break 2;
      }
  }
  return $client_addr;
}

REMOTE_ADDR変数はウェブサーバーへの接続元のIPアドレスに設定されますが、HTTP_で始まる変数はクライアントから受信した対応するHTTPヘッダーによって設定されます。クライアントとウェブサーバーの間にこれらのHTTPヘッダーをフィルタリングするインスタンス(リバースプロキシなど)がない場合、攻撃者はこれらの値を完全に制御できます。

前述のコードスニペットに戻ると、pollerテーブルにはCactiを実行しているサーバーのホスト名を持つデフォルトエントリが含まれています。このため、攻撃者はHTTPヘッダーX-Forwarded: <TARGET-IP>を提供することでremote_client_authorizedチェックをバイパスできます。この方法で、関数get_client_addrはCactiを実行しているサーバーのIPアドレスを返します。gethostbyaddrの呼び出しはこのIPアドレスをサーバーのホスト名に解決し、デフォルトエントリのためにポーラーホスト名チェックを通過します。

これにより、認証されていない攻撃者がremote_agent.phpの機能にアクセスできるようになります。

コマンドインジェクションの脆弱性

SonarQube CloudでCactiをスキャンしたところ、remote_agent.phpに興味深いコマンドインジェクションの脆弱性が見つかりました。SonarQube Cloudで直接発見を確認できます:

SonarQube Cloudで自分で試してみてください!

示されたインジェクションフローによれば、ユーザーが提供したパラメータpoller_idは、サニタイズやエスケープなしでproc_openの最初のパラメータに伝播されます。これにより、poll_for_data関数にコマンドインジェクションの脆弱性が生じます。

攻撃者はactionパラメータをpolldataに設定することで脆弱な関数をトリガーできます:

cacti/remote_agent.php

クリップボードにコピー

<?php
// ...
switch (get_request_var('action')) {
  case 'polldata':
      poll_for_data();

最初に、poll_for_data関数はパラメータhost_idとpoller_idを取得します。ただし、重要な違いがあります: host_idパラメータはget_filter_request_varから取得されますが、poller_idパラメータはget_nfilter_request_varから取得されます。ここで追加のn文字に注意してください:

cacti/remote_agent.php

クリップボードにコピー

<?php
// ...
function poll_for_data() {
  // ...
  $host_id        = get_filter_request_var('host_id');
  $poller_id      = get_nfilter_request_var('poller_id');

get_filter_request_var関数は取得したパラメータが整数であることを確認しますが、get_nfilter_request_varはpoller_idパラメータを取得するために使用され、任意の文字列を許可します。

インジェクションフローをさらに追跡すると、ポーラーアイテムがデータベースから取得されます。これらのアイテムのアクションがPOLLER_ACTION_SCRIPT_PHPに設定されている場合、脆弱なproc_openの呼び出しが発生します:

cacti/remote_agent.php

クリップボードにコピー

<?php

// ... retrieve poller items from database ...

foreach($items as $item) {
  switch ($item['action']) {
  // ...
  case POLLER_ACTION_SCRIPT_PHP: /* script (php script server) */
      // ...
      $cactiphp = proc_open(read_config_option('path_php_binary') . ' -q ' . $config['base_path'] . '/script_server.php realtime ' . $poller_id, $cactides, $pipes);

これは、攻撃者がpoller_idパラメータを利用して、POLLER_ACTION_SCRIPT_PHPアクションを持つアイテムが存在する場合に任意のコマンドをインジェクトできることを意味します。これは、"Device - Uptime"や"Device - Polling Time"などの事前定義されたテンプレートによってこのアクションが追加されるため、実稼働インスタンスでは非常に可能性が高いです。

攻撃者は対応するIDを提供して、データベースクエリがそのようなアイテムを返すようにする必要があります。IDは昇順で番号付けされており、数百のIDを配列として1つのリクエストで送信できるため、攻撃者は簡単に有効な識別子を見つけることができます。

パッチ

認証バイパス

認証バイパスは、クライアントのIPアドレスを決定する際にどのHTTPプロキシヘッダーを尊重するかを管理者が設定できるようにすることで軽減されました。デフォルトではREMOTE_ADDRサーバー変数のみが使用され、安全なデフォルト構成が確保されます。

さらに、このパッチにより、Cactiインスタンスがリバースプロキシの背後にあるシナリオなどで、管理者がHTTPプロキシヘッダーを使用できるようになります。

コマンドインジェクション

コマンドインジェクションの脆弱性は、ソース(ユーザー入力の取得)とシンク(proc_openの呼び出し)に適用された2つの修正で軽減されました。ソースでは、poller_idパラメータが整数であることを確認するために、関数get_nfilter_request_varがget_filter_request_varに置き換えられました:

cacti/remote_agent.php

クリップボードにコピー

<?php
// ...
function poll_for_data() {
  // ...
  $poller_id      = get_filter_request_var('poller_id');

シンクでは、$poller_id変数がproc_openのコマンド文字列に挿入される前にcacti_escapeshellargでエスケープされました:

cacti/remote_agent.php

クリップボードにコピー

<?php
// ...
$cactiphp = proc_open(read_config_option('path_php_binary') . ' -q ' . $config['base_path'] . '/script_server.php realtime ' . cacti_escapeshellarg($poller_id), $cactides, $pipes);

この2番目の修正は不要に見えるかもしれませんが、ソースでの検証により変数が整数を含むことが保証されているためです。しかし、ソースコードの調整により将来的にこの仮定が変更され、重大な脆弱性が再導入される可能性があります。

このため、両方の修正が重要です: ユーザー入力は常に検証され、想定される値(この場合は整数)に制限されるべきです。さらに、proc_openのような敏感な関数に渡される前に、値は常にエスケープされるべきです。