Перейти к содержимому

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

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

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, удалённое выполнение кода). Проще говоря: произвольные команды на сервере. Цепочка на практике:

  1. Атакующий отправляет специально сформированный HTTP-запрос к приложению на Struts.
  2. Legacy RESTful action mapper обрабатывает запрос без фильтрации OGNL-выражений.
  3. Выражение исполняется в контексте 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 при включённом devMode
  • There 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.