АйТи Фреш
Главная / Статьи / Безопасность
Безопасность

Tomcat и partial PUT: как проверить, грозит ли RCE именно вашей конфигурации, а не гадать по баннеру версии

Автор: Семёнов Евгений Сергеевич, директор ООО «АйТи-Фреш» · · ~20 мин чтения
Tomcat и partial PUT: как проверить, грозит ли RCE именно вашей конфигурации, а не гадать по баннеру версии
Иллюстрация к статье «Tomcat и partial PUT: как проверить, грозит ли RCE именно вашей конфигурации, а не гадать по баннеру версии».

Пришло письмо от безопасника: «У вас Tomcat 9.0.71, срочно обновляйтесь, там RCE». Паника, ночное окно работ, откат приложения. А по факту у вас readonly=true и никакой файловой сессии — эксплуатировать нечего. Или наоборот: версия «свежая», а конфиг дырявый. Баннер версии не отвечает на главный вопрос — уязвимы ли вы. В этой статье показываю, как за двадцать минут проверить свой стенд по CVE-2025-24813, где искать следы попыток в логах и что закрыть в первую очередь.

Почему номер версии ничего не решает

Я 15 лет обслуживаю чужие серверы, и почти каждый громкий CVE в Tomcat приходит одинаково. Сначала прилетает автоскан от подрядчика по ИБ или от крупного заказчика, который гоняет своих поставщиков через анкету безопасности: «обнаружен Apache Tomcat 9.0.71, критическая уязвимость, устраните в течение N дней». Сканер прочитал номер версии со стандартной страницы ошибки или из заголовка ответа и сопоставил его с базой CVE. Всё. Он не знает, включена ли у вас запись, где хранятся сессии и какие jar лежат в WEB-INF/lib. А именно это и определяет, есть у вас дыра или нет.

CVE-2025-24813 — как раз тот случай, где паника и игнор одинаково вредны. Заголовок «RCE в Tomcat» звучит апокалиптично, NVD ставит ему 9.8 по CVSS 3.1, а CISA 1 апреля 2025 года внесла его в каталог реально эксплуатируемых уязвимостей (KEV). Половина админов кидается обновляться в разгар рабочего дня, роняя прод. Вторая половина машет рукой — «у нас за nginx, не достанут» — и оставляет реально пробиваемый стенд. Правда в том, что удалённое выполнение кода тут возможно только при совпадении сразу четырёх условий, и в конфигурации по умолчанию первое из них не выполняется.

Мой подход простой: middleware — это не готовая граница безопасности. Tomcat, его Default Servlet, механизм сессий — это низкоуровневые кирпичи, а не крепость. Их поведение задаёте вы своим конфигом и своим приложением. Поэтому оценивать риск надо не только по версии, но и по фактической конфигурации. При этом обновление никто не отменяет: проверка конфига отвечает на вопрос «горит ли прямо сейчас», а не «можно ли не патчиться».

Сканер сопоставляет версию с CVE. Он НЕ проверяет ваш конфиг. «Найдена уязвимость» в отчёте автоскана — это гипотеза, а не приговор. Проверять надо руками, но быстро: уязвимость в каталоге KEV.
Цифры и версии: Почему номер версии ничего не решает — схема
Цифры и версии: Почему номер версии ничего не решает. Открыть схему в полном размере

Что на самом деле сломано в CVE-2025-24813

Разберём механику, потому что без неё проверка превращается в шаманство. О проблеме сообщили команде безопасности Tomcat 13 января 2025 года, публично раскрыли 10 марта 2025 года. Затронуты три ветки: 11.0.0-M1–11.0.2 (исправлено в 11.0.3), 10.1.0-M1–10.1.34 (исправлено в 10.1.35) и 9.0.0.M1–9.0.98 (исправлено в 9.0.99). Сами исправленные релизы вышли ещё в феврале 2025-го, за месяц до раскрытия. Проект оценивает уязвимость как Important. NVD даёт 9.8 (вектор AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H), вторичная оценка CNA — 10.0 со Scope: Changed. Классы слабостей — CWE-44 (path equivalence), CWE-502 (десериализация недоверенных данных) и CWE-706.

