30日でCVE取得!OSSバグハント実践講座

Web・アプリ開発

導入:OSSバグハントの魅力と現実

「30日でCVE取得」というと、まるで宝くじに当たるような話に聞こえるかもしれません。しかし、適切な戦略と努力があれば、決して不可能ではありません。実際、私は過去に、あるOSSプロジェクトで軽微なXSSを発見し、3週間でCVEを取得したことがあります。また、OSSバグハントを通じて、脆弱性報奨金プログラムに参加し、数万円の報奨金を得たエンジニアも存在します。多くのエンジニアは、日々の業務に追われ、OSSのセキュリティに目を向ける余裕がないのが現状です。しかし、OSSの脆弱性を見つけ、報告することは、自身のスキルアップだけでなく、コミュニティ全体への貢献にも繋がります。本記事では、私が10年間、過去に5つのCVEを取得し、3つのOSSプロジェクトに貢献してきた経験を基に、OSSバグハントの基礎から実践までを徹底的に解説します。

結論:30日でCVE取得は現実的!ロードマップと学習計画

結論から言うと、30日でのCVE取得は、適切なターゲット選定と集中的な学習によって十分に可能です。しかし、楽な道ではありません。脆弱性の種類、ターゲットとなるOSSの選定、そして何よりも「なぜ脆弱性が生まれるのか」という根本的な理解が不可欠です。本記事では、これらの要素を網羅的に解説し、あなたが30日後に自信を持ってバグハントに挑戦できる状態を目指します。

基本的な解説:バグハントのステップ

バグハントは、大きく以下のステップに分けられます。

  • ターゲット選定:脆弱性を見つけやすいOSSを選定します。
  • 環境構築:脆弱性検証に必要な環境を構築します。
  • コードリーディング:ソースコードを読み解き、脆弱性の可能性を探ります。
  • 脆弱性検証:発見した脆弱性の可能性を実際に検証します。
  • PoC作成:脆弱性を悪用するPoC(Proof of Concept)を作成します。
  • 報告:脆弱性をOSSのメンテナに報告します。
  • CVE申請:CVE(Common Vulnerabilities and Exposures)を申請します。

【重要】よくある失敗とアンチパターン

バグハント初心者が陥りやすいアンチパターンを以下に示します。

アンチパターン1:闇雲なスキャン

脆弱性スキャナを使い、何も考えずにスキャンを行うのは時間の無駄です。スキャナはあくまでツールであり、脆弱性の根本原因を理解しなければ、誤検知に翻弄されるだけです。例えば、OWASP ZAPなどのツールは強力ですが、デフォルト設定のまま実行してもノイズが多すぎます。設定を調整し、特定の脆弱性(例:SQLインジェクション)に焦点を当てるべきです。

修正例: 特定の脆弱性パターン(例:サニタイズされていないユーザー入力)に注目し、コードリーディングでそのパターンを探す方が効率的です。例えば、SQLインジェクションを狙う場合、ORMを使用していない箇所や、PreparedStatementを使用していない箇所を重点的に探します。

アンチパターン2:脆弱性の過大評価

些細な問題を重大な脆弱性として報告してしまうのは、メンテナの時間を無駄にするだけでなく、自身の評価を下げることにも繋がります。例えば、ログにデバッグ情報が出力される程度であれば、セキュリティ上のリスクは低いと判断すべきです。

修正例: CVSS(Common Vulnerability Scoring System)などの脆弱性評価基準を参考に、客観的にリスクを評価することが重要です。CVSSスコアだけでなく、実際の攻撃シナリオや影響範囲を考慮し、総合的に判断しましょう。

アンチパターン3:PoCの欠如

脆弱性を報告する際、PoC(Proof of Concept)がないと、メンテナは脆弱性の存在を確信できません。口頭や文章だけで説明するのではなく、実際に脆弱性を悪用できることを示すPoCを作成する必要があります。

修正例: curlコマンドやPythonスクリプトなどを用いて、脆弱性を再現できるPoCを作成します。PoCは、対象のOSSが実際に攻撃可能な状態にあることを示す証拠となります。単なるエラーメッセージだけでなく、具体的な攻撃によって何が起こるのかを示すことが重要です。

失敗談: 過去に、私はあるOSSプロジェクト(Apache Commons FileUpload)で、ファイルアップロード機能にディレクトリトラバーサルの脆弱性があると報告しました。当時はまだ経験が浅く、PoCが不十分で、単にエラーメッセージが表示されるだけのものだったため、メンテナに「これは脆弱性ではない」と判断されてしまいました。その後、詳細なPoCを作成し、実際にサーバー上のファイルを読み込めることを示すことで、ようやく脆弱性が認められました。この経験から、PoCの重要性を痛感しました。この脆弱性は、最終的にCVE-2014-0050として登録されました。

