VPN 機器に関する調査:研究者の道のり

Ben Barnea

執筆者

Ben Barnea

February 11, 2025

Ben Barnea

執筆者

Ben Barnea

Ben Barnea は、Akamai の Security Researcher です。Windows、Linux、IoT、モバイルなど、さまざまなアーキテクチャのローレベルなセキュリティ調査や脆弱性調査への関心が高く、豊富な調査経験を有しています。複雑なメカニズムがどのように機能するか、そして最も重要な問題である、どのように失敗するかを知ることに生きがいを感じています。

VPN は組織のネットワークへのゲートウェイであるため、この機器の脆弱性は組織に大きな影響を与えます。
VPN は組織のネットワークへのゲートウェイであるため、この機器の脆弱性は組織に大きな影響を与えます。
  • Akamai の研究者 Ben Barnea が Fortinet の FortiOS に複数の脆弱性を発見しました。
  • 未認証の攻撃者が DoS や RCE につながるおそれのある脆弱性をトリガーする可能性があります
  • DoS の脆弱性は悪用が容易で、Fortigate 機器の障害を引き起こします。
  • RCE の脆弱性は悪用が困難であると考えられます。
  • これらの脆弱性は Fortinet に責任を持って開示され、CVE-2024-46666CVE-2024-46668 が割り当てられました。
  • 2025 年 1 月 14 日、Fortinet は Barnea が発見した脆弱性に対処しました。FortiOS の最新バージョンを搭載しているデバイスは、これらの脆弱性から保護されます。

概要

ここ数年、VPN ソリューションには 数多くの重大な脆弱性があり、攻撃者に野放し状態で悪用されてきました。こうした脆弱性の中には悪用が非常に容易なものがあり、インターネットに露出されている VPN 機器に対する RCE という壊滅的な影響を及ぼします。ネットワークに侵入すると、攻撃者はラテラルムーブメント(横方向に移動)を行い、機微な情報、知的財産、その他の価値の高い資産にアクセスできるようになります。

Akamai の研究者 Ori David は、VPN の脆弱性の初期の悪用に加え、Post-exploitation の手法も実証しました。VPN サーバーが侵害されると、攻撃者がネットワーク内の他の重要な資産を簡単に制御できるようになるおそれがあることを示しました。

残念ながら、VPN 機器を調査しようとするセキュリティ研究者は、研究を開始するのに苦労しています。ファームウェアは必ずしも簡単に入手できるわけではなく、ベンダーが展開した暗号化メカニズムで保護されているからです。しかし、VPN 機器は攻撃の主要なターゲットであるため、そのような保護を打開することは攻撃者にとって確かに価値があります。

本ブログ記事では、Fortinet の VPN ソリューションに関する研究のプロセスについて説明します。ファームウェアの入手、復号、デバッガーのセットアップ、最後に、脆弱性の捜索のプロセスを取り上げます。

この記事で取り上げている研究の一部は、目新しいものではありません。OptistreamBishop FoxAssetnoteLexfo などが、FortiOS に関する優れた研究を行っています。  Akamai は、FortiOS の最新バージョンを使用し、それらの初期研究をアップデートしました。Fortinet は暗号化と復号の手法を頻繁に変更するため、デバイスの分析が困難だからです。

ファームウェアイメージを取得する

従来、VPN は個別の物理機器として販売されていたため、VPN の取得やファームウェアの抽出は容易ではありませんでした。しかし現在では、VPN 機器は仮想マシン(VM)に展開できる仮想機器として販売されることがはるかに多くなりました。

幸運なことに、Fortinet は登録後に Web サイトからダウンロードできる試用版 VM を提供しています(図 1)。この VM には制約があり、許可される CPU は 1 つのみで、2 GB の RAM に制限されています。

Fortinet は、登録後に Web サイトからダウンロードできる試用版 VM を提供しています(図 1)。 図 1:ダウンロード可能な試用版 VM

デバッグ環境を構築する

提供される VM には、重要な点が 2 つあります。(1)ブートイメージおよびカーネルイメージ(flatkc と呼ばれる)と、(2)その興味深いファイルのほとんどが含まれている暗号化されたファイルシステム rootfs です。そのファイルシステム内で、復号すると、/bin/ ディレクトリ内に init というバイナリーが見つかります。

