HOME > Scutumを支える技術 > Scutum技術ブログ

技術者ブログ

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

2021年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

例えば、Strutsを避ける

はじめに

筆者は10年以上ウェブアプリケーション開発を主な業務とするJavaプログラマであったにも関わらず、Strutsについてはこれまでずっと食わず嫌いでした。初期のStrutsは「XMLだらけで効率が悪そう」というイメージが強かったためです。最近はRuby on Rails等の影響を受けCoC(convention over configuration)を採り入れ、XML地獄もだいぶ解消したようです。

StrutsはJavaアプリケーションらしくない種類(任意のコード実行等)の脆弱性を連発することでも知られており、最近は我々の提供するSaaS型WAFサービス、Scutum(スキュータム)のお客様からも頻繁にStrutsについての問い合わせを受けるようになりました。また、去年見つかった任意のコード実行の脆弱性では、脆弱性の公表後すぐにPoCが出回り実際に攻撃が発生するなど、悪い意味で注目せざるをえないフレームワークとなっています。

そのため、最近になって私もようやく重い腰を上げ、Strutsと正面から向き合ってみることにしました。

OGNLとJSP

Javaはコンパイル型の言語であるため、通常はランタイムにおいて任意のコードを実行することはできません。しかし現実に、Strutsは任意のコード実行の脆弱性を過去にたびたび提供してきました。この原因はフレームワークやアプリケーションサーバ内にコードをコンパイルして実行する機能や、動的にクラスファイルを解析したりする機能が備わっているからです。

なぜそんな危険なことをわざわざ行っているのかと思ってしまいます。しかし、「実行時にコンパイルする」ような処理はある種の黒魔術的な破壊力を持っており、普通のJavaアプリケーションではできないだろうと思われていることが可能になるという側面があります。筆者も過去にJavassistを使って実行時にクラスファイルを動的に書き換えたり、Scutumにおいては(従来は単なる設定ファイルであった)シグネチャをコンパイルされるように仕様変更したりと似たようなことをやってきているので、気持ちはよく分かります。

かつて、PHPはHTTPリクエストからパラメータを与えると、それがそのままPHP言語内の変数にバインドされるという仕様(いわゆるRegister Globals)で登場しました。筆者は当時PerlによるCGIでの開発を主に行っていたため、「わざわざ自分たちのコードでHTTPリクエストをパースしなくてよい」というPHPの仕様はなるほどと納得できるもので、ウェブ時代の到来を体感したものです。しかしご存じの通りこの機能はセキュリティ的に非常に危険であることが判明し、PHP5.4.0からはついに削除されたようです。

Strutsでは動的な処理のためにOGNL(Object Graph Navigation Library)と呼ばれるライブラリが用いられており、このOGNLが基本的には非常に危険な脆弱性の火薬庫となっています。OGNLが行っていることは殆どPHPのRegister Globalsのようなもので、Actionクラスのgetterやsetterを外部から自由に実行することが可能となっています。Strutsのイロハを知らなかった私はまさかこんな危険な仕様であるとは知らなかったため、脆弱性の検証を行っている間も、「どこまでが問題とされている脆弱性で、どこからが仕様なのか」がさっぱり切り分けられませんでした。つまりStrutsのデザインそのものが「外部から任意のgetter及びsetterを実行されてしまう脆弱性」を持っているように見えたのです。

また、Struts以外でも、Javaのウェブアプリケーションにおいて非常にポピュラーなJSPは、ファイルの内容を書き換えてリロードするだけでJavaのコードの変更が反映されます。これはまさに毎回書き換えられたJSPのソースからアプリケーションサーバ内のコンパイラがJavaサーブレットのクラスファイルを生成することで実現されており、「実行時にコンパイルされる」ことが日常的に行われているというのが現状です。このような実行時のコンパイルはもちろん攻撃者にとっては最大級の魅力的なターゲットであり、先日見つかったStrutsの脆弱性 S2-020においても、中国人と思われるハッカーのPoCでは任意のコード実行そのものはJSPを用いて実現されています。

OGNLやJSPのような機能は便利である反面、安全に使うために(ミドルウェア実装者が)細心の注意を払う必要があります。


Strutsのここがダメ

セキュリティ的な観点から見た場合、Strutsは控えめに言っても「どうしようもない」という印象です。以降、筆者が今回の調査を通じて感じた点を列挙していきます。


その1: PHPのRegister Globalsと同じレベルの危険な仕様

先にも書きましたが、外部から任意のgetterとsetterを呼び出すことが可能という仕様それ自体があり得ません。最低でも、getterやsetterが呼び出されるのはAction等の特定のインターフェースやクラスを継承しているクラスに制限するなどすべきでしょう。今回のS2-020のようにアプリケーションサーバのクラスローダのgetterやsetterもかまわず呼び出すという実装はあり得ないです。


