Skip to main content

C/C++における3種類のディレクトリトラバーサル脆弱性を解説

著者
Headshot of Kirill Efimov

Kirill Efimov

blog feature security beta

2022年4月4日

0 分で読めます

ディレクトリトラバーサル脆弱性(パストラバーサル脆弱性とも呼ばれます)を悪用すると、攻撃者は本来アクセスできないフォルダーにアクセスできます。この記事では、C/C++で記述されたWebサーバーにおけるディレクトリトラバーサル脆弱性の仕組みと、その対策について解説します。

Security Researchチームでは、長い間CおよびC++のエコシステムを対象としていませんでした。しかし、FossIDがSnykに加わったことで状況が変わりました。FossIDはC/C++に関する専門知識を持っていたため、これらの言語の情報セキュリティ態勢を理解するためのプロジェクトをいくつか開始できました。その結果、深刻度の高い脆弱性を数多く発見し、調査結果を共有できることを楽しみにしています。

この記事では、制限されたディレクトリへのパス名の不適切な制限(CWE-22)に焦点を当てます。この脆弱性には多くの種類があり、最も危険なソフトウェアの弱点 CWE Top 25によると、2022年も広く蔓延し、危険な状態が続くとされています。3種類のディレクトリトラバーサルを取り上げ、脆弱なコード例とその修正方法、想定される影響を解説します。

任意ファイルの読み取り

Webサーバーでは、HTML、CSS、JSファイルなどの静的アセットを配信する機能がよく実装されています。通常、アセットはファイルシステム上の専用フォルダー(例:/static、/www、/assets)に保存されます。Webアプリケーションが静的ファイルへのパスを適切にサニタイズしないと、任意ファイル読み取りの脆弱性が発生します。ユーザーが「../」というパス要素を使って想定されたフォルダーの外に移動し、最終的にディスク上の任意のファイルを読み取れてしまいます。

さっそく、Crow Webフレームワークで最近発見したこの脆弱性の例を見てみましょう。Crowは、Webサービスを実行するためのC++マイクロフレームワークです。

この例では、main.cppファイル1つで構成されるシンプルな「Hello World」アプリケーションを作成します。

#define CROW_MAIN
#include <crow_all.h>

int main()
{
    crow::SimpleApp app;

    CROW_ROUTE(app, "/")
    ([]() {
        return "Hello world!";
    });

    app.port(8000).run();
    return 0;
}

例をコンパイルするには、Ubuntuで次のコマンドを実行します。g++ -pthread -o server main.cppこれで、ターミナルから./serverを実行してサーバーを起動できます。

ドキュメントによると、Crowはデフォルトで、実行ファイルserverと同じ場所にある/staticフォルダーから静的ファイルを配信します。/staticフォルダーを作成し、その中にテキストファイルを作成して動作を確認してみましょう。mkdir static && echo “test” > static/test.txt

別のターミナルウィンドウからcurlを実行し、動作を確認します。

curl http://localhost:8000/を実行すると、レスポンスとしてHello world!が返されます。

curl http://localhost:8000/static/test.txtを実行するとtestが表示されます。これはstatic/test.txtファイルの内容です。

脆弱性を悪用するには、curl --path-as-is "http://localhost:8000/static/../../etc/passwd"を実行します。レスポンスには/etc/passwdファイルの内容が表示されます。ポイントは--path-as-isフラグです。このフラグはURLのパス正規化を無効にします。つまり、サーバーは「../」要素を含め、指定したURLをそのまま受け取ります。

セキュリティへの影響

本番サーバーから任意のファイルが漏えいすることは、通常、重大な問題です。SSHキー、各種認証情報、サーバーのソースコードが悪意ある攻撃者の手に渡ると、攻撃をエスカレートさせ、サーバーやユーザーの機密データを乗っ取られるおそれがあります。

C++サーバーがよく使われる組み込みデバイスでも、同様の問題が起こります。ディレクトリトラバーサル脆弱性は、Wi-Fiルーターでよく見られます。NETGEAR、Belkin、TP-Linkなどがその例です。この場合、管理パネルの認証情報を盗まれ、ローカルネットワーク全体を掌握される可能性があります。

脆弱なサーバーがローカルマシン上で動作している場合でも、ディレクトリトラバーサルが危険になることがあります。Electronなどのハイブリッド型Webデスクトップアプリケーションでは、ローカルWebサーバーがよく使われます。このような攻撃ベクトルについて詳しく知りたい方は、脆弱なVS Codeプラグインに関する調査をご覧ください。

緩和策

Crow v0.3+4で、このディレクトリトラバーサル脆弱性は修正されました。問題はapp.hファイルで導入されました。コードがURLの一部と静的フォルダーのパスを単純に連結し、set_static_file_infoメソッドを呼び出してファイルを配信していました。

res.set_static_file_info(CROW_STATIC_DIRECTORY + file_path_partial);

この場合の修正では、set_static_file_infoメソッドの冒頭にutility::sanitize_filename(path)の呼び出しを追加しました。sanitize_filenameは「..」をすべて「_」に置き換え、脆弱性を解消します。ファイル名に「..」を含むファイルをサーバーが配信できなくなるため、完全な修正とは言えません。しかし、そのようなケースはまれであり、大きな問題にはならないでしょう。

