在本文中,我们将探讨JSON网络令牌(JWT)的设计问题以及不当的处理方式是如何让网站面临各种高危攻击的威胁的。由于JWT最常用于身份认证、会话管理和访问控制机制,因此,这些漏洞有可能危及整个网站及其用户。
如果还不熟悉JWT及其工作原理,那也不用担心——我们会顺便介绍所有相关细节。此外,我们还提供了一些含有相关漏洞的实验环境,这样你就可以针对真实的目标安全地进行渗透测试了。
实验环境
如果您已经熟悉了JWT攻击背后的基本概念,目前只想在一些现实的、故意易受攻击的目标上练习这些漏洞的利用方法,则可以通过下面的链接来访问本专题的所有实验缓解。
要想查看所有JWT实验环境,请访问https://portswigger.net/web-security/all-labs#jwt。
需要注意的是,从Burp Suite Professional 2022.5版本开始,Burp Scanner就可以替您自动检测JWT机制的某些漏洞。目前,这个版本只在我们的Early Adopter发布频道提供。关于如何切换渠道的更多信息,请参见https://portswigger.net/burp/documentation/desktop/early-adopter。
什么是JWT?
JSON Web令牌(JWT)是一种标准化的格式,用于在系统之间发送经过加密签名的JSON数据。它们理论上可以包含任何类型的数据,但最常用于发送关于用户的信息(“声明”),以进行身份认证、会话处理和访问控制。
与传统的会话令牌不同,服务器需要的所有数据都存储在JWT本身的客户端。这使得JWT成为高度分布式网站的热门选择,在这些网站中,用户需要与多个后端服务器进行无缝交互。
JWT格式
JWT由3部分组成:头部、载荷和签名。这些部分之间用点号隔开,具体如下面的例子所示:
eyJraWQiOiI5MTM2ZGRiMy1jYjBhLTRhMTktYTA3ZS1lYWRmNWE0NGM4YjUiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsImV4cCI6MTY0ODAzNzE2NCwibmFtZSI6IkNhcmxvcyBNb250b3lhIiwic3ViIjoiY2FybG9zIiwicm9sZSI6ImJsb2dfYXV0aG9yIiwiZW1haWwiOiJjYXJsb3NAY2FybG9zLW1vbnRveWEubmV0IiwiaWF0IjoxNTE2MjM5MDIyfQ.SYZBPIBg2CRjXAJ8vCER0LA_ENjII1JakvNQoP-Hw6GG1zfl4JyngsZReIfqRvIAEi5L4HV0q7_9qGhQZvy9ZdxEJbwTxRs_6Lb-fZTDpW6lKYNdMyjw45_alSCZ1fypsMWz_2mTpQzil0lOtps5Ei_z7mM7M8gCwe_AGpI53JxduQOaB5HkT5gVrv9cKu9CsW5MS6ZbqYXpGyOG5ehoxqm8DL5tFYaW3lB50ELxi0KsuTKEbD0t5BCl0aCR2MBJWAbN-xeLwEenaqBiwPVvKixYleeDQiBEIylFdNNIMviKRgXiYuAvMziVPbwSgkZVHeEdF5MQP1Oe2Spac-6IfA
JWT的头部和载荷部分其实就是用base64url编码的JSON对象。其中,头部包含关于令牌本身的元数据,而载荷包含关于用户的实际“声明”。例如,您可以对上述令牌的载荷进行解码,从而得到以下声明:
{ "iss": "portswigger", "exp": 1648037164, "name": "Carlos Montoya", "sub": "carlos", "role": "blog_author", "email": "[email protected]", "iat": 1516239022 }
在大多数情况下,任何有权访问令牌的人都可以轻松地读取或修改这些数据。因此,任何基于JWT机制的安全性都严重依赖于密码签名。
JWT签名
颁发令牌的服务器通常通过对头部和载荷计算哈希值来生成签名。在某些情况下,它们还对产生的哈希值进行加密处理。但是无论哪种方式,这个过程都涉及一个秘密密钥。如果不知道这个密钥,就无法为给定的头部和载荷生成有效的签名。这实际上就为服务器提供了一种机制,以验证自令牌颁发以来没有任何数据被篡改过,因为对头部或载荷部分的任何修改,都将意味着签名不再匹配。
如果您想更好地理解JWTs是如何构造的,可以使用jwt.io上的调试器对任意令牌进行实验。
JWT、JWS与JWE
JWT规范的约束实际上是非常有限的。它只定义了将信息(“声明”)表示为可以在双方之间传输的JSON对象的格式。在实践中,JWT并没有真正作为一个独立的实体使用。JWT规范由JSON Web签名(JWS)和JSON Web加密(JWE)规范组成,它们定义了实际实现JWT的具体方法。
换句话说,JWT通常是指JWS或JWE令牌。当人们使用“JWT”这个术语时,他们几乎总是指JWS令牌。JWE的情况也非常相似,只是令牌的实际内容是经过加密的,而不是仅仅经过编码处理的。
需要说明的是,为简单起见,在这些资料中,“JWT”主要是指JWS令牌,尽管所述的一些漏洞也可能适用于JWE令牌。
什么是JWT攻击?
所谓JWT攻击,是指用户向服务器发送修改过的JWT,以实现恶意目的。通常情况下,这个目的是通过冒充已经通过身份认证的另一个用户,以绕过认证和访问控制。
JWT攻击的危害是什么?
JWT攻击的影响通常很严重。如果攻击者能够用任意值创建自己的有效令牌,他们就能够提升自己的权限或冒充其他用户,从而完全接管这些用户的账户。
JWT攻击的漏洞是如何产生的?
JWT漏洞通常是由于应用程序本身对JWT的处理有缺陷而产生的。与JWT有关的各种规范在设计上相对灵活,允许网站开发人员自行决定许多实现细节。这可能会导致他们意外地引入安全漏洞,即使是在使用“身经百战”的代码库时。
这些实现缺陷通常意味着JWT的签名没有被正确验证。这使得攻击者可以通过令牌的载荷篡改传递给应用程序的值。即使签名得到了严格的检查,它是否真的可以被信任,在很大程度上也取决于服务器的秘钥是否仍然是“机密的”。如果这个密钥以某种方式被泄露,或者可以被猜测或破解,那么攻击者就可以为任意令牌生成有效的签名,从而攻陷整个机制。
如何通过Burp Suite处理JWT
如果您过去还没有使用过JWT,我们建议您在尝试本文中的实验之前先熟悉Burp Suite的相关功能。
如何利用存在缺陷的JWT签名验证
根据设计,服务器通常不存储任何关于其颁发的JWT的信息。相反,每个令牌都是一个完全独立的实体。虽然这样做有许多优点,但也引入了一个基本问题——服务器实际上不知道关于令牌的原始内容,甚至不知道原始签名是什么。因此,如果服务器没有正确地验证签名,就没有什么可以阻止攻击者对令牌的其他部分进行任意篡改。
例如,考虑一个包含以下声明的JWT:
{ "username": "carlos", "isAdmin": false }
如果服务器是根据username来识别会话,那么,攻击者就能够通过修改用户名来冒充其他已登录的用户。同样,如果isAdmin值被用于访问控制,攻击者也可以提通过篡改这个值来实现提权。
在前两个实验中,您将看到一些示例,展示了这些漏洞在实际应用程序中的具体表现。
漏洞:接受任意签名
JWT库通常会提供一个方法来验证令牌,同时,还会提供另一个方法对其进行解码。例如,对于Node.js库jsonwebtoken来说,这两个方法本别是verify()和decode()。
有时候,开发人员会混淆这两个方法,只把传入的令牌传给decode()方法。这实际上意味着应用程序根本就没有对签名进行验证。
关于通过未验证的签名绕过JWT认证的实验环境,请访问https://portswigger.net/web-security/jwt/lab-jwt-authentication-bypass-via-unverified-signature。
漏洞:接受未签名的令牌
实际上,JWT头部还包含一个alg参数。该参数的作用,就是告诉服务器对令牌进行签名时使用的是哪种算法,换句话说,在验证签名时需要使用哪种算法。
{ "alg": "HS256", "typ": "JWT" }
这在本质上是有缺陷的,因为服务器别无选择,只能隐式地信任提供令牌的用户的输入(注意,这些输入受控于该用户),而该令牌根本没有被验证过。换句话说,攻击者可以直接影响服务器检查令牌是否值得信任的方式。
JWT既可以使用一系列不同的算法进行签名,也可以不签名。在这种情况下,alg参数被设置为None,表示所谓的 "不安全的JWT"。由于这种情况具有显而易见的安全隐患,因此,服务器通常会拒绝没有签名的令牌。然而,由于这种过滤依赖于字符串解析,所以,攻击者可以使用经典的混淆技术绕过这些过滤器,如混合大写和非预期的编码。
需要注意的是,即使令牌是未签名的,载荷部分也必须以点号结尾。
实验环境:读者可以通过https://portswigger.net/web-security/jwt/lab-jwt-authentication-bypass-via-flawed-signature-verification提供的环境,来练习如何利用有缺陷的签名验证机制来绕过JWT认证。
暴力破解密钥
某些签名算法,如HS256(HMAC + SHA-256),会使用一个任意的、独立的字符串作为秘密密钥。就像密码一样,这个秘密不能被攻击者轻易猜到或暴力破解,这是至关重要的。否则,他们就能以任意的头部和载荷值来创建JWT,然后用密钥重新给令牌签名。
在实现JWT应用时,开发人员有时会犯一些错误,比如忘记改变默认或占位的密码。他们甚至可能复制和粘贴在网上找到的代码片段,然后忘记改变作为示例提供的硬编码的密码。在这种情况下,攻击者使用流行的密码本,轻松对服务器的登陆凭据进行暴力破解。
使用hashcat来暴力破解密钥
我们建议使用hashcat对密钥进行暴力破解。您可以手动安装hashcat,但它在Kali Linux上是预装的,可以直接使用。
如果您使用的是Kali中预构建的VirtualBox映像,而不是裸机安装程序版本,则可能没有足够的内存来运行Hashcat程序。
为此,您只需要一个来自目标服务器的有效的、已签名的JWT,以及一个众所周知的密码字典wordlist。然后,可以运行以下命令,将JWT和wordlist作为参数传入:
hashcat -a 0 -m 16500 < jwt > < wordlist >
Hashcat程序会使用密码字典wordlist中的每个密码对JWT的头部和载荷进行签名,然后将得到的签名与服务器的原始签名进行比较。如果任何一个签名匹配,hashcat就会以下列格式输出已识别的密码,以及其他各种细节:
< jwt >:< identified-secret >
如果多次运行该命令,则需要包含--show标志以输出结果。
由于hashcat在您的机器上本地运行,并且不依赖于向服务器发送请求,所以这个过程会非常快,即使在使用庞大的密码字典Wordlist时也是如此。
一旦确定了密钥,就可以使用它为您喜欢的任何JWT头部和载荷生成有效签名。有关如何利用Burp Suite重新给修改后的JWT签名的详细信息,请参见相关章节。
实验环境:通过弱签名密钥绕过JWT身份验证的实验,请访问https://portswigger.net/web-security/jwt/lab-jwt-authentication-bypass-via-weak-signing-key。
如果服务器使用了非常弱的密码,甚至能够用遍历字符的方式进行暴力破解,而不必使用Wordlist。
JWT头部参数注入
根据JWS规范,只有头部参数alg是必需的。然而,在实践中,JWT头部(也称为JOSE头部)通常包含其他几个参数。以下是攻击者特别感兴趣的参数:
jwk(JSON Web Key):提供一个表示密钥的嵌入式JSON对象。
jku(JSON Web Key Set URL):提供一个URL,服务器可以从中获取一组包含正确密钥的密钥。
kid(Key ID):提供一个ID,在有多个密钥可供选择的情况下,服务器可以使用该ID来识别正确的密钥。根据密钥的格式,它可能还有一个匹配的kid参数。
正如你所看到的,这些用户可控制的参数用于告诉接收方服务器在验证签名时使用哪些密钥。在本节中,你将学习如何利用这些参数来注入修改过的JWT,而这些JWT都是用你自己的任意密钥而非服务器的密钥来签名的。
通过jwk参数注入自签名的JWT
JSON Web签名(JWS)规范描述了一个可选的jwk头部参数,服务器可以用它将其公钥直接嵌入JWK格式的令牌本身。
JWK
JWK(JSON Web密钥)是一种标准化的格式,用于将密钥表示为JSON对象。
下面,我们为大家展示一个JWT头部示例:
{ "kid": "ed2Nf8sb-sD6ng0-scs5390g-fFD8sfxG", "typ": "JWT", "alg": "RS256", "jwk": { "kty": "RSA", "e": "AQAB", "kid": "ed2Nf8sb-sD6ng0-scs5390g-fFD8sfxG", "n": "yy1wpYmffgXBxhAUJzHHocCuJolwDqql75ZWuCQ_cb33K2vh9m" } }
对于不熟悉“公钥”和“私钥”这两个术语读者,请参阅https://portswigger.net/web-security/jwt/algorithm-confusion#symmetric-vs-asymmetric-algorithms。
理想情况下,服务器应该只使用有限的公钥白名单来验证JWT签名。然而,配置错误的服务器有时会使用jwk参数中嵌入的任何密钥来验证签名。
因此,攻击者可以利用这种行为,用自己的RSA私钥对修改过的JWT进行签名,然后在jwk头部中嵌入对应的公钥。
虽然我们也可以在Burp中手动添加或修改jwk参数,但JWT编辑器扩展提供了一个非常方便的功能,用于帮助我们测试这个漏洞。
1、在加载该扩展后,在Burp的主选项卡栏中,转到JWT Editor Keys选项卡。
2、创建一个新的RSA密钥。
3、向Burp Repeater发送一个包含JWT的请求。
4、在消息编辑器中,切换到扩展生成的JSON Web Token选项卡,并以你喜欢的方式修改令牌的载荷。
5、点击Attack按钮,然后选择Embedded JWK。当收到提示时,选择新生成的RSA密钥。
6、发送请求,测试服务器的响应情况。
您也可以通过自己添加jwk头部来手动执行这种攻击。然而,您可能还需要更新JWT的头部参数kid,以匹配嵌入的密钥的kid。实际上,该扩展的内置攻击可以替我们完成这个步骤。
实验环境:读者可以通过https://portswigger.net/web-security/jwt/lab-jwt-authentication-bypass-via-jwk-header-injection,来了解如何通过注入jwk头部来绕过JWT认证。
通过jku参数注入自签名的JWT
实际上,有些服务器并不会直接使用jwk头部参数来嵌入公钥,而是让你使用jku(JWK Set URL)头部参数来引用一个包含密钥的JWK Set。当验证签名时,服务器会从这个URL中获取相关的密钥。
实际上,所谓JWK Set就是一个JSON对象,其中包含一组表示密钥的JWK,例如:
{ "keys": [ { "kty": "RSA", "e": "AQAB", "kid": "75d0ef47-af89-47a9-9061-7c02a610d5ab", "n": "o-yy1wpYmffgXBxhAUJzHHocCuJolwDqql75ZWuCQ_cb33K2vh9mk6GPM9gNN4Y_qTVX67WhsN3JvaFYw-fhvsWQ" }, { "kty": "RSA", "e": "AQAB", "kid": "d8fDFo-fS9-faS14a9-ASf99sa-7c1Ad5abA", "n": "fc3f-yy1wpYmffgXBxhAUJzHql79gNNQ_cb33HocCuJolwDqmk6GPM4Y_qTVX67WhsN3JvaFYw-dfg6DH-asAScw" } ] }
像这样的JWK集有时会通过一个标准的端点对外公开,如/.known/jwks.json。
虽然更安全的网站只会从受信任的域中获取密钥,但有时可以利用URL解析的差异来绕过这种过滤机制。关于这方面的例子,请参阅https://portswigger.net/web-security/ssrf#ssrf-with-whitelist-based-input-filters。
实验环境:读者可以通过https://portswigger.net/web-security/jwt/lab-jwt-authentication-bypass-via-jku-header-injection,来练习如何通过注入jku头部来绕过JWT认证。
通过kid参数注入自签名的JWT
服务器可能会使用多个加密密钥来为不同类型的数据进行签名,而不仅仅是JWT。出于这个原因,JWT的头部可能包含一个kid(密钥ID)参数,以帮助服务器识别在验证签名时要使用的密钥。
验证密钥通常被存储为JWK Set。在这种情况下,服务器可以直接寻找与令牌具有相同kid参数的JWK。然而,JWS规范并没有为这个ID定义具体的结构:它只是开发人员任意选择的一个字符串。例如,他们可能使用kid参数来指向数据库中的一个特定条目,甚至是一个文件的名称。
如果这个参数也容易受到目录遍历的影响,攻击者就有可能迫使服务器使用其文件系统中的任意文件作为验证密钥。
{ "kid": "../../path/to/file", "typ": "JWT", "alg": "HS256", "k": "asGsADas3421-dfh9DGN-AFDFDbasfd8-anfjkvc" }
如果服务器也支持使用对称算法为JWT签名,这就非常危险了。在这种情况下,攻击者有可能将kid参数指向一个可预测的静态文件,然后用一个与该文件内容相匹配的秘密来给JWT签名。
理论上讲,攻击者可以用任何文件来做这件事,但最简单的方法之一是使用/dev/null,它存在于大多数Linux系统中。由于这是一个空文件,读取它时将返回null。因此,用一个Base64编码的null字节来给令牌签名将得到一个有效的签名。
实验环境:读者可以通过https://portswigger.net/web-security/jwt/lab-jwt-authentication-bypass-via-kid-header-path-traversal,来练习如何通过kid头部路径遍历漏洞来绕过JWT验证。
如果服务器将其验证密钥存储在数据库中,kid头部参数也是一个潜在的SQL注入攻击的载体。
其他有趣的JWT头部参数
以下头部参数也可能是攻击者感兴趣的:
●cty(内容类型):有时用于声明JWT载荷中内容的媒体类型。通常情况下,会省略该参数,但底层解析库可能还是支持它。如果已经找到了绕过签名验证的方法,可以尝试注入cty参数,将内容类型改为text/xml或application/x-java-serialized-object,这有可能为XXE和反序列化攻击提供新的向量。
●x5c(X.509证书链):有时用于传递用于对JWT进行数字签名的X.509公钥证书或证书链。这个头部参数可用于注入自签证书,类似于上面讨论的jwk头部注入攻击。由于X.509格式及其扩展的复杂性,解析这些证书也很可能会引入漏洞。这些攻击的细节超出了本文的讨论范围,但要了解更多细节,请参考CVE-2017-2800和CVE-2018-2633漏洞的相关资料。
JWT算法混淆
即使服务器使用了攻击者无法破解的强大密码,他们仍然可以通过使用开发人员没有预料到的算法签名令牌来伪造有效的JWT。这就是所谓的算法混淆攻击。关于该攻击方法的详细介绍,请访问这篇文章:https://portswigger.net/web-security/jwt/algorithm-confusion。
如何防御JWT攻击
您可以通过采取以下措施来保护自己的网站免受本文介绍的各种攻击:
使用最新的库来处理JWT,并确保开发人员完全了解它是如何工作的,以及所带来的任何安全影响。现代代码库的使用,降低了在代码实现中引入安全漏洞的可能性,但由于相关规范固有的灵活性,这也不是万无一失的。
确保对收到的任何JWT进行严格的签名验证,并考虑边缘情况,如使用非预期的算法签名的JWT。
为jku头部提供允许主机白名单,并严格执行。
确保不会受到通过kid头部参数进行路径穿越或SQL注入的影响。
JWT处理的其他最佳实践
我们建议在您的应用程序中使用JWT时遵守以下最佳实践:
始终为颁发的任何令牌设置一个到期日。
尽可能避免通过URL参数发送令牌。
提供aud声明(或类似内容),以指定令牌的预期接收者。这可以防止它被用在不同的网站上。
让颁发服务器能够撤销令牌(例如,在注销时)。
本文翻译自:https://portswigger.net/web-security/jwt如若转载,请注明原文地址