XXL-Job 未授权访问是分布式任务调度平台最常见的一类上线事故:调度中心后台没有改默认口令、执行器内置的 HTTP 端口直接暴露在公网、accessToken 保持空值,这三个条件任意成立一个,攻击者就能不经过任何认证触发任务,进而通过 GLUE 模式执行任意脚本拿到服务器权限。本文按「先隔离、再鉴权、后审计」的顺序,给出一套可以直接复制执行的加固配置。
适用场景
以下环境都适用本文的加固方案:
- 自建 XXL-Job 调度中心(2.1.x 至 2.4.x 版本),部署在云主机或 IDC,且调度中心与执行器跨网段部署;
- 执行器端口使用默认 9999,且安全组放通了 0.0.0.0/0;
- 调度中心后台使用默认账号 admin/123456,或口令从未轮换;
- 使用 GLUE(Shell/Python) 模式注册任务,任务内容可以在后台直接编辑;
- 生产与测试共用一套调度中心,测试执行器也注册在同一个注册中心里。
反过来说,如果调度中心只在内网、且已经设置了 32 位以上随机 accessToken、执行器端口只对调度中心单 IP 放行,那么风险等级会下降很多,本文可以作为核查清单做一次完整复验。
前置条件
- 已登录的调度中心管理后台,能修改任务与用户口令;
- 能编辑调度中心与执行器的
application.properties并重启服务; - 执行器所在主机有 root 权限,可操作 iptables 或安全组;
- 调度中心前置 Nginx,可增加 location 访问控制;
- 规划好网段:调度中心假设为 10.0.0.10,执行器假设为 10.0.0.20,实际部署时替换成自己的地址。
原理说明:三个暴露面
调度中心后台暴露
调度中心是一个标准 Spring Boot 应用,默认 context-path 为 /xxl-job-admin,登录页为 /xxl-job-admin/toLogin。只要这个路径能被公网访问,攻击者就可以对默认口令做撞库。管理员登录后能新建任务、编辑 GLUE 源码、立即执行,等于直接拥有执行器的命令执行能力。
执行器 EmbedServer 无鉴权
执行器启动时会内嵌一个轻量 HTTP 服务(Netty 实现,端口 xxl.job.executor.port),对外暴露 /beat、/idleBeat、/run、/kill、/log 五个端点。/run 的请求体里带有 jobId、executorHandler、glueType、glueSource 等字段——也就是说,只要能连通这个端口并构造出合法 JSON,就能指定要执行的脚本内容。这是 XXL-Job 未授权访问危害最大的一环。
accessToken 为空
调度中心与执行器之间的通讯依靠 HTTP Header XXL-JOB-ACCESS-TOKEN 做简单校验:两端配置的 accessToken 必须一致,且非空。2.3.0 之前的版本对空值不做事先拦截,两端都为空的默认配置下,任何能访问执行器端口的人都能直接调用 /run。2.3.0 之后虽然对空值做了校验告警,但如果沿用历史上线时的空配置,问题依旧存在。
操作步骤
第一步:设置强 accessToken 并统一两端
先生成 32 位随机串,不要用 default_token、123456 这类弱值:
openssl rand -hex 16
# 示例输出:k7m2x9vlp4nzc8swj6hy3qatrb5ude0f
调度中心 application.properties:
server.port=8080
server.servlet.context-path=/xxl-job-admin
xxl.job.accessToken=k7m2x9vlp4nzc8swj6hy3qatrb5ude0f
xxl.job.i18n=zh_CN
spring.datasource.url=jdbc:mysql://10.0.0.30:3306/xxl_job?useUnicode=true&characterEncoding=UTF-8&useSSL=false
spring.datasource.username=xxl_job
spring.datasource.password=替换为强口令
执行器 application.properties,accessToken 必须与调度中心完全一致,一个字符都不能差:
xxl.job.admin.addresses=http://10.0.0.10:8080/xxl-job-admin
xxl.job.accessToken=k7m2x9vlp4nzc8swj6hy3qatrb5ude0f
xxl.job.executor.appname=order-service
xxl.job.executor.ip=10.0.0.20
xxl.job.executor.port=9999
xxl.job.executor.logpath=/data/logs/xxl-job/jobhandler
xxl.job.executor.logretentiondays=30
改完重启两端,在调度中心的「执行器管理」里应看到执行器在线。
第二步:收敛调度中心入口
调度中心只应让运维网段访问。Nginx 增加来源白名单,其余一律拒绝:
server {
listen 443 ssl http2;
server_name job.example.com;
location /xxl-job-admin/ {
allow 10.0.0.0/8;
allow 203.0.113.0/28;
deny all;
proxy_pass http://127.0.0.1:8080/xxl-job-admin/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
client_max_body_size 8m;
proxy_read_timeout 120s;
}
}
同时确认 Spring Boot 只监听回环地址,不要让 8080 直接对外:server.address=127.0.0.1。如果调度中心确实是给外部系统回调用的,应单独开放回调路径,不要连带把 /toLogin、/jobinfo 一起放出去。
第三步:执行器端口只对调度中心放行
执行器 9999 端口只需调度中心能连,其它来源全部丢弃。iptables 规则要注意顺序,ACCEPT 必须在 DROP 之前:
# 只允许调度中心访问执行器内置端口
iptables -I INPUT 1 -p tcp --dport 9999 -s 10.0.0.10 -j ACCEPT
iptables -A INPUT -p tcp --dport 9999 -j DROP
# 持久化(Debian/Ubuntu)
apt-get install -y iptables-persistent
netfilter-persistent save
# 持久化(CentOS/RHEL 7)
service iptables save
如果是云主机,安全组规则要同步收紧到源 IP 为调度中心弹性 IP。安全组与主机防火墙是两层,任何一层放开都等于没做。
第四步:改掉默认管理员口令
XXL-Job 的用户口令在数据库里存的是 32 位小写 MD5,可以直接算好写入:
# 计算新口令的 MD5(不要带 -n 以外的换行)
echo -n 'N3wP@ssw0rd!2026' | md5sum | awk '{print $1}'
UPDATE xxl_job_user
SET password = '把上一步输出的 32 位 MD5 填在这里'
WHERE username = 'admin';
改完必须重启调度中心,否则会话缓存里仍是旧凭据。建议再建一个只读账号给值班同学看日志,管理员账号只留两个人在用。
第五步:关闭自动注册,收紧 GLUE 权限
自动注册打开时,任何人只要知道调度中心地址、能发心跳,就能把一个伪造的执行器挂进来。在「执行器管理」中关闭自动注册,改为手工录入 AppName 与地址。同时限制 GLUE 模式:生产环境优先用 BEAN 模式,脚本类任务若确有必要,限制可编辑 GLUE 源码的角色,仅管理员可改。
第六步:日志审计与告警
执行器被调用时会在日志里留下 accessToken 校验结果,用它做暴力探测的发现依据:
# 统计非法 token 访问次数(按分钟聚合)
grep -i "access token" /data/logs/xxl-job/xxl-job-executor.log | awk '{print $1, $2}' | cut -c1-16 | sort | uniq -c | sort -rn | head -20
# 调度中心侧的登录失败审计
grep -iE "login fail|password error" /data/logs/xxl-job/xxl-job-admin.log | tail -50
把「单 IP 每分钟非法 token 超过 10 次」作为告警条件接入监控,比事后翻日志有效得多。
配置验证
加固完成后按下面四项逐一验证,全部符合才算闭环:
# 1) 无 token 调用执行器,期望 500 或返回 token 错误,不能是 200
curl -s -o /dev/null -w "%{http_code}\n" http://10.0.0.20:9999/beat
# 2) 带错误 token 调用,同样应被拒绝
curl -s -H "XXL-JOB-ACCESS-TOKEN: wrong-token" http://10.0.0.20:9999/beat
# 3) 带正确 token,应返回 pong
curl -s -H "XXL-JOB-ACCESS-TOKEN: k7m2x9vlp4nzc8swj6hy3qatrb5ude0f" http://10.0.0.20:9999/beat
# 4) 从办公网直连调度中心后台,应被 Nginx 返回 403
curl -s -o /dev/null -w "%{http_code}\n" https://job.example.com/xxl-job-admin/toLogin
第 1、2 项如果仍然返回 200,说明 accessToken 没生效,检查两端配置是否被 Spring 的 profile 覆盖;第 3 项返回 pong 表示鉴权链路正确;第 4 项返回 403 表示入口收敛生效。最后用 nmap -Pn -p 9999 执行器IP 从外部扫一次,确认端口不可达。
常见问题
Q1:设置 accessToken 后执行器一直显示「通讯失败」怎么办?
九成是两端不一致或多了一层空格。检查三点:调度中心与执行器的 xxl.job.accessToken 是否逐字符相同(配置文件行尾不要留空格);执行器的 xxl.job.admin.addresses 是否带完整的 /xxl-job-admin 后缀;调度中心的 context-path 改动后,地址里的路径是否同步改过。三处都对仍失败,就在执行器上抓包确认请求头里有没有 XXL-JOB-ACCESS-TOKEN。
Q2:调度中心完全在内网,还需要配 accessToken 吗?
需要。内网不等于可信,横向移动、测试环境串网、跳板机失陷都会让「内网」这个前提消失。accessToken 的成本只有一行配置,属于零成本纵深防御。
Q3:执行器端口能不能改成随机端口来隐藏?
可以改端口,但不能把它当防护手段。端口扫描几秒钟就能找出来。正确做法仍是「只对调度中心 IP 放行 + accessToken 非空 + 关闭自动注册」三件套,改端口只是降低被顺手扫到的概率。
总结
XXL-Job 未授权访问的本质不是漏洞,而是默认配置在生产环境照搬上线。加固顺序建议固定为:先用 Nginx 与 iptables 把调度中心后台、执行器端口两个入口收窄到必要来源,再把 accessToken 换成 32 位随机值并统一两端,最后改默认口令、关自动注册、接日志告警。本文的配置在 2.1.x 至 2.4.x 均适用,命令与参数可直接复制,验证环节的四条 curl 建议固化成上线检查项,每次变更后复跑一遍。