技術者ブログ

クラウド型WAF「Scutum(スキュータム)」の開発者/エンジニアによるブログです。
金床“Kanatoko”をはじめとする株式会社ビットフォレストの技術チームが、“WAFを支える技術”をテーマに幅広く、不定期に更新中!

2017年10月

1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31        
  • お問い合わせはこちら
Scutum開発者/エンジニアによる技術ブログ WAF Tech Blog

Tomcatに見つかった3つの脆弱性について

はじめに

Apache Tomcatに立て続けに見つかったCVE-2017-12615~12617の3つの脆弱性は基本的には同じ原因によるものでした。3つのうち2つは任意のJSPファイルをPUTリクエストでアップロードできてしまい、アップロード後にアクセスすることでJavaのコードが実行できてしまう、というパターンのRCE。残りの1つはJSPがそのまま静的ファイルとしてアクセス可能なためにソースコードが漏洩してしまう、というものです。

今回JPCERT/CCも注意喚起するなど、広く知られることになりましたが、一方でなかなかパッチが提供されないという状況にもなっています。今回はこの脆弱性についての調査を報告します。

readonlyパラメータとは何か

JSPがアップロードできてしまうのはreadonlyパラメータがfalseになっている場合です。このパラメータはデフォルトでtrueであり、falseにするのは特別な理由がある場合のみとなります。CVE-2017-12615とCVE-2017-12617はこのパラメータをfalseにしている場合のみに脆弱性として発現します。

readonlyパラメータはTomcat全体に関するパラメータではなく、下記のように、あくまでもDefaultServletについてのパラメータです。

readonlyがfalseと設定された場合、DefaultServletはPUTとDELETEを受け付けるようになります。DefaultServletのPUTとDELETEの実装は非常に明確で、「サーバ上にファイルを置く・消す」というものになっています。

DefaultServletというのは既に完成されてTomcat上で使えるようになっているサーブレットであり、普通は開発者が機能を拡張したりすることはありません。つまりTomcatにおいてreadonlyパラメータをfalseにするのは、HTTP経由でファイルを置く・消すという操作をしたい場合のみであり、WebDAVのようなHTTPを使ったファイル置き場のようなサーバとしてTomcatを使いたいケースだけになるということです。

最近はPUTメソッドはREST API実装を中心に「サーバ側にリソースを確保する(例えば、新規ユーザを作成する)」という使われ方をしていますが、このTomcatのDefaultServletのPUT処理は20年くらい前の概念そのままであり、それは単純に「ファイルをアップロードする」ものです。

したがって、REST APIのサーバとしてPUTリクエストを処理するウェブアプリケーションをTomcat上で動作させたい場合、PUTの処理のためにDefaultServletを使うことはありません。フレームワークが用意したサーブレットや、開発者が書いたサーブレットが使われます。そのため、REST API用にPUTを使いたい場合はDefaultServletの設定を変更する必要はないので、上記readonlyパラメータをfalseにする必要はありません。

つまりreadonlyパラメータは「Tomcatをファイルサーバとして使う」場合のみにfalseに設定されるものです。そしてTomcatをファイルサーバとして使う場合は、ほぼ確実に認証があったり、そもそもイントラネット上に置かれているだろうと推測されるので、多くの場合インターネット経由で任意の攻撃者からアクセスすることはできないでしょう。そう考えると実質的に今回の脆弱性で攻撃される可能性のあるサーバというのは非常に少ない可能性が高く、CVE-2017-12615とCVE-2017-12617のRCEについてはそれほど大騒ぎする必要はないのではないかと考えられます。

PUTを許可することにリスクはあるか

今回は「任意のJSPをアップロードできてしまうことでRCEになる」ことが脆弱性であるとされていますが、元々readonlyパラメータをfalseにしてPUTを受け付けるということ自体がそこそこ危うい部分があります。

Tomcatをインストールしreadonlyをfalseにして起動すると、/index.htmlに対してPUTが可能になります。これは仕様であり、CVE-2017-12615のような「想定外の」挙動ではありません。