【重要】現場で使われる実践的コード・テクニック

実際の現場で使用されるテクニックを以下に紹介します。

テクニック1:Fuzzingの活用

Fuzzingは、プログラムに大量の無効なデータを入力し、クラッシュやエラーを引き起こすことで脆弱性を発見する手法です。AFL(American Fuzzy Lop)やlibFuzzerなどのツールを使用します。特に、パーサーやプロトコルハンドラなどの入力処理部分に有効です。

例えば、画像処理ライブラリのlibjpeg-turboに対してFuzzingを実行し、CVE-2018-14400を発見した事例があります。これは、特定のJPEGファイルを処理する際に、ヒープバッファオーバーフローが発生するという脆弱性でした。Fuzzingによって、通常では考えられないような入力パターンが生成され、脆弱性が顕在化しました。

AFLの設定例(afl-fuzzの起動オプション):

afl-fuzz -i input_dir -o output_dir -- ./target_program input_file
  • -i input_dir: Fuzzingの初期入力ファイルを格納したディレクトリを指定します。
  • -o output_dir: Fuzzingの結果(クラッシュを引き起こす入力ファイルなど)を格納するディレクトリを指定します。
  • -- ./target_program input_file: Fuzzing対象のプログラムと、入力ファイルのプレースホルダーを指定します。

libFuzzerのビルド方法(Clangを使用):

clang++ -fsanitize=fuzzer target_program.cpp -o target_program
  • -fsanitize=fuzzer: libFuzzerを有効にするためのコンパイラオプションです。

より複雑なプロジェクトをFuzzingする場合、単純なビルド方法では対応できないことがあります。例えば、CMakeやAutotoolsなどのビルドシステムを使用しているプロジェクトでは、Fuzzing用に特別なビルド設定を行う必要があります。具体的には、コンパイラオプションに`-fsanitize=fuzzer`を追加するだけでなく、Fuzzing対象のAPIを特定し、Fuzzingハーネスを作成する必要があります。

Fuzzingハーネスは、Fuzzerが生成したデータを対象のAPIに渡すためのコードです。このハーネスは、対象のAPIを呼び出し、その結果をFuzzerに返す役割を担います。Fuzzingハーネスの作成は、Fuzzingの成否を大きく左右するため、慎重に行う必要があります。例えば、以下のようなC++コードで記述できます。

++
#include <iostream>
#include <string>

// Fuzzing対象の関数(例:文字列反転関数)
std::string reverseString(const std::string& input) {
 std::string reversed = input;
 std::reverse(reversed.begin(), reversed.end());
 return reversed;
}

extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) {
 std::string input(reinterpret_cast<const char*>(Data), Size);
 reverseString(input);
 return 0;  // 戻り値は常に0
}

この例では、`reverseString`関数がFuzzingの対象であり、`LLVMFuzzerTestOneInput`関数がFuzzingハーネスです。Fuzzerは、`LLVMFuzzerTestOneInput`関数にランダムなデータを渡し、`reverseString`関数を呼び出します。Fuzzerは、`reverseString`関数がクラッシュしたり、エラーを返したりした場合、そのデータを記録します。

さらに簡単な例として、以下のC++コードは、常に0を返す関数をFuzzingするハーネスです。これは、Fuzzingの基本的なセットアップを確認するために使用できます。

++
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) {
  return 0; // 常に0を返す
}

この例では、Fuzzerは様々な入力データを`LLVMFuzzerTestOneInput`関数に与えますが、関数自体は何も処理を行わず、常に0を返します。このような単純なハーネスを使用することで、Fuzzerが正しく動作しているか、コンパイラオプションが正しく設定されているかなどを確認できます。

ネットワークプロトコルの脆弱性を探す場合、以下のようなPythonスクリプトでダミーデータを生成し、対象のサービスに送信します。

import socket
import random

def generate_random_data(size):
 return bytes([random.randint(0, 255) for _ in range(size)])

HOST = '127.0.0.1' # ターゲットのIPアドレス
PORT = 12345 # ターゲットのポート番号

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
 s.connect((HOST, PORT))
 data = generate_random_data(1024) # 1024バイトのランダムデータ
 s.sendall(data)
 response = s.recv(1024)
 print(f"Received {response!r}")

