OSSに空いたセキュリティーホールと「脆弱性のふさぎかた」 その対策方法を調べる

第3回wgetの脆弱性 ~競合状態~

はじめに

春めいた季節になってきましたが、いかがお過ごしでしょうか。本連載も第3回目となり、折り返し地点に入りました。そんな連載の中間地点となる今回は、著名なダウンローダである「wget」において2016年に発見・修正された、CVE-2016-7098の脆弱性を取り上げたいと思います。普段利用している方も多いwgetコマンドに、いったいどのような脆弱性が発見されたのでしょうか。一緒に見ていきましょう!

今回の脆弱性:CVE-2016-7098

今回取り上げるCVE-2016-7098は、wgetにおける「競合状態の脆弱性」にあたります。これは複数の処理が、想定しない順序で並列実行されることに起因する脆弱性です。より詳しく言えば、⁠複数の独立したプロセスやスレッドなどが、共有のデータやファイルなどのリソースに対し、不適切な結果を生むような変更を加えることが可能な状況」に起因する脆弱性のことです。この説明だけではイメージが湧きづらいと思いますので、まずは実際に脆弱性をみせます。

wgetとは

まずwgetは、簡単に言えばHTTP/HTTPS経由などでインターネット上からファイルをダウンロードしてくれる、コマンドラインのプログラムです。たとえばwgetを使って、技術評論社のWebサイトのトップページをダウンロードしたのが図1です。実行結果として、トップページであるindex.htmlがダウンロードされているのがわかります。wgetは、このような対象WebページのURLを指定してダウンロードするだけの単純な使い方以外にも、実行時に付与するオプションを通じて、さまざまな機能や使い方をユーザーに提供しています。

図1 wgetコマンドの使用例
$ wget https://gihyo.jp
(..略..)
index.html         [ <=>     ] 71.34K   --.-KB/s    in 0.04s

2021-02-12 13:00:50 (1.82 MB/s) - ‘index.html’ saved [73049]

$ ls
index.html

どこに脆弱性があるのか?

今回の脆弱性は数あるwgetの機能の中で、対象のWebページから辿れるページもダウンロードする「再帰的ダウンロード」を行う-rオプションまたは-mオプションの機能で見つかりました表1⁠。

表1 今回の脆弱性に関連するwgetのオプション
ショートオプション ロングオプション 意味 使い方
-r --recursive 再帰的ダウンロードを行う -r URL
-m --mirror Webサイトのミラーリングを行う(その際に再帰的ダウンロードを実行) -m URL
-A --accept 指定した拡張子やパターンを持つファイルのみをダウンロードする -A 拡張子
-A パターン

では、⁠再帰的にダウンロードする」といったシンプルな機能にいったいどのような問題があったのでしょうか。実はこの機能を利用する際、すべてのファイルを一律にダウンロードするほかに、⁠ユーザーが指定した拡張子やパターンを持つファイルのみを取得する」といったことも可能です。具体的には、-Aオプションを付与することでその機能を利用できます。そしてここに脆弱性がありました。具体的には脆弱性により、-Aオプションを使ったとしても、任意のファイルを取得できる⁠隙⁠が生じていました。その様子を以降で見せたいと思います。

wgetの-Aオプションの挙動

最初に、先ほど説明したwgetコマンドの通常の実行例を見せます図2⁠。この例では、技術評論社のWebサイトのロゴ画像(gh_logo.png)wget -rでダウンロードしようとしていますが、-Aオプションを付けて拡張子がjpgのファイルのみ取得を許可しています。そのためこのwgetコマンドを実行しても、pngの拡張子を持つ技術評論社のロゴファイルは取得できません。実際にwget実行後、ファイルのダウンロード先のフォルダである./gihyo.jp/assets/imagesの中身をlsコマンドでのぞいても、何もないことがわかります。

図2 -Aオプションを付与してwgetを利用
$ wget -r -A jpg https://gihyo.jp/assets/images/gh_logo.png
(..略..)
Saving to: ‘gihyo.jp/assets/images/gh_logo.png’

gihyo.jp/assets/ima 100%[====================>] 989 --.-KB/s in 0s