その2: 同じ間違いを繰り返す

ソフトウェアに脆弱性が入り込むこと自体は避けるのが難しいため、「脆弱性があったから、このソフトウェアはダメだ」とは思いません。あるソフトウェアが今後も継続して安全に使い続けられるものかどうか、という点において重要なのは、脆弱性が見つかった場合の直し方です。センスがある開発者あるいはチームがいる場合、指摘された脆弱性とその危険性、そしてそれが何故入り込んだのかをきちんと理解し、根本的にどのようなアプローチを採れば次に同じ脆弱性を作り込まずに済むかをその時点で考えます。同じような脆弱性をだらだら何度も見つけられてしまうものは失格です。

StrutsではOGNLを原因とした脆弱性が何度も見つかっています。「同じタイプの脆弱性をだらだら繰り返す」という、典型的な「セキュリティ的にダメな」ソフトウェアと言えるでしょう。

今回私が調査したS2-020という脆弱性に対しても、Strutsが採った対策というのは「NGワードを追加する」というアプローチです。「付け焼き刃的対策」の見本といった感じで、しかも後述しますが内容も間違っていました。

OGNLで脆弱性が見つかった場合にStrutsが採る対策として、Parameter Interceptorの初期パラメータ(excludeParams)にNGワードを追加するというものがあります。

2007年1月の時点で、最初のexcludeParamsが登場しました。

<param name="excludeParams">dojo\..*</param>

2008年6月に、strutsという単語が追加されます。

<param name="excludeParams">dojo\..*,^struts\..*</param>

2012年3月に、一気に複数の単語が追加されます。

<param name="excludeParams">dojo\..*,^struts\..*,^session\..*,^request\..*,^application\..*,^servlet(Request|Response)\..*,parameters\...*</param>
少なくともこの時点で「何かおかしい」と気付いて方向転換するべきではないかと思います。

2013年10月に、actionとmethodが追加されます。

<param name="excludeParams">^dojo\..*,^struts\..*,^session\..*,^request\..*,^application\..*,^servlet(Request|Response)\..*,^parameters\..*,^action:.*,^method:.*</param>

2014年3月になり、S2-020の修正としてclassが追加されます。現時点(2014年4月)で「最もセキュアである」とみなさんが認識されている、最新版(2.3.16.1)の内容がこちらです。

<param name="excludeParams">^class\..*,^dojo\..*,^struts\..*,^session\..*,^request\..*,^application\..*,^servlet(Request|Response)\..*,^parameters\..*,^action:.*,^method:.*</param>

このように、過去5回に渡って付け焼き刃的対策を繰り返してきたことがわかります。もしこれが人間だったら、「あいつには何度言ってもダメだよ」と言われるタイプでしょう。

今後はどうでしょうか?まだこのリストは増え続けていくのでしょうか。今、この記事を読んでいるStrutsユーザの方に、ぜひ訊いてみたい質問です。まだまだこのリストは増えていくと思われますか?

残念なことに、答えはYesです


その3: 脆弱性を直すセンスがない

(「センスがない」のではなくて「やる気がない」のかもしれませんが...)

今回S2-020についてのStruts側の説明は「クラスローダにアクセスできてしまう脆弱性」というものでした。JavaではすべてのクラスはObjectクラスを継承しているため、すべてのクラスにおいて必ずgetClass()というメソッドが呼び出し可能となります。getClass()そのものは無害なのですが、Strutsではgetterやsetterをチェーンして呼び出していくことができる(例えばfoo.bar.baz=123はgetFoo().getBar().setBaz(123)のようにOGNLによって変換され処理される)ため、getClass()の戻り値であるクラスのインスタンスに対してgetClassLoader()を呼び出すことでクラスローダーに到達することができます。

class.classLoader == getClass().getClassLoader()

クラスローダーの実装はアプリケーションサーバによって異なります。攻撃者はこうして辿り着いたクラスローダーについてもgetterかsetterを呼び出すことができるため、今回発見されたPoCのように、Tomcatにおいてクラスローダーを経由してAccessLogValveクラスにアクセスし、アクセスログの場所をJSPと同じディレクトリに変更して最終的に任意のコード実行が可能となりました。

上記の2.3.16.1での修正ではパラメータの内容は

