Пишем Python-скрипт для детекта CVE-2026 в Apache Struts по сигнатуре ответа сервера

5 октября 2026 года Apache Software Foundation выпустила бюллетень S2-075: CVE-2026-104711 — OGNL-инъекция через legacy RESTful action mapper. CVSS 9.8 (CRITICAL), CWE-917 (Expression Language Injection — когда пользовательский ввод попадает прямо в язык выражений и исполняется на сервере). Затронуты Apache Struts от ветки 2.x до 7.x (Struts 7.x уязвим только при отключённом OGNL allowlist, который по умолчанию включён). Через неделю на GitHub появились первые PoC-репозитории (uday952/CVE-2026-104711). Шаблоны автоматических сканеров подтягиваются с задержкой в несколько дней. Вот в этом окне — между раскрытием критической уязвимости и готовым шаблоном сканера — самописный Python-скрипт для детекта по сигнатурам HTTP-ответа становится единственным быстрым способом проверить периметр.
Ниже — как устроен такой детектор: что искать в ответах сервера, как собрать его на requests, где проверить на стенде и почему он не заменяет полноценный сканер.
Зачем писать самописный сканер уязвимостей на Python вместо ожидания шаблона
Между раскрытием CVE и появлением шаблона в Nuclei, плагина Nessus или модуля Metasploit проходит от 3 до 14 дней для критических уязвимостей. Для менее приоритетных — недели. При этом по данным IBM X-Force Threat Intelligence Index 2025, среднее время между публикацией CVE и устранением в организации — 29 месяцев. Эксплойты остаются самым частым вектором начального доступа: 38% расследований Mandiant M-Trends 2025 начались с эксплуатации известной уязвимости.
Конкретный сценарий: утром вышел бюллетень S2-075. К обеду нужно ответить — есть ли на периметре клиента уязвимый Struts? Ждать шаблон некогда. Писать полноценный эксплойт (PoC — Proof of Concept, демонстрационный код, который подтверждает: да, уязвимость реально эксплуатируема) тоже не задача: нужно обнаружить, а не ломать.
Если смотреть через призму MITRE ATT&CK (открытая база тактик и техник атак; T-коды вроде T1190 — её идентификаторы), наш скрипт реализует технику T1595.002 — Vulnerability Scanning (тактика Reconnaissance). Атакующий с CVE-2026-104711 действует по T1190 — Exploit Public-Facing Application (тактика Initial Access). Задача защитника — найти поверхность атаки раньше, чем это сделает кто-то с другими намерениями. Автоматизация пентеста на Python тут не роскошь, а рабочая необходимость.
Бизнес-логика атаки: зачем злоумышленнику эта уязвимость
OGNL (Object-Graph Navigation Language) — язык выражений, через который Struts привязывает данные из HTTP-запроса к Java-объектам. Если атакующий может внедрить своё OGNL-выражение — он получает RCE (Remote Code Execution, удалённое выполнение кода). Проще говоря: произвольные команды на сервере. Цепочка на практике:
- Атакующий отправляет специально сформированный HTTP-запрос к приложению на Struts.
- Legacy RESTful action mapper обрабатывает запрос без фильтрации OGNL-выражений.
- Выражение исполняется в контексте JVM сервера — атакующий получает шелл, вытаскивает данные, двигается вглубь сети.
Эталонный кейс: CVE-2017-5638 (другой вектор инъекции — OGNL-выражение в HTTP-заголовке Content-Type исполняется при некорректной обработке исключений в Jakarta Multipart parser, CWE-755; CVSS 9.8) привела к утечке данных 147 миллионов пользователей Equifax. Эта CVE включена в каталог CISA KEV (Known Exploited Vulnerabilities — реестр уязвимостей, активно используемых в атаках) с пометкой об использовании в ransomware-кампаниях. По данным EPSS (Exploit Prediction Scoring System — оценка вероятности от 0 до 1, что уязвимость начнут эксплуатировать в ближайшие 30 дней), CVE-2017-5638 до сих пор держит значение 1.0000 — абсолютный максимум. Верифицированные эксплойты лежат на Exploit-DB (EDB-41614 — Metasploit-модуль, remote/multiple; EDB-41570 — standalone PoC, webapps/linux, автор Vex Woo). Новая CVE-2026-104711 — не аномалия. Это системный паттерн фреймворка, построенного вокруг OGNL.
CVE-2026-104711 и Apache Struts уязвимости 2026 года
Только в октябре 2026 Apache выпустила четыре бюллетеня по Struts. CVE с разбивкой по критичности и вероятности эксплуатации:
| CVE | CVSS | Severity | CWE | EPSS | Что ломается |
|---|---|---|---|---|---|
| CVE-2026-104711 | 9.8 | CRITICAL | CWE-917 | 0.0119 | OGNL-инъекция через RESTful mapper, RCE |
| CVE-2026-104714 | 8.8 | HIGH | CWE-362 | 0.0068 | Race condition: данные одного пользователя в ответе другого (требует PR:L — низкие привилегии) |
| CVE-2026-104712 | 7.5 | HIGH | CWE-405 | 0.0095 | Amplification через BigDecimal, DoS |
| CVE-2026-104713 | 6.5 | MEDIUM | CWE-770 | 0.0069 | Unbounded body read в REST plugin, heap exhaustion |
В августе 2026 были закрыты ещё пять CVE (от CVE-2026-73631 до CVE-2026-73635), затрагивающие JSON-плагин (CWE-567 — exposure of data to wrong session), обработку CSP-отчётов и кэширование локалей. CVSS от 4.3 до 7.5 (MEDIUM–HIGH). Общий счёт за 2026 год — девять новых CVE по одному фреймворку (в рамках рассмотренных бюллетеней).
Разберём CVSS-вектор главной цели — CVE-2026-104711: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H. Для тех, кто видит такой вектор впервые, — вот что означает каждый компонент:
- AV:N — атака по сети (не нужен физический или локальный доступ)
- AC:L — низкая сложность эксплуатации (специальные условия не требуются)
- PR:N — привилегии не нужны (анонимный доступ)
- UI:N — участие пользователя не требуется (атака полностью автономна)
- C:H / I:H / A:H — полный импакт на конфиденциальность, целостность, доступность
Читать это нужно так: любой анонимный пользователь из интернета, без каких-либо привилегий и без участия жертвы, может получить полный контроль над сервером. Худший сценарий из возможных.
По данным CISA-ADP (обогащённые данные для принятия решений): активная эксплуатация CVE-2026-104711 пока не зафиксирована (Exploitation: none), но уязвимость автоматизируема (Automatable: yes) с полным техническим импактом (Technical Impact: total). Создать эксплойт можно массово и без ручной настройки под каждую цель. Вопрос не «если», а «когда».
Для масштаба: по неофициальным оценкам отрасли (Sonatype, HeroDevs), значительная доля загрузок пакетов org.apache.struts:* с Maven Central приходится на EOL-версии Struts 2.x, которые никогда не получат официальный патч.
Детект уязвимости по HTTP-ответу: сигнатуры Apache Struts
Прежде чем открывать редактор, нужно понять, что именно искать. Struts-приложение оставляет характерные следы в HTTP-ответах — даже если разработчик не хотел их показывать. Наш детектор отправляет безопасные GET-запросы (никакой эксплуатации) и анализирует результат.
URL-паттерны. Расширения .action и .do — классические маркеры Struts. Запрос к несуществующему эндпоинту вроде /nonexistent_zz.action на Struts-сервере вернёт специфическую страницу ошибки фреймворка, а не стандартный 404 веб-сервера. На обычном Nginx или Apache httpd без Java-стека такой запрос вернёт HTML с «Not Found» без Java-специфичных строк.
Тело ответа. Что ищем:
org.apache.struts2в stack trace — прямое подтверждение фреймворкаStruts Problem Report— стандартная страница ошибки Struts при включённом devModeThere is no Action mapped— сообщение маршрутизатора, когда action не найденstruts.devMode— конфигурационный параметр, который утекает при ошибках
Заголовки ответа. X-Powered-By иногда содержит указание на сервлет-контейнер (Tomcat, Jetty, WildFly). Заголовок Server: Apache-Coyote/1.1 косвенно указывает на Java-стек. Прямого заголовка «Struts» почти никогда не бывает, но связка контейнера и поведения при ошибках даёт достаточную уверенность.
Специфика CVE-2026-104711 — legacy RESTful action mapper. Уязвимость затрагивает только приложения с legacy RESTful action mapper. Такие приложения маршрутизируют запросы по REST-паттернам (/orders/1, /users/42) вместо классических /resource.action. Если при запросе к REST-style URL ответ содержит Struts-специфичные строки — поверхность атаки для CVE-2026-104711 вероятно присутствует. Приложения с default action mapper, restful2 mapper или REST plugin этой конкретной CVE не подвержены.
Собираем Python-скрипт для детекта CVE-2026 Apache Struts
Предпосылки: Python 3.8+, библиотека requests (установка: pip install "requests>=2.32.0"). Тестовый стенд — Docker-образ из vulhub (коллекция уязвимых окружений в Docker-контейнерах, доступна на GitHub) или любой сервер, на сканирование которого есть письменное разрешение. Запуск скрипта по чужим системам без авторизации — административное или уголовное правонарушение.
Делай раз — определяем присутствие Struts
Функция отправляет GET к заведомо несуществующему .action-эндпоинту и ищет в ответе характерные строки. Нашлась хотя бы одна — сервер с высокой вероятностью работает на Struts.
import requests
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) # подавляем предупреждения при verify=False
STRUTS_SIGS = [
"org.apache.struts2",
"Struts Problem Report",
"There is no Action mapped",
"struts.devMode",
]
def fingerprint_struts(base_url):
# verify=False отключает проверку TLS — используйте только на изолированных стендах, иначе возможен MITM
try:
resp = requests.get(f"{base_url}/zz_check_nonexist.action", timeout=10, verify=False)
hits = [s for s in STRUTS_SIGS if s in resp.text]
return {"code": resp.status_code, "sigs": hits, "server": resp.headers.get("Server", "")}
except requests.RequestException as e:
print(f"[!] Connection error: {e}")
return {"code": None, "sigs": [], "server": ""}
Что тут происходит: скрипт запрашивает action, которого гарантированно не существует. Struts обработает запрос своим маршрутизатором и вернёт собственную страницу ошибки. Обычный веб-сервер без Struts отдаст стандартный HTML без Java-специфичных строк. Ожидаемый вывод при обнаружении: {"code": 404, "sigs": ["There is no Action mapped"], "server": "Apache-Coyote/1.1"}.
Делай два — проверяем поверхность атаки CVE-2026-104711
Struts найден — теперь проверяем, отвечает ли приложение на REST-style URL без расширения .action. Это косвенный индикатор того, что используется RESTful action mapper — тот самый компонент, через который проходит OGNL-инъекция CVE-2026-104711.
REST_PATHS = ["/orders/1", "/api/status", "/rest/test"] # примерные пути; адаптируйте под реальную структуру приложения
def check_rest_mapper(base_url):
results = []
for path in REST_PATHS:
try:
# verify=False — только для тестовых стендов (см. GHSA-9wx4-h78v-vm56)
resp = requests.get(f"{base_url}{path}", timeout=10, verify=False)
has_struts = any(s in resp.text for s in STRUTS_SIGS + ["ActionSupport", "namespace"])
results.append({"path": path, "code": resp.status_code, "struts_body": has_struts})
except requests.RequestException:
results.append({"path": path, "code": None, "struts_body": False})
return results
Скрипт перебирает типичные REST-пути и смотрит, содержит ли ответ Struts-маркеры. Если при обращении к /orders/1 в теле появляется ActionSupport или namespace — Struts маршрутизирует REST-запросы. А это необходимое условие для CVE-2026-104711. Список REST_PATHS стоит расширить под конкретное приложение, если известна его структура.
Делай три — запускаем и читаем результаты
Для тестирования берём Docker-стенд. Запуск уязвимого Struts-окружения в vulhub — одна команда: cd vulhub/struts2/<сценарий> && docker-compose up -d. После этого приложение доступно на http://localhost:8080.
if __name__ == "__main__":
target = "http://localhost:8080"
print(f"[*] Target: {target}")
fp = fingerprint_struts(target)
if fp["sigs"]:
print(f"[+] Struts detected: {fp['sigs']} | Server: {fp['server']}")
for r in check_rest_mapper(target):
tag = "[!] REST mapper" if r["struts_body"] else "[-] No REST"
print(f" {tag}: {r['path']} -> HTTP {r['code']}")
else:
print(f"[-] No Struts signatures at {target}")
Как читать результат:
[+] Struts detectedс конкретными сигнатурами — фреймворк найден, дальше проверяем REST mapper.[!] REST mapperрядом с путём — этот путь маршрутизируется через Struts. Приложение потенциально уязвимо к CVE-2026-104711. Следующий шаг — ручной анализ в Burp Suite.[-] No REST— REST-путь не связан со Struts. Может обрабатываться другим фреймворком (Spring, JAX-RS).[-] No Struts signatures— сервер не похож на Struts. Возможен false negative (об этом ниже).
Границы самописного детектора и типичные ошибки
Самописный Python-скрипт — это разведка, не приговор. Вот конкретные сценарии, где он ошибётся.
False negatives (пропуски). Struts в production-режиме с struts.devMode=false подавляет stack traces и стандартные сообщения об ошибках. Большинство сигнатур из STRUTS_SIGS просто не попадёт в ответ. Кастомные error pages усугубляют ситуацию: если разработчик заменил страницу ошибки — сигнатуры исчезнут полностью. WAF (Web Application Firewall — межсетевой экран уровня приложений, фильтрующий HTTP-трафик) может фильтровать или модифицировать ответы. Все три случая означают одно: скрипт скажет «не Struts», хотя сервер уязвим.
False positives (ложные срабатывания). Строка org.apache.struts2 может встретиться в статических страницах документации JavaDoc, если приложение их хостит. REST-style URL не означает RESTful action mapper — приложение может использовать REST plugin (который CVE-2026-104711 не подвержен) или вообще другой маршрутизатор.
Что скрипт не проверяет. Конкретную версию Struts — для этого нужен доступ к META-INF/MANIFEST.MF или анализ поведения, специфичного для конкретной минорной версии. Включён ли OGNL-allowlist в Struts 7 — по advisory, он включён по умолчанию, и при активном allowlist Struts 7 не уязвим. Точный тип action mapper — отличить legacy RESTful от restful2 без доступа к конфигурации struts.xml невозможно снаружи сети.
Расширить скрипт на другие октябрьские CVE можно: для CVE-2026-104712 (amplification через BigDecimal, CWE-405) нужно отправить запрос с параметром типа param=1E999999 и измерить размер ответа относительно размера запроса. Для CVE-2026-104713 (unbounded body read в REST plugin, CWE-770) — проверить, принимает ли эндпоинт REST plugin произвольно большое тело без ограничения. Подход тот же: безопасный запрос → анализ поведения ответа.
Типичная ошибка — запускать детектор по всему периметру без предварительной фильтрации. Если на периметре 500 хостов, а Java-приложения стоят на 20 — сначала отфильтруйте по баннерам (Server: Apache-Coyote, X-Powered-By: Servlet) и только к ним применяйте Struts-детект. Экономит время и снижает шум в логах SOC.
Умение написать такой скрипт — то, что отделяет пентестера от оператора сканера. Готовые инструменты закрывают большинство задач, но свежие CVE, нестандартные конфигурации и кастомные приложения — это всегда ручная работа. По данным Mandiant M-Trends 2025, 57% организаций узнают об инциденте от внешней стороны. Часть этих «внешних сторон» — пентестеры, которые обнаружили поверхность атаки быстрее злоумышленника именно потому, что не ждали обновления сканера.
Если Apache Struts — часть стека, с которым приходится работать (а в enterprise-Java это до сих пор сотни тысяч загрузок в неделю с Maven Central), умение собрать детектор за вечер — производственная необходимость. Вопрос не в том, появится ли следующая OGNL-инъекция в Struts, а когда. CVE-2017-5638, CVE-2024-53677 (CVSS 9.5, CWE-434, EPSS 0.7014 — Top 1% по вероятности эксплуатации), CVE-2026-104711 — не случайные баги, а закономерность фреймворка, построенного вокруг выражений OGNL. Каждый следующий бюллетень будет порождать такое же окно между disclosure и шаблоном сканера. Тот, кто умеет закрывать это окно руками и Python-скриптом, стоит на рынке дороже того, кто ждёт кнопки «Scan». Если хочется не просто читать статьи, а пройти базу системно — IB Basics закрывает фундамент за пару месяцев, дальше уже адресные техники вроде этой.
Эту тему и смежные навыки разбирают на практике в курсе «Python для пентестера» Codeby Academy.