この VM のバイナリーのほとんどは、この 1 つのバイナリーに静的にコンパイルされています。/bin/init には、興味深いバイナリーが 2 つあり、SSLVPND と管理 Web サーバーです。これらのバイナリーについては、この記事の後半で取り上げます。

我々がやりたいのは、Fortinet が提供する制約のある CLI ではなく、完全なシェルを備えた環境を構築することです。さらに、バイナリーを簡単にデバッグできるようにする gdb バイナリーを用意したいと考えました。

そのような環境を構築するために、次の操作を行います。

  1. GZIP 圧縮 CPIO(ファイルアーカイブ形式)を解凍する
  2. Bishop Fox のスクリプトを使用して、rootfs を復号する
  3. bin.tar.xz アーカイブを解凍する
  4. /bin/init 整合性チェックにパッチを適用する
  5. vmlinux-to-elf を使用して flatkc を ELF に変換する
  6. IDA で fgt_verify_initrd のアドレスを見つけ、実行時にそれにパッチを適用して追加の完全性チェックを無効化できるようにする
  7. 静的にコンパイルされた busybox と gdb を /bin/ 内にドロップする
  8. telnet サーバーを作成するスタブをコンパイルし、このスタブを使用して /bin/smartctl を上書きする
  9. /bin/ フォルダーを tar コマンドで圧縮する
  10. rootfs を再パックし、暗号化する
  11. 暗号化された rootfs の末尾にパディングを追加する
  12. ヘルパー Ubuntu VM(VMDK をマウントする)を使用して、VMDK 内の rootfs を置き換える

図 2 は、デバッグ機能のある、編集された VM を作成する手順です。rootfs の完全性チェックは失敗するため、カーネルは実行を停止します。そのため、カーネル(flatkc)にパッチを適用してから、ブートローダーの完全性検証コードにもパッチを適用するか、カーネルの完全性チェックに動的にパッチを適用する必要があります。我々は後者の手法を採用することにしました。

図 2 は、デバッグ機能のある、編集された VM を作成する手順です。 図 2:研究環境のための FortiGate へのパッチ適用

我々は、VMDK ファイルを編集して VMware の VM デバッグ機能を使用することを試みました。これで、マシンの実行後に使用可能になる GDB デバッガーがセットアップされるはずです。残念ながら、Hyper-V が有効になっているマシンで実行すると、この機能の実装に問題が発生しました。ブレイクポイントに遭遇するとすぐに、マシンがクラッシュします。Hyper-V マシンで実行すると、カーネルデバッグ機能の実装が不完全であることが原因と思われます。

QEMU を使用して稼働中の VM を作成する

VMware でカーネルデバッグを実行する試みが複数回失敗した後、QEMU を使用して稼働中の VM を作成することにしました。同様の手順を使用して、変更を加えた VM を静的に作成しました。qcow2 ファイル形式と VMDK ファイル形式間で変換する必要がある場合を除きます。

QEMU を使用しているときにカーネルをデバッグするためには、qemu-system に -s フラグを指定します。最後に、VM を実行し、GDB をアタッチし、完全性チェックを上書きするためのブレイクポイントを追加すれば、CLI で迎えられます(図 3)。

最後に、VM を実行し、GDB をアタッチし、完全性チェックを上書きするためのブレイクポイントを追加すれば、CLI で迎えられます(図 3)。 図 3:CLI を備えた稼働中の VM

ネットワークの設定を行い、DHCP サーバーから有効な IP を受信した後、変更を加えた smartctl バイナリーを実行できます。このバイナリーは、現在のディレクトリーコンテンツである Linux ID コマンドを出力し、busybox telnet セッションを開きます(図 4)。

ネットワークの設定を行い、DHCP サーバーから有効な IP を受信した後、変更を加えた smartctl バイナリーを実行できます。このバイナリーは、現在のディレクトリーコンテンツである Linux ID コマンドを出力し、busybox telnet セッションを開きます(図 4)。 図 4:バックドアの有効化

最後に、図 5 は、新規作成した telnet サーバーに接続したことを示しています。

図 5 は、新規作成した telnet サーバーに接続したことを示しています。 図 5:シェルへの接続

これで終わりではありません 

