
Всем привет, меня зовут Артем Маркелов, и я специалист по анализу защищенности в компании SecWare . Мы занимаемся поиском сложных уязвимостей, аудитом корпоративных сетей и в целом тем, что принято называть информационной безопасностью. В свободное от работы время мы с ребятами решаем задачи на HackTheBox – лично для меня это и отдых, и прокачка скиллов, и способ оставаться в тонусе.
Недавно на площадке мне попалась машина Principal. Средний уровень сложности, но концептуально очень интересная. Оба этапа атаки (первоначальный доступ и повышение привилегий) построены на одной и той же фундаментальной ошибке: проверка криптографической оболочки без валидации содержимого внутри нее. Это не просто цепочка уязвимостей, а демонстрация того, как один принцип проявляется в двух совершенно разных технологиях – JWT/JWE и SSH Certificate Authority.
TL;DR
В данной HackTheBox-машине для получения первоначального доступа эксплуатируется CVE-2026-29000 – обход аутентификации в библиотеке pac4j-jwt, в которой токен PlainJWT без цифровой подписи и завернутый в валидный конверт JWE полностью обходит проверку подписи. После подделки токена администратора и извлечения аутентификационных данных SSH из корпоративной панели управления для повышения привилегий используются недостатки в конфигурации центра сертификации SSH, который доверяет любому сертификату. Имя пользователя (principal) не проверяется, что и позволяет подделать сертификат для root.
Цепочка атаки выглядит следующим образом:
Обнаружение pac4j-jwt/6.0.3;
Обнаружение pac4j-jwt/6.0.3;
Получение JWKS – публичного ключа RSA;
Получение JWKS – публичного ключа RSA;
Эксплуатация CVE-2026-29000 – PlainJWT в JWE;
Эксплуатация CVE-2026-29000 – PlainJWT в JWE;
Подделка токена администратора;
Подделка токена администратора;
Доступ к API – /api/users, /api/settings;
Доступ к API – /api/users, /api/settings;
Извлечение учетных данных svc-deploy;
Извлечение учетных данных svc-deploy;
SSH как svc-deploy;
SSH как svc-deploy;
Получение флага пользователя;
Получение флага пользователя;
Поиск доступных файлов – /opt/principal/ssh/ca;
Поиск доступных файлов – /opt/principal/ssh/ca;
Генерация ключевой пары Ed25519;
Генерация ключевой пары Ed25519;
Подпись сертификата с principal root;
Подпись сертификата с principal root;
SSH как root по сертификату;
SSH как root по сертификату;
Получение флага администратора.
Получение флага администратора.
Первым шагом является сканирование IP-адреса цели для выявления незащищенных служб. Для этого запускаем nmap:
Здесь сразу видим несколько ключевых находок:
Jetty на 8080 – Java-сервер, скорее всего Spring или аналогичный фреймворк;
Jetty на 8080 – Java-сервер, скорее всего Spring или аналогичный фреймворк;
pac4j-jwt/6.0.3 – конкретная библиотека с указанием конкретной версии. pac4j – это Java-фреймворк аутентификации, JWT – это модуль, который работает с токенами. Версия 6.0.3 вышла не так давно, но можем поискать на нее свежие уязвимости;
pac4j-jwt/6.0.3 – конкретная библиотека с указанием конкретной версии. pac4j – это Java-фреймворк аутентификации, JWT – это модуль, который работает с токенами. Версия 6.0.3 вышла не так давно, но можем поискать на нее свежие уязвимости;
OpenSSH 9.6 – относительно свежая версия, маловероятны уязвимости самого демона, но возможно, есть ошибки в конфигурации.
OpenSSH 9.6 – относительно свежая версия, маловероятны уязвимости самого демона, но возможно, есть ошибки в конфигурации.
Запускаем Burp Suite, открываем браузер, видим форму логина "Principal Internal Platform". Внизу страницы – версия и технологии: v1.2.0 | Powered by pac4j.
Пробуем стандартные креды (admin/admin, admin/password) – получаем ошибку, но в HTTP History в Burp Suite видим, что запрос уходит на /api/auth/login.
Также в HTTP History видим запрос к /static/js/app.js.
Взглянем поближе на JavaScript-файл, который содержит детальные комментарии разработчика.
Из JS-файла узнаем несколько полезных моментов:
Значимость для атаки
RSA-OAEP-256 + A128GCM
Асимметричное шифрование, нужен публичный ключ
Асимметричная подпись, ключ не экспонируется
Конечная точка JWKS
/api/auth/jwks
Отсюда получим публичный ключ для шифрования
sub, role, iss, iat, exp
Известная структура payload
Запрашиваем JWKS (JSON Web Key Set).
На скриншоте видим:
Обнаружение pac4j-jwt/6.0.3;
Обнаружение pac4j-jwt/6.0.3;
kty: тип ключа RSA;
kty: тип ключа RSA;
e: AQAB – стандартная экспонента 65537;
e: AQAB – стандартная экспонента 65537;
kid: enc-key-1 – ID ключа, подсказка, что это ключ шифрования;
kid: enc-key-1 – ID ключа, подсказка, что это ключ шифрования;
n – сам публичный ключ.
n – сам публичный ключ.
В JWKS только один ключ, и он предназначен для шифрования (по полю kid). Ключа для проверки подписи нет – подпись проверяется сервером своим приватным ключом, публичный клиенту не нужен.
Момент X: находим баг в коде pac4j
Чтобы понять уязвимость, нужно разобраться в архитектуре фреймворка. Pac4j поддерживает разные комбинации JWT:
header.payload
Без подписи, alg: none
SignedJWT (JWS)
header.payload.signature
Только подпись
EncryptedJWT (JWE)
header.encryptedKey.iv.ciphertext.authTag
Только шифрование
Signed then Encrypted
JWS внутри JWE
Сначала подпись, потом шифрование
В HackTheBox-машине используется последний вариант: подписанный JWT заворачивается в JWE. При такой структуре сервер должен:
Расшифровать внешний JWE своим приватным RSA-ключом.
Расшифровать внешний JWE своим приватным RSA-ключом.
Получить внутренний JWT.
Получить внутренний JWT.
Проверить подпись внутреннего JWT публичным ключом.
Проверить подпись внутреннего JWT публичным ключом.
Извлечь claims и авторизовать пользователя.
Извлечь claims и авторизовать пользователя.
Смотрим исходники pac4j-jwt 6.0.3 (класс JwtAuthenticator). Ниже приведён фрагмент метода validate, содержащий уязвимость:
Метод innerJWT.toSignedJWT() предпринимает попытку преобразовать внутренний JWT в SignedJWT. В соответствии с условным оператором, при signedJWT == null исполняется ветка B, которая не выбрасывает исключение, а лишь логирует отладочное сообщение. После этого выполнение продолжается, и claims извлекаются из неподписанного токена.
Уязвимость заключается в отсутствии обработки случая, когда toSignedJWT() возвращает null. Никакой проверки. Корректная реализация должна либо выбрасывать исключение в ветке B, либо требовать обязательного наличия подписи (signedJWT != null) для продолжения обработки.
Найденная ошибка – не теория. NIST подтверждает CVE-2026-29000:
Версии pac4j-JWT до 4.5.9, 5.7.9 и 6.3.3 содержат уязвимость обхода аутентификации в JwtAuthenticator при обработке зашифрованных JWT, что позволяет удаленным злоумышленникам подделывать токены аутентификации. Злоумышленники, обладающие публичным ключом RSA сервера, могут создать PlainJWT, обернутый JWE, с произвольными требованиями субъекта и роли, обходя проверку подписи для аутентификации как любой пользователь, включая администраторов.
Конструирование эксплойта
Для эксплуатации выявленной уязвимости нам понадобится:
Публичный RSA-ключ сервера (получен на этапе разведки через эндпоинт /api/auth/jwks).
Публичный RSA-ключ сервера (получен на этапе разведки через эндпоинт /api/auth/jwks).
Известная структура payload (формат claims: sub, role, iss, iat, exp).
Известная структура payload (формат claims: sub, role, iss, iat, exp).
Процесс конструирования поддельного токена включает следующие шаги:
Ниже – готовый эксплойт, реализованный на Python с использованием библиотеки python-jose:
Полученный JWE передаётся серверу в заголовке Authorization: Bearer <token>. Используя свой приватный RSA-ключ, сервер успешно расшифровывает JWE, извлекает PlainJWT, обнаруживает отсутствие подписи, но (в силу уязвимости) продолжает обработку и извлекает claims sub=admin, role=ROLE_ADMIN.
Запускаем скрипт и вводим IP-адрес цели:
Получаем поддельный JWT администратора.
Успех: API открылся, пароль найден
С помощью Burp Suite Repeater отправляем запрос на /api/users и /api/settings, используя полученный токен.
В ответе на запрос к /api/users находим пользователя с пометкой «Service account for automated deployments via SSH certificate auth.». Данный сервисный аккаунт используется для автоматического развертывания с помощью SSH-сертификата.
Получаем доступ к машине по SSH как пользователь svc-deploy. По стандарту в директории пользователя находим флаг:
Повышение привилегий: ошибка в SSH CA
Из описания машины узнаем, что в ней используется SSH CA. Стандартный SSH использует публичные ключи: authorized_keys содержит список доверенных ключей. SSH CA, в свою очередь, работает как альтернатива классическому методу авторизации по публичным ключам, позволяя централизованно управлять доступом без необходимости распространять ключи на все серверы.
В классическом виде архитектура SSH CA выглядит примерно так:
В этой схеме существуют три основных компонента: сам сервер SSH (sshd), который настроен доверять определенному Certification Authority, клиент, который предъявляет сертификат для аутентификации, и механизм проверки полномочий. Когда клиент пытается подключиться, он отправляет сертификат, представляющий собой его публичный ключ, подписанный приватным ключом CA с дополнительными метаданными. Сервер проверяет подпись публичным ключом CA, указанным в директиве TrustedUserCAKeys, и, если подпись валидна, извлекает из сертификата список principals – имен пользователей, от имени которых разрешен вход.
После получения доступа к системе под учетной записью svc-deploy, читаем файлы конфигурации SSH:
PermitRootLogin установлен в prohibit-password, что означает, что вход под root с использованием пароля заблокирован. Однако, аутентификация на основе сертификата разрешена. Также указан путь до приватного ключа CA. С помощью него можем подписать сертификат от имени любого пользователя, какого захотим, включая root.
Необходимо сгенерировать ключевую пару, подписать сертификат с principal root и, собственно, подключиться.
Генерируем Ed25519 ключ:
Подписываем сертификат, используя найденный приватный ключ CA:
Используем полученный сертификат для подключения по SSH как root:
Оба этапа атаки объединяет одна и та же фундаментальная ошибка: доверие к формату (криптографической оболочке) без проверки содержимого.
В случае с JWT шифрование JWE было валидным, поэтому сервер не проверил подпись внутри. В случае с SSH CA сертификат был подписан доверенным CA, поэтому сервер не проверил имя пользователя в principal.
Рекомендации по устранению
Как полагается, в конце статьи приведем рекомендации по устранению тех уязвимостей, с которыми столкнулись при решении HackTheBox-машины.
Устранение CVE-2026-29000:
Обновить pac4j-jwt до версии 6.3.3 или выше;
Обновить pac4j-jwt до версии 6.3.3 или выше;
Разрешать только ожидаемые алгоритмы (RS256 и т.д.), запрещать none;
Разрешать только ожидаемые алгоритмы (RS256 и т.д.), запрещать none;
Проверять подпись до извлечения claims.
Проверять подпись до извлечения claims.
Усиление SSH CA:
Настроить AuthorizedPrincipalsFile в файле /etc/ssh/auth_principals/(системный_пользователь);
Настроить AuthorizedPrincipalsFile в файле /etc/ssh/auth_principals/(системный_пользователь);
Ограничить principals для привилегированных пользователей (root);
Ограничить principals для привилегированных пользователей (root);
Разделить центры сертификации по зонам ответственности;
Разделить центры сертификации по зонам ответственности;
Аудировать выданные сертификаты.
Аудировать выданные сертификаты.