Суть в реализации частичного PUT (partial PUT — загрузка куска файла с заголовком Content-Range). Как сказано в бюллетене проекта, исходная реализация создавала временный файл, имя которого строилось из переданного пользователем имени и пути, где разделитель пути заменялся на точку «.». Отсюда два сценария. Первый — раскрытие информации или подмешивание содержимого в загружаемые файлы: для него нужно, чтобы защищённые загрузки шли в подкаталог публичных, атакующий знал имена файлов и эти файлы тоже грузились через partial PUT. Второй, громкий, — выполнение кода: если приложение хранит сессии в файлах в стандартном каталоге, атакующий может подложить туда сериализованный объект, который Tomcat затем прочитает и десериализует. Десериализация недоверенного объекта при наличии подходящей библиотеки в classpath и есть путь к выполнению кода.

Ключевое слово — «если». RCE срабатывает только когда сходятся все четыре условия. Уберите любое — и сценарий выполнения кода распадается. Вот эти четыре условия целиком, в формулировке бюллетеня проекта:

RCE = все четыре условия сразу. По умолчанию readonly=true, поэтому «из коробки» запись через Default Servlet отключена. Дыра появляется тогда, когда её открыли конфигом.
Tomcat и partial PUT: как проверить, грозит ли RCE именно вашей конфигурации, а не гадать по баннеру версии — схема
Схема к статье. Открыть схему в полном размере

Проверка стенда за двадцать минут

Не гадайте — идите смотреть конфиг. Начните с Default Servlet. Он объявляется в глобальном $CATALINA_BASE/conf/web.xml, но его могут переопределить в web.xml конкретного приложения. Ищем init-param readonly. Если его нет — действует значение по умолчанию true, запись запрещена, и первое условие не выполнено. Если кто-то явно прописал false — вот здесь загорается красная лампа. Заодно посмотрите allowPartialPut: явное false тоже разрывает цепочку.

Дальше проверяем сессии. Файловая персистентность не включается сама; её надо явно настроить через <Manager className="org.apache.catalina.session.PersistentManager«> со <Store className=»org.apache.catalina.session.FileStore"> в META-INF/context.xml приложения, в conf/context.xml или в файлах conf/Catalina/<host>/*.xml. По умолчанию Tomcat держит сессии в памяти (StandardManager) и сбрасывает их на диск только при остановке в файл SESSIONS.ser — это другой механизм. Для CVE важен именно FileStore без атрибута directory: тогда файлы сессий пишутся во временный рабочий каталог приложения, который назначает контейнер (обычно $CATALINA_BASE/work/Catalina/<host>/<app>).

И третье — пройдитесь по WEB-INF/lib в поисках библиотек, известных по атакам десериализации. Это самый скользкий пункт: полного списка не существует, и отсутствие commons-collections не гарантирует безопасность. Поэтому я считаю четвёртое условие выполненным «по умолчанию» и опираюсь на первые три. Мой порядок проверки на новом стенде такой:

Если readonly=false найден на сервере, доступном из интернета, не ждите окончания проверки остальных условий: сразу верните readonly=true или запретите PUT на прокси, а разбор делайте потом.
Памятка: Проверка стенда за двадцать минут — схема
Памятка: Проверка стенда за двадцать минут. Открыть схему в полном размере

Команды, которые я запускаю первыми

Ниже — то, что реально экономит время. Сначала узнаём точную версию на самом сервере, потом ищем опасные настройки. Скрипт version.sh из состава Tomcat берёт номер из catalina.jar, а номер на странице ошибки и в заголовках настраивается и может не совпадать с реальным.

Проверка точной версии и явно включённой записи:

# точная версия на сервере
$CATALINA_HOME/bin/version.sh | grep 'Server number'

# readonly и allowPartialPut в глобальном web.xml
grep -n -B2 -A2 -E 'readonly|allowPartialPut' $CATALINA_BASE/conf/web.xml

# то же в web.xml приложений
find $CATALINA_BASE/webapps -name web.xml -exec grep -l -E 'readonly|allowPartialPut' {} \;

Проверка файловой персистентности сессий и подозрительных библиотек. Если первая команда что-то нашла, а вторая показывает старый commons-collections, вы близки к полному набору из четырёх условий:

# PersistentManager + FileStore = файловые сессии
grep -rn -E 'PersistentManager|FileStore' \
  $CATALINA_BASE/conf/context.xml \
  $CATALINA_BASE/conf/Catalina \
  $CATALINA_BASE/webapps/*/META-INF/context.xml 2>/dev/null

# библиотеки, известные по атакам десериализации
find $CATALINA_BASE/webapps -path '*/WEB-INF/lib/*' \
  \( -name 'commons-collections*.jar' -o -name 'commons-beanutils*.jar' \)