^class\..*
となっているため、「class.」で始まるパラメータにマッチします。攻撃者が
GET /foo.action?class.classLoader.attack=123 HTTP/1.0
のようなリクエストを送ってきた場合には「class.」の部分に正しくマッチして防ぐことができますが、
GET /foo.action?bar.class.classLoader.attack=123 HTTP/1.0
のように、最初のgetterをgetClass()以外にして、その戻り値のインスタンスに対してgetClass()を呼び出すことが可能となってしまっています。foo.actionにgetBar()というメソッドが定義されており、戻り値が何かしらのオブジェクトの場合には、これによってやはりクラスローダーへのアクセスを許してしまいます。攻撃者の視点からは、前者に比べ後者はやや攻撃のためのハードルは上がる(存在する適切なgetterを探す必要がある)ことになりますが、まだ脆弱性が残っているということになります。

正規表現を間違えたのか、あるいは「getterのチェーン内においてどこからでもgetClass()が呼び出される可能性がある」ことに気付かなかったのかは(このコミットにはテストが存在しないので)わかりませんが、いずれにせよStrutsの修正はお粗末なものと言えるでしょう。そもそも「クラスローダへのアクセスを禁じる」ためには、明示的に

.*classLoader.*
等とするべきではないかと思います(これによって正規の使用が妨げられる可能性は殆ど無いでしょう)。 (2014/04/24修正 この部分、class.ClassLoderのように大文字が混ざるケースにマッチしないという情報があり、確認したところその通りでした。お詫びして削除します。)

筆者はS2-020の調査中に上記の修正漏れに気がついたのでGithubを使って修正依頼を行おうと思ったのですが、最新のソースコードを見ると、既にきちんとテスト付きで修正されていました。おそらく次回アップデートに反映されるでしょう。S2-020対策としてexcludeParamsを手動で修正しているユーザは、このコミットを参考に、該当部分以下のように修正しておいてください。

誤(2.3.16.1)

^class\..*

正(次回アップデート?)

(.*\.|^)class\..*

個人的には以下がおすすめです。

.*classLoader.*
(2014/04/24修正 この部分、class.ClassLoderのように大文字が混ざるケースにマッチしないという情報があり、確認したところその通りでした。お詫びして削除します。)


その4: ユーザに注意を要求する

これまでに触れてきたexcludeParamsによる付け焼き刃的対策はstruts-default.xmlというファイルのデフォルト値において行われてきました。このパラメータはユーザが自らの設定ファイルで上書きする可能性があるものです。設定ファイルで該当パラメータを上書きした場合、値は完全に無視される(上書きされる)ため、Strutsの危険性は2007年の時点に逆戻りします。

Strutsユーザはこのパラメータを自分で設定する場合、struts-default.xmlに記述されているNGワードを必ず記述する必要があります。

このように、安全に使うためにユーザに注意することを要求するというのは、あまりよいことではありません。設定ファイルをどのように変更しても脆弱性が入り込まないよう、ソースコードレベルなど、より低レベルでNGワードを禁止しておくべきでしょう。


その5: 脆弱性に対する認識が甘い

こちらのメールにて、「今回の脆弱性で考えられる最悪のケースは何なのか?リモートからの任意のコード実行か?」という質問に対して「いや、違う。クラスローダにアクセスされるということだ。」と答えています。

実際にはTomcatというポピュラーなアプリケーションサーバにおいてリモートからの任意のコード実行が可能であることが発覚しました。クラスローダにアクセスされてしまったら、その先何が可能になるのかは実装依存なのですから、「No,rather no」という返答はあり得ないと思います。「クラスローダの実装に依存するので、最悪のケースで何が起こるのかはわからないが、リモートからのコード実行もあり得る」と答えるべきでしょう。認識が甘いのか、問題を小さく見せたいのか、あるいはあまり深く考えていないのかはわかりませんが、いずれにせよ脆弱性に対する認識が非常に甘いように感じます。

まとめ

このように、残念ながらStrutsはセキュリティ的には相当にダメな部類に属します。しかも悪いことにStrutsはポピュラーであり、実際に攻撃が多く行われる(つまり狙われやすい)存在となっています。既存のStrutsを前提としたアプリケーションを他のフレームワークに乗り換えることは事実上無理だと思いますが、新規案件等では他のフレームワークを選ぶことをおすすめします。

筆者はStrutsについてはまるっきりの初心者なので、このエントリには間違いが含まれている可能性があります。何かお気づきの点などありましたら、ぜひ@kinyukaまでお知らせください。

Apache Struts2 の脆弱性に関するWAF「Scutum」の対応

Scutumでは、Apache Struts 2 の新たな脆弱性について随時対応を行っています。 Apache Struts2 の脆弱性について

【お知らせ】2018/08/23
2018年8月現在、Apache Struts2 には新たに深刻な脆弱性が見つかっています(S2-057、 CVE-2018-11776)。Scutumでは、既存の汎用的なStruts2脆弱性対策機能「OGNLインジェクション防御機能」により、公開された攻撃コードについて本脆弱性公開前から防御できていることを確認しております。