想象一下,你正安逸地躺在家里刷公众号,突然,你的手机嘀嘀嘀,一笔又一笔巨款从你的卡里不翼而飞,急得你惊起直跺脚,咋回事啊?!
突然你想到了刚刚在“读芯术”看到的有关“JSON Web Token”的文章。
你恍然大悟,哦~原来是有人伪造token来冒充自己登录银行网站取款呀,原来如此。
弄明白,安心了……个鬼,赶紧报警啊。
接下来,小芯就带你看看这篇JWT的文章,共同探讨使用JWT(和一般的基于签名的token)的安全隐患,以及攻击者利用这些隐患绕过访问控制的方法。
是JSONWeb Token?
有问必答:JSON Web Token是一种在商业应用程序中广泛使用的访问token。它基于JSON格式,并包含一个签名以确保token的完整性。
JWT的运行机制是什么?
JWT由三部分构成:头部(header),载荷(payload)和签名(signature)。
头部
JWT的头部声明了签名算法。下面这段代码就是一个JSON blob对象的base64url编码字符串的头部:
{
"alg" :"HS256",
"typ" : "JWT"
}base64url encoded string: eyBhbGcgOiBIUzI1NiwgdHlwIDogSldUIH0K
(Base64url编码是base64编码在URL格式上的修改版本。它同base64十分类似,但它使用了不同的符号字符并省略了填充。)
JWT最常用的算法是HMAC(Hash-based Message Authentication Code,哈希运算消息认证码)和RSA(一种非对称加密算法)。
载荷
载荷包含了用于访问控制的信息。该部分在用于生成token前也使用base64url进行编码。
{
"user_name" :"admin",
}base64url encoded string: eyB1c2VyX25hbWUgOiBhZG1pbiB9Cg
签名
签名用于验证token是否被篡改。其计算方法是:先将编码后的头部与载荷串联在一起,再使用头部声明的算法来签名。
signature= HMAC-SHA256(base64urlEncode(header) + '.' + base64urlEncode(payload),secret_key)// Let's just say the value of secret_key is "key".->signature function returns 4Hb/6ibbViPOzq9SJflsNGPWSk6B8F6EqVrkNjpXh7M
这个token使用密钥为“key”的HS256算法为字符串“eyBhbGcgOiBIUzI1NiwgdHlwIDogSldUIH0K.eyB1c2VyX25hbWUgOiBhZG1pbiB9Cg”签名。编码后得到字符串“4Hb /6ibbViPOzq9SJflsNGPWSk6B8F6EqVrkNjpXh7M”。
完整的token
将头部、载荷和签名用“.”连接起来可以构成一个完整的token。如下所示:
eyBhbGcgOiBIUzI1NiwgdHlwIDogSldUIH0K.
eyB1c2VyX25hbWUgOiBhZG1pbiB9Cg.
4Hb/ 6ibbViPOzq9SJflsNGPWSk6B8F6EqVrkNjpXh7M
如何绕过JWT访问控制
正常情况下,由于无法篡改JWT载荷的数据,它能提供一种安全途径来标识用户(由于用户无权访问密钥,她便无法在token上签名。)
然而,若JWT出现异常,攻击者可以通过多种方式绕过其安全机制来伪造token。
更改算法类型
攻击者伪造token的方法之一是篡改头部的alg字段。如果应用程序不限制JWT使用的算法类型,攻击者可能会通过指定算法破坏token的安全性。
1. 不使用加密算法
JWT支持将算法设定为“None”。如果“alg”字段设为“ None”,那么签名会被置空,这样任何token都是有效的。例如:
eyAiYWxnIiA6ICJOb25lIiwgInR5cCIgOiAiSldUIiB9Cg.
eyB1c2VyX25hbWUgOiBhZG1pbiB9Cg.
这个token只是下面这两个blob对象用base64url编码后得出的值,它并没有提供签名。
{
“ alg”:“None”,
“typ”:“ JWT”
}{
“user”:“admin”
}
设定该功能的最初目的是为了方便调试。但是,若不在生产环境中关闭该功能,攻击者可以通过将alg字段设置为“None”来伪造他们想要的任何token,接着便可以使用伪造的token冒充任意用户登陆网站。
2. HMAC算法
JWT两种最常用的算法是HMAC和RSA。HMAC用同一个密钥对token进行签名和认证。而RSA需要两个密钥,先用私钥加密创建token,然后使用其对应的公钥来认证。
HMAC->
signed with a key, verified with the same keyRSA ->
signed with aprivate key,
verified with the corresponding public key
注意,要保护好HMAC的密钥和RSA私钥信息,因为它们都是给token签名的。
现在,假设有一个用RSA签名的应用程序。这些token用对公众保密的私钥A签名。再使用任何人都能获取的公钥B来验证。只要不改变该token的算法,这个token就是有效的。
Token signed with key A -> Token verified with key B (RSA scenario)
但是,如果攻击者把算法改为HMAC,他便能利用原RSA下的公钥B来签名伪造token,而这种做法生成的token同样有效。
这是因为一开始用RSA给token签名时,应用程序会使用公钥B来认证用户身份。当算法更改为HMAC时,仍使用RSA的公钥B认证,但这次,公钥B也可以用于签名了(因为HMAC是对称性加密算法)。
Token signed with key B -> Token verified with key B (HMAC scenario)
提供无效签名
到达应用程序后,token签名可能永远不会被认证。这样,攻击者便可以通过提供无效签名简单地绕过安全机制。
暴力破解密钥
攻击者也可以暴力破解用于签名的JWT密钥。
第一步,攻击者要掌握一些信息:用于token签名的算法、加密后的载荷及最终得到的加密字符串。若密钥不够复杂,攻击者可能可以轻易地暴力破解它。
泄漏密钥
假设攻击者无法暴力破解密钥,他可能会想办法让密钥泄露出来。若攻击者利用文档漏洞(如目录遍历,XXE和SSRF漏洞)读取储存了密钥值的文档,他便能窃取密钥从而给任意token签名。
操纵KID
KID指的是“密钥序号”(Key ID)。它是JWT头部的一个可选字段,开发人员可以用它标识认证token的某一密钥。KID参数的正确用法如下所示:
{
"alg": "HS256",
"typ": "JWT",
"kid": "1" // use key number 1 to verify the token
}
由于该字段由用户控制,因此攻击者可能会操纵它,这会引发危险后果。
1. 目录遍历
由于KID通常用于从文件系统中检索密钥文件,因此,如果在使用前不清理KID,文件系统可能会遭到目录遍历攻击。这样,攻击者便能够在文件系统中指定任意文件作为认证的密钥。
“kid”: “../../public/css/main.css”// use the publicly available file main.css to verify the token
例如,攻击者可以强行设定应用程序使用公开可用文件作为密钥,并用该文件给HMAC加密的token签名。
2. SQL注入
KID也可以用于在数据库中检索密钥。在该情况下,攻击者很可能会利用SQL注入来绕过JWT安全机制。
如果可以在KID参数上进行SQL注入,攻击者便能使用该注入返回任意值。
“kid”:"aaaaaaa' UNION SELECT 'key';--"// use the string "key" toverify the token
上面这个注入会导致应用程序返回字符串“ key”(因为数据库中不存在名为“ aaaaaaa”的密钥)。然后使用字符串“ key”作为密钥来认证token。
操纵头部参数
除KID外,JWT标准还能让开发人员通过URL指定密钥。
1. JKU头部参数
JKU全称是“JWKSet URL”,它是头部的一个可选字段,用于指定链接到一组加密token密钥的URL。若允许使用该字段且不设置限定条件,攻击者就能托管自己的密钥文件,并指定应用程序,用它来认证token。
jku URL -> file containing JWK set -> JWK used to verify the token
2. JWK头部参数
头部可选参数JWK(JSON Web Key)使得攻击者能将认证的密钥直接嵌入token中。
3. 操纵X5U,X5C URL
同JKU或JWK头部类似,X5U和X5C头部参数让攻击者能够指定认证的公钥证书或证书链。X5U以URI形式指定信息,而X5C允许将证书值嵌入token中。
其他安全隐患
若JWT安全机制不能正常运行,还会产生其他安全隐患。这些问题不常见,但绝对值得关注:
信息泄漏
由于JWT用于访问控制,因此它通常包含用户的相关信息。
如果不加密token,则任何人都能通过base64解码并读取token上的载荷信息。因此,如果token包含敏感信息,它便可能成为信息泄漏的来源。正常的token签名仅保障数据完整性,而非机密性。
命令注入
有时,将KID参数直接传到不安全的文件读取操作可能会让一些命令注入代码流中。
一些函数就能给此类型攻击可乘之机,比如Ruby open()。攻击者只需在输入的KID文件名后面添加命令,即可执行系统命令:
“key_file” | whoami;
类似情况还有很多,这只是其中一个例子。理论上,每当应用程序将未审查的头部文件参数传递给类似system(),exec()的函数时,都会产生此种漏洞。
本质上,JWT仅仅是另一种用户输入的形式。程序开发者应谨慎使用JWT并对其进行严格审查。
免责声明:这篇文章是为了引起人们对JWT安全漏洞的关注,并帮助开发人员认识到常见的陷阱。请不要使用这些信息来攻击网站或冒充他人身份哦。
在您没有测试权限的系统上尝试此操作是非法的。如果您发现了安全漏洞,请务必向供应商披露,以营造一个更为安全的互联网环境。大家都是好孩子~