脆弱性を攻撃してみよう (1) OSコマンドインジェクション (OGNL式インジェクション)

はじめに

脆弱性を理解するには、実際に脆弱性を攻撃してみるのが一番です。

といっても、脆弱性のありそうなサイトを見つけて攻撃してみよう!と言っているわけではありません。自分だけが使っている仮想マシンの上で、脆弱性のあるアプリケーションを動かし、攻撃してみることをお勧めします。

脆弱性は、以前紹介したバグだらけのWebアプリケーションにたくさん実装してあります。Webアプリケーションはここからダウンロードして、次のコマンドで起動します。

java -jar easybuggy.jar

※実行するにはJavaが必要です。詳細については、こちらのページを参照して下さい。

このWebアプリケーションは、現時点で以下の脆弱性を実装しています。

  • XSS (クロスサイトスクリプティング)
  • SQLインジェクション
  • LDAPインジェクション
  • コードインジェクション
  • OSコマンドインジェクション
  • メールヘッダーインジェクション
  • Nullバイトインジェクション
  • サイズ制限の無いファイルアップロード
  • 拡張子制限の無いファイルアップロード
  • オープンリダイレクト可能なログイン画面
  • ブルートフォース攻撃可能なログイン画面
  • セッション固定攻撃可能なログイン画面
  • 親切過ぎる認証エラーメッセージ
  • 危険なファイルインクルード
  • パストラバーサル
  • 意図しないファイル公開
  • CSRF (クロスサイトリクエストフォージェリ)
  • クリックジャッキング
  • XEE (XMLエンティティ拡張)
  • XXE (XML外部エンティティ)

今回はこの中から「OSコマンドインジェクション」(より正確に言うと、「OGNL式インジェクション」)を攻撃して、サーバーに致命的なダメージを与えたいたいと思います:smiley:

OSコマンドインジェクションとは

OSコマンドインジェクションとは、Webアプリケーションなどを経由してサーバーのOSコマンドを実行する攻撃を指します。

oscmdinjct.png

例えば、Webアプリケーションが特定のリクエストパラメータを未検証のままOSのコマンドの引数に渡し、実行していたとします。一般のユーザーは通常の使い方しかしないので問題ないかもしれませんが、悪意のあるユーザーは「;」(セミコロン)などの区切り文字とともに「rm -fr /」のような文字列を付加したパラメータを送信して、サーバーのファイルを全て削除してしまうかもしれません。

攻撃してみよう

では、さっそくOSコマンドインジェクションの脆弱性を攻撃してみましょう。

Webアプリケーションをここからダウンロードして、java -jar easybuggy.jarで起動します。なお、今回はSELinuxを無効にしたCentOS 6に、コマンドの実行権限があるrootユーザーでログインしています。

※この記事では、バージョン1.3.2を使用しています。今後のリリースで動作は変わる可能性があります。

起動したら、http://localhost:8080にアクセスして下さい。以下のような画面の真ん中に「脆弱性」と書かれたセクションがあります。

main.png

この中の上から5番目に「OSコマンドインジェクション」のリンクがあるので、クリックします。

クリックすると、次のような画面が表示されます。

ognl1.png

数式を入力すると、その答えを表示するという単純な機能です。数式にはjava.lang.Mathを使用することができます。

まずは1 + 2Math.sqrt(5)などの簡単な数式を入力してみて下さい。正しい答えが返ってくるはずです。次にabcなどの数式として不正な値を入力して下さい。エラーメッセージが表示されます。

err1.png

この機能は一見正常に動作するように見えます。

しかし、画面下部のinfo.jpgに記載されているように@Runtime@getRuntime().exec('rm -fr /your-important-dir/')を入力し、計算ボタンをクリックすると、サーバー上のディレクトリ(/your-important-dir/)が削除されてしまいます。

※注意:/your-important-dir/は、サーバー上の削除してもいいディレクトリへの絶対パスにするか、削除する前にバックアップを取得して下さい。また、Windowsの場合は、rm -frコマンドの代わりにrmdir /s /qマンドを使って下さい。

では、実際にやってみます。対象のディレクトリは/etcにします。ll(ls -la)コマンドで現在の/配下のディレクトリ構成を確認すると、以下のようになっています。

