博客地址 https://drun1baby.github.io/
最近看到 PortSwigger 上新出了一个靶场,主题是 JWT 安全。
0x01 前言
JWT 全称 JSON Web Token,是一种标准化格式,用于在系统之间发送加密签名的 JSON 数据。
原始的 Token 只是一个 uuid,没有任何意义。
JWT 包含了部分业务信息,减少了 Token 验证等交互操作,效率更高。JWT 作为现如今高度分布式网站的首选 Cookie 之一,今天我们来谈论一下 JWT 的安全性问题。
0x02 JWT 是什么
要针对网站的 Cookie 下手,首先我们要知道它是什么。
JWT 由三部分组成,分别为 Header ———— 头部;Payload ———— 负载,Signature ———— 签名。它们之间由三个 **.**分隔,由 Base64 加密而来。
Signature 可用的 JWT 也被称作为 JWS。
我们以下面这一个 JWT 作为例子进行说明
eyJraWQiOiI5MTM2ZGRiMy1jYjBhLTRhMTktYTA3ZS1lYWRmNWE0NGM4YjUiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsImV4cCI6MTY0ODAzNzE2NCwibmFtZSI6IkNhcmxvcyBNb250b3lhIiwic3ViIjoiY2FybG9zIiwicm8sZSI6ImJsb2dfYXV0aG9yIiwiZW1haWwiOiJjYXJsb3NAY2FybG9zLW1vbnRveWEubmV0IiwiaWF0IjoxNTE2MjM5MDIyfQ.SYZBPIBg2CRjXAJ8vCER0LA_ENjII1JakvNQoP-Hw6GG1zfl4JyngsZReIfqRvIAEi5L4HV0q7_9qGhQZvy9ZdxEJbwTxRs_6Lb-fZTDpW6lKYNdMyjw45_alSCZ1fypsMWz_2mTpQzil0lOtps5Ei_z7mM7M8gCwe_AGpI53JxduQOaB5HkT5gVrv9cKu9CsW5MS6ZbqYXpGyOG5ehoxqm8DL5tFYaW3lB50ELxi0KsuTKEbD0t5BCl0aCR2MBJWAbN-xeLwEenaqBiwPVvKixYleeDQiBEIylFdNNIMviKRgXiYuAvMziVPbwSgkZVHeEdF5MQP1Oe2Spac-6IfA
Header 部分
Header 部分就像是货车的标志,告诉了我们这辆车上面装了什么易爆品啊,还是易燃品。
在 JWT 中 Header 部分存储的是 Token 类型和加密算法,它长成这样
{
"alg": "HS256", // 也可以是 HS512, RS256 等等
"typ": "JWT"
}
Payload 部分
Payload 这个词中文意思为负载,也就是我们大货车上面的货物。Payload 里面存具体的业务数据比如用户 id 等等。
Payload 部分长这样
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
Signature 部分
Signature 部分意为签名,这批货送到了,需要对方签收一下,这就是签名,我个人也喜欢把它理解成 Secret,它是经历这样一个算法所生成的。
alg 对应的加密算法(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
我们输入的 Secret
) secret base64 encoded
Signature 是具体的数字签名密文信息,这部分的密文信息是手动设置的,一般在 Java 开发的配置文件当中设置。像这样
我们可以在 jwt.io中体验一下 Signature 的使用。
JWT 与 JWT,JWE
"JWT vs JWS vs JWE"
这个概念了解就好了,从原理上来说是非常相似的。
简单来说,现在网上大多数介绍JWT的文章实际介绍的都是 JWS(JSON Web Signature),也往往导致了人们对于 JWT 的误解,但是 JWT 并不等于 JWS,JWS 只是 JWT 的一种实现,除了 JWS 外,JWE(JSON Web Encryption) 也是 JWT 的一种实现。
JWT 会造成的危害
JWT 的漏洞经常与提权类漏洞的危害放在一起说,所以提权类的漏洞能造成什么样的危害,JWT 产生的安全问题也能产生危害。
0x03 JWT 基础安全问题
JWT 所产生的安全问题,是设计上与逻辑上不恰当的设计所造成的。
其中,如果服务器未对签名进行校验,则可以任意修改数据,若进行了校验,则无法修改,需要我们通过其他手段进行辅助攻击。
1. 未对签名进行验证
前文我们说到,JWT 需要开发者提供一个 Signature ———— 签名,如果我们不对签名进行验证,极有可能产生如下的越权情况。
假设选择存在一个 JWT 如下
{
"username": "carlos",
"isAdmin": false
}
如果没有对签名进行认证,我们可以修改isAdmin
属性的值为true
,即可造成简单的越权。
Lab: JWT authentication bypass via unverified signature
JWT 未对签名进行验证
靶场要求,利用 admin 的越权账户删除 carlos 账户。
我们去到 "My Accout" 界面下,登录进去,抓包,认识一下 JWT
接着,在 Burpsuite 里面,我们查看这串 JWT。
{
"iss":"portswigger",
"sub":"wiener",
"exp":1656054440
}
在 Burpsuite 当中对其进行修改并发包。
此时我们已经是 admin 的账户了,然后去完成我们的删除 user 的操作
成功 ~!
2. 未对加密算法进行强验证
这一种情况,也就是未对 Header 部分的 **"alg"**进行验证
我们可以设置 alg 为 none,绕过验证,从而实现攻击。
Lab: JWT authentication bypass via flawed signature verification
未对加密算法进行强验证
靶场要求,利用 admin 的越权账户删除 carlos 账户。
登录,抓包,在 /my-accout 接口处,查看 JWT 的内容,修改接口为 /admin并发包,现在是 401 Unauthorized 的状态码。
如图所示修改 sub 属性为 administrator,修改 alg 为 none。这里还需要将我们的 Signature 删除掉,因为 Signature 是通过 alg 算法生成的,但要保留"分割点",让其成为 JWT 的形式。
此时我们已经是 admin 的账户了,然后去完成我们的删除 user 的操作。
3. 爆破 Secret 进行越权操作
这里用到一个叫做 hashcat的工具,kali 机一般是自带的,如果没有安装的话可以通过如下命令进行安装。
sudo apt-get update
sudo apt-get install hashcat
爆破需要字典,这里是我在本道靶场中使用的 字典
通过如下命令可以进行 JWT 的 Secret 的爆破
hashcat -a 0 -m 16500 你的jwt /path/to/jwt.secrets.list
获得 Secret 之后我们又能做什么呢?Secret 是用来生成 Signature 的一个未知的部分,所以知道它了之后便可以进行越权操作,我们通过这一道靶场感受一下。
Lab: JWT authentication bypass via weak signing key
爆破 Secret 进行越权
靶场要求,利用 admin 的越权账户删除 carlos 账户。
先抓包,获取到 jwt 之后,我们使用 hashcat 进行 Secret 的爆破。
这里获取到了 Secret 为 "secret1",接着我们去到 jwt.io 下伪造身份。
现在我们成功伪造了一个 admin 权限的用户,再进行越权操作。Success ~!
0x04 JWT 标头注入
这里的注入分为两种情况,当连接到数据库进行验证的时候,可能会存在 SQL 注入;若未连接至数据库时,我们可以进行越权设计。
在 JWT Header 当中会存在名为 "JWK" 的属性之一,JWK 这一属性是可选的,而非向 alg属性一样,是必填的。
JWK 英文全称为 JSON Web Key,是一个JSON对象,表示一个加密的密钥。JWK 中的字段表示密钥的属性。
因为是对象,所以 JWK 当中有一些参数如下。
jku
(JSON Web Key URL) - 提供一个 URL,服务器可以从中获取一组包含正确密钥的密钥。kid
(Key ID) - 提供一个 ID,服务器可以使用该 ID 在有多个密钥可供选择的情况下,根据密钥的格式,标识正确的密钥。
一个完整的 JWT Header 长成这样,其中便含有 jwk 这一属性。
{
"kid": "ed2Nf8sb-sD6ng0-scs5390g-fFD8sfxG",
"typ": "JWT",
"alg": "RS256",
"jwk":
{
"kty": "RSA",
"e": "AQAB",
"kid": "ed2Nf8sb-sD6ng0-scs5390g-fFD8sfxG",
"n":"yy1wpYmffgXBxhAUJzHHocCuJolwDqql75ZWuCQ_cb33K2vh9m"
}
}
JWT Header 的注入也分很多种,下面我们一一举例
1. 通过 jwk 注入 JWT,形成 JWS
理想情况下,服务器应当使用有限的公钥白名单来验证 JWT 签名。如果使用的公钥白名单中配置不恰当,。例如允许 jwk 的任意公钥,则会导致安全问题。
我们以下面一道靶场尝试攻击方式。需要先安装 Burpsuite 的 JWT Editor Keys插件。
Lab: JWT authentication bypass via jwk header injection
通过 jwk 注入 JWT
靶场要求,利用 admin 的越权账户删除 carlos 账户。
同样,登录,抓包。我们将 /my-accout 接口的包单独拿出来,选中 JSON Web Token 的界面。将 sub 的值从 "wiener" 切换为 "administrator"
切换完之后,点击我们安装的 JWT Editor Keys插件中的 New RSA Key,生成 2048 长度的 JWK 私钥。
这样子一来,我们的私钥就成功创建了,因为服务器后台不比对 JWK 的私钥是否合规,我们可以直接绕过,并进行越权认证。
现在我们成功认证为 administrator权限,发送删除请求即可 ~
2. 通过 jku 注入 JWT,形成 JWS
jku 通常在 JWT 的 Header 部分,它长这样。
以这一泄露为例,该 JWT 使用 RSASHA256 进行加密,而 RSA 的加密需要 n 与 e。我给大家先画幅流程图理一下攻击思路。
前三步是 Burpsuite 的插件 JWT Editor Keys 可以完成的,后续操作只需要我们将 jku 指向 JWK 文件即可进行越权操作。
下面这道靶场就是通过 kid 的值匹配进行攻击
Lab: JWT authentication bypass via jku header injection
通过 jku 注入 JWT
靶场要求,利用 admin 的越权账户删除 carlos 账户。
在明确了攻击方式之后,我们先利用 Burpsuite 的插件 JWT Editor Keys 生成公私钥对,上一道题目生成的公私钥对也可以拿来使用。
进入到 Expolit Server 中,在 Body 部分复制我们的 JWK 公钥。
直接拿出来的公钥是没有 "keys" 表头的,我们需要加上此标头,类似这样
{
"keys": [
{
"kty": "RSA",
"e": "AQAB",
"kid": "691edd2a-f7bf-4e14-b5c4-c5ec79c3dffb",
"n": "oCBNZNRji19rBXUxFRI9toMCRwzxSZ1ckkCvLnuKc8r_ZvRhIHNHo7Flr8_1efAyaIwx_YG7zJZG41hZcpnDE3BfZym09dAM8T65BVLTmFolqpPcZIoqO_CCIK8JU1T0oRgDaW-PCobwC0axi0-LAgb5YbODuTxmwre6bjOwCGftEL9FNgjS5TR9yB0ZxLEJTGGtk7tq5q_voRT93vmSXmjZvUnFmMXSUEIHshfXjh12XUOLs0Mjgn0gfAWM7RFbkXvF-MLvj_p26gdV3j4zHUhPbeFoepzxcBGfVQXajWjj7xfisVga1Okjqp3S53ouNMpY9OEy-UmFz-QGNxKCKw"
}
]
}
前半部分,我们在本地生成一个 JWK,并将其存放到自己的服务器上面,已经完成了,接下来是修改 jku,使得 jku 指向 服务器。
再点下面的 "Sign",选中生成的 Sign Key,并且选择 Don't modify header模式,Sign 即可。
现在我们已经提权完毕,执行删除操作 ~
3. 通过 kid 注入 JWT,与目录遍历攻击相结合
JWS 规范没有针对 kid 进行严格设置,比如必须是 uuid 的格式或者是其他的,它只是开发人员选择的任意字符串。
那么我们可以通过将 kid 指向数据库中的特定条目,或者是指向文件名,作为验证密钥。
当 JWT 使用的是对称加密算法的时候,极有可能存在目录遍历的漏洞,我们能够强制服务器使用其文件系统中的任意文件作为验证密钥。
当目录遍历与 kid 标头注入 JWT 结合在一起时,我们可以先尝试读取
dev/null
这一文件,dev/null
这一文件默认为空文件,返回为 null,我们可以在 Symmetric Key 中,将 k 值修改为AA==
也就是 null,进行攻击。
我们来看 Port 这里的靶场
Lab: JWT authentication bypass via kid header path traversal
通过 kid 注入 JWT,与目录遍历攻击相结合
靶场要求,利用 admin 的越权账户删除 carlos 账户。
这里我们先要生成一个 Symmetric Key,也就是对称密钥,并将 k 的值修改为AA==
,如图。
接着,我们在抓到的包中修改 kid 值,尝试用目录遍历读取dev/null
此文件。并将 sub 修改为 administrator
点击下面的 Sign,使用 OCT8 的密钥攻击。
成功越权,再进行删除操作即可 ~
4. 其他的 JWT 标头攻击
cty 标头,意义为 Content Type,有时用于为 JWT 负载中的内容声明媒体类型,一般情况下是省略的。
我们可以尝试修改 cty 标头为text/xml
或者是application/x-java-serialized-object
,这也可能导致 XXE 注入与反序列化漏洞。
x5c (X.509 Certificate Chain);有时用于传递用于对 JWT 进行数字签名的密钥的 X.509 公钥证书或证书链。此标头参数可用于注入自签名证书,类似于上面讨论的 jwk 标头注入攻击。由于 X.509 格式及其扩展的复杂性,解析这些证书也会引入漏洞。
类似的 CVE 有 CVE-2017-2800和 CVE-2018-2633。
0x05 Web 手的 Crypto 式攻击 ———— 算法混淆漏洞
一些基础知识,对称与非对称加密
可以使用一系列不同的算法对 JWT 进行签名。其中一些,如HS256(HMAC + SHA-256)使用“对称”密钥。这意味着服务器使用单个密钥对 Token 进行签名和验证。显然,这需要保密,就像密码一样。
其他算法,如RS256(RSA + SHA-256)使用“非对称”密钥对。这包括一个私钥(服务器用于对令牌进行签名)和一个可用于验证签名的数学上相关的公钥。
顾名思义,私钥必须保密,但公钥通常是共享的,以便任何人都可以验证服务器颁发的令牌的签名。
算法混淆漏洞的产生原因
根本原因:JWT 库的原生安全问题。
尽管实际的验证过程因所使用的算法而异,但许多库都提供了一种与算法无关的单一方法来验证签名。也就是说,我不论是对称加密方式 RS256,还是非对称加密方式 HS256,它们产生 token 的算法是与提供的加密算法无关的。
当时学的时候我也愣了一下,后面看懂了。
我们可以根据下面的伪代码加以理解
在 JWT 库中定义了verify()
方法
function verify(token, secretOrPublicKey){
algorithm = token.getAlgHeader();
if(algorithm == "RS256")
{
// Use the provided key as an RSA public key
}
else if (algorithm == "HS256")
{
// Use the provided key as an HMAC secret key
}
}
当我们使用 RS256 这种非对称加密的时候,他们可能总是将一个固定的公钥传递给该方法。
publicKey = <public-key-of-server>;
token = request.getCookie("session");
verify(token, publicKey);
因为publicKey
是一样的,所以会导致在使用对称加密的时候,造成publicKey
的泄露,容易造成一系列的安全问题。
执行算法混淆攻击
还是画个流程图,清楚一点
/jwks.json
泄露的算法混淆攻击
以下面这道靶场感受一下算法混淆攻击
Lab: JWT authentication bypass via algorithm confusion
执行算法混淆攻击
靶场要求,利用 admin 的越权账户删除 carlos 账户。
先进行第一步,获取它的服务器公钥,访问/jwks.json
接口,前文我们提到过这个接口是也可能存在信息泄露的可能的。将这个 JWK 复制下来,作为我们第二部的 RSA Key。
接着,进行第二步,到 Burpsuite 的 JWT Editor Keys 去。生成新的 RSA Key,这里用之前泄露的 JWK。
然后把这串东西选中 "Copy Public Key as PEM",将其进行 base64 编码操作,保存一下得到的字符串。这里编码要注意,上下的一串-----END PUBLIC KEY-----
不要删掉。
保存完后,在 JWT Editor Keys 处,生成新的对称加密 Key,用之前保存的 base64 编码去替换 k 的值。
修改 alg 为 HS256,修改 sub 为 administrator。再进行 Sign 操作,最后发包即可。
/jwks.json
未泄露的算法混淆攻击
在这种情况下,基于我们登录之后产生一个 JWT,在 logout 之后重新登录,会产生一个不同的 JWT,我们可以将两个 JWT 相比对,得到公钥。
Lab: JWT authentication bypass via algorithm confusion with no exposed key
/jwks.json 未泄露的算法混淆攻击
靶场要求,利用 admin 的越权账户删除 carlos 账户。
按照我们之前说的方式,登录登出,再登录,获取两个 JWT,将其放到 Port 提供的 docker 工具里面运行。
运行的命令如下
docker run --rm -it portswigger/sig2n <token1> <token2>
此脚本会输出一系列 token 的存在情况值,
这里我们尝试每一个 Tempered JWT,Port 这里给了提示说是 X.509 形式的,所以我们只需要将 X.509 形式的 JWT 进行验证即可。
当 Response 回应 200 时,代表 token 是有效的,若为 302 则代表了重定向。下图是一个成功的案例。
将这一 JWT 的 Base64 编码拿过来,先放到记事本里面暂存。去到 Burpsuite 的 JWT Editor Keys,点击 New Symmetric Key,将上面的 Base64 编码拿过来替换此对称密钥的 k 值。
生成对称密钥之后,进行和之前攻击一致的 Sign 操作
此时已经成功越权,执行删除操作即可。
0x06 JWT 安全问题的防护
使用最新的 JWT 库,虽然最新版本的稳定性有待商榷,但是安全性都是较高的。
对 jku 标头进行严格的白名单设置。
确保 kid 标头不容易受到通过 header 参数进行目录遍历或 SQL 注入的攻击。