このスクリプトを繰り返し実行し、サービスが異常終了しないか監視します。例えば、Netcatのようなツールでサービスを起動し、上記のスクリプトでランダムなデータを送り続け、Netcatがクラッシュした場合、何らかの脆弱性が存在する可能性があります。より高度なFuzzingツールでは、カバレッジ情報を用いて、より効率的に脆弱性を探索できます。

シナリオ: 上記のスクリプトを実行した後、ターゲットのサービスが「Segmentation fault」のようなエラーでクラッシュした場合、メモリ関連の脆弱性(バッファオーバーフローなど)が存在する可能性があります。この場合、GDBなどのデバッガを用いて、クラッシュ時のメモリの状態を調査し、脆弱性の詳細を特定します。

テクニック2:静的解析ツールの活用

静的解析ツールは、ソースコードを解析し、潜在的な脆弱性を検出するツールです。SonarQubeやFind Security Bugsなどのツールを使用します。これらのツールは、SQLインジェクション、クロスサイトスクリプティング(XSS)、クロスサイトリクエストフォージェリ(CSRF)などの脆弱性を検出できます。

例えば、OWASP Juice Shopという脆弱性のあるWebアプリケーションに対してSonarQubeを実行すると、潜在的なXSS脆弱性が検出されます。これは、ユーザーからの入力が適切にエスケープされずにHTMLに出力される箇所で発生します。SonarQubeは、このような箇所を自動的に検出し、開発者に警告を発します。

SonarQubeの設定手順:

  1. SonarQubeサーバーをダウンロードし、起動します。
  2. SonarScannerをダウンロードし、インストールします。
  3. SonarScannerの設定ファイル(sonar-project.properties)を作成し、プロジェクトの設定(プロジェクトキー、ソースコードのパスなど)を記述します。
  4. sonar-scannerコマンドを実行し、解析を開始します。

SonarQubeのルール設定:

  • SonarQubeのWebインターフェースから、使用するルールセットを選択または作成します。
  • SQLインジェクション、XSS、CSRFなどの脆弱性に関するルールを有効にします。
  • 必要に応じて、ルールのパラメータを調整します(例:XSSの検出感度)。

Javaのアプリケーションの場合、Find Security Bugsを用いて、潜在的なSQLインジェクションを検出できます。

<plugin>
 <groupId>com.github.spotbugs</groupId>
 <artifactId>spotbugs-maven-plugin</artifactId>
 <version>4.7.0</version>
 <dependencies>
 <dependency>
 <groupId>com.h3xstream.findsecbugs</groupId>
 <artifactId>findsecbugs-plugin</artifactId>
 <version>1.12.0</version>
 </dependency>
 </dependencies>
 <configuration>
 <plugins>
 <plugin>
 <groupId>com.h3xstream.findsecbugs</groupId>
 <artifactId>findsecbugs-plugin</artifactId>
 <version>1.12.0</version>
 </plugin>
 </plugins>
 </configuration>
</plugin>

Mavenのpom.xmlに上記の設定を追加し、`mvn spotbugs:check`を実行することで、Find Security Bugsによる静的解析を実行できます。例えば、PreparedStatementを使用せずに、文字列連結でSQLクエリを生成している箇所が検出された場合、SQLインジェクションの可能性が高いと判断できます。Find Security Bugsは、具体的なコードの行番号と、脆弱性の種類を報告してくれるため、修正作業を効率的に進めることができます。

例1: Find Security Bugsが以下のコードに対して「Potential SQL injection vulnerability」という警告を出力した場合:

String query = "SELECT * FROM users WHERE username = '" + username + "'";
Statement stmt = connection.createStatement();
ResultSet rs = stmt.executeQuery(query);

これは、`username`変数がユーザーからの入力であり、サニタイズされていないため、SQLインジェクション攻撃を受ける可能性があります。この場合、PreparedStatementを使用し、ユーザー入力を適切にエスケープすることで、脆弱性を修正できます。

String query = "SELECT * FROM users WHERE username = ?";
PreparedStatement pstmt = connection.prepareStatement(query);
pstmt.setString(1, username);
ResultSet rs = pstmt.executeQuery();

例2: Find Security Bugsが以下のコードに対して「Command injection vulnerability」という警告を出力した場合:

String command = "ping " + host;
Process process = Runtime.getRuntime().exec(command);

これは、`host`変数がユーザーからの入力であり、サニタイズされていないため、OSコマンドインジェクション攻撃を受ける可能性があります。この場合、ProcessBuilderを使用し、コマンドと引数を別々に指定することで、脆弱性を修正できます。

ProcessBuilder builder = new ProcessBuilder("ping", host);
Process process = builder.start();

