Сегодня я рассмотрю уязвимость в Apache Tomcat, которая позволяет читать файлы на сервере и при определенных условиях выполнять произвольный код. Проблема скрывается в особенностях реализации протокола AJP, по которому происходит взаимодействие с сервером Tomcat. Для эксплуатации злоумышленнику не требуется каких-либо прав в целевой системе.
Tomcat — это контейнер сервлетов с открытым исходным кодом. Он написан на языке Java и реализует такие спецификации, как JavaServer Pages (JSP) и JavaServer Faces (JSF). Это один из наиболее популярных веб-серверов, особенно часто он используется в корпоративной среде. Его ставят как самостоятельное решение или в качестве контейнера сервлетов в различных серверах приложений, например GlassFish или JBoss.
Баг нашел исследователь из Chaitin Tech в начале этого года. Уязвимость получила статус критической. Как сейчас стало модно, она обзавелась собственным названием — Ghostcat — и логотипом в виде кота-призрака.
docker run -it --rm -p 8080:8080 -p 8009:8009 tomcat:9.0.30
Очень важно расшарить порт 8009, это AJP-протокол, в котором и была найдена уязвимость.
Если хочется вместе со мной возиться с отладкой приложения, то нужно действовать немного по-другому. Для дебага я буду использовать IntelliJ IDEA. Сначала скачаем уязвимую версию Apache Tomcat. Я взял 9.0.30. Распакуем и откроем проект в IDEA. Теперь создадим новую конфигурацию отладки с шаблоном Remote.
Создание шаблона Remote в IDEA
Здесь в поле Command lines arguments строка СЃ параметрами, которые нужно указать РїСЂРё запуске сервера. Рекомендую выбрать версию JDK 1.4.x.
Сами параметры можно передать РІ Docker СЃ помощью ключа -e или --env. Переменная окружения, используемая для этих целей, называется JAVA_OPTS. Обрати внимание на опцию suspend: если РѕРЅР° включена (suspend=y), Java будет приостанавливать загрузку виртуальной машины и ждать подключения отладчика и только после успешного коннекта продолжит запуск. У меня получилась такая строка.
Открываем браузер, переходим на запущенный сервер (не забывай, что порт — 8080) и наблюдаем страницу 404. Дело в том, что в последних версиях официального докер-контейнера Tomcat папка webapps СЃРѕ стандартными приложениями была переименована РІ webapps.dist. Достаточно удалить папку и создать симлинк на оригинальную версию директории.
Проксируем трафик через Apache к Tomcat по протоколу AJP
Помимо веб-сервера, нам также понадобится какой-нибудь сниффер. Я буду использовать Wireshark. На этом стенд готов. Кстати, если ты не любишь Docker, то есть вариант скачать с сайта разработчика версию с готовыми бинарниками. Все версии можно найти в разделе с архивами.
Теперь можно переходить к разбору уязвимости.
Детали уязвимости. Чтение произвольных файлов на сервере
Apache JServ Protocol (AJP) — это бинарный протокол, созданный ради избавления от избыточности HTTP. AJP гораздо более эффективен, обладает высокой производительностью благодаря значительной оптимизации и отлично масштабируется.
AJP обычно используется для балансировки нагрузки, когда один или несколько внешних веб-серверов (front-end) отправляют запросы на сервер (или серверы) приложений. Сессии направляются к нужному благодаря механизмам роута, где каждый сервер приложений получает свое имя.
В современных Tomcat используется AJP 1.3 (AJP13). Поскольку это двоичный протокол, браузер напрямую не может отправлять запросы AJP13. Поэтому в качестве фронтенда выступает любой популярный веб-сервер — nginx, Apache, IIS.
Вариант взаимодействия с сервером Tomcat через связку веб-сервер — AJP
WWW
Подробнее о протоколе ты можешь прочитать в официальной документации.
По дефолту Tomcat принимает запросы AJP на порте 8009.
/tomcat9.0.30/conf/server.xml
<!-- Define an AJP 1.3 Connector on port 8009 --> <Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />
Первые РґРІР° байта РІ пакете — это Magic, который меняется в зависимости от направления отправки. Пакеты, отправленные от веб-сервера к контейнеру Tomcat, начинаются с 0x1234, Р° РѕС‚ контейнера Рє веб-серверу — 0x4142 (строка AB, если переводить в ASCII). В рамках уязвимости нас интересует только структура пакета, отправленного от клиента к контейнеру, то есть 0x1234.
Сразу привлекает внимание пакет СЃ РєРѕРґРѕРј 0x7 (Shutdown), который выключает сервер. Спешу тебя разочаровать — пакет такого плана обработается только в том случае, если он отправлен с хоста, на котором запущен Tomcat.
Нас интересует код 0С…2. РЎ таким РєРѕРґРѕРј отправляются, например, обычные сообщения типа GET/POST. Формат тела такого сообщения выглядит следующим образом.
// Translates integer codes to names of HTTP methods private static final String [] methodTransArray = { "OPTIONS", "GET", "HEAD", "POST", "PUT", "DELETE", "TRACE", ...
В нашем пакете используется метод GET, поэтому и значение байта — 0x2.
Далее РІСЃРµ содержимое пакета довольно привычно Рё похоже РЅР° обычный HTTP-запрос. После параметра is_ssl начинается блок заголовков запроса (request_headers). Следующие РґРІР° байта (num_headers) отвечают за общее количество заголовков в запросе. Следом за ним перечисляются сами хидеры. Каждый заголовок имеет следующий формат:
// id's for common request headers public static final int SC_REQ_ACCEPT = 1; public static final int SC_REQ_ACCEPT_CHARSET = 2; public static final int SC_REQ_ACCEPT_ENCODING = 3; public static final int SC_REQ_ACCEPT_LANGUAGE = 4; public static final int SC_REQ_AUTHORIZATION = 5; public static final int SC_REQ_CONNECTION = 6; public static final int SC_REQ_CONTENT_TYPE = 7; public static final int SC_REQ_CONTENT_LENGTH = 8; public static final int SC_REQ_COOKIE = 9; public static final int SC_REQ_COOKIE2 = 10; public static final int SC_REQ_HOST = 11; public static final int SC_REQ_PRAGMA = 12; public static final int SC_REQ_REFERER = 13; public static final int SC_REQ_USER_AGENT = 14;
Некоторые заголовки крайне важны, например если content-length (0xA008) ненулевой, то Tomcat будет предполагать, что запрос имеет тело (как, например, POST-запрос), Рё попытается прочитать отдельный пакет.
// Integer codes for common (optional) request attribute names public static final byte SC_A_CONTEXT = 1; // XXX Unused public static final byte SC_A_SERVLET_PATH = 2; // XXX Unused public static final byte SC_A_REMOTE_USER = 3; public static final byte SC_A_AUTH_TYPE = 4; public static final byte SC_A_QUERY_STRING = 5; public static final byte SC_A_JVM_ROUTE = 6; public static final byte SC_A_SSL_CERT = 7; public static final byte SC_A_SSL_CIPHER = 8; public static final byte SC_A_SSL_SESSION = 9; public static final byte SC_A_SSL_KEY_SIZE = 11; public static final byte SC_A_SECRET = 12; public static final byte SC_A_STORED_METHOD = 13;
Однако любое количество других атрибутов может быть передано и через тип req_attribute (0x0A). Пара РёРјСЏ:значение атрибута передается сразу же после указания этого кода. В этом случае структура примет следующий вид:
Например, так передаются переменные окружения. После перечисления необходимых атрибутов отправляется байт-терминатор (0xFF), который означает не только конец списка атрибутов, но и окончание всего пакета. Именно до него считается длина тела пакета.