逻辑缺陷——弱密钥
JWT安全漏洞
JWT(JSON Web Token)是一种用于身份认证和授权的开放标准,它通过在网络应用间传递被加密的JSON数据来安全地传输信息使得身份验证和授权变得更加简单和安全,JWT对于渗透测试人员而言可能是一种非常吸引人的攻击途径,因为它们不仅是让你获得无限访问权限的关键而且还被视为隐藏了通往以下特权的途径,例如:特权升级、信息泄露、SQLi、XSS、SSRF、RCE、LFI等
基础概念
- JWS:Signed JWT,签名过的JWT
- JWK:JWT的密钥,也就是我们常说的SECRET
- JWE:Encrypted JWT部分payload经过加密的JWT
- JKU:JKU(JSON Web Key Set URL)是JWT Header中的一个字段,字段内容是一个URI,该URI用于指定用于验证令牌秘钥的服务器,该服务器用于回复JWK
- X5U:X5U是JWT Header中的一个字段,指向一组X509公共证书的URL,与JKU功能类似
- X.509标准:X.509标准是密码学里公钥证书的格式标准,包括TLS/SSL(WWW万维网安全浏览的基石)在内的众多Internet协议都应用了X.509证书)
基本结构
JWT(JSON Web Token)的结构由三部分组成,分别是Header、Payload和Signature,下面是每一部分的详细介绍和示例:
Header
Header包含了JWT使用的算法和类型等元数据信息,通常使用JSON对象表示并使用Base64编码,Header中包含两个字段:alg和typ
alg(algorithm):指定了使用的加密算法,常见的有HMAC、RSA和ECDSA等算法
typ(type):指定了JWT的类型,通常为JWT
下面是一个示例Header:
1 | { |
其中alg指定了使用HMAC-SHA256算法进行签名,typ指定了JWT的类型为JWT
Payload
Payload包含了JWT的主要信息,通常使用JSON对象表示并使用Base64编码,Payload中包含三个类型的字段:注册声明、公共声明和私有声明
- 公共声明(Public Claims):是自定义的字段,用于传递非敏感信息,例如:用户ID、角色等
- 私有声明(Private Claims):是自定义的字段,用于传递敏感信息,例如密码、信用卡号等
- 注册声明(Registered Claims):预定义的标准字段,包含了一些JWT的元数据信息,例如:发行者、过期时间等
下面是一个示例Payload:
1 | { |
其中sub表示主题,name表示名称,iat表示JWT的签发时间
Signature
Signature是使用指定算法对Header和Payload进行签名生成的,用于验证JWT的完整性和真实性,Signature的生成方式通常是将Header和Payload连接起来然后使用指定算法对其进行签名,最终将签名结果与Header和Payload一起组成JWT,Signature的生成和验证需要使用相同的密钥,下面是一个示例Signature
1 | HMACSHA256(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret) |
其中HMACSHA256是使用HMAC SHA256算法进行签名,header和payload是经过Base64编码的Header和Payload,secret是用于签名和验证的密钥,最终将Header、Payload和Signature连接起来用句点(.)分隔就形成了一个完整的JWT,下面是一个示例JWT,其中第一部分是Header,第二部分是Payload,第三部分是Signature,注意JWT 中的每一部分都是经过Base64编码的,但并不是加密的,因此JWT中的信息是可以被解密的
1 | eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. |
攻击方式
有缺陷的JWT签名验证
接受任意签名
原理:JWT库通常提供两种方法来处理令牌:一种是验证令牌,另一种只是解码令牌。例如,在Node.js的jsonwebtoken库中,有verify()和decode()方法。
verify() 方法:用于验证令牌的签名是否有效,同时解码令牌内容。如果签名无效,该方法会抛出错误。
decode() 方法:仅用于解码令牌内容,不解密或验证签名。
有时,开发人员可能会混淆这两个方法,错误地只将传入的令牌传递给decode()方法。这样做实际上意味着应用程序没有验证签名,从而可能引入安全风险。
接收空签名
原理:在JWT的Header中,alg的值会告诉服务器用哪种算法对令牌签名(可变为客户端决定),这样服务器在验证签名时就知道该用哪种算法。
第一个示例:alg被设置为”HS256”,表示使用HMAC和SHA256算法对令牌进行签名。服务器在验证签名时会使用相同的算法和密钥。
{ “alg”: “HS256”, “typ”: “JWT” }
第二个示例:alg被设置为”None”,表示不使用签名。这意味着任何令牌都会被服务器视为有效,因为没有签名需要验证。这种设置在生产环境中非常危险,因为它允许攻击者伪造令牌。
{ “alg”: “None”, “typ”: “JWT” }
如果在生产环境中没有关闭这个功能,攻击者就可以利用它,把alg设为”None”,然后伪造出想要的令牌,用这个伪造的令牌冒充任意用户登录网站。
敏感信息泄露
JWT的header头base64解码(可泄露敏感数据)如:密钥文件或者密码
1 | eyJraWQiOiJrZXlzLzNjM2MyZWExYzNmMTEzZjY0OWRjOTM4OWRkNzFiODUxIiwidHlwIjoiSldUIiwiYWxnIjoiUlMyNTYifQ |
其中认证类型为JWT,加密算法为RS256,kid指定加密算法的密钥,密钥KID的路径为:keys/3c3c2ea1c3f113f649dc9389dd71b851k,则在 Web 根目录中查找 /key/3c3c2ea1c3f113f649dc9389dd71b851k 和/key/3c3c2ea1c3f113f649dc9389dd71b851k.pem
密钥文件后缀:.cer、 .crt、.jwk、.p8、.keystore、.pfx、.p12、.pem、.jks、.der
暴力破解密钥
原理:HS256(对称加密算法)签名算法使用任意独立字符串作为密钥,此秘密必须难以被攻击者猜到或暴力破解。否则,攻击者可利用此密钥,以任意头部和载荷值创建并重新签名 JWT。
开发人员在实现 JWT 应用时易犯错误,如未更改默认值(过于简单)或占位符密码(类似${db.password}的占位符),甚至直接使用从网上复制的代码片段中的示例硬编码秘密。此时,攻击者使用已知秘密的单词列表,可轻松暴力破解服务器秘密。
1 | hashcat -m 16500 -a 0 /root/JWT/jwt.txt /root/JWT/jwt_secrets_list.txt --force |
JWT头参数注入
空签名
同前
jwk (JSON Web Key) 注入
jwk 参数允许在 JWT 头部直接嵌入用于验证该 Token 签名的公钥。服务器理应仅借助预先设定的有限公钥白名单(例如:keys数组)来验证 JWT 签名,以此确保验证过程的安全性与可控性。然而,部分服务器由于配置失误,有时会直接采用 jwk 参数里嵌入的任意密钥进行验证,这无疑为系统埋下了安全隐患。
- 攻击原理:攻击者生成一对自己的公钥和私钥。他们使用自己的私钥对伪造的 Token 进行签名,然后将自己的公钥嵌入到 Header 的
jwk参数中。 - 结果:如果服务器盲目信任 Header 中的
jwk参数,它会提取出攻击者的公钥,并用该公钥成功验证攻击者伪造的签名。
攻击者生成自己的密钥对
生成 RSA 私钥 (2048位)
我们在本地生成一对全新的 RSA 密钥对。这是完全受我们自己控制的。1
openssl genrsa -out private.pem 2048
从私钥中提取公钥
1 | openssl rsa -in private.pem -pubout -out public.pem |
格式转换
1 | step crypto jwk create public_key.json --from-pem public_key.pem |
jku参数注入自签名JWT
有些服务器不直接使用jwk头参数嵌入公钥,而是允许您使用jku(或者x5u)头参数来引用包含密钥的JWK Set。当验证签名时,服务器从该URL获取相关密钥。
在公网上提供一个可以通过 HTTP/HTTPS 访问的静态 JSON 文件。目标存在漏洞的服务器会读取 JWT 头的 jku 字段,向你的这台利用服务器发起 GET 请求。
- 构造恶意的 JWT Header:
1
2
3
4{
"alg": "RS256",
"jku": "https://你搭建的服务器地址/jwks.json"
} - 构造你想要的 Payload(比如提权到 admin)。
- 用你本地的 私钥 (
private.pem) 对这个 Header 和 Payload 进行签名。 - 把生成的 Token 发送给目标系统。
目标系统解析 Header,看到 jku,就会去你的服务器下载那个 jwks.json,提取里面的公钥,然后完美通过验证!
JWT通过kid头路径(目录遍历)造成空密钥签名或者SQL注入或者命令注入
原理:密钥 ID (kid) 是一个可选header,是字符串类型,用于表示文件系统或数据库中存在的特定密钥,然后使用其中内容来验证签名。如果有多个用于签署令牌的密钥,则此参数很有帮助,但如果它是可注入的,则可能很危险,因为攻击者可以指向内容可预测的特定文件。
思考:可遍历存在危害(空密钥签名)、存储在数据库危害(SQL注入)思考:可遍历存在危害(空密钥签名)、存储在数据库危害(SQL注入)
JWT算法混淆
核心原理:把公钥当成“密码”
要理解这个漏洞,我们先对比一下这两种算法:
RS256(非对称):服务器用私钥签名,用公钥验证。公钥是可以公开的,谁都可以拿去验证 Token 的真伪,但只有持有私钥的服务器能生成合法的 Token。
HS256(对称):服务器用同一个共享密钥(Secret)进行签名和验证。这个 Secret 就像一段密码字符串,绝对不能泄露。
漏洞发生的条件:
目标服务器配置为使用 RS256(非对称加密)来生成和验证 JWT。
攻击者想办法获取了服务器的公钥(比如公钥通常会暴露在应用的公开接口,或者通过信息泄露漏洞拿到)。
服务器端的 JWT 验证库配置不当,盲目信任了 JWT Header 里的
alg字段。
魔术时刻(攻击流程):
篡改 Header:攻击者将原始 Token Header 中的
"alg": "RS256"修改为"alg": "HS256"。伪造 Payload:篡改提权信息(比如把
role改为admin)。重新签名:这是最绝的一步。攻击者手里没有私钥,但有公钥。因为 Header 已经被改成了 HS256,攻击者在本地将获取到的公钥文件内容(一段字符串)作为 HS256 的对称密钥(Secret),对伪造的 Token 进行 HMAC 签名!
JWT重放
简单来说,重放攻击就是攻击者拦截(或获取)了一个合法且有效的 JWT,然后在未经授权的情况下,再次(甚至多次)向服务器发送这个完全相同的 JWT,以此来欺骗服务器执行特定操作。
常见的重放攻击场景
在实战或者打靶机时,JWT 重放通常会引发以下两类致命的安全问题:
1. 身份验证凭证窃取(长期会话劫持)
场景:攻击者通过 XSS 漏洞、中间人攻击(如果未严格使用 HTTPS)或物理接触,窃取了用户的有效 JWT。
利用:由于 JWT 签发后无法轻易在服务端撤销,攻击者可以拿着这个 Token 不断重放请求,长期伪装成该用户,直到 Token 的
exp(过期时间)到达。
2. 业务逻辑漏洞(高发灾区)
场景:很多时候 JWT 不仅仅用于登录,还用于一次性授权。例如:密码重置链接中携带的 JWT、支付成功后的回调确认 JWT、或者是邀请注册的凭证。
利用:
如果是一个“转账 100 元”的接口,且授权信息全靠一个短效 JWT。攻击者拦截该请求并用脚本重放 100 次,如果后端没有做防重放校验,可能会导致账户被扣款 100 次,或者接收方收到 10000 元。
如果是“密码重置”的 JWT,攻击者截获后,即便受害者已经重置了密码,攻击者依然可以在 Token 过期前重放该 Token,再次把密码改成攻击者想要的。
Flask session安全漏洞
Flask中的Session,它是存在于客户端的,也就是说我们在进行登录过后可以看到自己的Session值,而当我们对这个Session值进行base64解码后,就可以读取它的具体内容。
对应Flask,它在生成session时会使用app.config['SECRET_KEY']中的值作为salt对session进行一个简单处理,那么这里的话,只要key不泄露,我们就只能得到具体内容,但是无法修改具体内容,因此这个时候就引发了一个问题,当key泄露的时候,就出现了内容伪造的情况,比如具体内容为{'name':'123'},而当我们掌握key时,可修改内容为{'name':'admin'},从而达到一个越权的效果。
CTF 中获取 Flask SECRET_KEY 的常见姿势
在 CTF 的 Web 题中,如果考点是 Flask Session 伪造,通常会伴随其他漏洞来让你先拿到 SECRET_KEY。常见的获取途径有以下几种:
1. SSTI (服务端模板注入)
这是 Flask 题目中最经典的组合拳。Jinja2 模板引擎中包含全局对象 config,如果存在 SSTI 漏洞,可以直接把应用的配置项打印出来。
- 常用 Payload:
{{ config['SECRET_KEY'] }}{{ config }}(直接 dump 所有配置)- 如果
config被过滤,可以尝试通过其它全局变量绕过:{{ url_for.__globals__['current_app'].config['SECRET_KEY'] }}
2. 任意文件读取 / 文件包含漏洞
如果题目存在目录遍历或任意文件读取漏洞,可以直接去读取包含 Key 的敏感文件。
- 读取源代码: 尝试读取主程序
app.py、main.py或者配置文件config.py、settings.py - 读取环境变量: 很多时候
SECRET_KEY会配置在环境变量里。在 Linux 系统下,可以尝试读取/proc/self/environ文件,里面包含了当前进程的所有环境变量。
3. 弱密钥爆破 (Brute-forcing)
如果出题人设置了一个很简单的弱口令(比如 secret、123456、dev_key),或者 Key 存在于常见的弱口令字典中,我们可以直接在本地对抓到的 Session 密文进行离线爆破。
- 这不需要与服务器进行任何交互,完全靠算力。
flask-unsign
专门针对 Flask 框架的 session 伪造与爆破工具。
爆破命令1
flask-unsign --unsign --cookie "你的Flask_Session字符串" --wordlist /path/to/wordlist.txt
4. 信息泄露
源码泄露: 比如
.git源码泄露、.svn泄露、或是提供的 Dockerfile /docker-compose.yml中硬编码了环境变量。Debug 模式: 如果 Flask 开启了 Debug 模式(Werkzeug Debugger),并且页面报错,报错页面中有时会泄露环境信息;甚至如果能算出 PIN 码进入控制台,就可以直接执行 Python 代码获取
app.config['SECRET_KEY']。


