knqyf263's blog

自分のためのメモとして残しておくためのブログ。

Tomcatの脆弱性(CVE-2018-1304) について調べてみた

Tomcat脆弱性(CVE-2018-1304, CVE-2018-1305)が先日公開されました。 最近自分が触ったことのないものの脆弱性を調べたりしているので、その一環で挑戦してみました。

脆弱性のあるバージョンとかは参考サイトを確認して下さい。 今回はどういう設定にすると脆弱性の影響があるのか、またそれの検証とTomcatソースコードデバッグあたりをやっていきます。 Tomcatmavenなど素人なので、とりあえず動かしてみたものの一般的じゃない設定なども多いかもしれません。 もし気付いた方がいればご指摘頂けると幸いです。

何となく番号が早かったということでCVE-2018-1304から調べたのですが、結構疲れてしまってCVE-2018-1305についてはちゃんと検証しておりません。 アノテーションの話みたいなので、元気になったらあとで調べるかもしれません。

参考

Tomcat脆弱性関連

Tomcatデバッグ関連

概要

CVE-2018-1304は一言で言うと、セキュリティの制約が回避されてしまう脆弱性になります。 セキュリティの制約には、特定のロールしかアクセスできない、GETは禁止、HTTPSのみ許可、など様々な設定があります。 特にadminのみ許可、のような制限を入れていた場合に回避されてしまうのでWebサイトによっては影響が大きいかもしれません。

ですが、影響があるのはURLのマッピング設定で ""(空文字)を使っている場合のみになります。 マッピング設定はweb.xmlアノテーションで設定するもので、 /users にリクエストが来たらこのクラスを呼ぶ、みたいなやつですね。 普通rootをマッピングするとしても / にすると思うので、 ""(空文字)になってることはあまりないんじゃないかなーと思ってます。 どこかのブログなどでにそういう設定例があったりすると、多く使われていたりするのかもしれませんが。

この脆弱性の影響がある人はかなり少ない気がしているので、レベルがImportantってのは高いんじゃないかなーと思ったり思わなかったりします。 ただ影響があるサイトにとっては影響度は大きいので、そういう付け方なんですかね。

検証

ということで実際に検証してみます。 例によってDockerfileを用意しました。

github.com

さらっと用意しました、とか言いましたが実際はかなり大変でした。 Tomcatを業務で触ったことがなかったのでServletHello 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 です。 まず、 @WebServleturlPatterns/ と設定しており、 / へアクセスした時にこのクラスが呼ぶための設定です。 今回はvulnの下にあるので、/vuln/にアクセスするとHelloServletが呼ばれます。 次に @ServletSecurity はセキュリティの設定を色々するためのアノテーションで、今回は rolesAllowedadmin ロールだけがアクセスできるように設定しています。

ロールは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 にアクセスします。

f:id:knqyf263:20180225210733p:plain

猫が見えればOKです。

ではその状態で、 http://localhost:8080/vuln にアクセスしてみます。 今度はBasic認証が求められます。 先程設定した admin/password でログインしてCVE-2018-1304!!と表示されていれば成功です。

ようやく検証の下準備が完了したので、CVE-2018-1304の確認をしてみます。 HelloServlet.javaurlPatterns を編集します。 先程までは / でしたが、ここを "" の空文字に変更します。

 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ページを表示できてしまいました。

f:id:knqyf263:20180225213944p:plain

概要で書いたとおり、 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://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/catalina/realm/RealmBase.java?revision=1812188&view=markup&pathrev=1823306#l535

これはHTTPリクエストのpathやmethodから関連するSecurityConstraintを取り出すメソッドです。

どうやって関連するかを判断しているかというと、以下の583行目にある通り、リクエストの uriurlPatterns の比較になります。 これら(と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                 }

http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/catalina/realm/RealmBase.java?revision=1812188&view=markup&pathrev=1823306#l583

もし 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を設定します。

f:id:knqyf263:20180225220543p:plain

そして先程の RealmBase.javafindSecurityConstraintsブレークポイントを貼り、デバッグボタンを押します。 ブラウザで http://localhost:8080/vuln にアクセスすると、ブレークポイントで止まります。

f:id:knqyf263:20180225220936p:plain

ステップオーバーしていき、constraints 変数の中を見ると以下のようになっていました。 authRolesadmin になっており、アノテーションで設定したとおりになっていることが分かります。

f:id:knqyf263:20180225221120p:plain

肝心の比較の箇所を見てみます。 uri には "/" が入っていて、patterns[0]には""が入っています。 よって、このif文はfalseになりfoundはfalseのままになります。 先程のコードリーディングの予想通りであることが分かりました。

f:id:knqyf263:20180225221413p:plain

ということで、修正はまずuriの長さが0の場合に/を代入しています。 そして比較の箇所を以下のようにpatterns が空文字の場合もfound=trueになるような修正を入れています。

if(uri.equals(patterns[k]) || patterns[k].length() == 0 && uri.equals("/")) {

http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/catalina/realm/RealmBase.java?revision=1823306&view=markup&pathrev=1823306#l584

まとめ

TomcatServletは業務などで触れる機会もないので、CVE-2018-1304の調査がてら触ってみました。 正直設定の難度が高すぎてまだ全然理解できてないですが、とりあえず動くようになって良かったです。 Dockerで動いているTomcatへのリモートデバッグもかなりハマりましたが、最終的にはブレークポイントも貼れるようになってデバッグが捗るようになりました。 というかDockerへのリモートデバッグだけでも記事書けそうだな...などと思いました。 今回も脆弱性自体は難しいものではないですが、デバッガでソースコードを追うところまでやると学びが多いのでオススメです。

CVE-2018-1304の影響を受ける設定になっている場合は影響が大きい可能性があるので、修正済みのバージョンにアップデートしましょう。