初期のTomcatのホームページは/index.jspが使われますが、/index.htmlをPUTすると、そちらが優先されて使われます。つまりPUTによってウェブサイトのコンテンツは自由にコントロールでき、これは「ホームページの改竄」と考えることもできるかもしれません。もちろんJavaScriptも自由に記述できるので、攻撃者は任意のJavaScriptのコードを訪問者のブラウザ上で動かすことができますし、Cookieを盗むこともできます。

また、誰でも好きなファイルを置けるということ自体、「ファイル置き場にされてしまう」という点で好ましいものではありません。

上記2点(PUTが有効な場合、サイトの改竄やファイル置き場にすることが可能であること)については誰もTomcatの脆弱性であるとは考えません。その理由は「基本的にPUT可能な状態のTomcatをインターネット上で誰でもアクセスできる状態に置く使い方というのは間違っているし、そんなことは誰もしないだろう」という認識があるためだと思われます。

今回の3つの脆弱性の根本原因はどこか

このように誰でもアクセス可能なサーバにおいてreadonlyパラメータをfalseにしているケースというのはあまり多くないと考えられますが、残念ながらCVE-2017-12616についてはreadonlyパラメータがtrueであっても発現するものです。根本的な原因はreadonlyパラメータ周りではありません。

今回の3つの脆弱性の根本原因、あるいは攻撃者が突いているポイントは、あるHTTPリクエストが来たときにどのサーブレットで処理するかの選択ロジックです。本来JspServletが処理するはずのHTTPリクエストがDefaultServletに処理されてしまうというバグを悪用することで、ファイルをPUTできたり、DefaultServlet経由で(JSPとして処理することなく)GETできてしまうことがセキュリティ上の問題点になります。

DefaultServletはJSPファイルを特別扱いしません。PUT要求を受ければ拡張子がjspであってもサーバ上にファイルを生成しますし、.jspのファイルにGET要求を受ければその中身を返します。前者はRCEに、後者はソースコードの漏洩となりますが、DefaultServlet自体は淡々と静的コンテンツを処理するという役割を果たしているだけです。

具体的にはWindowsの代替データストリームを使ったり、/を末尾に付けるなどの細工によってJspServletのマッピングに引っかからずにDefaultServletにリクエストを処理させることで攻撃が成立する状態となっています。

readonlyをtrueにするのが適切な対処法か

実は3つの脆弱性いずれについても、DefaultServletとJspServletの両方がweb.xml内の設定において有効になっているという前提で議論されています。

Tomcatの初期設定でどちらも有効となっているため、この点は特に不自然ではありませんが、仮にどちらかのサーブレットが無効になっている場合は、2つのRCE脆弱性は発現しません。

JspServletのみ有効にする場合、画像等が配信できなくなってしまうために現実的にまともなウェブサーバとして動作しません。そのため、JspServletのみ有効にしているケースというのは基本的に考えられないでしょう。

しかし一方で、JSPを使った動的な機能を必要としない、静的なコンテンツ配信や、前項等で説明した単純なファイルサーバとしてTomcatを使いたい場合には、DefaultServletのみ有効にしておけば十分であり、JspServletは必要ありません。

先に説明したように任意のファイルのPUTやDELETEができるサーバにおいてJSPで動的にコンテンツを配信するということは(そこからincludeされるCSSやJS、画像ファイルが改竄できてしまうので)セキュリティ的な視点からするとあり得ないものであるため、DefaultServletのみを有効にした状態で稼働させることが適切です。この場合、仮に任意のJSPファイルがアップロードされた場合でも、そのJSPを動的に処理するJspServletが無効になっているため、RCEは発現しません。

今回のCVE-2017-12615とCVE-2017-12617に対して「readonlyをtrueにする」という回避策が紹介されていますが、そもそも理由があって(つまりファイルサーバが必要で)readonlyをfalseにしている場合、これをtrueにすることでそのサーバが提供していた機能が停止してしまうでしょう。それよりも「JspServletを無効にする」方法の方が適切である場面が多いのではないかと思います(もちろん、ファイルサーバ上には閲覧されても問題ないファイルのみが置かれていることが前提です。もし閲覧されては困るJSPファイルがある場合にはまず削除してからJspServletを無効にする必要があります)。