2021-02-12 13:01:27 (17.1 MB/s) - ‘gihyo.jp/assets/images/gh_logo.png’ saved [989/989]

Removing gihyo.jp/assets/images/gh_logo.png since it should be rejected.

FINISHED --2021-02-12 13:01:27--
Total wall clock time: 0.1s
Downloaded: 1 files, 989 in 0s (17.1 MB/s)
$ ls ./gihyo.jp/assets/images/
$ ←何もない

この実行結果を見る限りは、確かに指定の拡張子以外のファイルは取得されておらず、一見何も問題がないように思えます。ですが実は、任意のファイルを取得できる隙があるのです。

脆弱性を利用して任意のファイルを取得する

前提として先ほどと同様に、-Aオプションを付与して、jpgの拡張子を持つファイルのみ取得を許可します。今回はwgetで取得したいファイルを、phpで書かれた次のHelloworldのプログラム(helloworld.php)だとします。

<?php
    echo 'Hello world!';
?>

このファイルは拡張子がphpのため、本来ならば取得できません。ですが、脆弱性をわざと発現させるために、少し細工した自作のWebサーバに対してwgetを使うと、なんとhelloworld.phpを取得できてしまいます図3⁠。先ほどと同じwgetコマンドのオプションを使ったにもかかわらず、です。

図3 helloworld.phpをwgetコマンドで取得、catコマンドで表示
$ wget -r -A jpg http://localhost/helloworld.php &
(..略..)
$ cat ./localhost/helloworld.php

<?php
    echo 'Hello world!';
?>

なぜ任意のファイルが取得できるのか?

この挙動は、-Aオプションを付けた状態で再帰ダウンロードする際のロジックに起因しています。実は、-Aオプションは「指定した拡張子のファイル以外を最初からダウンロードしない」というものではありません。一見そのような挙動に見えますが、正確には「ファイルをダウンロードしたあとに、指定した拡張子が付くファイル以外だったら、削除する」という動きをしています。しかも、ファイルを削除するのは、Webサーバとの接続(コネクション)が切れたあとです。これが今回の脆弱性の肝となります。

つまりこれは言い換えれば、⁠ファイルのダウンロードが終わったあと、Webサーバとの接続(コネクション)が切れるまでの間は、指定の拡張子以外のファイル(helloworld.phpなど)でも、まだ残っている状態ですので、アクセスできる隙がある」ということを意味します図4⁠。

図4 なぜhelloworld.phpが取得できたのか
図4

この状態を実現するため、先ほどの図3では簡単に言えば、wgetコマンドの要求を受けて)ファイルのデータを送ったあとにわざと接続を長時間切らない」という細工をしたWebサーバを筆者のほうで用意しています。そして、その自作Webサーバに対してwgetコマンドを実行したからこそ、本来ならばすぐ削除されてアクセスできないはずのhelloworld.phpにアクセスすることができたのです。

どのような問題が発生するのか?

今回の脆弱性は一見、単なるバグのようにも見えます。この挙動で何か問題が起こるようには思えません。ですが、もしこの脆弱なwgetがサーバ側でWebアプリケーションの一部として組み込まれていた場合を想像してください。

たとえば、簡易的な画像加工サービスを考えてみましょう。これは、ユーザーが指定したWebページ(URL)に含まれる画像をWebサーバ側で取得・保存し、サーバ上でその画像を加工したうえでユーザーに表示するサービスとします。その裏ではwgetを使い、ユーザーが指定したWebページに含まれるファイルを、-rオプションで再帰的にサーバにダウンロードしていたとします。加えて、-Aオプションを利用して取得できるファイルの種類を画像のみにするよう、拡張子を指定して制限していたとします。

もしここで、今回筆者がやったように細工したWebサーバを用意し、そのうえに悪意のあるphpのプログラムを設置し、そのURLを画像加工サービスに入力した場合どうなるでしょうか。その場合、Webサービス側のサーバでいったん、悪意のあるphpのプログラムがダウンロードされた状態となってしまいます。そしてもし、そのファイルに外部からアクセス、実行できれば、どうでしょう。どのような悪意のあるプログラムが設置されたかによりますが、サーバやユーザーに対してさまざまな攻撃が可能になるのが、容易に想像できると思います。

