JWT攻击详解与CTF实战

1. 前言

在现代 Web 应用中,JWT(JSON Web Token)因其轻量、跨语言、易于集成而被广泛用作身份认证与授权手段。但在 CTF 比赛和渗透测试场景里,JWT 也常常成为攻击突破口。攻击者若能篡改 Token 的内容并成功绕过服务端校验,就可能伪造任意身份,甚至获取系统的最高权限。本文将结合工具与实战题目,逐步展示 JWT 攻击的常见思路和实现方式,帮助理解其安全隐患与防御要点。

1.1 JWT简介

本部分来源于JSON Web Token 入门教程,可以看看大佬更详细的介绍。

JSON Web Token(缩写 JWT)是目前最流行的跨域认证解决方案。JWT 的原理是,服务器认证以后,生成一个 JSON 对象,发回给用户,之后用户与服务端通信的时候,都要发回这个 JSON 对象。服务器完全只靠这个对象认定用户身份。为了防止用户篡改数据,服务器在生成这个对象的时候,会加上签名。

一个JWT例子如下,是一个很长的字符串,中间用点.分隔成三个部分,分别是头部、负载、签名。

复制代码
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhZG1pbiIsImlhdCI6MTcyODU3MDg2MywiZXhwIjoxNzI4NTc4MDYzLCJuYmYiOjE3Mjg1NzA4NjMsInN1YiI6InVzZXIiLCJqdGkiOiI1NWQ3ZDBlNjI2YzFjMjk2NTY5MGEwZWFlYTk3ZjJlZSJ9.L7RdUb-HcLH4-MTea7n4S7iuwFhuAPnbCSQlpwtKFx0

1.1.1 Header

Header 通常是下面的样子。

js 复制代码
{
 "alg": "HS256",
 "typ": "JWT"
}

上面代码中,alg属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256);typ属性表示这个令牌(token)的类型(type),JWT 令牌统一写为JWT

1.1.2 Payload

Payload 用来存放实际需要传递的数据。JWT 规定了7个官方字段,供选用;除了官方字段,也可以定义私有字段

js 复制代码
iss (issuer):签发人
exp (expiration time):过期时间
sub (subject):主题
aud (audience):受众
nbf (Not Before):生效时间
iat (Issued At):签发时间
jti (JWT ID):编号

1.1.3 Signature

Signature 部分是对前两部分的签名,防止数据篡改。通过某个保存在服务器的密钥(secret),使用 Header 里面指定的签名算法(如HMAC SHA256)产生签名。

三个部分的内容用base64编码后用.连接,被返回给用户。

1.2 需要的工具

1.2.1 burpsuite插件JWT Editor

在JWT攻击中,最常用的工具之一就是 BurpSuite 的 JWT Editor 插件。安装后,Burp 会在拦截到带有 JWT 的请求时,自动识别并以可编辑的形式展示头部和负载。使用者可以直接修改其中的字段、管理或导入密钥并重新生成签名,在后续的 CTF 题目中会结合实例介绍其具体用法。插件的安装方法这里不展开,读者可自行查找相关资料。

1.2.2 hashcat算法密钥爆破

当怀疑目标使用了对称密钥(如 HS256)且强度不足时,可以借助 hashcat 进行离线爆破。若能成功得到密钥,就可以在 BurpSuite 中导入该密钥并重签令牌,从而进一步利用系统的逻辑缺陷。在后续的 CTF 题目中会结合实例介绍其具体用法。hashcat 的获取与安装方式在此不再赘述。

1.2.3 rockyou.txt密钥爆破字典

在 CTF 或渗透测试场景中,常用的密码字典是 rockyou.txt。它收录了大量真实泄露的常见密码,包括弱口令、生日组合和常见短语,对大多数弱密钥场景都能起到良好的效果。配合 hashcat 使用时,rockyou.txt 能显著提升爆破成功率。大多数 Kali 系统中默认自带该字典,一般位于 /usr/share/wordlists/ 目录下。

2. CTFshow Web 345------签名未验证

看一眼源代码,提示/admin,尝试去访问这个目录

显示302网站重定向到index.php,猜测应该是cookie中权限不足导致无法访问

这里不知道为啥JWT Editor没有识别,所以直接用decoder功能把cookie用base64解码一下,发现采用了jwt验证,但是网站未对jwt签名进行验证(alg:"None"),因此只需要把修改为sub:admin,再base64编码替换掉原来的cookie,发包成功访问/admin得到flag

3. CTFshow Web 346------JWT无签名

前端源码提示/admin/,访问/admin,使用JWT Editor自动解析jwt,可以看到本题采用HS256(HMAC+SHA-256)作为签名算法

如果网站接受没有签名的JWT,我们可以直接将alg改为none取消掉签名,一旦JWT没有签名机制,其中的内容就可以任意修改。如下,成功访问/admin得到flag

4. CTFshow Web 347------对称式签名密钥爆破

某些对称 的签名算法,例如本题的HS256(HMAC+SHA-256),可以通过爆破的方式得到密钥,从而实现修改Payloads后的重签名