JspServletはPUT/DELETEを実装しているか

少し詳細な点に入りますが、JspServletはPUTとDELETEを実装していません。そのため、普通のPUTリクエストを/index.jspなどの既存のファイルに対して送ると通常、GETと同じレスポンスが戻ります。また、/hoge.jspなどの存在しないjspファイルにPUTやDELETEリクスエストを送ると404が戻ります。

DefaultServletはJSPのPUTが可能か

DefaultServletのPUT/DELETEはシンプルに実装されていて、ファイル名が「*.jsp」であってもアップロードが可能です。JspServletが有効になっている場合はそちらがリクエストを処理してしまうために結果としてDefaultServletによるPUTが行われないだけであり、元々DefaultServletの実装には「*.jspという拡張子のファイルはRCEに繋がるからアップロードさせないようにしよう!」という意図は存在していません。

DefaultServletはJSPのソースコード漏洩を配慮しているか

JspServletを無効にしてテストしてみるとわかりますが、DefaultServletはJSPファイルの中身をGET要求に対してそのまま返します。つまり「*.jspという拡張子のファイルの中身を見せてしまうと情報漏洩になるのでダウンロードさせないようにしよう!」という意図は存在していません。

PUTは特別な設定の場合のみ動作することを考えると(JSPのPUTを許可しても)まぁ良いとしても、JSPのソースコードがそのまま見えてしまうということ自体を問題視するのであれば、DefaultServletに多少の配慮を加え、開こうとしているファイルが.jspである場合には動作を止めるような機能をフェイルセーフ的に付けても良さそうに感じます。しかし現在のTomcatはそのような思想では作られておらず、基本的にサーブレットのマッピングの段階でjspファイルへのアクセスはJspServletに割り当てることをセキュリティ的な分岐点として捉えているようです。

バージョン7.0.81においてGET /index.jsp/ HTTP/1.0のようなリクエストを送ると404が返ってきますが、実は内部ではDefaultServletの処理において選択されたcacheEntryに/index.jspが入ってしまっていて、ファイルの中身も読み込まれています。一応、最後の/を引っ掛けて404としていますが、なかなか危ないように見えます。

上記、nameの値は/index.jsp/だが、実際に開いているファイル(File)のpathの値はindex.jspを指しており、DefaultServletがJSPファイルを開いてしまっている状態

この時のスタックトレース

ServletFilterでJSPのソースコード漏洩を防ぐことができるか

DefaultServletが意図せず「*.jsp」の中身を送ってしまうケースをServletFilterを使って防ぐことができないかを少し考えてみました。最もよいアプローチはDefaultServletがリクエストに対してひも付けたCacheEntryの中のFileが「*.jsp」の場合には処理を中断することだと考えたのですが、ServletFilter側からCacheEntryにアクセスすることは基本的には無理なようです。そのためServletFilterを使って確実にJSPのソースコード漏洩を防ぐスマートなアプローチは見つけることができませんでした。

ServletFilterで苦労するよりも、DefaultServletに「*.jsp」の場合には処理を中断するようなパッチを当ててしまう方が簡単そうです。この場合、DefaultServletの836行目付近で

CacheEntry cacheEntry = resources.lookupCache(path);

という処理が終わった直後に、上記画像を参考にcacheEntry.attributes.file.pathが「*.jsp」や「*.jspx」でないことを確認すればよいでしょう。

まとめ

今回はTomcatの脆弱性について調査した結果を簡単にまとめました。DefaultServletとJspServletはお互いに干渉せずに動作していますが、JSPに向けたリクエストが誤ってDefaultServletに処理されてしまうバグは脆弱性となります。

このパターンのRCEは現実的にあまり問題にならないだろうと思いますが、一方でJSPソースコードの漏洩についてはDefaultServletにおいて根本的な対策はされていません。今回は(おそらく偶然に)VirtualDirContextが使われている場合のみでしたが、今後も何かのはずみで似たような脆弱性が見つかるような気がします。