FastJSON autoType is not support problem solving

overview

The problem reported by the product when using the internal background management system.

So I logged into the platform and found the following error details:
insert image description here

Troubleshoot

After analysis, it is not difficult to know that the request is forwarded from the gateway to the corresponding statistical service statistics, which has an interface /api/statistics/data/overview. Go to ELK to find the error log of the statistics service, but no ERROR log is found. Then go to the gateway gateway service and find the following error log:

ERROR | o.s.b.a.web.reactive.error.AbstractErrorWebExceptionHandler | error | 122 | - [96c03c98] 500 Server Error for HTTP POST "/api/statistics/data/overview"
com.alibaba.fastjson.JSONException: autoType is not support. com.aba.rbac.modules.security.security.JwtUser
    at com.alibaba.fastjson.parser.ParserConfig.checkAutoType(ParserConfig.java:1542)
    at com.alibaba.fastjson.parser.DefaultJSONParser.parseObject(DefaultJSONParser.java:343)
    at com.alibaba.fastjson.parser.DefaultJSONParser.parse(DefaultJSONParser.java:1430)
    at com.alibaba.fastjson.parser.DefaultJSONParser.parse(DefaultJSONParser.java:1390)
    at com.alibaba.fastjson.JSON.parse(JSON.java:181)
    at com.alibaba.fastjson.JSON.parse(JSON.java:191)
    at com.alibaba.fastjson.JSON.parse(JSON.java:147)
    at com.alibaba.fastjson.JSON.parseObject(JSON.java:252)
    at com.aba.gateway.filter.PermissionFilter.filter(PermissionFilter.java:123)

code analysis

The error code is as follows:

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.google.common.collect.Sets;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

/**
 * 后台操作权限过滤器
 **/
@Slf4j
@Component
public class PermissionFilter implements GlobalFilter, Ordered {
    
    

    @Autowired
    private RedisTemplate redisTemplate;
    @Resource
    private JwtTokenUtil jwtTokenUtil;
    @Value("${jwt.header}")
    private String tokenHeader;
    @Value("${gwb.referer}")
    private String imsHost;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    
    
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();
        String requestPath = request.getURI().getPath();
        HttpHeaders headers = request.getHeaders();
        // 初始值,默认为false,表示无权限
        AtomicBoolean isPermission = new AtomicBoolean(false);
        String username = headers.getFirst("username");
        String origin = headers.getFirst("origin");
        if (!StringUtils.isEmpty(origin)) {
    
    
            if (!origin.equals(imsHost) && !origin.contains("localhost")) {
    
    
                log.error("origin非法:{}", origin);
                throw new IllegalArgumentException("origin非法!");
            }
        }
		// 排除登录接口
        if (!requestPath.contains("/auth/login/ldap") && !requestPath.contains("/api/rbac")) {
    
    
            Assert.notNull(username, "header中的username不能为空");
            final String requestHeader = headers.getFirst(this.tokenHeader);
            Boolean invalid;
            if (StringUtils.isEmpty(requestHeader)) {
    
    
                log.error("token为空!");
                invalid = true;
            } else {
    
    
                try {
    
    
                    String authToken = requestHeader.substring(7);
                    invalid = jwtTokenUtil.isTokenExpired(authToken);
                    String tokenName = jwtTokenUtil.getUsernameFromToken(authToken);
                    if (!username.equals(tokenName)) {
    
    
                        Response<Void> response = Response.error(9642, "token非法!");
                        log.info("token中用户与username不一致!");
                        // 设置body
                        DataBuffer bodyDataBuffer = response.bufferFactory().wrap(JsonUtil.beanToJson(response).getBytes(StandardCharsets.UTF_8));
                        return response.writeWith(Mono.just(bodyDataBuffer));
                    }
                } catch (Exception e) {
    
    
                    log.error("jwt校验发生异常!", e);
                    invalid = true;
                }
            }
            if (invalid) {
    
    
                Response<Void> response = Response.error(9642, "token已失效!");
                log.info("token失效!");
                //设置body
                DataBuffer bodyDataBuffer = response.bufferFactory().wrap(JsonUtil.beanToJson(response).getBytes(StandardCharsets.UTF_8));
                return response.writeWith(Mono.just(bodyDataBuffer));
            }
            ValueOperations<String, Object> operations = redisTemplate.opsForValue();
            String postData = (String) operations.get(username);
            HashSet<String> roles;
            if (StringUtils.isBlank(postData)) {
    
    
                roles = Sets.newHashSet();
            } else {
    
    
            	// 报错行
                JSONObject jsonObject = JSON.parseObject(postData);
                roles = (HashSet<String>) jsonObject.get("roles");
            }
            if (roles.contains(requestPath)) {
    
    
                isPermission.set(true);
            } else {
    
    
                roles.forEach(role -> {
    
    
                    if (requestPath.contains(role)) {
    
    
                        isPermission.set(true);
                    }
                });
            }
            // 停止转发没有用户登录的请求
            if (!isPermission.get()) {
    
    
                Response<Void> response = Response.error(9641, "权限不足,请检查配置!");
                DataBuffer bodyDataBuffer = response.bufferFactory().wrap(JsonUtil.beanToJson(response).getBytes(StandardCharsets.UTF_8));
                return response.writeWith(Mono.just(bodyDataBuffer));
            }
        }
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
    
    
        return Integer.MIN_VALUE;
    }
}

