本サイトは、快適にご利用いただくためにクッキー(Cookie)を使用しております。
Cookieの使用に同意いただける場合は「同意する」ボタンを押してください。
なお本サイトのCookie使用については、「個人情報保護方針」をご覧ください。
はじめに
はじめまして。昨年春に入社した新人診断員です。
突然ですが、皆さんはWebアプリケーションのフレームワークは何がお好きですか?
私はここ4-5年ほどRuby on Railsを好んで使っています。最近は他のフレームワークも気になっているのですが、慣れていることもあって使い続けています。
Webアプリケーションのフレームワークを使うメリットとしては、エンジニアが意識しなくてもある程度の脆弱性対策を行うことができる点が挙げられます。独自実装するよりも既存のきちんと作られたフレームワークを使った方が、セキュリティ面で優れていることが多いです。
しかし、フレームワークを用いているからといって100%安全とは限りません。フレームワークそのものに脆弱性が発見されるケースがあります。
フレームワークに存在した脆弱性
2019年の3月にRuby on RailsでCVE-2019-5418が発見されました。この脆弱性が今回取り上げるディレクトリトラバーサルと呼ばれるもので、任意のファイルやディレクトリを参照、操作できる脆弱性です。
私が学生時代に作っていたアプリケーションでも影響があり、急いで対応しなくてはならなかった記憶があります。
フレームワークの脆弱性なので、幸いアップデートすることで対策ができましたが、診断員になって様々な脆弱性への知見を深めるにつれて、当時問題となったこの脆弱性の背景についてもっと知りたいと思うようになりました。
そこで本稿では、やられサイトを用いて基礎的なディレクトリトラバーサルについて解説します。
ディレクトリトラバーサルとは
前置きがやや長かったですが、ここから本題の解説に入っていきます。
ディレクトリトラバーサルとは、ファイルやディレクトリを操作する際に、不正なパスを挿入されることによって意図しないディレクトリやファイルを参照、操作されてしまう問題です。
サーバ上のファイルを参照され、機密情報が漏洩するなどの被害が発生します。また、取得できた機密情報の種類によっては不正なコード実行などにも繋がるため、影響の大きな脆弱性です。
ファイルパスとは
ディレクトリトラバーサルについて解説する前に、ファイルパスについて軽く解説します。
ファイルパスとは、コンピュータ内でのファイルの場所を差す文字列のことです。
$ pwd
/home/example/blog/directory
これは今いるディレクトリ(カレントディレクトリ)を表示するコマンドの実行結果です。上から順番に「ルートディレクトリ(/)の中のhomeディレクトリの中のexampleディレクトリの中のblogディレクトリの中のdirectoryディレクトリ」にいることを表しています。
directoryディレクトリの中のtop.htmlを表すファイルパスは、
/home/example/blog/directory/top.html
となります。
また、ファイルパスで「../」を使うことで、カレントディレクトリから見た上位ディレクトリを表現することもできます。
例えば、以下の二つのパスは同一のディレクトリ、blogを指しています。
/home/example/blog/
/home/example/blog/directory/../
directoryディレクトリからみた上位ディレクトリを../が表すので、同じものを指しているわけです。
簡単なアプリで学ぶディレクトリトラバーサル
それでは早速、パラメータで受け取ったファイル名のパスを参照し、画面に表示するWebアプリを使ってディレクトリトラバーサルの動作について見てみましょう。
サンプルアプリのディレクトリ構成は以下のようになっています。
$ tree
.
├── sub
│ └── sub.html
├── test.php
└── top.html
ソースは以下のようになっています。
<html>
<head><title>Directory Traversal</title></head>
<body>
<h1>以下にpageパラメータのファイルの中身が表示されます</h1>
<?php
$page = $_GET['page'];
include $page;
?>
</body>
</html>
test.php
$_GET[‘page’]で、クエリパラメータ:page(https://example.com?page=XXX のXXXの部分)を取得しています。取得したファイルパスをphpのincludeという関数を使って、ページの一部として表示します。
<html>
<body>
<h2>これはtop.htmlの中身です</h2>
</body>
</html>
top.html
<html>
<body>
<h2>これはsub.htmlの中身です</h2>
</body>
</html>
sub/sub.html
まずはtop.htmlの中身を表示してみましょう。
/test.php?page=top.html
top.htmlの中身が表示されましたね。今度はsub/sub.htmlを表示してみましょう。
/test.php?page=sub/sub.html
このようにパラメータに任意のファイルパスを渡した場合、値の検証を行わずに処理を行うと開発者が意図していないファイルを参照、操作可能となってしまいます。例えば以下の例では、/etc/hostsが表示されてしまいます。
/test.php?page=../../etc/hosts
また、ファイルの中身を参照できないように設定されていても、実在するファイルパスと実在しないファイルパスにおいて挙動の変化が生じる場合に、攻撃者はファイルの有無を探索することが可能となります。任意のファイルの中身を参照できない場合、直接攻撃につながるわけではありませんが、攻撃の手がかりとなる可能性があります。
対策
ユーザの入力値を、ファイルの入出力処理に直接用いないようにすることが主な対策です。それが難しい場合には、入力値検証を厳密に行うことが対策となります。
入力値を直接使わない場合
操作対象のファイルやディレクトリに識別子を付与し、それをパラメータとして受け取る仕様にします。
01 => top.html
02 => sub.html
のように指定しておけば、
GET /test.php?page=01 HTTP/1.1
でtop.htmlの内容を取得可能です。もしこの対応表にないデータが渡された場合には、エラーを返すように設定します。
入力値を直接使わざるを得ない場合
アプリケーションの仕様上、パラメータでファイル名を直接指定できることが望ましいケースも考えられます。
その場合は入力値検証を厳密に行い、パラメータの値に不正な記号などが含まれていたら処理を中断します。具体的には、「/」「../」「..\」などのディレクトリを指定できる文字列です。
なお、入力値から「../」といった上位参照を示す文字列を削除することで、ディレクトリトラバーサルを防ぐ実装が実案件でも時たま見られます。残念ながら、この対策では不十分です。
例として、test.phpの処理を書き換えて、「../」を入力値から削除してみます。
<html>
<head><title>Directory Traversal</title></head>
<body>
<h1>以下にpageパラメータのファイルの中身が表示されます</h1>
<?php
$page = $_GET['page'];
$page = str_replace('../', '', $page);
include $page;
?>
</body>
</html>
test2.php
str_replace関数を用いることで、入力値に含まれている「../」を削除しました。
それでは、pageパラメータを「../../etc/hosts」にしてもう一度アプリにアクセスしてみましょう。
何も表示されていませんね。「../」が取り除かれて、有効でないパスになったため/etc/hostsが表示されなくなりました。
この対策で一見よさそうに見えるのですが、先述した通り不十分です。 試しに、pageパラメータを「..././..././etc/hosts」に変えてアクセスしてみます。
/etc/hostsが表示されてしまいました。 「../」を削除した後の文字列が有効なファイルパスになってしまっていたためです。
これはディレクトリトラバーサル以外の脆弱性でも、任意の文字列の削除で対策している場合には起こり得ます。根本的な対策とはならないため、入力値検証や特殊文字のエスケープといった、各脆弱性それぞれの適切な対策を取ることが重要です。
また、パス名からファイル名のみを取り出す関数を用いることも効果的です。例えばPHPだとbasename関数が挙げられます。
test.phpでbasename関数を使ってみます。
<html>
<head><title>Directory Traversal</title></head>
<body>
<h1>以下にpageパラメータのファイルの中身が表示されます</h1>
<?php
$page = $_GET['page'];
$page = basename($page);
include $page;
?>
</body>
</html>
test3.php
sub/sub.htmlや../../etc/hostsで試すと、ファイルが表示されなくなりました。ファイル名だけ取り出されているので、includeに失敗したようです。
CVE-2019-5418の場合
一通りディレクトリトラバーサルの原理について学んだところで、最初のRuby on Railsの脆弱性に戻ります。
HTTPリクエストにはAcceptヘッダと呼ばれる、要求するコンテンツのMIMEタイプ(文書、ファイルなどの性質を表すもの)を示すヘッダが存在します。
CVE-2019-5418の場合このMIMEタイプを、ファイルをレンダリングするパターンマッチに直接用いていたため、細工したAcceptヘッダを送信することで任意のファイルを読み込むことが可能となっていました。
対策として、入力値の検証を厳密に行い、Ruby on Rails側のAllowlistにないMIMEタイプを無視するように修正されました。
修正コミットで追加されたテストでは、mime/anotherという不正なMIMEタイプを送信した際に、無視されることを期待した記述になっています。
[修正コミット]
actionpack/lib/action_dispatch/http/mime_negotiation.rb
[追加されたテストの例]
actionpack/test/controller/new_base/content_negotiation_test.rb
( https://github.com/rails/rails/commit/c79dcbce9bfd20fe7f72ca431c49965ee39bd645 より引用)
おわりに
今回は単純なケースを例に挙げて、ディレクトリトラバーサルについて解説しました。
単純なケースでも複雑なケースでも、ディレクトリトラバーサルが発生する根本的な原因は、ユーザの入力値をファイルなどの入出力処理にそのまま使用していることです。つまり、ユーザの入力値を使わないことが一番の対策です。
ですが、やられサイトを用いた解説の中でも述べた通り、ユーザの入力値を使わなくてはならないケースも存在します。その場合は、入力値の検証を厳密に行うことが重要となります。
おすすめ記事