SELinuxポリシー修正によるTomcat実行環境の防御力アップ

Apache Tomcat,Security-Enhanced Linux

こんにちは、プラットフォーム技術部の佐藤です。

以前『【CVE-2017-5638 / S2-045】SELinuxポリシーのバグフィックスでTomcatアプリのRCEは防げるようになったのか』で、記事を書かせていただきました。

そのときは「リモートコード実行(RCE)を防げないかなー、防ぎたいなー」という目的に対してはあまり期待した結果には至らなかったですが、それでも SELinux 無効状態よりは防御力はあがる、ということを確認できたと思っています。

今回は SELinux の tomcat 用デフォルトポリシーをカストマイズして、更なる防御力アップを目指したいと思います。

今回の目標

RCE 脆弱性を突いて悪用されそうなものは /sbin, /usr/sbin, /bin, /usr/bin に配備されているコマンド群です。これらのパスに含まれるコマンドの多くは、タイプが shell_exec_t や bin_t となっています。これらの実行権限を tomcat から剥奪することで RCE 悪用被害を軽減します。 実は tomcat 自体を起動するための処理に必要なコマンドもあるので、単純に剥奪しただけでは宜しくないのですが、それらへの対処についても実施していきます。

tomcat ポリシーの修正版作成

最初に selinux-policy-targeted のソースから tomcat ポリシーのソースを取り出して変更していきます。

SELinux ポリシーのソース入手

ソースパッケージのダウンロードとビルドに必要なパッケージをインストールします。

$ sudo yum install yum-utils rpm-build -y

selinux-policy-targeted のソースパッケージをダウンロードします。

$ yumdownloader --source selinux-policy-targeted
$ ls -1 selinux-policy-*
selinux-policy-3.13.1-192.el7_5.4.src.rpm

selinux-policy のビルドに必要な依存パッケージをインストールします。

$ sudo yum-builddep selinux-policy-3.13.1-192.el7_5.4.src.rpm -y

selinux-policy ソースパッケージをインストールします。~rpmbuild 以下に展開されます。

$ rpm -Uvh selinux-policy-3.13.1-192.el7_5.4.src.rpm

rpmbuild のパッチ適用まで実施します。

$ rpmbuild -bp rpmbuild/SPECS/selinux-policy.spec

これで tomcat ポリシーモジュールのソースが出来上がりました。

$ ls -1 rpmbuild/BUILD/serefpolicy-3.13.1/policy/modules/contrib/tomcat.*
rpmbuild/BUILD/serefpolicy-3.13.1/policy/modules/contrib/tomcat.fc
rpmbuild/BUILD/serefpolicy-3.13.1/policy/modules/contrib/tomcat.if
rpmbuild/BUILD/serefpolicy-3.13.1/policy/modules/contrib/tomcat.te

こちらのソースをベースに新ポリシーモジュールを作成していきます。

tomcat ポリシーから shell_exec_t と bin_t の実行権限を剥奪

先ほどの tomcat ポリシーモジュールのソースをコピーします。

cp rpmbuild/BUILD/serefpolicy-3.13.1/policy/modules/contrib/tomcat.* .

勇んで修正しようと思いましたが tomcat.te を確認しても bin_t や shell_exec_t を直接許可する定義はありません。

$ grep shell_exec_t tomcat.te
$ grep bin_t tomcat.te

いろいろ調べた結果 corecmd_exec_bin() と corecmd_exec_shell() でそれぞれ許可されるようなので、これを削除します。

$ grep corecmd_exec_ tomcat.te
corecmd_exec_bin(tomcat_domain)
corecmd_exec_shell(tomcat_domain)
$ sed -i /^corecmd_exec_/d tomcat.te

他にも不要そうな実行権限らしきものがあったので削除しています。オリジナルとの差分は以下になりました。

$ diff -U0 rpmbuild/BUILD/serefpolicy-3.13.1/policy/modules/contrib/tomcat.te tomcat.te
--- rpmbuild/BUILD/serefpolicy-3.13.1/policy/modules/contrib/tomcat.te  2018-07-11 08:44:59.131839628 +0000
+++ tomcat.te   2018-07-11 08:46:02.687332067 +0000
@@ -48 +47,0 @@
-    pki_exec_common_files(tomcat_t)
@@ -77,3 +75,0 @@
-corecmd_exec_bin(tomcat_domain)
-corecmd_exec_shell(tomcat_domain)
-
@@ -107,2 +102,0 @@
-libs_exec_ldconfig(tomcat_domain)
-
@@ -129 +122,0 @@
-    rpm_exec(tomcat_domain)