図 6 に示すように、有効なライセンスがないため、管理パネルによる操作ができません。

図 6 に示すように、有効なライセンスがないため、管理パネルによる操作ができません。 図 6:ライセンスエラー

バイパスしてはいけません

最初は、ライセンスが無効なのは、1 CPU と 2,048 MB RAM の制限を超えたためと考えました。このため、選択肢が 2 つありました。この制限を受け入れて VM を非常に低速にするか、バイパスするかです。

何度かリバースした後、デーモンで定期的に呼び出される関数 upd_vm_check_license が見つかりました(図 7)。この関数は、マシンの RAM と CPU の数が制限を超えていないことを確認します。

何度かリバースした後、デーモンで定期的に呼び出される関数 upd_vm_check_license が見つかりました(図 7)。 図 7:VM の制約に関与する、逆コンパイルされたコード

num_max_cpus()max_allowed_RAM() の戻り値を動的に変更して制限をバイパスすると、制約のない強力な VM が手に入り、マシンを起動するときのエラーが少なくなりますが、ライセンスが無効のエラーは引き続き発生します。

ライセンス検証機能のリバースに多大な時間を費やし、ここに至るまでに行った選択について検討したところ、ライセンスがマシンのシリアルキーを使用していることが判明しました。このシリアルキーは、SMBIOS UUID を使用して構築されます。我々は QEMU に SMBIOUS UUID を指定しなかったため、NULL が使用されました。そのため、作成されたシリアル番号は「FGVMEV0000000000」でした。そこで、次のフラグを使用して、QEMU に SMBIOS UUID を指定しました。

  -smbios type=1,manufacturer=t1manufacturer,product=t1product,version=t1version,serial=t1serial,
  uuid=25359cc8-5fe7-4d50-ab82-9fd15ecaf221,sku=t1sku,family=t1family

最終的にマシンが起動し、有効なライセンスを取得することができました。 

新しいバージョン、新しい暗号化、なぜ?

この時点で、正常に動作するデバッグ環境が手に入りました。我々は管理 Web サーバーの調査を開始し、この記事の後半で説明する脆弱性をいくつか発見しました。そうした脆弱性を発見した頃、Fortinet は FortiOS バージョン 7.4.4 をリリースしました。

その脆弱性が引き続き新しいリリースに存在するかどうかを確認したかったのですが、更新された VM の rootfs を復号することができず、暗号化の方法がまったく異なっており、復号の難易度がさらに増していることがわかりました。この時点での主な目的は脆弱性がまだ存在することを確認することだったため、今回は、デバッグ環境の構築ではなく、新しい rootfs の復号に注力することにしました。

それではまず、(v.7.4.4 より前の)古い rootfs 復号方法について説明します。

1. カーネルは rootfs の完全性を検証し、有効であれば次のステップに進む

2.カーネルは fgt_verifier_key_iv を呼び出し、次のようにキーと IV を計算する

a. キー:グローバルデータの sha256()

b. IV:同じグローバルデータの別の部分の sha256()、その後、結果を 16 バイトに切り捨てる

3. Chacha20 および 上記のキーと IV を使用して、rootfs を復号する

次に、新しいアルゴリズムを見てみましょう(図 8)。

1. 復号コードは、以前のアルゴリズムと同様に、グローバル・データ・バッファーの sha256() によってキーと IV を計算する

2.ChaCha は、キーと IV を使用してメモリーブロックを復号する。このメモリーブロックは、RSA 秘密鍵を表す ASN1

3. RSA 秘密鍵から d,n を取り出し、既知の式 M = Cd mod N を使用して、暗号化されたファームウェアの最後の 256 バイトに存在するデータブロックを復号する

4. データブロックから、以下を取り出す

  • 16 バイトのノンス + カウンター 

  • 32 バイトのキー  

5. キーとノンス + カウンターを使用して、CTR モードで AES 復号を行う。コードは追加のカスタム CTR を使用する

6. この追加は、ノンス + カウンターのニブルに対する XOR の結果

 

新しいアルゴリズム(図 8): 図 8:ファームウェアの復号のフローチャート