На Windows-сервере то же самое делается через version.bat и Select-String -Path conf\web.xml -Pattern 'readonly' в PowerShell.

Номер версии в HTTP-ответе ничего не доказывает: его правят атрибутом server у Connector в server.xml или подменой ServerInfo. Верьте version.sh на самом сервере.

Признаки эксплуатации в логах и на диске

Если хотя бы первые три условия у вас совпали и сервер смотрел в интернет после марта 2025 года, одной правки конфига мало — нужно проверить, не опоздали ли вы. Начните с access-логов Tomcat (по умолчанию localhost_access_log.*.txt в $CATALINA_BASE/logs) и логов reverse-proxy. Легитимных PUT в типичном веб-приложении мало или нет вовсе, поэтому любой PUT с ответом 201 или 204 к статическим путям — повод разбираться. Стандартный шаблон AccessLogValve заголовок Content-Range не пишет; если хотите его видеть, добавьте в pattern %{Content-Range}i.

Второе место — рабочий каталог приложения, куда FileStore складывает сессии. Файлы сессий там называются по идентификатору с расширением .session. Настораживать должны файлы .session с необычными именами (с точками, похожими на путь, или не похожими на обычный шестнадцатеричный идентификатор), файлы, созданные вне времени активности пользователей, и неожиданные новые файлы в корне приложения. Параллельно смотрите catalina.out на ошибки десериализации и дочерние процессы java, которых там быть не должно.

# PUT-запросы в access-логах Tomcat
grep -h '"PUT ' $CATALINA_BASE/logs/localhost_access_log.* | awk '{print $1, $4, $6, $7, $9}'

# файлы сессий в рабочих каталогах, изменённые за 30 дней
find $CATALINA_BASE/work -name '*.session' -mtime -30 -ls

# процессы, порождённые JVM Tomcat
ps -ef --forest | grep -A3 '[c]atalina'

Если что-то нашли — не удаляйте файлы сразу. Снимите копию логов и рабочего каталога, изолируйте сервер от внешнего доступа и только потом чистите и обновляйте. Сервер, на котором код, возможно, уже выполнился, патч не лечит: нужна проверка на закреплённые web-shell, новые учётки и задания планировщика, а в тяжёлом случае — пересборка с чистого образа и смена ключей, которые хранились на машине.

Нашли следы — сначала копия логов и рабочего каталога, изоляция, и только потом обновление. Иначе вы уничтожите улики и не узнаете, что успели сделать на сервере.

Разбор из практики: охранное агентство «Щит-Периметр», 27 рабочих мест

Охранное агентство, 27 рабочих мест в офисе и дежурной части, самописный портал на Java — графики смен постов, журналы обходов, отчёты старших смен для заказчиков. Крутится на Apache Tomcat 9.0.71 за nginx, снаружи в него заходят начальники объектов со смартфонов. Отчёт пришёл от крупного заказчика — сети бизнес-центров, чья служба ИБ сканирует внешние сервисы подрядчиков: «Tomcat 9.0.71, CVE-2025-24813, critical, устранить за 10 дней, иначе приостановка доступа». Директор нервничает, разработчик портала давно ушёл с фриланса в штат другой компании, меня зовут разбираться.

Первым делом я не полез обновляться. Полез проверять условия. version.sh подтвердил 9.0.71 — по номеру ветка уязвимая. Дальше conf/web.xml и web.xml приложения: readonly нигде не переопределён, значит действует true. Уже здесь стало ясно, что запись через Default Servlet отключена и RCE-сценарий не собирается. Для порядка проверил остальное: сессии — StandardManager в памяти, FileStore нет. В WEB-INF/lib нашёлся старый commons-collections 3.2.1, то есть подходящая библиотека была, но без первых условий она мёртвый груз. В access-логах за полгода — ни одного PUT с успешным ответом, только отбитые 403 от автосканеров. Итог: реального RCE на этом стенде нет. Я оформил проверку с выводами команд и отправил заказчику как обоснование.

Но обновлять всё равно надо — не под угрозой отключения за 10 дней, а планово. Согласовали окно на воскресное утро, когда в дежурной части один оператор. Я перевёл их с 9.0.71 на актуальную 9.0.x, заодно выкинул commons-collections 3.2.1, который не использовался ни одним живым куском кода, и запретил PUT/DELETE на nginx. Портал поднялся с первого раза, простой — 8 минут. Повторный скан заказчика прошёл чисто. Спокойная проверка дала и правильный ответ заказчику, и нормальный план вместо ночного героизма с откатами.