ビルドします。

$ make -f /usr/share/selinux/devel/Makefile tomcat.pp

いくつか Error: duplicate definition of のエラーが出ますが container 関係の定義ですので無視します。どうしてもエラーが気になる方は、

  1. rpmbuild -bp rpmbuild/SPECS/selinux-policy.spec で生成された rpmbuild/BUILD/serefpolicy-3.13.1/policy/modules/contrib/tomcat.te を直接編集
  2. rpmbuild -bi -short-circuit rpmbuild/SPECS/selinux-policy.spec を実行
  3. 生成された rpmbuild/BUILD/serefpolicy-3.13.1/tomcat.pp を使う

という手もあります。tomcat.pp さえ手に入ればよいので完了まで待つ必要はないももの、ビルドに gcc が必要かつ時間がかかります。

tomcat ポリシーモジュールのインストール

ビルドしたポリシーモジュールをインストールします。

$ sudo semodule -i tomcat.pp
libsemanage.semanage_direct_install_info: Overriding tomcat module at lower priority 100 with module at priority 400.

インストールしたポリシーモジュールはプライオリティ 400 でインストールされます。デフォルトのモジュールはプライオリティ 100 で残りますので、プライオリティ 400 のモジュールを削除すれば元の状態に戻すことができます。

$ sudo semodule -lfull | grep tomcat
400 tomcat            pp
100 tomcat            pp

新ポリシーの実行可能権限を確認すると、bin_t や shell_exec_t は含まれなくなっていることが確認できます。

$ sudo sesearch -A -s tomcat_t -c file -p execute
Found 7 semantic av rules:
   allow tomcat_domain tomcat_exec_t : file { ioctl read getattr lock execute execute_no_trans open } ;
   allow domain ld_so_t : file { ioctl read getattr execute open } ;
   allow domain lib_t : file { ioctl read getattr lock execute open } ;
   allow tomcat_t tomcat_exec_t : file { ioctl read getattr lock execute execute_no_trans entrypoint open } ;
   allow domain textrel_shlib_t : file { ioctl read getattr execute execmod open } ;
   allow domain abrt_helper_exec_t : file { read getattr execute open } ;
   allow domain prelink_exec_t : file { ioctl read getattr lock execute execute_no_trans open } ;

tomcat_exec_t 以外にもまだ execute 権限が残っていますが、shell_exec_t や bin_t は含まれなくなりました。因みにデフォルトのポリシーでの実行権限は以下でしたので、6 つの実行権限が減ったことになります。

$ sudo sesearch -A -s tomcat_t -c file -p execute
Found 13 semantic av rules:
   allow tomcat_t tomcat_exec_t : file { ioctl read getattr lock execute execute_no_trans entrypoint open } ;
   allow domain textrel_shlib_t : file { ioctl read getattr execute execmod open } ;
   allow tomcat_domain shell_exec_t : file { ioctl read getattr lock execute execute_no_trans open } ;
   allow domain lib_t : file { ioctl read getattr lock execute open } ;
   allow tomcat_domain ldconfig_exec_t : file { ioctl read getattr lock execute execute_no_trans open } ;
   allow tomcat_t pki_common_t : file { ioctl read write create getattr setattr lock append unlink link rename execute execute_no_trans open } ;
   allow domain ld_so_t : file { ioctl read getattr execute open } ;
   allow tomcat_domain base_ro_file_type : file { ioctl read getattr lock execute execute_no_trans open } ;
   allow tomcat_domain tomcat_exec_t : file { ioctl read getattr lock execute execute_no_trans open } ;
   allow tomcat_domain bin_t : file { ioctl read getattr lock execute execute_no_trans open } ;
   allow domain abrt_helper_exec_t : file { read getattr execute open } ;
   allow tomcat_domain rpm_exec_t : file { ioctl read getattr lock execute execute_no_trans open } ;
   allow domain prelink_exec_t : file { ioctl read getattr lock execute execute_no_trans open } ;

tomcat 実行環境整備

jsvc コマンドでの起動に変更

shell_exec_t や bin_t の実行権限は剥奪できましたが、このままでは tomcat は起動できません。tomcat は java コマンドで動いており、java コマンドのタイプが bin_t なためです。