$ ll /
合計 102
dr-xr-xr-x.   2 root root  4096  423 03:46 2017 bin
dr-xr-xr-x.   5 root root  5120  426 21:53 2017 boot
drwxr-xr-x.  20 root root  3820  61 22:57 2017 dev
drwxr-xr-x. 119 root root 12288  61 22:57 2017 etc
drwxr-xr-x.   4 root root  4096  42 00:41 2016 home
dr-xr-xr-x.  11 root root  4096  419 21:20 2017 lib
dr-xr-xr-x.  10 root root 12288  423 03:46 2017 lib64
drwx------.   2 root root 16384  88 01:21 2015 lost+found
drwxr-xr-x.   2 root root  4096  318 20:24 2017 media
drwxr-xr-x.   4 root root  4096 1115 18:00 2015 mnt
drwxr-xr-x.   8 root root  4096  41 21:49 2017 opt
dr-xr-xr-x. 277 root root     0  61 22:56 2017 proc
dr-xr-x---.  78 root root  4096  62 07:39 2017 root
dr-xr-xr-x.   2 root root 12288  420 03:27 2017 sbin
drwxr-xr-x.   7 root root     0  61 22:56 2017 selinux
drwxr-xr-x.   2 root root  4096  923 20:50 2011 srv
drwxr-xr-x   13 root root     0  61 22:56 2017 sys
drwxrwxrwx.   3 root root  4096  62 07:41 2017 tmp
drwxr-xr-x.  14 root root  4096  923 08:32 2016 usr
drwxr-xr-x.  23 root root  4096 105 17:12 2016 var

実行する前にバックアップを取っておきましょう。

$ cp -pr /etc etc_bk

@Runtime@getRuntime().exec('rm -fr /etc')を入力し、計算ボタンをクリックします。

rmetc1.png

そして、もう一度llコマンドを実行します。

$ ll /
合計 90
dr-xr-xr-x.   2 0 0  4096  422 18:46 2017 bin
dr-xr-xr-x.   5 0 0  5120  426 12:53 2017 boot
drwxr-xr-x.  20 0 0  3820  61 13:57 2017 dev
drwxr-xr-x.   4 0 0  4096  41 15:41 2016 home
dr-xr-xr-x.  11 0 0  4096  419 12:20 2017 lib
dr-xr-xr-x.  10 0 0 12288  422 18:46 2017 lib64
drwx------.   2 0 0 16384  87 16:21 2015 lost+found
drwxr-xr-x.   2 0 0  4096  318 11:24 2017 media
drwxr-xr-x.   4 0 0  4096 1115 09:00 2015 mnt
drwxr-xr-x.   8 0 0  4096  41 12:49 2017 opt
dr-xr-xr-x. 296 0 0     0  61 13:56 2017 proc
dr-xr-x---.  78 0 0  4096  69 14:09 2017 root
dr-xr-xr-x.   2 0 0 12288  419 18:27 2017 sbin
drwxr-xr-x.   7 0 0     0  61 13:56 2017 selinux
drwxr-xr-x.   2 0 0  4096  923 11:50 2011 srv
drwxr-xr-x   13 0 0     0  61 13:56 2017 sys
drwxrwxrwx.   8 0 0  4096  68 14:31 2017 tmp
drwxr-xr-x.  14 0 0  4096  922 23:32 2016 usr
drwxr-xr-x.  23 0 0  4096 105 08:12 2016 var

/etcというディレクトリは無くなりました。これがOSコマンドインジェクションです。ここでは/etcを削除しましたが、もちろん他のディレクトリも削除できますし、rmコマンド以外のコマンドも使えます。OSのコマンドを操れるので、いろいろな攻撃が考えられます。

攻撃者はどのように脆弱性を見つけるか

「とはいえ、問題を起こす文字列(@Runtime@...)を攻撃者は知ることができないのでは??」と思った方もいるかと思います。このアプリケーションでは、あえて脆弱性の攻撃方法を画面上に記載していますが、通常のアプリケーションには当然そのようなものはありません。攻撃者はどのように脆弱性を見つけるのでしょうか?

ヒントはエラーメッセージにあります。エラーメッセージは攻撃者にアプリケーションの実装を推測させる材料にもなりえます。

いろいろな種類の不正な入力値でエラーメッセージを表示させてみましょう。例えば、この画面でMath.sqrt(5x)と入力して計算ボタンをクリックしてみると、次のようなエラーが表示されます。

sqrt5x.png