Отдельно предупрежу про обратную ситуацию, которую я тоже встречал. Другая компания, документооборот на Tomcat: версия новее, но ещё не исправленная, зато кто-то из бывших разработчиков включил readonly=false ради «загрузки файлов через PUT» и настроил PersistentManager+FileStore, чтобы сессии переживали рестарты. Там совпали три условия из четырёх, а четвёртое на практике почти всегда найдётся. Это куда опаснее, чем «страшный» номер версии у агентства, и там мы начали не с планового окна, а с немедленного readonly=true.

Непропатченная версия с readonly=false и файловыми сессиями опаснее той же версии с конфигом по умолчанию. Номер релиза — не единственный показатель защищённости, но и не повод откладывать патч.

Что делать по приоритетам

Раскладываю по важности, чтобы вы не распыляли силы. Первое и главное — обновиться. Минимально исправленные версии — 11.0.3, 10.1.35 и 9.0.99, но ставить надо актуальный релиз своей ветки: на момент написания это 11.0.25 (Java 17+, Servlet 6.1), 10.1.59 (Java 11+, Servlet 6.0) и 9.0.121 (Java 8+, Servlet 4.0). Ветка 8.5 не поддерживается с 31 марта 2024 года, и исправлений для неё не выпускают. Проект обещает поддерживать 9.0 не раньше чем до 31 марта 2027 года. Обновление в рамках своей ветки (9.0.x → 9.0.x) обычно проходит без правок приложения, это первая цель.

Второе — если обновиться прямо сейчас нельзя (приложение привязано к конкретной версии или окно только через месяц), разорвите условия эксплуатации. Верните readonly=true, если запись через Default Servlet вам на самом деле не нужна (почти никогда не нужна — файлы обычно грузит логика приложения, а не сырой PUT). Если запись нужна, выставьте allowPartialPut=false. Уберите файловую персистентность сессий или вынесите хранилище FileStore из стандартного места явным атрибутом directory. Любое из этих действий разрывает цепочку RCE до обновления.

<!-- conf/web.xml, секция servlet default -->
<init-param>
    <param-name>readonly</param-name>
    <param-value>true</param-value>
</init-param>
<init-param>
    <param-name>allowPartialPut</param-name>
    <param-value>false</param-value>
</init-param>

Третье, гигиеническое — вычистите неиспользуемые библиотеки, известные по атакам десериализации. Старый commons-collections в WEB-INF/lib — классика: годами тянется транзитивной зависимостью, а по факту не используется. И не отдавайте наружу голый Tomcat: на nginx или другом reverse-proxy режьте лишние HTTP-методы (PUT/DELETE, если приложение их легитимно не использует). Это не заменяет патч, но снимает целый класс автоматических попыток.

location / {
    limit_except GET HEAD POST { deny all; }
    proxy_pass http://127.0.0.1:8080;
}
Порядок именно такой: сначала патч, если нельзя — разрыв условий конфигом, затем проверка следов и гигиена зависимостей. Смена баннера в этом списке отсутствует намеренно.
Порядок действий: Что делать по приоритетам — схема
Порядок действий: Что делать по приоритетам. Открыть схему в полном размере

Как встроить это в регулярную работу

Проблема не в одном CVE, а в том, что про Tomcat вспоминают, только когда прилетает скан. У меня на обслуживании это устроено иначе. Во-первых, инвентарь: я держу список всех Java-приложений с точной версией (по version.sh), веткой Tomcat и датой последнего обновления. Когда выходит новый бюллетень на tomcat.apache.org, я за пять минут вижу, кого он касается, а кого нет, и не поднимаю панику там, где её быть не должно.

Во-вторых, конфиг под контролем. Для каждого стенда зафиксировано, включена ли запись Default Servlet, какой Manager у сессий и что лежит в WEB-INF/lib. Это тот самый набор из четырёх пунктов, только собранный заранее. Когда приходит очередной «critical по версии», ответ уже готов, а не собирается ночью в стрессе. Именно это стоит показать заказчику или аудитору как обоснование, почему вы не роняете прод в панике, но и не игнорируете риск.