任意ファイルの書き込み

任意ファイル書き込みの脆弱性は、ファイルアップロード機能を実装したコードベースでよく見られる問題です。根本原因は前の例とよく似ていますが、少し分かりにくいものです。シンプルなHTMLのファイルアップロードフォームを考えてみましょう。

<form method=”post”>
<input type="file" name="avatar">
<input type="submit" value="submit">
</form>

「test」という内容の「test.txt」というテキストファイルを選択してみましょう。ブラウザーは次のようなHTTPリクエストを生成します。

POST /upload HTTP/1.1
Host: localhost:8000
Content-Length: 150
Content-Type: multipart/form-data; boundary=----xxx
Connection: close

------xxx
Content-Disposition: form-data; name="file"; filename="test.txt"
Content-Type: text/plain

test

------xxx--

リクエストボディを詳しく見ると、ファイルの内容以外の情報も含まれていることが分かります。具体的には、ファイル名フィールドがあり、「../」要素が含まれる可能性があるため、サーバー側で適切に処理する必要があります。

この問題を示すため、広く使われている組み込みWebサーバーフレームワークMongooseで最近発見された任意ファイル書き込みの脆弱性を取り上げます。

Mongooseリポジトリにあるファイルアップロードのサンプルでは、ファイルをチャンク単位でアップロードできます。サーバーを実行するデバイスのRAMが少ない場合に便利です。リポジトリをクローンし、サンプルフォルダーでmakeコマンドを実行するだけで、ビルドして実行できます。サーバーはすぐにポート8000でリッスンを開始し、アップロードファイルの保存先を/tmpに設定します(main.cファイルの13行目で指定されています)。

ここで、curlを使ってファイルをアップロードできます。curl -X POST --data-binary "test" http://localhost:8000/upload?offset=0&name=test.txtを実行すると、サーバー上に「test」というテキストを含む/tmp/test.txtファイルが作成されます。

脆弱性を悪用するには、nameクエリパラメーターの直前に「../」要素を追加します。curl -X POST --data-binary "pwned" http://localhost:8000/upload?offset=0&name=../malicious-fileこの攻撃により、ファイルシステムのルートにmalicious-fileが作成されます。

セキュリティへの影響

任意ファイル書き込みの悪用は、任意ファイル読み取りほど単純ではありません。しかし、多くの場合、リモートコード実行(RCE)につながる可能性があります。攻撃者が/etc/init.d/などの重要なシステムファイルや、サーバーの実行ファイル自体を上書きできると、RCEが可能になることがあります。

緩和策

前の例と同様に、Mongooseはユーザーが指定したファイル名から「..」を削除して脆弱性を修正しました。この修正はバージョン7.6に含まれています。

一般的な対策として、ユーザーが指定したファイル名を使わず、元のファイル名の代わりにランダムな文字列、タイムスタンプ、MD5ハッシュを使用することをおすすめします。ファイルアップロード機能を持つアプリケーションで特に有効で、XSSなど他の多くの脆弱性の回避にも役立ちます。

Zip Slip

今回取り上げる3つ目のディレクトリトラバーサルは、Zip Slipです。名前から分かるように、アーカイブの展開処理に関する脆弱性です。任意ファイル書き込みとよく似ていますが、アーカイブの展開処理はそれほど一般的ではないため、攻撃対象となる範囲はより限定的です。

Zip Slip脆弱性を説明するため、オープンソースのクロスプラットフォームC++アプリケーションフレームワークJUCEの脆弱なバージョン6.1.4を使います。

#include <juce_core/juce_core.h>

int main()
{
    juce::File file("archive.zip");
    juce::ZipFile zipFile(file);
    zipFile.uncompressTo(juce::File("data"));
    return 0;
}

これは、JUCE APIを使ってarchive.zipをdataフォルダーに展開する非常にシンプルなアプリケーションです。

動作を確認するため、test.txtファイルを含むサンプルアーカイブを作成します。

echo test > test.txt && zip archive.zip test.zip
  adding: test.zip (stored 0%)

サンプルアプリケーションをコンパイルして実行すると、dataフォルダーが作成され、その中にtest.txtファイルが格納されます。では、ファイルシステム上で1階層上にファイルが配置されるアーカイブを作成したらどうなるでしょうか。

echo bad > ../bad.txt && zip archive.zip ../bad.txt

この場合、プログラムを実行すると、対象のdataフォルダーの外にbad.txtファイルが作成されます。ご覧のとおり、Zip Slipのセキュリティ上の懸念は、任意ファイル書き込みの場合とまったく同じです。

シンボリックリンクを介した展開など、Zip Slipにはほかの種類もあります。この問題について詳しく知るには、調査レポートをご覧ください。

緩和策

紹介した脆弱性は、シンボリックリンクを介したZip Slipへの対策とともに、JUCEフレームワークのバージョン6.1.5で修正されました。JUCEは、解決後の完全なファイルパスが対象ディレクトリ内に収まることを検証することで、脆弱性を修正しました。

