Статья CVE-2026-35031 Jellyfin RCE: от загрузки субтитров до root через ld.so.preload

Плата одноплатного компьютера на чёрном антистатическом коврике: поднятый чип с подгоревшей пайкой, диагональный разрез на краю PCB, надпись CVE-2026-35031 на микросхеме.


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 отражают три уровня проблемы в коде.

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: содержимое для записи]
Значение Format уходит в path concatenation без санитизации. Базовый путь /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 chainMITRE ATT&CKПрименение в цепочке
Initial AccessExploit Public-Facing Application (T1190)Эксплуатация subtitle upload через сеть
ExecutionUnix Shell (T1059.004)Код в constructor .so → shell
Privilege EscalationExploitation for Privilege Escalation (T1068)От low-priv user до root через preload
DiscoveryFile and Directory Discovery (T1083)Чтение ФС через .strm-файлы
PersistenceWeb Shell (T1505.003)Backdoor через arbitrary file write
C2Ingress 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
Три правила покрывают ld.so.preload-инъекцию и аномальную запись в директорию данных Jellyfin (конкретный путь субтитров зависит от конфигурации инсталляции).

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.
 
Мы в соцсетях:

Взломай свой первый сервер и прокачай скилл — Начни игру на HackerLab

Похожие темы

🚀 Первый раз на Codeby?
Гайд для новичков: что делать в первые 15 минут, ключевые разделы, правила
Начать здесь →
🔴 Свежие CVE, 0-day и инциденты
То, о чём ChatGPT ещё не знает — обсуждаем в реальном времени
Threat Intel →
💼 Вакансии и заказы в ИБ
Pentest, SOC, DevSecOps, bug bounty — работа и проекты от проверенных компаний
Карьера в ИБ →

HackerLab