为了防止专栏没掉,本文首发 http://www.freebuf.com/column/170359.html 。略有更新。

0x01 初识JWT

JWT ( JSON Web Token 的缩写)是一串带有声明信息的字符串,由服务端用加密算法对信息签名来保证其完整性和不可伪造。Token里可以包含所有必要信息,这样服务端就无需保存任何关于用户或会话的信息,JWT可用于身份认证、会话状态维持、信息交换等。

JWT 由三部分构成,分别称为 headerpayloadsignature ,各部分用. 相连构成一个完整的Token,形如xxxxx.yyyyy.zzzzz

分别看下各个部分:

header

使用一个JSON格式字符串声明token的类型和签名用的算法等,形如{"alg": "HS256", "typ": "JWT"} 。该字符串经过Base64Url编码后形成JWT的第一部分xxxxx

Base64Url编码可以用这段代码直观理解:

1
2
3
4
5
6
7
8
9
10
from base64 import *
def base64URLen(s):
t0=b64encode(s)
t1=t0.strip('=').replace('+','-').replace('/','_')
return t1

def base64URLde(s):
t0=s.replace('-','+').replace('_','/')
t1=t0+'='*(4-len(t0)%4)%4
return b64decode(t1)

payload :

使用一个JSON格式字符串描述所要声明的信息,分为 registeredpublic 、 和 private 三类,形如{"name": "John Doe", "admin": true} ,具体信息可参考 RFC7519 的 JWT claims 部分。

同样的,该字符串经过Base64Url编码形成JWT的第二部分yyyyy

signature :

xxxxx.yyyyy 使用alg 指定的算法加密,然后再Base64Url编码得到JWT的第三部分zzzzz 。所支持的算法 类型取决于实现,但HS256none 是强制要求实现的。

0x02 简单应用

在本地运行起简单的基于Express的可发放和处理JWT的服务。

  1. 安装Node.js。Node.js是JavaScript运行时环境,采用轻量高效的事件驱动、无阻塞I/O模型,拥有最大的开源库生态nmp。
1
2
Windows平台可在 https://nodejs.org/en/download/ 下载安装包 
*nix 平台可根据 https://nodejs.org/en/download/package-manager/ 提示使用包管理器安装
  1. 安装Express,一款基于Node.js的快速、灵活、极简的Web框架。
1
2
3
4
5
# http://expressjs.com/en/starter/installing.html
mkdir D:\myapp && cd D:\myapp
(全部回车,保持默认配置)
npm init
npm install express --save
  1. 运行本地服务

新建 index.js ,内容如下

1
2
3
4
const express = require('express')
const app = express()
app.get('/', (req, res) => res.send('Hello World!'))
app.listen(3000, () => console.log('Example app listening on port 3000!'))

运行node index.js ,就可从本地访问 http://localhost:3000

  1. 安装必要模块: 适用于Node.js的JWT编解码模块 node-jwt-simple 和 cookie解析模块 cookie-parser
1
2
3
# https://github.com/hokaccha/node-jwt-simple
npm install jwt-simple
npm install cookie-parser
  1. 一个简单的本地demo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
//index.js
//http://expressjs.com/en/4x/api.html
//https://github.com/hokaccha/node-jwt-simple

//一些初始化的工作
const express = require('express')
var jwt = require('jwt-simple')
var cookieParser = require('cookie-parser')
var jwt_secret = "this is a secret for jwt"
const app = express()
app.use(cookieParser())

app.get('/',(req,res)=>res.redirect('/help'))
app.get('/help', (req, res) => {
var RequstedURL=req.protocol+'://'+req.get('Host')
res.send([
'GET '+RequstedURL+'/login?user=name&pass=passwd to get your JSON Web Token ' ,
'GET '+RequstedURL+'/whoami to identify yourself'
].join('<br>'))
})

app.get('/login',(req,res)=>{
var users={
"admin":"admin_password_is_hard_to_guess",
"test":"test123"
}
var payload = {"name":req.query.user}
if(users[req.query.user]===req.query.pass){
res.cookie('jwt',jwt.encode(payload,jwt_secret))
res.send(req.query.user +' logged in')
}
else{res.send('login failed!')}
})

app.get('/whoami',(req,res)=>{
try {res.send("you are logged in as :<br>" +jwt.decode(req.cookies.jwt,jwt_secret)['name'])}
catch(err) {res.send("your JWT is :<br>"+req.cookies.jwt)}
})

app.listen(3000, () => {
console.log('Example app listening on port 3000!');
console.log('Example app listening on port 3000!')
})

0x03 攻击面

发现敏感信息

JWT中的headerpayload 虽然看起来不可读,但实际上都只经过简单编码,开发者可能误将敏感信息存储在里面。使用上述工具可以方便地解码JWT中前两部分的信息。

指定算法为none

上面提到算法 none 是JWT规范中强制要求实现的,但有些实现JWT的库直接将使用none 算法的token视为已经过校验。这样攻击者就可以设置algnone ,使signature 部分为空,然后构造包含任意payload 的JWT来欺骗服务端。

1525158849660

将签名算法从非对称类型改为对称类型

使用非对称加密算法(主要基于RSA、ECDSA,如S256)分发JWT的过程是使用私钥(private)加密生成JWT,使用公钥(public)解密验证。

使用对称加密算法(主要基于HMAC,如HS256)分发JWT的过程是使用同一个密钥(secret)生成和验证JWT。

如果服务端期待收到的算法类型为RS256,然后以RS256和public去验证JWT,而实际上收到的算法类型是HS256,那么服务端就可能尝试把public当作secret,然后用HS256算法解密验证JWT。

由于RS256的public人人都可获得,攻击者可以预先以public为密钥,用HS256算法伪造包含任意payload 的JWT,从而成功通过服务端的验证。

1525160523495

爆破密钥

JWT的安全性依赖于密钥的保密性,任何拥有密钥的人都可以构造任何内容的合法token。

当一个JSON Web Token 被分发出去,如果密钥不够强壮就存在被爆破的风险,而且整个爆破过程可以离线进行。

已经有人写了一些工具,推荐如下:

伪造密钥

有时JWT采用header 中的kid 字段关联校验算法的密钥,这个密钥可能是对称加密的密钥,也可能是非对称加密的公钥。如果能够猜测kid 和 密钥的关联性,攻击者就可能修改kid 来欺骗服务端,使其校验时使用攻击者可控的密钥,于是攻击者就可以伪造任意内容的可通过校验的JWT。

2017 HITB Pasty

关联性:kid是密钥URI的一部分。

from cnblogs.com/dliv3/p/7450057.html

2018强网杯 easyweb

关联性:kid 带入数据库查询对应密钥,可联合查询或者盲注。

详见 http://findneo.tech/180430ciscn/#easyweb

0x04 安全建议

  • 验证函数应忽略JWT中的algo 字段,预先就明确JWT使用的算法,如果需要使用多种算法,可以在header 中使用表示”key ID” 的kid 字段,查询每个kid 对应的算法。
  • JWT/JWS 标准应该移除 header 中的algo 字段。JWT的许多安全缺陷都来自于开发者依赖这一客户端可控的字段。
  • 开发者应升级相应库到最新版本,因为旧版本可能存在致命缺陷。

0x05 参考文章