0x01 初识JWT
JWT ( JSON Web Token
的缩写)是一串带有声明信息的字符串,由服务端用加密算法对信息签名来保证其完整性和不可伪造。Token里可以包含所有必要信息,这样服务端就无需保存任何关于用户或会话的信息,JWT可用于身份认证、会话状态维持、信息交换等。
JWT 由三部分构成,分别称为 header
、payload
和 signature
,各部分用.
相连构成一个完整的Token,形如xxxxx.yyyyy.zzzzz
。
分别看下各个部分:
header :
使用一个JSON格式字符串声明token的类型和签名用的算法等,形如{"alg": "HS256", "typ": "JWT"}
。该字符串经过Base64Url编码后形成JWT的第一部分xxxxx
。
Base64Url编码可以用这段代码直观理解:
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格式字符串描述所要声明的信息,分为 registered
、public
、 和 private
三类,形如{"name": "John Doe", "admin": true}
,具体信息可参考 RFC7519 的 JWT claims 部分。
同样的,该字符串经过Base64Url编码形成JWT的第二部分yyyyy
。
signature :
将 xxxxx.yyyyy
使用alg
指定的算法加密,然后再Base64Url编码得到JWT的第三部分zzzzz
。所支持的算法 类型取决于实现,但HS256
和 none
是强制要求实现的。
0x02 简单应用
在本地运行起简单的基于Express的可发放和处理JWT的服务。
安装Node.js。Node.js是JavaScript运行时环境,采用轻量高效的事件驱动、无阻塞I/O模型,拥有最大的开源库生态nmp。
Windows平台可在 https://nodejs.org/en/download/ 下载安装包
*nix 平台可根据 https://nodejs.org/en/download/package-manager/ 提示使用包管理器安装
安装Express,一款基于Node.js的快速、灵活、极简的Web框架。
# http://expressjs.com/en/starter/installing.html
mkdir D:\myapp && cd D:\myapp
(全部回车,保持默认配置)
npm init
npm install express --save
运行本地服务
新建 index.js
,内容如下
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
。
安装必要模块: 适用于Node.js的JWT编解码模块
node-jwt-simple
和 cookie解析模块cookie-parser
# https://github.com/hokaccha/node-jwt-simple
npm install jwt-simple
npm install cookie-parser
一个简单的本地demo
//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!')
})
推荐几个在线工具
by Sjoerd Langkemper
by Kenji Urushima
支持更多算法 kjur/jsjws
0x03 攻击面
发现敏感信息
JWT中的header
和 payload
虽然看起来不可读,但实际上都只经过简单编码,开发者可能误将敏感信息存储在里面。使用上述工具可以方便地解码JWT中前两部分的信息。
指定算法为none
上面提到算法 none
是JWT规范中强制要求实现的,但有些实现JWT的库直接将使用none
算法的token视为已经过校验。这样攻击者就可以设置alg
为none
,使signature
部分为空,然后构造包含任意payload
的JWT来欺骗服务端。
将签名算法从非对称类型改为对称类型
使用非对称加密算法(主要基于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,从而成功通过服务端的验证。
爆破密钥
JWT的安全性依赖于密钥的保密性,任何拥有密钥的人都可以构造任何内容的合法token。
当一个JSON Web Token 被分发出去,如果密钥不够强壮就存在被爆破的风险,而且整个爆破过程可以离线进行。
已经有人写了一些工具,推荐如下:
伪造密钥
有时JWT采用header
中的kid
字段关联校验算法的密钥,这个密钥可能是对称加密的密钥,也可能是非对称加密的公钥。如果能够猜测kid
和 密钥的关联性,攻击者就可能修改kid
来欺骗服务端,使其校验时使用攻击者可控的密钥,于是攻击者就可以伪造任意内容的可通过校验的JWT。
一个例子:2017 HITB Pasty
0x04 安全建议
验证函数应忽略JWT中的
algo
字段,预先就明确JWT使用的算法,如果需要使用多种算法,可以在header
中使用表示"key ID" 的kid
字段,查询每个kid
对应的算法。JWT/JWS 标准应该移除
header
中的algo
字段。JWT的许多安全缺陷都来自于开发者依赖这一客户端可控的字段。开发者应升级相应库到最新版本,因为旧版本可能存在致命缺陷。