このマルチステージ復号アルゴリズムを複雑化するために、新しい flatkc には記号がなくなり、ファームウェアを自動的に復号するツールを作成することが容易ではなっています。たとえば、ChaCha 復号と暗号化された RSA 秘密鍵に使用されるグローバルデータの検索などです。

上記の手順すべてを実行すると、rootfs を表示できます(図 9)。

上記の手順すべてを実行すると、rootfs を表示できます(図 9)。 図 9:rootfs の tar

今回は、変更を加えた環境を作成しませんでした。それには rootfs アーカイブを作成する必要があり、このアーカイブを、前述したとおり正しく復号する必要があります。もう 1 つの選択肢は、ブレイクポイントを動的に設定し、復号された rootfs でメモリーを上書きすることです。

管理 Web サーバーをリバースする

ようやく、管理 Web サーバーのリバースに取りかかることができます。それは Apache をベースにしており、一般的にインターネットにアクセスできないようにする必要があります(これに対し、sslvpn インターフェースはインターネットにアクセスできます)。

httpd 設定を開くと、ハンドラーへの URL を示すロケーションディレクティブがいくつか表示されます(図 10)。

httpd 設定を開くと、ハンドラーへの URL を示すロケーションディレクティブがいくつか表示されます(図 10)。 図 10:httpd 設定ファイルのスニペット

次に、バイナリー内のハンドラー文字列のいずれかを検索して、ハンドラーテーブルを見つけます(図 11)。

次に、バイナリー内のハンドラー文字列のいずれかを検索して、ハンドラーテーブルを見つけます(図 11)。 図 11:IDA に表示されるハンドラーのリスト

我々の関心は不正な脆弱性を見つけることなので、/api/v2/authentication URL からアクセスできる api_authentication-handler に注目することにしました。

リバース作業に入る前に、作業を簡単にするために Apache 構造体と接続構造体を IDA に作成することが推奨されます(図 12 および 13)。

  struct __attribute__((aligned(8))) _request_rec
{
    apr_pool_t *pool;
    conn_rec *connection;
    void *server;
    _request_rec *next;
    _request_rec *prev;
    _request_rec *main;
    char *the_request;
    int assbackwards;
    int proxyreq;
    int header_only;
    int proto_num;
    char *protocol;
    const char *hostname;
    unsigned __int64 request_time;
    const char *status_line;
    int status;
    enum http_methods method_number;
    const char *method;
    unsigned __int64 allowed;
    void *allowed_xmethods;
    void *allowed_methods;
    unsigned __int64 sent_bodyct;
    unsigned __int64 bytes_sent;
    unsigned __int64 mtime;
    const char *range;
    unsigned __int64 clength;
    int chunked;
    int read_body;
    int read_chunked;
    unsigned int expecting_100;
    void *kept_body;
    void *body_table;
    unsigned __int64 remaining;
    unsigned __int64 read_length;
    void *headers_in;
    void *headers_out;
    void *err_headers_out;
    void *subprocess_env;
    void *notes;
    const char *content_type;
    const char *handler;
    const char *content_encoding;
    void *content_languages;
    char *vlist_validator;
    char *user;
    char *ap_auth_type;
    char *unparsed_uri;
    char *uri;
    char *filename;
    char *canonical_filename;
    char *path_info;
    char *args;
    int used_path_info;
    int eos_sent;
    void *per_dir_config;
    void *request_config;
    void *log;
    const char *log_id;
    void *htaccess;
    void *output_filters;
    void *input_filters;
    void *proto_output_filters;
    void *proto_input_filters;
    int no_cache;
    int no_local_copy;
    void *invoke_mtx;
    apr_uri_t parsed_uri;
    apr_finfo_t finfo;
    void *useragent_addr;
    char *useragent_ip;
    void *trailers_in;
    void *trailers_out;
    char *useragent_host;
    int double_reverse;
    unsigned __int64 bnotes;
};

図 12:Apache 構造体

  struct __attribute__((aligned(8))) conn_rec
{
    apr_pool_t *pool;
    void *base_server;
    void *vhost_lookup_data;
    apr_sockaddr_t *local_addr;
    sockaddr *client_addr;
    char *client_ip;
    char *remote_host;
    char *remote_logname;
    char *local_ip;
    char *local_host;
    __int64 id;
    void *conn_config;
    void *notes;
    void *input_filters;
    void *output_filters;
    void *sbh;
    void *bucket_alloc;
    void *cs;
    int data_in_input_filters;
    int data_in_output_filters;
    unsigned __int32 clogging_input_filters : 1;
    __int32 double_reverse : 2;
    unsigned int aborted;
    ap_conn_keepalive_e keepalive;
    int keepalives;
    void *log;
    const char *log_id;
    conn_rec *master;
    int outgoing;
};