И в-третьих — трезвость. Tomcat даёт мощные низкоуровневые механизмы, но не гарантирует безопасность сам по себе. readonly, allowPartialPut, персистентность сессий — это ручки, которые кто-то когда-то мог повернуть «ради удобства». Задача админа — знать, в каком положении эти ручки на его серверах, и регулярно обновляться, не дожидаясь писем от сканеров. Тогда любой следующий CVE — это двадцать минут проверки, а не бессонная ночь.

Инвентарь версий + зафиксированный конфиг четырёх пунктов = ответ на любой новый Tomcat-CVE за пять минут вместо ночного аврала.

Частые вопросы

У меня Tomcat 9.0.71, сканер пишет critical RCE. Меня точно взломают?

Не обязательно. RCE по CVE-2025-24813 требует одновременно четырёх условий: readonly=false у Default Servlet, включённый partial PUT (по умолчанию включён), файловая персистентность сессий Tomcat со стандартным местом хранения и библиотека в приложении, пригодная для атаки десериализации. По умолчанию readonly=true, поэтому «из коробки» сценарий не собирается. Проверьте конфиг по этим пунктам — сканер их не видит. Но обновиться всё равно нужно: уязвимость в каталоге CISA KEV.

Как быстро понять, включена ли запись через partial PUT?

Ищите init-param readonly и allowPartialPut в conf/web.xml и в web.xml приложения. Если параметров нет — действуют значения по умолчанию: readonly=true (запись отключена), allowPartialPut=true. Опасно, когда кто-то явно прописал readonly со значением false для Default Servlet и не выключил allowPartialPut.

Какие версии затронуты и на какую обновляться?

Затронуты 11.0.0-M1–11.0.2, 10.1.0-M1–10.1.34 и 9.0.0.M1–9.0.98; исправлено в 11.0.3, 10.1.35 и 9.0.99. Ставьте актуальный релиз своей ветки — на момент написания 11.0.25 (Java 17+), 10.1.59 (Java 11+) или 9.0.121 (Java 8+). Ветка 8.5 не поддерживается с 31 марта 2024 года, 9.0 проект обещает поддерживать не раньше чем до 31 марта 2027 года.

Как понять, что уязвимость уже пытались эксплуатировать?

Смотрите access-логи Tomcat и прокси на PUT-запросы с ответами 201/204 к путям, где приложение ничего не принимает, и рабочий каталог приложения ($CATALINA_BASE/work/Catalina/<host>/<app>) на файлы .session с нетипичными именами. Проверьте catalina.out на ошибки десериализации и дочерние процессы JVM. При находках сначала сохраните копии логов и файлов и изолируйте сервер.

Обновиться прямо сейчас не могу — окно только через месяц. Что делать?

Разорвите условия эксплуатации конфигом. Верните readonly=true, если запись через Default Servlet не нужна, или выставьте allowPartialPut=false. Уберите файловую персистентность сессий или вынесите хранилище из стандартного места атрибутом directory у FileStore. Любое из этих действий ломает цепочку RCE на непропатченной версии. И запретите лишние HTTP-методы на reverse-proxy.

Помогает ли то, что Tomcat стоит за nginx?

Частично. Reverse-proxy позволяет запретить PUT/DELETE и отсекает часть автоматических попыток, но это не патч и не заменяет разрыв условий. Если PUT проксируется насквозь к приложению, защита иллюзорна. Прокси — дополнение к обновлению, а не замена.

Стоит ли скрыть версию в HTTP-ответе, чтобы сканер отстал?

Скрыть номер версии со страниц ошибок — нормальная гигиена, но не защита от этой уязвимости: атакующему достаточно проверить поведение сервера, а у вас останется тот же риск. Реальную версию на сервере показывает version.sh. Тратьте время на патч и конфиг, а не на маскировку.

Столкнулись с похожей задачей? Обращайтесь — решим

Если у вас происходит что-то из описанного в этой статье — или любая другая проблема с ИТ-инфраструктурой, — обращайтесь в любое время. Мои специалисты и я лично разберём ситуацию, найдём настоящую причину и доведём до решения.

Возьмёмся и за разовую задачу, и за постоянное обслуживание. Первичная консультация — бесплатно и без обязательств.

📞 +7 903 729-62-41 💬 MAX: +7 903 729-62-41 ✈ Telegram @ITfresh_Boss

С уважением, Семёнов Евгений Сергеевич, директор «АйТи Фреш» — IT-аутсорсинг для компаний до 50 рабочих мест, 15+ лет практики

Источники

© ООО «АйТи-Фреш» · Москва · Все статьи