競合状態の脆弱性について

最後に、今回のCVE-2016-7098の脆弱性を競合状態の脆弱性の定義に当てはめ、問題を俯瞰したいと思います。

冒頭でも述べましたが、競合状態の脆弱性とは、複数の独立したプロセスやスレッド(①並列性)などが、共有のデータやファイルなどのリソース(②オブジェクトの共有)に対し、不適切な結果を生むような変更を加えることが可能な状況(③状態の変更)に起因します。そして、単なるバグならばまだ良いのですが、不適切な結果が何らかの悪用につながるものであった場合、脆弱性となります。

上記の定義に基づき、今回の脆弱性の要因を俯瞰すると次のようになります。

  1. wgetとは違う、別のプロセスから → 並列性
  2. ダウンロードされたファイル(helloworld.php)が → オブジェクトの共有
  3. 削除される前にファイルにアクセスできてしまう → 状態の変更

競合状態の脆弱性は、上記のようにプログラムのロジックに根付いたものであるため、開発者であっても発見や対策が難しいこともあります。ただ、競合状態の脆弱性となる代表的なロジックのパターン(TOCTOU[1]など)も知られているので、そちらを学ぶのも1つの手です。

脆弱性の修正方法

では、この脆弱性が実際にどのように修正されたかを見ていきます。最初に、修正の全体像と方針を説明します。補足になりますが、今回の修正パッチは3つのコミット[2]に分割されていますが、本記事ではそれらがすべて適用済みの状態を「修正後」の状態として扱います。

修正の方針

この脆弱性の修正方法については、wgetのバグを報告・議論するためのメーリングリスト(bug-wget@gnu.org)上にて、開発者同士が10通近くメールをやりとりしつつ、慎重に決められました。その議論の主な論点は「今回の事象は、Webアプリケーション提供者の設定や実装のミスから生じるものであり、wgetが悪いわけではないのでは?」です。たとえば、⁠ファイルのダウンロード先として指定しているフォルダが、外部からアクセス可能になっているのは管理者の設定ミスでは?」などです。

多くの開発者がこれに同意したうえで、⁠でも、⁠ダウンロード後に消されるはずのいわゆる)一時ファイルの取り扱いについては確かに少し問題があるから修正しよう」という結論に落ち着きました[3]。そこで一時ファイルの扱いを見直した結果、修正としてはwgetのソースコードの内、⁠http.c」に対し、簡単に言えば次の2つの処理を付け加えることとなりました。

  1. 再帰ダウンロードを行う際、ダウロード後に消される予定のファイル(一時ファイル)の名前の末尾に、tmpという拡張子を付与する
  2. 一時ファイルに対し、その所有者のみが読み出し/書き込みできる権限(パーミッションとしては600)を付与する

議論の当初は、⁠一時ファイルに対して、⁠第三者からは推測できないよう)ランダムなファイル名を付ければ良いのでは?」「一時ファイルのデータをすべてメモリ上に持てば良いのでは?」という意見も出ましたが、コードの修正範囲や、wget全体の利便性を加味したうえで、最終的には上のような修正になりました。

これにより、一時ファイルに対する安全性が、修正前よりもいくぶんかは高められたと言えます。たとえば今回のケースでも、サーバ上で悪意のあるphpなどのプログラムが実行できなくなります。では次に、実際に❶の修正後のコードを見ていきます。

tmp拡張子をファイルに付与する

修正では最初に、対象のファイルが、ダウンロード後に削除されるべき一時ファイルか否かを記録するためのフラグを追加しています。具体的には、HTTP経由の通信におけるさまざまな状態を保存する構造体であるhttp_statに、temporaryという名前のbool型のメンバを追加していますリスト1⁠。

リスト1 http_stat構造体にtemporaryメンバを追加
struct http_stat
{
  wgint len;                  /* received length */
  wgint contlen;              /* expected length */
(..略..)
+ bool temporary;             /* downloading a temporary file */ ←追加
};