図 13:接続構造体

認証ハンドラーをリバースする際、最初に api_login_handler という名前の POST メソッドハンドラーをリバースしました。この関数は、api_login_parse_param を呼び出して、リクエストからログインパラメーターを取得します。この関数は、コンテンツタイプヘッダーに応じて POST データの解析を試みます。

  1. 「multipart/form-data」に設定されている場合、リクエストには HTML フォームがある

  2. そうでない場合は、プレーン POST データを読み取る

2 つ目の選択肢は非常にシンプルなので、1 つ目に主眼を置きます。逆コンパイルされたコードを見ると、libapreq ライブラリーを示すデバッグ文字列があることがすぐにわかりました(図 14)。

逆コンパイルされたコードを見ると、libapreq ライブラリーを示すデバッグ文字列があることがすぐにわかりました(図 14)。 図 14:コードが libapreq を使用することを示す文字列

libapreq はオープンソースの Apache ライブラリーであるため、ソースコードではなく逆コンパイルされたコードに脆弱性がないかどうかを調べる必要は(ほぼ)ありませんでした。そのため、最初に行う必要があったのは、ライブラリーのバージョンを調べることでした。何度か繰り返した後、バイナリーと特定のコミットに存在するが、1 度コミットした後に削除される文字列を見つけて、バージョンを絞り込むことができました(図 15)。

最初に行う必要があったのは、ライブラリーのバージョンを見つけることでした。何度か繰り返した後、バイナリーと特定のコミットに存在するが、1 度コミットした後に削除される文字列を見つけて、バージョンを絞り込むことができました(図 15)。 図 15:バイナリーの逆コンパイルされたコードと、文字列を削除するソースコードのコミットの比較

驚くべき点は、バイナリーに存在するライブラリーは、2000 年 3 月から利用できる最も古いバージョンだということです(図 16)。

驚くべき点は、バイナリーに存在するライブラリーは、2000 年 3 月から利用できる最も古いバージョンだということです(図 16)。 図 16:libapreq の絞り込まれたバージョン

脆弱性

Fortinet は、最適化のための非常に小さな変更を除き、25 年前とほぼ同じようにモジュールを使用しています。最初にこれに気付いたとき、我々は、2000 年から使用されているコードに脆弱性があるはずはないと考えました。この考えは間違っていませんでした。

脆弱性について見ていく前に、ライブラリーの目的と使用方法を説明します。Apreq は、クライアント・リクエスト・データを処理するために使用される Apache ライブラリーです。ユーザーからデータを受信する一般的な方法としては、HTML フォームなどがあります。入力されたフォームデータは、さまざまなエンコード方式を使用してサーバーに渡すことができますが、一般的な方法は application/x-www-form-urlencodedmultipart/form-data です。

multipart/form-data を使用する場合、クライアント(通常はブラウザー)は、フォームデータの異なるフィールド間の境界として任意のテキストを選択します。境界は HTTP ヘッダーを介して指定されます。また、境界はフォームデータの終わりを示すためにも使用されます(図 17)。

  POST /foo HTTP/1.1
  Content-Length: 68137
  Content-Type: multipart/form-data; boundary=ExampleBoundaryString

  --ExampleBoundaryString
  Content-Disposition: form-data; name="description"

  Description input value
  --ExampleBoundaryString
  Content-Disposition: form-data; name="myFile"; filename="foo.txt"
  Content-Type: text/plain

  [content of the file foo.txt chosen by the user]
  --ExampleBoundaryString--

図 17:HTTP 境界フォームの例(出典)

それでは、我々が見つけた脆弱性の一部を見てみましょう。たとえば、NULL バイトの範囲外(OOB)書き込み、ワイルドコピー、デバイス DoS、Web サーバー DoS、OOB 読み取りなどです。

NULL バイトの OOB 書き込み

