JWT介绍&空加密&暴破密钥&私钥泄露&密钥混淆&黑盒

发布时间:2023年12月21日

目录

0x00 前言

0x01 JWT 介绍

1、什么是 JWT

2、两种身份验证方式的比较

3、JWT 的组成三个部分

4、JWT 的流量特征识别

0x02 JWT 漏洞利用

1、空加密算法(Header 中加密算法为 None)

2、暴力破解密钥

3、源码泄露私钥(JWT中私钥用于签名,公钥用于验证)

4、密钥混淆攻击(RS256 => HS256)

0x03 JWT 黑盒测试思路


0x00 前言

希望和各位大佬一起学习,如果文章内容有错请多多指正,谢谢!?

个人博客链接:CH4SER的个人BLOG – Welcome To Ch4ser's Blog

0x01 JWT 介绍

1、什么是 JWT

JWT(JSON?Web Token):JWT 是一个遵循 JSON 格式的 Token,主要用于身份验证和访问控制,它由 Header、Claims、Signature 三个部分组成

2、两种身份验证方式的比较

与传统的 Cookie+Session 验证方式不同,基于 Token 的认证方式对于服务器无需保存任何用户信息(用户信息在 JWT 里),只需要保存特定加密算法的密钥来验证 Token 即可验证用户身份,服务器负担更小

  • 1、基于 Cookie+Session 验证流程

用户账号密码验证通过后,服务器将用户信息存储到 Session 里(Session 存放在服务端),并返回用户唯一的 session_id 写入到客户端 Cookie 里(Cookie 存放在客户端)。

用户下次请求会带上 Cookie 中的 session_id,然后服务器以此验证用户身份。

  • 2、基于 Token 验证流程

用户账号密码验证通过后,服务器会签发一个 Token(比如 JWT)给客户端,客户端收到后会将 Token 存放在 Cookie 或其他字段里。

用户下次请求会带上 Token,然后服务器使用密钥解密并验证用户身份。

以下为 JWT 认证的流程图:

3、JWT 的组成三个部分

以下为 JWT 的组成示意图,由头部、声明、签名三个部分组成。

  • Header(头部):用于声明加密算法(alg字段)、JWT类型(typ字段)等
  • Claims(声明):用于存储用户信息,如JWT面向的用户(sub字段)、JWT过期时间(exp字段)等
  • Signature(签名):用于对 Header 和 Claims 进行 Base64 编码和算法加密,拥有该部分的 JWT 被称为 JWS,也就是签了名的 JWT

以下为 JWT 解密后的示意图,需要补充的是加密算法 HS 表示对称加密,SA 表示非对称加密。

  • HS 的签名和验证都使用同一个密钥
  • SA 的签名使用私钥,验证使用公钥

4、JWT 的流量特征识别

对于人工识别而言,JWT 一般存在于 Authorization、Cookie 或者请求体里面,由 . 号分割为三个部分,并且 Header、Claims 都是以 eyJ 开头的 Base64 编码。

在工具的角度,可以使用 BurpSuite 插件配合 JWT 在线解析来识别。

BurpSuite 插件:Hae、JSON?Web Tokens

JWT 在线解析:JSON Web Tokens - jwt.io

Hae 正常导入 jar 包后替换一下 config.yml 文件,JSON?Web Tokens 直接到 Burp 商店下载就行,整体效果就是能通过颜色清晰的看到相关的流量包。

0x02 JWT 漏洞利用

利用工具(jwt_tool):https://github.com/ticarpi/jwt_tool

1、空加密算法(Header 中加密算法为 None)

JWT 支持将加密算法 "alg" 字段设定为 "None",此时签名会被置空,任何 JWT 都是有效的。

以 CTFShow - Web345 为例,观察到返回包中指示访问 /admin 以获取 Flag。

访问 /admin 后观察返回包,并未发现 Flag,考虑是 JWT 的问题。

解密数据包中的 JWT 值,发现用户是 user 而不是 admin,尝试修改为 admin,但是发现无法在线修改。

