Nacos 认证绕过漏洞(CVE-2021-29441)及其补丁绕过分析

漏洞原理本身不复杂,但是整个分析过程、后续的补丁绕过,以及认证绕过的后续利用挖掘,还是很有意思,因此写这篇文章进行分析。

漏洞背景

阿里巴巴在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,不在进行认证校验。

  1. public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
  2. throws IOException, ServletException {
  3. if (!authConfigs.isAuthEnabled()) {
  4. chain.doFilter(request, response);
  5. return;
  6. }
  7. HttpServletRequest req = (HttpServletRequest) request;
  8. HttpServletResponse resp = (HttpServletResponse) response;
  9. String userAgent = WebUtils.getUserAgent(req);
  10. if (StringUtils.startsWith(userAgent, Constants.NACOS_SERVER_HEADER)) {
  11. chain.doFilter(request, response);
  12. return;
  13. }
  14. try {
  15. Method method = methodsCache.getMethod(req);
  16. if (method == null) {
  17. chain.doFilter(request, response);
  18. return;
  19. }
  20. if (method.isAnnotationPresent(Secured.class) && authConfigs.isAuthEnabled()) {
  21. if (Loggers.AUTH.isDebugEnabled()) {
  22. Loggers.AUTH.debug("auth start, request: {} {}", req.getMethod(), req.getRequestURI());
  23. }
  24. Secured secured = method.getAnnotation(Secured.class);
  25. String action = secured.action().toString();
  26. String resource = secured.resource();
  27. if (StringUtils.isBlank(resource)) {
  28. ResourceParser parser = secured.parser().newInstance();
  29. resource = parser.parseName(req);
  30. }
  31. if (StringUtils.isBlank(resource)) {
  32. // deny if we don't find any resource:
  33. throw new AccessException("resource name invalid!");
  34. }
  35. authManager.auth(new Permission(resource, action), authManager.login(req));
  36. }
  37. chain.doFilter(request, response);
  38. } catch (AccessException e) {
  39. if (Loggers.AUTH.isDebugEnabled()) {
  40. Loggers.AUTH.debug("access denied, request: {} {}, reason: {}", req.getMethod(), req.getRequestURI(),
  41. e.getErrMsg());
  42. }
  43. resp.sendError(HttpServletResponse.SC_FORBIDDEN, e.getErrMsg());
  44. return;
  45. } catch (IllegalArgumentException e) {
  46. resp.sendError(HttpServletResponse.SC_BAD_REQUEST, ExceptionUtil.getAllExceptionMsg(e));
  47. return;
  48. } catch (Exception e) {
  49. resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Server failed," + e.getMessage());
  50. return;
  51. }
  52. }

绕过认证之后就可以进行很多危险的操作,例如com.alibaba.nacos.console.controller.UserController中的操作。

  1. @Secured(resource = NacosAuthConfig.CONSOLE_RESOURCE_NAME_PREFIX + "users", action = ActionTypes.WRITE)
  2. @PostMapping
  3. public Object createUser(@RequestParam String username, @RequestParam String password) {
  4. User user = userDetailsService.getUserFromDatabase(username);
  5. if (user != null) {
  6. throw new IllegalArgumentException("user '" + username + "' already exist!");
  7. }
  8. userDetailsService.createUser(username, PasswordEncoderUtil.encode(password));
  9. return RestResultUtils.success("create user ok!");
  10. }

这个controller中包含了创建用户、删除用户等行为

如下poc即可创建一个新用户

  1. POST /nacos/v1/auth/users?username=123&password=123 HTTP/1.1
  2. User-Agent: Nacos-Server
  3. Host: 127.0.0.1:8848
  4. Accept: */*

补丁修复

在1.4.1版本中,增加了一段修复代码,第一个if中,为原本的逻辑,也是默认情况下的逻辑,依然是判断User-Agent头中是否是以Nacos-server开头,,第二个if中为新增逻辑,从用户的请求中获取一个键值对,判断与配置中的键值对是否相同,如果不相同则不会进入chain.doFilter

补丁绕过

在补丁的第二个if中,如果用户开启了这个安全配置,且攻击者匹配失败,那么不会进入chain.doFilter,而是继续往之后的流程走,而在这段代码的下方,是这段代码

  1. try {
  2. Method method = methodsCache.getMethod(req);
  3. if (method == null) {
  4. chain.doFilter(request, response);
  5. return;
  6. }

如果能使getMethod方法返回null,那么认证就会被绕过。

  1. public Method getMethod(HttpServletRequest request) {
  2. String path = getPath(request);
  3. if (path == null) {
  4. return null;
  5. }
  6. String httpMethod = request.getMethod();
  7. String urlKey = httpMethod + REQUEST_PATH_SEPARATOR + path.replace(contextPath, "");
  8. List<RequestMappingInfo> requestMappingInfos = urlLookup.get(urlKey);
  9. if (CollectionUtils.isEmpty(requestMappingInfos)) {
  10. return null;
  11. }
  12. List<RequestMappingInfo> matchedInfo = findMatchedInfo(requestMappingInfos, request);
  13. if (CollectionUtils.isEmpty(matchedInfo)) {
  14. return null;
  15. }

从代码来看,有多个返回null的机会,先看第一个getPath函数

  1. private String getPath(HttpServletRequest request) {
  2. String path = null;
  3. try {
  4. path = new URI(request.getRequestURI()).getPath();
  5. } catch (URISyntaxException e) {
  6. LOGGER.error("parse request to path error", e);
  7. }
  8. return path;
  9. }

这个是我们的请求路径,不可能为null,看第二部分

  1. String urlKey = httpMethod + REQUEST_PATH_SEPARATOR + path.replace(contextPath, "");
  2. List<RequestMappingInfo> requestMappingInfos = urlLookup.get(urlKey);
  3. if (CollectionUtils.isEmpty(requestMappingInfos)) {
  4. return null;
  5. }

这个urllookup存放了所有的api

这里的绕过用到了一个小trick,一个普通的请求

  1. http://127.0.0.1/user/login?username=1&amp;password=2

通过new URI(request.getRequestURI()).getPath();处理后,得到的path是/user/login

但是如果请求长这个样子

  1. http://127.0.0.1/user/login/?username=1&amp;password=2

那么得到的path会是/user/login/

而这样子的path,在urlkey中会get不到数据,从而导致了绕过,并且在后续的filter处理中这个多出来的/并不会影响路由结果。

绕过补丁

官方在这个commit中修复了这此绕过https://github.com/alibaba/nacos/commit/2cc0be6ae1cee1f2bcd2b19886380a15004eae47#diff-d5e3e36338473d502083b47c9a5d3e162203eb17eea81e406bfa2e046ff30c7f

在urllookup中存放URL路径时均会在最后增加一个/,导致之前的绕过失效。

  • 发表于 2021-08-31 16:39:08
  • 阅读 ( 12681 )
  • 分类:漏洞分析

1 条评论

无糖
无糖

8 篇文章

站长统计