multipart_buffer_read が内部バッファーを充填して境界を探すと、現在の位置と、見つかった境界の間の文字列が返されます。バグは、境界が内部バッファーの先頭にない場合、行の末尾であるはずの最後の 2 文字(“\r\n”)を削除した後に文字列を返すというものです。コードは、返される文字列の長さが 2 より大きいと誤って想定します。

図 18 では、retval は返される文字列、start はその長さで、start は 1 です。また、この場合、blen と start は同じ値です。その後、2 減らされて、値は -1 になります。したがって、バッファーの前に NULL を 1 バイト書き込むことができます。

図 18 では、retval は返される文字列、start はその長さで、start は 1 です。また、この場合、blen と start は同じ値です。その後、2 減らされて、値は -1 になります。したがって、バッファーの前に NULL を 1 バイト書き込むことができます。 図 18:バッファーの前に NULL の OOB 書き込みが行われることにつながる脆弱性

脆弱性の悪用

1 バイトのオーバーフロー(あるいは 1 ビットのオーバーフロー)でコードが実行される可能性はありますが、この脆弱性が実際に悪用される可能性は低いと考えられます。まず、記述できるのは NULL バイトのみで、バッファーの前に記述できるのは 1 バイトだけです。バッファーはヒープ上に割り当てられるため、次の 2 つの選択肢があります。

1. バッファーは、ヒープのノードに割り当てられる最初のバッファーです。この場合、割り当ての前にヒープのノードメタデータがあります(図 19)。

バッファーは、ヒープのノードに割り当てられる最初のバッファーです。この場合、割り当ての前にヒープのノードメタデータがあります(図 19)。 図 19:Apache メモリー・ヒープ・ノードを表す構造体

endp ポインターの 1 バイトを上書きします。エンディアンネスが原因でポインターの最大バイトを上書きするため、ポインターの値には影響しません。VM は x64 であるため、このバイトは常に 0 です。

2.以前に割り当てがあった場合は、1 バイトのデータを上書きできる。残念ながら、前の例のように、ほとんどの場合、構造体、パディング、またはすでに NULL で終了している C 文字列の末尾にポインターがあります。

興味深いオブジェクトが 1 つ見つかりました。multipart_buffer C 構造体です(図 20)。

興味深いオブジェクトが 1 つ見つかりました。multipart_buffer C 構造体です(図 20)。 図 20:multipart_buffer C 構造体

この場合、前の脆弱性によってその構造体の最後の変数である buffer_len を負の値にし、次にこの脆弱性によって負の値から大きい正の値に変える(数値を負の値として示す MSB バイトを上書きする)ことができると考えられました。

これは興味深い手法のように思えましたが、次の 2 つの問題があります。

1. 構造体は、フォームパーサーを作成するときに 1 回だけ作成される。このオブジェクトを簡単にはスプレーできないことを意味します。

2.前の脆弱性を使用した後、フィリング関数で読み取られる長さを制限した。フィリング関数が終了すると、リクエストの POST データ全体を読み取った後に self->length が 0 になることを意味します。次に multipart_buffer_read を呼び出すと、進められたバッファー内に境界が見つかりません(末尾ポインターが先頭より前に表示されるようにしたため)。また、self->length が 0 であるため、不正なアップロードのアラートを終了します。

競合状態での悪用を試みようと考えましたが、Apache マルチプロセッシングモード(MPM)を見たところ、図 21 が見つかりました。

競合状態での悪用を試みようと考えましたが、Apache マルチプロセッシングモード(MPM)を見たところ、図 21 が見つかりました。 図 21:Apache MPM モードの確認

Apache はプロセスを複数派生させ、そのそれぞれがリクエストを処理することを意味します。これらのプロセスはマルチスレッド化されていないこともわかります。競合状態では悪用できないことを意味します。

ワイルドコピー

同じ関数 multipart_buffer_read では、コードが境界を検出しない(start が -1)場合、返されるのは、内部バッファーの一部(bytes - boundary_length)のみです。ここでのエラーは、バイトが一定の値 5120 に設定されているのに対し、境界の長さは(ヘッダー長の限界まで)はるかに大きくなる可能性があることです。