java 実行ファイルのタイプを bin_t から tomcat_exec_t に変更してもよいのですが、tomcat 以外でも利用される可能性を考えると避けたいところです。

ということで java コマンドを使わずに jsvc コマンドで起動できるように起動処理を修正していきます。

まず tomcat-jsvc パッケージを導入します。

$ sudo yum install tomcat-jsvc -y

SELinux 無効状態で起動確認

Permissive 状態で起動してみて、いくつポリシーに引っかかるか確認してみます。

$ sudo setenforce 0
$ sudo systemctl start tomcat-jsvc.service

audit.log を確認します。

$ sudo cat /var/log/audit/audit.log | grep ' avc: *denied'
type=AVC msg=audit(1531226175.304:204): avc: denied { execute } for pid=2513 comm="server" name="readlink" dev="dm-0" ino=29039 scontext=system_u:system_r:tomcat_t:s0 tcontext=system_u:object_r:bin_t:s0 tclass=file
type=AVC msg=audit(1531226175.305:205): avc: denied { execute_no_trans } for pid=2514 comm="server" path="/usr/bin/readlink" dev="dm-0" ino=29039 scontext=system_u:system_r:tomcat_t:s0 tcontext=system_u:object_r:bin_t:s0 tclass=file
type=AVC msg=audit(1531226175.309:206): avc: denied { execute } for pid=2516 comm="server" name="bash" dev="dm-0" ino=28832 scontext=system_u:system_r:tomcat_t:s0 tcontext=system_u:object_r:shell_exec_t:s0 tclass=file

readlink と bash で denied となっています。tomcat-jsvc.service の起動処理を調べてみると

  1. /usr/libexec/tomcat/server
  2. /usr/libexec/tomcat/preamble
  3. /usr/libexec/tomcat/functions
  4. /usr/share/java-utils/java-functions

の順で読み込まれていることがわかります。

  • bash は /usr/libexec/tomcat/server や build-classpath シェルスクリプトの実行に必要
  • readlink は /usr/share/java-utils/java-functions で JVM_ROOT や JAVA_HOME の設定に必要

となっているようです。

シェルを介さない起動処理に変更

ちょっと対応に悩みましたが、シェルの実行権限(shell_exec_t)は剥奪したままにしておきたいので、シェルではなく jsvc を直接実行するように tomcat-jsvc.service を変更します。

/usr/libexec/tomcat/functions の run_jsvc() を確認すると、jsvc での起動コマンドは以下のようになることがわかります。

/usr/bin/jsvc -nodetach -pidfile /var/run/jsvc-tomcat.pid -user tomcat -outfile /usr/share/tomcat/logs/catalina.out -errfile /usr/share/tomcat/logs/catalina.out -classpath /usr/share/tomcat/bin/bootstrap.jar:/usr/share/tomcat/bin/tomcat-juli.jar:/usr/share/java/commons-daemon.jar -Dcatalina.base=/usr/share/tomcat -Dcatalina.home=/usr/share/tomcat -Djava.endorsed.dirs= -Djava.io.tmpdir=/usr/share/tomcat/temp -Djava.util.logging.config.file=/usr/share/tomcat/conf/logging.properties -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager org.apache.catalina.startup.Bootstrap start

停止コマンドは以下になります。

/usr/bin/jsvc -nodetach -pidfile /var/run/jsvc-tomcat.pid -user tomcat -outfile /usr/share/tomcat/logs/catalina.out -errfile /usr/share/tomcat/logs/catalina.out -stop -classpath /usr/share/tomcat/bin/bootstrap.jar:/usr/share/tomcat/bin/tomcat-juli.jar:/usr/share/java/commons-daemon.jar -Dcatalina.base=/usr/share/tomcat -Dcatalina.home=/usr/share/tomcat -Djava.endorsed.dirs= -Djava.io.tmpdir=/usr/share/tomcat/temp -Djava.util.logging.config.file=/usr/share/tomcat/conf/logging.properties -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager org.apache.catalina.startup.Bootstrap stop

これらを ExecStart=, ExecStop= に指定しようと思いますが、/usr/bin/jsvc のタイプは bin_t ですので、unconfined_service_t で起動してしまいます。tomcat_exec_t ではないので、せっかく作成した tomcat ポリシーが効きません。