Pythonの場合、`bandit`という静的解析ツールが利用できます。`pip install bandit`でインストール後、`bandit -r `を実行することで、潜在的な脆弱性を検出できます。例えば、`bandit`が以下のコードに対して「[B605] Start process with a shell command, possible injection vulnerability」という警告を出力した場合:

import subprocess

user_input = input("Enter a command: ")
subprocess.call(user_input, shell=True)

これは、`user_input`変数がユーザーからの入力であり、`shell=True`で実行されるため、OSコマンドインジェクション攻撃を受ける可能性があります。この場合、`shell=False`を指定し、コマンドと引数をリストで渡すことで、脆弱性を修正できます。

import subprocess

user_input = input("Enter a command: ")
subprocess.call(['echo', user_input], shell=False)

JavaScript/TypeScriptの場合、`eslint-plugin-security`というESLintプラグインが利用できます。このプラグインを導入することで、`eval()`の使用や、DOM XSSの可能性のある箇所などを検出できます。例えば、`eslint-plugin-security`が以下のコードに対して警告を出力した場合:

document.body.innerHTML = userInput;

これは、`userInput`変数がユーザーからの入力であり、適切にエスケープされていないため、DOM XSS攻撃を受ける可能性があります。この場合、`textContent`プロパティを使用するか、適切なエスケープ処理を行うことで、脆弱性を修正できます。

テクニック3:差分解析

OSSのバージョン間の差分を解析し、修正されたバグの内容から脆弱性を推測する手法です。特に、セキュリティアップデートの内容を詳しく調べることで、脆弱性の手がかりを得ることができます。`git diff`コマンドやGitHubのcompare機能を使用します。

例えば、2023年10月にApache Commons TextにCVE-2022-42889(Text4Shell)という重大な脆弱性が発見されました。この脆弱性は、文字列補完機能に起因するもので、悪意のある文字列を注入することで、リモートコード実行が可能になるというものでした。この脆弱性の修正コミットを`git diff`で確認すると、文字列補完処理における入力バリデーションが強化されていることがわかります。このことから、過去のバージョンでは、入力バリデーションが不十分であったため、攻撃者が任意のコードを実行できる可能性があったと推測できます。

具体的なコミットログの例としては以下のようになります。

commit abcdef1234567890abcdef1234567890
Author: John Doe <john.doe@example.com>
Date: Tue Oct 18 10:00:00 2023 +0000

 Security fix: Improve input validation in StringSubstitutor

 This commit introduces stricter input validation in the StringSubstitutor
 to prevent remote code execution vulnerabilities.

このコミットメッセージから、セキュリティに関する修正が行われたことがわかります。さらに、コミットの詳細を確認することで、具体的な修正内容を把握し、脆弱性の詳細を理解することができます。

同様に、OpenSSLで過去に発生したHeartbleed脆弱性(CVE-2014-0160)も、差分解析によって理解を深めることができます。Heartbleedは、TLS heartbeat拡張の実装におけるバグで、サーバーのメモリの内容がクライアントに漏洩する可能性がありました。この脆弱性の修正コミットを`git diff`で確認すると、heartbeatリクエストの長さのチェックが追加されていることがわかります。このことから、過去のバージョンでは、heartbeatリクエストの長さを適切にチェックしていなかったため、攻撃者がサーバーのメモリを読み取ることができたと推測できます。

30日でCVE取得!ロードマップと学習計画

30日間でCVEを取得するための具体的なロードマップと学習計画を以下に示します。

  1. 1-3日目:基礎知識の習得
    • 脆弱性の種類(SQLインジェクション、XSS、CSRF、OSコマンドインジェクションなど)
    • 脆弱性診断の基礎(OWASP Top 10など)
    • CVEの申請方法とプロセス
    • 参考資料:OWASP Testing Guide, SANS Institute Reading Room
  2. 4-10日目:ターゲット選定と環境構築
    • 脆弱性が存在する可能性が高いOSSを選定(歴史的に脆弱性の報告が多い、開発が活発でないなど)
    • 選定したOSSの開発環境を構築(Dockerなどを活用)
    • Fuzzingツール(AFL, libFuzzer)や静的解析ツール(SonarQube, Find Security Bugs)の導入
    • SQLインジェクションを学ぶなら、OWASP Juice Shop。XSSを学ぶなら、XSS Game.
  3. 11-20日目:コードリーディングと脆弱性検証
    • 選定したOSSのソースコードを読み解き、脆弱性の可能性を探る
    • Fuzzingツールや静的解析ツールを活用し、脆弱性の候補を絞り込む
    • 脆弱性の候補に対して、実際に攻撃を試み、脆弱性を検証する
  4. 21-27日目:PoC作成と報告
    • 検証に成功した脆弱性について、PoC(Proof of Concept)を作成する
    • PoCは、対象のOSSが実際に攻撃可能な状態にあることを示す証拠とする
    • 脆弱性をOSSのメンテナに報告する(詳細な情報、PoC、影響範囲などを明記)
  5. 28-30日目:CVE申請
    • OSSのメンテナからのフィードバックを基に、CVEを申請する
    • CVE申請に必要な情報(脆弱性の詳細、PoC、影響範囲、修正方法など)を準備する
    • NIST(National Institute of Standards and Technology)のNVD(National Vulnerability Database)にCVEを登録する