「不正な数式です」の後に英語のメッセージが続いています。アプリケーションの開発経験がある人であれば、「不正な数式です」の後に、何らかのライブラリが返したエラーメッセージをそのまま連結しているのではないか、と推測するのではないでしょうか。

次は、Mathを完全修飾クラス名にして、java.lang.Math.sqrt(5)と入力し、計算ボタンをクリックしてみます。

javalangMathsqrt5.png

入力していないはずの「@」(アットマーク)が入力されたことを示すようなエラーメッセージが出力されました。「@」には何か意味がありそうです。

さらに、Math.を付けずにsqrt(5)と入力して計算ボタンをクリックしてみましょう。次のようなエラーが表示されます。

sqrt5.png

今度は、入力していない「ognl.OgnlContext」というキーワードが現れました。これを見て、知識のある人であれば、この機能を実現しているのが「OGNL」という技術であると推測できます。OGNLはJavaに似た文法の式言語のライブラリです。

OGNLの仕様を見てみると、以下のような記述があります。

Calling Static Methods
You can call a static method using the syntax @class@method(args).

静的なメソッド呼び出しは@class@method(args)で実現できるようです。

「ということは...アプリケーションの内部では、次のように入力値の中にあるMath.@Math@に置換して、OGNL式として実行しているだけかもしれない」という推測ができます。

Math.sqrt(5)@Math@sqrt(5)に置換 → OGNL式として実行

この推測が正しいか確認するために、@System@currentTimeMillis()を入力してみましょう。

@System@currentTimeMillis()@System@currentTimeMillis()のまま(置換されず) → OGNL式として実行

正しければ、1970年1月1日0時からの経過時刻をミリ秒で取得できるはずです。

sct.png

結果が示す通り、推測が正しい可能性が高いと判断できます。

ここまできたら、「@System@exit(0)」と入力してWebアプリケーションのプロセスをシャットダウンさせるか、それともその前に...:sunglasses:といろいろなことが想像できます。

どのような実装になっているか

実際にどのような実装になっているのでしょうか。ソースコードを見てみましょう。重要な部分は以下です。