そのため、境界が最初のチャンク内になく、境界の長さが 5120 より大きいフィールドを送信すると、blen を負の値にすることができます。これにより、コードはバッファーの前に self->buffer を配置し、self->buffer_len をより大きな値にします(図 22)。

これにより、コードはバッファーの前に self->buffer を配置し、self->buffer_len をより大きな値にします(図 22)。 図 22:整数アンダーフローに起因する脆弱性

脆弱性の悪用

この脆弱性と前の脆弱性には違いがあります。今回は、start が負の値である(境界が見つかならなかった)ため、NULL バイトを書き込むコードの話はしません

blen は multipart_buffer_read 関数のパラメーターで、この関数を呼び出して blen を出力として受け取る関数 multipart_buffer_read_body について見てみましょう。

図 23 は、blen が 2 回使用されていることを示しています。

1. 最初に作成されたバッファーの場合、multipart_buffer_read から受け取った文字列は blen を使用して複製される。この場合、Apache はメモリー不足(OOM)エラーを発生させ、コードが中断します。これは DoS 攻撃を開始するために使用できます。

2.2 番目のチャンクの場合、関数 my_join を使用して、前のチャンクと現在のチャンクが結合される。この関数は負の値の memcpy を呼び出すため、ワイルドコピーにつながります。

図 23 は、blen が 2 回使用されていることを示しています。 図 23:multipart_buffer_read_body 内の 2 つの異なるフロー

(このコードに別のバグがあることに気付いた人もいるでしょう。old_len が old_len + blen ではなく blen に更新されるというバグです。ユーザーデータの切り捨てにつながります)

可能であったとしても、そのようなワイルドコピーを悪用することは非常に困難です。まず、ワイルドコピーはサイズが大きい memcpy で、これを「停止」する選択肢はありません。また、マルチスレッド化していないため、別のスレッドで同時に使用されるオブジェクトを上書きすることはできません。唯一可能な選択肢は、安全な終了を実行できない場合にシグナルハンドラーを悪用することです。

デバイス DoS

この脆弱性は、実際にはライブラリーそのものには存在しませんが、ライブラリーを使用する Fortinet のコードに存在します。

POST データ内の Content-Disposition ヘッダーで指定されたフォームを介してユーザーがファイルをアップロードすると(図 17 を参照)、/tmp/uploadXXXXXX というファイル名(X はランダムな文字)の 新しいファイルが /tmp/ フォルダー内に作成されます。

ファイルをアップロードするたびに、適切な構造体が作成され、リンクされたアップロード済みファイルのリストに挿入されます。このバグは、リンクされたリストの最初のノードのファイルのみが削除された場合に、解析の最後に発生します。これにより、攻撃者は /tmp/ フォルダーをいっぱいにすることで、攻撃を開始できます。/tmp/ は tmpfs ファイルシステムのため、データは RAM に保存されます。システム全体の OOM ケースが発生し、デバイスがスタックします。

デバイスを再起動するだけで、通常の使用状態に戻りますが、それさえも保証されてはいません。我々が行った試みの 1 つで、デバイスに何らかのネットワーク障害が発生しました。デバイスを再起動しても、ネットワーク機能は正常に動作せず、そのデバイスを使用することも、そのデバイスに接続することもできませんでした。

脆弱性の悪用

この脆弱性の悪用は非常に容易です。複数のファイルを含むフォームを使用してリクエストを繰り返し送信するだけです。しばらくすると、デバイスがスタックします(図 24)。

複数のファイルを含むフォームを使用してリクエストを繰り返し送信するだけです。しばらくすると、デバイスがスタックします(図 24)。 図 24:VPN 機器の RAM に削除されていないファイルを入力することにより、メモリー不足が原因で DoS が達成されます

Web サーバー DoS

関数 multipart_buffer_headers の小さな脆弱性です。内部バッファーを充填する multipart_buffer_fill を呼び出し、内部バッファーで二重改行を探します。

このバグは、multipart_buffer_fill を呼び出した後、内部バッファーが有効であることをコードがチェックしないというものです。multipart_buffer_fill が入力を待っている間にクライアントが接続を切断すると、バッファーは NULL に設定され、これが NULL の逆参照につながります(図 25)。