そのあと、追加したtemporaryメンバを用いる形(http_stat構造体はhsという名前)で、http.cのcheck_file_output関数にてリスト2のコードを追加しています。ざっと読んだところ、この関数は作成するファイルの名前を指定する機能を持つ関数のようです。では1行ずつ、追加されたコードを説明していきます。

リスト2 一時ファイルに対してはtmp拡張子を付与する処理
+ hs->temporary = opt.delete_after || opt.spider || !acceptable (hs->local_file);
+ if (hs->temporary)
+   {
+     char *tmp = aprintf ("%s.tmp", hs->local_file);
+     xfree (hs->local_file);
+     hs->local_file = tmp;
+   }

まず1行目で、今回作成するファイルが一時ファイルか否かを推定するためのフラグopt.delete_afterなどをチェックしています。これらはそれぞれbool型の値や結果を持っており、1つでもtrueの場合、対象ファイルが一時ファイルであることを意味します。

今回の話に直接関係あるのは、!acceptable (hs->local_file)の部分です。acceptable関数は、ダウンロードしてくるファイルの名前(hs->local_file)を受け取り、それが-Aオプションなどで許可されたものか否かを検査します。そして、もし許可されたファイルでなかった場合はfalseを返却します。その場合、一時ファイルか否かを表すhs->temporaryにtrueを格納したいため、この関数の先頭には論理否定であるNOTを表す!が付与されています。

続く2行目~6行目では、もし一時ファイルであった場合の処理について記載されています。具体的には、現在のファイル名の末尾に「.tmp」を付けた新しいファイル名を作成後、そのファイル名の文字列を、もともとのファイル名をhs->local_fileから削除したうえで、hs->local_fileに格納しています。

アクセス権限を限定する

次に、一時ファイルに対して所有者のみ読み出し/書き込みを許可する権限(パーミッションとしては600)を付与する部分について説明しますリスト3⁠。修正自体はhttp.cのopen_output_stream関数内で行われています。この関数は簡単に言えば、wgetで取得するファイルのデータの書き出し先となる、ファイルポインタを取得するためのものです。

リスト3 一時ファイルに対して所有者からの読み書きのみ許可する処理
- *fp = fopen (hs->local_file, "wb");                     ←削除
+ if (hs->temporary)                                      ↓これ以降は追加
+    {
+     *fp = fdopen (open (hs->local_file, O_BINARY | O_CREAT | O_TRUNC | O_WRONLY, S_IRUSR | S_IWUSR), "wb");
+    }
+  else
+    {
+     *fp = fopen (hs->local_file, "wb");
+    }

今回は一時ファイルに対しての処理を追いたいので、最初のif (hs->temporary)条件式が真であった場合に実行される部分(4行目)について説明します。ここでは、ファイルの作成/オープンを行う次のopen関数を用いて、作成する一時ファイルに対して指定の権限を設定しています。

int open(const char *pathname, int flags, mode_t mode);

具体的にはopen関数の第3引数で設定しており、所有者からの読み書きのみを許可するため、S_IRUSR(ユーザーに読み出しの許可がある)S_IWUSR(ユーザーに書き込みの許可がある)が指定されています。

補足になりますが、open関数は返却値としてファイルディスクリプタを返すため、そのままfdopen関数の引数として受け渡されます。そしてfdopen関数の実行の結果、一時ファイルのデータの書き出し先となるファイルポインタが得られているのです。


以上が今回の脆弱性の修正になります。いかがでしたでしょうか。最後になりますが、本脆弱性はwgetのバージョン1.18未満に存在しています。該当するバージョンを利用している方は、最新版にアップデートすることをお勧めします。

さらに勉強したい人向け

さらに勉強したい方に向けて、関連する書籍などを紹介します。競合状態の脆弱性自体について解説している文書としては、一般社団法人JPCERT/CC[4]が公開しているセキュアコーディングに関する無料のセミナー資料[5]が詳しくてお勧めです。また競合状態の脆弱性も含めた、脆弱性全般を生み出さないためのセキュアコーディングの手法については『C/C++セキュアコーディング 第2版』[6]が詳しいです。

それではみなさん、また次回お会いしましょう!

おすすめ記事

記事・ニュース一覧