try {
    Object expr = Ognl.parseExpression(expression.replaceAll("Math\\.", "@Math@"));
    value = Ognl.getValue(expr, ctx);
} catch (OgnlException e) {
    isValid = false;
    if (e.getReason() != null) {
        errMessage = e.getReason().getMessage();
    }

推測の通り、Math.@Math@に置換して、OGNL式として実行しているだけです。

ちなみに、今回実行したのはOSコマンドインジェクションですが、Javaのコードも実行できるため、「コードインジェクション」と言うこともできます。さらに、より詳細に分類すると「OGNL式インジェクション」とも言えます。

OGNL式インジェクションについて

OGNL式インジェクションの名前を広めたのは、Webアプリケーションのフレームワークとして有名な「Struts 2」です。今年の3月にも、OGNL式インジェクションの脆弱性がStruts 2に見つかり、複数のウェブサイトにおいて情報漏洩等の被害が発生しました。日本のサーバーにも、海外から大量の不正リクエストが送信されてきたという噂です。私もこの脆弱性は実際に試してみましたが(もちろんローカルのStrutsアプリで)、非常に簡単で危険性の高い攻撃ができるものでした。

Struts 2の脆弱性として報告されるもののうち、かなりの割合を占めるのがこの脆弱性です。Struts 2のいろいろな箇所でOGNL式が利用されており、各箇所でOGNL式インジェクションが起きないように正規表現を使って、式の妥当性チェックを行っています。しかし、脆弱性を対策した正規表現が公開されても、それをかいくぐるような新しい文字列での攻撃方法が見つかり、なかなか根本解決に至っていないのが現状です。

OSコマンド/コードインジェクションの対策

では、どのような対策をすればいいでしょうか?対策として思いつくのは、以下のようなことです。

  • そもそもそのような危険性を含む機能はつくらない(設計の段階で却下)
  • 別の実装方法を検討する
  • 実装を特定されるようなエラーメッセージをユーザーに返さない
  • 入力値の妥当性チェックを実施する
  • Webアプリケーションを適切なユーザー権限で実行する(Tomcatユーザー権限など)
  • OSの機能で保護する(Unix chroot jail、AppArmor、SELinuxなど)
  • JVMの機能で保護する(SecurityManager)

この中のいくつかを実際にやってみましょう。

適切なユーザー権限で実行する

rootユーザー以外でWebアプリケーションを実行していたら、どうなるでしょうか?tomcatユーザーに変更して、確認してみましょう。

$ su tomcat

Webアプリケーションを起動したら、@Runtime@getRuntime().exec('rm -fr /etc')と入力して、計算ボタンをクリックします。エラーは表示されませんでした。どうなったのでしょうか?

$ ll /
合計 102
dr-xr-xr-x.   2 root root  4096  423 03:46 2017 bin
dr-xr-xr-x.   5 root root  5120  426 21:53 2017 boot
drwxr-xr-x.  20 root root  3820  61 22:57 2017 dev
drwxr-xr-x. 119 root root 12288  61 22:57 2017 etc
drwxr-xr-x.   4 root root  4096  42 00:41 2016 home
dr-xr-xr-x.  11 root root  4096  419 21:20 2017 lib
dr-xr-xr-x.  10 root root 12288  423 03:46 2017 lib64
drwx------.   2 root root 16384  88 01:21 2015 lost+found
drwxr-xr-x.   2 root root  4096  318 20:24 2017 media
drwxr-xr-x.   4 root root  4096 1115 18:00 2015 mnt
drwxr-xr-x.   8 root root  4096  41 21:49 2017 opt
dr-xr-xr-x. 296 root root     0  61 22:56 2017 proc
dr-xr-x---.  78 root root  4096  69 23:09 2017 root
dr-xr-xr-x.   2 root root 12288  420 03:27 2017 sbin
drwxr-xr-x.   7 root root     0  61 22:56 2017 selinux
drwxr-xr-x.   2 root root  4096  923 20:50 2011 srv
drwxr-xr-x   13 root root     0  61 22:56 2017 sys
drwxrwxrwx.   8 root root  4096  69 23:11 2017 tmp
drwxr-xr-x.  14 root root  4096  923 08:32 2016 usr
drwxr-xr-x.  23 root root  4096 105 17:12 2016 var

この通り、/etcは削除されていません。とはいっても、Webアプリケーションを動かすユーザーが実行できるコマンド(rm -fr /home/tomcat/anydirなど)は、この攻撃でも実行できてしまいます。しかし、それだけでもかなり有効な対策であると言えます。

OSの機能で保護する(SELinux)

SELinuxを有効にしていたら、OSコマンドインジェクションを抑止してくれるのでしょうか?もう一度rootユーザーに変更して、SELinuxを有効にします。

$ vi /etc/selinux/config
# SELINUX=disabled
SELINUX=enforcing

変更したら、CentOSを再起動します。Webアプリケーションを起動したら、@Runtime@getRuntime().exec('rm -fr /etc')と入力して、計算ボタンをクリックします。

$ ll /
合計 90
dr-xr-xr-x.   2 0 0  4096  422 18:46 2017 bin
dr-xr-xr-x.   5 0 0  5120  426 12:53 2017 boot
drwxr-xr-x.  20 0 0  3820  61 13:57 2017 dev
drwxr-xr-x.   4 0 0  4096  41 15:41 2016 home
dr-xr-xr-x.  11 0 0  4096  419 12:20 2017 lib
dr-xr-xr-x.  10 0 0 12288  422 18:46 2017 lib64
drwx------.   2 0 0 16384  87 16:21 2015 lost+found
drwxr-xr-x.   2 0 0  4096  318 11:24 2017 media
drwxr-xr-x.   4 0 0  4096 1115 09:00 2015 mnt
drwxr-xr-x.   8 0 0  4096  41 12:49 2017 opt
dr-xr-xr-x. 296 0 0     0  61 13:56 2017 proc
dr-xr-x---.  78 0 0  4096  69 14:09 2017 root
dr-xr-xr-x.   2 0 0 12288  419 18:27 2017 sbin
drwxr-xr-x.   7 0 0     0  61 13:56 2017 selinux
drwxr-xr-x.   2 0 0  4096  923 11:50 2011 srv
drwxr-xr-x   13 0 0     0  61 13:56 2017 sys
drwxrwxrwx.   8 0 0  4096  68 14:31 2017 tmp
drwxr-xr-x.  14 0 0  4096  922 23:32 2016 usr
drwxr-xr-x.  23 0 0  4096 105 08:12 2016 var

消えてしまいました...デフォルトでSELinuxが保護するディレクトリ(例えば、/selinux)以外は、保護してくれないようです。少なくともデフォルト設定のSELinuxを有効にしたところで、OSコマンドインジェクションに効果は無いと言えそうです。

JVMの機能で保護する(SecurityManager)

Javaには悪意のあるユーザーからのJVMの操作を保護する「SecurityManager」という機能があります。これを有効にしていたら、どうなるのでしょうか?

SecurityManagerを有効にするには、easybuggy.jar起動時のオプションに以下を付加します。

-Djava.security.manager -Djava.security.policy=catalina.policy

mvnコマンドでEasyBuggyを起動する場合は、pom.xmlの以下の行のコメントを解除して下さい。
<!-- <argument>-Djava.security.manager</argument>
<argument>-Djava.security.policy=catalina.policy</argument> -->

catalina.policyここからダウンロードして下さい。このファイルは、デフォルトではOSコマンドインジェクションを防ぐようになっていませんが、以下のように修正を行うことでそれを抑止できます。

・ Linux (CentOS 6.3)の場合

grant {
    // 全てのファイルへの全ての操作を許可せず、必要なものだけ許可する
    // permission java.io.FilePermission "<<ALL FILES>>", "write, read, execute, delete";
    permission java.io.FilePermission "-", "write, read, execute, delete";
    permission java.io.FilePermission "/usr/java/-", "write, read, execute";
    permission java.io.FilePermission "/usr/lib/jvm/java-1.7.0-openjdk-1.7.0.79.x86_64/-", "write, read, execute";
  ・・・

Windowsの場合

grant {
    // 全てのファイルへの全ての操作を許可せず、必要なものだけ許可する
    // permission java.io.FilePermission "<<ALL FILES>>", "write, read, execute, delete";
    permission java.io.FilePermission "-", "write, read, execute, delete";
    permission java.io.FilePermission "C:/Program Files/Java/-", "write, read, execute";
    permission java.io.FilePermission "C:/Windows/Sun/Java/-", "write, read, execute";
  ・・・

(※設定は、環境により異なります)

変更が完了したら、SecurityManagerを有効にするオプションを付加してWebアプリケーションを起動します。そして、@Runtime@getRuntime().exec('rm -fr /etc')と入力して、計算ボタンをクリックします。

smrmetc.png

SecurityManagerにより、OSコマンドインジェクションは失敗しました。

$ ll /
合計 102
dr-xr-xr-x.   2 root root  4096  423 03:46 2017 bin
dr-xr-xr-x.   5 root root  5120  426 21:53 2017 boot
drwxr-xr-x.  20 root root  3820  61 22:57 2017 dev
drwxr-xr-x. 119 root root 12288  61 22:57 2017 etc
drwxr-xr-x.   4 root root  4096  42 00:41 2016 home
dr-xr-xr-x.  11 root root  4096  419 21:20 2017 lib
dr-xr-xr-x.  10 root root 12288  423 03:46 2017 lib64
drwx------.   2 root root 16384  88 01:21 2015 lost+found
drwxr-xr-x.   2 root root  4096  318 20:24 2017 media
drwxr-xr-x.   4 root root  4096 1115 18:00 2015 mnt
drwxr-xr-x.   8 root root  4096  41 21:49 2017 opt
dr-xr-xr-x. 296 root root     0  61 22:56 2017 proc
dr-xr-x---.  78 root root  4096  69 23:09 2017 root
dr-xr-xr-x.   2 root root 12288  420 03:27 2017 sbin
drwxr-xr-x.   7 root root     0  61 22:56 2017 selinux
drwxr-xr-x.   2 root root  4096  923 20:50 2011 srv
drwxr-xr-x   13 root root     0  61 22:56 2017 sys
drwxrwxrwx.   8 root root  4096  69 23:11 2017 tmp
drwxr-xr-x.  14 root root  4096  923 08:32 2016 usr
drwxr-xr-x.  23 root root  4096 105 17:12 2016 var

さらにcatalina.policyの次の行をコメントアウトすると、System.exit(n)も拒否できます。

permission java.lang.RuntimePermission "exitVM";

コメントアウトして、EasyBuggyを再起動したら、@System@exit(0)と入力して、計算ボタンをクリックします。

exitvm.png

このようにJVMのシャットダウンも防ぐことができます。SecurityManagerは、設定が直感的に分かりやすいわけではないので、しっかり理解して使用しなければなりませんが、OSコマンドインジェクションやコードインジェクションには効果的な対策と言えます。

参考