一般的な修正方法

この種の脆弱性を調査する中で、Snyk Security Researchチームは、ファイルシステムのパスを扱うさまざまなコード例を大量に確認しました。その結果、ディレクトリトラバーサル脆弱性から身を守るための一般的な指針を見つけたと考えています。

ユーザーが指定したファイルパスを使わずに済むなら、使わないでください。

この指針は、ファイルのアップロード処理やアーカイブの展開処理の多くに当てはまります。

ユーザーが指定したファイルパスを使わざるを得ない場合は、次のロジック(stdを使用)に従うことをおすすめします。

// #include <iostream>
// #include <string>
// #include <filesystem>
// #include <algorithm>

    // This is the path to your static files folder.
    std::string base_path("/tmp/path_to_the_content_root_directory/");
    // User input – possibly contains "../" path segments.
    std::string user_input("user_provided_path");

    // The "canonical" method resolves ".", ".." and symlinks.
    std::filesystem::path base_resolved_path(
        std::filesystem::canonical(base_path));
    // The "weakly_canonical" method does the same as "canonical" but not requires
    // the file to exist.
    std::filesystem::path requested_file_path(
        std::filesystem::weakly_canonical(base_resolved_path / user_input));

    // Using "equal" we can check if "requested_file_path" starts
    // with base_resolved_path. Because we previously canonicalized both paths
    // they can't contain any ".." segments, so this check is sufficient.
    if (std::equal(
            base_resolved_path.begin(),
            base_resolved_path.end(),
            requested_file_path.begin())) {
        std::cout << "It is safe to work with the file: " << requested_file_path << "\n";
    } else {
        std::cout << "Not safe! We have to throw an exception here.\n";
    }

上記のコードには少なくともC++17が必要ですが、boostでも同様に実装できます。stdもboostも利用できない環境では、「..」を空文字列に置き換えた後、連続する「/」を1つにまとめる方法でも十分です。

さらに、アプリケーションがクロスプラットフォームでWindowsでも動作する場合は、ディレクトリ区切り文字の正規化を検討してください。Windowsではほとんどの場合、スラッシュ(「/」)とバックスラッシュ(「\」)のどちらもディレクトリ区切り文字として使えます。つまり、「..\」というペイロードは「../」と同じ意味になり、ディレクトリトラバーサル攻撃にも利用できます。Windows上でコードの安全性を確保する最も簡単な方法は、パスを処理する前に「/」を「\」に置き換えることです。

まとめと主な調査結果

情報セキュリティの専門家がCやC++の脆弱性について考えるとき、さまざまな種類のメモリ破壊など、低レベルの問題に注目しがちです。メモリ破壊の問題がエコシステムに大きな影響を与えるのは確かですが、アプリケーションの利用状況にも目を向け、低レベルの脆弱性だけでなく、より高レベルの脆弱性も防ぐ必要があります。調査を始めた当初、C/C++のWeb開発者は一般的なWebの問題への注意が十分ではないと考えていました。実際に、ディレクトリトラバーサル脆弱性を多数発見しました。

  • CVE-2022-25299:Mongoose(組み込みWebサーバー)の任意ファイル書き込み。

  • CVE-2022-25297:Drogon(C++14/17ベースのHTTPアプリケーションフレームワーク)の任意ファイル書き込み。

  • CVE-2021-23520、CVE-2021-23521:JUCE(オープンソースのクロスプラットフォームC++アプリケーションフレームワーク)のZip Slip。

  • CVE-2021-23514:Crow(C++マイクロフレームワーク)のパストラバーサル。

  • CVE-2022-25298:Webcc(軽量なC++ HTTPクライアントおよびサーバー)のパストラバーサル。

この一覧は現在も増え続けており、貢献者が修正を公開するたびに更新していきます。

安全な開発を!

キャプチャー・ザ・フラッグを始めよう

オンデマンドのバーチャル入門ワークショップを視聴して、キャプチャー・ザ・フラッグの課題の解き方を学びましょう。

続きを読む

blog feature ai violet
Blog

VulnBump:AIエージェントはビルドを壊さずに脆弱な依存関係を修正できるか?

VulnBumpは、実際の依存関係アップグレード307件を対象にAIエージェントをテストします。脆弱性が解消され、ビルドとテストが成功した場合にのみ、修正としてカウントされます。

blog feature snyk container custom base recommendations
Blog

Evo Agentic AppSecのご紹介:AI開発のスピードに対応するセキュリティ

AI駆動型開発のスピードに、従来のセキュリティワークフローは追いついていません。SnykのAgentic AppSecは、セキュリティエージェントが脆弱性を継続的に検出、修正、検証し、人が基準を定められるようにします。

Blog

フロンティアモデルは脆弱性を発見した。攻撃者だけがエクスプロイトチェーンを見つけた。

静的解析で欠陥は見つかりましたが、ライブ攻撃テストで侵害につながる連鎖を実証できたのは唯一でした。Evo COS、Claude Security、Claude Code Securityを比較します。