/usr/libexec/tomcat/jsvc を用意してタイプを tomcat_exec_t に変更し、そちらを実行するようにします。

$ sudo cp -p /usr/bin/jsvc /usr/libexec/tomcat/
$ sudo chcon -u system_u -r object_r -t tomcat_exec_t /usr/libexec/tomcat/jsvc

タイプ変更は本来 tomcat.fc を変更して適用すべきですが、いったんコマンドで直接変更しました。

では tomcat-jsvc.service を修正します。

$ sudo cp -p /usr/lib/systemd/system/tomcat-jsvc.service /etc/systemd/system/
$ sudo vi /etc/systemd/system/tomcat-jsvc.service

ExecStart= と ExecStop= を以下のように変更します。

ExecStart=/usr/libexc/tomcat/jsvc -nodetach -pidfile /var/run/jsvc-tomcat.pid -user tomcat -outfile /usr/share/tomcat/logs/catalina.out -errfile /usr/share/tomcat/logs/catalina.out -classpath /usr/share/tomcat/bin/bootstrap.jar:/usr/share/tomcat/bin/tomcat-juli.jar:/usr/share/java/commons-daemon.jar -Dcatalina.base=/usr/share/tomcat -Dcatalina.home=/usr/share/tomcat -Djava.endorsed.dirs= -Djava.io.tmpdir=/usr/share/tomcat/temp -Djava.util.logging.config.file=/usr/share/tomcat/conf/logging.properties -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager org.apache.catalina.startup.Bootstrap start
ExecStop=/usr/libexc/tomcat/jsvc -nodetach -pidfile /var/run/jsvc-tomcat.pid -user tomcat -outfile /usr/share/tomcat/logs/catalina.out -errfile /usr/share/tomcat/logs/catalina.out -stop -classpath /usr/share/tomcat/bin/bootstrap.jar:/usr/share/tomcat/bin/tomcat-juli.jar:/usr/share/java/commons-daemon.jar -Dcatalina.base=/usr/share/tomcat -Dcatalina.home=/usr/share/tomcat -Djava.endorsed.dirs= -Djava.io.tmpdir=/usr/share/tomcat/temp -Djava.util.logging.config.file=/usr/share/tomcat/conf/logging.properties -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager org.apache.catalina.startup.Bootstrap stop

SELinux 有効状態で起動確認

SELinux を Enforcing にして起動してみます。

$ sudo setenforce 1
$ sudo systemctl daemon-reload
$ sudo systemctl start tomcat-jsvc.service

無事起動できたようです。

$ ps -efZ | grep jsvc | grep -v grep
system_u:system_r:tomcat_t:s0   root      2726     1  0 06:56 ?        00:00:00 jsvc.exec -nodetach -pidfile /var/run/jsvc-tomcat.pid -user tomcat -outfile /usr/share/tomcat/logs/catalina.out -errfile /usr/share/tomcat/logs/catalina.out -classpath /usr/share/tomcat/bin/bootstrap.jar:/usr/share/tomcat/bin/tomcat-juli.jar:/usr/share/java/commons-daemon.jar -Dcatalina.base=/usr/share/tomcat -Dcatalina.home=/usr/share/tomcat -Djava.endorsed.dirs= -Djava.io.tmpdir=/usr/share/tomcat/temp -Djava.util.logging.config.file=/usr/share/tomcat/conf/logging.properties -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager org.apache.catalina.startup.Bootstrap start
system_u:system_r:tomcat_t:s0   tomcat    2729  2726  6 06:56 ?        00:00:04 jsvc.exec -nodetach -pidfile /var/run/jsvc-tomcat.pid -user tomcat -outfile /usr/share/tomcat/logs/catalina.out -errfile /usr/share/tomcat/logs/catalina.out -classpath /usr/share/tomcat/bin/bootstrap.jar:/usr/share/tomcat/bin/tomcat-juli.jar:/usr/share/java/commons-daemon.jar -Dcatalina.base=/usr/share/tomcat -Dcatalina.home=/usr/share/tomcat -Djava.endorsed.dirs= -Djava.io.tmpdir=/usr/share/tomcat/temp -Djava.util.logging.config.file=/usr/share/tomcat/conf/logging.properties -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager org.apache.catalina.startup.Bootstrap start

リモートコード実行防御確認

struts2-showcase をデプロイして脆弱性回避できるか確認します。