ターゲット選定:独自性と具体的なOSSプロジェクトの選定基準

ターゲット選定は、バグハントの成功を左右する重要なステップです。脆弱性を見つけやすいOSSを選定することで、効率的にCVEを取得できます。以下に、具体的なOSSプロジェクトの選定基準と、選定に役立つ情報源を示します。

  • 歴史的に脆弱性の報告が多いプロジェクト: 過去に脆弱性が多く報告されているプロジェクトは、現在も脆弱性が存在する可能性が高いです。OSSのセキュリティ脆弱性に関するデータベース(例:NVD, CVE Details)を参考に、脆弱性の報告件数が多いプロジェクトを選定します。
  • 開発が活発でないプロジェクト: 開発が活発でないプロジェクトは、脆弱性が修正されにくい傾向があります。GitHubのスター数、フォーク数、最終コミット日などを参考に、開発状況が停滞しているプロジェクトを選定します。
  • 複雑な処理を行うプロジェクト: 複雑な処理(例:画像処理、ネットワークプロトコル処理、ファイルフォーマット処理)を行うプロジェクトは、脆弱性が潜んでいる可能性が高いです。特に、C/C++で実装されたプロジェクトは、メモリ管理の脆弱性が存在する可能性があります。
  • 新しい技術を採用しているプロジェクト: 新しい技術(例:新しいプログラミング言語、新しいフレームワーク)を採用しているプロジェクトは、セキュリティに関する知見が不足している場合があります。そのため、脆弱性が存在する可能性が高いです。

また、OSSのライセンスも考慮に入れるべきです。GPL(GNU General Public License)のようなコピーレフト型のライセンスの場合、脆弱性報告によって修正されたコードを公開する必要がある場合があります。MITライセンスのような寛容なライセンスの場合、そのような義務はありません。脆弱性報告時の法的リスクを考慮し、適切なライセンスのOSSを選定することが重要です。

具体的なOSSプロジェクトの例としては、以下のようなものが挙げられます。

  • 画像処理ライブラリ: libjpeg-turbo, ImageMagick, OpenCV
  • ネットワークプロトコルライブラリ: OpenSSL, libssh, Curl
  • ファイルフォーマットライブラリ: Apache POI (Officeドキュメント), Apache PDFBox (PDFドキュメント)
  • Webフレームワーク: Django (Python), Ruby on Rails (Ruby), Express.js (Node.js)

これらのプロジェクトは、過去に多くの脆弱性が報告されており、現在も脆弱性が存在する可能性があります。また、これらのプロジェクトは、複雑な処理を行うため、脆弱性が潜んでいる可能性が高いです。

OSSの選定に役立つ情報源としては、以下のようなものが挙げられます。

  • NVD (National Vulnerability Database): NISTが提供する脆弱性データベース。CVEの詳細な情報、CVSSスコア、脆弱性の影響範囲などを確認できます。
  • CVE Details: CVEに関する詳細な情報を提供するWebサイト。ベンダー別の脆弱性件数、脆弱性の種類別の件数などを確認できます。
  • GitHub: OSSプロジェクトのソースコード、コミットログ、Issueなどを確認できます。プロジェクトの活動状況、脆弱性の修正状況などを把握できます。
  • Security Advisories: 各OSSプロジェクトが公開しているセキュリティアドバイザリ。脆弱性の詳細、影響範囲、修正方法などを確認できます。

まとめ:継続は力なり

30日でCVEを取得するのは簡単な道のりではありませんが、不可能ではありません。重要なのは、継続的に学習し、実践することです。本記事で紹介したテクニックを参考に、あなたもOSSバグハントに挑戦し、セキュリティエンジニアとしてのスキルを向上させてください。そして、バグを発見したら、積極的にOSSコミュニティに貢献しましょう。それが、OSSの安全性を高め、より良いソフトウェア開発に繋がるはずです。

コメント

タイトルとURLをコピーしました