将 JWT 值发到 Burp 的 Decoder 模块,先进行 Base64 解码,将 user 替换为 admin 后,再 Base64 编码得到 admin 用户的 JWT 值。

重新访问 /admin,带上 admin 用户的 JWT 值,得到 Flag。

以 CTFShow - Web346 为例,同样观察到返回包中指示访问 /admin,解密访问 /admin 的 JWT 值,观察到加密算法为 HS256,面向用户为 user。

因为这里的 JWT 使用了 HS256 加密,也就是签名有效且会对 Header 和 Claims 进行编码和加密处理,所以当不清楚密钥的情况下,直接修改 user 为 admin 是不行的。

这里考虑两种思路:1、爆破密钥,显然难度较大;2、尝试将加密算法置空,也就是 alg=none

同样,将 JWT 值发到 Decoder 模块,先 Base64 解码,将 HS256 替换为 none,user 替换为 admin 后,再 Base64 编码。

但需要注意的是,使用此方法要先将 Signature 部分删除掉,因为加密算法为 none 必须将签名置空,并且需要将 Header 和 Claims 分别处理后再用 . 号连接,因为实测直接一起处理是不行的。

最终的格式为:Header.Claims.(注意最后面还有一个 . 号)

重新访问 /admin,带上 admin 用户的 JWT 值,得到 Flag。

上述方法略为复杂,更方便的当然是使用工具,比如 jwt_tool、Burp 插件 JSON?Web Tokens 等。

使用插件 JSON?Web Tokens,将 HS256 替换为 none,user 替换为 admin,并删除 Signature 部分,重新发包即可。

jwt_tool 常用命令如下:

# 使用None算法
python3 jwt_tool.py JWT_HERE -X a
# 自定义修改生成
python3 jwt_tool.py JWT_HERE -T
# 使用字典破解
python3 jwt_tool.py JWT_HERE -C -d dictionary.txt
# 指定密码测试
python3 jwt_tool.py JWT_HERE -C -p password_here

需要注意的是,jwt_tool 最好是在 Linux 上运行,我使用的 Kali 运行,并且需要先导入几个包,命令如下:

python3 -m pip install termcolor cprint pycryptodomex requests

然后启动 jwt_tool,使用 -T 自定义修改模式,根据提示将 HS256 替换为 none,user 替换为 admin,最后会自动生成对应 JWT 值。

复制生成 JWT 值并删除 Signature 部分,重新发包即可获取 Flag。

2、暴力破解密钥

以 CTFShow - Web347 为例,同样访问 /admin 得到的是 user 的 JWT 值,尝试空加密算法无果。

由于加密算法为 HS256 对称加密,比较容易爆破密钥,所以考虑使用 jwt_tool 爆破密钥,最终得知密钥为 123456。

python jwt_tool.py eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhZG1pbiIsImlhdCI6MTcwMTM3NTE3NywiZXhwIjoxNzAxMzgyMzc3LCJuYmYiOjE3MDEzNzUxNzcsInN1YiI6InVzZXIiLCJqdGkiOiJhMDE3MDdmNDRmN2RmOGI1Y2JlNWUxMjlhMGY1YzMxMSJ9.Vjqa5vYv9uRUqiaQpsDxlswGfK5n2umAp-NrY0p39bg -C -d key_dictionary.txt

在 jwt.io 输入密钥,修改 user 为 admin,重新生成 admin 用户的 JWT 值,发包即可获得 Flag。

3、源码泄露私钥(JWT中私钥用于签名,公钥用于验证)

以 CTFShow - Web349 为例,这一关给了一个 JS 文件,模拟的是源码泄露,具体代码如下:

代码解释

1、当GET请求发送到根路径(‘/’)时,会执行以下操作:

使用res.type()方法将响应的内容类型设置为html

使用fs.readFileSync()方法读取当前工作目录下的public/private.key文件,并将其内容赋值给privateKey变量