前端源码提示/admin/,访问/admin/抓包,可以看到使用HS256签名,考虑进行密钥爆破

用hashcat爆破密钥,-a 0指定字典攻击,-m 16500指定hash类型为HS256,后面跟上JWT,最后是指定字典,此处使用rockyou.txt,根据爆破结果,密钥是123456

bash 复制代码
hashcat.exe -a 0 -m 16500 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhZG1pbiIsImlhdCI6MTcyODQzNzMwNiwiZXhwIjoxNzI4NDQ0NTA2LCJuYmYiOjE3Mjg0MzczMDYsInN1YiI6InVzZXIiLCJqdGkiOiJhMDZkYTg3ZWRkYWE4Zjg5MzgxYmVjMWFjMzU4NGNkYyJ9.cZ1VQX4r1Ymo_gZCs8FqfYPlOD5IriiQZjFnA2jOBzU rockyou.txt

用jwt editor生成JWT key:在JWT Editor选择New Symmetic Key,点击generate,将k值修改为base64后的密钥,点击确认,我们就可以用这个key签名了

修改subadmin,然后点左下角签名,选择刚刚创建的key签名,发包成功访问/admin/得到flag

5. CTFshow Web 348------对称式签名密钥爆破

本题过程与347题过程相同,只是密钥没那么弱了,爆出来是aaab,大家按照上一题解析尝试即可。

6. CTFshow Web 349------非对称式加密签名私钥泄露

本题泄露了js源码,简单来说实现了两个功能:

  • 通过 GET /,服务器读取私钥文件(路径为 public/private.key)利用RS256算法生成一个JWT令牌并存储在 auth cookie 中。
  • 用户通过 POST / 提交请求时,服务器检查该JWT,如果用户是 admin,则返回flag;否则,返回 "you are not admin"。
js 复制代码
/* GET home page. */
router.get('/', function(req, res, next) {
  res.type('html');
  var privateKey = fs.readFileSync(process.cwd()+'//public//private.key');
  var token = jwt.sign({ user: 'user' }, privateKey, { algorithm: 'RS256' });
  res.cookie('auth',token);
  res.end('where is flag?');
  
});

router.post('/',function(req,res,next){
	var flag="flag_here";
	res.type('html');
	var auth = req.cookies.auth;
	var cert = fs.readFileSync(process.cwd()+'//public/public.key');  // get public key
	jwt.verify(auth, cert, function(err, decoded) {
	  if(decoded.user==='admin'){
	  	res.end(flag);
	  }else{
	  	res.end('you are not admin');
	  }
	});
});

访问https://xxx.challenge.ctf.show/private.key,下载到了RS256非对称加密签名的私钥,我们可以通过该私钥对Payload进行重签名。

key 复制代码
-----BEGIN RSA PRIVATE KEY-----
MIICWwIBAAKBgQDNioS2aSHtu6WIU88oWzpShhkb+r6QPBryJmdaR1a3ToD9sXDb
eni5WTsWVKrmzmCk7tu4iNtkmn/r9D/bFcadHGnXYqlTJItOdHZio3Bi1J2Elxg8
IEBKx9g6RggTOGXQFxSxlzLNMRzRC4d2PcA9mxjAbG1Naz58ibbtogeglQIDAQAB
AoGAE+mAc995fvt3zN45qnI0EzyUgCZpgbWg8qaPyqowl2+OhYVEJq8VtPcVB1PK
frOtnyzYsmbnwjZJgEVYTlQsum0zJBuTKoN4iDoV0Oq1Auwlcr6O0T35RGiijqAX
h7iFjNscfs/Dp/BnyKZuu60boXrcuyuZ8qXHz0exGkegjMECQQD1eP39cPhcwydM
cdEBOgkI/E/EDWmdjcwIoauczwiQEx56EjAwM88rgxUGCUF4R/hIW9JD1vlp62Qi
ST9LU4lxAkEA1lsfr9gF/9OdzAsPfuTLsl+l9zpo1jjzhXlwmHFgyCAn7gBKeWdv
ubocOClTTQ7Y4RqivomTmlNVtmcHda1XZQJAR0v0IZedW3wHPwnT1dJga261UFFA
+tUDjQJAERSE/SvAb143BtkVdCLniVBI5sGomIOq569Z0+zdsaOqsZs60QJAYqtJ
V7EReeQX8693r4pztSTQCZBKZ6mJdvwidxlhWl1q4+QgY+fYBt8DVFq5bHQUIvIW
zawYVGZdwvuD9IgY/QJAGCJbXA+Knw10B+g5tDZfVHsr6YYMY3Q24zVu4JXozWDV
x+G39IajrVKwuCPG2VezWfwfWpTeo2bDmQS0CWOPjA==
-----END RSA PRIVATE KEY-----

利用JWT Editor新建一个RSA密钥,选择PEM模式,点击生成,将刚才的密钥替换进去

篡改用户权限为admin,点击重签名,选择刚才生成的密钥,点击确定

因为源码中提示要用post请求才能获取flag,这里右键change request methond改变提交方式,然后发包获得flag

