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, механизм сессий — это низкоуровневые кирпичи, а не крепость. Их поведение задаёте вы своим конфигом и своим приложением. Поэтому оценивать риск надо не только по версии, но и по фактической конфигурации. При этом обновление никто не отменяет: проверка конфига отвечает на вопрос «горит ли прямо сейчас», а не «можно ли не патчиться».
- Сканер видит номер версии, но не видит init-param readonly у Default Servlet.
- Сканер не знает, какой Manager сессий настроен в context.xml и где лежит хранилище.
- Сканер не заглядывает в WEB-INF/lib и не знает, есть ли там библиотеки для атак десериализации.
- Высокий CVSS и попадание в CISA 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 срабатывает только когда сходятся все четыре условия. Уберите любое — и сценарий выполнения кода распадается. Вот эти четыре условия целиком, в формулировке бюллетеня проекта:
- Запись для Default Servlet включена: init-param readonly выставлен в false (по умолчанию true — запись выключена).
- Поддержка partial PUT включена: параметр allowPartialPut, по умолчанию true.
- Приложение использует файловую персистентность сессий Tomcat (PersistentManager + FileStore) со стандартным местом хранения.
- В приложении есть библиотека, которую можно использовать для атаки десериализации (классический пример — старые версии Commons Collections).
Проверка стенда за двадцать минут
Не гадайте — идите смотреть конфиг. Начните с 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 не гарантирует безопасность. Поэтому я считаю четвёртое условие выполненным «по умолчанию» и опираюсь на первые три. Мой порядок проверки на новом стенде такой:
- Точная версия по version.sh на самом сервере и сравнение с границами 9.0.99 / 10.1.35 / 11.0.3.
- Поиск readonly и allowPartialPut в conf/web.xml и во всех WEB-INF/web.xml приложений.
- Поиск PersistentManager и FileStore во всех context.xml, включая conf/Catalina/<host>/.
- Если FileStore найден — проверка атрибута directory и содержимого рабочего каталога приложения.
- Список jar в WEB-INF/lib с пометкой старых commons-collections, commons-beanutils и подобных.
- Просмотр access-логов на предмет 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.
- Если version.sh показывает 9.0.99+, 10.1.35+ или 11.0.3+ — уязвимый код уже исправлен, остаётся гигиена конфига.
- Если grep по web.xml ничего не нашёл — readonly и allowPartialPut в значениях по умолчанию, запись выключена.
- Если найден FileStore без атрибута directory — это «стандартное место хранения» из бюллетеня.
- Результаты всех команд сохраняйте в файл с датой — это готовое доказательство для заказчика или аудитора.
Признаки эксплуатации в логах и на диске
Если хотя бы первые три условия у вас совпали и сервер смотрел в интернет после марта 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, новые учётки и задания планировщика, а в тяжёлом случае — пересборка с чистого образа и смена ключей, которые хранились на машине.
- PUT-запросы с кодами 201/204 к путям, где приложение не принимает загрузки.
- Серии PUT с одного адреса, за которыми сразу идут GET с необычными cookie JSESSIONID.
- Файлы *.session с нетипичными именами в $CATALINA_BASE/work/Catalina/<host>/<app>.
- Исключения десериализации (ClassNotFoundException, InvalidClassException) в catalina.out рядом по времени с PUT.
- Дочерние процессы java вроде sh, bash, curl или wget, которых приложение не запускает.
Разбор из практики: охранное агентство «Щит-Периметр», 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.
- Проверка четырёх условий и логов заняла около 20 минут, отчёт заказчику — ещё час.
- Обновление ветки 9.0 в пределах минорных версий прошло без правок кода приложения.
- Неиспользуемый commons-collections 3.2.1 удалён из WEB-INF/lib.
- На nginx запрещены PUT и DELETE для портала, которому эти методы не нужны.
- Простой при обновлении — 8 минут в согласованное окно, повторный скан заказчика без замечаний.
Что делать по приоритетам
Раскладываю по важности, чтобы вы не распыляли силы. Первое и главное — обновиться. Минимально исправленные версии — 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;
}- Приоритет 1: обновить Tomcat до актуального релиза своей ветки (не ниже 9.0.99 / 10.1.35 / 11.0.3).
- Приоритет 2: если патч откладывается — readonly=true или allowPartialPut=false, убрать FileStore из стандартного места.
- Приоритет 3: проверить логи и рабочие каталоги на следы эксплуатации, если условия совпадали.
- Приоритет 4: выкинуть неиспользуемые библиотеки, зарезать лишние HTTP-методы на reverse-proxy.
- Не тратьте силы на смену номера версии в HTTP-ответе ради «обмана» сканера — это не защита.
Как встроить это в регулярную работу
Проблема не в одном CVE, а в том, что про Tomcat вспоминают, только когда прилетает скан. У меня на обслуживании это устроено иначе. Во-первых, инвентарь: я держу список всех Java-приложений с точной версией (по version.sh), веткой Tomcat и датой последнего обновления. Когда выходит новый бюллетень на tomcat.apache.org, я за пять минут вижу, кого он касается, а кого нет, и не поднимаю панику там, где её быть не должно.
Во-вторых, конфиг под контролем. Для каждого стенда зафиксировано, включена ли запись Default Servlet, какой Manager у сессий и что лежит в WEB-INF/lib. Это тот самый набор из четырёх пунктов, только собранный заранее. Когда приходит очередной «critical по версии», ответ уже готов, а не собирается ночью в стрессе. Именно это стоит показать заказчику или аудитору как обоснование, почему вы не роняете прод в панике, но и не игнорируете риск.
И в-третьих — трезвость. Tomcat даёт мощные низкоуровневые механизмы, но не гарантирует безопасность сам по себе. readonly, allowPartialPut, персистентность сессий — это ручки, которые кто-то когда-то мог повернуть «ради удобства». Задача админа — знать, в каком положении эти ручки на его серверах, и регулярно обновляться, не дожидаясь писем от сканеров. Тогда любой следующий CVE — это двадцать минут проверки, а не бессонная ночь.
- Раз в месяц сверять версии в инвентаре со страницами security-9/10/11 на tomcat.apache.org.
- Хранить эталонные web.xml и context.xml в системе контроля версий и сравнивать с боевыми через diff.
- Мониторить появление PUT/DELETE в access-логах и слать алерт при первом успешном запросе.
- Раз в квартал проверять зависимости приложений сканером состава (например, OWASP Dependency-Check).
- Держать заранее согласованное окно обновлений, чтобы не выбивать его под каждый новый 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. Тратьте время на патч и конфиг, а не на маскировку.
Источники
- Apache Tomcat 9 — Security Vulnerabilities — Бюллетень ветки 9.0, запись CVE-2025-24813 (Important; условия раскрытия информации и RCE; затронуты 9.0.0.M1–9.0.98, исправлено в 9.0.99), https://tomcat.apache.org/security-9.html
- Apache Tomcat 10 — Security Vulnerabilities — Бюллетень ветки 10.1, запись CVE-2025-24813 (затронуты 10.1.0-M1–10.1.34, исправлено в 10.1.35; сообщено 13.01.2025, раскрыто 10.03.2025), https://tomcat.apache.org/security-10.html
- Apache Tomcat 11 — Security Vulnerabilities — Бюллетень ветки 11.0, запись CVE-2025-24813 (затронуты 11.0.0-M1–11.0.2, исправлено в 11.0.3), https://tomcat.apache.org/security-11.html
- NVD — CVE-2025-24813 — Запись NIST NVD: CVSS 3.1 9.8 (NVD) и 10.0 (CNA), CWE-44/CWE-502/CWE-706, добавлено в CISA KEV 01.04.2025, https://nvd.nist.gov/vuln/detail/CVE-2025-24813
- Apache Tomcat — Which Version Do I Want? — Матрица веток: 11.0.x — Java 17+/Servlet 6.1, 10.1.x — Java 11+/Servlet 6.0, 9.0.x — Java 8+/Servlet 4.0 (поддержка не раньше чем до 31.03.2027); 8.5.x EOL 31.03.2024, https://tomcat.apache.org/whichversion.html
- Apache Tomcat 9 — Default Servlet Reference — Параметры DefaultServlet readonly (по умолчанию true) и allowPartialPut (по умолчанию true), https://tomcat.apache.org/tomcat-9.0-doc/default-servlet.html
- Apache Tomcat 9 — The Manager Component — PersistentManager, FileStore (атрибут directory, по умолчанию временный рабочий каталог приложения), StandardManager pathname SESSIONS.ser, https://tomcat.apache.org/tomcat-9.0-doc/config/manager.html
