Одна ошибка, две уязвимости: разбор машины Principal с HTB

Одна ошибка, две уязвимости: разбор машины Principal с HTB

Всем привет, меня зовут Артем Маркелов, и я специалист по анализу защищенности в компании 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);

Разделить центры сертификации по зонам ответственности;

Разделить центры сертификации по зонам ответственности;

Аудировать выданные сертификаты.

Аудировать выданные сертификаты.

← Cybersecurity