CVSS 9.9, три CWE в одном HTTP-параметре, шесть шагов от загрузки .srt-файла до root shell. CVE-2026-35031 в Jellyfin до версии 10.11.7 позволяет аутентифицированному пользователю с правом загрузки субтитров записать файл в произвольную директорию Linux-хоста - и раскрутить это до полного захвата системы через
ld.so.preload. Поле Format в эндпоинте POST /Videos/{itemId}/Subtitles не проходит ни whitelist-валидацию, ни канонизацию пути, ни фильтрацию ..-последовательностей. Значение /../../../etc/ld.so.preload подставляется в путь файла как легитимное расширение - и всё, дальше glibc сделает остальное. Разбираю конкретную механику каждого шага: от конкатенации строк в .NET-коде до момента, когда glibc загрузит вредоносный shared object при следующем запуске процесса.Корневая причина: три CWE в одном параметре Format
По advisory GHSA-j2hf-x4q5-47j3 и записи NVD, уязвимость затрагивает все версии Jellyfin ниже 10.11.7. Вектор CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H - разберём покомпонентно, потому что каждый элемент здесь работает на атакующего:- AV:N - эксплуатация по сети, достаточно HTTP-доступа к Jellyfin (дефолтный порт 8096)
- AC:L - низкая сложность, никаких race condition или специфических предусловий
- PR:L - нужен не admin, а обычный пользователь с правом Upload Subtitles
- UI:N - взаимодействие жертвы не требуется
- S:C (Scope Changed) - уязвимость в Jellyfin выходит за границы приложения и бьёт по хостовой ОС
CWE-20 (Improper Input Validation). Поле
Format принимает произвольную строку. Предполагается значение из набора srt, vtt, ass, ssa - по факту передаётся что угодно, и значение уходит в конкатенацию пути без проверки. По CWE-каталогу: «продукт получает ввод, но не валидирует или некорректно валидирует, что ввод имеет свойства, необходимые для безопасной обработки». Классика.CWE-22 (Path Traversal). Прямое следствие CWE-20. Значение
Format подставляется в строку пути: базовая директория /var/lib/jellyfin/subtitles/ конкатенируется с пользовательским вводом. Когда Format содержит /../../../etc/ld.so.preload, resolved path - /etc/ld.so.preload. Ни вызова канонизации с проверкой границ, ни whitelist допустимых значений.CWE-187 (Partial String Comparison). Менее очевидный, но ключевой компонент. По CWE-каталогу: «сравнение проверяет только часть фактора перед определением совпадения». Если в коде Jellyfin и присутствует проверка формата, она оценивает лишь фрагмент строки - суффикс, префикс или подстроку - позволяя traversal-последовательностям проскочить. Проверка «содержит ли строка подстроку
srt» принципиально отличается от проверки «равна ли строка строго одному из допустимых значений». Это две разные вселенные.Jellyfin - приложение на .NET/C#, не Java (ряд публичных разборов ошибочно указывает Java - Jellyfin написан на .NET/C# и запускается через dotnet, а не JVM). Для понимания ld.so.preload-инъекции это критично: процесс
dotnet при запуске на Linux проходит через glibc и подчиняется механизму preload точно так же, как нативные бинари.Цепочка эксплуатации Jellyfin RCE: от subtitle upload до root shell
Полная цепочка CVE-2026-35031 - шесть шагов. Preconditions, каждый шаг и альтернативные пути эскалации.Recon: fingerprinting уязвимой версии Jellyfin
Эндпоинт/System/Info/Public доступен без аутентификации и возвращает версию сервера, имя и ОС в JSON. curl -s http://target:8096/System/Info/Public | jq .Version покажет строку версии - если ниже 10.11.7, инстанс подвержен CVE-2026-35031 (и трём смежным CVE, исправленным в том же релизе: CVE-2026-35032, CVE-2026-35033 и CVE-2026-35034).Контекст применимости. Уязвимость наиболее реалистична на внутреннем пентесте: Jellyfin чаще развёрнут в домашней сети или сегменте мелкого бизнеса, где порт 8096 торчит наружу без фильтрации. На внешнем периметре Jellyfin обычно стоит за reverse proxy (nginx, Caddy), и субтитровый эндпоинт может быть закрыт - но если proxy пробрасывает все пути, цепочка работает.
Preconditions. Нужна учётная запись с правом Upload Subtitles. По умолчанию это право есть у администраторов. Обычному пользователю его должны выдать явно. Однако на домашних и small-business инсталляциях расширенные права у всех пользователей - я такое встречаю постоянно.
Path traversal и arbitrary file write через поле Format
Ключевой запрос - POST к эндпоинту загрузки субтитров с traversal-значением в параметре Format:
Код:
POST /Videos/{itemId}/Subtitles HTTP/1.1
Host: target:8096
Authorization: MediaBrowser Token="<user_token>"
Content-Type: multipart/form-data
Format: /../../../etc/ld.so.preload
Language: en
[Binary payload: содержимое для записи]
/var/lib/jellyfin/subtitles/ + пользовательский ввод /../../../etc/ld.so.preload = resolved path /etc/ld.so.preload. Для получения itemId достаточно любого элемента медиатеки - ID доступен через API после аутентификации. Токен авторизации получается через POST /Users/AuthenticateByName.Ограничение по правам ОС. Запись файла происходит с привилегиями процесса Jellyfin. Если сервис запущен под выделенным пользователем
jellyfin (рекомендуемая конфигурация), запись в /etc/ld.so.preload будет заблокирована - файл принадлежит root:root с правами 644. Но во многих реальных деплоях (Docker-контейнеры без drop capabilities, ручная установка, legacy-конфигурации) Jellyfin работает от root. Тут решает разведка: ps aux | grep jellyfin на скомпрометированном хосте или косвенные признаки через API дадут понять, под каким пользователем крутится процесс.Чтение файлов, извлечение базы данных и эскалация привилегий
Arbitrary file write - мощный примитив, но для полной цепочки CVE-2026-35031 нужно ещё и чтение. Трюк из advisory использует.strm-файлы: Jellyfin поддерживает stream files, где содержимое файла - путь к медиаресурсу. Через path traversal атакующий записывает .strm-файл, указывающий на /var/lib/jellyfin/data/jellyfin.db (SQLite-база Jellyfin). После индексации .strm-файла библиотекой (автоматический или ручной library scan) содержимое БД выгружается через стандартные download/stream-эндпоинты Jellyfin - точная механика описана в advisory GHSA-j2hf-x4q5-47j3.Из базы данных извлекаются хеши паролей пользователей (включая admin), API-ключи и конфигурация сервера. С административным доступом открываются дополнительные поверхности атаки - управление плагинами, LiveTV, полный контроль над правами пользователей.
Показательный момент: в том же релизе 10.11.7 закрыты ещё три CVE, две из которых дают альтернативные пути к тому же результату. CVE-2026-35033 (CVSS 9.3 по CVSS:4.0, unauthenticated file read через ffmpeg argument injection в StreamOptions) - тут даже аутентификация не нужна. CVE-2026-35032 (CVSS 8.6 по CVSS:4.0, SSRF через LiveTV M3U tuner с выходом на экстракцию БД). Если вектор через субтитры заблокирован - проверяйте остальные, корневая причина одна.
RCE: ld.so.preload injection при перезапуске процесса
Финальный шаг цепочки arbitrary file write → RCE - запись пути к вредоносному shared object в/etc/ld.so.preload. Механизм:
📚 Часть контента скрыта. Этот материал доступен участникам сообщества с рангом One Level или выше
Получить доступ просто — достаточно зарегистрироваться и проявить активность на форуме
Получить доступ просто — достаточно зарегистрироваться и проявить активность на форуме
Место в kill chain и операционный контекст пентеста
CVE-2026-35031 покрывает несколько этапов цепочки атаки:| Этап kill chain | MITRE ATT&CK | Применение в цепочке |
|---|---|---|
| Initial Access | Exploit Public-Facing Application (T1190) | Эксплуатация subtitle upload через сеть |
| Execution | Unix Shell (T1059.004) | Код в constructor .so → shell |
| Privilege Escalation | Exploitation for Privilege Escalation (T1068) | От low-priv user до root через preload |
| Discovery | File and Directory Discovery (T1083) | Чтение ФС через .strm-файлы |
| Persistence | Web Shell (T1505.003) | Backdoor через arbitrary file write |
| C2 | Ingress Tool Transfer (T1105) | Загрузка .so-payload через subtitle upload |
Паттерн в Jellyfin. Четыре CVE в одном релизе 10.11.7 - три критические/высокие (CVE-2026-35031, CVE-2026-35032, CVE-2026-35033) и одна DoS средней критичности (CVE-2026-35034, CVSS 6.5, CWE-400), две позволяют экстракцию БД и эскалацию привилегий. Это не случайность - это архитектурный паттерн: пользовательский ввод попадает в файловые пути и командные строки без полной санитизации. CVE-2025-31499 (обход патча CVE-2023-49096 через FFmpeg argument injection, тоже ведущий к arbitrary file write) подтверждает тенденцию. Для пентестера сигнал однозначный: обнаружили Jellyfin ниже 10.11.7 - проверяйте не одну CVE, а все четыре.
По данным CISA Vulnrichment, для CVE-2026-35031 Exploitation:
none (в дикой природе не зафиксирована), Automatable: no (массовая автоматизация маловероятна из-за необходимости аутентификации), Technical Impact: total. EPSS - 0.0049 при перцентиле 65.6 (выше медианы по всей базе CVE), что указывает на повышенную, но пока умеренную вероятность эксплуатации в ближайшие 30 дней.Где детект срабатывает и где цепочка проходит незамеченной
Цепочка CVE-2026-35031 Jellyfin RCE разбивается на фазы с разной видимостью для защитных систем. Одни шаги орут на весь SIEM, другие - тишина.File Integrity Monitoring. Запись в
/etc/ld.so.preload - высокоаномальное событие. Любой FIM-агент (OSSEC/Wazuh, Tripwire) с правилом на этот файл детектирует запись немедленно. Ручная настройка через auditd:
Bash:
auditctl -w /etc/ld.so.preload -p wa -k preload_write
auditctl -w /etc/ld.so.conf -p wa -k ldconf_write
auditctl -w /var/lib/jellyfin/ -p wa -k jf_write
EDR-специфика. CrowdStrike Falcon детектирует запись в
/etc/ld.so.preload как high-severity event - файл входит в список критических системных конфигов, мониторинг включён в дефолтной политике. Elastic Security 8.x+ с включённым модулем File Integrity Monitoring покрывает тот же вектор. SentinelOne ловит загрузку неизвестного .so через preload на этапе behavioral detection при старте процесса - не запись файла, а именно момент загрузки. Разные подходы, но все три ловят - если стоят на хосте.Слепые зоны. Первый шаг - POST-запрос с path traversal в параметре Format - на уровне EDR невидим. EDR мониторит файловую систему и процессы, но HTTP-запросы к приложению не разбирает. WAF теоретически перехватит
../-последовательности в теле multipart-запроса: ModSecurity с CRS v4 поймает через правило 930100 (Path Traversal Attack). При стандартной конфигурации (SecRequestBodyAccess On) поле Format в multipart-запросе попадает под инспекцию non-file частей (SecRequestBodyNoFilesLimit, по умолчанию 128 КБ) - traversal в Format детектируется даже на дефолтных настройках. Ограничения возникают при очень больших файловых частях (SecRequestBodyLimit) или режиме ProcessPartial. На практике Jellyfin за WAF стоит редко. Это домашний медиасервер, а не enterprise-приложение - кто туда WAF ставит?SIEM. Стандартный лог Jellyfin фиксирует URL и HTTP-статус, но не тело запроса. Sigma-правило на запросы к
/Videos/.*/Subtitles с аномальными паттернами в логах reverse proxy (если тот логирует тело) - возможный, но нестандартный подход. Без reverse proxy с расширенным логированием HTTP-фаза цепочки для SIEM невидима.Docker. Если Jellyfin развёрнут в Docker без монтирования
/etc хоста (без -v /etc:/etc и без --privileged), запись в /etc/ld.so.preload контейнера не влияет на хостовую ОС. Импакт снижается до компрометации контейнера - это существенно ограничивает цепочку, но не обнуляет её.OPSEC для пентестера. Для подтверждения уязвимости без деструктивного воздействия достаточно записать безвредный маркерный файл (пустой файл с уникальным именем) в предсказуемую директорию через path traversal и проверить его наличие через .strm-трюк. Полную цепочку с ld.so.preload запускать только с явного согласия заказчика: ошибка в
.so может привести к невозможности запуска процессов на хосте. Сломать машину заказчика кривым shared object - не тот результат, который хочется показывать в отчёте.CVE-2026-35031 заставляет задать неудобный вопрос: сколько ещё self-hosted приложений с десятками тысяч инсталляций содержат path traversal в file upload эндпоинтах? Jellyfin - open source, код публичный, community активное - и три разных эндпоинта с одной корневой причиной дожили до production-релиза. Проблема не в конкретной CVE, а в подходе: .NET-приложения с file processing логикой системно не применяют canonical path validation на уровне архитектуры. На пентестах self-hosted сервисов (Nextcloud, Gitea, Jellyfin) я проверяю upload-эндпоинты на path traversal первым делом - и нахожу. Arbitrary file write через ld.so.preload - примитив из каждого учебника по Linux privesc. Но он продолжает работать, потому что «домашний медиасервер» стоит на том же хосте, что и Wireguard, Pi-hole, SSH-ключи к production-серверам - и никто не ставит туда EDR. Единственная надёжная мера - обновление до 10.11.7. Временный workaround: отозвать право Upload Subtitles у всех пользователей без административных привилегий, добавить auditd-правило на
/etc/ld.so.preload и запустить Jellyfin под выделенным системным пользователем без доступа к /etc.