multipart_buffer_fill が入力を待っている間にクライアントが接続を切断すると、バッファーは NULL に設定され、これが NULL の逆参照につながります(図 25)。 図 25:内部バッファーが有効かどうかをチェックせずに内部バッファーにアクセス

脆弱性の悪用

この脆弱性の悪用も非常に簡単です。攻撃者は、接続の作成とクラッシュを引き起こすリクエストの送信を行う複数のスレッドを作成します。Apache pre-fork MPM はパフォーマンスが良くないため、サーバーは複数のクラッシュを処理できず、攻撃中は他のクライアントにサービスを提供できません。

OOB 読み取り

ライブラリーは、定期的に充填される内部バッファーを使用します。内部バッファーが充填される際、読み取りバイト数の計算は次のとおりです。

1. bytes_requested - current_internal_buffer_len を計算する

2.境界の長さ + 2 をする(「\r\n\r\n」の場合)

3. この結果と、POST データから読み取れるバイト数のうち小さい方を計算する

この脆弱性は multipart_buffer_read に存在します。内部バッファーの先頭で境界が検出され、それがフォームの末尾を示す境界ではない場合、境界を越えて内部バッファーポインターを進めて、改行のためにさらに 2 バイト追加します。

ここでのエラーは、攻撃者が内部バッファーを境界のサイズと一致させ、コードに内部バッファーの末尾を 2 バイト超過させることができることです(図 26)。boundary_length + bytes_requested よりも少ないバイト数を指定することで、内部バッファーの計算を「制限」できるからです。

ここでのエラーは、攻撃者が内部バッファーを境界のサイズと一致させ、コードに内部バッファーの末尾を 2 バイト超過させることができることです(図 26)。 図 26:末尾を越えて内部バッファーを進める

脆弱性の悪用

図 26 に示すように、コードは NULL を返します。しかし、前のバグで見たように、multipart_buffer_headers のコードは、戻り値ではなく内部バッファーに直接アクセスします。これにより、コードはバッファーの後に「\r\n」を探し、それをヘッダーとして返します。

ログインハンドラーで apreq を使用する場合、コードはフォームフィールドを読み取ります。しかし、それをクライアントに送信しないため、この場合、この OOB の脆弱性を情報漏えいとして使用することはできません。また、このライブラリーは Web サーバーで複数回使用されますが、使用できるのは認証されたユーザーに限られるようです。そのため、せいぜい、権限の低いユーザー認証情報を持つ攻撃者が、権限の高いユーザー認証情報をメモリーから読み取ることにより、これを権限昇格の脆弱性として使用する可能性はあります。

SSLVPND

SSLVPND は、Fortinet の SSL-VPN コンポーネントを処理するデーモンです。これはインターネットにアクセスできます。apreq ライブラリーは SSLVPND でも使用されます。そこで使用されるライブラリーは、同じ古いバージョンの apreq をベースとしており、修正がいくつか加えられています。前述したすべての脆弱性(デバイス DoS の脆弱性を除く)は、LVPSSND にも存在します。

残念ながら、SSLVPND で apreq ライブラリーをトリガーすることはできませんでした。そのため、認証されていないユーザーがこうした脆弱性を悪用しやすいかどうか、また SSLVPND コンテキストで悪用される可能性があるかどうかを確認することはできません。

まとめ

VPN にはインターネットにアクセスできるという性質があるため、攻撃者が VPN を標的とすることが少なくありません。本ブログ記事では、VPN 機器を調査するための手法の一例を取り上げました。重大な脆弱性は特定されませんでしたが、まだ発見されていない他の脆弱性があると考えて間違いないと思います。

VPN は組織のネットワークへのゲートウェイであるため、この機器の脆弱性は組織に大きな影響を与えます。本ブログ記事が、他のセキュリティ研究者が VPN の脆弱性を探すきっかけになれば幸いです。



Ben Barnea

執筆者

Ben Barnea

February 11, 2025

Ben Barnea

執筆者

Ben Barnea

Ben Barnea は、Akamai の Security Researcher です。Windows、Linux、IoT、モバイルなど、さまざまなアーキテクチャのローレベルなセキュリティ調査や脆弱性調査への関心が高く、豊富な調査経験を有しています。複雑なメカニズムがどのように機能するか、そして最も重要な問題である、どのように失敗するかを知ることに生きがいを感じています。