$ curl -O -R https://archive.apache.org/dist/struts/2.5.10/struts-2.5.10-apps.zip
$ unzip struts-2.5.10-apps.zip
$ sudo cp struts-2.5.10/apps/struts2-showcase.war /var/lib/tomcat/webapps/
$ ./struts2_S2-045.py http://localhost:8080/struts2-showcase/showcase.action 'head -1 /etc/passwd'
[*] CVE: 2017-5638 - Apache Struts2 S2-045
[*] cmd: head -1 /etc/passwd


<!DOCTYPE html>

コマンドの実行結果は表示されることなく、Struts2 Showcaseのトップページが表示されました。 /var/log/audit.log でも jsvc プロセス (scontext=system_u:system_r:tomcat_t:s0) から bash コマンド (tcontext=system_u:object_r:shell_exec_t:s0) が拒否されていることが確認できます。

type=AVC msg=audit(1531227375.478:72): avc:  denied  { execute } for  pid=1252 comm="jsvc" name="bash" dev="dm-0" ino=28832 scontext=system_u:system_r:tomcat_t:s0 tcontext=system_u:object_r:shell_exec_t:s0 tclass=file

コンテナにしたほうが楽なのか

コンテナ化をしたほうが楽なのではないかという意見もあるかと思いますので試してみましょう。 ベースイメージのサイズが小さいとされる Alpine ベースの tomcat を使用して Docker コンテナを作ります。Docker 実行環境自体の環境構築は割愛します。SELinux は無効化しています。 Dockerfile を用意してイメージをビルドします。

$ cat Dockerfile
FROM tomcat:7.0.90-jre8-alpine
COPY struts-2.5.10/apps/struts2-showcase.war /usr/local/tomcat/webapps/
$ sudo docker build -t struts2-showcase .
Sending build context to Docker daemon 84.16 MB
Step 1/2 : FROM tomcat:7.0.90-jre8-alpine
Trying to pull repository docker.io/library/tomcat ...
7.0.90-jre8-alpine: Pulling from docker.io/library/tomcat
911c6d0c7995: Pull complete
4001add52a90: Pull complete
820acf8d3a4c: Pull complete
331c53393eed: Pull complete
f9a15a1f9fec: Pull complete
509e8f58c0dc: Pull complete
Digest: sha256:f46b6cca825c93586842ece76658e09fadbe57184bd4c6616fd3cd2e29147bd3
Status: Downloaded newer image for docker.io/tomcat:7.0.90-jre8-alpine
 ---> b4209fa511a5
Step 2/2 : COPY struts-2.5.10/apps/struts2-showcase.war /usr/local/tomcat/webapps/
 ---> aa24ad860578
Removing intermediate container 49a6d615837e
Successfully built aa24ad860578

起動します。

$ sudo docker run -it --rm -p 8888:8080 struts2-showcase

RCE を試してみます。

$ ./struts2_S2-045.py http://localhost:8888/struts2-showcase/showcase.action 'head -1 /etc/passwd'
[*] CVE: 2017-5638 - Apache Struts2 S2-045
[*] cmd: head -1 /etc/passwd

root:x:0:0:root:/root:/bin/bash

$

単純にコンテナ化しただけでは当然 RCE 自体は防げませんし、サイズが小さいとされる Alpine ベースであっても、コマンド自体は多数含まれていますので「攻撃者ができることを減らす」という意味ではあまり効果がありません。

被害がコンテナ内に限定されるという考えもありますが、データベースにアクセスして内容を攻撃者のサイトにアップロードするといった手法も考えるとコンテナ内だから安心とは一概には言えないと思います。

最後に

結局のところ、RCE 脆弱性をつかれても被害が広がらないようにするためには、脆弱性を悪用して実行できるコマンドを減らすことが重要になると思います。それを SELinux でおこなうか、実行可能なコマンド自体を減らしたコンテナを作るか、になるのかと思います。

また、今回の検証では触れていませんが SELinux は管理者権限昇格といった攻撃にも有効です。 セキュリティ以外の要件も考慮したうえで、適切な方式を選択することが重要だなと感じました。

あ、あと /usr/libexec/tomcat/jsvc を用意して手動でタイプを変更していますが、本来こちらも tomcat.fc でタイプ定義すべきものです。ちょっとサボりましたが悪しからず。

  • Zabbix Enterprise Appliance
  • 低コスト・短納期で提供するまるごとおまかせZabbix