JWT攻击方式总结
JWT简介
JWT的全称是Json Web Token。它遵循JSON格式,将用户信息加密到token里,服务器不保存任何用户信息,只保存密钥信息,通过使用特定加密算法验证token,通过token验证用户身份。基于token的身份验证可以替代传统的cookie+session身份验证方法。
JWT组成
jwt由三个部分组成:header
.payload
.signature
header
第一部分头部解码后表示一个简单的JSON对象,一般来说这个对象描述了JWT所使用的签名算法和类型,最常用的两个字段是alg
和typ
,alg
指定了token加密使用的算法(最常用的为HMAC和RSA算法),typ
声明类型为JWT
header通常会长这个样子:
1 | { |
经过Base64URL编码之后就为eyJhbGciOiJIUzI1NiIsInR5cCI6Imp3dCJ9
【注】: Base64URL是base64修改版,为了方便地在Web中传输使用了不同的编码表,不会在末尾填充=号,并将+
和/
分别改为-
和_
payload
第二部分是JWT的核心,也就是载荷.储存一些用户的数据(用户名,时间戳,过期时间等等).通常遵守的原则是存储尽量少的必要数据在载荷中,因为Base64可解,基本上相当于明文传输了
1 | { |
经过Base64URL编码之后就为eyJrZXkiOiJ2YWwiLCJpYXQiOjE0MjI2MDU0NDV9
signature
最后一部分是签名,是根据头部和载荷通过密钥secret
和指定的签名算法进行加密计算出来的一个签名,密钥保存在服务端,用来校验JWT是否有效,保证完整性,抽象成公式就是
1 | signature = HMAC-SHA256(base64urlEncode(header) + '.' + base64urlEncode(payload), secret_key) |
这里的值为eUiabuiKv-8PYk2AkGY4Fb5KMZeorYBLw261JPQD5lM
综上述,这个JWT完整版为
1 | eyJhbGciOiJIUzI1NiIsInR5cCI6Imp3dCJ9.eyJrZXkiOiJ2YWwiLCJpYXQiOjE0MjI2MDU0NDV9.eUiabuiKv-8PYk2AkGY4F |
攻击方式
加密算法可控
修改加密算法为None
1 | docker pull gluckzhang/ctf-jwt-token |
访问8080端口,是一个登录框,尝试登录失败后会给出正确的账号密码
登陆进去后抓包
此时cookie中的Token就是认证通过之后的JWT,拿去解码:https://jwt.io/
1 | //Header |
这里Payload中的role为user,我们将其改成admin,并且将alg的值改为none,然后借助pyjwt库生成新的JWT
1 | import jwt |
encode()函数里面第一个参数是传入json格式的payload,第二个参数是公钥,第三个参数是加密算法
得到的token去替换之前的token,得到flag
修改加密算法为对称加密
在非对称加密算法(常见的RS256等)下,服务端会使用私钥进行签名,公钥进行验证;倘若攻击者获取到了服务端的公钥,并将签名算法改为对称加密算法(常见HS256等),服务端还是会用公钥进行验证签名,这样就成功的通过了服务端的验证
例题地址:http://demo.sjoerdlangkemper.nl/jwtdemo/rs256.php
进去即可拿到JWT
1 | eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwOlwvXC9kZW1vLnNqb2VyZGxhbmdrZW1wZXIubmxcLyIsImlhdCI6MTYwMjI5NzkxNSwiZXhwIjoxNjAyMjk4MDM1LCJkYXRhIjp7ImhlbGxvIjoid29ybGQifX0.oGNwXwulZp9NL0LH-GIAaoAzYwlZgMPyxHNp2l8Xyx2gvILAL0GSZeKv7xyDsoVSxFh4lTZnXQz-TSt3hlEn5Zuaya278Xd2e69bCba3QQabHXitOwCqMWTpSH7BJnvwivox4-G8zvrvzyQHd7ZikwYeVc1lSwNifwS8gkt1YNyRSnsFwb7rU3sBHs2gQ0y-9CeDG7OGl87c0R8JVav8LllxFn6hUtwXtqg3IODpUKUOpLapQhHNwuRh80mhicvOjTq7Fy4EuoEYSDw53BI6EsjigoSYCeMqXuX6nk03r-KnL3yUJf9ZsOkPFC9577eMBq3QMXVF_TLYm0y44bJP_A |
通过解密得到部分信息
1 | //Header |
RSA的公钥地址: public.pem
将算法改为HS256,然后利用该公钥对其进行签名
1 | import jwt |
但是运行的时候,pyjwt会校验密钥的格式,然后会报错
我们定位到algorithms.py
的150行
1 | if any([string_value in key for string_value in invalid_strings]): |
这里会进行判断然后报错,我们直接将这一段给注释掉,然后回来执行就没有问题了
密钥弱口令
工具:
- c-jwt-cracker
- Hashcat
- john
以下是几个使用示例(没有示例就直接搬网上图片了)
修改KID参数
kid
是jwt header中的一个可选参数,全称是key ID
,它用于指定加密算法的密钥
1 | { |
因为该参数可以由用户输入,所以也可能造成一些安全问题。
任意文件读取
kid
参数用于读取密钥文件,但系统并不会知道用户想要读取的到底是不是密钥文件,所以,如果在没有对参数进行过滤的前提下,攻击者是可以读取到系统的任意文件的。
1 | { |
SQL注入
kid
也可以从数据库中提取数据,这时候就有可能造成SQL注入攻击,通过构造SQL语句来获取数据或者是绕过signature的验证
1 | { |
命令注入
对kid
参数过滤不严也可能会出现命令注入问题,但是利用条件比较苛刻。如果服务器后端使用的是Ruby,在读取密钥文件时使用了open
函数,通过构造参数就可能造成命令注入。
1 | "/path/to/key_file|whoami" |
对于其他的语言,例如php,如果代码中使用的是exec
或者是system
来读取密钥文件,那么同样也可以造成命令注入,当然这个可能性就比较小了
修改JKU/X5U参数
JKU
的全称是”JSON Web Key Set URL”,用于指定一组用于验证令牌的密钥的URL。类似于kid
,JKU
也可以由用户指定输入数据,如果没有经过严格过滤,就可以指定一组自定义的密钥文件,并指定web应用使用该组密钥来验证token。
X5U
则以URI的形式数允许攻击者指定用于验证令牌的公钥证书或证书链,与JKU
的攻击利用方式类似