Tomcatの脆弱性(CVE-2018-1304, CVE-2018-1305)が先日公開されました。 最近自分が触ったことのないものの脆弱性を調べたりしているので、その一環で挑戦してみました。
脆弱性のあるバージョンとかは参考サイトを確認して下さい。 今回はどういう設定にすると脆弱性の影響があるのか、またそれの検証とTomcatのソースコードのデバッグあたりをやっていきます。 Tomcatやmavenなど素人なので、とりあえず動かしてみたものの一般的じゃない設定なども多いかもしれません。 もし気付いた方がいればご指摘頂けると幸いです。
何となく番号が早かったということでCVE-2018-1304から調べたのですが、結構疲れてしまってCVE-2018-1305についてはちゃんと検証しておりません。 アノテーションの話みたいなので、元気になったらあとで調べるかもしれません。
参考
Tomcat脆弱性関連
- http://tomcat.apache.org/security-9.html
- https://jvn.jp/vu/JVNVU95970576/
- https://oss.sios.com/security/tomcat-security-vulnerabiltiy-20180223
Tomcatデバッグ関連
- https://www.dontpanicblog.co.uk/2017/03/12/tomcat-debugging-in-docker/
- https://qiita.com/shintaro123/items/a8a3d222d3dd46aba876
概要
CVE-2018-1304は一言で言うと、セキュリティの制約が回避されてしまう脆弱性になります。 セキュリティの制約には、特定のロールしかアクセスできない、GETは禁止、HTTPSのみ許可、など様々な設定があります。 特にadminのみ許可、のような制限を入れていた場合に回避されてしまうのでWebサイトによっては影響が大きいかもしれません。
ですが、影響があるのはURLのマッピング設定で ""
(空文字)を使っている場合のみになります。
マッピング設定はweb.xmlやアノテーションで設定するもので、 /users
にリクエストが来たらこのクラスを呼ぶ、みたいなやつですね。
普通rootをマッピングするとしても /
にすると思うので、 ""
(空文字)になってることはあまりないんじゃないかなーと思ってます。
どこかのブログなどでにそういう設定例があったりすると、多く使われていたりするのかもしれませんが。
この脆弱性の影響がある人はかなり少ない気がしているので、レベルがImportantってのは高いんじゃないかなーと思ったり思わなかったりします。 ただ影響があるサイトにとっては影響度は大きいので、そういう付け方なんですかね。
検証
ということで実際に検証してみます。 例によってDockerfileを用意しました。
さらっと用意しました、とか言いましたが実際はかなり大変でした。 Tomcatを業務で触ったことがなかったのでServletのHello Worldだけで何時間も溶かしました。 未だにやり方はよく分かってないですが、一応動くようになったので良かったです。 次からTomcatの脆弱性が来たらすぐ調査できるようになったので、Tomcatの脆弱性はWelcomeです。
DockerはTomcat 9.0.4を使いました。 9.0.5で修正されているので、その直前のバージョンになります。
Servletのプロジェクトはmavenで雛形を作ったものを少しいじりました。
$ mvn archetype:generate -DgroupId=test.vuln -DartifactId=vuln -DarchetypeArtifactId=maven-archetype-webapp -DinteractiveMode=false
雛形を作ったあと、Servletを作ります。
$ mkdir -p vuln/src/main/java/test/vuln $ vim vuln/src/main/java/test/vuln/HelloServlet.java package test.vuln; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; import javax.servlet.annotation.HttpMethodConstraint; import javax.servlet.annotation.HttpConstraint; import javax.servlet.annotation.ServletSecurity; import javax.servlet.annotation.WebServlet; @WebServlet (name = "Root", urlPatterns = { "/" }) @ServletSecurity(value=@HttpConstraint(rolesAllowed={"admin"})) public class HelloServlet extends HttpServlet { public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException{ response.setContentType("text/html"); PrintWriter out = response.getWriter(); out.println("<html>"); out.println("<h1>CVE-2018-1304!!</h1>"); out.println("</body>"); out.println("</html>"); } }
GETが来たらCVE-2018-1304!!と返すだけの処理になっています。
ここでのポイントは @ServletSecurity
と @WebServlet
です。
まず、 @WebServlet
は urlPatterns
で /
と設定しており、 /
へアクセスした時にこのクラスが呼ぶための設定です。
今回はvuln
の下にあるので、/vuln/
にアクセスするとHelloServletが呼ばれます。
次に @ServletSecurity
はセキュリティの設定を色々するためのアノテーションで、今回は rolesAllowed
で admin
ロールだけがアクセスできるように設定しています。
ロールはtomcat-users.xml
で設定しています。
以下はadmin
ロールを定義し、 admin
ユーザを admin
ロールに所属させています。
$ cat tomcat-users.xml <?xml version='1.0' encoding='utf-8'?> <tomcat-users> <role rolename="admin"/> <user username="admin" password="password" roles="admin"/> </tomcat-users>
次にBasic認証の設定をします。
設定方法がよく分かっていないのですが、web.xml
に書いたら行けました。
(/
とかにアクセスするとBasic認証求められないのでよく分からない。。)
$ cat vuln/src/main/webapp/WEB-INF/web.xml <!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd" > <web-app> <display-name>Archetype Created Web Application</display-name> <login-config> <auth-method>BASIC</auth-method> <realm-name>default</realm-name> </login-config> </web-app>
これで、/vuln
にアクセスするとBasic認証が求められます。
adminロールじゃないとアクセス出来ないため、上記のID/PWでログインする必要があります。
次にmavenでビルドするためのpom.xml
を書きます。
とりあえず参考サイトからコピペしつつ、少しいじって以下のようにしたら動きました。
ちなみにこれもそれぞれの設定はあまり理解してません。
$ cd vuln $ cat pom.xml <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <properties> <maven.compiler.source>1.9</maven.compiler.source> <maven.compiler.target>1.9</maven.compiler.target> </properties> <modelVersion>4.0.0</modelVersion> <groupId>test.vuln</groupId> <artifactId>vuln</artifactId> <packaging>war</packaging> <version>1.0-SNAPSHOT</version> <name>vuln Maven Webapp</name> <url>http://maven.apache.org</url> <dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>3.8.1</version> <scope>test</scope> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> <scope>provided</scope> </dependency> </dependencies> <build> <!-- mavenでコンパイルするたmのプラグイン --> <finalName>${project.artifactId}-${project.version}</finalName> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.1</version> <configuration> <encoding>UTF-8</encoding> <source>1.8</source> <target>1.8</target> </configuration> </plugin> <!-- mavenからtomcatにwarファイル展開するためのプラグイン --> <plugin> <groupId>org.apache.tomcat.maven</groupId> <artifactId>tomcat7-maven-plugin</artifactId> <version>2.2</version> <configuration> <path>/</path><!-- webapps配下に展開するためのファイル(ディレクトリ名) --> <server>tomcat-localhost</server> <url>http://localhost/manager/text</url> </configuration> </plugin> </plugins> </build> </project>
ビルドする準備ができたので、mavenでビルドします。 macOSでしか検証してないです。
$ mvn clean package [INFO] Scanning for projects... [INFO] [INFO] ------------------------------------------------------------------------ [INFO] Building vuln Maven Webapp 1.0-SNAPSHOT [INFO] ------------------------------------------------------------------------ ... [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 4.725 s [INFO] Finished at: 2018-02-25T19:29:15+09:00 [INFO] Final Memory: 17M/285M [INFO] ------------------------------------------------------------------------
準備が終わったのでdocker buildします。イメージ名は適当にcve
にしましたが、何でも良いです。
$ cd .. $ docker build -t cve .
デフォルトだと8080番ポートでLISTENするので、8080番をポートフォワードします。
$ docker run --name cve -d -p 8080:8080 cve
起動したら、ブラウザで http://localhost:8080
にアクセスします。
猫が見えればOKです。
ではその状態で、 http://localhost:8080/vuln にアクセスしてみます。 今度はBasic認証が求められます。 先程設定した admin/password でログインしてCVE-2018-1304!!と表示されていれば成功です。
ようやく検証の下準備が完了したので、CVE-2018-1304の確認をしてみます。
HelloServlet.java
の urlPatterns
を編集します。
先程までは /
でしたが、ここを ""
の空文字に変更します。
import javax.servlet.annotation.ServletSecurity; import javax.servlet.annotation.WebServlet; -@WebServlet (name = "Root", urlPatterns = { "" }) +@WebServlet (name = "Root", urlPatterns = { "/" }) @ServletSecurity(value=@HttpConstraint(rolesAllowed={"admin"})) public class HelloServlet extends HttpServlet {
ビルドし直して、コンテナに再度デプロイします。
$ cd vuln $ mvn clean package $ docker cp target/vuln-1.0-SNAPSHOT.war cve:/usr/local/tomcat/webapps/vuln.war
デプロイが終わるまで少しだけ時間差がありますが、コンテナで以下のようなログが出れば完了です。
Deployment of web application archive [/usr/local/tomcat/webapps/vuln.war] has finished in [20] ms
この状態で再度 http://localhost:8080:vuln にアクセスします。 先ほどと同じブラウザだとBasic認証のヘッダが付いたままなので、別ブラウザやシークレットウィンドウなどでアクセスして下さい。 すると、Basic認証が求められずにWebページが表示されます。 単純ですが、これが脆弱性です。 認証を回避できてWebページを表示できてしまいました。
概要で書いたとおり、 urlPatterns
を ""
にしていると影響を受けます。
ということで検証は終わりです。
ソースコード確認
CVE-2018-1304のソースコード上の修正箇所は以下になります。
[Apache-SVN] Diff of /tomcat/trunk/java/org/apache/catalina/realm/RealmBase.java
これは findSecurityConstraints
のメソッド内にあります。
534 @Override 535 public SecurityConstraint [] findSecurityConstraints(Request request, 536 Context context) {
これはHTTPリクエストのpathやmethodから関連するSecurityConstraintを取り出すメソッドです。
どうやって関連するかを判断しているかというと、以下の583行目にある通り、リクエストの uri
と urlPatterns
の比較になります。
これら(とmethod)が一致した場合はSecurityConstraintを配列に入れて返します。
582 for(int k=0; k < patterns.length; k++) { 583 if(uri.equals(patterns[k])) { 584 found = true; 585 if(collection[j].findMethod(method)) { 586 if(results == null) { 587 results = new ArrayList<>(); 588 } 589 results.add(constraints[i]); 590 } 591 } 592 }
もし urlPatterns
が ""
になってて uri
が /
になっている場合、このif文に入らずresultsが空の配列になり、SecurityConstraintがないものとして扱われそうです。
結果として、制約をすり抜けてしまいます。
これが脆弱性の原因だろうと推測されます。
ソースコードをさっと読んだ感じでは上の理解になりますが、実際にデバッガで変数の中身を覗いて確かめてみましょう。
デバッグ
上記のリポジトリにDockerfile.debugも置いておいたので、それを用いてイメージをビルドするとデバッグ可能になります。 ですが、元のDockerfileでも環境変数を指定して起動スクリプトの引数を変えることでデバッグ出来るようになります。
$ docker run --name cve -e JPDA_SUSPEND=y -e JPDA_ADDRESS=0.0.0.0:8000 --rm -it -p 8080:8080 -p 8000:8000 cve catalina.sh jpda run
8000番ポートに繋ぐとデバッグ出来るような設定になっています。 JPDA_ADDRESSに0.0.0.0を書かないと繋がるようになりませんでしたし、JPDA_SUSPEND=yを付けないと繋がってもブレークポイントで止まってくれませんでした。 ココらへんググっても出なくて超絶ハマりました。
ブレークポイントを貼りたいので以下の公式サイトからソースコードを持ってきます。 ダウンロードしたら適当な場所で展開します。
Index of /dist/tomcat/tomcat-9/v9.0.4/src
次に、このソースをIntellij IDEAで開きます。 そしてRun/Debug Configurationsでリモートデバッグをする設定をします。 まず左上の+ボタンからRemoteを選びます。 名前は何でも良いです。ここではTomcatにしました。 そしてHostにlocalhost、Portに8000を設定します。
そして先程の RealmBase.java
の findSecurityConstraints
にブレークポイントを貼り、デバッグボタンを押します。
ブラウザで http://localhost:8080/vuln
にアクセスすると、ブレークポイントで止まります。
ステップオーバーしていき、constraints
変数の中を見ると以下のようになっていました。
authRoles
が admin
になっており、アノテーションで設定したとおりになっていることが分かります。
肝心の比較の箇所を見てみます。
uri
には "/"
が入っていて、patterns[0]
には""
が入っています。
よって、このif文はfalseになりfound
はfalseのままになります。
先程のコードリーディングの予想通りであることが分かりました。
ということで、修正はまずuri
の長さが0の場合に/
を代入しています。
そして比較の箇所を以下のようにpatterns
が空文字の場合もfound=trueになるような修正を入れています。
if(uri.equals(patterns[k]) || patterns[k].length() == 0 && uri.equals("/")) {
まとめ
TomcatやServletは業務などで触れる機会もないので、CVE-2018-1304の調査がてら触ってみました。 正直設定の難度が高すぎてまだ全然理解できてないですが、とりあえず動くようになって良かったです。 Dockerで動いているTomcatへのリモートデバッグもかなりハマりましたが、最終的にはブレークポイントも貼れるようになってデバッグが捗るようになりました。 というかDockerへのリモートデバッグだけでも記事書けそうだな...などと思いました。 今回も脆弱性自体は難しいものではないですが、デバッガでソースコードを追うところまでやると学びが多いのでオススメです。
CVE-2018-1304の影響を受ける設定になっている場合は影響が大きい可能性があるので、修正済みのバージョンにアップデートしましょう。