はじめに
春めいた季節になってきましたが、いかがお過ごしでしょうか。本連載も第3回目となり、折り返し地点に入りました。そんな連載の中間地点となる今回は、著名なダウンローダである
今回の脆弱性:CVE-2016-7098
今回取り上げるCVE-2016-7098は、wgetにおける
wgetとは
まずwgetは、簡単に言えばHTTP/
$ 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
オプションの機能で見つかりました
ショートオプション | ロングオプション | 意味 | 使い方 |
---|---|---|---|
-r |
--recursive |
再帰的ダウンロードを行う | -r URL |
-m |
--mirror |
Webサイトのミラーリングを行う |
-m URL |
-A |
--accept |
指定した拡張子やパターンを持つファイルのみをダウンロードする | -A 拡張子-A パターン |
では、-A
オプションを付与することでその機能を利用できます。そしてここに脆弱性がありました。具体的には脆弱性により、-A
オプションを使ったとしても、任意のファイルを取得できる
wgetの-A
オプションの挙動
最初に、先ほど説明したwgetコマンドの通常の実行例を見せますwget -r
でダウンロードしようとしていますが、-A
オプションを付けて拡張子がjpgのファイルのみ取得を許可しています。そのためこのwgetコマンドを実行しても、pngの拡張子を持つ技術評論社のロゴファイルは取得できません。実際にwget実行後、ファイルのダウンロード先のフォルダである./
の中身をls
コマンドでのぞいても、何もないことがわかります。
-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のプログラム
<?php
echo 'Hello world!';
?>
このファイルは拡張子がphpのため、本来ならば取得できません。ですが、脆弱性をわざと発現させるために、少し細工した自作のWebサーバに対してwget
を使うと、なんとhelloworld.
wget
コマンドで取得、cat
コマンドで表示$ wget -r -A jpg http://localhost/helloworld.php &
(..略..)
$ cat ./localhost/helloworld.php
<?php
echo 'Hello world!';
?>
なぜ任意のファイルが取得できるのか?
この挙動は、-A
オプションを付けた状態で再帰ダウンロードする際のロジックに起因しています。実は、-A
オプションは
つまりこれは言い換えれば、
![図4](https://gihyo.jp/assets/images/article/2022/11/prevent-vulnerability-0003/03-01.png)
この状態を実現するため、先ほどの図3では簡単に言えば、wget
コマンドの要求を受けて)wget
コマンドを実行したからこそ、本来ならばすぐ削除されてアクセスできないはずのhelloworld.
どのような問題が発生するのか?
今回の脆弱性は一見、単なるバグのようにも見えます。この挙動で何か問題が起こるようには思えません。ですが、もしこの脆弱なwgetがサーバ側でWebアプリケーションの一部として組み込まれていた場合を想像してください。
たとえば、簡易的な画像加工サービスを考えてみましょう。これは、ユーザーが指定したWebページ-r
オプションで再帰的にサーバにダウンロードしていたとします。加えて、-A
オプションを利用して取得できるファイルの種類を画像のみにするよう、拡張子を指定して制限していたとします。
もしここで、今回筆者がやったように細工したWebサーバを用意し、そのうえに悪意のあるphpのプログラムを設置し、そのURLを画像加工サービスに入力した場合どうなるでしょうか。その場合、Webサービス側のサーバでいったん、悪意のあるphpのプログラムがダウンロードされた状態となってしまいます。そしてもし、そのファイルに外部からアクセス、実行できれば、どうでしょう。どのような悪意のあるプログラムが設置されたかによりますが、サーバやユーザーに対してさまざまな攻撃が可能になるのが、容易に想像できると思います。
競合状態の脆弱性について
最後に、今回のCVE-2016-7098の脆弱性を競合状態の脆弱性の定義に当てはめ、問題を俯瞰したいと思います。
冒頭でも述べましたが、競合状態の脆弱性とは、複数の独立したプロセスやスレッド
上記の定義に基づき、今回の脆弱性の要因を俯瞰すると次のようになります。
- ① wgetとは違う、別のプロセスから → 並列性
- ② ダウンロードされたファイル
(helloworld. php) が → オブジェクトの共有 - ③ 削除される前にファイルにアクセスできてしまう → 状態の変更
競合状態の脆弱性は、上記のようにプログラムのロジックに根付いたものであるため、開発者であっても発見や対策が難しいこともあります。ただ、競合状態の脆弱性となる代表的なロジックのパターン
脆弱性の修正方法
では、この脆弱性が実際にどのように修正されたかを見ていきます。最初に、修正の全体像と方針を説明します。補足になりますが、今回の修正パッチは3つのコミット[2]に分割されていますが、本記事ではそれらがすべて適用済みの状態を
修正の方針
この脆弱性の修正方法については、wgetのバグを報告・
多くの開発者がこれに同意したうえで、
- ❶ 再帰ダウンロードを行う際、ダウロード後に消される予定のファイル
(一時ファイル) の名前の末尾に、tmpという拡張子を付与する - ❷ 一時ファイルに対し、その所有者のみが読み出し/
書き込みできる権限 (パーミッションとしては600) を付与する
議論の当初は、
これにより、一時ファイルに対する安全性が、修正前よりもいくぶんかは高められたと言えます。たとえば今回のケースでも、サーバ上で悪意のあるphpなどのプログラムが実行できなくなります。では次に、実際に❶の修正後のコードを見ていきます。
tmp拡張子をファイルに付与する
修正では最初に、対象のファイルが、ダウンロード後に削除されるべき一時ファイルか否かを記録するためのフラグを追加しています。具体的には、HTTP経由の通信におけるさまざまな状態を保存する構造体であるhttp_
に、temporaryという名前のbool型のメンバを追加しています
http_stat
構造体にtemporaryメンバを追加struct http_stat
{
wgint len; /* received length */
wgint contlen; /* expected length */
(..略..)
+ bool temporary; /* downloading a temporary file */ ←追加
};
そのあと、追加したtemporaryメンバを用いる形
+ 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.
)
今回の話に直接関係あるのは、!acceptable (hs->local_
の部分です。acceptable関数は、ダウンロードしてくるファイルの名前(hs->local_
を受け取り、それが-A
オプションなどで許可されたものか否かを検査します。そして、もし許可されたファイルでなかった場合はfalseを返却します。その場合、一時ファイルか否かを表すhs->temporary
にtrueを格納したいため、この関数の先頭には論理否定であるNOTを表す!
」
続く2行目~6行目では、もし一時ファイルであった場合の処理について記載されています。具体的には、現在のファイル名の末尾にhs->local_
から削除したうえで、hs->local_
に格納しています。
アクセス権限を限定する
次に、一時ファイルに対して所有者のみ読み出し/open_
関数内で行われています。この関数は簡単に言えば、wgetで取得するファイルのデータの書き出し先となる、ファイルポインタを取得するためのものです。
- *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)
条件式が真であった場合に実行される部分
int open(const char *pathname, int flags, mode_t mode);
具体的にはopen関数の第3引数で設定しており、所有者からの読み書きのみを許可するため、S_
S_
補足になりますが、open関数は返却値としてファイルディスクリプタを返すため、そのままfdopen関数の引数として受け渡されます。そしてfdopen関数の実行の結果、一時ファイルのデータの書き出し先となるファイルポインタが得られているのです。
以上が今回の脆弱性の修正になります。いかがでしたでしょうか。最後になりますが、本脆弱性はwgetのバージョン1.
さらに勉強したい人向け
さらに勉強したい方に向けて、関連する書籍などを紹介します。競合状態の脆弱性自体について解説している文書としては、一般社団法人JPCERT/
それではみなさん、また次回お会いしましょう!