7. CTFshow Web 350------篡改加密签名算法

访问源码包routes目录下的index.js,代码的基本逻辑和上题一样,就不在赘述,区别在于无法通过外网访问到private.key,因此无法直接通过私钥来进行重签名。

js 复制代码
var express = require('express');
var router = express.Router();
var jwt = require('jsonwebtoken');
var fs = require('fs');

/* GET home page. */
router.get('/', function(req, res, next) {
  res.type('html');
  var privateKey = fs.readFileSync(process.cwd()+'//routes/private.key');
  var token = jwt.sign({ user: 'user' }, privateKey, { algorithm: 'RS256' });

  res.cookie('auth',token);
  res.end('where is flag?');
  
});

router.post('/',function(req,res,next){
	var flag="flag_here";
	res.type('html');
	var auth = req.cookies.auth;
	var cert = fs.readFileSync(process.cwd()+'//routes/public.key');  // get public key
	jwt.verify(auth, cert,function(err, decoded) {
	  if(decoded.user==='admin'){
	  	res.end(flag);
	  }else{
	  	res.end('you are not admin'+err);
	  }
	});
});

module.exports = router;

但是,公钥public.key仍然可以访问,我们可以通过将 RS256 改为 HS256,并利用已知的公钥来进行重签名,能进行这样的攻击基于以下两点原理:

  1. 服务器通常在验证 JWT 时,会根据 alg 字段来选择签名验证的方式。但是如果服务器未能正地限制或验证使用的算法类型,攻击者就可以通过简单的修改(如将 RS256 改为 HS256)来利用暴露的公钥进行签名,从而绕过签名验证机制。
  2. HS256 是基于对称密钥的,因此如果攻击者获得了 RSA 公钥,我们可以将该公钥 错误地 作为 HMAC 的密钥。由于 HS256 使用 HMAC 生成签名,攻击者可以通过使用 RSA 公钥作为 HMAC 密钥来重新生成一个签名,这个签名对于服务器来说是合法的,因为服务器同样是使用公钥对签名进行校验。

具体的,先访问https://xxx.challenge.ctf.show/public.key拿到公钥,如果你尝试之前的思路:burpsuite -> JWT Editor -> New Symmetric Key -> Generate -> 然后把公钥粘贴进去替换原来的k,你就会发现PEM格式的公钥粘贴进去会报错,手动改换行,签名后也无法成功过关。看了网上的wp基本上都是用node写的脚本,因此咨询了一下 AI 大人:

node常见写法是直接 readFileSync 把 public.pem 读成字符串,然后把这整段文本(包含 BEGIN/END 行与中间的换行)原封不动喂给 HMAC 校验。别人用 node 的脚本之所以可用,往往因为它同样用 fs.readFileSync 读入整份 PEM,以完全一致的字节序列来签名;而你在 Burp 里如果把密钥处理成了"只保留中间那段 Base64"或手动改了换行、去掉了头尾、甚至拷贝时混入了 CRLF 与 LF 的差异,哪怕界面显示签名生成成功,服务端也会判定签名不匹配。

因此这边给个python脚本来跑token,拿到后替换到burp中,注意修改请求方式为POST,得到flag

python 复制代码
import jwt  
  
# 原始 JWT 粘贴到这里  
orig = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoidXNlciIsImlhdCI6MTc1NzQ4NDU1NH0.QKpMeBqmdq3D1i-3AXwpwxo1vJd7ZAoKUBD76pmOu8D9jhbXF8enHCECZ53218LPNyjbBG-h6xVycQ7kHi0vLzBJHM0P4zuqCJMd0CkDksNVf0vlznp4LcmxqWHhok38ohY5tNgR1uE5ULDa9rOVt2_T0juJPWDD-h_360-S0NA"  
  
# 解析 JWT,获取头部和载荷  
header = jwt.get_unverified_header(orig)  
payload = jwt.decode(orig, options={"verify_signature": False})  
print("Header:\n",header,"\n","Payload:\n",payload,"\n")  
  
# 修改头部和载荷  
header["alg"] = "HS256"  
payload["user"] = "admin"  
  
# 重新签名并输出新的 JWT
# 读取公钥全文
with open("public.key", "rb") as f:  
    key = f.read()  
  
token = jwt.encode(payload, key, algorithm="HS256", headers=header)  
print(token)

参考资料

JSON Web Token 入门教程
https://systw.net/note/archives/1448
https://xz.aliyun.com/t/6776?u_atoken=ffd74032d1723caf0489c188e5a6a3a7&u_asig=1a0c399b17284346082418631e0097
https://www.keepersecurity.com/blog/zh-hans/2023/08/04/understanding-rockyou-txt-a-tool-for-security-and-a-weapon-for-hackers/

宇宙安全声明

本博客所提供的内容仅供学习与交流,旨在提高网络安全技术水平,谨遵守国家相关法律法规,请勿用于违法用途,博主不对任何人因使用博客中提到的技术或工具而产生的任何后果负责。如果您对文章内容有疑问,可以留言私信。