技術者ブログ

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

2017年3月

      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

Struts2が危険である理由

はじめに

2017年3月、Struts2にまたしても新たな脆弱性(S2-045、S2-046)が見つかり、複数のウェブサイトにおいて情報漏洩等の被害が発生しました。筆者は2014年4月(およそ3年前)に「例えば、Strutsを避ける」という記事を書きましたが、今読み返してみると「やや調査不足の状態で書いてしまったな」と感じる点もあります。今回、良いタイミングなのでもう一度Struts2のセキュリティについてざっとまとめてみたいと思います。

なぜJavaなのにリモートからの任意のコード実行(いわゆるRCE)が可能なのか

Struts2はJavaアプリケーションであり、Java製のアプリケーションサーバ上で動作します。Javaはいわゆるコンパイル型の言語であるため、通常はランタイムにおいて任意のコードを実行することはできず、RCEは難しいはずです。

JavaのウェブアプリケーションでRCEが成立するパターンとしては以下のようなものが考えられます。Strutsで主に被害を出しているのは下記のうち、3と4です。

1. 外部システム(いわゆるコマンドライン)を呼び出している箇所にユーザ入力が使用されるパターン

これはJavaに限らず昔から知られているもので、いわゆるsystem関数的なものを使っている場合に発生します。最近は減っていると思いますがsendmailコマンドを実行したり、image magickでの画像変換などを行う際に発生することが考えられます。

2. デシリアライズ

Javaのオブジェクトをリモートに送る際、バイナリのデータに変換してネットワーク上を送信し、送信先でもう一度オブジェクトを再構築することがありますが、このとき再構築対象となるオブジェクトの再構築処理の実装次第ではコード実行が行えてしまうことがあります。

3. クラスローダを操作できてしまうパターン

Struts1およびStruts2、Springで過去に発生したことがあるパターンです。HTTPリクエストに含まれるパラメータ部分の処理において、アプリケーションサーバ上でJavaオブジェクトのgetterやsetterが実行されます。本来想定しているであろうActionクラス的なもの以外についても見境無くgetterやsetterを実行してしまう場合、JavaではすべてのクラスはObjectクラスを継承しているため、Objectクラスが持つgetClass()メソッドにアクセスしてClassオブジェクトに辿り着き、さらにそこからgetClassLoader()を実行することでクラスローダに辿り着いてしまうことがあります。

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

となるわけです。

クラスローダにアクセスしても必ずしもRCEが成立するわけではなく、さらに工夫が必要になります。過去に実際に見られたPoCの手法としては下記の2つがあります。

3.1 クラスローダを操作してアクセスログをJSPとして解釈させるパターン

Tomcatのクラスローダで使われた手法です。攻撃者が対象のウェブサイトにアクセスするとアクセスログに記録が残りますが、これはある意味で「任意の文字列をサーバ上に記述することができる」と考えることもできます。通常はログファイルは単なるテキストファイルですが、これをブラウザで開くことでXSSが発生したり、DBで処理する場合にSQLインジェクションが発生することもあり得ます。

Tomcatのクラスローダは必要以上に多機能であり、いくつかのsetterを呼び出して巧妙に操作することによって、ログファイルをJSPとして処理させることが可能です。攻撃者はJavaのコードを含むアクセスを行うことでアクセスログ上に任意のコードを記述し、それをさらに別のアクセスによってJSPとしてTomcatに処理させることでRCEが成立しました。非常にスマートな攻撃手法と言えるでしょう。

3.2 リモートのjarファイルをロードさせるパターン

Springへの攻撃で使われた手法です。クラスローダがURLClassLoaderクラスのインスタンスである場合、getURLs()の戻り値が配列であることを利用して情報の書き換えを行い、HTTP経由で攻撃者のウェブサーバからjarファイルを送り込んでロードさせるという手法です。PHPのRFIに少し似ているかもしれません。

4. OGNLインジェクション