使用jsonwebtoken库的sign()方法生成一个JSON Web Token(JWT),其中包含用户信息为’user’,使用privateKey进行签名,使用RS256算法

使用res.cookie()方法将生成的JWT写入名为’auth’的cookie

使用res.end()方法发送响应内容为’where is flag?'的响应


2、当POST请求发送到根路径(‘/’)时,会执行以下操作:

设置响应内容类型为html

获取名为’auth’的cookie的值赋值给auth变量

使用fs.readFileSync()方法读取当前工作目录下的public/public.key文件,并将其内容赋值给cert变量

使用jsonwebtoken库的verify()方法验证auth变量中的JWT是否有效,使用cert公钥进行验证。

如果JWT中的用户信息为’admin’,则发送包含flag的响应;否则,发送包含’you are not admin’的响应

通过解密 JWT 得知其使用的加密算法为 RS256,和前面的不同是这里是非对称加密。

在使用非对称加密算法的 JWT 中,私钥用于签名,公钥用于验证。这是因为公钥是公开的,如果用公钥进行签名,那么攻击者便很容易伪造 JWT 了,显然不合理。

于是我们只要拿到用于签名的私钥,便可伪造 admin 用户的 JWT 拿到 Flag。这里通过泄露的 JS 代码得知私钥存放位置为当前目录下,于是直接访问下载 private.key。

拿到了私钥之后,利用其重新生成 admin 用户的 JWT 值。

这里我使用 Python 来生成,具体代码如下(需要注意这段代码需要导入两个包,JWT 和 PyJWT)

pip install JWT PyJWT

生成的 JWT 如下,将其以 POST 方式重新发送(泄露的 JS 代码得知的逻辑),得到 Flag 值。

总结:由泄露的 JS 代码得知验证逻辑,并得到私钥(用于签名)存放位置,利用私钥重新生成了 admin 用户的 JWT 拿到了 Flag,整个过程就像是一次身份的伪造。

疑问:为什么爆破使用对称加密算法(如HS256)的 JWT 的密钥容易,但爆破非对称加密(如RS256)却很难?

4、密钥混淆攻击(RS256 => HS256)

以 CTFShow - Web349 为例,这一关给了一整个源码包,其实模拟的也是源码泄露。

查看该源码包,发现里面有公钥 public.key 却没有私钥 private.key,于是无法像之前一样利用私钥重新生成 admin 用户的 JWT。

观察处理逻辑,签名使用的是 private.key,验证使用的是 public.key,我们已经得到了 public.key,那么要是签名也使用 public.key 不就万事大吉了吗?

于是考虑将加密算法替换为 HS256,这样签名和验证都使用同一个密钥 public.key,利用其生成 admin 用户的 JWT 即可获取 Flag。

这里我使用 Node.js 生成,代码如下,将得到的 JWT 值以 POST 方式重新发送,得到 Flag 值。

0x03 JWT 黑盒测试思路

首先找到需要 JWT 鉴权后才能访问的页面,如个人资料页面,将该请求包重放测试:

  • 1)未授权访问:删除 Token 后仍然可以正常响应对应页面
    2)敏感信息泄露:通过 JWt.io 解密出 Claims 后查看其中是否包含敏感信息,如弱加密的密码等
    3)破解密钥 + 越权访问:通过 JWT.io 解密出 Claims 部分内容,通过空加密算法或密钥爆破等方式实现重新签发 Token 并修改 Claims 部分内容,重放请求包,观察响应包是否能够越权查看其他用户资料
    4)检查 Token 时效性:解密查看 Claims 中是否有 exp 字段键值对(Token 过期时间),等待过期时间后再次使用该 Token 发送请求,若正常响应则存在 Token 不过期
    5)通过页面回显进行探测:如修改 Claims 中键值对后页面报错信息是否存在注入,Claims 中 kid 字段的目录遍历问题与 sql 注入问题

文章来源:https://blog.csdn.net/qq_46081990/article/details/135128438
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。