はじめに
脆弱性を理解するには、実際に脆弱性を攻撃してみるのが一番です。
といっても、脆弱性のありそうなサイトを見つけて攻撃してみよう!と言っているわけではありません。自分だけが使っている仮想マシンの上で、脆弱性のあるアプリケーションを動かし、攻撃してみることをお勧めします。
脆弱性は、以前紹介したバグだらけのWebアプリケーションにたくさん実装してあります。Webアプリケーションはここからダウンロードして、次のコマンドで起動します。
java -jar easybuggy.jar
※実行するにはJavaが必要です。詳細については、こちらのページを参照して下さい。
このWebアプリケーションは、現時点で以下の脆弱性を実装しています。
- XSS (クロスサイトスクリプティング)
- SQLインジェクション
- LDAPインジェクション
- コードインジェクション
- OSコマンドインジェクション
- メールヘッダーインジェクション
- Nullバイトインジェクション
- サイズ制限の無いファイルアップロード
- 拡張子制限の無いファイルアップロード
- オープンリダイレクト可能なログイン画面
- ブルートフォース攻撃可能なログイン画面
- セッション固定攻撃可能なログイン画面
- 親切過ぎる認証エラーメッセージ
- 危険なファイルインクルード
- パストラバーサル
- 意図しないファイル公開
- CSRF (クロスサイトリクエストフォージェリ)
- クリックジャッキング
- XEE (XMLエンティティ拡張)
- XXE (XML外部エンティティ)
今回はこの中から**「OSコマンドインジェクション」**(より正確に言うと、「OGNL式インジェクション」)を攻撃して、サーバーに致命的なダメージを与えたいたいと思います
OSコマンドインジェクションとは
OSコマンドインジェクションとは、Webアプリケーションなどを経由してサーバーのOSコマンドを実行する攻撃を指します。
例えば、Webアプリケーションが特定のリクエストパラメータを未検証のままOSのコマンドの引数に渡し、実行していたとします。一般のユーザーは通常の使い方しかしないので問題ないかもしれませんが、悪意のあるユーザーは「;
」(セミコロン)などの区切り文字とともに「rm -fr /
」のような文字列を付加したパラメータを送信して、サーバーのファイルを全て削除してしまうかもしれません。
攻撃してみよう
では、さっそくOSコマンドインジェクションの脆弱性を攻撃してみましょう。
Webアプリケーションをここからダウンロードして、java -jar easybuggy.jar
で起動します。なお、今回はSELinuxを無効にしたCentOS 6に、コマンドの実行権限があるroot
ユーザーでログインしています。
この記事では、バージョン1.3.2を使用しています。今後のリリースで動作は変わる可能性があります。
起動したら、http://localhost:8080にアクセスして下さい。以下のような画面の真ん中に「脆弱性」と書かれたセクションがあります。
この中の上から5番目に「OSコマンドインジェクション」のリンクがあるので、クリックします。
クリックすると、次のような画面が表示されます。
数式を入力すると、その答えを表示するという単純な機能です。数式にはjava.lang.Math
を使用することができます。
まずは1 + 2
やMath.sqrt(5)
などの簡単な数式を入力してみて下さい。正しい答えが返ってくるはずです。次にabc
などの数式として不正な値を入力して下さい。エラーメッセージが表示されます。
この機能は一見正常に動作するように見えます。
しかし、画面下部のに記載されているように@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 4月 23 03:46 2017 bin
dr-xr-xr-x. 5 root root 5120 4月 26 21:53 2017 boot
drwxr-xr-x. 20 root root 3820 6月 1 22:57 2017 dev
drwxr-xr-x. 119 root root 12288 6月 1 22:57 2017 etc
drwxr-xr-x. 4 root root 4096 4月 2 00:41 2016 home
dr-xr-xr-x. 11 root root 4096 4月 19 21:20 2017 lib
dr-xr-xr-x. 10 root root 12288 4月 23 03:46 2017 lib64
drwx------. 2 root root 16384 8月 8 01:21 2015 lost+found
drwxr-xr-x. 2 root root 4096 3月 18 20:24 2017 media
drwxr-xr-x. 4 root root 4096 11月 15 18:00 2015 mnt
drwxr-xr-x. 8 root root 4096 4月 1 21:49 2017 opt
dr-xr-xr-x. 277 root root 0 6月 1 22:56 2017 proc
dr-xr-x---. 78 root root 4096 6月 2 07:39 2017 root
dr-xr-xr-x. 2 root root 12288 4月 20 03:27 2017 sbin
drwxr-xr-x. 7 root root 0 6月 1 22:56 2017 selinux
drwxr-xr-x. 2 root root 4096 9月 23 20:50 2011 srv
drwxr-xr-x 13 root root 0 6月 1 22:56 2017 sys
drwxrwxrwx. 3 root root 4096 6月 2 07:41 2017 tmp
drwxr-xr-x. 14 root root 4096 9月 23 08:32 2016 usr
drwxr-xr-x. 23 root root 4096 10月 5 17:12 2016 var
実行する前にバックアップを取っておきましょう。
$ cp -pr /etc etc_bk
@Runtime@getRuntime().exec('rm -fr /etc')
を入力し、計算ボタンをクリックします。
そして、もう一度ll
コマンドを実行します。
$ ll /
合計 90
dr-xr-xr-x. 2 0 0 4096 4月 22 18:46 2017 bin
dr-xr-xr-x. 5 0 0 5120 4月 26 12:53 2017 boot
drwxr-xr-x. 20 0 0 3820 6月 1 13:57 2017 dev
drwxr-xr-x. 4 0 0 4096 4月 1 15:41 2016 home
dr-xr-xr-x. 11 0 0 4096 4月 19 12:20 2017 lib
dr-xr-xr-x. 10 0 0 12288 4月 22 18:46 2017 lib64
drwx------. 2 0 0 16384 8月 7 16:21 2015 lost+found
drwxr-xr-x. 2 0 0 4096 3月 18 11:24 2017 media
drwxr-xr-x. 4 0 0 4096 11月 15 09:00 2015 mnt
drwxr-xr-x. 8 0 0 4096 4月 1 12:49 2017 opt
dr-xr-xr-x. 296 0 0 0 6月 1 13:56 2017 proc
dr-xr-x---. 78 0 0 4096 6月 9 14:09 2017 root
dr-xr-xr-x. 2 0 0 12288 4月 19 18:27 2017 sbin
drwxr-xr-x. 7 0 0 0 6月 1 13:56 2017 selinux
drwxr-xr-x. 2 0 0 4096 9月 23 11:50 2011 srv
drwxr-xr-x 13 0 0 0 6月 1 13:56 2017 sys
drwxrwxrwx. 8 0 0 4096 6月 8 14:31 2017 tmp
drwxr-xr-x. 14 0 0 4096 9月 22 23:32 2016 usr
drwxr-xr-x. 23 0 0 4096 10月 5 08:12 2016 var
/etc
というディレクトリは無くなりました。これがOSコマンドインジェクションです。ここでは/etc
を削除しましたが、もちろん他のディレクトリも削除できますし、rm
コマンド以外のコマンドも使えます。OSのコマンドを操れるので、いろいろな攻撃が考えられます。
攻撃者はどのように脆弱性を見つけるか
「とはいえ、問題を起こす文字列(@Runtime@...
)を攻撃者は知ることができないのでは??」と思った方もいるかと思います。このアプリケーションでは、あえて脆弱性の攻撃方法を画面上に記載していますが、通常のアプリケーションには当然そのようなものはありません。攻撃者はどのように脆弱性を見つけるのでしょうか?
**ヒントはエラーメッセージにあります。**エラーメッセージは攻撃者にアプリケーションの実装を推測させる材料にもなりえます。
いろいろな種類の不正な入力値でエラーメッセージを表示させてみましょう。例えば、この画面でMath.sqrt(5x)
と入力して計算ボタンをクリックしてみると、次のようなエラーが表示されます。
「不正な数式です」の後に英語のメッセージが続いています。アプリケーションの開発経験がある人であれば、「不正な数式です」の後に、何らかのライブラリが返したエラーメッセージをそのまま連結しているのではないか、と推測するのではないでしょうか。
次は、Math
を完全修飾クラス名にして、java.lang.Math.sqrt(5)
と入力し、計算ボタンをクリックしてみます。
入力していないはずの「@
」(アットマーク)が入力されたことを示すようなエラーメッセージが出力されました。「@
」には何か意味がありそうです。
さらに、Math.
を付けずにsqrt(5)
と入力して計算ボタンをクリックしてみましょう。次のようなエラーが表示されます。
今度は、入力していない「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時からの経過時刻をミリ秒で取得できるはずです。
結果が示す通り、推測が正しい可能性が高いと判断できます。
ここまできたら、「@System@exit(0)
」と入力してWebアプリケーションのプロセスをシャットダウンさせるか、それともその前に...といろいろなことが想像できます。
どのような実装になっているか
実際にどのような実装になっているのでしょうか。ソースコードを見てみましょう。重要な部分は以下です。
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 4月 23 03:46 2017 bin
dr-xr-xr-x. 5 root root 5120 4月 26 21:53 2017 boot
drwxr-xr-x. 20 root root 3820 6月 1 22:57 2017 dev
drwxr-xr-x. 119 root root 12288 6月 1 22:57 2017 etc
drwxr-xr-x. 4 root root 4096 4月 2 00:41 2016 home
dr-xr-xr-x. 11 root root 4096 4月 19 21:20 2017 lib
dr-xr-xr-x. 10 root root 12288 4月 23 03:46 2017 lib64
drwx------. 2 root root 16384 8月 8 01:21 2015 lost+found
drwxr-xr-x. 2 root root 4096 3月 18 20:24 2017 media
drwxr-xr-x. 4 root root 4096 11月 15 18:00 2015 mnt
drwxr-xr-x. 8 root root 4096 4月 1 21:49 2017 opt
dr-xr-xr-x. 296 root root 0 6月 1 22:56 2017 proc
dr-xr-x---. 78 root root 4096 6月 9 23:09 2017 root
dr-xr-xr-x. 2 root root 12288 4月 20 03:27 2017 sbin
drwxr-xr-x. 7 root root 0 6月 1 22:56 2017 selinux
drwxr-xr-x. 2 root root 4096 9月 23 20:50 2011 srv
drwxr-xr-x 13 root root 0 6月 1 22:56 2017 sys
drwxrwxrwx. 8 root root 4096 6月 9 23:11 2017 tmp
drwxr-xr-x. 14 root root 4096 9月 23 08:32 2016 usr
drwxr-xr-x. 23 root root 4096 10月 5 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 4月 22 18:46 2017 bin
dr-xr-xr-x. 5 0 0 5120 4月 26 12:53 2017 boot
drwxr-xr-x. 20 0 0 3820 6月 1 13:57 2017 dev
drwxr-xr-x. 4 0 0 4096 4月 1 15:41 2016 home
dr-xr-xr-x. 11 0 0 4096 4月 19 12:20 2017 lib
dr-xr-xr-x. 10 0 0 12288 4月 22 18:46 2017 lib64
drwx------. 2 0 0 16384 8月 7 16:21 2015 lost+found
drwxr-xr-x. 2 0 0 4096 3月 18 11:24 2017 media
drwxr-xr-x. 4 0 0 4096 11月 15 09:00 2015 mnt
drwxr-xr-x. 8 0 0 4096 4月 1 12:49 2017 opt
dr-xr-xr-x. 296 0 0 0 6月 1 13:56 2017 proc
dr-xr-x---. 78 0 0 4096 6月 9 14:09 2017 root
dr-xr-xr-x. 2 0 0 12288 4月 19 18:27 2017 sbin
drwxr-xr-x. 7 0 0 0 6月 1 13:56 2017 selinux
drwxr-xr-x. 2 0 0 4096 9月 23 11:50 2011 srv
drwxr-xr-x 13 0 0 0 6月 1 13:56 2017 sys
drwxrwxrwx. 8 0 0 4096 6月 8 14:31 2017 tmp
drwxr-xr-x. 14 0 0 4096 9月 22 23:32 2016 usr
drwxr-xr-x. 23 0 0 4096 10月 5 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の以下の行のコメントを解除して下さい。
|