Code analysis:
Check whether the header=origin of the front-end request is legal, check whether the header=username is empty, analyze whether the token in the header is empty or expired, and whether the username parsed from the token is consistent with the header=username. After all the checks are passed, the permissions of the user username are obtained from Redis. The permissions are a set collection, indicating all the request URL paths that the user has. Compare the permission set<url>with the URL requested by the user. If it is an element in the set collection, it will be allowed, otherwise an error will be reported for insufficient permission.

In addition, the cached data is placed in Redis when logging in, that is, in the above /auth/login/ldapinterface. After various checks are passed, the last thing to do. The code is omitted.

johnny.wong用户权限信息存入缓存中:com.aba.rbac.modules.security.security.JwtUser@32525c81

Google

Meanwhile, a Google search revealed. Refer to enable_autotype , FastJSON has a code execution vulnerability in versions 1.2.24 and earlier. When a malicious attacker submits a carefully constructed serialized data to the server, due to the vulnerability in FastJSON deserialization, it can lead to remote arbitrary code execution. .

Since version 1.2.25+, some autoType functions are disabled, that is, @typethe functions of this specified type will be limited to a certain range. When deserializing an object, you need to check whether autoType is enabled. An error will be reported if it is not turned on.

recurrent

If a problem is found in the production environment, if the problem is located simply by analyzing the logs, and the problem can be solved immediately, then it is not a problem.

If the problem can be reproduced in the local development environment, it is not a big problem.

Make a debugging breakpoint on the error reporting line mentioned above, and if an error occurs as scheduled, it will be easy to handle:
insert image description here
the error appears on the line of JSON deserialization: JSONObject jsonObject = JSON.parseObject(postData);. postData is the cached data in Redis. Let's take a look at what the data in Redis looks like:
insert image description here
it is found that it @typeis exactly the same as the full path name mentioned in the error report. In other words, the problem I am encountering now is 95% likely to be the deserialization vulnerability scenario problem found in the Google search above.

Let's take a look at the recent changes of the gateway gateway service:
insert image description here
remove the version number specified in the pom file in the gateway gateway service, and directly use parentthe version number specified in the pom file:
insert image description here
that is, upgrade from 1.2.20 to 1.2.83.

When the gateway service used FastJSON version 1.2.20, there was actually a deserialization vulnerability. There is a loophole in the gateway service! ! After upgrading to version 1.2.83, the deserialization vulnerability is resolved, but autoType needs to be manually enabled, and autoType is disabled by default, otherwise deserialization fails.

resolution process

Based on the above conclusions and the fact that the problem can be reproduced locally, the solution to the problem is of course simple.

global

In the Spring Bean class that will definitely be executed, add the following static code base and enable global autoType:

@Configuration
public class RedisConfig {
    
    
    static {
    
    
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
    }
}

But there is still an error:
insert image description here
from the screenshot above, the error at this time @typeis another full path package name.

Hold on tight, don't panic.

Continue to read the official documentation, Google search for the official GitHub issue.

Attachment: The source code snippet of the error:

if ((!internalWhite) && (autoTypeSupport || expectClassFlag)) {
    
    
    long hash = h3;
    for (int i = 3; i < className.length(); ++i) {
    
    
        hash ^= className.charAt(i);
        hash *= fnv1a_64_magic_prime;
        if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
    
    
            clazz = TypeUtils.loadClass(typeName, defaultClassLoader, true);
            if (clazz != null) {
    
    
                return clazz;
            }
        }
        if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
    
    
            if (Arrays.binarySearch(acceptHashCodes, fullHash) >= 0) {
    
    
                continue;
            }
            throw new JSONException("autoType is not support. " + typeName);
        }
    }
}

Full path

If there is a problem with global opening, then open the whitelist and specify the full path of each package name:

static {
    
    
//        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
    ParserConfig.getGlobalInstance().addAccept("com.aba.rbac.modules.security.security.JwtUser");
    ParserConfig.getGlobalInstance().addAccept("org.springframework.security.core.authority.SimpleGrantedAuthority");
}

But there is still an error:
insert image description here
the source code of the error is as follows:

if (!autoTypeSupport) {
    
    
    long hash = h3;
    for (int i = 3; i < className.length(); ++i) {
    
    
        char c = className.charAt(i);
        hash ^= c;
        hash *= fnv1a_64_magic_prime;
        if (Arrays.binarySearch(denyHashCodes, hash) >= 0) {
    
    
            if (typeName.endsWith("Exception") || typeName.endsWith("Error")) {
    
    
                return null;
            }
            throw new JSONException("autoType is not support. " + typeName);
        }
    }
}

Note: The above two times throw new JSONException("autoType is not support. " + typeName)are all methods in the source code

public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
    
    
}

in.

According to the above code, it is not difficult to know that the repair method is as follows, not only to enable the global autoType, but also to manually specify the following full path package name:

static {
    
    
    // https://github.com/alibaba/fastjson/wiki/enable_autotype
    ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
    ParserConfig.getGlobalInstance().addAccept("com.aba.rbac.modules.security.security.JwtUser");
    ParserConfig.getGlobalInstance().addAccept("org.springframework.security.core.authority.SimpleGrantedAuthority");
}

Note: In order to ensure that this static code block must be executed, it must be placed in the class scanned by Spring Bean, or directly in the Spring Boot startup class.

reference

Guess you like

Origin blog.csdn.net/lonelymanontheway/article/details/130975661