Struts2で大きく被害を出しているのがこのOGNLインジェクションです。OGNLはObject Graph Navigation Libraryの略で、Struts2においてはJavaの世界とHTTPの世界を結びつけるための役割(上記で解説したようにgetterやsetterを呼び出してHTTPパラメータをJavaオブジェクトに設定する)と、JSP内でのスマートな記述を可能にする役割などを担っています。つまり入力・出力の両方で活用されています。

OGNLは非常にパワフルで、ほぼJavaのコードそのもののような記述も可能です。例えばStrutsのサイトで紹介されているPoC(https://struts.apache.org/docs/s2-013.html)では

%{(#_memberAccess['allowStaticMethodAccess']=true)(#context['xwork.MethodAccessor.denyMethodExecution']=false)(#writer=@org.apache.struts2.ServletActionContext@getResponse().getWriter(),#writer.println('hacked'),#writer.close())}

となっています。上記はOGNLですが、見ていただければ明らかなようにほぼJavaのコードがそのまま実行できてしまっており、サーブレットのレスポンスを操作して「hacked」という文字列を出力しています。つまり攻撃者が任意のOGNLをインジェクトできるならば、それはすなわちJavaのコードを実行できるということであり、RCEが成立します。

5. JSPがアップロードできてしまうパターン

コンパイル型言語のJavaですが、JSPはPHPのように書き換えたらすぐにそれが反映されるように作られています。JSPはテキストファイルですが、TomcatなどのアプリケーションサーバがJavaに書き換えてコンパイルを行い、裏では通常のJavaのクラスに変換されています。JSP内には任意のJavaのコードを記述することができるので、仮に攻撃者が任意のJSPをアップロードしてからアクセス(実行)できてしまうのであれば、それはすなわちRCEが成立することになります。

Struts2が目立って大きな被害を出している理由その1

先に書いたようにこれまでStruts2は前項の攻撃パターンのうち3(クラスローダへのアクセス)と4(OGNLインジェクション)を経験していますが、特に4については繰り返し何度も発生させています。

OGNLインジェクションはXSSやSQLインジェクションと同じく「渡してはいけない文字列を渡してしまっている」というもので、ある意味攻撃の原理そのものは非常にわかりやすいのですが、それは防ぐのが簡単であるということにはなりません。その理由はStruts2がそもそもOGNLを中心に作成されており、多くの箇所でOGNLが呼び出されているからです。OGNLとStruts2は切り離すことは到底考えられないほど密接な関係を持っており、先述したようにHTTPリクエストの処理(入力)とHTMLのレンダリング(出力)の両方で使われています。また設定ファイルの柔軟な記述を可能としているのもOGNLの機能ですし、S2-045の原因ともなった多言語対応の場面でも使われています。

ユーザ入力がOGNLに到達してしまう可能性があらゆるところに潜んでいるため、根本的な対策を行うことは非常に難しいというのが私の感覚です。

私は過去記事において、Struts2が繰り返しブラックリスト(禁止ワード)を更新している点を踏まえ「付け焼き刃的な対策を繰り返している」と批判しました。しかしその後いくつかの脆弱性対応では単純なブラックリストではない、効果が見込めそうな方法を実装していると思われるものもあります。例えばS2-037の修正では文字列マッチングによる対策ではなく、入力されたOGNLが最終的に構造が正しい(単純な関数呼び出しである)かどうかを調べています。この方法は非常に良いものであり、Scutum開発チーム内では「これでしばらくStruts2のRCEは落ち着くかもしれないですね・・・」といった見方をしていました。

しかしS2-045では、これまで対策を行っていた入力パラメータに対するチェックや関数呼び出しのチェックなどとは全く関係のない経路に、OGNL式として扱われる文字列にユーザ入力が入り込んでしまう部分があり、そこでRCEが行われました。 今回とりあえず該当箇所は(当然ですが)修正されふさがれましたが、他にまだ誰も気づいていない意外な箇所でOGNLへの入り口が開いているかもしれません。また、今後追加されるコードもOGNLを活用していくことが当然あり得るでしょうから、いつの時点でまたRCEが可能となる脆弱性が混入してもまったく不思議ではありません。このような理由から、もはやStruts2が「安心できる状態」になることは今後ないと考えます。

OGNLはStruts2が活用しているレベルに比べて少しパワフル過ぎると感じます。上記のPoCのような「ほぼJavaのコードをそのまま記述できる」という機能はStruts2でそこまで必要とされているようには思えません。OGNLがもう少しだけ制限されたものであったら、ここまで多くの被害には繋がらなかったでしょう。

Struts2が目立って大きな被害を出している理由その2

脆弱性が見つかった際の修正方法について、前項ではS2-037での修正を例に「少し良い対策を行うこともある」と褒めましたが、基本的にはStruts2の脆弱性の修正はダメなケースが多いです。過去の記事において私が批判した通りで「脆弱性を直すセンスがない」「脆弱性に対する認識が甘い」という傾向が3年経った今でも相変わらず続いています。

最近5年のRCEだけに絞って見ていっても、S2-013の修正がまずくS2-014となりましたし、S2-015とS2-016、そしてS2-020とS2-021も同じです。

またS2-032はS2-033だけでなくS2-037まで行きました。そのために「さすがにひどいな」と感じて前述したような場当たり的ではない対策を行ったのかもしれません。

さらにS2-045は修正自体はとりあえず大丈夫でしたが「問題はContent-Typeで発生する」として「別のパーサ実装(jakarta-stream)にすれば大丈夫」とアナウンスされましたが、実際にはアップロード時のファイル名(Content-Disposition)」でも発生しjakarta-streamも影響を受けるため、S2-046をアナウンスする形になりました。このようにRCEが見つかっても修正しきれていない事や内容を理解できていない(しようとしていない?)場合が多いです。

当然攻撃者は「見つかった脆弱性と似たような経路」での攻撃を行うことを狙いますし、実際にこれがゼロデイ攻撃に繋がっていると考えることができます。Struts2では開発者よりも攻撃者の方が、きちんと脆弱性を評価し研究してしまっている状態かもしれません。

Struts2が目立って大きな被害を出している理由その3

前項まではStruts2のソフトウェアを中心に視点を置き理由を説明しましたが、他に社会的側面からもStruts2の被害が拡大している理由があると考えています。

  • これまでに多くのRCEを提供してきた実績があり、すでに完全に攻撃者が目を付ける侵入経路となっている
  • Struts2は(PHP等と比較して)それなりに価値があるシステム、ウェブサイトで使われているケースが多い
  • 中国でのStruts2の利用が多いため、中国の攻撃者も好んで攻撃対象とする
  • URLから、Struts2を利用していることを外部から推測しやすい
  • 特に日本国内においてはやや慎重なベンダーがシステムを運用しているケースが多く、バージョンアップが素早く行われない
  • 後述するようにリリースの方法に問題がある

特にこの数年は上記のような理由がStruts2の危険性を加速させているように感じます。

既にStruts2を利用してしまっているサイトはどうするべきか?

既にStruts2を利用しているサイトもあるかと思います。この場合、今後どのようにStruts2の脆弱性と向き合っていくべきでしょうか。 「新しいバージョンが出たらすぐにバージョンアップしましょう」というのは既に定説となっているかと思いますが、私たちがScutumの運用・監視を通じて感じている感触としては、「もはやStrutsのアップデートのアナウンスが出てからバージョンアップしても間に合わないかもしれない」という気がします。

Struts2はオープンソースであり、リリース前の投票が公開されたメーリングリスト上で行われています。この投票はおそらく24時間程度の時間を要するため、攻撃者は正式なリリースのアナウンスが出る前に、次にリリースされるであろうバージョンを入手することができます。また、若干わかりにくくやってはいるようですが、脆弱性の修正もバージョン管理システムできちんと管理されているため、投票中のタイミングでリリースノートやソースコードの差分を解析すれば、攻撃者はそれほど苦労することなくRCEのPoCを完成させることができると考えられます。

攻撃者はおそらくあらかじめ「Struts2を使っていて、盗むに値する情報を持っていそう」なウェブサイトをリストアップしているので、PoCが完成したら即座に撃ち込んでくるでしょう。Shellshockの頃から「脆弱性が公表されてから実際の攻撃が来るまでのインターバルが非常に短くなっている」と感じていましたが、最近のStruts2での早さは本当に驚くべきレベルに達しています。

Struts2の管理者が朝出社して「おっ、新しいバージョンがリリースされたな・・・」と認識したときには既に遅い、ということになる可能性が高まっていると思います。攻撃者は数時間というスパンで行動している可能性があります。

このような事を考えると、現実的にはまずWAFを入れておくのは非常に良い対策だと思います。とはいえ正直に書いてしまうと、Scutumはこれまで必ずしも100%、ゼロデイレベルでStruts2に対する攻撃を止めることができていたということではありません。

例えばS2-045については止めることが出来ていました(ゼロデイでも防ぐことが出来ていました)が、S2-046についてはギリギリ、わずか数時間の差で先にScutum側の防御機能のアップデートが間に合いました。これを受けて今後、より力を入れてStruts2のOGNLインジェクションに特化したゼロデイを想定した防御機能を開発する予定ですが、それはさておき一般的なサーバ管理者がStruts2をアップデートするよりは早めに防御できていると思います。個人的に、WAFは自信を持っておすすめできるStruts2における対策の一つです。

また、攻撃者と同じようにStruts2のメーリングリストを常に見ておくのも非常に良いでしょう。リリースが出る前に投票段階で導入してしまったり、あるいは数日ウェブサイトを停止して様子を見るという運用も有効かもしれません。

ホスト型IDSを使って不審なファイルがホスト上に登場するのを監視したり、攻撃者が良く使うwgetやcurlのコマンドなどの挙動を監視するなども良いかもしれません。もちろんStruts2の利用を停止することができるならばそれも選択肢となります。いずれにせよStruts2を使っている場合は「いつやられてもおかしくない」と意識しておく必要があります。

Struts1について

Struts1でもクラスローダにアクセスできてしまう脆弱性が過去に見つかりましたが、基本的にOGNLを使っていないので、Struts2に比べれば遙かに安全であると言ってよいと思います。

まとめ

今回はStruts2の危険性について簡単にまとめてみました。パワーがありすぎるOGNLと密接に結びついてしまっているという構造上、Struts2には常にRCEが付きまとってしまうようです。

本記事について、文そのものは金床(@kinyuka)が書いていますが、内容の多くは同じScutumチームのエンジニアでありS2-037の発見者でもある野村(彼はS2-046についてもリリース前に見つけていました)の知見によるものです。この場をお借りしてお礼申し上げます。

また、そこそこ調べてはみましたがStrutsについては私はまだまだ素人であり、上記内容に間違いが含まれている可能性があります。もしお気づきになられた点がありましたらお気軽に@kinyukaまでお知らせいただければ幸いです。

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

Scutumでは、Apache Struts 2 の新たな脆弱性について随時対応を行っています。 Apache Struts2 の脆弱性について 【お知らせ】2017/03/09
2017年3月現在、Apache Struts2 には新たに深刻な脆弱性が見つかっています(CVE-2017-5638、S2-045)。Scutumでは、2017年3月8日の段階で公開されていた攻撃コード、およびそれ以外の攻撃パターンに関しても検証を行い、Scutumを経由した通信については本脆弱性の影響を受けないことを確認済みです。

【お知らせ】2017/03/21
上記に関連して追加報告されたApache Struts2の脆弱性(S2-046)についても、WAF「Scutum」で防御できる状態に更新しました。